diff --git a/.gitattributes b/.gitattributes index 6f5933e4..c751ac98 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,4 +1,14 @@ +* text=input + *.php text eol=lf *.js text eol=lf *.css text eol=lf *.sql text eol=lf +aowow text eol=lf +prQueue text eol=lf + +*.png binary +*.jpg binary +*.gif binary +*.ttf binary +*.swf binary diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 00000000..f4bb6b76 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,22 @@ +--- +name: Bug report +about: issue template +title: '' +labels: '' +assignees: '' + +--- + +**Describe the bug and how to reproduce it** +additionally paste relevant lines from db table `aowow_errors` +or your browsers console here. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**System:** + - OS: [e.g. Win10] + - PHP version: + - revision used: + - Browser (in case of JavaScript / display errors): + - AzerothCore: yes/no diff --git a/.gitignore b/.gitignore index 17e79bb7..8fbefabd 100644 --- a/.gitignore +++ b/.gitignore @@ -1,26 +1,19 @@ -# Git -*.orig - # cache -/cache/template/* -/setup/generated/alphaMaps/*.png -/cache/firstrun +/cache/* # extract from MPQ /setup/mpqdata/* # generated files /static/js/profile_all.js -/static/js/locale.js +/static/js/global.js /static/widgets/power.js /static/widgets/power/demo.html /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/* @@ -49,4 +42,8 @@ /static/uploads/screenshots/* /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 b21219da..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 @@ -13,30 +13,35 @@ While the first releases can be found as early as 2008, today it is impossible t This is a complete rewrite of the serverside php code and update to the clientside javascripts from 2008 to something 2013ish. I myself take no credit for the clientside scripting, design and layout that these php-scripts cater to. -Also, this project is not meant to be used for commercial puposes of any kind! +Also, this project is not meant to be used for commercial purposes of any kind! ## Requirements -+ Webserver running PHP ≥ 5.5.0 including extensions: - + SimpleXML - + GD - + Mysqli - + mbString -+ MySQL ≥ 5.5.30 -+ [TDB 335.63](https://github.com/TrinityCore/TrinityCore/releases/tag/TDB335.63) - including world updates up to 04.05.2017 ++ Webserver running PHP ≥ 8.2 including extensions: + + [SimpleXML](https://www.php.net/manual/en/book.simplexml.php) + + [GD](https://www.php.net/manual/en/book.image) + + [MySQL Improved](https://www.php.net/manual/en/book.mysqli.php) + + [Multibyte String](https://www.php.net/manual/en/book.mbstring.php) + + [File Information](https://www.php.net/manual/en/book.fileinfo.php) + + [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 ++ [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) / [BLPConverter](https://github.com/Sarjuuk/BLPConverter) (optional) + + [MPQExtractor](https://github.com/Sarjuuk/MPQExtractor) / [FFmpeg](https://ffmpeg.org/download.html) / (optional: [BLPConverter](https://github.com/Sarjuuk/BLPConverter)) + WIN users may find it easier to use these alternatives - + [MPQEditor](http://www.zezula.net/en/mpq/download.html) / [FFmpeg](http://ffmpeg.zeranoe.com/builds/) / [BLPConverter](https://github.com/PatrickCyr/BLPConverter) (optional) + + [MPQEditor](http://www.zezula.net/en/mpq/download.html) / [FFmpeg](http://ffmpeg.zeranoe.com/builds/) / (optional: [BLPConverter](https://github.com/PatrickCyr/BLPConverter)) audio processing may require [lame](https://sourceforge.net/projects/lame/files/lame/3.99/) or [vorbis-tools](https://www.xiph.org/downloads/) (which may require libvorbis (which may require libogg)) #### Highly Recommended -+ setting the following configuration values on your TrintyCore 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.Gameoject.Zone.Area.Data = 1 + > Calculate.Gameobject.Zone.Area.Data = 1 ## Install @@ -46,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 `setup/db_structure.sql` into the AoWoW database `mysql -p {your-db-here} < setup/db_structure.sql` +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 @@ -69,27 +76,27 @@ Extract the following directories from the client archives into `setup/mpqdata/` .. once is enough (still apply the localeCode though): > \/Interface/TalentFrame/ - > \/Interface/Glues/Credits/ > \/Interface/Icons/ > \/Interface/Spellbook/ > \/Interface/PaperDoll/ - > \/Interface/GLUES/CHARACTERCREATE/ + > \/Interface/Glues/CharacterCreate/ > \/Interface/Pictures > \/Interface/PvPRankBadges > \/Interface/FlavorImages > \/Interface/Calendar/Holidays/ > \/Sound/ - - .. optionaly (not used in AoWoW): - > \/Interface/GLUES/LOADINGSCREENS/ #### 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 -`php aowow --firstrun`. +#### 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. @@ -97,35 +104,41 @@ 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 poperly. 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 confict 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. BUT you can convert the affected blp file into a png file in the same directory, using the provided BLPConverter. - AoWoW will priorize png files over blp files. + AoWoW will prioritize png files over blp files. Q: How can i get the modelviewer to work? A: You can't anymore. Wowhead switched from Flash to WebGL (as they should) and moved or deleted the old files in the process. Q: I'm getting random javascript errors! -A: Some server configurations or external services (like Cloudflare) come with modules, that automaticly minify js and css files. Sometimes they break in the process. Disable the module in this case. +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 agains the characters database and have to use the incomplete/outdated cached profiles of AoWoW. +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 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. + +Q: An Item, Quest or NPC i added or edited can't be searched. Why? +A: A search is only conducted against the currently used locale. You may have only edited the name field in the base table instead of adding multiple strings into the appropriate \*_locale tables. In this case searches in a non-english locale are run against an empty name field. ## Thanks -@mix: for providing the php-script to parse .blp and .dbc into usable images and tables -@LordJZ: the wrapper-class for DBSimple; the basic idea for the user-class -@kliver: basic implementation of screenshot uploads +@mix: for providing the php-script to parse .blp and .dbc into usable images and tables +@LordJZ: the wrapper-class for DBSimple; the basic idea for the user-class +@kliver: basic implementation of screenshot uploads +@Sarjuuk: maintainer of the project ## Special Thanks -Said website with the red smiling rocket, for providing this beautifull website! -Please do not reagard 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". +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 5acd6a6a..3e1adb62 100755 --- a/aowow +++ b/aowow @@ -1,12 +1,18 @@ +#!/usr/bin/env 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/config.php.in b/config/config.php.in deleted file mode 100644 index b4036d97..00000000 --- a/config/config.php.in +++ /dev/null @@ -1,49 +0,0 @@ - '127.0.0.1', - 'user' => '', - 'pass' => '', - 'db' => 'world', - 'prefix' => 'aowow_' -); - -// -- World Database -- -// used to generate data-tables -$AoWoWconf['world'] = array( - 'host' => '127.0.0.1', - 'user' => '', - 'pass' => '', - 'db' => 'world', - 'prefix' => '' -); - -// -- Auth Database -- -// used to generate user-tables -$AoWoWconf['auth'] = array( - 'host' => '127.0.0.1', - 'user' => '', - 'pass' => '', - 'db' => 'auth', - 'prefix' => '' -); - -// -- Characters Database -- -// used to display profiles -$AoWoWconf['characters'][] = array( - 'host' => '127.0.0.1', - 'user' => '', - 'pass' => '', - 'db' => 'characters', - 'prefix' => '' -); - -?> diff --git a/config/extAuth.php.in b/config/extAuth.php.in index 8d951569..6158987f 100644 --- a/config/extAuth.php.in +++ b/config/extAuth.php.in @@ -4,13 +4,16 @@ if (!defined('AOWOW_REVISION')) die('illegal access'); - function extAuth($user, $pass, &$userId = 0, &$userGroup = -1) + function extAuth(string &$usernameOrEmail, #[\SensitiveParameter] string $password, int &$userId = 0, int &$userGroup = -1) : int { /* insert some auth mechanism here - see defines for usable return values - set userId for identification + set usernameOrEmail to a valid username, do not pass back the email if used for login + set userId to uid from external auth provider for identification + (optional) set userGroup to a valid userGroup (see U_GROUP_* defines) + + return an AUTH_* result (see defines) */ return AUTH_INTERNAL_ERR; diff --git a/datasets/zones b/datasets/zones deleted file mode 100644 index e6766d53..00000000 --- a/datasets/zones +++ /dev/null @@ -1,46 +0,0 @@ -Mapper.multiLevelZones = { - 206: ['206-1', '206-2', '206-3'], - 209: ['209-1', '209-2', '209-3', '209-4', '209-5', '209-6', '209-7'], - 616: ['616-1', '616_1', '616_2'], - 719: ['719-1', '719-2', '719-3'], - 721: ['721-1', '721-2', '721-3', '721-4'], - 796: ['796-1', '796-2', '796-3', '796-4'], - 1196: ['1196-1', '1196-2'], - 1337: ['1337-1', '1337-2'], - 1581: ['1581-1', '1581-2'], - 1583: ['1583-1', '1583-2', '1583-3', '1583-4', '1583-5', '1583-6', '1583-7'], - 1584: ['1584-1', '1584-2'], - 2017: ['2017-1', '2017-2'], - 2057: ['2057-1', '2057-2', '2057-3', '2057-4'], - 2100: ['2100-1', '2100-2'], - 2557: ['2557-1', '2557-2', '2557-3', '2557-4', '2557-5', '2557-6'], - 2677: ['2677-1', '2677-2', '2677-3', '2677-4'], - 3959: ['3959', '3959-1', '3959-2', '3959-3', '3959-4', '3959-5', '3959-6', '3959-7'], - 3428: ['3428-1', '3428-2', '3428-3'], - 3456: ['3456-1', '3456-2', '3456-3', '3456-4', '3456-5', '3456-6'], - 3457: ['3457-1', '3457-2', '3457-3', '3457-4', '3457-5', '3457-6', '3457-7', '3457-8', '3457-9', '3457-10', '3457-11', '3457-12', '3457-13', '3457-14', '3457-15', '3457-16', '3457-17'], - 3477: ['3477-1', '3477-2', '3477-3'], - 3715: ['3715-1', '3715-2'], - 3790: ['3790-1', '3790-2'], - 3791: ['3791-1', '3791-2'], - 3848: ['3848-1', '3848-2', '3848-3'], - 3849: ['3849-1', '3849-2'], - 4075: ['4075', '4075-1'], - 4100: ['4100-1', '4100-2'], - 4131: ['4131-1', '4131-2'], - 4196: ['4196-1', '4196-2'], - 4228: ['4228-1', '4228-2', '4228-3', '4228-4'], - 4272: ['4272-1', '4272-2'], - 4273: ['4273-0', '4273-1', '4273-2', '4273-3', '4273-4', '4273-5'], - 4277: ['4277-1', '4277-2', '4277-3'], - 4395: ['4395-1', '4395-2'], - 4494: ['4494-1', '4494-2'], - 4714: ['4714-1', '4714_1', '4714_2', '4714_3'], - 4722: ['4722-1', '4722-2'], - 4812: ['4812-1', '4812-2', '4812-3', '4812-4', '4812-5', '4812-6', '4812-7', '4812-8'], -}; - -/* -var g_zone_areas = {}; -in locale files -*/ \ No newline at end of file diff --git a/endpoints/aboutus/aboutus.php b/endpoints/aboutus/aboutus.php new file mode 100644 index 00000000..524dfc0c --- /dev/null +++ b/endpoints/aboutus/aboutus.php @@ -0,0 +1,34 @@ +generateError(); + } + + protected function generate() : void + { + $this->h1 = Lang::main('moreTitles', $this->pageName); + + array_unshift($this->title, $this->h1); + + parent::generate(); + } +} + +?> diff --git a/endpoints/account/account.php b/endpoints/account/account.php new file mode 100644 index 00000000..5c4ed8dd --- /dev/null +++ b/endpoints/account/account.php @@ -0,0 +1,174 @@ +forwardToSignIn('account'); + + 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` = %i', User::$id); + + Lang::sort('game', 'ra'); + + parent::generate(); + + + /*************/ + /* Ban Popup */ + /*************/ + + $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` = %i AND ab.`typeMask` & %i AND (ab.`end` = 0 OR ab.`end` > UNIX_TIMESTAMP())', + User::$id, ACC_BAN_TEMP | ACC_BAN_PERM + ); + + $this->bans = $b ?: null; + + + /*******************/ + /* Status Messages */ + /*******************/ + + if (isset($_SESSION['msg'])) + { + [$var, $status, $msg] = $_SESSION['msg']; + if (property_exists($this, $var.'Message')) + $this->{$var.'Message'} = [$status, $msg]; + else + trigger_error('AccountBaseResponse::generate - unknown var in $_SESSION msg: '.$var, E_USER_WARNING); + + unset($_SESSION['msg']); + } + + + /*************/ + /* Form Data */ + /*************/ + + /* GENERAL */ + + // Modelviewer + 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 + $this->idsInLists = $user['debug'] ? 1 : 0; + + /* PERSONAL */ + + // Email address + $this->curEmail = $user['email'] ?? ''; + + // Username + $this->curName = User::$username; + $this->renameCD = DateTime::formatTimeElapsedFloat(Cfg::get('ACC_RENAME_DECAY') * 1000); + if ($user['renameCooldown'] > time()) + { + $locCode = substr_replace(Lang::getLocale()->json(), '_', 2, 0); // ._. + $this->activeCD = (new \IntlDateFormatter($locCode, pattern: Lang::main('dateFmtIntl')))->format($user['renameCooldown']); + } + + /* COMMUNITY */ + + // Public Description + $this->description = ['body' => $user['description']]; + + // Forum Signature + // $this->signature = ['body' => $user['signature']]; + + // Avatar + $this->wowicon = $user['wowicon']; + $this->avMode = $user['avatar']; + + /* PREMIUM */ + + $this->premium = User::isPremium(); + + 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', + 'id' => 'avatar', + 'name' => '$LANG.tab_avatars', + 'parent' => 'avatar-manage', + 'hideNav' => 1 | 2, // top | bottom + 'data' => $cuAvatars ?? [], + 'note' => Lang::account('avatarSlots', [count($this->customicons), Cfg::get('acc_max_avatar_uploads')]) + ]); + + // Premium Border Selector + // solved by js + } +} + +?> diff --git a/endpoints/account/activate.php b/endpoints/account/activate.php new file mode 100644 index 00000000..e56fad9e --- /dev/null +++ b/endpoints/account/activate.php @@ -0,0 +1,73 @@ + ['filter' => FILTER_VALIDATE_REGEXP, 'options' => ['regexp' => '/^[a-zA-Z0-9]{40}$/']] + ); + + private bool $success = false; + + public function __construct() + { + parent::__construct(); + + if (!Cfg::get('ACC_ALLOW_REGISTER') || Cfg::get('ACC_AUTH_MODE') != AUTH_MODE_SELF) + $this->generateError(); + } + + protected function generate() : void + { + $this->title[] = Lang::account('title'); + + $msg = $this->activate(); + + if ($this->success) + $this->inputbox = ['inputbox-status', ['head' => Lang::account('inputbox', 'head', 'register', [2]), 'message' => $msg]]; + else + { + $_SESSION['error']['activate'] = $msg; + $this->forward('?account=resend'); + } + + parent::generate(); + } + + private function activate() : string + { + if (!$this->assertGET('key')) + return Lang::main('intError'); + + 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()->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()->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; + return Lang::account('inputbox', 'message', 'accActivated', [$this->_get['key']]); + } + + // grace period expired and other user claimed name + return Lang::main('intError'); + } +} + +?> diff --git a/endpoints/account/confirm-delete.php b/endpoints/account/confirm-delete.php new file mode 100644 index 00000000..c0cdcbdc --- /dev/null +++ b/endpoints/account/confirm-delete.php @@ -0,0 +1,128 @@ + [FILTER_VALIDATE_REGEXP, 'options' => ['regexp' => '/^[a-zA-Z0-9]{40}$/']] + ); + protected array $expectedPOST = array( + 'submit' => [FILTER_UNSAFE_RAW ], + 'cancel' => [FILTER_UNSAFE_RAW ], + 'confirm' => [FILTER_CALLBACK, 'options' => [self::class, 'checkEmptySet'] ], + 'key' => [FILTER_VALIDATE_REGEXP, 'options' => ['regexp' => '/^[a-zA-Z0-9]{40}$/']] + ); + + public bool $confirm = true; // just to select the correct localized brick + public string $username = ''; + public string $deleteFormTarget = '?account=confirm-delete'; + public ?array $inputbox = null; + public string $key = ''; + + private bool $success = false; + + public function __construct(string $rawParam) + { + if (Cfg::get('ACC_AUTH_MODE') != AUTH_MODE_SELF) + $this->generateError(); + + parent::__construct($rawParam); + } + + protected function generate() : void + { + array_unshift($this->title, Lang::account('accDelete')); + + $this->username = User::$username; + + parent::generate(); + + $msg = Lang::account('inputbox', 'error', 'purgeTokenUsed'); + + // display default confirm template + 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` = %i AND `statusTimer` > UNIX_TIMESTAMP() AND `token` = %s', ACC_STATUS_PURGING, $this->_post['key']))) + { + if ($this->_post['cancel']) + $msg = $this->cancel($userId); + else if ($this->_post['submit'] && $this->_post['confirm']) + $msg = $this->purge($userId); + } + + // throw error and display in status + $this->inputbox = ['inputbox-status', array( + 'head' => Lang::account('inputbox', 'head', $this->success ? 'success' : 'error'), + 'message' => $this->success ? $msg : '', + 'error' => $this->success ? '' : $msg + )]; + } + + private function cancel(int $userId) : string + { + 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'); + } + + return Lang::main('intError'); + } + + private function purge(int $userId) : string + { + // empty all user settings and cookies + 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()->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()->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()->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` = %i, `statusTimer` = 0, `token` = "", `updateValue` = "", `renameCooldown` = 0 + WHERE `id` = %i', + ACC_STATUS_DELETED, $userId + ); + + $this->success = true; + return Lang::account('inputbox', 'message', 'deleteOk'); + } +} + +?> diff --git a/endpoints/account/confirm-email-address.php b/endpoints/account/confirm-email-address.php new file mode 100644 index 00000000..05a4f217 --- /dev/null +++ b/endpoints/account/confirm-email-address.php @@ -0,0 +1,62 @@ + ['filter' => FILTER_VALIDATE_REGEXP, 'options' => ['regexp' => '/^[a-zA-Z0-9]{40}$/']] + ); + + private bool $success = false; + + protected function generate() : void + { + parent::generate(); + + if (User::isBanned()) + return; + + $msg = $this->change(); + + $this->inputbox = ['inputbox-status', array( + 'head' => Lang::account('inputbox', 'head', $this->success ? 'success' : 'error'), + 'message' => $this->success ? $msg : '', + 'error' => $this->success ? '' : $msg, + )]; + } + + // this should probably leave change info intact for revert + // todo - move personal settings changes to separate table + private function change() : string + { + if (!$this->assertGET('key')) + return Lang::main('intError'); + + $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()->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; + return Lang::account('inputbox', 'message', 'mailChangeOk'); + } +} + +?> diff --git a/endpoints/account/confirm-password.php b/endpoints/account/confirm-password.php new file mode 100644 index 00000000..bc972f70 --- /dev/null +++ b/endpoints/account/confirm-password.php @@ -0,0 +1,60 @@ + ['filter' => FILTER_VALIDATE_REGEXP, 'options' => ['regexp' => '/^[a-zA-Z0-9]{40}$/']] + ); + + private bool $success = false; + + protected function generate() : void + { + parent::generate(); + + if (User::isBanned()) + return; + + $msg = $this->confirm(); + + $this->inputbox = ['inputbox-status', array( + 'head' => Lang::account('inputbox', 'head', $this->success ? 'success' : 'error'), + 'message' => $this->success ? $msg : '', + 'error' => $this->success ? '' : $msg, + )]; + } + + private function confirm() : string + { + if (!$this->assertGET('key')) + return Lang::main('intError'); + + $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()->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; + return Lang::account('inputbox', 'message', 'passChangeOk'); + } +} + +?> diff --git a/endpoints/account/delete-icon.php b/endpoints/account/delete-icon.php new file mode 100644 index 00000000..4e71147b --- /dev/null +++ b/endpoints/account/delete-icon.php @@ -0,0 +1,47 @@ + ['filter' => FILTER_VALIDATE_INT] + ); + + /* + * response not evaluated + */ + protected function generate() : void + { + if (User::isBanned() || !$this->assertPOST('id')) + return; + + // non-int > error + $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()->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()->qry('UPDATE ::account SET `avatar` = 0 WHERE `id` = %i', User::$id); + + $path = sprintf('static/uploads/avatars/%d.jpg', $this->_post['id']); + if (!unlink($path)) + trigger_error('AccountDeleteiconResponse - failed to delete file: '.$path, E_USER_ERROR); + } +} + +?> diff --git a/endpoints/account/delete.php b/endpoints/account/delete.php new file mode 100644 index 00000000..04a46e2b --- /dev/null +++ b/endpoints/account/delete.php @@ -0,0 +1,71 @@ + ['filter' => FILTER_UNSAFE_RAW] + ); + + public string $username = ''; + public string $deleteFormTarget = '?account=delete'; + public ?array $inputbox = null; + + public function __construct(string $rawParam) + { + if (Cfg::get('ACC_AUTH_MODE') != AUTH_MODE_SELF) + $this->generateError(); + + parent::__construct($rawParam); + } + + protected function generate() : void + { + array_unshift($this->title, Lang::account('accDelete')); + + parent::generate(); + + $this->username = User::$username; + + if ($this->_post['proceed']) + { + $error = false; + 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()->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]); + } + else + $error = true; + + $this->inputbox = ['inputbox-status', array( + 'head' => Lang::account('inputbox', 'head', $error ? 'error' : 'success'), + 'message' => $error ? '' : Lang::account('inputbox', 'message', 'deleteAccSent', [User::$email]), + 'error' => $error ? Lang::account('inputbox', 'error', 'isRecovering') : '' + )]; + } + } +} + +?> diff --git a/endpoints/account/exclude.php b/endpoints/account/exclude.php new file mode 100644 index 00000000..17a3a032 --- /dev/null +++ b/endpoints/account/exclude.php @@ -0,0 +1,81 @@ + ['filter' => FILTER_VALIDATE_INT, 'options' => ['min_range' => 1, 'max_range' => 1]], + 'reset' => ['filter' => FILTER_VALIDATE_INT, 'options' => ['min_range' => 1, 'max_range' => 1]], + 'id' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkIdList'] ], + 'type' => ['filter' => FILTER_VALIDATE_INT ], + 'groups' => ['filter' => FILTER_VALIDATE_INT ] + ); + + protected function generate() : void + { + if (User::isBanned()) + return; + + if ($this->_post['mode'] == 1) // directly set exludes + $this->excludeById(); + + else if ($this->_post['reset'] == 1) // defaults to unavailable + $this->resetExcludes(); + + else if ($this->_post['groups'] !== null) // exclude by group mask + $this->updateGroups(); + } + + private function excludeById() : void + { + if (!$this->assertPOST('type', 'id')) + return; + + if ($validIds = Type::validateIds($this->_post['type'], $this->_post['id'])) + { + // ready for some bullshit? here it comes! + // 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` = %i AND `typeId` IN %in', $this->_post['type'], $validIds); + $insert = []; + foreach ($validIds as $typeId) + { + $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); + } + + private function resetExcludes() : void + { + 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()->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 new file mode 100644 index 00000000..e459d253 --- /dev/null +++ b/endpoints/account/favorites.php @@ -0,0 +1,52 @@ + ['filter' => FILTER_VALIDATE_INT], + 'remove' => ['filter' => FILTER_VALIDATE_INT], + 'id' => ['filter' => FILTER_VALIDATE_INT], + // 'sessionKey' => ['filter' => FILTER_VALIDATE_REGEXP, 'options' => ['regexp' => '/^[a-zA-Z0-9]{40}$/']] // usage of sessionKey omitted + ); + + protected function generate() : void + { + if (User::isBanned()) + return; + + if ($this->_post['remove']) + $this->removeFavorite(); + + else if ($this->_post['add']) + $this->addFavorite(); + } + + private function removeFavorite() : void + { + if ($this->assertPOST('id', 'remove')) + 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()->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 new file mode 100644 index 00000000..c0ab9581 --- /dev/null +++ b/endpoints/account/forgot-password.php @@ -0,0 +1,101 @@ + display email form + * 2. submit email form > send mail with recovery link + * 3. click recovery link from mail > display password reset form + * 4. submit password reset form > update password + */ + +class AccountforgotpasswordResponse extends TemplateResponse +{ + use TrRecoveryHelper, TrGetNext; + + protected string $template = 'text-page-generic'; + protected string $pageName = 'forgot-password'; + + protected array $expectedPOST = array( + 'email' => ['filter' => FILTER_VALIDATE_EMAIL, 'flags' => FILTER_FLAG_STRIP_AOWOW] + ); + + private bool $success = false; + + public function __construct(string $rawParam) + { + // don't redirect logged in users + // you can be forgetful AND logged in + + if (Cfg::get('ACC_EXT_RECOVER_URL')) + $this->forward(Cfg::get('ACC_EXT_RECOVER_URL')); + + if (Cfg::get('ACC_AUTH_MODE') != AUTH_MODE_SELF) + $this->generateError(); + + parent::__construct($rawParam); + } + + protected function generate() : void + { + $this->title[] = Lang::account('title'); + + parent::generate(); + + $msg = $this->processMailForm(); + + if ($this->success) + $this->inputbox = ['inputbox-status', ['head' => Lang::account('inputbox', 'head', 'recoverPass', [1.5]), 'message' => $msg]]; + else + $this->inputbox = ['inputbox-form-email', array( + 'head' => Lang::account('inputbox', 'head', 'recoverPass', [1]), + 'error' => $msg, + 'action' => '?account=forgot-password&next='.$this->getNext(), + 'email' => $this->_post['email'] ?? '' + )]; + } + + private function processMailForm() : string + { + // no input yet. show clean email form + if (is_null($this->_post['email'])) + return ''; + + // truncated due to validation fail + if (!$this->_post['email']) + return Lang::account('emailInvalid'); + + $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: '.DateTime::formatTimeElapsed($timeout * 1000).' remaining' : Lang::account('inputbox', 'error', 'emailNotFound'); + + // pretend recovery started + 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'); + return Cfg::get('DEBUG') ? Lang::account('inputbox', 'error', 'emailNotFound') : Lang::account('inputbox', 'message', 'recovPassSent', [$this->_post['email']]); + } + + // recovery actually started + if ($err = $this->startRecovery(ACC_STATUS_RECOVER_PASS, 'reset-password', $this->_post['email'])) + return $err; + + 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; + return Lang::account('inputbox', 'message', 'recovPassSent', [$this->_post['email']]); + } +} + +?> diff --git a/endpoints/account/forgot-username.php b/endpoints/account/forgot-username.php new file mode 100644 index 00000000..c1cff916 --- /dev/null +++ b/endpoints/account/forgot-username.php @@ -0,0 +1,100 @@ + display email form + * 2. submit email form > send mail with recovery link + * ( 3. click recovery link from mail to go to signin page (so not on this page) ) + */ + +class AccountforgotusernameResponse extends TemplateResponse +{ + use TrRecoveryHelper; + + protected string $template = 'text-page-generic'; + protected string $pageName = 'forgot-username'; + + protected array $expectedPOST = array( + 'email' => ['filter' => FILTER_VALIDATE_EMAIL, 'flags' => FILTER_FLAG_STRIP_AOWOW] + ); + + private bool $success = false; + + public function __construct(string $rawParam) + { + // if the user is looged in goto account dashboard + if (User::isLoggedIn()) + $this->forward('?account'); + + if (Cfg::get('ACC_EXT_RECOVER_URL')) + $this->forward(Cfg::get('ACC_EXT_RECOVER_URL')); + + if (Cfg::get('ACC_AUTH_MODE') != AUTH_MODE_SELF) + $this->generateError(); + + parent::__construct($rawParam); + } + + protected function generate() : void + { + $this->title[] = Lang::account('title'); + + parent::generate(); + + $msg = $this->processMailForm(); + + if ($this->success) + $this->inputbox = ['inputbox-status', ['head' => Lang::account('inputbox', 'head', 'recoverUser'), 'message' => $msg]]; + else + $this->inputbox = ['inputbox-form-email', array( + 'head' => Lang::account('inputbox', 'head', 'recoverUser'), + 'error' => $msg, + 'action' => '?account=forgot-username' + )]; + } + + private function processMailForm() : string + { + // no input yet. show empty form + if (is_null($this->_post['email'])) + return ''; + + // truncated due to validation fail + if (!$this->_post['email']) + return Lang::account('emailInvalid'); + + $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: '.DateTime::formatTimeElapsed($timeout * 1000).' remaining' : Lang::account('inputbox', 'error', 'emailNotFound'); + + // pretend recovery started + 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'); + return Cfg::get('DEBUG') ? Lang::account('inputbox', 'error', 'emailNotFound') : Lang::account('inputbox', 'message', 'recovUserSent', [$this->_post['email']]); + } + + // recovery actually started + if ($err = $this->startRecovery(ACC_STATUS_RECOVER_USER, 'recover-user', $this->_post['email'])) + return $err; + + 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; + return Lang::account('inputbox', 'message', 'recovUserSent', [$this->_post['email']]); + } +} + +?> diff --git a/endpoints/account/forum-avatar.php b/endpoints/account/forum-avatar.php new file mode 100644 index 00000000..e7bd9840 --- /dev/null +++ b/endpoints/account/forum-avatar.php @@ -0,0 +1,108 @@ + ['filter' => FILTER_VALIDATE_INT, 'options' => ['min_range' => 0, 'max_range' => 2 ]], + 'wowicon' => ['filter' => FILTER_VALIDATE_REGEXP, 'options' => ['regexp' => '/^[[:print:]]+$/' ]], // file name can have \W chars: inv_misc_fork&knife, achievement_dungeon_drak'tharon_heroic + 'customicon' => ['filter' => FILTER_VALIDATE_INT, 'options' => ['min_range' => 1 ]] + ); + // called via ajax + protected array $expectedGET = array( + 'avatar' => ['filter' => FILTER_VALIDATE_INT, 'options' => ['min_range' => 2, 'max_range' => 2]], + 'customicon' => ['filter' => FILTER_VALIDATE_INT, 'options' => ['min_range' => 1 ]] + ); + + private bool $success = false; + + protected function generate() : void + { + if (User::isBanned()) + return; + + $msg = match ($this->_post['avatar'] ?? $this->_get['avatar']) + { + 0 => $this->unset(), // none + 1 => $this->fromIcon(), // wow icon + 2 => $this->fromUpload(!$this->_get['avatar']), // custom icon (premium feature) + default => Lang::main('genericError') + }; + + if ($msg) + $_SESSION['msg'] = ['avatar', $this->success, $msg]; + } + + private function unset() : string + { + $x = DB::Aowow()->qry('UPDATE ::account SET `avatar` = 0 WHERE `id` = %i', User::$id); + if ($x === null || $x === false) + return Lang::main('genericError'); + + $this->success = true; + + return Lang::account('updateMessage', $x === 0 ? 'avNoChange' : 'avSuccess'); + } + + private function fromIcon() : string + { + if (!$this->assertPOST('wowicon')) + return Lang::main('intError'); + + $icon = strtolower(trim($this->_post['wowicon'])); + + if (!DB::Aowow()->selectCell('SELECT 1 FROM ::icons WHERE `name` = %s', $icon)) + return Lang::account('updateMessage', 'avNotFound'); + + $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` = %s', $icon)) > 1) + $msg .= ' '.Lang::account('updateMessage', 'avNthUser', [$qty]); + else + $msg .= ' '.Lang::account('updateMessage', 'av1stUser'); + + return $msg; + } + + protected function fromUpload(bool $viaPOST) : string + { + if (!User::isPremium()) + return Lang::main('genericError'); + + if (($viaPOST && !$this->assertPOST('customicon')) || (!$viaPOST && !$this->assertGET('customicon'))) + return Lang::main('intError'); + + $customIcon = $this->_post['customicon'] ?? $this->_get['customicon']; + + $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()->qry('UPDATE ::account SET `avatar` = 2 WHERE `id` = %i', User::$id))) + return Lang::main('intError'); + + $this->success = true; + + return Lang::account('updateMessage', $x === 0 ? 'avNoChange' : 'avSuccess'); + } +} + +?> diff --git a/endpoints/account/premium-border.php b/endpoints/account/premium-border.php new file mode 100644 index 00000000..7374a210 --- /dev/null +++ b/endpoints/account/premium-border.php @@ -0,0 +1,41 @@ + ['filter' => FILTER_VALIDATE_INT, 'options' => ['min_range' => 0, 'max_range' => 4]], + ); + + protected function generate() : void + { + if (User::isBanned()) + return; + + if (!$this->assertPOST('avatarborder')) + return; + + $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')]; + else + $_SESSION['msg'] = ['premiumborder', true, Lang::account('updateMessage', 'avSuccess')]; + } +} + +?> diff --git a/endpoints/account/rename-icon.php b/endpoints/account/rename-icon.php new file mode 100644 index 00000000..c88ff2bb --- /dev/null +++ b/endpoints/account/rename-icon.php @@ -0,0 +1,36 @@ + ['filter' => FILTER_VALIDATE_INT ], + 'name' => ['filter' => FILTER_VALIDATE_REGEXP, 'options' => ['regexp' =>'/^[a-zA-Z][a-zA-Z0-9 ]{0,19}$/']] + ); + + /* + * response not evaluated + */ + protected function generate() : void + { + if (User::isBanned() || !$this->assertPOST('id', 'name')) + return; + + // regexp same as in account.js + 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 new file mode 100644 index 00000000..28fe9c51 --- /dev/null +++ b/endpoints/account/resend-submit.php @@ -0,0 +1,52 @@ + ['filter' => FILTER_VALIDATE_EMAIL, 'flags' => FILTER_FLAG_STRIP_AOWOW] + ); + + public function __construct(string $rawParam) + { + if (!Cfg::get('ACC_ALLOW_REGISTER') || Cfg::get('ACC_AUTH_MODE') != AUTH_MODE_SELF) + $this->generateError(); + + parent::__construct($rawParam); + } + + protected function generate() : void + { + $this->title[] = Lang::account('title'); + + $error = $message = ''; + + if ($this->assertPOST('email')) + $message = Lang::account('inputbox', 'message', 'createAccSent', [$this->_post['email']]); + else + $error = Lang::main('intError'); + + parent::generate(); + + $this->inputbox = ['inputbox-status', array( + 'head' => Lang::account('inputbox', 'head', 'register', [1.5]), + 'message' => $message, + 'error' => $error + )]; + } +} + +?> diff --git a/endpoints/account/resend.php b/endpoints/account/resend.php new file mode 100644 index 00000000..7c1fecbf --- /dev/null +++ b/endpoints/account/resend.php @@ -0,0 +1,98 @@ + ['filter' => FILTER_VALIDATE_EMAIL, 'flags' => FILTER_FLAG_STRIP_AOWOW] + ); + + private bool $success = false; + + public function __construct(string $rawParam) + { + if (Cfg::get('ACC_EXT_RECOVER_URL')) + $this->forward(Cfg::get('ACC_EXT_RECOVER_URL')); + + if (!Cfg::get('ACC_ALLOW_REGISTER') || Cfg::get('ACC_AUTH_MODE') != AUTH_MODE_SELF) + $this->generateError(); + + parent::__construct($rawParam); + } + + protected function generate() : void + { + $this->title[] = Lang::account('title'); + + parent::generate(); + + // error from account=activate + if (isset($_SESSION['error']['activate'])) + { + $msg = $_SESSION['error']['activate']; + unset($_SESSION['error']['activate']); + } + else + $msg = $this->resend(); + + if ($this->success) + $this->inputbox = ['inputbox-status', ['head' => Lang::account('inputbox', 'head', 'resendMail'), 'message' => $msg]]; + else + $this->inputbox = ['inputbox-form-email', array( + 'head' => Lang::account('inputbox', 'head', 'resendMail'), + 'message' => Lang::account('inputbox', 'message', 'resendMail'), + 'error' => $msg, + 'action' => '?account=resend', + )]; + } + + private function resend() : string + { + // no input yet. show clean form + if (is_null($this->_post['email'])) + return ''; + + // truncated due to validation fail + if (!$this->_post['email']) + return Lang::account('emailInvalid'); + + $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: '.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` = %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()->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; + return Lang::account('inputbox', 'message', 'createAccSent', [$this->_post['email']]); + } + + // pretend recovery started + // do not confirm or deny existence of email + $this->success = !Cfg::get('DEBUG'); + return Cfg::get('DEBUG') ? Lang::account('inputbox', 'error', 'emailNotFound') : Lang::account('inputbox', 'message', 'createAccSent', [$this->_post['email']]); + } +} + +?> diff --git a/endpoints/account/reset-password.php b/endpoints/account/reset-password.php new file mode 100644 index 00000000..4d71153f --- /dev/null +++ b/endpoints/account/reset-password.php @@ -0,0 +1,121 @@ + display email form + * 2. submit email form > send mail with recovery link + * 3. click recovery link from mail > display password reset form + * 4. submit password reset form > update password + */ + +class AccountresetpasswordResponse extends TemplateResponse +{ + use TrRecoveryHelper, TrGetNext; + + protected string $template = 'text-page-generic'; + protected string $pageName = 'reset-password'; + + protected array $expectedGET = array( + 'key' => ['filter' => FILTER_VALIDATE_REGEXP, 'options' => ['regexp' => '/^[a-zA-Z0-9]{40}$/']], + 'next' => ['filter' => FILTER_VALIDATE_REGEXP, 'options' => ['regexp' => '/^[[:print:]]+$/' ]] + ); + protected array $expectedPOST = array( + 'key' => ['filter' => FILTER_VALIDATE_REGEXP, 'options' => ['regexp' => '/^[a-zA-Z0-9]{40}$/']], + 'email' => ['filter' => FILTER_VALIDATE_EMAIL, 'flags' => FILTER_FLAG_STRIP_AOWOW ], + 'password' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkTextLine'] ], + 'c_password' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkTextLine'] ] + ); + + private bool $success = false; + + public function __construct() + { + $this->title[] = Lang::account('title'); + + parent::__construct(); + + // don't redirect logged in users + // you can be forgetful AND logged in + + if (Cfg::get('ACC_EXT_RECOVER_URL')) + $this->forward(Cfg::get('ACC_EXT_RECOVER_URL')); + + if (Cfg::get('ACC_AUTH_MODE') != AUTH_MODE_SELF) + $this->generateError(); + } + + protected function generate() : void + { + parent::generate(); + + $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` = %s AND `status` = %i AND `statusTimer` > UNIX_TIMESTAMP()', $this->_get['key'], ACC_STATUS_RECOVER_PASS)) + $errMsg = Lang::account('inputbox', 'error', 'passTokenUsed'); + + if ($errMsg) + { + $this->inputbox = ['inputbox-status', array( + 'head' => Lang::account('inputbox', 'head', 'error'), + 'error' => $errMsg + )]; + + return; + } + + // step "2.5" + $errMsg = $this->doResetPass(); + if ($this->success) + $this->forward('?account=signin'); + + // step 2 + $this->inputbox = ['inputbox-form-password', array( + 'head' => Lang::account('inputbox', 'head', 'recoverPass', [2]), + 'token' => $this->_post['key'] ?? $this->_get['key'], + 'action' => '?account=reset-password&next=account=signin', + 'error' => $errMsg, + )]; + } + + private function doResetPass() : string + { + // no input yet. show clean form + if (!$this->assertPOST('key', 'password', 'c_password') && is_null($this->_post['email'])) + return ''; + + // truncated due to validation fail + if (!$this->_post['email']) + return Lang::account('emailInvalid'); + + if ($this->_post['password'] != $this->_post['c_password']) + return Lang::account('passCheckFail'); + + $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 + ); + if (!$userData) + return Lang::account('inputbox', 'error', 'emailNotFound'); + + if (!User::verifyCrypt($this->_post['c_password'], $userData['passHash'])) + return Lang::account('newPassDiff'); + + 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; + return ''; + } +} + +?> diff --git a/endpoints/account/revert-email-address.php b/endpoints/account/revert-email-address.php new file mode 100644 index 00000000..106fb6f0 --- /dev/null +++ b/endpoints/account/revert-email-address.php @@ -0,0 +1,62 @@ + ['filter' => FILTER_VALIDATE_REGEXP, 'options' => ['regexp' => '/^[a-zA-Z0-9]{40}$/']] + ); + + private bool $success = false; + + protected function generate() : void + { + parent::generate(); + + if (User::isBanned()) + return; + + $msg = $this->revert(); + + $this->inputbox = ['inputbox-status', array( + 'head' => Lang::account('inputbox', 'head', $this->success ? 'success' : 'error'), + 'message' => $this->success ? $msg : '', + 'error' => $this->success ? '' : $msg, + )]; + } + + // this should probably take precedence over email-change + // todo - move personal settings changes to separate table + private function revert() : string + { + if (!$this->assertGET('key')) + return Lang::main('intError'); + + $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()->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; + return Lang::account('inputbox', 'message', 'mailRevertOk'); + } +} + +?> diff --git a/endpoints/account/signin.php b/endpoints/account/signin.php new file mode 100644 index 00000000..ed8fbf07 --- /dev/null +++ b/endpoints/account/signin.php @@ -0,0 +1,148 @@ + ['filter' => FILTER_CALLBACK, 'options' => [Util::class, 'validateLogin'] ], + 'password' => ['filter' => FILTER_CALLBACK, 'options' => [Util::class, 'validatePassword']], + 'remember_me' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkRememberMe'] ] + ); + protected array $expectedGET = array( + 'key' => ['filter' => FILTER_VALIDATE_REGEXP, 'options' => ['regexp' => '/^[a-zA-Z0-9]{40}$/']], + 'next' => ['filter' => FILTER_VALIDATE_REGEXP, 'options' => ['regexp' => '/^[[:print:]]+$/'] ] + ); + + private bool $success = false; + + public function __construct() + { + // if the user is logged in, goto user dashboard + if (User::isLoggedIn()) + $this->forward('?user='.User::$username); + + parent::__construct(); + } + + protected function generate() : void + { + $username = + $error = ''; + $rememberMe = !!$this->_post['remember_me']; + + $this->title = [Lang::account('title')]; + + // 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 %in AND a.`token` = %s', + [ACC_STATUS_RECOVER_USER, ACC_STATUS_NONE], $this->_get['key'])) + [$username, $rememberMe] = $userData; + } + + if ($this->doSignIn($error)) + $this->forward($this->getNext(true)); + + if ($error) + User::destroy(); + + $this->inputbox = ['inputbox-form-signin', array( + 'head' => Lang::account('inputbox', 'head', 'signin'), + 'action' => '?account=signin&next='.$this->getNext(), + 'error' => $error, + 'username' => $username, + 'rememberMe' => $rememberMe, + 'hasRecovery' => Cfg::get('ACC_EXT_RECOVER_URL') || Cfg::get('ACC_AUTH_MODE') == AUTH_MODE_SELF, + )]; + + parent::generate(); + } + + private function doSignIn(string &$error) : bool + { + if (is_null($this->_post['username']) && is_null($this->_post['password'])) + return false; + + if (!$this->assertPOST('username')) + { + $error = Lang::account('userNotFound'); + return false; + } + + if (!$this->assertPOST('password')) + { + $error = Lang::account('wrongPass'); + return false; + } + + $error = match (User::authenticate($this->_post['username'], $this->_post['password'])) + { + AUTH_OK, AUTH_BANNED => $this->onAuthSuccess(), + // 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', [DateTime::formatTimeElapsedFloat(Cfg::get('ACC_FAILED_AUTH_BLOCK') * 1000)]), + AUTH_INTERNAL_ERR => Lang::main('intError'), + default => Lang::main('intError') + }; + + return !$error; + } + + private function onAuthSuccess() : string + { + if (!User::$ip) + { + trigger_error('AccountSigninResponse::onAuthSuccess() - tried to login user without ip set', E_USER_ERROR); + return Lang::main('intError'); + } + + // reset account status, update expiration + $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 + ); + + if (!is_int($ok)) // num updated fields or null on fail + { + trigger_error('AccountSigninResponse::onAuthSuccess() - failed to update account status', E_USER_ERROR); + return Lang::main('intError'); + } + + // DELETE temp session + if ($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()->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 + User::save(); + + return ''; + } +} + +?> diff --git a/endpoints/account/signout.php b/endpoints/account/signout.php new file mode 100644 index 00000000..73374d1b --- /dev/null +++ b/endpoints/account/signout.php @@ -0,0 +1,40 @@ + ['filter' => FILTER_VALIDATE_REGEXP, 'options' => ['regexp' => '/^[[:print:]]+$/']], + 'global' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkEmptySet'] ] + ); + + public function __construct(string $rawParam) + { + // if the user not is logged in goto login page + if (!User::isLoggedIn()) + $this->forwardToSignIn(); + + parent::__construct($rawParam); + } + + protected function generate() : void + { + if ($this->_get['global']) + DB::Aowow()->qry('UPDATE ::account_sessions SET `touched` = %i, `status` = %i WHERE `userId` = %i', time(), SESSION_FORCED_LOGOUT, User::$id); + else + DB::Aowow()->qry('UPDATE ::account_sessions SET `touched` = %i, `status` = %i WHERE `sessionId` = %s', time(), SESSION_LOGOUT, session_id()); + + User::destroy(); + + $this->redirectTo = $this->getNext(true); + } +} + +?> diff --git a/endpoints/account/signup.php b/endpoints/account/signup.php new file mode 100644 index 00000000..2fa29278 --- /dev/null +++ b/endpoints/account/signup.php @@ -0,0 +1,163 @@ + ['filter' => FILTER_SANITIZE_SPECIAL_CHARS, 'flags' => FILTER_FLAG_STRIP_AOWOW ], + 'email' => ['filter' => FILTER_SANITIZE_EMAIL, 'flags' => FILTER_FLAG_STRIP_AOWOW ], + 'password' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkTextLine'] ], + 'c_password' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkTextLine'] ], + 'remember_me' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkRememberMe']] + ); + + protected array $expectedGET = array( + 'next' => ['filter' => FILTER_VALIDATE_REGEXP, 'options' => ['regexp' => '/^[[:print:]]+$/']] + ); + + private bool $success = false; + + public function __construct() + { + // if the user is logged in goto account dashboard + if (User::isLoggedIn()) + $this->forward('?account'); + + // redirect to external registration page, if set + if (Cfg::get('ACC_EXT_CREATE_URL')) + $this->forward(Cfg::get('ACC_EXT_CREATE_URL')); + + parent::__construct(); + + // registration not enabled on self + if (!Cfg::get('ACC_ALLOW_REGISTER')) + $this->generateError(); + + if (Cfg::get('ACC_AUTH_MODE') != AUTH_MODE_SELF) + $this->generateError(); + } + + protected function generate() : void + { + $this->title[] = Lang::account('title'); + + // step 1 - no params > signup form + // step 2 - any param > status box + // step 3 - on ?account=activate + + $message = $this->doSignUp(); + + if ($this->success) + { + $this->inputbox = ['inputbox-status', array( + 'head' => Lang::account('inputbox', 'head', 'register', [1.5]), + 'message' => Lang::account('inputbox', 'message', 'createAccSent', [$this->_post['email']]) + )]; + } + else + { + $this->inputbox = ['inputbox-form-signup', array( + 'head' => Lang::account('inputbox', 'head', 'register', [1]), + 'error' => $message, + 'action' => '?account=signup&next='.$this->getNext(), + 'username' => $this->_post['username'] ?? '', + 'email' => $this->_post['email'] ?? '', + 'rememberMe' => !!$this->_post['remember_me'], + )]; + } + + parent::generate(); + } + + private function doSignUp() : string + { + // no input yet. show clean form + if (!$this->assertPOST('username', 'password', 'c_password') && is_null($this->_post['email'])) + return ''; + + // truncated due to validation fail + if (!$this->_post['email']) + return Lang::account('emailInvalid'); + + // check username + if (!Util::validateUsername($this->_post['username'], $e)) + return Lang::account($e == 1 ? 'errNameLength' : 'errNameChars'); + + // check password + if (!Util::validatePassword($this->_post['password'], $e)) + return $e == 1 ? Lang::account('errPassLength') : Lang::main('intError'); + + if ($this->_post['password'] !== $this->_post['c_password']) + return Lang::account('passMismatch'); + + // check ip + if (!User::$ip) + return Lang::main('intError'); + + // limit account creation + 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()->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` = %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()->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()->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'], + $this->_post['email'], + User::$ip, + Lang::getLocale()->value, + U_GROUP_PENDING, + ACC_STATUS_NEW, + Cfg::get('ACC_CREATE_SAVE_DECAY'), + $token + ); + + if (!$userId) + return Lang::main('intError'); + + // create session tied to the token to store remember_me status + 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()->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); + + $this->success = true; + return ''; + } +} + +?> diff --git a/endpoints/account/update-community-settings.php b/endpoints/account/update-community-settings.php new file mode 100644 index 00000000..2bc1ed2d --- /dev/null +++ b/endpoints/account/update-community-settings.php @@ -0,0 +1,48 @@ + ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkTextBlob']] + ); + + private bool $success = false; + + protected function generate() : void + { + if (User::isBanned()) + return; + + if ($message = $this->updateSettings()) + $_SESSION['msg'] = ['community', $this->success, $message]; + } + + protected function updateSettings() + { + if (is_null($this->_post['desc'])) // assertPOST tests for empty string which is valid here + return Lang::main('genericError'); + + // description - 0 modified rows is still success + 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; + return Lang::account('updateMessage', 'community'); + } +} + +?> diff --git a/endpoints/account/update-email.php b/endpoints/account/update-email.php new file mode 100644 index 00000000..0cf750be --- /dev/null +++ b/endpoints/account/update-email.php @@ -0,0 +1,80 @@ + ['filter' => FILTER_VALIDATE_EMAIL, 'flags' => FILTER_FLAG_STRIP_AOWOW] + ); + + private bool $success = false; + + public function __construct(string $rawParam) + { + if (Cfg::get('ACC_AUTH_MODE') != AUTH_MODE_SELF) + (new TemplateResponse())->generateError(); + + parent::__construct($rawParam); + } + + protected function generate() : void + { + if (User::isBanned()) + return; + + if ($msg = $this->updateMail()) + $_SESSION['msg'] = ['email', $this->success, $msg]; + } + + private function updateMail() : string + { + // no input yet + if (is_null($this->_post['newemail'])) + return Lang::main('intError'); + // truncated due to validation fail + if (!$this->_post['newemail']) + return Lang::account('emailInvalid'); + + 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` = %i', User::$id); + if ($status != ACC_STATUS_NONE && $status != ACC_STATUS_CHANGE_EMAIL) + return Lang::account('inputbox', 'error', 'isRecovering', [DateTime::formatTimeElapsedFloat(Cfg::get('ACC_RECOVERY_DECAY') * 1000)]); + + $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()->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'); + + if (!Util::sendMail($this->_post['newemail'], 'change-email', [$token, $this->_post['newemail']], Cfg::get('ACC_RECOVERY_DECAY'))) + return Lang::main('intError2', ['send mail']); + + if (!Util::sendMail($oldEmail, 'revert-email', [$token, $oldEmail], Cfg::get('ACC_RECOVERY_DECAY'))) + return Lang::main('intError2', ['send mail']); + + $this->success = true; + return Lang::account('updateMessage', 'personal', [$this->_post['newemail']]); + } +} + +?> diff --git a/endpoints/account/update-general-settings.php b/endpoints/account/update-general-settings.php new file mode 100644 index 00000000..73a8cda0 --- /dev/null +++ b/endpoints/account/update-general-settings.php @@ -0,0 +1,60 @@ + ['filter' => FILTER_VALIDATE_INT, 'options' => ['default' => 0, 'min_range' => 1, 'max_range' => 11]], + 'modelgender' => ['filter' => FILTER_VALIDATE_INT, 'options' => ['default' => 0, 'min_range' => 1, 'max_range' => 2] ], + 'idsInLists' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkCheckbox'] ] + ); + + private bool $success = false; + + protected function generate() : void + { + if (User::isBanned()) + return; + + if ($message = $this->updateGeneral()) + $_SESSION['msg'] = ['general', $this->success, $message]; + } + + private function updateGeneral() : string + { + if (!$this->assertPOST('modelrace', 'modelgender')) + return Lang::main('genericError'); + + 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()->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()->qry('UPDATE ::account SET `debug` = %i WHERE `id` = %i', $this->_post['idsInLists'] ? 1 : 0, User::$id))) + return Lang::main('intError'); + + $this->success = true; + return Lang::account('updateMessage', 'general'); + } +} + +?> diff --git a/endpoints/account/update-password.php b/endpoints/account/update-password.php new file mode 100644 index 00000000..8038f8be --- /dev/null +++ b/endpoints/account/update-password.php @@ -0,0 +1,86 @@ + ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkTextLine']], + 'newPassword' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkTextLine']], + 'confirmPassword' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkTextLine']], + 'globalLogout' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkCheckbox']] + ); + + private bool $success = false; + + public function __construct(string $rawParam) + { + if (Cfg::get('ACC_AUTH_MODE') != AUTH_MODE_SELF) + (new TemplateResponse())->generateError(); + + parent::__construct($rawParam); + } + + protected function generate() : void + { + if (User::isBanned()) + return; + + if ($msg = $this->updatePassword()) + $_SESSION['msg'] = ['password', $this->success, $msg]; + } + + private function updatePassword() : string + { + if (!$this->assertPOST('currentPassword', 'newPassword', 'confirmPassword')) + return Lang::main('intError'); + + if (!Util::validatePassword($this->_post['newPassword'], $e)) + 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` = %i', User::$id); + if ($userData['status'] != ACC_STATUS_NONE && $userData['status'] != ACC_STATUS_CHANGE_PASS && $userData['statusTimer'] > time()) + 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'); + + if (User::verifyCrypt($this->_post['newPassword'], $userData['passHash'])) + return Lang::account('newPassDiff'); + + $token = Util::createHash(); + + // store new hash in updateValue field, exchange when confirmation mail gets confirmed + 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` = %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()->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 new file mode 100644 index 00000000..7876d605 --- /dev/null +++ b/endpoints/account/update-username.php @@ -0,0 +1,61 @@ + ['filter' => FILTER_CALLBACK, 'options' => [Util::class, 'validateUsername']] + ); + + private bool $success = false; + + public function __construct(string $rawParam) + { + if (Cfg::get('ACC_AUTH_MODE') != AUTH_MODE_SELF) + (new TemplateResponse())->generateError(); + + parent::__construct($rawParam); + } + + protected function generate() : void + { + if (User::isBanned()) + return; + + if ($msg = $this->updateUsername()) + $_SESSION['msg'] = ['username', $this->success, $msg]; + } + + private function updateUsername() : string + { + if (!$this->assertPOST('newUsername')) + return Lang::main('intError'); + + 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(%s)', $this->_post['newUsername'])) + return Lang::account('nameInUse'); + + 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 new file mode 100644 index 00000000..8bae7835 --- /dev/null +++ b/endpoints/account/weightscales.php @@ -0,0 +1,121 @@ + ['filter' => FILTER_VALIDATE_INT, 'options' => ['min_range' => 1, 'max_range' => 1]], + 'delete' => ['filter' => FILTER_VALIDATE_INT, 'options' => ['min_range' => 1, 'max_range' => 1]], + 'id' => ['filter' => FILTER_VALIDATE_INT ], + 'name' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkName'] ], + 'scale' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkScale'] ] + ); + + protected function generate() : void + { + if (User::isBanned()) + return; + + if ($this->_post['save'] && $this->_post['id']) + $this->updateWeights(); + + else if ($this->_post['save']) + $this->createWeights(); + + else if ($this->_post['delete']) + $this->deleteWeights(); + } + + private function createWeights() : void + { + if (!$this->assertPOST('name', 'scale')) + return; + + $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()->qry('INSERT INTO ::account_weightscales (`userId`, `name`) VALUES (%i, %s)', User::$id, $this->_post['name'])) + if ($this->storeScaleData($id)) + $this->result = $id; + } + + private function updateWeights() : void + { + if (!$this->assertPOST('name', 'scale', 'id')) + return; + + // not in DB or not owned by user + 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()->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 + $this->result = $this->_post['id']; + } + + private function deleteWeights() : void + { + if ($this->assertPOST('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()->qry('DELETE FROM ::account_weightscale_data WHERE `id` = %i', $scaleId))) + 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; + } + + + /*************************************/ + /* additional request data callbacks */ + /*************************************/ + + protected static function checkScale(string $val) : array + { + if (preg_match('/^((\w+:\d+)(,\w+:\d+)*)$/', $val)) + return array_map(fn($x) => array_combine(['field', 'val'], explode(':', $x)), explode(',', $val)); + + return []; + } + + protected static function checkName(string $val) : string + { + return mb_substr(preg_replace('/[^[:print:]]/', '', trim(urldecode($val))), 0, 32); + } +} + +?> diff --git a/endpoints/achievement/achievement.php b/endpoints/achievement/achievement.php new file mode 100644 index 00000000..6c63fd11 --- /dev/null +++ b/endpoints/achievement/achievement.php @@ -0,0 +1,520 @@ + 100 +* } +*/ + +class AchievementBaseResponse extends TemplateResponse implements ICache +{ + use TrDetailPage, TrCache; + + protected int $cacheType = CACHE_TYPE_DETAIL_PAGE; + + protected string $template = 'achievement'; + protected string $pageName = 'achievement'; + protected ?int $activeTab = parent::TAB_DATABASE; + protected array $breadcrumb = [0, 9]; + + public int $type = Type::ACHIEVEMENT; + public int $typeId = 0; + public int $reqCrtQty = 0; + public ?array $mail = null; + public string $description = ''; + public array $criteria = []; + public ?array $rewards = null; + + private AchievementList $subject; + + public function __construct(string $id) + { + parent::__construct($id); + + $this->typeId = intVal($id); + $this->contribute = Type::getClassAttrib($this->type, 'contribute') ?? CONTRIBUTE_NONE; + } + + protected function generate() : void + { + $this->subject = new AchievementList(array(['id', $this->typeId])); + if ($this->subject->error) + $this->generateNotFound(Lang::game('achievement'), Lang::achievement('notFound')); + + $this->extendGlobalData($this->subject->getJSGlobals(GLOBALINFO_REWARDS)); + + $this->h1 = $this->subject->getField('name', true); + + $this->gPageInfo += array( + 'type' => $this->type, + 'typeId' => $this->typeId, + 'name' => $this->h1 + ); + + + /*************/ + /* Menu Path */ + /*************/ + + // create page title and path + $curCat = $this->subject->getField('category'); + $catPath = []; + while ($curCat > 0) + { + $catPath[] = $curCat; + $curCat = DB::Aowow()->SelectCell('SELECT `parentCat` FROM ::achievementcategory WHERE `id` = %i', $curCat); + } + + $this->breadcrumb = array_merge($this->breadcrumb, array_reverse($catPath)); + + + /**************/ + /* Page Title */ + /**************/ + + array_unshift($this->title, $this->subject->getField('name', true), Util::ucFirst(Lang::game('achievement'))); + + + /***********/ + /* Infobox */ + /***********/ + + $infobox = Lang::getInfoBoxForFlags($this->subject->getField('cuFlags')); + + // points + if ($_ = $this->subject->getField('points')) + $infobox[] = Lang::achievement('points').Lang::main('colon').'[achievementpoints='.$_.']'; + + // location + // todo (low) + + // faction + $infobox[] = Lang::main('side') . match ($this->subject->getField('faction')) + { + 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 + }; + + // 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]'; + $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->subject->getField('flags') & ACHIEVEMENT_FLAG_COUNTER)); + + + /**********/ + /* Series */ + /**********/ + + $series = []; + if ($c = $this->subject->getField('chainId')) + { + $chainAcv = new AchievementList(array(['chainId', $c])); + + foreach ($chainAcv->iterate() as $aId => $__) + { + $pos = $chainAcv->getField('chainPos'); + if (!isset($series[$pos])) + $series[$pos] = []; + + $series[$pos][] = array( + 'side' => (int)$chainAcv->getField('faction'), + 'typeStr' => Type::getFileString(Type::ACHIEVEMENT), + 'typeId' => $aId, + 'name' => $chainAcv->getField('name', true) + ); + } + } + + if ($series) + $this->series = [[array_values($series), null]]; + + + /****************/ + /* Main Content */ + /****************/ + + $this->headIcons = [$this->subject->getField('iconString')]; + $this->description = $this->subject->getField('description', true); + $this->redButtons = array( + BUTTON_WOWHEAD => !($this->subject->getField('cuFlags') & CUSTOM_SERVERSIDE), + BUTTON_LINKS => array( + 'linkColor' => 'ffffff00', + 'linkId' => Type::getFileString(Type::ACHIEVEMENT).':'.$this->typeId.':"..UnitGUID("player")..":0:0:0:0:0:0:0:0', + 'linkName' => $this->h1, + 'type' => $this->type, + 'typeId' => $this->typeId + ) + ); + $this->reqCrtQty = $this->subject->getField('reqCriteriaCount'); + + if ($this->createMail()) + $this->addScript([SC_CSS_FILE, 'css/Book.css']); + + // create rewards + $rewItems = $rewTitles = []; + if ($foo = $this->subject->getField('rewards')) + { + if ($itemRewards = array_filter($foo, fn($x) => $x[0] == Type::ITEM)) + { + $bar = new ItemList(array(['i.id', array_column($itemRewards, 1)])); + foreach ($bar->iterate() as $id => $__) + $rewItems[] = new IconElement(Type::ITEM, $id, $bar->getField('name', true), quality: $bar->getField('quality')); + } + + if ($titleRewards = array_filter($foo, fn($x) => $x[0] == Type::TITLE)) + { + $bar = new TitleList(array(['id', array_column($titleRewards, 1)])); + foreach ($bar->iterate() as $id => $__) + $rewTitles[] = Lang::achievement('titleReward', [$id, trim(str_replace('%s', '', $bar->getField('male', true)))]); + } + } + + if (($text = $this->subject->getField('reward', true)) || $rewItems || $rewTitles) + $this->rewards = [$rewItems, $rewTitles, $text]; + + // factionchange-equivalent + 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) + { + $this->transfer = Lang::achievement('_transfer', array( + $altAcv->id, + ITEM_QUALITY_NORMAL, + $altAcv->getField('iconString'), + $altAcv->getField('name', true), + $pendant > 0 ? 'alliance' : 'horde', + $pendant > 0 ? Lang::game('si', SIDE_ALLIANCE) : Lang::game('si', SIDE_HORDE) + )); + } + } + + + /*****************/ + /* Criteria List */ + /*****************/ + + // 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()->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 = []; + + foreach ($this->subject->getCriteria() as $crt) + { + // hide hidden criteria for regular users (really do..?) + // if (($crt['completionFlags'] & ACHIEVEMENT_CRITERIA_FLAG_HIDDEN) && !User::isInGroup(U_GROUP_STAFF)) + // continue; + + // alternative display option + $crtName = Util::localizedString($crt, 'name'); + $killSuffix = null; + + $obj = (int)$crt['value1']; + $qty = (int)$crt['value2']; + + switch ($crt['type']) + { + // link to npc + case ACHIEVEMENT_CRITERIA_TYPE_KILL_CREATURE: + $killSuffix = Lang::achievement('slain'); + case ACHIEVEMENT_CRITERIA_TYPE_KILLED_BY_CREATURE: + $crtIcon = new IconElement(Type::NPC, $obj, $crtName ?: CreatureList::getName($obj), size: IconElement::SIZE_SMALL, element: 'iconlist-icon', extraText: $crtName ? null : $killSuffix); + break; + // link to area (by map) + case ACHIEVEMENT_CRITERIA_TYPE_WIN_BG: + case ACHIEVEMENT_CRITERIA_TYPE_WIN_ARENA: + 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` = %s', $obj); + $crtIcon = new IconElement(Type::ZONE, $zoneId ?: 0, $crtName ?: ZoneList::getName($zoneId), size: IconElement::SIZE_SMALL, element: 'iconlist-icon'); + break; + // link to area + case ACHIEVEMENT_CRITERIA_TYPE_COMPLETE_QUESTS_IN_ZONE: + case ACHIEVEMENT_CRITERIA_TYPE_HONORABLE_KILL_AT_AREA: + $crtIcon = new IconElement(Type::ZONE, $obj, $crtName ?: ZoneList::getName($obj), size: IconElement::SIZE_SMALL, element: 'iconlist-icon'); + break; + // link to skills + case ACHIEVEMENT_CRITERIA_TYPE_REACH_SKILL_LEVEL: + case ACHIEVEMENT_CRITERIA_TYPE_LEARN_SKILL_LEVEL: + case ACHIEVEMENT_CRITERIA_TYPE_LEARN_SKILLLINE_SPELLS: + case ACHIEVEMENT_CRITERIA_TYPE_LEARN_SKILL_LINE: + $crtIcon = new IconElement(Type::SKILL, $obj, $crtName ?: SkillList::getName($obj), size: IconElement::SIZE_SMALL, element: 'iconlist-icon'); + $this->extendGlobalIds(Type::SKILL, $obj); + break; + // link to class + case ACHIEVEMENT_CRITERIA_TYPE_HK_CLASS: + $crtIcon = new IconElement(Type::CHR_CLASS, $obj, $crtName ?: CharClassList::getName($obj), size: IconElement::SIZE_SMALL, element: 'iconlist-icon'); + break; + // link to race + case ACHIEVEMENT_CRITERIA_TYPE_HK_RACE: + $crtIcon = new IconElement(Type::CHR_RACE, $obj, $crtName ?: CharRaceList::getName($obj), size: IconElement::SIZE_SMALL, element: 'iconlist-icon'); + break; + // link to achivement + case ACHIEVEMENT_CRITERIA_TYPE_COMPLETE_ACHIEVEMENT: + $crtIcon = new IconElement(Type::ACHIEVEMENT, $obj, $crtName ?: AchievementList::getName($obj), size: IconElement::SIZE_SMALL, element: 'iconlist-icon'); + $this->extendGlobalIds(Type::ACHIEVEMENT, $obj); + break; + // link to quest + case ACHIEVEMENT_CRITERIA_TYPE_COMPLETE_QUEST: + $crtIcon = new IconElement(Type::QUEST, $obj, $crtName ?: QuestList::getName($obj), size: IconElement::SIZE_SMALL, element: 'iconlist-icon'); + break; + // link to spell + case ACHIEVEMENT_CRITERIA_TYPE_BE_SPELL_TARGET: + case ACHIEVEMENT_CRITERIA_TYPE_BE_SPELL_TARGET2: + case ACHIEVEMENT_CRITERIA_TYPE_CAST_SPELL: + case ACHIEVEMENT_CRITERIA_TYPE_LEARN_SPELL: + case ACHIEVEMENT_CRITERIA_TYPE_CAST_SPELL2: + $crtIcon = new IconElement(Type::SPELL, $obj, $crtName ?: SpellList::getName($obj), size: IconElement::SIZE_SMALL, element: 'iconlist-icon'); + $this->extendGlobalIds(Type::SPELL, $obj); + break; + // link to item + case ACHIEVEMENT_CRITERIA_TYPE_OWN_ITEM: + case ACHIEVEMENT_CRITERIA_TYPE_USE_ITEM: + case ACHIEVEMENT_CRITERIA_TYPE_LOOT_ITEM: + case ACHIEVEMENT_CRITERIA_TYPE_EQUIP_ITEM: + $item = new ItemList([['id', $obj]]); + $crtIcon = new IconElement(Type::ITEM, $obj, $crtName ?: $item->getField('name', true), quality: $item->getField('quality'), size: IconElement::SIZE_SMALL, element: 'iconlist-icon'); + $this->extendGlobalData($item->getJSGlobals()); + break; + // link to faction (/w target reputation) + case ACHIEVEMENT_CRITERIA_TYPE_GAIN_REPUTATION: + $crtIcon = new IconElement(Type::FACTION, $obj, $crtName ?: FactionList::getName($obj), size: IconElement::SIZE_SMALL, element: 'iconlist-icon', extraText: '('.Lang::getReputationLevelForPoints($qty).')'); + break; + // link to GObject + case ACHIEVEMENT_CRITERIA_TYPE_USE_GAMEOBJECT: + case ACHIEVEMENT_CRITERIA_TYPE_FISH_IN_GAMEOBJECT: + $crtIcon = new IconElement(Type::OBJECT, $obj, $crtName ?: GameObjectList::getName($obj), size: IconElement::SIZE_SMALL, element: 'iconlist-icon'); + break; + // link to emote + case ACHIEVEMENT_CRITERIA_TYPE_DO_EMOTE: + $crtIcon = new IconElement(Type::EMOTE, $obj, $crtName ?: EmoteList::getName($obj), size: IconElement::SIZE_SMALL, element: 'iconlist-icon'); + break; + default: + // Add a gold coin icon if required + if ($crt['completionFlags'] & ACHIEVEMENT_CRITERIA_FLAG_MONEY_COUNTER ) + $crtIcon = new IconElement(0, 0, '', extraText: Util::formatMoney($qty)); + else + $crtIcon = new IconElement(0, 0, $crtName); + break; + } + + if (User::isInGroup(U_GROUP_STAFF)) + $crtIcon->extraText .= ' [CriteriaId: '.$crt['id'].']'; + + $extraData = []; + foreach ($crtExtraData[$crt['id']] ?? [] as $xType => $xData) + { + switch ($xType) + { + case ACHIEVEMENT_CRITERIA_DATA_TYPE_T_CREATURE: + $extraData[] = CreatureList::makeLink($xData['value1']); + break; + case ACHIEVEMENT_CRITERIA_DATA_TYPE_T_PLAYER_CLASS_RACE: + case ACHIEVEMENT_CRITERIA_DATA_TYPE_S_PLAYER_CLASS_RACE: + if ($xData['value1']) + $extraData[] = CharClassList::makeLink($xData['value1']); + + if ($xData['value2']) + $extraData[] = CharRaceList::makeLink($xData['value2']); + + break; + case ACHIEVEMENT_CRITERIA_DATA_TYPE_S_AURA: + case ACHIEVEMENT_CRITERIA_DATA_TYPE_T_AURA: + $extraData[] = SpellList::makeLink($xData['value1']); + break; + case ACHIEVEMENT_CRITERIA_DATA_TYPE_S_AREA: + $extraData[] = ZoneList::makeLink($xData['value1']); + break; + case ACHIEVEMENT_CRITERIA_DATA_TYPE_SCRIPT: + if ($xData['ScriptName'] && User::isInGroup(U_GROUP_STAFF)) + $extraData[] = 'Script '.$xData['ScriptName']; + break; + case ACHIEVEMENT_CRITERIA_DATA_TYPE_HOLIDAY: + if ($we = new WorldEventList(array(['holidayId', $xData['value1']]))) + $extraData[] = ''.$we->getField('name', true).''; + break; + case ACHIEVEMENT_CRITERIA_DATA_TYPE_MAP_ID: + $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']); + break; + default: + if (User::isInGroup(U_GROUP_STAFF)) + $extraData[] = 'has extra criteria data'; + } + } + + if ($extraData) + $crtIcon->extraText .= '
('.implode(', ', $extraData).')'; + + $this->criteria[] = $crtIcon; + } + + + /**************/ + /* Extra Tabs */ + /**************/ + + $this->lvTabs = new Tabs(['parent' => "\$\$WH.ge('tabs-generic')"], 'tabsRelated', true); + + // tab: see also + $conditions = array( + ['name_loc'.Lang::getLocale()->value, $this->subject->getField('name', true)], + ['id', $this->typeId, '!'] + ); + $saList = new AchievementList($conditions); + if (!$saList->error) + { + $this->extendGlobalData($saList->getJSGlobals()); + + $this->lvTabs->addListviewTab(new Listview(array( + 'data' => $saList->getListviewData(), + 'id' => 'see-also', + 'name' => '$LANG.tab_seealso', + 'visibleCols' => ['category'] + ), AchievementList::$brickFile)); + } + + // tab: criteria of + $refs = DB::Aowow()->SelectCol('SELECT `refAchievementId` FROM ::achievementcriteria WHERE `type` = %i AND `value1` = %i', + ACHIEVEMENT_CRITERIA_TYPE_COMPLETE_ACHIEVEMENT, + $this->typeId + ); + + if (!empty($refs)) + { + $coList = new AchievementList(array(['id', $refs])); + if (!$coList->error) + { + $this->extendGlobalData($coList->getJSGlobals()); + + $this->lvTabs->addListviewTab(new Listview(array( + 'data' => $coList->getListviewData(), + 'id' => 'criteria-of', + 'name' => '$LANG.tab_criteriaof', + 'visibleCols' => ['category'] + ), AchievementList::$brickFile)); + } + } + + // tab: condition for + $cnd = new Conditions(); + $cnd->getByCondition(Type::ACHIEVEMENT, $this->typeId)->prepare(); + if ($tab = $cnd->toListviewTab('condition-for', '$LANG.tab_condition_for')) + { + $this->extendGlobalData($cnd->getJsGlobals()); + $this->lvTabs->addDataTab(...$tab); + } + + parent::generate(); + + if ($this->subject->getField('flags') & ACHIEVEMENT_FLAG_REALM_FIRST) + $this->result->registerDisplayHook('infobox', [self::class, 'infoboxHook']); + } + + private function createMail() : bool + { + if ($_ = $this->subject->getField('mailTemplate')) + { + $letter = DB::Aowow()->selectRow('SELECT * FROM ::mails WHERE `id` = %i', $_); + if (!$letter) + return false; + + $this->mail = array( + 'attachments' => [], + 'subject' => Util::parseHtmlText(Util::localizedString($letter, 'subject', true)), + 'text' => Util::parseHtmlText(Util::localizedString($letter, 'text', true)), + 'header' => [$_, null, null] + ); + } + else if ($_ = Util::parseHtmlText($this->subject->getField('text', true, true))) + { + $this->mail = array( + 'attachments' => [], + 'subject' => Util::parseHtmlText($this->subject->getField('subject', true, true)), + 'text' => $_, + 'header' => [-$this->typeId, null, null] + ); + } + else + return false; + + if ($senderId = $this->subject->getField('sender')) + if ($senderName = CreatureList::getName($senderId)) + $this->mail['header'][1] = Lang::mail('mailBy', [$senderId, $senderName]); + + return true; + } + + /* finalize infobox */ + public static function infoboxHook(Template\PageTemplate &$pt, ?InfoboxMarkup &$markup) : void + { + // realm first still available? + if (!DB::isConnectable(DB_AUTH)) + return; + + $avlb = []; + foreach (Profiler::getRealms() AS $rId => $rData) + if (!DB::Characters($rId)->selectCell('SELECT 1 FROM character_achievement WHERE `achievement` = %i', $pt->typeId)) + $avlb[] = Util::ucWords($rData['name']); + + if (!$avlb) + return; + + $addRow = Lang::achievement('rfAvailable').implode(', ', $avlb); + + if (!$markup) + $markup = new InfoboxMarkup([$addRow], ['allow' => Markup::CLASS_STAFF, 'dbpage' => true], 'infobox-contents0'); + else + $markup->addItem($addRow); + } +} + +?> diff --git a/endpoints/achievement/achievement_power.php b/endpoints/achievement/achievement_power.php new file mode 100644 index 00000000..05e78fc6 --- /dev/null +++ b/endpoints/achievement/achievement_power.php @@ -0,0 +1,50 @@ + ['filter' => FILTER_CALLBACK, 'options' => [Locale::class, 'tryFromDomain']] + ); + + public function __construct($id) + { + parent::__construct($id); + + // temp locale + if ($this->_get['domain']) + Lang::load($this->_get['domain']); + + $this->typeId = intVal($id); + } + + protected function generate() : void + { + $achievement = new AchievementList(array(['id', $this->typeId])); + if ($achievement->error) + $this->cacheType = CACHE_TYPE_NONE; + else + $opts = array( + 'name' => $achievement->getField('name', true), + 'tooltip' => $achievement->renderTooltip(), + 'icon' => $achievement->getField('iconString') + ); + + $this->result = new Tooltip(self::POWER_TEMPLATE, $this->typeId, $opts ?? []); + } +} + +?> diff --git a/endpoints/achievements/achievements.php b/endpoints/achievements/achievements.php new file mode 100644 index 00000000..6597306f --- /dev/null +++ b/endpoints/achievements/achievements.php @@ -0,0 +1,170 @@ + ['filter' => FILTER_VALIDATE_REGEXP, 'options' => ['regexp' => Filter::PATTERN_PARAM]] + ); + protected array $validCats = array( + 92 => true, + 96 => [14861, 14862, 14863], + 97 => [14777, 14778, 14779, 14780], + 95 => [165, 14801, 14802, 14803, 14804, 14881, 14901, 15003], + 168 => [14808, 14805, 14806, 14921, 14922, 14923, 14961, 14962, 15001, 15002, 15041, 15042], + 169 => [170, 171, 172], + 201 => [14864, 14865, 14866], + 155 => [160, 187, 159, 163, 161, 162, 158, 14981, 156, 14941], + 81 => true, + 1 => array ( + 130 => [140, 145, 147, 191], + 141 => true, + 128 => [135, 136, 137], + 122 => [123, 124, 125, 126, 127], + 133 => true, + 14807 => [14821, 14822, 14823, 14963, 15021, 15062], + 132 => [178, 173], + 134 => true, + 131 => true, + 21 => [152, 153, 154] + ) + ); + + public function __construct(string $rawParam) + { + $this->getCategoryFromUrl($rawParam); + + parent::__construct($rawParam); + + if ($this->category) + $this->subCat = '='.implode('.', $this->category); + + $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; + } + + protected function generate() : void + { + $this->h1 = Util::ucFirst(Lang::game('achievements')); + + $conditions = [Listview::DEFAULT_SIZE]; + if (!User::isInGroup(U_GROUP_EMPLOYEE)) + $conditions[] = [['cuFlags', CUSTOM_EXCLUDE_FOR_LISTVIEW, '&'], 0]; + + // include child categories if current category is empty + if ($this->category) + $conditions[] = ['category', end($this->category)]; + + if ($fiCnd = $this->filter->getConditions()) + $conditions[] = $fiCnd; + + + /*************/ + /* Menu Path */ + /*************/ + + foreach ($this->category as $cat) + $this->breadcrumb[] = $cat; + + + /**************/ + /* Page Title */ + /**************/ + + array_unshift($this->title, Util::ucFirst(Lang::game('achievements'))); + if ($this->category) + array_unshift($this->title, Lang::achievement('cat', end($this->category))); + + + /****************/ + /* Main Content */ + /****************/ + + // fix modern client achievement category structure: top catg [1:char, 2:statistic, 3:guild] + if ($this->category && $this->category[0] != 1) + $link = '=1.'.implode('.', $this->category); + else if ($this->category) + $link = '=2'.(count($this->category) > 1 ? '.'.implode('.', array_slice($this->category, 1)) : ''); + else + $link = ''; + + $this->redButtons[BUTTON_WOWHEAD] = true; + $this->wowheadLink = sprintf(WOWHEAD_LINK, Lang::getLocale()->domain(), $this->pageName, $link); + + if ($fiQuery = $this->filter->buildGETParam()) + $this->wowheadLink .= '&filter='.$fiQuery; + + $acvList = new AchievementList($conditions, ['calcTotal' => true]); + if (!$acvList->getMatches() && $this->category) + { + // ToDo - we also branch into here if the filter prohibits results. That should be skipped. + $conditions = [Listview::DEFAULT_SIZE]; + if ($fiCnd) + $conditions[] = $fiCnd; + 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]); + } + + $tabData = []; + if (!$acvList->error) + { + $tabData['data'] = $acvList->getListviewData(); + + // fill g_items, g_titles, g_achievements + $this->extendGlobalData($acvList->getJSGlobals()); + + // if we are have different cats display field + if ($acvList->hasDiffFields('category')) + $tabData['visibleCols'] = ['category']; + + if ($this->filter->fiExtraCols) + $tabData['extraCols'] = '$fi_getExtraCols(fi_extraCols, 0, 0)'; + + // create note if search limit was exceeded + if ($acvList->getMatches() > Listview::DEFAULT_SIZE) + { + $tabData['note'] = sprintf(Util::$tryFilteringString, 'LANG.lvnote_achievementsfound', $acvList->getMatches(), Listview::DEFAULT_SIZE); + $tabData['_truncated'] = 1; + } + } + $this->lvTabs = new Tabs(['parent' => "\$\$WH.ge('tabs-generic')"]); + + $this->lvTabs->addListviewTab(new Listview($tabData, AchievementList::$brickFile)); + + parent::generate(); + + $this->setOnCacheLoaded([self::class, 'onBeforeDisplay']); + } + + public static function onBeforeDisplay() + { + // sort for dropdown-menus in filter + Lang::sort('game', 'si'); + } +} + +?> diff --git a/endpoints/admin/announcements.php b/endpoints/admin/announcements.php new file mode 100644 index 00000000..b1fdc774 --- /dev/null +++ b/endpoints/admin/announcements.php @@ -0,0 +1,68 @@ + Content > Announcements + + protected array $expectedGET = array( + 'id' => ['filter' => FILTER_VALIDATE_INT ], + 'edit' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkEmptySet'] ], + 'status' => ['filter' => FILTER_VALIDATE_INT, 'options' => ['min_range' => 0, 'max_range' => 2]] + ); + + protected function generate() : void + { + if ($this->_get['id'] && isset($this->_get['status'])) + { + $this->updateStatus(); + $this->forward($_SERVER['HTTP_REFERER'] ?? '.'); + } + else if ($this->_get['edit']) + $this->displayEditor(); + else + $this->displayListing(); + + parent::generate(); + } + + private function updateStatus() : void + { + if (!$this->assertGET('status', 'id')) + { + trigger_error('AdminAnnouncementsResponse::updateStatus - error in _GET id/status'); + return; + } + + 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()->qry('UPDATE ::announcements SET `status` = %i WHERE `id` = %i', $this->_get['status'], $this->_get['id']); + } + + private function displayEditor() : void + { + // TBD + $this->extraHTML = 'TODO - editor'; + } + + private function displayListing() : void + { + // TBD + // some form of listview with [NEW] button somewhere near the head i guess + $this->extraHTML = 'TODO - announcements listing'; + } +} diff --git a/endpoints/admin/comment.php b/endpoints/admin/comment.php new file mode 100644 index 00000000..26b78e29 --- /dev/null +++ b/endpoints/admin/comment.php @@ -0,0 +1,51 @@ + ['filter' => FILTER_VALIDATE_INT ], + 'status' => ['filter' => FILTER_VALIDATE_INT, 'options' => ['min_range' => 0, 'max_range' => 1]] + ); + + protected function generate() : void + { + if (!$this->assertPOST('id', 'status')) + { + trigger_error('AdminCommentResponse - malformed request received', E_USER_ERROR); + $this->result = self::ERR_MISCELLANEOUS; + return; + } + + // check if is marked as outdated CC_FLAG_OUTDATED? + + $ok = false; + if ($this->_post['status']) // outdated, mark as deleted and clear other flags (sticky + outdated) + { + 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()->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); + } + + $this->result = $ok ? self::ERR_NONE : self::ERR_WRITE_DB; + } +} + +?> diff --git a/endpoints/admin/guide.php b/endpoints/admin/guide.php new file mode 100644 index 00000000..90ee553e --- /dev/null +++ b/endpoints/admin/guide.php @@ -0,0 +1,81 @@ + ['filter' => FILTER_VALIDATE_INT ], + 'status' => ['filter' => FILTER_VALIDATE_INT, 'options' => ['min_range' => GuideMgr::STATUS_APPROVED, 'max_range' => GuideMgr::STATUS_REJECTED]], + 'msg' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkTextBlob'] ] + ); + + protected function generate() : void + { + if (!$this->assertPOST('id', 'status')) + { + trigger_error('AdminGuideResponse - malformed request received', E_USER_ERROR); + $this->result = self::ERR_MISCELLANEOUS; + return; + } + + $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); + $this->result = self::ERR_GUIDE; + return; + } + + if ($this->_post['status'] == $guide['status']) + { + trigger_error('AdminGuideResponse - guide #'.$this->_post['id'].' already has status #'.$this->_post['status'], E_USER_ERROR); + $this->result = self::ERR_STATUS; + return; + } + + // status can only be APPROVED or REJECTED due to input validation + if (!$this->update($this->_post['id'], $this->_post['status'], $this->_post['msg'])) + { + trigger_error('AdminGuideResponse - write to db failed for guide #'.$this->_post['id'], E_USER_ERROR); + $this->result = self::ERR_WRITE_DB; + return; + } + + if ($this->_post['status'] == GuideMgr::STATUS_APPROVED) + Util::gainSiteReputation($guide['userId'], SITEREP_ACTION_ARTICLE, ['id' => $this->_post['id']]); + + $this->result = self::ERR_NONE; + } + + private function update(int $id, int $status, ?string $msg = null) : bool + { + if ($status == GuideMgr::STATUS_APPROVED) // set display rev to latest + $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()->qry('UPDATE ::guides SET `status` = %i WHERE `id` = %i', $status, $id); + + if (!$ok) + return false; + + 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()->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 new file mode 100644 index 00000000..26d6f844 --- /dev/null +++ b/endpoints/admin/guides.php @@ -0,0 +1,46 @@ + Content > Guides Awaiting Approval + + protected function generate() : void + { + $this->h1 = 'Pending Guides'; + array_unshift($this->title, $this->h1); + + $this->lvTabs = new Tabs(['parent' => "\$\$WH.ge('tabs-generic')"]); + + parent::generate(); + + $pending = new GuideList([['status', GuideMgr::STATUS_REVIEW]]); + if ($pending->error) + $data = []; + else + { + $data = $pending->getListviewData(); + $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; + } + + $this->lvTabs->addListviewTab(new Listview(array( + 'data' => array_values($data), + 'hiddenCols' => ['patch', 'comments', 'views', 'rating'], + 'extraCols' => '$_' + ), GuideList::$brickFile, 'guideAdminCol')); + } +} + +?> diff --git a/endpoints/admin/out-of-date.php b/endpoints/admin/out-of-date.php new file mode 100644 index 00000000..4f0f1b00 --- /dev/null +++ b/endpoints/admin/out-of-date.php @@ -0,0 +1,34 @@ + Content > Out of Date Comments + + protected function generate() : void + { + $this->h1 = 'Out of Date Comments'; + array_unshift($this->title, $this->h1); + + $this->lvTabs = new Tabs(['parent' => "\$\$WH.ge('tabs-generic')"]); + + parent::generate(); + + $this->lvTabs->addListviewTab(new Listview(array( + 'data' => CommunityContent::getCommentPreviews(['flags' => CC_FLAG_OUTDATED]), + 'extraCols' => '$_' + ), 'commentpreview', 'commentAdminCol')); + } +} + +?> diff --git a/endpoints/admin/phpinfo.php b/endpoints/admin/phpinfo.php new file mode 100644 index 00000000..1fd04b8c --- /dev/null +++ b/endpoints/admin/phpinfo.php @@ -0,0 +1,80 @@ + Development > PHP Information + + protected function generate() : void + { + $this->h1 = 'PHP Information'; + array_unshift($this->title, $this->h1); + + $this->lvTabs = new Tabs(['parent' => "\$\$WH.ge('tabs-generic')"]); + + parent::generate(); + + $this->addScript([SC_CSS_STRING, << $b) + { + ob_start(); + phpinfo($b); + $buff = ob_get_contents(); + ob_end_clean(); + + $buff = explode('
', $buff)[1]; + $buff = explode('
', $buff); + array_pop($buff); // remove last from stack + $buff = implode('', $buff); // sew it together + + if (strpos($buff, '

')) + $buff = explode('

', $buff)[1]; + + if (strpos($buff, '

')) + { + $parts = explode('

', $buff); + foreach ($parts as $p) + { + if (!preg_match('/\w/i', $p)) + continue; + + $p = explode('

', $p); + $name = $names[$i] ? $names[$i].': ' : ''; + if (preg_match('/]*>([\w\s\d]+)<\/a>/i', $p[0], $m)) + $name .= $m[1]; + else + $name .= $p[0]; + + $this->lvTabs->addDataTab(strtolower(strtr($name, [' ' => ''])), $name, $p[1]); + } + } + else + $this->lvTabs->addDataTab(strtolower($names[$i]), $names[$i], $buff); + } + } +} + +?> diff --git a/endpoints/admin/reports.php b/endpoints/admin/reports.php new file mode 100644 index 00000000..a64c8df8 --- /dev/null +++ b/endpoints/admin/reports.php @@ -0,0 +1,29 @@ + Reports + + protected function generate() : void + { + $this->h1 = 'Reports'; + array_unshift($this->title, $this->h1); + + $this->extraHTML = 'NYI'; + + parent::generate(); + } +} + +?> diff --git a/endpoints/admin/screenshots.php b/endpoints/admin/screenshots.php new file mode 100644 index 00000000..c8b87ba8 --- /dev/null +++ b/endpoints/admin/screenshots.php @@ -0,0 +1,68 @@ + Content > Screenshots + + protected array $scripts = array( + [SC_JS_FILE, 'js/screenshot.js'], + [SC_CSS_STRING, '.layout {margin: 0px 25px; max-width: inherit; min-width: 1200px; }'], + [SC_CSS_STRING, '#highlightedRow { background-color: #322C1C; }'] + ); + protected array $expectedGET = array( + 'action' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkTextLine']], + 'all' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkEmptySet']], + 'type' => ['filter' => FILTER_VALIDATE_INT ], + 'typeid' => ['filter' => FILTER_VALIDATE_INT ], + 'user' => ['filter' => FILTER_CALLBACK, 'options' => 'urldecode' ] + ); + + public ?bool $getAll = null; + public array $ssPages = []; + public array $ssData = []; + public int $ssNFound = 0; + public array $pageTypes = []; + + protected function generate() : void + { + $this->h1 = 'Screenshot Manager'; + + // types that can have screenshots + foreach (Type::getClassesFor(0, 'contribute', CONTRIBUTE_SS) as $type => $obj) + $this->pageTypes[$type] = Util::ucWords(Lang::game(Type::getFileString($type))); + + $ssGetAll = $this->_get['all']; + $ssPages = []; + $ssData = []; + $nMatches = 0; + + if ($this->_get['type'] && $this->_get['typeid']) + $ssData = ScreenshotMgr::getScreenshots($this->_get['type'], $this->_get['typeid'], nFound: $nMatches); + 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(%s)', $this->_get['user'])) + $ssData = ScreenshotMgr::getScreenshots(userId: $uId, nFound: $nMatches); + } + else + $ssPages = ScreenshotMgr::getPages($ssGetAll, $nMatches); + + $this->getAll = $ssGetAll; + $this->ssPages = $ssPages; + $this->ssData = $ssData; + $this->ssNFound = $nMatches; // ssm_numPagesFound + + parent::generate(); + } +} diff --git a/endpoints/admin/screenshots_approve.php b/endpoints/admin/screenshots_approve.php new file mode 100644 index 00000000..5bb528c2 --- /dev/null +++ b/endpoints/admin/screenshots_approve.php @@ -0,0 +1,60 @@ + ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkIdListUnsigned']] + ); + + protected function generate() : void + { + if (!$this->assertGET('id')) + { + trigger_error('AdminScreenshotsActionApproveResponse - screenshotId empty', E_USER_ERROR); + return; + } + + ScreenshotMgr::init(); + + // create resized and thumb version of screenshot + $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)) + continue; + + if (!ScreenshotMgr::createResized($id)) + continue; + + if (!ScreenshotMgr::createThumbnail($id)) + continue; + + // move pending > normal + if (!rename(sprintf(ScreenshotMgr::PATH_PENDING, $id), sprintf(ScreenshotMgr::PATH_NORMAL, $id))) + continue; + + // set as approved in DB + 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()->qry('UPDATE %n SET `cuFlags` = `cuFlags` | %i WHERE `id` = %i', $tbl, CUSTOM_HAS_SCREENSHOT, $ssData['typeId']); + + unset($ssEntries[$id]); + } + + if (!$ssEntries) + trigger_error('AdminScreenshotsActionApproveResponse - screenshot(s) # '.implode(', ', array_keys($ssEntries)).' not in db or already approved', E_USER_WARNING); + } +} diff --git a/endpoints/admin/screenshots_delete.php b/endpoints/admin/screenshots_delete.php new file mode 100644 index 00000000..bd61a07c --- /dev/null +++ b/endpoints/admin/screenshots_delete.php @@ -0,0 +1,62 @@ + ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkIdListUnsigned']] + ); + + // 2 steps: 1) remove from sight, 2) remove from disk + protected function generate() : void + { + if (!$this->assertGET('id')) + { + trigger_error('AdminScreenshotsActionDeleteResponse - screenshotId empty', E_USER_ERROR); + return; + } + + 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` & %i AND `id` = %i', CC_FLAG_DELETED, $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)); + + continue; + } + + // move normal to pending and remove resized and thumb + if (file_exists(sprintf(ScreenshotMgr::PATH_NORMAL, $id))) + rename(sprintf(ScreenshotMgr::PATH_NORMAL, $id), sprintf(ScreenshotMgr::PATH_PENDING, $id)); + + if (file_exists(sprintf(ScreenshotMgr::PATH_THUMB, $id))) + unlink(sprintf(ScreenshotMgr::PATH_THUMB, $id)); + + if (file_exists(sprintf(ScreenshotMgr::PATH_RESIZED, $id))) + unlink(sprintf(ScreenshotMgr::PATH_RESIZED, $id)); + } + + // flag as deleted if not aready + $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`) & %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()->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 new file mode 100644 index 00000000..2dca89cc --- /dev/null +++ b/endpoints/admin/screenshots_editalt.php @@ -0,0 +1,32 @@ + ['filter' => FILTER_VALIDATE_INT] + ); + protected array $expectedPOST = array( + 'alt' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkTextLine']] + ); + + protected function generate() : void + { + if (!$this->assertGET('id')) + return; + + 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_list.php b/endpoints/admin/screenshots_list.php new file mode 100644 index 00000000..bd884d42 --- /dev/null +++ b/endpoints/admin/screenshots_list.php @@ -0,0 +1,23 @@ + ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkEmptySet']] + ); + + protected function generate() : void + { + $pages = ScreenshotMgr::getPages($this->_get['all'], $nPages); + $this->result = 'ssm_screenshotPages = '.Util::toJSON($pages).";\n"; + $this->result .= 'ssm_numPagesFound = '.$nPages.';'; + } +} diff --git a/endpoints/admin/screenshots_manage.php b/endpoints/admin/screenshots_manage.php new file mode 100644 index 00000000..752469d1 --- /dev/null +++ b/endpoints/admin/screenshots_manage.php @@ -0,0 +1,31 @@ + ['filter' => FILTER_VALIDATE_INT ], + 'typeid' => ['filter' => FILTER_VALIDATE_INT ], + 'user' => ['filter' => FILTER_CALLBACK, 'options' => 'urldecode'] + ); + + protected function generate() : void + { + $res = []; + + 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(%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 new file mode 100644 index 00000000..03fd0d5d --- /dev/null +++ b/endpoints/admin/screenshots_relocate.php @@ -0,0 +1,48 @@ + ['filter' => FILTER_VALIDATE_INT], + 'typeid' => ['filter' => FILTER_VALIDATE_INT] + // (but not type..?) + ); + + protected function generate() : void + { + if (!$this->assertGET('id', 'typeid')) + { + trigger_error('AdminScreenshotsActionRelocateResponse - screenshotId or typeId empty', E_USER_ERROR); + return; + } + + [$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)) + { + $tbl = Type::getClassAttrib($type, 'dataTable'); + + // move screenshot + DB::Aowow()->qry('UPDATE ::screenshots SET `typeId` = %i WHERE `id` = %i', $typeId, $this->_get['id']); + + // flag target as having screenshot + 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`) & %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()->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 new file mode 100644 index 00000000..d95ac874 --- /dev/null +++ b/endpoints/admin/screenshots_sticky.php @@ -0,0 +1,72 @@ + ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkIdListUnsigned']] + ); + + protected function generate() : void + { + if (!$this->assertGET('id')) + { + trigger_error('AdminScreenshotsActionStickyResponse - screenshotId empty', E_USER_ERROR); + return; + } + + // 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()->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 + if (!($ssData['status'] & CC_FLAG_APPROVED)) + { + ScreenshotMgr::init(); + + if (!ScreenshotMgr::loadFile(ScreenshotMgr::PATH_PENDING, $id)) + continue; + + if (!ScreenshotMgr::createResized($id)) + continue; + + if (!ScreenshotMgr::createThumbnail($id)) + continue; + + // move pending > normal + if (!rename(sprintf(ScreenshotMgr::PATH_PENDING, $id), sprintf(ScreenshotMgr::PATH_NORMAL, $id))) + continue; + + // set as approved in DB + 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()->qry('UPDATE %n SET `cuFlags` = `cuFlags` | %i WHERE `id` = %i', $tbl, CUSTOM_HAS_SCREENSHOT, $ssData['typeId']); + } + + // reset all others + 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()->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]); + } + + if ($ssEntries) + trigger_error('AdminScreenshotsActionStickyResponse - screenshot(s) # '.implode(', ', array_keys($ssEntries)).' not in db or flagged as deleted', E_USER_WARNING); + } +} diff --git a/endpoints/admin/siteconfig.php b/endpoints/admin/siteconfig.php new file mode 100644 index 00000000..16db9400 --- /dev/null +++ b/endpoints/admin/siteconfig.php @@ -0,0 +1,113 @@ + Development > Site Configuration + + protected function generate() : void + { + $this->h1 = 'Site Configuration'; + array_unshift($this->title, $this->h1); + + $this->lvTabs = new Tabs(['parent' => "\$\$WH.ge('tabs-generic')"]); + + parent::generate(); + + $this->addScript([SC_CSS_STRING, << $catName) + { + $rows = ''; + foreach (Cfg::forCategory($idx) as $key => [$value, $flags, , $default, $comment]) + $rows .= $this->buildRow($key, $value, $flags, $default, $comment); + + if ($idx == Cfg::CAT_MISCELLANEOUS) + $rows .= 'new configuration'; + + if (!$rows) + continue; + + $this->lvTabs->addDataTab(Profiler::urlize($catName), $catName, '' . $head . $rows . '
'); + } + } + + private function buildRow(string $key, string $value, int $flags, ?string $default, string $comment) : string + { + $buff = ''; + $info = explode(' - ', $comment); + $key = $flags & Cfg::FLAG_PHP ? strtolower($key) : strtoupper($key); + + // name + if (!empty($info[0])) + $buff .= ''.sprintf(Util::$dfnString, $info[0], $key).''; + else + $buff .= ''.$key.''; + + // value + if ($flags & Cfg::FLAG_TYPE_BOOL) + $buff .= '
'; + else if ($flags & Cfg::FLAG_OPT_LIST && !empty($info[1])) + { + $buff .= ''; + } + else if ($flags & Cfg::FLAG_BITMASK && !empty($info[1])) + { + $buff .= '
'; + foreach (explode(', ', $info[1]) as $option) + { + [$idx, $name] = explode(':', $option); + $buff .= ''; + } + $buff .= '
'; + } + else + $buff .= ''; + + // actions + $buff .= ''; + + $buff .= ''; + + if ($default) + $buff .= '|'; + else + $buff .= '|'; + + if (!($flags & Cfg::FLAG_PERSISTENT)) + $buff .= '|'; + + $buff .= ''; + + return $buff; + } +} + +?> diff --git a/endpoints/admin/siteconfig_add.php b/endpoints/admin/siteconfig_add.php new file mode 100644 index 00000000..a99e77a2 --- /dev/null +++ b/endpoints/admin/siteconfig_add.php @@ -0,0 +1,34 @@ + ['filter' => FILTER_VALIDATE_REGEXP, 'options' => ['regexp' => Cfg::PATTERN_CONF_KEY_FULL]], + 'val' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkTextBlob'] ] + ); + + protected function generate() : void + { + if (!$this->assertGET('key', 'val')) + { + trigger_error('AdminSiteconfigActionAddResponse - malformed request received', E_USER_ERROR); + $this->result = Lang::main('intError'); + return; + } + + $key = trim($this->_get['key']); + $val = trim(urldecode($this->_get['val'])); + + $this->result = Cfg::add($key, $val); + } +} + +?> diff --git a/endpoints/admin/siteconfig_remove.php b/endpoints/admin/siteconfig_remove.php new file mode 100644 index 00000000..cef906d0 --- /dev/null +++ b/endpoints/admin/siteconfig_remove.php @@ -0,0 +1,30 @@ + ['filter' => FILTER_VALIDATE_REGEXP, 'options' => ['regexp' => Cfg::PATTERN_CONF_KEY_FULL]] + ); + + protected function generate() : void + { + if (!$this->assertGET('key')) + { + trigger_error('AdminSiteconfigActionRemoveResponse - malformed request received', E_USER_ERROR); + $this->result = Lang::main('intError'); + return; + } + + $this->result = Cfg::delete($this->_get['key']); + } +} + +?> diff --git a/endpoints/admin/siteconfig_update.php b/endpoints/admin/siteconfig_update.php new file mode 100644 index 00000000..5afe0bec --- /dev/null +++ b/endpoints/admin/siteconfig_update.php @@ -0,0 +1,34 @@ + ['filter' => FILTER_VALIDATE_REGEXP, 'options' => ['regexp' => Cfg::PATTERN_CONF_KEY_FULL]], + 'val' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkTextBlob'] ] + ); + + protected function generate() : void + { + if (!$this->assertGET('key', 'val')) + { + trigger_error('AdminSiteconfigActionUpdateResponse - malformed request received', E_USER_ERROR); + $this->result = Lang::main('intError'); + return; + } + + $key = trim($this->_get['key']); + $val = trim(urldecode($this->_get['val'])); + + $this->result = Cfg::set($key, $val); + } +} + +?> diff --git a/endpoints/admin/spawn-override.php b/endpoints/admin/spawn-override.php new file mode 100644 index 00000000..8b6931ab --- /dev/null +++ b/endpoints/admin/spawn-override.php @@ -0,0 +1,105 @@ + ['filter' => FILTER_VALIDATE_INT], + 'guid' => ['filter' => FILTER_VALIDATE_INT], + 'area' => ['filter' => FILTER_VALIDATE_INT], + 'floor' => ['filter' => FILTER_VALIDATE_INT] + ); + + protected function generate() : void + { + if (!$this->assertGET('type', 'guid', 'area', 'floor')) + { + trigger_error('AdminSpawnoverrideResponse - malformed request received', E_USER_ERROR); + $this->result = self::ERR_MISCELLANEOUS; + return; + } + + $guid = $this->_get['guid']; + $type = $this->_get['type']; + $area = $this->_get['area']; + $floor = $this->_get['floor']; + + if (!in_array($type, [Type::NPC, Type::OBJECT, Type::SOUND, Type::AREATRIGGER, Type::ZONE])) + { + trigger_error('AdminSpawnoverrideResponse - can\'t move pip of type '.Type::getFileString($type), E_USER_ERROR); + $this->result = self::ERR_WRONG_TYPE; + return; + } + + 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) + { + $this->result = self::ERR_WORLD_POS; + return; + } + + $point = WorldPosition::toZonePos($wPos[$guid]['mapId'], $wPos[$guid]['posX'], $wPos[$guid]['posY'], $area, $floor); + if (!$point) + { + $this->result = self::ERR_NO_POINTS; + return; + } + + $updGUIDs = [$guid]; + $newPos = array( + 'posX' => $point[0]['posX'], + 'posY' => $point[0]['posY'], + 'areaId' => $point[0]['areaId'], + 'floor' => $point[0]['floor'] + ); + + // if creature try for waypoints + if ($type == Type::NPC) + { + 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)) + { + foreach ($swp as $w) + { + 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'] + ); + + 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` = %i', $guid)); + } + + 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 new file mode 100644 index 00000000..04956306 --- /dev/null +++ b/endpoints/admin/videos.php @@ -0,0 +1,68 @@ + Content > Videos + + protected array $scripts = array( + [SC_JS_FILE, 'js/video.js'], + [SC_CSS_STRING, '.layout {margin: 0px 25px; max-width: inherit; min-width: 1200px; }'], + [SC_CSS_STRING, '#highlightedRow { background-color: #322C1C; }'] + ); + protected array $expectedGET = array( + 'action' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkTextLine']], + 'all' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkEmptySet']], + 'type' => ['filter' => FILTER_VALIDATE_INT ], + 'typeid' => ['filter' => FILTER_VALIDATE_INT ], + 'user' => ['filter' => FILTER_CALLBACK, 'options' => 'urldecode' ] + ); + + public ?bool $getAll = null; + public array $viPages = []; + public array $viData = []; + public int $viNFound = 0; + public array $pageTypes = []; + + protected function generate() : void + { + $this->h1 = 'Video Manager'; + + // types that can have videos + foreach (Type::getClassesFor(0, 'contribute', CONTRIBUTE_SS) as $type => $obj) + $this->pageTypes[$type] = Util::ucWords(Lang::game(Type::getFileString($type))); + + $viGetAll = $this->_get['all']; + $viPages = []; + $viData = []; + $nMatches = 0; + + if ($this->_get['type'] && $this->_get['typeid']) + $viData = VideoMgr::getVideos($this->_get['type'], $this->_get['typeid'], nFound: $nMatches); + 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(%s)', $this->_get['user'])) + $viData = VideoMgr::getVideos(userId: $uId, nFound: $nMatches); + } + else + $viPages = VideoMgr::getPages($viGetAll, $nMatches); + + $this->getAll = $viGetAll; + $this->viPages = $viPages; + $this->viData = $viData; + $this->viNFound = $nMatches; // ssm_numPagesFound + + parent::generate(); + } +} diff --git a/endpoints/admin/videos_approve.php b/endpoints/admin/videos_approve.php new file mode 100644 index 00000000..efa12210 --- /dev/null +++ b/endpoints/admin/videos_approve.php @@ -0,0 +1,44 @@ + ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkIdListUnsigned']] + ); + + protected function generate() : void + { + if (!$this->assertGET('id')) + { + trigger_error('AdminVideosActionApproveResponse - videoId empty', E_USER_ERROR); + return; + } + + $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()->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()->qry('UPDATE %n SET `cuFlags` = `cuFlags` | %i WHERE `id` = %i', $tbl, CUSTOM_HAS_VIDEO, $viData['typeId']); + + unset($viEntries[$id]); + } + + if (!$viEntries) + trigger_error('AdminVideosActionApproveResponse - video(s) # '.implode(', ', array_keys($viEntries)).' not in db or already approved', E_USER_WARNING); + } +} diff --git a/endpoints/admin/videos_delete.php b/endpoints/admin/videos_delete.php new file mode 100644 index 00000000..7a404124 --- /dev/null +++ b/endpoints/admin/videos_delete.php @@ -0,0 +1,43 @@ + ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkEmptySet']] + ); + + // 2 steps: 1) remove from sight, 2) remove from disk + protected function generate() : void + { + if (!$this->assertGET('id')) + { + trigger_error('AdminVideosActionDeleteResponse - videoId empty', E_USER_ERROR); + return; + } + + // 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` & %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 %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`) & %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()->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 new file mode 100644 index 00000000..d6523f6d --- /dev/null +++ b/endpoints/admin/videos_edittitle.php @@ -0,0 +1,31 @@ + ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkIdListUnsigned']] + ); + protected array $expectedPOST = array( + 'title' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkTextLine']] + ); + + protected function generate() : void + { + if (!$this->assertGET('id')) + return; + + $caption = $this->handleCaption($this->_post['title']); + + DB::Aowow()->qry('UPDATE ::videos SET `caption` = %s WHERE `id` = %i', $caption, $this->_get['id'][0]); + } +} diff --git a/endpoints/admin/videos_list.php b/endpoints/admin/videos_list.php new file mode 100644 index 00000000..7b02d12b --- /dev/null +++ b/endpoints/admin/videos_list.php @@ -0,0 +1,23 @@ + ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkEmptySet']] + ); + + protected function generate() : void + { + $pages = VideoMgr::getPages($this->_get['all'], $nPages); + $this->result = 'vim_videoPages = '.Util::toJSON($pages).";\n"; + $this->result .= 'vim_numPagesFound = '.$nPages.';'; + } +} diff --git a/endpoints/admin/videos_manage.php b/endpoints/admin/videos_manage.php new file mode 100644 index 00000000..fb5180f7 --- /dev/null +++ b/endpoints/admin/videos_manage.php @@ -0,0 +1,31 @@ + ['filter' => FILTER_VALIDATE_INT ], + 'typeid' => ['filter' => FILTER_VALIDATE_INT ], + 'user' => ['filter' => FILTER_CALLBACK, 'options' => 'urldecode'] + ); + + protected function generate() : void + { + $res = []; + + 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(%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 new file mode 100644 index 00000000..05fd749e --- /dev/null +++ b/endpoints/admin/videos_order.php @@ -0,0 +1,57 @@ + ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkIdListUnsigned'] ], + 'move' => ['filter' => FILTER_VALIDATE_INT, 'options' => ['min_range' => -1, 'max_range' => 1]] // -1 = up, 1 = down + ); + + protected function generate() : void + { + if (!$this->assertGET('id', 'move') || $this->_get['move'] === 0) + { + trigger_error('AdminVideosActionOrderResponse - id or move empty', E_USER_ERROR); + return; + } + + $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` & %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); + return; + } + + $dir = $this->_get['move']; + $curPos = $videos[$id]; + + if ($dir == -1 && $curPos == 0) + { + trigger_error('AdminVideosActionOrderResponse - video #'.$id.' already in top position', E_USER_WARNING); + return; + } + + if ($dir == 1 && $curPos + 1 == count($videos)) + { + trigger_error('AdminVideosActionOrderResponse - video #'.$id.' already in bottom position', E_USER_WARNING); + return; + } + + $oldKey = array_search($curPos + $dir, $videos); + $videos[$oldKey] -= $dir; + $videos[$id] += $dir; + + foreach ($videos as $id => $pos) + 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 new file mode 100644 index 00000000..12230906 --- /dev/null +++ b/endpoints/admin/videos_relocate.php @@ -0,0 +1,49 @@ + ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkIdListUnsigned']], + 'typeid' => ['filter' => FILTER_VALIDATE_INT ] + // (but not type..?) + ); + + protected function generate() : void + { + if (!$this->assertGET('id', 'typeid')) + { + trigger_error('AdminVideosActionRelocateResponse - videoId or typeId empty', E_USER_ERROR); + return; + } + + $id = $this->_get['id'][0]; + [$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)) + { + $tbl = Type::getClassAttrib($type, 'dataTable'); + + // move video + DB::Aowow()->qry('UPDATE ::videos SET `typeId` = %i WHERE `id` = %i', $typeId, $id); + + // flag target as having video + 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`) & %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()->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 new file mode 100644 index 00000000..40c537af --- /dev/null +++ b/endpoints/admin/videos_sticky.php @@ -0,0 +1,56 @@ + ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkIdListUnsigned']] + ); + + protected function generate() : void + { + if (!$this->assertGET('id')) + { + trigger_error('AdminVideosActionStickyResponse - videoId empty', E_USER_ERROR); + return; + } + + // 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()->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()->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()->qry('UPDATE %n SET `cuFlags` = `cuFlags` | %i WHERE `id` = %i', $tbl, CUSTOM_HAS_VIDEO, $viData['typeId']); + } + + // reset all others + 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()->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]); + } + + if ($viEntries) + trigger_error('AdminVideosActionStickyResponse - video(s) # '.implode(', ', array_keys($viEntries)).' not in db or flagged as deleted', E_USER_WARNING); + } +} diff --git a/endpoints/admin/weight-presets.php b/endpoints/admin/weight-presets.php new file mode 100644 index 00000000..3fa4a0ab --- /dev/null +++ b/endpoints/admin/weight-presets.php @@ -0,0 +1,54 @@ + Development > Weight Presets + + protected array $scripts = array( + [SC_JS_FILE, 'js/filters.js'], + [SC_CSS_STRING, '.wt-edit {display:inline-block; vertical-align:top; width:350px;}'] + ); + + protected function generate() : void + { + $this->h1 = 'Weight Presets'; + array_unshift($this->title, $this->h1); + + $head = $body = ''; + + $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 = ''; + foreach ($data as $id => $s) + { + $weights[$id]['__icon'] = $s['icon']; + $ul .= '[url=# onclick="loadScale.bind(this, '.$id.')();"]'.$s['name'].'[/url][br]'; + } + + $head .= '[td=header][class='.$cl.'][/td]'; + $body .= '[td valign=top]'.$ul.'[/td]'; + $this->extendGlobalIds(Type::CHR_CLASS, $cl); + } + + $this->extraText = new Markup('[table class=grid][tr]'.$head.'[/tr][tr]'.$body.'[/tr][/table]', ['allow' => Markup::CLASS_ADMIN], 'text-generic'); + + $this->extraHTML = '\n\n"; + + parent::generate(); + } +} + +?> diff --git a/endpoints/admin/weight-presets_save.php b/endpoints/admin/weight-presets_save.php new file mode 100644 index 00000000..c73c17ff --- /dev/null +++ b/endpoints/admin/weight-presets_save.php @@ -0,0 +1,74 @@ + ['filter' => FILTER_VALIDATE_INT ], + '__icon' => ['filter' => FILTER_VALIDATE_REGEXP, 'options' => ['regexp' => Cfg::PATTERN_CONF_KEY_FULL]], + 'scale' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkScale'] ] + ); + + protected function generate() : void + { + if (!$this->assertPOST('id', '__icon', 'scale')) + { + trigger_error('AdminWeightpresetsActionSaveResponse - malformed request received', E_USER_ERROR); + $this->result = self::ERR_MISCELLANEOUS; + return; + } + + // save to db + 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 (!Stat::getWeightJson($k) || $v < 1) + continue; + + 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; + return; + } + } + + // write dataset + exec('php aowow --build=weightPresets', $out); + foreach ($out as $o) + if (strstr($o, 'ERR')) + { + trigger_error('AdminWeightpresetsActionSaveResponse - failed to write dataset' . $o, E_USER_ERROR); + $this->result = self::ERR_WRITE_FILE; + return; + } + + // all done + $this->result = self::ERR_NONE; + } + + protected static function checkScale(string $val) : string + { + if (preg_match('/^((\w+:\d+)(,\w+:\d+)*)$/', $val)) + return $val; + + return ''; + } +} + +?> diff --git a/endpoints/areatrigger/areatrigger.php b/endpoints/areatrigger/areatrigger.php new file mode 100644 index 00000000..c8a02e54 --- /dev/null +++ b/endpoints/areatrigger/areatrigger.php @@ -0,0 +1,141 @@ +typeId = intVal($id); + $this->contribute = Type::getClassAttrib($this->type, 'contribute') ?? CONTRIBUTE_NONE; + } + + protected function generate() : void + { + $this->subject = new AreaTriggerList(array(['id', $this->typeId])); + if ($this->subject->error) + $this->generateNotFound(Lang::game('areatrigger'), Lang::areatrigger('notFound')); + + $this->h1 = $this->subject->getField('name') ?: 'Areatrigger #'.$this->typeId; + + $this->gPageInfo += array( + 'type' => $this->type, + 'typeId' => $this->typeId, + 'name' => $this->h1 + ); + + + /*************/ + /* Menu Path */ + /*************/ + + $this->breadcrumb[] = $this->subject->getField('type'); + + + /**************/ + /* Page Title */ + /**************/ + + array_unshift($this->title, $this->h1, Util::ucFirst(Lang::game('areatrigger'))); + + + /****************/ + /* Main Content */ + /****************/ + + $_type = $this->subject->getField('type'); + + // get spawns + if ($spawns = $this->subject->getSpawns(SPAWNINFO_FULL)) + { + $this->addDataLoader('zones'); + $this->map = array( + ['parent' => 'mapper-generic'], // Mapper + $spawns, // mapperData + null, // ShowOnMap + [Lang::areatrigger('foundIn')] // foundIn + ); + foreach ($spawns as $areaId => $_) + $this->map[3][$areaId] = ZoneList::getName($areaId); + } + + // Smart AI + if ($_type == AT_TYPE_SMART) + { + $sai = new SmartAI(SmartAI::SRC_TYPE_AREATRIGGER, $this->typeId, ['teleportTargetArea' => $this->subject->getField('areaId')]); + if ($sai->prepare()) + { + $this->extendGlobalData($sai->getJSGlobals()); + $this->smartAI = $sai->getMarkup(); + } + } + + $this->redButtons = array( + BUTTON_LINKS => false, + BUTTON_WOWHEAD => false + ); + + + /**************/ + /* Extra Tabs */ + /**************/ + + $this->lvTabs = new Tabs(['parent' => "\$\$WH.ge('tabs-generic')"], 'tabsRelated', true); + + // tab: conditions + $cnd = new Conditions(); + $cnd->getBySource(Conditions::SRC_AREATRIGGER_CLIENT, entry: $this->typeId)->prepare(); + if ($tab = $cnd->toListviewTab()) + { + $this->extendGlobalData($cnd->getJsGlobals()); + $this->lvTabs->addDataTab(...$tab); + } + + if ($_type == AT_TYPE_OBJECTIVE) + { + $relQuest = new QuestList(array(['id', $this->subject->getField('quest')])); + if (!$relQuest->error) + { + $this->extendGlobalData($relQuest->getJSGlobals(GLOBALINFO_SELF | GLOBALINFO_REWARDS)); + $this->lvTabs->addListviewTab(new Listview(['data' => $relQuest->getListviewData()], QuestList::$brickFile)); + } + } + else if ($_type == AT_TYPE_TELEPORT) + { + $relZone = new ZoneList(array(['id', $this->subject->getField('areaId')])); + if (!$relZone->error) + $this->lvTabs->addListviewTab(new Listview(['data' => $relZone->getListviewData()], ZoneList::$brickFile)); + } + else if ($_type == AT_TYPE_SCRIPT) + { + $relTrigger = new AreaTriggerList(array(['id', $this->typeId, '!'], ['name', $this->subject->getField('name')])); + if (!$relTrigger->error) + $this->lvTabs->addListviewTab(new Listview(['data' => $relTrigger->getListviewData(), 'name' => Util::ucFirst(Lang::game('areatrigger'))]), AreaTriggerList::$brickFile, 'areatrigger'); + } + + parent::generate(); + } +} + +?> diff --git a/endpoints/areatriggers/areatriggers.php b/endpoints/areatriggers/areatriggers.php new file mode 100644 index 00000000..0d681c95 --- /dev/null +++ b/endpoints/areatriggers/areatriggers.php @@ -0,0 +1,102 @@ + ['filter' => FILTER_VALIDATE_REGEXP, 'options' => ['regexp' => Filter::PATTERN_PARAM]]]; + protected array $validCats = [0, 1, 2, 3, 4, 5]; + + public function __construct(string $rawParam) + { + $this->getCategoryFromUrl($rawParam); + + if (isset($this->category[0])) + $this->forward('?areatriggers&filter=ty='.$this->category[0]); + + 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; + } + + protected function generate() : void + { + $this->h1 = Util::ucFirst(Lang::game('areatriggers')); + + $fiForm = $this->filter->values; + + + /**************/ + /* Page Title */ + /**************/ + + array_unshift($this->title, $this->h1); + + if (count($fiForm['ty']) == 1) + array_unshift($this->title, Lang::areatrigger('types', $fiForm['ty'][0])); + + + /*************/ + /* Menu Path */ + /*************/ + + if (count($fiForm['ty']) == 1) + $this->breadcrumb[] = $fiForm['ty']; + + + /****************/ + /* Main Content */ + /****************/ + + $this->redButtons[BUTTON_WOWHEAD] = false; + + $conditions = [Listview::DEFAULT_SIZE]; + if ($_ = $this->filter->getConditions()) + $conditions[] = $_; + + $tabData = []; + $trigger = new AreaTriggerList($conditions, ['calcTotal' => true]); + if (!$trigger->error) + { + $tabData['data'] = $trigger->getListviewData(); + + // create note if search limit was exceeded; overwriting 'note' is intentional + if ($trigger->getMatches() > Listview::DEFAULT_SIZE) + { + $tabData['note'] = sprintf(Util::$tryFilteringEntityString, $trigger->getMatches(), '"'.Lang::game('areatriggers').'"', Listview::DEFAULT_SIZE); + $tabData['_truncated'] = 1; + } + } + + $this->lvTabs = new Tabs(['parent' => "\$\$WH.ge('tabs-generic')"]); + + $this->lvTabs->addListviewTab(new Listview($tabData, AreaTriggerList::$brickFile, 'areatrigger')); + + parent::generate(); + } +} + +?> diff --git a/endpoints/arena-team/arena-team.php b/endpoints/arena-team/arena-team.php new file mode 100644 index 00000000..fc1e1a52 --- /dev/null +++ b/endpoints/arena-team/arena-team.php @@ -0,0 +1,148 @@ + Profiler > Arena Team + + protected array $dataLoader = ['realms', 'weight-presets']; + protected array $scripts = array( + [SC_JS_FILE, 'js/profile_all.js'], + [SC_JS_FILE, 'js/profile.js'], + [SC_CSS_FILE, 'css/Profiler.css'] + ); + + public int $type = Type::ARENA_TEAM; + + public function __construct(string $idOrProfile) + { + parent::__construct($idOrProfile); + + if (!Cfg::get('PROFILER_ENABLE')) + $this->generateError(); + + if (!$idOrProfile) + $this->generateError(); + + $this->getSubjectFromUrl($idOrProfile); + + // we have an ID > ok + if ($this->typeId) + return; + + // param was incomplete profile > error + if (!$this->subjectName) + $this->generateError(); + + // 3 possibilities + // 1) already synced to aowow + 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['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) + $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['stub'] = 1; + $subject['nameUrl'] = Profiler::urlize($subject['name']); + + // create entry from realm with basic info + DB::Aowow()->qry('INSERT IGNORE INTO ::profiler_arena_team %v', $subject); + + $this->handleIncompleteData(Type::ARENA_TEAM, $subject['realmGUID']); + return; + } + + // 3) does not exist at all + $this->notFound(); + } + + protected function generate() : void + { + if ($this->doResync) + { + parent::generate(); + return; + } + + $subject = new LocalArenaTeamList(array(['at.id', $this->typeId])); + if ($subject->error) + $this->notFound(); + + // arena team accessed by id + if (!$this->subjectName) + $this->forward($subject->getProfileUrl()); + + $this->h1 = Lang::profiler('arenaRoster', [$subject->getField('name')]); + + + /*************/ + /* Menu Path */ + /*************/ + + $this->followBreadcrumbPath(); + + + /**************/ + /* Page Title */ + /**************/ + + array_unshift( + $this->title, + $subject->getField('name').' ('.$this->realm.' - '.Lang::profiler('regions', $this->region).')', + Util::ucFirst(Lang::profiler('profiler')) + ); + + + /****************/ + /* Main Content */ + /****************/ + + parent::generate(); + + $this->redButtons[BUTTON_RESYNC] = [$this->typeId, 'arena-team']; + + // statistic calculations here + + + /**************/ + /* Extra Tabs */ + /**************/ + + $this->lvTabs = new Tabs(['parent' => "\$\$WH.ge('tabs-generic')"], 'tabsRelated'); + + // tab: members + $member = new LocalProfileList(array(['atm.arenaTeamId', $this->typeId])); + $this->lvTabs->addListviewTab(new Listview(array( + 'data' => $member->getListviewData(PROFILEINFO_CHARACTER | PROFILEINFO_ARENA), + 'sort' => [-15], + 'visibleCols' => ['race', 'classs', 'level', 'talents', 'gearscore', 'rating', 'wins', 'losses'], + 'hiddenCols' => ['guild', 'location'] + ), ProfileList::$brickFile)); + } + + private function notFound() : never + { + parent::generateNotFound(Lang::game('arenateam'), Lang::profiler('notFound', 'arenateam')); + } +} + +?> diff --git a/endpoints/arena-team/resync.php b/endpoints/arena-team/resync.php new file mode 100644 index 00000000..ac57929d --- /dev/null +++ b/endpoints/arena-team/resync.php @@ -0,0 +1,48 @@ + ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkIdList'] ], + 'profile' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkEmptySet']] + ); + + public function __construct(string $rawParam) + { + parent::__construct($rawParam); + + if (!Cfg::get('PROFILER_ENABLE')) + $this->generate404(); + } + + /* params + id: + user: [optional, not used] + profile: [optional, also get related chars] + return: 1 + */ + protected function generate() : void + { + if (!$this->assertGET('id')) + return; + + 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()->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']); + + $this->result = 1; // as string? + } +} + +?> diff --git a/endpoints/arena-team/status.php b/endpoints/arena-team/status.php new file mode 100644 index 00000000..ad89f016 --- /dev/null +++ b/endpoints/arena-team/status.php @@ -0,0 +1,29 @@ + ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkIdList']] + ); + + public function __construct(string $rawParam) + { + parent::__construct($rawParam); + + if (!Cfg::get('PROFILER_ENABLE')) + $this->generate404(); + } + + protected function generate() : void + { + $this->result = Profiler::resyncStatus(Type::ARENA_TEAM, $this->_get['id']); + } +} + +?> diff --git a/endpoints/arena-teams/arena-teams.php b/endpoints/arena-teams/arena-teams.php new file mode 100644 index 00000000..9983422d --- /dev/null +++ b/endpoints/arena-teams/arena-teams.php @@ -0,0 +1,160 @@ + Profiler > Arena Teams + + protected array $dataLoader = ['realms']; + protected array $scripts = array( + [SC_JS_FILE, 'js/filters.js'], + [SC_JS_FILE, 'js/profile_all.js'], + [SC_JS_FILE, 'js/profile.js'] + ); + protected array $expectedGET = array( + 'filter' => ['filter' => FILTER_VALIDATE_REGEXP, 'options' => ['regexp' => Filter::PATTERN_PARAM]] + ); + + public int $type = Type::ARENA_TEAM; + + private int $sumSubjects = 0; + + public function __construct(string $rawParam) + { + if (!Cfg::get('PROFILER_ENABLE')) + $this->generateError(); + + $this->getSubjectFromUrl($rawParam); + + parent::__construct($rawParam); + + $realms = []; + foreach (Profiler::getRealms() as $idx => $r) + { + if ($this->region && $r['region'] != $this->region) + continue; + + if ($this->realm && $r['name'] != $this->realm) + continue; + + $this->sumSubjects += DB::Characters($idx)->selectCell('SELECT count(*) FROM arena_team'); + $realms[] = $idx; + } + + 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; + } + + protected function generate() : void + { + $this->h1 = Lang::game('arenateams'); + + + /*************/ + /* Menu Path */ + /*************/ + + $this->followBreadcrumbPath(); + + + /**************/ + /* Page Title */ + /**************/ + + if ($this->realm) + array_unshift($this->title, $this->realm,/* Cfg::get('BATTLEGROUP'),*/ Lang::profiler('regions', $this->region), Lang::game('arenateams')); + else if ($this->region) + array_unshift($this->title, Lang::profiler('regions', $this->region), Lang::game('arenateams')); + else + array_unshift($this->title, Lang::game('arenateams')); + + + /****************/ + /* Main Content */ + /****************/ + + $conditions = [Listview::DEFAULT_SIZE]; + if (!User::isInGroup(U_GROUP_EMPLOYEE)) + $conditions[] = ['at.seasonGames', 0, '>']; + + if ($_ = $this->filter->getConditions()) + $conditions[] = $_; + + $this->getRegions(); + + $tabData = array( + 'id' => 'arena-teams', + 'data' => [], + 'hideCount' => 1, + 'sort' => [-16], + 'extraCols' => ['$Listview.extraCols.members'], + 'visibleCols' => ['rank', 'wins', 'losses', 'rating'], + 'hiddenCols' => ['arenateam', 'guild'] + ); + + if (!$this->filter->values['sz']) + $tabData['visibleCols'][] = 'size'; + + if ($this->filter->values['si']) + $tabData['hiddenCols'][] = 'faction'; + + $miscParams = ['calcTotal' => true]; + if ($this->realm) + $miscParams['sv'] = $this->realm; + if ($this->region) + $miscParams['rg'] = $this->region; + + $teams = new RemoteArenaTeamList($conditions, $miscParams); + if (!$teams->error) + { + $teams->initializeLocalEntries(); + + $tabData['data'] = $teams->getListviewData(); + + // create note if search limit was exceeded + 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() > Listview::DEFAULT_SIZE) + $tabData['note'] = sprintf(Util::$tryFilteringString, 'LANG.lvnote_arenateamsfound', $this->sumSubjects, 0); + } + + $this->lvTabs = new Tabs(['parent' => "\$\$WH.ge('tabs-generic')"], 'tabsRelated'); + + $this->lvTabs->addListviewTab(new Listview($tabData, ArenaTeamList::$brickFile, 'membersCol')); + + parent::generate(); + + $this->result->registerDisplayHook('filter', [self::class, 'filterFormHook']); + } + + public static function filterFormHook(Template\PageTemplate &$pt, ArenaTeamListFilter $filter) : void + { + // sort for dropdown-menus + Lang::sort('game', 'cl'); + Lang::sort('game', 'ra'); + } +} + +?> diff --git a/endpoints/class/class.php b/endpoints/class/class.php new file mode 100644 index 00000000..47be4379 --- /dev/null +++ b/endpoints/class/class.php @@ -0,0 +1,304 @@ +typeId = intVal($id); + $this->contribute = Type::getClassAttrib($this->type, 'contribute') ?? CONTRIBUTE_NONE; + } + + protected function generate() : void + { + $this->subject = new CharClassList(array(['id', $this->typeId])); + if ($this->subject->error) + $this->generateNotFound(Lang::game('class'), Lang::chrClass('notFound')); + + $this->h1 = $this->subject->getField('name', true); + + $this->gPageInfo += array( + 'type' => $this->type, + 'typeId' => $this->typeId, + 'name' => $this->h1 + ); + + + /*************/ + /* Menu Path */ + /*************/ + + $this->breadcrumb[] = $this->typeId; + + + /**************/ + /* Page Title */ + /**************/ + + array_unshift($this->title, $this->h1, Util::ucFirst(Lang::game('class'))); + + + /***********/ + /* Infobox */ + /***********/ + + $cl = ChrClass::from($this->typeId); + + $infobox = Lang::getInfoBoxForFlags($this->subject->getField('cuFlags')); + + // hero class + if ($this->subject->getField('flags') & 0x40) + $infobox[] = '[tooltip=tooltip_heroclass]'.Lang::game('heroClass').'[/tooltip]'; + + // resource + if ($cl == ChrClass::DRUID) // special Druid case + $infobox[] = Lang::game('resources'). + '[tooltip name=powertype1]'.Lang::game('st', 0).', '.Lang::game('st', 31).', '.Lang::game('st', 2).'[/tooltip][span class=tip tooltip=powertype1]'.Util::ucFirst(Lang::spell('powerTypes', POWER_MANA)).'[/span], '. + '[tooltip name=powertype2]'.Lang::game('st', 5).', '.Lang::game('st', 8).'[/tooltip][span class=tip tooltip=powertype2]'.Util::ucFirst(Lang::spell('powerTypes', POWER_RAGE)).'[/span], '. + '[tooltip name=powertype8]'.Lang::game('st', 1).'[/tooltip][span class=tip tooltip=powertype8]'.Util::ucFirst(Lang::spell('powerTypes', POWER_ENERGY)).'[/span]'; + else if ($cl == ChrClass::DEATHKNIGHT) // special DK case + $infobox[] = Lang::game('resources').'[span]'.Util::ucFirst(Lang::spell('powerTypes', POWER_RUNE)).', '.Util::ucFirst(Lang::spell('powerTypes', $this->subject->getField('powerType'))).'[/span]'; + else // regular case + $infobox[] = Lang::game('resource').'[span]'.Util::ucFirst(Lang::spell('powerTypes', $this->subject->getField('powerType'))).'[/span]'; + + // roles + $roles = []; + for ($i = 0; $i < 4; $i++) + if ($this->subject->getField('roles') & (1 << $i)) + $roles[] = (count($roles) == 2 ? "[br]" : '').Lang::game('_roles', $i); + + if ($roles) + $infobox[] = (count($roles) > 1 ? Lang::game('roles') : Lang::game('role')).implode(', ', $roles); + + // specs + $specList = []; + $skills = new SkillList(array(['id', $this->subject->getField('skills')])); + foreach ($skills->iterate() as $k => $__) + $specList[$k] = '[icon name='.$skills->getField('iconString').'][url=?spells=7.'.$this->typeId.'.'.$k.']'.$skills->getField('name', true).'[/url][/icon]'; + + 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'); + + + /****************/ + /* Main Content */ + /****************/ + + $this->expansion = Util::$expansionString[$this->subject->getField('expansion')]; + $this->redButtons = array( + BUTTON_LINKS => ['type' => $this->type, 'typeId' => $this->typeId], + BUTTON_WOWHEAD => true, + BUTTON_TALENT => ['href' => '?talent#'.Util::$tcEncoding[self::TC_CLASS_IDS[$this->typeId] * 3], 'pet' => false], + BUTTON_FORUM => false // todo (low): Cfg::get('BOARD_URL') + X + ); + + if ($_ = $this->subject->getField('iconString')) + $this->headIcons[] = $_; + + + /**************/ + /* Extra Tabs */ + /**************/ + + $this->lvTabs = new Tabs(['parent' => "\$\$WH.ge('tabs-generic')"], 'tabsRelated', true); + + // tab: spells (grouped) + // '$LANG.tab_armorproficiencies', + // '$LANG.tab_weaponskills', + // '$LANG.tab_glyphs', + // '$LANG.tab_abilities', + // '$LANG.tab_talents', + $conditions = array( + ['s.typeCat', [-13, -11, -2, 7]], + [['s.cuFlags', (SPELL_CU_TRIGGERED | CUSTOM_EXCLUDE_FOR_LISTVIEW), '&'], 0], + [ + DB::OR, + // Glyphs, Proficiencies + ['s.reqClassMask', $cl->toMask(), '&'], + // Abilities / Talents + ['s.skillLine1', $this->subject->getField('skills')], + [DB::AND, ['s.skillLine1', 0, '>'], ['s.skillLine2OrMask', $this->subject->getField('skills')]] + ], + [ // last rank or unranked + DB::OR, + ['s.cuFlags', SPELL_CU_LAST_RANK, '&'], + ['s.rankNo', 0] + ] + ); + + $genSpells = new SpellList($conditions); + if (!$genSpells->error) + { + $this->extendGlobalData($genSpells->getJSGlobals(GLOBALINFO_SELF | GLOBALINFO_RELATED)); + + $this->lvTabs->addListviewTab(new Listview(array( + 'data' => $genSpells->getListviewData(), + 'id' => 'spells', + 'name' => '$LANG.tab_spells', + 'visibleCols' => ['level', 'schools', 'type', 'classes'], + 'hiddenCols' => ['reagents', 'skill'], + 'sort' => ['-level', 'type', 'name'], + 'computeDataFunc' => '$Listview.funcBox.initSpellFilter', + 'onAfterCreate' => '$Listview.funcBox.addSpellIndicator' + ), SpellList::$brickFile)); + } + + // tab: items (grouped) + $conditions = array( + ['requiredClass', $cl->toMask(), '&'], + ['itemset', 0] + ); + + $items = new ItemList($conditions); + if (!$items->error) + { + $this->extendGlobalData($items->getJSGlobals()); + + $hiddenCols = null; + if ($items->hasDiffFields('requiredRace')) + $hiddenCols = ['side']; + + $this->lvTabs->addListviewTab(new Listview(array( + 'data' => $items->getListviewData(), + 'id' => 'items', + 'name' => '$LANG.tab_items', + 'visibleCols' => ['dps', 'armor', 'slot'], + 'hiddenCols' => $hiddenCols, + 'computeDataFunc' => '$Listview.funcBox.initSubclassFilter', + 'onAfterCreate' => '$Listview.funcBox.addSubclassIndicator', + 'note' => sprintf(Util::$filterResultString, '?items&filter=cr=152;crs='.$this->typeId.';crv=0'), + '_truncated' => 1 + ), ItemList::$brickFile)); + } + + // tab: quests + $conditions = array( + ['reqClassMask', $cl->toMask(), '&'], + [['reqClassMask', ChrClass::MASK_ALL, '&'], ChrClass::MASK_ALL, '!'] + ); + + $quests = new QuestList($conditions); + if (!$quests->error) + { + $this->extendGlobalData($quests->getJSGlobals()); + + $this->lvTabs->addListviewTab(new Listview(array( + 'data' => $quests->getListviewData(), + 'sort' => ['reqlevel', 'name'] + ), QuestList::$brickFile)); + } + + // tab: itemsets + $sets = new ItemsetList(array(['classMask', $cl->toMask(), '&'])); + if (!$sets->error) + { + $this->extendGlobalData($sets->getJSGlobals(GLOBALINFO_SELF)); + + $this->lvTabs->addListviewTab(new Listview(array( + 'data' => $sets->getListviewData(), + 'note' => sprintf(Util::$filterResultString, '?itemsets&filter=cl='.$this->typeId), + 'hiddenCols' => ['classes'], + 'sort' => ['-level', 'name'] + ), ItemsetList::$brickFile)); + } + + // tab: trainers + $conditions = array( + ['npcflag', NPC_FLAG_TRAINER | NPC_FLAG_CLASS_TRAINER, '&'], + ['trainerType', 0], // trains class spells + ['trainerRequirement', $this->typeId] + ); + + $trainer = new CreatureList($conditions); + if (!$trainer->error) + { + $this->addDataLoader('zones'); + $this->lvTabs->addListviewTab(new Listview(array( + 'data' => $trainer->getListviewData(), + 'id' => 'trainers', + 'name' => '$LANG.tab_trainers' + ), CreatureList::$brickFile)); + } + + // 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(); + if ($tab = $cnd->toListviewTab('condition-for', '$LANG.tab_condition_for')) + { + $this->extendGlobalData($cnd->getJsGlobals()); + $this->lvTabs->addDataTab(...$tab); + } + + parent::generate(); + } +} + +?> diff --git a/endpoints/classes/classes.php b/endpoints/classes/classes.php new file mode 100644 index 00000000..07e1f6f9 --- /dev/null +++ b/endpoints/classes/classes.php @@ -0,0 +1,48 @@ +getCategoryFromUrl($rawParam); + + parent::__construct($rawParam); + } + + protected function generate() : void + { + $this->h1 = Util::ucFirst(Lang::game('classes')); + + + array_unshift($this->title, Util::ucFirst(Lang::game('classes'))); + + + $this->redButtons[BUTTON_WOWHEAD] = true; + + $this->lvTabs = new Tabs(['parent' => "\$\$WH.ge('tabs-generic')"]); + + $classes = new CharClassList(); + if (!$classes->error) + $this->lvTabs->addListviewTab(new Listview(['data' => $classes->getListviewData()], CharClassList::$brickFile)); + + parent::generate(); + } +} + +?> diff --git a/endpoints/comment/add-reply.php b/endpoints/comment/add-reply.php new file mode 100644 index 00000000..6b9d7931 --- /dev/null +++ b/endpoints/comment/add-reply.php @@ -0,0 +1,51 @@ + ['filter' => FILTER_VALIDATE_INT ], + 'replyId' => ['filter' => FILTER_VALIDATE_INT ], + 'body' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkTextBlob']] + ); + + protected function generate(): void + { + if (!$this->assertPOST('commentId', 'replyId', 'body')) + { + trigger_error('CommentAddreplyResponse - malformed request received', E_USER_ERROR); + $this->generate404(User::isInGroup(U_GROUP_STAFF) ? 'request malformed' : ''); + } + + if (!User::canReply()) + $this->generate404(Lang::main('cannotComment')); + + 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')); + } + + 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()->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')); + } + + $this->result = Util::toJSON(CommunityContent::getCommentReplies($this->_post['commentId'])); + } +} + +?> diff --git a/endpoints/comment/add.php b/endpoints/comment/add.php new file mode 100644 index 00000000..78dffad6 --- /dev/null +++ b/endpoints/comment/add.php @@ -0,0 +1,79 @@ + ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkTextBlob']] + ); + protected array $expectedGET = array( + 'type' => ['filter' => FILTER_VALIDATE_INT], + 'typeid' => ['filter' => FILTER_VALIDATE_INT] + ); + + // i .. have problems believing, that everything uses nifty ajax while adding comments requires a brutal header(Loacation: ), yet, thats how it is + protected function generate() : void + { + if (!$this->assertGET('type', 'typeid') || !$this->assertPOST('commentbody') || !Type::validateIds($this->_get['type'], $this->_get['typeid'])) + { + trigger_error('CommentAddResponse - malforemd request received', E_USER_ERROR); + return; // whatever, we cant even send him back + } + + // 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` = %i', $this->_get['typeid'])) + $idOrUrl = $_; + + $this->redirectTo = '?'.Type::getFileString($this->_get['type']).'='.$idOrUrl.'#comments'; + + // this type cannot be commented on + if (!Type::checkClassAttrib($this->_get['type'], 'contribute', CONTRIBUTE_CO)) + { + trigger_error('CommentAddResponse - tried to comment on unsupported type: '.Type::getFileString($this->_get['type']), E_USER_ERROR); + $_SESSION['error']['co'] = Lang::main('intError'); + return; + } + + if (!User::canComment()) + { + $_SESSION['error']['co'] = Lang::main('cannotComment'); + return; + } + + $len = mb_strlen($this->_post['commentbody']); + + if ((!User::isInGroup(U_GROUP_MODERATOR) && $len < CommunityContent::COMMENT_LENGTH_MIN) || ($len > CommunityContent::COMMENT_LENGTH_MAX * (User::isPremium() ? 3 : 1))) + { + $_SESSION['error']['co'] = Lang::main('textLength', [$len, CommunityContent::COMMENT_LENGTH_MIN, CommunityContent::COMMENT_LENGTH_MAX * (User::isPremium() ? 3 : 1)]); + return; + } + + 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()->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()->qry('UPDATE %n SET `cuFlags` = `cuFlags` | %i WHERE `id` = %i', $tbl, CUSTOM_HAS_COMMENT, $this->_get['typeid']); + + return; + } + + trigger_error('CommentAddResponse - write to db failed', E_USER_ERROR); + $_SESSION['error']['co'] = Lang::main('intError'); + } +} + +?> diff --git a/endpoints/comment/delete-reply.php b/endpoints/comment/delete-reply.php new file mode 100644 index 00000000..096a62dd --- /dev/null +++ b/endpoints/comment/delete-reply.php @@ -0,0 +1,41 @@ + ['filter' => FILTER_VALIDATE_INT] + ); + + protected function generate() : void + { + if (!$this->assertPOST('id')) + { + trigger_error('CommentDeletereplyResponse - malformed request received', E_USER_ERROR); + $this->generate404(User::isInGroup(U_GROUP_STAFF) ? 'request malformed' : ''); + } + + $where = [['`id` = %i', $this->_post['id']]]; + if (!User::isInGroup(U_GROUP_MODERATOR)) + $where[] = ['`userId` = %i', User::$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); + $this->generate404(Lang::main('intError')); + } + } +} + +?> diff --git a/endpoints/comment/delete.php b/endpoints/comment/delete.php new file mode 100644 index 00000000..582a0b01 --- /dev/null +++ b/endpoints/comment/delete.php @@ -0,0 +1,50 @@ + ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkIdListUnsigned']], + // 'username' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkTextLine'] ] + ); + + protected function generate() : void + { + if (!$this->assertPOST('id')) + { + trigger_error('CommentDeleteResponse - malformed request received', E_USER_ERROR); + return; + } + + // in theory, there is a username passed alongside if executed from userpage... lets just use the current user (see user.js) + $where = [['`id` IN %in', $this->_post['id']]]; + if (!User::isInGroup(U_GROUP_MODERATOR)) + $where[] = ['`userId` = %i', User::$id]; + + // 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()->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()->qry('UPDATE %n SET `cuFlags` = `cuFlags` & ~%i WHERE `id` = %i', $tbl, CUSTOM_HAS_COMMENT, $typeId); + + return; + } + + trigger_error('CommentDeleteResponse - user #'.User::$id.' could not flag comment(s) #'.implode(', ', $this->_post['id']).' as deleted', E_USER_ERROR); + } +} + +?> diff --git a/endpoints/comment/detach-reply.php b/endpoints/comment/detach-reply.php new file mode 100644 index 00000000..7c79f29a --- /dev/null +++ b/endpoints/comment/detach-reply.php @@ -0,0 +1,30 @@ + ['filter' => FILTER_VALIDATE_INT] + ); + + protected function generate() : void + { + if (!$this->assertPOST('id')) + { + trigger_error('CommentDetachreplyResponse - malformed request received', E_USER_ERROR); + $this->generate404(User::isInGroup(U_GROUP_STAFF) ? 'request malformed' : ''); + } + + 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 new file mode 100644 index 00000000..cd035abc --- /dev/null +++ b/endpoints/comment/downvote-reply.php @@ -0,0 +1,55 @@ + ['filter' => FILTER_VALIDATE_INT] + ); + + protected function generate() : void + { + if (!$this->assertPOST('id')) + { + trigger_error('CommentDownvotereplyResponse - malformed request received', E_USER_ERROR); + $this->generate404(User::isInGroup(U_GROUP_STAFF) ? 'request malformed' : ''); + } + + if (!User::canDownvote()) + $this->generate404(User::isInGroup(U_GROUP_STAFF) ? 'cannot downvote' : ''); + + $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); + $this->generate404(User::isInGroup(U_GROUP_STAFF) ? 'replyID not found' : ''); + } + + if (User::$id == $comment['userId']) // not worth logging? + $this->generate404('LANG.voteself_tip'); + + if ($comment['deleted']) + $this->generate404('LANG.votedeleted_tip'); + + 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' : ''); + } + + Util::gainSiteReputation($comment['userId'], SITEREP_ACTION_DOWNVOTED, ['id' => $this->_post['id'], 'voterId' => User::$id]); + User::decrementDailyVotes(); + } +} + +?> diff --git a/endpoints/comment/edit-reply.php b/endpoints/comment/edit-reply.php new file mode 100644 index 00000000..7a6aed50 --- /dev/null +++ b/endpoints/comment/edit-reply.php @@ -0,0 +1,65 @@ + ['filter' => FILTER_VALIDATE_INT ], + 'replyId' => ['filter' => FILTER_VALIDATE_INT ], + 'body' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkTextBlob']] + ); + + protected function generate() : void + { + if (!$this->assertPOST('commentId', 'replyId', 'body')) + { + trigger_error('CommentEditreplyResponse - malformed request received', E_USER_ERROR); + $this->generate404(User::isInGroup(U_GROUP_STAFF) ? 'request malformed' : ''); + } + + $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')); + + if (!$ownerId) + { + trigger_error('CommentEditreplyResponse - comment #'.$this->_post['commentId'].' or reply #'.$this->_post['replyId'].' does not exist', E_USER_ERROR); + $this->generate404(Lang::main('intError')); + } + + 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])); + + $update = array( + 'body' => $this->_post['body'], + 'editUserId' => User::$id, + 'editDate' => time() + ); + if (User::$id == $ownerId) + $update['roles'] = User::$groups; + + $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')); + } + + $this->result = Util::toJSON(CommunityContent::getCommentReplies($this->_post['commentId'])); + } +} + +?> diff --git a/endpoints/comment/edit.php b/endpoints/comment/edit.php new file mode 100644 index 00000000..a3a2e3cb --- /dev/null +++ b/endpoints/comment/edit.php @@ -0,0 +1,63 @@ + ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkTextBlob']], + 'response' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkTextBlob']] + ); + protected array $expectedGET = array( + 'id' => ['filter' => FILTER_VALIDATE_INT] + ); + + protected function generate() : void + { + if (!$this->assertGET('id') || !$this->assertPOST('body')) + { + trigger_error('CommentEditResponse - malforemd request received', E_USER_ERROR); + return; + } + + $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))) + { + trigger_error('CommentEditResponse - user #'.User::$id.' not allowed to edit comment #'.$this->_get['id'], E_USER_ERROR); + return; + } + + if (!User::isInGroup(U_GROUP_MODERATOR) && mb_strlen($this->_post['body']) < CommunityContent::COMMENT_LENGTH_MIN) + return; // no point in reporting this trifle + + // trim to max length + if (!User::isInGroup(U_GROUP_MODERATOR)) + $this->_post['body'] = mb_substr($this->_post['body'], 0, (CommunityContent::COMMENT_LENGTH_MAX * (User::isPremium() ? 3 : 1))); + + $update = array( + 'body' => $this->_post['body'], + 'editUserId' => User::$id, + 'editDate' => time() + ); + if (User::$id == $ownerId) + $update['roles'] = User::$groups; + + if (User::isInGroup(U_GROUP_MODERATOR)) + { + $update['responseBody'] = $this->_post['response'] ?? ''; + $update['responseUserId'] = User::$id; + $update['responseRoles'] = User::$groups; + } + + 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 new file mode 100644 index 00000000..55589b45 --- /dev/null +++ b/endpoints/comment/flag-reply.php @@ -0,0 +1,45 @@ + ['filter' => FILTER_VALIDATE_INT] + ); + + protected function generate() : void + { + if (!$this->assertPOST('id')) + { + trigger_error('CommentFlagreplyResponse - malformed request received', E_USER_ERROR); + $this->generate404(User::isInGroup(U_GROUP_STAFF) ? 'request malformed' : ''); + } + + $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); + $this->generate404(Lang::main('intError')); + } + + // ui element should not be present + if ($replyOwner == User::$id) + $this->generate404(); + + $report = new Report(Report::MODE_COMMENT, Report::CO_INAPPROPRIATE, $this->_post['id']); + 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()->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 new file mode 100644 index 00000000..70cb3dfd --- /dev/null +++ b/endpoints/comment/out-of-date.php @@ -0,0 +1,58 @@ + ['filter' => FILTER_VALIDATE_INT ], + 'remove' => ['filter' => FILTER_VALIDATE_INT, 'options' => ['min_range' => 1, 'max_range' => 1]], + 'reason' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkTextBlob'] ] + ); + + protected function generate() : void + { + if (!$this->assertPOST('id')) + { + trigger_error('CommentOutofdateResponse - malformed request received', E_USER_ERROR); + if (User::isInGroup(U_GROUP_STAFF)) + $this->result = 'malformed request received'; + } + + $ok = false; + if (User::isInGroup(U_GROUP_MODERATOR)) // directly mark as outdated + { + if (!$this->_post['remove']) + $ok = DB::Aowow()->qry('UPDATE ::comments SET `flags` = `flags` | %i WHERE `id` = %i', CC_FLAG_OUTDATED, $this->_post['id']); + else + $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 + { + $report = new Report(Report::MODE_COMMENT, Report::CO_OUT_OF_DATE, $this->_post['id']); + if (!$report->create($this->_post['reason'])) + $this->result = Lang::main('intError'); + + if (count($report->getSimilar()) >= CommunityContent::REPORT_THRESHOLD_AUTO_OUT_OF_DATE) + $ok = DB::Aowow()->qry('UPDATE ::comments SET `flags` = `flags` | %i WHERE `id` = %i', CC_FLAG_OUTDATED, $this->_post['id']); + } + + if (!$ok) + { + trigger_error('CommentOutofdateResponse - failed to update comment in db', E_USER_ERROR); + $this->result = Lang::main('intError'); + return; + } + + $this->result = 'ok'; // the js expects the actual characters 'ok' on success, not some json string like '"ok"' + } +} + +?> diff --git a/endpoints/comment/rating.php b/endpoints/comment/rating.php new file mode 100644 index 00000000..d080e71e --- /dev/null +++ b/endpoints/comment/rating.php @@ -0,0 +1,31 @@ + ['filter' => FILTER_VALIDATE_INT] + ); + + protected function generate() : void + { + if (!$this->assertGET('id')) + { + $this->result = Util::toJSON(['success' => 0]); + 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` = %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/show-replies.php b/endpoints/comment/show-replies.php new file mode 100644 index 00000000..2de0ed02 --- /dev/null +++ b/endpoints/comment/show-replies.php @@ -0,0 +1,24 @@ + ['filter' => FILTER_VALIDATE_INT] + ); + + protected function generate() : void + { + if (!$this->assertGET('id')) + $this->result = Util::toJSON([]); + else + $this->result = Util::toJSON(CommunityContent::getCommentReplies($this->_get['id'])); + } +} + +?> diff --git a/endpoints/comment/sticky.php b/endpoints/comment/sticky.php new file mode 100644 index 00000000..16fcf3a7 --- /dev/null +++ b/endpoints/comment/sticky.php @@ -0,0 +1,34 @@ + ['filter' => FILTER_VALIDATE_INT ], + 'sticky' => ['filter' => FILTER_VALIDATE_INT, 'options' => ['min_range' => 0, 'max_range' => 1]] + ); + + protected function generate() : void + { + if (!$this->assertPOST('id', 'sticky')) + { + trigger_error('CommentStickyResponse - malformed request received', E_USER_ERROR); + return; + } + + if ($this->_post['sticky']) + DB::Aowow()->qry('UPDATE ::comments SET `flags` = `flags` | %i WHERE `id` = %i', CC_FLAG_STICKY, $this->_post['id']); + else + 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 new file mode 100644 index 00000000..1d21a6a1 --- /dev/null +++ b/endpoints/comment/undelete.php @@ -0,0 +1,49 @@ + ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkIdListUnsigned']], + // 'username' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkTextLine'] ] + ); + + protected function generate() : void + { + if (!$this->assertPOST('id')) + { + trigger_error('CommentUndeleteResponse - malformed request received', E_USER_ERROR); + return; + } + + // in theory, there is a username passed alongside if executed from userpage... lets just use the current user (see user.js) + $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 (DB::Aowow()->qry('UPDATE ::comments SET `flags` = `flags` & ~%i WHERE %and', CC_FLAG_DELETED, $where)) + { + $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()->qry('UPDATE %n SET `cuFlags` = `cuFlags` | %i WHERE `id` = %i', $tbl, CUSTOM_HAS_COMMENT, $typeId); + + return; + } + + trigger_error('CommentUndeleteResponse - user #'.User::$id.' could not unflag comment(s) #'.implode(', ', $this->_post['id']).' from deleted', E_USER_ERROR); + } +} + +?> diff --git a/endpoints/comment/upvote-reply.php b/endpoints/comment/upvote-reply.php new file mode 100644 index 00000000..f6e84a34 --- /dev/null +++ b/endpoints/comment/upvote-reply.php @@ -0,0 +1,55 @@ + ['filter' => FILTER_VALIDATE_INT] + ); + + protected function generate() : void + { + if (!$this->assertPOST('id')) + { + trigger_error('CommentUpvotereplyResponse - malformed request received', E_USER_ERROR); + $this->generate404(User::isInGroup(U_GROUP_STAFF) ? 'request malformed' : ''); + } + + if (!User::canUpvote()) + $this->generate404(User::isInGroup(U_GROUP_STAFF) ? 'cannot upvote' : ''); + + $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); + $this->generate404(User::isInGroup(U_GROUP_STAFF) ? 'replyID not found' : ''); + } + + if (User::$id == $comment['userId']) // not worth logging? + $this->generate404('LANG.voteself_tip'); + + if ($comment['deleted']) + $this->generate404('LANG.votedeleted_tip'); + + 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' : ''); + } + + Util::gainSiteReputation($comment['userId'], SITEREP_ACTION_UPVOTED, ['id' => $this->_post['id'], 'voterId' => User::$id]); + User::decrementDailyVotes(); + } +} + +?> diff --git a/endpoints/comment/vote.php b/endpoints/comment/vote.php new file mode 100644 index 00000000..1bf3b1ff --- /dev/null +++ b/endpoints/comment/vote.php @@ -0,0 +1,84 @@ + ['filter' => FILTER_VALIDATE_INT ], + 'rating' => ['filter' => FILTER_VALIDATE_INT, 'options' => ['min_range' => -2, 'max_range' => 2]] + ); + + protected function generate(): void + { + if (!$this->assertGET('id', 'rating')) + { + trigger_error('CommentVoteResponse - malformed request received', E_USER_ERROR); + $this->result = Util::toJSON(['error' => 1, 'message' => Lang::main('genericError')]); + return; + } + + if (User::getCurrentDailyVotes() <= 0) + { + $this->result = Util::toJSON(['error' => 1, 'message' => Lang::main('tooManyVotes')]); + return; + } + + $target = DB::Aowow()->selectRow( + '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) + { + trigger_error('CommentVoteResponse - target comment #'.$this->_get['id'].' not found', E_USER_ERROR); + $this->result = Util::toJSON(['error' => 1, 'message' => Lang::main('genericError')]); + return; + } + + $val = User::canSupervote() ? 2 : 1; + if ($this->_get['rating'] < 0) + $val *= -1; + + if (User::$id == $target['owner'] || $val != $this->_get['rating'] || $target['deleted']) + { + // circumvented the checks in JS + $this->result = Util::toJSON(['error' => 1, 'message' => Lang::main('genericError')]); + return; + } + + if (($val > 0 && !User::canUpvote()) || ($val < 0 && !User::canDownvote())) + { + $this->result = Util::toJSON(['error' => 1, 'message' => Lang::main('bannedRating')]); + return; + } + + $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()->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()->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) + { + if ($val > 0) // gain rep + Util::gainSiteReputation($target['owner'], SITEREP_ACTION_UPVOTED, ['id' => $this->_get['id'], 'voterId' => User::$id]); + else if ($val < 0) + Util::gainSiteReputation($target['owner'], SITEREP_ACTION_DOWNVOTED, ['id' => $this->_get['id'], 'voterId' => User::$id]); + + $this->result = Util::toJSON(['error' => 0]); + } + else + $this->result = Util::toJSON(['error' => 1, 'message' => Lang::main('intError')]); + } +} + +?> diff --git a/endpoints/compare/compare.php b/endpoints/compare/compare.php new file mode 100644 index 00000000..67604b4f --- /dev/null +++ b/endpoints/compare/compare.php @@ -0,0 +1,116 @@ + ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkCompareString']] + ); + protected array $expectedCOOKIE = array( + 'compare_groups' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkCompareString']] + ); + + public Summary $summary; + public array $cmpItems = []; + + private string $compareString = ''; + + public function __construct($rawParam) + { + parent::__construct($rawParam); + + // prefer GET over COOKIE + if ($this->_get['compare']) + $this->compareString = $this->_get['compare']; + else if ($this->_cookie['compare_groups']) + $this->compareString = $this->_cookie['compare_groups']; + } + + protected function generate() : void + { + $this->h1 = Lang::main('compareTool'); + + + array_unshift($this->title, $this->h1); + + + $this->summary = new Summary(array( + 'template' => 'compare', + 'id' => 'compare', + 'parent' => 'compare-generic' + )); + + if ($this->compareString) + { + $items = []; + foreach (explode(';', $this->compareString) as $itemsString) + { + $suGroup = []; + foreach (explode(':', $itemsString) as $itemDef) + { + // [itemId, subItem, permEnch, tempEnch, gem1, gem2, gem3, gem4] + $params = array_pad(array_map('intVal', explode('.', $itemDef)), 8, 0); + $items[] = $params[0]; + $suGroup[] = $params; + } + + $this->summary->addGroup($suGroup); + } + + $iList = new ItemList(array(['i.id', $items])); + $data = $iList->getListviewData(ITEMINFO_SUBITEMS | ITEMINFO_JSON); + + foreach ($iList->iterate() as $itemId => $__) + { + if (empty($data[$itemId])) + continue; + + if (!empty($data[$itemId]['subitems'])) + foreach ($data[$itemId]['subitems'] as &$si) + { + $si['enchantment'] = implode(', ', $si['enchantment']); + unset($si['chance']); + } + + $this->cmpItems[$itemId] = [ + 'name_'.Lang::getLocale()->json() => $iList->getField('name', true), + 'quality' => $iList->getField('quality'), + 'icon' => $iList->getField('iconString'), + 'jsonequip' => $data[$itemId] + ]; + } + } + + parent::generate(); + } + + protected static function checkCompareString(string $val) : string + { + $val = urldecode($val); + if (preg_match('/[^-?\d\.:;]/', $val)) + return ''; + + return $val; + } +} + +?> diff --git a/endpoints/contactus/contactus.php b/endpoints/contactus/contactus.php new file mode 100644 index 00000000..ace3fa02 --- /dev/null +++ b/endpoints/contactus/contactus.php @@ -0,0 +1,49 @@ + ['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 + 0: success + 1: captcha invalid + 2: description too long + 3: reason missing + 7: already reported + $: prints response + */ + protected function generate() : void + { + if (!$this->assertPOST('mode', 'reason')) + { + $this->result = 4; + return; + } + + $report = new Report($this->_post['mode'], $this->_post['reason'], $this->_post['id']); + if ($report->create($this->_post['desc'], $this->_post['ua'], $this->_post['appname'], $this->_post['page'], $this->_post['relatedurl'], $this->_post['email'])) + $this->result = 0; + else if (($e = $report->getError()) > 0) + $this->result = $e; + else + $this->result = Lang::main('intError'); + } +} + +?> diff --git a/endpoints/cookie/cookie.php b/endpoints/cookie/cookie.php new file mode 100644 index 00000000..5b5f9af0 --- /dev/null +++ b/endpoints/cookie/cookie.php @@ -0,0 +1,54 @@ + ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkEmptySet']] + ); + + public function __construct(private string $param) + { + // note that parent::__construct has to come after this + if ($param && preg_match('/^[\w-]+$/i', $param)) + $this->expectedGET = [$param => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkTextLine']]]; + + // NOW we know, what to expect and sanitize + parent::__construct($param); + } + + /* responses + 0: success + $: silent error + */ + protected function generate() : void + { + if (!$this->param && $this->_get['purge']) + { + 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; + } + + if (!$this->param || !$this->assertGET($this->param)) + { + trigger_error('CookieBaseResponse - malformed request received', E_USER_ERROR); + return; + } + + 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 new file mode 100644 index 00000000..bc07dbca --- /dev/null +++ b/endpoints/currencies/currencies.php @@ -0,0 +1,76 @@ +getCategoryFromUrl($rawParam); + + parent::__construct($rawParam); + } + + protected function generate() : void + { + $this->h1 = Util::ucFirst(Lang::game('currencies')); + + + /**************/ + /* Page Title */ + /**************/ + + array_unshift($this->title, $this->h1); + if ($this->category) + array_unshift($this->title, Lang::currency('cat', $this->category[0])); + + + /*************/ + /* Menu Path */ + /*************/ + + if ($this->category) + $this->breadcrumb[] = $this->category[0]; + + + /****************/ + /* Main Content */ + /****************/ + + $this->redButtons[BUTTON_WOWHEAD] = true; + + $conditions = []; + + if (!User::isInGroup(U_GROUP_EMPLOYEE)) + $conditions[] = [['cuFlags', CUSTOM_EXCLUDE_FOR_LISTVIEW, '&'], 0]; + + if ($this->category) + $conditions[] = ['category', $this->category[0]]; + + $money = new CurrencyList($conditions); + + $this->lvTabs = new Tabs(['parent' => "\$\$WH.ge('tabs-generic')"]); + + $this->lvTabs->addListviewTab(new Listview(['data' => $money->getListviewData()], CurrencyList::$brickFile)); + + parent::generate(); + } +} + +?> diff --git a/endpoints/currency/currency.php b/endpoints/currency/currency.php new file mode 100644 index 00000000..92a761a7 --- /dev/null +++ b/endpoints/currency/currency.php @@ -0,0 +1,267 @@ +typeId = intVal($id); + $this->contribute = Type::getClassAttrib($this->type, 'contribute') ?? CONTRIBUTE_NONE; + } + + + protected function generate() : void + { + $this->subject = new CurrencyList(array(['id', $this->typeId])); + if ($this->subject->error) + $this->generateNotFound(Lang::game('currency'), Lang::currency('notFound')); + + $this->h1 = $this->subject->getField('name', true); + + $this->gPageInfo += array( + 'type' => $this->type, + 'typeId' => $this->typeId, + 'name' => $this->h1 + ); + + $_relItemId = $this->subject->getField('itemId'); + + + /**************/ + /* Page Title */ + /**************/ + + array_unshift($this->title, $this->h1, Util::ucFirst(Lang::game('currency'))); + + + /*************/ + /* Menu Path */ + /*************/ + + $this->breadcrumb[] = $this->subject->getField('category'); + + + /***********/ + /* Infobox */ + /**********/ + + $infobox = Lang::getInfoBoxForFlags(intval($this->subject->getField('cuFlags'))); + + // cap + 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]'; + $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'); + + + /****************/ + /* Main Content */ + /****************/ + + $hi = $this->subject->getJSGlobals()[Type::CURRENCY][$this->typeId]['icon']; + if ($hi[0] == $hi[1]) + unset($hi[1]); + + $this->headIcons = $hi; + $this->redButtons = array( + BUTTON_WOWHEAD => true, + BUTTON_LINKS => true + ); + + if ($_ = $this->subject->getField('description', true)) + $this->extraText = new Markup($_, ['dbpage' => true, 'allow' => Markup::CLASS_ADMIN], 'text-generic'); + + + /**************/ + /* Extra Tabs */ + /**************/ + + $this->lvTabs = new Tabs(['parent' => "\$\$WH.ge('tabs-generic')"], 'tabsRelated', true); + + if ($this->typeId != CURRENCY_HONOR_POINTS && $this->typeId != CURRENCY_ARENA_POINTS) + { + // tabs: this currency is contained in.. + $lootTabs = new LootByItem($_relItemId); + + if ($lootTabs->getByItem()) + { + $this->extendGlobalData($lootTabs->jsGlobals); + + foreach ($lootTabs->iterate() as [$template, $tabData]) + { + 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)); + } + } + + // tab: sold by + $itemObj = new ItemList(array(['id', $_relItemId])); + if (!empty($itemObj->getExtendedCost()[$_relItemId])) + { + $vendors = $itemObj->getExtendedCost()[$_relItemId]; + $this->extendGlobalData($itemObj->getJSGlobals(GLOBALINFO_SELF | GLOBALINFO_RELATED)); + + $soldBy = new CreatureList(array(['id', array_keys($vendors)])); + if (!$soldBy->error) + { + $sbData = $soldBy->getListviewData(); + $extraCols = ['$Listview.extraCols.stock', "\$Listview.funcBox.createSimpleCol('stack', 'stack', '10%', 'stack')", '$Listview.extraCols.cost', '$Listview.extraCols.condition']; + foreach ($sbData as $k => &$row) + { + $items = []; + $tokens = []; + // note: can only display one entry per row, so only use first entry of each vendor + foreach ($vendors[$k][0] as $id => $qty) + { + if (is_string($id)) + continue; + + if ($id > 0) + $tokens[] = [$id, $qty]; + else if ($id < 0) + $items[] = [-$id, $qty]; + } + + if ($e = $vendors[$k][0]['event']) + if (Conditions::extendListviewRow($row, Conditions::SRC_NONE, $k, [Conditions::ACTIVE_EVENT, $e])) + $this->extendGlobalIds(Type::WORLDEVENT, $e); + + $row['stock'] = $vendors[$k][0]['stock']; + $row['stack'] = $itemObj->getField('buyCount'); + $row['cost'] = array( + $itemObj->getField('buyPrice'), + $items ?: null, + $tokens ?: null + ); + } + + // no conditions > remove conditions column + if (!array_column($sbData, 'condition')) + array_pop($extraCols); + + $this->addDataLoader('zones'); + $this->lvTabs->addListviewTab(new Listview(array( + 'data' => $sbData, + 'name' => '$LANG.tab_soldby', + 'id' => 'sold-by-npc', + 'extraCols' => $extraCols, + 'hiddenCols' => ['level', 'type'] + ), CreatureList::$brickFile)); + } + } + } + + // 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], DB::OR)); + if (!$createdBy->error) + { + $this->extendGlobalData($createdBy->getJSGlobals(GLOBALINFO_SELF | GLOBALINFO_RELATED)); + + $tabData = array( + 'data' => $createdBy->getListviewData(), + 'name' => '$LANG.tab_createdby', + 'id' => 'created-by', + ); + + if ($createdBy->hasSetFields('reagent1', 'reagent2', 'reagent3', 'reagent4', 'reagent5', 'reagent6', 'reagent7', 'reagent8')) + $tabData['visibleCols'] = ['reagents']; + + $this->lvTabs->addListviewTab(new Listview($tabData, SpellList::$brickFile)); + } + } + + // tab: currency for + $n = $w = null; + if ($this->typeId == CURRENCY_ARENA_POINTS) + { + $n = '?items&filter=cr=145;crs=1;crv=0'; + $w = '`reqArenaPoints` > 0'; + } + else if ($this->typeId == CURRENCY_HONOR_POINTS) + { + $n = '?items&filter=cr=144;crs=1;crv=0'; + $w = '`reqHonorPoints` > 0'; + } + else + $w = '`reqItemId1` = '.$_relItemId.' OR `reqItemId2` = '.$_relItemId.' OR `reqItemId3` = '.$_relItemId.' OR `reqItemId4` = '.$_relItemId.' OR `reqItemId5` = '.$_relItemId; + + 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 %in UNION SELECT `item` FROM game_event_npc_vendor WHERE `extendedCost` IN %in', $xCosts, $xCosts) : []; + if ($boughtBy) + { + $boughtBy = new ItemList(array(['id', $boughtBy])); + if (!$boughtBy->error) + { + $tabData = array( + 'data' => $boughtBy->getListviewData(ITEMINFO_VENDOR, [Type::CURRENCY => $this->typeId]), + 'name' => '$LANG.tab_currencyfor', + 'id' => 'currency-for', + 'extraCols' => ["\$Listview.funcBox.createSimpleCol('stack', 'stack', '10%', 'stack')", '$Listview.extraCols.cost'] + ); + + if ($n) + $tabData['note'] = sprintf(Util::$filterResultString, $n); + + $this->lvTabs->addListviewTab(new Listview($tabData, ItemList::$brickFile)); + + $this->extendGlobalData($boughtBy->getJSGlobals(GLOBALINFO_SELF | GLOBALINFO_RELATED)); + } + } + + parent::generate(); + } +} + +?> diff --git a/endpoints/currency/currency_power.php b/endpoints/currency/currency_power.php new file mode 100644 index 00000000..57b9c076 --- /dev/null +++ b/endpoints/currency/currency_power.php @@ -0,0 +1,50 @@ + ['filter' => FILTER_CALLBACK, 'options' => [Locale::class, 'tryFromDomain']] + ); + + public function __construct(string $id) + { + parent::__construct($id); + + // temp locale + if ($this->_get['domain']) + Lang::load($this->_get['domain']); + + $this->typeId = intVal($id); + } + + protected function generate() : void + { + $currency = new CurrencyList(array(['id', $this->typeId])); + if ($currency->error) + $this->cacheType = CACHE_TYPE_NONE; + else + $opts = array( + 'name' => $currency->getField('name', true), + 'tooltip' => $currency->renderTooltip(), + 'icon' => $currency->getField('iconString') + ); + + $this->result = new Tooltip(self::POWER_TEMPLATE, $this->typeId, $opts ?? []); + } +} + +?> diff --git a/endpoints/data/data.php b/endpoints/data/data.php new file mode 100644 index 00000000..8b38397b --- /dev/null +++ b/endpoints/data/data.php @@ -0,0 +1,150 @@ + ['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' ]], + 'class' => ['filter' => FILTER_VALIDATE_INT, 'options' => ['min_range' => 1, 'max_range' => 11]], + 'callback' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkCallback' ]] + ); + + public function __construct(string $rawParam) + { + parent::__construct($rawParam); + + if ($this->_get['locale']?->validate()) + Lang::load($this->_get['locale']); + } + + protected function generate() : void + { + // different data can be strung together + foreach ($this->params as $set) + { + // requires valid token to hinder automated access + 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; + } + + /* issue on no initial data: + when we loadOnDemand, the jScript tries to generate the catg-tree before it is initialized + it cant be initialized, without loading the data as empty catg are omitted + loading the data triggers the generation of the catg-tree + */ + + $this->result .= match($set) + { + 'factions' => $this->loadProfilerData($set), + 'mounts' => $this->loadProfilerData($set, SKILL_MOUNTS), + 'companions' => $this->loadProfilerData($set, SKILL_COMPANIONS), + 'quests' => $this->loadProfilerQuests($set, $this->_get['catg']), + 'recipes' => $this->loadProfilerRecipes(), + // locale independent + 'quick-excludes', + 'weight-presets', + 'item-scaling', + 'spell-scaling', + 'realms', + 'statistics' => $this->loadAgnosticFile($set), + // localized + 'talents', + 'achievements', + 'pet-talents', + 'glyphs', + 'gems', + 'enchants', + 'itemsets', + 'pets', + 'zones' => $this->loadLocalizedFile($set), + default => (function($x) { trigger_error('DataBaseResponse::generate - invalid file "'.$x.'" in request', E_USER_ERROR); })($set), + }; + } + } + + private function loadProfilerRecipes() : string + { + if (!$this->_get['callback'] || !$this->_get['skill']) + return ''; + + $result = ''; + + foreach ($this->_get['skill'] as $s) + Util::loadStaticFile('p-recipes-'.$s, $result, true); + + Util::loadStaticFile('p-recipes-sec', $result, true); + $result .= "\n\$WowheadProfiler.loadOnDemand('recipes', null);\n"; + + return $result; + } + + private function loadProfilerQuests(string $file, ?string $catg = null) : string + { + $result = ''; + + if ($catg === null) + Util::loadStaticFile('p-'.$file, $result, false); + else + Util::loadStaticFile('p-'.$file.'-'.$catg, $result, true); + + $result .= "\n\$WowheadProfiler.loadOnDemand('".$file."', ".($catg ?? 'null').");\n"; + + return $result; + } + + private function loadProfilerData(string $file, ?string $catg = null) : string + { + $result = ''; + + if ($this->_get['callback']) + if (Util::loadStaticFile('p-'.$file, $result, true)) + $result .= "\n\$WowheadProfiler.loadOnDemand('".$file."', ".($catg ?? 'null').");\n"; + + return $result; + } + + private function loadAgnosticFile(string $file) : string + { + $result = ''; + + if (!Util::loadStaticFile($file, $result) && Cfg::get('DEBUG')) + $result .= "alert('could not fetch static data: ".$file."');"; + + return $result . "\n\n"; + } + + private function loadLocalizedFile(string $file) : string + { + $result = ''; + + if ($file == 'talents' && ($_ = $this->_get['class'])) + $file .= "-".$_; + + if (!Util::loadStaticFile($file, $result, true) && Cfg::get('DEBUG')) + $result .= "alert('could not fetch static data: ".$file." for locale: ".Lang::getLocale()->json()."');"; + + return $result . "\n\n"; + } + + protected static function checkSkill(string $val) : array + { + return array_intersect(array_merge(SKILLS_TRADE_PRIMARY, [SKILL_FIRST_AID, SKILL_COOKING, SKILL_FISHING]), explode(',', $val)); + } + + protected static function checkCallback(string $val) : bool + { + return substr($val, 0, 29) === '$WowheadProfiler.loadOnDemand'; + } +} + +?> diff --git a/endpoints/edit/image.php b/endpoints/edit/image.php new file mode 100644 index 00000000..587d5a5e --- /dev/null +++ b/endpoints/edit/image.php @@ -0,0 +1,48 @@ + ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkTextLine'] ], + 'guide' => ['filter' => FILTER_VALIDATE_INT, 'options' => ['min_range' => 1, 'max_range' => 1]] + ); + + /* + success: bool + id: image enumerator + type: 3 ? png : jpg + name: old filename + error: errString + */ + protected function generate() : void + { + if (!$this->assertGET('qqfile', 'guide')) + { + $this->result = Util::toJSON(['success' => false, 'error' => Lang::main('genericError')]); + return; + } + + if (!User::canWriteGuide()) + { + $this->result = Util::toJSON(['success' => false, 'error' => Lang::main('genericError')]); + return; + } + + $this->result = GuideMgr::handleUpload(); + + if (isset($this->result['success'])) + $this->result += ['name' => $this->_get['qqfile']]; + + $this->result = Util::toJSON($this->result); + } +} + +?> diff --git a/endpoints/emote/emote.php b/endpoints/emote/emote.php new file mode 100644 index 00000000..8a73cfdd --- /dev/null +++ b/endpoints/emote/emote.php @@ -0,0 +1,226 @@ + 0: player text emote + * id < 0: creature emote + */ + + $this->typeId = intVal($id); + $this->contribute = Type::getClassAttrib($this->type, 'contribute') ?? CONTRIBUTE_NONE; + } + + protected function generate() : void + { + $this->subject = new EmoteList(array(['id', $this->typeId])); + if ($this->subject->error) + $this->generateNotFound(Lang::game('emote'), Lang::emote('notFound')); + + $this->h1 = Util::ucFirst($this->subject->getField('cmd')); + + $this->gPageInfo += array( + 'type' => $this->type, + 'typeId' => $this->typeId, + 'name' => $this->h1 + ); + + + /**************/ + /* Page Title */ + /**************/ + + array_unshift($this->title, $this->h1, Util::ucFirst(Lang::game('emote'))); + + + /***********/ + /* Infobox */ + /***********/ + + $infobox = Lang::getInfoBoxForFlags($this->subject->getField('cuFlags')); + + // has Animation + if ($this->subject->getField('isAnimated') && !$this->subject->getField('stateParam')) + { + $infobox[] = Lang::emote('isAnimated'); + + // anim state + $state = Lang::emote('state', $this->subject->getField('state')); + if ($this->subject->getField('state') == 1) + $state .= Lang::main('colon').Lang::unit('bytes1', 0, $this->subject->getField('stateParam')); + $infobox[] = $state; + } + + if (User::isInGroup(U_GROUP_STAFF | U_GROUP_TESTER)) + { + // player emote: point to internal data + if ($_ = $this->subject->getField('parentEmote')) + { + $this->extendGlobalIds(Type::EMOTE, $_); + $infobox[] = '[emote='.$_.']'; + } + + if ($flags = $this->subject->getField('flags')) + { + $box = Lang::game('flags').Lang::main('colon').'[ul]'; + foreach (Lang::emote('flags') as $bit => $str) + if ($bit & $flags) + $box .= '[li][tooltip name=hint-'.$bit.']'.Util::asHex($bit).'[/tooltip][span class=tip tooltip=hint-'.$bit.']'.$str.'[/span][/li]'; + $infobox[] = $box.'[/ul]'; + } + } + + // id + $infobox[] = Lang::emote('id') . $this->typeId; + + if ($infobox) + $this->infobox = new InfoboxMarkup($infobox, ['allow' => Markup::CLASS_STAFF, 'dbpage' => true], 'infobox-contents0'); + + + /****************/ + /* Main Content */ + /****************/ + + $text = ''; + + 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` = %i AND `locales` & %i', $this->typeId, 1 << Lang::getLocale()->value)) + { + $text .= '[h3]'.Lang::emote('aliases').'[/h3][ul]'; + foreach ($aliasses as $a) + $text .= '[li]/'.$a.'[/li]'; + + $text .= '[/ul][br][br]'; + } + + $target = $noTarget = []; + if ($_ = $this->subject->getField('extToExt', true)) + $target[] = $this->prepare($_); + if ($_ = $this->subject->getField('extToMe', true)) + $target[] = $this->prepare($_); + if ($_ = $this->subject->getField('meToExt', true)) + $target[] = $this->prepare($_); + if ($_ = $this->subject->getField('extToNone', true)) + $noTarget[] = $this->prepare($_); + if ($_ = $this->subject->getField('meToNone', true)) + $noTarget[] = $this->prepare($_); + + if (!$target && !$noTarget) + $text .= '[div][i class=q0]'.Lang::emote('noText').'[/i][/div]'; + + if ($target) + { + $text .= '[pad][b]'.Lang::emote('targeted').'[/b][ul]'; + foreach ($target as $t) + $text .= '[li][span class=s4]'.$t.'[/span][/li]'; + $text .= '[/ul]'; + } + + if ($noTarget) + { + $text .= '[pad][b]'.Lang::emote('untargeted').'[/b][ul]'; + foreach ($noTarget as $t) + $text .= '[li][span class=s4]'.$t.'[/span][/li]'; + $text .= '[/ul]'; + } + + // event sound + if ($_ = $this->subject->getField('soundId')) + { + $this->extendGlobalIds(Type::SOUND, $_); + $text .= '[h3]'.Lang::emote('eventSound').'[/h3][sound='.$_.']'; + } + + if ($text) + $this->extraText = new Markup($text, ['dbpage' => true, 'allow' => Markup::CLASS_ADMIN], 'text-generic'); + + $this->redButtons = array( + BUTTON_LINKS => ['type' => $this->type, 'typeId' => $this->typeId], + BUTTON_WOWHEAD => false + ); + + + /**************/ + /* Extra Tabs */ + /**************/ + + $this->lvTabs = new Tabs(['parent' => "\$\$WH.ge('tabs-generic')"], 'tabsRelated', true); + + // tab: achievement + $condition = array( + ['ac.type', ACHIEVEMENT_CRITERIA_TYPE_DO_EMOTE], + ['ac.value1', $this->typeId], + ); + $acv = new AchievementList($condition); + if (!$acv->error) + { + $this->extendGlobalData($acv->getJsGlobals()); + $this->lvTabs->addListviewTab(new Listview(['data' => $acv->getListviewData()], AchievementList::$brickFile)); + } + + // tab: sound + $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 %if', $this->typeId < 0, '-`emoteId` = %i OR', $this->subject->getField('parentEmote'), '%end `emoteId` = %i + GROUP BY `soundId`', + $this->typeId, + ); + + if ($ems) + { + $sounds = new SoundList(array(['id', array_keys($ems)])); + if (!$sounds->error) + { + $this->extendGlobalData($sounds->getJSGlobals(GLOBALINFO_SELF)); + $data = $sounds->getListviewData(); + foreach ($data as $id => &$d) + { + $d['races'] = $ems[$id]['raceMask']; + $d['gender'] = $ems[$id]['gender']; + } + + $this->lvTabs->addListviewTab(new Listview(array( + 'data' => $data, + // gender races + 'extraCols' => ['$Listview.templates.title.columns[1]', '$Listview.templates.classs.columns[1]'] + ), SoundList::$brickFile)); + } + } + + parent::generate(); + } + + private function prepare(string $emote) : string + { + $emote = Util::parseHtmlText($emote, true); + return preg_replace('/%\d?\$?s/', '<'.Util::ucFirst(Lang::main('name')).'>', $emote); + } +} + +?> diff --git a/endpoints/emotes/emotes.php b/endpoints/emotes/emotes.php new file mode 100644 index 00000000..de77397a --- /dev/null +++ b/endpoints/emotes/emotes.php @@ -0,0 +1,61 @@ +getCategoryFromUrl($rawParam); + + parent::__construct($rawParam); + } + + protected function generate() : void + { + $this->h1 = Util::ucFirst(Lang::game('emotes')); + + + /**************/ + /* Page Title */ + /**************/ + + array_unshift($this->title, $this->h1); + + + /****************/ + /* Main Content */ + /****************/ + + $cnd = []; // don't limit, for we have no filter or category + if (!User::isInGroup(U_GROUP_STAFF)) + $cnd[] = [['cuFlags', CUSTOM_EXCLUDE_FOR_LISTVIEW, '&'], 0]; + + $this->lvTabs = new Tabs(['parent' => "\$\$WH.ge('tabs-generic')"]); + + $tabData = array( + 'data' => (new EmoteList($cnd))->getListviewData(), + 'name' => Util::ucFirst(Lang::game('emotes')) + ); + + $this->lvTabs->addListviewTab(new Listview($tabData, EmoteList::$brickFile, 'emote')); + + parent::generate(); + } +} + +?> diff --git a/endpoints/enchantment/enchantment.php b/endpoints/enchantment/enchantment.php new file mode 100644 index 00000000..98ae613f --- /dev/null +++ b/endpoints/enchantment/enchantment.php @@ -0,0 +1,320 @@ +typeId = intVal($id); + $this->contribute = Type::getClassAttrib($this->type, 'contribute') ?? CONTRIBUTE_NONE; + } + + protected function generate() : void + { + $this->subject = new EnchantmentList(array(['id', $this->typeId])); + if ($this->subject->error) + $this->generateNotFound(Lang::game('enchantment'), Lang::enchantment('notFound')); + + $this->extendGlobalData($this->subject->getJSGlobals()); + + $this->h1 = $this->subject->getField('name', true); + + $this->gPageInfo += array( + 'type' => $this->type, + 'typeId' => $this->typeId, + 'name' => $this->h1 + ); + + /*************/ + /* Menu Path */ + /*************/ + + if ($_ = $this->getDistinctType()) + $this->breadcrumb[] = $_; + + + /**************/ + /* Page Title */ + /**************/ + + array_unshift($this->title, $this->h1, Util::ucFirst(Lang::game('enchantment'))); + + + /***********/ + /* Infobox */ + /***********/ + + $infobox = Lang::getInfoBoxForFlags($this->subject->getField('cuFlags')); + + // reqLevel + if ($_ = $this->subject->getField('requiredLevel')) + $infobox[] = sprintf(Lang::game('reqLevel'), $_); + + // reqskill + if ($_ = $this->subject->getField('skillLine')) + { + $this->extendGlobalIds(Type::SKILL, $_); + + $foo = Lang::game('requires', [' [skill='.$_.']']); + if ($_ = $this->subject->getField('skillLevel')) + $foo .= ' ('.$_.')'; + + $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'); + + + /****************/ + /* Main Content */ + /****************/ + + $this->redButtons = array( + BUTTON_LINKS => ['type' => $this->type, 'typeId' => $this->typeId], + BUTTON_WOWHEAD => false + ); + + $this->effects = []; + // 3 effects + for ($i = 1; $i < 4; $i++) + { + $_ty = $this->subject->getField('type'.$i); + $_qty = $this->subject->getField('amount'.$i); + $_obj = $this->subject->getField('object'.$i); + $_tip = []; + + switch ($_ty) + { + case ENCHANTMENT_TYPE_COMBAT_SPELL: + case ENCHANTMENT_TYPE_EQUIP_SPELL: + case ENCHANTMENT_TYPE_USE_SPELL: + [$spellId, $trigger, $charges, $procChance] = $this->subject->getField('spells')[$i]; + $spl = $this->subject->getRelSpell($spellId); + $this->effects[$i] = array( + 'name' => $this->fmtStaffTip(Lang::item('trigger', $trigger), 'Type: '.$_ty), + 'proc' => $procChance, + 'value' => $_qty ?: null, + 'tip' => [], + 'icon' => new IconElement( + Type::SPELL, + $spellId, + !$spl ? Util::ucFirst(Lang::game('spell')).' #'.$spellId : Util::localizedString($spl, 'name'), + $charges, + link: !!$spl + ) + ); + break; + case ENCHANTMENT_TYPE_STAT: + if ($idx = Stat::getIndexFrom(Stat::IDX_ITEM_MOD, $_obj)) + if ($jsonStat = Stat::getJsonString($idx)) + $_tip = [User::isInGroup(U_GROUP_STAFF) ? $_obj : null, $jsonStat]; + // DO NOT BREAK! + case ENCHANTMENT_TYPE_DAMAGE: + case ENCHANTMENT_TYPE_TOTEM: + case ENCHANTMENT_TYPE_PRISMATIC_SOCKET: + case ENCHANTMENT_TYPE_RESISTANCE: + $this->effects[$i] = array( + 'name' => $this->fmtStaffTip(Lang::enchantment('types', $_ty), 'Type: '.$_ty), + 'proc' => null, + 'value' => $_qty, + 'tip' => $_tip, + 'icon' => null + ); + if ($_ty == ENCHANTMENT_TYPE_RESISTANCE) + $this->effects[$i]['name'] .= Lang::main('colon').'('.$this->fmtStaffTip(Lang::getMagicSchools(1 << $_obj), 'Object: '.$_obj).')'; + } + } + + // activation conditions + if ($_ = $this->subject->getField('conditionId')) + $this->activation = Game::getEnchantmentCondition($_); + + + /**************/ + /* Extra Tabs */ + /**************/ + + $this->lvTabs = new Tabs(['parent' => "\$\$WH.ge('tabs-generic')"], 'tabsRelated', true); + + // used by gem + $gemList = new ItemList(array(['gemEnchantmentId', $this->typeId])); + if (!$gemList->error) + { + $this->extendGlobalData($gemList->getJsGlobals()); + $this->lvTabs->addListviewTab(new Listview(array( + 'data' => $gemList->getListviewData(), + 'name' => '$LANG.tab_usedby + \' \' + LANG.gems', + 'id' => 'used-by-gem', + ), ItemList::$brickFile)); + } + + // used by socket bonus + $socketsList = new ItemList(array(['socketBonus', $this->typeId])); + if (!$socketsList->error) + { + $this->extendGlobalData($socketsList->getJsGlobals()); + $this->lvTabs->addListviewTab(new Listview(array( + 'data' => $socketsList->getListviewData(), + 'name' => '$LANG.tab_socketbonus', + 'id' => 'used-by-socketbonus', + ), ItemList::$brickFile)); + } + + // used by spell + // used by useItem + $cnd = array( + 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) + { + $spellData = $spellList->getListviewData(); + $this->extendGlobalData($spellList->getJsGlobals()); + + $spellIds = $spellList->getFoundIDs(); + $conditions = array( + 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); + if (!$ubItems->error) + { + $this->extendGlobalData($ubItems->getJSGlobals(GLOBALINFO_SELF)); + $this->lvTabs->addListviewTab(new Listview(array( + 'data' => $ubItems->getListviewData(), + 'name' => '$LANG.tab_usedby + \' \' + LANG.types[3][0]', + 'id' => 'used-by-item', + ), ItemList::$brickFile)); + } + + // remove found spells if they are used by an item + if (!$ubItems->error) + { + foreach ($spellList->iterate() as $sId => $__) + { + // if Perm. Enchantment display both + for ($i = 1; $i < 4; $i++) + if ($spellList->getField('effect'.$i.'Id') == SPELL_EFFECT_ENCHANT_ITEM) + continue 2; + + foreach ($ubItems->iterate() as $__) + { + for ($i = 1; $i < 6; $i++) + { + if ($ubItems->getField('spellId'.$i) == $sId) + { + unset($spellData[$sId]); + break 2; + } + } + } + } + } + + if ($spellData) + $this->lvTabs->addListviewTab(new Listview(array( + 'data' => $spellData, + 'name' => '$LANG.tab_usedby + \' \' + LANG.types[6][0]', + 'id' => 'used-by-spell', + ), SpellList::$brickFile)); + } + + // used by randomAttrItem + $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()->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(['randomEnchant', array_keys($randIds)])); + if (!$randItems->error) + { + $data = $randItems->getListviewData(); + foreach ($randItems->iterate() as $iId => $__) + { + $re = $randItems->getField('randomEnchant'); + + $data[$iId]['percent'] = $iet[abs($re)]['chance']; + $data[$iId]['count'] = 1; // expected by js or the pct-col becomes unsortable + $data[$iId]['rel'] = 'rand='.$ire[$iet[abs($re)]['ench']]['id']; + $data[$iId]['name'] .= ' '.Util::localizedString($ire[$iet[abs($re)]['ench']], 'name'); + } + + $this->extendGlobalData($randItems->getJSGlobals(GLOBALINFO_SELF)); + $this->lvTabs->addListviewTab(new Listview(array( + 'data' => $data, + 'id' => 'used-by-rand', + 'name' => '$LANG.tab_usedby + \' \' + \''.Lang::item('_rndEnchants').'\'', + 'extraCols' => ['$Listview.extraCols.percent'] + ), ItemList::$brickFile)); + } + } + } + + parent::generate(); + } + + private function getDistinctType() : int + { + $type = 0; + for ($i = 1; $i < 4; $i++) + { + if ($_ = $this->subject->getField('type'.$i)) + { + if ($type && $type != $_) // already set + return 0; + else + $type = $_; + } + } + + return $type; + } +} + +?> diff --git a/endpoints/enchantments/enchantments.php b/endpoints/enchantments/enchantments.php new file mode 100644 index 00000000..3dfbb79a --- /dev/null +++ b/endpoints/enchantments/enchantments.php @@ -0,0 +1,131 @@ + ['filter' => FILTER_VALIDATE_REGEXP, 'options' => ['regexp' => Filter::PATTERN_PARAM]] + ); + protected array $validCats = [1, 2, 3, 4, 5, 6, 7, 8]; + + public function __construct(string $rawParam) + { + $this->getCategoryFromUrl($rawParam); + + parent::__construct($rawParam); + + if ($this->category) + $this->forward('?enchantments&filter=ty='.$this->category[0]); + + 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; + } + + protected function generate() : void + { + $this->h1 = Util::ucFirst(Lang::game('enchantments')); + + $conditions = [Listview::DEFAULT_SIZE]; + if (!User::isInGroup(U_GROUP_EMPLOYEE)) + $conditions[] = [['cuFlags', CUSTOM_EXCLUDE_FOR_LISTVIEW, '&'], 0]; + + if ($_ = $this->filter->getConditions()) + $conditions[] = $_; + + + /**************/ + /* Page Title */ + /**************/ + + $fiForm = $this->filter->values; + + array_unshift($this->title, $this->h1); + if (isset($fiForm['ty']) && count($fiForm['ty']) == 1 && $fiForm['ty'][0] > ENCHANTMENT_TYPE_NONE && $fiForm['ty'][0] <= ENCHANTMENT_TYPE_PRISMATIC_SOCKET) + array_unshift($this->title, Lang::enchantment('types', $fiForm['ty'][0])); + + + /*************/ + /* Menu Path */ + /*************/ + + if (isset($fiForm['ty']) && count($fiForm['ty']) == 1) + $this->breadcrumb[] = $fiForm['ty'][0]; + + + /****************/ + /* Main Content */ + /****************/ + + $this->redButtons[BUTTON_WOWHEAD] = false; + + $tabData = array( + 'data' => [], + 'name' => Util::ucFirst(Lang::game('enchantments')) + ); + + $ench = new EnchantmentList($conditions, ['calcTotal' => true]); + + $tabData['data'] = $ench->getListviewData(); + $this->extendGlobalData($ench->getJSGlobals()); + + $xCols = []; + foreach (Stat::getFilterCriteriumIdFor() as $idx => $fiId) + if (array_filter(array_column($tabData['data'], Stat::getJsonString($idx)))) + $xCols[] = $fiId; + + // some kind of declaration conflict going on here..., expects colId for WEAPON_DAMAGE_MAX but jsonString is WEAPON_DAMAGE + if (array_filter(array_column($tabData['data'], 'dmg'))) + $xCols[] = Stat::getFilterCriteriumId(Stat::WEAPON_DAMAGE_MAX); + + if ($xCols) + $this->filter->fiExtraCols = array_merge($this->filter->fiExtraCols, $xCols); + + if ($this->filter->fiExtraCols) + $tabData['extraCols'] = '$fi_getExtraCols(fi_extraCols, 0, 0)'; + + if (array_filter(array_column($tabData['data'], 'spells'))) + $tabData['visibleCols'] = ['trigger']; + + if (!$ench->hasSetFields('skillLine')) + $tabData['hiddenCols'] = ['skill']; + + if ($ench->getMatches() > Listview::DEFAULT_SIZE) + { + $tabData['note'] = sprintf(Util::$tryFilteringString, 'LANG.lvnote_enchantmentsfound', $ench->getMatches(), Listview::DEFAULT_SIZE); + $tabData['_truncated'] = 1; + } + + $this->lvTabs = new Tabs(['parent' => "\$\$WH.ge('tabs-generic')"]); + + $this->lvTabs->addListviewTab(new Listview($tabData, EnchantmentList::$brickFile, 'enchantment')); + + parent::generate(); + } +} + +?> diff --git a/endpoints/event/event.php b/endpoints/event/event.php new file mode 100644 index 00000000..7796afc1 --- /dev/null +++ b/endpoints/event/event.php @@ -0,0 +1,373 @@ +typeId = intVal($id); + $this->contribute = Type::getClassAttrib($this->type, 'contribute') ?? CONTRIBUTE_NONE; + } + + protected function generate() : void + { + $this->subject = new WorldEventList(array(['id', $this->typeId])); + if ($this->subject->error) + $this->generateNotFound(Lang::game('event'), Lang::event('notFound')); + + $this->h1 = $this->subject->getField('name', true); + $this->dates = array( + 'firstDate' => $this->subject->getField('startTime'), + 'lastDate' => $this->subject->getField('endTime'), + 'length' => $this->subject->getField('length'), + 'rec' => $this->subject->getField('occurence') + ); + + $this->gPageInfo += array( + 'type' => $this->type, + 'typeId' => $this->typeId, + 'name' => $this->h1 + ); + + $_holidayId = $this->subject->getField('holidayId'); + + + /*************/ + /* Menu Path */ + /*************/ + + $this->breadcrumb[] = match ($this->subject->getField('scheduleType')) + { + -1 => 1, + 0, 1 => 2, + 2 => 3, + '' => 0, + default => 0 + }; + + + /**************/ + /* Page Title */ + /**************/ + + array_unshift($this->title, $this->h1, Util::ucWords(Lang::game('event'))); + + + /***********/ + /* Infobox */ + /***********/ + + $infobox = Lang::getInfoBoxForFlags($this->subject->getField('cuFlags')); + + // boss + if ($_ = $this->subject->getField('bossCreature')) + { + $this->extendGlobalIds(Type::NPC, $_); + $infobox[] = Lang::npc('rank', 3).Lang::main('colon').'[npc='.$_.']'; + } + + // 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'); + + + /****************/ + /* Main Content */ + /****************/ + + // no entry in ::articles? use default HolidayDescription + if ($_holidayId && empty($this->article)) + $this->article = new Markup($this->subject->getField('description', true), ['dbpage' => true]); + + if ($_holidayId) + $this->wowheadLink = sprintf(WOWHEAD_LINK, Lang::getLocale()->domain(), 'event=', $_holidayId); + + $this->headIcons = [$this->subject->getField('iconString')]; + $this->redButtons = array( + BUTTON_WOWHEAD => $_holidayId > 0, + BUTTON_LINKS => ['type' => $this->type, 'typeId' => $this->typeId] + ); + + parent::generate(); + + + /**************/ + /* Extra Tabs */ + /**************/ + + $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`) = %i', $this->typeId)) + { + $creatures = new CreatureList(array(['id', array_keys($npcIds)])); + if (!$creatures->error) + { + $data = $creatures->getListviewData(); + foreach ($data as &$d) + $d['method'] = $npcIds[$d['id']]; + + $tabData = ['data' => $data]; + + if ($_holidayId && CreatureListFilter::getCriteriaIndex(38, $_holidayId)) + $tabData['note'] = sprintf(Util::$filterResultString, '?npcs&filter=cr=38;crs='.$_holidayId.';crv=0'); + + $this->result->addDataLoader('zones'); // req. by secondary tooltip in this tab + $this->lvTabs->addListviewTab(new Listview($tabData, CreatureList::$brickFile)); + } + } + + // 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`) = %i', $this->typeId)) + { + $objects = new GameObjectList(array(['id', array_keys($objectIds)])); + if (!$objects->error) + { + $data = $objects->getListviewData(); + foreach ($data as &$d) + $d['method'] = $objectIds[$d['id']]; + + $tabData = ['data' => $data]; + + if ($_holidayId && GameObjectListFilter::getCriteriaIndex(16, $_holidayId)) + $tabData['note'] = sprintf(Util::$filterResultString, '?objects&filter=cr=16;crs='.$_holidayId.';crv=0'); + + $this->result->addDataLoader('zones'); // req. by secondary tooltip in this tab + $this->lvTabs->addListviewTab(new Listview($tabData, GameObjectList::$brickFile)); + } + } + + // tab: achievements + $exclAcvs = []; + if ($_ = $this->subject->getField('achievementCatOrId')) + { + $condition = $_ > 0 ? [['category', $_]] : [['id', -$_]]; + $acvs = new AchievementList($condition); + if (!$acvs->error) + { + $this->extendGlobalData($acvs->getJSGlobals(GLOBALINFO_SELF | GLOBALINFO_RELATED)); + + $tabData = array( + 'data' => $acvs->getListviewData(), + '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'); + + $this->lvTabs->addListviewTab(new Listview($tabData, AchievementList::$brickFile)); + } + } + + $itemCnd = []; + if ($_holidayId) + { + // 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)) + { + $condition = array(['ac.id', $extraCrt]); + if ($exclAcvs) + $condition[] = ['a.id', $exclAcvs, '!']; + + $crtOf = new AchievementList($condition); + 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)); + } + } + + $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 + if ($npcIds && !$creatures->error) + { + // vendor + $cIds = $creatures->getFoundIDs(); + if ($sells = DB::World()->selectCol( + '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]; + } + + // tab: items + // not checking for loot ... cant distinguish between eventLoot and fillerCrapLoot + if ($itemCnd) + { + array_unshift($itemCnd, DB::OR); + $eventItems = new ItemList($itemCnd); + if (!$eventItems->error) + { + $this->extendGlobalData($eventItems->getJSGlobals(GLOBALINFO_SELF)); + + $tabData = ['data'=> $eventItems->getListviewData()]; + + if ($_holidayId && ItemListFilter::getCriteriaIndex(160, $_holidayId)) + $tabData['note'] = sprintf(Util::$filterResultString, '?items&filter=cr=160;crs='.$_holidayId.';crv=0'); + + $this->lvTabs->addListviewTab(new Listview($tabData, ItemList::$brickFile)); + } + } + + // tab: see also (event conditions) + 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); + + if ($seeAlso = array_filter($rel, fn($x) => $x > 0)) + { + $relEvents = new WorldEventList(array(['id', $seeAlso])); + $this->extendGlobalData($relEvents->getJSGlobals()); + $relData = $relEvents->getListviewData(); + foreach ($relEvents->getFoundIDs() as $id) + Conditions::extendListviewRow($relData[$id], Conditions::SRC_NONE, $this->typeId, [-Conditions::ACTIVE_EVENT, $this->typeId]); + + $this->extendGlobalData($this->subject->getJSGlobals()); + $d = $this->subject->getListviewData(); + foreach ($rel as $r) + if ($r > 0) + if (Conditions::extendListviewRow($d[$this->typeId], Conditions::SRC_NONE, $this->typeId, [-Conditions::ACTIVE_EVENT, $r])) + $this->extendGlobalIds(Type::WORLDEVENT, $r); + + $tabData = array( + 'data' => array_merge($relData, $d), + 'id' => 'see-also', + 'name' => '$LANG.tab_seealso', + 'hiddenCols' => ['date'], + 'extraCols' => ['$Listview.extraCols.condition'] + ); + $this->lvTabs->addListviewTab(new Listview($tabData, WorldEventList::$brickFile)); + } + } + + // tab: condition for + $cnd = new Conditions(); + $cnd->getByCondition(Type::WORLDEVENT, $this->typeId)->prepare(); + if ($tab = $cnd->toListviewTab('condition-for', '$LANG.tab_condition_for')) + { + $this->extendGlobalData($cnd->getJsGlobals()); + $this->lvTabs->addDataTab(...$tab); + } + + $this->result->registerDisplayHook('lvTabs', [self::class, 'tabsHook']); + $this->result->registerDisplayHook('infobox', [self::class, 'infoboxHook']); + } + + // update dates to now() + public static function tabsHook(Template\PageTemplate &$pt, Tabs &$lvTabs) : void + { + foreach ($lvTabs->iterate() as &$listview) + if (is_object($listview) && $listview?->getTemplate() == 'holiday') + WorldEventList::updateListview($listview); + } + + /* finalize infobox */ + public static function infoboxHook(Template\PageTemplate &$pt, ?InfoboxMarkup &$markup) : void + { + WorldEventList::updateDates($pt->dates, $start, $end, $rec); + $infobox = []; + + // start + if ($start) + $infobox[] = Lang::event('start').date(Lang::main('dateFmtLong'), $start); + + // end + if ($end) + $infobox[] = Lang::event('end').date(Lang::main('dateFmtLong'), $end); + + // interval + if ($rec > 0) + $infobox[] = Lang::event('interval').DateTime::formatTimeElapsed($rec * 1000); + + // in progress + if ($start < time() && $end > time()) + $infobox[] = '[span class=q2]'.Lang::event('inProgress').'[/span]'; + + if ($infobox && !$markup) + $markup = new InfoboxMarkup($infobox, ['allow' => Markup::CLASS_STAFF, 'dbpage' => true], 'infobox-contents0'); + else if ($markup) + foreach ($infobox as $ib) + $markup->addItem($ib); + } +} + +?> diff --git a/endpoints/event/event_power.php b/endpoints/event/event_power.php new file mode 100644 index 00000000..48c75683 --- /dev/null +++ b/endpoints/event/event_power.php @@ -0,0 +1,79 @@ + ['filter' => FILTER_CALLBACK, 'options' => [Locale::class, 'tryFromDomain']] + ); + + private array $dates = []; + + public function __construct(string $id) + { + parent::__construct($id); + + // temp locale + if ($this->_get['domain']) + Lang::load($this->_get['domain']); + + $this->typeId = intVal($id); + } + + protected function generate() : void + { + $worldevent = new WorldEventList(array(['id', $this->typeId])); + if ($worldevent->error) + $this->cacheType = CACHE_TYPE_NONE; + else + { + $icon = $worldevent->getField('iconString'); + if ($icon == 'trade_engineering') + $icon = null; + + $opts = array( + 'name' => $worldevent->getField('name', true), + 'tooltip' => $worldevent->renderTooltip(), + 'icon' => $icon + ); + + $this->dates = array( + 'firstDate' => $worldevent->getField('startTime'), + 'lastDate' => $worldevent->getField('endTime'), + 'length' => $worldevent->getField('length'), + 'rec' => $worldevent->getField('occurence') + ); + + $this->setOnCacheLoaded([self::class, 'onBeforeDisplay'], $this->dates); + } + + $this->result = new Tooltip(self::POWER_TEMPLATE, $this->typeId, $opts ?? []); + } + + public static function onBeforeDisplay(string $tooltip, array $dates) : string + { + // update dates to now() + WorldEventList::updateDates($dates, $start, $end); + + return sprintf( + $tooltip, + $start ? date(Lang::main('dateFmtLong'), $start) : null, + $end ? date(Lang::main('dateFmtLong'), $end) : null + ); + } +} + +?> diff --git a/endpoints/events/events.php b/endpoints/events/events.php new file mode 100644 index 00000000..885a7821 --- /dev/null +++ b/endpoints/events/events.php @@ -0,0 +1,96 @@ +getCategoryFromUrl($rawParam); + + parent::__construct($rawParam); + } + + protected function generate() : void + { + $this->h1 = Util::ucWords(Lang::game('events')); + + + /**************/ + /* Page Title */ + /**************/ + + array_unshift($this->title, $this->h1); + if ($this->category) + array_unshift($this->title, Lang::event('category')[$this->category[0]]); + + + /*************/ + /* Menu Path */ + /*************/ + + if ($this->category) + $this->breadcrumb[] = $this->category[0]; + + + /****************/ + /* Main Content */ + /****************/ + + $this->redButtons[BUTTON_WOWHEAD] = true; + + $condition = [Listview::DEFAULT_SIZE]; + + if (!User::isInGroup(U_GROUP_EMPLOYEE)) + $condition[] = [['cuFlags', CUSTOM_EXCLUDE_FOR_LISTVIEW, '&'], 0]; + + if ($this->category) + $condition[] = match ($this->category[0]) + { + 1 => ['h.scheduleType', -1], + 2 => ['h.scheduleType', [0, 1]], + 3 => ['h.scheduleType', 2], + default => ['e.holidayId', 0] // also cat 0 + }; + + $events = new WorldEventList($condition); + $this->extendGlobalData($events->getJSGlobals()); + + $this->lvTabs = new Tabs(['parent' => "\$\$WH.ge('tabs-generic')"]); + + $this->lvTabs->addListviewTab(new Listview(['data' => $events->getListviewData()], WorldEventList::$brickFile)); + + if ($_ = array_filter($events->getListviewData(), fn($x) => $x['category'] > 0)) + $this->lvTabs->addListviewTab(new Listview(['data' => $_, 'hideCount' => 1], 'calendar')); + + parent::generate(); + + $this->result->registerDisplayHook('lvTabs', [self::class, 'tabsHook']); + } + + // recalculate dates with now() + public static function tabsHook(Template\PageTemplate &$pt, Tabs &$lvTabs) : void + { + foreach ($lvTabs->iterate() as &$listview) + if (is_object($listview) && ($listview?->getTemplate() == 'holiday' || $listview?->getTemplate() == 'holidaycal')) + WorldEventList::updateListview($listview); + } +} + +?> diff --git a/endpoints/faction/faction.php b/endpoints/faction/faction.php new file mode 100644 index 00000000..e85f5a79 --- /dev/null +++ b/endpoints/faction/faction.php @@ -0,0 +1,362 @@ +typeId = intVal($id); + $this->contribute = Type::getClassAttrib($this->type, 'contribute') ?? CONTRIBUTE_NONE; + } + + protected function generate() : void + { + $this->subject = new FactionList(array(['id', $this->typeId])); + if ($this->subject->error) + $this->generateNotFound(Lang::game('faction'), Lang::faction('notFound')); + + $this->h1 = $this->subject->getField('name', true); + + $this->gPageInfo += array( + 'type' => $this->type, + 'typeId' => $this->typeId, + 'name' => $this->h1 + ); + + + /*************/ + /* Menu Path */ + /*************/ + + array_unshift($this->title, $this->h1, Util::ucFirst(Lang::game('faction'))); + + + /**************/ + /* Page Title */ + /**************/ + + if ($foo = $this->subject->getField('cat')) + { + if ($bar = $this->subject->getField('cat2')) + $this->breadcrumb[] = $bar; + + $this->breadcrumb[] = $foo; + } + + + /***********/ + /* Infobox */ + /***********/ + + $infobox = Lang::getInfoBoxForFlags($this->subject->getField('cuFlags')); + + // Quartermaster if any + if ($ids = $this->subject->getField('qmNpcIds')) + { + $this->extendGlobalIds(Type::NPC, ...$ids); + + $qmStr = Lang::faction('quartermaster'); + + if (count($ids) == 1) + $qmStr .= '[npc='.$ids[0].']'; + else if (count($ids) > 1) + { + $qmStr .= '[ul]'; + foreach ($ids as $id) + $qmStr .= '[li][npc='.$id.'][/li]'; + + $qmStr .= '[/ul]'; + } + + $infobox[] = $qmStr; + } + + // side if any + if ($_ = $this->subject->getField('side')) + $infobox[] = Lang::main('side').'[span class=icon-'.($_ == SIDE_ALLIANCE ? 'alliance' : 'horde').']'.Lang::game('si', $_).'[/span]'; + + // 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); + + + /****************/ + /* Main Content */ + /****************/ + + $this->redButtons = array( + BUTTON_WOWHEAD => true, + BUTTON_LINKS => ['type' => $this->type, 'typeId' => $this->typeId] + ); + + // Spillover Effects + /* todo (low): also check on reputation_spillover_template (but its data is identical to calculation below + $rst = DB::World()->selectRow('SELECT + 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 = %i', $this->typeId); + */ + + + $conditions = array( + ['id', $this->typeId, '!'], // not self + ['repIdx', -1, '!'] // only gainable + ); + + if ($p = $this->subject->getField('parentFactionId')) // linked via parent + $conditions[] = [DB::OR, ['id', $p], ['parentFactionId', $p]]; + else // self as parent + $conditions[] = ['parentFactionId', $this->typeId]; + + $spillover = new FactionList($conditions); + $this->extendGlobalData($spillover->getJSGlobals()); + + $buff = ''; + foreach ($spillover->iterate() as $spillId => $__) + if ($val = ($spillover->getField('spilloverRateIn') * $this->subject->getField('spilloverRateOut') * 100)) + $buff .= '[tr][td][faction='.$spillId.'][/td][td][span class=q'.($val > 0 ? '2]+' : '10]').$val.'%[/span][/td][td]'.Lang::game('rep', $spillover->getField('spilloverMaxRank')).'[/td][/tr]'; + + if ($buff) + $this->extraText = new Markup( + '[h3 class=clear]'.Lang::faction('spillover').'[/h3][div margin=15px]'.Lang::faction('spilloverDesc').'[/div][table class=grid width=400px][tr][td width=150px][b]'.Util::ucFirst(Lang::game('faction')).'[/b][/td][td width=100px][b]'.Lang::spell('_value').'[/b][/td][td width=150px][b]'.Lang::faction('maxStanding').'[/b][/td][/tr]'.$buff.'[/table]', + ['dbpage' => true, 'allow' => Markup::CLASS_ADMIN], + 'text-generic' + ); + + // 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` = %i', $this->typeId)) + { + $buff = ''; + foreach ($rates as $k => $v) + { + if ($v == 1) + continue; + + $head = match ($k) + { + 'quest_rate' => Lang::game('quests'), + 'quest_daily_rate' => Lang::game('quests').' ('.Lang::quest('daily').')', + 'quest_weekly_rate' => Lang::game('quests').' ('.Lang::quest('weekly').')', + 'quest_monthly_rate' => Lang::game('quests').' ('.Lang::quest('monthly').')', + 'quest_repeatable_rate' => Lang::game('quests').' ('.Lang::quest('repeatable').')', + 'creature_rate' => Lang::game('npcs'), + 'spell_rate' => Lang::game('spells') + }; + + $buff .= '[tr][td]'.$head.Lang::main('colon').'[/td][td width=35px align=right][span class=q'.($v < 1 ? '10]' : '2]+').intVal(($v - 1) * 100).'%[/span][/td][/tr]'; + } + + if ($buff && $this->extraText) + $this->extraText->append('[h3 class=clear]'.Lang::faction('customRewRate').'[/h3][table class=grid width=250px]'.$buff.'[/table]'); + else if ($buff) + $this->extraText = new Markup('[h3 class=clear]'.Lang::faction('customRewRate').'[/h3][table class=grid width=250px]'.$buff.'[/table]', ['dbpage' => true, 'allow' => Markup::CLASS_ADMIN], 'text-generic'); + } + + // factionchange-equivalent + 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) + { + $this->transfer = Lang::faction('_transfer', array( + $altFac->id, + $altFac->getField('name', true), + $pendant > 0 ? 'alliance' : 'horde', + $pendant > 0 ? Lang::game('si', SIDE_ALLIANCE) : Lang::game('si', SIDE_HORDE) + )); + } + } + + + /**************/ + /* Extra Tabs */ + /**************/ + + $this->lvTabs = new Tabs(['parent' => "\$\$WH.ge('tabs-generic')"], 'tabsRelated', true); + + // tab: items + $items = new ItemList(array(Listview::DEFAULT_SIZE, ['requiredFaction', $this->typeId]), ['calcTotal' => true]); + if (!$items->error) + { + $this->extendGlobalData($items->getJSGlobals(GLOBALINFO_SELF)); + + $tabData = array( + 'data' => $items->getListviewData(), + 'extraCols' => '$_', + 'sort' => ['standing', 'name'] + ); + + 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')); + } + + // tab: creatures with onKill reputation + // only if you can actually gain reputation by kills + if ($this->subject->getField('reputationIndex') != -1) + { + // 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` = %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() ?: [0], + $this->typeId, $spillover->getFoundIDs() ?: [0] + ); + + if ($cRep) + { + $killCreatures = new CreatureList(array(Listview::DEFAULT_SIZE, ['id', array_keys($cRep)]), ['calcTotal' => true]); + if (!$killCreatures->error) + { + $data = $killCreatures->getListviewData(); + foreach ($data as $id => &$d) + $d['reputation'] = $cRep[$id]; + + $tabData = array( + 'data' => $data, + 'extraCols' => '$_', + 'sort' => ['-reputation', 'name'] + ); + + 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')); + } + } + } + + // tab: members + if ($_ = $this->subject->getField('templateIds')) + { + $members = new CreatureList(array(Listview::DEFAULT_SIZE, ['faction', $_]), ['calcTotal' => true]); + if (!$members->error) + { + $tabData = array( + 'data' => $members->getListviewData(), + 'id' => 'member', + 'name' => '$LANG.tab_members' + ); + + 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)); + } + } + + // tab: objects + if ($_ = $this->subject->getField('templateIds')) + { + $objects = new GameObjectList(array(['faction', $_])); + if (!$objects->error) + { + $this->addDataLoader('zones'); + $this->lvTabs->addListviewTab(new Listview(['data' => $objects->getListviewData()], GameObjectList::$brickFile)); + } + } + + // tab: quests + $conditions = array( + 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) + { + $this->extendGlobalData($quests->getJSGlobals(GLOBALINFO_ANY)); + + $tabData = array( + 'data' => $quests->getListviewData($this->typeId), + 'extraCols' => '$_' + ); + + 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')); + } + + // tab: achievements + $conditions = array( + ['ac.type', ACHIEVEMENT_CRITERIA_TYPE_GAIN_REPUTATION], + ['ac.value1', $this->typeId] + ); + $acvs = new AchievementList($conditions); + if (!$acvs->error) + { + $this->extendGlobalData($acvs->getJSGlobals(GLOBALINFO_ANY)); + + $this->lvTabs->addListviewTab(new Listview(array( + 'data' => $acvs->getListviewData(), + 'id' => 'criteria-of', + 'name' => '$LANG.tab_criteriaof', + 'visibleCols' => ['category'] + ), AchievementList::$brickFile)); + } + + // tab: condition-for + $cnd = new Conditions(); + $cnd->getByCondition(Type::FACTION, $this->typeId)->prepare(); + if ($tab = $cnd->toListviewTab('condition-for', '$LANG.tab_condition_for')) + { + $this->extendGlobalData($cnd->getJsGlobals()); + $this->lvTabs->addDataTab(...$tab); + } + + parent::generate(); + } +} + +?> diff --git a/endpoints/factions/factions.php b/endpoints/factions/factions.php new file mode 100644 index 00000000..5e8dd308 --- /dev/null +++ b/endpoints/factions/factions.php @@ -0,0 +1,104 @@ + [469, 891, 67, 892, 169], + 980 => [936], + 1097 => [1037, 1052, 1117], + 0 => true + ); + + public function __construct(string $rawParam) + { + $this->getCategoryFromUrl($rawParam); + + parent::__construct($rawParam); + } + + protected function generate() : void + { + $this->h1 = Util::ucFirst(Lang::game('factions')); + + + /**************/ + /* Page Title */ + /**************/ + + array_unshift($this->title, $this->h1); + if ($this->category) + { + switch (count($this->category)) + { + case 1: + $t = Lang::faction('cat', $this->category[0]); + array_unshift($this->title, is_array($t) ? $t[0] : $t); + break; + case 2: + array_unshift($this->title, Lang::faction('cat', $this->category[0], $this->category[1])); + break; + } + } + + + /*************/ + /* Menu Path */ + /*************/ + + foreach ($this->category as $c) + $this->breadcrumb[] = $c; + + + /****************/ + /* Main Content */ + /****************/ + + $this->redButtons[BUTTON_WOWHEAD] = true; + + $conditions = [Listview::DEFAULT_SIZE]; + + if (!User::isInGroup(U_GROUP_EMPLOYEE)) // unlisted factions + $conditions[] = [['cuFlags', CUSTOM_EXCLUDE_FOR_LISTVIEW, '&'], 0]; + + if (isset($this->category[1])) + $conditions[] = ['parentFactionId', $this->category[1]]; + else if (isset($this->category[0])) + { + if ($this->category[0]) + $subs = DB::Aowow()->selectCol('SELECT `id` FROM ::factions WHERE `parentFactionId` = %i', $this->category[0]); + else + $subs = [0]; + + $conditions[] = [DB::OR, ['parentFactionId', $subs], ['id', $subs]]; + } + + $data = []; + $factions = new FactionList($conditions); + if (!$factions->error) + $data = $factions->getListviewData(); + + $this->lvTabs = new Tabs(['parent' => "\$\$WH.ge('tabs-generic')"]); + + $this->lvTabs->addListviewTab(new Listview(['data' => $data], FactionList::$brickFile)); + + parent::generate(); + } +} + +?> diff --git a/endpoints/faq/faq.php b/endpoints/faq/faq.php new file mode 100644 index 00000000..7946f354 --- /dev/null +++ b/endpoints/faq/faq.php @@ -0,0 +1,34 @@ +generateError(); + } + + protected function generate() : void + { + $this->h1 = Lang::main('moreTitles', $this->pageName); + + array_unshift($this->title, $this->h1); + + parent::generate(); + } +} + +?> diff --git a/endpoints/filter/filter.php b/endpoints/filter/filter.php new file mode 100644 index 00000000..9830385c --- /dev/null +++ b/endpoints/filter/filter.php @@ -0,0 +1,86 @@ +page = strtolower($page); + + if ($catg !== null) + { + // 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]; + + // so usually the page call is just the DBTypes file string with a plural 's' .. but then there are currencies + $fileStr = match ($this->page) + { + 'currencies' => 'currency', + default => substr($this->page, 0, -1) + }; + + // yes, the whole _POST! .. should the input fields be exposed and static so they can be evaluated via BaseResponse::initRequestData() ? + 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); + + if ($this->catg) + $url .= '='.implode('.', $this->catg); + + if ($x = $this->filter?->buildGETParam()) + $url .= '&filter='.$x; + + if ($this->filter->error) + $_SESSION['error']['fi'] = $this->filter::class; + + // do get request + $this->redirectTo = $url; + } +} + +?> diff --git a/endpoints/get-description/get-description.php b/endpoints/get-description/get-description.php new file mode 100644 index 00000000..139c6949 --- /dev/null +++ b/endpoints/get-description/get-description.php @@ -0,0 +1,35 @@ + ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkTextBlob']] + ); + + public function __construct(string $param) + { + if ($param) // should be empty + $this->generate404(); + + parent::__construct($param); + } + + protected function generate() : void + { + if (!User::canWriteGuide()) + return; + + $this->result = GuideMgr::createDescription($this->_post['description']); + } +} + +?> diff --git a/endpoints/go-to-comment/go-to-comment.php b/endpoints/go-to-comment/go-to-comment.php new file mode 100644 index 00000000..c99d55f4 --- /dev/null +++ b/endpoints/go-to-comment/go-to-comment.php @@ -0,0 +1,45 @@ + ['filter' => FILTER_VALIDATE_INT] + ); + + protected function generate() : void + { + if (!$this->assertGET('id')) + { + trigger_error('GotocommentBaseResponse - malformed request received', E_USER_ERROR); + return; + } + + // 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); + return; + } + + if (!Type::validateIds($comment['type'], $comment['typeId'])) + { + trigger_error('GotocommentBaseResponse - comment #'.$this->_get['id'].' belongs to nonexistent type/typeID combo '.$comment['type'].'/'.$comment['typeId'], E_USER_ERROR); + return; + } + + $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 new file mode 100644 index 00000000..e3cbbcc4 --- /dev/null +++ b/endpoints/go-to-reply/go-to-reply.php @@ -0,0 +1,42 @@ + ['filter' => FILTER_VALIDATE_INT] + ); + + protected function generate() : void + { + if (!$this->assertGET('id')) + { + trigger_error('GotoreplyBaseResponse - malformed request received', E_USER_ERROR); + return; + } + + // 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` = %i', $this->_get['id']); + if (!$reply) + { + trigger_error('GotoreplyBaseResponse - reply #'.$this->_get['id'].' not found', E_USER_ERROR); + return; + } + + if (!Type::validateIds($reply['type'], $reply['typeId'])) + { + trigger_error('GotoreplyBaseResponse - parent comment #'.$reply['id'].' belongs to nonexistent type/typeID combo '.$reply['type'].'/'.$reply['typeId'], E_USER_ERROR); + return; + } + + $this->redirectTo = sprintf('?%s=%d#comments:id=%d:reply=%d', Type::getFileString($reply['type']), $reply['typeId'], $reply['id'], $reply['reply']); + } +} + +?> diff --git a/endpoints/guide/changelog.php b/endpoints/guide/changelog.php new file mode 100644 index 00000000..eee823e7 --- /dev/null +++ b/endpoints/guide/changelog.php @@ -0,0 +1,105 @@ + ['filter' => FILTER_VALIDATE_INT] + ); + + protected function generate() : void + { + // main container should be tagged:
+ + if (!$this->assertGET('id')) + $this->generateNotFound(Lang::game('guide'), Lang::guide('notFound')); + + $guide = new GuideList(array(['id', $this->_get['id']])); + if ($guide->error) + $this->generateNotFound(Lang::game('guide'), Lang::guide('notFound')); + + if (!$guide->canBeViewed() && !$guide->userCanView()) + $this->forward('?guides='.$guide->getField('category')); + + $this->h1 = Lang::guide('clTitle', [$this->_get['id'], $guide->getField('title')]); + if (!$this->h1) + $this->h1 = $guide->getField('name'); + + $this->gPageInfo += ['name' => $guide->getField('name')]; + + + $this->breadcrumb[] = $guide->getField('category'); + + + parent::generate(); + + /* - NYI (see "&& false") + $this->addScript([SC_JS_STRING, + + <<= parseInt(e.value)); + }); + + }; + + radios.each(function (i, e) { + e.onchange = limit.bind(this, e.name, parseInt(e.value)); + + if (i < 2 && e.name == "b") // first pair + $(e).trigger("click"); + else if (e.value == 0 && e.name == "a") // last pair + $(e).trigger("click"); + }); + }); + JS + ]); + */ + + $buff = '
    '; + $inp = fn($rev) => User::isInGroup(U_GROUP_STAFF) && false ? ($rev !== null ? '' : '') : ''; + $now = new DateTime(); + + $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'])]).''.$now->formatDate($log['date'], true)."
  • \n"; + else if ($log['msg']) + $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']).''.$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').''.$now->formatDate($guide->getField('date'), true)."
  • \n
\n"; + + if (User::isInGroup(U_GROUP_STAFF) && false) + $buff .= ''; + + $this->extraHTML = $buff; + } +} + +?> diff --git a/endpoints/guide/edit.php b/endpoints/guide/edit.php new file mode 100644 index 00000000..be86d440 --- /dev/null +++ b/endpoints/guide/edit.php @@ -0,0 +1,214 @@ + span { display: block; height: 22px; } + #upload-result { display: inline-block; text-align: right; } + #upload-progress { display: inline-block; margin-right: 8px; } + + CSS] + ); + protected array $expectedPOST = array( + 'save' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkEmptySet'] ], // saved for more editing + 'submit' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkEmptySet'] ], // submitted for review + 'title' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkTextLine'] ], + 'name' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkTextLine'] ], + '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' => [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]] + ); + protected array $expectedGET = array( + 'id' => ['filter' => FILTER_VALIDATE_INT], + 'rev' => ['filter' => FILTER_VALIDATE_INT] + ); + + public function __construct(string $param) + { + parent::__construct($param); + + if (!User::canWriteGuide()) + $this->generateError(); + + if (!is_int($this->_get['id'])) // edit existing guide + return; + + $this->typeId = $this->_get['id']; // just to display sensible not-found msg + $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) + return; + + // 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` = %i AND `typeId` = %i ORDER BY `rev` DESC', Type::GUIDE, $this->typeId); + } + + protected function generate() : void + { + if ($this->_post['save'] || $this->_post['submit']) + { + if (!$this->saveGuide()) + $this->error = Lang::main('intError'); + else if ($this->_get['id'] === 0) + $this->forward('?guide=edit&id='.$this->typeId); + } + + $guide = new GuideList(array(['id', $this->typeId])); + + $this->h1 = Lang::guide('editTitle'); + array_unshift($this->title, $this->h1.Lang::main('colon').$guide->getField('title'), Lang::game('guides')); + + Lang::sort('guide', 'category'); + + // init required template vars + $this->editCategory = $this->_post['category'] ?? $guide->getField('category'); + $this->editTitle = $this->_post['title'] ?? $guide->getField('title'); + $this->editName = $this->_post['name'] ?? $guide->getField('name'); + $this->editDescription = $this->_post['description'] ?? $guide->getField('description'); + $this->editText = $this->_post['body'] ?? $guide->getArticle(); + $this->editClassId = $this->_post['classId'] ?? $guide->getField('classId'); + $this->editSpecId = $this->_post['specId'] ?? $guide->getField('specId'); + $this->editLocale = $this->_post['locale'] ?? Locale::tryFrom($guide->getField('locale')); + $this->editStatus = $this->editStatus ?: $guide->getField('status'); + $this->editStatusColor = GuideMgr::STATUS_COLORS[$this->editStatus]; + + $this->extendGlobalData($guide->getJSGlobals()); + + parent::generate(); + } + + private function saveGuide() : bool + { + // test requiered fields set + if (!$this->assertPOST('title', 'name', 'body', 'locale', 'category')) + { + trigger_error('GuideEditResponse::saveGuide - received malformed request', E_USER_ERROR); + return false; + } + + // test required fields context + if (!$this->_post['locale']->validate()) + return false; + + // sanitize: spec / class + if ($this->_post['category'] == 1) // Classes + { + if ($this->_post['classId'] && !ChrClass::tryFrom($this->_post['classId'])) + $this->_post['classId'] = 0; + + if ($this->_post['specId'] > -1 && !$this->_post['classId']) + $this->_post['specId'] = -1; + } + else + { + $this->_post['classId'] = 0; + $this->_post['specId'] = -1; + } + + $guideData = array( + 'category' => $this->_post['category'], + 'classId' => $this->_post['classId'], + 'specId' => $this->_post['specId'], + 'title' => $this->_post['title'], + 'name' => $this->_post['name'], + 'description' => $this->_post['description'] ?: GuideMgr::createDescription($this->_post['body']), + 'locale' => $this->_post['locale']->value, + 'roles' => User::$groups, + 'status' => $this->_post['submit'] ? GuideMgr::STATUS_REVIEW : GuideMgr::STATUS_DRAFT, + 'date' => time() + ); + + // new guide > reload editor + if ($this->_get['id'] === 0) + { + $guideData += ['userId' => User::$id]; + 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()->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); + return false; + } + + // insert Article + $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, + ++$this->editRev, + User::$groups & U_GROUP_STAFF ? User::$groups : User::$groups | U_GROUP_BLOGGER, + $this->_post['body'] + ); + + if (!is_int($articleId)) + { + if ($this->_get['id'] === 0) + 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()->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']; + + return true; + } + + protected static function checkDescription(string $str) : string + { + // run checkTextBlob and also replace \n => \s and \s+ => \s + $str = preg_replace(parent::PATTERN_TEXT_BLOB, '', $str); + + $str = strtr($str, ["\n" => ' ', "\r" => ' ']); + + return preg_replace('/\s+/', ' ', trim($str)); + } +} + +?> diff --git a/endpoints/guide/guide.php b/endpoints/guide/guide.php new file mode 100644 index 00000000..45e30c07 --- /dev/null +++ b/endpoints/guide/guide.php @@ -0,0 +1,250 @@ + ['filter' => FILTER_VALIDATE_INT], + 'rev' => ['filter' => FILTER_VALIDATE_INT] + ); + + public int $type = Type::GUIDE; + public int $typeId = 0; + public int $guideStatus = 0; + public array $guideRating = []; + public ?int $guideRevision = null; + + private GuideList $subject; + + public function __construct(string $nameOrId) + { + parent::__construct($nameOrId); + + /**********************/ + /* get mode + guideId */ + /**********************/ + + if (Util::checkNumeric($nameOrId, NUM_CAST_INT)) + $this->typeId = $nameOrId; + else if (preg_match(GuideMgr::VALID_URL, $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); + } + } + + $this->contribute = Type::getClassAttrib($this->type, 'contribute') ?? CONTRIBUTE_NONE; + } + + protected function generate() : void + { + $this->subject = new GuideList(array(['id', $this->typeId])); + if ($this->subject->error) + $this->generateNotFound(Lang::game('guide'), Lang::guide('notFound')); + + if (!$this->subject->canBeViewed() && !$this->subject->userCanView()) + $this->forward('?guides='.$this->subject->getField('category')); + + $this->guideStatus = $this->subject->getField('status'); + if ($this->guideStatus != GuideMgr::STATUS_APPROVED && $this->guideStatus != GuideMgr::STATUS_ARCHIVED) + { + $this->cacheType = CACHE_TYPE_NONE; + $this->contribute = CONTRIBUTE_NONE; + } + + // 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'); + + $this->h1 = $this->subject->getField('name'); + + $this->gPageInfo += array( + 'name' => $this->h1, + 'author' => $this->subject->getField('author') + ); + + + /*************/ + /* Menu Path */ + /*************/ + + if ($x = $this->subject?->getField('category')) + $this->breadcrumb[] = $x; + + + /**************/ + /* Page Title */ + /**************/ + + array_unshift($this->title, $this->subject->getField('title'), Lang::game('guides')); + + + /***********/ + /* Infobox */ + /***********/ + + if (!($this->subject->getField('cuFlags') & GUIDE_CU_NO_QUICKFACTS)) + $this->generateInfobox(); + + // needs post-cache updating + if (!($this->subject->getField('cuFlags') & GUIDE_CU_NO_RATING)) + $this->guideRating = array( + $this->subject->getField('rating'), // avg rating + User::canUpvote() && User::canDownvote() ? 'true' : 'false', + $this->subject->getField('_self'), // my rating amt; 0 = no vote + $this->typeId // guide Id + ); + + + /****************/ + /* Main Content */ + /****************/ + + if ($this->subject->userCanView()) + $this->redButtons[BUTTON_GUIDE_EDIT] = User::canWriteGuide() && $this->guideStatus != GuideMgr::STATUS_ARCHIVED; + + $this->redButtons[BUTTON_GUIDE_LOG] = true; + $this->redButtons[BUTTON_GUIDE_REPORT] = $this->subject->canBeReported(); + + $this->lvTabs = new Tabs(['parent' => "\$\$WH.ge('tabs-generic')"], __forceTabs: true); + + // the article text itself is added by TemplateResponse::addArticle() + parent::generate(); + + $this->result->registerDisplayHook('infobox', [self::class, 'infoboxHook']); + if ($this->guideRating) + $this->result->registerDisplayHook('guideRating', [self::class, 'starsHook']); + } + + private function generateInfobox() : void + { + $infobox = []; + + if ($this->subject->getField('cuFlags') & CC_FLAG_STICKY) + $infobox[] = '[span class=guide-sticky]'.Lang::guide('sticky').'[/span]'; + + $infobox[] = Lang::guide('author').'[url=?user='.$this->subject->getField('author').']'.$this->subject->getField('author').'[/url]'; + + if ($this->subject->getField('category') == 1) + { + $c = $this->subject->getField('classId'); + $s = $this->subject->getField('specId'); + if ($c > 0) + { + $this->extendGlobalIds(Type::CHR_CLASS, $c); + $infobox[] = Util::ucFirst(Lang::game('class')).Lang::main('colon').'[class='.$c.']'; + } + if ($s > -1) + $infobox[] = Lang::guide('spec').'[icon class="c'.$c.' icontiny" name='.Game::$specIconStrings[$c][$s].']'.Lang::game('classSpecs', $c, $s).'[/icon]'; + } + + // $infobox[] = Lang::guide('patch').Lang::main('colon').'3.3.5'; // replace with date + $infobox[] = Lang::guide('added').'[tooltip name=added]'.date('l, G:i:s', $this->subject->getField('date')).'[/tooltip][span class=tip tooltip=added]'.date(Lang::main('dateFmtShort'), $this->subject->getField('date')).'[/span]'; + + if ($this->guideStatus == GuideMgr::STATUS_ARCHIVED) + $infobox[] = Lang::guide('status', GuideMgr::STATUS_ARCHIVED); + + $this->infobox = new InfoboxMarkup($infobox, ['allow' => Markup::CLASS_STAFF, 'dbpage' => true], 'infobox-contents0'); + + if ($this->guideStatus == GuideMgr::STATUS_REVIEW && User::isInGroup(U_GROUP_STAFF) && $this->_get['rev']) + { + $this->addScript([SC_JS_STRING, <<infobox->append('[h3 style="text-align:center"]Admin[/h3]'); + $this->infobox->append('[div style="text-align:center"][url=# id="btn-accept" class=icon-tick]Approve[/url][url=# style="margin-left:20px" id="btn-reject" class=icon-delete]Reject[/url][/div]'); + } + } + + public static function infoboxHook(Template\PageTemplate &$pt, ?InfoboxMarkup &$infobox) : void + { + if ($pt->guideStatus != GuideMgr::STATUS_APPROVED) + return; + + // increment and display views + DB::Aowow()->qry('UPDATE ::guides SET `views` = `views` + 1 WHERE `id` = %i', $pt->typeId); + + $nViews = DB::Aowow()->selectCell('SELECT `views` FROM ::guides WHERE `id` = %i', $pt->typeId); + + $infobox->addItem(Lang::guide('views').'[n5='.$nViews.']'); + + // should we have a rating item in the lv? + if (!$pt->guideRating) + return; + + $rating = GuideMgr::getRatings([$pt->typeId]); + if ($rating[$pt->typeId]['nvotes'] < 5) + $infobox->addItem(Lang::guide('rating').Lang::guide('noVotes')); + else + $infobox->addItem(Lang::guide('rating').Lang::guide('votes', [round($rating[$pt->typeId]['rating'], 1), $rating[$pt->typeId]['nvotes']])); + } + + public static function starsHook(Template\PageTemplate &$pt, ?array &$guideRating) : void + { + if ($pt->guideStatus != GuideMgr::STATUS_APPROVED) + return; + + $rating = GuideMgr::getRatings([$pt->typeId]); + $guideRating = array( + $rating[$pt->typeId]['rating'], + User::canUpvote() && User::canDownvote() ? 'true' : 'false', + $rating[$pt->typeId]['_self'] ?? 0, + $pt->typeId + ); + } +} + +?> diff --git a/endpoints/guide/guide_power.php b/endpoints/guide/guide_power.php new file mode 100644 index 00000000..970bc302 --- /dev/null +++ b/endpoints/guide/guide_power.php @@ -0,0 +1,59 @@ + ['filter' => FILTER_CALLBACK, 'options' => [Locale::class, 'tryFromDomain']] + ); + + private string $url = ''; + + public function __construct(string $idOrName) + { + parent::__construct($idOrName); + + // temp locale + if ($this->_get['domain']) + Lang::load($this->_get['domain']); + + if (Util::checkNumeric($idOrName, NUM_CAST_INT)) + $this->typeId = $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); + } + } + + protected function generate() : void + { + $opts = []; + if ($this->typeId) + if (!($guide = new GuideList(array(['id', $this->typeId])))->error) + $opts = array( + 'name' => $guide->getField('name', true), + 'tooltip' => $guide->renderTooltip() + ); + + if (!$opts) + $this->cacheType = CACHE_TYPE_NONE; + + $this->result = new Tooltip(self::POWER_TEMPLATE, $this->url ?: $this->typeId, $opts); + } +} + +?> diff --git a/endpoints/guide/new.php b/endpoints/guide/new.php new file mode 100644 index 00000000..95de5c4d --- /dev/null +++ b/endpoints/guide/new.php @@ -0,0 +1,66 @@ + span { display: block; height: 22px; } + #upload-result { display: inline-block; text-align: right; } + #upload-progress { display: inline-block; margin-right: 8px; } + + CSS] + ); + + public function __construct(string $param) + { + parent::__construct($param); + + if (!User::canWriteGuide()) + $this->generateError(); + } + + protected function generate() : void + { + $this->h1 = Lang::guide('newTitle'); + + array_unshift($this->title, $this->h1, Lang::game('guides')); + + Lang::sort('guide', 'category'); + + // update required template vars + $this->editLocale = Lang::getLocale(); + + parent::generate(); + } +} + +?> diff --git a/endpoints/guide/vote.php b/endpoints/guide/vote.php new file mode 100644 index 00000000..ff82c837 --- /dev/null +++ b/endpoints/guide/vote.php @@ -0,0 +1,50 @@ + ['filter' => FILTER_VALIDATE_INT ], + 'rating' => ['filter' => FILTER_VALIDATE_INT, 'options' => ['min_range' => 0, 'max_range' => 5]] + ); + + protected function generate() : void + { + if (!$this->assertPOST('id', 'rating')) + { + trigger_error('GuideVoteResponse - malformed request received', E_USER_ERROR); + $this->generate404(); + } + + if (!User::canUpvote() || !User::canDownvote()) // same logic as comments? + $this->generate403(); + + // by id, not own, published + $points = $votes = 0; + 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()->qry('DELETE FROM ::user_ratings WHERE `type` = %i AND `entry` = %i AND `userId` = %i', RATING_GUIDE, $this->_post['id'], User::$id); + else + 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` = %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 new file mode 100644 index 00000000..23116f41 --- /dev/null +++ b/endpoints/guides/guides.php @@ -0,0 +1,73 @@ +getCategoryFromUrl($rawParam); + + parent::__construct($rawParam); + } + + protected function generate() : void + { + $this->h1 = Util::ucFirst(Lang::game('guides')); + + + if ($this->category) + $this->breadcrumb[] = $this->category[0]; + + + array_unshift($this->title, $this->h1); + if ($this->category) + array_unshift($this->title, Lang::guide('category', $this->category[0])); + + + $conditions = array( + ['locale', Lang::getLocale()->value], + ['status', GuideMgr::STATUS_ARCHIVED, '!'], // never archived guides + [ + DB::OR, + ['status', GuideMgr::STATUS_APPROVED], // currently approved + ['rev', 0, '>'] // has previously approved revision + ] + ); + if ($this->category) + $conditions[] = ['category', $this->category[0]]; + + $this->redButtons = [BUTTON_GUIDE_NEW => User::canWriteGuide()]; + + $guides = new GuideList($conditions); + + $this->lvTabs = new Tabs(['parent' => "\$\$WH.ge('tabs-generic')"]); + + $this->lvTabs->addListviewTab(new Listview(array( + 'data' => $guides->getListviewData(), + 'name' => Util::ucFirst(Lang::game('guides')), + 'hiddenCols' => ['patch'], // pointless: display date instead + 'extraCols' => ['$Listview.extraCols.date'] // ok + ), GuideList::$brickFile)); + + parent::generate(); + } +} + +?> diff --git a/endpoints/guild/guild.php b/endpoints/guild/guild.php new file mode 100644 index 00000000..9db0d252 --- /dev/null +++ b/endpoints/guild/guild.php @@ -0,0 +1,153 @@ + Profiler > Guilds + + protected array $dataLoader = ['realms', 'weight-presets']; + protected array $scripts = array( + [SC_JS_FILE, 'js/profile_all.js'], + [SC_JS_FILE, 'js/profile.js'], + [SC_CSS_FILE, 'css/Profiler.css'] + ); + + public int $type = Type::GUILD; + + public function __construct(string $idOrProfile) + { + parent::__construct($idOrProfile); + + if (!Cfg::get('PROFILER_ENABLE')) + $this->generateError(); + + if (!$idOrProfile) + $this->generateError(); + + $this->getSubjectFromUrl($idOrProfile); + + // we have an ID > ok + if ($this->typeId) + return; + + // param was incomplete profile > error + if (!$this->subjectName) + $this->generateError(); + + // 3 possibilities + // 1) already synced to aowow + 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['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) + $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['stub'] = 1; + $subject['nameUrl'] = Profiler::urlize($subject['name']); + + // create entry from realm with basic info + DB::Aowow()->qry('INSERT IGNORE INTO ::profiler_guild %v', $subject); + + $this->handleIncompleteData(Type::GUILD, $subject['realmGUID']); + return; + } + + // 3) does not exist at all + $this->notFound(); + } + + protected function generate() : void + { + if ($this->doResync) + { + parent::generate(); + return; + } + + $subject = new LocalGuildList(array(['id', $this->typeId])); + if ($subject->error) + $this->notFound(); + + // guild accessed by id + if (!$this->subjectName) + $this->forward($subject->getProfileUrl()); + + $this->h1 = Lang::profiler('guildRoster', [$subject->getField('name')]); + + + /*************/ + /* Menu Path */ + /*************/ + + $this->followBreadcrumbPath(); + + + /**************/ + /* Page Title */ + /**************/ + + array_unshift( + $this->title, + $subject->getField('name').' ('.$this->realm.' - '.Lang::profiler('regions', $this->region).')', + Util::ucFirst(Lang::profiler('profiler')) + ); + + + /****************/ + /* Main Content */ + /****************/ + + $this->redButtons[BUTTON_RESYNC] = [$this->typeId, 'guild']; + + // 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` = %i', $this->typeId)) + $this->extraHTML = ''; + + + /**************/ + /* Extra Tabs */ + /**************/ + + $this->lvTabs = new Tabs(['parent' => "\$\$WH.ge('tabs-generic')"], 'tabsRelated'); + + // tab: members + $member = new LocalProfileList(array(['p.guild', $this->typeId])); + $this->lvTabs->addListviewTab(new Listview(array( + 'data' => $member->getListviewData(PROFILEINFO_CHARACTER | PROFILEINFO_GUILD), + 'sort' => [-15], + 'visibleCols' => ['race', 'classs', 'level', 'talents', 'gearscore', 'achievementpoints', 'guildrank'], + 'hiddenCols' => ['guild', 'location'] + ), ProfileList::$brickFile)); + + parent::generate(); + } + + public function notFound() : never + { + parent::generateNotFound(Lang::game('guild'), Lang::profiler('notFound', 'guild')); + } + +} + +?> diff --git a/endpoints/guild/resync.php b/endpoints/guild/resync.php new file mode 100644 index 00000000..82df7f8f --- /dev/null +++ b/endpoints/guild/resync.php @@ -0,0 +1,48 @@ + ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkIdList'] ], + 'profile' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkEmptySet']] + ); + + public function __construct(string $rawParam) + { + parent::__construct($rawParam); + + if (!Cfg::get('PROFILER_ENABLE')) + $this->generate404(); + } + + /* params + id: + user: [optional, not used] + profile: [optional, also get related chars] + return: 1 + */ + protected function generate() : void + { + if (!$this->assertGET('id')) + return; + + 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()->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']); + + $this->result = 1; // as string? + } +} + +?> diff --git a/endpoints/guild/status.php b/endpoints/guild/status.php new file mode 100644 index 00000000..df311ee0 --- /dev/null +++ b/endpoints/guild/status.php @@ -0,0 +1,29 @@ + ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkIdList']] + ); + + public function __construct(string $rawParam) + { + parent::__construct($rawParam); + + if (!Cfg::get('PROFILER_ENABLE')) + $this->generate404(); + } + + protected function generate() : void + { + $this->result = Profiler::resyncStatus(Type::GUILD, $this->_get['id']); + } +} + +?> diff --git a/endpoints/guilds/guilds.php b/endpoints/guilds/guilds.php new file mode 100644 index 00000000..38f76557 --- /dev/null +++ b/endpoints/guilds/guilds.php @@ -0,0 +1,158 @@ + Profiler > Guilds + + protected array $dataLoader = ['realms']; + protected array $scripts = array( + [SC_JS_FILE, 'js/filters.js'], + [SC_JS_FILE, 'js/profile_all.js'], + [SC_JS_FILE, 'js/profile.js'] + ); + protected array $expectedGET = array( + 'filter' => ['filter' => FILTER_VALIDATE_REGEXP, 'options' => ['regexp' => Filter::PATTERN_PARAM]] + ); + + public int $type = Type::GUILD; + + private int $sumSubjects = 0; + + public function __construct(string $rawParam) + { + if (!Cfg::get('PROFILER_ENABLE')) + $this->generateError(); + + $this->getSubjectFromUrl($rawParam); + + parent::__construct($rawParam); + + $realms = []; + foreach (Profiler::getRealms() as $idx => $r) + { + if ($this->region && $r['region'] != $this->region) + continue; + + if ($this->realm && $r['name'] != $this->realm) + continue; + + $this->sumSubjects += DB::Characters($idx)->selectCell('SELECT count(*) FROM guild'); + $realms[] = $idx; + } + + 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; + } + + protected function generate() : void + { + $this->h1 = Lang::game('guilds'); + + + /*************/ + /* Menu Path */ + /*************/ + + $this->followBreadcrumbPath(); + + + /**************/ + /* Page Title */ + /**************/ + + if ($this->realm) + array_unshift($this->title, $this->realm,/* Cfg::get('BATTLEGROUP'),*/ Lang::profiler('regions', $this->region), Lang::game('guilds')); + else if ($this->region) + array_unshift($this->title, Lang::profiler('regions', $this->region), Lang::game('guilds')); + else + array_unshift($this->title, Lang::game('guilds')); + + + /****************/ + /* Main Content */ + /****************/ + + $conditions = array( + Listview::DEFAULT_SIZE, + ['c.deleteInfos_Account', null], + ['c.level', MAX_LEVEL, '<='], // prevents JS errors + [['c.extra_flags', Profiler::CHAR_GMFLAGS, '&'], 0] + ); + if ($_ = $this->filter->getConditions()) + $conditions[] = $_; + + $this->getRegions(); + + $tabData = array( + 'id' => 'guilds', + 'data' => [], + 'hideCount' => 1, + 'sort' => [-3], + 'visibleCols' => ['members', 'achievementpoints', 'gearscore'], + 'hiddenCols' => ['guild'] + ); + + if ($this->filter->values['si']) + $tabData['hiddenCols'][] = 'faction'; + + $miscParams = ['calcTotal' => true]; + if ($this->realm) + $miscParams['sv'] = $this->realm; + if ($this->region) + $miscParams['rg'] = $this->region; + + $guilds = new RemoteGuildList($conditions, $miscParams); + if (!$guilds->error) + { + $guilds->initializeLocalEntries(); + + $tabData['data'] = $guilds->getListviewData(); + + // create note if search limit was exceeded + 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() > Listview::DEFAULT_SIZE) + $tabData['note'] = sprintf(Util::$tryFilteringString, 'LANG.lvnote_guildsfound', $this->sumSubjects, 0); + } + + $this->lvTabs = new Tabs(['parent' => "\$\$WH.ge('tabs-generic')"], 'tabsRelated'); + + $this->lvTabs->addListviewTab(new Listview($tabData, GuildList::$brickFile, 'membersCol')); + + parent::generate(); + + $this->result->registerDisplayHook('filter', [self::class, 'filterFormHook']); + } + + public static function filterFormHook(Template\PageTemplate &$pt, GuildListFilter $filter) : void + { + // sort for dropdown-menus + Lang::sort('game', 'cl'); + Lang::sort('game', 'ra'); + } +} + +?> diff --git a/endpoints/help/help.php b/endpoints/help/help.php new file mode 100644 index 00000000..92e5f7f8 --- /dev/null +++ b/endpoints/help/help.php @@ -0,0 +1,45 @@ +generateError(); + + $pageId = array_search($rawParam, $this->validCats); + if ($pageId === false) + $this->generateError(); + + $this->catg = $rawParam; + $this->articleUrl = $this->pageName.'='.$rawParam; + } + + protected function generate() : void + { + $this->h1 = Lang::main('moreTitles', $this->pageName, $this->catg); + + array_unshift($this->title, $this->h1); + + parent::generate(); + } +} + +?> diff --git a/endpoints/home/home.php b/endpoints/home/home.php new file mode 100644 index 00000000..8ca0caba --- /dev/null +++ b/endpoints/home/home.php @@ -0,0 +1,75 @@ + element + 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')) + $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 %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); + + if ($box['altHomeLogo']) + $this->altHomeLogo = $box['altHomeLogo']; + + $this->featuredBox = array( + 'markup' => new Markup(new LocString($box, 'text'), ['allow' => Markup::CLASS_ADMIN], 'news-generic'), + 'extended' => $box['extraWide'], + 'boxBG' => $box['boxBG'] ?? Cfg::get('STATIC_URL').'/images/'.Lang::getLocale()->json().'/mainpage-bg-news.jpg', + 'overlays' => [] + ); + + if ($_ = $this->featuredBox['markup']->getJsGlobals()) + $this->extendGlobalData($_); + + // load overlay links + foreach (DB::Aowow()->selectAssoc('SELECT * FROM ::home_featuredbox_overlay WHERE `featureId` = %i', $box['id']) as $ovl) + { + $ovl = Util::defStatic((array)$ovl); + + $this->featuredBox['overlays'][] = array( + 'url' => $ovl['url'], + 'left' => $ovl['left'], + 'width' => $ovl['width'], + 'title' => new LocString($ovl, 'title') + ); + } + } + + parent::generate(); + } +} + +?> diff --git a/endpoints/icon/get-id-from-name.php b/endpoints/icon/get-id-from-name.php new file mode 100644 index 00000000..a74d9f62 --- /dev/null +++ b/endpoints/icon/get-id-from-name.php @@ -0,0 +1,29 @@ + ['filter' => FILTER_VALIDATE_REGEXP, 'options' => ['regexp' => '/^[\w_-]+$/']] + ); + + protected function generate() : void + { + if (!$this->assertGET('name')) + { + $this->result = 'null'; + return; + } + + $this->result = 0; + 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 new file mode 100644 index 00000000..b0620539 --- /dev/null +++ b/endpoints/icon/icon.php @@ -0,0 +1,158 @@ +typeId = intVal($id); + $this->contribute = Type::getClassAttrib($this->type, 'contribute') ?? CONTRIBUTE_NONE; + } + + protected function generate() : void + { + $this->subject = new IconList(array(['id', $this->typeId])); + if ($this->subject->error) + $this->generateNotFound(Lang::game('icon'), Lang::icon('notFound')); + + $this->extendGlobalData($this->subject->getJSGlobals()); + + $this->h1 = $this->subject->getField('name_source'); + $this->icon = $this->subject->getField('name', true, true); + + $this->gPageInfo += array( + 'type' => $this->type, + 'typeId' => $this->typeId, + 'name' => $this->h1 + ); + + + /*************/ + /* Menu Path */ + /*************/ + + $cats = [1 => 'nItems', 2 => 'nSpells', 3 => 'nAchievements', 6 => 'nCurrencies', 9 => 'nPets'/* , 11 => '' */]; + $crumb = ''; + foreach ($cats as $cat => $field) + { + if (!$this->subject->getField($field)) + continue; + + if ($crumb) + { + $crumb = 0; + break; + } + + $crumb = $cat; + } + + if ($crumb) + $this->breadcrumb[] = $crumb; + + + /**************/ + /* Page Title */ + /**************/ + + array_unshift($this->title, $this->h1, Util::ucFirst(Lang::game('icon'))); + + + /****************/ + /* Main Content */ + /****************/ + + $this->redButtons = array( + BUTTON_LINKS => ['type' => $this->type, 'typeId' => $this->typeId], + BUTTON_WOWHEAD => false + ); + + + /**************/ + /* Extra Tabs */ + /**************/ + + $this->lvTabs = new Tabs(['parent' => "\$\$WH.ge('tabs-generic')"], 'tabsRelated', true); + + // used by: spell + $ubSpells = new SpellList(array(['iconId', $this->typeId])); + if (!$ubSpells->error) + { + $this->extendGlobalData($ubSpells->getJsGlobals(GLOBALINFO_RELATED | GLOBALINFO_SELF)); + $this->lvTabs->addListviewTab(new Listview(array( + 'data' => $ubSpells->getListviewData(), + 'id' => 'used-by-spell' + ), SpellList::$brickFile)); + } + + // used by: item + $ubItems = new ItemList(array(['iconId', $this->typeId])); + if (!$ubItems->error) + { + $this->extendGlobalData($ubItems->getJsGlobals()); + $this->lvTabs->addListviewTab(new Listview(array( + 'data' => $ubItems->getListviewData(), + 'id' => 'used-by-item' + ), ItemList::$brickFile)); + } + + // used by: achievement + $ubAchievements = new AchievementList(array(['iconId', $this->typeId])); + if (!$ubAchievements->error) + { + $this->extendGlobalData($ubAchievements->getJsGlobals()); + $this->lvTabs->addListviewTab(new Listview(array( + 'data' => $ubAchievements->getListviewData(), + 'id' => 'used-by-achievement' + ), AchievementList::$brickFile)); + } + + // used by: currency + $ubCurrencies = new CurrencyList(array(['iconId', $this->typeId])); + if (!$ubCurrencies->error) + { + $this->extendGlobalData($ubCurrencies->getJsGlobals()); + $this->lvTabs->addListviewTab(new Listview(array( + 'data' => $ubCurrencies->getListviewData(), + 'id' => 'used-by-currency' + ), CurrencyList::$brickFile)); + } + + // used by: hunter pet + $ubPets = new PetList(array(['iconId', $this->typeId])); + if (!$ubPets->error) + { + $this->extendGlobalData($ubPets->getJsGlobals()); + $this->lvTabs->addListviewTab(new Listview(array( + 'data' => $ubPets->getListviewData(), + 'id' => 'used-by-pet' + ), PetList::$brickFile)); + } + + parent::generate(); + } +} + +?> diff --git a/endpoints/icons/icons.php b/endpoints/icons/icons.php new file mode 100644 index 00000000..75464fae --- /dev/null +++ b/endpoints/icons/icons.php @@ -0,0 +1,113 @@ + ['filter' => FILTER_VALIDATE_REGEXP, 'options' => ['regexp' => Filter::PATTERN_PARAM]] + ); + protected array $validCats = [0, 1, 2, 3]; + + public function __construct(string $rawParam) + { + $this->getCategoryFromUrl($rawParam); + + parent::__construct($rawParam); + + if ($this->category) + $this->subCat = '='.implode('.', $this->category); + + $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; + } + + protected function generate() : void + { + $this->h1 = Util::ucWords(Lang::game('icons')); + + $conditions = [600]; // LIMIT 600 - fits better onto the grid + if (!User::isInGroup(U_GROUP_EMPLOYEE)) + $conditions[] = [['cuFlags', CUSTOM_EXCLUDE_FOR_LISTVIEW, '&'], 0]; + + if ($_ = $this->filter->getConditions()) + $conditions[] = $_; + + + /**************/ + /* Page Title */ + /**************/ + + $title = $this->h1; + $setCr = $this->filter->getSetCriteria(1, 2, 3, 6, 9, 11); + if (count($setCr) == 1) + $title = match ($setCr[0]) + { + 1 => Util::ucFirst(Lang::game('item')), + 2 => Util::ucFirst(Lang::game('spell')), + 3 => Util::ucFirst(Lang::game('achievement')), + 6 => Util::ucFirst(Lang::game('currency')), + 9 => Util::ucFirst(Lang::game('pet')), + 11 => Util::ucFirst(Lang::game('class')), + } . ' ' . $this->h1; + + array_unshift($this->title, $title); + + + /*************/ + /* Menu Path */ + /*************/ + + if (count($setCr) == 1) + $this->breadcrumb[] = $setCr[0]; + + + /****************/ + /* Main Content */ + /****************/ + + $this->redButtons[BUTTON_WOWHEAD] = true; + if ($fiQuery = $this->filter->buildGETParam()) + $this->wowheadLink .= '&filter='.$fiQuery; + + $icons = new IconList($conditions, ['calcTotal' => true]); + + $tabData['data'] = $icons->getListviewData(); + $this->extendGlobalData($icons->getJSGlobals()); + + if ($icons->getMatches() > $conditions[0]) // LIMIT + { + $tabData['note'] = sprintf(Util::$tryFilteringEntityString, $icons->getMatches(), 'LANG.types[29][3]', $conditions[0]); + $tabData['_truncated'] = 1; + } + + $this->lvTabs = new Tabs(['parent' => "\$\$WH.ge('tabs-generic')"]); + + $this->lvTabs->addListviewTab(new Listview($tabData, IconList::$brickFile)); + + parent::generate(); + } +} + +?> diff --git a/endpoints/item/item.php b/endpoints/item/item.php new file mode 100644 index 00000000..87778919 --- /dev/null +++ b/endpoints/item/item.php @@ -0,0 +1,1096 @@ +typeId = intVal($id); + $this->contribute = Type::getClassAttrib($this->type, 'contribute') ?? CONTRIBUTE_NONE; + } + + protected function generate() : void + { + $this->subject = new ItemList(array(['i.id', $this->typeId])); + if ($this->subject->error) + $this->generateNotFound(Lang::game('item'), Lang::item('notFound')); + + $jsg = $this->subject->getJSGlobals(GLOBALINFO_EXTRA | GLOBALINFO_SELF, $extra); + $this->extendGlobalData($jsg, $extra); + + $this->h1 = Lang::unescapeUISequences($this->subject->getField('name', true), Lang::FMT_HTML); + + $this->gPageInfo += array( + 'type' => $this->type, + 'typeId' => $this->typeId, + 'name' => $this->h1 + ); + + $_flags = $this->subject->getField('flags'); + $_slot = $this->subject->getField('slot'); + $_class = $this->subject->getField('class'); + $_subClass = $this->subject->getField('subClass'); + $_bagFamily = $this->subject->getField('bagFamily'); + $_displayId = $this->subject->getField('displayId'); + $_ilvl = $this->subject->getField('itemLevel'); + + + /*************/ + /* Menu Path */ + /*************/ + + if ($path = $this->followBreadcrumbPath()) + array_push($this->breadcrumb, ...$path); + + + /**************/ + /* Page Title */ + /**************/ + + array_unshift($this->title, Lang::unescapeUISequences($this->subject->getField('name', true), Lang::FMT_RAW), Util::ucFirst(Lang::game('item'))); + + + /***********/ + /* Infobox */ + /***********/ + + $infobox = Lang::getInfoBoxForFlags($this->subject->getField('cuFlags')); + + // itemlevel + if ($_ilvl && in_array($_class, [ITEM_CLASS_ARMOR, ITEM_CLASS_WEAPON, ITEM_CLASS_AMMUNITION, ITEM_CLASS_GEM])) + $infobox[] = Lang::game('level').Lang::main('colon').$_ilvl; + + // account-wide + if ($_flags & ITEM_FLAG_ACCOUNTBOUND) + $infobox[] = Lang::item('accountWide'); + + // side + if ($si = $this->subject->json[$this->typeId]['side']) + $infobox[] = Lang::main('side') . match ($si) + { + SIDE_ALLIANCE => '[span class=icon-alliance]'.Lang::game('si', SIDE_ALLIANCE).'[/span]', + SIDE_HORDE => '[span class=icon-horde]'.Lang::game('si', SIDE_HORDE).'[/span]', + 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]'; + $this->extendGlobalIds(Type::ICON, $_); + } + + // consumable / not consumable + if (!$_slot) + { + $hasUse = false; + for ($i = 1; $i < 6; $i++) + { + if ($this->subject->getField('spellId'.$i) <= 0 || in_array($this->subject->getField('spellTrigger'.$i), [SPELL_TRIGGER_EQUIP, SPELL_TRIGGER_HIT])) + continue; + + $hasUse = true; + + if ($this->subject->getField('spellCharges'.$i) >= 0) + continue; + + $tt = '[tooltip=tooltip_consumedonuse]'.Lang::item('consumable').'[/tooltip]'; + break; + } + + if ($hasUse) + $infobox[] = $tt ?? '[tooltip=tooltip_notconsumedonuse]'.Lang::item('nonConsumable').'[/tooltip]'; + } + + // related holiday + if ($eId = $this->subject->getField('eventId')) + { + $this->extendGlobalIds(Type::WORLDEVENT, $eId); + $infobox[] = Lang::game('eventShort', ['[event='.$eId.']']); + } + + // tool + if ($tId = $this->subject->getField('totemCategory')) + 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->typeId])) + { + $vendors = $this->subject->getExtendedCost()[$this->typeId]; + $stack = $this->subject->getField('buyCount'); + $divisor = $stack; + $each = ''; + $handled = []; + $costList = []; + foreach ($vendors as $npcId => $entries) + { + foreach ($entries as $data) + { + $tokens = []; + $currency = []; + + if (!is_array($data)) + continue; + + foreach ($data as $c => $qty) + { + if (is_string($c)) + { + unset($data[$c]); // unset miscData to prevent having two vendors /w the same cost being cached, because of different stock or rating-requirements + continue; + } + + if ($c < 0) // currency items (and honor or arena) + { + if (is_float($qty / $stack)) + $divisor = 1; + + $currency[] = [-$c, $qty]; + $this->extendGlobalIds(Type::CURRENCY, -$c); + } + else if ($c > 0) // plain items (item1,count1,item2,count2,...) + { + if (is_float($qty / $stack)) + $divisor = 1; + + $tokens[] = [$c, $qty]; + $this->extendGlobalIds(Type::ITEM, $c); + } + } + + // display every cost-combination only once + $hash = md5(serialize($data)); + if (in_array($hash, $handled)) + continue; + + $handled[] = $hash; + + if (isset($data[0])) + { + if (is_float($data[0] / $stack)) + $divisor = 1; + + $cost = '[money='.($data[0] / $divisor); + } + else + $cost = '[money'; + + $stringify = fn(&$x) => $x = $x[0] . ',' . ($x[1] / $divisor); + + if ($tokens) + { + array_walk($tokens, $stringify); + $cost .= ' items='.implode(',', $tokens); + } + + if ($currency) + { + array_walk($currency, $stringify); + $cost .= ' currency='.implode(',', $currency); + } + + $cost .= ']'; + + $costList[] = $cost; + } + } + + if ($stack > 1 && $divisor > 1) + $each = '[color=q0] ('.Lang::item('each').')[/color]'; + else if ($stack > 1) + $each = '[color=q0] ('.$stack.')[/color]'; + + if (count($costList) == 1) + $infobox[] = Lang::item('cost').Lang::main('colon').$costList[0].$each; + else if (count($costList) > 1) + $infobox[] = Lang::item('cost').$each.Lang::main('colon').'[ul][li]'.implode('[/li][li]', $costList).'[/li][/ul]'; + + if ($_reqRating && $_reqRating[0]) + { + $text = str_replace('
', ' ', Lang::item('reqRating', $_reqRating[1], [$_reqRating[0]])); + $infobox[] = Lang::breakTextClean($text, 30, Lang::FMT_MARKUP); + } + } + + // repair cost + if ($_ = $this->subject->getField('repairPrice')) + $infobox[] = Lang::item('repairCost').'[money='.$_.']'; + + // avg auction buyout + if (in_array($this->subject->getField('bonding'), [0, 2, 3])) + if ($_ = Profiler::getBuyoutForItem($this->typeId)) + $infobox[] = '[tooltip=tooltip_buyoutprice]'.Lang::item('buyout.').'[/tooltip]'.Lang::main('colon').'[money='.$_.']'.$each; + + // avg money contained + if ($_flags & ITEM_FLAG_OPENABLE) + if ($_ = intVal(($this->subject->getField('minMoneyLoot') + $this->subject->getField('maxMoneyLoot')) / 2)) + $infobox[] = Lang::item('worth').'[tooltip=tooltip_avgmoneycontained][money='.$_.'][/tooltip]'; + + // if it goes into a slot it may be disenchanted + if ($_slot && $_class != ITEM_CLASS_CONTAINER) + { + if ($this->subject->getField('disenchantId')) + { + $_ = $this->subject->getField('requiredDisenchantSkill'); + if ($_ < 1) // these are some items, that never went live .. extremely rough emulation here + $_ = intVal($_ilvl / 7.5) * 25; + + $infobox[] = Lang::item('disenchantable').' ([tooltip=tooltip_reqenchanting]'.$_.'[/tooltip])'; + } + else + $infobox[] = Lang::item('cantDisenchant'); + } + + if (($_flags & ITEM_FLAG_MILLABLE) && $this->subject->getField('requiredSkill') == SKILL_INSCRIPTION) + { + $infobox[] = Lang::item('millable').' ([tooltip=tooltip_reqinscription]'.$this->subject->getField('requiredSkillRank').'[/tooltip])'; + $infobox[] = Lang::formatSkillBreakpoints(Game::getBreakpointsForSkill(SKILL_INSCRIPTION, $this->subject->getField('requiredSkillRank'))); + } + + if (($_flags & ITEM_FLAG_PROSPECTABLE) && $this->subject->getField('requiredSkill') == SKILL_JEWELCRAFTING) + { + $infobox[] = Lang::item('prospectable').' ([tooltip=tooltip_reqjewelcrafting]'.$this->subject->getField('requiredSkillRank').'[/tooltip])'; + $infobox[] = Lang::formatSkillBreakpoints(Game::getBreakpointsForSkill(SKILL_JEWELCRAFTING, $this->subject->getField('requiredSkillRank'))); + } + + if ($_flags & ITEM_FLAG_DEPRECATED) + $infobox[] = '[tooltip=tooltip_deprecated]'.Lang::item('deprecated').'[/tooltip]'; + + if ($_flags & ITEM_FLAG_NO_EQUIPCD) + $infobox[] = '[tooltip=tooltip_noequipcooldown]'.Lang::item('noEquipCD').'[/tooltip]'; + + if ($_flags & ITEM_FLAG_PARTYLOOT) + $infobox[] = '[tooltip=tooltip_partyloot]'.Lang::item('partyLoot').'[/tooltip]'; + + if ($_flags & ITEM_FLAG_REFUNDABLE) + $infobox[] = '[tooltip=tooltip_refundable]'.Lang::item('refundable').'[/tooltip]'; + + if ($_flags & ITEM_FLAG_SMARTLOOT) + $infobox[] = '[tooltip=tooltip_smartloot]'.Lang::item('smartLoot').'[/tooltip]'; + + if ($_flags & ITEM_FLAG_INDESTRUCTIBLE) + $infobox[] = Lang::item('indestructible'); + + if ($_flags & ITEM_FLAG_USABLE_ARENA) + $infobox[] = Lang::item('useInArena'); + + if ($_flags & ITEM_FLAG_USABLE_SHAPED) + $infobox[] = Lang::item('useInShape'); + + // cant roll need + if ($this->subject->getField('flagsExtra') & 0x0100) + $infobox[] = '[tooltip=tooltip_cannotrollneed]'.Lang::item('noNeedRoll').'[/tooltip]'; + + // fits into keyring + 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', $hasCompletion); + + + /****************/ + /* Main Content */ + /****************/ + + if ($canBeWeighted = in_array($_class, [ITEM_CLASS_WEAPON, ITEM_CLASS_ARMOR, ITEM_CLASS_GEM])) + $this->addDataLoader('weight-presets'); + + // pageText + if ($this->book = Game::getBook($this->subject->getField('pageTextId'))) + $this->addScript( + [SC_JS_FILE, 'js/Book.js'], + [SC_CSS_FILE, 'css/Book.css'] + ); + + $this->tooltip = [$this->subject->getField('iconString'), $this->subject->getField('stackable'), false]; + $this->redButtons = array( + BUTTON_WOWHEAD => true, + BUTTON_VIEW3D => $this->subject->isDisplayable() ? ['displayId' => $_displayId, 'slot' => $_slot, 'type' => Type::ITEM, 'typeId' => $this->typeId] : false, + BUTTON_COMPARE => $canBeWeighted, + BUTTON_EQUIP => in_array($_class, [ITEM_CLASS_WEAPON, ITEM_CLASS_ARMOR]), + BUTTON_UPGRADE => $canBeWeighted ? ['class' => $_class, 'slot' => $_slot] : false, + BUTTON_LINKS => array( + 'linkColor' => 'ff'.Game::$rarityColorStings[$this->subject->getField('quality')], + 'linkId' => 'item:'.$this->typeId.':0:0:0:0:0:0:0:0', + 'linkName' => Lang::unescapeUISequences($this->subject->getField('name', true), Lang::FMT_RAW), + 'type' => $this->type, + 'typeId' => $this->typeId + ) + ); + + // availablility + $this->unavailable = !!($this->subject->getField('cuFlags') & CUSTOM_UNAVAILABLE); + + // subItems + $this->subject->initSubItems(); + if (!empty($this->subject->subItems[$this->typeId])) + { + uaSort($this->subject->subItems[$this->typeId], fn($a, $b) => $a['name'] <=> $b['name']); + $this->subItems = array( + 'data' => array_values($this->subject->subItems[$this->typeId]), + 'randIds' => array_keys($this->subject->subItems[$this->typeId]), + 'quality' => $this->subject->getField('quality') + ); + + // 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)) + { + for ($i = 1; $i < count($this->subItems['data']); $i++) + { + $prev = &$this->subItems['data'][$i - 1]; + $cur = &$this->subItems['data'][$i]; + if ($prev['jsonequip'] == $cur['jsonequip'] && $prev['name'] == $cur['name']) + { + $prev['chance'] += $cur['chance']; + array_splice($this->subItems['data'], $i, 1); + array_splice($this->subItems['randIds'], $i, 1); + $i = 1; + } + } + } + } + + // factionchange-equivalent + 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) + { + $this->transfer = Lang::item('_transfer', [ + $altItem->id, + $altItem->getField('quality'), + $altItem->getField('iconString'), + $altItem->getField('name', true), + $pendant > 0 ? 'alliance' : 'horde', + $pendant > 0 ? Lang::game('si', SIDE_ALLIANCE) : Lang::game('si', SIDE_HORDE) + ]); + } + } + + + /**************/ + /* Extra Tabs */ + /**************/ + + $this->lvTabs = new Tabs(['parent' => "\$\$WH.ge('tabs-generic')"], 'tabsRelated', true); + + // tab: createdBy (perfect item specific) + 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) + { + $lvData = $perfSpells->getListviewData(); + $this->extendGlobalData($perfSpells->getJSGlobals(GLOBALINFO_RELATED)); + + foreach ($lvData as $sId => &$data) + { + $data['percent'] = $perfItem[$sId]['perfectCreateChance']; + if (Conditions::extendListviewRow($data, Conditions::SRC_NONE, $this->typeId, [Conditions::SPELL, $perfItem[$sId]['requiredSpecialization']])) + $this->extendGlobalIDs(Type::SPELL, $perfItem[$sId]['requiredSpecialization']); + } + + $this->lvTabs->addListviewTab(new Listview(array( + 'data' => $lvData, + 'name' => '$LANG.tab_createdby', + 'id' => 'created-by', // should by exclusive with created-by from spell_loot + 'extraCols' => ['$Listview.extraCols.percent', '$Listview.extraCols.condition'] + ), SpellList::$brickFile)); + } + } + + // tabs: this item is contained in.. + $lootTabs = new LootByItem($this->typeId); + $createdBy = []; + if ($lootTabs->getByItem()) + { + $this->extendGlobalData($lootTabs->jsGlobals); + + foreach ($lootTabs->iterate() as $idx => [$template, $tabData]) + { + if (!$tabData['data']) + continue; + + if ($idx == LootByItem::SPELL_CREATED) + $createdBy = array_column($tabData['data'], 'id'); + + if ($idx == LootByItem::ITEM_DISENCHANTED) + $tabData['note'] = sprintf(Util::$filterResultString, '?items&filter=cr=163;crs='.$this->typeId.';crv=0'); + + if ($idx == LootByItem::NPC_DROPPED && $this->subject->getSources($s, $sm) && $s[0] == SRC_DROP && isset($sm[0]['dd'])) + $tabData['note'] = match($sm[0]['dd']) + { + -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); + } + } + } + + if ($template == 'npc' || $template == 'object') + $this->addDataLoader('zones'); + + $this->lvTabs->addListviewTab(new Listview($tabData, $template)); + } + } + + // tabs: this item contains.. + $sourceFor = array( + [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 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, + 'computeDataFunc' => '$Listview.funcBox.initLootTable' + ); + + if ($extraCols) + $tabData['extraCols'] = array_values(array_unique($extraCols)); + + if ($hiddenCols) + $tabData['hiddenCols'] = array_unique($hiddenCols); + + $this->lvTabs->addListviewTab(new Listview($tabData, ItemList::$brickFile)); + } + } + + // 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, '<'])); + if (!$contains->error) + { + $this->extendGlobalData($contains->getJSGlobals(GLOBALINFO_SELF)); + + $hCols = ['side']; + if (!$contains->hasSetFields('slot')) + $hCols[] = 'slot'; + + $this->lvTabs->addListviewTab(new Listview(array( + 'data' => $contains->getListviewData(), + 'name' => '$LANG.tab_cancontain', + 'id' => 'can-contain', + 'hiddenCols' => $hCols + ), ItemList::$brickFile)); + } + } + + // tab: can be contained in (except keys) + else if ($_bagFamily != 0x0100) + { + $contains = new ItemList(array(['bagFamily', $_bagFamily, '&'], ['slots', 0, '>'])); + if (!$contains->error) + { + $this->extendGlobalData($contains->getJSGlobals(GLOBALINFO_SELF)); + + $this->lvTabs->addListviewTab(new Listview(array( + 'data' => $contains->getListviewData(), + 'name' => '$LANG.tab_canbeplacedin', + 'id' => 'can-be-placed-in', + 'hiddenCols' => ['side'] + ), ItemList::$brickFile)); + } + } + + // tab: criteria of + $conditions = array( + ['ac.type', [ACHIEVEMENT_CRITERIA_TYPE_OWN_ITEM, ACHIEVEMENT_CRITERIA_TYPE_USE_ITEM, ACHIEVEMENT_CRITERIA_TYPE_LOOT_ITEM, ACHIEVEMENT_CRITERIA_TYPE_EQUIP_ITEM]], + ['ac.value1', $this->typeId] + ); + + $criteriaOf = new AchievementList($conditions); + if (!$criteriaOf->error) + { + $this->extendGlobalData($criteriaOf->getJSGlobals(GLOBALINFO_SELF | GLOBALINFO_REWARDS)); + + $tabData = array( + 'data' => $criteriaOf->getListviewData(), + 'name' => '$LANG.tab_criteriaof', + 'id' => 'criteria-of', + 'visibleCols' => ['category'] + ); + + if (!$criteriaOf->hasSetFields('reward_loc0')) + $tabData['hiddenCols'] = ['rewards']; + + $this->lvTabs->addListviewTab(new Listview($tabData, AchievementList::$brickFile)); + } + + // tab: reagent for + $conditions = array( + 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] + ); + + $reagent = new SpellList($conditions); + if (!$reagent->error) + { + $this->extendGlobalData($reagent->getJSGlobals(GLOBALINFO_SELF | GLOBALINFO_RELATED)); + + $this->lvTabs->addListviewTab(new Listview(array( + 'data' => $reagent->getListviewData(), + 'name' => '$LANG.tab_reagentfor', + 'id' => 'reagent-for', + 'visibleCols' => ['reagents'] + ), SpellList::$brickFile)); + } + + // 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_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) + { + // 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', + ), GameObjectList::$brickFile)); + } + + // items (generally unused. It's the spell on the item, that unlocks stuff) + $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)); + } + } + + // tab: starts (quest) + if ($qId = $this->subject->getField('startQuest')) + { + $starts = new QuestList(array(['id', $qId])); + if (!$starts->error) + { + $this->extendGlobalData($starts->getJSGlobals(GLOBALINFO_SELF | GLOBALINFO_REWARDS)); + + $this->lvTabs->addListviewTab(new Listview(array( + 'data' => $starts->getListviewData(), + 'name' => '$LANG.tab_starts', + 'id' => 'starts-quest' + ), QuestList::$brickFile)); + } + } + + // tab: objective of (quest) + $conditions = array( + DB::OR, + ['reqItemId1', $this->typeId], ['reqItemId2', $this->typeId], ['reqItemId3', $this->typeId], + ['reqItemId4', $this->typeId], ['reqItemId5', $this->typeId], ['reqItemId6', $this->typeId] + ); + $objective = new QuestList($conditions); + if (!$objective->error) + { + $this->extendGlobalData($objective->getJSGlobals(GLOBALINFO_SELF | GLOBALINFO_REWARDS)); + + $this->lvTabs->addListviewTab(new Listview(array( + 'data' => $objective->getListviewData(), + 'name' => '$LANG.tab_objectiveof', + 'id' => 'objective-of-quest' + ), QuestList::$brickFile)); + } + + // tab: provided for (quest) + $conditions = array( + DB::OR, ['sourceItemId', $this->typeId], + ['reqSourceItemId1', $this->typeId], ['reqSourceItemId2', $this->typeId], + ['reqSourceItemId3', $this->typeId], ['reqSourceItemId4', $this->typeId] + ); + $provided = new QuestList($conditions); + if (!$provided->error) + { + $this->extendGlobalData($provided->getJSGlobals(GLOBALINFO_SELF | GLOBALINFO_REWARDS)); + + $this->lvTabs->addListviewTab(new Listview(array( + 'data' => $provided->getListviewData(), + 'name' => '$LANG.tab_providedfor', + 'id' => 'provided-for-quest' + ), QuestList::$brickFile)); + } + + // tab: sold by + if (!empty($this->subject->getExtendedCost()[$this->typeId])) + { + $vendors = $this->subject->getExtendedCost()[$this->typeId]; + $soldBy = new CreatureList(array(['id', array_keys($vendors)])); + if (!$soldBy->error) + { + // show mapper for vendors + 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 + Lang::item('vendorLoc') // title + ); + foreach ($vendorSpawns as $areaId => $_) + $this->map[3][$areaId] = ZoneList::getName($areaId); + } + + $sbData = $soldBy->getListviewData(); + $this->extendGlobalData($soldBy->getJSGlobals(GLOBALINFO_SELF)); + + $extraCols = ['$Listview.extraCols.stock', "\$Listview.funcBox.createSimpleCol('stack', 'stack', '10%', 'stack')", '$Listview.extraCols.cost']; + + $cnd = new Conditions(); + $cnd->getBySource(Conditions::SRC_NPC_VENDOR, entry: $this->typeId)->prepare(); + foreach ($sbData as $k => &$row) + { + $currency = []; + $tokens = []; + + // note: can only display one entry per row, so only use first entry of each vendor + foreach ($vendors[$k][0] as $id => $qty) + { + if (is_string($id)) + continue; + + if ($id > 0) + $tokens[] = [$id, $qty]; + else if ($id < 0) + $currency[] = [-$id, $qty]; + } + + $row['stock'] = $vendors[$k][0]['stock']; + $row['cost'] = [empty($vendors[$k][0][0]) ? 0 : $vendors[$k][0][0]]; + + if ($e = $vendors[$k][0]['event']) + $cnd->addExternalCondition(Conditions::SRC_NONE, $k.':'.$this->typeId, [Conditions::ACTIVE_EVENT, $e]); + + if ($currency || $tokens) // fill idx:3 if required + $row['cost'][] = $currency; + + if ($tokens) + $row['cost'][] = $tokens; + + if ($x = $this->subject->getField('buyPrice')) + $row['buyprice'] = $x; + + if ($x = $this->subject->getField('sellPrice')) + $row['sellprice'] = $x; + + if ($x = $this->subject->getField('buyCount')) + $row['stack'] = $x; + } + + if ($cnd->toListviewColumn($sbData, $extraCols, 'id', $this->typeId)) + $this->extendGlobalData($cnd->getJsGlobals()); + + $this->addDataLoader('zones'); + $this->lvTabs->addListviewTab(new Listview(array( + 'data' => $sbData, + 'name' => '$LANG.tab_soldby', + 'id' => 'sold-by-npc', + 'extraCols' => $extraCols, + 'hiddenCols' => ['level', 'type'] + ), CreatureList::$brickFile)); + } + } + + // tab: currency for + // some minor trickery: get arenaPoints(43307) and honorPoints(43308) directly + $n = $w = null; + if ($this->typeId == 43307) + { + $n = '?items&filter=cr=145;crs=1;crv=0'; + $w = '`reqArenaPoints` > 0'; + } + else if ($this->typeId == 43308) + { + $n = '?items&filter=cr=144;crs=1;crv=0'; + $w = '`reqHonorPoints` > 0'; + } + else + $w = '`reqItemId1` = '.$this->typeId.' OR `reqItemId2` = '.$this->typeId.' OR `reqItemId3` = '.$this->typeId.' OR `reqItemId4` = '.$this->typeId.' OR `reqItemId5` = '.$this->typeId; + + 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 %in UNION SELECT `item` FROM game_event_npc_vendor WHERE `extendedCost` IN %in', $xCosts, $xCosts) : null; + if ($boughtBy) + { + $boughtBy = new ItemList(array(['id', $boughtBy])); + if (!$boughtBy->error) + { + $iCur = new CurrencyList(array(['itemId', $this->typeId])); + $filter = $iCur->error ? [Type::ITEM => $this->typeId] : [Type::CURRENCY => $iCur->id]; + + $tabData = array( + 'data' => $boughtBy->getListviewData(ITEMINFO_VENDOR, $filter), + 'name' => '$LANG.tab_currencyfor', + 'id' => 'currency-for', + 'extraCols' => ["\$Listview.funcBox.createSimpleCol('stack', 'stack', '10%', 'stack')", '$Listview.extraCols.cost'] + ); + + if ($n) + $tabData['note'] = sprintf(Util::$filterResultString, $n); + + $this->lvTabs->addListviewTab(new Listview($tabData, ItemList::$brickFile)); + + $this->extendGlobalData($boughtBy->getJSGlobals(GLOBALINFO_SELF | GLOBALINFO_RELATED)); + } + } + + // tab: teaches + $ids = $indirect = []; + for ($i = 1; $i < 6; $i++) + { + if ($this->subject->getField('spellTrigger'.$i) == SPELL_TRIGGER_LEARN) + $ids[] = $this->subject->getField('spellId'.$i); + else if ($this->subject->getField('spellTrigger'.$i) == SPELL_TRIGGER_USE && $this->subject->getField('spellId'.$i) > 0) + $indirect[] = $this->subject->getField('spellId'.$i); + } + + // taught indirectly + if ($indirect) + { + $indirectSpells = new SpellList(array(['id', $indirect])); + foreach ($indirectSpells->iterate() as $__) + if ($_ = $indirectSpells->canTeachSpell()) + foreach ($_ as $idx) + $ids[] = $indirectSpells->getField('effect'.$idx.'TriggerSpell'); + + $ids = array_merge($ids, Game::getTaughtSpells($indirect)); + } + + if ($ids) + { + $taughtSpells = new SpellList(array(['id', $ids])); + if (!$taughtSpells->error) + { + $this->extendGlobalData($taughtSpells->getJSGlobals(GLOBALINFO_SELF | GLOBALINFO_RELATED)); + + $visCols = ['level', 'schools']; + if ($taughtSpells->hasSetFields('reagent1', 'reagent2', 'reagent3', 'reagent4', 'reagent5', 'reagent6', 'reagent7', 'reagent8')) + $visCols[] = 'reagents'; + + $this->lvTabs->addListviewTab(new Listview(array( + 'data' => $taughtSpells->getListviewData(), + 'name' => '$LANG.tab_teaches', + 'id' => 'teaches', + 'visibleCols' => $visCols + ), SpellList::$brickFile)); + } + } + + // 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 = []; + for ($i = 1; $i < 6; $i++) + { + // as defined on item + if ($this->subject->getField('spellId'.$i) > 0 && $this->subject->getField('spellCategory'.$i) > 0) + $cdCats[] = $this->subject->getField('spellCategory'.$i); + + // as defined in spell + if ($this->subject->getField('spellId'.$i) > 0) + $useSpells[] = $this->subject->getField('spellId'.$i); + } + if ($useSpells) + if ($_ = DB::Aowow()->selectCol('SELECT `category` FROM ::spell WHERE `id` IN %in AND `recoveryCategory` > 0', $useSpells)) + $cdCats += $_; + + if ($cdCats) + { + $conditions = array( + ['id', $this->typeId, '!'], + [ + DB::OR, + ['spellCategory1', $cdCats], + ['spellCategory2', $cdCats], + ['spellCategory3', $cdCats], + ['spellCategory4', $cdCats], + ['spellCategory5', $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]; + + $cdItems = new ItemList($conditions); + if (!$cdItems->error) + { + $this->lvTabs->addListviewTab(new Listview(array( + 'data' => $cdItems->getListviewData(), + 'name' => '$LANG.tab_sharedcooldown', + 'id' => 'shared-cooldown' + ), ItemList::$brickFile)); + + $this->extendGlobalData($cdItems->getJSGlobals(GLOBALINFO_SELF)); + } + } + + // tab: sounds + $soundIds = []; + if ($_class == ITEM_CLASS_WEAPON) + { + $scm = (1 << $_subClass); + if ($this->subject->getField('soundOverrideSubclass') > 0) + $scm = (1 << $this->subject->getField('soundOverrideSubclass')); + + $soundIds = DB::Aowow()->selectCol('SELECT `soundId` FROM ::items_sounds WHERE `subClassMask` & %i', $scm); + } + + $fields = ['pickUpSoundId', 'dropDownSoundId', 'sheatheSoundId', 'unsheatheSoundId']; + foreach ($fields as $f) + if ($x = $this->subject->getField($f)) + $soundIds[] = $x; + + if ($x = $this->subject->getField('spellVisualId')) + { + if ($spellSounds = DB::Aowow()->selectRow('SELECT * FROM ::spell_sounds WHERE `id` = %i', $x)) + { + array_shift($spellSounds); // bye 'id'-field + foreach ($spellSounds as $ss) + if ($ss) + $soundIds[] = $ss; + } + } + + if ($soundIds) + { + $sounds = new SoundList(array(['id', $soundIds])); + if (!$sounds->error) + { + $this->extendGlobalData($sounds->getJSGlobals(GLOBALINFO_SELF)); + $this->lvTabs->addListviewTab(new Listview(['data' => $sounds->getListviewData()], SoundList::$brickFile)); + } + } + + // tab: condition-for + $cnd = new Conditions(); + $cnd->getByCondition(Type::ITEM, $this->typeId)->prepare(); + if ($tab = $cnd->toListviewTab('condition-for', '$LANG.tab_condition_for')) + { + $this->extendGlobalData($cnd->getJsGlobals()); + $this->lvTabs->addDataTab(...$tab); + } + + + // // todo - tab: taught by + // use var $createdBy to find source of this spell + // id: 'taught-by-X', + // name: LANG.tab_taughtby + + parent::generate(); + } + + private function followBreadcrumbPath() : array + { + $c = $this->subject->getField('class'); + $sc = $this->subject->getField('subClass'); + $ssc = $this->subject->getField('subSubClass'); + $slot = $this->subject->getField('slot'); + + if ($c == ITEM_CLASS_REAGENT) + return [ITEM_CLASS_MISC, 1]; // misc > reagents + + if ($c == ITEM_CLASS_GENERIC || $c == ITEM_CLASS_PERMANENT) + return [ITEM_CLASS_MISC, 4]; // misc > other + + // depths: 1 + $path = [$c]; + + if (in_array($c, [ITEM_CLASS_MONEY, ITEM_CLASS_QUEST, ITEM_CLASS_KEY])) + return $path; + + // depths: 2 + $path[] = $sc; + + // maybe depths: 3 + if ($this->subject->isBodyArmor() && $slot) + $path[] = $slot; + else if (($c == ITEM_CLASS_CONSUMABLE && $sc == ITEM_SUBCLASS_ELIXIR) || $c == ITEM_CLASS_GLYPH) + $path[] = $ssc; + + return $path; + } +} + +?> diff --git a/endpoints/item/item_power.php b/endpoints/item/item_power.php new file mode 100644 index 00000000..25617b47 --- /dev/null +++ b/endpoints/item/item_power.php @@ -0,0 +1,70 @@ + ['filter' => FILTER_CALLBACK, 'options' => [Locale::class, 'tryFromDomain']], + 'rand' => ['filter' => FILTER_VALIDATE_INT ], + 'ench' => ['filter' => FILTER_VALIDATE_INT ], + 'gems' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkIntArray'] ], + 'sock' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkEmptySet'] ] + ); + + public function __construct($param) + { + parent::__construct($param); + + // temp locale + if ($this->_get['domain']) + Lang::load($this->_get['domain']); + + $this->typeId = intVal($param); + + if ($this->_get['rand']) + $this->enhancedTT['r'] = $this->_get['rand']; + if ($this->_get['ench']) + $this->enhancedTT['e'] = $this->_get['ench']; + if ($this->_get['gems']) + $this->enhancedTT['g'] = $this->_get['gems']; + if ($this->_get['sock']) + $this->enhancedTT['s'] = ''; + } + + protected function generate() : void + { + $item = new ItemList(array(['i.id', $this->typeId])); + if ($item->error) + $this->cacheType = CACHE_TYPE_NONE; + else + { + $itemString = $this->typeId; + foreach ($this->enhancedTT as $k => $val) + $itemString .= $k.(is_array($val) ? implode(',', $val) : $val); + + $opts = array( + 'name' => Lang::unescapeUISequences($item->getField('name', true, false, $this->enhancedTT), Lang::FMT_RAW), + 'tooltip' => $item->renderTooltip(enhance: $this->enhancedTT), + 'icon' => $item->getField('iconString'), + 'quality' => $item->getField('quality') + ); + } + + $this->result = new Tooltip(self::POWER_TEMPLATE, $itemString ?? $this->typeId, $opts ?? []); + } +} + +?> diff --git a/endpoints/item/item_xml.php b/endpoints/item/item_xml.php new file mode 100644 index 00000000..19997124 --- /dev/null +++ b/endpoints/item/item_xml.php @@ -0,0 +1,230 @@ + ['filter' => FILTER_CALLBACK, 'options' => [Locale::class, 'tryFromDomain']] + ); + + private ItemList $subject; + private string $search = ''; + + public function __construct(string $param) + { + parent::__construct($param); + + // temp locale + if ($this->_get['domain']) + Lang::load($this->_get['domain']); + + // allow lookup by name + if (is_numeric($param)) + $this->typeId = intVal($param); + else + $this->search = urldecode($param); + } + + protected function generate() : void + { + if ($this->search) + $conditions = [['name_loc'.Lang::getLocale()->value, $this->search]]; + else + $conditions = [['i.id', $this->typeId]]; + + $this->subject = new ItemList($conditions); + if ($this->subject->error) + { + $this->cacheType = CACHE_TYPE_NONE; + header('HTTP/1.0 404 Not Found', true, 404); + + $root = new SimpleXML(''); + $root->addChild('error', 'Item not found!'); + $this->result = $root->asXML(); + + return; + } + else + $this->typeId = $this->subject->id; + + $root = new SimpleXML(''); + + // item root + $xml = $root->addChild('item'); + $xml->addAttribute('id', $this->typeId); + + // name + $xml->addChild('name')->addCData($this->subject->getField('name', true)); + // itemlevel + $xml->addChild('level', $this->subject->getField('itemLevel')); + // quality + $xml->addChild('quality', Lang::item('quality', $this->subject->getField('quality')))->addAttribute('id', $this->subject->getField('quality')); + // class + $x = Lang::item('cat', $this->subject->getField('class')); + $xml->addChild('class')->addCData(is_array($x) ? $x[0] : $x)->addAttribute('id', $this->subject->getField('class')); + // subclass + $xml->addChild('subclass')->addCData($this->getSubclass())->addAttribute('id', $this->subject->getField('subClass')); + // icon + displayId + $xml->addChild('icon', $this->subject->getField('iconString'))->addAttribute('displayId', $this->subject->getField('displayId')); + // inventorySlot + $xml->addChild('inventorySlot', Lang::item('inventoryType', $this->subject->getField('slot')))->addAttribute('id', $this->subject->getField('slot')); + // tooltip + $xml->addChild('htmlTooltip')->addCData($this->subject->renderTooltip()); + + $this->subject->extendJsonStats(); + + // json + $fields = ['classs', 'displayid', 'dps', 'id', 'level', 'name', 'reqlevel', 'slot', 'slotbak', 'speed', 'subclass']; + $json = []; + foreach ($fields as $f) + { + if (isset($this->subject->json[$this->typeId][$f])) + { + $_ = $this->subject->json[$this->typeId][$f]; + if ($f == 'name') + $_ = (7 - $this->subject->getField('quality')).$_; + + $json[$f] = $_; + } + } + + // itemsource + if ($this->subject->getSources($s, $sm)) + { + $json['source'] = $s; + if ($sm) + $json['sourcemore'] = $sm; + } + + $xml->addChild('json')->addCData(substr(json_encode($json), 1, -1)); + + // jsonEquip missing: avgbuyout + $json = []; + if ($_ = $this->subject->getField('sellPrice')) // sellprice + $json['sellprice'] = $_; + + if ($_ = $this->subject->getField('requiredLevel')) // reqlevel + $json['reqlevel'] = $_; + + if ($_ = $this->subject->getField('requiredSkill')) // reqskill + $json['reqskill'] = $_; + + if ($_ = $this->subject->getField('requiredSkillRank')) // reqskillrank + $json['reqskillrank'] = $_; + + if ($_ = $this->subject->getField('cooldown')) // cooldown + $json['cooldown'] = $_ / 1000; + + Util::arraySumByKey($json, $this->subject->jsonStats[$this->typeId] ?? []); + + foreach ($this->subject->json[$this->typeId] as $name => $qty) + if ($idx = Stat::getIndexFrom(Stat::IDX_JSON_STR, $name)) + if (Stat::getFilterCriteriumId($idx)) + $json[$name] = $qty; + + $xml->addChild('jsonEquip')->addCData(substr(json_encode($json), 1, -1)); + + // jsonUse + if ($onUse = $this->subject->getOnUseStats()) + { + $j = ''; + foreach ($onUse->toJson(includeEmpty: false) as $key => $amt) + $j .= ',"'.$key.'":'.$amt; + + $xml->addChild('jsonUse')->addCData(substr($j, 1)); + } + + // reagents + $cnd = array( + 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); + if (!$spellSource->error) + { + $cbNode = $xml->addChild('createdBy'); + + foreach ($spellSource->iterate() as $sId => $__) + { + foreach ($spellSource->canCreateItem() as $idx) + { + if ($spellSource->getField('effect'.$idx.'CreateItemId') != $this->typeId) + continue; + + $splNode = $cbNode->addChild('spell'); + $splNode->addAttribute('id', $sId); + $splNode->addAttribute('name', $spellSource->getField('name', true)); + $splNode->addAttribute('icon', $this->subject->getField('iconString')); + $splNode->addAttribute('minCount', $spellSource->getField('effect'.$idx.'BasePoints') + 1); + $splNode->addAttribute('maxCount', $spellSource->getField('effect'.$idx.'BasePoints') + $spellSource->getField('effect'.$idx.'DieSides')); + + foreach ($spellSource->getReagentsForCurrent() as $rId => $qty) + { + if ($reagent = $spellSource->relItems->getEntry($rId)) + { + $rgtNode = $splNode->addChild('reagent'); + $rgtNode->addAttribute('id', $rId); + $rgtNode->addAttribute('name', Util::localizedString($reagent, 'name')); + $rgtNode->addAttribute('quality', $reagent['quality']); + $rgtNode->addAttribute('icon', $reagent['iconString']); + $rgtNode->addAttribute('count', $qty[1]); + } + } + + break; + } + } + } + + // link + $xml->addChild('link', Cfg::get('HOST_URL').'?item='.$this->typeId); + + $this->result = $root->asXML(); + } + + private function getSubclass() : string + { + $c = $this->subject->getField('class'); + $sc = $this->subject->getField('subClass'); + + if ($c == ITEM_CLASS_WEAPON) + $langRef = Lang::spell('weaponSubClass'); + else + $langRef = Lang::item('cat', $c, 1); + + if (!is_array($langRef)) + return Lang::item('cat', $c); + + if (is_array($langRef[$sc])) + return $langRef[$sc][0]; + + return $langRef[$sc]; + } + + public function getCacheKeyComponents() : array + { + return array( + $this->type, // DBType + $this->typeId, // DBTypeId/category + -1, // staff mask (content does not diff) + '' // misc (unused) + ); + } +} + +?> diff --git a/pages/items.php b/endpoints/items/items.php similarity index 56% rename from pages/items.php rename to endpoints/items/items.php index a40eaa16..0f5f48fd 100644 --- a/pages/items.php +++ b/endpoints/items/items.php @@ -1,22 +1,29 @@ 0 class => subclass + protected int $type = Type::ITEM; + protected int $cacheType = CACHE_TYPE_LIST_PAGE; + + protected string $template = 'items'; + protected string $pageName = 'items'; + protected ?int $activeTab = parent::TAB_DATABASE; + protected array $breadcrumb = [0, 0]; + + protected array $dataLoader = ['weight-presets']; + protected array $scripts = [[SC_JS_FILE, 'js/filters.js']]; + protected array $expectedGET = array( + 'filter' => ['filter' => FILTER_VALIDATE_REGEXP, 'options' => ['regexp' => Filter::PATTERN_PARAM]] + ); + protected array $validCats = array( // if > 0 class => subclass 2 => [15, 13, 0, 4, 7, 6, 10, 1, 5, 8, 2, 18, 3, 16, 19, 20, 14], 4 => array( 0 => true, @@ -73,77 +80,100 @@ class ItemsPage extends GenericPage 13 => true ); - private $sharedLV = array( // common listview components across all tabs + public array $gemScores = []; + public array $typeList = []; // rightPanel - content by context + public array $slotList = []; // rightPanel - INV_TYPE_Xs + + private array $filterOpts = []; + private array $sharedLV = array( // common listview components across all tabs 'hiddenCols' => [], 'visibleCols' => [], 'extraCols' => [] ); - public function __construct($pageCall, $pageParam) + public function __construct(string $rawParam) { - $this->getCategoryFromUrl($pageParam); + $this->getCategoryFromUrl($rawParam); - $this->filterObj = new ItemListFilter(false, ['parentCats' => $this->category]); + parent::__construct($rawParam); - parent::__construct($pageCall, $pageParam); + if ($this->category) + $this->subCat = '='.implode('.', $this->category); - $this->name = Util::ucFirst(Lang::game('items')); - $this->subCat = $pageParam !== null ? '='.$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; } - protected function generateContent() + protected function generate() : void { - $this->addJS('?data=weight-presets&locale='.User::$localeId.'&t='.$_SESSION['dataKey']); - - $conditions = []; + $this->h1 = Util::ucFirst(Lang::game('items')); + $conditions = [Listview::DEFAULT_SIZE]; if (!User::isInGroup(U_GROUP_EMPLOYEE)) $conditions[] = [['cuFlags', CUSTOM_EXCLUDE_FOR_LISTVIEW, '&'], 0]; + if ($_ = $this->filter->getConditions()) + $conditions[] = $_; + + /*******************/ /* evaluate filter */ /*******************/ - // recreate form selection (must be evaluated first via getConditions()) - if ($_ = $this->filterObj->getConditions()) - $conditions[] = $_; - - $this->filter = $this->filterObj->getForm(); - $this->filter['query'] = isset($_GET['filter']) ? $_GET['filter'] : null; - $this->filter['initData'] = ['init' => 'items']; - - if ($x = $this->filterObj->getSetCriteria()) - $this->filter['initData']['sc'] = $x; - - $xCols = $this->filterObj->getExtraCols(); - if ($xCols) - $this->filter['initData']['ec'] = $xCols; - - if ($x = $this->filterObj->getSetWeights()) - $this->filter['initData']['sw'] = $x; - - $menu = $this->createExtraMenus(); - foreach ($menu['type'][0] as $k => $str) - if ($str && (!$menu['type'][1] || ($menu['type'][1] & (1 << $k)))) - $this->filter['type'][$k] = $str; - - foreach ($menu['slot'][0] as $k => $str) - if ($str && (!$menu['slot'][1] || ($menu['slot'][1] & (1 << $k)))) - $this->filter['slot'][$k] = $str; - - if (isset($this->filter['slot'][INVTYPE_SHIELD])) // "Off Hand" => "Shield" - $this->filter['slot'][INVTYPE_SHIELD] = Lang::item('armorSubClass', 6); + $fiForm = $this->filter->values; + $xCols = $this->filter->fiExtraCols; $infoMask = ITEMINFO_JSON; if (array_intersect([63, 64, 125], $xCols)) // 63:buyPrice; 64:sellPrice; 125:reqarenartng $infoMask |= ITEMINFO_VENDOR; if ($xCols) - $this->sharedLV['extraCols'] = '$fi_getExtraCols(fi_extraCols, '.(isset($this->filter['gm']) ? $this->filter['gm'] : 0).', '.(array_intersect([63], $xCols) ? 1 : 0).')'; + $this->sharedLV['extraCols'] = '$fi_getExtraCols(fi_extraCols, '.($fiForm['gm'] ?? 0).', '.(array_intersect([63], $xCols) ? 1 : 0).')'; + + $this->createExtraMenus(); // right side panels in search form + + + /*************/ + /* Menu Path */ + /*************/ + + foreach ($this->category as $c) + $this->breadcrumb[] = $c; + + // if slot-dropdown is available && Armor && $path points to Armor-Class + if (count($this->breadcrumb) == 4 && $this->category[0] == 4 && count($fiForm['sl']) == 1) + $this->breadcrumb[] = $fiForm['sl'][0]; + else if (isset($this->category[0]) && $this->category[0] == 0 && count($fiForm['ty']) == 1) + $this->breadcrumb[] = $fiForm['ty'][0]; + + + /**************/ + /* Page Title */ + /**************/ + + array_unshift($this->title, $this->h1); + + if ($this->category) + { + if (isset($this->category[2]) && is_array(Lang::item('cat', $this->category[0], 1, $this->category[1]))) + $tPart = Lang::item('cat', $this->category[0], 1, $this->category[1], 1, $this->category[2]); + else if (isset($this->category[1]) && is_array(Lang::item('cat', $this->category[0]))) + $tPart = Lang::item('cat', $this->category[0], 1, $this->category[1]); + else if ($this->category[0] == 0 && count($fiForm['ty']) == 1) + $tPart = Lang::item('cat', 0, 1, $fiForm['ty'][0]); + else + $tPart = Lang::item('cat', $this->category[0]); + + array_unshift($this->title, is_array($tPart) ? $tPart[0] : $tPart); + } - if ($this->filterObj->error) - $this->sharedLV['_errors'] = '$1'; /******************/ /* set conditions */ @@ -156,32 +186,34 @@ class ItemsPage extends GenericPage if (isset($this->category[2])) $conditions[] = ['i.subSubClass', $this->category[2]]; + /***********************/ /* handle auto-gemming */ /***********************/ - $this->gemScores = $this->createGemScores(); + $this->createGemScores(); + /*************************/ /* handle upgrade search */ /*************************/ $upgItemData = []; - if (!empty($this->filter['upg']) && !empty($this->filterObj->getSetWeights())) + if ($this->filter->upgrades && $this->filter->fiSetWeights) { - $upgItems = new ItemList(array(['id', array_keys($this->filter['upg'])]), ['extraOpts' => $this->filterObj->extraOpts]); + $upgItems = new ItemList(array(['id', array_keys($this->filter->upgrades)]), ['extraOpts' => $this->filter->extraOpts]); if (!$upgItems->error) { - $this->extendGlobalData($upgItems->getJSGlobals()); $upgItemData = $upgItems->getListviewData($infoMask); + $this->extendGlobalData($upgItems->getJSGlobals(GLOBALINFO_SELF | GLOBALINFO_RELATED)); } } - if ($upgItemData) // check if upItems cover multiple slots + if ($upgItemData) // check if upgItems cover multiple slots { $singleSlot = true; - $ref = reset($this->filter['upg']); - foreach ($this->filter['upg'] as $slot) + $ref = reset($this->filter->upgrades); + foreach ($this->filter->upgrades as $slot) { if ($slot == $ref) continue; @@ -190,16 +222,17 @@ class ItemsPage extends GenericPage break; } - if ($singleSlot && empty($this->filter['gb'])) // enforce group by slot - $this->filter['gb'] = 1; + if ($singleSlot && empty($fiForm['gb'])) // enforce group by slot + $fiForm['gb'] = ItemListFilter::GROUP_BY_SLOT; else if (!$singleSlot) // multiples can only be grouped by slot { - $this->filter['gb'] = 1; + $fiForm['gb'] = ItemListFilter::GROUP_BY_SLOT; $maxResults = 25; $this->sharedLV['customFilter'] = '$fi_filterUpgradeListview'; } } + /********************************************************************************************************************************/ /* group by */ /* */ @@ -222,15 +255,17 @@ class ItemsPage extends GenericPage ); $groups = []; $nameSource = []; - $grouping = isset($this->filter['gb']) ? $this->filter['gb'] : null; + $grouping = $fiForm['gb'] ?? ItemListFilter::GROUP_BY_NONE; $extraOpts = []; - $maxResults = CFG_SQL_LIMIT_DEFAULT; + $maxResults = Listview::DEFAULT_SIZE; + $forceTabs = false; + $tabs = []; switch ($grouping) { // slot: (try to limit the lookups by class grouping and intersecting with preselected slots) // if intersect yields an empty array no lookups will occur - case 1: + case ItemListFilter::GROUP_BY_SLOT: if (isset($this->category[0]) && $this->category[0] == ITEM_CLASS_ARMOR) $groups = $availableSlots[ITEM_CLASS_ARMOR]; else if (isset($this->category[0]) && $this->category[0] == ITEM_CLASS_WEAPON) @@ -238,23 +273,23 @@ class ItemsPage extends GenericPage else $groups = array_merge($availableSlots[ITEM_CLASS_ARMOR], $availableSlots[ITEM_CLASS_WEAPON]); - if (isset($this->filter['sl'])) // skip lookups for unselected slots - $groups = array_intersect($groups, (array)$this->filter['sl']); + if ($fiForm['sl']) // skip lookups for unselected slots + $groups = array_intersect($groups, $fiForm['sl']); - if (!empty($this->filter['upg'])) // skip lookups for slots we dont have items to upgrade for - $groups = array_intersect($groups, (array)$this->filter['upg']); + if ($this->filter->upgrades) // skip lookups for slots we dont have items to upgrade for + $groups = array_intersect($groups, $this->filter->upgrades); if ($groups) { $nameSource = Lang::item('inventoryType'); - $this->forceTabs = true; + $forceTabs = true; } break; - case 2: // itemlevel: first, try to find 10 level steps within range (if given) as tabs + 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->filterObj->extraOpts; - $this->filterOpts['is']['o'] = [null]; // remove 'order by' from itemStats + $this->filterOpts = $this->filter->extraOpts; + $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]); @@ -271,62 +306,68 @@ class ItemsPage extends GenericPage $groups[] = $l; // push last value as negativ to signal misc group after $this level $extraOpts = ['i' => ['o' => ['itemlevel DESC']]]; $nameSource[$l] = Lang::item('tabOther'); - $this->forceTabs = true; + $forceTabs = true; } break; - case 3: // source + case ItemListFilter::GROUP_BY_SOURCE: // source $groups = [1, 2, 3, 4, 5, 10, 11, 12, 0]; $nameSource = Lang::game('sources'); - $this->forceTabs = true; + $forceTabs = true; break; // none default: - $grouping = 0; + $grouping = ItemListFilter::GROUP_BY_NONE; $groups[0] = null; } + // write back 'gb' to filter + if ($grouping) + $this->filter->values['gb'] = $grouping; + + /*****************************/ /* create lv-tabs for groups */ /*****************************/ foreach ($groups as $group) { - switch ($grouping) + $finalCnd = match ($grouping) { - case 1: - $finalCnd = array_merge($conditions, [['slot', $group], $maxResults]); - break; - case 2: - $finalCnd = array_merge($conditions, [['itemlevel', abs($group), $group > 0 ? null : '<'], $maxResults]); - break; - case 3: - $finalCnd = array_merge($conditions, [$group ? ['src.src'.$group, null, '!'] : ['src.typeId', null], $maxResults]); - break; - default: - $finalCnd = $conditions; - } + ItemListFilter::GROUP_BY_SLOT => array_merge($conditions, [['slot', $group], $maxResults]), + ItemListFilter::GROUP_BY_LEVEL => array_merge($conditions, [['itemlevel', abs($group), $group > 0 ? null : '<'], $maxResults]), + ItemListFilter::GROUP_BY_SOURCE => array_merge($conditions, [$group ? ['src.src'.$group, null, '!'] : ['src.typeId', null], $maxResults]), + default => $conditions + }; - $items = new ItemList($finalCnd, ['extraOpts' => array_merge($extraOpts, $this->filterObj->extraOpts)]); + $items = new ItemList($finalCnd, ['extraOpts' => array_merge($extraOpts, $this->filter->extraOpts), 'calcTotal' => true]); if ($items->error) continue; - $this->extendGlobalData($items->getJSGlobals()); + // 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)); $upg = []; if ($upgItemData) { - if ($grouping == 1) // slot: match upgradeItem to slot + // slot: match upgradeItem to slot + if ($grouping == ItemListFilter::GROUP_BY_SLOT) { - $upg = array_keys(array_filter($this->filter['upg'], function ($v) use ($group) { - return $v == $group; - })); + $upg = array_keys(array_filter($this->filter->upgrades, fn($x) => $x == $group)); foreach ($upg as $uId) $tabData['data'][$uId] = $upgItemData[$uId]; @@ -336,7 +377,7 @@ class ItemsPage extends GenericPage } else if ($grouping) { - $upg = array_keys($this->filter['upg']); + $upg = array_keys($this->filter->upgrades); $tabData['_upgradeIds'] = $upg; foreach ($upgItemData as $uId => $data) // using numeric keys => cant use array_merge $tabData['data'][$uId] = $data; @@ -345,25 +386,19 @@ class ItemsPage extends GenericPage if ($grouping) { - switch ($grouping) + $tabData['id'] = match ($grouping) { - case 1: - $tabData['id'] = 'slot-'.$group; - break; - case 2: - $tabData['id'] = $group > 0 ? 'level-'.$group : 'other'; - break; - case 3: - $tabData['id'] = $group ? 'source-'.$group : 'unknown'; - break; - } + ItemListFilter::GROUP_BY_SLOT => 'slot-'.$group, + ItemListFilter::GROUP_BY_LEVEL => $group > 0 ? 'level-'.$group : 'other', + ItemListFilter::GROUP_BY_SOURCE => $group ? 'source-'.$group : 'unknown' + }; $tabData['name'] = $nameSource[$group]; $tabData['tabs'] = '$tabsGroups'; } - if (!empty($this->filterObj->getSetWeights())) - if ($items->hasSetFields(['armor'])) + if ($this->filter->fiSetWeights) + if ($items->hasSetFields('tplArmor')) $tabData['visibleCols'][] = 'armor'; // create note if search limit was exceeded; overwriting 'note' is intentional @@ -371,18 +406,18 @@ class ItemsPage extends GenericPage { $tabData['_truncated'] = 1; - $cls = isset($this->category[0]) ? '='.$this->category[0] : ''; + $catg = isset($this->category[0]) ? '='.$this->category[0] : ''; $override = ['gb' => '']; if ($upg) - $override['upg'] = implode(':', $upg); + $override['upg'] = $upg; switch ($grouping) { - case 1: + case ItemListFilter::GROUP_BY_SLOT: $override['sl'] = $group; - $tabData['note'] = '$$WH.sprintf(LANG.lvnote_viewmoreslot, \''.$cls.'\', \''.$this->filterObj->getFilterString($override).'\')'; + $tabData['note'] = '$$WH.sprintf(LANG.lvnote_viewmoreslot, \''.$catg.'\', \''.$this->filter->buildGETParam($override).'\')'; break; - case 2: + case ItemListFilter::GROUP_BY_LEVEL: if ($group > 0) { $override['minle'] = $group; @@ -391,121 +426,58 @@ class ItemsPage extends GenericPage else $override['maxle'] = abs($group) - 1; - $tabData['note'] = '$$WH.sprintf(LANG.lvnote_viewmorelevel, \''.$cls.'\', \''.$this->filterObj->getFilterString($override).'\')'; + $tabData['note'] = '$$WH.sprintf(LANG.lvnote_viewmorelevel, \''.$catg.'\', \''.$this->filter->buildGETParam($override).'\')'; break; - case 3: + case ItemListFilter::GROUP_BY_SOURCE: if ($_ = [null, 3, 4, 5, 6, 7, 9, 10, 11][$group]) - $tabData['note'] = '$$WH.sprintf(LANG.lvnote_viewmoresource, \''.$cls.'\', \''.$this->filterObj->getFilterString($override, ['cr' => 128, 'crs' => $_, 'crv' => 0]).'\')'; + $tabData['note'] = '$$WH.sprintf(LANG.lvnote_viewmoresource, \''.$catg.'\', \''.$this->filter->buildGETParam($override, ['cr' => 128, 'crs' => $_, 'crv' => 0]).'\')'; break; } } else if ($items->getMatches() > $maxResults) { - $tabData['note'] = sprintf(Util::$tryFilteringString, 'LANG.lvnote_itemsfound', $items->getMatches(), CFG_SQL_LIMIT_DEFAULT); + $tabData['note'] = sprintf(Util::$tryFilteringString, 'LANG.lvnote_itemsfound', $items->getMatches(), Listview::DEFAULT_SIZE); $tabData['_truncated'] = 1; } - foreach ($tabData as $k => $p) - if (!$p && $k != 'data') - unset($tabData[$k]); + // inherited from >sharedLV, may be empty + if (!$tabData['hiddenCols']) + unset($tabData['hiddenCols']); + if (!$tabData['visibleCols']) + unset($tabData['visibleCols']); + if (!$tabData['extraCols']) + unset($tabData['extraCols']); if ($grouping) $tabData['hideCount'] = 1; - $tabData['data'] = array_values($tabData['data']); - - $this->lvTabs[] = ['item', $tabData]; + $tabs[] = $tabData; } - // reformat for use in template - if (!empty($this->filter['upg'])) - $this->filter['upg'] = implode(':', array_keys($this->filter['upg'])); + $this->lvTabs = new Tabs(['parent' => "\$\$WH.ge('tabs-generic')"], 'tabsGroups', $forceTabs && $tabs); // whoops, we have no data? create emergency content - if (empty($this->lvTabs)) - { - $this->forceTabs = false; - $this->lvTabs[] = ['item', ['data' => []]]; - } + if (!count($tabs)) + $tabs[] = ['data' => []]; - // sort for dropdown-menus - Lang::sort('game', 'ra'); - Lang::sort('game', 'cl'); - } + foreach ($tabs as $t) + $this->lvTabs->addListviewTab(new Listview($t, ItemList::$brickFile)); - protected function generateTitle() - { - array_unshift($this->title, $this->name); + $this->redButtons[BUTTON_WOWHEAD] = true; + if ($fiQuery = $this->filter->buildGETParam()) + $this->wowheadLink .= '&filter='.$fiQuery; - if (!$this->category) - return; + parent::generate(); - if (isset($this->category[2]) && is_array(Lang::item('cat', $this->category[0], 1, $this->category[1]))) - $tPart = Lang::item('cat', $this->category[0], 1, $this->category[1], 1, $this->category[2]); - else if (isset($this->category[1]) && is_array(Lang::item('cat', $this->category[0]))) - $tPart = Lang::item('cat', $this->category[0], 1, $this->category[1]); - else if ($this->category[0] == 0 && isset($this->filter['ty']) && !is_array($this->filter['ty'])) - $tPart = Lang::item('cat', 0, 1, $this->filter['ty']); - else - $tPart = Lang::item('cat', $this->category[0]); - - array_unshift($this->title, is_array($tPart) ? $tPart[0] : $tPart); - } - - protected function generatePath() - { - foreach ($this->category as $c) - $this->path[] = $c; - - // if slot-dropdown is available && Armor && $path points to Armor-Class - $form = $this->filterObj->getForm(); - if (count($this->path) == 4 && $this->category[0] == 4 && isset($form['sl']) && !is_array($form['sl'])) - $this->path[] = $form['sl']; - else if (!empty($this->category[0]) && $this->category[0] == 0 && isset($form['ty']) && !is_array($form['ty'])) - $this->path[] = $form['ty']; + $this->setOnCacheLoaded([self::class, 'onBeforeDisplay']); } // fetch best possible gems for chosen weights - private function createGemScores() + private function createGemScores() : void { - $gemScores = []; - - if (empty($this->filterObj->getSetWeights())) - return []; - - if (!empty($this->filter['gm'])) - { - $this->sharedLV['computeDataFunc'] = '$fi_scoreSockets'; - - $q = intVal($this->filter['gm']); - $mask = 14; - $cnd = [10, ['class', ITEM_CLASS_GEM], ['gemColorMask', &$mask, '&'], ['quality', &$q]]; - if (!isset($this->filter['jc'])) - $cnd[] = ['itemLimitCategory', 0]; // Jeweler's Gems - - If ($this->filterObj->wtCnd) - $cnd[] = $this->filterObj->wtCnd; - - $anyColor = new ItemList($cnd, ['extraOpts' => $this->filterObj->extraOpts]); - if (!$anyColor->error) - { - $this->extendGlobalData($anyColor->getJSGlobals()); - $gemScores[0] = array_values($anyColor->getListviewData(ITEMINFO_GEM)); - } - - for ($i = 0; $i < 4; $i++) - { - $mask = 1 << $i; - $q = !$i ? 3 : intVal($this->filter['gm']); // meta gems are always included.. ($q is backReferenced) - $byColor = new ItemList($cnd, ['extraOpts' => $this->filterObj->extraOpts]); - if (!$byColor->error) - { - $this->extendGlobalData($byColor->getJSGlobals()); - $gemScores[$mask] = array_values($byColor->getListviewData(ITEMINFO_GEM)); - } - } - } + if (!$this->filter->fiSetWeights) + return; $this->sharedLV['onBeforeCreate'] = '$fi_initWeightedListview'; $this->sharedLV['onAfterCreate'] = '$fi_addUpgradeIndicator'; @@ -513,21 +485,50 @@ class ItemsPage extends GenericPage array_push($this->sharedLV['hiddenCols'], 'type', 'source'); - return $gemScores; + if (!$this->filter->values['gm']) + return; + + $this->sharedLV['computeDataFunc'] = '$fi_scoreSockets'; + + $q = $this->filter->values['gm']; + $mask = 0xE; + $cnd = [10, ['class', ITEM_CLASS_GEM], ['gemColorMask', &$mask, '&'], ['quality', &$q]]; + if (!$this->filter->values['jc']) + $cnd[] = ['itemLimitCategory', 0]; // Jeweler's Gems + + if ($this->filter->wtCnd) + $cnd[] = $this->filter->wtCnd; + + $anyColor = new ItemList($cnd, ['extraOpts' => $this->filter->extraOpts]); + if (!$anyColor->error) + { + $this->extendGlobalData($anyColor->getJSGlobals()); + $this->gemScores[0] = array_values($anyColor->getListviewData(ITEMINFO_GEM)); + } + + for ($i = 0; $i < 4; $i++) + { + $mask = 1 << $i; + $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) + { + $this->extendGlobalData($byColor->getJSGlobals()); + $this->gemScores[$mask] = array_values($byColor->getListviewData(ITEMINFO_GEM)); + } + } } // display available submenus 'type' and 'slot', if applicable - private function createExtraMenus() + private function createExtraMenus() : void { - $menu = array( - 'type' => [[], null], - 'slot' => [[], null] - ); + $typeData = $slotData = []; + $typeMask = $slotMask = 0x0; if (!$this->category) { - $menu['slot'] = [Lang::item('inventoryType'), null]; - asort($menu['slot'][0]); + $slotData = Lang::item('inventoryType'); + asort($slotData); } else { @@ -541,31 +542,36 @@ class ItemsPage extends GenericPage switch ($this->category[0]) { case 0: - $menu['type'] = [Lang::item('cat', 0, 1), null]; + $typeData = Lang::item('cat', 0, 1); if (!isset($this->category[1]) || in_array($this->category[1], [6, -3])) { - $menu['slot'] = [Lang::item('inventoryType'), 0x63EFEA]; - asort($menu['slot'][0]); + $slotData = Lang::item('inventoryType'); + $slotMask = 0x63EFEA; + asort($slotData); } break; case 2: if (!isset($this->category[1])) - $menu['type'] = [Lang::spell('weaponSubClass'), null]; + $typeData = Lang::spell('weaponSubClass'); - $menu['slot'] = [Lang::item('inventoryType'), 0x262A000]; - asort($menu['slot'][0]); + $slotData = Lang::item('inventoryType'); + $slotMask = 0x262A000; + asort($slotData); break; case 4: if (!isset($this->category[1])) { - $menu['slot'] = [Lang::item('inventoryType'), 0x10895FFE]; - $menu['type'] = [Lang::item('cat', 4, 1), null]; + $slotData = Lang::item('inventoryType'); + $slotMask = 0x10895FFE; + $typeData = Lang::item('cat', 4, 1); } else if (in_array($this->category[1], [1, 2, 3, 4])) - $menu['slot'] = [Lang::item('inventoryType'), 0x7EA]; - - asort($menu['slot'][0]); + { + $slotData = Lang::item('inventoryType'); + $slotMask = 0x7EA; + } + asort($slotData); break; case 16: if (!isset($this->category[2])) @@ -581,13 +587,26 @@ class ItemsPage extends GenericPage $this->sharedLV['hiddenCols'][] = 'slot'; case 15: if (!isset($this->category[1])) - $menu['type'] = [$catList[1], null]; + $typeData = $catList[1]; break; } } - return $menu; + foreach ($typeData as $k => $str) + if ($str && (!$typeMask || ($typeMask & (1 << $k)))) + $this->typeList[$k] = is_array($str) ? $str[0] : $str; + + foreach ($slotData as $k => $str) // "Off Hand" => "Shield" + if ($str && (!$slotMask || ($slotMask & (1 << $k)))) + $this->slotList[$k] = $k == INVTYPE_SHIELD ? Lang::item('armorSubClass', 6) : $str; + } + + protected static function onBeforeDisplay() : void + { + // sort for dropdown-menus + Lang::sort('game', 'ra'); + Lang::sort('game', 'cl'); } } diff --git a/endpoints/itemset/itemset.php b/endpoints/itemset/itemset.php new file mode 100644 index 00000000..c86ae527 --- /dev/null +++ b/endpoints/itemset/itemset.php @@ -0,0 +1,260 @@ +typeId = intVal($id); + $this->contribute = Type::getClassAttrib($this->type, 'contribute') ?? CONTRIBUTE_NONE; + } + + protected function generate() : void + { + $this->subject = new ItemsetList(array(['id', $this->typeId])); + if ($this->subject->error) + $this->generateNotFound(Lang::game('itemset'), Lang::itemset('notFound')); + + $this->extendGlobalData($this->subject->getJSGlobals()); + + $this->h1 = $this->subject->getField('name', true); + + $this->gPageInfo += array( + 'type' => $this->type, + 'typeId' => $this->typeId, + 'name' => $this->h1 + ); + + $_ta = $this->subject->getField('contentGroup'); + $_ty = $this->subject->getField('type'); + $_sk = $this->subject->getField('skillId'); + $_evt = $this->subject->getField('eventId'); + $_cnt = count($this->subject->getField('pieces')); + $_cl = ChrClass::fromMask($this->subject->getField('classMask')); + + + /*************/ + /* Menu Path */ + /*************/ + + if (count($_cl) == 1) + $this->breadcrumb[] = $_cl[0]; + + + /**************/ + /* Page Title */ + /**************/ + + array_unshift($this->title, $this->h1, Util::ucWords(Lang::game('itemset'))); + + + /***********/ + /* Infobox */ + /***********/ + + $infobox = Lang::getInfoBoxForFlags($this->subject->getField('cuFlags')); + + if ($this->subject->getField('cuFlags') & CUSTOM_UNAVAILABLE) + $infobox[] = Lang::main('unavailable'); + + // worldevent + if ($_evt) + { + $infobox[] = Lang::game('eventShort', ['[event='.$_evt.']']); + $this->extendGlobalIds(Type::WORLDEVENT, $_evt); + } + + // itemLevel + if ($min = $this->subject->getField('minLevel')) + $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); + $t = count($jsg) == 1 ? Lang::game('class') : Lang::game('classes'); + $infobox[] = Util::ucFirst($t).Lang::main('colon').$cl; + } + + // required level + if ($lvl = $this->subject->getField('reqLevel')) + $infobox[] = Lang::game('reqLevel', [$lvl]); + + // type + if ($_ty) + $infobox[] = Lang::game('type').Lang::itemset('types', $_ty); + + // tag + 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'); + + + /****************/ + /* Main Content */ + /****************/ + + // pieces + Summary + $eqList = []; + $compare = []; + + if (!$this->subject->pieceToSet) + $cnd = [0]; + else + $cnd = ['i.id', array_keys($this->subject->pieceToSet)]; + + $iList = new ItemList(array($cnd)); + $data = $iList->getListviewData(ITEMINFO_SUBITEMS | ITEMINFO_JSON); + foreach ($iList->iterate() as $itemId => $__) + { + if (empty($data[$itemId])) + continue; + + $slot = $iList->getField('slot'); + $disp = $iList->getField('displayId'); + if ($slot && $disp) + $eqList[] = [$slot, $disp]; + + $compare[] = $itemId; + + $this->pieces[$itemId] = array( + array( + 'name_'.Lang::getLocale()->json() => $iList->getField('name', true), + 'quality' => $iList->getField('quality'), + 'icon' => $iList->getField('iconString'), + 'jsonequip' => $data[$itemId] + ), + new IconElement(Type::ITEM, $itemId, $iList->getField('name', true), quality: $iList->getField('quality'), size: IconElement::SIZE_SMALL, align: 'right', element: 'iconlist-icon') + ); + } + + if ($compare) + $this->summary = new Summary(array( + 'template' => 'itemset', + 'id' => 'itemset', + 'parent' => 'summary-generic', + 'groups' => array_map(fn ($x) => [[$x]], $compare), + 'level' => $this->subject->getField('reqLevel') + )); + + // required skill + if ($_sk) + { + $spellLink = sprintf('%s (%s)', $_sk, Lang::spell('cat', 11, $_sk, 0), $this->subject->getField('skillLevel')); + $this->bonusExt = ' – '.Lang::game('requires', [$spellLink]).''; + } + + $this->description = $_ta ? Lang::itemset('_desc', [$this->h1, Lang::itemset('notes', $_ta), $_cnt]) : Lang::itemset('_descTagless', [$this->h1, $_cnt]); + $this->unavailable = !!($this->subject->getField('cuFlags') & CUSTOM_UNAVAILABLE); + $this->spells = $this->subject->getBonuses(); + // $this->expansion = $this->subject->getField('expansion'); NYI - todo: add col to table + $this->redButtons = array( + BUTTON_WOWHEAD => $this->typeId > 0, // bool only + BUTTON_LINKS => ['type' => $this->type, 'typeId' => $this->typeId], + BUTTON_VIEW3D => ['type' => Type::ITEMSET, 'typeId' => $this->typeId, 'equipList' => $eqList], + BUTTON_COMPARE => $compare ? ['eqList' => implode(':', $compare), 'qty' => $_cnt] : false + ); + + + /**************/ + /* Extra Tabs */ + /**************/ + + // related sets (priority: 1: similar tag + class; 2: has event; 3: no tag + similar type, 4: similar type + profession) + $rel = []; + + if ($_ta && count($_cl) == 1) + { + $rel[] = ['id', $this->typeId, '!']; + $rel[] = ['classMask', 1 << ($_cl[0] - 1), '&']; + $rel[] = ['contentGroup', (int)$_ta]; + } + else if ($_evt) + { + $rel[] = ['id', $this->typeId, '!']; + $rel[] = ['eventId', 0, '!']; + } + else if ($_sk) + { + $rel[] = ['id', $this->typeId, '!']; + $rel[] = ['contentGroup', 0]; + $rel[] = ['skillId', 0, '!']; + $rel[] = ['type', $_ty]; + } + else if (!$_ta && $_ty) + { + $rel[] = ['id', $this->typeId, '!']; + $rel[] = ['contentGroup', 0]; + $rel[] = ['type', $_ty]; + $rel[] = ['skillId', 0]; + } + + $this->lvTabs = new Tabs(['parent' => "\$\$WH.ge('tabs-generic')"], 'tabsRelated', true); + + if ($rel) + { + $relSets = new ItemsetList($rel); + if (!$relSets->error) + { + $tabData = array( + 'data' => $relSets->getListviewData(), + 'id' => 'see-also', + 'name' => '$LANG.tab_seealso' + ); + + if (!$relSets->hasDiffFields('classMask')) + $tabData['hiddenCols'] = ['classes']; + + $this->lvTabs->addListviewTab(new Listview($tabData, ItemsetList::$brickFile)); + + $this->extendGlobalData($relSets->getJSGlobals()); + } + } + + parent::generate(); + } +} + + + + +?> diff --git a/endpoints/itemset/itemset_power.php b/endpoints/itemset/itemset_power.php new file mode 100644 index 00000000..042354ae --- /dev/null +++ b/endpoints/itemset/itemset_power.php @@ -0,0 +1,49 @@ + ['filter' => FILTER_CALLBACK, 'options' => [Locale::class, 'tryFromDomain']] + ); + + public function __construct(string $id) + { + parent::__construct($id); + + // temp locale + if ($this->_get['domain']) + Lang::load($this->_get['domain']); + + $this->typeId = intVal($id); + } + + protected function generate() : void + { + $itemset = new ItemsetList(array(['id', $this->typeId])); + if ($itemset->error) + $this->cacheType = CACHE_TYPE_NONE; + else + $opts = array( + 'name' => $itemset->getField('name', true), + 'tooltip' => $itemset->renderTooltip(), + ); + + $this->result = new Tooltip(self::POWER_TEMPLATE, $this->typeId, $opts ?? []); + } +} + +?> diff --git a/endpoints/itemsets/itemsets.php b/endpoints/itemsets/itemsets.php new file mode 100644 index 00000000..945e5100 --- /dev/null +++ b/endpoints/itemsets/itemsets.php @@ -0,0 +1,116 @@ + ['filter' => FILTER_VALIDATE_REGEXP, 'options' => ['regexp' => Filter::PATTERN_PARAM]] + ); + + public function __construct(string $rawParam) + { + $this->getCategoryFromUrl($rawParam); + + parent::__construct($rawParam); + + if ($this->category) + $this->subCat = '='.implode('.', $this->category); + + $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; + } + + protected function generate() : void + { + $this->h1 = Util::ucWords(Lang::game('itemsets')); + + $conditions = [Listview::DEFAULT_SIZE]; + if (!User::isInGroup(U_GROUP_EMPLOYEE)) + $conditions[] = [['cuFlags', CUSTOM_EXCLUDE_FOR_LISTVIEW, '&'], 0]; + + if ($_ = $this->filter->getConditions()) + $conditions[] = $_; + + + /*************/ + /* Menu Path */ + /*************/ + + if ($cl = $this->filter->values['cl']) + $this->breadcrumb[] = $cl; + + + /**************/ + /* Page Title */ + /**************/ + + array_unshift($this->title, $this->h1); + + if ($cl = $this->filter->values['cl']) + array_unshift($this->title, Lang::game('cl', $cl)); + + + /****************/ + /* Main Content */ + /****************/ + + $this->redButtons[BUTTON_WOWHEAD] = true; + if ($fiQuery = $this->filter->buildGETParam()) + $this->wowheadLink .= '&filter='.$fiQuery; + + $itemsets = new ItemsetList($conditions, ['calcTotal' => true]); + $this->extendGlobalData($itemsets->getJSGlobals()); + + $tabData = ['data' => $itemsets->getListviewData()]; + + if ($this->filter->fiExtraCols) + $tabData['extraCols'] = '$fi_getExtraCols(fi_extraCols, 0, 0)'; + + // create note if search limit was exceeded + if ($itemsets->getMatches() > Listview::DEFAULT_SIZE) + { + $tabData['note'] = sprintf(Util::$tryFilteringString, 'LANG.lvnote_itemsetsfound', $itemsets->getMatches(), Listview::DEFAULT_SIZE); + $tabData['_truncated'] = 1; + } + + $this->lvTabs = new Tabs(['parent' => "\$\$WH.ge('tabs-generic')"]); + + $this->lvTabs->addListviewTab(new Listview($tabData, ItemsetList::$brickFile)); + + parent::generate(); + + $this->setOnCacheLoaded([self::class, 'onBeforeDisplay']); + } + + public static function onBeforeDisplay() : void + { + // sort for dropdown-menus + Lang::sort('itemset', 'notes', SORT_NATURAL); + Lang::sort('game', 'cl'); + } +} + +?> diff --git a/endpoints/latest-comments/latest-comments.php b/endpoints/latest-comments/latest-comments.php new file mode 100644 index 00000000..fbb809db --- /dev/null +++ b/endpoints/latest-comments/latest-comments.php @@ -0,0 +1,46 @@ + Util > Latest Comments + + protected function generate() : void + { + $this->h1 = Lang::main('utilities', 2); + $this->h1Link = '?'.$this->pageName.'&rss' . (Lang::getLocale()->value ? '&locale='.Lang::getLocale()->value : ''); + $this->rss = Cfg::get('HOST_URL').'/?' . $this->pageName . '&rss' . (Lang::getLocale()->value ? '&locale='.Lang::getLocale()->value : ''); + + + /*********/ + /* Title */ + /*********/ + + array_unshift($this->title, $this->h1); + + + /****************/ + /* Main Content */ + /****************/ + + $this->lvTabs = new Tabs(['parent' => "\$\$WH.ge('tabs-generic')"]); + + $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], 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 new file mode 100644 index 00000000..20b61fa5 --- /dev/null +++ b/endpoints/latest-comments/latest-comments_rss.php @@ -0,0 +1,41 @@ + 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, [], $url], + 'description' => [true, [], htmlentities($comment['preview'])."

".Lang::main('byUser', [$comment['user'], '']) . $now->formatDate($comment['date'], true)], + 'pubDate' => [false, [], date(DATE_RSS, $comment['date'])], + 'guid' => [false, [], $url] + // 'domain' => [false, [], null] + ); + } + + $this->result = $this->generateRSS(Lang::main('utilities', 2), 'latest-comments'); + } +} + +?> diff --git a/endpoints/latest-screenshots/latest-screenshots.php b/endpoints/latest-screenshots/latest-screenshots.php new file mode 100644 index 00000000..6e8ba355 --- /dev/null +++ b/endpoints/latest-screenshots/latest-screenshots.php @@ -0,0 +1,43 @@ + Util > Latest Screenshots + + protected function generate() : void + { + $this->h1 = Lang::main('utilities', 3); + $this->h1Link = '?'.$this->pageName.'&rss' . (Lang::getLocale()->value ? '&locale='.Lang::getLocale()->value : ''); + $this->rss = Cfg::get('HOST_URL').'/?' . $this->pageName . '&rss' . (Lang::getLocale()->value ? '&locale='.Lang::getLocale()->value : ''); + + + /*********/ + /* Title */ + /*********/ + + array_unshift($this->title, $this->h1); + + + /****************/ + /* Main Content */ + /****************/ + + $this->lvTabs = new Tabs(['parent' => "\$\$WH.ge('tabs-generic')"]); + + $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 new file mode 100644 index 00000000..50e6f215 --- /dev/null +++ b/endpoints/latest-screenshots/latest-screenshots_rss.php @@ -0,0 +1,42 @@ +'; + if ($screenshot['caption']) + $desc .= '
'.$screenshot['caption']; + $desc .= "

".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( + 'title' => [true, [], Lang::typeName($screenshot['type']).Lang::main('colon').htmlentities($screenshot['subject'])], + 'link' => [false, [], Cfg::get('HOST_URL').'/?'.Type::getFileString($screenshot['type']).'='.$screenshot['typeId'].'#screenshots:id='.$screenshot['id']], + 'description' => [true, [], $desc], + 'pubDate' => [false, [], date(DATE_RSS, $screenshot['date'])], + 'enclosure' => [false, ['url' => Cfg::get('STATIC_URL').'/uploads/screenshots/thumb/'.$screenshot['id'].'.jpg', 'length' => 12345, 'type' => 'image/jpeg'], null], + 'guid' => [false, [], Cfg::get('HOST_URL').'/?'.Type::getFileString($screenshot['type']).'='.$screenshot['typeId'].'#screenshots:id='.$screenshot['id']], + // 'domain' => [false, [], live|ptr] + ); + } + + $this->result = $this->generateRSS(Lang::main('utilities', 3), 'latest-screenshots'); + } +} + +?> diff --git a/endpoints/latest-videos/latest-videos.php b/endpoints/latest-videos/latest-videos.php new file mode 100644 index 00000000..610552e3 --- /dev/null +++ b/endpoints/latest-videos/latest-videos.php @@ -0,0 +1,43 @@ + Util > Latest Videos + + protected function generate() : void + { + $this->h1 = Lang::main('utilities', 11); + $this->h1Link = '?'.$this->pageName.'&rss' . (Lang::getLocale()->value ? '&locale='.Lang::getLocale()->value : ''); + $this->rss = Cfg::get('HOST_URL').'/?' . $this->pageName . '&rss' . (Lang::getLocale()->value ? '&locale='.Lang::getLocale()->value : ''); + + + /*********/ + /* Title */ + /*********/ + + array_unshift($this->title, $this->h1); + + + /****************/ + /* Main Content */ + /****************/ + + $this->lvTabs = new Tabs(['parent' => "\$\$WH.ge('tabs-generic')"]); + + $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 new file mode 100644 index 00000000..5e3980ec --- /dev/null +++ b/endpoints/latest-videos/latest-videos_rss.php @@ -0,0 +1,42 @@ +'; + if ($video['caption']) + $desc .= '
'.$video['caption']; + $desc .= "

".Lang::main('byUser', [$video['user'], '']) . $now->formatDate($video['date'], true); + + // is enclosure/length .. is this even relevant..? + $this->feedData[] = array( + 'title' => [true, [], Lang::typeName($video['type']).Lang::main('colon').htmlentities($video['subject'])], + 'link' => [false, [], Cfg::get('HOST_URL').'/?'.Type::getFileString($video['type']).'='.$video['typeId'].'#videos:id='.$video['id']], + 'description' => [true, [], $desc], + 'pubDate' => [false, [], date(DATE_RSS, $video['date'])], + 'enclosure' => [false, ['url' => '//i3.ytimg.com/vi/'.$video['videoId'].'/default.jpg', 'length' => 12345, 'type' => 'image/jpeg'], null], + 'guid' => [false, [], Cfg::get('HOST_URL').'/?'.Type::getFileString($video['type']).'='.$video['typeId'].'#videos:id='.$video['id']], + // 'domain' => [false, [], live|ptr] + ); + } + + $this->result = $this->generateRSS(Lang::main('utilities', 11), 'latest-videos'); + } +} + +?> diff --git a/endpoints/locale/locale.php b/endpoints/locale/locale.php new file mode 100644 index 00000000..260ae500 --- /dev/null +++ b/endpoints/locale/locale.php @@ -0,0 +1,27 @@ + ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkLocale']] + ); + + protected function generate() : void + { + if ($this->_get['locale']?->validate()) + { + User::$preferedLoc = $this->_get['locale']; + User::save(true); + } + + $this->redirectTo = $_SERVER['HTTP_REFERER'] ?? '.'; + } +} + +?> diff --git a/endpoints/mail/mail.php b/endpoints/mail/mail.php new file mode 100644 index 00000000..0b6fe31b --- /dev/null +++ b/endpoints/mail/mail.php @@ -0,0 +1,183 @@ +typeId = intVal($id); + $this->contribute = Type::getClassAttrib($this->type, 'contribute') ?? CONTRIBUTE_NONE; + } + + protected function generate() : void + { + $this->subject = new MailList(array(['id', $this->typeId])); + if ($this->subject->error) + $this->generateNotFound(Lang::game('mail'), Lang::mail('notFound')); + + $this->extendGlobalData($this->subject->getJSGlobals()); + + $this->h1 = Util::htmlEscape($this->subject->getField('name', true)); + + $this->gPageInfo += array( + 'type' => $this->type, + 'typeId' => $this->typeId, + 'name' => $this->subject->getField('name', true) + ); + + + /**************/ + /* Page Title */ + /**************/ + + array_unshift($this->title, $this->subject->getField('name', true), Util::ucFirst(Lang::game('mail'))); + + + /***********/ + /* Infobox */ + /***********/ + + $infobox = Lang::getInfoBoxForFlags($this->subject->getField('cuFlags')); + + // sender + delay + if ($this->typeId < 0) // def. achievement + { + 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` = %i', $this->typeId)) // level rewards + { + if ($mlr['level']) + $infobox[] = Lang::game('level').Lang::main('colon').$mlr['level']; + + $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']); + } + else // achievement or quest + { + 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` = %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` = %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', [DateTime::formatTimeElapsed($q['rewardMailDelay'] * 1000)]); + } + 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'); + + + /****************/ + /* Main Content */ + /****************/ + + $this->redButtons = array( + BUTTON_LINKS => ['type' => $this->type, 'typeId' => $this->typeId], + BUTTON_WOWHEAD => false + ); + + $this->extraText = new Markup(Util::parseHtmlText($this->subject->getField('text', true), true), ['dbpage' => true, 'allow' => Markup::CLASS_ADMIN], 'text-generic'); + + + /**************/ + /* Extra Tabs */ + /**************/ + + $this->lvTabs = new Tabs(['parent' => "\$\$WH.ge('tabs-generic')"], 'tabsRelated', true); + + // tab: attachment + if ($itemId = $this->subject->getField('attachment')) + { + $attachment = new ItemList(array(['id', $itemId])); + if (!$attachment->error) + { + $this->extendGlobalData($attachment->getJsGlobals()); + $this->lvTabs->addListviewTab(new Listview(array( + 'data' => $attachment->getListviewData(), + 'name' => Lang::mail('attachment'), + 'id' => 'attachment' + ), ItemList::$brickFile)); + } + } + + if ($this->typeId < 0 || // used by: achievement + ($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) + { + $this->extendGlobalData($ubAchievements->getJsGlobals()); + $this->lvTabs->addListviewTab(new Listview(array( + 'data' => $ubAchievements->getListviewData(), + 'id' => 'used-by-achievement' + ), AchievementList::$brickFile)); + } + } + else // used by: quest + { + $ubQuests = new QuestList(array(['rewardMailTemplateId', $this->typeId])); + if (!$ubQuests->error) + { + $this->extendGlobalData($ubQuests->getJsGlobals()); + $this->lvTabs->addListviewTab(new Listview(array( + 'data' => $ubQuests->getListviewData(), + 'id' => 'used-by-quest' + ), QuestList::$brickFile)); + } + } + + parent::generate(); + } +} + +?> diff --git a/endpoints/mails/mails.php b/endpoints/mails/mails.php new file mode 100644 index 00000000..e1319aca --- /dev/null +++ b/endpoints/mails/mails.php @@ -0,0 +1,59 @@ +getCategoryFromUrl($rawParam); + + parent::__construct($rawParam); + } + + protected function generate() : void + { + $this->h1 = Util::ucFirst(Lang::game('mails')); + + + /**************/ + /* Page Title */ + /**************/ + + array_unshift($this->title, $this->h1); + + + /****************/ + /* Main Content */ + /****************/ + + $tabData = []; + $mails = new MailList(); + if (!$mails->error) + $tabData['data'] = $mails->getListviewData(); + + $this->extendGlobalData($mails->getJsGlobals()); + + $this->lvTabs = new Tabs(['parent' => "\$\$WH.ge('tabs-generic')"]); + + $this->lvTabs->addListviewTab(new Listview(['data' => $mails->getListviewData()], MailList::$brickFile, 'mail')); + + parent::generate(); + } +} + +?> diff --git a/endpoints/maps/maps.php b/endpoints/maps/maps.php new file mode 100644 index 00000000..0f99f470 --- /dev/null +++ b/endpoints/maps/maps.php @@ -0,0 +1,29 @@ +h1 = Lang::maps('maps'); + + array_unshift($this->title, $this->h1); + + parent::generate(); + } +} + +?> diff --git a/endpoints/missing-screenshots/missing-screenshots.php b/endpoints/missing-screenshots/missing-screenshots.php new file mode 100644 index 00000000..8341008c --- /dev/null +++ b/endpoints/missing-screenshots/missing-screenshots.php @@ -0,0 +1,58 @@ + Util > Missing Screenshots + + protected function generate() : void + { + $this->h1 = Lang::main('utilities', 13); + + + /*********/ + /* Title */ + /*********/ + + array_unshift($this->title, $this->h1); + + + /****************/ + /* Main Content */ + /****************/ + + $this->lvTabs = new Tabs(['parent' => "\$\$WH.ge('tabs-generic')"]); + + // limit to 200 entries each (it generates faster, consumes less memory and should be enough options) + $cnd = [[['cuFlags', CUSTOM_HAS_SCREENSHOT, '&'], 0], 200]; + if (!User::isInGroup(U_GROUP_EMPLOYEE)) + $cnd[] = [['cuFlags', CUSTOM_EXCLUDE_FOR_LISTVIEW, '&'], 0]; + + $hasTabs = false; + foreach (Type::getClassesFor(Type::FLAG_RANDOM_SEARCHABLE, 'contribute', CONTRIBUTE_SS) as $classStr) + { + $typeObj = new $classStr($cnd); + if ($typeObj->error) + continue; + + $this->extendGlobalData($typeObj->getJSGlobals(GLOBALINFO_ANY)); + $this->lvTabs->addListviewTab(new Listview(['data' => $typeObj->getListviewData()], $typeObj::$brickFile)); + $hasTabs = true; + } + + if (!$hasTabs) + $this->lvTabs->addListviewTab(new Listview(['data' => []], 'item')); + + parent::generate(); + } +} + +?> diff --git a/endpoints/most-comments/most-comments.php b/endpoints/most-comments/most-comments.php new file mode 100644 index 00000000..20985809 --- /dev/null +++ b/endpoints/most-comments/most-comments.php @@ -0,0 +1,110 @@ + Util > Most Comments + + protected array $validCats = [1, 7, 30]; + + public function __construct($rawParam) + { + $this->getCategoryFromUrl($rawParam); + + parent::__construct($rawParam); + } + + protected function onInvalidCategory() : never + { + $this->forward('?most-comments=1'); + } + + protected function generate() : void + { + $this->h1 = Lang::main('utilities', 12); + if ($this->category && $this->category[0] > 1) + $this->h1 .= Lang::main('colon') . Lang::main('mostComments', 1, $this->category); + else + $this->h1 .= Lang::main('colon') . Lang::main('mostComments', 0); + + $this->h1Link = '?' . $this->pageName.($this->category ? '='.$this->category[0] : '').'&rss' . (Lang::getLocale()->value ? '&locale='.Lang::getLocale()->value : ''); + $this->rss = Cfg::get('HOST_URL').'/?' . $this->pageName.($this->category ? '='.$this->category[0] : '') . '&rss' . (Lang::getLocale()->value ? '&locale='.Lang::getLocale()->value : ''); + + + /*********/ + /* Title */ + /*********/ + + array_unshift($this->title, $this->h1); + + + /**************/ + /* Breadcrumb */ + /**************/ + + $this->breadcrumb[] = $this->category[0] ?? 1; + + + /****************/ + /* Main Content */ + /****************/ + + $this->lvTabs = new Tabs(['parent' => "\$\$WH.ge('tabs-generic')"], __forceTabs: true); + + $tabBase = array( + 'extraCols' => ["\$Listview.funcBox.createSimpleCol('ncomments', 'tab_comments', '10%', 'ncomments')"], + 'sort' => ['-ncomments'] + ); + + $hasTabs = false; + foreach (Type::getClassesFor() as $type => $classStr) + { + $comments = DB::Aowow()->selectCol( + '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, + $type, + ($this->category[0] ?? 1) * DAY + ); + if (!$comments) + continue; + + $typeClass = new $classStr(array(['id', array_keys($comments)])); + if ($typeClass->error) + continue; + + $data = $typeClass->getListviewData(); + + foreach ($data as $typeId => &$d) + $d['ncomments'] = $comments[$typeId]; + + $addIn = ''; + if (in_array($type, [Type::AREATRIGGER, Type::ENCHANTMENT, Type::ENCHANTMENT, Type::EMOTE])) + { + $addIn = Type::getFileString($type); + $tabBase['name'] = '$LANG.types['.$type.'][2]'; + } + + $this->extendGlobalData($typeClass->getJSGlobals(GLOBALINFO_ANY)); + $this->lvTabs->addListviewTab(new Listview($tabBase + ['data' => $data], $typeClass::$brickFile, $addIn)); + $hasTabs = true; + } + + if (!$hasTabs) + $this->lvTabs->addListviewTab(new Listview(['data' => []], 'commentpreview')); + + parent::generate(); + } +} + +?> diff --git a/endpoints/most-comments/most-comments_rss.php b/endpoints/most-comments/most-comments_rss.php new file mode 100644 index 00000000..1f99587d --- /dev/null +++ b/endpoints/most-comments/most-comments_rss.php @@ -0,0 +1,63 @@ +params && !in_array($this->params[0], $this->validCats)) + $this->forward('?most-comments=1&rss'); + } + + protected function generate() : void + { + foreach (Type::getClassesFor() as $type => $classStr) + { + $comments = DB::Aowow()->selectCol( + '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, + $type, + ($this->category[0] ?? 1) * DAY + ); + if (!$comments) + continue; + + $typeClass = new $classStr(array(['id', array_keys($comments)])); + if ($typeClass->error) + continue; + + $data = $typeClass->getListviewData(); + + foreach ($data as $typeId => &$d) + { + $this->feedData[] = array( + 'title' => [true, [], htmlentities(Type::getFileString($type) == 'item' ? mb_substr($d['name'], 1) : $d['name'])], + 'type' => [false, [], Type::getFileString($type)], + 'link' => [false, [], Cfg::get('HOST_URL').'/?'.Type::getFileString($type).'='.$d['id']], + 'ncomments' => [false, [], $comments[$typeId]] + ); + } + + } + + $this->result = $this->generateRSS(Lang::main('utilities', 12), 'most-comments' . ($this->params ? '='.$this->params[0] : '')); + } +} + +?> diff --git a/endpoints/my-guides/my-guides.php b/endpoints/my-guides/my-guides.php new file mode 100644 index 00000000..f335369f --- /dev/null +++ b/endpoints/my-guides/my-guides.php @@ -0,0 +1,52 @@ +generateError(); + } + + protected function generate() : void + { + $this->h1 = Util::ucFirst(Lang::guide('myGuides')); + + array_unshift($this->title, $this->h1); + + $this->redButtons = [BUTTON_GUIDE_NEW => User::canWriteGuide()]; + + $guides = new GuideList(array(['userId', User::$id])); + + $this->lvTabs = new Tabs(['parent' => "\$\$WH.ge('tabs-generic')"]); + + $this->lvTabs->addListviewTab(new Listview(array( + 'data' => $guides->getListviewData(), + 'name' => Util::ucFirst(Lang::game('guides')), + 'hiddenCols' => ['patch', 'author'], + 'visibleCols' => ['status'], + 'extraCols' => ['$Listview.extraCols.date'] + ), GuideList::$brickFile)); + + parent::generate(); + } +} + +?> diff --git a/endpoints/npc/npc.php b/endpoints/npc/npc.php new file mode 100644 index 00000000..1ddf45e5 --- /dev/null +++ b/endpoints/npc/npc.php @@ -0,0 +1,1125 @@ +typeId = intVal($id); + $this->contribute = Type::getClassAttrib($this->type, 'contribute') ?? CONTRIBUTE_NONE; + } + + protected function generate() : void + { + $this->subject = new CreatureList(array(['id', $this->typeId])); + if ($this->subject->error) + $this->generateNotFound(Lang::game('npc'), Lang::npc('notFound')); + + $this->h1 = Util::htmlEscape($this->subject->getField('name', true)); + $this->subname = $this->subject->getField('subname', true); + + $this->gPageInfo += array( + 'type' => $this->type, + 'typeId' => $this->typeId, + 'name' => $this->subject->getField('name', true) + ); + + $_typeFlags = $this->subject->getField('typeFlags'); + $_altIds = []; + + + /*************/ + /* Menu Path */ + /*************/ + + $this->breadcrumb[] = $this->subject->getField('type'); + if ($_ = $this->subject->getField('family')) + $this->breadcrumb[] = $_; + + + /**************/ + /* Page Title */ + /**************/ + + array_unshift($this->title, $this->subject->getField('name', true), mb_strtoupper(Lang::game('npc'))); + + + /***********************/ + /* Difficulty versions */ + /***********************/ + + if ($this->subject->getField('cuFlags') & NPC_CU_DIFFICULTY_DUMMY) + $this->placeholder = [$this->subject->getField('parentId'), $this->subject->getField('parent', true)]; + else + { + for ($i = 1; $i < 4; $i++) + if ($_ = $this->subject->getField('difficultyEntry'.$i)) + $_altIds[$_] = $i; + + if ($_altIds) + $this->altNPCs = new CreatureList(array(['id', array_keys($_altIds)])); + } + + 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 => $__) + $this->accessory[] = [$id, $vehicles->getField('name', true)]; + } + + + /**********************/ + /* Determine Map Type */ + /**********************/ + + $mapType = 0; + 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)) + { + $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` = %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 %in', array_merge($_altIds, [$this->typeId]))) + $mapType = max($mapType, $d > 2 ? 2 : 1); + + + /***********/ + /* Infobox */ + /***********/ + + $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` = %i', $this->typeId)) + { + $this->extendGlobalIds(Type::WORLDEVENT, ...$_); + $ev = []; + foreach ($_ as $i => $e) + $ev[] = ($i % 2 ? '[br]' : ' ') . '[event='.$e.']'; + + $infobox[] = Lang::game('eventShort', [implode(',', $ev)]); + } + + // Level + if ($this->subject->getField('rank') != NPC_RANK_BOSS) + { + $level = $this->subject->getField('minLevel'); + $maxLvl = $this->subject->getField('maxLevel'); + if ($level < $maxLvl) + $level .= ' - '.$maxLvl; + } + else // Boss Level + $level = '??'; + + $infobox[] = Lang::game('level').Lang::main('colon').$level; + + // Classification + if ($_ = $this->subject->getField('rank')) // != NPC_RANK_NORMAL + { + $str = $this->subject->isBoss() ? '[span class=icon-boss]'.Lang::npc('rank', $_).'[/span]' : Lang::npc('rank', $_); + $infobox[] = Lang::npc('classification', [$str]); + } + + // Reaction + $color = fn (int $r) : string => match($r) + { + 1 => 'q2', // q2 green + -1 => 'q10', // q10 red + default => 'q' // q yellow + }; + $infobox[] = Lang::npc('react', ['[color='.$color($this->subject->getField('A')).']A[/color] [color='.$color($this->subject->getField('H')).']H[/color]']); + + // Faction + $this->extendGlobalIds(Type::FACTION, $this->subject->getField('factionId')); + $infobox[] = Util::ucFirst(Lang::game('faction')).Lang::main('colon').'[faction='.$this->subject->getField('factionId').']'; + + // Tameable + if ($_typeFlags & NPC_TYPEFLAG_TAMEABLE) + if ($_ = $this->subject->getField('family')) + $infobox[] = Lang::npc('tameable', ['[url=pet='.$_.']'.Lang::game('fa', $_).'[/url]']); + + // Wealth + if ($_ = intVal(($this->subject->getField('minGold') + $this->subject->getField('maxGold')) / 2)) + $infobox[] = Lang::npc('worth', ['[tooltip=tooltip_avgmoneydropped][money='.$_.'][/tooltip]']); + + // is Vehicle + if ($this->subject->getField('vehicleId')) + $infobox[] = Lang::npc('vehicle'); + + // is visible as ghost (redundant to extraFlags) + 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 + $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')) + { + $buff = []; + for ($i = 0; $i < 31; $i++) + if ($immuneMask & (1 << $i)) + $buff[] = (!fMod(count($buff), 3) ? "\n" : '').'[url=?spells&filter=me='.($i + 1).']'.Lang::game('me', $i + 1).'[/url]'; + + $infobox[] = Lang::npc('mechanicimmune', [implode(', ', $buff)]); + } + + // extra flags + if ($flagsExtra = $this->subject->getField('flagsExtra')) + { + $buff = []; + foreach (Lang::npc('extraFlags') as $idx => $str) + if ($flagsExtra & $idx) + $buff[] = $str; + + if ($buff) + $infobox[] = Lang::npc('_extraFlags').'[ul][li]'.implode('[/li][li]', $buff).'[/li][/ul]'; + } + + // Mode dummy references + if ($this->altNPCs) + { + $this->extendGlobalData($this->altNPCs->getJSGlobals()); + $buff = Lang::npc('versions').'[ul]'; + foreach ($this->altNPCs->iterate() as $id => $__) + $buff .= '[li][npc='.$id.'][/li]'; + $infobox[] = $buff.'[/ul]'; + } + } + + if ($stats = $this->getCreatureStats($mapType, $_altIds)) + $infobox[] = Lang::npc('stats').($_altIds ? ' ('.Lang::game('modes', $mapType, 0).')' : '').Lang::main('colon').'[ul][li]'.implode('[/li][li]', $stats).'[/li][/ul]'; + + if ($infobox) + { + $this->infobox = new InfoboxMarkup($infobox, ['allow' => Markup::CLASS_STAFF, 'dbpage' => true], 'infobox-contents0'); + $this->extendGlobalData($this->infobox->getJsGlobals()); + } + + + /****************/ + /* Main Content */ + /****************/ + + // get spawns and path + if ($spawns = $this->subject->getSpawns(SPAWNINFO_FULL)) + { + $this->addDataLoader('zones'); + $this->map = array( + ['parent' => 'mapper-generic'], // Mapper + $spawns, // mapperData + null, // ShowOnMap + [Lang::npc('foundIn')] // foundIn + ); + foreach ($spawns as $areaId => $_) + $this->map[3][$areaId] = ZoneList::getName($areaId); + } + + // smart AI + $sai = null; + 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` = %i', $this->typeId); + while ($_ = array_pop($guids)) + { + $sai = new SmartAI(SmartAI::SRC_TYPE_CREATURE, -$_, ['baseEntry' => $this->typeId, 'title' => ' [small](for GUID: '.$_.')[/small]']); + if ($sai->prepare()) + break; + } + } + + if ($sai->prepare()) + { + $this->extendGlobalData($sai->getJSGlobals()); + $this->smartAI = $sai->getMarkup(); + } + else + trigger_error('Creature has `AIName`: SmartAI set in template but no SmartAI defined.'); + } + + // consider pooled spawns + $this->quotes = $this->getQuotes(); + $this->reputation = $this->getOnKillRep($_altIds, $mapType); + $this->redButtons = array( + BUTTON_WOWHEAD => true, + BUTTON_LINKS => ['type' => $this->type, 'typeId' => $this->typeId], + BUTTON_VIEW3D => ['type' => Type::NPC, 'typeId' => $this->typeId, 'displayId' => $this->subject->getRandomModelId()] + ); + + if ($this->subject->getField('humanoid')) + $this->redButtons[BUTTON_VIEW3D]['humanoid'] = 1; + + + /**************/ + /* Extra Tabs */ + /**************/ + + $this->lvTabs = new Tabs(['parent' => "\$\$WH.ge('tabs-generic')"], 'tabsRelated', true); + + // tab: abilities / tab_controlledabilities (dep: VehicleId) + $tplSpells = []; + $genSpells = []; + $spellClick = []; + $conditions = [DB::OR]; + + for ($i = 1; $i < 9; $i++) + if ($_ = $this->subject->getField('spell'.$i)) + $tplSpells[] = $_; + + if ($tplSpells) + $conditions[] = ['id', $tplSpells]; + + 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` = %i', $this->typeId)) + { + $auras = preg_replace('/[^\d ]/', ' ', $auras); // remove erroneous chars from string + $genSpells = array_merge($genSpells, array_filter(explode(' ', $auras))); + } + + 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'))) + { + $skill = 0; + $mask = 0x0; + foreach (Game::$skillLineMask[-1] as $idx => [$familyId, $skillLineId]) + { + if ($familyId != $_) + continue; + + $skill = $skillLineId; + $mask = 1 << $idx; + break; + } + $conditions[] = [ + DB::AND, + ['s.typeCat', -3], + [ + DB::OR, + ['skillLine1', $skill], + [DB::AND, ['skillLine1', 0, '>'], ['skillLine2OrMask', $skill]], + [DB::AND, ['skillLine1', -1], ['skillLine2OrMask', $mask, '&']] + ] + ]; + } + + if (count($conditions) > 1) + { + $abilities = new SpellList($conditions); + if (!$abilities->error) + { + $this->extendGlobalData($abilities->getJSGlobals(GLOBALINFO_SELF | GLOBALINFO_RELATED)); + $controled = $abilities->getListviewData(); + $normal = []; + + foreach ($controled as $id => $values) + { + if (isset($spellClick[$id])) + $values['spellclick'] = $spellClick[$id]; + + if (in_array($id, $genSpells)) + { + $normal[$id] = $values; + if (!in_array($id, $tplSpells)) + unset($controled[$id]); + } + } + + $cnd = new Conditions(); + $cnd->getBySource(Conditions::SRC_VEHICLE_SPELL, group: $this->typeId)->prepare(); + if ($cnd->toListviewColumn($controled, $extraCols, $this->typeId, 'id')) + $this->extendGlobalData($cnd->getJsGlobals()); + + if ($normal) + $this->lvTabs->addListviewTab(new Listview(array( + 'data' => $normal, + 'name' => '$LANG.tab_abilities', + 'id' => 'abilities' + ), SpellList::$brickFile)); + + if ($controled) + $this->lvTabs->addListviewTab(new Listview(array( + 'data' => $controled, + 'name' => '$LANG.tab_controlledabilities', + 'id' => 'controlled-abilities', + 'extraCols' => $extraCols ?: null + ), SpellList::$brickFile)); + } + } + + // tab: summoned by [spell] + $conditions = array( + 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); + if (!$sbSpell->error) + { + $this->extendGlobalData($sbSpell->getJSGlobals()); + + $this->lvTabs->addListviewTab(new Listview(array( + 'data' => $sbSpell->getListviewData(), + 'name' => '$LANG.tab_summonedby', + 'id' => 'summoned-by-spell' + ), SpellList::$brickFile)); + } + + // tab: summoned by [NPC] + $sb = SmartAI::getOwnerOfNPCSummon($this->typeId); + if (!empty($sb[Type::NPC])) + { + $sbNPC = new CreatureList(array(['id', $sb[Type::NPC]])); + if (!$sbNPC->error) + { + $this->extendGlobalData($sbNPC->getJSGlobals()); + + $this->addDataLoader('zones'); + $this->lvTabs->addListviewTab(new Listview(array( + 'data' => $sbNPC->getListviewData(), + 'name' => '$LANG.tab_summonedby', + 'id' => 'summoned-by-npc' + ), CreatureList::$brickFile)); + } + } + + // tab: summoned by [Object] + if (!empty($sb[Type::OBJECT])) + { + $sbGO = new GameObjectList(array(['id', $sb[Type::OBJECT]])); + if (!$sbGO->error) + { + $this->extendGlobalData($sbGO->getJSGlobals()); + + $this->addDataLoader('zones'); + $this->lvTabs->addListviewTab(new Listview(array( + 'data' => $sbGO->getListviewData(), + 'name' => '$LANG.tab_summonedby', + 'id' => 'summoned-by-object' + ), GameObjectList::$brickFile)); + } + } + + // tab: teaches + if ($this->subject->getField('npcflag') & NPC_FLAG_TRAINER) + { + $teachQuery = + '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` = %i'; + + if ($tSpells = DB::World()->selectAssoc($teachQuery, $this->typeId)) + { + $teaches = new SpellList(array(['id', array_keys($tSpells)])); + if (!$teaches->error) + { + $this->extendGlobalData($teaches->getJSGlobals(GLOBALINFO_SELF | GLOBALINFO_RELATED)); + $data = $teaches->getListviewData(); + + $extraCols = []; + $cnd = new Conditions(); + foreach ($tSpells as $sId => $train) + { + if (empty($data[$sId])) + continue; + + if ($_ = $train['reqSkillId']) + if (count($data[$sId]['skill']) == 1 && $_ != $data[$sId]['skill'][0]) + $cnd->addExternalCondition(Conditions::SRC_NONE, $sId, [Conditions::SKILL, $_, $train['reqSkillValue']]); + + for ($i = 1; $i < 3; $i++) + if ($_ = $train['reqSpellId'.$i]) + $cnd->addExternalCondition(Conditions::SRC_NONE, $sId, [Conditions::SPELL, $_]); + + if ($_ = $train['reqLevel']) + { + if (!isset($extraCols[1])) + $extraCols[1] = "\$Listview.funcBox.createSimpleCol('reqLevel', LANG.tooltip_reqlevel, '7%', 'reqLevel')"; + + $data[$sId]['reqLevel'] = $_; + } + + if ($_ = $train['cost']) + $data[$sId]['trainingcost'] = $_; + } + + if ($cnd->toListviewColumn($data, $extraCols)) + $this->extendGlobalData($cnd->getJsGlobals()); + + $this->lvTabs->addListviewTab(new Listview(array( + 'data' => $data, + 'name' => '$LANG.tab_teaches', + 'id' => 'teaches', + 'visibleCols' => ['trainingcost'], + 'extraCols' => $extraCols ?: null + ), SpellList::$brickFile)); + } + } + else + trigger_error('NPC '.$this->typeId.' is flagged as trainer, but doesn\'t have any spells set', E_USER_WARNING); + } + + // tab: sells + if ($sells = DB::World()->selectCol( + '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 = ''; + $extraCols = ["\$Listview.funcBox.createSimpleCol('stack', 'stack', '10%', 'stack')", '$Listview.extraCols.cost']; + + $lvData = $soldItems->getListviewData(ITEMINFO_VENDOR, [Type::NPC => [$this->typeId]]); + + if (array_column($lvData, 'condition')) + $extraCols[] = '$Listview.extraCols.condition'; + + if (array_filter(array_column($lvData, 'restock'))) + { + $extraCols[] = '$_'; + $colAddIn = 'vendorRestockCol'; + } + + $cnd = new Conditions(); + if ($cnd->getBySource(Conditions::SRC_NPC_VENDOR, group: $this->typeId)->prepare()) + { + $this->extendGlobalData($cnd->getJsGlobals()); + $cnd->toListviewColumn($lvData, $extraCols, $this->typeId, 'id'); + } + + $this->lvTabs->addListviewTab(new Listview(array( + 'data' => $lvData, + 'name' => '$LANG.tab_sells', + 'id' => 'currency-for', + 'extraCols' => array_unique($extraCols) + ), ItemList::$brickFile, $colAddIn)); + + $this->extendGlobalData($soldItems->getJSGlobals(GLOBALINFO_SELF | GLOBALINFO_RELATED)); + } + } + + // tabs: this creature contains.. + 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, [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'], ''] + ); + + /* 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) + { + 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 => $__) + { + 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[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 ($sourceFor as [$lootTpl, $lootEntries, $tabName, $tabId, $hiddenCols, $note]) + { + $creatureLoot = new LootByContainer(); + if ($creatureLoot->getByContainer($lootTpl, $lootEntries)) + { + $extraCols = $creatureLoot->extraCols; + 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(), + '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) + $tabData['note'] = ''.Lang::formatSkillBreakpoints(Game::getBreakpointsForSkill($skinTab[2], $this->subject->getField('maxLevel') * 5), Lang::FMT_HTML).''; + + $this->lvTabs->addListviewTab(new Listview($tabData, ItemList::$brickFile)); + } + } + + // tab: starts quest + // tab: ends quest + $startEnd = new QuestList(array(['qse.type', Type::NPC], ['qse.typeId', $this->typeId])); + if (!$startEnd->error) + { + $this->extendGlobalData($startEnd->getJSGlobals()); + $lvData = $startEnd->getListviewData(); + $start = $end = []; + + foreach ($startEnd->iterate() as $id => $__) + { + if ($startEnd->getField('method') & 0x1) + $start[] = $lvData[$id]; + if ($startEnd->getField('method') & 0x2) + $end[] = $lvData[$id]; + } + + if ($start) + $this->lvTabs->addListviewTab(new Listview(array( + 'data' => $start, + 'name' => '$LANG.tab_starts', + 'id' => 'starts' + ), QuestList::$brickFile)); + + if ($end) + $this->lvTabs->addListviewTab(new Listview(array( + 'data' => $end, + 'name' => '$LANG.tab_ends', + 'id' => 'ends' + ), QuestList::$brickFile)); + } + + // tab: objective of quest + $conditions = array( + 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) + { + $this->extendGlobalData($objectiveOf->getJSGlobals()); + + $this->lvTabs->addListviewTab(new Listview(array( + 'data' => $objectiveOf->getListviewData(), + 'name' => '$LANG.tab_objectiveof', + 'id' => 'objective-of' + ), QuestList::$brickFile)); + } + + // 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) + { + $this->extendGlobalData($crtOf->getJSGlobals()); + + $this->lvTabs->addListviewTab(new Listview(array( + 'data' => $crtOf->getListviewData(), + 'name' => '$LANG.tab_criteriaof', + 'id' => 'criteria-of' + ), AchievementList::$brickFile)); + } + + // tab: passengers + 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) + { + $data = $passengers->getListviewData(); + + if (User::isInGroup(U_GROUP_STAFF)) + foreach ($data as $id => &$d) + $d['seat'] = $_[$id]; + + $this->extendGlobalData($passengers->getJSGlobals(GLOBALINFO_SELF)); + + $tabData = array( + 'data' => $data, + 'name' => Lang::npc('accessory'), + 'id' => 'accessory' + ); + + if (User::isInGroup(U_GROUP_STAFF)) + $tabData['extraCols'] = ["\$Listview.funcBox.createSimpleCol('seat', '".Lang::npc('seat')."', '10%', 'seat')"]; + + $this->addDataLoader('zones'); + $this->lvTabs->addListviewTab(new Listview($tabData, CreatureList::$brickFile)); + } + } + + /* tab sounds: + * activity sounds => CreatureDisplayInfo.dbc => (CreatureModelData.dbc => ) CreatureSoundData.dbc + * AI => smart_scripts + * Dialogue VO => creature_text + * onClick VO => CreatureDisplayInfo.dbc => NPCSounds.dbc + */ + $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` = %i', $this->subject->getField('displayId1')); + array_shift($activitySounds); // remove id-column + $this->soundIds = array_merge($this->soundIds, array_values($activitySounds)); + + if ($this->soundIds) + { + $sounds = new SoundList(array(['id', $this->soundIds])); + if (!$sounds->error) + { + $data = $sounds->getListviewData(); + foreach ($activitySounds as $activity => $id) + if (isset($data[$id])) + $data[$id]['activity'] = $activity; // no index, js wants a string :( + + $this->extendGlobalData($sounds->getJSGlobals(GLOBALINFO_SELF)); + $this->lvTabs->addListviewTab(new Listview(array( + 'data' => $data, + 'visibleCols' => $activitySounds ? 'activity' : null + ), SoundList::$brickFile)); + } + } + + // tab: conditions + $cnd = new Conditions(); + $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()) + { + $this->extendGlobalData($cnd->getJsGlobals()); + $this->lvTabs->addDataTab(...$tab); + } + + parent::generate(); + } + + private function getRepForId(array $entries, array &$spillover) : array + { + $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 %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 %in AND `RewOnKillRepFaction2` > 0', + $entries, $entries + ); + + $factions = new FactionList(array(['id', array_column($rows, 'faction')])); + $result = []; + + foreach ($rows as $row) + { + if (!$factions->getEntry($row['faction'])) + continue; + + $set = array( + $row['faction'], // factionId + [$row['qty'], 0], // qty + $factions->getField('name', true), // name + $row['maxRank'] && $row['maxRank'] < REP_EXALTED ? Lang::game('rep', $row['maxRank']) : null, // cap + $row['npc'], // npcId + 0 // spilloverCat + ); + + $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) + $set[1][1] = $set[1][0] * $cuRate; + + if ($row['spillover']) + { + $spill = [[$set[1][0] / 2, 0], $row['maxRank']]; + if ($cuRate && User::isInGroup(U_GROUP_EMPLOYEE)) + $spill[0][1] = $spill[0][0] . sprintf(Util::$dfnString, Lang::faction('customRewRate'), ($set[1][0] > 0 ? '+' : '').($spill[0][0] * ($cuRate - 1) * 0.5)); + else if ($cuRate) + $spill[0][1] = $set[1][1] / 2; + + $spillover[$factions->getField('cat')] = $spill; + $set[5] = $factions->getField('cat'); // set spillover + } + + $result[] = $set; + } + + return $result; + } + + private function getOnKillRep(array $dummyIds, int $mapType) : array + { + $spilledParents = []; + $reputation = []; + + // base NPC + if ($base = $this->getRepForId([$this->typeId], $spilledParents)) + $reputation[] = [Lang::game('modes', 1, 0), $base]; + + // difficulty dummys + if ($dummyIds && ($mapType == 1 || $mapType == 2)) + { + $alt = []; + $rep = $this->getRepForId(array_keys($dummyIds), $spilledParents); + + // order by difficulty + foreach ($rep as $i => [, , , , $npcId]) + $alt[$dummyIds[$npcId]][] = $rep[$i]; + + // apply by difficulty + foreach ($alt as $mode => $dat) + $reputation[] = [Lang::game('modes', $mapType, $mode), $dat]; + } + + // get spillover factions and apply + if ($spilledParents) + { + $spilled = new FactionList(array(['parentFactionId', array_keys($spilledParents)])); + + foreach ($reputation as $i => [, $data]) + { + foreach ($data as [$factionId, , , , , $spillover]) + { + if (!$spillover) + continue; + + foreach ($spilled->iterate() as $spId => $__) + { + // find parent + if ($spilled->getField('parentFactionId') != $spillover) + continue; + + // don't readd parent + if ($factionId == $spId) + continue; + + $spMax = $spilledParents[$spillover][1]; + + $reputation[$i][1][] = array( + $spId, + $spilledParents[$spillover][0], + $spilled->getField('name', true), + $spMax && $spMax < REP_EXALTED ? Lang::game('rep', $spMax) : null + ); + } + } + } + } + + return $reputation; + } + + private function getQuotes() : ?array + { + [$quotes, $nQuotes, $soundIds] = Game::getQuotesForCreature($this->typeId, true, $this->subject->getField('name', true)); + + if ($soundIds) + $this->soundIds = array_merge($this->soundIds, $soundIds); + + return $quotes ? [$quotes, $nQuotes] : null; + } + + private function getCreatureStats(int $mapType, array $altIds) : array + { + $stats = []; + $modes = []; // get difficulty versions if set + $hint = '[tooltip name=%3$s][table cellspacing=10][tr]%1s[/tr][/table][/tooltip][span class=tip tooltip=%3$s]%2s[/span]'; + $modeRow = '[tr][td]%s  [/td][td]%s[/td][/tr]'; + // Health + $health = $this->subject->getBaseStats('health'); + $stats['health'] = Util::ucFirst(Lang::spell('powerTypes', -2)).Lang::main('colon').($health[0] < $health[1] ? Lang::nf($health[0]).' - '.Lang::nf($health[1]) : Lang::nf($health[0])); + + // Mana (may be 0) + $mana = $this->subject->getBaseStats('power'); + $stats['mana'] = $mana[0] ? Lang::spell('powerTypes', 0).Lang::main('colon').($mana[0] < $mana[1] ? Lang::nf($mana[0]).' - '.Lang::nf($mana[1]) : Lang::nf($mana[0])) : ''; + + // Armor + $armor = $this->subject->getBaseStats('armor'); + $stats['armor'] = Lang::npc('armor').($armor[0] < $armor[1] ? Lang::nf($armor[0]).' - '.Lang::nf($armor[1]) : Lang::nf($armor[0])); + + // Resistances + $resNames = [null, 'hol', 'fir', 'nat', 'fro', 'sha', 'arc']; + $tmpRes = []; + $res = $this->subject->getBaseStats('resistance'); // $sc => $amt + $stats['resistance'] = ''; + 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) + { + $stats['resistance'] = Lang::npc('resistances').'[br]'; + if (count($tmpRes) > 3) + $stats['resistance'] .= implode(' ', array_slice($tmpRes, 0, 3)).'[br]'.implode(' ', array_slice($tmpRes, 3)); + else + $stats['resistance'] .= implode(' ', $tmpRes); + } + + // Melee Damage + $melee = $this->subject->getBaseStats('melee'); + if ($_ = $this->subject->getField('dmgSchool')) // magic damage + $stats['melee'] = Lang::npc('melee').Lang::nf($melee[0]).' - '.Lang::nf($melee[1]).' ('.Lang::game('sc', $_).')'; + else // phys. damage + $stats['melee'] = Lang::npc('melee').Lang::nf($melee[0]).' - '.Lang::nf($melee[1]); + + // Ranged Damage + $ranged = $this->subject->getBaseStats('ranged'); + $stats['ranged'] = Lang::npc('ranged').Lang::nf($ranged[0]).' - '.Lang::nf($ranged[1]); + + foreach ($altIds as $id => $mode) + { + if (!$this->altNPCs->getEntry($id)) + continue; + + $m = Lang::game('modes', $mapType, $mode); + + // Health + $health = $this->altNPCs->getBaseStats('health'); + $modes['health'][] = sprintf($modeRow, $m, $health[0] < $health[1] ? Lang::nf($health[0]).' - '.Lang::nf($health[1]) : Lang::nf($health[0])); + + // Mana (may be 0) + $mana = $this->altNPCs->getBaseStats('power'); + $modes['mana'][] = $mana[0] ? sprintf($modeRow, $m, $mana[0] < $mana[1] ? Lang::nf($mana[0]).' - '.Lang::nf($mana[1]) : Lang::nf($mana[0])) : null; + + // Armor + $armor = $this->altNPCs->getBaseStats('armor'); + $modes['armor'][] = sprintf($modeRow, $m, $armor[0] < $armor[1] ? Lang::nf($armor[0]).' - '.Lang::nf($armor[1]) : Lang::nf($armor[0])); + + // Resistances + if (array_filter($this->altNPCs->getBaseStats('resistance'))) + { + if (!isset($modes['resistance'])) // init table head + $modes['resistance'][] = '[td][/td][td][span class="moneyschoolhol" style="margin: 0px 5px"][/span][/td][td][span class="moneyschoolfir" style="margin: 0px 5px"][/span][/td][td][span class="moneyschoolnat" style="margin: 0px 5px"][/span][/td][td][span class="moneyschoolfro" style="margin: 0px 5px"][/span][/td][td][span class="moneyschoolsha" style="margin: 0px 5px"][/span][/td][td][span class="moneyschoolarc" style="margin: 0px 5px"][/span][/td]'; + + if (!$stats['resistance']) // base creature has no resistance. -> display list item. + $stats['resistance'] = Lang::npc('resistances').'…'; + + $tmpRes = ''; + $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; + } + + // Melee Damage + $melee = $this->altNPCs->getBaseStats('melee'); + if ($_ = $this->altNPCs->getField('dmgSchool')) // magic damage + $modes['melee'][] = sprintf($modeRow, $m, Lang::nf($melee[0]).' - '.Lang::nf($melee[1]).' ('.Lang::game('sc', $_).')'); + else // phys. damage + $modes['melee'][] = sprintf($modeRow, $m, Lang::nf($melee[0]).' - '.Lang::nf($melee[1])); + + // Ranged Damage + $ranged = $this->altNPCs->getBaseStats('ranged'); + $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] = isset($modes[$k]) ? sprintf($hint, implode('[/tr][tr]', $modes[$k]), $v, $k) : $v; + + return $stats; + } +} + + +?> diff --git a/endpoints/npc/npc_power.php b/endpoints/npc/npc_power.php new file mode 100644 index 00000000..56fb95af --- /dev/null +++ b/endpoints/npc/npc_power.php @@ -0,0 +1,50 @@ + ['filter' => FILTER_CALLBACK, 'options' => [Locale::class, 'tryFromDomain']] + ); + + public function __construct(string $id) + { + parent::__construct($id); + + // temp locale + if ($this->_get['domain']) + Lang::load($this->_get['domain']); + + $this->typeId = intVal($id); + } + + protected function generate() : void + { + $creature = new CreatureList(array(['id', $this->typeId])); + if ($creature->error) + $this->cacheType = CACHE_TYPE_NONE; + else + $opts = array( + 'name' => $creature->getField('name', true), + 'tooltip' => $creature->renderTooltip(), + 'map' => $creature->getSpawns(SPAWNINFO_SHORT) + ); + + $this->result = new Tooltip(self::POWER_TEMPLATE, $this->typeId, $opts ?? []); + } +} + +?> diff --git a/endpoints/npcs/npcs.php b/endpoints/npcs/npcs.php new file mode 100644 index 00000000..de03c9a4 --- /dev/null +++ b/endpoints/npcs/npcs.php @@ -0,0 +1,139 @@ + ['filter' => FILTER_VALIDATE_REGEXP, 'options' => ['regexp' => Filter::PATTERN_PARAM]] + ); + protected array $validCats = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13]; + + public bool $petFamPanel = false; + + public function __construct(string $rawParam) + { + $this->getCategoryFromUrl($rawParam); + + parent::__construct($rawParam); + + if ($this->category) + $this->subCat = '='.implode('.', $this->category); + + $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; + } + + protected function generate() : void + { + $this->h1 = Lang::game('npcs'); + + $conditions = [Listview::DEFAULT_SIZE]; + if (!User::isInGroup(U_GROUP_EMPLOYEE)) + $conditions[] = [['cuFlags', CUSTOM_EXCLUDE_FOR_LISTVIEW, '&'], 0]; + + if ($_ = $this->filter->getConditions()) + $conditions[] = $_; + + if ($this->category) + { + $conditions[] = ['type', $this->category[0]]; + $this->petFamPanel = $this->category[0] == 1; + } + + $fiForm = $this->filter->values; + $fiRepCols = $this->filter->fiReputationCols; + + + /*************/ + /* Menu Path */ + /*************/ + + if ($this->category) + $this->breadcrumb[] = $this->category[0]; + + if (count($fiForm['fa']) == 1) + $this->breadcrumb[] = $fiForm['fa'][0]; + + + /**************/ + /* Page Title */ + /**************/ + + array_unshift($this->title, $this->h1); + if ($this->category) + array_unshift($this->title, Lang::npc('cat', $this->category[0])); + + if (count($fiForm['fa']) == 1) + array_unshift($this->title, Lang::game('fa', $fiForm['fa'][0])); + + + /****************/ + /* Main Content */ + /****************/ + + $this->redButtons[BUTTON_WOWHEAD] = true; + if ($fiQuery = $this->filter->buildGETParam()) + $this->wowheadLink .= '&filter='.$fiQuery; + + // beast subtypes are selected via filter + $tabData = ['data' => []]; + $npcs = new CreatureList($conditions, ['extraOpts' => $this->filter->extraOpts, 'calcTotal' => true]); + if (!$npcs->error) + { + $tabData['data'] = $npcs->getListviewData($fiRepCols ? NPCINFO_REP : 0x0); + if ($fiRepCols) // never use pretty-print + $tabData['extraCols'] = '$fi_getReputationCols('.Util::toJSON($fiRepCols, JSON_NUMERIC_CHECK | JSON_UNESCAPED_UNICODE).')'; + else if ($this->filter->fiExtraCols) + $tabData['extraCols'] = '$fi_getExtraCols(fi_extraCols, 0, 0)'; + + if ($this->category) + $tabData['hiddenCols'] = ['type']; + + // create note if search limit was exceeded + if ($npcs->getMatches() > Listview::DEFAULT_SIZE) + { + $tabData['note'] = sprintf(Util::$tryFilteringString, 'LANG.lvnote_npcsfound', $npcs->getMatches(), Listview::DEFAULT_SIZE); + $tabData['_truncated'] = 1; + } + } + + $this->lvTabs = new Tabs(['parent' => "\$\$WH.ge('tabs-generic')"]); + + $this->lvTabs->addListviewTab(new Listview($tabData, CreatureList::$brickFile)); + + parent::generate(); + + $this->setOnCacheLoaded([self::class, 'onBeforeDisplay']); + } + + public static function onBeforeDisplay() : void + { + // sort for dropdown-menus + Lang::sort('game', 'fa'); + } +} + +?> diff --git a/endpoints/object/object.php b/endpoints/object/object.php new file mode 100644 index 00000000..60d9281e --- /dev/null +++ b/endpoints/object/object.php @@ -0,0 +1,650 @@ +typeId = intVal($id); + $this->contribute = Type::getClassAttrib($this->type, 'contribute') ?? CONTRIBUTE_NONE; + } + + protected function generate() : void + { + $this->subject = new GameObjectList(array(['id', $this->typeId])); + if ($this->subject->error) + $this->generateNotFound(Lang::game('object'), Lang::gameObject('notFound')); + + $this->h1 = Lang::unescapeUISequences($this->subject->getField('name', true), Lang::FMT_HTML); + + $this->gPageInfo += array( + 'type' => $this->type, + 'typeId' => $this->typeId, + 'name' => $this->h1 + ); + + + /*************/ + /* Menu Path */ + /*************/ + + $this->breadcrumb[] = $this->subject->getField('typeCat'); + + + /**************/ + /* Page Title */ + /**************/ + + 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 */ + /***********/ + + $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` = %i', $this->typeId)) + { + $this->extendGlobalIds(Type::WORLDEVENT, ...$_); + $ev = []; + foreach ($_ as $i => $e) + $ev[] = ($i % 2 ? '[br]' : ' ') . '[event='.$e.']'; + + $infobox[] = Lang::game('eventShort', [implode(',', $ev)]); + } + + // 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='.$_.']'; + } + + // Reaction + $color = fn (int $r) : string => match($r) + { + 1 => 'q2', // q2 green + -1 => 'q10', // q10 red + default => 'q' // q yellow + }; + $infobox[] = Lang::npc('react', ['[color='.$color($this->subject->getField('A')).']A[/color] [color='.$color($this->subject->getField('H')).']H[/color]']); + + // reqSkill + difficulty + switch ($this->subject->getField('typeCat')) + { + case -3: // Herbalism + $infobox[] = Lang::game('requires', [Lang::spell('lockType', 2).' ('.$this->subject->getField('reqSkill').')']); + $infobox[] = Lang::formatSkillBreakpoints(Game::getBreakpointsForSkill(SKILL_HERBALISM, $this->subject->getField('reqSkill'))); + break; + case -4: // Mining + $infobox[] = Lang::game('requires', [Lang::spell('lockType', 3).' ('.$this->subject->getField('reqSkill').')']); + $infobox[] = Lang::formatSkillBreakpoints(Game::getBreakpointsForSkill(SKILL_MINING, $this->subject->getField('reqSkill'))); + break; + case -5: // Lockpicking + $infobox[] = Lang::game('requires', [Lang::spell('lockType', 1).' ('.$this->subject->getField('reqSkill').')']); + $infobox[] = Lang::formatSkillBreakpoints(Game::getBreakpointsForSkill(SKILL_LOCKPICKING, $this->subject->getField('reqSkill'))); + break; + default: // requires key .. maybe + { + $locks = Lang::getLocks($this->subject->getField('lockId'), $ids, true, Lang::FMT_MARKUP); + $l = []; + + foreach ($ids as $type => $typeIds) + $this->extendGlobalIds($type, ...$typeIds); + + foreach ($locks as $idx => $str) + { + if ($idx > 0) + $l[] = Lang::gameObject('key').Lang::main('colon').$str; + else if ($idx < 0) + $l[] = Lang::game('requires', [$str]); + } + + if ($l) + $infobox[] = implode('[br]', $l); + } + } + + // linked trap + if ($_ = $this->subject->getField('linkedTrap')) + { + $this->extendGlobalIds(Type::OBJECT, $_); + $infobox[] = Lang::gameObject('trap').Lang::main('colon').'[object='.$_.']'; + } + + // trap for X (note: moved to lv-tabs) + + // SpellFocus + if ($_ = $this->subject->getField('spellFocusId')) + { + 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]'; + + $infobox[] = '[tooltip name=focus]'.Lang::gameObject('focusDesc').'[/tooltip][span class=tip tooltip=focus]'.Lang::gameObject('focus').Lang::main('colon').$n.'[/span]'; + } + } + + // lootinfo: [min, max, restock] + if (([$min, $max, $restock] = $this->subject->getField('lootStack')) && $min) + { + $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 = 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 (only on type: OBJECT_CAPTURE_POINT) + if ([$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) // 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').Util::createNumRange($minPlayer, $maxPlayer).'[/li]'; + + if ($radius) + $buff .= '[li]'.Lang::spell('range', [$radius]).'[/li]'; + + if ($minTime > 1 || $minPlayer || $radius) + $buff .= '[/ul]'; + + $infobox[] = $buff; + } + + // 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')) + $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'); + + + /****************/ + /* Main Content */ + /****************/ + + // pageText / book + if ($this->book = Game::getBook($this->subject->getField('pageTextId'))) + $this->addScript( + [SC_JS_FILE, 'js/Book.js'], + [SC_CSS_FILE, 'css/Book.css'] + ); + + // get spawns and path + if ($spawns = $this->subject->getSpawns(SPAWNINFO_FULL)) + { + $this->addDataLoader('zones'); + $this->map = array( + ['parent' => 'mapper-generic'], // Mapper + $spawns, // mapperData + null, // ShowOnMap + [Lang::gameObject('foundIn')] // foundIn + ); + foreach ($spawns as $areaId => $_) + $this->map[3][$areaId] = ZoneList::getName($areaId); + } + + + // todo (low): consider pooled spawns + + + 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` = %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` = %i', $ll['npcId'])) + $this->relBoss = [$c['id'], Util::localizedString($c, 'name')]; + } + + // Smart AI + $sai = null; + if ($this->subject->getField('ScriptOrAI') == 'SmartGameObjectAI') + { + $sai = new SmartAI(SmartAI::SRC_TYPE_OBJECT, $this->typeId); + if (!$sai->prepare()) // no smartAI found .. check per guid + { + // at least one of many + $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]']); + if ($sai->prepare()) + break; + } + } + + if ($sai->prepare()) + { + $this->extendGlobalData($sai->getJSGlobals()); + $this->smartAI = $sai->getMarkup(); + } + else + trigger_error('Gameobject has `AIName`: SmartGameObjectAI set in template but no SmartAI defined.'); + } + + $this->redButtons = array( + BUTTON_WOWHEAD => true, + BUTTON_LINKS => ['type' => $this->type, 'typeId' => $this->typeId], + BUTTON_VIEW3D => ['displayId' => $this->subject->getField('displayId'), 'type' => Type::OBJECT, 'typeId' => $this->typeId] + ); + + + /**************/ + /* Extra Tabs */ + /**************/ + + $this->lvTabs = new Tabs(['parent' => "\$\$WH.ge('tabs-generic')"], 'tabsRelated', true); + + // tab: summoned by + $summonEffects = array( + SPELL_EFFECT_TRANS_DOOR, + SPELL_EFFECT_SUMMON_OBJECT_WILD, + SPELL_EFFECT_SUMMON_OBJECT_SLOT1, + SPELL_EFFECT_SUMMON_OBJECT_SLOT2, + SPELL_EFFECT_SUMMON_OBJECT_SLOT3, + SPELL_EFFECT_SUMMON_OBJECT_SLOT4 + ); + $conditions = array( + 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); + if (!$summons->error) + { + $this->extendGlobalData($summons->getJSGlobals(GLOBALINFO_SELF | GLOBALINFO_RELATED)); + + $this->lvTabs->addListviewTab(new Listview(array( + 'data' => $summons->getListviewData(), + 'id' => 'summoned-by', + 'name' => '$LANG.tab_summonedby' + ), SpellList::$brickFile)); + } + + // tab: related spells + if ($_ = $this->subject->getField('spells')) + { + $relSpells = new SpellList(array(['id', $_])); + if (!$relSpells->error) + { + $this->extendGlobalData($relSpells->getJSGlobals(GLOBALINFO_SELF | GLOBALINFO_RELATED)); + $data = $relSpells->getListviewData(); + + foreach ($data as $relId => $d) + $data[$relId]['trigger'] = array_search($relId, $_); + + $this->lvTabs->addListviewTab(new Listview(array( + 'data' => $data, + 'id' => 'spells', + 'name' => '$LANG.tab_spells', + 'hiddenCols' => ['skill'], + 'extraCols' => ["\$Listview.funcBox.createSimpleCol('trigger', 'Condition', '10%', 'trigger')"] + ), SpellList::$brickFile)); + } + } + + // tab: criteria of + $acvs = new AchievementList(array(['ac.type', [ACHIEVEMENT_CRITERIA_TYPE_USE_GAMEOBJECT, ACHIEVEMENT_CRITERIA_TYPE_FISH_IN_GAMEOBJECT]], ['ac.value1', $this->typeId])); + if (!$acvs->error) + { + $this->extendGlobalData($acvs->getJSGlobals(GLOBALINFO_SELF | GLOBALINFO_RELATED)); + + $this->lvTabs->addListviewTab(new Listview(array( + 'data' => $acvs->getListviewData(), + 'id' => 'criteria-of', + 'name' => '$LANG.tab_criteriaof' + ), AchievementList::$brickFile)); + } + + // tab: starts quest + // tab: ends quest + $startEnd = new QuestList(array(['qse.type', Type::OBJECT], ['qse.typeId', $this->typeId])); + if (!$startEnd->error) + { + $this->extendGlobalData($startEnd->getJSGlobals()); + $lvData = $startEnd->getListviewData(); + $start = $end = []; + + foreach ($startEnd->iterate() as $id => $__) + { + if ($startEnd->getField('method') & 0x1) + $start[] = $lvData[$id]; + if ($startEnd->getField('method') & 0x2) + $end[] = $lvData[$id]; + } + + if ($start) + $this->lvTabs->addListviewTab(new Listview(array( + 'data' => $start, + 'name' => '$LANG.tab_starts', + 'id' => 'starts' + ), QuestList::$brickFile)); + + if ($end) + $this->lvTabs->addListviewTab(new Listview(array( + 'data' => $end, + 'name' => '$LANG.tab_ends', + 'id' => 'ends' + ), QuestList::$brickFile)); + } + + // tab: related quests + if ($_ = $this->subject->getField('reqQuest')) + { + $relQuest = new QuestList(array(['id', $_])); + if (!$relQuest->error) + { + $this->extendGlobalData($relQuest->getJSGlobals()); + + $this->lvTabs->addListviewTab(new Listview(array( + 'data' => $relQuest->getListviewData(), + 'name' => '$LANG.tab_quests', + 'id' => 'quests' + ), QuestList::$brickFile)); + } + } + + // tab: contains + if ($_ = $this->subject->getField('lootId')) + { + // 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)) + { + // 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(); + + foreach ($hiddenCols as $k => $str) + { + if ($k == 1 && array_filter(array_column($lootResult, $str), fn ($x) => $x != SIDE_BOTH)) + unset($hiddenCols[$k]); + else if ($k != 1 && !array_filter(array_column($lootResult, $str))) + unset($hiddenCols[$k]); + } + + $this->lvTabs->addListviewTab(new Listview(array( + '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)); + } + } + + // tab: Spell Focus for + if ($sfId = $this->subject->getField('spellFocusId')) + { + $focusSpells = new SpellList(array(Listview::DEFAULT_SIZE, ['spellFocusObject', $sfId]), ['calcTotal' => true]); + if (!$focusSpells->error) + { + $tabData = array( + 'data' => $focusSpells->getListviewData(), + 'name' => Lang::gameObject('focus'), + 'id' => 'focus-for' + ); + + $this->extendGlobalData($focusSpells->getJSGlobals(GLOBALINFO_SELF | GLOBALINFO_RELATED)); + + // create note if search limit was exceeded + if ($focusSpells->getMatches() > Listview::DEFAULT_SIZE) + { + $tabData['note'] = sprintf(Util::$tryNarrowingString, 'LANG.lvnote_spellsfound', $focusSpells->getMatches(), Listview::DEFAULT_SIZE); + $tabData['_truncated'] = 1; + } + + $this->lvTabs->addListviewTab(new Listview($tabData, SpellList::$brickFile)); + } + } + + // tab: trap for X + $trigger = new GameObjectList(array(['linkedTrap', $this->typeId])); + if (!$trigger->error) + { + $this->extendGlobalData($trigger->getJSGlobals()); + + $this->addDataLoader('zones'); + $this->lvTabs->addListviewTab(new Listview(array( + 'data' => $trigger->getListviewData(), + 'name' => Lang::gameObject('triggeredBy'), + 'id' => 'triggerd-by', + 'note' => sprintf(Util::$filterResultString, '?objects=6') + ), 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) + { + $this->extendGlobalData($sameModel->getJSGlobals()); + + $this->addDataLoader('zones'); + $this->lvTabs->addListviewTab(new Listview(array( + 'data' => $sameModel->getListviewData(), + 'name' => '$LANG.tab_samemodelas', + 'id' => 'same-model-as' + ), GameObjectList::$brickFile)); + } + + // tab: condition-for + $cnd = new Conditions(); + $cnd->getByCondition(Type::OBJECT, $this->typeId)->prepare(); + if ($tab = $cnd->toListviewTab('condition-for', '$LANG.tab_condition_for')) + { + $this->extendGlobalData($cnd->getJsGlobals()); + $this->lvTabs->addDataTab(...$tab); + } + + parent::generate(); + } +} + +?> diff --git a/endpoints/object/object_power.php b/endpoints/object/object_power.php new file mode 100644 index 00000000..bfd73a60 --- /dev/null +++ b/endpoints/object/object_power.php @@ -0,0 +1,50 @@ + ['filter' => FILTER_CALLBACK, 'options' => [Locale::class, 'tryFromDomain']] + ); + + public function __construct(string $id) + { + parent::__construct($id); + + // temp locale + if ($this->_get['domain']) + Lang::load($this->_get['domain']); + + $this->typeId = intVal($id); + } + + protected function generate() : void + { + $object = new GameObjectList(array(['id', $this->typeId])); + if ($object->error) + $this->cacheType = CACHE_TYPE_NONE; + else + $opts = array( + 'name' => Lang::unescapeUISequences($object->getField('name', true), Lang::FMT_RAW), + 'tooltip' => $object->renderTooltip(), + 'map' => $object->getSpawns(SPAWNINFO_SHORT) + ); + + $this->result = new Tooltip(self::POWER_TEMPLATE, $this->typeId, $opts ?? []); + } +} + +?> diff --git a/endpoints/objects/objects.php b/endpoints/objects/objects.php new file mode 100644 index 00000000..cf4d8976 --- /dev/null +++ b/endpoints/objects/objects.php @@ -0,0 +1,113 @@ + ['filter' => FILTER_VALIDATE_REGEXP, 'options' => ['regexp' => Filter::PATTERN_PARAM]] + ); + protected array $validCats = [-2, -3, -4, -5, -6, 0, 3, 6, 9, 25]; + + public bool $petFamPanel = false; + + public function __construct(string $rawParam) + { + $this->getCategoryFromUrl($rawParam); + + parent::__construct($rawParam); + + if ($this->category) + $this->subCat = '='.implode('.', $this->category); + + $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; + } + + protected function generate() : void + { + $this->h1 = Util::ucFirst(Lang::game('objects')); + + $conditions = [Listview::DEFAULT_SIZE]; + if (!User::isInGroup(U_GROUP_EMPLOYEE)) + $conditions[] = [['cuFlags', CUSTOM_EXCLUDE_FOR_LISTVIEW, '&'], 0]; + + if ($_ = $this->filter->getConditions()) + $conditions[] = $_; + + if ($this->category) + $conditions[] = ['typeCat', (int)$this->category[0]]; + + + /*************/ + /* Menu Path */ + /*************/ + + if ($this->category) + $this->breadcrumb[] = $this->category[0]; + + + /**************/ + /* Page Title */ + /**************/ + + array_unshift($this->title, $this->h1); + if ($this->category) + array_unshift($this->title, Lang::gameObject('cat', $this->category[0])); + + + /****************/ + /* Main Content */ + /****************/ + + $this->redButtons[BUTTON_WOWHEAD] = true; + if ($fiQuery = $this->filter->buildGETParam()) + $this->wowheadLink .= '&filter='.$fiQuery; + + $tabData = ['data' => []]; + $objects = new GameObjectList($conditions, ['extraOpts' => $this->filter->extraOpts, 'calcTotal' => true]); + if (!$objects->error) + { + $tabData['data'] = $objects->getListviewData(); + if ($objects->hasSetFields('reqSkill')) + $tabData['visibleCols'] = ['skill']; + + // create note if search limit was exceeded + if ($objects->getMatches() > Listview::DEFAULT_SIZE) + { + $tabData['note'] = sprintf(Util::$tryFilteringString, 'LANG.lvnote_objectsfound', $objects->getMatches(), Listview::DEFAULT_SIZE); + $tabData['_truncated'] = 1; + } + } + + $this->lvTabs = new Tabs(['parent' => "\$\$WH.ge('tabs-generic')"]); + + $this->lvTabs->addListviewTab(new Listview($tabData, GameObjectList::$brickFile)); + + parent::generate(); + } +} + +?> diff --git a/endpoints/pet/pet.php b/endpoints/pet/pet.php new file mode 100644 index 00000000..70064a1d --- /dev/null +++ b/endpoints/pet/pet.php @@ -0,0 +1,222 @@ +typeId = intVal($id); + $this->contribute = Type::getClassAttrib($this->type, 'contribute') ?? CONTRIBUTE_NONE; + } + + protected function generate() : void + { + $this->subject = new PetList(array(['id', $this->typeId])); + if ($this->subject->error) + $this->generateNotFound(Lang::game('pet'), Lang::pet('notFound')); + + $this->h1 = $this->subject->getField('name', true); + + $this->gPageInfo += array( + 'type' => $this->type, + 'typeId' => $this->typeId, + 'name' => $this->h1 + ); + + + /*************/ + /* Menu Path */ + /*************/ + + $this->breadcrumb[] = $this->subject->getField('type'); + + + /**************/ + /* Page Title */ + /**************/ + + array_unshift($this->title, $this->h1, Util::ucFirst(Lang::game('pet'))); + + + /***********/ + /* Infobox */ + /***********/ + + $infobox = Lang::getInfoBoxForFlags($this->subject->getField('cuFlags')); + + // level range + $infobox[] = Lang::game('level').Lang::main('colon').$this->subject->getField('minLevel').' - '.$this->subject->getField('maxLevel'); + + // exotic + 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]'; + $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'); + + + /****************/ + /* Main Content */ + /****************/ + + $this->headIcons = [$this->subject->getField('iconString')]; + $this->expansion = Util::$expansionString[$this->subject->getField('expansion')]; + $this->redButtons = array( + BUTTON_WOWHEAD => true, + BUTTON_LINKS => ['type' => $this->type, 'typeId' => $this->typeId], + BUTTON_TALENT => ['href' => '?petcalc#'.Util::$tcEncoding[(int)($this->typeId / 10)] . Util::$tcEncoding[(2 * ($this->typeId % 10) + ($this->subject->getField('exotic') ? 1 : 0))], 'pet' => true] + ); + + + /**************/ + /* Extra Tabs */ + /**************/ + + $this->lvTabs = new Tabs(['parent' => "\$\$WH.ge('tabs-generic')"], 'tabsRelated', true); + + // tab: tameable & gallery + $condition = array( + ['ct.type', 1], // Beast + ['ct.typeFlags', NPC_TYPEFLAG_TAMEABLE, '&'], + ['ct.family', $this->typeId], // displayed petType + [ + DB::OR, // at least neutral to at least one faction + ['ft.A', 1, '<'], + ['ft.H', 1, '<'] + ] + ); + $tng = new CreatureList($condition); + + $this->addDataLoader('zones'); + $this->lvTabs->addListviewTab(new Listview(array( + 'data' => $tng->getListviewData(NPCINFO_TAMEABLE), + 'name' => '$LANG.tab_tameable', + 'hiddenCols' => ['type'], + 'visibleCols' => ['skin'], + 'note' => sprintf(Util::$filterResultString, '?npcs=1&filter=fa=38'), + 'id' => 'tameable' + ), CreatureList::$brickFile)); + + $this->lvTabs->addListviewTab(new Listview(['data' => $tng->getListviewData(NPCINFO_MODEL)], 'model')); + + // tab: diet + $list = []; + $mask = $this->subject->getField('foodMask'); + for ($i = 1; $i < 9; $i++) + if ($mask & (1 << ($i - 1))) + $list[] = $i; + + $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( + 'data' => $food->getListviewData(), + 'name' => '$LANG.diet', + 'hiddenCols' => ['source', 'slot', 'side'], + 'sort' => ['level'], + 'id' => 'diet' + ), ItemList::$brickFile)); + + // tab: spells + $mask = 0x0; + foreach (Game::$skillLineMask[-1] as $idx => [$familyId,]) + { + if ($familyId == $this->typeId) + { + $mask = 1 << $idx; + break; + } + } + $conditions = [ + ['s.typeCat', -3], // Pet-Ability + [ + DB::OR, + // match: first skillLine + ['skillLine1', $this->subject->getField('skillLineId')], + // match: second skillLine (if not mask) + [DB::AND, ['skillLine1', 0, '>'], ['skillLine2OrMask', $this->subject->getField('skillLineId')]], + // match: skillLineMask (if mask) + [DB::AND, ['skillLine1', -1], ['skillLine2OrMask', $mask, '&']] + ] + ]; + + $spells = new SpellList($conditions); + $this->extendGlobalData($spells->getJSGlobals(GLOBALINFO_SELF)); + + $this->lvTabs->addListviewTab(new Listview(array( + 'data' => $spells->getListviewData(), + 'name' => '$LANG.tab_abilities', + 'visibleCols' => ['schools', 'level'], + 'id' => 'abilities' + ), SpellList::$brickFile)); + + // tab: talents + $conditions = array( + ['s.typeCat', -7], + [ // last rank or unranked + DB::OR, + ['s.cuFlags', SPELL_CU_LAST_RANK, '&'], + ['s.rankNo', 0] + ] + ); + + $conditions[] = match($this->subject->getField('type')) + { + PET_TALENT_TYPE_FEROCITY => ['s.cuFlags', SPELL_CU_PET_TALENT_TYPE0, '&'], + PET_TALENT_TYPE_TENACITY => ['s.cuFlags', SPELL_CU_PET_TALENT_TYPE1, '&'], + PET_TALENT_TYPE_CUNNING => ['s.cuFlags', SPELL_CU_PET_TALENT_TYPE2, '&'] + }; + + $talents = new SpellList($conditions); + $this->extendGlobalData($talents->getJSGlobals(GLOBALINFO_SELF)); + + $this->lvTabs->addListviewTab(new Listview(array( + 'data' => $talents->getListviewData(), + 'visibleCols' => ['tier', 'level'], + 'name' => '$LANG.tab_talents', + 'id' => 'talents', + 'sort' => ['tier', 'name'], + '_petTalents' => 1 + ), SpellList::$brickFile)); + + parent::generate(); + } +} + +?> diff --git a/endpoints/petcalc/petcalc.php b/endpoints/petcalc/petcalc.php new file mode 100644 index 00000000..cff9539b --- /dev/null +++ b/endpoints/petcalc/petcalc.php @@ -0,0 +1,40 @@ +h1 = Lang::main('petCalc'); + $this->chooseType = Lang::main('chooseFamily'); + + array_unshift($this->title, $this->h1); + + parent::generate(); + } +} + +?> diff --git a/endpoints/pets/pets.php b/endpoints/pets/pets.php new file mode 100644 index 00000000..c95fcc5c --- /dev/null +++ b/endpoints/pets/pets.php @@ -0,0 +1,88 @@ +getCategoryFromUrl($rawParam); + + parent::__construct($rawParam); + } + + protected function generate() : void + { + $this->h1 = Util::ucFirst(Lang::game('pets')); + + + /*************/ + /* Menu Path */ + /*************/ + + if ($this->category) + $this->breadcrumb[] = $this->category[0]; + + + /**************/ + /* Page Title */ + /**************/ + + array_unshift($this->title, $this->h1); + if ($this->category) + array_unshift($this->title, Lang::pet('cat', $this->category[0])); + + + /****************/ + /* Main Content */ + /****************/ + + $this->redButtons[BUTTON_WOWHEAD] = true; + + $conditions = [Listview::DEFAULT_SIZE]; + + if (!User::isInGroup(U_GROUP_EMPLOYEE)) + $conditions[] = [['cuFlags', CUSTOM_EXCLUDE_FOR_LISTVIEW, '&'], 0]; + + if ($this->category) + $conditions[] = ['type', $this->category[0]]; + + $tabData = []; + $pets = new PetList($conditions); + if (!$pets->error) + { + $this->extendGlobalData($pets->getJSGlobals(GLOBALINFO_RELATED)); + + $tabData = array( + 'data' => $pets->getListviewData(), + 'visibleCols' => ['abilities'], + 'computeDataFunc' => '$_', + 'hiddenCols' => !$pets->hasDiffFields('type') ? ['type'] : null + ); + }; + + $this->lvTabs = new Tabs(['parent' => "\$\$WH.ge('tabs-generic')"]); + + $this->lvTabs->addListviewTab(new Listview($tabData, PetList::$brickFile, 'petFoodCol')); + + parent::generate(); + } +} + +?> diff --git a/endpoints/privilege/privilege.php b/endpoints/privilege/privilege.php new file mode 100644 index 00000000..9065193e --- /dev/null +++ b/endpoints/privilege/privilege.php @@ -0,0 +1,71 @@ + 'REP_REQ_COMMENT', // write comments + 2 => 'REP_REQ_EXT_LINKS', // post external links + // 4 => 'REP_REQ_NO_CAPTCHA', // NYI no captcha + 5 => 'REP_REQ_SUPERVOTE', // votes count for more + 9 => 'REP_REQ_VOTEMORE_BASE', // more votes per day + 10 => 'REP_REQ_UPVOTE', // can upvote + 11 => 'REP_REQ_DOWNVOTE', // can downvote + 12 => 'REP_REQ_REPLY', // can reply + 13 => 'REP_REQ_BORDER_UNCOMMON', // uncommon avatar border + 14 => 'REP_REQ_BORDER_RARE', // rare avatar border + 15 => 'REP_REQ_BORDER_EPIC', // epic avatar border + 16 => 'REP_REQ_BORDER_LEGENDARY', // legendary avatar border + 17 => 'REP_REQ_PREMIUM' // premium status + ); + + public function __construct(string $rawParam) + { + $this->getCategoryFromUrl($rawParam); + + parent::__construct($rawParam); + + if (!$rawParam) + $this->generateError(); + + // apply actual values + $this->repVal = Cfg::get($this->req2priv[$rawParam]); + } + + protected function generate() : void + { + $this->h1 = Lang::privileges('_privileges', $this->category[0]); + + array_unshift($this->title, $this->h1); + + $this->breadcrumb[] = $this->category[0]; + + $this->privReqPoints = Lang::privileges('reqPoints', [Lang::nf($this->repVal)]); + + parent::generate(); + + $this->result->registerDisplayHook('article', [self::class, 'articleHook']); + } + + public static function articleHook(Template\PageTemplate &$pt, Markup &$article) : void + { + $article->apply(Cfg::applyToString(...)); + } +} + +?> diff --git a/endpoints/privileges/privileges.php b/endpoints/privileges/privileges.php new file mode 100644 index 00000000..83857f04 --- /dev/null +++ b/endpoints/privileges/privileges.php @@ -0,0 +1,65 @@ + 'REP_REQ_COMMENT', // write comments + 2 => 'REP_REQ_EXT_LINKS', // post external links + // 4 => 'REP_REQ_NO_CAPTCHA', // NYI no captcha + 5 => 'REP_REQ_SUPERVOTE', // votes count for more + 9 => 'REP_REQ_VOTEMORE_BASE', // more votes per day + 10 => 'REP_REQ_UPVOTE', // can upvote + 11 => 'REP_REQ_DOWNVOTE', // can downvote + 12 => 'REP_REQ_REPLY', // can reply + 13 => 'REP_REQ_BORDER_UNCOMMON', // uncommon avatar border + 14 => 'REP_REQ_BORDER_RARE', // rare avatar border + 15 => 'REP_REQ_BORDER_EPIC', // epic avatar border + 16 => 'REP_REQ_BORDER_LEGENDARY', // legendary avatar border + 17 => 'REP_REQ_PREMIUM' // premium status + ); + + public function __construct(string $rawParam) + { + parent::__construct($rawParam); + + if ($rawParam) + $this->generateError(); + + // apply actual values and order by requirement ASC + foreach ($this->req2priv as &$var) + $var = Cfg::get($var); + + asort($this->req2priv); + } + + protected function generate() : void + { + $this->h1 = Lang::main('moreTitles', $this->pageName); + + array_unshift($this->title, $this->h1); + + foreach (array_filter($this->req2priv) as $id => $val) + $this->privileges[$id] = array( + User::getReputation() >= $val, + Lang::privileges('_privileges', $id), + $val + ); + + parent::generate(); + } +} + +?> diff --git a/endpoints/profile/avatar.php b/endpoints/profile/avatar.php new file mode 100644 index 00000000..aa8a9ed2 --- /dev/null +++ b/endpoints/profile/avatar.php @@ -0,0 +1,68 @@ + ['filter' => FILTER_VALIDATE_REGEXP, 'options' => ['regexp' => '/^\d+\.jpg$/'] ], + 'size' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkTextLine']] + ); + + public function __construct(string $rawParam) + { + parent::__construct($rawParam); + + if (!Cfg::get('PROFILER_ENABLE')) + $this->generate404(); + } + + /* params + id: + size: [optional] + return: + + */ + protected function generate() : void + { + if (!$this->assertGET('id')) + $this->generate404(); + + $profileId = substr($this->_get['id'], 0, -4); + + $charData = DB::Aowow()->selectRow('SELECT `race`, `gender` FROM ::profiler_profiles WHERE id = %i', $profileId); + if (!$charData) + $this->generate404(); + + $gender = $charData['gender'] ? 'female' : 'male'; + $race = ChrRace::tryFrom($charData['race'])?->json() ?? 'human'; + $size = match($this->_get['size']) + { + 'small', + 'medium', + 'large' => $this->_get['size'], + default => 'medium' + }; + + $this->redirectTo = sprintf('%s/images/armory/%s/default_%s_%s.jpg', Cfg::get('STATIC_URL'), $size, $race, $gender); + } +} + +?> diff --git a/endpoints/profile/delete.php b/endpoints/profile/delete.php new file mode 100644 index 00000000..ed7572fd --- /dev/null +++ b/endpoints/profile/delete.php @@ -0,0 +1,47 @@ + ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkIdList']], + ); + + public function __construct(string $rawParam) + { + parent::__construct($rawParam); + + if (!Cfg::get('PROFILER_ENABLE')) + $this->generate404(); + } + + /* params + id: + return + null + */ + protected function generate() : void + { + if (!$this->assertGET('id')) + { + trigger_error('ProfileDeleteResponse - profileId empty', E_USER_WARNING); + 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()->qry('UPDATE ::profiler_profiles SET `deleted` = 1 WHERE %and', $where); + } +} + +?> diff --git a/endpoints/profile/link.php b/endpoints/profile/link.php new file mode 100644 index 00000000..ad15f385 --- /dev/null +++ b/endpoints/profile/link.php @@ -0,0 +1,50 @@ + ['filter' => FILTER_VALIDATE_INT] + ); + + public function __construct(string $rawParam) + { + parent::__construct($rawParam); + + if (!Cfg::get('PROFILER_ENABLE')) + $this->generate404(); + } + + /* params + id: + return: + null + */ + protected function generate() : void // links char with account + { + if (!$this->assertGET('id')) + { + trigger_error('ProfileLinkResponse - profileId empty', E_USER_ERROR); + return; + } + + // only link characters, not custom profiles + $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)) + trigger_error('ProfileLinkResponse - some of the profileIds were custom or do not exist', E_USER_ERROR); + } +} + +?> diff --git a/endpoints/profile/load.php b/endpoints/profile/load.php new file mode 100644 index 00000000..17277037 --- /dev/null +++ b/endpoints/profile/load.php @@ -0,0 +1,302 @@ + ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkIdList'] ], + 'items' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkItemList']] + ); + + public function __construct(string $rawParam) + { + parent::__construct($rawParam); + + if (!Cfg::get('PROFILER_ENABLE')) + $this->generate404(); + } + + /* params + id: profileId + items: string [itemIds.join(':')] + unnamed: unixtime [only to force the browser to reload instead of cache] + return + lots... + */ + protected function generate() : void + { + // titles, achievements, characterData, talents, pets + // and some onLoad-hook to .. load it registerProfile($data) + // everything else goes through data.php .. strangely enough + + if (!$this->assertGET('id')) + { + trigger_error('ProfileLoadResponse - profileId empty', E_USER_ERROR); + 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` = %i', $this->_get['id'][0]); + if (!$pBase) + { + trigger_error('ProfileLoadResponse - called with invalid profileId #'.$this->_get['id'][0], E_USER_WARNING); + return; + } + + if ($pBase['deleted'] && !User::isInGroup(U_GROUP_ADMIN | U_GROUP_BUREAU)) + return; + + + $rData = []; + foreach (Profiler::getRealms() as $rId => $rData) + if ($rId == $pBase['realm']) + break; + + if ($pBase['realm'] && !$rData) // realm doesn't exist or access is restricted + return; + + $profile = array( + 'id' => $pBase['id'], + 'source' => $pBase['id'], + 'level' => $pBase['level'], + 'classs' => $pBase['class'], + 'race' => $pBase['race'], + 'faction' => ChrRace::tryFrom($pBase['race'])?->getTeam() ?? TEAM_NEUTRAL, + 'gender' => $pBase['gender'], + 'skincolor' => $pBase['skincolor'], + 'hairstyle' => $pBase['hairstyle'], + 'haircolor' => $pBase['haircolor'], + 'facetype' => $pBase['facetype'], + 'features' => $pBase['features'], + 'title' => $pBase['title'], + 'name' => $pBase['name'], + 'guild' => "$'".$pBase['guildname']."'", + 'published' => !!($pBase['cuFlags'] & PROFILER_CU_PUBLISHED), + 'pinned' => !!($pBase['cuFlags'] & PROFILER_CU_PINNED), + 'nomodel' => $pBase['nomodelMask'], + 'playedtime' => $pBase['playedtime'], + 'lastupdated' => $pBase['lastupdated'] * 1000, + 'talents' => array( + 'builds' => array( // notice the bullshit to prevent the talent-string from becoming a float! NOTICE IT!! + ['talents' => '$"'.$pBase['talentbuild1'].'"', 'glyphs' => $pBase['glyphs1']], + ['talents' => '$"'.$pBase['talentbuild2'].'"', 'glyphs' => $pBase['glyphs2']] + ), + 'active' => $pBase['activespec'] + ), + // set later + 'inventory' => [], + 'bookmarks' => [], // list of userIds who claimed this profile (claiming and owning are two different things) + + // completion lists: [subjectId => amount/timestamp/1] + 'skills' => [], // skillId => [curVal, maxVal] + 'reputation' => [], // factionId => curVal + 'titles' => [], // titleId => 1 + 'spells' => [], // spellId => 1; recipes, vanity pets, mounts + 'achievements' => [], // achievementId => timestamp + 'quests' => [], // questId => 1 + 'achievementpoints' => 0, // max you have + 'statistics' => [], // all raid activity [achievementId => killCount] + 'activity' => [], // recent raid activity [achievementId => 1] (is a subset of statistics) + ); + + if ($pBase['custom']) + { + // this parameter is _really_ strange .. probably still not doing this right + $profile['source'] = $pBase['realm'] ? $pBase['sourceId'] : 0; + + $profile['sourcename'] = $pBase['sourceName']; + $profile['description'] = $pBase['description']; + $profile['user'] = $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 :( + if ($pBase['realm']) + { + $profile['region'] = [$rData['region'], Lang::profiler('regions', $rData['region'])]; + $profile['battlegroup'] = [Profiler::urlize(Cfg::get('BATTLEGROUP')), Cfg::get('BATTLEGROUP')]; + $profile['realm'] = [Profiler::urlize($rData['name'], true), $rData['name']]; + } + + // bookmarks + 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` = %i', $pBase['id'])) + $profile['arenateams'] = $at; + + // pets if hunter fields: [name:name, family:petFamily, npc:npcId, displayId:modelId, talents:talentString] + 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()->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) + { + if (!$cu['icon']) + unset($cu['icon']); + + $profile['customs'][$id] = array_values($cu); + } + } + + + /* $profile[] + // CUSTOM + 'auras' => [], // custom list of buffs, debuffs [spellId] + + // UNUSED + 'glyphs' => [], // provided list of already known glyphs (post cataclysm feature) + */ + + + // questId => [cat1, cat2] + $profile['quests'] = []; + if ($quests = DB::Aowow()->selectCol('SELECT `questId` FROM ::profiler_completion_quests WHERE `id` = %i', $pBase['id'])) + { + $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()->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` = %i', $pBase['id']); + + // titleId => 1 + $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` = %i', $pBase['id']); + + // just points + $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` = %i', $pBase['id']); + + // achievementId => 1 + $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` = %i', $pBase['id']); + + + $gItems = []; + + $usedSlots = []; + if ($this->_get['items']) + { + $phItems = new ItemList(array(['id', $this->_get['items']], ['slot', INVTYPE_NON_EQUIP, '!'])); + if (!$phItems->error) + { + $data = $phItems->getListviewData(ITEMINFO_JSON | ITEMINFO_SUBITEMS); + foreach ($phItems->iterate() as $iId => $__) + { + $sl = $phItems->getField('slot'); + foreach (Profiler::$slot2InvType as $slot => $invTypes) + { + if (in_array($sl, $invTypes) && !in_array($slot, $usedSlots)) + { + // get and apply inventory + $gItems[$iId] = array( + 'name_'.Lang::getLocale()->json() => $phItems->getField('name', true), + 'quality' => $phItems->getField('quality'), + 'icon' => $phItems->getField('iconString'), + 'jsonequip' => $data[$iId] + ); + $profile['inventory'][$slot] = [$iId, 0, 0, 0, 0, 0, 0, 0]; + + $usedSlots[] = $slot; + break; + } + } + } + } + } + + if ($items = DB::Aowow()->selectAssoc('SELECT * FROM ::profiler_items WHERE `id` = %i', $pBase['id'])) + { + $itemz = new ItemList(array(['id', array_column($items, 'item')])); + if (!$itemz->error) + { + $data = $itemz->getListviewData(ITEMINFO_JSON | ITEMINFO_SUBITEMS); + + foreach ($items as $i) + { + if ($itemz->getEntry($i['item']) && !in_array($i['slot'], $usedSlots)) + { + // get and apply inventory + $gItems[$i['item']] = array( + 'name_'.Lang::getLocale()->json() => $itemz->getField('name', true), + 'quality' => $itemz->getField('quality'), + 'icon' => $itemz->getField('iconString'), + 'jsonequip' => $data[$i['item']] + ); + $profile['inventory'][$i['slot']] = [$i['item'], $i['subItem'], $i['permEnchant'], $i['tempEnchant'], $i['gem1'], $i['gem2'], $i['gem3'], $i['gem4']]; + } + } + } + } + + $buff = ''; + foreach ($gItems as $id => $item) + $buff .= 'g_items.add('.$id.', '.Util::toJSON($item, JSON_NUMERIC_CHECK | JSON_UNESCAPED_UNICODE).");\n"; + + + // if ($au = $char->getField('auras')) + // { + // $auraz = new SpellList(array(['id', $char->getField('auras')])); + // $dataz = $auraz->getListviewData(); + // $modz = $auraz->getProfilerMods(); + + // // get and apply aura-mods + // foreach ($dataz as $id => $data) + // { + // $mods = []; + // if (!empty($modz[$id])) + // { + // foreach ($modz[$id] as $k => $v) + // { + // if (is_array($v)) + // $mods[] = $v; + // else if ($str = @Game::$itemMods[$k]) + // $mods[$str] = $v; + // } + // } + + // $buff .= 'g_spells.add('.$id.", {id:".$id.", name:'".Util::jsEscape(mb_substr($data['name'], 1))."', icon:'".$data['icon']."', callback:".Util::toJSON($mods)."});\n"; + // } + // $buff .= "\n"; + // } + + + // load available titles + Util::loadStaticFile('p-titles-'.$pBase['gender'], $buff, true); + + // add profile to buffer + $buff .= "\n\n\$WowheadProfiler.registerProfile(".Util::toJSON($profile).");"; + + $this->result = $buff."\n"; + } + + protected static function checkItemList(string $val) : array + { + // expecting item-list + if (preg_match('/\d+(:\d+)*/', $val)) + return array_map('intVal', explode(':', $val)); + + return []; + } +} + +?> diff --git a/endpoints/profile/pin.php b/endpoints/profile/pin.php new file mode 100644 index 00000000..9b871a44 --- /dev/null +++ b/endpoints/profile/pin.php @@ -0,0 +1,58 @@ + ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkIdList'] ], + 'user' => ['filter' => FILTER_CALLBACK, 'options' => [Util::class, 'validateUsername']] + ); + + public function __construct(string $rawParam) + { + parent::__construct($rawParam); + + if (!Cfg::get('PROFILER_ENABLE')) + $this->generate404(); + } + + /* params + id: + user: [optional] + return: null + */ + protected function generate() : void // (un)favorite + { + if (!$this->assertGET('id')) + { + trigger_error('ProfilePinResponse - profileId empty', E_USER_ERROR); + return; + } + + $uid = 0; + 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(%s)', $this->_get['user']); + + if (!$uid) + { + trigger_error('ProfilePinResponse - user "'.$this->_get['user'].'" does not exist', E_USER_ERROR); + return; + } + + // since only one character can be pinned at a time we can reset everything + 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()->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 new file mode 100644 index 00000000..c42a50c2 --- /dev/null +++ b/endpoints/profile/private.php @@ -0,0 +1,64 @@ + ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkIdList'] ], + 'user' => ['filter' => FILTER_CALLBACK, 'options' => [Util::class, 'validateUsername']], + // 'bookmarked' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkEmptySet'] ] // something with signatures? (must have bookmarked profile to create signature from) + ); + + public function __construct(string $rawParam) + { + parent::__construct($rawParam); + + if (!Cfg::get('PROFILER_ENABLE')) + $this->generate404(); + } + + /* params + id: + user: [optional] // user page this is may be executed from + return: + null + */ + protected function generate() : void + { + if (!$this->assertGET('id')) + { + trigger_error('ProfilePrivateResponse - profileId empty', E_USER_ERROR); + return; + } + + if ($this->_get['user'] && User::$username != $this->_get['user'] && !User::isInGroup(U_GROUP_ADMIN | U_GROUP_BUREAU)) + { + trigger_error('ProfilePrivateResponse - user #'.User::$id.' tried to mark profiles of "'.$this->_get['user'].'" as private.', E_USER_ERROR); + return; + } + + $uid = 0; + 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(%s)', $this->_get['user']); + + if (!$uid) + { + trigger_error('ProfilePrivateResponse - user "'.$this->_get['user'].'" does not exist', E_USER_ERROR); + return; + } + + 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 new file mode 100644 index 00000000..53a7f8be --- /dev/null +++ b/endpoints/profile/profile.php @@ -0,0 +1,187 @@ + Profiler > New + + protected array $dataLoader = ['enchants', 'gems', 'glyphs', 'itemsets', 'pets', 'pet-talents', 'quick-excludes', 'realms', 'statistics', 'weight-presets', 'achievements']; + protected array $scripts = array( + [SC_JS_FILE, 'js/filters.js'], + [SC_JS_FILE, 'js/TalentCalc.js'], + [SC_JS_FILE, 'js/profile_all.js'], + [SC_JS_FILE, 'js/profile.js'], + [SC_JS_FILE, 'js/Profiler.js'], + [SC_CSS_FILE, 'css/talentcalc.css'], + [SC_CSS_FILE, 'css/Profiler.css'] + ); + protected array $expectedGET = array( + 'new' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkEmptySet']] + ); + + public int $type = Type::PROFILE; + public bool $gDataKey = true; + + public function __construct(string $idOrProfile) + { + parent::__construct($idOrProfile); + + if (!Cfg::get('PROFILER_ENABLE')) + $this->generateError(); + + // neither param nor &new > error + if (!$idOrProfile && !$this->_get['new']) + $this->generateError(); + + // display empty/new profile editor > ok + if (!$idOrProfile && $this->_get['new']) + return; + + $this->getSubjectFromUrl($idOrProfile); + + // we have an ID > ok + if ($this->typeId) + return; + + // param was incomplete profile > error + if (!$this->subjectName) + $this->notFound(); + + $rnItr = 0; + // pending rename + if (preg_match('/^([^\-]+)-(\d+)$/i', $this->subjectName, $m)) + { + $this->subjectName = $m[1]; + $rnItr = $m[2]; + } + + // 3 possibilities + // 1) already synced to aowow + 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['stub']) + $this->handleIncompleteData(Type::PROFILE, $subject['realmGUID']); + + return; + } + + // can not be used to look up char on realm + if ($rnItr) + $this->notFound(); + + // 2) not yet synced but exists on realm (and not a gm character) + $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` = %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['stub'] = 1; + + if ($subject['at_login'] & 0x1) + $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 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()->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()->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; + } + + // 3) does not exist at all + $this->notFound(); + } + + protected function generate() : void + { + if ($this->doResync) + { + parent::generate(); + return; + } + + if ($this->typeId) + { + $subject = new LocalProfileList(array(['id', $this->typeId])); + if ($subject->error) + $this->notFound(); + + if (!$subject->isVisibleToUser()) + $this->notFound(); + + // character profile accessed by id + if (!$subject->isCustom() && !$this->subjectName) + $this->forward($subject->getProfileUrl()); + } + + parent::generate(); + + array_unshift($this->title, Util::ucFirst(Lang::game('profile'))); + + + // as demanded by the raid activity tracker + $bossIds = array( + // ruby: Halion + 39863, + // icc: Valanar, Lana'thel, Saurfang, Festergut, Deathwisper, Marrowgar, Putricide, Rotface, Sindragosa, Valithria, Lich King + 37970, 37955, 37813, 36626, 36855, 36612, 36678, 36627, 36853, 36789, 36597, + // toc: Jaraxxus, Anub'arak + 34780, 34564, + // ony: Onyxia + 10184, + // uld: Flame Levi, Ignis, Razorscale, XT-002, Kologarn, Auriaya, Freya, Hodir, Mimiron, Thorim, Vezaxx, Yogg, Algalon + 33113, 33118, 33186, 33293, 32930, 33515, 32906, 32845, 33350, 32864, 33271, 33288, 32871, + // nax: Anub, Faerlina, Maexxna, Noth, Heigan, Loatheb, Razuvious, Gothik, Patchwerk, Grobbulus, Gluth, Thaddius, Sapphiron, Kel'Thuzad + 15956, 15953, 15952, 15954, 15936, 16011, 16061, 16060, 16028, 15931, 15932, 15928, 15989, 15990 + ); + $this->extendGlobalIds(Type::NPC, ...$bossIds); + + // dummy title from dungeon encounter + foreach (Lang::profiler('encounterNames') as $id => $name) + $this->extendGlobalData([Type::NPC => [$id => ['name_'.Lang::getLocale()->json() => $name]]]); + } + + private function notFound() : never + { + if ($this->subjectName && $this->realm) + $head = Lang::profiler('firstUseTitle', [Util::ucFirst($this->subjectName), $this->realm]); + else + $head = Lang::profiler('profiler'); + + // unsetting typeId to prevent it from being added to the title string in the input-box is jank galore + // but it isn't needed for the not-found case anyway, right...? + unset($this->typeId); + + parent::generateNotFound($head, Lang::profiler('notFound', 'profile')); + } +} + +?> diff --git a/endpoints/profile/profile_power.php b/endpoints/profile/profile_power.php new file mode 100644 index 00000000..cbc0d0e6 --- /dev/null +++ b/endpoints/profile/profile_power.php @@ -0,0 +1,88 @@ + ['filter' => FILTER_CALLBACK, 'options' => [Locale::class, 'tryFromDomain']] + ); + + public function __construct(private string $rawParam) + { + parent::__construct($rawParam); + + if (!Cfg::get('PROFILER_ENABLE')) + $this->generate404(); + + // temp locale + if ($this->_get['domain']) + Lang::load($this->_get['domain']); + + $this->getSubjectFromUrl($rawParam); + + 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` = %i AND `custom` = 0 AND `name` = %s AND `renameItr` = %i', $this->realmId, Util::ucWords($this->subjectName), $renameItr ?? 0)) + $this->typeId = $x; + } + + if (!$this->typeId) + $this->generate404(); + } + + protected function generate() : void + { + $profile = new LocalProfileList(array(['id', $this->typeId])); + if ($profile->error || !$profile->isVisibleToUser()) + $this->cacheType = CACHE_TYPE_NONE; + else + { + $n = $profile->getField('name'); + $r = $profile->getField('race'); + $c = $profile->getField('class'); + $g = $profile->getField('gender'); + $l = $profile->getField('level'); + + if (!$this->subjectName) // implicit isCustom + $n .= Lang::profiler('customProfile'); + else if ($_ = $profile->getField('title')) + if ($title = (new TitleList(array(['id', $_])))?->getField($g ? 'female' : 'male', true)) + $n = sprintf($title, $n); + + $opts = array( + 'name' => $n, + 'tooltip' => $profile->renderTooltip(), + 'icon' => '$$WH.g_getProfileIcon('.$r.', '.$c.', '.$g.', '.$l.', \''.$profile->getIcon().'\')' + ); + } + + 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 = urlencode($this->rawParam) . ($ri ?? ''); + else + $id = $this->typeId; + + $this->result = new Tooltip(self::POWER_TEMPLATE, $id, $opts ?? []); + } +} + +?> diff --git a/endpoints/profile/public.php b/endpoints/profile/public.php new file mode 100644 index 00000000..98f70eae --- /dev/null +++ b/endpoints/profile/public.php @@ -0,0 +1,64 @@ + ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkIdList'] ], + 'user' => ['filter' => FILTER_CALLBACK, 'options' => [Util::class, 'validateUsername']], + // 'bookmarked' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkEmptySet'] ] // something with signatures? (must have bookmarked profile to create signature from) + ); + + public function __construct(string $rawParam) + { + parent::__construct($rawParam); + + if (!Cfg::get('PROFILER_ENABLE')) + $this->generate404(); + } + + /* params + id: + user: [optional] // user page this is may be executed from + return: + null + */ + protected function generate() : void + { + if (!$this->assertGET('id')) + { + trigger_error('ProfilePublicResponse - profileId empty', E_USER_ERROR); + return; + } + + if ($this->_get['user'] && User::$username != $this->_get['user'] && !User::isInGroup(U_GROUP_ADMIN | U_GROUP_BUREAU)) + { + trigger_error('ProfilePublicResponse - user #'.User::$id.' tried to mark profiles of "'.$this->_get['user'].'" as public.', E_USER_ERROR); + return; + } + + $uid = 0; + 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(%s)', $this->_get['user']); + + if (!$uid) + { + trigger_error('ProfilePublicResponse - user "'.$this->_get['user'].'" does not exist', E_USER_ERROR); + return; + } + + 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/purge.php b/endpoints/profile/purge.php new file mode 100644 index 00000000..05d63a64 --- /dev/null +++ b/endpoints/profile/purge.php @@ -0,0 +1,22 @@ + + data: [string, tabName] + return + null + */ + protected function generate() : void { } // removes completion data (as uploaded by the wowhead client) Just fail silently if someone triggers this manually +} + +?> diff --git a/endpoints/profile/resync.php b/endpoints/profile/resync.php new file mode 100644 index 00000000..0b93276a --- /dev/null +++ b/endpoints/profile/resync.php @@ -0,0 +1,42 @@ + ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkIdList']] + ); + + public function __construct(string $rawParam) + { + parent::__construct($rawParam); + + if (!Cfg::get('PROFILER_ENABLE')) + $this->generate404(); + } + + /* params + id: + return: + 1 + */ + protected function generate() : void + { + 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']); + } + else + trigger_error('ProfileResyncResponse - profiles '.implode(', ', $this->_get['id']).' not found in db', E_USER_ERROR); + + $this->result = 1; + } +} + +?> diff --git a/endpoints/profile/save.php b/endpoints/profile/save.php new file mode 100644 index 00000000..70a4e9f7 --- /dev/null +++ b/endpoints/profile/save.php @@ -0,0 +1,204 @@ + ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkIdList']], + ); + protected array $expectedPOST = array( + 'name' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkTextLine'] ], + 'level' => ['filter' => FILTER_VALIDATE_INT ], + 'class' => ['filter' => FILTER_VALIDATE_INT ], + 'race' => ['filter' => FILTER_VALIDATE_INT ], + 'gender' => ['filter' => FILTER_VALIDATE_INT ], + 'nomodel' => ['filter' => FILTER_VALIDATE_INT ], + 'talenttree1' => ['filter' => FILTER_VALIDATE_INT ], + 'talenttree2' => ['filter' => FILTER_VALIDATE_INT ], + 'talenttree3' => ['filter' => FILTER_VALIDATE_INT ], + 'activespec' => ['filter' => FILTER_VALIDATE_INT ], + 'talentbuild1' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkTalentString']], + 'glyphs1' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkGlyphString'] ], + 'talentbuild2' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkTalentString']], + 'glyphs2' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkGlyphString'] ], + 'icon' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkTextLine'] ], + 'description' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkTextBlob'] ], + 'source' => ['filter' => FILTER_VALIDATE_INT ], + 'copy' => ['filter' => FILTER_VALIDATE_INT ], + 'public' => ['filter' => FILTER_VALIDATE_INT ], + 'gearscore' => ['filter' => FILTER_VALIDATE_INT ], + 'inv' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkIdList'], 'flags' => FILTER_REQUIRE_ARRAY] + ); + + public function __construct(string $rawParam) + { + parent::__construct($rawParam); + + if (!Cfg::get('PROFILER_ENABLE')) + $this->generate404(); + } + + /* params (get)) + id: [0: new profile] + params (post) + [see below] + return: + proileId [onSuccess] + -1 [onError] + */ + protected function generate() : void + { + $cuProfile = array( + 'user' => User::$id, + // 'userName' => User::$username, + 'name' => $this->_post['name'], + 'level' => $this->_post['level'], + 'class' => $this->_post['class'], + 'race' => $this->_post['race'], + 'gender' => $this->_post['gender'], + 'nomodelMask' => $this->_post['nomodel'], + 'talenttree1' => $this->_post['talenttree1'], + 'talenttree2' => $this->_post['talenttree2'], + 'talenttree3' => $this->_post['talenttree3'], + 'talentbuild1' => $this->_post['talentbuild1'], + 'talentbuild2' => $this->_post['talentbuild2'], + 'activespec' => $this->_post['activespec'], + 'glyphs1' => $this->_post['glyphs1'], + 'glyphs2' => $this->_post['glyphs2'], + 'gearscore' => $this->_post['gearscore'], + 'icon' => $this->_post['icon'], + '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) + if (strstr($cuProfile['icon'], 'profile=avatar')) + $cuProfile['icon'] = ''; + + if ($_ = $this->_post['description']) + $cuProfile['description'] = $_; + + if ($_ = $this->_post['source']) // should i also set sourcename? + $cuProfile['sourceId'] = $_; + + 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` = %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` = %i', $cuProfile['sourceId']); + + $charId = -1; + if ($id = $this->_get['id'][0]) // update + { + 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` = %i AND `deleted` = 0 AND `custom` = 1', User::$id); + if ($nProfiles < 10 || User::isPremium()) + if ($newId = DB::Aowow()->qry('INSERT INTO ::profiler_profiles %v', $cuProfile)) + $charId = $newId; + } + + // update items + if ($charId != -1) + { + // ok, 'funny' thing: whether an item has en extra prismatic socket is determined contextually + // either the socket is -1 or it has an itemId on an index where there shouldn't be one + $keys = ['id', 'slot', 'item', 'subitem', 'permEnchant', 'tempEnchant', 'gem1', 'gem2', 'gem3', 'gem4']; + + // validate Enchantments + $enchIds = array_merge( + array_column($this->_post['inv'], 3), // perm enchantments + array_column($this->_post['inv'], 4) // temp enchantments (not used..?) + ); + $enchs = new EnchantmentList(array(['id', $enchIds])); + + // validate items + $itemIds = array_merge( + array_column($this->_post['inv'], 1), // base item + array_column($this->_post['inv'], 5), // gem slot 1 + array_column($this->_post['inv'], 6), // gem slot 2 + array_column($this->_post['inv'], 7), // gem slot 3 + array_column($this->_post['inv'], 8) // gem slot 4 + ); + + $items = new ItemList(array(['id', $itemIds])); + if (!$items->error) + { + 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()->qry('DELETE FROM ::profiler_items WHERE `id` = %i AND `slot` = %i', $charId, $itemData[0]); + continue; + } + + // item does not exist + if (!$items->getEntry($itemData[1])) + continue; + + // sub-item check + if (!$items->getRandEnchantForItem($itemData[1])) + $itemData[2] = 0; + + // item sockets are fubar + $nSockets = $items->json[$itemData[1]]['nsockets'] ?? 0; + $nSockets += in_array($slot, [SLOT_WAIST, SLOT_WRISTS, SLOT_HANDS]) ? 1 : 0; + for ($i = 5; $i < 9; $i++) + if ($itemData[$i] > 0 && (!$items->getEntry($itemData[$i]) || $i >= (5 + $nSockets))) + $itemData[$i] = 0; + + // item enchantments are borked + if ($itemData[3] && !$enchs->getEntry($itemData[3])) + $itemData[3] = 0; + + if ($itemData[4] && !$enchs->getEntry($itemData[4])) + $itemData[4] = 0; + + // looks good + array_unshift($itemData, $charId); + DB::Aowow()->qry('REPLACE INTO ::profiler_items %v', array_combine($keys, $itemData)); + } + } + } + + $this->result = $charId; + } + + protected static function checkTalentString(string $val) : string + { + if (preg_match('/^\d+$/', $val)) + return $val; + + return ''; + } + + protected static function checkGlyphString(string $val) : string + { + if (preg_match('/^\d+(:\d+)*$/', $val)) + return $val; + + return ''; + } +} + +?> diff --git a/endpoints/profile/status.php b/endpoints/profile/status.php new file mode 100644 index 00000000..ac086f84 --- /dev/null +++ b/endpoints/profile/status.php @@ -0,0 +1,50 @@ + ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkIdList'] ], + 'guild' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkEmptySet']], + 'arena-team' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkEmptySet']] + ); + + public function __construct(string $rawParam) + { + parent::__construct($rawParam); + + if (!Cfg::get('PROFILER_ENABLE')) + $this->generate404(); + } + + /* params + id: + return + + */ + protected function generate() : void + { + // 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 %in', $this->_get['id']); + else if ($this->_get['arena-team']) + $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['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]]); + } + + $this->result = Profiler::resyncStatus(Type::PROFILE, $ids); + } +} + +?> diff --git a/endpoints/profile/summary.php b/endpoints/profile/summary.php new file mode 100644 index 00000000..26eddd1d --- /dev/null +++ b/endpoints/profile/summary.php @@ -0,0 +1,17 @@ + diff --git a/endpoints/profile/unlink.php b/endpoints/profile/unlink.php new file mode 100644 index 00000000..322521e3 --- /dev/null +++ b/endpoints/profile/unlink.php @@ -0,0 +1,62 @@ + ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkIdList'] ], + 'user' => ['filter' => FILTER_CALLBACK, 'options' => [Util::class, 'validateUsername']] + ); + + public function __construct(string $rawParam) + { + parent::__construct($rawParam); + + if (!Cfg::get('PROFILER_ENABLE')) + $this->generate404(); + } + + /* params + id: + user: [optional] // user page this is may be executed from + return: + null + */ + protected function generate() : void // links char with account + { + if (!$this->assertGET('id')) + { + trigger_error('ProfileUnlinkResponse - profileId empty', E_USER_ERROR); + return; + } + + if ($this->_get['user'] && User::$username != $this->_get['user'] && !User::isInGroup(U_GROUP_ADMIN | U_GROUP_BUREAU)) + { + trigger_error('ProfileUnlinkResponse - user #'.User::$id.' tried to unlink profiles from "'.$this->_get['user'], E_USER_ERROR); + return; + } + + $uid = 0; + 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(%s)', $this->_get['user']); + + if (!$uid) + { + trigger_error('ProfileUnlinkResponse - user "'.$this->_get['user'].'" does not exist', E_USER_ERROR); + return; + } + + 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 new file mode 100644 index 00000000..dceb00f8 --- /dev/null +++ b/endpoints/profile/unpin.php @@ -0,0 +1,55 @@ + ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkIdList'] ], + 'user' => ['filter' => FILTER_CALLBACK, 'options' => [Util::class, 'validateUsername']] + ); + + public function __construct(string $rawParam) + { + parent::__construct($rawParam); + + if (!Cfg::get('PROFILER_ENABLE')) + $this->generate404(); + } + + /* params + id: + user: [optional] + return: null + */ + protected function generate() : void // (un)favorite + { + if (!$this->assertGET('id')) + { + trigger_error('ProfileUnpinResponse - profileId empty', E_USER_ERROR); + return; + } + + $uid = 0; + 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(%s)', $this->_get['user']); + + if (!$uid) + { + trigger_error('ProfileUnpinResponse - user "'.$this->_get['user'].'" does not exist', E_USER_ERROR); + return; + } + + DB::Aowow()->qry('UPDATE ::account_profiles SET `extraFlags` = `extraFlags` & ~%i WHERE `accountId` = %i', PROFILER_CU_PINNED, $uid); + } +} + +?> diff --git a/endpoints/profiler/profiler.php b/endpoints/profiler/profiler.php new file mode 100644 index 00000000..eb6f79e0 --- /dev/null +++ b/endpoints/profiler/profiler.php @@ -0,0 +1,52 @@ +generateError(); + } + + protected function generate() : void + { + // just so the form does not break. There won't be any results. + $usedRegions = array_column(Profiler::getRealms(), 'region') ?: ['us']; + foreach (Util::$regions as $idx => $id) + if (in_array($id, $usedRegions)) + $this->regions[$id] = [Lang::profiler('regions', $id), $idx + 1]; + + if (!in_array($this->rg, $usedRegions)) + $this->rg = key($this->regions); + + array_unshift($this->title, Util::ucFirst(Lang::profiler('profiler'))); + + parent::generate(); + } +} + +?> diff --git a/endpoints/profiles/profiles.php b/endpoints/profiles/profiles.php new file mode 100644 index 00000000..8821cb0c --- /dev/null +++ b/endpoints/profiles/profiles.php @@ -0,0 +1,224 @@ + Profiler > Characters + + protected array $dataLoader = ['weight-presets', 'realms']; + protected array $scripts = array( + [SC_JS_FILE, 'js/filters.js'], + [SC_JS_FILE, 'js/profile_all.js'], + [SC_JS_FILE, 'js/profile.js'], + [SC_CSS_FILE, 'css/Profiler.css'] + ); + protected array $expectedGET = array( + 'filter' => ['filter' => FILTER_VALIDATE_REGEXP, 'options' => ['regexp' => Filter::PATTERN_PARAM]], + // 1 guild; 2,3,4 arenateam (4 => 5-man): puts a resync button on the lv (was probably used before arenateams and guilds had a dedicated page) + 'roster' => ['filter' => FILTER_VALIDATE_INT, 'options' => ['min_value' => 1, 'max_value' => 4]] + ); + + public int $type = Type::PROFILE; + public string $roster = ''; + + private int $sumSubjects = 0; + + public function __construct(string $rawParam) + { + $this->getSubjectFromUrl($rawParam); + + parent::__construct($rawParam); + + if (!Cfg::get('PROFILER_ENABLE')) + $this->generateError(); + + $realms = []; + foreach (Profiler::getRealms() as $idx => $r) + { + if ($this->region && $r['region'] != $this->region) + continue; + + 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` <= %i AND (`extra_flags` & ?) = 0', MAX_LEVEL, Profiler::CHAR_GMFLAGS); + $realms[] = $idx; + } + + 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; + } + + protected function generate() : void + { + $this->h1 = Util::ucFirst(Lang::game('profiles')); + + + /*************/ + /* Menu Path */ + /*************/ + + $this->followBreadcrumbPath(); + + + /**************/ + /* Page Title */ + /**************/ + + if ($this->realm) + array_unshift($this->title, $this->realm,/* Cfg::get('BATTLEGROUP'),*/ Lang::profiler('regions', $this->region), Lang::game('profiles')); + else if ($this->region) + array_unshift($this->title, Lang::profiler('regions', $this->region), Lang::game('profiles')); + else + array_unshift($this->title, Lang::game('profiles')); + + + /****************/ + /* Main Content */ + /****************/ + + $conditions = [Listview::DEFAULT_SIZE]; + if ($_ = $this->filter->getConditions()) + $conditions[] = $_; + + $fiExtraCols = $this->filter->fiExtraCols; + + $lvData = []; + $lvExtraCols = []; + $lvVisibleCols = ['race', 'classs', 'level', 'talents', 'achievementpoints', 'gearscore']; + $lvHiddenCols = []; + $lvNote = ''; + $lv_truncated = 0; + + $this->getRegions(); + + foreach ($fiExtraCols as $skill => $idx) + $lvExtraCols[] = "\$Listview.funcBox.createSimpleCol('skill-' + ".$skill.", g_spell_skills[".$skill."], '7%', 'skill-' + ".$skill.")"; + + if (!$this->filter->useLocalList) + { + $conditions[] = ['deleteInfos_Name', null]; + $conditions[] = ['level', MAX_LEVEL, '<=']; // prevents JS errors + $conditions[] = [['extra_flags', Profiler::CHAR_GMFLAGS, '&'], 0]; + } + + $miscParams = ['calcTotal' => true]; + if ($this->realm) + $miscParams['sv'] = $this->realm; + if ($this->region) + $miscParams['rg'] = $this->region; + if ($_ = $this->filter->extraOpts) + $miscParams['extraOpts'] = $_; + + if ($this->filter->useLocalList) + $profiles = new LocalProfileList($conditions, $miscParams); + else + $profiles = new RemoteProfileList($conditions, $miscParams); + + if (!$profiles->error) + { + // init these chars on our side and get local ids + if (!$this->filter->useLocalList) + $profiles->initializeLocalEntries(); + + // Roster only if single realm selected + $roster = $this->realmId ? $this->_get['roster'] : 0; + if (!$roster && $this->realmId) + if (count($r = $this->filter->getSetCriteria(9, 12, 15, 18)) == 1) + $roster = ($r[0] - 6) / 3; // 1, 2, 3, or 4 + + $addInfoMask = PROFILEINFO_CHARACTER; + + // team rating filters + if ($this->filter->getSetCriteria(13, 16, 19)) + { + $lvVisibleCols[] = 'rating'; + $addInfoMask |= PROFILEINFO_ARENA; + } + + // init roster-listview + if ($roster == 1 && !$profiles->hasDiffFields('guild') && $profiles->getField('guild')) + { + $lvVisibleCols[] = 'guildrank'; + $lvHiddenCols[] = 'guild'; + + $this->roster = Lang::profiler('guildRoster', [$profiles->getField('guildname')]); + } + else if ($roster && !$profiles->hasDiffFields('arenateam') && $profiles->getField('arenateam')) + { + $lvVisibleCols[] = 'rating'; + + $addInfoMask |= PROFILEINFO_ARENA; + $this->roster = Lang::profiler('arenaRoster', [$profiles->getField('arenateam')]); + } + + $lvData = $profiles->getListviewData($addInfoMask, $fiExtraCols); + + if ($this->filter->getSetCriteria(10) && !in_array('guildrank', $lvHiddenCols)) + $lvVisibleCols[] = 'guildrank'; + + // create note if search limit was exceeded + 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() > Listview::DEFAULT_SIZE) + $lvNote = sprintf(Util::$tryFilteringString, 'LANG.lvnote_charactersfound', $this->sumSubjects, 0); + + if ($this->filter->useLocalList) + { + if (!empty($lvNote)) + $lvNote .= ' + "
'.Lang::profiler('complexFilter').'"'; + else + $lvNote = ''.Lang::profiler('complexFilter').''; + } + } + + $this->lvTabs = new Tabs(['parent' => "\$\$WH.ge('tabs-generic')"], 'tabsRelated'); + + $this->lvTabs->addListviewTab(new Listview(array( + 'id' => 'characters', + 'data' => $lvData, + 'hideCount' => 1, + 'onBeforeCreate' => '$pr_initRosterListview', // puts a resync button on the lv + 'extraCols' => $lvExtraCols ?: null, + 'visibleCols' => $lvVisibleCols, + 'hiddenCols' => $lvHiddenCols ?: null, + 'note' => $lvNote ?: null, + '_truncated' => $lv_truncated ?: null + ), ProfileList::$brickFile)); + + parent::generate(); + + $this->result->registerDisplayHook('filter', [self::class, 'filterFormHook']); + } + + public static function filterFormHook(Template\PageTemplate &$pt, ProfileListFilter $filter) : void + { + // sort for dropdown-menus + Lang::sort('game', 'ra'); + Lang::sort('game', 'cl'); + } +} + +?> diff --git a/endpoints/quest/quest.php b/endpoints/quest/quest.php new file mode 100644 index 00000000..b3d1d74e --- /dev/null +++ b/endpoints/quest/quest.php @@ -0,0 +1,1359 @@ +rewards): " will either work or cleanly branch to else + public string $objectives = ''; + public string $details = ''; + public string $offerReward = ''; + public string $requestItems = ''; + public string $completed = ''; + public string $end = ''; + public int $suggestedPl = 1; + public bool $unavailable = false; + + private QuestList $subject; + + public function __construct(string $id) + { + parent::__construct($id); + + $this->typeId = intVal($id); + $this->contribute = Type::getClassAttrib($this->type, 'contribute') ?? CONTRIBUTE_NONE; + } + + protected function generate() : void + { + $this->subject = new QuestList(array(['id', $this->typeId])); + if ($this->subject->error) + $this->generateNotFound(Lang::game('quest'), Lang::quest('notFound')); + + $this->h1 = Lang::unescapeUISequences(Util::htmlEscape($this->subject->getField('name', true)), Lang::FMT_HTML); + + $this->gPageInfo += array( + 'type' => $this->type, + 'typeId' => $this->typeId, + 'name' => Lang::unescapeUISequences($this->subject->getField('name', true), Lang::FMT_HTML) + ); + + $_level = $this->subject->getField('level'); + $_minLevel = $this->subject->getField('minLevel'); + $_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); + + + /*************/ + /* Menu Path */ + /*************/ + + $this->breadcrumb[] = $this->subject->getField('cat2'); + if ($cat = $this->subject->getField('cat1')) + { + foreach (Game::$questSubCats as $parent => $children) + if (in_array($cat, $children)) + $this->breadcrumb[] = $parent; + + $this->breadcrumb[] = $cat; + } + + + /**************/ + /* Page Title */ + /**************/ + + array_unshift($this->title, Lang::unescapeUISequences($this->subject->getField('name', true), Lang::FMT_RAW), Util::ucFirst(Lang::game('quest'))); + + + /***********/ + /* Infobox */ + /***********/ + + $infobox = Lang::getInfoBoxForFlags($this->subject->getField('cuFlags')); + + // event (todo: assign eventData) + if ($_ = $this->subject->getField('eventId')) + { + $this->extendGlobalIds(Type::WORLDEVENT, $_); + $infobox[] = Lang::game('eventShort', ['[event='.$_.']']); + } + + // level + if ($_level > 0) + $infobox[] = Lang::game('level').Lang::main('colon').$_level; + + // reqlevel + if ($_minLevel) + { + $lvl = $_minLevel; + if ($_ = $this->subject->getField('maxLevel')) + $lvl .= ' - '.$_; + + $infobox[] = Lang::game('reqLevel', [$lvl]); + } + + // loremaster (i dearly hope those flags cover every case...) + 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('questSortIdBak')], + ['a.faction', $_side, '&'] + ); + $loremaster = new AchievementList($conditions); + $this->extendGlobalData($loremaster->getJSGlobals(GLOBALINFO_SELF)); + + switch (count($loremaster->getFoundIds())) + { + case 0: + break; + case 1: + $infobox[] = Lang::quest('loremaster').'[achievement='.$loremaster->id.']'; + break; + default: + $lm = Lang::quest('loremaster').'[ul]'; + foreach ($loremaster->iterate() as $id => $__) + $lm .= '[li][achievement='.$id.'][/li]'; + + $infobox[] = $lm.'[/ul]'; + break; + } + } + + // type (maybe expand uppon?) + $_ = []; + if ($_flags & QUEST_FLAG_DAILY) + $_[] = '[tooltip=tooltip_dailyquest]'.Lang::quest('daily').'[/tooltip]'; + else if ($_flags & QUEST_FLAG_WEEKLY) + $_[] = Lang::quest('weekly'); + else if ($_specialFlags & QUEST_FLAG_SPECIAL_MONTHLY) + $_[] = Lang::quest('monthly'); + + if ($t = $this->subject->getField('questInfoId')) + $_[] = Lang::quest('questInfo', $t); + + if ($_) + $infobox[] = Lang::game('type').implode(' ', $_); + + // side + $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 + }; + + // races + $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'); + $infobox[] = Util::ucFirst($t).Lang::main('colon').$_; + } + + // classes + $jsg = []; + if ($_ = Lang::getClassString($this->subject->getField('reqClassMask'), $jsg, Lang::FMT_MARKUP)) + { + $this->extendGlobalIds(Type::CHR_CLASS, ...$jsg); + $t = count($jsg) == 1 ? Lang::game('class') : Lang::game('classes'); + $infobox[] = Util::ucFirst($t).Lang::main('colon').$_; + } + + // profession / skill + if ($_ = $this->subject->getField('reqSkillId')) + { + $this->extendGlobalIds(Type::SKILL, $_); + $sk = '[skill='.$_.']'; + if ($_ = $this->subject->getField('reqSkillPoints')) + $sk .= ' ('.$_.')'; + + $infobox[] = Lang::quest('profession').$sk; + } + + // timer + if ($_ = $this->subject->getField('timeLimit')) + $infobox[] = Lang::quest('timer').DateTime::formatTimeElapsedFloat($_ * 1000); + + $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]'; + $s = []; + foreach ($startEnd as $se) + { + if ($se['method'] & 0x1) + { + $this->extendGlobalIds($se['type'], $se['typeId']); + $s[] = ($s ? '[span=invisible]'.$start.'[/span] ' : $start.' ') .'['.Type::getFileString($se['type']).'='.$se['typeId'].']'; + } + } + + if ($s) + $infobox[] = implode('[br]', $s); + + // end + $end = '[icon name=quest_end'.($this->subject->isRepeatable() ? '_daily' : '').']'.Lang::event('end').'[/icon]'; + $e = []; + foreach ($startEnd as $se) + { + if ($se['method'] & 0x2) + { + $this->extendGlobalIds($se['type'], $se['typeId']); + $e[] = ($e ? '[span=invisible]'.$end.'[/span] ' : $end.' ') . '['.Type::getFileString($se['type']).'='.$se['typeId'].']'; + } + } + + if ($e) + $infobox[] = implode('[br]', $e); + + // auto accept + if ($this->subject->isAutoAccept()) + $infobox[] = Lang::quest('autoaccept'); + + // Repeatable + if ($this->subject->isRepeatable()) + $infobox[] = Lang::quest('repeatable'); + + // sharable | not sharable + $infobox[] = $_flags & QUEST_FLAG_SHARABLE ? Lang::quest('sharable') : Lang::quest('notSharable'); + + // Keeps you PvP flagged + 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)) + if ($_level > 0) + { + $_ = []; + + // red + if ($_minLevel && $_minLevel < $_level - 4) + $_[] = '[color=q10]'.$_minLevel.'[/color]'; + + // orange + if (!$_minLevel || $_minLevel < $_level - 2) + $_[] = '[color=r1]'.(!$_ && $_minLevel > $_level - 4 ? $_minLevel : $_level - 4).'[/color]'; + + // yellow + $_[] = '[color=r2]'.(!$_ && $_minLevel > $_level - 2 ? $_minLevel : $_level - 2).'[/color]'; + + // green + $_[] = '[color=r3]'.($_level + 3).'[/color]'; + + // grey (is about +/-1 level off) + $_[] = '[color=r4]'.($_level + 3 + ceil(12 * $_level / MAX_LEVEL)).'[/color]'; + + if ($_) + $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', $hasCompletion); + + + /*******************/ + /* Objectives List */ + /*******************/ + + // gather ids for lookup + $olItems = $olNPCs = $olGOs = $olFactions = []; + $olItemData = $olNPCData = $olGOData = null; + + // items + $olItems[0] = array( // srcItem on idx:0 + $this->subject->getField('sourceItemId'), + $this->subject->getField('sourceItemCount'), + false + ); + + for ($i = 1; $i < 7; $i++) // reqItem in idx:1-6 + { + $id = $this->subject->getField('reqItemId'.$i); + $qty = $this->subject->getField('reqItemCount'.$i); + if (!$id || !$qty) + continue; + + $olItems[$i] = [$id, $qty, $id == $olItems[0][0]]; + } + + if ($ids = array_filter(array_column($olItems, 0))) + { + $olItemData = new ItemList(array(['id', $ids])); + $this->extendGlobalData($olItemData->getJSGlobals(GLOBALINFO_SELF)); + + $providedRequired = false; + foreach ($olItems as $i => [$itemId, $qty, $provided]) + { + if (!$i || !$itemId) + continue; + + if ($provided) + $providedRequired = true; + + if (!$olItemData->getEntry($itemId)) + { + $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[] = new IconElement( + Type::ITEM, + $itemId, + Lang::unescapeUISequences($olItemData->json[$itemId]['name'], Lang::FMT_HTML), + num: $qty > 1 ? $qty : '', + quality: 7 - $olItemData->json[$itemId]['quality'], + 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 + if (!$providedRequired && $olItems[0][0]) + { + if (!$olItemData->getEntry($olItems[0][0])) + $this->providedItem = new IconElement(0, 0, Util::ucFirst(Lang::game('item')).' #'.$itemId, $olItems[0][1] > 1 ? $olItems[0][1] : ''); + else + $this->providedItem = new IconElement( + Type::ITEM, + $olItems[0][0], + Lang::unescapeUISequences($olItemData->json[$olItems[0][0]]['name'], Lang::FMT_HTML), + num: $olItems[0][1] > 1 ? $olItems[0][1] : '', + quality: 7 - $olItemData->json[$olItems[0][0]]['quality'], + size: IconElement::SIZE_SMALL, + element: 'iconlist-icon' + ); + } + } + + // creature or GO... + for ($i = 1; $i < 5; $i++) + { + $id = $this->subject->getField('reqNpcOrGo'.$i); + $qty = $this->subject->getField('reqNpcOrGoCount'.$i); + $altTxt = $this->subject->getField('objectiveText'.$i, true); + if ($id > 0 && $qty) + $olNPCs[$id] = [$qty, $altTxt, []]; + else if ($id < 0 && $qty) + $olGOs[-$id] = [$qty, $altTxt]; + } + + // .. creature kills + if ($ids = array_keys($olNPCs)) + { + $olNPCData = new CreatureList(array(DB::OR, ['id', $ids], ['killCredit1', $ids], ['killCredit2', $ids])); + $this->extendGlobalData($olNPCData->getJSGlobals(GLOBALINFO_SELF)); + + // create proxy-references + foreach ($olNPCData->iterate() as $id => $__) + { + if ($p = $olNPCData->getField('KillCredit1')) + if (isset($olNPCs[$p])) + $olNPCs[$p][2][$id] = $olNPCData->getField('name', true); + + if ($p = $olNPCData->getField('KillCredit2')) + if (isset($olNPCs[$p])) + $olNPCs[$p][2][$id] = $olNPCData->getField('name', true); + } + + foreach ($olNPCs as $i => [$qty, $altText, $proxies]) + { + if (!$i) + continue; + + if ($proxies) // has proxies assigned, add yourself as another proxy + { + $proxies[$i] = Util::localizedString($olNPCData->getEntry($i), 'name'); + + // split in two blocks for display + $proxies = array( + array_slice($proxies, 0, ceil(count($proxies) / 2), true), + array_slice($proxies, ceil(count($proxies) / 2), null, true) + ); + + $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[] = new IconElement(0, 0, Util::ucFirst(Lang::game('npc')).' #'.$i, $qty > 1 ? $qty : ''); + else + $this->objectiveList[] = new IconElement( + Type::NPC, + $i, + $altText ?: Util::localizedString($olNPCData->getEntry($i), 'name'), + $qty > 1 ? $qty : '', + size: IconElement::SIZE_SMALL, + element: 'iconlist-icon', + extraText: (($_specialFlags & QUEST_FLAG_SPECIAL_SPELLCAST) || $altText) ? '' : Lang::achievement('slain'), + ); + } + } + + // .. GO interactions + if ($ids = array_keys($olGOs)) + { + $olGOData = new GameObjectList(array(['id', $ids])); + $this->extendGlobalData($olGOData->getJSGlobals(GLOBALINFO_SELF)); + + foreach ($olGOs as $i => [$qty, $altText]) + { + if (!$i) + continue; + + if (!$olGOData->getEntry($i)) + $this->objectiveList[] = new IconElement(0, 0, Util::ucFirst(Lang::game('object')).' #'.$i, $qty > 1 ? $qty : '', size: IconElement::SIZE_SMALL); + else + $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', + ); + } + } + + // reputation required + for ($i = 1; $i < 3; $i++) + { + $id = $this->subject->getField('reqFactionId'.$i); + $val = $this->subject->getField('reqFactionValue'.$i); + if (!$id) + continue; + + $olFactions[$id] = $val; + } + + if ($ids = array_keys($olFactions)) + { + $olFactionsData = new FactionList(array(['id', $ids])); + $this->extendGlobalData($olFactionsData->getJSGlobals(GLOBALINFO_SELF)); + + foreach ($olFactions as $i => $val) + { + if (!$i || !in_array($i, $olFactionsData->getFoundIDs())) + continue; + + $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).')') + ); + } + } + + // granted spell + if ($_ = $this->subject->getField('sourceSpellId')) + { + $this->extendGlobalIds(Type::SPELL, $_); + $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[] = Lang::quest('reqMoney', [Util::formatMoney(abs($this->subject->getField('rewardOrReqMoney')))]); + + // required pvp kills + if ($_ = $this->subject->getField('reqPlayerKills')) + $this->objectiveList[] = Lang::quest('playerSlain', [$_]); + + + /**********/ + /* Mapper */ + /**********/ + + // gather points of interest + $mapNPCs = $mapGOs = []; // [typeId, start|end|objective, startItemId] + + // todo (med): this double list creation very much sucks ... + $getItemSource = function ($itemId, $method = 0) use (&$mapNPCs, &$mapGOs) + { + $lootTabs = new LootByItem($itemId); + if ($lootTabs->getByItem()) + { + /* + todo (med): sanity check: + there are loot templates that are absolute tosh, containing hundreds of random items (e.g. Peacebloom for Quest "The Horde Needs Peacebloom!") + even without these .. consider quests like "A Donation of Runecloth" .. oh my ..... + should we... + .. display only a maximum of sources? + .. filter sources for low drop chance? + + for the moment: + if an item has >10 sources, only display sources with >80% 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'], fn($x) => $x['percent'] >= 5.0)); + + foreach ($lootTabs->iterate() as [$file, $tabData]) + { + if (!$tabData['data']) + continue; + + foreach ($tabData['data'] as $data) + { + if ($data['percent'] < 5.0) + continue; + + if ($nSources > 10 && $data['percent'] < 80.0) + continue; + + switch ($file) + { + case 'npc': + $mapNPCs[] = [$data['id'], $method, $itemId]; + break; + case 'object': + $mapGOs[] = [$data['id'], $method, $itemId]; + break; + default: + break; + } + } + } + } + + // also there's vendors... + // 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` = %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) + $mapNPCs[] = [$v, $method, $itemId]; + }; + + $addObjectiveSpawns = function (array $spawns, callable $processing) use (&$mObjectives) + { + foreach ($spawns as $zoneId => $zoneData) + { + if (!isset($mObjectives[$zoneId])) + $mObjectives[$zoneId] = array( + 'zone' => 'Zone #'.$zoneId, + 'mappable' => 1, + 'levels' => [] + ); + + foreach ($zoneData as $floor => $floorData) + { + if (!isset($mObjectives[$zoneId]['levels'][$floor])) + $mObjectives[$zoneId]['levels'][$floor] = []; + + foreach ($floorData as $objId => $objData) + $mObjectives[$zoneId]['levels'][$floor][] = $processing($objId, $objData); + } + } + }; + + + // POI: start + end + foreach ($startEnd as $se) + { + if ($se['type'] == Type::NPC) + $mapNPCs[] = [$se['typeId'], $se['method'], 0]; + else if ($se['type'] == Type::OBJECT) + $mapGOs[] = [$se['typeId'], $se['method'], 0]; + else if ($se['type'] == Type::ITEM) + $getItemSource($se['typeId'], $se['method']); + } + + $itemObjectives = []; + $mObjectives = []; + $mZones = []; + $objectiveIdx = 0; + + // POI objectives + // also map olItems to objectiveIdx so every container gets the same pin color + foreach ($olItems as $i => [$itemId, $qty, $provided]) + { + if (!$provided && $itemId) + { + $itemObjectives[$itemId] = $objectiveIdx++; + $getItemSource($itemId); + } + } + + // PSA: 'redundant' data is on purpose (e.g. creature required for kill, also dropps item required to collect) + + // external events + $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` = %i AND `quest` = %i', AT_TYPE_OBJECTIVE, $this->typeId)) + { + 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)) + $endText = ''.($endText ?: Lang::areatrigger('unnamed', [$atir[0]])).''; + + foreach ($atSpawns as $atId => $atsp) + { + $atSpawn = array ( + 'type' => User::isInGroup(U_GROUP_STAFF) ? Type::AREATRIGGER : -1, + 'id' => $atId, + 'point' => 'requirement', + 'name' => $this->subject->parseText('end', false) ?: Lang::areatrigger('unnamed', [$atir[0]]), + 'coord' => [$atsp['posX'], $atsp['posY']], + 'coords' => [[$atsp['posX'], $atsp['posY']]], + 'objective' => $objectiveIdx++ + ); + + if (isset($mObjectives[$atsp['areaId']]['levels'][$atsp['floor']])) + { + $mObjectives[$atsp['areaId']]['levels'][$atsp['floor']][] = $atSpawn; + continue; + } + + $mObjectives[$atsp['areaId']] = array( + 'zone' => 'Zone #'.$atsp['areaId'], + 'mappable' => 1, + 'levels' => [$atsp['floor'] => [$atSpawn]] + ); + } + } + } + // complete-spell + 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) + $endText = ''.($endText ?: $endSpell->getField('name', true)).''; + } + + // ..adding creature kill requirements + if ($olNPCData && !$olNPCData->error) + { + $spawns = $olNPCData->getSpawns(SPAWNINFO_QUEST); + $addObjectiveSpawns($spawns, function ($npcId, $npcData) use ($olNPCs, &$objectiveIdx) + { + $npcData['point'] = 'requirement'; // always requirement + foreach ($olNPCs as $proxyNpcId => $npc) + { + if ($npc[1] && $npcId == $proxyNpcId) // overwrite creature name with quest specific text, if set. + $npcData['name'] = $npc[1]; + + if (!empty($npc[2][$npcId])) + $npcData['objective'] = $proxyNpcId; + } + + if (!$npcData['objective']) + $npcData['objective'] = $objectiveIdx++; + + return $npcData; + }); + } + + // ..adding object interaction requirements + if ($olGOData && !$olGOData->error) + { + $spawns = $olGOData->getSpawns(SPAWNINFO_QUEST); + $addObjectiveSpawns($spawns, function ($goId, $goData) use ($olGOs, &$objectiveIdx) + { + foreach ($olGOs as $_goId => $go) + { + if ($go[1] && $goId == $_goId) // overwrite object name with quest specific text, if set. + { + $goData['name'] = $go[1]; + break; + } + } + + $goData['point'] = 'requirement'; // always requirement + $goData['objective'] = $objectiveIdx++; + return $goData; + }); + } + + // .. adding npc from: droping queststart item; dropping item needed to collect; starting quest; ending quest + if ($mapNPCs) + { + $npcs = new CreatureList(array(['id', array_column($mapNPCs, 0)])); + if (!$npcs->error) + { + $startEndDupe = []; // if quest starter/ender is the same creature, we need to add it twice + $spawns = $npcs->getSpawns(SPAWNINFO_QUEST); + $addObjectiveSpawns($spawns, function ($npcId, $npcData) use ($mapNPCs, &$startEndDupe, $itemObjectives) + { + foreach ($mapNPCs as $mn) + { + if ($mn[0] != $npcId) + continue; + + if ($mn[2]) // source for itemId + $npcData['item'] = ItemList::getName($mn[2]); + + switch ($mn[1]) // method + { + case 1: // quest start + $npcData['point'] = $mn[2] ? 'sourcestart' : 'start'; + break; + case 2: // quest end (sourceend doesn't actually make sense .. oh well....) + $npcData['point'] = $mn[2] ? 'sourceend' : 'end'; + break; + case 3: // quest start & end + $npcData['point'] = $mn[2] ? 'sourcestart' : 'start'; + $startEndDupe = $npcData; + $startEndDupe['point'] = $mn[2] ? 'sourceend' : 'end'; + break; + default: // just something to kill for quest + $npcData['point'] = $mn[2] ? 'sourcerequirement' : 'requirement'; + if ($mn[2] && !empty($itemObjectives[$mn[2]])) + $npcData['objective'] = $itemObjectives[$mn[2]]; + } + } + + return $npcData; + }); + + if ($startEndDupe) + foreach ($spawns as $zoneId => $zoneData) + foreach ($zoneData as $floor => $floorData) + foreach ($floorData as $objId => $objData) + if ($objId == $startEndDupe['id']) + { + $mObjectives[$zoneId]['levels'][$floor][] = $startEndDupe; + break 3; + } + } + } + + // .. adding go from: containing queststart item; containing item needed to collect; starting quest; ending quest + if ($mapGOs) + { + $gos = new GameObjectList(array(['id', array_column($mapGOs, 0)])); + if (!$gos->error) + { + $startEndDupe = []; // if quest starter/ender is the same object, we need to add it twice + $spawns = $gos->getSpawns(SPAWNINFO_QUEST); + $addObjectiveSpawns($spawns, function ($goId, $goData) use ($mapGOs, &$startEndDupe, $itemObjectives) + { + foreach ($mapGOs as $mgo) + { + if ($mgo[0] != $goId) + continue; + + if ($mgo[2]) // source for itemId + $goData['item'] = ItemList::getName($mgo[2]); + + switch ($mgo[1]) // method + { + case 1: // quest start + $goData['point'] = $mgo[2] ? 'sourcestart' : 'start'; + break; + case 2: // quest end (sourceend doesn't actually make sense .. oh well....) + $goData['point'] = $mgo[2] ? 'sourceend' : 'end'; + break; + case 3: // quest start & end + $goData['point'] = $mgo[2] ? 'sourcestart' : 'start'; + $startEndDupe = $goData; + $startEndDupe['point'] = $mgo[2] ? 'sourceend' : 'end'; + break; + default: // just something to kill for quest + $goData['point'] = $mgo[2] ? 'sourcerequirement' : 'requirement'; + if ($mgo[2] && !empty($itemObjectives[$mgo[2]])) + $goData['objective'] = $itemObjectives[$mgo[2]]; + } + } + + return $goData; + }); + + if ($startEndDupe) + foreach ($spawns as $zoneId => $zoneData) + foreach ($zoneData as $floor => $floorData) + foreach ($floorData as $objId => $objData) + if ($objId == $startEndDupe['id']) + { + $mObjectives[$zoneId]['levels'][$floor][] = $startEndDupe; + break 3; + } + } + } + + // ..process zone data + if ($mObjectives) + { + // sort zones by amount of mapper points most -> least + $zoneOrder = []; + foreach ($mObjectives as $zoneId => $data) + $zoneOrder[$zoneId] = array_reduce($data['levels'], function($carry, $spawns) { foreach ($spawns as $s) { $carry += count($s['coords']); } return $carry; }); + + arsort($zoneOrder); + $zoneOrder = array_flip(array_keys($zoneOrder)); + + $areas = new ZoneList(array(['id', array_keys($mObjectives)])); + if (!$areas->error) + { + foreach ($areas->iterate() as $id => $__) + { + // [zoneId, selectionPriority] - determines which map link is preselected. (highest index) + $mZones[$zoneOrder[$id]] = [$id, count($zoneOrder) - $zoneOrder[$id]]; + $mObjectives[$id]['zone'] = $areas->getField('name', true); + } + } + + ksort($mZones); + } + + // has start & end? + $hasStartEnd = 0x0; + foreach ($mObjectives as $levels) + { + foreach ($levels['levels'] as $floor) + { + foreach ($floor as $entry) + { + if ($entry['point'] == 'start' || $entry['point'] == 'sourcestart') + $hasStartEnd |= 0x1; + else if ($entry['point'] == 'end' || $entry['point'] == 'sourceend') + $hasStartEnd |= 0x2; + } + } + } + + if ($mObjectives) + { + $this->addDataLoader('zones'); + $this->map = array( + array( // Mapper + 'parent' => 'mapper-generic', + 'objectives' => $mObjectives, + 'zoneparent' => 'mapper-zone-generic', + 'zones' => $mZones, + 'missing' => count($mZones) > 1 || $hasStartEnd != 0x3 ? 1 : 0 // 0 if everything happens in one zone, else 1 + ), + new \StdClass(), // mapperData + null, // ShowOnMap + null // foundIn + ); + } + + + /****************/ + /* Main Content */ + /****************/ + + $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 = $endText; + $this->suggestedPl = $this->subject->getField('suggestedPlayers'); + $this->unavailable = $_flags & QUEST_FLAG_UNAVAILABLE || $this->subject->getField('cuFlags') & CUSTOM_EXCLUDE_FOR_LISTVIEW; + $this->redButtons = array( + BUTTON_WOWHEAD => true, + BUTTON_LINKS => array( + 'linkColor' => 'ffffff00', + 'linkId' => 'quest:'.$this->typeId.':'.$_level, + 'linkName' => Util::jsEscape($this->subject->getField('name', true)), + 'type' => $this->type, + 'typeId' => $this->typeId + ) + ); + + if ($this->createMail($startEnd)) + $this->addScript([SC_CSS_FILE, 'css/Book.css']); + + // factionchange-equivalent + 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) + { + $this->transfer = Lang::quest('_transfer', array( + $altQuest->id, + $altQuest->getField('name', true), + $pendant > 0 ? 'alliance' : 'horde', + $pendant > 0 ? Lang::game('si', SIDE_ALLIANCE) : Lang::game('si', SIDE_HORDE) + )); + } + } + + + /**************/ + /* Extra Tabs */ + /**************/ + + $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, '!'])); + if (!$seeAlso->error) + { + $this->extendGlobalData($seeAlso->getJSGlobals()); + $this->lvTabs->addListviewTab(new Listview(array( + 'data' => $seeAlso->getListviewData(), + 'name' => '$LANG.tab_seealso', + 'id' => 'see-also' + ), QuestList::$brickFile)); + } + + // tab: criteria of + $criteriaOf = new AchievementList(array(['ac.type', ACHIEVEMENT_CRITERIA_TYPE_COMPLETE_QUEST], ['ac.value1', $this->typeId])); + if (!$criteriaOf->error) + { + $this->extendGlobalData($criteriaOf->getJSGlobals()); + $this->lvTabs->addListviewTab(new Listview(array( + 'data' => $criteriaOf->getListviewData(), + 'name' => '$LANG.tab_criteriaof', + 'id' => 'criteria-of' + ), AchievementList::$brickFile)); + } + + // 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` = %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` = %i', $this->typeId); + $pooledQuests = new QuestList(array(['id', $qp])); + if (!$pooledQuests->error) + { + $this->extendGlobalData($pooledQuests->getJSGlobals()); + $this->lvTabs->addListviewTab(new Listview(array( + 'data' => $pooledQuests->getListviewData(), + 'name' => 'Quest Pool', + 'id' => 'quest-pool', + 'note' => Lang::quest('questPoolDesc', [$max]) + ), QuestList::$brickFile)); + } + } + + // tab: conditions + $cnd = new Conditions(); + $cnd->getBySource([Conditions::SRC_QUEST_AVAILABLE, Conditions::SRC_QUEST_SHOW_MARK], entry: $this->typeId) + ->getByCondition(Type::QUEST, $this->typeId) + ->prepare(); + + + $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()) + { + $this->extendGlobalData($cnd->getJsGlobals()); + $this->lvTabs->addDataTab(...$tab); + } + + parent::generate(); + } + + private function createRewards() : ?array + { + $rewards = [[], [], [], '']; // [spells, items, choice, money] + + // moneyReward / maxLevelCompensation + $comp = $this->subject->getField('rewardMoneyMaxLevel'); + $questMoney = $this->subject->getField('rewardOrReqMoney'); + $realComp = max($comp, $questMoney); + if ($questMoney > 0) + { + $rewards[3] = Util::formatMoney($questMoney); + if ($realComp > $questMoney) + $rewards[3] .= ' ' . Lang::quest('expConvert', [Util::formatMoney($realComp), MAX_LEVEL]); + } + else if ($questMoney <= 0 && $realComp > 0) + $rewards[3] = Lang::quest('expConvert2', [Util::formatMoney($realComp), MAX_LEVEL]); + + // itemChoices + if (!empty($this->subject->choices[$this->typeId][Type::ITEM])) + { + $choices = $this->subject->choices[$this->typeId][Type::ITEM]; + $choiceItems = new ItemList(array(['id', array_keys($choices)])); + if (!$choiceItems->error) + { + $this->extendGlobalData($choiceItems->getJSGlobals()); + foreach ($choices as $id => $num) // itr over $choices to preserve display order + if ($choiceItems->getEntry($id)) + $rewards[2][] = new IconElement( + Type::ITEM, + $id, + Lang::unescapeUISequences($choiceItems->getField('name', true), Lang::FMT_HTML), + quality: $choiceItems->getField('quality'), + num: $num + ); + } + } + + // itemRewards + if (!empty($this->subject->rewards[$this->typeId][Type::ITEM])) + { + $reward = $this->subject->rewards[$this->typeId][Type::ITEM]; + $rewItems = new ItemList(array(['id', array_keys($reward)])); + if (!$rewItems->error) + { + $this->extendGlobalData($rewItems->getJSGlobals()); + foreach ($reward as $id => $num) // itr over $reward to preserve display order + if ($rewItems->getEntry($id)) + $rewards[1][] = new IconElement( + Type::ITEM, + $id, + Lang::unescapeUISequences($rewItems->getField('name', true), Lang::FMT_HTML), + quality: $rewItems->getField('quality'), + num: $num + ); + } + } + + 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)) + { + $rewCurr = new CurrencyList(array(['id', array_keys($currency)])); + if (!$rewCurr->error) + { + $this->extendGlobalData($rewCurr->getJSGlobals()); + foreach ($rewCurr->iterate() as $id => $__) + $rewards[1][] = new IconElement( + Type::CURRENCY, + $id, + $rewCurr->getField('name', true), + quality: ITEM_QUALITY_NORMAL, + num: $currency[$id] + ); + } + } + + // spellRewards + $displ = $this->subject->getField('rewardSpell'); + $cast = $this->subject->getField('rewardSpellCast'); + if ($cast <= 0 && $displ > 0) + { + $cast = $displ; + $displ = 0; + } + + if ($cast > 0 || $displ > 0) + { + $rewSpells = new SpellList(array(['id', [$displ, $cast]])); + $this->extendGlobalData($rewSpells->getJSGlobals()); + + if (User::isInGroup(U_GROUP_EMPLOYEE)) // accurately display, what spell is what + { + $extra = null; + if ($_ = $rewSpells->getEntry($displ)) + $extra = Lang::quest('spellDisplayed', [$displ, Util::localizedString($_, 'name')]); + + if ($_ = $rewSpells->getEntry($cast)) + $rewards[0] = array( + 'title' => Lang::quest('rewardAura'), + 'cast' => [new IconElement(Type::SPELL, $cast, Util::localizedString($_, 'name'))], + 'extra' => $extra + ); + } + else // if it has effect:learnSpell display the taught spell instead + { + $teach = []; + foreach ($rewSpells->iterate() as $id => $__) + if ($_ = $rewSpells->canTeachSpell()) + foreach ($_ as $idx) + $teach[$rewSpells->getField('effect'.$idx.'TriggerSpell')] = $id; + + if ($teach) + { + $taught = new SpellList(array(['id', array_keys($teach)])); + if (!$taught->error) + { + $this->extendGlobalData($taught->getJSGlobals()); + $rewards[0] = ['cast' => [], 'extra' => null]; + + $isTradeSkill = 0; + foreach ($taught->iterate() as $id => $__) + { + $isTradeSkill |= array_intersect($taught->getField('skillLines'), array_merge(SKILLS_TRADE_PRIMARY, SKILLS_TRADE_SECONDARY)) ? 1 : 0; + $rewards[0]['cast'][] = new IconElement(Type::SPELL, $id, $taught->getField('name', true)); + } + + $rewards[0]['title'] = $isTradeSkill ? Lang::quest('rewardTradeSkill') : Lang::quest('rewardSpell'); + } + } + else if (($_ = $rewSpells->getEntry($displ)) || ($_ = $rewSpells->getEntry($cast))) + { + $rewards[0] = array( + 'title' => Lang::quest('rewardAura'), + 'cast' => [new IconElement(Type::SPELL, $cast, Util::localizedString($_, 'name'))], + 'extra' => null + ); + } + } + } + + if (!array_filter($rewards)) + return null; + + return $rewards; + } + + private function createMail(array $startEnd) : bool + { + $rmtId = $this->subject->getField('rewardMailTemplateId'); + if (!$rmtId) + return false; + + $delay = $this->subject->getField('rewardMailDelay'); + $letter = DB::Aowow()->selectRow('SELECT * FROM ::mails WHERE `id` = %i', $rmtId); + + $this->mail = array( + 'attachments' => [], + 'text' => $letter ? Util::parseHtmlText(Util::localizedString($letter, 'text')) : null, + 'subject' => Util::parseHtmlText(Util::localizedString($letter, 'subject')), + 'header' => array( + $rmtId, + null, + $delay ? Lang::mail('mailIn', [DateTime::formatTimeElapsed($delay * 1000)]) : null, + ) + ); + + $senderTypeId = 0; + if ($_= DB::World()->selectCell('SELECT `RewardMailSenderEntry` FROM quest_mail_sender WHERE `QuestId` = %i', $this->typeId)) + $senderTypeId = $_; + else + foreach ($startEnd as $se) + if (($se['method'] & 0x2) && $se['type'] == Type::NPC) + $senderTypeId = $se['typeId']; + + if ($ti = CreatureList::getName($senderTypeId)) + $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 LootByContainer(); + if ($mailLoot->getByContainer(Loot::MAIL, [$rmtId])) + { + $this->extendGlobalData($mailLoot->jsGlobals); + foreach ($mailLoot->getResult() as $loot) + $this->mail['attachments'][] = new IconElement(Type::ITEM, $loot['id'], substr($loot['name'], 1), $loot['stack'][0], quality: 7 - $loot['name'][0]); + } + + return true; + } + + 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'); + + // title + if ($tId = $this->subject->getField('rewardTitleId')) + $gains[2] = [$tId, (new TitleList(array(['id', $tId])))->getHtmlizedName()]; + else + $gains[2] = null; + + // reputation + $repGains = []; + for ($i = 1; $i < 6; $i++) + { + $fac = $this->subject->getField('rewardFactionId'.$i); + $qty = $this->subject->getField('rewardFactionValue'.$i); + if (!$fac || !$qty) + continue; + + $rep = array( + 'qty' => [$qty, 0], + 'id' => $fac, + 'name' => FactionList::getName($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); + else + $rep['qty'][1] = $rep['qty'][0] * match ($this->subject->isDaily()) + { + 1 => $cuRates['quest_daily_rate'] - 1, + 2 => $cuRates['quest_weekly_rate'] - 1, + 3 => $cuRates['quest_monthly_rate'] - 1, + default => $cuRates['quest_rate'] - 1 + }; + } + + if (User::isInGroup(U_GROUP_STAFF)) + $rep['qty'][1] = $rep['qty'][0] . ($rep['qty'][1] ? $this->fmtStaffTip(($rep['qty'][1] > 0 ? '+' : '').$rep['qty'][1], Lang::faction('customRewRate')) : ''); + else + $rep['qty'][1] += $rep['qty'][0]; + + $repGains[] = $rep; + } + $gains[1] = $repGains; + + if (!array_filter($gains)) + return null; + + return $gains; + } + + private function createSeries() : array + { + $series = []; + + $makeSeriesItem = function (array $questData) : array + { + return array( + 'side' => ChrRace::sideFromMask($questData['reqRaceMask']), + 'typeStr' => Type::getFileString(Type::QUEST), + 'typeId' => $questData['id'], + 'name' => Util::htmlEscape(Lang::trimTextClean(Util::localizedString($questData, 'name'), 40)), + ); + }; + + // Assumption + // a chain always ends in a single quest, but can have an arbitrary amount of quests leading into it. + // 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` = %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` = %i', $lastQuestId ?: $this->typeId); + $chain = array(array($makeSeriesItem($end))); // series / step / quest + + $prevStepIds = [$lastQuestId ?: $this->typeId]; + 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) + $step[$pQuest['id']] = $makeSeriesItem($pQuest); + + $prevStepIds = array_keys($step); + $chain[] = $step; + } + + if (count($chain) > 1) + $series[] = [array_reverse($chain), null]; + + // todo (low): sensibly merge the following lists into 'series' + $listGen = function($cnd) use ($makeSeriesItem) + { + $chain = []; + $list = new QuestList($cnd); + if ($list->error) + return null; + + foreach ($list->iterate() as $tpl) + $chain[] = [$makeSeriesItem($tpl)]; + + return $chain; + }; + + $extraLists = array( + // Requires all of these quests (Quests that you must follow to get this quest) + ['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(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(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, '!'])], + + // During the quest available these quests (Quests that are available only at run time this quest) + ['enablesQ', array(['prevQuestId', -$this->typeId])], + + // Requires an active quest (Quests during the execution of which is available on the quest) + ['enabledByQ', array(['id', -$this->subject->getField('prevQuestId')])] + ); + + foreach ($extraLists as [$section, $condition]) + if ($_ = $listGen($condition)) + $series[] = [$_, sprintf(Util::$dfnString, Lang::quest($section.'Desc'), Lang::quest($section))]; + + return $series; + } +} + +?> diff --git a/endpoints/quest/quest_power.php b/endpoints/quest/quest_power.php new file mode 100644 index 00000000..d0eccf52 --- /dev/null +++ b/endpoints/quest/quest_power.php @@ -0,0 +1,50 @@ + ['filter' => FILTER_CALLBACK, 'options' => [Locale::class, 'tryFromDomain']] + ); + + public function __construct(string $id) + { + parent::__construct($id); + + // temp locale + if ($this->_get['domain']) + Lang::load($this->_get['domain']); + + $this->typeId = intVal($id); + } + + protected function generate() : void + { + $quest = new QuestList(array(['id', $this->typeId])); + if ($quest->error) + $this->cacheType = CACHE_TYPE_NONE; + else + $opts = array( + 'name' => Lang::unescapeUISequences($quest->getField('name', true), Lang::FMT_RAW), + 'tooltip' => $quest->renderTooltip(), + 'daily' => $quest->isDaily() ? 1 : null + ); + + $this->result = new Tooltip(self::POWER_TEMPLATE, $this->typeId, $opts ?? []); + } +} + +?> diff --git a/endpoints/quests/quests.php b/endpoints/quests/quests.php new file mode 100644 index 00000000..86dc7e9b --- /dev/null +++ b/endpoints/quests/quests.php @@ -0,0 +1,153 @@ + 3519, 4024 => 3537, 25 => 46, 1769 => 361, + // Startzones: Horde + 132 => 1, 9 => 12, 3431 => 3430, 154 => 85, + // Startzones: Alliance + 3526 => 3524, 363 => 14, 220 => 215, 188 => 141, + // Group: Caverns of Time + 2366 => 1941, 2367 => 1941, 4100 => 1941, + // Group: Hellfire Citadell + 3562 => 3535, 3713 => 3535, 3714 => 3535, + // Group: Auchindoun + 3789 => 3688, 3790 => 3688, 3791 => 3688, 3792 => 3688, + // Group: Tempest Keep + 3847 => 3842, 3848 => 3842, 3849 => 3842, + // Group: Coilfang Reservoir + 3715 => 3905, 3716 => 3905, 3717 => 3905, + // Group: Icecrown Citadel + 4809 => 4522, 4813 => 4522, 4820 => 4522 + ); + + protected int $type = Type::QUEST; + protected int $cacheType = CACHE_TYPE_LIST_PAGE; + + protected string $template = 'quests'; + protected string $pageName = 'quests'; + protected ?int $activeTab = parent::TAB_DATABASE; + protected array $breadcrumb = [0, 3]; + + protected array $scripts = [[SC_JS_FILE, 'js/filters.js']]; + protected array $expectedGET = array( + 'filter' => ['filter' => FILTER_VALIDATE_REGEXP, 'options' => ['regexp' => Filter::PATTERN_PARAM]] + ); + protected array $validCats = Game::QUEST_CLASSES; + + public function __construct(string $rawParam) + { + $this->getCategoryFromUrl($rawParam); + + 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->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; + } + + protected function generate() : void + { + $this->h1 = Util::ucFirst(Lang::game('quests')); + + $conditions = [Listview::DEFAULT_SIZE]; + if (!User::isInGroup(U_GROUP_EMPLOYEE)) + $conditions[] = [['cuFlags', CUSTOM_EXCLUDE_FOR_LISTVIEW, '&'], 0]; + + if ($_ = $this->filter->getConditions()) + $conditions[] = $_; + + if (isset($this->category[1])) + $conditions[] = ['questSortId', $this->category[1]]; + else if (isset($this->category[0])) + $conditions[] = ['questSortId', $this->validCats[$this->category[0]]]; + + + /*************/ + /* Menu Path */ + /*************/ + + foreach ($this->category as $c) + $this->breadcrumb[] = $c; + + if (isset($this->category[1]) && isset(self::SUB_SUB_CAT[$this->category[1]])) + array_splice($this->breadcrumb, 3, 0, self::SUB_SUB_CAT[$this->category[1]]); + + + /**************/ + /* Page Title */ + /**************/ + + array_unshift($this->title, $this->h1); + + if (isset($this->category[1])) + array_unshift($this->title, Lang::quest('cat', $this->category[0], $this->category[1])); + else if (isset($this->category[0])) + { + $c0 = Lang::quest('cat', $this->category[0]); + array_unshift($this->title, is_array($c0) ? $c0[0] : $c0); + } + + + /****************/ + /* Main Content */ + /****************/ + + $this->redButtons[BUTTON_WOWHEAD] = true; + if ($fiQuery = $this->filter->buildGETParam()) + $this->wowheadLink .= '&filter='.$fiQuery; + + $quests = new QuestList($conditions, ['extraOpts' => $this->filter->extraOpts, 'calcTotal' => true]); + + $this->extendGlobalData($quests->getJSGlobals()); + + $tabData = ['data' => $quests->getListviewData()]; + + if ($rc = $this->filter->fiReputationCols) + $tabData['extraCols'] = '$fi_getReputationCols('.json_encode($rc, JSON_NUMERIC_CHECK | JSON_UNESCAPED_UNICODE).')'; + else if ($this->filter->fiExtraCols) + $tabData['extraCols'] = '$fi_getExtraCols(fi_extraCols, 0, 0)'; + + // create note if search limit was exceeded + if ($quests->getMatches() > Listview::DEFAULT_SIZE) + { + $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) + $tabData['note'] = '$$WH.sprintf(LANG.lvnote_questgivers, '.$this->category[1].', g_zones['.$this->category[1].'], '.$this->category[1].')'; + + $this->lvTabs = new Tabs(['parent' => "\$\$WH.ge('tabs-generic')"]); + + $this->lvTabs->addListviewTab(new Listview($tabData, QuestList::$brickFile)); + + parent::generate(); + } +} + +?> diff --git a/endpoints/race/race.php b/endpoints/race/race.php new file mode 100644 index 00000000..e331f1b0 --- /dev/null +++ b/endpoints/race/race.php @@ -0,0 +1,296 @@ + [starter, argent tournament] + null, [384, 33307], [3362, 33553], [1261, 33310], + [4730, 33653], [4731, 33555], [3685, 33556], [7955, 33650], + [7952, 33554], null, [16264, 33557], [17584, 33657] + ); + + protected int $cacheType = CACHE_TYPE_DETAIL_PAGE; + + protected string $template = 'detail-page-generic'; + protected string $pageName = 'race'; + protected ?int $activeTab = parent::TAB_DATABASE; + protected array $breadcrumb = [0, 13]; + + public int $type = Type::CHR_RACE; + public int $typeId = 0; + public ?string $expansion = null; + + private CharRaceList $subject; + + public function __construct(string $id) + { + parent::__construct($id); + + $this->typeId = intVal($id); + $this->contribute = Type::getClassAttrib($this->type, 'contribute') ?? CONTRIBUTE_NONE; + } + + protected function generate() : void + { + $this->subject = new CharRaceList(array(['id', $this->typeId])); + if ($this->subject->error) + $this->generateNotFound(Lang::game('race'), Lang::race('notFound')); + + $this->h1 = $this->subject->getField('name', true); + + $this->gPageInfo += array( + 'type' => $this->type, + 'typeId' => $this->typeId, + 'name' => $this->h1 + ); + + + /*************/ + /* Menu Path */ + /*************/ + + $this->breadcrumb[] = $this->typeId; + + + /**************/ + /* Page Title */ + /**************/ + + array_unshift($this->title, $this->h1, Util::ucFirst(Lang::game('race'))); + + + /***********/ + /* Infobox */ + /***********/ + + $ra = ChrRace::from($this->typeId); + + $infobox = Lang::getInfoBoxForFlags($this->subject->getField('cuFlags')); + + // side + if ($_ = $this->subject->getField('side')) + $infobox[] = Lang::main('side').'[span class=icon-'.($_ == SIDE_HORDE ? 'horde' : 'alliance').']'.Lang::game('si', $_).'[/span]'; + + // faction + if ($_ = $this->subject->getField('factionId')) + { + $this->extendGlobalIds(Type::FACTION, $_); + $infobox[] = Util::ucFirst(Lang::game('faction')).Lang::main('colon').'[faction='.$_.']'; + } + + // leader + if ($_ = $this->subject->getField('leader')) + { + $this->extendGlobalIds(Type::NPC, $_); + $infobox[] = Lang::race('racialLeader').'[npc='.$_.']'; + } + + // start area + if ($_ = $this->subject->getField('startAreaId')) + { + $this->extendGlobalIds(Type::ZONE, $_); + $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'); + + + /****************/ + /* Main Content */ + /****************/ + + $this->expansion = Util::$expansionString[$this->subject->getField('expansion')]; + $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 */ + /**************/ + + $this->lvTabs = new Tabs(['parent' => "\$\$WH.ge('tabs-generic')"], 'tabsRelated', true); + + // tab: classes + $classes = new CharClassList(array(['racemask', $ra->toMask(), '&'])); + if (!$classes->error) + { + $this->extendGlobalData($classes->getJSGlobals()); + $this->lvTabs->addListviewTab(new Listview(['data' => $classes->getListviewData()], CharClassList::$brickFile)); + } + + // tab: languages + $conditions = array( + ['typeCat', -11], // proficiencies + ['reqRaceMask', $ra->toMask(), '&'] // only languages are race-restricted + ); + + $tongues = new SpellList($conditions); + if (!$tongues->error) + { + $this->extendGlobalData($tongues->getJSGlobals()); + $this->lvTabs->addListviewTab(new Listview(array( + 'data' => $tongues->getListviewData(), + 'id' => 'languages', + 'name' => '$LANG.tab_languages', + 'hiddenCols' => ['reagents'] + ), SpellList::$brickFile)); + } + + // tab: racial-traits + $conditions = array( + ['typeCat', -4], // racial traits + ['reqRaceMask', $ra->toMask(), '&'] + ); + + $racials = new SpellList($conditions); + if (!$racials->error) + { + $this->extendGlobalData($racials->getJSGlobals()); + $tabData = array( + 'data' => $racials->getListviewData(), + 'id' => 'racial-traits', + 'name' => '$LANG.tab_racialtraits', + 'hiddenCols' => ['reagents'] + ); + if ($racials->hasDiffFields('reqClassMask')) + $tabData['visibleCols'] = ['classes']; + + $this->lvTabs->addListviewTab(new Listview($tabData, SpellList::$brickFile)); + } + + // tab: quests + $conditions = array( + ['reqRaceMask', $ra->toMask(), '&'], + [['reqRaceMask', ChrRace::MASK_HORDE, '&'], ChrRace::MASK_HORDE, '!'], + [['reqRaceMask', ChrRace::MASK_ALLIANCE, '&'], ChrRace::MASK_ALLIANCE, '!'] + ); + + $quests = new QuestList($conditions); + if (!$quests->error) + { + $this->extendGlobalData($quests->getJSGlobals()); + $this->lvTabs->addListviewTab(new Listview(['data' => $quests->getListviewData()], QuestList::$brickFile)); + } + + // 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 %in', self::MOUNT_VENDORS[$this->typeId])) + { + $conditions = array( + ['i.id', $items], + ['i.class', ITEM_CLASS_MISC], + ['i.subClass', 5], // mounts + ); + + $mounts = new ItemList($conditions); + if (!$mounts->error) + { + $this->extendGlobalData($mounts->getJSGlobals()); + $this->lvTabs->addListviewTab(new Listview(array( + 'data' => $mounts->getListviewData(), + 'id' => 'mounts', + 'name' => '$LANG.tab_mounts', + 'hiddenCols' => ['slot', 'type'] + ), ItemList::$brickFile)); + } + } + } + + // 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) + { + $this->extendGlobalData($sounds->getJSGlobals(GLOBALINFO_SELF)); + $data = $sounds->getListviewData(); + foreach ($data as $id => &$d) + $d['gender'] = $vo[$id]; + + $this->lvTabs->addListviewTab(new Listview(array( + 'data' => $data, + 'extraCols' => ['$Listview.templates.title.columns[1]'] + ), SoundList::$brickFile)); + } + } + + // 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(); + if ($tab = $cnd->toListviewTab('condition-for', '$LANG.tab_condition_for')) + { + $this->extendGlobalData($cnd->getJsGlobals()); + $this->lvTabs->addDataTab(...$tab); + } + + parent::generate(); + } +} + +?> diff --git a/endpoints/races/races.php b/endpoints/races/races.php new file mode 100644 index 00000000..43d370b6 --- /dev/null +++ b/endpoints/races/races.php @@ -0,0 +1,52 @@ +getCategoryFromUrl($rawParam); + + parent::__construct($rawParam); + } + + protected function generate() : void + { + $this->h1 = Util::ucFirst(Lang::game('races')); + + + array_unshift($this->title, $this->h1); + + + $this->redButtons[BUTTON_WOWHEAD] = true; + + $conditions = [Listview::DEFAULT_SIZE]; + if (!User::isInGroup(U_GROUP_EMPLOYEE)) + $conditions[] = [['cuFlags', CUSTOM_EXCLUDE_FOR_LISTVIEW, '&'], 0]; + + $this->lvTabs = new Tabs(['parent' => "\$\$WH.ge('tabs-generic')"]); + + $races = new CharRaceList($conditions); + if (!$races->error) + $this->lvTabs->addListviewTab(new Listview(['data' => $races->getListviewData()], CharRaceList::$brickFile)); + + parent::generate(); + } +} + +?> diff --git a/endpoints/random/random.php b/endpoints/random/random.php new file mode 100644 index 00000000..21250899 --- /dev/null +++ b/endpoints/random/random.php @@ -0,0 +1,48 @@ +h1 = 'Random Page'; + // array_unshift($this->title, $this->h1); + + $type = array_rand(Type::getClassesFor(Type::FLAG_RANDOM_SEARCHABLE)); + $typeId = (Type::newList($type))?->getRandomId(); + + $this->redirectTo = '?'.Type::getFileString($type).'='.$typeId; + + // $this->extraHTML = <<// + // JS; + } +} + +?> diff --git a/endpoints/reputation/reputation.php b/endpoints/reputation/reputation.php new file mode 100644 index 00000000..0e160637 --- /dev/null +++ b/endpoints/reputation/reputation.php @@ -0,0 +1,58 @@ +generateError(); + } + + protected function generate() : void + { + $this->h1 = Lang::main('moreTitles', $this->pageName); + + array_unshift($this->title, $this->h1); + + 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'])); + + $this->tabsTitle = Lang::main('yourRepHistory'); + $this->lvTabs = new Tabs(['parent' => "\$\$WH.ge('tabs-generic')"], __forceTabs: true); + + $this->lvTabs->addListviewTab(new Listview(array( + 'id' => 'reputation-history', + 'name' => '$LANG.reputationhistory', + 'data' => $repData + ), 'reputationhistory')); + } + + parent::generate(); + + $this->result->registerDisplayHook('article', [self::class, 'articleHook']); + } + + public static function articleHook(Template\PageTemplate &$pt, Markup &$article) : void + { + $article->apply(Cfg::applyToString(...)); + } +} + +?> diff --git a/endpoints/screenshot/add.php b/endpoints/screenshot/add.php new file mode 100644 index 00000000..294a4285 --- /dev/null +++ b/endpoints/screenshot/add.php @@ -0,0 +1,98 @@ + 1. =add: receives user upload + 1.1. checks and processing on the upload + 1.2. forward to =crop or blank response + 2. =crop: user edites upload + 3. =complete: store edited screenshot file and data + 4. =thankyou +*/ + +// filename: Username-type-typeId-[_original].jpg + +class ScreenshotAddResponse extends TextResponse +{ + protected bool $requiresLogin = true; + + private string $imgHash = ''; + private int $destType = 0; + private int $destTypeId = 0; + + public function __construct(string $rawParam) + { + parent::__construct($rawParam); + + // get screenshot destination + // target delivered as screenshot=&.. (hash is optional) + if (!preg_match('/^screenshot=\w+&(-?\d+)\.(-?\d+)(\.(\w{16}))?$/i', $_SERVER['QUERY_STRING'] ?? '', $m, PREG_UNMATCHED_AS_NULL)) + $this->generate404(); + + [, $this->destType, $this->destTypeId, , $imgHash] = $m; + + // no such type or this type cannot receive screenshots + if (!Type::checkClassAttrib($this->destType, 'contribute', CONTRIBUTE_SS)) + $this->generate404(); + + // no such typeId + if (!Type::validateIds($this->destType, $this->destTypeId)) + $this->generate404(); + + // only accept/expect hash for crop & complete + if ($imgHash) + $this->generate404(); + } + + protected function generate() : void + { + if ($this->handleAdd()) + $this->redirectTo = '?screenshot=crop&'.$this->destType.'.'.$this->destTypeId.'.'.$this->imgHash; + else if ($this->destType && $this->destTypeId) + $this->redirectTo = '?'.Type::getFileString($this->destType).'='.$this->destTypeId.'#submit-a-screenshot'; + else + $this->generate404(); + } + + private function handleAdd() : bool + { + if (!User::canUploadScreenshot()) + { + $_SESSION['error']['ss'] = Lang::screenshot('error', 'notAllowed'); + return false; + } + + if (!ScreenshotMgr::init()) + { + $_SESSION['error']['ss'] = Lang::main('intError'); + return false; + } + + if (!ScreenshotMgr::validateUpload()) + { + $_SESSION['error']['ss'] = ScreenshotMgr::$error; + return false; + } + + if (!ScreenshotMgr::loadUpload()) + { + $_SESSION['error']['ss'] = Lang::main('intError'); + return false; + } + + if (!ScreenshotMgr::tempSaveUpload([$this->destType, $this->destTypeId], $this->imgHash)) + { + $_SESSION['error']['ss'] = Lang::main('intError'); + return false; + } + + return true; + } +} + +?> diff --git a/endpoints/screenshot/complete.php b/endpoints/screenshot/complete.php new file mode 100644 index 00000000..5bf80622 --- /dev/null +++ b/endpoints/screenshot/complete.php @@ -0,0 +1,106 @@ + 3. =complete: store edited screenshot file and data + 4. =thankyou +*/ + +// filename: Username-type-typeId-[_original].jpg + +class ScreenshotCompleteResponse extends TextResponse +{ + use TrCommunityHelper; + + protected bool $requiresLogin = true; + + protected array $expectedPOST = array( + 'coords' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkCoords'] ], + 'screenshotalt' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkTextLine']] + ); + + private int $destType = 0; + private int $destTypeId = 0; + private string $imgHash = ''; + + public function __construct(string $rawParam) + { + parent::__construct($rawParam); + + // get screenshot destination + // target delivered as screenshot=&.. (hash is optional) + if (!preg_match('/^screenshot=\w+&(-?\d+)\.(-?\d+)(\.(\w{16}))?$/i', $_SERVER['QUERY_STRING'] ?? '', $m, PREG_UNMATCHED_AS_NULL)) + $this->generate404(); + + [, $this->destType, $this->destTypeId, , $this->imgHash] = $m; + + // no such type or this type cannot receive screenshots + if (!Type::checkClassAttrib($this->destType, 'contribute', CONTRIBUTE_SS)) + $this->generate404(); + + // no such typeId + if (!Type::validateIds($this->destType, $this->destTypeId)) + $this->generate404(); + + // hash required for crop & complete + if (!$this->imgHash) + $this->generate404(); + } + + protected function generate() : void + { + if ($this->handleComplete()) + $this->forward('?screenshot=thankyou&'.$this->destType.'.'.$this->destTypeId); + else + $this->generate404(); + } + + private function handleComplete() : bool + { + if (!$this->assertPOST('coords')) + return false; + + ScreenshotMgr::init(); + + if (!ScreenshotMgr::loadFile(ScreenshotMgr::PATH_TEMP, User::$username.'-'.$this->destType.'-'.$this->destTypeId.'-'.$this->imgHash.'_original')) + return false; + + ScreenshotMgr::cropImg(...$this->_post['coords']); + + ['oWidth' => $w, 'oHeight' => $h] = ScreenshotMgr::calcImgDimensions(); + + // write to db + $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, + $this->handleCaption($this->_post['screenshotalt']) + ); + if (!is_int($newId)) // 0 is valid, NULL or FALSE is not + { + trigger_error('ScreenshotCompleteResponse - screenshot query failed', E_USER_ERROR); + return false; + } + + // write to file + return ScreenshotMgr::writeImage(ScreenshotMgr::PATH_PENDING, $newId); + } + + protected static function checkCoords(string $val) : ?array + { + if (preg_match('/^[01]\.[0-9]{3}(,[01]\.[0-9]{3}){3}$/', $val)) + return explode(',', $val); + + return null; + } +} + +?> diff --git a/endpoints/screenshot/crop.php b/endpoints/screenshot/crop.php new file mode 100644 index 00000000..9f446899 --- /dev/null +++ b/endpoints/screenshot/crop.php @@ -0,0 +1,92 @@ + 2. =crop: user edites upload + 2.1. just show edit page + 2.2. user submits coords and description to =complete + 3. =complete: store edited screenshot file and data + 4. =thankyou +*/ + +// filename: Username-type-typeId-[_original].jpg + +class ScreenshotCropResponse extends TemplateResponse +{ + protected bool $requiresLogin = true; + + protected string $template = 'screenshot'; + protected string $pageName = 'screenshot'; + + protected array $scripts = [[SC_JS_FILE, 'js/Cropper.js'], [SC_CSS_FILE, 'css/Cropper.css']]; + + public ?Markup $infobox = null; + public array $cropper = []; + public int $destType = 0; + public int $destTypeId = 0; + public string $imgHash = ''; + + public function __construct(string $rawParam) + { + parent::__construct($rawParam); + + // get screenshot destination + // target delivered as screenshot=&.. (hash is optional) + if (!preg_match('/^screenshot=\w+&(-?\d+)\.(-?\d+)(\.(\w{16}))?$/i', $_SERVER['QUERY_STRING'] ?? '', $m, PREG_UNMATCHED_AS_NULL)) + $this->generateError(); + + [, $this->destType, $this->destTypeId, , $this->imgHash] = $m; + + // no such type or this type cannot receive screenshots + if (!Type::checkClassAttrib($this->destType, 'contribute', CONTRIBUTE_SS)) + $this->generateError(); + + // no such typeId + if (!Type::validateIds($this->destType, $this->destTypeId)) + $this->generateError(); + + // hash required for crop & complete + if (!$this->imgHash) + $this->generateError(); + } + + protected function generate() : void + { + $this->h1 = Lang::screenshot('submission'); + $fileBase = User::$username.'-'.$this->destType.'-'.$this->destTypeId.'-'.$this->imgHash; + + array_unshift($this->title, $this->h1); + + ScreenshotMgr::init(); + + if (!ScreenshotMgr::loadFile(ScreenshotMgr::PATH_TEMP, $fileBase.'_original')) + { + $_SESSION['error']['ss'] = Lang::main('intError'); + $this->forward('?'.Type::getFileString($this->destType).'='.$this->destTypeId.'#submit-a-screenshot'); + } + + $dims = ScreenshotMgr::calcImgDimensions(); + + $this->cropper = $dims + array( + 'url' => Cfg::get('STATIC_URL').'/uploads/screenshots/temp/'.$fileBase.'.jpg', + 'parent' => 'ss-container', + 'minCrop' => ScreenshotMgr::$MIN_SIZE, // optional; defaults to 150 - min selection size (a square) + 'type' => $this->destType, // only used to check against NPC: 15384 [OLDWorld Trigger (DO NOT DELETE)] for U_GROUP_MODERATOR | U_GROUP_EDITOR. If successful drops minCrop constraint + 'typeId' => $this->destTypeId // i guess this was used to upload arbitrary imagery for articles, blog posts, etc + ); + + // target + $this->infobox = new Markup(Lang::screenshot('displayOn', [Lang::typeName($this->destType), Type::getFileString($this->destType), $this->destTypeId]), ['allow' => Markup::CLASS_STAFF, 'dbpage' => true], 'infobox-contents0'); + $this->extendGlobalIds($this->destType, $this->destTypeId); + + parent::generate(); + } +} + +?> diff --git a/endpoints/screenshot/thankyou.php b/endpoints/screenshot/thankyou.php new file mode 100644 index 00000000..d2517671 --- /dev/null +++ b/endpoints/screenshot/thankyou.php @@ -0,0 +1,66 @@ + 4. =thankyou +*/ + +// filename: Username-type-typeId-[_original].jpg + +class ScreenshotThankyouResponse extends TemplateResponse +{ + protected bool $requiresLogin = true; + + protected string $template = 'text-page-generic'; + protected string $pageName = 'screenshot'; + + private int $destType = 0; + private int $destTypeId = 0; + + public function __construct(string $rawParam) + { + parent::__construct($rawParam); + + // get screenshot destination + // target delivered as screenshot=&.. (hash is optional) + if (!preg_match('/^screenshot=\w+&(-?\d+)\.(-?\d+)(\.(\w{16}))?$/i', $_SERVER['QUERY_STRING'] ?? '', $m, PREG_UNMATCHED_AS_NULL)) + $this->generateError(); + + [, $this->destType, $this->destTypeId, , $imgHash] = $m; + + // no such type or this type cannot receive screenshots + if (!Type::checkClassAttrib($this->destType, 'contribute', CONTRIBUTE_SS)) + $this->generateError(); + + // no such typeId + if (!Type::validateIds($this->destType, $this->destTypeId)) + $this->generateError(); + + // only accept/expect hash for crop & complete + if ($imgHash) + $this->generateError(); + } + + protected function generate() : void + { + $this->h1 = Lang::screenshot('submission'); + + array_unshift($this->title, $this->h1); + + $this->extraHTML = Lang::screenshot('thanks', 'contrib').'

'; + $this->extraHTML .= Lang::screenshot('thanks', 'goBack', [Type::getFileString($this->destType), $this->destTypeId])."

\n"; + $this->extraHTML .= ''.Lang::screenshot('thanks', 'note').''; + + parent::generate(); + } +} + +?> diff --git a/endpoints/search/search.php b/endpoints/search/search.php new file mode 100644 index 00000000..11bce6f4 --- /dev/null +++ b/endpoints/search/search.php @@ -0,0 +1,113 @@ + Templated Page /w Listviews +*/ + + +class SearchBaseResponse extends TemplateResponse implements ICache +{ + use TrCache, TrSearch; + + private const SEARCH_MODS_ALL = 0x0FFFFFFF; // yeah im lazy, now what? + + protected int $cacheType = CACHE_TYPE_SEARCH; + + protected string $template = 'search'; + protected string $pageName = 'search'; + protected ?int $activeTab = parent::TAB_DATABASE; + + protected array $expectedGET = array( + 'search' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkTextLine']] + ); + + public string $invalidTerms = ''; + + public function __construct(string $rawParam) + { + parent::__construct($rawParam); // just to set g_user and g_locale + + $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); + } + + protected function generate() : void + { + if (!$this->query) // empty search > goto home page + $this->forward(); + + $this->search = $this->query; // escaped by TemplateResponse + + if ($iv = $this->searchObj->invalid) + $this->invalidTerms = implode(', ', Util::htmlEscape($iv)); + + array_unshift($this->title, $this->search, Lang::main('search')); + + $this->redButtons[BUTTON_WOWHEAD] = true; + $this->wowheadLink = sprintf(WOWHEAD_LINK, Lang::getLocale()->domain(), 'search=', Util::htmlEscape($this->query)); + + $this->lvTabs = new Tabs(['parent' => "\$\$WH.ge('tabs-generic')"], __forceTabs: true); + + $canRedirect = true; + $redirectTo = ''; + + if ($this->searchObj->canPerform()) + { + foreach ($this->searchObj->perform() as $lvData) + { + if ($lvData[1] == 'npc' || $lvData[1] == 'object') + $this->addDataLoader('zones'); + + $this->lvTabs->addListviewTab(new Listview(...$lvData)); + + // 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']); + } + } + + $this->extendGlobalData($this->searchObj->getJSGlobals()); + + parent::generate(); + + $this->result->registerDisplayHook('lvTabs', [self::class, 'tabsHook']); + + // this one stings.. + // we have to manually call saveCache, beacause normally it would be called AFTER the page is rendered.. + // .. which will not happen if we forward to somewhere + // also we have to set a postCacheHook in this case that handles future forwards (gets called in display() so the currenct call is also covered) + if ($canRedirect && $redirectTo) + { + $this->setOnCacheLoaded([self::class, 'onBeforeDisplay'], $redirectTo); + $this->saveCache($this->result); + } + } + + // update dates to now() + public static function tabsHook(Template\PageTemplate $pt, Tabs &$lvTabs) : void + { + foreach ($lvTabs->iterate() as &$listview) + if ($listview instanceof Listview && $listview->getTemplate() == 'holiday') + WorldEventList::updateListview($listview); + } + + public static function onBeforeDisplay(Template\PageTemplate $pt, string $url) : never + { + header('Location: '.$url, true, 302); // we no longer have access to BaseResponse .. so thats fun + exit(); + } +} + +?> diff --git a/endpoints/search/search_json.php b/endpoints/search/search_json.php new file mode 100644 index 00000000..fa8d4b5c --- /dev/null +++ b/endpoints/search/search_json.php @@ -0,0 +1,91 @@ + search by compare or profiler (only items + itemsets) + array:[ + searchString, + [itemData], + [itemsetData] + ] +*/ + + +class SearchJsonResponse extends TextResponse implements ICache +{ + use TrCache, TrSearch; + + protected int $cacheType = CACHE_TYPE_SEARCH; + + protected array $expectedGET = array( + 'search' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkTextLine'] ], + 'wt' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkIntArray'] ], + 'wtv' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkIntArray'] ], + 'slots' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkIntArray'] ], + 'type' => ['filter' => FILTER_VALIDATE_INT, 'options' => ['min_range' => Type::ITEM, 'max_range' => Type::ITEMSET]] + ); + + private array $extraOpts = []; // for weighted search + private array $extraCnd = []; // for weighted search + + public function __construct(string $rawParam) + { + parent::__construct($rawParam); + + $this->query = $this->_get['search']; // technically rawParam, but prepared + + if ($this->_get['wt'] && $this->_get['wtv']) // slots and type should get ignored + { + $itemFilter = new ItemListFilter($this->_get); + if ($_ = $itemFilter->createConditionsForWeights()) + { + $this->extraOpts = $itemFilter->extraOpts; + $this->extraCnd[] = $_; + } + } + + if ($_ = array_filter($this->_get['slots'] ?? [])) + $this->extraCnd[] = ['slot', $_]; + + $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->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; + protected function generate() : void + { + $outItems = []; + $outSets = []; + + // invalid conditions: not enough characters to search OR no types to search + if (!$this->searchObj->canPerform()) + $this->generate404($this->query); + + foreach ($this->searchObj->perform() as $modId => $data) + { + if ($modId == Search::MOD_ITEM) + $outItems = $data; + else if ($modId == Search::MOD_ITEMSET) + $outSets = $data; + } + + $this->result = Util::toJSON([$this->query, $outItems, $outSets]); + } + + public function generate404(?string $search = ''): never + { + parent::generate404(Util::toJSON([$search, [], []])); + } +} + +?> diff --git a/endpoints/search/search_open.php b/endpoints/search/search_open.php new file mode 100644 index 00000000..699fc3fa --- /dev/null +++ b/endpoints/search/search_open.php @@ -0,0 +1,128 @@ + suggestions when typing into searchboxes + array:[ + str, // search + str[10], // found + [], // unused - description for found result? + str[10], // url to found result + [], // unused + [], // unused + [], // unused + str[10][4] // type, typeId, param1 (4:quality, 3,6,9,10,17:icon, 5,11:faction), param2 (3:quality, 6:rank) + ] + + WH walked away from this hybrid approach and has separate endpoints for internal search suggestions and opensearch, with the latter only providing found text (index 1) + + we move the appendix of ' (TypeName)' on found text to descriptions as it fucks over Firefox users when they apply the suggestion +*/ + + +class SearchOpenResponse extends TextResponse implements ICache +{ + use TrCache, TrSearch; + + private const /* int */ SEARCH_MODS_OPEN = + 1 << Search::MOD_CLASS | 1 << Search::MOD_RACE | 1 << Search::MOD_TITLE | 1 << Search::MOD_WORLDEVENT | + 1 << Search::MOD_CURRENCY | 1 << Search::MOD_ITEMSET | 1 << Search::MOD_ITEM | 1 << Search::MOD_ABILITY | + 1 << Search::MOD_TALENT | 1 << Search::MOD_CREATURE | 1 << Search::MOD_QUEST | 1 << Search::MOD_ACHIEVEMENT | + 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; + + protected array $expectedGET = array( + 'search' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkTextLine']] + ); + + public function __construct(string $rawParam) + { + parent::__construct($rawParam); // just to set g_user and g_locale + + $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, maxResults: $this->maxResults); + } + + protected function generate() : void + { + // this one is funny: we want 10 results, ideally equally distributed over each type + $foundTotal = 0; + $result = array( // 0:query, 1:[names], 3:[links]; 7:[extraInfo] + $this->query, + [], [], [], [], [], [], [] + ); + + // invalid conditions: not enough characters to search OR no types to search + if (!$this->searchObj->canPerform()) + $this->generate404($this->query); + + foreach ($this->searchObj->perform() as [, , $nMatches, , , ]) + $foundTotal += $nMatches; + + foreach ($this->searchObj->perform() as [$data, $type, $nMatches, $param1, $param2, $desc]) + { + $max = max(1, intVal($this->maxResults * $nMatches / $foundTotal)); + + $i = 0; + foreach ($data as $id => $name) + { + if (++$i > $max) + break; + + if (count($result[1]) >= $this->maxResults) + break 2; + + $result[1][] = $name; // originally - $name . ' ('.$desc.')' + $result[2][] = $desc; // .. and here empty + $result[3][] = Cfg::get('HOST_URL').'/?'.Type::getFileString($type).'='.$id; + + $extra = [$type, $id]; // type, typeId + if (isset($param1[$id])) + $extra[] = $param1[$id]; // param1 + if (isset($param2[$id])) + $extra[] = $param2[$id]; // param2 + + $result[7][] = $extra; + } + } + + $this->result = Util::toJSON($result); + } + + public function generate404(?string $search = null) : never + { + parent::generate404(Util::toJSON([$search, [], [], [], [], [], [], []])); + } +} + +?> diff --git a/endpoints/searchbox/searchbox.php b/endpoints/searchbox/searchbox.php new file mode 100644 index 00000000..22c289d9 --- /dev/null +++ b/endpoints/searchbox/searchbox.php @@ -0,0 +1,34 @@ +generateError(); + } + + protected function generate() : void + { + $this->h1 = Lang::main('moreTitles', $this->pageName); + + array_unshift($this->title, $this->h1); + + parent::generate(); + } +} + +?> diff --git a/endpoints/searchplugins/searchplugins.php b/endpoints/searchplugins/searchplugins.php new file mode 100644 index 00000000..6dd00b1b --- /dev/null +++ b/endpoints/searchplugins/searchplugins.php @@ -0,0 +1,34 @@ +generateError(); + } + + protected function generate() : void + { + $this->h1 = Lang::main('moreTitles', $this->pageName); + + array_unshift($this->title, $this->h1); + + parent::generate(); + } +} + +?> 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 @@ + ['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 @@ + ['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 @@ + ['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 @@ + ['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 new file mode 100644 index 00000000..a95221bf --- /dev/null +++ b/endpoints/skill/skill.php @@ -0,0 +1,374 @@ +typeId = intVal($id); + $this->contribute = Type::getClassAttrib($this->type, 'contribute') ?? CONTRIBUTE_NONE; + } + + protected function generate() : void + { + $this->subject = new SkillList(array(['id', $this->typeId])); + if ($this->subject->error) + $this->generateNotFound(Lang::game('skill'), Lang::skill('notFound')); + + $this->h1 = $this->subject->getField('name', true); + + $this->gPageInfo += array( + 'type' => $this->type, + 'typeId' => $this->typeId, + 'name' => $this->h1 + ); + + $_cat = $this->subject->getField('typeCat'); + + + /*************/ + /* Menu Path */ + /*************/ + + if (in_array($this->typeId, SKILLS_TRADE_PRIMARY) || in_array($this->typeId, SKILLS_TRADE_SECONDARY)) + $this->breadcrumb[] = $this->typeId; + else + $this->breadcrumb[] = $_cat; + + + /**************/ + /* Page Title */ + /**************/ + + array_unshift($this->title, $this->h1, Util::ucFirst(Lang::game('skill'))); + + + /***********/ + /* Infobox */ + /***********/ + + $infobox = Lang::getInfoBoxForFlags($this->subject->getField('cuFlags')); + + // id + $infobox[] = Lang::skill('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'); + + + /****************/ + /* Main Content */ + /****************/ + + $this->headIcons = [$this->subject->getField('iconString')]; + $this->redButtons = array( + BUTTON_WOWHEAD => true, + BUTTON_LINKS => ['type' => $this->type, 'typeId' => $this->typeId] + ); + + if ($_ = $this->subject->getField('description', true)) + $this->extraText = new Markup($_, ['dbpage' => true, 'allow' => Markup::CLASS_ADMIN], 'text-generic'); + + + /**************/ + /* Extra Tabs */ + /**************/ + + parent::generate(); + + $this->lvTabs = new Tabs(['parent' => "\$\$WH.ge('tabs-generic')"], 'tabsRelated', true); + + if (in_array($_cat, [-5, 9, 11])) + { + // tab: recipes [spells] (crafted) + $condition = array( + [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 + if (!$recipes->error) + { + $this->extendGlobalData($recipes->getJSGlobals(GLOBALINFO_SELF | GLOBALINFO_RELATED)); + $this->lvTabs->addListviewTab(new Listview(array( + 'data' => $recipes->getListviewData(), + 'id' => 'recipes', + 'name' => '$LANG.tab_recipes', + 'visibleCols' => ['reagents', 'source'], + 'note' => sprintf(Util::$filterResultString, '?spells='.$_cat.'.'.$this->typeId.'&filter=cr=20;crs=1;crv=0') + ), SpellList::$brickFile)); + } + + // tab: recipe Items [items] (Books) + $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] + ); + + $recipeItems = new ItemList($conditions); + if (!$recipeItems->error) + { + $this->extendGlobalData($recipeItems->getJSGlobals(GLOBALINFO_SELF)); + + $tabData = array( + 'data' => $recipeItems->getListviewData(), + 'id' => 'recipe-items', + 'name' => '$LANG.tab_recipeitems', + ); + + if ($_ = array_search($this->typeId, $filterRecipe)) + $tabData['note'] = sprintf(Util::$filterResultString, "?items=9.".$_); + + $this->lvTabs->addListviewTab(new Listview($tabData, ItemList::$brickFile)); + } + + // tab: crafted items [items] + $created = []; + foreach ($recipes->iterate() as $__) + if ($idx = $recipes->canCreateItem()) + foreach ($idx as $i) + $created[] = $recipes->getField('effect'.$i.'CreateItemId'); + + if ($created) + { + $created = new ItemList(array(['i.id', $created])); + if (!$created->error) + { + $this->extendGlobalData($created->getJSGlobals(GLOBALINFO_SELF)); + + $tabData = array( + 'data' => $created->getListviewData(), + 'id' => 'crafted-items', + 'name' => '$LANG.tab_crafteditems', + ); + + if (!is_null($_ = ItemListFilter::getCriteriaIndex(86, $this->typeId))) + $tabData['note'] = sprintf(Util::$filterResultString, "?items&filter=cr=86;crs=".$_.";crv=0"); + + $this->lvTabs->addListviewTab(new Listview($tabData, ItemList::$brickFile)); + } + } + + // tab: required by [item] + $conditions = array( + ['requiredSkill', $this->typeId], + ['class', ITEM_CLASS_RECIPE, '!'] + ); + + $reqBy = new ItemList($conditions); + if (!$reqBy->error) + { + $this->extendGlobalData($reqBy->getJSGlobals(GLOBALINFO_SELF)); + + $tabData = array( + 'data' => $reqBy->getListviewData(), + 'id' => 'required-by', + 'name' => '$LANG.tab_requiredby', + ); + + if (!is_null($_ = ItemListFilter::getCriteriaIndex(99, $this->typeId))) + $tabData['note'] = sprintf(Util::$filterResultString, "?items&filter=cr=99:168;crs=".$_.":2;crv=0:0"); + + $this->lvTabs->addListviewTab(new Listview($tabData, ItemList::$brickFile)); + } + + // tab: required by [itemset] + $reqBy = new ItemsetList(array(['skillId', $this->typeId])); + if (!$reqBy->error) + { + $this->extendGlobalData($reqBy->getJSGlobals(GLOBALINFO_SELF)); + + $this->lvTabs->addListviewTab(new Listview(array( + 'data' => $reqBy->getListviewData(), + 'id' => 'required-by-set', + 'name' => '$LANG.tab_requiredby' + ), ItemsetList::$brickFile)); + } + } + + // 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( + [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(DB::AND, ['s.skillLine1', $line1], ['s.skillLine2OrMask', 1 << $idx, '&']); + break 2; + } + + $spells = new SpellList($condition); + if (!$spells->error) + { + foreach ($spells->iterate() as $__) + { + $reqClass |= $spells->getField('reqClassMask'); + $reqRace |= $spells->getField('reqRaceMask'); + } + + $this->extendGlobalData($spells->getJSGlobals(GLOBALINFO_SELF)); + + $tabData = array( + 'data' => $spells->getListviewData(), + 'visibleCols' => ['source'] + ); + + if ($this->typeId != 769) // Internal + $tabData['note'] = match ($_cat) + { + -4, 7 => sprintf(Util::$filterResultString, '?spells=-4'), + 7 => sprintf(Util::$filterResultString, '?spells='.$_cat.'.'.ChrClass::fromMask($reqClass)[0].'.'.$this->typeId), // doesn't matter what spell; reqClass should be identical for all Class Spells + 9, 11 => sprintf(Util::$filterResultString, '?spells='.$_cat.'.'.$this->typeId), + default => null + }; + + $this->lvTabs->addListviewTab(new Listview($tabData, SpellList::$brickFile)); + } + + // tab: trainers [npcs] + if (in_array($_cat, [-5, 6, 7, 8, 9, 11])) + { + $mask = 0; + foreach (Game::$skillLineMask[-3] as $idx => [, $skillLineId]) + if ($skillLineId == $this->typeId) + $mask |= 1 << $idx; + + $spellIds = DB::Aowow()->selectCol( + '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 + ); + + $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(['ct.id', $list], ['s.guid', NULL, '!'], ['ct.npcflag', 0x10, '&'])); + + if (!$trainer->error) + { + $this->extendGlobalData($trainer->getJSGlobals()); + + $this->addDataLoader('zones'); + $this->lvTabs->addListviewTab(new Listview(array( + 'data' => $trainer->getListviewData(), + 'id' => 'trainer', + 'name' => '$LANG.tab_trainers', + ), CreatureList::$brickFile)); + } + } + } + + // tab: quests [quests] + // only for professions + $sort = match ($this->typeId) + { + SKILL_HERBALISM => 24, + SKILL_FISHING => 101, + SKILL_BLACKSMITHING => 121, + SKILL_ALCHEMY => 181, + SKILL_LEATHERWORKING => 182, + SKILL_ENGINEERING => 201, + SKILL_TAILORING => 264, + SKILL_COOKING => 304, + SKILL_FIRST_AID => 324, + SKILL_INSCRIPTION => 371, + SKILL_JEWELCRAFTING => 373, + default => 0 + }; + + if ($sort) + { + $quests = new QuestList(array(['questSortId', -$sort])); + if (!$quests->error) + { + $this->extendGlobalData($quests->getJSGlobals()); + $this->lvTabs->addListviewTab(new Listview(['data' => $quests->getListviewData()], QuestList::$brickFile)); + } + } + + // tab: related classes (apply classes from [spells]) + if ($class = ChrClass::fromMask($reqClass)) + { + $classes = new CharClassList(array(['id', $class])); + if (!$classes->error) + $this->lvTabs->addListviewTab(new Listview(['data' => $classes->getListviewData()], CharClassList::$brickFile)); + } + + // tab: related races (apply races from [spells]) + if ($race = ChrRace::fromMask($reqRace)) + { + $races = new CharRaceList(array(['id', $race])); + if (!$races->error) + $this->lvTabs->addListviewTab(new Listview(['data' => $races->getListviewData()], CharRaceList::$brickFile)); + } + + // tab: condition-for + $cnd = new Conditions(); + $cnd->getByCondition(Type::SKILL, $this->typeId)->prepare(); + if ($tab = $cnd->toListviewTab('condition-for', '$LANG.tab_condition_for')) + { + $this->extendGlobalData($cnd->getJsGlobals()); + $this->lvTabs->addDataTab(...$tab); + } + } +} + +?> diff --git a/endpoints/skills/skills.php b/endpoints/skills/skills.php new file mode 100644 index 00000000..1d1f1a36 --- /dev/null +++ b/endpoints/skills/skills.php @@ -0,0 +1,66 @@ +getCategoryFromUrl($rawParam); + + parent::__construct($rawParam); + } + + protected function generate() : void + { + $this->h1 = Util::ucFirst(Lang::game('skills')); + + + if ($this->category) + $this->breadcrumb[] = $this->category[0]; + + + array_unshift($this->title, $this->h1); + if ($this->category) + array_unshift($this->title, Lang::skill('cat', $this->category[0])); + + + $this->redButtons[BUTTON_WOWHEAD] = true; + + $conditions = [Listview::DEFAULT_SIZE]; + if (!User::isInGroup(U_GROUP_EMPLOYEE)) + $conditions[] = [['cuFlags', CUSTOM_EXCLUDE_FOR_LISTVIEW, '&'], 0]; + + if ($this->category) + $conditions[] = ['typeCat', $this->category[0]]; + + $tabData = ['data' => []]; + $skills = new SkillList($conditions); + if (!$skills->error) + $tabData['data'] = $skills->getListviewData(); + + $this->lvTabs = new Tabs(['parent' => "\$\$WH.ge('tabs-generic')"]); + + $this->lvTabs->addListviewTab(new Listview($tabData, SkillList::$brickFile)); + + parent::generate(); + } +} + +?> diff --git a/endpoints/sound/sound.php b/endpoints/sound/sound.php new file mode 100644 index 00000000..8db7cf8f --- /dev/null +++ b/endpoints/sound/sound.php @@ -0,0 +1,320 @@ +typeId = intVal($id); + $this->contribute = Type::getClassAttrib($this->type, 'contribute') ?? CONTRIBUTE_NONE; + } + + protected function generate() : void + { + $this->subject = new SoundList(array(['id', $this->typeId])); + if ($this->subject->error) + $this->generateNotFound(Lang::game('sound'), Lang::sound('notFound')); + + $this->h1 = $this->subject->getField('name'); + + $this->gPageInfo += array( + 'type' => $this->type, + 'typeId' => $this->typeId, + 'name' => $this->h1 + ); + + $_cat = $this->subject->getField('cat'); + + + /*************/ + /* Menu Path */ + /*************/ + + $this->breadcrumb[] = $_cat; + + + /**************/ + /* Page Title */ + /**************/ + + array_unshift($this->title, $this->h1, Util::ucFirst(Lang::game('sound'))); + + + /****************/ + /* Main Content */ + /****************/ + + // get spawns + if ($spawns = $this->subject->getSpawns(SPAWNINFO_FULL)) + { + $this->addDataLoader('zones'); + $this->map = array( + ['parent' => 'mapper-generic'], // Mapper + $spawns, // mapperData + null, // ShowOnMap + [Lang::sound('foundIn')] // foundIn + ); + foreach ($spawns as $areaId => $__) + $this->map[3][$areaId] = ZoneList::getName($areaId); + } + + // 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` = %i', $this->typeId); + + $this->redButtons = array( + BUTTON_WOWHEAD => true, + BUTTON_PLAYLIST => true, + BUTTON_LINKS => array( + 'type' => Type::SOUND, + 'typeId' => $this->typeId, + 'sound' => str_replace('\\', '\\\\', $fullpath) // escape for wow client + ) + ); + + $this->extendGlobalData($this->subject->getJSGlobals()); + + parent::generate(); + + + /**************/ + /* Extra Tabs */ + /**************/ + + $this->lvTabs = new Tabs(['parent' => "\$\$WH.ge('tabs-generic')"], 'tabsRelated', true); + + // tab: Spells + // skipping (always empty): ready, castertargeting, casterstate, targetstate + $displayIds = DB::Aowow()->selectCol( + 'SELECT `id` + 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` = %i OR `ambienceNight` = %i OR `musicDay` = %i OR `musicNight` = %i', + $this->typeId, $this->typeId, $this->typeId, $this->typeId + ); + + $cnd = array( + 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) + $cnd[] = ['spellVisualId', $displayIds]; + + if ($seMiscValues) + $cnd[] = array( + 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); + if (!$spells->error) + { + $this->extendGlobalData($spells->getJSGlobals(GLOBALINFO_SELF)); + $this->lvTabs->addListviewTab(new Listview(['data' => $spells->getListviewData()], SpellList::$brickFile)); + } + + // tab: Items + $subClasses = []; + 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; + + $where = array( + ['`pickUpSoundId` = %i', $this->typeId], + ['`dropDownSoundId` = %i', $this->typeId], + ['`sheatheSoundId` = %i', $this->typeId], + ['`unsheatheSoundId` = %i', $this->typeId] + ); + 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) + { + $this->extendGlobalData($items->getJSGlobals(GLOBALINFO_SELF)); + $this->lvTabs->addListviewTab(new Listview(['data' => $items->getListviewData()], ItemList::$brickFile)); + } + } + + // tab: Zones + 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) + { + $this->extendGlobalData($zones->getJSGlobals(GLOBALINFO_SELF)); + + $zoneData = $zones->getListviewData(); + $parents = $zones->getAllFields('parentArea'); + $tabData = []; + $pIds = array_filter(array_unique(array_values($parents))); + if ($pIds) + { + $pZones = new ZoneList(array(['id', $pIds])); + if (!$pZones->error) + { + $this->extendGlobalData($pZones->getJSGlobals(GLOBALINFO_SELF)); + + $pData = $pZones->getListviewData(); + foreach ($parents as $child => $parent) + { + if (!$parent || empty($pData[$parent])) + continue; + + if (!isset($pData[$parent]['subzones'])) + $pData[$parent]['subzones'] = []; + + $pData[$parent]['subzones'][] = $child; + unset($parents[$child]); + } + + // these are original parents + foreach ($parents as $parent => $__) + if (empty($pData[$parent])) + $pData[$parent] = $zoneData[$parent]; + + $zoneData = $pData; + } + } + + if ($worldStates = array_filter($zoneIds, fn($x) => $x['worldStateId'] > 0)) + { + $tabData['extraCols'] = ['$Listview.extraCols.condition']; + + foreach ($worldStates as $state) + { + if (isset($zoneData[$state['id']])) + 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'] ?? [])) + Conditions::extendListviewRow($d, Conditions::SRC_NONE, $this->typeId, [Conditions::WORLD_STATE, $state['worldStateId'], $state['worldStateValue']]); + } + } + + $tabData['data'] = $zoneData; + $tabData['hiddenCols'] = ['territory']; + + $this->lvTabs->addListviewTab(new Listview($tabData, ZoneList::$brickFile)); + } + } + + // tab: Races (VocalUISounds (containing error voice overs)) + 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) + { + $this->extendGlobalData($races->getJSGlobals(GLOBALINFO_SELF)); + $this->lvTabs->addListviewTab(new Listview(['data' => $races->getListviewData()], CharRaceList::$brickFile)); + } + } + + // tab: Emotes (EmotesTextSound (containing emote audio)) + 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) + { + $this->extendGlobalData($races->getJSGlobals(GLOBALINFO_SELF)); + $this->lvTabs->addListviewTab(new Listview(array( + 'data' => $races->getListviewData(), + 'name' => Util::ucFirst(Lang::game('emotes')) + ), EmoteList::$brickFile, 'emote')); + } + } + + $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)) + $creatureIds = array_merge($creatureIds, $goosp[Type::NPC]); + + // tab: NPC (dialogues...?, generic creature sound) + // skipping (always empty): transforms, footsteps + $displayIds = DB::Aowow()->selectCol( + 'SELECT `id` + 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, + $this->typeId, $this->typeId, $this->typeId, $this->typeId, $this->typeId, + $this->typeId, $this->typeId, $this->typeId, $this->typeId, $this->typeId, $this->typeId + ); + + // broadcast_text <-> creature_text + if ($creatureIds || $displayIds) + { + $extra = []; + $cnds = [&$extra]; + if (!User::isInGroup(U_GROUP_STAFF)) + $cnds[] = [['cuFlags', CUSTOM_EXCLUDE_FOR_LISTVIEW, '&'], 0]; + + if ($creatureIds) + $extra[] = ['id', $creatureIds]; + + if ($displayIds) + $extra[] = ['displayId1', $displayIds]; + + if (count($extra) > 1) + array_unshift($extra, DB::OR); + else + $extra = array_pop($extra); + + $npcs = new CreatureList($cnds); + if (!$npcs->error) + { + $this->extendGlobalData($npcs->getJSGlobals(GLOBALINFO_SELF)); + + $this->addDataLoader('zones'); + $this->lvTabs->addListviewTab(new Listview(['data' => $npcs->getListviewData()], CreatureList::$brickFile)); + } + } + } +} + + +?> diff --git a/endpoints/sound/sound_playlist.php b/endpoints/sound/sound_playlist.php new file mode 100644 index 00000000..334117ae --- /dev/null +++ b/endpoints/sound/sound_playlist.php @@ -0,0 +1,26 @@ +h1 = Lang::sound('cat', 1000); + + array_unshift($this->title, $this->h1, Util::ucFirst(Lang::game('sound'))); + + parent::generate(); + } +} + +?> diff --git a/endpoints/sounds/sounds.php b/endpoints/sounds/sounds.php new file mode 100644 index 00000000..eef7a1ea --- /dev/null +++ b/endpoints/sounds/sounds.php @@ -0,0 +1,120 @@ + ['filter' => FILTER_VALIDATE_REGEXP, 'options' => ['regexp' => Filter::PATTERN_PARAM]] + ); + 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 $rawParam) + { + $this->getCategoryFromUrl($rawParam); + if ($this->category) + $this->forward('?sounds&filter=ty='.$this->category[0]); + + parent::__construct($rawParam); + + if ($this->category) + $this->subCat = '='.implode('.', $this->category); + + $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; + } + + protected function generate() : void + { + $this->h1 = Util::ucFirst(Lang::game('sounds')); + + $conditions = [Listview::DEFAULT_SIZE]; + if (!User::isInGroup(U_GROUP_EMPLOYEE)) + $conditions[] = [['cuFlags', CUSTOM_EXCLUDE_FOR_LISTVIEW, '&'], 0]; + + if ($_ = $this->filter->getConditions()) + $conditions[] = $_; + + + /**************/ + /* Page Title */ + /**************/ + + $fiForm = $this->filter->values; + + array_unshift($this->title, $this->h1); + if (count($fiForm['ty']) == 1) + array_unshift($this->title, Lang::sound('cat', $fiForm['ty'][0])); + + + /*************/ + /* Menu Path */ + /*************/ + + if (count($fiForm['ty']) == 1) + $this->breadcrumb[] = $fiForm['ty'][0]; + + + /****************/ + /* Main Content */ + /****************/ + + $this->redButtons = array( + BUTTON_WOWHEAD => true, + BUTTON_PLAYLIST => true + ); + if ($fiQuery = $this->filter->buildGETParam()) + $this->wowheadLink .= '&filter='.$fiQuery; + + $tabData = []; + $sounds = new SoundList($conditions, ['calcTotal' => true]); + if (!$sounds->error) + { + $tabData['data'] = $sounds->getListviewData(); + + // create note if search limit was exceeded; overwriting 'note' is intentional + if ($sounds->getMatches() > Listview::DEFAULT_SIZE) + { + $tabData['note'] = sprintf(Util::$tryFilteringString, 'LANG.lvnote_soundsfound', $sounds->getMatches(), Listview::DEFAULT_SIZE); + $tabData['_truncated'] = 1; + } + } + + $this->lvTabs = new Tabs(['parent' => "\$\$WH.ge('tabs-generic')"]); + + $this->lvTabs->addListviewTab(new Listview($tabData, SoundList::$brickFile)); + + parent::generate(); + + $this->setOnCacheLoaded([self::class, 'onBeforeDisplay']); + } + + public static function onBeforeDisplay() + { + // sort for dropdown-menus in filter + Lang::sort('sound', 'cat'); + } +} + +?> diff --git a/endpoints/spell/spell.php b/endpoints/spell/spell.php new file mode 100644 index 00000000..3819ddc6 --- /dev/null +++ b/endpoints/spell/spell.php @@ -0,0 +1,2536 @@ +typeId = intVal($id); + $this->contribute = Type::getClassAttrib($this->type, 'contribute') ?? CONTRIBUTE_NONE; + } + + protected function generate() : void + { + $this->subject = new SpellList(array(['id', $this->typeId])); + if ($this->subject->error) + $this->generateNotFound(Lang::game('spell'), Lang::spell('notFound')); + + if ($jsg = $this->subject->getJSGlobals(GLOBALINFO_ANY, $extra)) + $this->extendGlobalData($jsg, $extra); + + $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", + `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` = %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 + 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` = %i', + $this->typeId + ); + + $this->h1 = Util::htmlEscape($this->subject->getField('name', true)); + + $this->gPageInfo += array( + 'type' => $this->type, + 'typeId' => $this->typeId, + 'name' => $this->subject->getField('name', true) + ); + + + /*************/ + /* Menu Path */ + /*************/ + + $this->generatePath(); + + + /**************/ + /* Page Title */ + /**************/ + + array_unshift($this->title, $this->subject->getField('name', true), Util::ucFirst(Lang::game('spell'))); + + + /***********/ + /* Infobox */ + /***********/ + + $this->createInfobox(); + + + /***************/ + /* Red Buttons */ + /***************/ + + $this->redButtons = array( + BUTTON_VIEW3D => false, + BUTTON_WOWHEAD => true, + BUTTON_LINKS => array( + 'linkColor' => 'ff71d5ff', + 'linkId' => Type::getFileString(Type::SPELL).':'.$this->typeId, + 'linkName' => $this->subject->getField('name', true), + 'type' => $this->type, + 'typeId' => $this->typeId + ) + ); + + // could have multiple models set, one per effect + foreach ($this->modelInfo as $mI) + { + $this->redButtons[BUTTON_VIEW3D] = ['type' => $mI['type'], 'displayId' => $mI['displayId']]; + + if (isset($mI['humanoid'])) + { + $this->redButtons[BUTTON_VIEW3D]['typeId'] = $mI['typeId']; + $this->redButtons[BUTTON_VIEW3D]['humanoid'] = 1; + } + + break; + } + + + /*******************/ + /* Reagent Listing */ + /*******************/ + + $this->createReagentList(); + + + /******************/ + /* Required Items */ + /******************/ + + $this->createRequiredItems(); + + + /*************************/ + /* Required Tools/Totems */ + /*************************/ + + // prepare Tools + foreach ($this->subject->getToolsForCurrent() as $tool) + $this->tools[] = new IconElement( + Type::ITEM, + $tool['itemId'] ?? 0, + $tool['name'], + quality: ITEM_QUALITY_NORMAL, + size: IconElement::SIZE_SMALL, + url: !isset($tool['itemId']) ? '?items&filter=cr=91;crs='.$tool['id'].';crv=0' : '', + element: 'iconlist-icon' + ); + + + /**********************/ + /* Spell Effect Block */ + /**********************/ + + $this->createEffects(); + + + /**************************************************/ + /* Spell Attributes listing and SpellFilter links */ + /**************************************************/ + + $this->createAttributesList(); + + + /*************************/ + /* Factionchange pendant */ + /*************************/ + + // factionchange-equivalent + 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) + { + $this->transfer = Lang::spell('_transfer', array( + $altSpell->id, + ITEM_QUALITY_NORMAL, + $altSpell->getField('iconString'), + $altSpell->getField('name', true), + $pendant > 0 ? 'alliance' : 'horde', + $pendant > 0 ? Lang::game('si', SIDE_ALLIANCE) : Lang::game('si', SIDE_HORDE) + )); + } + } + + + /****************/ + /* Main Content */ + /****************/ + + $this->powerCost = $this->subject->createPowerCostForCurrent(); + $this->castTime = $this->subject->createCastTimeForCurrent(false, false); + $this->level = $this->subject->getField('spellLevel'); + $this->rangeName = $this->subject->getField('rangeText', true); + $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; + $this->tooltip = array( + $this->subject->getField('iconString'), + $this->subject->getField('stackAmount') ?: ($this->subject->getField('procCharges') > 1 ? $this->subject->getField('procCharges') : 0), + $this->subject->getField('buff', true, true) ? 1 : 0 + ); + $this->gcdCat = match((int)$this->subject->getField('startRecoveryCategory')) + { + 133 => Lang::spell('normal'), + 330, // Mounts + 1156, // Heart of the Phoenix + 1159, // Ignis Grab and Slag Pot + 1164, // Kessel Run Elek + 1173, // Birmingham Test Spells + 1178, // Stealth (Druid Cat, Rogue, Hunter Cat Pets) + Charge (Warrior) + 1244 => Lang::spell('special'), // Argent Tournament Vehcile Jousting Abilities + default => '' // n/a + }; + + $this->range = $this->subject->getField('rangeMaxHostile'); + if ($_ = $this->subject->getField('rangeMinHostile')) + $this->range = $_.' - '.$this->range; + + if (!($this->subject->getField('attributes2') & SPELL_ATTR2_NOT_NEED_SHAPESHIFT)) + $this->stances = Lang::getStances($this->subject->getField('stanceMask')); + + if (($_ = $this->subject->getField('recoveryTime')) && $_ > 0) + $this->cooldown = DateTime::formatTimeElapsedFloat($_); + else if (($_ = $this->subject->getField('recoveryCategory')) && $_ > 0) + $this->cooldown = DateTime::formatTimeElapsedFloat($_); + + if (($_ = $this->subject->getField('duration')) && $_ > 0) + $this->duration = DateTime::formatTimeElapsedFloat($_); + + + /**************/ + /* Extra Tabs */ + /**************/ + + $this->lvTabs = new Tabs(['parent' => "\$\$WH.ge('tabs-generic')"], 'tabsRelated', true); + + $ubSAI = SmartAI::getOwnerOfSpellCast($this->typeId); + + // tab: abilities [of shapeshift form] + $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` = %i', $this->subject->getField('effect'.$i.'MiscValue'))) + $formSpells = array_merge($formSpells, $_); + + if ($formSpells) + { + $abilities = new SpellList(array(['id', $formSpells])); + if (!$abilities->error) + { + $tabData = array( + 'data' => $abilities->getListviewData(), + 'id' => 'controlledabilities', + 'name' => '$LANG.tab_controlledabilities', + 'visibleCols' => ['level'], + ); + + if (!$abilities->hasSetFields('skillLines')) + $tabData['hiddenCols'] = ['skill']; + + $this->lvTabs->addListviewTab(new Listview($tabData, SpellList::$brickFile)); + + $this->extendGlobalData($abilities->getJSGlobals(GLOBALINFO_SELF)); + } + } + + // tab: [$this] modifies + $sub = []; + $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')], + &$sub + ]; + $modifiesData = []; + $hideSkillCol = true; + + for ($i = 1; $i < 4; $i++) + { + if (!in_array($this->subject->getField('effect'.$i.'AuraId'), self::MOD_AURAS)) + continue; + + $m1 = $this->subject->getField('effect'.$i.'SpellClassMaskA'); + $m2 = $this->subject->getField('effect'.$i.'SpellClassMaskB'); + $m3 = $this->subject->getField('effect'.$i.'SpellClassMaskC'); + + if (!$m1 && !$m2 && !$m3) + continue; + + $classSpells = $miscSpells = []; + $this->effects[$i]['modifies'] = [&$classSpells, &$miscSpells]; + + $sub = [DB::OR, ['s.spellFamilyFlags1', $m1, '&'], ['s.spellFamilyFlags2', $m2, '&'], ['s.spellFamilyFlags3', $m3, '&']]; + + $modSpells = new SpellList($conditions); + if (!$modSpells->error) + { + foreach ($modSpells->iterate() as $id => $__) + { + if (in_array($modSpells->getField('typeCat'), [-2, 7])) + $classSpells[$id] = [new IconElement(Type::SPELL, $id, $modSpells->getField('name', true), size: IconElement::SIZE_SMALL), []]; + else + $miscSpells[$id] = [new IconElement(Type::SPELL, $id, $modSpells->getField('name', true), size: IconElement::SIZE_SMALL), []]; + } + + if ($classSpells) + { + 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); + + if ($spellId != $firstSpellId) + unset($classSpells[$spellId]); + } + + array_walk($classSpells, function(&$x) { + if ($x[1] && $x[1][0] == $x[1][1]) // only one rank => unset + $x[1] = null; + }); + } + + $modifiesData += $modSpells->getListviewData(); + if ($modSpells->hasSetFields('skillLines')) + $hideSkillCol = false; + + $this->extendGlobalData($modSpells->getJSGlobals(GLOBALINFO_SELF | GLOBALINFO_RELATED)); + } + + $classSpells = array_values($classSpells); + $miscSpells = array_values($miscSpells); + + unset($classSpells, $miscSpells); + } + + if ($modifiesData) + { + $tabData = array( + 'data' => $modifiesData, + 'id' => 'modifies', + 'name' => '$LANG.tab_modifies', + 'visibleCols' => ['level'], + ); + + if ($hideSkillCol) + $tabData['hiddenCols'] = ['skill']; + + $this->lvTabs->addListviewTab(new Listview($tabData, SpellList::$brickFile)); + } + + // tab: [$this is] modified by + $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')], + &$sub + ]; + + for ($i = 1; $i < 4; $i++) + { + $m1 = $this->subject->getField('spellFamilyFlags1'); + $m2 = $this->subject->getField('spellFamilyFlags2'); + $m3 = $this->subject->getField('spellFamilyFlags3'); + + if (!$m1 && !$m2 && !$m3) + continue; + + $sub[] = array( + DB::AND, + ['s.effect'.$i.'AuraId', self::MOD_AURAS], + [ + DB::OR, + ['s.effect'.$i.'SpellClassMaskA', $m1, '&'], + ['s.effect'.$i.'SpellClassMaskB', $m2, '&'], + ['s.effect'.$i.'SpellClassMaskC', $m3, '&'] + ] + ); + } + + if (count($sub) > 1) + { + $modsSpell = new SpellList($conditions); + if (!$modsSpell->error) + { + $tabData = array( + 'data' => $modsSpell->getListviewData(), + 'id' => 'modified-by', + 'name' => '$LANG.tab_modifiedby', + 'visibleCols' => ['level'], + ); + + if (!$modsSpell->hasSetFields('skillLines')) + $tabData['hiddenCols'] = ['skill']; + + $this->lvTabs->addListviewTab(new Listview($tabData, SpellList::$brickFile)); + + $this->extendGlobalData($modsSpell->getJSGlobals(GLOBALINFO_SELF | GLOBALINFO_RELATED)); + } + } + + // tab: see also + $conditions = array( + ['s.schoolMask', $this->subject->getField('schoolMask')], + ['s.effect1Id', $this->subject->getField('effect1Id')], + ['s.effect2Id', $this->subject->getField('effect2Id')], + ['s.effect3Id', $this->subject->getField('effect3Id')], + ['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) + { + $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 (!$saSpells->hasSetFields('skillLines')) + $tabData['hiddenCols'] = ['skill']; + + if (isset($saE)) + $tabData['extraCols'] = $saE; + + $this->lvTabs->addListviewTab(new Listview($tabData, SpellList::$brickFile)); + + $this->extendGlobalData($saSpells->getJSGlobals(GLOBALINFO_SELF | GLOBALINFO_RELATED)); + } + + // tab: shared cooldown + if ($this->subject->getField('recoveryCategory')) + { + $conditions = array( + ['id', $this->typeId, '!'], + ['category', $this->subject->getField('category')], + ['recoveryCategory', 0, '>'], + ); + + // limit shared cooldowns to same player class for regulat users + if (!User::isInGroup(U_GROUP_STAFF) && $this->subject->getField('spellFamilyId')) + $conditions[] = ['spellFamilyId', $this->subject->getField('spellFamilyId')]; + + $cdSpells = new SpellList($conditions); + if (!$cdSpells->error) + { + $this->lvTabs->addListviewTab(new Listview(array( + 'data' => $cdSpells->getListviewData(), + 'name' => '$LANG.tab_sharedcooldown', + 'id' => 'shared-cooldown' + ), SpellList::$brickFile)); + + $this->extendGlobalData($cdSpells->getJSGlobals(GLOBALINFO_SELF | GLOBALINFO_RELATED)); + } + } + + // tab: glyphs + if ($gpIds = DB::Aowow()->selectCol('SELECT `id` FROM ::glyphproperties WHERE `spellId` = %i', $this->typeId)) + { + $conditions = array( + 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) + { + $this->lvTabs->addListviewTab(new Listview(array( + 'data' => $ubSpells->getListviewData(), + 'id' => 'used-by-spell', + 'name' => '$LANG.tab_usedby' + ), SpellList::$brickFile)); + + $this->extendGlobalData($ubSpells->getJSGlobals(GLOBALINFO_SELF)); + } + } + + // tab: used by - itemset + $conditions = array( + 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); + if (!$ubSets->error) + { + $this->lvTabs->addListviewTab(new Listview(array( + 'data' => $ubSets->getListviewData(), + 'id' => 'used-by-itemset', + 'name' => '$LANG.tab_usedby' + ), ItemsetList::$brickFile)); + + $this->extendGlobalData($ubSets->getJSGlobals(GLOBALINFO_SELF | GLOBALINFO_RELATED)); + } + + // tab: used by - item + $conditions = array( + 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); + if (!$ubItems->error) + { + $this->lvTabs->addListviewTab(new Listview(array( + 'data' => $ubItems->getListviewData(), + 'id' => 'used-by-item', + 'name' => '$LANG.tab_usedby' + ), ItemList::$brickFile)); + + $this->extendGlobalData($ubItems->getJSGlobals(GLOBALINFO_SELF)); + } + + // tab: used by - object + $conditions = array( + DB::OR, + ['onUseSpell', $this->typeId], ['onSuccessSpell', $this->typeId], + ['auraSpell', $this->typeId], ['triggeredSpell', $this->typeId] + ); + if (!empty($ubSAI[Type::OBJECT])) + $conditions[] = ['id', $ubSAI[Type::OBJECT]]; + + $ubObjects = new GameObjectList($conditions); + if (!$ubObjects->error) + { + $this->addDataLoader('zones'); + $this->lvTabs->addListviewTab(new Listview(array( + 'data' => $ubObjects->getListviewData(), + 'id' => 'used-by-object', + 'name' => '$LANG.tab_usedby' + ), GameObjectList::$brickFile)); + + $this->extendGlobalData($ubObjects->getJSGlobals()); + } + + // tab: used by - areatrigger + if (User::isInGroup(U_GROUP_EMPLOYEE)) + { + if (!empty($ubSAI[Type::AREATRIGGER])) + { + $ubTriggers = new AreaTriggerList(array(['id', $ubSAI[Type::AREATRIGGER]])); + if (!$ubTriggers->error) + { + $this->lvTabs->addListviewTab(new Listview(array( + 'data' => $ubTriggers->getListviewData(), + 'id' => 'used-by-areatrigger', + 'name' => '$LANG.tab_usedby' + ), AreaTriggerList::$brickFile, 'areatrigger')); + } + } + } + + // 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) + { + $this->lvTabs->addListviewTab(new Listview(array( + 'data' => $coAchievemnts->getListviewData(), + 'id' => 'criteria-of', + 'name' => '$LANG.tab_criteriaof' + ), AchievementList::$brickFile)); + + $this->extendGlobalData($coAchievemnts->getJSGlobals(GLOBALINFO_SELF | GLOBALINFO_RELATED)); + } + + // tab: contains + // spell_loot_template + $spellLoot = new LootByContainer(); + if ($spellLoot->getByContainer(Loot::SPELL, [$this->typeId])) + { + $this->extendGlobalData($spellLoot->jsGlobals); + + $extraCols = $spellLoot->extraCols; + $extraCols[] = '$Listview.extraCols.percent'; + + $this->lvTabs->addListviewTab(new Listview(array( + '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` = %i', $this->firstRank)) + { + $groups = DB::World()->selectCol('SELECT `id` AS ARRAY_KEY, `spell_id` AS ARRAY_KEY2, `spell_id` FROM spell_group'); + // unpack recursion + foreach ($groups as $i => $group) + { + foreach ($group as $j => $g) + { + if ($g > 0) + continue; + + foreach ($groups[-$g] ?? [] as $new) + $groups[$i][] = $new; + + unset($group[$j]); + } + } + + // find ourselves + 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 %in', array_keys($filtered)); + + // only use groups that have rules set + if ($filtered = array_intersect_key($filtered, $rules)) + { + $cnd = [DB::OR]; + foreach ($filtered as $gr) + $cnd[] = ['s.id', $gr]; + + $stacks = new SpellList($cnd); + if (!$stacks->error) + { + $lvData = $stacks->getListviewData(); + $this->extendGlobalData($stacks->getJSGlobals(GLOBALINFO_SELF | GLOBALINFO_RELATED)); + if (!$stacks->hasSetFields('skillLines')) + $sH = ['skill']; + + foreach ($filtered as $gId => $spellIds) + { + $data = []; + foreach ($spellIds as $id) + if (isset($lvData[$id]) && $id != $this->firstRank) + $data[] = array_merge($lvData[$id], ['stackRule' => $rules[$gId]]); + + if (!$data) + continue; + + $tabData = array( + 'data' => $data, + 'id' => 'spell-group-stack-'.$gId, + 'name' => Lang::spell('stackGroup'), + 'visibleCols' => ['stackRules'] + ); + + if (isset($sH)) + $tabData['hiddenCols'] = $sH; + + $this->lvTabs->addListviewTab(new Listview($tabData, SpellList::$brickFile)); + } + } + } + } + } + + // tab: linked with + $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`) = %i OR ABS(`spell_trigger`) = %i', + $this->typeId, $this->typeId, $this->typeId + ); + + $related = []; + foreach ($rows as $row) + $related[] = $row['related']; + + if ($related) + $linked = new SpellList(array(['s.id', $related])); + + if (isset($linked) && !$linked->error) + { + $lv = $linked->getListviewData(); + $data = []; + + foreach ($rows as $r) + { + foreach ($lv as $dk => $d) + { + if ($r['related'] != $dk) + continue; + + $lv[$dk]['linked'] = [$r['trigger'], $r['effect'], $r['type']]; + $data[] = $lv[$dk]; + break; + } + } + + $this->lvTabs->addListviewTab(new Listview(array( + 'data' => $data, + 'id' => 'spell-link', + 'name' => Lang::spell('linkedWith'), + 'hiddenCols' => ['skill', 'name'], + 'visibleCols' => ['linkedTrigger', 'linkedEffect'] + ), SpellList::$brickFile)); + + $this->extendGlobalData($linked->getJSGlobals(GLOBALINFO_SELF | GLOBALINFO_RELATED)); + } + + + // tab: triggered by + $conditions = array( + 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); + if (!$trigger->error) + { + $this->lvTabs->addListviewTab(new Listview(array( + 'data' => $trigger->getListviewData(), + 'id' => 'triggered-by', + 'name' => '$LANG.tab_triggeredby' + ), SpellList::$brickFile)); + + $this->extendGlobalData($trigger->getJSGlobals(GLOBALINFO_SELF)); + } + + // tab: used by - creature + $conditions = array( + 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 %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) + { + $this->addDataLoader('zones'); + $this->lvTabs->addListviewTab(new Listview(array( + 'data' => $ubCreature->getListviewData(), + 'id' => 'used-by-npc', + 'name' => '$LANG.tab_usedby' + ), CreatureList::$brickFile)); + + $this->extendGlobalData($ubCreature->getJSGlobals(GLOBALINFO_SELF)); + } + + // tab: zone + 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) + { + $lvZones = $zones->getListviewData(); + $this->extendGlobalData($zones->getJSGlobals()); + + $resultLv = []; + $parents = []; + $extraCols = []; + foreach ($areaSpells as $areaId => $condition) + { + if (empty($lvZones[$areaId])) + continue; + + $row = $lvZones[$areaId]; + + // attach to lv row and evaluate after merging + $row['__condition'] = $condition; + + // merge subzones, into one row, if: spell_area data is identical && parentZone is shared + if ($p = $zones->getEntry($areaId)['parentArea']) + { + $parents[] = $p; + $row['__parent'] = $p; + $row['subzones'] = [$areaId]; + } + else + $row['__parent'] = 0; + + $set = false; + foreach ($resultLv as &$v) + { + if ($v['__parent'] != $row['__parent'] && $v['id'] != $row['__parent']) + continue; + + if ($v['__condition'] != $row['__condition']) + continue; + + if (!$row['__parent'] && $v['id'] != $row['__parent']) + continue; + + $set = true; + $v['subzones'][] = $row['id']; + break; + } + + // add self as potential subzone; IF we are a parentZone without added children, we get filtered in JScript + if (!$set) + { + $row['subzones'] = [$row['id']]; + $resultLv[] = $row; + } + } + + // overwrite lvData with parent-lvData (condition and subzones are kept) + if ($parents) + { + $parents = (new ZoneList(array(['id', $parents])))->getListviewData(); + foreach ($resultLv as &$_) + if (isset($parents[$_['__parent']])) + $_ = array_merge($_, $parents[$_['__parent']]); + } + + $cnd = new Conditions(); + foreach ($resultLv as $idx => $lv) + { + [$auraSpell, $questStart, $questEnd, $questStartState, $questEndState, $raceMask, $gender] = $lv['__condition']; + + if ($auraSpell) + $cnd->addExternalCondition(Conditions::SRC_NONE, $lv['id'], [$auraSpell > 0 ? Conditions::AURA : -Conditions::AURA, abs($auraSpell)]); + + if ($questStart) + $cnd->addExternalCondition(Conditions::SRC_NONE, $lv['id'], [Conditions::QUESTSTATE, $questStart, $questStartState]); + + if ($questEnd && $questEnd != $questStart) + $cnd->addExternalCondition(Conditions::SRC_NONE, $lv['id'], [Conditions::QUESTSTATE, $questEnd, $questEndState]); + + if ($raceMask) + $cnd->addExternalCondition(Conditions::SRC_NONE, $lv['id'], [Conditions::CHR_RACE, $raceMask]); + + if ($gender != 2) // 2: both + $cnd->addExternalCondition(Conditions::SRC_NONE, $lv['id'], [Conditions::GENDER, $gender]); + + // remove temp storage from result + unset($resultLv[$idx]['__condition']); + unset($resultLv[$idx]['__parent']); + } + + if ($cnd->toListviewColumn($resultLv, $extraCols)) + $this->extendGlobalData($cnd->getJsGlobals()); + + $tabData = ['data' => $resultLv]; + + if ($extraCols) + { + $tabData['extraCols'] = $extraCols; + $tabData['hiddenCols'] = ['instancetype']; + } + + $this->lvTabs->addListviewTab(new Listview($tabData, ZoneList::$brickFile)); + } + } + + // tab: teaches + if ($ids = Game::getTaughtSpells($this->subject)) + { + $teaches = new SpellList(array(['id', $ids])); + if (!$teaches->error) + { + $this->extendGlobalData($teaches->getJSGlobals(GLOBALINFO_SELF | GLOBALINFO_RELATED)); + $vis = ['level', 'schools']; + + foreach ($teaches->iterate() as $__) + { + if (!$teaches->canCreateItem()) + continue; + + $vis[] = 'reagents'; + break; + } + + $tabData = array( + 'data' => $teaches->getListviewData(), + 'id' => 'teaches-spell', + 'name' => '$LANG.tab_teaches', + 'visibleCols' => $vis, + ); + + if (!$teaches->hasSetFields('skillLines')) + $tabData['hiddenCols'] = ['skill']; + + $this->lvTabs->addListviewTab(new Listview($tabData, SpellList::$brickFile)); + } + } + + // tab: taught by npc + if ($this->subject->getRawSource(SRC_TRAINER)) + { + $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` = %i', + $this->typeId + ); + + if ($trainers) + { + $tbTrainer = new CreatureList(array(['ct.id', array_keys($trainers)], ['s.guid', null, '!'], ['ct.npcflag', NPC_FLAG_TRAINER, '&'])); + if (!$tbTrainer->error) + { + $this->extendGlobalData($tbTrainer->getJSGlobals()); + + $cnd = new Conditions(); + $skill = $this->subject->getField('skillLines'); + + foreach ($trainers as $tId => $train) + { + if ($_ = $train['reqLevel']) + $cnd->addExternalCondition(Conditions::SRC_NONE, $tId, [Conditions::LEVEL, $_, Conditions::OP_GT_E]); + + if ($_ = $train['reqSkillId']) + if (count($skill) == 1 && $_ != $skill[0]) + $cnd->addExternalCondition(Conditions::SRC_NONE, $tId, [Conditions::SKILL, $_, $train['reqSkillValue']]); + + for ($i = 1; $i < 3; $i++) + if ($_ = $train['reqSpellId'.$i]) + $cnd->addExternalCondition(Conditions::SRC_NONE, $tId, [Conditions::SPELL, $_]); + } + + $lvData = $tbTrainer->getListviewData(); + $extraCols = []; + if ($cnd->toListviewColumn($lvData, $extraCols)) + $this->extendGlobalData($cnd->getJsGlobals()); + + $tabData = array( + 'data' => $lvData, + 'id' => 'taught-by-npc', + 'name' => '$LANG.tab_taughtby', + ); + + if ($extraCols) + $tabData['extraCols'] = $extraCols; + + $this->addDataLoader('zones'); + $this->lvTabs->addListviewTab(new Listview($tabData, CreatureList::$brickFile)); + } + } + } + + // tab: taught by spell + $conditions = array( + 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', + 'name' => '$LANG.tab_taughtby' + ), SpellList::$brickFile)); + + $this->extendGlobalData($tbSpell->getJSGlobals(GLOBALINFO_SELF)); + } + + // tab: taught by quest + $conditions = array( + DB::OR, + ['sourceSpellId', $this->typeId], + ['rewardSpell', $this->typeId], + ['rewardSpellCast', $this->typeId] + ); + if ($tbsData) + array_push($conditions, ['rewardSpell', $tbsData], ['rewardSpellCast', $tbsData]); + + $tbQuest = new QuestList($conditions); + if (!$tbQuest->error) + { + $this->lvTabs->addListviewTab(new Listview(array( + 'data' => $tbQuest->getListviewData(), + 'id' => 'reward-from-quest', + 'name' => '$LANG.tab_rewardfrom' + ), QuestList::$brickFile)); + + $this->extendGlobalData($tbQuest->getJSGlobals()); + } + + // 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( + 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); + if (!$tbItem->error) + { + $this->lvTabs->addListviewTab(new Listview(array( + 'data' => $tbItem->getListviewData(), + 'id' => 'taught-by-item', + 'name' => '$LANG.tab_taughtby' + ), ItemList::$brickFile)); + + $this->extendGlobalData($tbItem->getJSGlobals(GLOBALINFO_SELF)); + } + + // tab: enchantments + $conditions = array( + 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) + { + $this->lvTabs->addListviewTab(new Listview(array( + 'data' => $enchList->getListviewData(), + 'name' => Util::ucFirst(Lang::game('enchantments')) + ), EnchantmentList::$brickFile, 'enchantment')); + + $this->extendGlobalData($enchList->getJSGlobals()); + } + + // tab: sounds + $data = []; + $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` = %i', $this->subject->getField('effect'.$i.'MiscValue')); + + $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) + { + $sounds = new SoundList(array(['id', $soundIDs])); + if (!$sounds->error) + { + $data = $sounds->getListviewData(); + foreach ($activitySounds as $activity => $id) + if (isset($data[$id])) + $data[$id]['activity'] = $activity; // no index, js wants a string :( + + $tabData = ['data' => $data]; + if ($activitySounds) + $tabData['visibleCols'] = ['activity']; + + $this->extendGlobalData($sounds->getJSGlobals(GLOBALINFO_SELF)); + $this->lvTabs->addListviewTab(new Listview($tabData, SoundList::$brickFile)); + } + } + + // 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) + // taughtbytrainers + // taughtbyitem + + // tab: conditions + $cnd = new Conditions(); + $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()) + { + $this->extendGlobalData($cnd->getJsGlobals()); + $this->lvTabs->addDataTab(...$tab); + } + + parent::generate(); + } + + + /******************************************/ + /* SpellLoot recursive dropchance builder */ + /******************************************/ + + 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% + $maxStack = $maxStack ?: 1; + $pctStack = []; + for ($i = 1; $i <= $maxStack; $i++) + { + $pctStack[$i] = (($baseChance ** $i) * 100) / $baseChance; + + // remove chance from previous stacks + if ($i > 1) + $pctStack[$i-1] -= $pctStack[$i]; + } + + // cleanup rounding errors + $pctStack = array_map(fn($x) => round($x, 3), $pctStack); + + // 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 ! + } + + + /**********************************/ + /* recursive reagent list builder */ + /**********************************/ + + private function appendReagentItem(array &$reagentResult, int $itemId, int $qty, int $mult, int $level, string $path, array $alreadyUsed, int $fromSpell = 0) : bool + { + if (in_array($itemId, $alreadyUsed)) + return false; + + $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` = %i', + $itemId + ); + + if (!$item) + return false; + + $this->extendGlobalIds(Type::ITEM, $item['id']); + + // the spell calling this is also on the item and triggering it destroys the item + // so effectively we need one more. (see elemental particles and enchantment essences) + if ($fromSpell && $fromSpell == $item['spellId1'] && $item['spellCharges1'] == -1) + $qty++; + + $level++; + + $data = array( + 'path' => $path.'.'.Type::ITEM.'-'.$item['id'], + 'level' => $level, + 'final' => false, + 'typeStr' => Type::getFileString(Type::ITEM), + 'icon' => new IconElement(Type::ITEM, $item['id'], Util::localizedString($item, 'name'), $qty * $mult, '', $item['quality'], IconElement::SIZE_SMALL, align: 'right', element: 'iconlist-icon') + ); + + $idx = count($reagentResult); + $reagentResult[] = $data; + $alreadyUsed[] = $item['id']; + + if (!$this->appendReagentSpell($reagentResult, $item['id'], $qty * $mult, $data['level'], $data['path'], $alreadyUsed)) + $reagentResult[$idx]['final'] = true; + + return true; + } + + private function appendReagentSpell(array &$reagentResult, int $itemId, int $qty, int $level, string $path, array $alreadyUsed) : bool + { + $level++; + // assume that tradeSpells only use the first index to create items, so this runs somewhat efficiently >.< + $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 (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) + return false; + + $didAppendSomething = false; + foreach ($spells as $sId => $row) + { + if (in_array(-$sId, $alreadyUsed)) + continue; + + $this->extendGlobalIds(Type::SPELL, $sId); + + $data = array( + 'path' => $path.'.'.Type::SPELL.'-'.$sId, + 'level' => $level, + 'final' => false, + 'typeStr' => Type::getFileString(Type::SPELL), + 'icon' => new IconElement(Type::SPELL, $sId, Util::localizedString($row, 'name'), $qty, size: IconElement::SIZE_SMALL, align: 'right', element: 'iconlist-icon') + ); + + $reagentResult[] = $data; + $_aU = $alreadyUsed; + $_aU[] = -$sId; + + $hasUnusedReagents = false; + for ($i = 1; $i < 9; $i++) + { + if ($row['reagent'.$i] <= 0 || $row['reagentCount'.$i] <= 0) + continue; + + if ($this->appendReagentItem($reagentResult, $row['reagent'.$i], $row['reagentCount'.$i], $qty, $data['level'], $data['path'], $_aU, $sId)) + { + $hasUnusedReagents = true; + $didAppendSomething = true; + } + } + + if (!$hasUnusedReagents) // no reagents were added, remove spell from result set + array_pop($reagentResult); + } + + return $didAppendSomething; + } + + private function createReagentList() : void + { + $reagentResult = []; + $enhanced = false; + $reagents = $this->subject->getReagentsForCurrent(); + + if (!$reagents) + return; + + foreach ($reagents as [$iId, $num]) + { + $relItem = $this->subject->relItems->getEntry($iId); + + $data = array( + 'path' => Type::ITEM.'-'.$iId, // id of the html-element + 'level' => 0, // depths in array, used for indentation + 'final' => false, + 'typeStr' => Type::getFileString(Type::ITEM), + 'icon' => new IconElement( + Type::ITEM, + 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' + ) + ); + + $idx = count($reagentResult); + $reagentResult[] = $data; + + // start with self and current original item in usedEntries (spell < 0; item > 0) + if ($this->appendReagentSpell($reagentResult, $iId, $reagents[$iId][1], 0, $data['path'], [-$this->typeId, $iId])) + $enhanced = true; + else + $reagentResult[$idx]['final'] = true; + } + + // increment all indizes (by prepending null and removing it again) + array_unshift($reagentResult, null); + unset($reagentResult[0]); + + $this->reagents = [$enhanced, $reagentResult]; + } + + private function calculateEffectScaling() : array // calculation mostly like seen in TC + { + 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; + + for ($i = 1; $i < 4; $i++) + { + if (!$this->subject->getField('effect'.$i.'Id')) + continue; + + if ($pMask & 1 << ($i - 1)) + { + $scaling[1] = $this->subject->getField('effect'.$i.'BonusMultiplier'); + continue; + } + 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 "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 (!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) + { + // recalculate if spell_bonus_data says so + if ($v != -1) + continue; + + // no known calculation for physical abilities + 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) && in_array($k, [0, 1])) + continue; + + $isDOT = false; + if (in_array($k, [1, 3])) // [DoT SP, DoT AP] + { + if ($pMask) + $isDOT = true; + else + continue; + } + else if ($allDoTs) // if all used effects are periodic, dont calculate direct component + continue; + + // damage over time spells bonus calculation + $dotFactor = 1.0; + if ($isDOT) + { + $dotDuration = $this->subject->getField('duration'); + // 200% limit + if ($dotDuration > 0) + { + if ($dotDuration > 30000) + $dotDuration = 30000; + if (!$this->subject->isChanneledSpell()) + $dotFactor = $dotDuration / 15000; + } + } + + // distribute damage over multiple effects, reduce by AoE + $castingTime = $this->subject->getCastingTimeForBonus($isDOT); + + // 50% for damage and healing spells for leech spells from damage bonus and 0% from healing + for ($j = 1; $j < 4; ++$j) + { + if ($this->subject->getField('effectId'.$j) == SPELL_EFFECT_HEALTH_LEECH || $this->subject->getField('effect'.$j.'AuraId') == SPELL_AURA_PERIODIC_LEECH) + { + $castingTime /= 2; + break; + } + } + + if ($this->subject->isScalableHealingSpell()) + $castingTime *= 1.88; + + // SPELL_SCHOOL_MASK_NORMAL + if ($this->subject->getField('schoolMask') != (1 << SPELL_SCHOOL_NORMAL)) + $scaling[$k] = ($castingTime / 3500.0) * $dotFactor; + else + $scaling[$k] = 0; // would be 1 ($dotFactor), but we dont want it to be displayed + } + + return array_map(fn($x) => $x < 0 ? 0 : $x, $scaling); + } + + private function createRequiredItems() : void + { + // parse itemClass & itemSubClassMask + $class = $this->subject->getField('equippedItemClass'); + $subClass = $this->subject->getField('equippedItemSubClassMask'); + $invType = $this->subject->getField('equippedItemInventoryTypeMask'); + + if ($class <= 0) + return; + + $tip = 'Class: '.$class.'
SubClass: '.Util::asHex($subClass); + $text = Lang::getRequiredItems($class, $subClass, false); + + if ($invType) + { + // remap some duplicated strings 'Off Hand' and 'Shield' are never used simultaneously + if ($invType & (1 << INVTYPE_ROBE)) // Robe => Chest + { + $invType &= ~(1 << INVTYPE_ROBE); + $invType |= (1 << INVTYPE_CHEST); + } + + if ($invType & (1 << INVTYPE_RANGEDRIGHT)) // Ranged2 => Ranged + { + $invType &= ~(1 << INVTYPE_RANGEDRIGHT); + $invType |= (1 << INVTYPE_RANGED); + } + + $_ = []; + $strs = Lang::item('inventoryType'); + foreach ($strs as $k => $str) + if ($invType & 1 << $k && $str) + $_[] = $str; + + $tip .= '
'.Lang::item('slot').Lang::main('colon').Util::asHex($invType); + $text .= ' '.Lang::spell('_inSlot').implode(', ', $_); + } + + $this->items = $this->fmtStaffTip($text, $tip); + } + + private function createEffects() : void + { + // proc data .. maybe use more information..? + $procData = array( + 'chance' => $this->subject->getField('procChance'), + 'cooldown' => 0 + ); + + 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']; + } + + $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` = %i', $this->typeId); + $scaling = $this->calculateEffectScaling(); + + // Iterate through all effects: + for ($i = 1; $i < 4; $i++) + { + if ($this->subject->getField('effect'.$i.'Id') <= 0) + continue; + + $effId = $this->subject->getField('effect'.$i.'Id'); + $effMV = $this->subject->getField('effect'.$i.'MiscValue'); + $effMVB = $this->subject->getField('effect'.$i.'MiscValueB'); + $effBP = $this->subject->getField('effect'.$i.'BasePoints'); + $effDS = $this->subject->getField('effect'.$i.'DieSides'); + $effRPPL = $this->subject->getField('effect'.$i.'RealPointsPerLevel'); + $effPPCP = $this->subject->getField('effect'.$i.'PointsPerComboPoint'); + $effAura = $this->subject->getField('effect'.$i.'AuraId'); + + /* Effect Format + * + * EffectName<: AuraName>< (effMV)>< [effMVB]> + * Value: A<, plus y per level> + * Radius: Byd + * Interval: Cms + * Mechanic: D + * Proc Chance: E% || (E procs per minute) + * Cooldown: Fs + * < formated markup > + * < iconlist > + * < icon > + */ + + $_nameEffect = $_nameAura = $_nameMV = $_nameMVB = $_markup = ''; + $_icon = $_perfItem = $_footer = []; + + $_footer['value'] = [0, 0]; + $valueFmt = '%s'; + + // Icons: + // .. from item + if (in_array($i, $itemIdx)) + { + if ($itemId = $this->subject->getField('effect'.$i.'CreateItemId')) + { + $itemEntry = $this->subject->relItems->getEntry($itemId); + + $_icon = new IconElement( + Type::ITEM, + $itemId, + $itemEntry ? $this->subject->relItems->getField('name', true) : Util::ucFirst(Lang::game('item')).' #'.$itemId, + $this->createNumRange($effBP, $effDS), + quality: $itemEntry ? $this->subject->relItems->getField('quality') : '', + link: !empty($itemEntry) + ); + } + + // perfect Items + if ($perfItem && $this->subject->relItems->getEntry($perfItem['itemId'])) + { + $cndSpell = new SpellList(array(['id', $perfItem['reqSpellId']])); + if (!$cndSpell->error) + { + $_perfItem = array( + 'spellId' => $cndSpell->id, + 'spellName' => $cndSpell->getField('name', true), + 'icon' => $cndSpell->getField('iconString'), + 'chance' => $perfItem['chance'], + 'item' => new IconElement( + Type::ITEM, + $perfItem['itemId'], + $this->subject->relItems->getField('name', true), + quality: $this->subject->relItems->getField('quality') + ) + ); + } + } + 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) + { + if ($effId == SPELL_EFFECT_TITAN_GRIP) + $triggeredSpell = $effMV; + else + $triggeredSpell = $this->subject->getField('effect'.$i.'TriggerSpell'); + + if ($triggeredSpell > 0) // Dummy Auras are probably scripted + { + $trig = new SpellList(array(['s.id', (int)$triggeredSpell])); + + $_icon = new IconElement( + Type::SPELL, + $triggeredSpell, + $trig->error ? Util::ucFirst(Lang::game('spell')).' #'.$triggeredSpell : $trig->getField('name', true), + link: !$trig->error + ); + + $this->extendGlobalData($trig->getJSGlobals(GLOBALINFO_SELF | GLOBALINFO_RELATED)); + } + } + + // small text under effect name and resolved links .. order of adding to _footer determines order of output. + + // cases where we dont want 'Value' to be displayed [min, max, staffTT] + if (!in_array($i, $itemIdx) && !in_array($effAura, [SPELL_AURA_MOD_TAUNT, SPELL_AURA_MOD_STUN, SPELL_AURA_MOD_SHAPESHIFT, SPELL_AURA_MECHANIC_IMMUNITY]) && !in_array($effId, [SPELL_EFFECT_PLAY_MUSIC]) && ($effBP + $effDS) != 0) + $_footer['value'] = [$effBP + ($effDS ? 1 : 0), $effBP + $effDS]; + + if ($this->subject->getField('effect'.$i.'RadiusMax') > 0) + $_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').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', [-$procData['chance']]) : Lang::spell('procChance', [$procData['chance']]); + if ($procData['cooldown']) + $_footer['procCD'] = Lang::game('cooldown', [DateTime::formatTimeElapsed($procData['cooldown'])]); + } + + // Effect Name + if ($_ = Lang::spell('effects', $effId)) + $_nameEffect = ''.$this->fmtStaffTip($_, 'EffectId: '.$effId).''; + else + $_nameEffect = Lang::spell('unkEffect', [$effId]); + + // parse masks and indizes + switch ($effId) + { + case SPELL_EFFECT_ENERGIZE_PCT: + $valueFmt = '%s%%'; + case SPELL_EFFECT_POWER_DRAIN: + case SPELL_EFFECT_ENERGIZE: + case SPELL_EFFECT_POWER_BURN: + if ($_ = Lang::spell('powerTypes', $effMV)) + $_nameMV = $this->fmtStaffTip($_, 'MiscValue: '.$effMV); + + if ($effMV == POWER_RAGE || $effMV == POWER_RUNIC_POWER) + array_walk($_footer['value'], fn(&$x) => $x /= 10); + break; + case SPELL_EFFECT_BIND: + if ($effMV <= 0) + $_nameMV = $this->fmtStaffTip(Lang::spell('currentArea'), 'MiscValue: '.$effMV); + else if ($a = ZoneList::makeLink($effMV)) + $_nameMV = $a; + else + $_nameMV = Util::ucFirst(Lang::game('zone')).' #'.$effMV; + break; + case SPELL_EFFECT_QUEST_COMPLETE: + case SPELL_EFFECT_CLEAR_QUEST: + case SPELL_EFFECT_QUEST_FAIL: + if ($a = QuestList::makeLink($effMV)) + $_nameMV = $a; + else + $_nameMV = Util::ucFirst(Lang::game('quest')).' #'.$effMV; + break; + case SPELL_EFFECT_SUMMON_PET: + $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` = %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: + case SPELL_EFFECT_KILL_CREDIT: + case SPELL_EFFECT_KILL_CREDIT2: + if ($a = CreatureList::makeLink($effMV)) + $_nameMV = $a; + else + $_nameMV = Util::ucFirst(Lang::game('npc')).' #'.$effMV; + break; + case SPELL_EFFECT_OPEN_LOCK: + if ($effMV && ($_ = Lang::spell('lockType', $effMV))) + $_nameMV = $this->fmtStaffTip($_, 'MiscValue: '.$effMV); + break; + case SPELL_EFFECT_ENCHANT_ITEM: + case SPELL_EFFECT_ENCHANT_ITEM_TEMPORARY: + case SPELL_EFFECT_ENCHANT_HELD_ITEM: + case SPELL_EFFECT_ENCHANT_ITEM_PRISMATIC: + if ($a = EnchantmentList::makeLink($effMV, cssClass: 'q2')) + $_nameMV = $a; + else + $_nameMV = Util::ucFirst(Lang::game('enchantment')).' #'.$effMV; + break; + case SPELL_EFFECT_DISPEL: + case SPELL_EFFECT_STEAL_BENEFICIAL_BUFF: + if ($effMV && ($_ = Lang::game('dt', $effMV))) + $_nameMV = $this->fmtStaffTip($_, 'MiscValue: '.$effMV); + break; + case SPELL_EFFECT_LANGUAGE: + if ($effMV && ($_ = Lang::game('languages', $effMV))) + $_nameMV = $this->fmtStaffTip($_, 'MiscValue: '.$effMV); + break; + case SPELL_EFFECT_TRANS_DOOR: + case SPELL_EFFECT_SUMMON_OBJECT_WILD: + case SPELL_EFFECT_SUMMON_OBJECT_SLOT1: + case SPELL_EFFECT_SUMMON_OBJECT_SLOT2: + case SPELL_EFFECT_SUMMON_OBJECT_SLOT3: + case SPELL_EFFECT_SUMMON_OBJECT_SLOT4: + if ($a = GameobjectList::makeLink($effMV)) + $_nameMV = $a; + else + $_nameMV = Util::ucFirst(Lang::game('object')).' #'.$effMV; + break; + case SPELL_EFFECT_ACTIVATE_OBJECT: + if ($_ = Lang::gameObject('actions', $effMV)) + $_nameMV = $this->fmtStaffTip($_, 'MiscValue: '.$effMV); + break; + case SPELL_EFFECT_APPLY_GLYPH: + if ($_ = DB::Aowow()->selectCell('SELECT `spellId` FROM ::glyphproperties WHERE `id` = %i', $effMV)) + { + if ($a = SpellList::makeLink($_)) + $_nameMV = $a; + else + $_nameMV = Util::ucFirst(Lang::game('spell')).' #'.$_; + } + break; + case SPELL_EFFECT_SKINNING: + $_ = match ($effMV) + { + 0 => Lang::game('ct', 1).', '.Lang::game('ct', 2), // Skinning > Beast, Dragonkin + 1, 2 => Lang::game('ct', 4), // Gathering, Mining > Elemental + 3 => Lang::game('ct', 9), // Dismantling > Mechanic + default => '' + }; + if ($_) + $_nameMV = $this->fmtStaffTip($_, 'MiscValue: '.$effMV); + break; + case SPELL_EFFECT_DISPEL_MECHANIC: + if ($_ = Lang::game('me', $effMV)) + $_nameMV = $this->fmtStaffTip($_, 'MiscValue: '.$effMV); + break; + case SPELL_EFFECT_SKILL_STEP: + case SPELL_EFFECT_SKILL: + if ($a = SkillList::makeLink($effMV)) + $_nameMV = $a; + else + $_nameMV = Util::ucFirst(Lang::game('skill')).' #'.$effMV; + break; + case SPELL_EFFECT_ACTIVATE_RUNE: + if ($_ = Lang::spell('powerRunes', $effMV)) + $_nameMV = $this->fmtStaffTip($_, 'MiscValue: '.$effMV); + break; + case SPELL_EFFECT_PLAY_SOUND: + case SPELL_EFFECT_PLAY_MUSIC: + if (DB::Aowow()->selectCell('SELECT 1 FROM ::sounds WHERE `id` = %i', $effMV)) + { + $_markup = '[sound='.$effMV.']'; + $effMV = 0; // prevent default display + } + else + $_nameMV = Util::ucFirst(Lang::game('sound')).' #'.$effMV; + break; + case SPELL_EFFECT_REPUTATION: + if ($a = FactionList::makeLink($effMV)) + $_nameMV = $a; + else + $_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` = %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.`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 ($_) + { + $start = Util::localizedString($_, 'start'); + if ($_['startAreaId']) + $start = sprintf('%s', $_['startAreaId'], $_['startPosX'] * 10, $_['startPosY'] * 10, $start); + $end = Util::localizedString($_, 'end'); + if ($_['endAreaId']) + $end = sprintf('%s', $_['endAreaId'], $_['endPosX'] * 10, $_['endPosY'] * 10, $end); + + $_nameMV = $this->fmtStaffTip(''.$start.''.$end, 'MiscValue: '.$effMV); + } + break; + case SPELL_EFFECT_TITAN_GRIP: + $effMV = 0; // effMV is trigger spell and was handled earlier + break; + case SPELL_EFFECT_CAST_BUTTON: + $_nameMV = $effMV; // has a valid 0 value + break; + // Aura + case SPELL_EFFECT_APPLY_AURA: + case SPELL_EFFECT_PERSISTENT_AREA_AURA: + case SPELL_EFFECT_APPLY_AREA_AURA_PARTY: + case SPELL_EFFECT_APPLY_AREA_AURA_RAID: + case SPELL_EFFECT_APPLY_AREA_AURA_PET: + case SPELL_EFFECT_APPLY_AREA_AURA_FRIEND: + case SPELL_EFFECT_APPLY_AREA_AURA_ENEMY: + case SPELL_EFFECT_APPLY_AREA_AURA_OWNER: + { + if ($effAura > 0 && ($_ = Lang::spell('auras', $effAura))) + $_nameAura = ''.$this->fmtStaffTip($_, 'AuraId: '.$effAura).''; + else if ($effAura > 0) + $_nameAura = Lang::spell('unkAura', [$effAura]); + + switch ($effAura) + { + case SPELL_AURA_MOD_STEALTH_DETECT: + if ($_ = Lang::spell('stealthType', $effMV)) + $_nameMV = $this->fmtStaffTip($_, 'MiscValue: '.$effMV); + break; + case SPELL_AURA_MOD_INVISIBILITY: + case SPELL_AURA_MOD_INVISIBILITY_DETECT: + if ($_ = Lang::spell('invisibilityType', $effMV)) + $_nameMV = $this->fmtStaffTip($_, 'MiscValue: '.$effMV); + break; + case SPELL_AURA_MOD_POWER_REGEN_PERCENT: + case SPELL_AURA_MOD_INCREASE_ENERGY_PERCENT: + case SPELL_AURA_OBS_MOD_POWER: + $valueFmt = '%s%%'; + case SPELL_AURA_PERIODIC_ENERGIZE: + case SPELL_AURA_MOD_INCREASE_ENERGY: + case SPELL_AURA_MOD_POWER_REGEN: + if ($_ = Lang::spell('powerTypes', $effMV)) + $_nameMV = $this->fmtStaffTip($_, 'MiscValue: '.$effMV); + + if ($effMV == POWER_RAGE || $effMV == POWER_RUNIC_POWER) + array_walk($_footer['value'], fn(&$x) => $x /= 10); + break; + case SPELL_AURA_MOD_PERCENT_STAT: + case SPELL_AURA_MOD_TOTAL_STAT_PERCENTAGE: + case SPELL_AURA_MOD_SPELL_HEALING_OF_STAT_PERCENT: + case SPELL_AURA_MOD_RANGED_ATTACK_POWER_OF_STAT_PERCENT: + case SPELL_AURA_MOD_ATTACK_POWER_OF_STAT_PERCENT: + case SPELL_AURA_MOD_MANA_REGEN_FROM_STAT: + $valueFmt = '%s%%'; + case SPELL_AURA_MOD_STAT: + if ($effMV < 0) + $_nameMV = $this->fmtStaffTip(Lang::main('all'), 'MiscValue: '.$effMV); + else if ($_ = Lang::game('stats', $effMV)) + $_nameMV = $this->fmtStaffTip($_, 'MiscValue: '.$effMV); + break; + case SPELL_AURA_MOD_SHAPESHIFT: + if ($_ = Lang::game('st', $effMV)) + $_nameMV = $this->fmtStaffTip($_, 'MiscValue: '.$effMV); + break; + case SPELL_AURA_EFFECT_IMMUNITY: + if ($_ = Lang::spell('effects', $effMV)) + $_nameMV = $this->fmtStaffTip($_, 'MiscValue: '.$effMV); + break; + case SPELL_AURA_STATE_IMMUNITY: + if ($_ = Lang::spell('auras', $effMV)) + $_nameMV = $this->fmtStaffTip($_, 'MiscValue: '.$effMV); + break; + case SPELL_AURA_MOD_DEBUFF_RESISTANCE: + case SPELL_AURA_MOD_AURA_DURATION_BY_DISPEL: + case SPELL_AURA_MOD_AURA_DURATION_BY_DISPEL_NOT_STACK: + $valueFmt = '%s%%'; + case SPELL_AURA_DISPEL_IMMUNITY: + if ($_ = Lang::game('dt', $effMV)) + $_nameMV = $this->fmtStaffTip($_, 'MiscValue: '.$effMV); + break; + case SPELL_AURA_TRACK_CREATURES: + if ($_ = Lang::game('ct', $effMV)) + $_nameMV = $this->fmtStaffTip($_, 'MiscValue: '.$effMV); + break; + case SPELL_AURA_TRACK_RESOURCES: + if ($_ = Lang::spell('lockType', $effMV)) + $_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; + case SPELL_AURA_MOD_MECHANIC_DAMAGE_TAKEN_PERCENT: + case SPELL_AURA_MOD_MECHANIC_RESISTANCE: + case SPELL_AURA_MECHANIC_DURATION_MOD: + case SPELL_AURA_MECHANIC_DURATION_MOD_NOT_STACK: + case SPELL_AURA_MOD_DAMAGE_DONE_FOR_MECHANIC: + $valueFmt = '%s%%'; + case SPELL_AURA_MECHANIC_IMMUNITY: + if ($_ = Lang::game('me', $effMV)) + $_nameMV = $this->fmtStaffTip($_, 'MiscValue: '.$effMV); + break; + case SPELL_AURA_MECHANIC_IMMUNITY_MASK: + $_ = []; + foreach (Lang::game('me') as $k => $str) + if ($k && ($effMV & (1 << $k - 1))) + $_[] = $str; + + if ($_ = implode(', ', $_)) + $_nameMV = $this->fmtStaffTip($_, 'MiscValue: '.Util::asHex($effMV)); + break; + case SPELL_AURA_MOD_SPELL_DAMAGE_OF_STAT_PERCENT: + case SPELL_AURA_MOD_RESISTANCE_OF_STAT_PERCENT: + if ($_ = Lang::game('stats', $effMVB)) + $_nameMVB = $this->fmtStaffTip($_, 'MiscValueB: '.$effMVB); + // ! DO NOT BREAK ! + case SPELL_AURA_MOD_POWER_COST_SCHOOL_PCT: + case SPELL_AURA_SPLIT_DAMAGE_PCT: + case SPELL_AURA_MOD_RESISTANCE_PCT: + case SPELL_AURA_MOD_HEALING_PCT: + case SPELL_AURA_MOD_BASE_RESISTANCE_PCT: + case SPELL_AURA_MOD_INCREASES_SPELL_PCT_TO_HIT: + case SPELL_AURA_MOD_HOT_PCT: + case SPELL_AURA_SHARE_DAMAGE_PCT: + case SPELL_AURA_MOD_THREAT: + case SPELL_AURA_MOD_DAMAGE_PERCENT_DONE: + case SPELL_AURA_MOD_DAMAGE_PERCENT_TAKEN: + case SPELL_AURA_MOD_HEALING_DONE_PERCENT: + case SPELL_AURA_MOD_WEAPON_CRIT_PERCENT: + case SPELL_AURA_MOD_CRITICAL_HEALING_AMOUNT: + case SPELL_AURA_MOD_SPELL_CRIT_CHANCE_SCHOOL: + case SPELL_AURA_REFLECT_SPELLS_SCHOOL: + case SPELL_AURA_REDUCE_PUSHBACK: + case SPELL_AURA_MOD_CRIT_DAMAGE_BONUS: + case SPELL_AURA_MOD_ATTACKER_SPELL_HIT_CHANCE: + case SPELL_AURA_MOD_TARGET_ABSORB_SCHOOL: + case SPELL_AURA_MOD_TARGET_ABILITY_ABSORB_SCHOOL: + case SPELL_AURA_MOD_AOE_DAMAGE_AVOIDANCE: + case SPELL_AURA_MOD_DAMAGE_FROM_CASTER: + case SPELL_AURA_MOD_CREATURE_AOE_DAMAGE_AVOIDANCE: + case SPELL_AURA_MOD_SPELL_DAMAGE_OF_ATTACK_POWER: + case SPELL_AURA_MOD_SPELL_HEALING_OF_ATTACK_POWER: + case SPELL_AURA_MOD_SPELL_DAMAGE_FROM_HEALING: // ? Mod Spell & Healing Power by % of Int + $valueFmt = '%s%%'; + case SPELL_AURA_SCHOOL_ABSORB: + case SPELL_AURA_MOD_DAMAGE_DONE: + case SPELL_AURA_MOD_DAMAGE_TAKEN: + case SPELL_AURA_MOD_RESISTANCE: + case SPELL_AURA_SCHOOL_IMMUNITY: + case SPELL_AURA_DAMAGE_IMMUNITY: + case SPELL_AURA_MOD_POWER_COST_SCHOOL: + case SPELL_AURA_MOD_BASE_RESISTANCE: + case SPELL_AURA_MANA_SHIELD: + case SPELL_AURA_MOD_HEALING: + case SPELL_AURA_MOD_TARGET_RESISTANCE: + case SPELL_AURA_MOD_HEALING_DONE: + case SPELL_AURA_MOD_RESISTANCE_EXCLUSIVE: + case SPELL_AURA_MOD_IMMUNE_AURA_APPLY_SCHOOL: // ? Cancel Aura Buffer at % of Caster Health + case SPELL_AURA_MOD_IGNORE_TARGET_RESIST: + case SPELL_AURA_MOD_ATTACK_POWER_OF_ARMOR: // ? Mod Attack Power by School Resistance + case SPELL_AURA_SCHOOL_HEAL_ABSORB: + if ($_ = Lang::getMagicSchools($effMV)) + $_nameMV = $this->fmtStaffTip($_, 'MiscValue: '.Util::asHex($effMV)); + break; + case SPELL_AURA_MOD_SKILL: // temp + case SPELL_AURA_MOD_SKILL_TALENT: // perm + $valueFmt = '%+d'; + if ($a = SkillList::makeLink($effMV)) + $_nameMV = $a; + else + $_nameMV = Util::ucFirst(Lang::game('skill')).' #'.$effMV; + break; + case SPELL_AURA_ADD_PCT_MODIFIER: + $valueFmt = '%s%%'; + case SPELL_AURA_ADD_FLAT_MODIFIER: + if ($_ = Lang::spell('spellModOp', $effMV)) + $_nameMV = $this->fmtStaffTip($_, 'MiscValue: '.$effMV); + break; + case SPELL_AURA_MOD_RATING_FROM_STAT: + $valueFmt = '%s%%'; + if ($_ = Lang::game('stats', $effMVB)) + $_nameMVB = $this->fmtStaffTip($_, 'MiscValueB: '.$effMVB); + // DO NOT BREAK ! + case SPELL_AURA_MOD_RATING: + foreach (Lang::spell('combatRatingMask') as $m => $str) + { + if ($effMV != $m) + continue; + $_nameMV = $this->fmtStaffTip($str, 'MiscValue: '.Util::asHex($effMV)); + break 2; + } + + $_ = []; + foreach (Lang::spell('combatRating') as $k => $str) + if ((1 << $k) & $effMV) + $_[] = $str; + + if ($_ = implode(', ', $_)) + $_nameMV = $this->fmtStaffTip($_, 'MiscValue: '.Util::asHex($effMV)); + break; + case SPELL_AURA_MOD_DAMAGE_DONE_VERSUS: + $valueFmt = '%s%%'; + case SPELL_AURA_MOD_DAMAGE_DONE_CREATURE: + case SPELL_AURA_MOD_MELEE_ATTACK_POWER_VERSUS: + case SPELL_AURA_MOD_RANGED_ATTACK_POWER_VERSUS: + case SPELL_AURA_MOD_FLAT_SPELL_DAMAGE_VERSUS: + $_ = []; + foreach (Lang::game('ct') as $k => $str) + if ($k && ($effMV & (1 << $k - 1))) + $_[] = $str; + + if ($_ = implode(', ', $_)) + $_nameMV = $this->fmtStaffTip($_, 'MiscValue: '.Util::asHex($effMV)); + break; + case SPELL_AURA_CONVERT_RUNE: + $from = $effMV; + if ($_ = Lang::spell('powerRunes', $effMV)) + $from = $_; + + $to = $effMVB; + if ($_ = Lang::spell('powerRunes', $effMVB)) + $to = $_; + + $effMVB = 0; // prevent default display + $_nameMV = $this->fmtStaffTip(''.$from.'', 'MiscValue: '.$effMV).$this->fmtStaffTip($to, 'MiscValueB: '.$effMVB); + break; + case SPELL_AURA_MOUNTED: + case SPELL_AURA_TRANSFORM: + case SPELL_AURA_CHANGE_MODEL_FOR_ALL_HUMANOIDS: + case SPELL_AURA_X_RAY: + case SPELL_AURA_MOD_FAKE_INEBRIATE: + if ($effMV && $a = CreatureList::makeLink($effMV)) + $_nameMV = $a; + else + $_nameMV = Util::ucFirst(Lang::game('npc')).' #'.$effMV; + break; + case SPELL_AURA_FORCE_REACTION: + $_footer['value'][1] = $this->fmtStaffTip(Lang::game('rep', $_footer['value'][1]), $_footer['value'][1]); + $_footer['value'][0] = null; // disable range here as the string replacement will fail the comparison at the end + // DO NOT BREAK ! + case SPELL_AURA_MOD_FACTION_REPUTATION_GAIN: + if ($effAura == SPELL_AURA_MOD_FACTION_REPUTATION_GAIN) + $valueFmt = '%s%%'; + if ($a = FactionList::makeLink($effMV)) + $_nameMV = $a; + else + $_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` = %i', $effMV)) + { + if ($so = array_filter($so)) + { + $this->extendGlobalData([Type::SPELL => $so]); + $_markup = '[spell='.implode('], [spell=', $so).']'; + $effMV = 0; // prevent default display + } + } + break; + case SPELL_AURA_MOD_COMBAT_RESULT_CHANCE: + $valueFmt = '%s%%'; + case SPELL_AURA_IGNORE_COMBAT_RESULT: + $what = match ($effMV) + { + 2 => Lang::spell('combatRating', 2), // Dodged + 3 => Lang::spell('combatRating', 4), // Blocked + 4 => Lang::spell('combatRating', 3), // Parried + default => '' // Evaded(0) Missed(1) Glanced(5) Crited'ed..ed(6) Crushed(7) Regular(8) + }; + + if ($what) + $_nameMV = $this->fmtStaffTip($what, 'MiscValue: '.$effMV); + else + 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` = %i', $effMV)) + { + $_nameMV = $this->fmtStaffTip($ses['name'], 'MiscValue: '.$effMV); + for ($j = 0; $j < 4; $j++) + if ($ses[$j]) + $_markup .= '[sound='.$ses[$j].']'; + } + break; + case SPELL_AURA_MOD_HEALTH_REGEN_PERCENT: + case SPELL_AURA_OBS_MOD_HEALTH: + case SPELL_AURA_MOD_INCREASE_HEALTH_PERCENT: + case SPELL_AURA_MOD_ARMOR_PENETRATION_PCT: + case SPELL_AURA_MOD_SCALE: + case SPELL_AURA_MOD_SCALE_2: + case SPELL_AURA_MOD_SPEED_ALWAYS: + case SPELL_AURA_MOD_SPEED_SLOW_ALL: + case SPELL_AURA_MOD_SPEED_NOT_STACK: + case SPELL_AURA_MOD_INCREASE_SPEED: + case SPELL_AURA_MOD_INCREASE_MOUNTED_SPEED: + case SPELL_AURA_MOD_DECREASE_SPEED: + case SPELL_AURA_MOD_INCREASE_SWIM_SPEED: + case SPELL_AURA_MOD_PARRY_PERCENT: + case SPELL_AURA_MOD_DODGE_PERCENT: + case SPELL_AURA_MOD_BLOCK_PERCENT: + case SPELL_AURA_MOD_BLOCK_CRIT_CHANCE: + case SPELL_AURA_MOD_HIT_CHANCE: + case SPELL_AURA_MOD_CRIT_PCT: + case SPELL_AURA_MOD_SPELL_HIT_CHANCE: + case SPELL_AURA_MOD_SPELL_CRIT_CHANCE: + case SPELL_AURA_MOD_MELEE_RANGED_HASTE: + case SPELL_AURA_MOD_CASTING_SPEED_NOT_STACK: + $valueFmt = '%s%%'; + break; + } + break; + } + case SPELL_EFFECT_RESURRECT: + case SPELL_EFFECT_SPIRIT_HEAL: + case SPELL_EFFECT_WEAPON_PERCENT_DAMAGE: + case SPELL_EFFECT_DURABILITY_DAMAGE_PCT: + case SPELL_EFFECT_MODIFY_THREAT_PERCENT: + case SPELL_EFFECT_REDIRECT_THREAT: + case SPELL_EFFECT_HEAL_PCT: + $valueFmt = '%s%%'; + break; + } + + if ($_footer['value'][1]) + { + $buffer = Lang::spell('_value').Lang::main('colon').sprintf($valueFmt, $_footer['value'][0]); + if ($_footer['value'][0] != $_footer['value'][1]) + $buffer .= Lang::game('valueDelim').sprintf($valueFmt, $_footer['value'][1]); + if ($effRPPL != 0) + $buffer .= Lang::spell('costPerLevel', [sprintf($valueFmt, $effRPPL)]); + if ($effPPCP != 0) + $buffer .= Lang::spell('pointsPerCP', [sprintf($valueFmt, $effPPCP)]); + 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 + unset($_footer['value']); + + $_header = $_nameEffect; + + if ($_nameAura) + $_header .= Lang::main('colon').$_nameAura; + + if (strlen($_nameMV)) + $_header .= ' ('.$_nameMV.')'; + else if ($effMV) + $_header .= ' ('.$effMV.')'; + + if (strlen($_nameMVB)) + $_header .= ' ['.$_nameMVB.']'; + else if ($effMVB) + $_header .= ' ['.$effMVB.']'; + + $effects[$i] = array( + 'icon' => $_icon, + 'perfectItem' => $_perfItem, + 'name' => $_header, + 'footer' => $_footer, + 'markup' => $_markup, + 'modifies' => [] // may be set later + ); + } + + $this->effects = $effects; + } + + private function createAttributesList() : void + { + $list = []; + for ($i = 0; $i < 8; $i++) + { + $attributes = $this->subject->getField('attributes'.$i); + for ($j = 1; $j <= (1 << 31); $j <<= 1) + { + if (!($attributes & $j)) + continue; + + $listItem = Lang::spell('attributes'.$i, $j); + if (!$listItem && User::isInGroup(U_GROUP_STAFF)) + $listItem = 'Unknown SpellAttribute'.$i.''; + else if (!$listItem) + continue; + + if ($crId = (SpellListFilter::$attributesFilter[$i][$j] ?? 0)) + $listItem = sprintf('%1$s', $listItem, abs($crId), $crId > 0 ? 1 : 2); + + $list[] = $this->fmtStaffTip($listItem, 'Attributes'.$i.': '.Util::asHex($j)); + } + } + + $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; + + // reconstruct path + switch ($cat) + { + case -2: + case 7: + case -13: + if ($cl = $this->subject->getField('reqClassMask')) + $this->breadcrumb[] = ChrClass::fromMask($cl)[0]; + else if ($sf = $this->subject->getField('spellFamilyId')) + foreach (ChrClass::cases() as $cl) + if ($cl->spellFamily() == $sf) + { + $this->breadcrumb[] = $cl->value; + break; + } + + if ($cat == -13) + $this->breadcrumb[] = ($cf & (SPELL_CU_GLYPH_MAJOR | SPELL_CU_GLYPH_MINOR)) >> 6; + else if ($sl) + $this->breadcrumb[] = $sl[0]; + + break; + case 9: + case -3: + case 11: + if ($sl) + $this->breadcrumb[] = $sl[0]; + + if ($cat == 11) + if ($_ = $this->subject->getField('reqSpellId')) + $this->breadcrumb[] = $_; + + break; + case -11: + foreach (SpellList::$skillLines as $line => $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 :/ + if ($cf & SPELL_CU_PET_TALENT_TYPE0) + $this->breadcrumb[] = 411; // Ferocity + else if ($cf & SPELL_CU_PET_TALENT_TYPE1) + $this->breadcrumb[] = 409; // Tenacity + else if ($cf & SPELL_CU_PET_TALENT_TYPE2) + $this->breadcrumb[] = 410; // Cunning + break; + case -5: + if ($this->subject->getField('effect2AuraId') == SPELL_AURA_MOD_INCREASE_MOUNTED_FLIGHT_SPEED || + $this->subject->getField('effect3AuraId') == SPELL_AURA_MOD_INCREASE_MOUNTED_FLIGHT_SPEED) + $this->breadcrumb[] = 2; // flying (also contains SPELL_AURA_MOD_INCREASE_MOUNTED_SPEED, so checked first) + else if ($this->subject->getField('effect2AuraId') == SPELL_AURA_MOD_INCREASE_MOUNTED_SPEED || + $this->subject->getField('effect3AuraId') == SPELL_AURA_MOD_INCREASE_MOUNTED_SPEED) + $this->breadcrumb[] = 1; // ground + else + $this->breadcrumb[] = 3; // misc + } + } + + private function createInfobox() : void + { + $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 + { + if ($_ = $this->subject->getField('talentLevel')) + $infobox[] = (in_array($typeCat, [-2, 7, -13]) ? Lang::game('reqLevel', [$_]) : Lang::game('level').Lang::main('colon').$_); + else if ($_ = $this->subject->getField('spellLevel')) + $infobox[] = (in_array($typeCat, [-2, 7, -13]) ? Lang::game('reqLevel', [$_]) : Lang::game('level').Lang::main('colon').$_); + } + + // races + $jsg = []; + if ($_ = Lang::getRaceString($this->subject->getField('reqRaceMask'), $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').$_; + } + + // classes + $jsg = []; + if ($_ = Lang::getClassString($this->subject->getField('reqClassMask'), $jsg, Lang::FMT_MARKUP)) + { + $this->extendGlobalIds(Type::CHR_CLASS, ...$jsg); + $t = count($jsg) == 1 ? Lang::game('class') : Lang::game('classes'); + $infobox[] = Util::ucFirst($t).Lang::main('colon').$_; + } + + // spell focus + if ($_ = $this->subject->getField('spellFocusObject')) + { + if ($sfObj = DB::Aowow()->selectRow('SELECT * FROM ::spellfocusobject WHERE `id` = %i', $_)) + { + $n = Util::localizedString($sfObj, 'name'); + 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; + } + } + + // primary & secondary trades + if (in_array($typeCat, [9, 11])) + { + // skill + if ($_ = $this->subject->getField('skillLines')) + { + $this->extendGlobalIds(Type::SKILL, $_[0]); + + $bar = Lang::game('requires', [' [skill='.$_[0].']']); + if ($_ = $this->subject->getField('learnedAt')) + $bar .= ' ('.$_.')'; + + $infobox[] = $bar; + } + + // specialization + if ($_ = $this->subject->getField('reqSpellId')) + { + $this->extendGlobalIds(Type::SPELL, $_); + $infobox[] = Lang::game('requires2').' [spell='.$_.']'; + } + + // difficulty + if ($_ = $this->subject->getColorsForCurrent()) + $infobox[] = Lang::formatSkillBreakpoints($_); + } + + // accquisition.. 10: starter spell; 7: discovery + 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, $_); + } + + // 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', $this->mapType, $n); + + // Creature Type from Aura: Shapeshift + foreach ($this->modelInfo as $mI) + { + if (!isset($mI['creatureType'])) + continue; + + if ($mI['creatureType'] > 0) + $infobox[] = Lang::game('type').Lang::game('ct', $mI['creatureType']); + + break; + } + + // spell script + if (User::isInGroup(U_GROUP_STAFF)) + 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', $hasCompletion); + + // append glyph symbol if available + $glyphId = 0; + for ($i = 1; $i < 4; $i++) + 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 %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/spell/spell_power.php b/endpoints/spell/spell_power.php new file mode 100644 index 00000000..957232f0 --- /dev/null +++ b/endpoints/spell/spell_power.php @@ -0,0 +1,58 @@ + ['filter' => FILTER_CALLBACK, 'options' => [Locale::class, 'tryFromDomain']] + ); + + public function __construct(string $param) + { + parent::__construct($param); + + // temp locale + if ($this->_get['domain']) + Lang::load($this->_get['domain']); + + $this->typeId = intVal($param); + } + + protected function generate() : void + { + $spell = new SpellList([['id', $this->typeId]]); + if ($spell->error) + $this->cacheType = CACHE_TYPE_NONE; + else + { + $tooltip = $spell->renderTooltip(ttSpells: $ttSpells); + $buff = $spell->renderBuff(buffSpells: $bfSpells); + + $opts = array( + 'name' => $spell->getField('name', true), + 'icon' => $spell->getField('iconString'), + 'tooltip' => $tooltip, + 'spells' => $ttSpells, + 'buff' => $buff, + 'buffspells' => $bfSpells + ); + } + + $this->result = new Tooltip(self::POWER_TEMPLATE, $this->typeId, $opts ?? []); + } +} + +?> diff --git a/pages/spells.php b/endpoints/spells/spells.php similarity index 55% rename from pages/spells.php rename to endpoints/spells/spells.php index 241131fb..eadebde5 100644 --- a/pages/spells.php +++ b/endpoints/spells/spells.php @@ -1,22 +1,43 @@ [ 6, 7], + SKILL_BLACKSMITHING => [ 2, 4], + SKILL_LEATHERWORKING => [ 8, 1], + SKILL_ALCHEMY => [ 1, 6], + SKILL_COOKING => [ 3, 5], + SKILL_MINING => [ 9, 0], + SKILL_TAILORING => [10, 2], + SKILL_ENGINEERING => [ 5, 3], + SKILL_ENCHANTING => [ 4, 8], + SKILL_FISHING => [ 0, 9], + SKILL_JEWELCRAFTING => [ 7, 10], + SKILL_INSCRIPTION => [15, 0], + ); + + protected int $type = Type::SPELL; + protected int $cacheType = CACHE_TYPE_LIST_PAGE; + + protected string $template = 'spells'; + protected string $pageName = 'spells'; + protected ?int $activeTab = parent::TAB_DATABASE; + protected array $breadcrumb = [0, 1]; + + protected array $scripts = [[SC_JS_FILE, 'js/filters.js']]; + protected array $expectedGET = array( + 'filter' => ['filter' => FILTER_VALIDATE_REGEXP, 'options' => ['regexp' => Filter::PATTERN_PARAM]] + ); + protected array $validCats = array( -2 => array( // Talents: Class => Skill 1 => [ 26, 256, 257], 2 => [594, 267, 184], @@ -31,7 +52,7 @@ class SpellsPage extends GenericPage ), -3 => [782, 270, 653, 210, 655, 211, 213, 209, 780, 787, 214, 212, 781, 763, 215, 654, 775, 764, 217, 767, 786, 236, 768, 783, 203, 788, 765, 218, 251, 766, 785, 656, 208, 784, 761, 189, 188, 205, 204], // Pet Spells => Skill -4 => true, // Racial Traits - -5 => true, // Mounts + -5 => [1, 2, 3], // Mounts [Ground, Flying, Misc] -6 => true, // Companions -7 => [409, 410, 411], // PetTalents => TalentTabId -8 => true, // NPC Abilities @@ -53,56 +74,105 @@ class SpellsPage extends GenericPage ), 9 => [129, 185, 356, 762], // Secondary Skills 11 => array( // Professions: Skill => Spell - 171 => true, - 164 => [9788, 9787, 17041, 17040, 17039], - 333 => true, - 202 => [20219, 20222], - 182 => true, - 773 => true, - 755 => true, - 165 => [10656, 10658, 10660], - 186 => true, - 393 => true, - 197 => [26798, 26801, 26797], + SKILL_ALCHEMY => true, + SKILL_BLACKSMITHING => [9788, 9787, 17041, 17040, 17039], + SKILL_ENCHANTING => true, + SKILL_ENGINEERING => [20219, 20222], + SKILL_HERBALISM => true, + SKILL_INSCRIPTION => true, + SKILL_JEWELCRAFTING => true, + SKILL_LEATHERWORKING => [10656, 10658, 10660], + SKILL_MINING => true, + SKILL_SKINNING => true, + SKILL_TAILORING => [26798, 26801, 26797], ) ); - private $shortFilter = array( - 129 => [ 6, 7], // First Aid - 164 => [ 2, 4], // Blacksmithing - 165 => [ 8, 1], // Leatherworking - 171 => [ 1, 6], // Alchemy - 185 => [ 3, 5], // Cooking - 186 => [ 9, 0], // Mining - 197 => [10, 2], // Tailoring - 202 => [ 5, 3], // Engineering - 333 => [ 4, 8], // Enchanting - 356 => [ 0, 9], // Fishing - 755 => [ 7, 10], // Jewelcrafting - 773 => [15, 0], // Inscription - ); + public bool $classPanel = false; + public bool $glyphPanel = false; - - public function __construct($pageCall, $pageParam) + public function __construct(string $rawParam) { - $this->getCategoryFromUrl($pageParam);; - $this->filterObj = new SpellListFilter(false, ['parentCats' => $this->category]); + $this->getCategoryFromUrl($rawParam); - parent::__construct($pageCall, $pageParam); + parent::__construct($rawParam); - $this->name = Util::ucFirst(Lang::game('spells')); - $this->subCat = $pageParam !== null ? '='.$pageParam : ''; + if ($this->category) + $this->subCat = '='.implode('.', $this->category); - $this->classPanel = false; - $this->glyphPanel = false; + $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; } - protected function generateContent() + protected function generate() : void { - $conditions = []; - $visibleCols = []; - $hiddenCols = []; - $tabData = ['data' => []]; + $this->h1 = Util::ucFirst(Lang::game('spells')); + + $conditions = [Listview::DEFAULT_SIZE]; + if (!User::isInGroup(U_GROUP_EMPLOYEE)) + $conditions[] = [['cuFlags', CUSTOM_EXCLUDE_FOR_LISTVIEW, '&'], 0]; + + if ($_ = $this->filter->getConditions()) + $conditions[] = $_; + + + /*************/ + /* Menu Path */ + /*************/ + + foreach ($this->category as $c) + $this->breadcrumb[] = $c; + + $fiForm = $this->filter->values; + if (count($this->breadcrumb) == 4 && $this->category[0] == -13 && count($fiForm['gl']) == 1) + $this->breadcrumb[] = $fiForm['gl']; + + + /**************/ + /* Page Title */ + /**************/ + + $foo = []; + $c = $this->category; // shothand + if (isset($c[2]) && $c[0] == 11) + array_unshift($foo, Lang::spell('cat', $c[0], $c[1], $c[2])); + else if (isset($c[1])) + { + $_ = in_array($c[0], [-2, -13, 7]) ? Lang::game('cl') : Lang::spell('cat', $c[0]); + array_unshift($foo, is_array($_[$c[1]]) ? $_[$c[1]][0] : $_[$c[1]]); + } + + if (isset($c[0]) && count($foo) < 2) + { + $_ = Lang::spell('cat', $c[0]); + array_unshift($foo, is_array($_) ? $_[0] : $_); + } + + if (count($foo) < 2) + array_unshift($foo, $this->h1); + + foreach ($foo as $bar) + array_unshift($this->title, $bar); + + + /****************/ + /* Main Content */ + /****************/ + + $visibleCols = []; + $hiddenCols = []; + $extraCols = []; + $tabData = ['data' => []]; + + $this->redButtons[BUTTON_WOWHEAD] = true; + if ($fiQuery = $this->filter->buildGETParam()) + $this->wowheadLink .= '&filter='.$fiQuery; // the next lengthy ~250 lines determine $conditions and lvParams if ($this->category) @@ -133,30 +203,30 @@ class SpellsPage extends GenericPage $xCond = null; for ($i = -2; $i < 0; $i++) { - foreach (Game::$skillLineMask[$i] as $idx => $pair) + foreach (Game::$skillLineMask[$i] as $idx => [, $skillLineId]) { - if ($pair[1] == $this->category[1]) + 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]]] ]; } @@ -171,6 +241,29 @@ class SpellsPage extends GenericPage case -9: // GM Spells array_push($visibleCols, 'level'); case -5: // Mounts + array_push($extraCols, "\$Listview.funcBox.createSimpleCol('speed', 'speed', '90px', 'speed')"); + + if (isset($this->category[1])) + { + switch ($this->category[1]) + { + case 1: + $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[] = [DB::OR, ['effect2AuraId', SPELL_AURA_MOD_INCREASE_MOUNTED_FLIGHT_SPEED], ['effect3AuraId', SPELL_AURA_MOD_INCREASE_MOUNTED_FLIGHT_SPEED]]; + break; + case 3: + $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, '!'] + ]; + break; + } + } case -6: // Companions $conditions[] = ['s.typeCat', $this->category[0]]; @@ -184,15 +277,15 @@ class SpellsPage extends GenericPage { switch ($this->category[1]) // Spells can be used by multiple specs { - case 409: // Tenacity + case 409: // TalentTab - Tenacity $conditions[] = ['s.cuFlags', SPELL_CU_PET_TALENT_TYPE1, '&']; $url = '?pets=1'; break; - case 410: // Cunning + case 410: // TalentTab - Cunning $conditions[] = ['s.cuFlags', SPELL_CU_PET_TALENT_TYPE2, '&']; $url = '?pets=2'; break; - case 411: // Ferocity + case 411: // TalentTab - Ferocity $conditions[] = ['s.cuFlags', SPELL_CU_PET_TALENT_TYPE0, '&']; $url = '?pets=0'; break; @@ -201,7 +294,7 @@ class SpellsPage extends GenericPage $tabData['note'] = '$$WH.sprintf(LANG.lvnote_pettalents, "'.$url.'")'; } - $tabData['_petTalents'] = 1; // not conviced, this is correct, but .. it works + $tabData['_petTalents'] = 1; break; case -11: // Proficiencies ... the subIds are actually SkillLineCategories @@ -213,7 +306,7 @@ class SpellsPage extends GenericPage 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]]]; } @@ -246,24 +339,24 @@ class SpellsPage extends GenericPage // $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]]]] ]; } @@ -277,27 +370,27 @@ class SpellsPage extends GenericPage 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($this->shortFilter[$this->category[1]])) + if (!empty(self::SHORT_FILTER[$this->category[1]])) { - $sf = $this->shortFilter[$this->category[1]]; + [$crafted, $relItems] = self::SHORT_FILTER[$this->category[1]]; $txt = ''; - if ($sf[0] && $sf[1]) - $txt = sprintf(Lang::spell('relItems', 'crafted'), $sf[0]) . Lang::spell('relItems', 'link') . sprintf(Lang::spell('relItems', 'recipes'), $sf[1]); - else if ($sf[0]) - $txt = sprintf(Lang::spell('relItems', 'crafted'), $sf[0]); - else if ($sf[1]) - $txt = sprintf(Lang::spell('relItems', 'recipes'), $sf[1]); + if ($crafted && $relItems) + $txt = Lang::spell('relItems', 'crafted', [$crafted]) . Lang::spell('relItems', 'link') . Lang::spell('relItems', 'recipes', [$relItems]); + else if ($crafted) + $txt = Lang::spell('relItems', 'crafted', [$crafted]); + else if ($relItems) + $txt = Lang::spell('relItems', 'recipes', [$relItems]); $note = Lang::spell('cat', $this->category[0], $this->category[1]); if (is_array($note)) $note = $note[0]; - $tabData['note'] = sprintf(Lang::spell('relItems', 'base'), $txt, $note); + $tabData['note'] = Lang::spell('relItems', 'base', [$txt, $note]); $tabData['sort'] = ['skill', 'name']; } } @@ -322,22 +415,22 @@ class SpellsPage extends GenericPage { $conditions[] = ['s.skillLine1', $this->category[1]]; - if (!empty($this->shortFilter[$this->category[1]])) + if (!empty(self::SHORT_FILTER[$this->category[1]])) { - $sf = $this->shortFilter[$this->category[1]]; + [$crafted, $relItems] = self::SHORT_FILTER[$this->category[1]]; $txt = ''; - if ($sf[0] && $sf[1]) - $txt = sprintf(Lang::spell('relItems', 'crafted'), $sf[0]) . Lang::spell('relItems', 'link') . sprintf(Lang::spell('relItems', 'recipes'), $sf[1]); - else if ($sf[0]) - $txt = sprintf(Lang::spell('relItems', 'crafted'), $sf[0]); - else if ($sf[1]) - $txt = sprintf(Lang::spell('relItems', 'recipes'), $sf[1]); + if ($crafted && $relItems) + $txt = Lang::spell('relItems', 'crafted', [$crafted]) . Lang::spell('relItems', 'link') . Lang::spell('relItems', 'recipes', [$relItems]); + else if ($crafted) + $txt = Lang::spell('relItems', 'crafted', [$crafted]); + else if ($relItems) + $txt = Lang::spell('relItems', 'recipes', [$relItems]); $note = Lang::spell('cat', $this->category[0], $this->category[1]); if (is_array($note)) $note = $note[0]; - $tabData['note'] = sprintf(Lang::spell('relItems', 'base'), $txt, $note); + $tabData['note'] = Lang::spell('relItems', 'base', [$txt, $note]); $tabData['sort'] = ['skill', 'name']; } } @@ -347,66 +440,67 @@ class SpellsPage extends GenericPage 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; } } - if (!User::isInGroup(U_GROUP_EMPLOYEE)) - $conditions[] = [['cuFlags', CUSTOM_EXCLUDE_FOR_LISTVIEW, '&'], 0]; - - if ($_ = $this->filterObj->getConditions()) - $conditions[] = $_; - - $spells = new SpellList($conditions); + $spells = new SpellList($conditions, ['calcTotal' => true]); $this->extendGlobalData($spells->getJSGlobals(GLOBALINFO_SELF | GLOBALINFO_RELATED)); - $tabData['data'] = array_values($spells->getListviewData()); - // recreate form selection - $this->filter = $this->filterObj->getForm(); - $this->filter['query'] = isset($_GET['filter']) ? $_GET['filter'] : NULL; - $this->filter['initData'] = ['init' => 'spells']; + $lvData = $spells->getListviewData(); - if ($ec = $this->filterObj->getExtraCols()) + // add speed-data for mounts + if ($this->category && $this->category[0] == -5) { - $this->filter['initData']['ec'] = $ec; + foreach ($spells->iterate() as $spellId => $__) + { + $lvData[$spellId]['speed'] = 0; + + if (in_array($spells->getField('effect2AuraId'), [SPELL_AURA_MOD_INCREASE_MOUNTED_SPEED, SPELL_AURA_MOD_INCREASE_MOUNTED_FLIGHT_SPEED, SPELL_AURA_MOD_INCREASE_SWIM_SPEED])) + $lvData[$spellId]['speed'] = $spells->getField('effect2BasePoints') + 1; + if (in_array($spells->getField('effect3AuraId'), [SPELL_AURA_MOD_INCREASE_MOUNTED_SPEED, SPELL_AURA_MOD_INCREASE_MOUNTED_FLIGHT_SPEED, SPELL_AURA_MOD_INCREASE_SWIM_SPEED])) + $lvData[$spellId]['speed'] = max($lvData[$spellId]['speed'], $spells->getField('effect3BasePoints') + 1); + + if (!$lvData[$spellId]['speed'] && ($spells->getField('effect2AuraId') == SPELL_AURA_DUMMY || $spells->getField('effect3AuraId') == SPELL_AURA_DUMMY)) + $lvData[$spellId]['speed'] = '?'; + else + $lvData[$spellId]['speed'] = '+'.$lvData[$spellId]['speed'].'%'; + } + } + + $tabData['data'] = $lvData; + + if ($this->filter->fiExtraCols) $tabData['extraCols'] = '$fi_getExtraCols(fi_extraCols, 0, 0)'; - } + else if ($extraCols) + $tabData['extraCols'] = $extraCols; - if ($sc = $this->filterObj->getSetCriteria()) - { - $this->filter['initData']['sc'] = $sc; - - // add source to cols if explicitly searching for it - if (in_array(9, $sc['cr']) && !in_array('source', $visibleCols)) - $visibleCols[] = 'source'; - } + // add source to cols if explicitly searching for it + if ($this->filter->getSetCriteria(9) && !in_array('source', $visibleCols)) + $visibleCols[] = 'source'; // create note if search limit was exceeded; overwriting 'note' is intentional - if ($spells->getMatches() > CFG_SQL_LIMIT_DEFAULT) + if ($spells->getMatches() > Listview::DEFAULT_SIZE) { - $tabData['note'] = sprintf(Util::$tryFilteringString, 'LANG.lvnote_spellsfound', $spells->getMatches(), CFG_SQL_LIMIT_DEFAULT); + $tabData['note'] = sprintf(Util::$tryFilteringString, 'LANG.lvnote_spellsfound', $spells->getMatches(), Listview::DEFAULT_SIZE); $tabData['_truncated'] = 1; } - if ($this->filterObj->error) - $tabData['_errors'] = 1; - - - $mask = $spells->hasSetFields(['reagent1', 'skillLines', 'trainingCost', 'reqClassMask']); - if ($mask & 0x1) - $visibleCols[] = 'reagents'; - if (!($mask & 0x2) && $this->category && !in_array($this->category[0], [9, 11])) + $mask = $spells->hasSetFields('skillLines', 'trainingCost', 'reqClassMask', null, 'reagent1', 'reagent2', 'reagent3', 'reagent4', 'reagent5', 'reagent6', 'reagent7', 'reagent8'); + if (!($mask & 0x1) && $this->category && !in_array($this->category[0], [9, 11])) $hiddenCols[] = 'skill'; - if (($mask & 0x4)) + if ($mask & 0x2) $visibleCols[] = 'trainingcost'; - if (($mask & 0x8) && !in_array('classes', $visibleCols)) - $visibleCols[] = 'singleclass'; + if (($mask & 0x4) && !in_array('singleclass', $visibleCols)) + $visibleCols[] = 'classes'; + if ($mask & 0xFF0) + $visibleCols[] = 'reagents'; if ($visibleCols) @@ -415,49 +509,22 @@ class SpellsPage extends GenericPage if ($hiddenCols) $tabData['hiddenCols'] = array_unique($hiddenCols); - $this->lvTabs[] = ['spell', $tabData]; + $this->lvTabs = new Tabs(['parent' => "\$\$WH.ge('tabs-generic')"]); + $this->lvTabs->addListviewTab(new Listview($tabData, SpellList::$brickFile)); + parent::generate(); + + $this->setOnCacheLoaded([self::class, 'onBeforeDisplay']); + } + + protected static function onBeforeDisplay() : void + { // sort for dropdown-menus Lang::sort('game', 'ra'); Lang::sort('game', 'cl'); Lang::sort('game', 'sc'); Lang::sort('game', 'me'); } - - protected function generateTitle() - { - $foo = []; - $c = $this->category; // shothand - if (isset($c[2]) && $c[0] == 11) - array_unshift($foo, Lang::spell('cat', $c[0], $c[1], $c[2])); - else if (isset($c[1])) - { - $_ = in_array($c[0], [-2, -13, 7]) ? Lang::game('cl') : Lang::spell('cat', $c[0]); - array_unshift($foo, is_array($_[$c[1]]) ? $_[$c[1]][0] : $_[$c[1]]); - } - - if (isset($c[0]) && count($foo) < 2) - { - $_ = Lang::spell('cat', $c[0]); - array_unshift($foo, is_array($_) ? $_[0] : $_); - } - - if (count($foo) < 2) - array_unshift($foo, $this->name); - - foreach ($foo as $bar) - array_unshift($this->title, $bar); - } - - protected function generatePath() - { - foreach ($this->category as $c) - $this->path[] = $c; - - $form = $this->filterObj->getForm(); - if (count($this->path) == 4 && $this->category[0] == -13 && isset($form['gl']) && count($form['gl']) == 1) - $this->path[] = $form['gl']; - } } ?> diff --git a/endpoints/talent/talent.php b/endpoints/talent/talent.php new file mode 100644 index 00000000..0ea35fd7 --- /dev/null +++ b/endpoints/talent/talent.php @@ -0,0 +1,39 @@ +h1 = Lang::main('talentCalc'); + $this->chooseType = Lang::main('chooseClass'); + + array_unshift($this->title, $this->h1); + + parent::generate(); + } +} + +?> diff --git a/endpoints/title/title.php b/endpoints/title/title.php new file mode 100644 index 00000000..21959316 --- /dev/null +++ b/endpoints/title/title.php @@ -0,0 +1,203 @@ +typeId = intVal($id); + $this->contribute = Type::getClassAttrib($this->type, 'contribute') ?? CONTRIBUTE_NONE; + } + + protected function generate() : void + { + $this->subject = new TitleList(array(['id', $this->typeId])); + if ($this->subject->error) + $this->generateNotFound(Lang::game('title'), Lang::title('notFound')); + + $this->h1 = $this->subject->getHtmlizedName(); + + $this->gPageInfo += array( + 'type' => $this->type, + 'typeId' => $this->typeId, + 'name' => $this->h1 + ); + + $_title = Util::ucFirst(trim(strtr($this->subject->getField('male', true), ['%s' => '', ',' => '']))); + + + /*************/ + /* Menu Path */ + /*************/ + + $this->breadcrumb[] = $this->subject->getField('category');; + + + /**************/ + /* Page Title */ + /**************/ + + array_unshift($this->title, $_title, Util::ucFirst(Lang::game('title'))); + + + /***********/ + /* Infobox */ + /***********/ + + $infobox = Lang::getInfoBoxForFlags($this->subject->getField('cuFlags')); + + $infobox[] = Lang::main('side') . match ($this->subject->getField('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 + }; + + if ($g = $this->subject->getField('gender')) + $infobox[] = Lang::main('gender').Lang::main('colon').'[span class=icon-'.($g == 2 ? 'female' : 'male').']'.Lang::main('sex', $g).'[/span]'; + + if ($eId = $this->subject->getField('eventId')) + { + $this->extendGlobalIds(Type::WORLDEVENT, $eId); + $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', 1); + + + /****************/ + /* Main Content */ + /****************/ + + $this->expansion = Util::$expansionString[$this->subject->getField('expansion')]; + $this->redButtons = array( + BUTTON_WOWHEAD => true, + BUTTON_LINKS => ['type' => $this->type, 'typeId' => $this->typeId] + ); + + // factionchange-equivalent + 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) + { + $this->transfer = Lang::title('_transfer', array( + $altTitle->id, + $altTitle->getHtmlizedName(), + $pendant > 0 ? 'alliance' : 'horde', + $pendant > 0 ? Lang::game('si', SIDE_ALLIANCE) : Lang::game('si', SIDE_HORDE) + )); + } + } + + + /**************/ + /* Extra Tabs */ + /**************/ + + $this->lvTabs = new Tabs(['parent' => "\$\$WH.ge('tabs-generic')"], 'tabsRelated', true); + + // tab: reward-from-quest + $quests = new QuestList(array(['rewardTitleId', $this->typeId])); + if (!$quests->error) + { + $this->extendGlobalData($quests->getJSGlobals(GLOBALINFO_REWARDS)); + + $this->lvTabs->addListviewTab(new Listview(array( + 'data' => $quests->getListviewData(), + 'id' => 'reward-from-quest', + 'name' => '$LANG.tab_rewardfrom', + 'hiddenCols' => ['experience', 'money'], + 'visibleCols' => ['category'] + ), QuestList::$brickFile)); + } + + // 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) + { + $this->extendGlobalData($acvs->getJSGlobals()); + + $this->lvTabs->addListviewTab(new Listview(array( + 'data' => $acvs->getListviewData(), + 'id' => 'reward-from-achievement', + 'name' => '$LANG.tab_rewardfrom', + 'visibleCols' => ['category'], + 'sort' => ['reqlevel', 'name'] + ), AchievementList::$brickFile)); + } + } + + // 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) + { + $this->extendGlobalData($acvs->getJSGlobals()); + + $this->lvTabs->addListviewTab(new Listview(array( + 'data' => $acvs->getListviewData(), + 'id' => 'criteria-of', + 'name' => '$LANG.tab_criteriaof', + 'visibleCols' => ['category'] + ), AchievementList::$brickFile)); + } + } + + // tab: condition-for + $cnd = new Conditions(); + $cnd->getByCondition(Type::TITLE, $this->typeId)->prepare(); + if ($tab = $cnd->toListviewTab('condition-for', '$LANG.tab_condition_for')) + { + $this->extendGlobalData($cnd->getJsGlobals()); + $this->lvTabs->addDataTab(...$tab); + } + + parent::generate(); + } +} + +?> diff --git a/endpoints/titles/titles.php b/endpoints/titles/titles.php new file mode 100644 index 00000000..a6c3d4d5 --- /dev/null +++ b/endpoints/titles/titles.php @@ -0,0 +1,75 @@ +getCategoryFromUrl($rawParam); + + parent::__construct($rawParam); + } + + protected function generate() : void + { + $this->h1 = Util::ucFirst(Lang::game('titles')); + + + if ($this->category) + $this->breadcrumb[] = $this->category[0]; + + + array_unshift($this->title, $this->h1); + if ($this->category) + array_unshift($this->title, Lang::title('cat', $this->category[0])); + + + $this->redButtons[BUTTON_WOWHEAD] = true; + + $conditions = [Listview::DEFAULT_SIZE]; + + if (!User::isInGroup(U_GROUP_EMPLOYEE)) // hide unused titles + $conditions[] = [['cuFlags', CUSTOM_EXCLUDE_FOR_LISTVIEW, '&'], 0]; + + if ($this->category) + $conditions[] = ['category', $this->category[0]]; + + $tabData = ['data' => []]; + $titles = new TitleList($conditions); + if (!$titles->error) + { + $tabData['data'] = $titles->getListviewData(); + + if ($titles->hasDiffFields('category')) + $tabData['visibleCols'] = ['category']; + + if (!$titles->hasAnySource()) + $tabData['hiddenCols'] = ['source']; + } + + $this->lvTabs = new Tabs(['parent' => "\$\$WH.ge('tabs-generic')"]); + + $this->lvTabs->addListviewTab(new Listview($tabData, TitleList::$brickFile)); + + parent::generate(); + } +} + +?> diff --git a/endpoints/tooltips/tooltips.php b/endpoints/tooltips/tooltips.php new file mode 100644 index 00000000..7ead16a0 --- /dev/null +++ b/endpoints/tooltips/tooltips.php @@ -0,0 +1,34 @@ +generateError(); + } + + protected function generate() : void + { + $this->h1 = Lang::main('moreTitles', $this->pageName); + + array_unshift($this->title, $this->h1); + + parent::generate(); + } +} + +?> diff --git a/endpoints/top-users/top-users.php b/endpoints/top-users/top-users.php new file mode 100644 index 00000000..464fea0a --- /dev/null +++ b/endpoints/top-users/top-users.php @@ -0,0 +1,96 @@ +generateError(); + } + + protected function generate() : void + { + $this->h1 = Lang::main('moreTitles', $this->pageName); + + array_unshift($this->title, $this->h1); + + $tabs = array( + [0, 'top-users-alltime', '$LANG.alltime_stc' ], + [time() - MONTH, 'top-users-monthly', '$LANG.lastmonth_stc'], + [time() - WEEK, 'top-users-weekly', '$LANG.lastweek_stc' ] + ); + + // expected by javascript but metrics are not used by us + $nullFields = array( + 'uploads' => 0, // wow client cache uploads + 'posts' => 0, // forum posts + 'gold' => 0, // site achievements + 'silver' => 0, + 'copper' => 0 + ); + + $this->lvTabs = new Tabs(['parent' => "\$\$WH.ge('tabs-generic')"], __forceTabs: true); + + foreach ($tabs as [$time, $tabId, $tabName]) + { + // stuff received + $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` = %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 %i', + self::MAX_RESULTS + ); + + $data = []; + if ($res) + { + // stuff given + $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) + { + $r['creation'] = date('c', $r['creation']); + $r['votes'] = empty($votes[$uId]) ? 0 : $votes[$uId]; + $r += $nullFields; + } + + $data = $res; + } + + $this->lvTabs->addListviewTab(new Listview(array( + 'hiddenCols' => ['achievements', 'posts', 'uploads', 'reports'], + 'visibleCols' => ['created'], + 'name' => '$LANG.lastweek_stc', + 'name' => $tabName, + 'id' => $tabId, + 'data' => $data + ), 'topusers')); + } + + parent::generate(); + } +} + +?> diff --git a/endpoints/unrated-comments/unrated-comments.php b/endpoints/unrated-comments/unrated-comments.php new file mode 100644 index 00000000..2b7fefc5 --- /dev/null +++ b/endpoints/unrated-comments/unrated-comments.php @@ -0,0 +1,41 @@ + Util > Unrated Comments + + protected function generate() : void + { + $this->h1 = Lang::main('utilities', 5); + + + /*********/ + /* Title */ + /*********/ + + array_unshift($this->title, $this->h1); + + + /****************/ + /* Main Content */ + /****************/ + + $this->lvTabs = new Tabs(['parent' => "\$\$WH.ge('tabs-generic')"]); + + $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 new file mode 100644 index 00000000..ab9c111d --- /dev/null +++ b/endpoints/upload/image-complete.php @@ -0,0 +1,88 @@ + ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkCoords']], + ); + + public string $imgHash; + public int $newId; + + public function __construct(string $rawParam) + { + if (User::isBanned()) + $this->generate404(); + + parent::__construct($rawParam); + + if (!preg_match('/^upload=image-complete&(\d+)\.(\w{16})$/i', $_SERVER['QUERY_STRING'] ?? '', $m, PREG_UNMATCHED_AS_NULL)) + $this->generate404(); + + [, $this->newId, $this->imgHash] = $m; + + if (!$this->imgHash || !$this->newId) + $this->generate404(); + } + + protected function generate() : void + { + if (!$this->handleComplete()) + $_SESSION['msg'] = ['avatar', false, AvatarMgr::$error ?: Lang::main('intError')]; + } + + private function handleComplete() : bool + { + if (!$this->assertPOST('coords')) + return false; + + if (!AvatarMgr::init()) + return false; + + if (!AvatarMgr::loadFile(AvatarMgr::PATH_TEMP, User::$username.'-avatar-'.$this->newId.'-'.$this->imgHash.'_original')) + return false; + + if (!AvatarMgr::cropImg(...$this->_post['coords'])) + return false; + + if (!AvatarMgr::createAtlas($this->newId)) + return false; + + $fSize = filesize(sprintf(AvatarMgr::PATH_AVATARS, $this->newId)); + if (!$fSize) + return false; + + $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); + return false; + } + + // delete temp files + unlink(sprintf(AvatarMgr::PATH_TEMP, User::$username.'-avatar-'.$this->newId.'-'.$this->imgHash.'_original')); + unlink(sprintf(AvatarMgr::PATH_TEMP, User::$username.'-avatar-'.$this->newId.'-'.$this->imgHash)); + + return true; + } + + protected static function checkCoords(string $val) : ?array + { + if (preg_match('/^[01]\.[0-9]{3}(,[01]\.[0-9]{3}){3}$/', $val)) + return explode(',', $val); + + return null; + } +} + +?> diff --git a/endpoints/upload/image-crop.php b/endpoints/upload/image-crop.php new file mode 100644 index 00000000..cf7f8279 --- /dev/null +++ b/endpoints/upload/image-crop.php @@ -0,0 +1,84 @@ +generateError(); + + parent::__construct($rawParam); + } + + protected function generate() : void + { + if ($err = $this->handleUpload()) + { + $_SESSION['msg'] = ['avatar', false, $err]; + $this->forward('?account#community'); + } + + $this->h1 = Lang::account('avatarSubmit'); + + $fileBase = User::$username.'-avatar-'.$this->nextId.'-'.$this->imgHash; + $dimensions = AvatarMgr::calcImgDimensions(); + + $this->cropper = $dimensions + array( + 'url' => Cfg::get('STATIC_URL').'/uploads/temp/'.$fileBase.'.jpg', + 'parent' => 'av-container', + 'minCrop' => ICON_SIZE_LARGE, // optional; defaults to 150 - min selection size (a square) + 'type' => Type::NPC, // NPC: 15384 [OLDWorld Trigger (DO NOT DELETE)] + 'typeId' => 15384, // = arbitrary image upload + 'constraint' => [1, 1] // [xMult, yMult] - relative size to each other (here: be square) + ); + + parent::generate(); + } + + private function handleUpload() : string + { + if (!AvatarMgr::init()) + return Lang::main('intError'); + + if (!AvatarMgr::validateUpload()) + return AvatarMgr::$error; + + if (!AvatarMgr::loadUpload()) + return Lang::main('intError'); + + $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 ++(); illegal syntax? WHO KNOWS!? + $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'); + + return ''; + } +} + +?> diff --git a/endpoints/user/user.php b/endpoints/user/user.php new file mode 100644 index 00000000..cff2c560 --- /dev/null +++ b/endpoints/user/user.php @@ -0,0 +1,390 @@ +forward('?user='.User::$username); + + 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 a.`id` <> 0 AND LOWER(a.`username`) = LOWER(%s) GROUP BY a.`id`', $rawParam)) + $this->user = $user; + else + $this->generateNotFound(Lang::user('notFound', [Util::htmlEscape($rawParam)])); + } + + protected function generate() : void + { + /*********/ + /* Title */ + /*********/ + + array_unshift($this->title, Lang::user('profileTitle', [$this->user['username']])); + + + /***********/ + /* Infobox */ + /***********/ + + $infobox = $contrib = $groups = []; + + foreach (Lang::account('groups') as $idx => $grp) + if ($idx >= 0 && $this->user['userGroups'] & (1 << $idx)) + $groups[] = (!fMod(count($groups) + 1, 3) ? '[br]' : '').$grp; + + if (User::isInGroup(U_GROUP_STAFF)) + { + $infobox[] = Lang::account('lastIP'). $this->user['prevIP']; + $infobox[] = Lang::account('email') . Lang::main('colon') . $this->user['email']; + } + + 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]'.(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]'.(new DateTime())->formatDate($this->user['prevLogin']).'[/span]'; + if ($groups) + $infobox[] = Lang::user('userGroups') . implode(', ', $groups); + + $infobox[] = Lang::user('consecVisits'). $this->user['consecutiveVisits']; + + 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'); + + if ($_ = $this->getCommentStats()) + $contrib[] = $_; + + if ($_ = $this->getScreenshotStats()) + $contrib[] = $_; + + if ($_ = $this->getVideoStats()) + $contrib[] = $_; + + if ($_ = $this->getForumStats()) + $contrib[] = $_; + + // $contrib[] = [url=http://www.wowhead.com/client]Data uploads: n [small]([tooltip=tooltip_totaldatauploads]xx.y MB[/tooltip])[/small][/url] + + if ($contrib) + $this->contributions = new InfoboxMarkup($contrib, ['allow' => Markup::CLASS_STAFF], 'infobox-contents1'); + + + /****************/ + /* Main Content */ + /****************/ + + $this->h1 = $this->user['title'] ? $this->user['username'].' <'.$this->user['title'].'>' : Lang::user('profileTitle', [$this->user['username']]); + + if ($this->user['avatar']) + { + $avatarMore = match ((int)$this->user['avatar']) + { + 1 => $this->user['wowicon'], + 2 => DB::Aowow()->selectCell('SELECT `id` FROM ::account_avatars WHERE `current` = 1 AND `userId` = %i', $this->user['id']), + default => '' + }; + + if (!($this->user['userGroups'] & U_GROUP_PREMIUM)) + $this->user['avatarborder'] = 2; + + $this->userIcon = array( // JS: Icon.createUser() + $this->user['avatar'], // avatar: 1(iconString), 2(customId) + $avatarMore, // avatarMore: iconString or customId + IconElement::SIZE_MEDIUM, // size: (always medium) + null, // url: (always null) + $this->user['avatarborder'], // premiumLevel: affixes css class ['-premium', '-gold', '', '-premiumred', '-red'] + false, // noBorder: always false + '$Icon.getPrivilegeBorder('.$this->user['sumRep'].')' // reputationLevel: calculated in js from passed rep points + ); + } + + $this->username = $this->user['username']; + + if ($this->user['description']) // seen CLASS_STAFF, but wouldn't dare.. filtered for restricted tags before sent? + $this->description = new Markup($this->user['description'], ['allow' => ($this->user['userGroups'] & U_GROUP_PREMIUM) ? Markup::CLASS_PREMIUM : Markup::CLASS_USER], 'description-generic'); + + + /**************/ + /* Extra Tabs */ + /**************/ + + $this->lvTabs = new Tabs(['parent' => "\$\$WH.ge('tabs-generic')"], 'tabsRelated', true); + + // [unused] Site Achievements + + // Reputation changelog (params only for comment-events) + if (User::$id == $this->user['id'] || User::isInGroup(U_GROUP_MODERATOR)) + 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, resultLimit: Listview::DEFAULT_SIZE)) + { + $tabData = array( + 'data' => $_, + 'hiddenCols' => ['author'], + 'onBeforeCreate' => '$Listview.funcBox.beforeUserComments', + '_totalCount' => $nFound + ); + + if ($nFound > Listview::DEFAULT_SIZE) + { + $tabData['name'] = '$LANG.tab_latestcomments'; + $tabData['note'] = '$$WH.sprintf(LANG.lvnote_usercomments, '.$nFound.')'; + } + + $this->lvTabs->addListviewTab(new Listview($tabData, 'commentpreview')); + } + + // Comment Replies + if ($_ = CommunityContent::getCommentPreviews(['user' => $this->user['id'], 'replies' => true], $nFound, resultLimit: Listview::DEFAULT_SIZE)) + { + $tabData = array( + 'data' => $_, + 'hiddenCols' => ['author'], + 'onBeforeCreate' => '$Listview.funcBox.beforeUserComments', + '_totalCount' => $nFound + ); + + if ($nFound > Listview::DEFAULT_SIZE) + { + $tabData['name'] = '$LANG.tab_latestreplies'; + $tabData['note'] = '$$WH.sprintf(LANG.lvnote_userreplies, '.$nFound.')'; + } + + $this->lvTabs->addListviewTab(new Listview($tabData, 'replypreview')); + } + + // Screenshots + if ($_ = CommunityContent::getScreenshots(-$this->user['id'], 0, $nFound, resultLimit: Listview::DEFAULT_SIZE)) + { + $tabData = array( + 'data' => $_, + '_totalCount' => $nFound + ); + + if ($nFound > Listview::DEFAULT_SIZE) + { + $tabData['name'] = '$LANG.tab_latestscreenshots'; + $tabData['note'] = '$$WH.sprintf(LANG.lvnote_userscreenshots, '.$nFound.')'; + } + + $this->lvTabs->addListviewTab(new Listview($tabData, 'screenshot')); + } + + // Videos + if ($_ = CommunityContent::getVideos(-$this->user['id'], 0, $nFound, resultLimit: Listview::DEFAULT_SIZE)) + { + $tabData = array( + 'data' => $_, + '_totalCount' => $nFound + ); + + if ($nFound > Listview::DEFAULT_SIZE) + { + $tabData['name'] = '$LANG.tab_latestvideos'; + $tabData['note'] = '$$WH.sprintf(LANG.lvnote_uservideos, '.$nFound.')'; + } + + $this->lvTabs->addListviewTab(new Listview($tabData, 'video')); + } + + // forum -> latest topics [unused] + + // forum -> latest replies [unused] + + if (Cfg::get('PROFILER_ENABLE')) + { + $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'); + + 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(array(['status', [GuideMgr::STATUS_APPROVED, GuideMgr::STATUS_ARCHIVED]], ['userId', $this->user['id']])); + if (!$guides->error) + { + $this->lvTabs->addListviewTab(new Listview(array( + 'data' => $guides->getListviewData(), + 'hiddenCols' => ['patch'] + ), GuideList::$brickFile)); + } + + parent::generate(); + } + + 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` = %i AND ur.`userId` <> 0 WHERE c.`replyTo` = 0 AND c.`userId` = %i', + RATING_COMMENT, $this->user['id'] + ); + + if (!$co) + return null; + + [$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` & %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 + ); + + if (!$ss) + return null; + + [$sum, $nSticky, $nPending] = $ss; + + if (!$sum) + return null; + + $buff = []; + if ($nSticky || $nPending) + { + if ($normal = ($sum - $nSticky - $nPending)) + $buff[] = '[tooltip=tooltip_normal]'.$normal.'[/tooltip]'; + + if ($nSticky) + $buff[] = '[tooltip=tooltip_sticky]'.$nSticky.'[/tooltip]'; + + if ($nPending) + $buff[] = '[tooltip=tooltip_pending]'.$nPending.'[/tooltip]'; + } + + return Lang::user('screenshots').$sum.($buff ? ' [small]('.implode(' + ', $buff).')[/small]' : ''); + } + + private function getVideoStats() : ?string + { + $vi = DB::Aowow()->selectRow( + '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 + ); + + if (!$vi) + return null; + + [$sum, $nSticky, $nPending] = $vi; + + if (!$sum) + return null; + + $buff = []; + if ($nSticky || $nPending) + { + if ($normal = ($sum - $nSticky - $nPending)) + $buff[] = '[tooltip=tooltip_normal]'.$normal.'[/tooltip]'; + + if ($nSticky) + $buff[] = '[tooltip=tooltip_sticky]'.$nSticky.'[/tooltip]'; + + if ($nPending) + $buff[] = '[tooltip=tooltip_pending]'.$nPending.'[/tooltip]'; + } + + return Lang::user('videos').$sum.($buff ? ' [small]('.implode(' + ', $buff).')[/small]' : ''); + } + + private function getForumStats() : ?string + { + $fo = null; // some query + + if (!$fo) + return null; + + [$nTopics, $nReplies] = $fo; + + $buff = []; + if ($nTopics) + $buff[] = '[tooltip=topics]'.$nTopics.'[/tooltip]'; + + 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 new file mode 100644 index 00000000..9459e9a9 --- /dev/null +++ b/endpoints/video/add.php @@ -0,0 +1,124 @@ + 1. =add: receives user upload + 1.1. checks and processing on the upload + 1.2. forward to =confirm or blank response + 2. =confirm: user edites upload + 3. =complete: store edited video file and data + 4. =thankyou +*/ + +class VideoAddResponse extends TextResponse +{ + protected bool $requiresLogin = true; + + protected array $expectedPOST = array( + 'videourl' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkTextLine']] + ); + + private string $videoHash = ''; + private int $destType = 0; + private int $destTypeId = 0; + + public function __construct(string $rawParam) + { + parent::__construct($rawParam); + + // get video destination + // target delivered as video=&.. (hash is optional) + if (!preg_match('/^video=\w+&(-?\d+)\.(-?\d+)(\.(\w{16}))?$/i', $_SERVER['QUERY_STRING'] ?? '', $m, PREG_UNMATCHED_AS_NULL)) + $this->generate404(); + + [, $this->destType, $this->destTypeId, , $videoHash] = $m; + + // no such type or this type cannot receive videos + if (!Type::checkClassAttrib($this->destType, 'contribute', CONTRIBUTE_VI)) + $this->generate404(); + + // no such typeId + if (!Type::validateIds($this->destType, $this->destTypeId)) + $this->generate404(); + + // only accept/expect hash for confirm & complete + if ($videoHash) + $this->generate404(); + } + + protected function generate() : void + { + if ($this->handleAdd()) + $this->redirectTo = '?video=confirm&'.$this->destType.'.'.$this->destTypeId.'.'.$this->videoHash; + else if ($this->destType && $this->destTypeId) + $this->redirectTo = '?'.Type::getFileString($this->destType).'='.$this->destTypeId.'#suggest-a-video'; + else + $this->generate404(); + } + + private function handleAdd() : bool + { + if (!User::canSuggestVideo()) + { + $_SESSION['error']['vi'] = Lang::video('error', 'notAllowed'); + return false; + } + + if (!$this->assertPOST('videourl')) + { + $_SESSION['error']['vi'] = Lang::video('error', 'selectVI'); + return false; + } + + $videoId = ''; + if (preg_match('/^https?:\/\/(www\.)?youtu(\.be|be\.com\/watch\?v=)([a-zA-Z0-9_-]{11})/', $this->_post['videourl'], $m)) + $videoId = $m[3]; + else + { + $_SESSION['error']['vi'] = Lang::video('error', 'selectVI'); + return false; + } + + $curl = curl_init('https://youtube.com/oembed?format=json&url=https://www.youtube.com/watch?v='.$videoId); + if (!$curl) + { + trigger_error('VideoAddResponse - curl_init fail', E_USER_ERROR); + $_SESSION['error']['vi'] = Lang::main('intError'); + return false; + } + + curl_setopt($curl, CURLOPT_RETURNTRANSFER, true); + $ytOembed = curl_exec($curl); + $status = curl_getinfo($curl, CURLINFO_RESPONSE_CODE); + curl_close($curl); + + if ($status == 401) + { + $_SESSION['error']['vi'] = Lang::video('error', 'isPrivate'); + return false; + } + else if ($status != 200) // 404, 500 seen .. does it matter why its inaccessible? + { + $_SESSION['error']['vi'] = Lang::video('error', 'noExist'); + return false; + } + + $videoInfo = json_decode($ytOembed); + $videoInfo->id = $videoId; + + if (!VideoMgr::saveSuggestion($videoInfo, $this->destType, $this->destTypeId, $this->videoHash)) + { + $_SESSION['error']['ss'] = Lang::main('intError'); + return false; + } + + return true; + } +} + +?> diff --git a/endpoints/video/complete.php b/endpoints/video/complete.php new file mode 100644 index 00000000..3bb554aa --- /dev/null +++ b/endpoints/video/complete.php @@ -0,0 +1,92 @@ + 3. =complete: store edited video file and data + 4. =thankyou +*/ + +class VideoCompleteResponse extends TextResponse +{ + use TrCommunityHelper; + + protected bool $requiresLogin = true; + + protected array $expectedPOST = array( + 'caption' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkTextLine']] + ); + + private string $videoHash = ''; + private int $destType = 0; + private int $destTypeId = 0; + + public function __construct(string $rawParam) + { + parent::__construct($rawParam); + + // get video destination + // target delivered as video=&.. (hash is optional) + if (!preg_match('/^video=\w+&(-?\d+)\.(-?\d+)(\.(\w{16}))?$/i', $_SERVER['QUERY_STRING'] ?? '', $m, PREG_UNMATCHED_AS_NULL)) + $this->generate404(); + + [, $this->destType, $this->destTypeId, , $this->videoHash] = $m; + + // no such type or this type cannot receive videos + if (!Type::checkClassAttrib($this->destType, 'contribute', CONTRIBUTE_VI)) + $this->generate404(); + + // no such typeId + if (!Type::validateIds($this->destType, $this->destTypeId)) + $this->generate404(); + } + + protected function generate() : void + { + if ($this->handleComplete()) + $this->forward('?video=thankyou&'.$this->destType.'.'.$this->destTypeId); + else + $this->generate404(); + } + + private function handleComplete() : bool + { + if (!VideoMgr::loadSuggestion($videoInfo, $this->destType, $this->destTypeId, $this->videoHash)) + $this->generate404(); + + $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()->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, + $videoInfo->thumbnail_url, + $videoInfo->thumbnail_width, + $videoInfo->thumbnail_height, + $videoInfo->title, + $this->handleCaption($this->_post['caption']) + ); + + if (!is_int($newId)) // 0 is valid, NULL or FALSE is not + { + trigger_error('VideoCompleteResponse - video query failed', E_USER_ERROR); + return false; + } + + VideoMgr::dropTempFile(); + + return true; + } +} + +?> diff --git a/endpoints/video/confirm.php b/endpoints/video/confirm.php new file mode 100644 index 00000000..46ed385a --- /dev/null +++ b/endpoints/video/confirm.php @@ -0,0 +1,81 @@ + 2. =crop: user edites upload + 2.1. just show edit page + 2.2. user submits coords and description to =complete + 3. =complete: store edited video file and data + 4. =thankyou +*/ + +class VideoConfirmResponse extends TemplateResponse +{ + protected bool $requiresLogin = true; + + protected string $template = 'video'; + protected string $pageName = 'video'; + + public ?Markup $infobox = null; + public string $videoHash = ''; + public int $destType = 0; + public int $destTypeId = 0; + public string $url = ''; + public int $width = 0; + public int $height = 0; + public array $video = []; + public string $viTitle = ''; + + public function __construct(string $rawParam) + { + parent::__construct($rawParam); + + // get video destination + // target delivered as video=&.. (hash is optional) + if (!preg_match('/^video=\w+&(-?\d+)\.(-?\d+)(\.(\w{16}))?$/i', $_SERVER['QUERY_STRING'] ?? '', $m, PREG_UNMATCHED_AS_NULL)) + $this->generateError(); + + [, $this->destType, $this->destTypeId, , $this->videoHash] = $m; + + // no such type or this type cannot receive videos + if (!Type::checkClassAttrib($this->destType, 'contribute', CONTRIBUTE_VI)) + $this->generateError(); + + // no such typeId + if (!Type::validateIds($this->destType, $this->destTypeId)) + $this->generateError(); + } + + protected function generate() : void + { + $this->h1 = Lang::video('submission'); + array_unshift($this->title, $this->h1); + + if (!VideoMgr::loadSuggestion($videoInfo, $this->destType, $this->destTypeId, $this->videoHash)) + $this->generateError(); + + $this->viTitle = $videoInfo->title; + $this->url = $videoInfo->thumbnail_url; + $this->width = $videoInfo->thumbnail_width; + $this->height = $videoInfo->thumbnail_height; + $this->video = [[ + 'videoType' => VideoMgr::TYPE_YOUTUBE, + 'videoId' => $videoInfo->id, + 'caption' => $videoInfo->title + ]]; + + // target + $this->infobox = new Markup(Lang::screenshot('displayOn', [Lang::typeName($this->destType), Type::getFileString($this->destType), $this->destTypeId]), ['allow' => Markup::CLASS_STAFF, 'dbpage' => true], 'infobox-contents0'); + $this->extendGlobalIds($this->destType, $this->destTypeId); + + parent::generate(); + } +} + +?> diff --git a/endpoints/video/thankyou.php b/endpoints/video/thankyou.php new file mode 100644 index 00000000..f07822ee --- /dev/null +++ b/endpoints/video/thankyou.php @@ -0,0 +1,60 @@ + 4. =thankyou +*/ + +class VideoThankyouResponse extends TemplateResponse +{ + protected bool $requiresLogin = true; + + protected string $template = 'text-page-generic'; + protected string $pageName = 'video'; + + private int $destType = 0; + private int $destTypeId = 0; + + public function __construct(string $rawParam) + { + parent::__construct($rawParam); + + // get video destination + // target delivered as video=&. + if (!preg_match('/^video=\w+&(-?\d+)\.(-?\d+)$/i', $_SERVER['QUERY_STRING'] ?? '', $m, PREG_UNMATCHED_AS_NULL)) + $this->generateError(); + + [, $this->destType, $this->destTypeId] = $m; + + // no such type or this type cannot receive videos + if (!Type::checkClassAttrib($this->destType, 'contribute', CONTRIBUTE_VI)) + $this->generateError(); + + // no such typeId + if (!Type::validateIds($this->destType, $this->destTypeId)) + $this->generateError(); + } + + protected function generate() : void + { + $this->h1 = Lang::video('submission'); + + array_unshift($this->title, $this->h1); + + $this->extraHTML = Lang::video('thanks', 'contrib').'

'; + $this->extraHTML .= Lang::video('thanks', 'goBack', [Type::getFileString($this->destType), $this->destTypeId])."

\n"; + $this->extraHTML .= ''.Lang::video('thanks', 'note').''; + + parent::generate(); + } +} + +?> diff --git a/endpoints/whats-new/whats-new.php b/endpoints/whats-new/whats-new.php new file mode 100644 index 00000000..63124dfe --- /dev/null +++ b/endpoints/whats-new/whats-new.php @@ -0,0 +1,34 @@ +generateError(); + } + + protected function generate() : void + { + $this->h1 = Lang::main('moreTitles', $this->pageName); + + array_unshift($this->title, $this->h1); + + parent::generate(); + } +} + +?> diff --git a/endpoints/zone/zone.php b/endpoints/zone/zone.php new file mode 100644 index 00000000..3e54eb4f --- /dev/null +++ b/endpoints/zone/zone.php @@ -0,0 +1,952 @@ +typeId = intVal($id); + $this->contribute = Type::getClassAttrib($this->type, 'contribute') ?? CONTRIBUTE_NONE; + } + + protected function generate() : void + { + $this->subject = new ZoneList(array(['id', $this->typeId])); + if ($this->subject->error) + $this->generateNotFound(Lang::game('zone'), Lang::zone('notFound')); + + $this->h1 = $this->subject->getField('name', true); + + $this->gPageInfo += array( + 'type' => $this->type, + 'typeId' => $this->typeId, + 'name' => $this->h1 + ); + + $_parentArea = $this->subject->getField('parentArea'); + $_type = $this->subject->getField('type'); + + + /*************/ + /* Menu Path */ + /*************/ + + $this->breadcrumb[] = $this->subject->getField('category'); + + if (in_array($this->subject->getField('category'), [MAP_TYPE_DUNGEON, MAP_TYPE_RAID])) + $this->breadcrumb[] = $this->subject->getField('expansion'); + + + /**************/ + /* Page Title */ + /**************/ + + array_unshift($this->title, $this->h1, Util::ucFirst(Lang::game('zone'))); + + + /***********/ + /* Infobox */ + /***********/ + + $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; + $args = array_filter(explode(':', $args), fn($x) => $x != ''); + + return Lang::$grp(...$args); + }, $quickFactsRows); + + foreach ($quickFactsRows as $er) + $this->extendGlobalData(Markup::parseTags($er)); + + $infobox = Lang::getInfoBoxForFlags($this->subject->getField('cuFlags')); + + if ($topRows = array_filter($quickFactsRows, fn($x) => $x < 0, ARRAY_FILTER_USE_KEY)) + $infobox = array_merge($infobox, $topRows); + + // City + if ($this->subject->getField('flags') & AREA_FLAG_SLAVE_CAPITAL && !$_parentArea) + $infobox[] = Lang::zone('city'); + + // Auto repop + if ($this->subject->getField('flags') & AREA_FLAG_NEED_FLY && !$_parentArea) + $infobox[] = Lang::zone('autoRez'); + + // Level + if ($_ = $this->subject->getField('levelMin')) + { + if ($_ < $this->subject->getField('levelMax')) + $_ .= ' - '.$this->subject->getField('levelMax'); + + $infobox[] = Lang::game('level').Lang::main('colon').$_; + } + + // required Level + if ($_ = $this->subject->getField('levelReq')) + { + if ($__ = $this->subject->getField('levelReqLFG')) + $buff = Lang::zone('reqLevels', [$_, $__]); + else + $buff = Lang::main('_reqLevel').Lang::main('colon').$_; + + $infobox[] = $buff; + } + + // Territory + $faction = $this->subject->getField('faction'); + $wrap = match ($faction) + { + TEAM_ALLIANCE => '[span class=icon-alliance]%s[/span]', + TEAM_HORDE => '[span class=icon-horde]%s[/span]', + 4, 5 => '[span class=icon-ffa]%s[/span]', + default => '%s' + }; + + $infobox[] = Lang::zone('territory').sprintf($wrap, Lang::zone('territories', $faction)); + + // Instance Type + $infobox[] = Lang::zone('instanceType').'[span class=icon-instance'.$this->subject->getField('type').']'.Lang::zone('instanceTypes', $this->subject->getField('type')).'[/span]'; + + // Heroic mode + if ($_ = $this->subject->getField('levelHeroic')) + $infobox[] = '[icon preset=heroic]'.Lang::zone('hcAvailable', [$_]).'[/icon]'; + + // number of players + if ($_ = $this->subject->getField('maxPlayer')) + { + if (in_array($this->subject->getField('category'), [6, 9])) + $infobox[] = Lang::zone('numPlayersVs', [$_]); + else + $infobox[] = Lang::zone('numPlayers', [$_ == -2 ? '10/25' : $_]); + } + + // Instances + 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` = %i', $this->typeId)) + { + $this->extendGlobalIds(Type::CHR_RACE, ...$_); + $infobox[] = Lang::concat($_, Lang::CONCAT_NONE, fn($x) => '[race='.$x.']').' '.Lang::race('startZone'); + } + + 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`= %i AND `typeId` = %i ', Type::ZONE, $this->typeId)) + { + $this->addMoveLocationMenu($pa['areaId'], $pa['floor']); + + $pins = str_pad($pa['posX'] * 10, 3, '0', STR_PAD_LEFT) . str_pad($pa['posY'] * 10, 3, '0', STR_PAD_LEFT); + $infobox[] = Lang::zone('location').'[lightbox=map zone='.$pa['areaId'].' '.($pa['floor'] > 1 ? 'floor='.--$pa['floor'] : '').' pins='.$pins.']'.ZoneList::getName($pa['areaId']).'[/lightbox]'; + } + + // Attunement Quest/Achievements & Keys + if ($attmnt = $this->subject->getField('attunes')) + { + foreach ($attmnt as $type => $ids) + { + $this->extendGlobalIds($type, ...array_map('abs', $ids)); + foreach ($ids as $id) + { + if ($type == Type::ITEM) + $infobox[] = Lang::zone('key', (int)($id < 0)).'[item='.abs($id).']'; + else + $infobox[] = Lang::zone('attunement', (int)($id < 0)).'['.Type::getFileString($type).'='.abs($id).']'; + } + } + } + + // 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); + + if ($infobox) + $this->infobox = new InfoboxMarkup($infobox, ['allow' => Markup::CLASS_STAFF, 'dbpage' => true], 'infobox-contents0'); + + + /****************/ + /* Main Content */ + /****************/ + + $addToSOM = function (string $what, string $group, array $entry) use (&$som) : void + { + // entry always contains: type, id, name, level, coords[] + if (!isset($som[$what][$group])) // not found yet + $som[$what][$group][] = $entry; + else // found .. something.. + { + // check for identical floors + foreach ($som[$what][$group] as &$byFloor) + { + if ($byFloor['level'] != $entry['level']) + continue; + + // found existing floor, ammending coords + $byFloor['coords'][] = $entry['coords'][0]; + return; + } + + // floor not used yet, create it + $som[$what][$group][] = $entry; + } + }; + + if ($_parentArea) + { + $this->extraText = new Markup(Lang::zone('zonePartOf', [$_parentArea]), ['dbpage' => true, 'allow' => Markup::CLASS_ADMIN], 'text-generic'); + $this->extendGlobalIds(Type::ZONE, $_parentArea); + } + + // we cannot fetch spawns via lists. lists are grouped by entry + $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 = [['s.areaId', $this->typeId]]; + if (!User::isInGroup(U_GROUP_STAFF)) + $conditions[] = [['cuFlags', CUSTOM_EXCLUDE_FOR_LISTVIEW, '&'], 0]; + + $objectSpawns = new GameObjectList($conditions, ['calcTotal' => true]); + $creatureSpawns = new CreatureList($conditions, ['calcTotal' => true]); + $atSpawns = new AreaTriggerList($conditions); + + $questsLV = $rewardsLV = []; + + $relQuestZOS = [$this->typeId]; + foreach (Game::$questSubCats as $parent => $children) + { + if (in_array($this->typeId, $children)) + $relQuestZOS[] = $parent; + else if ($this->typeId == $parent) + $relQuestZOS = array_merge($relQuestZOS, $children); + } + + // see if we can actually display a map + $mapFilePath = 'static/images/wow/maps/%s/normal/%d%s.jpg'; + $options = array( + [Lang::getLocale()->json(), ''], // default case + [Lang::getLocale()->json(), '-1'], // try multifloor + ['enus', ''], // try english fallback + ['enus', '-1'] // try english fallback, multifloor + ); + $hasMap = false; + foreach ($options as [$lang, $floor]) + { + if (!file_exists(sprintf($mapFilePath, $lang, $this->typeId, $floor))) + continue; + + $hasMap = true; + break; + } + + if ($hasMap) + { + $som = []; + foreach ($oSpawns as $spawn) + { + $tpl = $objectSpawns->getEntry($spawn['typeId']); + if (!$tpl) + continue; + + $n = Util::localizedString($tpl, 'name'); + + $what = match ((int)$tpl['typeCat']) + { + -3 => 'herb', + -4 => 'vein', + 9 => 'book', + 25 => 'pool', + 0 => $tpl['type'] == 19 ? 'mail' : '', + -6 => $tpl['spellFocusId'] == 1 ? 'anvil' : ($tpl['spellFocusId'] == 3 ? 'forge' : ''), + default => '' + }; + + if ($what) + { + $blob = array( + 'coords' => [[$spawn['posX'], $spawn['posY']]], + 'level' => $spawn['floor'], + 'name' => $n, + 'type' => Type::OBJECT, + 'id' => $tpl['id'] + ); + + if ($what == 'mail') + { + $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']) + { + $started = new QuestList(array(['qse.method', 1, '&'], ['qse.type', Type::OBJECT], ['qse.typeId', $tpl['id']])); + if ($started->error) + continue; + + // store data for misc tabs + foreach ($started->getListviewData() as $id => $data) + { + if ($started->getField('questSortId') > 0 && !in_array($started->getField('questSortId'), $relQuestZOS)) + continue; + + if (!empty($started->rewards[$id][Type::ITEM])) + $rewardsLV = array_merge($rewardsLV, array_keys($started->rewards[$id][Type::ITEM])); + + if (!empty($started->choices[$id][Type::ITEM])) + $rewardsLV = array_merge($rewardsLV, array_keys($started->choices[$id][Type::ITEM])); + + $questsLV[$id] = $data; + } + + $this->extendGlobalData($started->getJSGlobals()); + + if (($tpl['A'] != -1) && ($_ = $started->getSOMData(SIDE_ALLIANCE))) + $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 : SIDE_ALLIANCE) | ($tpl['H'] < 0 ? 0 : SIDE_HORDE)), + 'quests' => array_values($_) + )); + + if (($tpl['H'] != -1) && ($_ = $started->getSOMData(SIDE_HORDE))) + $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 : SIDE_ALLIANCE) | ($tpl['H'] < 0 ? 0 : SIDE_HORDE)), + 'quests' => array_values($_) + )); + } + } + + $flightNodes = []; + foreach ($cSpawns as $spawn) + { + $tpl = $creatureSpawns->getEntry($spawn['typeId']); + if (!$tpl) + continue; + + $n = Util::localizedString($tpl, 'name'); + $sn = Util::localizedString($tpl, 'subname'); + + $flagsMap = array( + NPC_FLAG_REPAIRER => 'repair', + NPC_FLAG_AUCTIONEER => 'auctioneer', + NPC_FLAG_BANKER => 'banker', + NPC_FLAG_BATTLEMASTER => 'battlemaster', + NPC_FLAG_INNKEEPER => 'innkeeper', + NPC_FLAG_TRAINER => 'trainer', + NPC_FLAG_VENDOR => 'vendor', + NPC_FLAG_FLIGHT_MASTER => 'flightmaster', + NPC_FLAG_STABLE_MASTER => 'stablemaster', + NPC_FLAG_GUILD_MASTER => 'guildmaster', + NPC_FLAG_SPIRIT_HEALER | + NPC_FLAG_SPIRIT_GUIDE => 'spirithealer', + 0 => '' // set 'unused' if no match + ); + + if ($creatureSpawns->isBoss()) + $what = 'boss'; + else if ($tpl['rank'] == NPC_RANK_RARE_ELITE || $tpl['rank'] == NPC_RANK_RARE) + $what = 'rare'; + else + foreach ($flagsMap as $flag => $what) + if ($tpl['npcflag'] & $flag) + break; + + if ($what == 'flightmaster') + $flightNodes[$tpl['id']] = [$spawn['posX'], $spawn['posY']]; + + if ($what) + $addToSOM($what, $n, array( + 'coords' => [[$spawn['posX'], $spawn['posY']]], + 'level' => $spawn['floor'], + 'name' => $n, + 'type' => Type::NPC, + 'id' => $tpl['id'], + 'reacthorde' => $tpl['H'] ?: 1, // no neutral (0) setting + 'reactalliance' => $tpl['A'] ?: 1, + 'description' => $sn + )); + + if ($tpl['startsQuests']) + { + $started = new QuestList(array(['qse.method', 1, '&'], ['qse.type', Type::NPC], ['qse.typeId', $tpl['id']])); + if ($started->error) + continue; + + // store data for misc tabs + foreach ($started->getListviewData() as $id => $data) + { + if ($started->getField('questSortId') > 0 && !in_array($started->getField('questSortId'), $relQuestZOS)) + continue; + + if (!empty($started->rewards[$id][Type::ITEM])) + $rewardsLV = array_merge($rewardsLV, array_keys($started->rewards[$id][Type::ITEM])); + + if (!empty($started->choices[$id][Type::ITEM])) + $rewardsLV = array_merge($rewardsLV, array_keys($started->choices[$id][Type::ITEM])); + + $questsLV[$id] = $data; + } + + $this->extendGlobalData($started->getJSGlobals()); + + if (($tpl['A'] != -1) && ($_ = $started->getSOMData(SIDE_ALLIANCE))) + $addToSOM('alliancequests', $n, array( + 'coords' => [[$spawn['posX'], $spawn['posY']]], + 'level' => $spawn['floor'], + 'name' => $n, + 'type' => Type::NPC, + 'id' => $tpl['id'], + 'reacthorde' => $tpl['H'], + 'reactalliance' => $tpl['A'], + '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', $n, array( + 'coords' => [[$spawn['posX'], $spawn['posY']]], + 'level' => $spawn['floor'], + 'name' => $n, + 'type' => Type::NPC, + 'id' => $tpl['id'], + 'reacthorde' => $tpl['H'], + 'reactalliance' => $tpl['A'], + 'side' => (($tpl['A'] < 0 ? 0 : SIDE_ALLIANCE) | ($tpl['H'] < 0 ? 0 : SIDE_HORDE)), + 'quests' => array_values($_) + )); + } + } + + foreach ($aSpawns as $spawn) + { + if ($spawn['guid'] < 0) // skip teleporter endpoints + continue; + + $tpl = $atSpawns->getEntry($spawn['typeId']); + if (!$tpl) + continue; + + $n = Util::localizedString($tpl, 'name', true, true); + $addToSOM('areatrigger', $n, array( + 'coords' => [[$spawn['posX'], $spawn['posY']]], + 'level' => $spawn['floor'], + 'name' => $n, + 'type' => Type::AREATRIGGER, + 'id' => $spawn['typeId'], + 'description' => Lang::game('type').Lang::areatrigger('types', $tpl['type']) + )); + } + + // remove unwanted indizes + foreach ($som as $what => &$dataz) + { + if (empty($som[$what])) + continue; + + foreach ($dataz as &$data) + $data = array_values($data); + + if (!in_array($what, ['vein', 'herb', 'rare', 'pool'])) + { + $foo = []; + foreach ($dataz as $d) + foreach ($d as $_) + $foo[] = $_; + + $dataz = $foo; + } + } + + unset($data); + + // append paths between nodes + if ($flightNodes) + { + // neutral nodes come last as the line is colored by the node it's attached to + usort($som['flightmaster'], function($a, $b) { + $n1 = (int)$a['reactalliance'] == $a['reacthorde']; + $n2 = (int)$b['reactalliance'] == $b['reacthorde']; + + return $n1 <=> $n2; + }); + + $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) + { + foreach ($som['flightmaster'] as &$fm) + { + if ($fm['id'] != $path[0] && $fm['id'] != $path[1]) + continue; + + if ($fm['id'] == $path[0]) + $fm['paths'][] = $flightNodes[$path[1]]; + + if ($fm['id'] == $path[1]) + $fm['paths'][] = $flightNodes[$path[0]]; + + unset($paths[$k]); + break; + } + } + } + + // preselect bosses for raids/dungeons + if (in_array($_type, [MAP_TYPE_DUNGEON, MAP_TYPE_RAID, MAP_TYPE_BATTLEGROUND, MAP_TYPE_DUNGEON_HC, MAP_TYPE_MMODE_RAID, MAP_TYPE_MMODE_RAID_HC])) + $som['instance'] = true; + + $this->map = array( + array( // Mapper + 'parent' => 'mapper-generic', + 'zone' => $this->typeId, + 'zoneLink' => false + ), + null, // mapperData + $som, // ShowOnMap + null // foundIn + ); + } + + $this->expansion = Util::$expansionString[$this->subject->getField('expansion')]; + $this->redButtons = array( + BUTTON_WOWHEAD => true, + BUTTON_LINKS => ['type' => $this->type, 'typeId' => $this->typeId] + ); + + + /**************/ + /* Extra Tabs */ + /**************/ + + $this->lvTabs = new Tabs(['parent' => "\$\$WH.ge('tabs-generic')"], 'tabsRelated', true); + + // 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' => $data, + 'id' => 'drops', + 'name' => '$LANG.tab_drops', + 'extraCols' => $subTabs ? ['$Listview.extraCols.mode'] : null, + 'computeDataFunc' => '$Listview.funcBox.initLootTable', + 'onAfterCreate' => $subTabs ? '$Listview.funcBox.addModeIndicator' : null + ); + + 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)); + + $this->lvTabs->addListviewTab(new Listview($tabData, CreatureList::$brickFile)); + } + + // tab: objects + if ($oSpawns && !$objectSpawns->error) + { + $tabData = ['data' => $objectSpawns->getListviewData()]; + + 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)); + + $this->lvTabs->addListviewTab(new Listview($tabData, GameObjectList::$brickFile)); + } + + $quests = new QuestList(array(['questSortId', $this->typeId])); + if (!$quests->error) + { + $this->extendGlobalData($quests->getJSGlobals()); + foreach ($quests->getListviewData() as $id => $data) + { + if (!empty($quests->rewards[$id][Type::ITEM])) + $rewardsLV = array_merge($rewardsLV, array_keys($quests->rewards[$id][Type::ITEM])); + + if (!empty($quests->choices[$id][Type::ITEM])) + $rewardsLV = array_merge($rewardsLV, array_keys($quests->choices[$id][Type::ITEM])); + + $questsLV[$id] = $data; + } + } + + // tab: quests [including data collected by SOM-routine] + if ($questsLV) + { + $tabData = ['data' => $questsLV]; + + foreach (Game::QUEST_CLASSES as $parent => $children) + { + if (!in_array($this->typeId, $children)) + continue; + + 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: starts-quest + // select every quest starter, that is a drop + $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` = %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] + ); + + if ($questStartItem) + { + $qsiList = new ItemList(array(['id', array_keys($questStartItem)])); + if (!$qsiList->error) + { + $this->lvTabs->addListviewTab(new Listview(array( + 'data' => $qsiList->getListviewData(), + 'name' => '$LANG.tab_startsquest', + 'id' => 'starts-quest' + ), ItemList::$brickFile)); + + $this->extendGlobalData($qsiList->getJSGlobals(GLOBALINFO_SELF)); + } + } + + // 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' => $note + ), ItemList::$brickFile)); + + $this->extendGlobalData($rewards->getJSGlobals(GLOBALINFO_SELF)); + } + } + + // tab: achievements + + // 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` = %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` = %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, + 'computeDataFunc' => '$Listview.funcBox.initLootTable' + ), ItemList::$brickFile)); + } + + // tab: spells + 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) + { + $lvSpells = $spells->getListviewData(); + $this->extendGlobalData($spells->getJSGlobals()); + + $cnd = new Conditions(); + foreach ($saData as $a) + { + if (empty($lvSpells[$a['spell']])) + continue; + + if ($a['aura_spell']) + $cnd->addExternalCondition(Conditions::SRC_NONE, $a['spell'], [$a['aura_spell'] > 0 ? Conditions::AURA : -Conditions::AURA, abs($a['aura_spell'])]); + + if ($a['quest_start']) // status for quests needs work + $cnd->addExternalCondition(Conditions::SRC_NONE, $a['spell'], [Conditions::QUESTSTATE, $a['quest_start'], $a['quest_start_status']]); + + if ($a['quest_end'] && $a['quest_end'] != $a['quest_start']) + $cnd->addExternalCondition(Conditions::SRC_NONE, $a['spell'], [Conditions::QUESTSTATE, $a['quest_end'], $a['quest_end_status']]); + + if ($a['racemask']) + $cnd->addExternalCondition(Conditions::SRC_NONE, $a['spell'], [Conditions::CHR_RACE, $a['racemask']]); + + if ($a['gender'] != 2) // 2: both + $cnd->addExternalCondition(Conditions::SRC_NONE, $a['spell'], [Conditions::GENDER, $a['gender']]); + } + + if ($cnd->toListviewColumn($lvSpells, $extraCols)) + $this->extendGlobalData($cnd->getJsGlobals()); + + $this->lvTabs->addListviewTab(new Listview(array( + 'data' => $lvSpells, + 'hiddenCols' => ['skill'], + 'extraCols' => $extraCols ?: null + ), SpellList::$brickFile)); + } + } + + // tab: subzones + $subZones = new ZoneList(array(['parentArea', $this->typeId])); + if (!$subZones->error) + { + $this->lvTabs->addListviewTab(new Listview(array( + 'data' => $subZones->getListviewData(), + 'name' => '$LANG.tab_zones', + 'id' => 'subzones', + 'hiddenCols' => ['territory', 'instancetype'] + ), ZoneList::$brickFile)); + + $this->extendGlobalData($subZones->getJSGlobals(GLOBALINFO_SELF)); + } + + // tab: sound (including subzones; excluding parents) + $areaIds = []; + if (!$subZones->error) + $areaIds = $subZones->getFoundIDs(); + + $areaIds[] = $this->typeId; + + $soundIds = []; + $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 %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` = %i AND `type` = %i', $this->typeId, Type::SOUND)) + $soundIds = array_merge($soundIds, $sSpawns); + + if ($zoneMusic) + $soundIds = array_merge($soundIds, array_column($zoneMusic, 'soundId')); + + if ($soundIds) + { + $music = new SoundList(array(['id', array_unique($soundIds)])); + if (!$music->error) + { + // tab + $data = $music->getListviewData(); + $tabData = []; + + if (array_filter(array_column($zoneMusic, 'worldStateId'))) + { + $tabData['extraCols'] = ['$Listview.extraCols.condition']; + + foreach ($soundIds as $sId) + if (!empty($zoneMusic[$sId]['worldStateId'])) + Conditions::extendListviewRow($data[$sId], Conditions::SRC_NONE, $this->typeId, [Conditions::WORLD_STATE, $zoneMusic[$sId]['worldStateId'], $zoneMusic[$sId]['worldStateValue']]); + } + + $tabData['data'] = $data; + + $this->lvTabs->addListviewTab(new Listview($tabData, SoundList::$brickFile)); + + $this->extendGlobalData($music->getJSGlobals(GLOBALINFO_SELF)); + + $typeFilter = function(array $music, int $type) use ($data) : array + { + $result = []; + foreach (array_filter($music, fn ($x) => $x['type'] == $type) as $sId => $_) + $result = array_merge($result, $data[$sId]['files'] ?? []); + + return $result; + }; + + // audio controls (order how it appears on page) + // [title, data, divID, options] + if ($_ = $typeFilter($zoneMusic, 2)) + $this->zoneMusic[] = [Lang::sound('music'), $_, 'zonemusic', (object)['loop' => true]]; + + if ($_ = $typeFilter($zoneMusic, 3)) + $this->zoneMusic[] = [Lang::sound('intro'), $_, 'zonemusicintro', (object)[]]; + + if ($_ = $typeFilter($zoneMusic, 1)) + $this->zoneMusic[] = [Lang::sound('ambience'), $_, 'soundambience', (object)['loop' => true]]; + } + } + + // tab: condition-for + $cnd = new Conditions(); + $cnd->getByCondition(Type::ZONE, $this->typeId)->prepare(); + if ($tab = $cnd->toListviewTab('condition-for', '$LANG.tab_condition_for')) + { + $this->extendGlobalData($cnd->getJsGlobals()); + $this->lvTabs->addDataTab(...$tab); + } + } + + private function addMoveLocationMenu(int $_parentArea, int $parentFloor) : void + { + // hide for non-staff + if (!User::isInGroup(U_GROUP_EMPLOYEE)) + return; + + $worldPos = WorldPosition::getForGUID(Type::ZONE, -$this->typeId); + if (!$worldPos) + return; + + $menu = Util::buildPosFixMenu($worldPos[-$this->typeId]['mapId'], $worldPos[-$this->typeId]['posX'], $worldPos[-$this->typeId]['posY'], Type::ZONE, -$this->typeId, $_parentArea, $parentFloor); + if (!$menu) + return; + + $menu = [1002, 'Edit DB Entry', null, $menu]; + + $this->addScript([SC_JS_STRING, '$(document).ready(function () { mn_staff.push('.Util::toJSON(array_values($menu)).'); });']); + } +} + +?> diff --git a/endpoints/zones/zones.php b/endpoints/zones/zones.php new file mode 100644 index 00000000..0f58fd4a --- /dev/null +++ b/endpoints/zones/zones.php @@ -0,0 +1,188 @@ +getCategoryFromUrl($rawParam); + + parent::__construct($rawParam); + } + + protected function generate() : void + { + $this->h1 = Util::ucFirst(Lang::game('zones')); + + + /*************/ + /* Menu Path */ + /*************/ + + foreach ($this->category as $c) + $this->breadcrumb[] = $c; + + + /**************/ + /* Page Title */ + /**************/ + + if (isset($this->category[1])) + array_unshift($this->title, Lang::game('expansions', $this->category[1])); + + if (isset($this->category[0])) + array_unshift($this->title, Lang::zone('cat', $this->category[0])); + + + /****************/ + /* Main Content */ + /****************/ + + $this->redButtons[BUTTON_WOWHEAD] = true; + + $conditions = []; // do not limit + $visibleCols = []; + $hiddenCols = []; + + if (!User::isInGroup(U_GROUP_EMPLOYEE)) // sub-areas and unused zones + $conditions[] = [['cuFlags', CUSTOM_EXCLUDE_FOR_LISTVIEW, '&'], 0]; + + if ($this->category) + { + $conditions[] = ['z.category', $this->category[0]]; + $hiddenCols[] = 'category'; + + if (isset($this->category[1]) && in_array($this->category[0], [2, 3])) + $conditions[] = ['z.expansion', $this->category[1]]; + + switch ($this->category[0]) + { + case 6: + case 2: + case 3: + array_push($visibleCols, 'level', 'players'); + case 9: + $hiddenCols[] = 'territory'; + break; + } + } + + $zones = new ZoneList($conditions); + + if (!$zones->hasSetFields('type')) + $hiddenCols[] = 'instancetype'; + + $this->lvTabs = new Tabs(['parent' => "\$\$WH.ge('tabs-generic')"]); + + $this->lvTabs->addListviewTab(new Listview(array( + 'data' => $zones->getListviewData(), + 'visibleCols'=> $visibleCols ?: null, + 'hiddenCols' => $hiddenCols ?: null + ), ZoneList::$brickFile)); + + + /**************/ + /* Flight Map */ + /**************/ + + [$mapFile, $spawnMap] = match ($this->category[0] ?? null) + { + 0 => [-3, 0], + 1 => [-6, 1], + 8 => [-2, 530], + 10 => [-5, 571], + default => [ 0, -1] + }; + + if ($mapFile) + { + $somData = ['flightmaster' => []]; + $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.`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 %in OR tp.`EndNodeId` IN %in)', + array_keys($nodes), array_keys($nodes) + ); + + foreach ($nodes as $i => $n) + { + $neutral = $n['reactH'] == $n['reactA']; + + $data = array( + 'coords' => [[$n['mapX'], $n['mapY']]], + 'level' => 0, // floor + 'name' => Util::localizedString($n, 'name'), + 'type' => $n['type'], + 'id' => $n['typeId'], + 'reacthorde' => $n['reactH'], + 'reactalliance' => $n['reactA'], + 'paths' => [] + ); + + foreach ($paths as $j => $p) + { + if ($i != $p['startId'] && $i != $p['endId']) + continue; + + if ($i == $p['startId'] && (!$neutral || $p['neutral'])) + { + $data['paths'][] = [$p['startPosX'], $p['startPosY']]; + unset($paths[$j]); + } + else if ($i == $p['endId'] && (!$neutral || $p['neutral'])) + { + $data['paths'][] = [$p['endPosX'], $p['endPosY']]; + unset($paths[$j]); + } + } + + if (empty($data['paths'])) + unset($data['paths']); + + $somData['flightmaster'][] = $data; + } + + $this->map = array( + array( // Mapper + 'parent' => 'mapper-generic', + 'zone' => $mapFile, + 'zoom' => 1, + 'overlay' => true, + 'zoomable' => false + ), + null, // mapperData + $somData, // ShowOnMap + null // foundIn + ); + } + + parent::generate(); + } +} + +?> diff --git a/includes/ajaxHandler.class.php b/includes/ajaxHandler.class.php deleted file mode 100644 index 2387c506..00000000 --- a/includes/ajaxHandler.class.php +++ /dev/null @@ -1,91 +0,0 @@ -params = $params; - - foreach ($this->_post as $k => &$v) - $v = isset($_POST[$k]) ? filter_input(INPUT_POST, $k, $v[0], $v[1]) : null; - - foreach ($this->_get as $k => &$v) - $v = isset($_GET[$k]) ? filter_input(INPUT_GET, $k, $v[0], $v[1]) : null; - } - - public function handle(&$out) - { - if (!$this->handler) - return false; - - if ($this->validParams) - { - if (count($this->params) != 1) - return false; - - if (!in_array($this->params[0], $this->validParams)) - return false; - } - - $h = $this->handler; - $out = (string)$this->$h(); - - return true; - } - - public function getContentType() - { - return $this->contentType; - } - - protected function checkEmptySet($val) - { - return $val === ''; // parameter is expected to be empty - } - - protected function checkLocale($val) - { - if (preg_match('/^'.implode('|', array_keys(array_filter(Util::$localeStrings))).'$/', $val)) - return intval($val); - - return null; - } - - protected function checkInt($val) - { - if (preg_match('/^-?\d+$/', $val)) - return intval($val); - - return null; - } - - protected function checkIdList($val) - { - if (preg_match('/^-?\d+(,-?\d+)*$/', $val)) - return array_map('intval', explode(',', $val)); - - return null; - } - - protected function checkFulltext($val) - { - // trim non-printable chars - return preg_replace('/[\p{C}]/ui', '', $val); - } -} -?> diff --git a/includes/ajaxHandler/account.class.php b/includes/ajaxHandler/account.class.php deleted file mode 100644 index ae25b4f8..00000000 --- a/includes/ajaxHandler/account.class.php +++ /dev/null @@ -1,138 +0,0 @@ - [FILTER_SANITIZE_NUMBER_INT, null], - 'save' => [FILTER_SANITIZE_NUMBER_INT, null], - 'delete' => [FILTER_SANITIZE_NUMBER_INT, null], - 'id' => [FILTER_CALLBACK, ['options' => 'AjaxHandler::checkIdList']], - 'name' => [FILTER_CALLBACK, ['options' => 'AjaxAccount::checkName']], - 'scale' => [FILTER_CALLBACK, ['options' => 'AjaxAccount::checkScale']], - 'reset' => [FILTER_SANITIZE_NUMBER_INT, null], - 'mode' => [FILTER_SANITIZE_NUMBER_INT, null], - 'type' => [FILTER_SANITIZE_NUMBER_INT, null], - ); - protected $_get = array( - 'locale' => [FILTER_CALLBACK, ['options' => 'AjaxHandler::checkLocale']] - ); - - public function __construct(array $params) - { - parent::__construct($params); - - if (is_numeric($this->_get['locale'])) - User::useLocale($this->_get['locale']); - - if (!$this->params || !User::$id) - return; - - // select handler - if ($this->params[0] == 'exclude') - $this->handler = 'handleExclude'; - else if ($this->params[0] == 'weightscales') - $this->handler = 'handleWeightscales'; - } - - protected function handleExclude() - { - if (!User::$id) - return; - - if ($this->_post['mode'] == 1) // directly set exludes - { - $type = $this->_post['type']; - $ids = $this->_post['id']; - - if (!isset(Util::$typeStrings[$type]) || empty($ids)) - return; - - // ready for some bullshit? here it comes! - // 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)', $type, $ids); - - foreach ($ids as $typeId) - DB::Aowow()->query('INSERT INTO ?_account_excludes (`userId`, `type`, `typeId`, `mode`) VALUES (?a) ON DUPLICATE KEY UPDATE mode = (mode ^ 0x3)', array( - User::$id, $type, $typeId, in_array($includes, $typeId) ? 2 : 1 - )); - - return; - } - else if ($this->_post['reset'] == 1) // defaults to unavailable - { - $mask = PR_EXCLUDE_GROUP_UNAVAILABLE; - DB::Aowow()->query('DELETE FROM ?_account_excludes WHERE userId = ?d', User::$id); - } - else // clamp to real groups - $mask = $this->_post['groups'] & PR_EXCLUDE_GROUP_ANY; - - DB::Aowow()->query('UPDATE ?_account SET excludeGroups = ?d WHERE id = ?d', $mask, User::$id); - - return; - } - - protected function handleWeightscales() - { - if ($this->_post['save']) - { - if (!$this->_post['scale']) - return 0; - - $id = 0; - - if ($this->_post['id'] && ($id = $this->_post['id'][0])) - { - if (!DB::Aowow()->selectCell('SELECT 1 FROM ?_account_weightscales WHERE userId = ?d AND id = ?d', User::$id, $id)) - return 0; - - DB::Aowow()->query('UPDATE ?_account_weightscales SET `name` = ? WHERE id = ?d', $this->_post['name'], $id); - } - else - { - $nScales = DB::Aowow()->selectCell('SELECT COUNT(id) FROM ?_account_weightscales WHERE userId = ?d', User::$id); - if ($nScales >= 5) // more or less hard-defined in LANG.message_weightscalesaveerror - return 0; - - $id = DB::Aowow()->query('INSERT INTO ?_account_weightscales (`userId`, `name`) VALUES (?d, ?)', User::$id, $this->_post['name']); - } - - DB::Aowow()->query('DELETE FROM ?_account_weightscale_data WHERE id = ?d', $id); - - foreach (explode(',', $this->_post['scale']) as $s) - { - list($k, $v) = explode(':', $s); - if (!in_array($k, Util::$weightScales) || $v < 1) - continue; - - DB::Aowow()->query('INSERT INTO ?_account_weightscale_data VALUES (?d, ?, ?d)', $id, $k, $v); - } - - return $id; - } - else if ($this->_post['delete'] && $this->_post['id'] && $this->_post['id'][0]) - DB::Aowow()->query('DELETE FROM ?_account_weightscales WHERE id = ?d AND userId = ?d', $this->_post['id'][0], User::$id); - else - return 0; - } - - protected function checkScale($val) - { - if (preg_match('/^((\w+:\d+)(,\w+:\d+)*)$/', $val)) - return $val; - - return null; - } - - protected function checkName($val) - { - $var = trim(urldecode($val)); - - return filter_var($var, FILTER_SANITIZE_STRING, FILTER_FLAG_STRIP_LOW); - } -} diff --git a/includes/ajaxHandler/admin.class.php b/includes/ajaxHandler/admin.class.php deleted file mode 100644 index b9edffcb..00000000 --- a/includes/ajaxHandler/admin.class.php +++ /dev/null @@ -1,497 +0,0 @@ - [FILTER_SANITIZE_STRING, 0xC], // FILTER_FLAG_STRIP_LOW | *_HIGH - 'id' => [FILTER_CALLBACK, ['options' => 'AjaxAdmin::checkId']], - 'key' => [FILTER_CALLBACK, ['options' => 'AjaxAdmin::checkKey']], - 'all' => [FILTER_UNSAFE_RAW, null], - 'type' => [FILTER_CALLBACK, ['options' => 'AjaxHandler::checkInt']], - 'typeid' => [FILTER_CALLBACK, ['options' => 'AjaxHandler::checkInt']], - 'user' => [FILTER_CALLBACK, ['options' => 'AjaxAdmin::checkUser']], - 'val' => [FILTER_UNSAFE_RAW, null] - ); - protected $_post = array( - 'alt' => [FILTER_SANITIZE_STRING, FILTER_FLAG_STRIP_LOW], - 'id' => [FILTER_SANITIZE_NUMBER_INT, null], - 'scale' => [FILTER_CALLBACK, ['options' => 'AjaxAdmin::checkScale']], - '__icon' => [FILTER_CALLBACK, ['options' => 'AjaxAdmin::checkKey']], - ); - - public function __construct(array $params) - { - parent::__construct($params); - - // requires 'action' parameter in any case - if (!$this->_get['action'] || !$this->params) - return; - - if ($this->params[0] == 'screenshots') - { - if (!User::isInGroup(U_GROUP_ADMIN | U_GROUP_BUREAU | U_GROUP_SCREENSHOT)) - return; - - if ($this->_get['action'] == 'list') - $this->handler = 'ssList'; - else if ($this->_get['action'] == 'manage') - $this->handler = 'ssManage'; - else if ($this->_get['action'] == 'editalt') - $this->handler = 'ssEditAlt'; - else if ($this->_get['action'] == 'approve') - $this->handler = 'ssApprove'; - else if ($this->_get['action'] == 'sticky') - $this->handler = 'ssSticky'; - else if ($this->_get['action'] == 'delete') - $this->handler = 'ssDelete'; - else if ($this->_get['action'] == 'relocate') - $this->handler = 'ssRelocate'; - } - else if ($this->params[0] == 'siteconfig') - { - if (!User::isInGroup(U_GROUP_DEV | U_GROUP_ADMIN)) - return; - - if ($this->_get['action'] == 'add') - $this->handler = 'confAdd'; - else if ($this->_get['action'] == 'remove') - $this->handler = 'confRemove'; - else if ($this->_get['action'] == 'update') - $this->handler = 'confUpdate'; - } - else if ($this->params[0] == 'weight-presets') - { - if (!User::isInGroup(U_GROUP_DEV | U_GROUP_ADMIN | U_GROUP_BUREAU)) - return; - - if ($this->_get['action'] == 'save') - $this->handler = 'wtSave'; - } - } - - // get all => null (optional) - // evaled response .. UNK - protected function ssList() - { - // ssm_screenshotPages - // ssm_numPagesFound - - $pages = CommunityContent::getScreenshotPagesForManager($this->_get['all'], $nPages); - $buff = 'ssm_screenshotPages = '.Util::toJSON($pages).";\n"; - $buff .= 'ssm_numPagesFound = '.$nPages.';'; - - return $buff; - } - - // get: [type => type, typeId => typeId] || [user => username] - // evaled response .. UNK - protected function ssManage() - { - $res = []; - - if ($this->_get['type'] && $this->_get['type'] && $this->_get['typeid'] && $this->_get['typeid']) - $res = CommunityContent::getScreenshotsForManager($this->_get['type'], $this->_get['typeid']); - else if ($this->_get['user']) - if ($uId = DB::Aowow()->selectCell('SELECT id FROM ?_account WHERE displayName = ?', $this->_get['user'])) - $res = CommunityContent::getScreenshotsForManager(0, 0, $uId); - - return 'ssm_screenshotData = '.Util::toJSON($res); - } - - // get: id => SSid - // resp: '' - protected function ssEditAlt() - { - // doesn't need to be htmlEscaped, ths javascript does that - if ($this->_get['id'] && $this->_post['alt'] !== null) - DB::Aowow()->query('UPDATE ?_screenshots SET caption = ? WHERE id = ?d', trim($this->_post['alt']), $this->_get['id'][0]); - - return ''; - } - - // get: id => comma-separated SSids - // resp: '' - protected function ssApprove() - { - if (!$this->_get['id']) - return ''; - - // create resized and thumb version of screenshot - $resized = [772, 618]; - $thumb = [150, 150]; - $path = 'static/uploads/screenshots/%s/%d.jpg'; - - foreach ($this->_get['id'] as $id) - { - // must not be already approved - if ($_ = DB::Aowow()->selectRow('SELECT userIdOwner, date, type, typeId FROM ?_screenshots WHERE (status & ?d) = 0 AND id = ?d', CC_FLAG_APPROVED, $id)) - { - // should also error-log - if (!file_exists(sprintf($path, 'pending', $id))) - continue; - - $srcImg = imagecreatefromjpeg(sprintf($path, 'pending', $id)); - $srcW = imagesx($srcImg); - $srcH = imagesy($srcImg); - - // write thumb - $scale = min(1.0, min($thumb[0] / $srcW, $thumb[1] / $srcH)); - $destW = $srcW * $scale; - $destH = $srcH * $scale; - $destImg = imagecreatetruecolor($destW, $destH); - - imagefill($destImg, 0, 0, imagecolorallocate($destImg, 255, 255, 255)); - imagecopyresampled($destImg, $srcImg, 0, 0, 0, 0, $destW, $destH, $srcW, $srcH); - - imagejpeg($destImg, sprintf($path, 'thumb', $id), 100); - - // write resized (only if required) - if ($srcW > $resized[0] || $srcH > $resized[1]) - { - $scale = min(1.0, min($resized[0] / $srcW, $resized[1] / $srcH)); - $destW = $srcW * $scale; - $destH = $srcH * $scale; - $destImg = imagecreatetruecolor($destW, $destH); - - imagefill($destImg, 0, 0, imagecolorallocate($destImg, 255, 255, 255)); - imagecopyresampled($destImg, $srcImg, 0, 0, 0, 0, $destW, $destH, $srcW, $srcH); - - imagejpeg($destImg, sprintf($path, 'resized', $id), 100); - } - - imagedestroy($srcImg); - - // move screenshot from pending to normal - rename(sprintf($path, 'pending', $id), sprintf($path, 'normal', $id)); - - // set as approved in DB and gain rep (once!) - DB::Aowow()->query('UPDATE ?_screenshots SET status = ?d, userIdApprove = ?d WHERE id = ?d', CC_FLAG_APPROVED, User::$id, $id); - Util::gainSiteReputation($_['userIdOwner'], SITEREP_ACTION_UPLOAD, ['id' => $id, 'what' => 1, 'date' => $_['date']]); - // flag DB entry as having screenshots - if (Util::$typeClasses[$_['type']] && ($tbl = get_class_vars(Util::$typeClasses[$_['type']])['dataTable'])) - DB::Aowow()->query('UPDATE '.$tbl.' SET cuFlags = cuFlags | ?d WHERE id = ?d', CUSTOM_HAS_SCREENSHOT, $_['typeId']); - } - } - - return ''; - } - - // get: id => comma-separated SSids - // resp: '' - protected function ssSticky() - { - if (!$this->_get['id']) - return ''; - - // approve soon to be sticky screenshots - $this->ssApprove(); - - // 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 - foreach ($this->_get['id'] as $id) - { - // 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); - - // 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); - } - - return ''; - } - - // get: id => comma-separated SSids - // resp: '' - // 2 steps: 1) remove from sight, 2) remove from disk - protected function ssDelete() - { - if (!$this->_get['id']) - return ''; - - $path = 'static/uploads/screenshots/%s/%d.jpg'; - - foreach ($this->_get['id'] as $id) - { - // irrevocably remove already deleted files - if (User::isInGroup(U_GROUP_ADMIN) && DB::Aowow()->selectCell('SELECT 1 FROM ?_screenshots WHERE status & ?d AND id = ?d', CC_FLAG_DELETED, $id)) - { - DB::Aowow()->query('DELETE FROM ?_screenshots WHERE id = ?d', $id); - if (file_exists(sprintf($path, 'pending', $id))) - unlink(sprintf($path, 'pending', $id)); - - continue; - } - - // move pending or normal to pending - if (file_exists(sprintf($path, 'normal', $id))) - rename(sprintf($path, 'normal', $id), sprintf($path, 'pending', $id)); - - // remove resized and thumb - if (file_exists(sprintf($path, 'thumb', $id))) - unlink(sprintf($path, 'thumb', $id)); - - if (file_exists(sprintf($path, 'resized', $id))) - unlink(sprintf($path, 'resized', $id)); - } - - // 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']); - // 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); - if ($toUnflag && Util::$typeClasses[$type] && ($tbl = get_class_vars(Util::$typeClasses[$type])['dataTable'])) - DB::Aowow()->query('UPDATE '.$tbl.' SET cuFlags = cuFlags & ~?d WHERE id IN (?a)', CUSTOM_HAS_SCREENSHOT, array_keys($toUnflag)); - } - - return ''; - } - - // get: id => ssId, typeid => typeId (but not type..?) - // resp: '' - protected function ssRelocate() - { - if (!$this->_get['id'] || !$this->_get['typeid']) - return ''; - - $id = $this->_get['id'][0]; - list($type, $oldTypeId) = array_values(DB::Aowow()->selectRow('SELECT type, typeId FROM ?_screenshots WHERE id = ?d', $id)); - $typeId = (int)$this->_get['typeid']; - - $tc = new Util::$typeClasses[$type]([['id', $typeId]]); - if (!$tc->error) - { - // move screenshot - DB::Aowow()->query('UPDATE ?_screenshots SET typeId = ?d WHERE id = ?d', $typeId, $id); - - // flag target as having screenshot - DB::Aowow()->query('UPDATE '.$tc::$dataTable.' SET cuFlags = cuFlags | ?d WHERE id = ?d', 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); - if($ssInfo || !$ssInfo['hasMore']) - DB::Aowow()->query('UPDATE '.$tc::$dataTable.' SET cuFlags = cuFlags & ~?d WHERE id = ?d', CUSTOM_HAS_SCREENSHOT, $oldTypeId); - } - - return ''; - } - - protected function confAdd() - { - $key = trim($this->_get['key']); - $val = trim(urldecode($this->_get['val'])); - - if ($key === null) - return 'empty option name given'; - - if (!strlen($key)) - return 'invalid chars in option name: [a-z 0-9 _ . -] are allowed'; - - if (ini_get($key) === false || ini_set($key, $val) === false) - return 'this configuration option cannot be set'; - - if (DB::Aowow()->selectCell('SELECT 1 FROM ?_config WHERE `flags` & ?d AND `key` = ?', CON_FLAG_PHP, $key)) - return 'this configuration option is already in use'; - - DB::Aowow()->query('INSERT IGNORE INTO ?_config (`key`, `value`, `cat`, `flags`) VALUES (?, ?, 0, ?d)', $key, $val, CON_FLAG_TYPE_STRING | CON_FLAG_PHP); - return ''; - } - - protected function confRemove() - { - if (!$this->_get['key']) - return 'invalid configuration option given'; - - if (DB::Aowow()->query('DELETE FROM ?_config WHERE `key` = ? AND (`flags` & ?d) = 0', $this->_get['key'], CON_FLAG_PERSISTENT)) - return ''; - else - return 'option name is either protected or was not found'; - } - - protected function confUpdate() - { - $key = trim($this->_get['key']); - $val = trim(urldecode($this->_get['val'])); - $msg = ''; - - if (!strlen($key)) - return 'empty option name given'; - - $cfg = DB::Aowow()->selectRow('SELECT `flags`, `value` FROM ?_config WHERE `key` = ?', $key); - if (!$cfg) - return 'configuration option not found'; - - if (!($cfg['flags'] & CON_FLAG_TYPE_STRING) && !strlen($val)) - return 'empty value given'; - else if ($cfg['flags'] & CON_FLAG_TYPE_INT && !preg_match('/^-?\d+$/i', $val)) - return "value must be integer"; - else if ($cfg['flags'] & CON_FLAG_TYPE_FLOAT && !preg_match('/^-?\d*(,|.)?\d+$/i', $val)) - return "value must be float"; - else if ($cfg['flags'] & CON_FLAG_TYPE_BOOL) - $val = (int)!!$val; // *snort* bwahahaa - - DB::Aowow()->query('UPDATE ?_config SET `value` = ? WHERE `key` = ?', $val, $key); - if (!$this->confOnChange($key, $val, $msg)) - DB::Aowow()->query('UPDATE ?_config SET `value` = ? WHERE `key` = ?', $cfg['value'], $key); - - return $msg; - } - - protected function wtSave() - { - if (!$this->_post['id'] || !$this->_post['__icon']) - return 3; - - $writeFile = function($file, $content) - { - $success = false; - if ($handle = @fOpen($file, "w")) - { - if (fWrite($handle, $content)) - $success = true; - - fClose($handle); - } - else - die('me no file'); - - if ($success) - @chmod($file, Util::FILE_ACCESS); - - return $success; - }; - - - // 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']); - - foreach (explode(',', $this->_post['scale']) as $s) - { - list($k, $v) = explode(':', $s); - - if (!in_array($k, Util::$weightScales) || $v < 1) - continue; - - if (DB::Aowow()->query('INSERT INTO ?_account_weightscale_data VALUES (?d, ?, ?d)', $this->_post['id'], $k, $v) === null) - return 1; - } - - - // write dataset - - $wtPresets = []; - $scales = DB::Aowow()->select('SELECT id, name, icon, class FROM ?_account_weightscales WHERE userId = 0 ORDER BY class, id ASC'); - - foreach ($scales as $s) - { - $weights = DB::Aowow()->selectCol('SELECT field AS ARRAY_KEY, val FROM ?_account_weightscale_data WHERE id = ?d', $s['id']); - if (!$weights) - continue; - - $wtPresets[$s['class']]['pve'][$s['name']] = array_merge(['__icon' => $s['icon']], $weights); - } - - $toFile = "var wt_presets = ".Util::toJSON($wtPresets).";"; - $file = 'datasets/weight-presets'; - - if (!$writeFile($file, $toFile)) - return 2; - - - // all done - - return 0; - } - - protected function checkId($val) - { - // expecting id-list - if (preg_match('/\d+(,\d+)*/', $val)) - return array_map('intVal', explode(',', $val)); - - return null; - } - - protected function checkKey($val) - { - // expecting string - if (preg_match('/[^a-z0-9_\.\-]/i', $val)) - return ''; - - return strtolower($val); - } - - protected function checkUser($val) - { - $n = Util::lower(trim(urldecode($val))); - - if (User::isValidName($n)) - return $n; - - return null; - } - - protected function checkScale($val) - { - if (preg_match('/^((\w+:\d+)(,\w+:\d+)*)$/', $val)) - return $val; - - return null; - } - - private function confOnChange($key, $val, &$msg) - { - $fn = $buildList = null; - - switch ($key) - { - case 'battlegroup': - $buildList = 'realms,realmMenu'; - break; - case 'name_short': - $buildList = 'searchboxBody,demo,searchplugin'; - break; - case 'site_host': - $buildList = 'searchplugin,demo,power,searchboxBody'; - break; - case 'static_host': - $buildList = 'searchplugin,power,searchboxBody,searchboxScript'; - break; - case 'locales': - $buildList = 'locales'; - $msg .= ' * remember to rebuild all static files for the language you just added.
'; - $msg .= ' * you can speed this up by supplying the regionCode to the setup:
--locales= -f
'; - break; - case 'profiler_queue': - $fn = function($x) use (&$msg) { - if (!$x) - return true; - - return Profiler::queueStart($msg); - }; - break; - default: // nothing to do, everything is fine - return true; - } - - if ($buildList) - { - // we need to use exec as build() can only be run from CLI - exec('php aowow --build='.$buildList, $out); - foreach ($out as $o) - if (strstr($o, 'ERR')) - $msg .= explode('0m]', $o)[1]."
\n"; - } - - return $fn ? $fn($val) : true; - } -} diff --git a/includes/ajaxHandler/arenateam.class.php b/includes/ajaxHandler/arenateam.class.php deleted file mode 100644 index fdffe473..00000000 --- a/includes/ajaxHandler/arenateam.class.php +++ /dev/null @@ -1,82 +0,0 @@ - [FILTER_CALLBACK, ['options' => 'AjaxHandler::checkIdList']], - 'profile' => [FILTER_CALLBACK, ['options' => 'AjaxHandler::checkEmptySet']], - ); - - public function __construct(array $params) - { - parent::__construct($params); - - if (!$this->params) - return; - - switch ($this->params[0]) - { - case 'resync': - $this->handler = 'handleResync'; - break; - case 'status': - $this->handler = 'handleStatus'; - break; - } - } - - /* params - id: - user: [optional, not used] - profile: [optional, also get related chars] - return: 1 - */ - protected function handleResync() - { - if ($teams = DB::Aowow()->select('SELECT realm, realmGUID FROM ?_profiler_arena_team WHERE id IN (?a)', $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'])) - foreach ($chars as $c) - Profiler::scheduleResync(TYPE_PROFILE, $c['realm'], $c['realmGUID']); - - return '1'; - } - - /* params - id: - return - - [ - nQueueProcesses, - [statusCode, timeToRefresh, curQueuePos, errorCode, nResyncTries], - [] - ... - ] - - not all fields are required, if zero they are omitted - statusCode: - 0: end the request - 1: waiting - 2: working... - 3: ready; click to view - 4: error / retry - errorCode: - 0: unk error - 1: char does not exist - 2: armory gone - */ - protected function handleStatus() - { - $response = Profiler::resyncStatus(TYPE_ARENA_TEAM, $this->_get['id']); - return Util::toJSON($response); - } -} - -?> diff --git a/includes/ajaxHandler/comment.class.php b/includes/ajaxHandler/comment.class.php deleted file mode 100644 index 35c22dd1..00000000 --- a/includes/ajaxHandler/comment.class.php +++ /dev/null @@ -1,410 +0,0 @@ - [FILTER_CALLBACK, ['options' => 'AjaxComment::checkId']], - 'body' => [FILTER_UNSAFE_RAW, null],// escaped by json_encode - 'commentbody' => [FILTER_UNSAFE_RAW, null],// escaped by json_encode - 'response' => [FILTER_SANITIZE_STRING, FILTER_FLAG_STRIP_LOW], - 'reason' => [FILTER_SANITIZE_STRING, FILTER_FLAG_STRIP_LOW], - 'remove' => [FILTER_SANITIZE_NUMBER_INT, null], - 'commentId' => [FILTER_SANITIZE_NUMBER_INT, null], - 'replyId' => [FILTER_SANITIZE_NUMBER_INT, null], - 'sticky' => [FILTER_SANITIZE_NUMBER_INT, null], - // 'username' => [FILTER_SANITIZE_STRING, 0xC] // FILTER_FLAG_STRIP_LOW | *_HIGH - ); - - protected $_get = array( - 'id' => [FILTER_CALLBACK, ['options' => 'AjaxHandler::checkInt']], - 'type' => [FILTER_CALLBACK, ['options' => 'AjaxHandler::checkInt']], - 'typeid' => [FILTER_CALLBACK, ['options' => 'AjaxHandler::checkInt']], - 'rating' => [FILTER_SANITIZE_NUMBER_INT, null] - ); - - public function __construct(array $params) - { - parent::__construct($params); - - if (!$this->params || count($this->params) != 1) - return; - - // note: return values must be formated as STRICT json! - - // select handler - if ($this->params[0] == 'add') - $this->handler = 'handleCommentAdd'; - else if ($this->params[0] == 'edit') - $this->handler = 'handleCommentEdit'; - else if ($this->params[0] == 'delete') - $this->handler = 'handleCommentDelete'; - else if ($this->params[0] == 'undelete') - $this->handler = 'handleCommentUndelete'; - else if ($this->params[0] == 'rating') // up/down - distribution - $this->handler = 'handleCommentRating'; - else if ($this->params[0] == 'vote') // up, down and remove - $this->handler = 'handleCommentVote'; - else if ($this->params[0] == 'sticky') // toggle flag - $this->handler = 'handleCommentSticky'; - else if ($this->params[0] == 'out-of-date') // toggle flag - $this->handler = 'handleCommentOutOfDate'; - else if ($this->params[0] == 'show-replies') - $this->handler = 'handleCommentShowReplies'; - else if ($this->params[0] == 'add-reply') // also returns all replies on success - $this->handler = 'handleReplyAdd'; - else if ($this->params[0] == 'edit-reply') // also returns all replies on success - $this->handler = 'handleReplyEdit'; - else if ($this->params[0] == 'detach-reply') - $this->handler = 'handleReplyDetach'; - else if ($this->params[0] == 'delete-reply') - $this->handler = 'handleReplyDelete'; - else if ($this->params[0] == 'flag-reply') - $this->handler = 'handleReplyFlag'; - else if ($this->params[0] == 'upvote-reply') - $this->handler = 'handleReplyUpvote'; - else if ($this->params[0] == 'downvote-reply') - $this->handler = 'handleReplyDownvote'; - } - - // i .. have problems believing, that everything uses nifty ajax while adding comments requires a brutal header(Loacation: ), yet, thats how it is - protected function handleCommentAdd() - { - if (!$this->_get['typeid'] || !$this->_get['type'] || !isset(Util::$typeClasses[$this->_get['type']])) - return; // whatever, we cant even send him back - - // this type cannot be commented on - if (!(get_class_vars(Util::$typeClasses[$this->_get['type']])['contribute'] & CONTRIBUTE_CO)) - return; - - // trim to max length - if (!User::isInGroup(U_GROUP_MODERATOR) && mb_strlen($this->_post['commentbody']) > (self::COMMENT_LENGTH_MAX * (User::isPremium() ? 3 : 1))) - $this->post['commentbody'] = mb_substr($this->_post['commentbody'], 0, (self::COMMENT_LENGTH_MAX * (User::isPremium() ? 3 : 1))); - - if (User::canComment() && !empty($this->_post['commentbody']) && mb_strlen($this->_post['commentbody']) >= self::COMMENT_LENGTH_MIN) - { - if ($postIdx = 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'])) - { - Util::gainSiteReputation(User::$id, SITEREP_ACTION_COMMENT, ['id' => $postIdx]); - - // 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 ?_comments_rates (commentId, userId, value) VALUES (?d, 0, 1)', $postIdx); - - // flag target with hasComment - if ($tbl = get_class_vars(Util::$typeClasses[$this->_get['type']])['dataTable']) - DB::Aowow()->query('UPDATE '.$tbl.' SET cuFlags = cuFlags | ?d WHERE id = ?d', CUSTOM_HAS_COMMENT, $this->_get['typeid']); - } - } - - $this->doRedirect = true; - return '?'.Util::$typeStrings[$this->_get['type']].'='.$this->_get['typeid'].'#comments'; - } - - protected function handleCommentEdit() - { - if ((!User::canComment() && !User::isInGroup(U_GROUP_MODERATOR)) || !$this->_get['id'] || !$this->_post['body']) - return; - - if (mb_strlen($this->_post['body']) < self::COMMENT_LENGTH_MIN) - return; - - // trim to max length - if (!User::isInGroup(U_GROUP_MODERATOR) && mb_strlen($this->_post['body']) > (self::COMMENT_LENGTH_MAX * (User::isPremium() ? 3 : 1))) - $this->post['body'] = mb_substr($this->_post['body'], 0, (self::COMMENT_LENGTH_MAX * (User::isPremium() ? 3 : 1))); - - $update = array( - 'body' => $this->_post['body'], - 'editUserId' => User::$id, - 'editDate' => time() - ); - - if (User::isInGroup(U_GROUP_MODERATOR)) - { - $update['responseBody'] = !$this->_post['response'] ? '' : $this->_post['response']; - $update['responseUserId'] = !$this->_post['response'] ? 0 : User::$id; - $update['responseRoles'] = !$this->_post['response'] ? 0 : User::$groups; - } - - DB::Aowow()->query('UPDATE ?_comments SET editCount = editCount + 1, ?a WHERE id = ?d', $update, $this->_get['id']); - } - - protected function handleCommentDelete() - { - if (!$this->_post['id'] || !User::$id) - return; - - // in theory, there is a username passed alongside... 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 - ); - - // deflag hasComment - if ($ok) - { - $coInfo = DB::Aowow()->selectRow('SELECT IF(BIT_OR(~b.flags) & ?d, 1, 0) as hasMore, b.type, b.typeId FROM ?_comments a JOIN ?_comments b ON a.type = b.type AND a.typeId = b.typeId WHERE a.id = ?d', - CC_FLAG_DELETED, - $this->_post['id'] - ); - - if (!$coInfo['hasMore'] && Util::$typeClasses[$coInfo['type']] && ($tbl = get_class_vars(Util::$typeClasses[$coInfo['type']])['dataTable'])) - DB::Aowow()->query('UPDATE '.$tbl.' SET cuFlags = cuFlags & ~?d WHERE id = ?d', CUSTOM_HAS_COMMENT, $coInfo['typeId']); - } - } - - protected function handleCommentUndelete() - { - if (!$this->_post['id'] || !User::$id) - return; - - // in theory, there is a username passed alongside... 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 - ); - - // reflag hasComment - if ($ok) - { - $coInfo = DB::Aowow()->selectRow('SELECT type, typeId FROM ?_comments WHERE id = ?d', $this->_post['id']); - if (Util::$typeClasses[$coInfo['type']] && ($tbl = get_class_vars(Util::$typeClasses[$coInfo['type']])['dataTable'])) - DB::Aowow()->query('UPDATE '.$tbl.' SET cuFlags = cuFlags | ?d WHERE id = ?d', CUSTOM_HAS_COMMENT, $coInfo['typeId']); - } - } - - protected function handleCommentRating() - { - if (!$this->_get['id']) - return Util::toJSON(['success' => 0]); - - 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 ?_comments_rates WHERE commentId = ?d and userId <> 0 GROUP BY commentId', $this->_get['id'])) - return Util::toJSON($votes); - else - return Util::toJSON(['success' => 1, 'up' => 0, 'down' => 0]); - } - - protected function handleCommentVote() - { - if (!User::$id || !$this->_get['id'] || !$this->_get['rating']) - return Util::toJSON(['error' => 1, 'message' => Lang::main('genericError')]); - - $target = DB::Aowow()->selectRow('SELECT c.userId AS owner, cr.value FROM ?_comments c LEFT JOIN ?_comments_rates cr ON cr.commentId = c.id AND cr.userId = ?d WHERE c.id = ?d', User::$id, $this->_get['id']); - $val = User::canSupervote() ? 2 : 1; - if ($this->_get['rating'] < 0) - $val *= -1; - - if (User::getCurDailyVotes() <= 0) - return Util::toJSON(['error' => 1, 'message' => Lang::main('tooManyVotes')]); - else if (!$target || $val != $this->_get['rating']) - return Util::toJSON(['error' => 1, 'message' => Lang::main('genericError')]); - else if (($val > 0 && !User::canUpvote()) || ($val < 0 && !User::canDownvote())) - return Util::toJSON(['error' => 1, 'message' => Lang::main('bannedRating')]); - - $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 ?_comments_rates WHERE commentId = ?d AND userId = ?d', $this->_get['id'], User::$id); - else // replace, because we may be overwriting an old, opposing vote - if ($ok = DB::Aowow()->query('REPLACE INTO ?_comments_rates (commentId, userId, value) VALUES (?d, ?d, ?d)', (int)$this->_get['id'], User::$id, $val)) - User::decrementDailyVotes(); // do not refund retracted votes! - - if (!$ok) - return Util::toJSON(['error' => 1, 'message' => Lang::main('genericError')]); - - if ($val > 0) // gain rep - Util::gainSiteReputation($target['owner'], SITEREP_ACTION_UPVOTED, ['id' => $this->_get['id'], 'voterId' => User::$id]); - else if ($val < 0) - Util::gainSiteReputation($target['owner'], SITEREP_ACTION_DOWNVOTED, ['id' => $this->_get['id'], 'voterId' => User::$id]); - - return Util::toJSON(['error' => 0]); - } - - protected function handleCommentSticky() - { - if (!$this->_post['id'] || !User::isInGroup(U_GROUP_MODERATOR)) - return; - - if ($this->_post['sticky']) - DB::Aowow()->query('UPDATE ?_comments SET flags = flags | ?d WHERE id = ?d', CC_FLAG_STICKY, $this->_post['id'][0]); - else - DB::Aowow()->query('UPDATE ?_comments SET flags = flags & ~?d WHERE id = ?d', CC_FLAG_STICKY, $this->_post['id'][0]); - } - - protected function handleCommentOutOfDate() - { - $this->contentType = 'text/plain'; - - if (!$this->_post['id']) - return 'The comment does not exist.'; - - $ok = false; - if (User::isInGroup(U_GROUP_MODERATOR)) // directly mark as outdated - { - if (!$this->_post['remove']) - $ok = DB::Aowow()->query('UPDATE ?_comments SET flags = flags | 0x4 WHERE id = ?d', $this->_post['id'][0]); - else - $ok = DB::Aowow()->query('UPDATE ?_comments SET flags = flags & ~0x4 WHERE id = ?d', $this->_post['id'][0]); - } - else if (User::$id && !$this->_post['reason'] || mb_strlen($this->_post['reason']) < self::REPLY_LENGTH_MIN) - return 'Your message is too short.'; - else if (User::$id) // only report as outdated - { - $ok = DB::Aowow()->query( - 'INSERT INTO ?_reports (userId, mode, reason, subject, ip, description, userAgent, appName) VALUES (?d, 1, 17, ?d, ?, "", ?, ?)', - User::$id, - $this->_post['id'][0], - User::$ip, - $_SERVER['HTTP_USER_AGENT'], - get_browser(null, true)['browser'] - ); - } - - if ($ok) // this one is very special; as in: completely retarded - return 'ok'; // the script expects the actual characters 'ok' not some string like "ok" - - return Lang::main('genericError'); - } - - protected function handleCommentShowReplies() - { - return Util::toJSON(!$this->_get['id'] ? [] : CommunityContent::getCommentReplies($this->_get['id'])); - } - - protected function handleReplyAdd() - { - $this->contentType = 'text/plain'; - - if (!User::canComment()) - return 'You are not allowed to reply.'; - - else if (!$this->_post['commentId'] || !DB::Aowow()->selectCell('SELECT 1 FROM ?_comments WHERE id = ?d', $this->_post['commentId'])) - return Lang::main('genericError'); - - else if (!$this->_post['body'] || mb_strlen($this->_post['body']) < self::REPLY_LENGTH_MIN || mb_strlen($this->_post['body']) > self::REPLY_LENGTH_MAX) - return 'Your reply has '.mb_strlen($this->_post['body']).' characters and must have at least '.self::REPLY_LENGTH_MIN.' and at most '.self::REPLY_LENGTH_MAX.'.'; - - else 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'])) - return Util::toJSON(CommunityContent::getCommentReplies($this->_post['commentId'])); - - else - return Lang::main('genericError'); - } - - protected function handleReplyEdit() - { - $this->contentType = 'text/plain'; - - if (!User::canComment()) - return 'You are not allowed to reply.'; - - else if (!$this->_post['replyId'] || !$this->_post['commentId']) - return Lang::main('genericError'); - - else if (!$this->_post['body'] || mb_strlen($this->_post['body']) < self::REPLY_LENGTH_MIN || mb_strlen($this->_post['body']) > self::REPLY_LENGTH_MAX) - return 'Your reply has '.mb_strlen($this->_post['body']).' characters and must have at least '.self::REPLY_LENGTH_MIN.' and at most '.self::REPLY_LENGTH_MAX.'.'; - - if (DB::Aowow()->query('UPDATE ?_comments SET body = ?, editUserId = ?d, editDate = UNIX_TIMESTAMP(), editCount = editCount + 1 WHERE id = ?d AND replyTo = ?d{ AND userId = ?d}', - $this->_post['body'], User::$id, $this->_post['replyId'], $this->_post['commentId'], User::isInGroup(U_GROUP_MODERATOR) ? DBSIMPLE_SKIP : User::$id)) - return Util::toJSON(CommunityContent::getCommentReplies($this->_post['commentId'])); - else - return Lang::main('genericError'); - } - - protected function handleReplyDetach() - { - if (!User::isInGroup(U_GROUP_MODERATOR) || !$this->_post['id']) - return; - - 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'][0]); - } - - protected function handleReplyDelete() - { - if (!User::$id || !$this->_post['id']) - return; - - if (DB::Aowow()->query('DELETE FROM ?_comments WHERE id = ?d{ AND userId = ?d}', $this->_post['id'][0], User::isInGroup(U_GROUP_MODERATOR) ? DBSIMPLE_SKIP : User::$id)) - DB::Aowow()->query('DELETE FROM ?_comments_rates WHERE commentId = ?d', $this->_post['id'][0]); - } - - protected function handleReplyFlag() - { - if (!User::$id || !$this->_post['id']) - return; - - DB::Aowow()->query( - 'INSERT INTO ?_reports (userId, mode, reason, subject, ip, description, userAgent, appName) VALUES (?d, 1, 19, ?d, ?, "", ?, ?)', - User::$id, - $this->_post['id'][0], - User::$ip, - $_SERVER['HTTP_USER_AGENT'], - get_browser(null, true)['browser'] - ); - } - - protected function handleReplyUpvote() - { - if (!$this->_post['id'] || !User::canUpvote()) - return; - - $owner = DB::Aowow()->selectCell('SELECT userId FROM ?_comments WHERE id = ?d', $this->_post['id'][0]); - if (!$owner) - return; - - $ok = DB::Aowow()->query( - 'INSERT INTO ?_comments_rates (commentId, userId, value) VALUES (?d, ?d, ?d)', - $this->_post['id'][0], - User::$id, - User::canSupervote() ? 2 : 1 - ); - - if ($ok) - { - Util::gainSiteReputation($owner, SITEREP_ACTION_UPVOTED, ['id' => $this->_post['id'][0], 'voterId' => User::$id]); - User::decrementDailyVotes(); - } - } - - protected function handleReplyDownvote() - { - if (!$this->_post['id'] || !User::canDownvote()) - return; - - $owner = DB::Aowow()->selectCell('SELECT userId FROM ?_comments WHERE id = ?d', $this->_post['id'][0]); - if (!$owner) - return; - - $ok = DB::Aowow()->query( - 'INSERT INTO ?_comments_rates (commentId, userId, value) VALUES (?d, ?d, ?d)', - $this->_post['id'][0], - User::$id, - User::canSupervote() ? -2 : -1 - ); - - if ($ok) - { - Util::gainSiteReputation($owner, SITEREP_ACTION_DOWNVOTED, ['id' => $this->_post['id'][0], 'voterId' => User::$id]); - User::decrementDailyVotes(); - } - } - - protected function checkId($val) - { - // expecting id-list - if (preg_match('/\d+(,\d+)*/', $val)) - return array_map('intVal', explode(',', $val)); - - return null; - } -} -?> diff --git a/includes/ajaxHandler/contactus.class.php b/includes/ajaxHandler/contactus.class.php deleted file mode 100644 index 1a2a17d6..00000000 --- a/includes/ajaxHandler/contactus.class.php +++ /dev/null @@ -1,100 +0,0 @@ - [FILTER_SANITIZE_NUMBER_INT, null], - 'reason' => [FILTER_SANITIZE_NUMBER_INT, null], - 'ua' => [FILTER_SANITIZE_STRING, FILTER_FLAG_STRIP_LOW], - 'appname' => [FILTER_SANITIZE_STRING, FILTER_FLAG_STRIP_LOW], - 'page' => [FILTER_SANITIZE_URL, null], - 'desc' => [FILTER_SANITIZE_STRING, FILTER_FLAG_STRIP_LOW], - 'id' => [FILTER_SANITIZE_NUMBER_INT, null], - 'relatedurl' => [FILTER_SANITIZE_URL, null], - 'email' => [FILTER_SANITIZE_EMAIL, null] - ); - - public function __construct(array $params) - { - parent::__construct($params); - - // always this one - $this->handler = 'handleContactUs'; - } - - /* responses - 0: success - 1: captcha invalid - 2: description too long - 3: reason missing - 7: already reported - $: prints response - */ - protected function handleContactUs() - { - $mode = $this->_post['mode']; - $rsn = $this->_post['reason']; - $ua = $this->_post['ua']; - $app = $this->_post['appname']; - $url = $this->_post['page']; - $desc = $this->_post['desc']; - - $contexts = array( - [1, 2, 3, 4, 5, 6, 7, 8], - [15, 16, 17, 18, 19, 20], - [30, 31, 32, 33, 34, 35, 36, 37], - [45, 46, 47, 48], - [60, 61], - [45, 46, 47, 48], - [45, 46, 48] - ); - - if ($mode === null || $rsn === null || $ua === null || $app === null || $url === null) - return 'required field missing'; - - if (!isset($contexts[$mode]) || !in_array($rsn, $contexts[$mode])) - return 'mode invalid'; - - if (!$desc) - return 3; - - if (mb_strlen($desc) > 500) - return 2; - - if (!User::$id && !User::$ip) - return 'your ip could not be determined'; - - // check already reported - $field = User::$id ? 'userId' : 'ip'; - if (DB::Aowow()->selectCell('SELECT 1 FROM ?_reports WHERE `mode` = ?d AND `reason`= ?d AND `subject` = ?d AND ?# = ?', $mode, $rsn, $this->_post['id'], $field, User::$id ?: User::$ip)) - return 7; - - $update = array( - 'userId' => User::$id, - 'mode' => $mode, - 'reason' => $rsn, - 'ip' => User::$ip, - 'description' => $desc, - 'userAgent' => $ua, - 'appName' => $app, - 'url' => $url - ); - - if ($_ = $this->_post['id']) - $update['subject'] = $_; - - if ($_ = $this->_post['relatedurl']) - $update['relatedurl'] = $_; - - if ($_ = $this->_post['email']) - $update['email'] = $_; - - if (DB::Aowow()->query('INSERT INTO ?_reports (?#) VALUES (?a)', array_keys($update), array_values($update))) - return 0; - - return 'save to db unsuccessful'; - } -} \ No newline at end of file diff --git a/includes/ajaxHandler/cookie.class.php b/includes/ajaxHandler/cookie.class.php deleted file mode 100644 index a1ac8b81..00000000 --- a/includes/ajaxHandler/cookie.class.php +++ /dev/null @@ -1,37 +0,0 @@ -_get = array( - $this->params[0] => [FILTER_SANITIZE_STRING, 0xC], // FILTER_FLAG_STRIP_LOW | *_HIGH - ); - - // NOW we know, what to expect and sanitize - parent::__construct($params); - - // always this one - $this->handler = 'handleCookie'; - } - - /* responses - 0: success - $: silent error - */ - protected function handleCookie() - { - if (User::$id && $this->params && $this->_get[$this->params[0]]) - if (DB::Aowow()->query('REPLACE INTO ?_account_cookies VALUES (?d, ?, ?)', User::$id, $this->params[0], $this->_get[$this->params[0]])) - return 0; - - return null; - } -} \ No newline at end of file diff --git a/includes/ajaxHandler/data.class.php b/includes/ajaxHandler/data.class.php deleted file mode 100644 index cd1c666d..00000000 --- a/includes/ajaxHandler/data.class.php +++ /dev/null @@ -1,139 +0,0 @@ - [FILTER_CALLBACK, ['options' => 'AjaxHandler::checkLocale']], - 't' => [FILTER_SANITIZE_STRING, 0xC], // FILTER_FLAG_STRIP_LOW | *_HIGH - 'catg' => [FILTER_SANITIZE_NUMBER_INT, null], - 'skill' => [FILTER_CALLBACK, ['options' => 'AjaxData::checkSkill']], - 'class' => [FILTER_SANITIZE_NUMBER_INT, null], - 'callback' => [FILTER_CALLBACK, ['options' => 'AjaxData::checkCallback']] - ); - - public function __construct(array $params) - { - parent::__construct($params); - - if (is_numeric($this->_get['locale'])) - User::useLocale($this->_get['locale']); - - // always this one - $this->handler = 'handleData'; - } - - /* responses - - */ - protected function handleData() - { - $result = ''; - - // different data can be strung together - foreach ($this->params as $set) - { - // requires valid token to hinder automated access - if ($set != 'item-scaling') - if (!$this->_get['t'] || empty($_SESSION['dataKey']) || $this->_get['t'] != $_SESSION['dataKey']) - continue; - - switch ($set) - { - /* issue on no initial data: - when we loadOnDemand, the jScript tries to generate the catg-tree before it is initialized - it cant be initialized, without loading the data as empty catg are omitted - loading the data triggers the generation of the catg-tree - */ - case 'factions': - $result .= $this->loadProfilerData($set); - break; - case 'companions': - $result .= $this->loadProfilerData($set, '778'); - break; - case 'mounts': - $result .= $this->loadProfilerData($set, '777'); - break; - case 'quests': - // &partial: im not doing this right - // it expects a full quest dump on first lookup but will query subCats again if clicked..? - // for now omiting the detail clicks with empty results and just set catg update - $catg = isset($this->_get['catg']) ? $this->_get['catg'] : 'null'; - if ($catg == 'null') - $result .= $this->loadProfilerData($set); - else if ($this->_get['callback']) - $result .= "\n\$WowheadProfiler.loadOnDemand('quests', ".$catg.");\n"; - - break; - case 'recipes': - if (!$this->_get['callback'] || !$this->_get['skill']) - break; - - foreach ($this->_get['skill'] as $s) - Util::loadStaticFile('p-recipes-'.$s, $result, true); - - Util::loadStaticFile('p-recipes-sec', $result, true); - $result .= "\n\$WowheadProfiler.loadOnDemand('recipes', null);\n"; - - break; - // locale independant - case 'quick-excludes': - case 'zones': - case 'weight-presets': - case 'item-scaling': - case 'realms': - case 'statistics': - if (!Util::loadStaticFile($set, $result) && CFG_DEBUG) - $result .= "alert('could not fetch static data: ".$set."');"; - - $result .= "\n\n"; - break; - // localized - case 'talents': - if ($_ = $this->_get['class']) - $set .= "-".$_; - case 'achievements': - case 'pet-talents': - case 'glyphs': - case 'gems': - case 'enchants': - case 'itemsets': - case 'pets': - if (!Util::loadStaticFile($set, $result, true) && CFG_DEBUG) - $result .= "alert('could not fetch static data: ".$set." for locale: ".User::$localeString."');"; - - $result .= "\n\n"; - break; - default: - break; - } - } - - return $result; - } - - protected function checkSkill($val) - { - return array_intersect([171, 164, 333, 202, 182, 773, 755, 165, 186, 393, 197, 185, 129, 356], explode(',', $val)); - } - - protected function checkCallback($val) - { - return substr($val, 0, 29) == '$WowheadProfiler.loadOnDemand'; - } - - private function loadProfilerData($file, $catg = 'null') - { - $result = ''; - if ($this->_get['callback']) - if (Util::loadStaticFile('p-'.$file, $result, true)) - $result .= "\n\$WowheadProfiler.loadOnDemand('".$file."', ".$catg.");\n"; - - return $result; - } - -} - -?> diff --git a/includes/ajaxHandler/filter.class.php b/includes/ajaxHandler/filter.class.php deleted file mode 100644 index f01152bc..00000000 --- a/includes/ajaxHandler/filter.class.php +++ /dev/null @@ -1,107 +0,0 @@ -page = $p[0]; - - if (isset($p[1])) - $this->cat[] = $p[1]; - - if (count($params) > 1) - for ($i = 1; $i < count($params); $i++) - $this->cat[] = $params[$i]; - - $opts = ['parentCats' => $this->cat]; - - switch ($p[0]) - { - case 'achievements': - $this->filter = (new AchievementListFilter(true, $opts)); - break; - case 'enchantments': - $this->filter = (new EnchantmentListFilter(true, $opts)); - break; - case 'icons': - $this->filter = (new IconListFilter(true, $opts)); - break; - case 'items': - $this->filter = (new ItemListFilter(true, $opts)); - break; - case 'itemsets': - $this->filter = (new ItemsetListFilter(true, $opts)); - break; - case 'npcs': - $this->filter = (new CreatureListFilter(true, $opts)); - break; - case 'objects': - $this->filter = (new GameObjectListFilter(true, $opts)); - break; - case 'quests': - $this->filter = (new QuestListFilter(true, $opts)); - break; - case 'sounds': - $this->filter = (new SoundListFilter(true, $opts)); - break; - case 'spells': - $this->filter = (new SpellListFilter(true, $opts)); - break; - case 'profiles': - $this->filter = (new ProfileListFilter(true, $opts)); - break; - case 'guilds': - $this->filter = (new GuildListFilter(true, $opts)); - break; - case 'arena-teams': - $this->filter = (new ArenaTeamListFilter(true, $opts)); - break; - default: - return; - } - - parent::__construct($params); - - // always this one - $this->handler = 'handleFilter'; - } - - protected function handleFilter() - { - $url = '?'.$this->page; - - $this->filter->mergeCat($this->cat); - - if ($this->cat) - $url .= '='.implode('.', $this->cat); - - $fi = []; - if ($x = $this->filter->getFilterString()) - $url .= '&filter='.$x; - - if ($this->filter->error) - $_SESSION['fiError'] = get_class($this->filter); - - if ($fi) - $url .= '&filter='.implode(';', $fi); - - // do get request - return $url; - } - -} \ No newline at end of file diff --git a/includes/ajaxHandler/gotocomment.class.php b/includes/ajaxHandler/gotocomment.class.php deleted file mode 100644 index b5e1a02e..00000000 --- a/includes/ajaxHandler/gotocomment.class.php +++ /dev/null @@ -1,36 +0,0 @@ - [FILTER_CALLBACK, ['options' => 'AjaxHandler::checkInt']] - ); - - public function __construct(array $params) - { - parent::__construct($params); - - // always this one - $this->handler = 'handleGoToComment'; - $this->doRedirect = true; - } - - /* responses - header() - */ - protected function handleGoToComment() - { - if (!$this->_get['id']) - exit; // just be blank - - if ($_ = 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 = ?d', $this->_get['id'])) - return '?'.Util::$typeStrings[$_['type']].'='.$_['typeId'].'#comments:id='.$_['id'].($_['id'] != $this->_get['id'] ? ':reply='.$this->_get['id'] : null); - else - exit; - } -} - -?> \ No newline at end of file diff --git a/includes/ajaxHandler/guild.class.php b/includes/ajaxHandler/guild.class.php deleted file mode 100644 index f791dab0..00000000 --- a/includes/ajaxHandler/guild.class.php +++ /dev/null @@ -1,82 +0,0 @@ - [FILTER_CALLBACK, ['options' => 'AjaxHandler::checkIdList']], - 'profile' => [FILTER_CALLBACK, ['options' => 'AjaxHandler::checkEmptySet']], - ); - - public function __construct(array $params) - { - parent::__construct($params); - - if (!$this->params) - return; - - switch ($this->params[0]) - { - case 'resync': - $this->handler = 'handleResync'; - break; - case 'status': - $this->handler = 'handleStatus'; - break; - } - } - - /* params - id: - user: [optional, not used] - profile: [optional, also get related chars] - return: 1 - */ - protected function handleResync() - { - if ($guilds = DB::Aowow()->select('SELECT realm, realmGUID FROM ?_profiler_guild WHERE id IN (?a)', $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'])) - foreach ($chars as $c) - Profiler::scheduleResync(TYPE_PROFILE, $c['realm'], $c['realmGUID']); - - return '1'; - } - - /* params - id: - return - - [ - nQueueProcesses, - [statusCode, timeToRefresh, curQueuePos, errorCode, nResyncTries], - [] - ... - ] - - not all fields are required, if zero they are omitted - statusCode: - 0: end the request - 1: waiting - 2: working... - 3: ready; click to view - 4: error / retry - errorCode: - 0: unk error - 1: char does not exist - 2: armory gone - */ - protected function handleStatus() - { - $response = Profiler::resyncStatus(TYPE_GUILD, $this->_get['id']); - return Util::toJSON($response); - } -} - -?> diff --git a/includes/ajaxHandler/locale.class.php b/includes/ajaxHandler/locale.class.php deleted file mode 100644 index dee7a5e6..00000000 --- a/includes/ajaxHandler/locale.class.php +++ /dev/null @@ -1,33 +0,0 @@ - [FILTER_CALLBACK, ['options' => 'AjaxHandler::checkLocale']] - ); - - public function __construct(array $params) - { - parent::__construct($params); - - // always this one - $this->handler = 'handleLocale'; - $this->doRedirect = true; - } - - /* responses - header() - */ - protected function handleLocale() - { - User::setLocale($this->_get['locale']); - User::save(); - - return isset($_SERVER['HTTP_REFERER']) ? $_SERVER['HTTP_REFERER'] : '.'; - } -} - -?> diff --git a/includes/ajaxHandler/profile.class.php b/includes/ajaxHandler/profile.class.php deleted file mode 100644 index 50cded7d..00000000 --- a/includes/ajaxHandler/profile.class.php +++ /dev/null @@ -1,716 +0,0 @@ - [FILTER_CALLBACK, ['options' => 'AjaxHandler::checkIdList']], - 'items' => [FILTER_CALLBACK, ['options' => 'AjaxProfile::checkItemList']], - 'size' => [FILTER_SANITIZE_STRING, 0xC], // FILTER_FLAG_STRIP_LOW | *_HIGH - 'guild' => [FILTER_CALLBACK, ['options' => 'AjaxHandler::checkEmptySet']], - 'arena-team' => [FILTER_CALLBACK, ['options' => 'AjaxHandler::checkEmptySet']], - ); - - protected $_post = array( - 'name' => [FILTER_CALLBACK, ['options' => 'AjaxHandler::checkFulltext']], - 'level' => [FILTER_SANITIZE_NUMBER_INT, null], - 'class' => [FILTER_SANITIZE_NUMBER_INT, null], - 'race' => [FILTER_SANITIZE_NUMBER_INT, null], - 'gender' => [FILTER_SANITIZE_NUMBER_INT, null], - 'nomodel' => [FILTER_SANITIZE_NUMBER_INT, null], - 'talenttree1' => [FILTER_SANITIZE_NUMBER_INT, null], - 'talenttree2' => [FILTER_SANITIZE_NUMBER_INT, null], - 'talenttree3' => [FILTER_SANITIZE_NUMBER_INT, null], - 'activespec' => [FILTER_SANITIZE_NUMBER_INT, null], - 'talentbuild1' => [FILTER_SANITIZE_STRING, 0xC],// FILTER_FLAG_STRIP_LOW | *_HIGH - 'glyphs1' => [FILTER_SANITIZE_STRING, 0xC], - 'talentbuild2' => [FILTER_SANITIZE_STRING, 0xC], - 'glyphs2' => [FILTER_SANITIZE_STRING, 0xC], - 'icon' => [FILTER_SANITIZE_STRING, 0xC], - 'description' => [FILTER_CALLBACK, ['options' => 'AjaxHandler::checkFulltext']], - 'source' => [FILTER_SANITIZE_NUMBER_INT, null], - 'copy' => [FILTER_SANITIZE_NUMBER_INT, null], - 'public' => [FILTER_SANITIZE_NUMBER_INT, null], - 'gearscore' => [FILTER_SANITIZE_NUMBER_INT, null], - 'inv' => [FILTER_CALLBACK, ['options' => 'AjaxProfile::checkItemString', 'flags' => FILTER_REQUIRE_ARRAY]], - ); - - public function __construct(array $params) - { - parent::__construct($params); - - if (!$this->params) - return; - - switch ($this->params[0]) - { - case 'unlink': - $this->undo = true; - case 'link': - $this->handler = 'handleLink'; // always returns null - break; - case 'unpin': - $this->undo = true; - case 'pin': - $this->handler = 'handlePin'; // always returns null - break; - case 'private': - $this->undo = true; - case 'public': - $this->handler = 'handlePrivacy'; // always returns null - break; - case 'avatar': - $this->handler = 'handleAvatar'; // sets an image header - break; // so it has to die here or another header will be set - case 'resync': - $this->handler = 'handleResync'; // always returns "1" - break; - case 'status': - $this->handler = 'handleStatus'; // returns status object - break; - case 'save': - $this->handler = 'handleSave'; - break; - case 'delete': - $this->handler = 'handleDelete'; - break; - case 'purge': - $this->handler = 'handlePurge'; - break; - case 'summary': // page is generated by jScript - die(); // just be empty - case 'load': - $this->handler = 'handleLoad'; - break; - } - } - - /* params - id: - user: [optional] - return: null - */ - protected function handleLink() // links char with account - { - if (!User::$id || empty($this->_get['id'])) - return; - - $uid = User::$id; - if ($this->_get['user'] && User::isInGroup(U_GROUP_ADMIN | U_GROUP_BUREAU)) - $uid = DB::Aowow()->selectCell('SELECT id FROM ?_account WHERE user = ?', $this->_get['user']); - else if ($this->_get['user']) - return; - - if ($this->undo) - DB::Aowow()->query('DELETE FROM ?_account_profiles WHERE accountId = ?d AND profileId IN (?a)', $uid, $this->_get['id']); - else - foreach ($this->_get['id'] as $prId) // only link characters, not custom profiles - if ($prId = DB::Aowow()->selectCell('SELECT id FROM ?_profiler_profiles WHERE id = ?d AND realm IS NOT NULL', $prId)) - DB::Aowow()->query('INSERT IGNORE INTO ?_account_profiles VALUES (?d, ?d, 0)', $uid, $prId); - } - - /* params - id: - user: [optional] - return: null - */ - protected function handlePin() // (un)favorite - { - if (!User::$id || empty($this->_get['id'][0])) - return; - - $uid = User::$id; - if ($this->_get['user'] && User::isInGroup(U_GROUP_ADMIN | U_GROUP_BUREAU)) - $uid = DB::Aowow()->selectCell('SELECT id FROM ?_account WHERE user = ?', $this->_get['user']); - else if ($this->_get['user']) - return; - - // 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); - // and set a single char if nesecary - if (!$this->undo) - DB::Aowow()->query('UPDATE ?_account_profiles SET extraFlags = extraFlags | ?d WHERE profileId = ?d AND accountId = ?d', PROFILER_CU_PINNED, $this->_get['id'][0], $uid); - } - - /* params - id: - user: [optional] - return: null - */ - protected function handlePrivacy() // public visibility - { - if (!User::$id || empty($this->_get['id'][0])) - return; - - $uid = User::$id; - if ($this->_get['user'] && User::isInGroup(U_GROUP_ADMIN | U_GROUP_BUREAU)) - $uid = DB::Aowow()->selectCell('SELECT id FROM ?_account WHERE user = ?', $this->_get['user']); - else if ($this->_get['user']) - return; - - if ($this->undo) - { - 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); - } - else - { - 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); - } - } - - /* params - id: - size: [optional] - return: image-header - */ - protected function handleAvatar() // image - { - // something happened in the last years: those textures do not include tiny icons - $sizes = [/* 'tiny' => 15, */'small' => 18, 'medium' => 36, 'large' => 56]; - $aPath = 'uploads/avatars/%d.jpg'; - $s = $this->_get['size'] ?: 'medium'; - - if (!$this->_get['id'] || !preg_match('/^([0-9]+)\.(jpg|gif)$/', $this->_get['id'][0], $matches) || !in_array($s, array_keys($sizes))) - return; - - $this->contentType = 'image/'.$matches[2]; - - $id = $matches[1]; - $dest = imageCreateTruecolor($sizes[$s], $sizes[$s]); - - if (file_exists(sprintf($aPath, $id))) - { - $offsetX = $offsetY = 0; - - switch ($s) - { - case 'tiny': - $offsetX += $sizes['small']; - case 'small': - $offsetY += $sizes['medium']; - case 'medium': - $offsetX += $sizes['large']; - } - - $src = imageCreateFromJpeg(printf($aPath, $id)); - imagecopymerge($dest, $src, 0, 0, $offsetX, $offsetY, $sizes[$s], $sizes[$s], 100); - } - - if ($matches[2] == 'gif') - imageGif($dest); - else - imageJpeg($dest); - - return; - } - - /* params - id: - user: [optional, not used] - return: 1 - */ - protected function handleResync() - { - if ($chars = DB::Aowow()->select('SELECT realm, realmGUID FROM ?_profiler_profiles WHERE id IN (?a)', $this->_get['id'])) - foreach ($chars as $c) - Profiler::scheduleResync(TYPE_PROFILE, $c['realm'], $c['realmGUID']); - - return '1'; - } - - /* params - id: - return - - [ - nQueueProcesses, - [statusCode, timeToRefresh, curQueuePos, errorCode, nResyncTries], - [] - ... - ] - - not all fields are required, if zero they are omitted - statusCode: - 0: end the request - 1: waiting - 2: working... - 3: ready; click to view - 4: error / retry - errorCode: - 0: unk error - 1: char does not exist - 2: armory gone - */ - protected function handleStatus() - { - // 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']); - else if ($this->_get['arena-team']) - $ids = DB::Aowow()->selectCol('SELECT profileId FROM ?_profiler_arena_team_member WHERE arenaTeamId IN (?a)', $this->_get['id']); - else - $ids = $this->_get['id']; - - $response = Profiler::resyncStatus(TYPE_PROFILE, $ids); - return Util::toJSON($response); - } - - /* params (get)) - id: [0: new profile] - params (post) - [see below] - return: - proileId [onSuccess] - -1 [onError] - */ - protected function handleSave() // unKill a profile - { - // todo (med): detail check this post-data - $cuProfile = array( - 'user' => User::$id, - // 'userName' => User::$displayName, - 'name' => $this->_post['name'], - 'level' => $this->_post['level'], - 'class' => $this->_post['class'], - 'race' => $this->_post['race'], - 'gender' => $this->_post['gender'], - 'nomodelMask' => $this->_post['nomodel'], - 'talenttree1' => $this->_post['talenttree1'], - 'talenttree2' => $this->_post['talenttree2'], - 'talenttree3' => $this->_post['talenttree3'], - 'talentbuild1' => $this->_post['talentbuild1'], - 'talentbuild2' => $this->_post['talentbuild2'], - 'activespec' => $this->_post['activespec'], - 'glyphs1' => $this->_post['glyphs1'], - 'glyphs2' => $this->_post['glyphs2'], - 'gearscore' => $this->_post['gearscore'], - 'icon' => $this->_post['icon'], - 'cuFlags' => PROFILER_CU_PROFILE | ($this->_post['public'] ? PROFILER_CU_PUBLISHED : 0) - ); - - if (strstr($cuProfile['icon'], 'profile=avatar')) // how the profiler is supposed to handle icons is beyond me - $cuProfile['icon'] = ''; - - if ($_ = $this->_post['description']) - $cuProfile['description'] = $_; - - if ($_ = $this->_post['source']) // should i also set sourcename? - $cuProfile['sourceId'] = $_; - - 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', $_)) - $cuProfile['realm'] = $r; - - $cuProfile['sourceId'] = $_; - } - - if ($cuProfile['sourceId']) - $cuProfile['sourceName'] = DB::Aowow()->selectCell('SELECT name FROM ?_profiler_profiles WHERE id = ?d', $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); - } - else // new - { - $nProfiles = DB::Aowow()->selectCell('SELECT COUNT(*) FROM ?_profiler_profiles WHERE user = ?d AND realmGUID IS NULL', User::$id); - if ($nProfiles < 10 || User::isPremium()) - if ($newId = DB::Aowow()->query('INSERT INTO ?_profiler_profiles (?#) VALUES (?a)', array_keys($cuProfile), array_values($cuProfile))) - $charId = $newId; - } - - // update items - if ($charId != -1) - { - // ok, 'funny' thing: wether an item has en extra prismatic sockel is determined contextual - // either the socket is -1 or it has an itemId in a socket where there shouldn't be one - $keys = ['id', 'slot', 'item', 'subitem', 'permEnchant', 'tempEnchant', 'gem1', 'gem2', 'gem3', 'gem4']; - - // validate Enchantments - $enchIds = array_merge( - array_column($this->_post['inv'], 3), // perm enchantments - array_column($this->_post['inv'], 4) // temp enchantments (not used..?) - ); - $enchs = new EnchantmentList(array(['id', $enchIds])); - - // validate items - $itemIds = array_merge( - array_column($this->_post['inv'], 1), // base item - array_column($this->_post['inv'], 5), // gem slot 1 - array_column($this->_post['inv'], 6), // gem slot 2 - array_column($this->_post['inv'], 7), // gem slot 3 - array_column($this->_post['inv'], 8) // gem slot 4 - ); - - $items = new ItemList(array(['id', $itemIds])); - if (!$items->error) - { - foreach ($this->_post['inv'] as $slot => $itemData) - { - 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]); - continue; - } - - // item does not exist - if (!$items->getEntry($itemData[1])) - continue; - - // sub-item check - if (!$items->getRandEnchantForItem($itemData[1])) - $itemData[2] = 0; - - // item sockets are fubar - $nSockets = $items->json[$itemData[1]]['nsockets']; - $nSockets += in_array($slot, [SLOT_WAIST, SLOT_WRISTS, SLOT_HANDS]) ? 1 : 0; - for ($i = 5; $i < 9; $i++) - if ($itemData[$i] > 0 && (!$items->getEntry($itemData[$i]) || $i >= (5 + $nSockets))) - $itemData[$i] = 0; - - // item enchantments are borked - if ($itemData[3] && !$enchs->getEntry($itemData[3])) - $itemData[3] = 0; - - if ($itemData[4] && !$enchs->getEntry($itemData[4])) - $itemData[4] = 0; - - // looks good - array_unshift($itemData, $charId); - DB::Aowow()->query('REPLACE INTO ?_profiler_items (?#) VALUES (?a)', $keys, $itemData); - } - } - } - - return $charId; - } - - /* params - id: - return - null - */ - protected function handleDelete() // kill a profile - { - if (!$this->_get['id']) - return; - - // 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 - ); - } - - /* params - id: profileId - items: string [itemIds.join(':')] - unnamed: unixtime [only to force the browser to reload instead of cache] - return - lots... - */ - protected function handleLoad() - { - // titles, achievements, characterData, talents, pets - // and some onLoad-hook to .. load it registerProfile($data) - // everything else goes through data.php .. strangely enough - - if (!$this->_get['id']) - 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]); - if (!$pBase) - { - trigger_error('Profiler::handleLoad() - 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)) - return; - - - $rData = []; - foreach (Profiler::getRealms() as $rId => $rData) - if ($rId == $pBase['realm']) - break; - - $profile = array( - 'id' => $pBase['id'], - 'source' => $pBase['id'], - 'level' => $pBase['level'], - 'classs' => $pBase['class'], - 'race' => $pBase['race'], - 'faction' => Game::sideByRaceMask(1 << ($pBase['race'] - 1)) - 1, - 'gender' => $pBase['gender'], - 'skincolor' => $pBase['skincolor'], - 'hairstyle' => $pBase['hairstyle'], - 'haircolor' => $pBase['haircolor'], - 'facetype' => $pBase['facetype'], - 'features' => $pBase['features'], - 'title' => $pBase['title'], - 'name' => $pBase['name'], - 'guild' => "$'".$pBase['guildname']."'", - 'published' => !!($pBase['cuFlags'] & PROFILER_CU_PUBLISHED), - 'pinned' => !!($pBase['cuFlags'] & PROFILER_CU_PINNED), - 'nomodel' => $pBase['nomodelMask'], - 'playedtime' => $pBase['playedtime'], - 'lastupdated' => $pBase['lastupdated'] * 1000, - 'talents' => array( - 'builds' => array( // notice the bullshit to prevent the talent-string from becoming a float! NOTICE IT!! - ['talents' => '$"'.$pBase['talentbuild1'].'"', 'glyphs' => $pBase['glyphs1']], - ['talents' => '$"'.$pBase['talentbuild2'].'"', 'glyphs' => $pBase['glyphs2']] - ), - 'active' => $pBase['activespec'] - ), - // set later - 'inventory' => [], - 'bookmarks' => [], // list of userIds who claimed this profile (claiming and owning are two different things) - - // completion lists: [subjectId => amount/timestamp/1] - 'skills' => [], // skillId => [curVal, maxVal] - 'reputation' => [], // factionId => curVal - 'titles' => [], // titleId => 1 - 'spells' => [], // spellId => 1; recipes, vanity pets, mounts - 'achievements' => [], // achievementId => timestamp - 'quests' => [], // questId => 1 - 'achievementpoints' => 0, // max you have - 'statistics' => [], // all raid activity [achievementId => killCount] - 'activity' => [], // recent raid activity [achievementId => 1] (is a subset of statistics) - ); - - if ($pBase['cuFlags'] & PROFILER_CU_PROFILE) - { - // this parameter is _really_ strange .. probably still not doing this right - $profile['source'] = $pBase['realm'] ? $pBase['sourceId'] : 0; - - $profile['sourcename'] = $pBase['sourceName']; - $profile['description'] = $pBase['description']; - $profile['user'] = $pBase['user']; - $profile['username'] = DB::Aowow()->selectCell('SELECT displayName FROM ?_account WHERE id = ?d', $pBase['user']); - } - - // custom profiles inherit this when copied from real char :( - if ($pBase['realm']) - { - $profile['region'] = [$rData['region'], Lang::profiler('regions', $rData['region'])]; - $profile['battlegroup'] = [Profiler::urlize(CFG_BATTLEGROUP), CFG_BATTLEGROUP]; - $profile['realm'] = [Profiler::urlize($rData['name']), $rData['name']]; - } - - // bookmarks - if ($_ = DB::Aowow()->selectCol('SELECT accountId FROM ?_account_profiles WHERE profileId = ?d', $pBase['id'])) - $profile['bookmarks'] = $_; - - // arena teams - [size(2|3|5) => DisplayName]; DisplayName 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'])) - $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, talents FROM ?_profiler_pets WHERE owner = ?d', $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', $pBase['id'])) - { - foreach ($customs as $id => $cu) - { - if (!$cu['icon']) - unset($cu['icon']); - - $profile['customs'][$id] = array_values($cu); - } - } - - - /* $profile[] - // CUSTOM - 'auras' => [], // custom list of buffs, debuffs [spellId] - - // UNUSED - 'glyphs' => [], // provided list of already known glyphs (post cataclysm feature) - */ - - - $completion = DB::Aowow()->select('SELECT type AS ARRAY_KEY, typeId AS ARRAY_KEY2, cur, max FROM ?_profiler_completion WHERE id = ?d', $pBase['id']); - foreach ($completion as $type => $data) - { - switch ($type) - { - case TYPE_FACTION: // factionId => amount - $profile['reputation'] = array_combine(array_keys($data), array_column($data, 'cur')); - break; - case TYPE_TITLE: - foreach ($data as &$d) - $d = 1; - - $profile['titles'] = $data; - break; - case TYPE_QUEST: - foreach ($data as &$d) - $d = 1; - - $profile['quests'] = $data; - break; - case TYPE_SPELL: - foreach ($data as &$d) - $d = 1; - - $profile['spells'] = $data; - break; - case TYPE_ACHIEVEMENT: - $achievements = array_filter($data, function ($x) { return $x['max'] === null; }); - $statistics = array_filter($data, function ($x) { return $x['max'] !== null; }); - - // achievements - $profile['achievements'] = array_combine(array_keys($achievements), array_column($achievements, 'cur')); - $profile['achievementpoints'] = DB::Aowow()->selectCell('SELECT SUM(points) FROM ?_achievement WHERE id IN (?a)', array_keys($achievements)); - - // raid progression - $activity = array_filter($statistics, function ($x) { return $x['cur'] > (time() - MONTH); }); - foreach ($activity as &$r) - $r = 1; - - // ony .. subtract 10-man from 25-man - - $profile['statistics'] = array_combine(array_keys($statistics), array_column($statistics, 'max')); - $profile['activity'] = $activity; - break; - case TYPE_SKILL: - foreach ($data as &$d) - $d = [$d['cur'], $d['max']]; - - $profile['skills'] = $data; - break; - } - } - - $buff = ''; - - $usedSlots = []; - if ($this->_get['items']) - { - $phItems = new ItemList(array(['id', $this->_get['items']], ['slot', INVTYPE_NON_EQUIP, '!'])); - if (!$phItems->error) - { - $data = $phItems->getListviewData(ITEMINFO_JSON | ITEMINFO_SUBITEMS); - foreach ($phItems->iterate() as $iId => $__) - { - $sl = $phItems->getField('slot'); - foreach (Profiler::$slot2InvType as $slot => $invTypes) - { - if (in_array($sl, $invTypes) && !in_array($slot, $usedSlots)) - { - // get and apply inventory - $buff .= 'g_items.add('.$iId.', {name_'.User::$localeString.":'".Util::jsEscape($phItems->getField('name', true))."', quality:".$phItems->getField('quality').", icon:'".$phItems->getField('iconString')."', jsonequip:".Util::toJSON($data[$iId])."});\n"; - $profile['inventory'][$slot] = [$iId, 0, 0, 0, 0, 0, 0, 0]; - - $usedSlots[] = $slot; - break; - } - } - } - } - } - - if ($items = DB::Aowow()->select('SELECT * FROM ?_profiler_items WHERE id = ?d', $pBase['id'])) - { - $itemz = new ItemList(array(['id', array_column($items, 'item')], CFG_SQL_LIMIT_NONE)); - if (!$itemz->error) - { - $data = $itemz->getListviewData(ITEMINFO_JSON | ITEMINFO_SUBITEMS); - - foreach ($items as $i) - { - if ($itemz->getEntry($i['item']) && !in_array($i['slot'], $usedSlots)) - { - // get and apply inventory - $buff .= 'g_items.add('.$i['item'].', {name_'.User::$localeString.":'".Util::jsEscape($itemz->getField('name', true))."', quality:".$itemz->getField('quality').", icon:'".$itemz->getField('iconString')."', jsonequip:".Util::toJSON($data[$i['item']])."});\n"; - $profile['inventory'][$i['slot']] = [$i['item'], $i['subItem'], $i['permEnchant'], $i['tempEnchant'], $i['gem1'], $i['gem2'], $i['gem3'], $i['gem4']]; - } - } - } - } - - if ($buff) - $buff .= "\n"; - - - // if ($au = $char->getField('auras')) - // { - // $auraz = new SpellList(array(['id', $char->getField('auras')], CFG_SQL_LIMIT_NONE)); - // $dataz = $auraz->getListviewData(); - // $modz = $auraz->getProfilerMods(); - - // // get and apply aura-mods - // foreach ($dataz as $id => $data) - // { - // $mods = []; - // if (!empty($modz[$id])) - // { - // foreach ($modz[$id] as $k => $v) - // { - // if (is_array($v)) - // $mods[] = $v; - // else if ($str = @Game::$itemMods[$k]) - // $mods[$str] = $v; - // } - // } - - // $buff .= 'g_spells.add('.$id.", {id:".$id.", name:'".Util::jsEscape(mb_substr($data['name'], 1))."', icon:'".$data['icon']."', modifier:".Util::toJSON($mods)."});\n"; - // } - // $buff .= "\n"; - // } - - - // load available titles - Util::loadStaticFile('p-titles-'.$pBase['gender'], $buff, true); - - // add profile to buffer - $buff .= "\n\n\$WowheadProfiler.registerProfile(".Util::toJSON($profile).");"; - - return $buff."\n"; - } - - /* params - id: - data: [string, tabName] - return - null - */ - protected function handlePurge() { } // removes completion data (as uploaded by the wowhead client) Just fail silently if someone triggers this manually - - protected function checkItemList($val) - { - // expecting item-list - if (preg_match('/\d+(:\d+)*/', $val)) - return array_map('intval', explode(':', $val)); - - return null; - } - - protected function checkItemString($val) - { - // expecting item-list - if (preg_match('/\d+(,\d+)*/', $val)) - return array_map('intval', explode(',', $val)); - - return null; - } - -} - -?> diff --git a/includes/basetype.class.php b/includes/basetype.class.php deleted file mode 100644 index f5b1acab..00000000 --- a/includes/basetype.class.php +++ /dev/null @@ -1,1385 +0,0 @@ - - * int - operator defaults to: = - * array - operator defaults to: IN () - * null - operator defaults to: IS [NULL] - * operator: modifies/overrides default - * ! - negated default value (NOT LIKE; <>; NOT IN) - * condition as str - * defines linking (AND || OR) - * condition as int - * defines LIMIT - * - * example: - * array( - * ['id', 45], - * ['name', 'test%', '!'], - * [ - * 'AND', - * ['flags', 0xFF, '&'], - * ['flags2', 0xF, '&'], - * ] - * [['mask', 0x3, '&'], 0], - * ['joinedTbl.field', NULL] // NULL must be explicitly specified "['joinedTbl.field']" would be skipped as erronous definition (only really usefull when left-joining) - * '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 - */ - public function __construct($conditions = [], $miscData = null) - { - $where = []; - $linking = ' AND '; - $limit = CFG_SQL_LIMIT_DEFAULT; - $className = get_class($this); - - if (!$this->queryBase || $conditions === null) - return; - - $prefixes = []; - if (preg_match('/FROM \??[\w\_]+( AS)?\s?`?(\w+)`?$/i', $this->queryBase, $match)) - $prefixes['base'] = $match[2]; - else - $prefixes['base'] = ''; - - if ($miscData && !empty($miscData['extraOpts'])) - $this->extendQueryOpts($miscData['extraOpts']); - - $resolveCondition = function ($c, $supLink) use (&$resolveCondition, &$prefixes, $miscData) - { - $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($f) use(&$prefixes) - { - 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], function(&$item, $key) { - $item = Util::checkNumeric($item) ? $item : DB::Aowow()->escape($item); - }); - - $op = (isset($c[2]) && $c[2] == '!') ? 'NOT IN' : 'IN'; - $val = '('.implode(', ', $c[1]).')'; - } - else if (Util::checkNumeric($c[1])) - { - $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)) - { - case 'array': - break; - case 'string': - case 'integer': - if (is_string($c)) - $linking = $c == 'AND' ? ' AND ' : ' OR '; - else - $limit = $c > 0 ? $c : 0; - default: - unset($conditions[$i]); - } - } - - foreach ($conditions as $c) - if ($x = $resolveCondition($c, $linking)) - $where[] = $x; - - // optional query parts may require other optional parts to work - foreach ($prefixes as $pre) - if (isset($this->queryOpts[$pre][0])) - foreach ($this->queryOpts[$pre][0] as $req) - if (!in_array($req, $prefixes)) - $prefixes[] = $req; - - // remove optional query parts, that are not required - foreach ($this->queryOpts as $k => $arr) - if (!in_array($k, $prefixes)) - unset($this->queryOpts[$k]); - - // prepare usage of guids if using multiple realms (which have non-zoro indizes) - if (key($this->dbNames) != 0) - $this->queryBase = preg_replace('/\s([^\s]+)\sAS\sARRAY_KEY/i', ' CONCAT("DB_IDX", ":", \1) AS ARRAY_KEY', $this->queryBase); - - // insert additional selected fields - if ($s = array_column($this->queryOpts, 's')) - $this->queryBase = str_replace('ARRAY_KEY', 'ARRAY_KEY '.implode('', $s), $this->queryBase); - - // append joins - if ($j = array_column($this->queryOpts, 'j')) - foreach ($j as $_) - $this->queryBase .= is_array($_) ? (empty($_[1]) ? ' JOIN ' : ' LEFT JOIN ').$_[0] : ' JOIN '.$_; - - // append conditions - if ($where) - $this->queryBase .= ' WHERE ('.implode($linking, $where).')'; - - // append grouping - if ($g = array_filter(array_column($this->queryOpts, 'g'))) - $this->queryBase .= ' GROUP BY '.implode(', ', $g); - - // append post filtering - if ($h = array_filter(array_column($this->queryOpts, 'h'))) - $this->queryBase .= ' HAVING '.implode(' AND ', $h); - - // append ordering - if ($o = array_filter(array_column($this->queryOpts, 'o'))) - $this->queryBase .= ' ORDER BY '.implode(', ', $o); - - // apply limit - if ($limit) - $this->queryBase .= ' LIMIT '.$limit; - - // execute query (finally) - $mtch = 0; - // 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)->SelectPage($mtch, $query)) - { - $this->matches += $mtch; - foreach ($rows as $id => $row) - { - if (isset($this->templates[$id])) - trigger_error('guid for List already in use #'.$id, E_USER_WARNING); - else - $this->templates[$id] = $row; - } - } - } - - if (!$this->templates) - return; - - // push first element for instant use - $this->reset(); - - // all clear - $this->error = false; - } - - public function &iterate() - { - $oldIdx = $this->id; - - // reset on __construct - $this->reset(); - - while (list($id, $_) = each($this->templates)) - { - $this->id = $id; - $this->curTpl = &$this->templates[$id]; // do not use $tpl from each(), as we want to be referenceable - - yield $id => $this->curTpl; - - unset($this->curTpl); // kill reference or it will 'bleed' into the next iteration - } - - // fforward to old index - $this->reset(); - do - { - if (key($this->templates) != $oldIdx) - continue; - - $this->curTpl = current($this->templates); - $this->id = key($this->templates); - break; - } - while (next($this->templates)); - } - - protected function reset() - { - unset($this->curTpl); // kill reference or strange stuff will happen - $this->curTpl = reset($this->templates); - $this->id = key($this->templates); - } - - // read-access to templates - public function getEntry($id) - { - if (isset($this->templates[$id])) - { - $this->curTpl = $this->templates[$id]; - $this->id = $id; - return $this->templates[$id]; - } - - return null; - } - - public function getField($field, $localized = false, $silent = false) - { - if (!$this->curTpl || (!$localized && !isset($this->curTpl[$field]))) - return ''; - - if ($localized) - return Util::localizedString($this->curTpl, $field, $silent); - - $value = $this->curTpl[$field]; - Util::checkNumeric($value); - - return $value; - } - - public function getAllFields($field, $localized = false, $silent = false) - { - $data = []; - - foreach ($this->iterate() as $__) - $data[$this->id] = $this->getField($field, $localized, $silent); - - return $data; - } - - public function getRandomId() - { - // 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' : null; - $pattern = '/SELECT .* (-?`?[\w_]*\`?.?`?(id|entry)`?) AS ARRAY_KEY,?.* FROM (\?[\w_-]+) (`?\w*`?)/i'; - $replace = 'SELECT $1 FROM $3 $4 '.$where.' ORDER BY RAND() ASC LIMIT 1'; - - $query = preg_replace($pattern, $replace, $this->queryBase); - - return DB::Aowow()->selectCell($query); - } - - public function getFoundIDs() - { - return array_keys($this->templates); - } - - public function getMatches() - { - return $this->matches; - } - - protected function extendQueryOpts($extra) // needs to be called from __construct - { - foreach ($extra as $tbl => $sets) - { - foreach ($sets as $module => $value) - { - if (!$value || !is_array($value)) - continue; - - switch ($module) - { - // additional (str) - case 'g': // group by - case 's': // select - if (!empty($this->queryOpts[$tbl][$module])) - $this->queryOpts[$tbl][$module] .= implode(' ', $value); - else - $this->queryOpts[$tbl][$module] = implode(' ', $value); - - break; - case 'h': // having - if (!empty($this->queryOpts[$tbl][$module])) - $this->queryOpts[$tbl][$module] .= implode(' AND ', $value); - else - $this->queryOpts[$tbl][$module] = implode(' AND ', $value); - - break; - // additional (arr) - case 'j': // join - if (!empty($this->queryOpts[$tbl][$module]) && is_array($this->queryOpts[$tbl][$module])) - $this->queryOpts[$tbl][$module][0][] = $value; - else - $this->queryOpts[$tbl][$module] = $value; - - break; - // replacement (str) - case 'l': // limit - case 'o': // order by - $this->queryOpts[$tbl][$module] = $value[0]; - break; - } - } - } - } - - /* source More .. keys seen used - 'n': name [always set] - '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] - 'q': cssQuality [Items] - 'z': zone [set when all happens in here] - 'p': PvP [pvpSourceId] - 's': TYPE_TITLE: side; TYPE_SPELL: skillId (yeah, double use. Ain't life just grand) - 'c': category [Spells / Quests] - 'c2': subCat [Quests] - 'icon': iconString - */ - public function getSourceData() {} - - // should return data required to display a listview of any kind - // this is a rudimentary example, that will not suffice for most Types - abstract public function getListviewData(); - - // should return data to extend global js variables for a certain type (e.g. g_items) - abstract public function getJSGlobals($addMask = GLOBALINFO_ANY); - - // NPC, GO, Item, Quest, Spell, Achievement, Profile would require this - abstract public function renderTooltip(); -} - -trait listviewHelper -{ - public function hasSetFields($fields) - { - if (!is_array($fields)) - return 0x0; - - $result = 0x0; - - foreach ($this->iterate() as $__) - { - foreach ($fields as $k => $str) - { - if ($this->getField($str)) - { - $result |= 1 << $k; - unset($fields[$k]); - } - } - - if (empty($fields)) // all set .. return early - { - $this->reset(); // Generators have no __destruct, reset manually, when not doing a full iteration - return $result; - } - } - - return $result; - } - - public function hasDiffFields($fields) - { - if (!is_array($fields)) - return 0x0; - - $base = []; - $result = 0x0; - - foreach ($fields as $k => $str) - $base[$str] = $this->getField($str); - - foreach ($this->iterate() as $__) - { - foreach ($fields as $k => $str) - { - if ($base[$str] != $this->getField($str)) - { - $result |= 1 << $k; - unset($fields[$k]); - } - } - - if (empty($fields)) // all fields diff .. return early - { - $this->reset(); // Generators have no __destruct, reset manually, when not doing a full iteration - return $result; - } - } - - return $result; - } - - public function hasAnySource() - { - if (!isset($this->sources)) - return false; - - foreach ($this->sources as $src) - { - if (!is_array($src)) - continue; - - if (!empty($src)) - return true; - } - - return false; - } -} - -/* - !IMPORTANT! - It is flat out impossible to distinguish between floors for multi-level areas, if the floors overlap each other! - The coordinates generated by the script WILL be on every level and will have to be removed MANUALLY! - - impossible := you are not keen on reading wmo-data; -*/ -trait spawnHelper -{ - private $spawnResult = array( - SPAWNINFO_FULL => null, - SPAWNINFO_SHORT => null, - SPAWNINFO_ZONES => null, - SPAWNINFO_QUEST => null - ); - - private function createShortSpawns() // [zoneId, floor, [[x1, y1], [x2, y2], ..]] as tooltip2 if enabled by or anchor #map (one area, one floor, one creature, no survivors) - { - // first get zone/floor with the most spawns - if ($res = DB::Aowow()->selectRow('SELECT areaId, floor FROM ?_spawns WHERE type = ?d && typeId = ?d 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 && typeId = ?d && areaId = ?d && floor = ?d', self::$type, $this->id, $res['areaId'], $res['floor']); - $spawns = []; - foreach ($points as $p) - $spawns[] = [$p['posX'], $p['posY']]; - - $this->spawnResult[SPAWNINFO_SHORT] = [$res['areaId'], $res['floor'], $spawns]; - } - } - - private function createFullSpawns() // for display on map (objsct/npc detail page) - { - $data = []; - $wpSum = []; - $wpIdx = 0; - $spawns = DB::Aowow()->select("SELECT * FROM ?_spawns WHERE type = ?d AND typeId = ?d", self::$type, $this->id); - if (!$spawns) - return; - - foreach ($spawns as $s) - { - // check, if we can attach waypoints to creature - // we will get a nice clusterfuck of dots if we do this for more GUIDs, than we have colors though - if (count($spawns) < 6 && self::$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'])) - { - 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); - - $opts = array( // \0 doesn't get printed and tricks Util::toJSON() into handling this as a string .. i feel slightly dirty now - 'label' => "\0$
".implode('
', $label).'
', - 'type' => $wpIdx - ); - - // connective line - if ($i > 0) - $opts['lines'] = [[$wPoints[$i - 1]['posX'], $wPoints[$i - 1]['posY']]]; - - $data[$s['areaId']][$s['floor']]['coords'][] = [$p['posX'], $p['posY'], $opts]; - if (empty($wpSum[$s['areaId']][$s['floor']])) - $wpSum[$s['areaId']][$s['floor']] = 1; - else - $wpSum[$s['areaId']][$s['floor']]++; - } - $wpIdx++; - } - } - - $opts = $menu = $tt = $info = []; - $footer = ''; - - if ($s['respawn']) - $info[1] = ''.Lang::npc('respawnIn').Lang::main('colon').Util::formatTime($s['respawn'] * 1000, false).''; - - if (User::isInGroup(U_GROUP_STAFF)) - { - $info[0] = $s['guid'] < 0 ? 'Vehicle Accessory' : 'GUID'.Lang::main('colon').$s['guid']; - - if ($s['phaseMask'] > 1 && ($s['phaseMask'] & 0xFFFF) != 0xFFFF) - $info[2] = Lang::game('phases').Lang::main('colon').Util::asHex($s['phaseMask']); - - if ($s['spawnMask'] == 15) - $info[3] = Lang::game('mode').Lang::main('colon').Lang::game('modes', -1); - else if ($s['spawnMask']) - { - $_ = []; - for ($i = 0; $i < 4; $i++) - if ($s['spawnMask'] & 1 << $i) - $_[] = Lang::game('modes', $i); - - $info[4] = Lang::game('mode').Lang::main('colon').implode(', ', $_); - } - - $footer = 'Click to move to different floor'; - } - - if ($info) - $tt['info'] = $info; - - if ($footer) - $tt['footer'] = $footer; - - if ($tt) - $opts['tooltip'] = [$this->getField('name', true) => $tt]; - - if ($menu) - $opts['menu'] = $menu; - - $data[$s['areaId']] [$s['floor']] ['coords'] [] = [$s['posX'], $s['posY'], $opts]; - } - foreach ($data as $a => &$areas) - foreach ($areas as $f => &$floor) - $floor['count'] = count($floor['coords']) - (!empty($wpSum[$a][$f]) ? $wpSum[$a][$f] : 0); - - $this->spawnResult[SPAWNINFO_FULL] = $data; - } - - private function createZoneSpawns() // [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) GROUP BY typeId", self::$type, $this->getfoundIDs()); - foreach ($res as &$r) - { - $r = explode(',', $r); - if (count($r) > 3) - array_splice($r, 3, count($r), -1); - } - - $this->spawnResult[SPAWNINFO_ZONES] = $res; - } - - private function createQuestSpawns() // [zoneId => [floor => [[x1, y1], [x2, y2], ..]]] - { - if (self::$type == TYPE_SOUND) - return; - - $res = DB::Aowow()->select('SELECT areaId, floor, typeId, posX, posY FROM ?_spawns WHERE type = ?d && typeId IN (?a)', self::$type, $this->getFoundIDs()); - $spawns = []; - foreach ($res as $data) - { - // zone => floor => spawnData - // todo (low): why is there a single set of coordinates; which one should be picked, instead of the first? gets used in ShowOnMap.buildTooltip i think - if (!isset($spawns[$data['areaId']][$data['floor']][$data['typeId']])) - { - $spawns[$data['areaId']][$data['floor']][$data['typeId']] = array( - 'type' => self::$type, - 'id' => $data['typeId'], - 'point' => '', // tbd later (start, end, requirement, sourcestart, sourceend, sourcerequirement) - 'name' => Util::localizedString($this->templates[$data['typeId']], 'name'), - 'coord' => [$data['posX'], $data['posY']], - 'coords' => [[$data['posX'], $data['posY']]], - 'objective' => 0, // tbd later (1-4 set a color; id of creature this entry gives credit for) - 'reactalliance' => $this->templates[$data['typeId']]['A'] ?: 0, - 'reacthorde' => $this->templates[$data['typeId']]['H'] ?: 0 - ); - } - else - $spawns[$data['areaId']][$data['floor']][$data['typeId']]['coords'][] = [$data['posX'], $data['posY']]; - } - - $this->spawnResult[SPAWNINFO_QUEST] = $spawns; - } - - public function getSpawns($mode) - { - // only Creatures, GOs and SoundEmitters can be spawned - if (!self::$type || !$this->getfoundIDs() || (self::$type != TYPE_NPC && self::$type != TYPE_OBJECT && self::$type != TYPE_SOUND)) - return []; - - switch ($mode) - { - case SPAWNINFO_SHORT: - if (empty($this->spawnResult[SPAWNINFO_SHORT])) - $this->createShortSpawns(); - - return $this->spawnResult[SPAWNINFO_SHORT]; - case SPAWNINFO_FULL: - if (empty($this->spawnResult[SPAWNINFO_FULL])) - $this->createFullSpawns(); - - return $this->spawnResult[SPAWNINFO_FULL]; - case SPAWNINFO_ZONES: - if (empty($this->spawnResult[SPAWNINFO_ZONES])) - $this->createZoneSpawns(); - - return !empty($this->spawnResult[SPAWNINFO_ZONES][$this->id]) ? $this->spawnResult[SPAWNINFO_ZONES][$this->id] : []; - case SPAWNINFO_QUEST: - if (empty($this->spawnResult[SPAWNINFO_QUEST])) - $this->createQuestSpawns(); - - return $this->spawnResult[SPAWNINFO_QUEST]; - } - - return []; - } -} - -trait profilerHelper -{ - public static $type = 0; // arena teams dont actually have one - public static $brickFile = 'profile'; // profile is multipurpose - - private static $subjectGUID = 0; - - public function selectRealms($fi) - { - $this->dbNames = []; - - foreach(Profiler::getRealms() as $idx => $r) - { - if (!empty($fi['sv']) && Profiler::urlize($r['name']) != Profiler::urlize($fi['sv']) && intVal($fi['sv']) != $idx) - continue; - - if (!empty($fi['rg']) && Profiler::urlize($r['region']) != Profiler::urlize($fi['rg'])) - continue; - - $this->dbNames[$idx] = 'Characters'; - } - - return !!$this->dbNames; - } -} - -/* - roight! - just noticed, that the filters on pages originally pointed to ?filter= - wich probably checked for correctness of inputs and redirected the correct values as a get-request - .. - well, as it is now, its working .. and you never change a running system .. -*/ - -abstract class Filter -{ - private static $wCards = ['*' => '%', '?' => '_']; - - public $error = false; // erronous search fields - - private $cndSet = []; - - protected $parentCats = []; // used to validate ty-filter - protected $inputFields = []; // list of input fields defined per page - protected $fiData = ['c' => [], 'v' =>[]]; - protected $formData = array( // data to fill form fields - 'form' => [], // base form - unsanitized - 'setCriteria' => [], // dynamic criteria list - index checked - 'setWeights' => [], // dynamic weights list - index checked - 'extraCols' => [], // extra columns for LV - added as required - 'reputationCols' => [] // simlar and exclusive to extraCols - added as required - ); - - // parse the provided request into a usable format - public function __construct($fromPOST = false, $opts = []) - { - if (!empty($opts['parentCats'])) - $this->parentCats = $opts['parentCats']; - - if ($fromPOST) - $this->evaluatePOST(); - else - { - // an error occured, while processing POST - if (isset($_SESSION['fiError'])) - { - $this->error = $_SESSION['fiError'] == get_class($this); - unset($_SESSION['fiError']); - } - - $this->evaluateGET(); - } - } - - // use to generate cacheKey for filterable pages - public function __sleep() - { - return ['formData']; - } - - public function mergeCat(&$cats) - { - foreach ($this->parentCats as $idx => $cat) - $cats[$idx] = $cat; - } - - private function &criteriaIterator() - { - if (!$this->fiData['c']) - return; - - for ($i = 0; $i < count($this->fiData['c']['cr']); $i++) - { - // throws a notice if yielded directly "Only variable references should be yielded by reference" - $v = [&$this->fiData['c']['cr'][$i], &$this->fiData['c']['crs'][$i], &$this->fiData['c']['crv'][$i]]; - yield $i => $v; - } - } - - - /***********************/ - /* get prepared values */ - /***********************/ - - public function getFilterString(array $override = [], array $addCr = []) - { - $_ = []; - foreach (array_merge($this->fiData['c'], $this->fiData['v'], $override) as $k => $v) - { - if (isset($addCr[$k])) - { - $v = $v ? array_merge((array)$v, (array)$addCr[$k]) : $addCr[$k]; - unset($addCr[$k]); - } - - if (is_array($v) && !empty($v)) - $_[$k] = $k.'='.implode(':', $v); - else if ($v !== '') - $_[$k] = $k.'='.$v; - } - - // no criteria were set, so no merge occured .. append - if ($addCr) - { - $_['cr'] = 'cr='.$addCr['cr']; - $_['crs'] = 'crs='.$addCr['crs']; - $_['crv'] = 'crv='.$addCr['crv']; - } - - return implode(';', $_); - } - - public function getExtraCols() - { - return $this->formData['extraCols']; - } - - public function getSetCriteria() - { - return $this->formData['setCriteria']; - } - - public function getSetWeights() - { - return $this->formData['setWeights']; - } - - public function getReputationCols() - { - return $this->formData['reputationCols']; - } - - public function getForm() - { - return $this->formData['form']; - } - - public function getConditions() - { - if (!$this->cndSet) - { - // values - $this->cndSet = $this->createSQLForValues(); - - // criteria - foreach ($this->criteriaIterator() as &$_cr) - $this->cndSet[] = $this->createSQLForCriterium($_cr); - - if ($this->cndSet) - array_unshift($this->cndSet, empty($this->fiData['v']['ma']) ? 'AND' : 'OR'); - } - - return $this->cndSet; - } - - - /**********************/ - /* input sanitization */ - /**********************/ - - private function evaluatePOST() - { - // doesn't need to set formData['form']; this happens in GET-step - - foreach ($this->inputFields as $inp => list($type, $valid, $asArray)) - { - if (!isset($_POST[$inp]) || $_POST[$inp] === '') - continue; - - $val = $_POST[$inp]; - $k = in_array($inp, ['cr', 'crs', 'crv']) ? 'c' : 'v'; - - if ($asArray) - { - $buff = []; - foreach ((array)$val as $v) - if ($v !== '' && $this->checkInput($type, $valid, $v) && $v !== '') - $buff[] = $v; - - if ($buff) - $this->fiData[$k][$inp] = $buff; - } - else if ($val !== '' && $this->checkInput($type, $valid, $val) && $val !== '') - $this->fiData[$k][$inp] = $val; - } - - $this->setWeights(); - $this->setCriteria(); - } - - private function evaluateGET() - { - if (empty($_GET['filter'])) - return; - - // squash into usable format - $post = []; - foreach (explode(';', $_GET['filter']) as $f) - { - if (!strstr($f, '=')) - { - $this->error = true; - continue; - } - - $_ = explode('=', $f); - $post[$_[0]] = $_[1]; - } - - $cr = $crs = $crv = []; - foreach ($this->inputFields as $inp => list($type, $valid, $asArray)) - { - if (!isset($post[$inp]) || $post[$inp] === '') - continue; - - $val = $post[$inp]; - $k = in_array($inp, ['cr', 'crs', 'crv']) ? 'c' : 'v'; - - if ($asArray) - { - $buff = []; - foreach (explode(':', $val) as $v) - if ($v !== '' && $this->checkInput($type, $valid, $v) && $v !== '') - $buff[] = $v; - - if ($buff) - { - if ($k == 'v') - $this->formData['form'][$inp] = $buff; - - $this->fiData[$k][$inp] = array_map(function ($x) { return strtr($x, Filter::$wCards); }, $buff); - } - } - else if ($val !== '' && $this->checkInput($type, $valid, $val) && $val !== '') - { - if ($k == 'v') - $this->formData['form'][$inp] = $val; - - $this->fiData[$k][$inp] = strtr($val, Filter::$wCards); - } - } - - $this->setWeights(); - $this->setCriteria(); - } - - private function setCriteria() // [cr]iterium, [cr].[s]ign, [cr].[v]alue - { - if (empty($this->fiData['c']['cr']) && empty($this->fiData['c']['crs']) && empty($this->fiData['c']['crv'])) - return; - else if (empty($this->fiData['c']['cr']) || empty($this->fiData['c']['crs']) || empty($this->fiData['c']['crv'])) - { - unset($this->fiData['c']['cr']); - unset($this->fiData['c']['crs']); - unset($this->fiData['c']['crv']); - - $this->error = true; - - return; - } - - $_cr = &$this->fiData['c']['cr']; - $_crs = &$this->fiData['c']['crs']; - $_crv = &$this->fiData['c']['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))); - if (count($_cr) > $min) - array_splice($_cr, $min); - - if (count($_crv) > $min) - array_splice($_crv, $min); - - if (count($_crs) > $min) - array_splice($_crs, $min); - - $this->error = true; - } - - for ($i = 0; $i < count($_cr); $i++) - { - // conduct filter specific checks & casts here - $unsetme = false; - if (isset($this->genericFilter[$_cr[$i]])) - { - $gf = $this->genericFilter[$_cr[$i]]; - switch ($gf[0]) - { - case FILTER_CR_NUMERIC: - $_ = $_crs[$i]; - if (!Util::checkNumeric($_crv[$i], $gf[2]) || !$this->int2Op($_)) - $unsetme = true; - break; - case FILTER_CR_BOOLEAN: - case FILTER_CR_FLAG: - case FILTER_CR_STAFFFLAG: - $_ = $_crs[$i]; - if (!$this->int2Bool($_)) - $unsetme = true; - break; - case FILTER_CR_ENUM: - if (!Util::checkNumeric($_crs[$i], NUM_REQ_INT)) - $unsetme = true; - break; - } - } - - if (!$unsetme && intval($_cr[$i]) && $_crs[$i] !== '' && $_crv[$i] !== '') - continue; - - unset($_cr[$i]); - unset($_crs[$i]); - unset($_crv[$i]); - - $this->error = true; - } - - $this->formData['setCriteria'] = array( - 'cr' => $_cr, - 'crs' => $_crs, - 'crv' => $_crv - ); - } - - private function setWeights() - { - if (empty($this->fiData['v']['wt']) && empty($this->fiData['v']['wtv'])) - return; - - $_wt = &$this->fiData['v']['wt']; - $_wtv = &$this->fiData['v']['wtv']; - - if (empty($_wt) && !empty($_wtv)) - { - unset($_wtv); - $this->error = true; - return; - } - - if (empty($_wtv) && !empty($_wt)) - { - unset($_wt); - $this->error = true; - return; - } - - $nwt = count($_wt); - $nwtv = count($_wtv); - - if ($nwt > $nwtv) - { - array_splice($_wt, $nwtv); - $this->error = true; - } - else if ($nwtv > $nwt) - { - array_splice($_wtv, $nwt); - $this->error = true; - } - - $this->formData['setWeights'] = [$_wt, $_wtv]; - } - - protected function checkInput($type, $valid, &$val, $recursive = false) - { - switch ($type) - { - case FILTER_V_EQUAL: - if (gettype($valid) == 'integer') - $val = intval($val); - else if (gettype($valid) == 'double') - $val = floatval($val); - else /* if (gettype($valid) == 'string') */ - $var = strval($val); - - if ($valid == $val) - return true; - - break; - case FILTER_V_LIST: - if (!Util::checkNumeric($val, NUM_CAST_INT)) - return false; - - foreach ($valid as $k => $v) - { - if (gettype($v) != 'array') - continue; - - if ($this->checkInput(FILTER_V_RANGE, $v, $val, true)) - return true; - - unset($valid[$k]); - } - - if (in_array($val, $valid)) - return true; - - break; - case FILTER_V_RANGE: - if (Util::checkNumeric($val, NUM_CAST_INT) && $val >= $valid[0] && $val <= $valid[1]) - return true; - - break; - case FILTER_V_CALLBACK: - if ($this->$valid($val)) - return true; - - break; - case FILTER_V_REGEX: - if (!preg_match($valid, $val)) - return true; - - break; - } - - if (!$recursive) - $this->error = true; - - return false; - } - - protected function modularizeString(array $fields, $string = '', $exact = false, $shortStr = false) - { - if (!$string && !empty($this->fiData['v']['na'])) - $string = $this->fiData['v']['na']; - - $qry = []; - $exPH = $exact ? '%s' : '%%%s%%'; - foreach ($fields as $n => $f) - { - $sub = []; - $parts = array_filter(explode(' ', $string)); - foreach ($parts as $p) - { - if ($p[0] == '-' && (mb_strlen($p) > 3 || $shortStr)) - $sub[] = [$f, sprintf($exPH, mb_substr($p, 1)), '!']; - else if ($p[0] != '-' && (mb_strlen($p) > 2 || $shortStr)) - $sub[] = [$f, sprintf($exPH, $p)]; - } - - // single cnd? - if (!$sub) - continue; - else if (count($sub) > 1) - array_unshift($sub, 'AND'); - else - $sub = $sub[0]; - - $qry[] = $sub; - } - - // single cnd? - if (!$qry) - $this->error = true; - else if (count($qry) > 1) - array_unshift($qry, 'OR'); - else - $qry = $qry[0]; - - return $qry; - } - - protected function int2Op(&$op) - { - switch ($op) - { - case 1: $op = '>'; return true; - case 2: $op = '>='; return true; - case 3: $op = '='; return true; - case 4: $op = '<='; return true; - case 5: $op = '<'; return true; - case 6: $op = '!='; return true; - default: return false; - } - } - - protected function int2Bool(&$op) - { - switch ($op) - { - case 1: $op = true; return true; - case 2: $op = false; return true; - default: return false; - } - } - - protected function list2Mask(array $list, $noOffset = false) - { - $mask = 0x0; - $o = $noOffset ? 0 : 1; // schoolMask requires this..? - - foreach ($list as $itm) - $mask += (1 << (intval($itm) - $o)); - - return $mask; - } - - - /**************************/ - /* create conditions from */ - /* generic criteria */ - /**************************/ - - private function genericBoolean($field, $op, $isString) - { - if ($this->int2Bool($op)) - { - $value = $isString ? '' : 0; - $operator = $op ? '!' : null; - - return [$field, $value, $operator]; - } - - return null; - } - - private function genericBooleanFlags($field, $value, $op) - { - if ($this->int2Bool($op)) - return [[$field, $value, '&'], $op ? $value : 0]; - - return null; - } - - private function genericString($field, $value, $strFlags) - { - if ($strFlags & STR_LOCALIZED) - $field .= '_loc'.User::$localeId; - - return $this->modularizeString([$field], (string)$value, $strFlags & STR_MATCH_EXACT, $strFlags & STR_ALLOW_SHORT); - } - - private function genericNumeric($field, &$value, $op, $castInt) - { - if (!Util::checkNumeric($value, $castInt)) - return null; - - if ($this->int2Op($op)) - return [$field, $value, $op]; - - return null; - } - - private function genericEnum($field, $value) - { - if (is_bool($value)) - return [$field, 0, ($value ? '>' : '<=')]; - else if ($value == FILTER_ENUM_ANY) // any - return [$field, 0, '!']; - else if ($value == FILTER_ENUM_NONE) // none - return [$field, 0]; - else if ($value !== null) - return [$field, $value]; - - return null; - } - - protected function genericCriterion(&$cr) - { - $gen = $this->genericFilter[$cr[0]]; - $result = null; - - if(!isset($gen[2])) - $gen[2] = 0; - - switch ($gen[0]) - { - case FILTER_CR_NUMERIC: - $result = $this->genericNumeric($gen[1], $cr[2], $cr[1], $gen[2]); - break; - case FILTER_CR_FLAG: - $result = $this->genericBooleanFlags($gen[1], $gen[2], $cr[1]); - break; - case FILTER_CR_STAFFFLAG: - if (User::isInGroup(U_GROUP_EMPLOYEE) && $cr[1] >= 0) - $result = $this->genericBooleanFlags($gen[1], (1 << $cr[1]), true); - break; - case FILTER_CR_BOOLEAN: - $result = $this->genericBoolean($gen[1], $cr[1], !empty($gen[2])); - break; - case FILTER_CR_STRING: - $result = $this->genericString($gen[1], $cr[2], $gen[2]); - break; - case FILTER_CR_ENUM: - if (isset($this->enums[$cr[0]][$cr[1]])) - $result = $this->genericEnum($gen[1], $this->enums[$cr[0]][$cr[1]]); - else if (intval($cr[1]) != 0) - $result = $this->genericEnum($gen[1], intval($cr[1])); - break; - case FILTER_CR_CALLBACK: - $result = $this->{$gen[1]}($cr, $gen[2], $gen[3]); - break; - case FILTER_CR_NYI_PH: // do not limit with not implemented filters - if (is_int($gen[2])) - return [$gen[2]]; - - // for nonsensical values; compare against 0 - if ($this->int2Op($cr[1]) && Util::checkNumeric($cr[2])) - { - if ($cr[1] == '=') - $cr[1] = '=='; - - return eval('return ('.$cr[2].' '.$cr[1].' 0);') ? [1] : [0]; - } - else - return [0]; - } - - if ($result && $gen[0] == FILTER_CR_NUMERIC && !empty($gen[3])) - $this->formData['extraCols'][] = $cr[0]; - - return $result; - } - - - /***********************************/ - /* create conditions from */ - /* non-generic values and criteria */ - /***********************************/ - - abstract protected function createSQLForCriterium(&$cr); - abstract protected function createSQLForValues(); -} - -?> diff --git a/includes/cfg.class.php b/includes/cfg.class.php new file mode 100644 index 00000000..56b287c8 --- /dev/null +++ b/includes/cfg.class.php @@ -0,0 +1,499 @@ + 'Site', 'Caching', 'Account', 'Session', 'Site Reputation', 'Google Analytics', 'Profiler', 0 => 'Other' + ); + + private const IDX_VALUE = 0; + private const IDX_FLAGS = 1; + private const IDX_CATEGORY = 2; + private const IDX_DEFAULT = 3; + private const IDX_COMMENT = 4; + + private static $store = []; // name => [value, flags, cat, default, comment] + private static $isLoaded = false; + + private static $rebuildScripts = array( + 'rep_req_border_uncommon' => ['globaljs'], + 'rep_req_border_rare' => ['globaljs'], + 'rep_req_border_epic' => ['globaljs'], + 'rep_req_border_legendary' => ['globaljs'], + 'profiler_enable' => ['realms', 'realmMenu'], + 'battlegroup' => ['realms', 'realmMenu'], + 'name_short' => ['searchplugin', 'searchboxBody', 'searchboxScript', 'demo'], + 'site_host' => ['searchplugin', 'searchboxBody', 'searchboxScript', 'demo', 'power', 'robots'], + 'static_host' => ['searchplugin', 'searchboxBody', 'searchboxScript', 'power'], + 'contact_email' => ['globaljs'], + 'locales' => ['globaljs'] + ); + + public static function load() : void + { + if (!DB::isConnected(DB_AOWOW)) + return; + + $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; + + if ($err = self::validate($value, $flags, $comment)) + { + self::throwError('Aowow config '.strtoupper($key).' failed validation and was skipped: '.$err); + continue; + } + + if ($flags & self::FLAG_INTERNAL) + { + self::throwError('Aowow config '.strtoupper($key).' is flagged as internaly generated and should not have been set in DB.'); + continue; + } + + if ($flags & self::FLAG_ON_LOAD_FN) + { + if (!method_exists(__CLASS__, $key)) + self::throwError('Aowow config '.strtoupper($key).' flagged for onLoadFN handling, but no handler was set'); + else + self::{$key}($value); + } + + if ($php) + ini_set(strtolower($key), $value); + + self::$store[strtolower($key)] = [$value, $flags, $catg, $default, $comment]; + } + + if (CLI && !count(self::$store)) + { + CLI::write('Cfg::load - aowow_config unexpectedly empty.', CLI::LOG_WARN); + return; + } + + self::$isLoaded = true; + } + + public static function add(string $key, /*int|string*/ $value) : string + { + if (!self::$isLoaded) + return 'used add() on uninitialized config'; + + if (!$key) + return 'empty option name given'; + + $key = strtolower($key); + + if (!preg_match(self::PATTERN_CONF_KEY_FULL, $key)) + return 'invalid chars in option name: [a-z 0-9 _ . -] are allowed'; + + if (isset(self::$store[$key])) + return 'this configuration option is already in use'; + + if ($errStr = self::validate($value)) + return $errStr; + + if (ini_get($key) === false || ini_set($key, $value) === false) + return 'this configuration option cannot be set'; + + $flags = self::FLAG_TYPE_STRING | self::FLAG_PHP; + 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]; + return ''; + } + + public static function delete(string $key) : string + { + if (!self::$isLoaded) + return 'used delete() on uninitialized config'; + + $key = strtolower($key); + + if (!isset(self::$store[$key])) + return 'configuration option not found'; + + if (self::$store[$key][self::IDX_FLAGS] & self::FLAG_PERSISTENT) + return 'can\'t delete persistent option'; + + if (!(self::$store[$key][self::IDX_FLAGS] & self::FLAG_PHP)) + return 'can\'t delete non-php option'; + + if (self::$store[$key][self::IDX_FLAGS] & self::FLAG_INTERNAL) + return 'can\'t delete internal option'; + + 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]); + return ''; + } + + public static function get(string $key, bool $fromDB = false, bool $fullInfo = false) // : int|float|string + { + $key = strtolower($key); + + if (!isset(self::$store[$key])) + { + if (self::$isLoaded) + self::throwError('cfg not defined: '.strtoupper($key)); + + return null; + } + + if ($fromDB && $fullInfo) + 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` = %s', $key); + if ($fullInfo) + return self::$store[$key]; + + return self::$store[$key][self::IDX_VALUE]; + } + + public static function set(string $key, /*int|string*/ $value, ?array &$rebuildFiles = []) : string + { + if (!self::$isLoaded) + return 'used set() on uninitialized config'; + + $key = strtolower($key); + + if (!isset(self::$store[$key])) + return 'configuration option not found'; + + [$oldValue, $flags, , , $comment] = self::$store[$key]; + + if ($flags & self::FLAG_INTERNAL) + return 'can\'t set an internal option directly'; + + if ($err = self::validate($value, $flags, $comment)) + return $err; + + if ($flags & self::FLAG_REQUIRED && !strlen($value)) + return 'empty value given for required config'; + + DB::Aowow()->qry('UPDATE ::config SET `value` = %s WHERE `key` = %s', $value, $key); + self::$store[$key][self::IDX_VALUE] = $value; + + // validate change + if ($flags & self::FLAG_ON_SET_FN) + { + $errMsg = ''; + if (!method_exists(__CLASS__, $key)) + $errMsg = 'Aowow config '.strtoupper($key).' flagged for onSetFN validation, but no handler was set'; + else + self::{$key}($value, $errMsg); + + if ($errMsg) + { + // rollback change + DB::Aowow()->qry('UPDATE ::config SET `value` = %s WHERE `key` = %s', $oldValue, $key); + self::$store[$key][self::IDX_VALUE] = $oldValue; + + return $errMsg; + } + } + + if ($flags & self::FLAG_ON_LOAD_FN) + { + if (!method_exists(__CLASS__, $key)) + return 'Aowow config '.strtoupper($key).' flagged for onLoadFN handling, but no handler was set'; + else + self::{$key}($value); + } + + // trigger setup build + return self::handleFileBuild($key, $rebuildFiles); + } + + public static function reset(string $key, ?array &$rebuildFiles = []) : string + { + if (!self::$isLoaded) + return 'used reset() on uninitialized config'; + + $key = strtolower($key); + + if (!isset(self::$store[$key])) + return 'configuration option not found'; + + [$oldValue, $flags, , $default, ] = self::$store[$key]; + + if ($flags & self::FLAG_INTERNAL) + return 'can\'t set an internal option directly'; + + if (!$default) + return 'config option has no default value'; + + // @eval .. some dafault values are supplied as bitmask or the likes + if (!($flags & Cfg::FLAG_TYPE_STRING)) + $default = @eval('return ('.$default.');'); + + DB::Aowow()->qry('UPDATE ::config SET `value` = %s WHERE `key` = %s', $default, $key); + self::$store[$key][self::IDX_VALUE] = $default; + + // validate change + if ($flags & self::FLAG_ON_SET_FN) + { + $errMsg = ''; + if (!method_exists(__CLASS__, $key)) + $errMsg = 'required onSetFN validator not set'; + else + self::{$key}($default, $errMsg); + + if ($errMsg) + { + // rollback change + DB::Aowow()->qry('UPDATE ::config SET `value` = %s WHERE `key` = %s', $oldValue, $key); + self::$store[$key][self::IDX_VALUE] = $oldValue; + + return $errMsg; + } + } + + // trigger setup build + return self::handleFileBuild($key, $rebuildFiles); + } + + public static function forCategory(int $category) : \Generator + { + foreach (self::$store as $k => [, $flags, $catg, , ]) + if ($catg == $category && !($flags & self::FLAG_INTERNAL)) + yield $k => self::$store[$k]; + } + + public static function applyToString(string $string, bool $nf = true) : string + { + return preg_replace_callback( + ['/CFG_([A-Z_]+)/', '/((HOST|STATIC)_URL)/'], + function ($m) use ($nf) { + if (!isset(self::$store[strtolower($m[1])])) + return $m[1]; + + [$val, $flags, , , ] = self::$store[strtolower($m[1])]; + return ($flags & (self::FLAG_TYPE_FLOAT | self::FLAG_TYPE_INT)) && $nf ? Lang::nf($val) : $val; + }, + $string + ); + } + + + /************/ + /* internal */ + /************/ + + private static function validate(&$value, int $flags = self::FLAG_TYPE_STRING | self::FLAG_PHP, string $comment = ' - ') : string + { + $value = preg_replace(self::PATTERN_INVALID_CHARS, '', $value); + + if (!($flags & (self::FLAG_TYPE_BOOL | self::FLAG_TYPE_FLOAT | self::FLAG_TYPE_INT | self::FLAG_TYPE_STRING))) + return 'no type set for value'; + + if ($flags & self::FLAG_TYPE_INT && !Util::checkNumeric($value, NUM_CAST_INT)) + return 'value must be integer'; + + if ($flags & self::FLAG_TYPE_FLOAT && !Util::checkNumeric($value, NUM_CAST_FLOAT)) + return 'value must be float'; + + if ($flags & self::FLAG_OPT_LIST) + { + $info = explode(' - ', $comment)[1]; + foreach (explode(', ', $info) as $option) + if (explode(':', $option)[0] == $value) + return ''; + + return 'value not in range'; + } + + if ($flags & self::FLAG_BITMASK) + { + $mask = 0x0; + $info = explode(' - ', $comment)[1]; + foreach (explode(', ', $info) as $option) + $mask |= (1 << explode(':', $option)[0]); + + if (!($value &= $mask) && ($flags & self::FLAG_REQUIRED)) + return 'value not in range'; + } + + if ($flags & self::FLAG_TYPE_BOOL) + $value = $value ? 1 : 0; + + return ''; + } + + private static function handleFileBuild(string $key, array &$rebuildFiles) : string + { + if (!isset(self::$rebuildScripts[$key])) + return ''; + + $msg = ''; + + if (CLI) + { + $rebuildFiles = array_merge($rebuildFiles, self::$rebuildScripts[$key]); + return ''; + } + + // not in CLI mode and build() can only be run from CLI. .. todo: other options..? + exec('php aowow --build='.implode(',', self::$rebuildScripts[$key]), $out); + foreach ($out as $o) + if (strstr($o, 'ERR')) + $msg .= explode('0m]', $o)[1]."
\n"; + + return $msg; + } + + private static function throwError($msg) : void + { + if (CLI) + CLI::write($msg, CLI::LOG_ERROR); + else + trigger_error($msg, E_USER_ERROR); + } + + private static function useSSL() : bool + { + return (($_SERVER['HTTPS'] ?? 'off') != 'off') || (self::$store['force_ssl'][self::IDX_VALUE] ?? 0); + } + + + /***************************/ + /* onSet/onLoad validators */ + /***************************/ + + private static function locales(int|string $value, ?string &$msg = '') : bool + { + if (!CLI) + return true; + + // note: Change is written to db and storage at this point, but can be rolled back. + if (CLISetup::setLocales()) + return true; + + $msg .= 'no valid locales set'; + return false; + } + + private static function acc_auth_mode(int|string $value, ?string &$msg = '') : bool + { + if ($value == 1 && !extension_loaded('gmp')) + { + $msg .= 'PHP extension GMP is required to use TrinityCore as auth source, but is not currently enabled.'; + return false; + } + + return true; + } + + private static function profiler_enable(int|string $value, ?string &$msg = '') : bool + { + if ($value != 1) + return true; + + return Profiler::queueStart($msg); + } + + private static function static_host(int|string $value, ?string &$msg = '') : bool + { + self::$store['static_url'] = array( // points js to images & scripts + (self::useSSL() ? 'https://' : 'http://').$value, + self::FLAG_PERSISTENT | self::FLAG_TYPE_STRING | self::FLAG_INTERNAL, + self::CAT_SITE, + null, // no default value + null, // no comment/info + ); + + return true; + } + + private static function site_host(int|string $value, ?string &$msg = '') : bool + { + self::$store['host_url'] = array( // points js to executable files + (self::useSSL() ? 'https://' : 'http://').$value, + self::FLAG_PERSISTENT | self::FLAG_TYPE_STRING | self::FLAG_INTERNAL, + self::CAT_SITE, + null, // no default value + null, // no comment/info + ); + + return true; + } + + private static function cache_mode(int|string $value, ?string &$msg = '') : bool + { + if ($value & 0x2 && !class_exists('\Memcached')) + { + $msg .= 'PHP extension Memcached is not enabled.'; + return false; + } + + return true; + } + + private static function screenshot_min_size(int|string $value, ?string &$msg = '') : bool + { + if ($value < 200) + { + $msg .= 'Value must be at least 200 (px).'; + return false; + } + + 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/community.class.php b/includes/community.class.php deleted file mode 100644 index db9ca888..00000000 --- a/includes/community.class.php +++ /dev/null @@ -1,534 +0,0 @@ - 0 AND cr.userId = ?d, cr.value, 0)) AS userRating, - SUM(IF( r.userId > 0 AND r.userId = ?d, 1, 0)) 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 - ?_comments_rates cr ON c.id = cr.commentId - LEFT JOIN - ?_reports r ON r.subject = c.id AND r.mode = 1 AND r.reason = 19 - WHERE - c.replyTo = ?d AND c.type = ?d AND c.typeId = ?d AND - ((c.flags & ?d) = 0 OR c.userId = ?d OR ?d) - GROUP BY - c.id - ORDER BY - rating ASC - '; - - private static $previewQuery = ' - SELECT - c.id, - c.body AS preview, - c.date, - c.replyTo AS commentid, - UNIX_TIMESTAMP() - c.date AS elapsed, - IF(c.flags & ?d, 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(cr.value), 0) AS rating, - a.displayName AS user - FROM - ?_comments c - JOIN - ?_account a ON c.userId = a.id - LEFT JOIN - ?_comments_rates cr ON cr.commentId = c.id AND cr.userId <> 0 - LEFT JOIN - ?_comments c2 ON c.replyTo = c2.id - WHERE - {c.userId = ?d AND} - {c.replyTo <> ?d AND} - {c.replyTo = ?d AND} - ((c.flags & ?d) = 0 OR c.userId = ?d OR ?d) - GROUP BY - c.id - ORDER BY - date DESC - LIMIT - ?d - '; - - private static function addSubject($type, $typeId) - { - if (!isset(self::$subjCache[$type][$typeId])) - self::$subjCache[$type][$typeId] = 0; - } - - private static function getSubjects() - { - foreach (self::$subjCache as $type => $ids) - { - $_ = array_filter(array_keys($ids), 'is_numeric'); - if (!$_) - continue; - - $cnd = [CFG_SQL_LIMIT_NONE, ['id', $_]]; - - switch ($type) - { - case TYPE_NPC: $obj = new CreatureList($cnd); break; - case TYPE_OBJECT: $obj = new GameobjectList($cnd); break; - case TYPE_ITEM: $obj = new ItemList($cnd); break; - case TYPE_ITEMSET: $obj = new ItemsetList($cnd); break; - case TYPE_QUEST: $obj = new QuestList($cnd); break; - case TYPE_SPELL: $obj = new SpellList($cnd); break; - case TYPE_ZONE: $obj = new ZoneList($cnd); break; - case TYPE_FACTION: $obj = new FactionList($cnd); break; - case TYPE_PET: $obj = new PetList($cnd); break; - case TYPE_ACHIEVEMENT: $obj = new AchievementList($cnd); break; - case TYPE_TITLE: $obj = new TitleList($cnd); break; - case TYPE_WORLDEVENT: $obj = new WorldEventList($cnd); break; - case TYPE_CLASS: $obj = new CharClassList($cnd); break; - case TYPE_RACE: $obj = new CharRaceList($cnd); break; - case TYPE_SKILL: $obj = new SkillList($cnd); break; - case TYPE_CURRENCY: $obj = new CurrencyList($cnd); break; - case TYPE_EMOTE: $obj = new EmoteList($cnd); break; - case TYPE_ENCHANTMENT: $obj = new EnchantmentList($cnd); break; - case TYPE_SOUND: $obj = new SoundList($cnd); break; - case TYPE_ICON: $obj = new IconList($cnd); break; - default: continue; - } - - foreach ($obj->iterate() as $id => $__) - self::$subjCache[$type][$id] = $obj->getField('name', true); - } - } - - public static function getCommentPreviews($params = [], &$nFound = 0) - { - /* - purged:0, <- doesnt seem to be used anymore - domain:'live' <- irrelevant for our case - */ - - $comments = DB::Aowow()->selectPage( - $nFound, - self::$previewQuery, - CC_FLAG_DELETED, - empty($params['user']) ? DBSIMPLE_SKIP : $params['user'], - empty($params['replies']) ? DBSIMPLE_SKIP : 0, // i dont know, how to switch the sign around - !empty($params['replies']) ? DBSIMPLE_SKIP : 0, - CC_FLAG_DELETED, - User::$id, - User::isInGroup(U_GROUP_COMMENTS_MODERATOR), - CFG_SQL_LIMIT_DEFAULT - ); - - foreach ($comments as $c) - self::addSubject($c['type'], $c['typeId']); - - self::getSubjects(); - - foreach ($comments as $idx => &$c) - { - if (!empty(self::$subjCache[$c['type']][$c['typeId']])) - { - // apply subject - $c['subject'] = self::$subjCache[$c['type']][$c['typeId']]; - - // format date - $c['date'] = date(Util::$dateFormatInternal, $c['date']); - - // remove commentid if not looking for replies - if (empty($params['replies'])) - unset($c['commentid']); - - // remove line breaks - $c['preview'] = strtr($c['preview'], ["\n" => ' ', "\r" => ' ']); - // limit whitespaces to one at a time - $c['preview'] = preg_replace('/\s+/', ' ', $c['preview']); - // limit previews to 100 chars + whatever it takes to make the last word full - if (mb_strlen($c['preview']) > 100) - { - $n = 0; - $b = []; - $parts = explode(' ', $c['preview']); - while ($n < 100 && $parts) - { - $_ = array_shift($parts); - $n += mb_strlen($_); - $b[] = $_; - } - - $c['preview'] = implode(' ', $b).'…'; - } - } - else - { - trigger_error('Comment '.$c['id'].' belongs to nonexistant subject.', E_USER_NOTICE); - unset($comments[$idx]); - } - } - - return $comments; - } - - public static function getCommentReplies($commentId, $limit = 0, &$nFound = 0) - { - $replies = []; - $query = $limit > 0 ? self::$commentQuery.' LIMIT '.$limit : self::$commentQuery; - - // get replies - $results = DB::Aowow()->selectPage($nFound, $query, User::$id, User::$id, $commentId, 0, 0, CC_FLAG_DELETED, User::$id, User::isInGroup(U_GROUP_COMMENTS_MODERATOR)); - foreach ($results as $r) - { - (new Markup($r['body']))->parseGlobalsFromText(self::$jsGlobals); - - $reply = array( - 'commentid' => $commentId, - 'id' => $r['id'], - 'body' => $r['body'], - 'username' => $r['user'], - 'roles' => $r['roles'], - 'creationdate' => date(Util::$dateFormatInternal, $r['date']), - 'lasteditdate' => date(Util::$dateFormatInternal, $r['editDate']), - 'rating' => (string)$r['rating'] - ); - - if ($r['userReported']) - $reply['reportedByUser'] = true; - - if ($r['userRating'] > 0) - $reply['votedByUser'] = true; - else if ($r['userRating'] < 0) - $reply['downvotedByUser'] = true; - - $replies[] = $reply; - } - - return $replies; - } - - public static function getScreenshotsForManager($type, $typeId, $userId = 0) - { - $screenshots = DB::Aowow()->select(' - SELECT s.id, a.displayName 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 100', - $userId ? DBSIMPLE_SKIP : $type, - $userId ? DBSIMPLE_SKIP : $typeId, - $userId ? $userId : DBSIMPLE_SKIP - ); - - $num = []; - foreach ($screenshots as $s) - { - if (empty($num[$s['type']][$s['typeId']])) - $num[$s['type']][$s['typeId']] = 1; - else - $num[$s['type']][$s['typeId']]++; - } - - // format data to meet requirements of the js - foreach ($screenshots as $idx => &$s) - { - $s['date'] = date(Util::$dateFormatInternal, $s['date']); - - $s['name'] = "Screenshot #".$s['id']; // what should we REALLY name it? - - if (isset($screenshots[$idx - 1])) - $s['prev'] = $idx - 1; - - if (isset($screenshots[$idx + 1])) - $s['next'] = $idx + 1; - - // order gives priority for 'status' - if (!($s['flags'] & CC_FLAG_APPROVED)) - { - $s['pending'] = 1; - $s['status'] = 0; - } - else - $s['status'] = 100; - - if ($s['flags'] & CC_FLAG_STICKY) - { - $s['sticky'] = 1; - $s['status'] = 105; - } - - if ($s['flags'] & CC_FLAG_DELETED) - { - $s['deleted'] = 1; - $s['status'] = 999; - } - - // something todo with massSelect .. am i doing this right? - if ($num[$s['type']][$s['typeId']] == 1) - $s['unique'] = 1; - - if (!$s['user']) - unset($s['user']); - } - - return $screenshots; - } - - public static function getScreenshotPagesForManager($all, &$nFound) - { - // 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) - { - // limit to one actually existing type each - $types = array_intersect(array_unique(array_column($pages, 'type')), array_keys(Util::$typeClasses)); - foreach ($types as $t) - { - $ids = []; - foreach ($pages as $row) - if ($row['type'] == $t) - $ids[] = $row['typeId']; - - if (!$ids) - continue; - - $tClass = new Util::$typeClasses[$t](array(['id', $ids], CFG_SQL_LIMIT_NONE)); - foreach ($pages as &$p) - if ($p['type'] == $t) - if ($tClass->getEntry($p['typeId'])) - $p['name'] = $tClass->getField('name', true); - } - - foreach ($pages as &$p) - { - if (empty($p['name'])) - { - trigger_error('Screenshot linked to nonexistant type/typeId combination: '.$p['type'].'/'.$p['typeId'], E_USER_NOTICE); - unset($p); - } - else - { - $nFound += $p['count']; - $p['date'] = date(Util::$dateFormatInternal, $p['date']); - } - } - } - - return $pages; - } - - private static function getComments($type, $typeId) - { - - $results = DB::Aowow()->query(self::$commentQuery, User::$id, User::$id, 0, $type, $typeId, CC_FLAG_DELETED, User::$id, (int)User::isInGroup(U_GROUP_COMMENTS_MODERATOR)); - $comments = []; - - // additional informations - $i = 0; - foreach ($results as $r) - { - (new Markup($r['body']))->parseGlobalsFromText(self::$jsGlobals); - - self::$jsGlobals[TYPE_USER][$r['userId']] = $r['userId']; - - $c = array( - 'commentv2' => 1, // always 1.. enables some features i guess..? - 'number' => $i++, // some iterator .. unsued? - 'id' => $r['id'], - 'date' => date(Util::$dateFormatInternal, $r['date']), - 'roles' => $r['roles'], - 'body' => $r['body'], - 'rating' => $r['rating'], - 'userRating' => $r['userRating'], - 'user' => $r['user'], - ); - - $c['replies'] = self::getCommentReplies($r['id'], 5, $c['nreplies']); - - if ($r['responseBody']) // adminResponse - { - $c['response'] = $r['responseBody']; - $c['responseroles'] = $r['responseRoles']; - $c['responseuser'] = $r['responseUser']; - - (new Markup($r['responseBody']))->parseGlobalsFromText(self::$jsGlobals); - } - - if ($r['editCount']) // lastEdit - $c['lastEdit'] = [date(Util::$dateFormatInternal, $r['editDate']), $r['editCount'], $r['editUser']]; - - if ($r['flags'] & CC_FLAG_STICKY) - $c['sticky'] = true; - - if ($r['flags'] & CC_FLAG_DELETED) - { - $c['deleted'] = true; - $c['deletedInfo'] = [date(Util::$dateFormatInternal, $r['deleteDate']), $r['deleteUser']]; - } - - if ($r['flags'] & CC_FLAG_OUTDATED) - $c['outofdate'] = true; - - $comments[] = $c; - } - - return $comments; - } - - public static function getVideos($typeOrUser = 0, $typeId = 0, &$nFound = 0) - { - $videos = DB::Aowow()->selectPage($nFound, " - SELECT v.id, a.displayName 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 ?# DESC} - {LIMIT ?d}", - 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_SQL_LIMIT_SEARCH : DBSIMPLE_SKIP - ); - - if ($typeOrUser <= 0) // not for search by type/typeId - { - foreach ($videos as $v) - self::addSubject($v['type'], $v['typeId']); - - self::getSubjects(); - } - - // format data to meet requirements of the js - foreach ($videos as &$v) - { - if ($typeOrUser <= 0) // not for search by type/typeId - { - if (!empty(self::$subjCache[$v['type']][$v['typeId']]) && !is_numeric(self::$subjCache[$v['type']][$v['typeId']])) - $v['subject'] = self::$subjCache[$v['type']][$v['typeId']]; - else - $v['subject'] = Lang::user('removed'); - } - - $v['date'] = date(Util::$dateFormatInternal, $v['date']); - $v['videoType'] = 1; // always youtube - - if (!$v['sticky']) - unset($v['sticky']); - - if (!$v['user']) - unset($v['user']); - } - - return $videos; - } - - public static function getScreenshots($typeOrUser = 0, $typeId = 0, &$nFound = 0) - { - $screenshots = DB::Aowow()->selectPage($nFound, " - SELECT s.id, a.displayName 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}", - 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_SQL_LIMIT_SEARCH : DBSIMPLE_SKIP - ); - - if ($typeOrUser <= 0) // not for search by type/typeId - { - foreach ($screenshots as $s) - self::addSubject($s['type'], $s['typeId']); - - self::getSubjects(); - } - - // format data to meet requirements of the js - foreach ($screenshots as &$s) - { - if ($typeOrUser <= 0) // not for search by type/typeId - { - if (!empty(self::$subjCache[$s['type']][$s['typeId']]) && !is_numeric(self::$subjCache[$s['type']][$s['typeId']])) - $s['subject'] = self::$subjCache[$s['type']][$s['typeId']]; - else - $s['subject'] = Lang::user('removed'); - } - - $s['date'] = date(Util::$dateFormatInternal, $s['date']); - - if (!$s['sticky']) - unset($s['sticky']); - - if (!$s['user']) - unset($s['user']); - } - - return $screenshots; - } - - public static function getAll($type, $typeId, &$jsg) - { - $result = array( - 'vi' => self::getVideos($type, $typeId), - 'sc' => self::getScreenshots($type, $typeId), - 'co' => self::getComments($type, $typeId) - ); - - Util::mergeJsGlobals($jsg, self::$jsGlobals); - - return $result; - } -} -?> diff --git a/includes/components/Conditions/Conditions.class.php b/includes/components/Conditions/Conditions.class.php new file mode 100644 index 00000000..9512d766 --- /dev/null +++ b/includes/components/Conditions/Conditions.class.php @@ -0,0 +1,770 @@ + + public const OP_LT = 2; // < + public const OP_GT_E = 3; // >= + public const OP_LT_E = 4; // <= + // Group, Entry, Id + public const SRC_NONE = 0; // null, null, null - use when adding external conditions + public const SRC_CREATURE_LOOT_TEMPLATE = 1; // tplEntry, itemId, null + public const SRC_DISENCHANT_LOOT_TEMPLATE = 2; // tplEntry, itemId, null + public const SRC_FISHING_LOOT_TEMPLATE = 3; // tplEntry, itemId, null + public const SRC_GAMEOBJECT_LOOT_TEMPLATE = 4; // tplEntry, itemId, null + public const SRC_ITEM_LOOT_TEMPLATE = 5; // tplEntry, itemId, null + public const SRC_MAIL_LOOT_TEMPLATE = 6; // tplEntry, itemId, null + public const SRC_MILLING_LOOT_TEMPLATE = 7; // tplEntry, itemId, null + public const SRC_PICKPOCKETING_LOOT_TEMPLATE = 8; // tplEntry, itemId, null + public const SRC_PROSPECTING_LOOT_TEMPLATE = 9; // tplEntry, itemId, null + public const SRC_REFERENCE_LOOT_TEMPLATE = 10; // tplEntry, itemId, null + public const SRC_SKINNING_LOOT_TEMPLATE = 11; // tplEntry, itemId, null + public const SRC_SPELL_LOOT_TEMPLATE = 12; // tplEntry, itemId, null + public const SRC_SPELL_IMPLICIT_TARGET = 13; // effectMask, spellId, null + public const SRC_GOSSIP_MENU = 14; // menuId, textId, null + public const SRC_GOSSIP_MENU_OPTION = 15; // menuId, optionId, null + public const SRC_CREATURE_TEMPLATE_VEHICLE = 16; // npcId, null, null + public const SRC_SPELL = 17; // null, spellId, null + public const SRC_SPELL_CLICK_EVENT = 18; // npcId, spellId, null + public const SRC_QUEST_AVAILABLE = 19; // null, questId, null + public const SRC_QUEST_SHOW_MARK = 20; // null, questId, null - ⚠️ unused as of 01.05.2024 + public const SRC_VEHICLE_SPELL = 21; // npcId, spellId, null + public const SRC_SMART_EVENT = 22; // id, entryGuid, srcType + public const SRC_NPC_VENDOR = 23; // npcId, itemId, null + public const SRC_SPELL_PROC = 24; // null, spellId, null +// public const SRC_SPELL_TERRAIN_SWAP = 25; // - ❌ reserved for TC master +// public const SRC_SPELL_PHASE = 26; // - ❌ reserved for TC master +// public const SRC_SPELL_GRAVEYARD = 27; // - ❌ reserved for TC master +// public const SRC_SPELL_AREATRIGGER = 28; // - ❌ reserved for TC master +// public const SRC_SPELL_CONVERSATION_LINE = 29; // - ❌ reserved for TC master + public const SRC_AREATRIGGER_CLIENT = 30; // null, atId, null +// public const SRC_SPELL_TRAINER_SPELL = 31; // - ❌ reserved for TC master +// public const SRC_SPELL_OBJECT_VISIBILITY = 32; // - ❌ reserved for TC master +// public const SRC_SPELL_SPAWN_GROUP = 33; // - ❌ reserved for TC master + + public const NONE = 0; // always true: NULL, NULL, NULL + public const AURA = 1; // aura is applied: spellId, effIdx, NULL + public const ITEM = 2; // owns item: itemId, count, includeBank? + public const ITEM_EQUIPPED = 3; // has item equipped: itemId, NULL, NULL + public const ZONEID = 4; // is in zone: areaId, NULL, NULL + public const REPUTATION_RANK = 5; // reputation status: factionId, rankMask, NULL + public const TEAM = 6; // is on team: teamId, NULL, NULL + public const SKILL = 7; // has skill: skillId, value, NULL + public const QUESTREWARDED = 8; // has finished quest: questId, NULL, NULL + public const QUESTTAKEN = 9; // has accepted quest: questId, NULL, NULL + public const DRUNKENSTATE = 10; // has drunken status: stateId, NULL, NULL + public const WORLD_STATE = 11; // world var == value: worldStateId, value, NULL + public const ACTIVE_EVENT = 12; // world event is active: eventId, NULL, NULL + public const INSTANCE_INFO = 13; // instance var == data: entry data, type + public const QUEST_NONE = 14; // never seen quest: questId, NULL, NULL + public const CHR_CLASS = 15; // belongs to classes: classMask, NULL, NULL + public const CHR_RACE = 16; // belongs to races: raceMask, NULL, NULL + public const ACHIEVEMENT = 17; // obtained achievement: achievementId, NULL, NULL + public const TITLE = 18; // obtained title: titleId, NULL, NULL + public const SPAWNMASK = 19; // spawnMask, NULL, NULL + public const GENDER = 20; // has gender: genderId, NULL, NULL + public const UNIT_STATE = 21; // unit has state: unitState, NULL, NULL + public const MAPID = 22; // is on map: mapId, NULL, NULL + public const AREAID = 23; // is in area: areaId, NULL, NULL + public const CREATURE_TYPE = 24; // creature is of type: creaturetypeId, NULL, NULL + public const SPELL = 25; // knows spell: spellId, NULL, NULL + public const PHASEMASK = 26; // is in phase: phaseMask, NULL, NULL + public const LEVEL = 27; // player level is..: level, comparator, NULL + public const QUEST_COMPLETE = 28; // has completed quest: questId, NULL, NULL + public const NEAR_CREATURE = 29; // is near creature: creatureId, dist, includeCorpse? + public const NEAR_GAMEOBJECT = 30; // is near gameObject: gameObjectId, dist, NULL + public const OBJECT_ENTRY_GUID = 31; // target is ???: objectType, id, guid + public const TYPE_MASK = 32; // target matches type: typeMask, NULL, NULL + public const RELATION_TO = 33; // Cond.Target, relation, NULL + public const REACTION_TO = 34; // Cond.Target, rankMask, NULL + public const DISTANCE_TO = 35; // distance to target Cond.Target, dist, comparator + public const ALIVE = 36; // target is alive: NULL, NULL, NULL + public const HP_VAL = 37; // targets absolute health: amount, comparator, NULL + public const HP_PCT = 38; // targets relative health: amount, comparator, NULL + public const REALM_ACHIEVEMENT = 39; // realmfirst was achieved: achievementId, NULL, NULL + public const IN_WATER = 40; // unit is swimming: NULL, NULL, NULL +// public const TERRAIN_SWAP = 41; // ❌ reserved for TC master + public const STAND_STATE = 42; // stateType, state, NULL + public const DAILY_QUEST_DONE = 43; // repeatable quest done: questId, NULL, NULL + public const CHARMED = 44; // unit is charmed: NULL, NULL, NULL + public const PET_TYPE = 45; // player has pet of type: petType, NULL, NULL + public const TAXI = 46; // player is on taxi: NULL, NULL, NULL + public const QUESTSTATE = 47; // questId, stateMask, NULL + public const QUEST_OBJECTIVE_PROGRESS = 48; // questId, objectiveIdx, count + public const DIFFICULTY_ID = 49; // map has difficulty id: difficulty, NULL, NULL + public const GAMEMASTER = 50; // player is GM: canBeGM?, NULL, NULL +// public const OBJECT_ENTRY_GUID_MASTER = 51; // ❌ reserved for TC master +// public const TYPE_MASK_MASTER = 52; // ❌ reserved for TC master +// public const BATTLE_PET_COUNT = 53; // ❌ reserved for TC master +// 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; + private const IDX_SRC_ID = 2; + private const IDX_SRC_FN = 3; + + private static $source = array( // [Group, Entry, Id, typeResolverFN] + self::SRC_NONE => [null, null, null, null], + self::SRC_CREATURE_LOOT_TEMPLATE => [Type::NPC, Type::ITEM, null, 'lootIdToNpc'], + self::SRC_DISENCHANT_LOOT_TEMPLATE => [Type::ITEM, Type::ITEM, null, 'disenchantIdToItem'], + self::SRC_FISHING_LOOT_TEMPLATE => [Type::ZONE, Type::ITEM, null, null], + self::SRC_GAMEOBJECT_LOOT_TEMPLATE => [Type::OBJECT, Type::ITEM, null, 'lootIdToGObject'], + self::SRC_ITEM_LOOT_TEMPLATE => [Type::ITEM, Type::ITEM, null, null], + self::SRC_MAIL_LOOT_TEMPLATE => [Type::QUEST, Type::ITEM, null, 'RewardTemplateToQuest'], + self::SRC_MILLING_LOOT_TEMPLATE => [Type::ITEM, Type::ITEM, null, null], + self::SRC_PICKPOCKETING_LOOT_TEMPLATE => [Type::NPC, Type::ITEM, null, 'PickpocketLootToNpc'], + self::SRC_PROSPECTING_LOOT_TEMPLATE => [Type::ITEM, Type::ITEM, null, null], + self::SRC_REFERENCE_LOOT_TEMPLATE => [null, Type::ITEM, null, null], + self::SRC_SKINNING_LOOT_TEMPLATE => [Type::NPC, Type::ITEM, null, 'SkinLootToNpc'], + self::SRC_SPELL_LOOT_TEMPLATE => [Type::SPELL, Type::ITEM, null, null], + self::SRC_SPELL_IMPLICIT_TARGET => [true, Type::SPELL, null, null], + self::SRC_GOSSIP_MENU => [true, true, null, null], + self::SRC_GOSSIP_MENU_OPTION => [true, true, null, null], + self::SRC_CREATURE_TEMPLATE_VEHICLE => [null, Type::NPC, null, null], + self::SRC_SPELL => [null, Type::SPELL, null, null], + self::SRC_SPELL_CLICK_EVENT => [Type::NPC, Type::SPELL, null, null], + self::SRC_QUEST_AVAILABLE => [null, Type::QUEST, null, null], + self::SRC_QUEST_SHOW_MARK => [null, Type::QUEST, null, null], + self::SRC_VEHICLE_SPELL => [Type::NPC, Type::SPELL, null, null], + self::SRC_SMART_EVENT => [true, true, true, null], + self::SRC_NPC_VENDOR => [Type::NPC, Type::ITEM, null, null], + self::SRC_SPELL_PROC => [null, Type::SPELL, null, null], + self::SRC_AREATRIGGER_CLIENT => [null, Type::AREATRIGGER, null, null] + ); + + private const IDX_CND_VAL1 = 0; + private const IDX_CND_VAL2 = 1; + private const IDX_CND_VAL3 = 2; + private const IDX_CND_FN = 3; + + private static $conditions = array(// [Value1, Value2, Value3, handlerFn] + self::NONE => [null, null, null, null], + self::AURA => [Type::SPELL, null, null, null], + self::ITEM => [Type::ITEM, true, true, null], + self::ITEM_EQUIPPED => [Type::ITEM, null, null, null], + self::ZONEID => [Type::ZONE, null, null, null], + self::REPUTATION_RANK => [Type::FACTION, true, null, null], + self::TEAM => [true, null, null, 'factionToSide'], + self::SKILL => [Type::SKILL, true, null, null], + self::QUESTREWARDED => [Type::QUEST, null, null, null], + self::QUESTTAKEN => [Type::QUEST, null, null, null], + self::DRUNKENSTATE => [true, null, null, null], + self::WORLD_STATE => [true, true, null, null], + self::ACTIVE_EVENT => [Type::WORLDEVENT, null, null, null], + self::INSTANCE_INFO => [true, true, true, null], + self::QUEST_NONE => [Type::QUEST, null, null, null], + self::CHR_CLASS => [Type::CHR_CLASS, null, null, 'maskToBits'], + self::CHR_RACE => [Type::CHR_RACE, null, null, 'maskToBits'], + self::ACHIEVEMENT => [Type::ACHIEVEMENT, null, null, null], + self::TITLE => [Type::TITLE, null, null, null], + self::SPAWNMASK => [true, null, null, null], + self::GENDER => [true, null, null, null], + self::UNIT_STATE => [true, null, null, null], + self::MAPID => [true, true, null, 'mapToZone'], + self::AREAID => [Type::ZONE, null, null, null], + self::CREATURE_TYPE => [true, null, null, null], + self::SPELL => [Type::SPELL, null, null, null], + self::PHASEMASK => [true, null, null, null], + self::LEVEL => [true, true, null, null], + self::QUEST_COMPLETE => [Type::QUEST, null, null, null], + self::NEAR_CREATURE => [Type::NPC, true, true, null], + self::NEAR_GAMEOBJECT => [Type::OBJECT, true, true, null], + self::OBJECT_ENTRY_GUID => [true, true, true, 'typeidToId'], + self::TYPE_MASK => [true, null, null, null], + self::RELATION_TO => [true, true, null, null], + self::REACTION_TO => [true, true, null, null], + self::DISTANCE_TO => [true, true, true, null], + self::ALIVE => [null, null, null, null], + self::HP_VAL => [true, true, null, null], + self::HP_PCT => [true, true, null, null], + self::REALM_ACHIEVEMENT => [Type::ACHIEVEMENT, null, null, null], + self::IN_WATER => [null, null, null, null], + self::STAND_STATE => [true, true, null, null], + self::DAILY_QUEST_DONE => [Type::QUEST, null, null, null], + self::CHARMED => [null, null, null, null], + self::PET_TYPE => [true, null, null, null], + self::TAXI => [null, null, null, null], + 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::STRING_ID => [true, null, null, null] + ); + + private $jsGlobals = []; + private $rows = []; + private $result = []; + private $resultExtra = []; + + + /******/ + /* IN */ + /******/ + + public function getBySource(int|array $type, int|array $group = 0, int|array $entry = 0, int|array $id = 0) : self + { + 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; + + $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]; + + $this->rows = array_merge($this->rows, DB::World()->selectAssoc( + 'SELECT `SourceTypeOrReferenceId`, `SourceEntry`, `SourceGroup`, `SourceId`, `ElseGroup`, + `ConditionTypeOrReference`, `ConditionTarget`, `ConditionValue1`, `ConditionValue2`, `ConditionValue3`, `ConditionStringValue1`, `NegativeCondition` + FROM conditions + WHERE %and + ORDER BY `SourceTypeOrReferenceId`, `SourceEntry`, `SourceGroup`, `ElseGroup` ASC', + $where + )); + + return $this; + } + + public function getByCondition(int $type, int $typeId/* , int ...$conditionIds */) : self + { + $lookups = []; // can only be in val1 for now + foreach (self::$conditions as $cId => [$cVal1, , , ]) + if ($type === $cVal1 /* && (!$conditionIds || in_array($cId, $conditionIds)) */ ) + { + if ($cId == self::CHR_CLASS || $cId == self::CHR_RACE) + $lookups[] = [DB::AND, [['c2.`ConditionTypeOrReference` = %i', $cId], ['(c2.`ConditionValue1` & %i) > 0', 1 << ($typeId - 1)]]]; + else + $lookups[] = [DB::AND, [['c2.`ConditionTypeOrReference` = %i', $cId], ['c2.`ConditionValue1` = %i', $typeId]]]; + } + + if (!$lookups) + return $this; + + $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.`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 %or + GROUP BY `SourceTypeOrReferenceId`,`SourceGroup`,`SourceEntry`,`SourceId`,`ElseGroup`,`ConditionTypeOrReference`,`ConditionTarget`,`ConditionValue1`,`ConditionValue2`,`ConditionValue3` + ORDER BY `SourceTypeOrReferenceId`, `SourceEntry`, `SourceGroup`, `ElseGroup` ASC', + $lookups + )); + + return $this; + } + + public function addExternalCondition(int $srcType, string $groupKey, array $condition, bool $orGroup = false) : void + { + if (!isset(self::$source[$srcType])) + return; + + [$cId, $cVal1, $cVal2, $cVal3, $cString] = array_pad(array_pad($condition, 5, 0), 6, ''); + if (!isset(self::$conditions[abs($cId)])) + return; + + while (substr_count($groupKey, ':') < 3) + $groupKey .= ':0'; // pad with missing srcEntry, SrcId, cndTarget to group key + + if (!$this->prepareSource($srcType, ...explode(':', $groupKey))) + return; + + if ($c = $this->prepareCondition($cId, $cVal1, $cVal2, $cVal3, $cString)) + { + if ($orGroup) + $this->result[$srcType][$groupKey][] = [$c]; + else if (!isset($this->result[$srcType][$groupKey][0])) + $this->result[$srcType][$groupKey][0] = [$c]; + else + $this->result[$srcType][$groupKey][0][] = $c; + } + } + + + /*******/ + /* OUT */ + /*******/ + + public function toListviewTab(string $id = 'conditions', string $name = '') : array + { + if (!$this->result) + return []; + + $out = []; + $nCnd = 0; + foreach ($this->result as $srcType => $srcData) + { + foreach ($srcData as $grpKey => $grpData) + { + if (!isset($this->resultExtra[$srcType][$grpKey])) + { + $nCnd++; + $out[$srcType][$grpKey] = $grpData; + } + else + { + $nCnd += count($this->resultExtra[$srcType][$grpKey]); + foreach ($this->resultExtra[$srcType][$grpKey] as $extraGrp) + $out[$srcType][$extraGrp] = $grpData; + } + } + } + + $data = ""; + + $tab = array( + 'data' => $data, + 'id' => $id, + 'name' => ($name ?: '$LANG.tab_conditions') . '+" ('.$nCnd.')"' + ); + + return $tab; + } + + // $keyX params are string(ref to lv column) or int(fixed value) + public function toListviewColumn(array &$lvRows, ?array &$extraCols = [], $keyGroup = 'id', $keyEntry = 0, $keyId = 0) : bool + { + if (!$this->result) + return false; + + $success = false; + foreach ($lvRows as &$row) + { + $srcKey = implode(':', array( + is_string($keyGroup) ? ($row[$keyGroup] ?? 0) : $keyGroup, + is_string($keyEntry) ? ($row[$keyEntry] ?? 0) : $keyEntry, + is_string($keyId) ? ($row[$keyId] ?? 0) : $keyId, + '' // cndTarget - 0 / 1 + )); + + foreach ($this->result as $cndData) + { + if (isset($cndData[$srcKey.'0'])) + { + $row['condition'][self::SRC_NONE][$srcKey.'0'] = $cndData[$srcKey.'0']; + $success = true; + } + + if (isset($cndData[$srcKey.'1'])) + { + $row['condition'][self::SRC_NONE][$srcKey.'1'] = $cndData[$srcKey.'1']; + $success = true; + } + } + } + + if ($success) + $extraCols[] = '$Listview.extraCols.condition'; + + 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; + } + + + /*********/ + /* Other */ + /*********/ + + public static function lootTableToConditionSource(string $lootTable) : int + { + return match ($lootTable) + { + 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 + { + if (!isset(self::$source[$srcType])) + return false; + + [$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, $cString1)) + $lvRow['condition'][$srcType][$groupKey][] = [$c]; + + return true; + } + + public function prepare() : bool + { + // itr over rows and prep data + if (!$this->rows) + return !empty($this->result); // respect previously added externalCnd + + foreach ($this->rows as $r) + { + if (!isset(self::$source[$r['SourceTypeOrReferenceId']])) + { + trigger_error('Conditions: skipping condition with unknown SourceTypeOrReferenceId #'.$r['SourceTypeOrReferenceId'], E_USER_WARNING); + continue; + } + + if (!isset(self::$conditions[$r['ConditionTypeOrReference']])) + { + trigger_error('Conditions: skipping condition with unknown ConditionTypeOrReference #'.$r['ConditionTypeOrReference'], E_USER_WARNING); + continue; + } + + [$sType, $sGroup, $sEntry, $sId, $cTarget] = $this->prepareSource($r['SourceTypeOrReferenceId'], $r['SourceGroup'], $r['SourceEntry'], $r['SourceId'], $r['ConditionTarget']); + if ($sType === null) + continue; + + $cnd = $this->prepareCondition( + $r['NegativeCondition'] ? -$r['ConditionTypeOrReference'] : $r['ConditionTypeOrReference'], + $r['ConditionValue1'], + $r['ConditionValue2'], + $r['ConditionValue3'], + $r['ConditionStringValue1'] + ); + if (!$cnd) + continue; + + $group = $sGroup . ':' . $sEntry . ':' . $sId . ':' . $cTarget; + $this->result[$r['SourceTypeOrReferenceId']] [$group] [$r['ElseGroup']] [] = $cnd; + } + + return true; + } + + private function prepareSource(int $sType, int $sGroup, int $sEntry, int $sId, int $cTarget) : array + { + // only one entry in array expected + if ($fn = self::$source[$sType][self::IDX_SRC_FN]) + if (!$this->$fn($sType, $sGroup, $sEntry, $sId, $cTarget)) + return [null, null, null, null, null]; + + [$grp, $entry, $id, $_] = self::$source[$sType]; + if (is_int($grp)) + $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; + + // more checks? not all sources can retarget + $cTarget = min(1, max(0, $cTarget)); + + return [$sType, $sGroup, $sEntry, $sId, $cTarget]; + } + + 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, $cString1)) + return []; + + $result = [$cId]; + + for ($i = 0; $i < 3; $i++) + { + $field = self::$conditions[abs($cId)][$i]; + + if (is_int($field)) + $this->jsGlobals[$field][${'cVal'.($i+1)}] = ${'cVal'.($i+1)}; + if ($field) + $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, $cString1) : bool + { + if ($cVal1 == 469) + $cVal1 = SIDE_ALLIANCE; + else if ($cVal1 == 67) + $cVal1 = SIDE_HORDE; + else + $cVal1 = SIDE_BOTH; + + return true; + } + + private function mapToZone($cndId, &$cVal1, &$cVal2, $cVal3, $cString1) : bool + { + // use g_zone_categories id + if ($cVal1 == 530) // outland + $cVal1 = 8; + else if ($cVal1 == 571) // northrend + $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` = %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; + $cVal2 = $id; + $cVal1 = 0; + } + else + { + trigger_error('Conditions - CONDITION_MAPID has invalid mapId #'.$cVal1, E_USER_WARNING); + return false; + } + + return true; + } + + private function maskToBits($cndId, &$cVal1, $cVal2, $cVal3, $cString1) : bool + { + if ($cndId == self::CHR_CLASS) + { + $cVal1 &= ChrClass::MASK_ALL; + foreach (Util::mask2bits($cVal1, 1) as $cId) + $this->jsGlobals[Type::CHR_CLASS][$cId] = $cId; + } + + if ($cndId == self::CHR_RACE) + { + $cVal1 &= ChrRace::MASK_ALL; + foreach (Util::mask2bits($cVal1, 1) as $rId) + $this->jsGlobals[Type::CHR_RACE][$rId] = $rId; + } + + return true; + } + + 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` = %i AND `guid` = %i', Type::NPC, $cVal3))) + $cVal2 = intVal($_); + + if ($cVal2) + $this->jsGlobals[Type::NPC][$cVal2] = $cVal2; + } + else if ($cVal1 == self::TYPEID_GAMEOBJECT) + { + if ($cVal3 && ($_ = DB::Aowow()->selectCell('SELECT `typeId` FROM ::spawns WHERE `type` = %i AND `guid` = %i', Type::OBJECT, $cVal3))) + $cVal2 = intVal($_); + + if ($cVal2) + $this->jsGlobals[Type::OBJECT][$cVal2] = $cVal2; + } + else // Player or Corpse .. no guid + $cVal2 = $cVal3 = 0; + + // maybe prepare other types? + return true; + } + + private function lootIdToNpc(int $sType, int $sGroup, int $sEntry, int $sId, int $cTarget) : bool + { + if (!$sGroup) + { + trigger_error('Conditions::lootToNpc - skipping reference to creature_loot_template entry 0', E_USER_WARNING); + return false; + } + + if ($npcs = DB::Aowow()->selectCol('SELECT `id` FROM ::creature WHERE `lootId` = %i', $sGroup)) + { + $group = $sGroup . ':' . $sEntry . ':' . $sId . ':' . $cTarget; + foreach ($npcs as $npcId) + { + $this->jsGlobals[Type::NPC][$npcId] = $npcId; + $this->resultExtra[$sType][$group][] = $npcId . ':' . $sEntry . ':' . $sId . ':' . $cTarget; + } + + return true; + } + + trigger_error('Conditions::lootToNpc - creature_loot_template #'.$sGroup.' unreferenced?', E_USER_WARNING); + return false; + } + + private function disenchantIdToItem(int $sType, int $sGroup, int $sEntry, int $sId, int $cTarget) : bool + { + if (!$sGroup) + { + trigger_error('Conditions::disenchantIdToItem - skipping reference to disenchant_loot_template entry 0', E_USER_WARNING); + return false; + } + + if ($items = DB::Aowow()->selectCol('SELECT `id` FROM ::items WHERE `disenchantId` = %i', $sGroup)) + { + $group = $sGroup . ':' . $sEntry . ':' . $sId . ':' . $cTarget; + foreach ($items as $itemId) + { + $this->jsGlobals[Type::ITEM][$itemId] = $itemId; + $this->resultExtra[$sType][$group][] = $itemId . ':' . $sEntry . ':' . $sId . ':' . $cTarget; + } + + return true; + } + + trigger_error('Conditions::disenchantIdToItem - disenchant_loot_template #'.$sGroup.' unreferenced?', E_USER_WARNING); + return false; + } + + private function lootIdToGObject(int $sType, int $sGroup, int $sEntry, int $sId, int $cTarget) : bool + { + if (!$sGroup) + { + trigger_error('Conditions::lootIdToGObject - skipping reference to gameobject_loot_template entry 0', E_USER_WARNING); + return false; + } + + if ($gos = DB::Aowow()->selectCol('SELECT `id` FROM ::objects WHERE `lootId` = %i', $sGroup)) + { + $group = $sGroup . ':' . $sEntry . ':' . $sId . ':' . $cTarget; + foreach ($gos as $goId) + { + $this->jsGlobals[Type::OBJECT][$goId] = $goId; + $this->resultExtra[$sType][$group][] = $goId . ':' . $sEntry . ':' . $sId . ':' . $cTarget; + } + + return true; + } + + trigger_error('Conditions::lootIdToGObject - gameobject_loot_template #'.$sGroup.' unreferenced?', E_USER_WARNING); + return false; + } + + private function RewardTemplateToQuest(int $sType, int $sGroup, int $sEntry, int $sId, int $cTarget) : bool + { + if (!$sGroup) + { + trigger_error('Conditions::RewardTemplateToQuest - skipping reference to mail_loot_template entry 0', E_USER_WARNING); + return false; + } + + if ($quests = DB::Aowow()->selectCol('SELECT `id` FROM ::quests WHERE `rewardMailTemplateId` = %i', $sGroup)) + { + $group = $sGroup . ':' . $sEntry . ':' . $sId . ':' . $cTarget; + foreach ($quests as $questId) + { + $this->jsGlobals[Type::QUEST][$questId] = $questId; + $this->resultExtra[$sType][$group][] = $questId . ':' . $sEntry . ':' . $sId . ':' . $cTarget; + } + + return true; + } + + trigger_error('Conditions::RewardTemplateToQuest - mail_loot_template #'.$sGroup.' unreferenced?', E_USER_WARNING); + return false; + } + + private function PickpocketLootToNpc(int $sType, int $sGroup, int $sEntry, int $sId, int $cTarget) : bool + { + if (!$sGroup) + { + trigger_error('Conditions::PickpocketLootToNpc - skipping reference to pickpocketing_loot_template entry 0', E_USER_WARNING); + return false; + } + + if ($npcs = DB::Aowow()->selectCol('SELECT `id` FROM ::creature WHERE `pickpocketLootId` = %i', $sGroup)) + { + $group = $sGroup . ':' . $sEntry . ':' . $sId . ':' . $cTarget; + foreach ($npcs as $npcId) + { + $this->jsGlobals[Type::NPC][$npcId] = $npcId; + $this->resultExtra[$sType][$group][] = $npcId . ':' . $sEntry . ':' . $sId . ':' . $cTarget; + } + + return true; + } + + trigger_error('Conditions::PickpocketLootToNpc - pickpocketing_loot_template #'.$sGroup.' unreferenced?', E_USER_WARNING); + return false; + } + + private function SkinLootToNpc(int $sType, int $sGroup, int $sEntry, int $sId, int $cTarget) : bool + { + if (!$sGroup) + { + trigger_error('Conditions::SkinLootToNpc - skipping reference to skinning_loot_template entry 0', E_USER_WARNING); + return false; + } + + if ($npcs = DB::Aowow()->selectCol('SELECT `id` FROM ::creature WHERE `skinLootId` = %i', $sGroup)) + { + $group = $sGroup . ':' . $sEntry . ':' . $sId . ':' . $cTarget; + foreach ($npcs as $npcId) + { + $this->jsGlobals[Type::NPC][$npcId] = $npcId; + $this->resultExtra[$sType][$group][] = $npcId . ':' . $sEntry . ':' . $sId . ':' . $cTarget; + } + + return true; + } + + trigger_error('Conditions::SkinLootToNpc - skinning_loot_template #'.$sGroup.' unreferenced?', E_USER_WARNING); + return false; + } +} + +?> diff --git a/includes/components/SmartAI/SmartAI.class.php b/includes/components/SmartAI/SmartAI.class.php new file mode 100644 index 00000000..3bf9b55e --- /dev/null +++ b/includes/components/SmartAI/SmartAI.class.php @@ -0,0 +1,837 @@ +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); + return null; + } + + private function numRange(int $min, int $max, bool $isTime) : string + { + if ($isTime) + return Util::createNumRange($min, $max, ' – ', fn($x) => DateTime::formatTimeElapsedFloat($x)); + + return Util::createNumRange($min, $max, ' – ') ?: 0; + } + + private function formatTime(int $time, int $_, bool $isMilliSec) : string + { + if (!$time) + return ''; + + 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))) + $cf[] = $x; + + return Lang::concat($cf); + } + + 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))) + $nf[] = $x; + + return Lang::concat($nf ?: [Lang::smartAI('empty')]); + } + + 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))) + $df[] = $x; + + return Lang::concat($df ?: [Lang::smartAI('empty')]); + } + + 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))) + $gf[] = $x; + + return Lang::concat($gf ?: [Lang::smartAI('empty')]); + } + + 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))) + $sf[] = $x; + + return Lang::concat($sf ?: [Lang::smartAI('empty')]); + } + + 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 = []; + + for ($i = 1; $i <= $max; $i <<= 1) + if (($flags & $i) && ($x = Lang::unit($field, $i))) + $uf[] = $x; + + return Lang::concat($uf ?: [Lang::smartAI('empty')]); + } + + private function unitFieldBytes1(int $flags, int $idx) : string + { + switch ($idx) + { + case 0: + case 3: + return Lang::unit('bytes1', 'bytesIdx', $idx).Lang::main('colon').(Lang::unit('bytes1', $idx, $flags) ?? Lang::unit('bytes1', 'valueUNK', [$flags, $idx])); + case 2: + $buff = []; + for ($i = 1; $i <= 0x10; $i <<= 1) + if (($flags & $i) && ($x = Lang::unit('bytes1', $idx, $flags))) + $buff[] = $x; + + return Lang::unit('bytes1', 'bytesIdx', $idx).Lang::main('colon').($buff ? Lang::concat($buff) : Lang::unit('bytes1', 'valueUNK', [$flags, $idx])); + default: + return Lang::unit('bytes1', 'idxUNK', [$idx]); + } + } + + private function summonType(int $x) : string + { + return Lang::smartAI('summonTypes', $x) ?? Lang::smartAI('summonType', 'summonTypeUNK', [$x]); + } + + private function sheathState(int $x) : string + { + return Lang::smartAI('sheaths', $x) ?? Lang::smartAI('sheathUNK', [$x]); + } + + private function aiTemplate(int $x) : string + { + return Lang::smartAI('aiTpl', $x) ?? Lang::smartAI('aiTplUNK', [$x]); + } + + private function reactState(int $x) : string + { + return Lang::smartAI('reactStates', $x) ?? Lang::smartAI('reactStateUNK', [$x]); + } + + private function powerType(int $x) : string + { + return Lang::spell('powerTypes', $x) ?? Lang::smartAI('powerTypeUNK', [$x]); + } + + private function hostilityMode(int $x) : string + { + return Lang::smartAI('hostilityModes', $x) ?? Lang::smartAI('hostilityModeUNK', [$x]); + } + + private function motionType(int $x) : string + { + return Lang::smartAI('motionTypes', $x) ?? Lang::smartAI('motionTypeUNK', [$x]); + } + + private function lootState(int $x) : string + { + return Lang::smartAI('lootStates', $x) ?? Lang::smartAI('lootStateUNK', [$x]); + } + private function weatherState(int $x) : string + { + return Lang::smartAI('weatherStates', $x) ?? Lang::smartAI('weatherStateUNK', [$x]); + } + + private function magicSchool(int $x) : string + { + return Lang::getMagicSchools($x); + } +} + +class SmartAI +{ + public const SRC_TYPE_CREATURE = 0; + public const SRC_TYPE_OBJECT = 1; + public const SRC_TYPE_AREATRIGGER = 2; + public const SRC_TYPE_ACTIONLIST = 9; + + public const CAST_FLAG_INTERRUPT_PREV = 0x01; // Interrupt any spell casting + public const CAST_FLAG_TRIGGERED = 0x02; // Triggered (this makes spell cost zero mana and have no cast time) +// public const CAST_FORCE_CAST = 0x04; // Forces cast even if creature is out of mana or out of range +// public const CAST_NO_MELEE_IF_OOM = 0x08; // Prevents creature from entering melee if out of mana or out of range +// 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; + public const REACT_AGGRESSIVE = 2; + public const REACT_ASSIST = 3; + + public const SUMMON_TIMED_OR_DEAD_DESPAWN = 1; + public const SUMMON_TIMED_OR_CORPSE_DESPAWN = 2; + public const SUMMON_TIMED_DESPAWN = 3; + public const SUMMON_TIMED_DESPAWN_OOC = 4; + public const SUMMON_CORPSE_DESPAWN = 5; + public const SUMMON_CORPSE_TIMED_DESPAWN = 6; + public const SUMMON_DEAD_DESPAWN = 7; + public const SUMMON_MANUAL_DESPAWN = 8; + + public const TEMPLATE_BASIC = 0; // + public const TEMPLATE_CASTER = 1; // +JOIN: target_param1 as castFlag + public const TEMPLATE_TURRET = 2; // +JOIN: target_param1 as castflag + public const TEMPLATE_PASSIVE = 3; // + public const TEMPLATE_CAGED_GO_PART = 4; // + public const TEMPLATE_CAGED_NPC_PART = 5; // + + public const SPAWN_FLAG_NONE = 0x00; + 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 = []; + + public string $css = <<baseEntry = $miscData['baseEntry'] ?? 0; + $this->title = $miscData['title'] ?? ''; + $this->teleportTargetArea = $miscData['teleportTargetArea'] ?? 0; + + 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), + 'condition' => (new Conditions())->getBySource(Conditions::SRC_SMART_EVENT, $r['id'] + 1, $entry, $srcType) + ); + } + } + + + /*********************/ + /* Lookups by action */ + /*********************/ + + public static function getOwnerOfNPCSummon(int $npcId, int $typeFilter = 0) : array + { + if ($npcId <= 0) + return []; + + $lookup = array( + SmartAction::ACTION_SUMMON_CREATURE => [1 => $npcId], + SmartAction::ACTION_MOUNT_TO_ENTRY_OR_MODEL => [1 => $npcId] + ); + + 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()->selectAssoc('SELECT `summonerType` AS "0", `summonerId` AS "1" FROM creature_summon_groups WHERE `entry` = %i', $npcId)) + foreach ($sgs as [$type, $typeId]) + $result[$type][] = $typeId; + + return $result; + } + + public static function getOwnerOfObjectSummon(int $objectId, int $typeFilter = 0) : array + { + if ($objectId <= 0) + return []; + + $lookup = array( + SmartAction::ACTION_SUMMON_GO => [1 => $objectId] + ); + + 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; + + return self::getActionOwner($lookup, $typeFilter); + } + + public static function getOwnerOfSpellCast(int $spellId, int $typeFilter = 0) : array + { + if ($spellId <= 0) + return []; + + $lookup = array( + SmartAction::ACTION_CAST => [1 => $spellId], + SmartAction::ACTION_ADD_AURA => [1 => $spellId], + SmartAction::ACTION_SELF_CAST => [1 => $spellId], + SmartAction::ACTION_CROSS_CAST => [1 => $spellId], + SmartAction::ACTION_INVOKER_CAST => [1 => $spellId] + ); + + return self::getActionOwner($lookup, $typeFilter); + } + + public static function getOwnerOfSoundPlayed(int $soundId, int $typeFilter = 0) : array + { + if ($soundId <= 0) + return []; + + $lookup = array( + SmartAction::ACTION_SOUND => [1 => $soundId] + ); + + return self::getActionOwner($lookup, $typeFilter); + } + + // lookup: SmartActionId => [[paramIdx => value], ...] + private static function getActionOwner(array $lookup, int $typeFilter = 0) : array + { + $qParts = []; + $result = []; + $genFilter = $talFilter = []; + switch ($typeFilter) + { + case Type::NPC: + $genFilter = [self::SRC_TYPE_CREATURE, self::SRC_TYPE_ACTIONLIST]; + $talFilter = [self::SRC_TYPE_CREATURE]; + break; + case Type::OBJECT: + $genFilter = [self::SRC_TYPE_OBJECT, self::SRC_TYPE_ACTIONLIST]; + $talFilter = [self::SRC_TYPE_OBJECT]; + break; + case Type::AREATRIGGER: + $genFilter = [self::SRC_TYPE_AREATRIGGER, self::SRC_TYPE_ACTIONLIST]; + $talFilter = [self::SRC_TYPE_AREATRIGGER]; + break; + } + + $where = $qParts = []; + foreach ($lookup as $action => $params) + { + $pq = []; + $aq = [DB::AND, [['`action_type` = %i', $action], [DB::OR, &$pq]]]; + foreach ($params as $idx => $p) + $pq[] = ["`action_param$idx` = %i", $p]; + + $qParts[] = $aq; + unset($pq); + } + + 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 = $where = []; + foreach ($smartTAL as [, $eog]) + { + // SmartAction::ACTION_CALL_TIMED_ACTIONLIST + $q[] = [DB::AND, array( + ['`action_type` = %i', SmartAction::ACTION_CALL_TIMED_ACTIONLIST], + ['`action_param1` = %i', $eog] + )]; + + // SmartAction::ACTION_CALL_RANDOM_TIMED_ACTIONLIST + $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[] = [DB::AND, array( + ['`action_type` = %i', SmartAction::ACTION_CALL_RANDOM_RANGE_TIMED_ACTIONLIST], + ['%i BETWEEN `action_param1` AND `action_param2`', $eog] + )]; + } + + 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, $_); + } + + // filter guids for entries + if ($smartG = array_filter($smartS, fn($x) => $x[1] < 0)) + { + $smartS = array_diff_key($smartS, $smartG); + + $where = []; + foreach ($smartG as [$st, $eog]) + { + if ($st == self::SRC_TYPE_CREATURE) + $where[] = [DB::AND, [['`type` = %i', Type::NPC], ['`guid` = %i', -$eog]]]; + else if ($st == self::SRC_TYPE_OBJECT) + $where[] = [DB::AND, [['`type` = %i', Type::OBJECT], ['`guid` = %i', -$eog]]]; + } + + if ($where) + { + $owner = DB::Aowow()->selectAssoc('SELECT `type`, `typeId` FROM ::spawns WHERE %or', $where); + foreach ($owner as $o) + $result[$o['type']][] = $o['typeId']; + } + } + + foreach ($smartS as [$st, $eog]) + { + if ($st == self::SRC_TYPE_CREATURE) + $result[Type::NPC][] = $eog; + else if ($st == self::SRC_TYPE_OBJECT) + $result[Type::OBJECT][] = $eog; + else if ($st == self::SRC_TYPE_AREATRIGGER) + $result[Type::AREATRIGGER][] = $eog; + } + + return $result; + } + + + /********************/ + /* Lookups by owner */ + /********************/ + + public static function getNPCSummonsForOwner(int $entry, int $srcType = self::SRC_TYPE_CREATURE) : array + { + // action => paramIdx with npcIds/spawnGoupIds + $lookup = array( + SmartAction::ACTION_SUMMON_CREATURE => [1], + SmartAction::ACTION_MOUNT_TO_ENTRY_OR_MODEL => [1], + SmartAction::ACTION_SPAWN_SPAWNGROUP => [1] + ); + + $result = self::getOwnerAction($srcType, $entry, $lookup, $moreInfo); + + // can skip lookups for SmartAction::ACTION_SUMMON_CREATURE_GROUP as creature_summon_groups already contains summoner info + 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` = %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` = %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); + } + + return $result; + } + + public static function getObjectSummonsForOwner(int $entry, int $srcType = self::SRC_TYPE_CREATURE) : array + { + // action => paramIdx with gobIds/spawnGoupIds + $lookup = array( + SmartAction::ACTION_SUMMON_GO => [1], + SmartAction::ACTION_SPAWN_SPAWNGROUP => [1] + ); + + $result = self::getOwnerAction($srcType, $entry, $lookup, $moreInfo); + + 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` = %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); + } + + return $result; + } + + public static function getSpellCastsForOwner(int $entry, int $srcType = self::SRC_TYPE_CREATURE) : array + { + // action => paramIdx with spellIds + $lookup = array( + SmartAction::ACTION_CAST => [1], + SmartAction::ACTION_ADD_AURA => [1], + SmartAction::ACTION_INVOKER_CAST => [1], + SmartAction::ACTION_CROSS_CAST => [1] + ); + + return self::getOwnerAction($srcType, $entry, $lookup); + } + + public static function getSoundsPlayedForOwner(int $entry, int $srcType = self::SRC_TYPE_CREATURE) : array + { + // action => paramIdx with soundIds + $lookup = array( + SmartAction::ACTION_SOUND => [1] + ); + + return self::getOwnerAction($srcType, $entry, $lookup); + } + + // lookup: [SmartActionId => [paramIdx, ...], ...] + private static function getOwnerAction(int $sourceType, int $entry, array $lookup, ?array &$moreInfo = []) : array + { + 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` = %i AND `action_type` IN %in AND `entryOrGUID` IN %in'; + + $smartScripts = DB::World()->selectAssoc($actionQuery, $sourceType, array_merge(array_keys($lookup), SmartAction::ACTION_ALL_TIMED_ACTION_LISTS), [$entry]); + $smartResults = []; + $smartTALs = []; + foreach ($smartScripts as $s) + { + if ($s['action_type'] == SmartAction::ACTION_SPAWN_SPAWNGROUP) + $moreInfo[SmartAction::ACTION_SPAWN_SPAWNGROUP][] = $s['action_param1']; + else if (in_array($s['action_type'], array_keys($lookup))) + { + foreach ($lookup[$s['action_type']] as $p) + $smartResults[] = $s['action_param'.$p]; + } + else if ($s['action_type'] == SmartAction::ACTION_CALL_TIMED_ACTIONLIST) + $smartTALs[] = $s['action_param1']; + else if ($s['action_type'] == SmartAction::ACTION_CALL_RANDOM_TIMED_ACTIONLIST) + { + for ($i = 1; $i < 7; $i++) + if ($s['action_param'.$i]) + $smartTALs[] = $s['action_param'.$i]; + } + else if ($s['action_type'] == SmartAction::ACTION_CALL_RANDOM_RANGE_TIMED_ACTIONLIST) + { + for ($i = $s['action_param1']; $i <= $s['action_param2']; $i++) + $smartTALs[] = $i; + } + } + + if ($smartTALs) + { + if ($TALActList = DB::World()->selectAssoc($actionQuery, self::SRC_TYPE_ACTIONLIST, array_keys($lookup), $smartTALs)) + { + foreach ($TALActList as $e) + { + foreach ($lookup[$e['action_type']] as $i) + { + if ($e['action_type'] == SmartAction::ACTION_SPAWN_SPAWNGROUP) + $moreInfo[SmartAction::ACTION_SPAWN_SPAWNGROUP][] = $e['action_param'.$i]; + else + $smartResults[] = $e['action_param'.$i]; + } + } + } + } + + return $smartResults; + } + + + /******************************/ + /* Structured Lisview Display */ + /******************************/ + + private function &iterate() : \Generator + { + reset($this->rawData); + + foreach ($this->rawData as $k => $__) + { + $this->itr = &$this->rawData[$k]; + + yield $this->itr; + } + } + + public function prepare() : bool + { + if (!$this->rawData) + return false; + + if ($this->result) + return true; + + $visibleCols = (1 << 0) | (1 << 2) | (1 << 4); + + foreach ($this->iterate() as $__) + { + $rowIdx = Util::createHash(8); + + if ($this->itr['action']->type == SmartAction::ACTION_TALK || $this->itr['action']->type == SmartAction::ACTION_SIMPLE_TALK) + if ($ts = $this->itr['target']->getTalkSource()) + $this->initQuotes($ts); + + [$evtBody, $evtFooter] = $this->itr['event']->process(); + [$actBody, $actFooter] = $this->itr['action']->process(); + + $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()) + $visibleCols |= (1 << 1); + + if ($this->itr['event']->chance != 100) + $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 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 width=100% align=right][i][small class=q0]'.$actFooter.'[/small][/i][/div]' : ''), + $this->itr['condition']->toMarkupTag() + ); + } + + $th = array( + ['#' , '24px'], + ['Phase', '48px'], + ['Event', '30%%'], + ['Chance', '60px'], + ['Action', 'auto'], + ['Condition', 'auto'] + ); + + for ($i = 0, $j = count($th); $i < $j; $i++) + { + if ($visibleCols & (1 << $i)) + continue; + + unset($th[$i]); + foreach ($this->result as &$r) + unset($r[$i]); + + 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 + $this->tabs[0] = $tbl; + + return true; + } + + public function getMarkup() : ?Markup + { + # id | event (footer phase) | chance | action + target + + if (!$this->rawData) + return null; + + $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]%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').'"]'.$data.'[/tab]'; + if ($guid) + $tabs .= $buff; + else + $tabs = $buff . $tabs; + } + } + + return new Markup(sprintf($return, sprintf($wrapper, $tabs ?: $this->tabs[0])), ['allow' => Markup::CLASS_ADMIN], 'smartai-generic'); + } + + public function addJsGlobals(array $jsg) : void + { + Util::mergeJsGlobals($this->jsGlobals, $jsg); + } + + public function getJSGlobals() : array + { + return $this->jsGlobals; + } + + public function getTabs() : array + { + return $this->tabs; + } + + public function addTab(int $guid, string $tt) : void + { + $this->tabs[$guid] = $tt; + } + + public function getTarget(int $id = -1) : ?SmartTarget + { + if ($id < 0) + return $this->itr['target']; + + return $this->rawData[$id]['target'] ?? null; + } + + public function getAction(int $id = -1) : ?SmartAction + { + if ($id < 0) + return $this->itr['action']; + + return $this->rawData[$id]['action'] ?? null; + } + + public function getEvent(int $id = -1) : ?SmartEvent + { + if ($id < 0) + return $this->itr['event']; + + return $this->rawData[$id]['event'] ?? null; + } + + public function getEntry() : int + { + return $this->baseEntry ?: $this->entry; + } + + private function initQuotes(int $creatureId) : void + { + if (isset($this->quotes[$creatureId])) + return; + + [$quotes, , ] = Game::getQuotesForCreature($creatureId); + + $this->quotes[$creatureId] = $quotes; + + if (!empty($this->quotes[$creatureId])) + $this->quotes[$creatureId]['src'] = CreatureList::getName($creatureId); + } + + public function getQuote(int $creatureId, int $group, ?string &$npcSrc) : array + { + if (isset($this->quotes[$creatureId][$group])) + { + $npcSrc = $this->quotes[$creatureId]['src']; + return $this->quotes[$creatureId][$group]; + } + + return []; + } +} + +?> diff --git a/includes/components/SmartAI/SmartAction.class.php b/includes/components/SmartAI/SmartAction.class.php new file mode 100644 index 00000000..05a354eb --- /dev/null +++ b/includes/components/SmartAI/SmartAction.class.php @@ -0,0 +1,760 @@ + [null, null, null, null, null, null, 0], // No action + self::ACTION_TALK => [null, ['formatTime', -1, true], null, null, null, null, 0], // groupID from creature_text, duration to wait before TEXT_OVER event is triggered, useTalkTarget (0/1) - use target as talk target + self::ACTION_SET_FACTION => [null, null, null, null, null, null, 0], // FactionId (or 0 for default) + self::ACTION_MORPH_TO_ENTRY_OR_MODEL => [Type::NPC, null, null, null, null, null, 0], // Creature_template entry(param1) OR ModelId (param2) (or 0 for both to demorph) + self::ACTION_SOUND => [Type::SOUND, null, null, null, null, null, 0], // SoundId, onlySelf + self::ACTION_PLAY_EMOTE => [null, null, null, null, null, null, 0], // EmoteId + self::ACTION_FAIL_QUEST => [Type::QUEST, null, null, null, null, null, 0], // QuestID + self::ACTION_OFFER_QUEST => [Type::QUEST, null, null, null, null, null, 0], // QuestID, directAdd + self::ACTION_SET_REACT_STATE => [['reactState', 10, false], null, null, null, null, null, 0], // state + self::ACTION_ACTIVATE_GOBJECT => [null, null, null, null, null, null, 0], // + self::ACTION_RANDOM_EMOTE => [null, null, null, null, null, null, 0], // EmoteId1, EmoteId2, EmoteId3... + self::ACTION_CAST => [Type::SPELL, ['castFlags', -1, false], null, null, null, null, 0], // SpellId, CastFlags, TriggeredFlags + self::ACTION_SUMMON_CREATURE => [Type::NPC, ['summonType', -1, false], ['formatTime', 10, true], null, null, null, 0], // CreatureID, summonType, duration in ms, attackInvoker, flags(SmartActionSummonCreatureFlags) + self::ACTION_THREAT_SINGLE_PCT => [null, null, null, null, null, null, 0], // Threat% + self::ACTION_THREAT_ALL_PCT => [null, null, null, null, null, null, 0], // Threat% + self::ACTION_CALL_AREAEXPLOREDOREVENTHAPPENS => [Type::QUEST, null, null, null, null, null, 0], // QuestID + self::ACTION_SET_INGAME_PHASE_ID => [null, null, null, null, null, null, 2], // used on 4.3.4 and higher scripts + self::ACTION_SET_EMOTE_STATE => [null, null, null, null, null, null, 0], // emoteID + self::ACTION_SET_UNIT_FLAG => [['unitFlags', 10, false], null, null, null, null, null, 1], // UNUSED, DO NOT REUSE + self::ACTION_REMOVE_UNIT_FLAG => [['unitFlags', 10, false], null, null, null, null, null, 1], // UNUSED, DO NOT REUSE + self::ACTION_AUTO_ATTACK => [null, null, null, null, null, null, 0], // AllowAttackState (0 = stop attack, anything else means continue attacking) + self::ACTION_ALLOW_COMBAT_MOVEMENT => [null, null, null, null, null, null, 0], // AllowCombatMovement (0 = stop combat based movement, anything else continue attacking) + self::ACTION_SET_EVENT_PHASE => [null, null, null, null, null, null, 0], // Phase + self::ACTION_INC_EVENT_PHASE => [null, null, null, null, null, null, 0], // Value (may be negative to decrement phase, should not be 0) + self::ACTION_EVADE => [null, null, null, null, null, null, 0], // toRespawnPosition (0 = Move to RespawnPosition, 1 = Move to last stored home position) + self::ACTION_FLEE_FOR_ASSIST => [null, null, null, null, null, null, 0], // With Emote + self::ACTION_CALL_GROUPEVENTHAPPENS => [Type::QUEST, null, null, null, null, null, 0], // QuestID + self::ACTION_COMBAT_STOP => [null, null, null, null, null, null, 0], // + self::ACTION_REMOVEAURASFROMSPELL => [Type::SPELL, null, null, null, null, null, 0], // Spellid (0 removes all auras), charges (0 removes aura) + self::ACTION_FOLLOW => [null, null, null, null, null, null, 0], // Distance (0 = default), Angle (0 = default), EndCreatureEntry, credit, creditType (0monsterkill, 1event) + self::ACTION_RANDOM_PHASE => [null, null, null, null, null, null, 0], // PhaseId1, PhaseId2, PhaseId3... + self::ACTION_RANDOM_PHASE_RANGE => [null, null, null, null, null, null, 0], // PhaseMin, PhaseMax + self::ACTION_RESET_GOBJECT => [null, null, null, null, null, null, 0], // + self::ACTION_CALL_KILLEDMONSTER => [Type::NPC, null, null, null, null, null, 0], // CreatureId, + self::ACTION_SET_INST_DATA => [null, null, null, null, null, null, 0], // Field, Data, Type (0 = SetData, 1 = SetBossState) + self::ACTION_SET_INST_DATA64 => [null, null, null, null, null, null, 0], // Field, + self::ACTION_UPDATE_TEMPLATE => [Type::NPC, null, null, null, null, null, 0], // Entry + self::ACTION_DIE => [null, null, null, null, null, null, 0], // No Params + self::ACTION_SET_IN_COMBAT_WITH_ZONE => [null, null, null, null, null, null, 0], // No Params + self::ACTION_CALL_FOR_HELP => [null, null, null, null, null, null, 0], // Radius, With Emote + self::ACTION_SET_SHEATH => [['sheathState', 10, false], null, null, null, null, null, 0], // Sheath (0-unarmed, 1-melee, 2-ranged) + self::ACTION_FORCE_DESPAWN => [['formatTime', 10, true], ['formatTime', 11, false], null, null, null, null, 0], // timer + self::ACTION_SET_INVINCIBILITY_HP_LEVEL => [null, null, null, null, null, null, 0], // MinHpValue(+pct, -flat) + self::ACTION_MOUNT_TO_ENTRY_OR_MODEL => [Type::NPC, null, null, null, null, null, 0], // Creature_template entry(param1) OR ModelId (param2) (or 0 for both to dismount) + self::ACTION_SET_INGAME_PHASE_MASK => [null, null, null, null, null, null, 0], // mask + self::ACTION_SET_DATA => [null, null, null, null, null, null, 0], // Field, Data (only creature @todo) + self::ACTION_ATTACK_STOP => [null, null, null, null, null, null, 0], // + self::ACTION_SET_VISIBILITY => [null, null, null, null, null, null, 0], // on/off + self::ACTION_SET_ACTIVE => [null, null, null, null, null, null, 0], // on/off + self::ACTION_ATTACK_START => [null, null, null, null, null, null, 0], // + self::ACTION_SUMMON_GO => [Type::OBJECT, ['formatTime', 10, false], null, null, null, null, 0], // GameObjectID, DespawnTime in s + self::ACTION_KILL_UNIT => [null, null, null, null, null, null, 0], // + self::ACTION_ACTIVATE_TAXI => [null, null, null, null, null, null, 0], // TaxiID + self::ACTION_WP_START => [null, null, null, Type::QUEST, ['formatTime', 10, true], ['reactState', 11, false], 0], // run/walk, pathID, canRepeat, quest, despawntime + self::ACTION_WP_PAUSE => [['formatTime', 10, true], null, null, null, null, null, 0], // time + self::ACTION_WP_STOP => [['formatTime', 10, true], Type::QUEST, null, null, null, null, 0], // despawnTime, quest, fail? + self::ACTION_ADD_ITEM => [Type::ITEM, null, null, null, null, null, 0], // itemID, count + self::ACTION_REMOVE_ITEM => [Type::ITEM, null, null, null, null, null, 0], // itemID, count + self::ACTION_INSTALL_AI_TEMPLATE => [['aiTemplate', 10, false], null, null, null, null, null, 1], // UNUSED, DO NOT REUSE + self::ACTION_SET_RUN => [null, null, null, null, null, null, 0], // 0/1 + self::ACTION_SET_DISABLE_GRAVITY => [null, null, null, null, null, null, 0], // 0/1 + self::ACTION_SET_SWIM => [null, null, null, null, null, null, 1], // UNUSED, DO NOT REUSE + self::ACTION_TELEPORT => [null, null, null, null, null, null, 0], // mapID, + self::ACTION_SET_COUNTER => [null, null, null, null, null, null, 0], // id, value, reset (0/1) + self::ACTION_STORE_TARGET_LIST => [null, null, null, null, null, null, 0], // varID, + self::ACTION_WP_RESUME => [null, null, null, null, null, null, 0], // none + self::ACTION_SET_ORIENTATION => [null, null, null, null, null, null, 0], // + self::ACTION_CREATE_TIMED_EVENT => [null, ['numRange', 10, true], null, ['numRange', -1, true], null, null, 0], // id, InitialMin, InitialMax, RepeatMin(only if it repeats), RepeatMax(only if it repeats), chance + self::ACTION_PLAYMOVIE => [null, null, null, null, null, null, 0], // entry + self::ACTION_MOVE_TO_POS => [null, null, null, null, null, null, 0], // PointId, transport, disablePathfinding, ContactDistance + self::ACTION_ENABLE_TEMP_GOBJ => [['formatTime', 10, false], null, null, null, null, null, 0], // despawnTimer (sec) + self::ACTION_EQUIP => [null, null, Type::ITEM, Type::ITEM, Type::ITEM, null, 0], // entry, slotmask slot1, slot2, slot3 , only slots with mask set will be sent to client, bits are 1, 2, 4, leaving mask 0 is defaulted to mask 7 (send all), slots1-3 are only used if no entry is set + self::ACTION_CLOSE_GOSSIP => [null, null, null, null, null, null, 0], // none + self::ACTION_TRIGGER_TIMED_EVENT => [null, null, null, null, null, null, 0], // id(>1) + self::ACTION_REMOVE_TIMED_EVENT => [null, null, null, null, null, null, 0], // id(>1) + self::ACTION_ADD_AURA => [null, null, null, null, null, null, 1], // UNUSED, DO NOT REUSE + self::ACTION_OVERRIDE_SCRIPT_BASE_OBJECT => [null, null, null, null, null, null, 1], // UNUSED, DO NOT REUSE + self::ACTION_RESET_SCRIPT_BASE_OBJECT => [null, null, null, null, null, null, 1], // UNUSED, DO NOT REUSE + self::ACTION_CALL_SCRIPT_RESET => [null, null, null, null, null, null, 0], // none + self::ACTION_SET_RANGED_MOVEMENT => [null, null, null, null, null, null, 0], // Distance, angle + self::ACTION_CALL_TIMED_ACTIONLIST => [null, null, null, null, null, null, 0], // ID (overwrites already running actionlist), stop after combat?(0/1), timer update type(0-OOC, 1-IC, 2-ALWAYS) + self::ACTION_SET_NPC_FLAG => [['npcFlags', 10, false], null, null, null, null, null, 0], // Flags + self::ACTION_ADD_NPC_FLAG => [['npcFlags', 10, false], null, null, null, null, null, 0], // Flags + self::ACTION_REMOVE_NPC_FLAG => [['npcFlags', 10, false], null, null, null, null, null, 0], // Flags + self::ACTION_SIMPLE_TALK => [null, null, null, null, null, null, 0], // groupID, can be used to make players say groupID, Text_over event is not triggered, whisper can not be used (Target units will say the text) + self::ACTION_SELF_CAST => [Type::SPELL, ['castFlags', -1, false], null, null, null, null, 0], // spellID, castFlags + self::ACTION_CROSS_CAST => [Type::SPELL, ['castFlags', -1, false], null, null, null, null, 0], // spellID, castFlags, CasterTargetType, CasterTarget param1, CasterTarget param2, CasterTarget param3, ( + the origonal target fields as Destination target), CasterTargets will cast spellID on all Targets (use with caution if targeting multiple * multiple units) + self::ACTION_CALL_RANDOM_TIMED_ACTIONLIST => [null, null, null, null, null, null, 0], // script9 ids 1-9 + self::ACTION_CALL_RANDOM_RANGE_TIMED_ACTIONLIST => [null, null, null, null, null, null, 0], // script9 id min, max + self::ACTION_RANDOM_MOVE => [null, null, null, null, null, null, 0], // maxDist + self::ACTION_SET_UNIT_FIELD_BYTES_1 => [['unitFieldBytes1', 10, false], null, null, null, null, null, 0], // bytes, target + self::ACTION_REMOVE_UNIT_FIELD_BYTES_1 => [['unitFieldBytes1', 10, false], null, null, null, null, null, 0], // bytes, target + self::ACTION_INTERRUPT_SPELL => [null, Type::SPELL, null, null, null, null, 0], // + self::ACTION_SEND_GO_CUSTOM_ANIM => [['dynFlags', 10, false], null, null, null, null, null, 1], // UNUSED, DO NOT REUSE + self::ACTION_SET_DYNAMIC_FLAG => [['dynFlags', 10, false], null, null, null, null, null, 1], // UNUSED, DO NOT REUSE + self::ACTION_ADD_DYNAMIC_FLAG => [['dynFlags', 10, false], null, null, null, null, null, 1], // UNUSED, DO NOT REUSE + self::ACTION_REMOVE_DYNAMIC_FLAG => [null, null, null, null, null, null, 1], // UNUSED, DO NOT REUSE + self::ACTION_JUMP_TO_POS => [null, null, null, null, null, null, 0], // speedXY, speedZ, targetX, targetY, targetZ + self::ACTION_SEND_GOSSIP_MENU => [null, null, null, null, null, null, 0], // menuId, optionId + self::ACTION_GO_SET_LOOT_STATE => [['lootState', 10, false], null, null, null, null, null, 0], // state + self::ACTION_SEND_TARGET_TO_TARGET => [null, null, null, null, null, null, 0], // id + self::ACTION_SET_HOME_POS => [null, null, null, null, null, null, 0], // none + self::ACTION_SET_HEALTH_REGEN => [null, null, null, null, null, null, 0], // 0/1 + self::ACTION_SET_ROOT => [null, null, null, null, null, null, 0], // off/on + self::ACTION_SET_GO_FLAG => [['goFlags', 10, false], null, null, null, null, null, 1], // UNUSED, DO NOT REUSE + self::ACTION_ADD_GO_FLAG => [['goFlags', 10, false], null, null, null, null, null, 1], // UNUSED, DO NOT REUSE + self::ACTION_REMOVE_GO_FLAG => [['goFlags', 10, false], null, null, null, null, null, 1], // UNUSED, DO NOT REUSE + self::ACTION_SUMMON_CREATURE_GROUP => [null, null, null, null, null, null, 0], // Group, attackInvoker + self::ACTION_SET_POWER => [['powerType', 10, false], null, null, null, null, null, 0], // PowerType, newPower + self::ACTION_ADD_POWER => [['powerType', 10, false], null, null, null, null, null, 0], // PowerType, newPower + self::ACTION_REMOVE_POWER => [['powerType', 10, false], null, null, null, null, null, 0], // PowerType, newPower + self::ACTION_GAME_EVENT_STOP => [Type::WORLDEVENT, null, null, null, null, null, 0], // GameEventId + self::ACTION_GAME_EVENT_START => [Type::WORLDEVENT, null, null, null, null, null, 0], // GameEventId + self::ACTION_START_CLOSEST_WAYPOINT => [null, null, null, null, null, null, 0], // wp1, wp2, wp3, wp4, wp5, wp6, wp7 + self::ACTION_MOVE_OFFSET => [null, null, null, null, null, null, 0], // + self::ACTION_RANDOM_SOUND => [Type::SOUND, Type::SOUND, Type::SOUND, Type::SOUND, null, null, 0], // soundId1, soundId2, soundId3, soundId4, soundId5, onlySelf + self::ACTION_SET_CORPSE_DELAY => [['formatTime', 10, false], null, null, null, null, null, 0], // timer + self::ACTION_DISABLE_EVADE => [null, null, null, null, null, null, 0], // 0/1 (1 = disabled, 0 = enabled) + self::ACTION_GO_SET_GO_STATE => [null, null, null, null, null, null, 0], // state + self::ACTION_SET_CAN_FLY => [null, null, null, null, null, null, 1], // UNUSED, DO NOT REUSE + self::ACTION_REMOVE_AURAS_BY_TYPE => [null, null, null, null, null, null, 1], // UNUSED, DO NOT REUSE + self::ACTION_SET_SIGHT_DIST => [null, null, null, null, null, null, 1], // UNUSED, DO NOT REUSE + self::ACTION_FLEE => [['formatTime', 10, false], null, null, null, null, null, 1], // UNUSED, DO NOT REUSE + self::ACTION_ADD_THREAT => [null, null, null, null, null, null, 0], // +threat, -threat + self::ACTION_LOAD_EQUIPMENT => [null, null, null, null, null, null, 0], // id + self::ACTION_TRIGGER_RANDOM_TIMED_EVENT => [['numRange', 10, false], null, null, null, null, null, 0], // id min range, id max range + self::ACTION_REMOVE_ALL_GAMEOBJECTS => [null, null, null, null, null, null, 1], // UNUSED, DO NOT REUSE + self::ACTION_PAUSE_MOVEMENT => [null, ['formatTime', 10, true], null, null, null, null, 0], // MovementSlot (default = 0, active = 1, controlled = 2), PauseTime (ms), Force + self::ACTION_PLAY_ANIMKIT => [null, null, null, null, null, null, 2], // don't use on 3.3.5a + self::ACTION_SCENE_PLAY => [null, null, null, null, null, null, 2], // don't use on 3.3.5a + self::ACTION_SCENE_CANCEL => [null, null, null, null, null, null, 2], // don't use on 3.3.5a + self::ACTION_SPAWN_SPAWNGROUP => [null, null, null, ['spawnFlags', 11, false], null, null, 0], // Group ID, min secs, max secs, spawnflags + self::ACTION_DESPAWN_SPAWNGROUP => [null, null, null, ['spawnFlags', 11, false], null, null, 0], // Group ID, min secs, max secs, spawnflags + self::ACTION_RESPAWN_BY_SPAWNID => [null, null, null, null, null, null, 0], // spawnType, spawnId + self::ACTION_INVOKER_CAST => [Type::SPELL, ['castFlags', -1, false], null, null, null, null, 0], // spellID, castFlags + self::ACTION_PLAY_CINEMATIC => [null, null, null, null, null, null, 0], // entry, cinematic + self::ACTION_SET_MOVEMENT_SPEED => [null, null, null, null, null, null, 0], // movementType, speedInteger, speedFraction + self::ACTION_PLAY_SPELL_VISUAL_KIT => [null, null, null, null, null, null, 2], // spellVisualKitId (RESERVED, PENDING CHERRYPICK) + self::ACTION_OVERRIDE_LIGHT => [Type::ZONE, null, null, ['formatTime', -1, true], null, null, 0], // zoneId, overrideLightID, transitionMilliseconds + self::ACTION_OVERRIDE_WEATHER => [Type::ZONE, ['weatherState', 10, false], null, null, null, null, 0], // zoneId, weatherId, intensity + self::ACTION_SET_AI_ANIM_KIT => [null, null, null, null, null, null, 2], // DEPRECATED, DO REUSE (it was never used in any branch, treat as free action id) + self::ACTION_SET_HOVER => [null, null, null, null, null, null, 0], // 0/1 + self::ACTION_SET_HEALTH_PCT => [null, null, null, null, null, null, 0], // percent + self::ACTION_CREATE_CONVERSATION => [null, null, null, null, null, null, 2], // don't use on 3.3.5a + self::ACTION_SET_IMMUNE_PC => [null, null, null, null, null, null, 0], // 0/1 + self::ACTION_SET_IMMUNE_NPC => [null, null, null, null, null, null, 0], // 0/1 + self::ACTION_SET_UNINTERACTIBLE => [null, null, null, null, null, null, 0], // 0/1 + self::ACTION_ACTIVATE_GAMEOBJECT => [null, null, null, null, null, null, 0], // GameObjectActions + self::ACTION_ADD_TO_STORED_TARGET_LIST => [null, null, null, null, null, null, 0], // varID + self::ACTION_BECOME_PERSONAL_CLONE_FOR_PLAYER => [null, null, null, null, null, null, 2], // don't use on 3.3.5a + self::ACTION_TRIGGER_GAME_EVENT => [null, null, null, null, null, null, 2], // eventId, useSaiTargetAsGameEventSource (RESERVED, PENDING CHERRYPICK) + self::ACTION_DO_ACTION => [null, null, null, null, null, null, 2] // actionId (RESERVED, PENDING CHERRYPICK) + ); + + private array $jsGlobals = []; + private ?array $summons = null; + + public function __construct( + private int $id, + public readonly int $type, + private array $param, + private SmartAI &$smartAI) + { + // init additional parameters + Util::checkNumeric($this->param, NUM_CAST_INT); + $this->param = array_pad($this->param, 15, ''); + } + + public function process() : array + { + $body = + $footer = ''; + + $actionTT = Lang::smartAI('actionTT', array_merge([$this->type], $this->param)); + + for ($i = 0; $i < 5; $i++) + { + $aParams = $this->data[$this->type]; + + if (is_array($aParams[$i])) + { + [$fn, $idx, $extraParam] = $aParams[$i]; + + if ($idx < 0) + $footer = $this->{$fn}($this->param[$i], $this->param[$i + 1], $extraParam); + else + $this->param[$idx] = $this->{$fn}($this->param[$i], $this->param[$i + 1], $extraParam); + } + else if (is_int($aParams[$i]) && $this->param[$i]) + $this->jsGlobals[$aParams[$i]][$this->param[$i]] = $this->param[$i]; + } + + // non-generic cases + switch ($this->type) + { + case self::ACTION_FLEE_FOR_ASSIST: // 25 -> none + case self::ACTION_CALL_FOR_HELP: // 39 -> self + if ($this->param[0]) + $footer = $this->param; + break; + case self::ACTION_INTERRUPT_SPELL: // 92 -> self + if (!$this->param[1]) + $footer = $this->param; + break; + case self::ACTION_UPDATE_TEMPLATE: // 36 + case self::ACTION_SET_CORPSE_DELAY: // 116 + if ($this->param[1]) + $footer = $this->param; + break; + case self::ACTION_PAUSE_MOVEMENT: // 127 -> any target [ye, not gonna resolve this nonsense] + case self::ACTION_REMOVEAURASFROMSPELL: // 28 -> any target + case self::ACTION_SOUND: // 4 -> self [param3 set in DB but not used in core?] + case self::ACTION_SUMMON_GO: // 50 -> self, world coords + case self::ACTION_MOVE_TO_POS: // 69 -> any target + if ($this->param[2]) + $footer = $this->param; + break; + case self::ACTION_WP_START: // 53 -> any .. why tho? + if ($this->param[2] || $this->param[5]) + $footer = $this->param; + break; + case self::ACTION_PLAY_EMOTE: // 5 -> any target + case self::ACTION_SET_EMOTE_STATE: // 17 -> any target + if ($this->param[0]) + { + $this->param[0] *= -1; // handle creature emote + $this->jsGlobals[Type::EMOTE][$this->param[0]] = $this->param[0]; + } + break; + case self::ACTION_RANDOM_EMOTE: // 10 -> any target + $buff = []; + for ($i = 0; $i < 6; $i++) + { + if (empty($this->param[$i])) + continue; + + $this->param[$i] *= -1; // handle creature emote + $buff[] = '[emote='.$this->param[$i].']'; + $this->jsGlobals[Type::EMOTE][$this->param[$i]] = $this->param[$i]; + } + $this->param[10] = Lang::concat($buff, Lang::CONCAT_OR); + break; + case self::ACTION_SET_FACTION: // 2 -> any target + if ($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; + case self::ACTION_MORPH_TO_ENTRY_OR_MODEL: // 3 -> self + case self::ACTION_MOUNT_TO_ENTRY_OR_MODEL: // 43 -> self + if (!$this->param[0] && !$this->param[1]) + $this->param[10] = 1; + break; + case self::ACTION_THREAT_SINGLE_PCT: // 13 -> victim + case self::ACTION_THREAT_ALL_PCT: // 14 -> self + case self::ACTION_ADD_THREAT: // 123 -> any target + $this->param[10] = $this->param[0] - $this->param[1]; + break; + case self::ACTION_FOLLOW: // 29 -> any target + if ($this->param[1]) + { + $this->param[10] = Util::O2Deg($this->param[1])[0]; + $footer = $this->param; + } + if ($this->param[3]) + { + if ($this->param[4]) + { + $this->jsGlobals[Type::QUEST][$this->param[3]] = $this->param[3]; + $this->param[11] = 1; + } + else + { + $this->jsGlobals[Type::NPC][$this->param[3]] = $this->param[3]; + $this->param[12] = 1; + } + } + break; + case self::ACTION_RANDOM_PHASE: // 30 -> self + $buff = []; + for ($i = 0; $i < 7; $i++) + if ($_ = $this->param[$i]) + $buff[] = $_; + + $this->param[10] = Lang::concat($buff); + break; + case self::ACTION_ACTIVATE_TAXI: // 52 -> invoker + $nodes = DB::Aowow()->selectRow( + '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::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]) + $this->param[10] = Lang::concat(Util::mask2bits($this->param[0])); + break; + case self::ACTION_TELEPORT: // 62 -> invoker + [$x, $y, $z, $o] = $this->smartAI->getTarget()->getWorldPos(); + // try from areatrigger setup data + if ($this->smartAI->teleportTargetArea) + $this->param[10] = $this->smartAI->teleportTargetArea; + // try calc from SmartTarget data + else if ($pos = WorldPosition::toZonePos($this->param[0], $x, $y)) + { + $this->param[10] = $pos[0]['areaId']; + $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` = %i', $this->param[0])) + $this->param[10] = $areaId; + // ...whelp + else + trigger_error('SmartAction::process - could not resolve teleport target: map:'.$this->param[0].' x:'.$x.' y:'.$y); + + if ($this->param[10]) + $this->jsGlobals[Type::ZONE][$this->param[10]] = $this->param[10]; + break; + case self::ACTION_SET_ORIENTATION: // 66 -> any target + if ($this->smartAI->getTarget()->type == SmartTarget::TARGET_POSITION) + $this->param[10] = Util::O2Deg($this->smartAI->getTarget()->getWorldPos()[3])[1]; + else if ($this->smartAI->getTarget()->type != SmartTarget::TARGET_SELF) + $this->param[10] = '#target#'; + break; + case self::ACTION_EQUIP: // 71 -> any + $equip = []; + + 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` = %i AND `ID` = %i', $this->smartAI->getEntry(), $this->param[0]); + + foreach ($slots as $s) + if ($_ = $items['ItemID'.$s]) + $equip[] = $_; + } + else if ($this->param[2] || $this->param[3] || $this->param[4]) + { + if ($_ = $this->param[2]) + $equip[] = $_; + if ($_ = $this->param[3]) + $equip[] = $_; + if ($_ = $this->param[4]) + $equip[] = $_; + } + + if ($equip) + { + $this->param[10] = Lang::concat($equip, callback: fn($x) => '[item='.$x.']'); + $footer = true; + + foreach ($equip as $_) + $this->jsGlobals[Type::ITEM][$_] = $_; + } + break; + case self::ACTION_LOAD_EQUIPMENT: // 124 -> any target + $buff = []; + if ($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) + continue; + + $this->jsGlobals[Type::ITEM][$i] = $i; + $buff[] = '[item='.$i.']'; + } + } + else if (!$this->param[1]) + trigger_error('SmartAI::action - action #124 (SmartAction::ACTION_LOAD_EQIPMENT) is malformed'); + + $this->param[10] = Lang::concat($buff); + $footer = true; + break; + case self::ACTION_CALL_TIMED_ACTIONLIST: // 80 -> any target + $this->param[10] = match ($this->param[1]) + { + 0, 1, 2 => Lang::smartAI('saiUpdate', $this->param[1]), + default => Lang::smartAI('saiUpdateUNK', [$this->param[1]]) + }; + + $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) + $this->smartAI->addTab($guid, $tt); + + break; + case self::ACTION_CALL_KILLEDMONSTER: // 33: Note: If target is SMART_TARGET_NONE (0) or SMART_TARGET_SELF (1), the kill is credited to all players eligible for loot from this creature. + if ($this->smartAI->getTarget()->type == SmartTarget::TARGET_SELF || $this->smartAI->getTarget()->type == SmartTarget::TARGET_NONE) + $this->param[10] = (new SmartTarget($this->id, SmartTarget::TARGET_LOOT_RECIPIENTS, [], [], $this->smartAI))->process(); + break; + case self::ACTION_CROSS_CAST: // 86 -> entity by TargetingBlock(param3, param4, param5, param6) cross cast spell at any target + $this->param[10] = (new SmartTarget($this->id, $this->param[2], [$this->param[3], $this->param[4], $this->param[5]], [], $this->smartAI))->process(); + break; + case self::ACTION_CALL_RANDOM_TIMED_ACTIONLIST: // 87 -> self + $talBuff = []; + for ($i = 0; $i < 6; $i++) + { + if (!$this->param[$i]) + continue; + + $talBuff[] = sprintf(self::TAL_TAB_ANCHOR, $this->param[$i]); + + $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) + $this->smartAI->addTab($guid, $tt); + } + $this->param[10] = Lang::concat($talBuff, Lang::CONCAT_OR); + break; + case self::ACTION_CALL_RANDOM_RANGE_TIMED_ACTIONLIST:// 88 -> self + $talBuff = []; + for ($i = $this->param[0]; $i <= $this->param[1]; $i++) + { + $talBuff[] = sprintf(self::TAL_TAB_ANCHOR, $i); + + $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) + $this->smartAI->addTab($guid, $tt); + } + $this->param[10] = Lang::concat($talBuff, Lang::CONCAT_OR); + break; + case self::ACTION_SET_HOME_POS: // 101 -> self + if ($this->smartAI->getTarget()?->type == Smarttarget::TARGET_SELF) + $this->param[10] = 1; + // do not break; + case self::ACTION_JUMP_TO_POS: // 97 -> self + case self::ACTION_MOVE_OFFSET: // 114 -> self + array_splice($this->param, 11, replacement: $this->smartAI->getTarget()->getWorldPos()); + 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` = %i GROUP BY `groupId`, `entry`', $this->smartAI->getEntry()); + + $buff = []; + if (!empty($this->summons[$this->param[0]])) + { + foreach ($this->summons[$this->param[0]] as $id => $n) + { + $this->jsGlobals[Type::NPC][$id] = $id; + $buff[] = $n.'x [npc='.$id.']'; + } + } + + if ($buff) + $this->param[10] = Lang::concat($buff); + break; + case self::ACTION_START_CLOSEST_WAYPOINT: // 113 -> any target + $this->param[10] = Lang::concat(array_filter($this->param), Lang::CONCAT_OR, fn($x) => '#[b]'.$x.'[/b]'); + break; + case self::ACTION_RANDOM_SOUND: // 115 -> self + for ($i = 0; $i < 4; $i++) + { + if ($x = $this->param[$i]) + { + $this->jsGlobals[Type::SOUND][$x] = $x; + $this->param[10] .= '[sound='.$x.']'; + } + } + + if ($this->param[5]) + $footer = true; + break; + case self::ACTION_GO_SET_GO_STATE: // 118 -> ??? + $this->param[10] = match ($this->param[0]) + { + 0, 1, 2 => Lang::smartAI('GOStates', $this->param[0]), + default => Lang::smartAI('GOStateUNK', [$this->param[0]]) + }; + break; + case self::ACTION_REMOVE_AURAS_BY_TYPE: // 120 -> any target + $this->param[10] = Lang::spell('auras', $this->param[0]); + 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` = %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 = []; + foreach ($entities as [$spawnType, $guid]) + { + $type = Type::NPC; + if ($spawnType == 1) + $type == Type::OBJECT; + + if ($_ = $this->resolveGuid($type, $guid)) + { + $this->jsGlobals[$type][$_] = $_; + $buff[] = '['.Type::getFileString($type).'='.$_.'][small class=q0] (GUID: '.$guid.')[/small]'; + } + else + $buff[] = Lang::smartAI('entityUNK').'[small class=q0] (GUID: '.$guid.')[/small]'; + + if (!--$n) + break; + } + + if (count($entities) > 5) + $buff[] = '+'.(count($entities) - 5).'…'; + + $this->param[12] = '[ul][li]'.implode('[/li][li]', $buff).'[/li][/ul]'; + + // i'd like this stored in $data but numRange can only handle msec + if ($time = $this->numRange($this->param[1] * 1000, $this->param[2] * 1000, true)) + $footer = [$time]; + break; + case self::ACTION_RESPAWN_BY_SPAWNID: // 133 + $type = Type::NPC; + if ($this->param[0] == 1) + $type == Type::OBJECT; + + if ($_ = $this->resolveGuid($type, $this->param[1])) + { + $this->param[10] = '['.Type::getFileString($type).'='.$_.']'; + $this->jsGlobals[$type][$_] = $_; + } + else + $this->param[10] = Lang::smartAI('entityUNK'); + break; + case self::ACTION_SET_MOVEMENT_SPEED: // 136 + $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]) + { + $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); + break; + } + + $this->smartAI->addJsGlobals($this->jsGlobals); + + $body = Lang::smartAI('actions', $this->type, 0, $this->param) ?? Lang::smartAI('actionUNK', [$this->type]); + if ($footer) + $footer = Lang::smartAI('actions', $this->type, 1, (array)$footer); + + // resolve conditionals + $i = 0; + while (strstr($body, ')?') && $i++ < 3) + $body = preg_replace_callback('/\(([^\)]*?)\)\?([^:]*):(([^;]*);*);/i', fn($m) => $m[1] ? $m[2] : $m[3], $body); + + $i = 0; + while (strstr($footer, ')?') && $i++ < 3) + $footer = preg_replace_callback('/\(([^\)]*?)\)\?([^:]*):(([^;]*);*);/i', fn($m) => $m[1] ? $m[2] : $m[3], $footer); + + // wrap body in tooltip + return [sprintf(self::ACTION_CELL_TPL, $actionTT, $body), $footer]; + } +} + +?> diff --git a/includes/components/SmartAI/SmartEvent.class.php b/includes/components/SmartAI/SmartEvent.class.php new file mode 100644 index 00000000..5a1f0316 --- /dev/null +++ b/includes/components/SmartAI/SmartEvent.class.php @@ -0,0 +1,395 @@ + 0: type, array: [fn, newIdx, extraParam]; error class: int + self::EVENT_UPDATE_IC => [['numRange', 10, true], null, ['numRange', -1, true], null, null, 0], // InitialMin, InitialMax, RepeatMin, RepeatMax + self::EVENT_UPDATE_OOC => [['numRange', 10, true], null, ['numRange', -1, true], null, null, 0], // InitialMin, InitialMax, RepeatMin, RepeatMax + self::EVENT_HEALTH_PCT => [['numRange', 10, false], null, ['numRange', -1, true], null, null, 0], // HPMin%, HPMax%, RepeatMin, RepeatMax + self::EVENT_MANA_PCT => [['numRange', 10, false], null, ['numRange', -1, true], null, null, 0], // ManaMin%, ManaMax%, RepeatMin, RepeatMax + self::EVENT_AGGRO => [null, null, null, null, null, 0], // NONE + self::EVENT_KILL => [['numRange', -1, true], null, null, Type::NPC, null, 0], // CooldownMin0, CooldownMax1, playerOnly2, else creature entry3 + self::EVENT_DEATH => [null, null, null, null, null, 0], // NONE + self::EVENT_EVADE => [null, null, null, null, null, 0], // NONE + self::EVENT_SPELLHIT => [Type::SPELL, ['magicSchool', 10, false], ['numRange', -1, true], null, null, 0], // SpellID, School, CooldownMin, CooldownMax + self::EVENT_RANGE => [['numRange', 10, false], null, ['numRange', -1, true], null, null, 0], // MinDist, MaxDist, RepeatMin, RepeatMax + self::EVENT_OOC_LOS => [['hostilityMode', 10, false], null, ['numRange', -1, true], null, null, 0], // hostilityModes, MaxRange, CooldownMin, CooldownMax + self::EVENT_RESPAWN => [null, null, Type::ZONE, null, null, 0], // type, MapId, ZoneId + self::EVENT_TARGET_HEALTH_PCT => [['numRange', 10, false], null, ['numRange', -1, true], null, null, 1], // UNUSED, DO NOT REUSE + self::EVENT_VICTIM_CASTING => [['numRange', -1, true], null, Type::SPELL, null, null, 0], // RepeatMin, RepeatMax, spellid + self::EVENT_FRIENDLY_HEALTH => [null, null, ['numRange', -1, true], null, null, 1], // UNUSED, DO NOT REUSE + self::EVENT_FRIENDLY_IS_CC => [null, ['numRange', -1, true], null, null, null, 0], // Radius, RepeatMin, RepeatMax + self::EVENT_FRIENDLY_MISSING_BUFF => [Type::SPELL, null, ['numRange', -1, true], null, null, 0], // SpellId, Radius, RepeatMin, RepeatMax + self::EVENT_SUMMONED_UNIT => [Type::NPC, ['numRange', -1, true], null, null, null, 0], // CreatureId(0 all), CooldownMin, CooldownMax + self::EVENT_TARGET_MANA_PCT => [['numRange', 10, false], null, ['numRange', -1, true], null, null, 1], // UNUSED, DO NOT REUSE + self::EVENT_ACCEPTED_QUEST => [Type::QUEST, ['numRange', -1, true], null, null, null, 0], // QuestID (0 = any), CooldownMin, CooldownMax + self::EVENT_REWARD_QUEST => [Type::QUEST, ['numRange', -1, true], null, null, null, 0], // QuestID (0 = any), CooldownMin, CooldownMax + self::EVENT_REACHED_HOME => [null, null, null, null, null, 0], // NONE + self::EVENT_RECEIVE_EMOTE => [Type::EMOTE, ['numRange', -1, true], null, null, null, 0], // EmoteId, CooldownMin, CooldownMax, condition, val1, val2, val3 + self::EVENT_HAS_AURA => [Type::SPELL, null, ['numRange', -1, true], null, null, 0], // Param1 = SpellID, Param2 = Stack amount, Param3/4 RepeatMin, RepeatMax + self::EVENT_TARGET_BUFFED => [Type::SPELL, null, ['numRange', -1, true], null, null, 0], // Param1 = SpellID, Param2 = Stack amount, Param3/4 RepeatMin, RepeatMax + self::EVENT_RESET => [null, null, null, null, null, 0], // Called after combat, when the creature respawn and spawn. + self::EVENT_IC_LOS => [['hostilityMode', 10, false], null, ['numRange', -1, true], null, null, 0], // hostilityModes, MaxRnage, CooldownMin, CooldownMax + self::EVENT_PASSENGER_BOARDED => [['numRange', -1, true], null, null, null, null, 0], // CooldownMin, CooldownMax + self::EVENT_PASSENGER_REMOVED => [['numRange', -1, true], null, null, null, null, 0], // CooldownMin, CooldownMax + self::EVENT_CHARMED => [null, null, null, null, null, 0], // onRemove (0 - on apply, 1 - on remove) + self::EVENT_CHARMED_TARGET => [null, null, null, null, null, 1], // UNUSED, DO NOT REUSE + self::EVENT_SPELLHIT_TARGET => [Type::SPELL, ['magicSchool', 10, false], ['numRange', -1, true], null, null, 0], // SpellID, School, CooldownMin, CooldownMax + self::EVENT_DAMAGED => [['numRange', 10, false], null, ['numRange', -1, true], null, null, 0], // MinDmg, MaxDmg, CooldownMin, CooldownMax + self::EVENT_DAMAGED_TARGET => [['numRange', 10, false], null, ['numRange', -1, true], null, null, 0], // MinDmg, MaxDmg, CooldownMin, CooldownMax + self::EVENT_MOVEMENTINFORM => [['motionType', 10, false], null, null, null, null, 0], // MovementType(any), PointID + self::EVENT_SUMMON_DESPAWNED => [Type::NPC, ['numRange', -1, true], null, null, null, 0], // Entry, CooldownMin, CooldownMax + self::EVENT_CORPSE_REMOVED => [null, null, null, null, null, 0], // NONE + self::EVENT_AI_INIT => [null, null, null, null, null, 0], // NONE + self::EVENT_DATA_SET => [null, null, ['numRange', -1, true], null, null, 0], // Id, Value, CooldownMin, CooldownMax + self::EVENT_WAYPOINT_START => [null, null, null, null, null, 1], // UNUSED, DO NOT REUSE + self::EVENT_WAYPOINT_REACHED => [null, null, null, null, null, 0], // PointId(0any), pathID(0any) + self::EVENT_TRANSPORT_ADDPLAYER => [null, null, null, null, null, 2], // NONE + self::EVENT_TRANSPORT_ADDCREATURE => [null, null, null, null, null, 2], // Entry (0 any) + self::EVENT_TRANSPORT_REMOVE_PLAYER => [null, null, null, null, null, 2], // NONE + self::EVENT_TRANSPORT_RELOCATE => [null, null, null, null, null, 2], // PointId + self::EVENT_INSTANCE_PLAYER_ENTER => [null, null, null, null, null, 2], // Team (0 any), CooldownMin, CooldownMax + self::EVENT_AREATRIGGER_ONTRIGGER => [Type::AREATRIGGER, null, null, null, null, 0], // TriggerId(0 any) + self::EVENT_QUEST_ACCEPTED => [null, null, null, null, null, 2], // none + self::EVENT_QUEST_OBJ_COMPLETION => [null, null, null, null, null, 2], // none + self::EVENT_QUEST_COMPLETION => [null, null, null, null, null, 2], // none + self::EVENT_QUEST_REWARDED => [null, null, null, null, null, 2], // none + self::EVENT_QUEST_FAIL => [null, null, null, null, null, 2], // none + self::EVENT_TEXT_OVER => [null, Type::NPC, null, null, null, 0], // GroupId from creature_text, creature entry who talks (0 any) + self::EVENT_RECEIVE_HEAL => [['numRange', 10, false], null, ['numRange', -1, true], null, null, 0], // MinHeal, MaxHeal, CooldownMin, CooldownMax + self::EVENT_JUST_SUMMONED => [null, null, null, null, null, 0], // none + self::EVENT_WAYPOINT_PAUSED => [null, null, null, null, null, 0], // PointId(0any), pathID(0any) + self::EVENT_WAYPOINT_RESUMED => [null, null, null, null, null, 0], // PointId(0any), pathID(0any) + self::EVENT_WAYPOINT_STOPPED => [null, null, null, null, null, 0], // PointId(0any), pathID(0any) + self::EVENT_WAYPOINT_ENDED => [null, null, null, null, null, 0], // PointId(0any), pathID(0any) + self::EVENT_TIMED_EVENT_TRIGGERED => [null, null, null, null, null, 0], // id + self::EVENT_UPDATE => [['numRange', 10, true], null, ['numRange', -1, true], null, null, 0], // InitialMin, InitialMax, RepeatMin, RepeatMax + self::EVENT_LINK => [null, null, null, null, null, 0], // INTERNAL USAGE, no params, used to link together multiple events, does not use any extra resources to iterate event lists needlessly + self::EVENT_GOSSIP_SELECT => [null, null, null, null, null, 0], // menuID, actionID + self::EVENT_JUST_CREATED => [null, null, null, null, null, 0], // none + self::EVENT_GOSSIP_HELLO => [null, null, null, null, null, 0], // noReportUse (for GOs) + self::EVENT_FOLLOW_COMPLETED => [null, null, null, null, null, 0], // none + self::EVENT_EVENT_PHASE_CHANGE => [null, null, null, null, null, 1], // UNUSED, DO NOT REUSE + self::EVENT_IS_BEHIND_TARGET => [['numRange', -1, true], null, null, null, null, 1], // UNUSED, DO NOT REUSE + self::EVENT_GAME_EVENT_START => [Type::WORLDEVENT, null, null, null, null, 0], // game_event.Entry + self::EVENT_GAME_EVENT_END => [Type::WORLDEVENT, null, null, null, null, 0], // game_event.Entry + self::EVENT_GO_LOOT_STATE_CHANGED => [['lootState', 10, false], null, null, null, null, 0], // go LootState + self::EVENT_GO_EVENT_INFORM => [null, null, null, null, null, 0], // eventId + self::EVENT_ACTION_DONE => [null, null, null, null, null, 0], // eventId (SharedDefines.EventId) + self::EVENT_ON_SPELLCLICK => [null, null, null, null, null, 0], // clicker (unit) + self::EVENT_FRIENDLY_HEALTH_PCT => [['numRange', 10, false], null, ['numRange', -1, true], null, null, 0], // minHpPct, maxHpPct, repeatMin, repeatMax + self::EVENT_DISTANCE_CREATURE => [null, Type::NPC, null, ['numRange', -1, true], null, 0], // guid, entry, distance, repeat + self::EVENT_DISTANCE_GAMEOBJECT => [null, Type::OBJECT, null, ['numRange', -1, true], null, 0], // guid, entry, distance, repeat + self::EVENT_COUNTER_SET => [null, null, ['numRange', -1, true], null, null, 0], // id, value, cooldownMin, cooldownMax + self::EVENT_SCENE_START => [null, null, null, null, null, 2], // don't use on 3.3.5a + self::EVENT_SCENE_TRIGGER => [null, null, null, null, null, 2], // don't use on 3.3.5a + self::EVENT_SCENE_CANCEL => [null, null, null, null, null, 2], // don't use on 3.3.5a + self::EVENT_SCENE_COMPLETE => [null, null, null, null, null, 2], // don't use on 3.3.5a + self::EVENT_SUMMONED_UNIT_DIES => [Type::NPC, ['numRange', -1, true], null, null, null, 0], // CreatureId(0 all), CooldownMin, CooldownMax + 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_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 = []; + + public function __construct( + private int $id, + public readonly int $type, + public readonly int $phaseMask, + public readonly int $chance, + private int $flags, + private array $param, + private SmartAI &$smartAI) + { + // additional parameters + Util::checkNumeric($this->param, NUM_CAST_INT); + $this->param = array_pad($this->param, 15, ''); + } + + public function process() : array + { + $body = + $footer = ''; + + $phases = Util::mask2bits($this->phaseMask, 1) ?: [0]; + $eventTT = Lang::smartAI('eventTT', array_merge([$this->type, $phases, $this->chance, $this->flags], $this->param)); + + for ($i = 0; $i < 5; $i++) + { + $eParams = $this->data[$this->type]; + + if (is_array($eParams[$i])) + { + [$fn, $idx, $extraParam] = $eParams[$i]; + + if ($idx < 0) + $footer = $this->{$fn}($this->param[$i], $this->param[$i + 1], $extraParam); + else + $this->param[$idx] = $this->{$fn}($this->param[$i], $this->param[$i + 1], $extraParam); + } + else if (is_int($eParams[$i]) && $this->param[$i]) + $this->jsGlobals[$eParams[$i]][$this->param[$i]] = $this->param[$i]; + } + + // non-generic cases + switch ($this->type) + { + case self::EVENT_UPDATE_IC: // 0 - In combat. + case self::EVENT_UPDATE_OOC: // 1 - Out of combat. + if ($this->smartAI->srcType == SmartAI::SRC_TYPE_ACTIONLIST) + $this->param[11] = 1; + // do not break; + case self::EVENT_GOSSIP_HELLO: // 64 - On Right-Click Creature/Gameobject that have gossip enabled. + if ($this->smartAI->srcType == SmartAI::SRC_TYPE_OBJECT) + $footer = array( + $this->param[0] == 1, + $this->param[0] == 2, + ); + break; + case self::EVENT_RESPAWN: // 11 - On Creature/Gameobject Respawn in Zone/Map + if ($this->param[0] == 1) // per map + { + switch ($this->param[1]) + { + case 0: $this->param[10] = Lang::maps('EasternKingdoms'); break; + case 1: $this->param[10] = Lang::maps('Kalimdor'); break; + 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` = %i', $this->param[1])) + { + $this->param[11] = $aId; + $this->jsGlobals[Type::ZONE][$aId] = $aId; + } + else + $this->param[11] = '[span class=q10]Unknown Map[/span] #'.$this->param[1]; + }; + } + else if ($this->param[0] == 2) // per zone + $this->param[11] = $this->param[2]; + + 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` = %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" %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` = %s + WHERE gmo.`MenuId` = %i AND gmo.`OptionID` = %i', + Lang::getLocale()->json(), $this->param[0], $this->param[1] + ); + + if ($gmo) + $this->param[10] = Util::localizedString($gmo, 'text'); + else + trigger_error('SmartAI::event - could not find gossip menu option for event #'.$this->type); + break; + case self::EVENT_DISTANCE_CREATURE: // 75 - On creature guid OR any instance of creature entry is within distance. + if ($this->param[0]) + if ($_ = $this->resolveGuid(Type::NPC, $this->param[0])) + { + $this->jsGlobals[Type::NPC][$this->param[0]] = $this->param[0]; + $this->param[10] = $_; + } + // do not break; + case self::EVENT_DISTANCE_GAMEOBJECT: // 76 - On gameobject guid OR any instance of gameobject entry is within distance. + if ($this->param[0] && !$this->param[10]) + { + if ($_ = $this->resolveGuid(Type::OBJECT, $this->param[0])) + { + $this->jsGlobals[Type::OBJECT][$this->param[0]] = $this->param[0]; + $this->param[10] = $_; + } + } + else if ($this->param[1]) + $this->param[10] = $this->param[1]; + else if (!$this->param[10]) + trigger_error('SmartAI::event - entity for event #'.$this->type.' not defined'); + break; + case self::EVENT_EVENT_PHASE_CHANGE: // 66 - On event phase mask set + $this->param[10] = Lang::concat(Util::mask2bits($this->param[0]), Lang::CONCAT_OR); + break; + } + + $this->smartAI->addJsGlobals($this->jsGlobals); + + $body = Lang::smartAI('events', $this->type, 0, $this->param) ?? Lang::smartAI('eventUNK', [$this->type]); + if ($footer) + $footer = Lang::smartAI('events', $this->type, 1, (array)$footer); + + // resolve conditionals + $i = 0; + while (strstr($body, ')?') && $i++ < 3) + $body = preg_replace_callback('/\(([^\)]*?)\)\?([^:]*):(([^;]*);*);/i', fn($m) => $m[1] ? $m[2] : $m[3], $body); + + $i = 0; + while (strstr($footer, ')?') && $i++ < 3) + $footer = preg_replace_callback('/\(([^\)]*?)\)\?([^:]*):(([^;]*);*);/i', fn($m) => $m[1] ? $m[2] : $m[3], $footer); + + if ($_ = $this->formatFlags()) + $footer = $_ . ($footer ? '; '.$footer : ''); + + if (User::isInGroup(U_GROUP_EMPLOYEE)) + { + if ($eParams[5] == 1) + $footer = '[span class=rep2]DEPRECATED[/span] ' . $footer; + else if ($eParams[5] == 2) + $footer = '[span class=rep0]RESERVED[/span] ' . $footer; + } + + // wrap body in tooltip + return [sprintf(self::EVENT_CELL_TPL, $eventTT, $body), $footer]; + } + + public function hasPhases() : bool + { + 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; + + $ef = []; + for ($i = 1; $i <= self::FLAG_WHILE_CHARMED; $i <<= 1) + if ($flags & $i) + if ($x = Lang::smartAI('eventFlags', $i)) + $ef[] = $x; + + return Lang::concat($ef); + } +} + +?> diff --git a/includes/components/SmartAI/SmartTarget.class.php b/includes/components/SmartAI/SmartTarget.class.php new file mode 100644 index 00000000..76aacd7c --- /dev/null +++ b/includes/components/SmartAI/SmartTarget.class.php @@ -0,0 +1,185 @@ + [null, null, null, null], // NONE + self::TARGET_SELF => [null, null, null, null], // Self cast + self::TARGET_VICTIM => [null, null, null, null], // Our current target (ie: highest aggro) + self::TARGET_HOSTILE_SECOND_AGGRO => [null, null, null, null], // Second highest aggro, maxdist, playerOnly, powerType + 1 + self::TARGET_HOSTILE_LAST_AGGRO => [null, null, null, null], // Dead last on aggro, maxdist, playerOnly, powerType + 1 + self::TARGET_HOSTILE_RANDOM => [null, null, null, null], // Just any random target on our threat list, maxdist, playerOnly, powerType + 1 + self::TARGET_HOSTILE_RANDOM_NOT_TOP => [null, null, null, null], // Any random target except top threat, maxdist, playerOnly, powerType + 1 + self::TARGET_ACTION_INVOKER => [null, null, null, null], // Unit who caused this Event to occur + self::TARGET_POSITION => [null, null, null, null], // use xyz from event params + self::TARGET_CREATURE_RANGE => [Type::NPC, ['numRange', 10, false], null, null], // CreatureEntry(0any), minDist, maxDist + self::TARGET_CREATURE_GUID => [null, Type::NPC, null, null], // guid, entry + self::TARGET_CREATURE_DISTANCE => [Type::NPC, null, null, null], // CreatureEntry(0any), maxDist + self::TARGET_STORED => [null, null, null, null], // id, uses pre-stored target(list) + self::TARGET_GAMEOBJECT_RANGE => [Type::OBJECT, ['numRange', 10, false], null, null], // entry(0any), min, max + self::TARGET_GAMEOBJECT_GUID => [null, Type::OBJECT, null, null], // guid, entry + self::TARGET_GAMEOBJECT_DISTANCE => [Type::OBJECT, null, null, null], // entry(0any), maxDist + self::TARGET_INVOKER_PARTY => [null, null, null, null], // invoker's party members + self::TARGET_PLAYER_RANGE => [['numRange', 10, false], null, null, null], // min, max + self::TARGET_PLAYER_DISTANCE => [null, null, null, null], // maxDist + self::TARGET_CLOSEST_CREATURE => [Type::NPC, null, null, null], // CreatureEntry(0any), maxDist, dead? + self::TARGET_CLOSEST_GAMEOBJECT => [Type::OBJECT, null, null, null], // entry(0any), maxDist + self::TARGET_CLOSEST_PLAYER => [null, null, null, null], // maxDist + self::TARGET_ACTION_INVOKER_VEHICLE => [null, null, null, null], // Unit's vehicle who caused this Event to occur + self::TARGET_OWNER_OR_SUMMONER => [null, null, null, null], // Unit's owner or summoner, Use Owner/Charmer of this unit + self::TARGET_THREAT_LIST => [null, null, null, null], // All units on creature's threat list, maxdist + self::TARGET_CLOSEST_ENEMY => [null, null, null, null], // maxDist, playerOnly + self::TARGET_CLOSEST_FRIENDLY => [null, null, null, null], // maxDist, playerOnly + self::TARGET_LOOT_RECIPIENTS => [null, null, null, null], // all players that have tagged this creature (for kill credit) + self::TARGET_FARTHEST => [null, null, null, null], // maxDist, playerOnly, isInLos + self::TARGET_VEHICLE_PASSENGER => [null, null, null, null], // seatMask (0 - all seats) + self::TARGET_CLOSEST_UNSPAWNED_GO => [Type::OBJECT, null, null, null] // entry(0any), maxDist + ); + + private array $jsGlobals = []; + + public function __construct( + private int $id, + public readonly int $type, + private array $param, + private array $worldPos, + private SmartAI &$smartAI) + { + // additional parameters + Util::checkNumeric($this->param, NUM_CAST_INT); + Util::checkNumeric($this->worldPos, NUM_CAST_FLOAT); + $this->param = array_pad($this->param, 15, ''); + $this->worldPos = array_pad($this->worldPos, 4, 0.0); + } + + public function process() : string + { + $target = ''; + + $targetTT = Lang::smartAI('targetTT', array_merge([$this->type], $this->param, $this->worldPos)); + + for ($i = 0; $i < 4; $i++) + { + $tParams = $this->targets[$this->type]; + + if (is_array($tParams[$i])) + { + [$fn, $idx, $extraParam] = $tParams[$i]; + + $this->param[$idx] = $this->{$fn}($this->param[$i], $this->param[$i + 1], $extraParam); + } + else if (is_int($tParams[$i]) && $this->param[$i]) + $this->jsGlobals[$tParams[$i]][$this->param[$i]] = $this->param[$i]; + } + + // non-generic cases + switch ($this->type) + { + case self::TARGET_HOSTILE_SECOND_AGGRO: + case self::TARGET_HOSTILE_LAST_AGGRO: + case self::TARGET_HOSTILE_RANDOM: + case self::TARGET_HOSTILE_RANDOM_NOT_TOP: + if ($this->param[2]) + $this->param[10] = Lang::spell('powerTypes', $this->param[2] - 1); + break; + case self::TARGET_VEHICLE_PASSENGER: + if ($this->param[0]) + $this->param[10] = Lang::concat(Util::mask2bits($this->param[0])); + break; + case self::TARGET_CREATURE_GUID: + if ($_ = $this->resolveGuid(Type::NPC, $this->param[0])) + { + $this->jsGlobals[Type::NPC][$_] = $_; + $this->param[10] = $_; + } + break; + case self::TARGET_GAMEOBJECT_GUID: + if ($_ = $this->resolveGuid(Type::OBJECT, $this->param[0])) + { + $this->jsGlobals[Type::OBJECT][$_] = $_; + $this->param[10] = $_; + } + break; + } + + $this->smartAI->addJsGlobals($this->jsGlobals); + + $target = Lang::smartAI('targets', $this->type, $this->param) ?? Lang::smartAI('targetUNK', [$this->type]); + + // resolve conditionals + $i = 0; + while (strstr($target, ')?') && $i++ < 3) + $target = preg_replace_callback('/\(([^\)]*?)\)\?([^:]*):(([^;]*);*);/i', fn($m) => $m[1] ? $m[2] : $m[3], $target); + + // wrap in tooltip (suspend action-tooltip) + return '[/span]'.sprintf(self::TARGET_TPL, $targetTT, $target).'[span tooltip=a-#rowIdx#]'; + } + + public function getWorldPos() : array + { + return $this->worldPos; + } + + // not really feasable. Too many target types can be players or creatures, depending on context + public function getTalkSource(bool &$playerSrc = false) : int + { + if ($this->type == SmartTarget::TARGET_CLOSEST_PLAYER) + $playerSrc = true; + + return match ($this->type) + { + SmartTarget::TARGET_CREATURE_GUID => $this->resolveGuid(Type::NPC, $this->param[0]) ?? 0, + SmartTarget::TARGET_CREATURE_RANGE, + SmartTarget::TARGET_CREATURE_DISTANCE, + SmartTarget::TARGET_CLOSEST_CREATURE => $this->param[0], + SmartTarget::TARGET_CLOSEST_PLAYER, + SmartTarget::TARGET_SELF => $this->smartAI->getEntry(), + default => $this->smartAI->getEntry() + }; + } +} + +?> diff --git a/includes/components/avatarmgr.class.php b/includes/components/avatarmgr.class.php new file mode 100644 index 00000000..4df080c8 --- /dev/null +++ b/includes/components/avatarmgr.class.php @@ -0,0 +1,122 @@ + self::MAX_W || $is[1] > self::MAX_H) + self::$error = Lang::account('selectAvatar'); + } + else + self::$error = Lang::account('selectAvatar'); + + if (!self::$error) + return true; + + self::$fileName = ''; + return false; + } + + /* create icon texture atlas + * ****************************** + * * LARGE * MEDIUM * + * * * * + * * * * + * * ************* + * * * SMOL * * + * * * * * + * * ********* * + * ****************************** + * + * as static/uploads/avatars/.jpg + */ + + public static function createAtlas(string $fileName) : bool + { + if (!self::$img) + return false; + + $sizes = [ICON_SIZE_LARGE, ICON_SIZE_MEDIUM, ICON_SIZE_SMALL]; + + $dest = imagecreatetruecolor(ICON_SIZE_LARGE + ICON_SIZE_MEDIUM, ICON_SIZE_LARGE); + $srcW = imagesx(self::$img); + $srcH = imagesx(self::$img); + + $destX = $destY = 0; + foreach ($sizes as $idx => $dim) + { + imagecopyresampled($dest, self::$img, $destX, $destY, 0, 0, $dim, $dim, $srcW, $srcH); + + if ($idx % 2) + $destY += $dim; + else + $destX += $dim; + } + + if (!imagejpeg($dest, sprintf(self::PATH_AVATARS, $fileName), self::JPEG_QUALITY)) + return false; + + self::$img = null; + $dest = null; + return true; + } + + + /*************/ + /* Admin Mgr */ + /*************/ + + // unsure yet how that's supposed to work + // for now pending uploads can be used right away +} + +?> diff --git a/includes/components/communitycontent.class.php b/includes/components/communitycontent.class.php new file mode 100644 index 00000000..ce7999af --- /dev/null +++ b/includes/components/communitycontent.class.php @@ -0,0 +1,413 @@ + 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` = %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 + %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` & %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` & %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` & %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 %and + GROUP BY c.`id` + ORDER BY c.`date` DESC + %lmt'; + + private static function addSubject(int $type, int $typeId) : void + { + if (!isset(self::$subjCache[$type][$typeId])) + self::$subjCache[$type][$typeId] = 0; + } + + private static function getSubjects() : void + { + foreach (self::$subjCache as $type => $ids) + { + $_ = array_filter(array_keys($ids), 'is_numeric'); + if (!$_) + continue; + + $obj = Type::newList($type, [['id', $_]]); + if (!$obj) + continue; + + foreach ($obj->iterate() as $id => $__) + self::$subjCache[$type][$id] = $obj->getField('name', true, true); + } + } + + 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 + domain:'live' <- irrelevant for our case + */ + + // add default values + $opt += ['user' => 0, 'unrated' => 0, 'comments' => 0, 'replies' => 0, 'flags' => 0]; + + $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']) + $where[] = ['c.`userId` = %i', $opt['user']]; + if ($opt['unrated']) + $where[] = ['ur.`entry` IS %sN', null]; + if ($opt['flags']) + $where[] = ['(c.`flags` & %i) > 0', $opt['flags']]; + if ($opt['comments'] && !$opt['replies']) + $where[] = ['c.`replyTo` = 0']; + else if (!$opt['comments'] && $opt['replies']) + $where[] = ['c.`replyTo` <> 0']; + // else + // pick both and no extra constraint needed for that + + $comments = DB::Aowow()->selectAssoc(self::$previewQuery, CC_FLAG_DELETED, $where, $resultLimit); + + if (!$comments) + return []; + + $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']); + + self::getSubjects(); + + foreach ($comments as $idx => &$c) + { + if (!empty(self::$subjCache[$c['type']][$c['typeId']])) + { + // apply subject + $c['subject'] = self::$subjCache[$c['type']][$c['typeId']]; + + // format date + $c['elapsed'] = time() - $c['date']; + $c['date'] = $dateFmt ? date(Util::$dateFormatInternal, $c['date']) : intVal($c['date']); + + // remove commentid if not looking for replies + if (empty($opt['replies'])) + unset($c['commentid']); + + // format text for listview + $c['preview'] = Lang::trimTextClean($c['preview']); + } + else + { + trigger_error('Comment '.$c['id'].' belongs to nonexistent subject.', E_USER_NOTICE); + unset($comments[$idx]); + } + } + + return array_values($comments); + } + + public static function getCommentReplies(int $commentId, int $resultLimit = PHP_INT_MAX, ?int &$nFound = 0) : array + { + $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 + $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, $where); + + foreach ($results as $r) + { + Markup::parseTags($r['body'], self::$jsGlobals); + + $reply = array( + 'commentid' => $commentId, + 'id' => $r['id'], + 'body' => $r['body'], + 'username' => $r['user'], + 'roles' => $r['roles'], + 'creationdate' => date(Util::$dateFormatInternal, $r['date']), + 'lasteditdate' => date(Util::$dateFormatInternal, $r['editDate']), + 'rating' => (string)$r['rating'] + ); + + if ($r['userReported']) + $reply['reportedByUser'] = true; + + if ($r['userRating'] > 0) + $reply['votedByUser'] = true; + else if ($r['userRating'] < 0) + $reply['downvotedByUser'] = true; + + $replies[] = $reply; + } + } + + return $replies; + } + + public static function getComments(int $type, int $typeId) : array + { + + $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 + $i = 0; + foreach ($results as $r) + { + Markup::parseTags($r['body'], self::$jsGlobals); + + self::$jsGlobals[Type::USER][$r['userId']] = $r['userId']; + + $c = array( + 'commentv2' => 1, // always 1.. enables some features i guess..? + 'number' => $i++, // some iterator .. unsued? + 'id' => $r['id'], + 'date' => date(Util::$dateFormatInternal, $r['date']), + 'roles' => $r['roles'], + 'body' => $r['body'], + 'rating' => $r['rating'], + 'userRating' => $r['userRating'], + 'user' => $r['user'], + 'nreplies' => 0 + ); + + $c['replies'] = self::getCommentReplies($r['id'], 5, $c['nreplies']); + + if ($r['responseBody']) // adminResponse + { + $c['response'] = $r['responseBody']; + $c['responseroles'] = $r['responseRoles']; + $c['responseuser'] = $r['responseUser']; + + Markup::parseTags($r['responseBody'], self::$jsGlobals); + } + + if ($r['editCount']) // lastEdit + $c['lastEdit'] = [date(Util::$dateFormatInternal, $r['editDate']), $r['editCount'], $r['editUser']]; + + if ($r['flags'] & CC_FLAG_STICKY) + $c['sticky'] = true; + + if ($r['flags'] & CC_FLAG_DELETED) + { + $c['deleted'] = true; + $c['deletedInfo'] = [date(Util::$dateFormatInternal, $r['deleteDate']), $r['deleteUser']]; + } + + if ($r['flags'] & CC_FLAG_OUTDATED) + $c['outofdate'] = true; + + $comments[] = $c; + } + + return $comments; + } + + public static function getVideos(int $typeOrUser = 0, int $typeId = 0, ?int &$nFound = 0, bool $dateFmt = true, int $resultLimit = PHP_INT_MAX) : array + { + $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')), $where, $typeOrUser ? ['date' => false] : ['pos' => true], PHP_INT_MAX); + + if ($typeOrUser <= 0) // not for search by type/typeId + { + foreach ($videos as $v) + self::addSubject($v['type'], $v['typeId']); + + self::getSubjects(); + } + + // format data to meet requirements of the js + foreach ($videos as &$v) + { + if ($typeOrUser <= 0) // not for search by type/typeId + { + if (!empty(self::$subjCache[$v['type']][$v['typeId']]) && !is_numeric(self::$subjCache[$v['type']][$v['typeId']])) + $v['subject'] = self::$subjCache[$v['type']][$v['typeId']]; + else + $v['subject'] = Lang::user('removed'); + } + + $v['date'] = $dateFmt ? date(Util::$dateFormatInternal, $v['date']) : intVal($v['date']); + $v['videoType'] = 1; // always youtube + + if (!$v['sticky']) + unset($v['sticky']); + + if (!$v['user']) + unset($v['user']); + } + + return array_values($videos); + } + + public static function getScreenshots(int $typeOrUser = 0, int $typeId = 0, ?int &$nFound = 0, bool $dateFmt = true, int $resultLimit = PHP_INT_MAX) : array + { + $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, + $where, + $resultLimit + ); + + if (!$screenshots) + return []; + + $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 + { + foreach ($screenshots as $s) + self::addSubject($s['type'], $s['typeId']); + + self::getSubjects(); + } + + // format data to meet requirements of the js + foreach ($screenshots as &$s) + { + if ($typeOrUser <= 0) // not for search by type/typeId + { + if (!empty(self::$subjCache[$s['type']][$s['typeId']]) && !is_numeric(self::$subjCache[$s['type']][$s['typeId']])) + $s['subject'] = self::$subjCache[$s['type']][$s['typeId']]; + else + $s['subject'] = Lang::user('removed'); + } + + $s['date'] = $dateFmt ? date(Util::$dateFormatInternal, $s['date']) : intVal($s['date']); + + if (!$s['sticky']) + unset($s['sticky']); + + if (!$s['user']) + unset($s['user']); + } + + return array_values($screenshots); + } + + public static function getJSGlobals() : array + { + return self::$jsGlobals; + } +} +?> diff --git a/includes/components/dbtypelist.class.php b/includes/components/dbtypelist.class.php new file mode 100644 index 00000000..c0d98f82 --- /dev/null +++ b/includes/components/dbtypelist.class.php @@ -0,0 +1,965 @@ +: + public bool $error = true; + + /* + * condition as array [expression, value, operator] + * expression: str - must match fieldname; + * int - 1: select everything; 0: select nothing + * array - another condition array + * value: str - operator defaults to: = + * num - operator defaults to: = + * array - operator defaults to: IN () + * 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 + * defines LIMIT + * + * example: + * array( + * ['id', 45], + * ['name', 'test%', '!'], + * [ + * 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) + * DB::OR, + * 5 + * ) + * results in + * 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 = DB::AND; + $limit = 0; + + $calcTotal = false; + $totalQuery = ''; + + if (!$this->queryBase || $conditions === null) + return; + + if (preg_match('/FROM (?:::)?[\w\_]+( AS)?\s?`?(\w+)`?$/i', $this->queryBase, $match)) + $this->prefixes['base'] = $match[2]; + else + $this->prefixes['base'] = ''; + + if (!empty($miscData['extraOpts'])) + $this->extendQueryOpts($miscData['extraOpts']); + + if (!empty($miscData['calcTotal'])) + $calcTotal = true; + + foreach ($conditions as $i => $c) + { + switch (getType($c)) + { + case 'array': + break; + case 'string': + case 'integer': + 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 = $this->resolveCondition($c, $linking)) + $where[] = $x; + + // optional query parts may require other optional parts to work + foreach ($this->prefixes as $pre) + if (isset($this->queryOpts[$pre][0])) + foreach ($this->queryOpts[$pre][0] as $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, $this->prefixes)) + unset($this->queryOpts[$k]); + + // prepare usage of guids if using multiple realms (which have non-zoro indizes) + if (key($this->dbNames) != 0) + $this->queryBase = preg_replace('/\s([^\s]+)\sAS\sARRAY_KEY/i', ' CONCAT("DB_IDX", ":", \1) AS ARRAY_KEY', $this->queryBase); + + // insert additional selected fields + if ($s = array_column($this->queryOpts, 's')) + $this->queryBase = str_replace('ARRAY_KEY', 'ARRAY_KEY '.implode('', $s), $this->queryBase); + + // append joins + if ($j = array_column($this->queryOpts, 'j')) + foreach ($j as $_) + $this->queryBase .= is_array($_) ? (empty($_[1]) ? ' JOIN ' : ' LEFT JOIN ').$_[0] : ' JOIN '.$_; + + // append conditions + if ($where) + $this->queryBase .= ' WHERE '.$linking; + + // append grouping + if ($g = array_filter(array_column($this->queryOpts, 'g'))) + $this->queryBase .= ' GROUP BY '.implode(', ', $g); + + // append post filtering + 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; + + // append ordering + if ($o = array_filter(array_column($this->queryOpts, 'o'))) + $this->queryBase .= ' ORDER BY '.implode(', ', $o); + + // apply limit + if ($limit) + $this->queryBase .= ' LIMIT '.$limit; + + // execute query (finally) + // this is purely because of multiple realms per server + foreach ($this->dbNames as $dbIdx => $n) + { + try // does not go through the compatibility layer as we need to be able to fetch individual rows here + { + $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 + // soon™.... + 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', $where); + } + + foreach ($result->getIterator() as $row) + { + // 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[$row['ARRAY_KEY']] = (array)$row; + } + + $result->free(); + } + catch (\Exception $e) {} // logged via \Dibi\Event in DB::errorLogger + } + + if (!$this->templates) + return; + + // push first element for instant use + $this->reset(); + + // all clear + $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) + return; + + $this->itrStack[] = $this->id; + + // reset on __construct + $this->reset(); + + foreach ($this->templates as $id => $__) + { + $this->id = $id; + $this->curTpl = &$this->templates[$id]; // do not use $tpl from each(), as we want to be referenceable + + yield $id => $this->curTpl; + + unset($this->curTpl); // kill reference or it will 'bleed' into the next iteration + } + + // fforward to old index + $this->reset(); + $oldIdx = array_pop($this->itrStack); + do + { + if (key($this->templates) != $oldIdx) + continue; + + $this->curTpl = current($this->templates); + $this->id = key($this->templates); + next($this->templates); + break; + } + while (next($this->templates)); + } + + protected function reset() : void + { + unset($this->curTpl); // kill reference or strange stuff will happen + if (!$this->templates) + return; + + $this->curTpl = reset($this->templates); + $this->id = key($this->templates); + } + + // read-access to templates + public function getEntry(string|int $id) : ?array + { + if (isset($this->templates[$id])) + { + unset($this->curTpl); // kill reference or strange stuff will happen + $this->curTpl = $this->templates[$id]; + $this->id = $id; + return $this->templates[$id]; + } + + return null; + } + + public function getField(string $field, bool $localized = false, bool $silent = false) : mixed + { + if (!$this->curTpl || (!$localized && !isset($this->curTpl[$field]))) + return ''; + + if ($localized) + return Util::localizedString($this->curTpl, $field, $silent); + + $value = $this->curTpl[$field]; + Util::checkNumeric($value); + + return $value; + } + + public function getAllFields(string $field, bool $localized = false, bool $silent = false) : array + { + $data = []; + + foreach ($this->iterate() as $__) + $data[$this->id] = $this->getField($field, $localized, $silent); + + return $data; + } + + public function getRandomId() : int + { + 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; + } + + public function getFoundIDs() : array + { + return array_keys($this->templates); + } + + public function getMatches() : int + { + return $this->matches; + } + + protected function extendQueryOpts(array $extra) : void // needs to be called from __construct + { + foreach ($extra as $tbl => $sets) + { + foreach ($sets as $module => $value) + { + if (!$value || !is_array($value)) + continue; + + switch ($module) + { + // additional (str) + case 'g': // group by + case 's': // select + if (!empty($this->queryOpts[$tbl][$module])) + $this->queryOpts[$tbl][$module] .= implode(' ', $value); + else + $this->queryOpts[$tbl][$module] = implode(' ', $value); + + break; + case 'h': // having + if (!empty($this->queryOpts[$tbl][$module])) + $this->queryOpts[$tbl][$module] .= implode(' AND ', $value); + else + $this->queryOpts[$tbl][$module] = implode(' AND ', $value); + + break; + // additional (arr) + case 'j': // join + if (!empty($this->queryOpts[$tbl][$module]) && is_array($this->queryOpts[$tbl][$module])) + $this->queryOpts[$tbl][$module][0][] = $value; + else + $this->queryOpts[$tbl][$module] = $value; + + break; + // replacement (str) + case 'l': // limit + case 'o': // order by + $this->queryOpts[$tbl][$module] = $value[0]; + break; + } + } + } + } + + 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 %n WHERE `id` = %i', static::$dataTable, $id)) + return new LocString($n); + return null; + } + + public static function makeLink(int $id, int $fmt = Lang::FMT_HTML, string $cssClass = '') : string + { + if ($n = static::getName($id)) + { + return match ($fmt) + { + Lang::FMT_HTML => '
'.$n.'', + Lang::FMT_MARKUP => '[url=?'.Type::getFileString(static::$type).'='.$id.']'.$n.'[/url]', + default => $n + }; + } + + return ''; + } + + /* source More .. keys seen used + 'n': name [always set] + '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; 99: filler trash] [Creature / GO] + 'q': cssQuality [Items] + 'z': zone [set when all happens in here] + 'p': PvP [pvpSourceId] + 's': Type::TITLE: side; Type::SPELL: skillId (yeah, double use. Ain't life just grand) + 'c': category [Spells / Quests] + 'c2': subCat [Quests] + 'icon': iconString + */ + public function getSourceData(int $id = 0) : array { return []; } + + // should return data required to display a listview of any kind + // this is a rudimentary example, that will not suffice for most Types + abstract public function getListviewData() : array; + + // should return data to extend global js variables for a certain type (e.g. g_items) + abstract public function getJSGlobals(int $addMask = GLOBALINFO_ANY) : array; + + // NPC, GO, Item, Quest, Spell, Achievement, Profile would require this + abstract public function renderTooltip() : ?string; +} + +trait listviewHelper +{ + public function hasSetFields(?string ...$fields) : int + { + $result = 0x0; + + foreach ($this->iterate() as $__) + { + foreach ($fields as $k => $str) + { + if (!$str) + { + unset($fields[$k]); + continue; + } + + if ($this->getField($str)) + { + $result |= 1 << $k; + unset($fields[$k]); + } + } + + if (empty($fields)) // all set .. return early + { + $this->reset(); // Generators have no __destruct, reset manually, when not doing a full iteration + return $result; + } + } + + return $result; + } + + public function hasDiffFields(?string ...$fields) : int + { + $base = []; + $result = 0x0; + + foreach ($fields as $k => $str) + $base[$str] = $this->getField($str); + + foreach ($this->iterate() as $__) + { + foreach ($fields as $k => $str) + { + if (!$str) + { + unset($fields[$k]); + continue; + } + + if ($base[$str] != $this->getField($str)) + { + $result |= 1 << $k; + unset($fields[$k]); + } + } + + if (empty($fields)) // all fields diff .. return early + { + $this->reset(); // Generators have no __destruct, reset manually, when not doing a full iteration + return $result; + } + } + + return $result; + } + + public function hasAnySource() : bool + { + if (!isset($this->sources)) + return false; + + foreach ($this->sources as $src) + { + if (!is_array($src)) + continue; + + if (!empty($src)) + return true; + } + + return false; + } +} + +/* + !IMPORTANT! + It is flat out impossible to distinguish between floors for multi-level areas, if the floors overlap each other! + The coordinates generated by the script WILL be on every level and will have to be removed MANUALLY! + + impossible := you are not keen on reading wmo-data; +*/ +trait spawnHelper +{ + private $spawnResult = array( + SPAWNINFO_FULL => null, + SPAWNINFO_SHORT => null, + SPAWNINFO_ZONES => null, + SPAWNINFO_QUEST => null + ); + + private function createShortSpawns() : void // [zoneId, floor, [[x1, y1], [x2, y2], ..]] as tooltip2 if enabled by or anchor #map (one area, one floor, one creature, no survivors) + { + $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` = %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()->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']]; + + $this->spawnResult[SPAWNINFO_SHORT]->zone = $res['areaId']; + $this->spawnResult[SPAWNINFO_SHORT]->coords = [$res['floor'] => $spawns]; + } + } + + // for display on map (object/npc detail page) + private function createFullSpawns(bool $skipWPs = false, bool $skipAdmin = false, bool $hasLabel = false, bool $hasLink = false) : void + { + $data = []; + $wpSum = []; + $wpIdx = 0; + $worldPos = []; + $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')) + $worldPos = WorldPosition::getForGUID(self::$type, ...$guids); + + foreach ($spawns as $s) + { + $isAccessory = $s['guid'] < 0 && $s['type'] == Type::NPC; + + // check, if we can attach waypoints to creature + // 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()->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').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$
".implode('
', $label).'
', + 'type' => $wpIdx + ); + + // connective line + if ($i > 0 && $wPoints[$i - 1]['areaId'] == $p['areaId']) + $opts['lines'] = [[$wPoints[$i - 1]['posX'], $wPoints[$i - 1]['posY']]]; + + $data[$p['areaId']][$p['floor']]['coords'][] = [$p['posX'], $p['posY'], $opts]; + if (empty($wpSum[$p['areaId']][$p['floor']])) + $wpSum[$p['areaId']][$p['floor']] = 1; + else + $wpSum[$p['areaId']][$p['floor']]++; + } + $wpIdx++; + } + } + + $opts = $menu = $tt = $info = []; + $footer = ''; + + if ($s['respawn'] > 0) + $info[1] = ''.Lang::npc('respawnIn', [Lang::formatTime($s['respawn'] * 1000, 'game', 'timeAbbrev', true)]).''; + else if ($s['respawn'] < 0) + { + $info[1] = ''.Lang::npc('despawnAfter', [Lang::formatTime(-$s['respawn'] * 1000, 'game', 'timeAbbrev', true)]).''; + $opts['type'] = 4; // make pip purple + } + + if (!$skipAdmin && User::isInGroup(U_GROUP_STAFF)) + { + if ($isAccessory) + $info[0] = 'Vehicle Accessory'; + else if ($s['guid'] > 0 && ($s['type'] == Type::NPC || $s['type'] == Type::OBJECT)) + $info[0] = 'GUID'.Lang::main('colon').$s['guid']; + + if ($s['phaseMask'] > 1 && ($s['phaseMask'] & 0xFFFF) != 0xFFFF) + $info[2] = Lang::game('phases').Lang::main('colon').Util::asHex($s['phaseMask']); + + if ($s['spawnMask'] == 15) + $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', $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[7] = 'Teleport Destination'; + } + else + { + $o = Util::O2Deg($this->getField('orientation')); + $info[7] = 'Orientation'.Lang::main('colon').$o[0].'° ('.$o[1].')'; + } + } + + // guid < 0 are vehicle accessories. those are moved by moving the vehicle + if (User::isInGroup(U_GROUP_MODERATOR) && $worldPos && !$isAccessory && isset($worldPos[$s['guid']])) + $menu = Util::buildPosFixMenu($worldPos[$s['guid']]['mapId'], $worldPos[$s['guid']]['posX'], $worldPos[$s['guid']]['posY'], $s['type'], $s['guid'], $s['areaId'], $s['floor']); + + if ($menu) + $footer = '
Click to move pin'; + } + + /* recognized opts + * url: string - makes pin clickable + * tooltip: array - title => [info: lines, footer: line] + * label: string - single line tooltip (skipped if 'tooltip' is set) + * menu: array - menu definiton (conflicts with url) + * type: int - colors the pip [default, green, red, blue, purple] + * lines: array - [[destX, destY]] - draws line from point to dest + */ + + if ($info) + $tt['info'] = $info; + + if ($footer) + $tt['footer'] = $footer; + + if ($tt && $this->getEntry($s['typeId'])) + $opts['tooltip'] = [$this->getField('name', true) => $tt]; + else if ($hasLabel && $this->getEntry($s['typeId'])) + $opts['label'] = $this->getField('name', true); + + if ($hasLink) + $opts['url'] = '?' . Type::getFileString(self::$type) . '=' . $s['typeId']; + + if ($menu) + $opts['menu'] = $menu; + + $data[$s['areaId']] [$s['floor']] ['coords'] [] = [$s['posX'], $s['posY'], $opts]; + } + foreach ($data as $a => &$areas) + foreach ($areas as $f => &$floor) + $floor['count'] = count($floor['coords']) - ($wpSum[$a][$f] ?? 0); + + uasort($data, [$this, 'sortBySpawnCount']); + $this->spawnResult[SPAWNINFO_FULL] = $data; + } + + private function sortBySpawnCount(array $a, array $b) : int + { + $aCount = current($a)['count']; + $bCount = current($b)['count']; + + return $bCount <=> $aCount; // sort descending + } + + 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` = %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); + if (count($r) > 3) + array_splice($r, 3, count($r), -1); + } + + $this->spawnResult[SPAWNINFO_ZONES] = $res; + } + + private function createQuestSpawns() :void // [zoneId => [floor => [[x1, y1], [x2, y2], ..]]] mapper on quest detail page + { + if (self::$type == Type::SOUND) + return; + + $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) + { + // zone => floor => spawnData + // todo (low): why is there a single set of coordinates; which one should be picked, instead of the first? gets used in ShowOnMap.buildTooltip i think + if (!isset($spawns[$data['areaId']][$data['floor']][$data['typeId']])) + { + $spawns[$data['areaId']][$data['floor']][$data['typeId']] = array( + 'type' => self::$type, + 'id' => $data['typeId'], + 'point' => '', // tbd later (start, end, requirement, sourcestart, sourceend, sourcerequirement) + 'name' => Util::localizedString($this->templates[$data['typeId']], 'name'), + 'coord' => [$data['posX'], $data['posY']], + 'coords' => [[$data['posX'], $data['posY']]], + 'objective' => 0, // tbd later (1-4 set a color; id of creature this entry gives credit for) + 'reactalliance' => $this->templates[$data['typeId']]['A'] ?: 0, + 'reacthorde' => $this->templates[$data['typeId']]['H'] ?: 0 + ); + } + else + $spawns[$data['areaId']][$data['floor']][$data['typeId']]['coords'][] = [$data['posX'], $data['posY']]; + } + + $this->spawnResult[SPAWNINFO_QUEST] = $spawns; + } + + public function getSpawns(int $mode, bool ...$info) : array|\StdClass + { + // only Creatures, GOs and SoundEmitters can be spawned + if (!self::$type || !$this->getfoundIDs() || (self::$type != Type::NPC && self::$type != Type::OBJECT && self::$type != Type::SOUND && self::$type != Type::AREATRIGGER)) + return []; + + switch ($mode) + { + case SPAWNINFO_SHORT: + if ($this->spawnResult[SPAWNINFO_SHORT] === null) + $this->createShortSpawns(); + + return $this->spawnResult[SPAWNINFO_SHORT]; + case SPAWNINFO_FULL: + if (empty($this->spawnResult[SPAWNINFO_FULL])) + $this->createFullSpawns(...$info); + + return $this->spawnResult[SPAWNINFO_FULL]; + case SPAWNINFO_ZONES: + if (empty($this->spawnResult[SPAWNINFO_ZONES])) + $this->createZoneSpawns(); + + return !empty($this->spawnResult[SPAWNINFO_ZONES][$this->id]) ? $this->spawnResult[SPAWNINFO_ZONES][$this->id] : []; + case SPAWNINFO_QUEST: + if (empty($this->spawnResult[SPAWNINFO_QUEST])) + $this->createQuestSpawns(); + + return $this->spawnResult[SPAWNINFO_QUEST]; + } + + return []; + } +} + +trait profilerHelper +{ + public static $brickFile = 'profile'; // profile is multipurpose + + private static $subjectGUID = 0; + + public function selectRealms(?array $fi) : bool + { + $this->dbNames = []; + + foreach(Profiler::getRealms() as $idx => $r) + { + if (!empty($fi['sv']) && Profiler::urlize($r['name']) != Profiler::urlize($fi['sv']) && intVal($fi['sv']) != $idx) + continue; + + if (!empty($fi['rg']) && Profiler::urlize($r['region']) != Profiler::urlize($fi['rg'])) + continue; + + $this->dbNames[$idx] = 'Characters'; + } + + return !!$this->dbNames; + } +} + +trait sourceHelper +{ + 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 + { + $s = $sm = []; + if (empty($this->sources[$this->id])) + return false; + + if ($this->sourceMore === null) + { + $buff = []; + $this->sourceMore = []; + + foreach ($this->iterate() as $_curTpl) + if ($_curTpl['moreType'] && $_curTpl['moreTypeId']) + $buff[$_curTpl['moreType']][] = $_curTpl['moreTypeId']; + + foreach ($buff as $type => $ids) + $this->sourceMore[$type] = Type::newList($type, [['id', $ids]]); + } + + $s = array_keys($this->sources[$this->id]); + if ($this->curTpl['moreType'] && $this->curTpl['moreTypeId'] && ($srcData = $this->sourceMore[$this->curTpl['moreType']]->getSourceData($this->curTpl['moreTypeId']))) + $sm = $srcData[$this->curTpl['moreTypeId']]; + else if (!empty($this->sources[$this->id][SRC_PVP])) + $sm['p'] = $this->sources[$this->id][SRC_PVP][0]; + + if ($z = $this->curTpl['moreZoneId']) + $sm['z'] = $z; + + if ($this->curTpl['moreMask'] & SRC_FLAG_BOSSDROP) + $sm['bd'] = 1; + + if (isset($this->sources[$this->id][SRC_DROP][0])) + { + /* + mode srcFlag log2 dd Flag + 10N/D-NH 0b0001 0 0b001 + 25N/D-HC 0b0010 1 0b010 + 10H 0b0100 2 0b011 + 25H 0b1000 3 0b100 + */ + 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) + { + $dd = log($this->sources[$this->id][SRC_DROP][0], 2); + if ($dd == intVal($dd)) // only one bit set + $sm['dd'] = $dd + 1; + } + } + + if ($sm) + $sm = [$sm]; + + return true; + } +} + +?> diff --git a/includes/components/filter.class.php b/includes/components/filter.class.php new file mode 100644 index 00000000..de0339f2 --- /dev/null +++ b/includes/components/filter.class.php @@ -0,0 +1,882 @@ +parentCats[0] = $v; // directly redirect onto this region + $v = ''; // remove from filter + + return true; + } + + return false; + } + + protected function cbServerCheck(string &$v) : bool + { + foreach (Profiler::getRealms() as $realm) + if (Profiler::urlize($realm['name'], true) == $v) + { + $this->parentCats[1] = $v; // directly redirect onto this server + $v = ''; // remove from filter + + return true; + } + + return false; + } +} + +abstract class Filter +{ + private static $wCards = ['*' => '%', '?' => '_']; + + public const CR_BOOLEAN = 1; + public const CR_FLAG = 2; + public const CR_NUMERIC = 3; + public const CR_STRING = 4; + public const CR_ENUM = 5; + public const CR_STAFFFLAG = 6; + public const CR_CALLBACK = 7; + public const CR_NYI_PH = 999; + + public const V_EQUAL = 8; + public const V_RANGE = 9; + 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; + + 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}]+$/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, 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( 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); + protected const ENUM_HEROICRAID = array( 4987, 4812, 4722); + protected const ENUM_CLASSS = array( null, 1, 2, 3, 4, 5, 6, 7, 8, 9, null, 11, true, false); + 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; + 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 + + private array $cndSet = []; // db type query storage + private array $rawData = []; + + 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, , , null] + [self::CR_FLAG, , , ] # default param2: matchExact + [self::CR_NUMERIC, , , ] + [self::CR_STRING, , , , , ] # param3 ? crv is val in enum : key in enum + [self::CR_STAFFFLAG, , null, null] + [self::CR_CALLBACK, , , ] + [self::CR_NYI_PH, null, , param2] # mostly 1: to ignore this criterium; 0: to fail the whole query + + $inputFields: fieldName => [VALUE_TYPE, checkInfo, fieldIsArray] + [self::V_EQUAL, , ] + [self::V_RANGE, , ] + [self::V_LIST, , ] + [self::V_CALLBACK, , ] + [self::V_REGEX, , ] + [self::V_NAME, , ] + */ + protected static array $genericFilter = []; + 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 + public string $fiInit = ''; // str: filter template (and init html form) + public string $fiType = ''; // str: filter template (set without init) + public array $fiSetCriteria = []; // fn params (cr, crs, crv) + public array $fiSetWeights = []; // fn params (weights, nt, ids, stealth) + public array $fiReputationCols = []; // fn params ([[factionId, factionName], ...]) + public array $fiExtraCols = []; // + public string $query = ''; // as in url query params + public array $values = []; // prefiltered rawData + + // parse the provided request into a usable format + public function __construct(string|array $data, array $opts = []) + { + $this->parentCats = $opts['parentCats'] ?? []; + + // use fn fi_init() if we have a criteria selector, else use var fi_type + if (static::$genericFilter) + $this->fiInit = $this->type; + else + $this->fiType = $this->type; + + if (is_array($data)) + $this->rawData = $data; // could set >query for consistency sake, but is not used when converting from POST + + if (is_string($data)) + { + // an error occured, while processing POST + if (isset($_SESSION['error']['fi'])) + { + $this->error = $_SESSION['error']['fi'] == get_class($this); + unset($_SESSION['error']['fi']); + } + + $this->query = $data; + $this->rawData = $this->transformGET($data); + } + + $this->initFields(); + $this->evalCriteria(); + $this->evalWeights(); + } + + public function mergeCat(array &$cats) : void + { + foreach ($this->parentCats as $idx => $cat) + $cats[$idx] = $cat; + } + + private function &criteriaIterator() : \Generator + { + if (empty($this->values['cr'])) + return; + + 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->values['cr'][$i], &$this->values['crs'][$i], &$this->values['crv'][$i]]; + yield $i => $v; + } + } + + public static function getCriteriaIndex(int $cr, int|bool $lookup) : ?int + { + // can't use array_search() as bools are valid enum content + foreach (static::$enums[$cr] ?? [] as $k => $v) + if ($v === $lookup) + return $k; + return null; + } + + + /***********************/ + /* get prepared values */ + /***********************/ + + public function buildGETParam(array $override = [], array $addCr = []) : string + { + $get = []; + foreach (array_merge($this->values, $override) as $k => $v) + { + if (isset($addCr[$k])) + { + $v = $v ? array_merge((array)$v, (array)$addCr[$k]) : $addCr[$k]; + unset($addCr[$k]); + } + + if ($v === '' || $v === null || $v === []) + continue; + + $get[$k] = $k.'='.(is_array($v) ? implode(':', $v) : $v); + } + + // no criteria were set, so no merge occured .. append + if ($addCr) + { + $get['cr'] = 'cr='.$addCr['cr']; + $get['crs'] = 'crs='.$addCr['crs']; + $get['crv'] = 'crv='.$addCr['crv']; + } + + return implode(';', $get); + } + + public function getConditions() : array + { + if (!$this->cndSet) + { + // values + $this->cndSet = $this->createSQLForValues(); + + // criteria + $filters = []; + foreach ($this->criteriaIterator() as $_cr) + if ($cnd = $this->createSQLForCriterium(...$_cr)) + $filters[] = $cnd; + + 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 || empty($this->values['cr'])) + return []; + + return array_values(array_intersect($this->values['cr'], $cr)); + } + + + /**********************/ + /* input sanitization */ + /**********************/ + + private function transformGET(string $get) : array + { + if (!$get) + 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 = + $this->shouldReload = true; + continue; + } + + [$k, $v] = explode('=', $field); + + if (!isset(static::$inputFields[$k])) + { + trigger_error('Filter::transformGET - GET param not in filter: '.$k, E_USER_NOTICE); + $this->error = + $this->shouldReload = true; + continue; + } + + $asArray = static::$inputFields[$k][2]; + + $data[$k] = $asArray ? explode(':', $v) : $v; + } + + return $data; + } + + 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]) + { + if (!isset($this->rawData[$inp]) || $this->rawData[$inp] === '') + { + $this->values[$inp] = $asArray ? [] : null; + continue; + } + + $val = $this->rawData[$inp]; + + if ($asArray) + { + $buff = []; + 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->values[$inp] = $buff; + } + else + $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']); + } + } + + private function evalCriteria() : void // [cr]iterium, [cr].[s]ign, [cr].[v]alue + { + if (empty($this->values['cr']) && empty($this->values['crs']) && empty($this->values['crv'])) + return; + + 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->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 = min(5, count($_cr), count($_crv), count($_crs)); + if (count($_cr) > $min) + array_splice($_cr, $min); + + if (count($_crv) > $min) + array_splice($_crv, $min); + + if (count($_crs) > $min) + array_splice($_crs, $min); + + trigger_error('Filter::evalCriteria - cr, crs, crv are imbalanced', E_USER_NOTICE); + $this->error = + $this->shouldReload = true; + } + + for ($i = 0; $i < count($_cr); $i++) + { + if (!isset(static::$genericFilter[$_cr[$i]]) || $_crs[$i] === '' || $_crv[$i] === '') + { + 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; + } + + [$crType, $colOrFn, $param1, $param2] = array_pad(static::$genericFilter[$_cr[$i]], 4, null); + + // 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; + + 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 = [$_cr, $_crs, $_crv]; + } + + private function evalWeights() : void + { + // both empty: not in use; not an error + if (empty($this->values['wt']) && empty($this->values['wtv'])) + return; + + // one empty: erroneous manual input? + if (!$this->values['wt'] || !$this->values['wtv']) + { + trigger_error('Filter::setWeights - one of wt, wtv is missing', E_USER_NOTICE); + unset($this->values['wt'], $this->values['wtv']); + + $this->error = + $this->shouldReload = true; + return; + } + + $_wt = &$this->values['wt']; + $_wtv = &$this->values['wtv']; + + $nwt = count($_wt); + $nwtv = count($_wtv); + + if ($nwt != $nwtv) + { + trigger_error('Filter::setWeights - wt, wtv are imbalanced', E_USER_NOTICE); + $this->error = + $this->shouldReload = true; + } + + if ($nwt > $nwtv) + array_splice($_wt, $nwtv); + else if ($nwtv > $nwt) + array_splice($_wtv, $nwt); + + $this->fiSetWeights = [$_wt, $_wtv]; + } + + protected function checkInput(int $type, mixed $checkInfo, mixed &$val, bool $recursive = false) : bool + { + switch ($type) + { + case self::V_EQUAL: + if (gettype($checkInfo) == 'integer') + $val = intval($val); + else if (gettype($checkInfo) == 'double') + $val = floatval($val); + else /* if (gettype($checkInfo) == 'string') */ + $val = strval($val); + + if ($checkInfo == $val) + return true; + + break; + case self::V_LIST: + if (!Util::checkNumeric($val, NUM_CAST_INT)) + return false; + + 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; + } + + break; + case self::V_RANGE: + if (Util::checkNumeric($val, NUM_CAST_INT) && $val >= $checkInfo[0] && $val <= $checkInfo[1]) + return true; + + break; + case self::V_CALLBACK: + if ($this->$checkInfo($val)) + return true; + + break; + case self::V_REGEX: + 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: '.Util::toString($checkInfo).' val: '.((string)$val).']', E_USER_NOTICE); + $this->error = true; + } + + return false; + } + + 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 + // then replace search wildcards with sql wildcards + $lk = strtr(str_replace('_', '\\_', $str), self::$wCards); + + return [$lk, $ft, $neg]; + } + + protected function tokenizeString(string $field, string $string, bool $exact = false, bool $allowShort = false) : bool + { + // always allow sub 3 chars for logographic locales + if (Lang::getLocale()->isLogographic()) + $allowShort = true; + + $tokens = $exact ? [$string] : explode(' ', $string); + foreach ($tokens as $t) + { + if ([$like, $fulltext, $ex] = self::transformToken($t, $allowShort)) + { + if ($like) + $this->{$ex ? 'exTokens' : 'inTokens'}[$field][] = $like; + + // 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]; + + if ($sub) + $qry[] = $sub; + } + + 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]) + { + 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]; + } + } + } + + return $qry ? [DB::OR, ...$qry] : []; + } + + protected function int2Op(mixed &$op) : bool + { + $op = match ($op) { + 1 => '>', + 2 => '>=', + 3 => '=', + 4 => '<=', + 5 => '<', + 6 => '!=', + default => null + }; + + return $op !== null; + } + + protected function int2Bool(mixed &$op) : bool + { + $op = match ($op) { + 1 => true, + 2 => false, + default => null + }; + + return $op !== null; + } + + protected function list2Mask(array $list, bool $noOffset = false) : int + { + $mask = 0x0; + $o = $noOffset ? 0 : 1; // schoolMask requires this..? + + foreach ($list as $itm) + $mask += (1 << (intval($itm) - $o)); + + return $mask; + } + + + /**************************/ + /* create conditions from */ + /* generic criteria */ + /**************************/ + + private function genericBoolean(string $field, int $op, bool $isString) : ?array + { + if ($this->int2Bool($op)) + { + $value = $isString ? '' : 0; + $operator = $op ? '!' : null; + + return [$field, $value, $operator]; + } + + return null; + } + + private function genericBooleanFlags(string $field, int $value, int $op, ?bool $matchAny = false) : ?array + { + if (!$this->int2Bool($op)) + return null; + + if (!$op) + return [[$field, $value, '&'], 0]; + else if ($matchAny) + return [[$field, $value, '&'], 0, '!']; + else + return [[$field, $value, '&'], $value]; + } + + private function genericString(string $field, ?int $strFlags, ?string $fulltextCol = null) : ?array + { + $strFlags ??= 0x0; + + $lkCol = $field; + if ($strFlags & STR_LOCALIZED) + $lkCol .= '_loc'.Lang::getLocale()->value; + + $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 + { + if (!Util::checkNumeric($value, $typeCast)) + return null; + + if ($this->int2Op($op)) + return [$field, $value, $op]; + + return null; + } + + private function genericEnum(string $field, mixed $value) : ?array + { + if (is_bool($value)) + return [$field, 0, ($value ? '>' : '<=')]; + else if ($value == self::ENUM_ANY) + return [$field, 0, '!']; + else if ($value == self::ENUM_NONE) + return [$field, 0]; + else if ($value !== null) + return [$field, $value]; + + return null; + } + + + /***********************************/ + /* create conditions from */ + /* non-generic values and criteria */ + /***********************************/ + + protected function createSQLForCriterium(int $cr, int $crs, string $crv) : array + { + if (!static::$genericFilter) // criteria not in use - no error + return []; + + [$crType, $colOrFn, $param1, $param2] = array_pad(static::$genericFilter[$cr], 4, null); + + $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); + + return null; + }; + + $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/announcement.class.php b/includes/components/frontend/announcement.class.php new file mode 100644 index 00000000..16382009 --- /dev/null +++ b/includes/components/frontend/announcement.class.php @@ -0,0 +1,69 @@ +editable = true; + + if ($this->mode != self::MODE_PAGE_TOP && $this->mode != self::MODE_CONTENT_TOP) + $this->mode = self::MODE_PAGE_TOP; + + if ($status != self::STATUS_DISABLED && $status != self::STATUS_ENABLED && $status != self::STATUS_DELETED) + $this->status = self::STATUS_DELETED; + else + $this->status = $status; + } + + public function jsonSerialize() : array + { + $json = array( + 'parent' => 'announcement-' . $this->id, + 'id' => $this->editable ? -$this->id : $this->id, + 'mode' => $this->mode, + 'status' => $this->status, + 'name' => $this->name, + 'text' => (string)$this->text // force LocString to naive string for display + ); + + if ($this->style) + $json['style'] = $this->style; + + return $json; + } + + public function __toString() : string + { + if ($this->status == self::STATUS_DELETED) + return ''; + + return "new Announcement(".Util::toJSON($this).");\n"; + } +} + +?> diff --git a/includes/components/frontend/book.class.php b/includes/components/frontend/book.class.php new file mode 100644 index 00000000..b1ef62e7 --- /dev/null +++ b/includes/components/frontend/book.class.php @@ -0,0 +1,50 @@ +parent) + trigger_error(self::class.'::__construct - initialized without parent element', E_USER_WARNING); + + if (!$this->pages) + trigger_error(self::class.'::__construct - initialized without content', E_USER_WARNING); + else + $this->pages = Util::parseHtmlText($this->pages); + } + + public function &iterate() : \Generator + { + reset($this->pages); + + foreach ($this->pages as $idx => &$page) + yield $idx => $page; + } + + public function jsonSerialize() : array + { + $result = []; + + foreach ($this as $prop => $val) + if ($val !== null && $prop[0] != '_') + $result[$prop] = $val; + + return $result; + } + + public function __toString() : string + { + return "new Book(".Util::toJSON($this).");\n"; + } +} + +?> diff --git a/includes/components/frontend/iconelement.class.php b/includes/components/frontend/iconelement.class.php new file mode 100644 index 00000000..6014bd31 --- /dev/null +++ b/includes/components/frontend/iconelement.class.php @@ -0,0 +1,161 @@ +quality = 'q'.$quality; + else if ($quality !== null) + $this->quality = 'q'; + else + $this->quality = ''; + + if ($size < self::SIZE_SMALL || $size > self::SIZE_LARGE) + { + trigger_error('IconElement::__construct - invalid icon size '.$size.'. Normalied to 1 [small]', E_USER_WARNING); + $this->size = self::SIZE_SMALL; + } + else + $this->size = $size; + + if ($align && !in_array($align, ['left', 'right', 'center', 'justify'])) + { + trigger_error('IconElement::__construct - unset invalid align value "'.$align.'".', E_USER_WARNING); + $this->align = null; + } + else + $this->align = $align; + + if ($type && $typeId && !Type::validateIds($type, $typeId)) + { + $link = false; + trigger_error('IconElement::__construct - invalid typeId '.$typeId.' for '.Type::getFileString($type).'.', E_USER_WARNING); + } + else if (!$type || !$typeId) + $link = false; + + 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); + } + + public function renderContainer(int $lpad = 0, int &$iconIdxOffset = 0, bool $rowWrap = false) : string + { + if (!$this->noIcon) + $this->idx = ++$iconIdxOffset; + + $dom = new \DOMDocument('1.0', 'UTF-8'); + + $td = $dom->createElement('td'); + $th = $dom->createElement('th'); + + if ($this->noIcon) // see Spell/Tools or AchievementCriteria having no actual icon, but placeholder + { + $ul = $dom->createElement('ul'); + $li = $dom->createElement('li'); + $var = $dom->createElement('var', ' '); + $li->appendChild($var); + $ul->appendChild($li); + $th->appendChild($ul); + } + else + { + $th->setAttribute('id', $this->element . $this->idx); + if ($this->align) + $th->setAttribute('align', $this->align); + } + + if ($this->href) + ($a = $dom->createElement('a', htmlentities($this->text)))->setAttribute('href', $this->href); + else + $a = $dom->createTextNode($this->text); + + if ($this->quality) + { + ($sp = $dom->createElement('span'))->setAttribute('class', $this->quality); + $sp->appendChild($a); + $td->appendChild($sp); + } + else + $td->appendChild($a); + + // extraText can be HTML, so import it as a fragment + if ($this->extraText) + { + $fragment = $dom->createDocumentFragment(); + $fragment->appendXML(' '.$this->extraText); + $td->appendChild($fragment); + } + // only for objectives list..? + if ($this->num && $this->size == self::SIZE_SMALL) + $td->appendChild($dom->createTextNode(' ('.$this->num.')')); + + if ($rowWrap) + { + $tr = $dom->createElement('tr'); + $tr->appendChild($th); + $tr->appendChild($td); + $dom->append($tr); + } + else + $dom->append($th, $td); + + return str_repeat(' ', $lpad) . $dom->saveHTML(); + } + + // $WH.ge('icontab-icon1').appendChild(g_spells.createIcon(40120, 1, '1-4', 0)); + + public function renderJS(int $lpad = 0) : string + { + if ($this->noIcon) + return ''; + + $params = [$this->typeId, $this->size]; + if ($this->num || $this->qty) + $params[] = is_int($this->num) ? $this->num : "'".$this->num."'"; + if ($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 new file mode 100644 index 00000000..dff22075 --- /dev/null +++ b/includes/components/frontend/infoboxmarkup.class.php @@ -0,0 +1,70 @@ += count($this->items)) + $this->items[] = $item; + else + array_splice($this->items, $pos, 0, $item); + } + + public function append(string $text) : self + { + if ($_ = $this->prepare()) + $this->replace($_); + + return parent::append($text); + } + + public function __toString() : string + { + // 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->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 new file mode 100644 index 00000000..0c6da5c8 --- /dev/null +++ b/includes/components/frontend/listview.class.php @@ -0,0 +1,191 @@ + ['template' => 'achievement', 'id' => 'achievements', 'name' => '$LANG.tab_achievements' ], + 'areatrigger' => ['template' => 'areatrigger', 'id' => 'areatrigger', ], + 'calendar' => ['template' => 'holidaycal', 'id' => 'calendar', 'name' => '$LANG.tab_calendar' ], + 'class' => ['template' => 'classs', 'id' => 'classes', 'name' => '$LANG.tab_classes' ], + 'commentpreview' => ['template' => 'commentpreview', 'id' => 'comments', 'name' => '$LANG.tab_comments' ], + 'npc' => ['template' => 'npc', 'id' => 'npcs', 'name' => '$LANG.tab_npcs' ], + 'currency' => ['template' => 'currency', 'id' => 'currencies', 'name' => '$LANG.tab_currencies' ], + 'emote' => ['template' => 'emote', 'id' => 'emotes', ], + 'enchantment' => ['template' => 'enchantment', 'id' => 'enchantments', ], + 'event' => ['template' => 'holiday', 'id' => 'holidays', 'name' => '$LANG.tab_holidays' ], + 'faction' => ['template' => 'faction', 'id' => 'factions', 'name' => '$LANG.tab_factions' ], + 'genericmodel' => ['template' => 'genericmodel', 'id' => 'same-model-as', 'name' => '$LANG.tab_samemodelas' ], + 'icongallery' => ['template' => 'icongallery', 'id' => 'icons', ], + 'item' => ['template' => 'item', 'id' => 'items', 'name' => '$LANG.tab_items' ], + 'itemset' => ['template' => 'itemset', 'id' => 'itemsets', 'name' => '$LANG.tab_itemsets' ], + 'mail' => ['template' => 'mail', 'id' => 'mails', ], + 'model' => ['template' => 'model', 'id' => 'gallery', 'name' => '$LANG.tab_gallery' ], + 'object' => ['template' => 'object', 'id' => 'objects', 'name' => '$LANG.tab_objects' ], + 'pet' => ['template' => 'pet', 'id' => 'hunter-pets', 'name' => '$LANG.tab_pets' ], + 'profile' => ['template' => 'profile', 'id' => 'profiles', 'name' => '$LANG.tab_profiles' ], + 'quest' => ['template' => 'quest', 'id' => 'quests', 'name' => '$LANG.tab_quests' ], + 'race' => ['template' => 'race', 'id' => 'races', 'name' => '$LANG.tab_races' ], + 'replypreview' => ['template' => 'replypreview', 'id' => 'comment-replies', 'name' => '$LANG.tab_commentreplies'], + 'reputationhistory' => ['template' => 'reputationhistory', 'id' => 'reputation', 'name' => '$LANG.tab_reputation' ], + 'screenshot' => ['template' => 'screenshot', 'id' => 'screenshots', 'name' => '$LANG.tab_screenshots' ], + 'skill' => ['template' => 'skill', 'id' => 'skills', 'name' => '$LANG.tab_skills' ], + 'sound' => ['template' => 'sound', 'id' => 'sounds', 'name' => '$LANG.types[19][2]' ], + 'spell' => ['template' => 'spell', 'id' => 'spells', 'name' => '$LANG.tab_spells' ], + 'title' => ['template' => 'title', 'id' => 'titles', 'name' => '$LANG.tab_titles' ], + 'topusers' => ['template' => 'topusers', 'id' => 'topusers', 'name' => '$LANG.topusers' ], + 'video' => ['template' => 'video', 'id' => 'videos', 'name' => '$LANG.tab_videos' ], + 'zone' => ['template' => 'zone', 'id' => 'zones', 'name' => '$LANG.tab_zones' ], + 'guide' => ['template' => 'guide', 'id' => 'guides', ] + ); + + private string $id = ''; + private ?string $name = null; + private ?array $data = null; // js:array of object + private ?string $tabs = null; // js:Object; instance of "Tabs" + private ?string $parent = 'lv-generic'; // HTMLNode.id; can be null but is pretty much always 'lv-generic' + private ?string $template = null; + private ?int $mode = null; // js:int; defaults to MODE_DEFAULT + private ?string $note = null; // text in top band + + private ?int $poundable = null; // 0 (no); 1 (always); 2 (yes, w/o sorting); defaults to 1 + private ?int $searchable = null; // js:bool; defaults to FALSE + private ?int $filtrable = null; // js:bool; defaults to FALSE + private ?int $sortable = null; // js:bool; defaults to FALSE + private ?int $searchDelay = null; // in ms; defalts to 333 + private ?int $clickable = null; // js:bool; defaults to TRUE + private ?int $hideBands = null; // js:int; 1:top, 2:bottom, 3:both; + private ?int $hideNav = null; // js:int; 1:top, 2:bottom, 3:both; + private ?int $hideHeader = null; // js:bool + private ?int $hideCount = null; // js:bool + private ?int $debug = null; // js:bool + private ?int $_truncated = null; // js:bool; adds predefined note to top band, because there was too much data to display + private ?int $_errors = null; // js:bool; adds predefined note to top band, because there was an error + private ?int $_petTalents = null; // js:bool; applies modifier for talent levels + + private ?int $nItemsPerPage = null; // js:int; defaults to 50 + private ?int $_totalCount = null; // js:int; used by loot and comments + private ?array $clip = null; // js:array of int {w:, h:} + private ?string $customPound = null; + private ?string $genericlinktype = null; // sometimes set when expecting to display model + private ?array $_upgradeIds = null; // js:array of int (itemIds) + + private null|array|string $extraCols = null; // js:callable or js:array of object + private null|array|string $visibleCols = null; // js:callable or js:array of string + private null|array|string $hiddenCols = null; // js:callable or js:array of string + private null|array|string $sort = null; // js:callable or js:array of colIndizes + + private ?string $onBeforeCreate = null; // js:callable + private ?string $onAfterCreate = null; // js:callable + private ?string $onNoData = null; // js:callable + private ?string $computeDataFunc = null; // js:callable + private ?string $onSearchSubmit = null; // js:callable + private ?string $createNote = null; // js:callable + private ?string $createCbControls = null; // js:callable + private ?string $customFilter = null; // js:callable + private ?string $getItemLink = null; // js:callable + private ?array $sortOptions = null; // js:array of object {id:, name:, hidden:, type:"text", sortFunc:} + + private string $__addIn = ''; + + public function __construct(array $opts, string $template = '', string $addIn = '') + { + if ($template && isset(self::TEMPLATES[$template])) + foreach (self::TEMPLATES[$template] as $k => $v) + $this->$k = $v; + + foreach ($opts as $k => $v) + { + if (property_exists($this, $k)) + { + // reindex arrays to force json_encode to treat them as arrays + if (is_array($v)) // in_array($k, ['data', 'extraCols', 'visibleCols', 'hiddenCols', 'sort', 'sortOptions'])) + $v = array_values($v); + $this->$k = $v; + } + else + trigger_error(self::class.'::__construct - unrecognized option: ' . $k); + } + + if ($addIn && !Template\PageTemplate::test('listviews/', $addIn.'.tpl')) + trigger_error('Nonexistent Listview addin requested: template/listviews/'.$addIn.'.tpl', E_USER_ERROR); + else if ($addIn) + $this->__addIn = 'template/listviews/'.$addIn.'.tpl'; + } + + /** + * @return \Generator rowIndex => dataRow + */ + public function &iterate() : \Generator + { + reset($this->data); + + foreach ($this->data as $idx => &$row) + 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 $ + $tabVar = '$' . $tabVar; + + $this->tabs = $tabVar; + } + + public function setError(bool $enable) : void + { + $this->_errors = $enable ? 1 : null; + } + + public function jsonSerialize() : array + { + $result = []; + + foreach ($this as $prop => $val) + if ($val !== null && substr($prop, 0, 2) != '__') + $result[$prop] = $val; + + return $result; + } + + public function __toString() : string + { + $addIn = ''; + if ($this->__addIn) + $addIn = file_get_contents($this->__addIn).PHP_EOL; + + return $addIn.'new Listview('.Util::toJSON($this).');'.PHP_EOL; + } +} + +?> diff --git a/includes/components/frontend/markup.class.php b/includes/components/frontend/markup.class.php new file mode 100644 index 00000000..23cfc5de --- /dev/null +++ b/includes/components/frontend/markup.class.php @@ -0,0 +1,294 @@ + $v) + { + if (property_exists($this, $k)) + $this->$k = $v; + else + trigger_error(self::class.'::__construct - unrecognized option: ' . $k); + } + + $this->__text = $text; + + if ($parent) + $this->__parent = $parent; + } + + public function getJsGlobals() : array + { + return $this->_parseTags(); + } + + public function getParent() : string + { + return $this->__parent; + } + + + /***********************/ + /* Markup tag handling */ + /***********************/ + + private function _parseTags(array &$jsg = []) : array + { + return self::parseTags($this->__text, $jsg); + } + + public static function parseTags(string $text, array &$jsg = []) : array + { + $jsGlobals = []; + + if (preg_match_all(self::DB_TAG_PATTERN, $text, $matches, PREG_SET_ORDER)) + { + foreach ($matches as $match) + { + if ($match[1] == 'statistic') + $match[1] = 'achievement'; + else if ($match[1] == 'icondb') + $match[1] = 'icon'; + + // todo - respecte forced locale + // match[0] => [achievement=3579 domain=ru], [spell=40120 site=fr] + + if ($match[1] == 'money') + { + if (stripos($match[0], 'items')) + { + if (preg_match('/items=([0-9,]+)/i', $match[0], $submatch)) + { + $sm = explode(',', $submatch[1]); + for ($i = 0; $i < count($sm); $i+=2) + $jsGlobals[Type::ITEM][$sm[$i]] = $sm[$i]; + } + } + + if (stripos($match[0], 'currency')) + { + if (preg_match('/currency=([0-9,]+)/i', $match[0], $submatch)) + { + $sm = explode(',', $submatch[1]); + for ($i = 0; $i < count($sm); $i+=2) + $jsGlobals[Type::CURRENCY][$sm[$i]] = $sm[$i]; + } + } + } + else if ($type = Type::getIndexFrom(Type::IDX_FILE_STR, $match[1])) + $jsGlobals[$type][$match[2]] = $match[2]; + } + } + + Util::mergeJsGlobals($jsg, $jsGlobals); + + return $jsGlobals; + } + + private function _stripTags(array $jsgData = []) : string + { + return self::stripTags($this->__text, $jsgData); + } + + public static function stripTags(string $text, array $jsgData = []) : string + { + // replace DB Tags + $text = preg_replace_callback(self::DB_TAG_PATTERN, function ($match) use ($jsgData) { + if ($match[1] == 'statistic') + $match[1] = 'achievement'; + else if ($match[1] == 'icondb') + $match[1] = 'icon'; + else if ($match[1] == 'money') + { + $moneys = []; + if (stripos($match[0], 'items')) + { + if (preg_match('/items=([0-9,]+)/i', $match[0], $submatch)) + { + $sm = explode(',', $submatch[1]); + for ($i = 0; $i < count($sm); $i += 2) + { + if (!empty($jsgData[Type::ITEM][1][$sm[$i]])) + $moneys[] = $jsgData[Type::ITEM][1][$sm[$i]]['name'] ?? $jsgData[Type::ITEM][1][$match[2]]['name_' . Lang::getLocale()->json()]; + else + $moneys[] = Util::ucFirst(Lang::game('item')).' #'.$sm[$i]; + } + } + } + + if (stripos($match[0], 'currency')) + { + if (preg_match('/currency=([0-9,]+)/i', $match[0], $submatch)) + { + $sm = explode(',', $submatch[1]); + for ($i = 0; $i < count($sm); $i += 2) + { + if (!empty($jsgData[Type::CURRENCY][1][$sm[$i]])) + $moneys[] = $jsgData[Type::CURRENCY][1][$sm[$i]]['name'] ?? $jsgData[Type::CURRENCY][1][$match[2]]['name_' . Lang::getLocale()->json()]; + else + $moneys[] = Util::ucFirst(Lang::game('curency')).' #'.$sm[$i]; + } + } + } + + return Lang::concat($moneys); + } + if ($type = Type::getIndexFrom(Type::IDX_FILE_STR, $match[1])) + { + if (!empty($jsgData[$type][1][$match[2]])) + return $jsgData[$type][1][$match[2]]['name'] ?? $jsgData[$type][1][$match[2]]['name_' . Lang::getLocale()->json()]; + else + return Util::ucFirst(Lang::game($match[1])).' #'.$match[2]; + } + + trigger_error('Markup::stripTags() - encountered unhandled db-tag: '.var_export($match)); + return ''; + }, $text); + + // replace line endings + $text = str_replace('[br]', "\n", $text); + + // strip other Tags + $stripped = ''; + $inTag = false; + for ($i = 0; $i < strlen($text); $i++) + { + if ($text[$i] == '[' && (!$i || $text[$i - 1] != '\\')) + $inTag = true; + if (!$inTag) + $stripped .= $text[$i]; + if ($inTag && $text[$i] == ']' && (!$i || $text[$i - 1] != '\\')) + $inTag = false; + } + + return $stripped; + } + + + /*********************/ + /* String Operations */ + /*********************/ + + public function append(string $text) : self + { + $this->__text .= $text; + return $this; + } + + public function prepend(string $text) : self + { + $this->__text = $text . $this->__text; + return $this; + } + + public function apply(\Closure $fn) : void + { + $this->__text = $fn($this->__text); + } + + public function replace(string $middle, int $offset = 0, ?int $len = null) : self + { + // y no mb_substr_replace >:( + $start = $end = ''; + + if ($offset < 0) + $offset = mb_strlen($this->__text) + $offset; + + $start = mb_substr($this->__text, 0, $offset); + + if (!is_null($len) && $len >= 0) + $end = mb_substr($this->__text, $offset + $len); + else if (!is_null($len) && $len < 0) + $end = mb_substr($this->__text, $offset + mb_strlen($this->__text) + $len); + + $this->__text = $start . $middle . $end; + return $this; + } + + private function cleanText() : string + { + // break script-tags, unify newlines + $val = preg_replace(['/script\s*\>/i', "/\r\n/", "/\r/"], ['script>', "\n", "\n"], $this->__text); + + return strtr(Util::jsEscape($val), ['script>' => 'scr"+"ipt>']); + } + + public function jsonSerialize() : array + { + $result = []; + + foreach ($this as $prop => $val) + if ($val !== null && $prop[0] != '_') + $result[$prop] = $val; + + return $result; + } + + public function __toString() : string + { + if ($this->jsonSerialize()) + return 'Markup.printHtml("'.$this->cleanText().'", "'.$this->__parent.'", '.Util::toJSON($this).");\n"; + + return 'Markup.printHtml("'.$this->cleanText().'", "'.$this->__parent."\");\n"; + } +} + +?> diff --git a/includes/components/frontend/summary.class.php b/includes/components/frontend/summary.class.php new file mode 100644 index 00000000..62ad87e5 --- /dev/null +++ b/includes/components/frontend/summary.class.php @@ -0,0 +1,70 @@ + $v) + { + if (property_exists($this, $k)) + $this->$k = $v; + else + trigger_error(self::class.'::__construct - unrecognized option: ' . $k); + } + + if (!$this->template) + trigger_error(self::class.'::__construct - initialized without template', E_USER_WARNING); + if (!$this->id) + trigger_error(self::class.'::__construct - initialized without HTMLNode#id to reference', E_USER_WARNING); + } + + public function &iterate() : \Generator + { + reset($this->groups); + + foreach ($this->groups as $idx => &$group) + yield $idx => $group; + } + + public function addGroup(array $group) : void + { + $this->groups[] = $group; + } + + public function jsonSerialize() : array + { + $result = []; + + foreach ($this as $prop => $val) + if ($val !== null && $prop[0] != '_') + $result[$prop] = $val; + + return $result; + } + + public function __toString() : string + { + return "new Summary(".Util::toJSON($this).");\n"; + } +} + +?> diff --git a/includes/components/frontend/tabs.class.php b/includes/components/frontend/tabs.class.php new file mode 100644 index 00000000..1fa14cd1 --- /dev/null +++ b/includes/components/frontend/tabs.class.php @@ -0,0 +1,145 @@ + $v) + { + if (property_exists($this, $k)) + $this->$k = $v; + else + trigger_error(self::class.'::__construct - unrecognized option: ' . $k); + } + } + + /** + * @return \Generator tabIndex => Listview + */ + public function &iterate() : \Generator + { + reset($this->__tabs); + + foreach ($this->__tabs as $idx => &$tab) + yield $idx => $tab; + } + + public function addListviewTab(Listview $lv) : void + { + $this->__tabs[] = $lv; + } + + public function addDataTab(string $id, string $name, string $data) : void + { + $this->__tabs[] = ['id' => $id, 'name' => $name, 'data' => $data]; + $this->__forceTabs = true; // otherwise a single DataTab could not be accessed + } + + public function getDataContainer() : \Generator + { + foreach ($this->__tabs as $tab) + if (is_array($tab)) + yield ''; + } + + public function getFlush() : string + { + if ($this->isTabbed()) + return $this->__tabVar.".flush();"; + + return ''; + } + + public function isTabbed() : bool + { + return count($this->__tabs) > 1 || $this->__forceTabs; + } + + + /***********************/ + /* enable deep cloning */ + /***********************/ + + public function __clone() + { + foreach ($this->__tabs as $idx => $tab) + { + if (is_array($tab)) + continue; + + $this->__tabs[$idx] = clone $tab; + } + } + + + /******************/ + /* make countable */ + /******************/ + + public function count() : int + { + return count($this->__tabs); + } + + + /************************/ + /* make Tabs stringable */ + /************************/ + + public function jsonSerialize() : array + { + $result = []; + + foreach ($this as $prop => $val) + if ($val !== null && $prop[0] != '_') + $result[$prop] = $val; + + return $result; + } + + public function __toString() : string + { + $result = ''; + + if ($this->isTabbed()) + $result .= "var ".$this->__tabVar." = new Tabs(".Util::toJSON($this).");\n"; + + foreach ($this->__tabs as $tab) + { + if (is_array($tab)) + { + $n = $tab['name'][0] == '$' ? substr($tab['name'], 1) : "'".$tab['name']."'"; + $result .= $this->__tabVar.".add(".$n.", { id: '".$tab['id']."' });\n"; + } + else + { + if ($this->isTabbed()) + $tab->setTabs($this->__tabVar); + + $result .= $tab; // Listview::__toString here + } + } + + return $result . "\n"; + } +} + +?> diff --git a/includes/components/frontend/tooltip.class.php b/includes/components/frontend/tooltip.class.php new file mode 100644 index 00000000..0042c417 --- /dev/null +++ b/includes/components/frontend/tooltip.class.php @@ -0,0 +1,64 @@ +__subject)) + $this->__subject = Util::toJSON($this->__subject, JSON_UNESCAPED_UNICODE); + + foreach ($opts as $k => $v) + { + if (property_exists($this, $k)) + $this->$k = $v; + else + trigger_error(self::class.'::__construct - unrecognized option: ' . $k); + } + } + + public function jsonSerialize() : array + { + $out = []; + + $locString = Lang::getLocale()->json(); + + foreach ($this as $k => $v) + { + if ($v === null || $k[0] == '_') + continue; + + if ($k == 'icon') + $out[$k] = rawurldecode($v); + else if ($k == 'quality' || $k == 'map' || $k == 'daily') + $out[$k] = $v; + else + $out[$k . '_' . $locString] = $v; + } + + return $out; + } + + public function __toString() : string + { + 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 new file mode 100644 index 00000000..61a41b5b --- /dev/null +++ b/includes/components/guidemgr.class.php @@ -0,0 +1,104 @@ + '#71D5FF', + self::STATUS_REVIEW => '#FFFF00', + self::STATUS_APPROVED => '#1EFF00', + self::STATUS_REJECTED => '#FF4040', + self::STATUS_ARCHIVED => '#FFD100' + ); + + private static array $ratingsStore = []; + private static ?int $imgUploadIdx = null; + + public static function createDescription(string $text) : string + { + return Lang::trimTextClean(Markup::stripTags($text), 120); + } + + public static function getRatings(array $guideIds) : array + { + if (!$guideIds) + return []; + + if (array_keys(self::$ratingsStore) == $guideIds) + return self::$ratingsStore; + + self::$ratingsStore = array_fill_keys($guideIds, ['nvotes' => 0, 'rating' => -1]); + + $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; + self::$ratingsStore[$id]['_self'] = (int)$self; + if ($count >= 5 ) + self::$ratingsStore[$id]['rating'] = $total / $count; + } + + return self::$ratingsStore; + } + + public static function handleUpload() : array + { + require_once('includes/libs/qqFileUploader.class.php'); + + $tmpFile = User::$username.'-'.Type::GUIDE.'-0-'.Util::createHash(16); + + $uploader = new \qqFileUploader(['jpg', 'jpeg', 'png'], 10 * 1024 * 1024); + $result = $uploader->handleUpload(self::IMG_TMP_DIR, $tmpFile, true); + + if (isset($result['error'])) + return $result; + + $mime = (new \finfo(FILEINFO_MIME))?->file(self::IMG_TMP_DIR . $result['newFilename']); + + if (!preg_match('/^image\/(png|jpe?g)/i', $mime, $m)) + return ['error' => Lang::screenshot('error', 'unkFormat')]; + + // find next empty image name (an int) + if (is_null(self::$imgUploadIdx)) + { + if ($files = scandir(self::IMG_DEST_DIR, SCANDIR_SORT_DESCENDING)) + if (rsort($files, SORT_NATURAL) && $files[0] != '.' && $files[0] != '..') + $i = explode('.', $files[0])[0] + 1; + + self::$imgUploadIdx = $i ?? 1; + } + + $targetFile = self::$imgUploadIdx . ($m[1] == 'png' ? '.png' : '.jpg'); + + // move to final location + if (!rename(self::IMG_TMP_DIR.$result['newFilename'], self::IMG_DEST_DIR.$targetFile)) + { + trigger_error('GuideMgr::handleUpload - failed to move file', E_USER_ERROR); + return ['error' => Lang::main('intError')]; + } + + return array( + 'success' => true, + 'id' => self::$imgUploadIdx, + 'type' => $m[1] == 'png' ? 3 : 2 + ); + } +} + +?> diff --git a/includes/components/imageupload.class.php b/includes/components/imageupload.class.php new file mode 100644 index 00000000..e8fa79a5 --- /dev/null +++ b/includes/components/imageupload.class.php @@ -0,0 +1,306 @@ + self::loadFromJPG(), + self::MIME_PNG => self::loadFromPNG(), + self::MIME_WEBP => self::loadFromWEBP(), + default => false + }; + } + + public static function loadFile(string $path, string $nameBase) : bool + { + self::$fileName = sprintf($path, $nameBase); + + if (!file_exists(self::$fileName)) + { + trigger_error('ImageUpload::loadFile - image ('.self::$fileName.') not found', E_USER_ERROR); + self::$fileName = ''; + return false; + } + + // we are using only jpg internally + return self::loadFromJPG(); + } + + public static function calcImgDimensions() : array + { + if (!self::$img) + return []; + + $oSize = $rSize = [imagesx(self::$img), imagesy(self::$img)]; + $rel = $oSize[0] / $oSize[1]; + + // check for oversize and refit to crop-screen + if ($rel >= 1.5 && $oSize[0] > self::CROP_W) + $rSize = [self::CROP_W, self::CROP_W / $rel]; + else if ($rel < 1.5 && $oSize[1] > self::CROP_H) + $rSize = [self::CROP_H * $rel, self::CROP_H]; + + // r: resized; o: original + // r: x <= 488 && y <= 325 while x proportional to y + return array( + 'oWidth' => $oSize[0], + 'rWidth' => $rSize[0], + 'oHeight' => $oSize[1], + 'rHeight' => $rSize[1] + ); + } + + public static function tempSaveUpload(array $tmpNameParts, ?string &$uid) : bool + { + if (!self::$img || !$tmpNameParts) + return false; + + $uid = Util::createHash(16); + + $nameBase = User::$username.'-'.implode('-', $tmpNameParts).'-'.$uid; + + // use this image for work + if (!self::writeImage(static::$tmpPath, $nameBase.'_original')) + return false; + + ['oWidth' => $oW, 'rWidth' => $rW, 'oHeight' => $oH, 'rHeight' => $rH] = self::calcImgDimensions(); + + // use this image to display in cropper + $res = imagecreatetruecolor($rW, $rH); + if (!$res) + { + trigger_error('ImageUpload::tempSaveUpload - imagecreate failed', E_USER_ERROR); + return false; + } + + if (!imagecopyresampled($res, self::$img, 0, 0, 0, 0, $rW, $rH, $oW, $oH)) + { + trigger_error('ImageUpload::tempSaveUpload - imagecopy failed', E_USER_ERROR); + return false; + } + + self::$img = $res; + unset($res); + + return self::writeImage(static::$tmpPath, $nameBase); + } + + public static function cropImg(float $scaleX, float $scaleY, float $scaleW, float $scaleH) : bool + { + if (!self::$img) + return false; + + $x = (int)(imagesx(self::$img) * $scaleX); + $y = (int)(imagesy(self::$img) * $scaleY); + $w = (int)(imagesx(self::$img) * $scaleW); + $h = (int)(imagesy(self::$img) * $scaleH); + + $destImg = imagecreatetruecolor($w, $h); + if (!$destImg) + return false; + + // imagefill($destImg, 0, 0, imagecolorallocate($destImg, 255, 255, 255)); + imagecopy($destImg, self::$img, 0, 0, $x, $y, $w, $h); + + self::$img = $destImg; + imagedestroy($destImg); + + return true; + } + + public static function writeImage(string $path, string $file) : bool + { + if (!self::$img) + return false; + + if (imagejpeg(self::$img, sprintf($path, $file), self::JPEG_QUALITY)) + return true; + + trigger_error('ImageUpload::writeImage - write failed', E_USER_ERROR); + return false; + } + + private static function setMimeType() : bool + { + if (!self::$hasUpload) + return false; + + $mime = (new \finfo(FILEINFO_MIME))?->file(self::$fileName); + + if ($mime && stripos($mime, 'image/png') === 0) + self::$mimeType = self::MIME_PNG; + else if ($mime && stripos($mime, 'image/webp') === 0) + self::$mimeType = self::MIME_WEBP; + else if ($mime && preg_match('/^image\/jpe?g/i', $mime)) + self::$mimeType = self::MIME_JPG; + else + trigger_error('ImageUpload::setMimeType - uploaded file is of type: '.$mime, E_USER_WARNING); + + return self::$mimeType != self::MIME_UNK; + } + + private static function loadFromPNG() : bool + { + // straight self::$img = imagecreatefrompng(self::$fileName); causes issues when transforming the alpha channel + // this roundabout way through imagealphablending() avoids that + $image = imagecreatefrompng(self::$fileName); + if (!$image) + return false; + + self::$img = imagecreatetruecolor(imagesx($image), imagesy($image)) ?: null; + if (!self::$img) + return false; + + imagealphablending(self::$img, true); + imagecopy(self::$img, $image, 0, 0, 0, 0, imagesx($image), imagesy($image)); + imagedestroy($image); + + return true; + } + + private static function loadFromJPG() : bool + { + self::$img = imagecreatefromjpeg(self::$fileName) ?: null; + + return !is_null(self::$img); + } + + private static function loadFromWEBP() : bool + { + $image = imagecreatefromwebp(self::$fileName); + if (!$image) + return false; + + self::$img = imagecreatetruecolor(imagesx($image), imagesy($image)) ?: null; + if (!self::$img) + return false; + + imagealphablending(self::$img, true); + imagecopy(self::$img, $image, 0, 0, 0, 0, imagesx($image), imagesy($image)); + imagedestroy($image); + + return true; + } + + protected static function resizeAndWrite(int $limitW, int $limitH, string $path, string $file) : bool + { + $srcW = imagesx(self::$img); + $srcH = imagesy(self::$img); + + // already small enough + if ($srcW < $limitW && $srcH < $limitH) + return true; + + $scale = min(1.0, $limitW / $srcW, $limitH / $srcH); + $destW = $srcW * $scale; + $destH = $srcH * $scale; + + $destImg = imagecreatetruecolor($destW, $destH); + + // imagefill($destImg, 0, 0, imagecolorallocate($destImg, 255, 255, 255)); + imagecopyresampled($destImg, self::$img, 0, 0, 0, 0, $destW, $destH, $srcW, $srcH); + + return imagejpeg($destImg, sprintf($path, $file), self::JPEG_QUALITY); + } +} + +?> diff --git a/includes/components/locstring.class.php b/includes/components/locstring.class.php new file mode 100644 index 00000000..2bfec006 --- /dev/null +++ b/includes/components/locstring.class.php @@ -0,0 +1,67 @@ +store = new \WeakMap(); + + $callback ??= fn($x) => $x; + + if (!array_filter($data, fn($v, $k) => $v && strstr($k, $key.'_loc'), ARRAY_FILTER_USE_BOTH)) + trigger_error('LocString - is entrirely empty', E_USER_WARNING); + + foreach (Locale::cases() as $l) + if ($l->validate()) + $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()]) + return $str; + + foreach (Locale::cases() as $l) // desired loc not set, use any other + if (isset($this->store[$l])) + return Cfg::get('DEBUG') ? '['.$this->store[$l].']' : $this->store[$l]; + + return Cfg::get('DEBUG') ? '[LOCSTRING]' : ''; + } + + public function __serialize(): array + { + $data = []; + foreach (Locale::cases() as $l) + if (isset($this->store[$l])) + $data[$l->value] = $this->store[$l]; + + return ['store' => $data]; + } + + public function __unserialize(array $data): void + { + $this->store = new \WeakMap(); + + if (empty($data['store'])) + return; + + foreach ($data['store'] as $locId => $str) + if (($l = Locale::tryFrom($locId))?->validate()) + $this->store[$l] = (string)$str; + } +} + +?> diff --git a/includes/components/pagetemplate.class.php b/includes/components/pagetemplate.class.php new file mode 100644 index 00000000..59c3d50b --- /dev/null +++ b/includes/components/pagetemplate.class.php @@ -0,0 +1,579 @@ +locale = Lang::getLocale(); + $this->user = User::class; + + self::__wakeup(); // init non-cached properties; + } + + public function addDataLoader(string ...$dataFile) : void + { + foreach ($dataFile as $df) + $this->dataLoader[] = $df; + } + + public function addScript(int $type, string $str, int $flags = 0x0) : bool + { + $tpl = match ($type) + { + SC_CSS_FILE => '', + SC_CSS_STRING => '', + SC_JS_FILE => '', + SC_JS_STRING => '', + default => '' + }; + + if (!$str) + { + trigger_error('PageTemplate::addScript - content empty', E_USER_WARNING); + return false; + } + + if (!$tpl) + { + trigger_error('PageTemplate::addScript - unknown script type #'.$type, E_USER_WARNING); + return false; + } + + // insert locale string + if ($flags & SC_FLAG_LOCALIZED) + $str = sprintf($str, Lang::getLocale()->json()); + + $this->scripts[] = [$type, $str, $flags, $tpl]; + return true; + } + + /* (optional) set pre-render hooks */ + + public function registerDisplayHook(string $var, callable $fn) : void + { + $this->displayHooks[$var][] = $fn; + } + + private function getDisplayHooks(string $var) : array + { + return $this->displayHooks[$var] ?? []; + } + + /* 3) self test, ready to be cached now */ + + public function prepare() : bool + { + if (!self::test('template/pages/', $this->template)) + { + trigger_error('Error: nonexistent template requested: template/pages/'.$this->template.'.tpl.php', E_USER_ERROR); + return false; + } + + // TODO - more checks and preparations + + return true; + } + + /* 4) display */ + + public function render() : void + { + $this->update(); + + include('template/pages/'.$this->template.'.tpl.php'); + } + + + /***********/ + /* loaders */ + /***********/ + + // "template_exists" + public static function test(string $path, string $file) : bool + { + if (!preg_match('/^[\w\-_]+(\.tpl(\.php)?)?$/i', $file)) + return false; + + if ($path && preg_match('/\\{2,}|\/{2,}|\.{2,}|~/i', $path)) + return false; + + if (!is_file('template/'.$path.$file)) + return false; + + return true; + } + + // load brick + private function brick(string $file, array $localVars = []) : void + { + $file .= '.tpl.php'; + + if (!self::test('bricks/', $file)) + { + trigger_error('Nonexistent template requested: template/bricks/'.$file, E_USER_ERROR); + return; + } + + foreach ($localVars as $n => $v) + $$n = $v; + + include('template/bricks/'.$file); + } + + private function brickIf(mixed $boolish, string $file, array $localVars = []) : void + { + if ($boolish) + $this->brick($file, $localVars); + } + + // load brick with more text then vars + private function localizedBrick(string $file, array $localVars = []) : void + { + foreach ($localVars as $n => $v) + $$n = $v; + + $_file = $file.'_'.$this->locale->value.'.tpl.php'; + if (self::test('localized/', $_file)) + { + include('template/localized/'.$_file); + return; + } + + $_file = $file.'_'.$this->locale->getFallback()->value.'.tpl.php'; + if (self::test('localized/', $_file)) + { + include('template/localized/'.$_file); + return; + } + + trigger_error('Nonexistent template requested: template/localized/'.$_file, E_USER_ERROR); + } + + private function localizedBrickIf(mixed $boolish, string $file, array $localVars = []) : void + { + if ($boolish) + $this->localizedBrick($file, $localVars); + } + + + /****************/ + /* Util wrapper */ + /****************/ + + private function cfg(string $name) : mixed + { + return Cfg::get($name); + } + + private function json(mixed $var, int $jsonFlags = 0x0, bool $varRef = false) : string + { + 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($varRef ? $this->$var : $var, $jsonFlags) ?: "{}"); + } + + private function escHTML(string $var, bool $varRef = false) : string|array + { + return Util::htmlEscape($varRef ? $this->$var : $var); + } + + private function escJS(string $var, bool $varRef = false) : string|array + { + return Util::jsEscape($varRef ? $this->$var : $var); + } + + private function ucFirst(string $var, bool $varRef = false) : string + { + return Util::ucFirst($varRef ? $this->$var : $var); + } + + + /*****************/ + /* render helper */ + /*****************/ + + private function concat(string $arrVar, string $separator = '') : string + { + if (!is_array($this->$arrVar)) + return ''; + + return implode($separator, $this->$arrVar); + } + + private function renderArray(string|array $arrVar, int $lpad = 0) : string + { + $data = []; + if (is_string($arrVar) && isset($this->$arrVar) && is_array($this->$arrVar)) + $data = $this->$arrVar; + else if (is_array($arrVar)) + $data = $arrVar; + + $buff = ''; + foreach ($data as $x) + $buff .= str_repeat(' ', $lpad) . $x . "\n"; + + return $buff; + } + + // load jsGlobals + private function renderGlobalVars(int $lpad = 0) : string + { + $buff = ''; + + if ($this->guideRating) + $buff .= str_repeat(' ', $lpad).sprintf(self::GUIDE_RATING_TPL, ...$this->guideRating); + + foreach ($this->jsGlobals as [$jsVar, $data, $extraData]) + { + $buff .= str_repeat(' ', $lpad).'var _ = '.$jsVar.';'; + + foreach ($data as $key => $data) + $buff .= ' _['.(is_numeric($key) ? $key : "'".$key."'")."]=".Util::toJSON($data).';'; + + $buff .= "\n"; + + if (isset($this->gPageInfo['type']) && isset($this->gPageInfo['typeId']) && isset($extraData[$this->gPageInfo['typeId']])) + { + $buff .= "\n"; + foreach ($extraData[$this->gPageInfo['typeId']] as $k => $v) + if ($v) + $buff .= str_repeat(' ', $lpad).'_['.$this->gPageInfo['typeId'].'].'.$k.' = '.Util::toJSON($v).";\n"; + $buff .= "\n"; + } + } + + return $buff; + } + + private function renderSeriesItem(int $idx, array $list, int $lpad = 0) : string + { + $result = ''.($idx + 1).'
\n"; + } + + private function renderFilter(int $lpad = 0) : string + { + $result = []; + + // it's worth noting, that this only works on non-cached page calls. Luckily Profiler pages are not cached. + if ($this->context instanceof \Aowow\IProfilerList) + { + $result[] = "pr_setRegionRealm(\$WH.ge('fi').firstChild, '".$this->region."', '".$this->realm."');"; + + if (!empty($this->filter->values['ra'])) + $result[] = "pr_onChangeRace();"; + } + + if ($this->filter->fiInit) // str: filter template (and init html form) + $result[] = "fi_init('".$this->filter->fiInit."');"; + else if ($this->filter->fiType) // str: filter template (set without init) + $result[] = "var fi_type = '".$this->filter->fiType."'"; + + if ($this->filter->fiSetCriteria) // arr:criteria, arr:signs, arr:values + $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 ..?) + ids: weights are encoded as ids, not by their js name and need conversion before use + stealth: the ub-selector (items filter) will not visually change (so what..?) + */ + if ($this->filter->fiSetWeights) // arr:weights, bool:nt[0], bool:ids[1], bool:stealth[1] + $result[] = 'fi_setWeights('.Util::toJSON(array_values($this->filter->fiSetWeights)).', 0, 1, 1);'; + + if ($this->filter->fiExtraCols) // arr:extraCols + $result[] = 'fi_extraCols = '.Util::toJSON(array_values(array_unique($this->filter->fiExtraCols))).";"; + + return str_repeat(' ', $lpad)."\n"; + } + + private function makeOptionsList(array $data, mixed $selectedIdx = null, int $lpad = 0, ?callable $callback = null) : string + { + $callback ??= fn(&$v, &$k) => $v; // default callback: skip empty descriptors + $options = ''; + + foreach ($data as $idx => $str) + { + $extraAttributes = []; + if (!$callback($str, $idx, $extraAttributes)) + continue; + + if ($idx === '' || !$str) + continue; + + $options .= str_repeat(' ', max(0, $lpad)).' $v) + $options .= ' '.$k.'="'.$v.'"'; + + if (is_array($selectedIdx) && in_array($idx, $selectedIdx)) + $options .= ' selected="selected"'; + else if (!is_null($selectedIdx) && $selectedIdx == $idx) + $options .= ' selected="selected"'; + + $options .= ' value="'.$idx.'">'.$str.''.($lpad < 0 ? '' : "\n"); + } + + return $options; + } + + private function makeRadiosList(string $name, array $data, mixed $selectedIdx = null, int $lpad = 0, ?callable $callback = null) : string + { + $callback ??= fn(&$v, &$k) => $v; // default callback: skip empty descriptors + $options = ''; + + foreach ($data as $idx => [$title, $id]) + { + $extraAttributes = []; + if (!$callback($title, $idx, $extraAttributes)) + continue; + + if ($id === '' || !$title) + continue; + + $options .= str_repeat(' ', max(0, $lpad)).' $v) + $options .= ' '.$k.'="'.$v.'"'; + + $options .= '>'.$title.''.($lpad < 0 ? '' : "\n"); + } + + return $options; + } + + // unordered stuff + + private function prepareScripts() : void + { + $this->js = $this->css = []; + + foreach ($this->scripts as [$type, $str, $flags, $tpl]) + { + $app = []; + + if (($flags & SC_FLAG_APPEND_LOCALE) && $this->locale != \Aowow\Locale::EN) + $app[] = 'lang='.$this->locale->domain(); + + // append anti-cache timestamp + if (!($flags & SC_FLAG_NO_TIMESTAMP)) + if ($type == SC_JS_FILE || $type == SC_CSS_FILE) + $app[] = filemtime('static/'.$str) ?: 0; + + if ($app) + $appendix = '?'.implode('&', $app); + + if ($type == SC_JS_FILE || $type == SC_CSS_FILE) + $str = Cfg::get('STATIC_URL').'/'.$str; + + if ($flags & SC_FLAG_PREFIX) + { + if ($type == SC_JS_FILE || $type == SC_JS_STRING) + array_unshift($this->js, sprintf($tpl, $str, $appendix ?? '')); + else + array_unshift($this->css, sprintf($tpl, $str, $appendix ?? '')); + } + else + { + if ($type == SC_JS_FILE || $type == SC_JS_STRING) + array_push($this->js, sprintf($tpl, $str, $appendix ?? '')); + else + array_push($this->css, sprintf($tpl, $str, $appendix ?? '')); + } + } + + if ($data = array_unique($this->dataLoader)) + { + $args = array( + 'data' => implode('.', $data), + 'locale' => $this->locale->value, + 't' => $_SESSION['dataKey'] + ); + + array_push($this->js, ''); + } + } + + // refresh vars that shouldn't be cached + private function update() : void + { + // not set, but should be + if (!isset($_COOKIE['consent']) && $this->hasAnalytics) + { + $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; + } + else + $this->consentFooter = false; + + // analytics + consent + // not set or declined + if (empty($_COOKIE['consent'])) + $this->hasAnalytics = false; + + // js + css + $this->prepareScripts(); + + // db profiling + if (Cfg::get('DEBUG') >= LOG_LEVEL_INFO && User::isInGroup(U_GROUP_DEV | U_GROUP_ADMIN)) + $this->dbProfiles = \Aowow\DB::getProfiles(); + } + + public function setListviewError() : void + { + if (!$this->lvTabs) + return; + + foreach ($this->lvTabs->iterate() as $lv) + if ($lv instanceof \Aowow\Listview) + $lv->setError(true); + } + + // pre-serialization: if a var is relevant it was stored in $rawData + public function __sleep() : array + { + $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; + + return $vars; + } + + public function __wakeup() : void + { + $this->gStaticUrl = Cfg::get('STATIC_URL'); + $this->gHost = Cfg::get('HOST_URL'); + $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 + { + $this->pageData[$var] = $value; + } + + public function __get(string $var) : mixed + { + // modified data exists + if (isset($this->pageData[$var])) + return $this->pageData[$var]; + + if (!isset($this->rawData[$var])) + { + if (!$this->context) + return null; + + if (!isset(get_object_vars($this->context)[$var])) + return null; + + $this->rawData[$var] = $this->context->$var; + } + + if ($hooks = $this->getDisplayHooks($var)) + { + if (is_object($this->rawData[$var])) // is frontend component + $this->pageData[$var] = clone $this->rawData[$var]; + else + $this->pageData[$var] = $this->rawData[$var]; + + foreach ($hooks as $fn) + $fn($this, $this->pageData[$var]); + } + + return $this->pageData[$var] ?? $this->rawData[$var]; + } +} diff --git a/includes/components/profiler.class.php b/includes/components/profiler.class.php new file mode 100644 index 00000000..e3c4d452 --- /dev/null +++ b/includes/components/profiler.class.php @@ -0,0 +1,1078 @@ + [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) + 'tw' => [14, 15], // TW (tw, tournament) + 'cn' => [16, 17, 18, 19, 20, 21, 22, 23, 24, 25], // CN (cn, CN1-8, tournament) + 'dev' => [1, 26, 27, 28, 30] // Development, Test Server, Test Server - tournament, QA Server, Test Server 2 + ); + + private static array $realms = []; + + public static array $slot2InvType = array( + 1 => [INVTYPE_HEAD], // head + 2 => [INVTYPE_NECK], // neck + 3 => [INVTYPE_SHOULDERS], // shoulder + 4 => [INVTYPE_BODY], // shirt + 5 => [INVTYPE_CHEST, INVTYPE_ROBE], // chest + 6 => [INVTYPE_WAIST], // waist + 7 => [INVTYPE_LEGS], // legs + 8 => [INVTYPE_FEET], // feet + 9 => [INVTYPE_WRISTS], // wrists + 10 => [INVTYPE_HANDS], // hands + 11 => [INVTYPE_FINGER], // finger1 + 12 => [INVTYPE_FINGER], // finger2 + 13 => [INVTYPE_TRINKET], // trinket1 + 14 => [INVTYPE_TRINKET], // trinket2 + 15 => [INVTYPE_CLOAK], // chest + 16 => [INVTYPE_WEAPONMAINHAND, INVTYPE_WEAPON, INVTYPE_2HWEAPON], // mainhand + 17 => [INVTYPE_WEAPONOFFHAND, INVTYPE_WEAPON, INVTYPE_HOLDABLE, INVTYPE_SHIELD], // offhand + 18 => [INVTYPE_RANGED, INVTYPE_THROWN, INVTYPE_RELIC], // ranged + relic + 19 => [INVTYPE_TABARD], // tabard + ); + + 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 + 2872 => 9954, 2873 => 9955, 2874 => 9956, 2884 => 9957, 2875 => 9959, 2879 => 9963, 2880 => 9964, 2882 => 9966, 2883 => 9967, 3236 => 10542, 3257 => 10561, 3256 => 10562, 3258 => 10563, 2881 => 10566, 2885 => 10581, // Ulduar 25 + 1098 => 3271, // Onyxia's Lair 10 + 1756 => 13345, // Onyxia's Lair 25 + 4031 => 12230, 4034 => 12234, 4038 => 12238, 4042 => 12242, 4046 => 12246, // Trial of the Crusader 25 nh + 4029 => 12231, 4035 => 12235, 4039 => 12239, 4043 => 12243, 4047 => 12247, // Trial of the Crusader 25 hc + 4030 => 12229, 4033 => 12233, 4037 => 12237, 4041 => 12241, 4045 => 12245, // Trial of the Crusader 10 hc + 4028 => 12228, 4032 => 12232, 4036 => 12236, 4040 => 12240, 4044 => 12244, // Trial of the Crusader 10 nh + 4642 => 13091, 4656 => 13106, 4661 => 13111, 4664 => 13114, 4667 => 13117, 4670 => 13120, 4673 => 13123, 4676 => 13126, 4679 => 13129, 4682 => 13132, 4685 => 13135, 4688 => 13138, // Icecrown Citadel 25 hc + 4641 => 13092, 4655 => 13105, 4660 => 13109, 4663 => 13112, 4666 => 13115, 4669 => 13118, 4672 => 13121, 4675 => 13124, 4678 => 13127, 4681 => 13130, 4683 => 13133, 4687 => 13136, // Icecrown Citadel 25 nh + 4640 => 13090, 4654 => 13104, 4659 => 13110, 4662 => 13113, 4665 => 13116, 4668 => 13119, 4671 => 13122, 4674 => 13125, 4677 => 13128, 4680 => 13131, 4684 => 13134, 4686 => 13137, // Icecrown Citadel 10 hc + 4639 => 13089, 4643 => 13093, 4644 => 13094, 4645 => 13095, 4646 => 13096, 4647 => 13097, 4648 => 13098, 4649 => 13099, 4650 => 13100, 4651 => 13101, 4652 => 13102, 4653 => 13103, // Icecrown Citadel 10 nh + 4823 => 13467, // Ruby Sanctum 25 hc + 4820 => 13465, // Ruby Sanctum 25 nh + 4822 => 13468, // Ruby Sanctum 10 hc + 4821 => 13466, // Ruby Sanctum 10 nh + ); + + 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 = %i', $itemId); + return 0; + } + + public static function queueStart(?string &$msg = '') : bool + { + $queuePID = self::queueStatus(); + + if ($queuePID) + { + $msg = 'queue already running'; + return true; + } + + if (OS_WIN) // here be gremlins! .. suggested was "start /B php prQueue" as background process. but that closes itself + pclose(popen('start php prQueue --log=cache/profiling.log', 'r')); + else + exec('php prQueue --log=cache/profiling.log > /dev/null 2>/dev/null &'); + + usleep(500000); + if (self::queueStatus()) + return true; + else + { + $msg = 'failed to start queue'; + return false; + } + } + + public static function queueStatus() : int + { + if (!file_exists(self::PID_FILE)) + return 0; + + $pid = file_get_contents(self::PID_FILE); + $cmd = OS_WIN ? 'tasklist /NH /FO CSV /FI "PID eq %d"' : 'ps --no-headers p %d'; + + exec(sprintf($cmd, $pid), $out); + if ($out && stripos($out[0], $pid) !== false) + return $pid; + + // have pidFile but no process with this pid + self::queueFree(); + return 0; + } + + public static function queueLock(int $pid) : bool + { + $queuePID = self::queueStatus(); + if ($queuePID && $queuePID != $pid) + { + trigger_error('pSync - another queue with PID #'.$queuePID.' is already running', E_USER_ERROR); + return false; + } + + // no queue running; create or overwrite pidFile + $ok = false; + if ($fh = fopen(self::PID_FILE, 'w')) + { + if (fwrite($fh, $pid)) + $ok = true; + + fclose($fh); + } + + return $ok; + } + + public static function queueFree() : void + { + unlink(self::PID_FILE); + } + + public static function urlize(string $str, bool $allowLocales = false, bool $profile = false) : string + { + $search = ['<', '>', ' / ', "'"]; + $replace = ['<', '>', '-', '' ]; + $str = str_replace($search, $replace, $str); + + if ($profile) + { + $str = str_replace(['(', ')'], ['', ''], $str); + $accents = array( + "ß" => "ss", + "á" => "a", "ä" => "a", "à" => "a", "â" => "a", + "è" => "e", "ê" => "e", "é" => "e", "ë" => "e", + "í" => "i", "î" => "i", "ì" => "i", "ï" => "i", + "ñ" => "n", + "ò" => "o", "ó" => "o", "ö" => "o", "ô" => "o", + "ú" => "u", "ü" => "u", "û" => "u", "ù" => "u", + "œ" => "oe", + "Á" => "A", "Ä" => "A", "À" => "A", "Â" => "A", + "È" => "E", "Ê" => "E", "É" => "E", "Ë" => "E", + "Í" => "I", "Î" => "I", "Ì" => "I", "Ï" => "I", + "Ñ" => "N", + "Ò" => "O", "Ó" => "O", "Ö" => "O", "Ô" => "O", + "Ú" => "U", "Ü" => "U", "Û" => "U", "Ù" => "U", + "Œ" => "Oe" + ); + $str = strtr($str, $accents); + } + + $str = trim($str); + + if ($allowLocales) + $str = str_replace(' ', '-', $str); + else + $str = preg_replace('/[^a-z0-9]/i', '-', $str); + + $str = str_replace('--', '-', $str); + $str = str_replace('--', '-', $str); + + $str = rtrim($str, '-'); + $str = strtolower($str); + + return $str; + } + + public static function getRealms() : array + { + if (!DB::isConnectable(DB_AUTH) || self::$realms) + return self::$realms; + + $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 + WHEN `timezone` BETWEEN 6 AND 7 THEN "kr" # KR, KR-Tournament + WHEN `timezone` BETWEEN 8 AND 13 THEN "eu" # GB, DE, FR, ES, RU, EU-Tournament + WHEN `timezone` BETWEEN 14 AND 15 THEN "tw" # TW, TW-Tournament + WHEN `timezone` BETWEEN 16 AND 25 THEN "cn" # CN, CN1-8, CN-Tournament + 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` = %i', + WOW_BUILD + ); + + if (!$realms) + return []; + + foreach ($realms as $rId => $rData) + { + // realm in db but no connection info set + if (!DB::isConnectable(DB_CHARACTERS . $rId)) + continue; + + // filter by access level + if ($rData['access'] == SEC_ADMINISTRATOR && (CLI || User::isInGroup(U_GROUP_DEV | U_GROUP_ADMIN))) + $rData['access'] = U_GROUP_DEV | U_GROUP_ADMIN; + else if ($rData['access'] == SEC_GAMEMASTER && (CLI || User::isInGroup(U_GROUP_DEV | U_GROUP_ADMIN | U_GROUP_MOD))) + $rData['access'] = U_GROUP_DEV | U_GROUP_ADMIN | U_GROUP_MOD; + 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) + continue; + + // filter dev realms + if ($rData['region'] === 'dev') + { + if (CLI || User::isInGroup(U_GROUP_DEV | U_GROUP_ADMIN)) + $rData['access'] = U_GROUP_DEV | U_GROUP_ADMIN; + else + continue; + } + + self::$realms[$rId] = $rData; + } + + return self::$realms; + } + + public static function getRegions() : array + { + self::getRealms(); + + // sort depends on encountered order in `auth`.`realmlist`. Is that a problem? + return array_unique(array_column(self::$realms, 'region')); + } + + 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` = %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()->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()->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(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` = %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` = %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` = %i AND `realmGUID` = %i', $realmId, $guid)) + self::queueInsert($realmId, $guid, Type::ARENA_TEAM, $newId); + + break; + default: + trigger_error('scheduling resync for unknown type #'.$type.' omiting..', E_USER_WARNING); + return 0; + } + + if (!$newId) + trigger_error('Profiler::scheduleResync() - tried to resync type #'.$type.' guid #'.$guid.' from realm #'.$realmId.' without preloaded data', E_USER_ERROR); + else if (!self::queueStart($msg)) + trigger_error('Profiler::scheduleResync() - '.$msg, E_USER_ERROR); + + return $newId; + } + + /* return + [ + nQueueProcesses, + [statusCode, timeToRefresh, curQueuePos, errorCode, nResyncs], + [] + ... + ] + + statusCode: + 0: end the request + 1: waiting + 2: working... + 3: ready; click to view + 4: error / retry + timeToRefresh: + msec till the client may ask for another update + curQueuePos: + position in the queue + errorCode: + 0: unk error + 1: char does not exist + 2: armory gone + nResyncs: + ??? .. if !nResyncs && !timeToRefresh prints "Adding to queue..." but will not ping the server for updates...? + */ + public static function resyncStatus(int $type, array $subjectGUIDs) : string + { + $response = [Cfg::get('PROFILER_ENABLE') ? 2 : 0]; // in theory you could have multiple queues; used as divisor in wait time estimation: (15 / x) + 2 + if (!$subjectGUIDs) + $response[] = [PR_QUEUE_STATUS_ENDED, 0, 0, PR_QUEUE_ERROR_CHAR]; + else + { + // error out all profiles with status WORKING, that are older than 60sec + 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()->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'], + $subjectStatus[$guid]['status'] != PR_QUEUE_STATUS_READY ? Cfg::get('PROFILER_RESYNC_PING') : 0, + array_search($type.':'.$guid, $queue) + 1, + 0, + 1 // nResyncs - unsure about this one + ); + } + } + + return Util::toJSON($response); + } + + public static function getCharFromRealm(int $realmId, int $charGuid) : int + { + $char = DB::Characters($realmId)->selectRow('SELECT c.* FROM characters c WHERE c.`guid` = %i', $charGuid); + if (!$char) + return self::FETCH_RESULT_ERR_NOT_FOUND; + + if (!$char['name']) + 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` = %i AND `realmGUID` = %i', $realmId, $char['guid']); + if (!$profile) + return self::FETCH_RESULT_ERR_INTERNAL; // well ... it failed + + $profileId = $profile['id']; + + CLI::write('fetching char '.$char['name'].' (#'.$charGuid.') from realm #'.$realmId); + + if (!$char['online'] && $char['logout_time'] <= $profile['lastupdated']) + { + DB::Aowow()->qry('UPDATE ::profiler_profiles SET `lastupdated` = %i WHERE `id` = %i', time(), $profileId); + return self::FETCH_RESULT_OK_UNCHANGED; + } + + CLI::write('writing...'); + + $ra = ChrRace::from($char['race']); + $cl = ChrClass::from($char['class']); + + + /*************/ + /* equipment */ + /*************/ + + /* enchantment-Indizes + * 0: permEnchant + * 3: tempEnchant + * 6: gem1 + * 9: gem2 + * 12: gem3 + * 15: socketBonus [not used] + * 18: extraSocket [only check existance] + * 21 - 30: randomProp enchantments + */ + + + 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 = []; + $mhItem = 0; + $ohItem = 0; + + foreach ($items as $slot => $item) + { + $ench = explode(' ', $item['enchantments']); + $gEnch = []; + foreach ([6, 9, 12] as $idx) + if ($ench[$idx]) + $gEnch[$idx] = $ench[$idx]; + + if ($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])) + $gemItems[$eId][1]++; + else + $gemItems[$eId] = [$gi[$eId], 1]; + } + } + + if ($slot + 1 == 16) + $mhItem = $item['itemEntry']; + if ($slot + 1 == 17) + $ohItem = $item['itemEntry']; + + if ($ench[0]) + $permEnch[$slot] = $ench[0]; + + $data = array( + 'id' => $profileId, + 'slot' => $slot + 1, + 'item' => $item['itemEntry'], + 'subItem' => $item['randomPropertyId'], + 'permEnchant' => $ench[0], + 'tempEnchant' => $ench[3], + 'extraSocket' => (int)!!$ench[18], + 'gem1' => isset($gemItems[$ench[6]]) ? $gemItems[$ench[6]][0] : 0, + 'gem2' => isset($gemItems[$ench[9]]) ? $gemItems[$ench[9]][0] : 0, + 'gem3' => isset($gemItems[$ench[12]]) ? $gemItems[$ench[12]][0] : 0, + 'gem4' => 0 // serverside items cant have more than 3 sockets. (custom profile thing) + ); + + DB::Aowow()->qry('INSERT INTO ::profiler_items %v', $data); + } + + CLI::write(' ..inventory'); + + + /**************/ + /* basic info */ + /**************/ + + $data = array( + 'realm' => $realmId, + 'realmGUID' => $charGuid, + 'name' => $char['name'], + 'renameItr' => 0, + 'race' => $char['race'], + 'class' => $char['class'], + 'level' => $char['level'], + 'gender' => $char['gender'], + 'skincolor' => $char['skin'], + 'facetype' => $char['face'], // maybe features + 'hairstyle' => $char['hairStyle'], + 'haircolor' => $char['hairColor'], + 'features' => $char['facialStyle'], // maybe facetype + '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, + 'talenttree2' => 0, + 'talenttree3' => 0, + 'talentbuild1' => '', + 'talentbuild2' => '', + 'glyphs1' => '', + 'glyphs2' => '', + 'activespec' => $char['activeTalentGroup'], + 'guild' => null, + 'guildRank' => null, + 'gearscore' => 0, + 'achievementpoints' => 0 + ); + + // char is flagged for rename + if ($char['at_login'] & 0x1) + { + 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; + } + + + /********************/ + /* talents + glyphs */ + /********************/ + + $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 %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($_); + } + + // glyphs + if (isset($g[$i])) + { + $gProps = []; + for ($j = 1; $j <= 6; $j++) + if ($g[$i]['g'.$j]) + $gProps[$j] = $g[$i]['g'.$j]; + + if ($gProps) + { + $gItems = DB::Aowow()->selectCol( + 'SELECT i.`id` + 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 + ); + + if ($gItems) + $data['glyphs'.($i + 1)] = implode(':', $gItems); + } + } + } + + $t = array( + 'spent' => [$data['talenttree1'], $data['talenttree2'], $data['talenttree3']], + 'spec' => 0 + ); + if ($t['spent'][0] > $t['spent'][1] && $t['spent'][0] > $t['spent'][2]) + $t['spec'] = 1; + else if ($t['spent'][1] > $t['spent'][0] && $t['spent'][1] > $t['spent'][2]) + $t['spec'] = 2; + else if ($t['spent'][2] > $t['spent'][1] && $t['spent'][2] > $t['spent'][0]) + $t['spec'] = 3; + + // calc gearscore + if ($items) + $data['gearscore'] += (new ItemList(array(['id', array_column($items, 'itemEntry')])))->getScoreTotal($data['class'], $t, $mhItem, $ohItem); + + if ($gemItems) + { + $gemScores = new ItemList(array(['id', array_column($gemItems, 0)])); + foreach ($gemItems as [$itemId, $mult]) + if (isset($gemScores->json[$itemId]['gearscore'])) + $data['gearscore'] += $gemScores->json[$itemId]['gearscore'] * $mult; + } + + if ($permEnch) // fuck this shit .. we are guestimating this! + { + // enchantId => multiple spells => multiple items with varying itemlevels, quality, whatevs + // cant reasonably get to the castItem from enchantId and slot + + $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])) + { + trigger_error('char #'.$charGuid.' on realm #'.$realmId.' has item in slot #'.$slot.' with invalid perm enchantment #'.CLI::bold($eId), E_USER_WARNING); + continue; + } + + if ($x = Util::getEnchantmentScore(0, 0, !!$profSpec[$eId][1], $eId)) + $data['gearscore'] += $x; + else if ($profSpec[$eId][0] != 776) // not runeforging + $data['gearscore'] += 17; // assume high quality enchantment for unknown cases + } + } + + $data['lastupdated'] = time(); + + CLI::write(' ..basic info'); + + + /***************/ + /* hunter pets */ + /***************/ + + if ($cl == ChrClass::HUNTER) + { + 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` = %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` = %i', + $petData['entry'] + ); + + 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, + 'name' => $petData['name'], + 'family' => $morePet['family'], + 'npc' => $morePet['entry'], + 'displayId' => $petData['modelId'], + 'talents' => implode('', $_) + ); + + DB::Aowow()->qry('INSERT INTO ::profiler_pets %v', $pet); + } + + CLI::write(' ..hunter pets'); + } + + + /*******************/ + /* completion data */ + /*******************/ + + // done quests // + + DB::Aowow()->qry('DELETE FROM ::profiler_completion_quests WHERE `id` = %i', $profileId); + + 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()->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` & %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)) + { + $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); + + foreach ($skills as &$sk) // apply racial profession bonuses + { + if (!isset($racials[$sk['skill']])) + continue; + + $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()->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 ::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() + ); + + $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) + { + $insCols['id'][] = $profileId; + $insCols['factionId'][] = $set['faction']; + $insCols['standing'][] = $set['standing'] + ($baseRep[$set['faction']] ?? 0); + + unset($baseRep[$set['faction']]); + } + } + + // insert base values for not yet encountered factions + foreach ($baseRep as $id => $val) + { + $insCols['id'][] = $profileId; + $insCols['factionId'][] = $id; + $insCols['standing'][] = $val; + } + + DB::Aowow()->qry('INSERT INTO ::profiler_completion_reputation %m', $insCols); + + CLI::write(' ..reputation'); + + + // known titles // + + DB::Aowow()->qry('DELETE FROM ::profiler_completion_titles WHERE `id` = %i', $profileId); + + 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()->qry('DELETE FROM ::profiler_completion_achievements WHERE `id` = %i', $profileId); + + if ($achievements = DB::Characters($realmId)->selectAssoc('SELECT `achievement`, `date` FROM character_achievement WHERE `guid` = %i', $char['guid'])) + { + 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 %in AND (`flags` & %i) = 0', array_column($achievements, 'achievement'), ACHIEVEMENT_FLAG_COUNTER); + } + + CLI::write(' ..achievements'); + + + // raid progression // + + DB::Aowow()->qry('DELETE FROM ::profiler_completion_statistics WHERE `id` = %i', $profileId); + + 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, 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'); + + + // known spells // + + DB::Aowow()->qry('DELETE FROM ::profiler_completion_spells WHERE `id` = %i', $profileId); + + 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()->qry( + 'INSERT INTO ::profiler_completion_spells + SELECT %i, `spellId` + FROM ::skilllineability + WHERE `skillLineId` IN %in AND + `acquireMethod` = 1 AND + (`reqRaceMask` = 0 OR `reqRaceMask` & %i) AND + (`reqClassMask` = 0 OR `reqClassMask` & %i)', + $profileId, + array_column($skills, 'skillId'), + $ra->toMask(), + $cl->toMask() + ); + + CLI::write(' ..known spells (vanity pets & mounts)'); + + + /****************/ + /* related data */ + /****************/ + + // 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` = %i', $char['guid'])) + { + $guildId = 0; + 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']), + 'stub' => 1 + ); + + $guildId = DB::Aowow()->qry('INSERT IGNORE INTO ::profiler_guild %v', $gData); + } + + $data['guild'] = $guildId; + $data['guildRank'] = $guild['rank']; + } + + CLI::write(' ..basic guild data'); + + + // arena teams + $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` = %i AND `realmGUID` = %i', $realmId, $rGuid))) + { + $team = array( // only most basic data + 'realm' => $realmId, + 'realmGUID' => $rGuid, + 'name' => $t['name'], + 'nameUrl' => self::urlize($t['name']), + 'type' => $t['type'], + 'stub' => 1 + ); + + $teamId = DB::Aowow()->qry('INSERT IGNORE INTO ::profiler_arena_team %v', $team); + } + + $member = array( + 'arenaTeamId' => $teamId, + 'profileId' => $profileId, + 'captain' => $t['captain'], + 'weekGames' => $t['weekGames'], + 'weekWins' => $t['weekWins'], + 'seasonGames' => $t['seasonGames'], + 'seasonWins' => $t['seasonWins'], + 'personalRating' => $t['personalRating'] + ); + + // 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` = %i + WHERE atm.`profileId` = %i AND atm.`arenaTeamId` <> %i', + $t['type'], $profileId, $teamId + ); + + 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'); + + /*********************/ + /* mark char as done */ + /*********************/ + + 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 self::FETCH_RESULT_OK; + } + + 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` = %i', $guildGuid); + if (!$guild) + return self::FETCH_RESULT_ERR_NOT_FOUND; + + if (!$guild['name']) + 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` = %i AND `realmGUID` = %i', $realmId, $guild['guildId']); + + CLI::write('fetching guild #'.$guildGuid.' from realm #'.$realmId); + CLI::write('writing...'); + + + /**************/ + /* Guild Data */ + /**************/ + + unset($guild['guildId']); + $guild['nameUrl'] = self::urlize($guild['name']); + + DB::Aowow()->qry('UPDATE ::profiler_guild SET %a WHERE `realm` = %i AND `realmGUID` = %i', $guild, $realmId, $guildGuid); + + // 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'); + + + /***************/ + /* Member Data */ + /***************/ + + $conditions = array( + ['g.guildid', $guildGuid], + ['deleteInfos_Account', null], + ['level', MAX_LEVEL, '<='], // prevents JS errors + [['extra_flags', self::CHAR_GMFLAGS, '&'], 0] // not a staff char + ); + + // this here should all happen within ProfileList + $members = new RemoteProfileList($conditions, ['sv' => $realmId]); + if ($members->error) + return self::FETCH_RESULT_ERR_NO_MEMBERS; + + $members->initializeLocalEntries(); + + CLI::write(' ..guild members'); + + + /*********************/ + /* mark guild as done */ + /*********************/ + + DB::Aowow()->qry('UPDATE ::profiler_guild SET `stub` = 0 WHERE `id` = %i', $guildId); + + return self::FETCH_RESULT_OK; + } + + 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` = %i', $teamGuid); + if (!$team) + return self::FETCH_RESULT_ERR_NOT_FOUND; + + if (!$team['name']) + 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` = %i AND `realmGUID` = %i', $realmId, $team['arenaTeamId']); + + CLI::write('fetching arena team #'.$teamGuid.' from realm #'.$realmId); + CLI::write('writing...'); + + + /*************/ + /* Team Data */ + /*************/ + + $captain = $team['captainGuid']; + unset($team['captainGuid'], $team['arenaTeamId']); + $team['nameUrl'] = self::urlize($team['name']); + + DB::Aowow()->qry('UPDATE ::profiler_arena_team SET %a WHERE `realm` = %i AND `realmGUID` = %i', $team, $realmId, $teamGuid); + + CLI::write(' ..team data'); + + + /***************/ + /* Member Data */ + /***************/ + + $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` <= %i AND + (c.`extra_flags` & %i) = 0 + WHERE `arenaTeamId` = %i', + MAX_LEVEL, + self::CHAR_GMFLAGS, + $teamGuid + ); + + $conditions = array( + ['c.guid', array_keys($members)], + ['deleteInfos_Account', null], + ['level', MAX_LEVEL, '<='], // prevents JS errors + [['extra_flags', self::CHAR_GMFLAGS, '&'], 0] // not a staff char + ); + + $mProfiles = new RemoteProfileList($conditions, ['sv' => $realmId]); + if ($mProfiles->error) + return self::FETCH_RESULT_ERR_NO_MEMBERS; + + $mProfiles->initializeLocalEntries(); + foreach ($mProfiles->iterate() as $__) + { + + $mGuid = $mProfiles->getField('guid'); + + $members[$mGuid]['arenaTeamId'] = $teamId; + $members[$mGuid]['captain'] = (int)($mGuid == $captain); + $members[$mGuid]['profileId'] = $mProfiles->getField('id'); + } + + // 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` = %i + WHERE atm.`profileId` IN %in', + $team['type'], + array_column($members, 'profileId') + ); + + // ...and purge this teams member + DB::Aowow()->qry('DELETE FROM ::profiler_arena_team_member WHERE `arenaTeamId` = %i', $teamId); + + 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'); + + + /*********************/ + /* mark team as done */ + /*********************/ + + DB::Aowow()->qry('UPDATE ::profiler_arena_team SET `stub` = 0 WHERE `id` = %i', $teamId); + + return self::FETCH_RESULT_OK; + } +} + +?> diff --git a/includes/components/report.class.php b/includes/components/report.class.php new file mode 100644 index 00000000..26b4ca63 --- /dev/null +++ b/includes/components/report.class.php @@ -0,0 +1,305 @@ + array( + self::GEN_FEEDBACK => true, + self::GEN_BUG_REPORT => true, + self::GEN_TYPO_TRANSLATION => true, + self::GEN_OP_ADVERTISING => true, + self::GEN_OP_PARTNERSHIP => true, + self::GEN_PRESS_INQUIRY => true, + self::GEN_MISCELLANEOUS => true, + self::GEN_MISINFORMATION => true + ), + self::MODE_COMMENT => array( + self::CO_ADVERTISING => U_GROUP_MODERATOR, + self::CO_INACCURATE => true, + self::CO_OUT_OF_DATE => true, + self::CO_SPAM => U_GROUP_MODERATOR, + self::CO_INAPPROPRIATE => U_GROUP_MODERATOR, + self::CO_MISCELLANEOUS => U_GROUP_MODERATOR + ), + self::MODE_FORUM_POST => array( + self::FO_ADVERTISING => U_GROUP_MODERATOR, + self::FO_AVATAR => true, + self::FO_INACCURATE => true, + self::FO_OUT_OF_DATE => U_GROUP_MODERATOR, + self::FO_SPAM => U_GROUP_MODERATOR, + self::FO_STICKY_REQUEST => U_GROUP_MODERATOR, + self::FO_INAPPROPRIATE => U_GROUP_MODERATOR + ), + self::MODE_SCREENSHOT => array( + self::SS_INACCURATE => true, + self::SS_OUT_OF_DATE => true, + self::SS_INAPPROPRIATE => U_GROUP_MODERATOR, + self::SS_MISCELLANEOUS => U_GROUP_MODERATOR + ), + self::MODE_CHARACTER => array( + self::PR_INACCURATE_DATA => true, + self::PR_MISCELLANEOUS => true + ), + self::MODE_VIDEO => array( + self::VI_INACCURATE => true, + self::VI_OUT_OF_DATE => true, + self::VI_INAPPROPRIATE => U_GROUP_MODERATOR, + self::VI_MISCELLANEOUS => U_GROUP_MODERATOR + ), + self::MODE_GUIDE => array( + self::AR_INACCURATE => true, + self::AR_OUT_OF_DATE => true, + self::AR_MISCELLANEOUS => true + ) + ); + + private const ERR_NONE = 0; // aka: success + private const ERR_INVALID_CAPTCHA = 1; // captcha not in use + private const ERR_DESC_TOO_LONG = 2; + private const ERR_NO_DESC = 3; + private const ERR_ALREADY_REPORTED = 7; + private const ERR_MISCELLANEOUS = -1; + + public const STATUS_OPEN = 0; + public const STATUS_ASSIGNED = 1; + public const STATUS_CLOSED_WONTFIX = 2; + public const STATUS_CLOSED_SOLVED = 3; + + private int $errorCode = self::ERR_NONE; + + + public function __construct(private int $mode, private int $reason, private ?int $subject = 0) + { + if ($mode < 0 || $reason <= 0) + { + trigger_error('Report - malformed contact request received', E_USER_ERROR); + $this->errorCode = self::ERR_MISCELLANEOUS; + return; + } + + if (!isset($this->context[$mode][$reason])) + { + trigger_error('Report - report has invalid context (mode:'.$mode.' / reason:'.$reason.')', E_USER_ERROR); + $this->errorCode = self::ERR_MISCELLANEOUS; + return; + } + + if (!User::isLoggedIn() && !User::$ip) + { + trigger_error('Report - could not determine IP for anonymous user', E_USER_ERROR); + $this->errorCode = self::ERR_MISCELLANEOUS; + return; + } + + $this->subject ??= 0; // 0 for utility, tools and misc pages? + } + + private function checkTargetContext(?string $url) : int + { + $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 + $ctxCheck = $this->context[$this->mode][$this->reason]; + if (is_int($ctxCheck)) + { + $roles = User::$groups; + if ($this->mode == self::MODE_COMMENT) + $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` = %i', $this->subject); + + return $roles & $ctxCheck ? self::ERR_NONE : self::ERR_MISCELLANEOUS; + } + else + return $ctxCheck ? self::ERR_NONE : self::ERR_MISCELLANEOUS; + + // Forum not in use, else: + // check post owner + // User::$id == post.op && !post.sticky; + // check user custom avatar + // g_users[post.user].avatar == 2 && (post.roles & U_GROUP_MODERATOR) == 0 + } + + public function create(string $desc, ?string $userAgent = null, ?string $appName = null, ?string $pageUrl = null, ?string $relUrl = null, ?string $email = null) : bool + { + if ($this->errorCode) + return false; + + if (!$desc) + { + $this->errorCode = self::ERR_NO_DESC; + return false; + } + + if (mb_strlen($desc) > 500) + { + $this->errorCode = self::ERR_DESC_TOO_LONG; + return false; + } + + // 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; + } + + $update = array( + 'userId' => User::$id, + 'createDate' => time(), + 'mode' => $this->mode, + 'reason' => $this->reason, + 'subject' => $this->subject, + 'ip' => User::$ip, + 'description' => $desc, + 'userAgent' => $userAgent ?: User::$agent, + 'appName' => $appName ?: (get_browser(null, true)['browser'] ?: '') + ); + + if ($pageUrl) + $update['url'] = $pageUrl; + + if ($relUrl) + $update['relatedurl'] = $relUrl; + + if ($email) + $update['email'] = $email; + + return DB::Aowow()->qry('INSERT INTO ::reports %v', $update); + } + + public function getSimilar(int ...$status) : array + { + if ($this->errorCode) + return []; + + foreach ($status as &$s) + if ($s < self::STATUS_OPEN || $s > self::STATUS_CLOSED_SOLVED) + unset($s); + + 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 + { + if ($closeStatus != self::STATUS_CLOSED_SOLVED && $closeStatus != self::STATUS_CLOSED_WONTFIX) + return false; + + if (!User::isInGroup(U_GROUP_ADMIN | U_GROUP_BUREAU | U_GROUP_MOD)) + return false; + + $fromStatus = [self::STATUS_OPEN]; + if ($inclAssigned) + $fromStatus[] = self::STATUS_ASSIGNED; + + 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()->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]); + + return true; + } + + return false; + } + + public function reopen(int $assignedTo = 0) : bool + { + // assignedTo = 0 ? status = STATUS_OPEN : status = STATUS_ASSIGNED, userId = assignedTo + return false; + } + + public function getError() : int + { + return $this->errorCode; + } +} + +?> diff --git a/includes/components/response/baseresponse.class.php b/includes/components/response/baseresponse.class.php new file mode 100644 index 00000000..29c0bbc2 --- /dev/null +++ b/includes/components/response/baseresponse.class.php @@ -0,0 +1,677 @@ + ACC_STATUS_CHANGE_PASS) + return Lang::main('intError'); + + // check if already processing + 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()->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 + if (!Util::sendMail($email, $mailTemplate, [$token], Cfg::get('ACC_RECOVERY_DECAY'))) + return Lang::main('intError2', ['send mail']); + + return ''; + } +} + +trait TrGetNext +{ + private function getNext(bool $forHeader = false) : string + { + $next = ''; + if (!empty($this->_get['next'])) + $next = $this->_get['next']; + else if (isset($_SERVER['HTTP_REFERER']) && strstr($_SERVER['HTTP_REFERER'], '?')) + $next = explode('?', $_SERVER['HTTP_REFERER'])[1]; + else if ($forHeader) + return '.'; + + return ($forHeader ? '?' : '').$next; + } +} + + +Interface ICache +{ + public function saveCache(string|Template\PageTemplate $toCache) : void; + public function loadCache(bool|string|Template\PageTemplate &$fromCache) : bool; + public function setOnCacheLoaded(callable $callback, mixed $params = null) : void; + public function getCacheKeyComponents() : array; + public function applyOnCacheLoaded(mixed &$data) : mixed; +} + +trait TrCache +{ + private const STORE_METHOD_OBJECT = 0; + private const STORE_METHOD_STRING = 1; + + private int $_cacheType = CACHE_TYPE_NONE; + private int $skipCache = 0x0; + private ?int $decay = null; + private string $cacheDir = 'cache/template/'; + private bool $cacheInited = false; + private ?\Memcached $memcached = null; + private array $onCacheLoaded = [null, null]; // post-load updater + + public static array $cacheStats = []; // load info for page footer + + // visible properties or given strings are cached + public function saveCache(string|object $toCache) : void + { + $this->initCache(); + + if ($this->_cacheType == CACHE_TYPE_NONE) + return; + + if (!Cfg::get('CACHE_MODE') /* || Cfg::get('DEBUG') */) + return; + + if (!$this->decay) + return; + + $cKey = $this->formatCacheKey(); + $method = is_object($toCache) ? self::STORE_METHOD_OBJECT : self::STORE_METHOD_STRING; + + if ($method == self::STORE_METHOD_OBJECT) + $toCache = serialize($toCache); + else + $toCache = (string)$toCache; + + if (is_callable($this->onCacheLoaded[0])) + $postCache = serialize($this->onCacheLoaded); + + if (Cfg::get('CACHE_MODE') & CACHE_MODE_MEMCACHED) + { + // on &refresh also clear related + if ($this->skipCache & CACHE_MODE_MEMCACHED) + $this->deleteCache(CACHE_MODE_MEMCACHED); + + $data = array( + 'timestamp' => time(), + 'lifetime' => $this->decay, + 'revision' => AOWOW_REVISION, + 'method' => $method, + 'postCache' => $postCache ?? null, + 'data' => $toCache + ); + + $this->memcached()?->set($cKey[2], $data); + } + + if (Cfg::get('CACHE_MODE') & CACHE_MODE_FILECACHE) + { + $data = time()." ".$this->decay." ".AOWOW_REVISION." ".$method."\n"; + $data .= ($postCache ?? '')."\n"; + $data .= gzcompress($toCache, 9); + + // on &refresh also clear related + if ($this->skipCache & CACHE_MODE_FILECACHE) + $this->deleteCache(CACHE_MODE_FILECACHE); + + if (Util::writeDir($this->cacheDir . implode(DIRECTORY_SEPARATOR, array_slice($cKey, 0, 2)))) + file_put_contents($this->cacheDir . implode(DIRECTORY_SEPARATOR, $cKey), $data); + } + } + + public function loadCache(mixed &$fromCache) : bool + { + $this->initCache(); + + if ($this->_cacheType == CACHE_TYPE_NONE) + return false; + + if (!Cfg::get('CACHE_MODE') /* || Cfg::get('DEBUG') */) + return false; + + $cKey = $this->formatCacheKey(); + $rev = $method = $data = $postCache = null; + + if ((Cfg::get('CACHE_MODE') & CACHE_MODE_MEMCACHED) && !($this->skipCache & CACHE_MODE_MEMCACHED)) + { + if ($cache = $this->memcached()?->get($cKey[2])) + { + $method = $cache['method']; + $data = $cache['data']; + $postCache = $cache['postCache']; + + if (($cache['timestamp'] + $cache['lifetime']) > time() && $cache['revision'] == AOWOW_REVISION) + self::$cacheStats = [CACHE_MODE_MEMCACHED, $cache['timestamp'], $cache['lifetime']]; + } + } + + if (!$data && (Cfg::get('CACHE_MODE') & CACHE_MODE_FILECACHE) && !($this->skipCache & CACHE_MODE_FILECACHE)) + { + $file = $this->cacheDir . implode(DIRECTORY_SEPARATOR, $cKey); + if (!file_exists($file)) + return false; + + $content = file_get_contents($file); + if (!$content) + return false; + + [$head, $postCache, $data] = explode("\n", $content, 3); + if (substr_count($head, ' ') != 3) + return false; + + [$time, $lifetime, $rev, $method] = explode(' ', $head); + + if (($time + $lifetime) < time() || $rev != AOWOW_REVISION) + return false; + + self::$cacheStats = [CACHE_MODE_FILECACHE, $time, $lifetime]; + $data = gzuncompress($data); + } + + if (!$data) + return false; + + if ($postCache) + $this->onCacheLoaded = unserialize($postCache); + + $fromCache = false; + if ($method == self::STORE_METHOD_OBJECT) + $fromCache = unserialize($data); + else if ($method == self::STORE_METHOD_STRING) + $fromCache = $data; + + return $fromCache !== false; + } + + public function deleteCache(int $modeMask = 0x3) : void + { + $this->initCache(); + + // type+typeId/catg+mode; 3+10+1 + $cKey = $this->formatCacheKey(); + $cKey[2] = substr($cKey[2], 0, 13); + + if ($modeMask & CACHE_MODE_MEMCACHED) + foreach ($this->memcached()?->getAllKeys() ?? [] as $k) + if (strpos($k, $cKey[2]) === 0) + $this->memcached()?->delete($k); + + if ($modeMask & CACHE_MODE_FILECACHE) + foreach (glob(implode(DIRECTORY_SEPARATOR, $cKey).'*') as $file) + unlink($file); + } + + private function memcached() : ?\Memcached + { + if (!class_exists('\Memcached')) + { + trigger_error('Memcached is enabled by us but not in php!', E_USER_ERROR); + return null; + } + + if (!$this->memcached && (Cfg::get('CACHE_MODE') & CACHE_MODE_MEMCACHED)) + { + $this->memcached = new \Memcached(); + $this->memcached->addServer('localhost', 11211); + } + + return $this->memcached; + } + + private function initCache() : void + { + // php's missing trait property conflict resolution is going to be the end of me + // also allow reevaluation even if inited. It may have changed in generate(), because of an error. + if (isset($this->cacheType)) + $this->_cacheType = $this->cacheType; + + if ($this->cacheInited) + return; + + // force refresh + if (isset($_GET['refresh']) && User::isInGroup(U_GROUP_ADMIN | U_GROUP_BUREAU | U_GROUP_DEV)) + { + if ($_GET['refresh'] == 'filecache') + $this->skipCache = CACHE_MODE_FILECACHE; + else if ($_GET['refresh'] == 'memcached') + $this->skipCache = CACHE_MODE_MEMCACHED; + else if ($_GET['refresh'] == '') + $this->skipCache = CACHE_MODE_FILECACHE | CACHE_MODE_MEMCACHED; + } + + $this->decay ??= Cfg::get('CACHE_DECAY'); + + $cacheDir = Cfg::get('CACHE_DIR'); + if ($cacheDir && Util::writeDir($cacheDir)) + $this->cacheDir = mb_substr($cacheDir, -1) != '/' ? $cacheDir.'/' : $cacheDir; + + $this->cacheInited = true; + } + + // https://stackoverflow.com/questions/466521 + private function formatCacheKey() : array + { + [$dbType, $dbTypeIdOrCat, $staffMask, $miscInfo] = $this->getCacheKeyComponents(); + + $fileKey = ''; + // DBType: 3 + $fileKey .= str_pad(dechex($dbType & 0xFFF), 3, 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); + // optional: miscInfo + if ($miscInfo) + $fileKey .= '-'.$miscInfo; + + // topDir, 2ndDir, file + return array( + str_pad(dechex($dbType & 0xFF), 2, 0, STR_PAD_LEFT), + str_pad(dechex(($dbTypeIdOrCat) & 0xFF), 2, 0, STR_PAD_LEFT), + $fileKey + ); + } + + public function setOnCacheLoaded(callable $callback, mixed $params = null) : void + { + $this->onCacheLoaded = [$callback, $params]; + } + + public function applyOnCacheLoaded(mixed &$data) : mixed + { + if (is_callable($this->onCacheLoaded[0])) + return $this->onCacheLoaded[0]($data, $this->onCacheLoaded[1]); + + return $data; + } + + public function setCacheDecay(int $seconds) : void + { + if ($seconds < 0) + return; + + $this->decay = $seconds; + } + + abstract public function getCacheKeyComponents() : array; +} + +trait TrSearch +{ + 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 + $this->searchMask, // DBTypeId/category + User::$groups, // staff mask + md5($misc) // misc + ); + } +} + +Interface IProfilerList +{ + public function getRegions() : void; +} + +trait TrProfiler +{ + protected int $realmId = 0; + protected string $battlegroup = ''; // not implemented, since no pserver supports it + + public string $region = ''; + public string $realm = ''; + + private function getSubjectFromUrl(string $pageParam) : void + { + if (!$pageParam) + return; + + // cat[0] is always region + // cat[1] is realm or bGroup (must be realm if cat[2] is set) + // cat[2] is arena-team, guild or character + $cat = explode('.', mb_strtolower($pageParam), 3); + + $cat = array_map('urldecode', $cat); + + if (array_search($cat[0], Util::$regions) === false) + return; + + $this->region = $cat[0]; + + // if ($cat[1] == Profiler::urlize(Cfg::get('BATTLEGROUP'))) + // $this->battlegroup = Cfg::get('BATTLEGROUP'); + if (isset($cat[1])) + { + foreach (Profiler::getRealms() as $rId => $r) + { + if (Profiler::urlize($r['name'], true) == $cat[1]) + { + $this->realm = $r['name']; + $this->realmId = $rId; + if (isset($cat[2]) && mb_strlen($cat[2]) >= 2) + $this->subjectName = mb_strtolower($cat[2]); // cannot reconstruct original name from urlized form; match against special name field + + break; + } + } + } + } + + private function followBreadcrumbPath() : void + { + if ($this->region) + { + $this->breadcrumb[] = $this->region; + + if ($this->realm) + $this->breadcrumb[] = Profiler::urlize($this->realm, true); + // else + // $this->breadcrumb[] = Profiler::urlize(Cfg::get('BATTLEGROUP')); + } + } +} + +trait TrProfilerDetail +{ + use TrProfiler { TrProfiler::getSubjectFromUrl as _getSubjectFromUrl; } + + protected string $subjectName = ''; + + public int $typeId = 0; + public ?array $doResync = null; + + private function getSubjectFromUrl(string $pageParam) : void + { + if (!$pageParam) + return; + + if (Util::checkNumeric($pageParam, NUM_CAST_INT)) + $this->typeId = $pageParam; + else + $this->_getSubjectFromUrl($pageParam); + } + + private function handleIncompleteData(int $type, int $guid) : void + { + // queue full fetch + if ($newId = Profiler::scheduleResync($type, $this->realmId, $guid)) + { + $this->template = 'text-page-generic'; + $this->doResync = [Type::getFileString($type), $newId]; + $this->inputbox = ['inputbox-status', ['head' => Lang::profiler('firstUseTitle', [Util::ucFirst($this->subjectName), $this->realm])]]; + + return; + } + + // todo: base info should have been created in __construct .. why are we here..? + $this->forward('?'.Type::getFileString($type).'s='.$this->region.'.'.Profiler::urlize($this->realm, true).'&filter=na='.Util::ucFirst($this->subjectName).';ex=on'); + } +} + +trait TrProfilerList +{ + use TrProfiler; + + public array $regions = []; + + public function getRegions() : void + { + $usedRegions = array_column(Profiler::getRealms(), 'region'); + foreach (Util::$regions as $idx => $id) + if (in_array($id, $usedRegions)) + $this->regions[$id] = Lang::profiler('regions', $id); + } +} + + +abstract class BaseResponse +{ + 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 + + protected array $expectedPOST = []; // fill with variables you that are going to be used; eg: + protected array $expectedGET = []; // 'id' => ['filter' => FILTER_CALLBACK, 'options' => 'AjaxHandler::checkIdList'] + protected array $expectedCOOKIE = []; + + protected array $_post = []; // the filtered variable result + protected array $_get = []; + protected array $_cookie = []; + + protected int $requiredUserGroup = U_GROUP_NONE; // by default accessible to everone + protected bool $requiresLogin = false; // normal users and guests are both U_GROUP_NONE, soooo..... + protected mixed $result = null; + + public function __construct() + { + $this->initRequestData(); + + if (!User::isInGroup($this->requiredUserGroup)) + $this->onUserGroupMismatch(); + + if ($this->requiresLogin && !User::isLoggedIn()) + $this->onUserGroupMismatch(); + } + + public function process() : void + { + $fromCache = false; + + if ($this instanceof ICache) + $fromCache = $this->loadCache($this->result); + + if (!$this->result) + $this->generate(); + + $this->display(); + + if ($this instanceof ICache && !$fromCache) + $this->saveCache($this->result); + } + + private function initRequestData() : void + { + // php bug? If INPUT_X is empty, filter_input_array returns null/fails + // only really relevant for INPUT_POST + // manuall set everything null in this case + + if ($this->expectedPOST) + { + if ($_POST) + $this->_post = filter_input_array(INPUT_POST, $this->expectedPOST); + else + $this->_post = array_fill_keys(array_keys($this->expectedPOST), null); + } + + if ($this->expectedGET) + { + if ($_GET) + $this->_get = filter_input_array(INPUT_GET, $this->expectedGET); + else + $this->_get = array_fill_keys(array_keys($this->expectedGET), null); + } + + if ($this->expectedCOOKIE) + { + if ($_COOKIE) + $this->_cookie = filter_input_array(INPUT_COOKIE, $this->expectedCOOKIE); + else + $this->_cookie = array_fill_keys(array_keys($this->expectedCOOKIE), null); + } + } + + protected function forward(string $url = '') : never + { + $this->sendNoCacheHeader(); + header('Location: '.($url ?: '.'), true, 302); + exit; + } + + protected function forwardToSignIn(string $next = '') : never + { + $this->forward('?account=signin'.($next ? '&next='.$next : '')); + } + + protected function sumSQLStats() : void + { + self::$sql = array( + 'count' => \dibi::$numOfQueries, + 'time' => \dibi::$totalTime, + 'elapsed' => \dibi::$elapsedTime + ); + } + + protected function sendNoCacheHeader() + { + header('Expires: Sat, 01 Jan 2000 01:00:00 GMT'); + header('Last-Modified: ' . gmdate('D, d M Y H:i:s') . ' GMT'); + header('Cache-Control: no-store, no-cache, must-revalidate'); + header('Cache-Control: post-check=0, pre-check=0', false); + header('Pragma: no-cache'); + } + + + /****************************/ + /* required Parameter tests */ + /****************************/ + + protected function assertPOST(string ...$keys) : bool + { + foreach ($keys as $k) // not sent by browser || empty text field sent || validation failed + if (!isset($this->_post[$k]) || $this->_post[$k] === null || $this->_post[$k] === '' || $this->_post[$k] === false) + return false; + + return true; + } + + protected function assertGET(string ...$keys) : bool + { + foreach ($keys as $k) + if (!isset($this->_get[$k]) || $this->_get[$k] === null || $this->_get[$k] === '' || $this->_get[$k] === false) + return false; + + return true; + } + + protected function assertCOOKIE(string ...$keys) : bool + { + foreach ($keys as $k) + if (!isset($this->_cookie[$k]) || $this->_cookie[$k] === null || $this->_cookie[$k] === '' || $this->_cookie[$k] === false) + return false; + + return true; + } + + + /*******************************/ + /* Parameter validation checks */ + /*******************************/ + + protected static function checkRememberMe(string $val) : bool + { + return $val === 'yes'; + } + + protected static function checkCheckbox(string $val) : bool + { + return $val === 'on'; + } + + protected static function checkEmptySet(string $val) : bool + { + return $val === ''; // parameter is set and expected to be empty + } + + protected static function checkIdList(string $val) : array + { + if (preg_match('/^-?\d+(,-?\d+)*$/', $val)) + return array_map('intVal', explode(',', $val)); + + return []; + } + + protected static function checkIntArray(string $val) : array + { + if (preg_match('/^-?\d+(:-?\d+)*$/', $val)) + return array_map('intVal', explode(':', $val)); + + return []; + } + + protected static function checkIdListUnsigned(string $val) : array + { + if (preg_match('/^\d+(,\d+)*$/', $val)) + return array_map('intVal', explode(',', $val)); + + return []; + } + + 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, '', $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, '', $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 */ + /********************/ + + // calc response + abstract protected function generate() : void; + + // send response + abstract protected function display() : void; + + // handling differs by medium + abstract protected function onUserGroupMismatch() : never; +} + +?> diff --git a/includes/components/response/templateresponse.class.php b/includes/components/response/templateresponse.class.php new file mode 100644 index 00000000..d2eb3c94 --- /dev/null +++ b/includes/components/response/templateresponse.class.php @@ -0,0 +1,703 @@ +type, // DBType + $this->typeId, // DBTypeId/category + User::$groups, // staff mask + '' // misc (here unused) + ); + } +} + + +trait TrListPage +{ + public string $subCat = ''; + public ?Filter $filter = null; + + public function getCacheKeyComponents() : array + { + // max. 3 catgs + // 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 * 4; + if (!isset($this->category[$i])) + continue; + + if ($this->category[$i]) + $catg |= ($this->category[$i] << 1) & 0xFFFF; + else + $catg |= 1; + } + } + + if ($get = $this->filter?->buildGETParam()) + $misc = md5($get); + + return array( + $this->type, // DBType + $catg ?? -1, // DBTypeId/category + User::$groups, // staff mask + $misc ?? '' // misc (here filter) + ); + } +} + + +trait TrGuideEditor +{ + public int $typeId = 0; + + public int $editCategory = 0; + public int $editClassId = 0; + public int $editSpecId = 0; + public int $editRev = 0; + public int $editStatus = GuideMgr::STATUS_DRAFT; + public string $editStatusColor = GuideMgr::STATUS_COLORS[GuideMgr::STATUS_DRAFT]; + public string $editTitle = ''; + public string $editName = ''; + public string $editDescription = ''; + public string $editText = ''; + public string $error = ''; + public Locale $editLocale = Locale::EN; + public bool $isDraft = false; +} + +class TemplateResponse extends BaseResponse +{ + final protected const /* int */ TAB_DATABASE = 0; + final protected const /* int */ TAB_TOOLS = 1; + final protected const /* int */ TAB_MORE = 2; + final protected const /* int */ TAB_COMMUNITY = 3; + final protected const /* int */ TAB_STAFF = 4; + final protected const /* int */ TAB_GUIDES = 6; + + private array $jsgBuffer = []; // throw any db type references in here to be processed later + private array $header = []; + private string $fullParams = ''; // effectively articleUrl + + protected string $template = ''; + protected array $breadcrumb = []; + protected ?int $activeTab = null; // [Database, Tools, More, Community, Staff, null, Guides] ?? none + protected string $pageName = ''; + protected array $category = []; + protected array $validCats = []; + protected ?string $articleUrl = null; + protected bool $filterError = false; // retroactively apply error notice to fixed filter result + + protected array $dataLoader = []; // ?data=x.y.z as javascript + protected array $scripts = array( + [SC_JS_FILE, 'js/jquery-3.7.0.min.js', SC_FLAG_NO_TIMESTAMP ], + [SC_JS_FILE, 'js/basic.js' ], + [SC_JS_FILE, 'widgets/power.js', SC_FLAG_NO_TIMESTAMP | SC_FLAG_APPEND_LOCALE], + [SC_JS_FILE, 'js/locale_%s.js', SC_FLAG_LOCALIZED ], + [SC_JS_FILE, 'js/global.js' ], + [SC_CSS_FILE, 'css/basic.css' ], + [SC_CSS_FILE, 'css/global.css' ], + [SC_CSS_FILE, 'css/aowow.css' ], + [SC_CSS_FILE, 'css/locale_%s.css', SC_FLAG_LOCALIZED ] + ); + + // debug: stats + protected static float $time = 0.0; + // protected static array $sql = []; + // protected static array $cacheStats = []; + public array $pageStats = []; // static properties carry the values, this is just for the PageTemplate to reference + + // send to template + public array $title = []; // head title components + public string $h1 = ''; // body title + public string $h1Link = ''; // + public ?string $headerLogo = null; // url to non-standard logo for events etc. + public string $search = ''; // prefilled search bar + public string $wowheadLink = 'https://wowhead.com/'; + public int $contribute = CONTRIBUTE_NONE; + public ?array $inputbox = null; + public ?string $rss = null; // link rel=alternate for rss auto-discovery + public ?string $tabsTitle = null; + public ?Markup $extraText = null; + public ?string $extraHTML = null; + public array $redButtons = []; // see template/redButtons.tpl.php + + // send to template, but it is js stuff + public array $gPageInfo = []; + public bool $gDataKey = false; // send g_DataKey to template or don't (stored in $_SESSION) + public ?Markup $article = null; + public ?Tabs $lvTabs = null; + public array $pageTemplate = []; // js PageTemplate object + public array $jsGlobals = []; // ready to be used in template + + public function __construct(string $rawParam = '') + { + $this->title[] = Cfg::get('NAME'); + self::$time = microtime(true); + + parent::__construct(); + + $this->fullParams = $this->pageName; + 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__); + if ($parentVars['scripts'] != $this->scripts) // additions set in child class + $this->scripts = array_merge($parentVars['scripts'], $this->scripts); + + if (User::isInGroup(U_GROUP_STAFF | U_GROUP_SCREENSHOT | U_GROUP_VIDEO)) + 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 %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->fullParams, ''); + $this->pageTemplate['pageName'] = $this->pageName; + } + + if (!is_null($this->activeTab)) + $this->pageTemplate['activeTab'] = $this->activeTab; + + if (!$this->isValidPage()) + $this->onInvalidCategory(); + + if (Cfg::get('MAINTENANCE') && !User::isInGroup(U_GROUP_EMPLOYEE)) + $this->generateMaintenance(); + else if (Cfg::get('MAINTENANCE') && User::isInGroup(U_GROUP_EMPLOYEE)) + Util::addNote('Maintenance mode enabled!'); + } + + // by default goto login page + protected function onUserGroupMismatch() : never + { + if (User::isLoggedIn()) + $this->generateError(); + + $this->forwardToSignIn($_SERVER['QUERY_STRING'] ?? ''); + } + + // by default show error page + protected function onInvalidCategory() : never + { + $this->generateError(); + } + + // just pass through + protected function addScript(array ...$scriptDefs) : void + { + if (!$this->result) + $this->scripts = array_merge($this->scripts, $scriptDefs); + else + foreach ($scriptDefs as $s) + $this->result->addScript(...$s); + } + + protected function addDataLoader(string ...$dataFiles) : void + { + if (!$this->result) + $this->dataLoader = array_merge($this->dataLoader, $dataFiles); + else + $this->result->addDataLoader(...$dataFiles); + } + + public static function pageStatsHook(Template\PageTemplate &$pt, array &$stats) : void + { + if (User::isInGroup(U_GROUP_EMPLOYEE)) + { + $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 = []; + } + + protected function getCategoryFromUrl(string $pageParam) : void + { + $arr = explode('.', $pageParam); + foreach ($arr as $v) + { + if (!is_numeric($v)) + break; + + $this->category[] = (int)$v; + } + } + + // functionally this should be in PageTemplate but inaccessible there + protected function fmtStaffTip(?string $text, string $tip) : string + { + if (!$text || !User::isInGroup(U_GROUP_EMPLOYEE)) + return $text ?? ''; + else + return sprintf(Util::$dfnString, $tip, $text); + } + + + /**********************/ + /* Prepare js-Globals */ + /**********************/ + + // add typeIds that should be displayed as jsGlobal on the page + public function extendGlobalIds(int $type, int ...$ids) : void + { + if (!$type || !$ids) + return; + + if (!isset($this->jsgBuffer[$type])) + $this->jsgBuffer[$type] = []; + + foreach ($ids as $id) + $this->jsgBuffer[$type][] = $id; + } + + // add jsGlobals or typeIds (can be mixed in one array: TYPE => [mixeddata]) to display on the page + public function extendGlobalData(array $data, ?array $extra = null) : void + { + foreach ($data as $type => $globals) + { + if (!is_array($globals) || !$globals) + continue; + + $this->initJSGlobal($type); + + // can be id => data + // or idx => id + // and may be mixed + foreach ($globals as $k => $v) + { + if (is_array($v)) + { + // localize name fields .. except for icons .. icons are special + if ($type != Type::ICON) + { + foreach (['name', 'namefemale'] as $n) + { + if (!isset($v[$n])) + continue; + + $v[$n . '_'.Lang::getLocale()->json()] = $v[$n]; + unset($v[$n]); + } + } + + $this->jsGlobals[$type][1][$k] = $v; + } + else if (is_numeric($v)) + $this->extendGlobalIds($type, $v); + } + } + + if ($extra) + { + $namedExtra = []; + foreach ($extra as $typeId => $data) + foreach ($data as $k => $v) + $namedExtra[$typeId][$k.'_'.Lang::getLocale()->json()] = $v; + + $this->jsGlobals[$type][2] = $namedExtra; + } + } + + // init store for type + private function initJSGlobal(int $type) : void + { + $jsg = &$this->jsGlobals; // shortcut + + if (isset($jsg[$type])) + return; + + if ($tpl = Type::getJSGlobalTemplate($type)) + $jsg[$type] = $tpl; + } + + // lookup jsGlobals from collected typeIds + private function applyGlobals() : void + { + foreach ($this->jsgBuffer as $type => $ids) + { + foreach ($ids as $k => $id) // filter already generated data, maybe we can save a lookup or two + if (isset($this->jsGlobals[$type][1][$id])) + unset($ids[$k]); + + if (!$ids) + continue; + + $this->initJSGlobal($type); + + $obj = Type::newList($type, [['id', array_unique($ids, SORT_NUMERIC)]]); + if (!$obj) + continue; + + $this->extendGlobalData($obj->getJSGlobals(GLOBALINFO_SELF)); + + // delete processed ids + $this->jsgBuffer[$type] = []; + } + } + + + /************************/ + /* Generic Page Content */ + /************************/ + + // get announcements and notes for user + private function addAnnouncements(bool $onlyGenerics = false) : void + { + $announcements = []; + + // display occured notices + $notes = $_SESSION['notes'] ?? []; + unset($_SESSION['notes']); + + $notes[] = [...Util::getNotes(), 'One or more issues occured during page generation']; + + foreach ($notes as $i => [$messages, $logLevel, $head]) + { + if (!$messages) + continue; + + array_unshift($messages, $head); + + $colors = array( // [border, text] + LOG_LEVEL_ERROR => ['C50F1F', 'E51223'], + LOG_LEVEL_WARN => ['C19C00', 'E5B700'], + LOG_LEVEL_INFO => ['3A96DD', '42ADFF'] + ); + + $text = new LocString(['name_loc' . Lang::getLocale()->value => '[span]'.implode("[br]", $messages).'[/span]'], callback: Util::defStatic(...)); + $style = 'color: #'.($colors[$logLevel][1] ?? 'fff').'; font-weight: bold; font-size: 14px; padding-left: 40px; background-image: url('.Cfg::get('STATIC_URL').'/images/announcements/warn-small.png); background-size: 15px 15px; background-position: 12px center; border: dashed 2px #'.($colors[$logLevel][0] ?? 'fff').';'; + + $announcements[] = new Announcement(-$i, 'internal error', $text, style: $style); + } + + // fetch announcements + $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 (`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 + ); + + foreach ($fromDB as $a) + if (($ann = new Announcement($a['id'], $a['name'], new LocString($a, 'text', Util::defStatic(...)), $a['mode'], $a['status'], Util::defStatic($a['style'])))->status != Announcement::STATUS_DELETED) + $announcements[] = $ann; + + $this->result->announcements = $announcements; + } + + // get article & static infobox (run before processing jsGlobals) + private function addArticle() : void + { + if ($this->article) + return; + + $article = []; + if (isset($this->guideRevision)) + $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 && !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` = %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) + return; + + $text = Util::defStatic($article['article']); + $opts = []; + + // convert U_GROUP_* to MARKUP.CLASS_* (as seen in js-object Markup) + if ($article['editAccess'] & (U_GROUP_ADMIN | U_GROUP_VIP | U_GROUP_DEV)) + $opts['allow'] = Markup::CLASS_ADMIN; + else if ($article['editAccess'] & U_GROUP_STAFF) + $opts['allow'] = Markup::CLASS_STAFF; + else if ($article['editAccess'] & U_GROUP_PREMIUM) + $opts['allow'] = Markup::CLASS_PREMIUM; + else if ($article['editAccess'] & U_GROUP_PENDING) + $opts['allow'] = Markup::CLASS_PENDING; + else + $opts['allow'] = Markup::CLASS_USER; + + if (!empty($this->type) && isset($this->typeId)) + $opts['dbpage'] = 1; + + if ($article['locale'] != Lang::getLocale()->value) + $opts['prepend'] = '
'.Lang::main('langOnly', [Lang::lang($article['locale'])]).'
'; + + $this->article = new Markup($text, $opts); + + if ($jsg = $this->article->getJsGlobals()) + $this->extendGlobalData($jsg); + + $this->gPageInfo['editAccess'] = $article['editAccess']; + + if (method_exists($this, 'postArticle')) // e.g. update variables in article + $this->postArticle($this->article['text']); + } + + private function addCommunityContent() : void + { + $community = array( + 'coError' => $_SESSION['error']['co'] ?? null, + 'ssError' => $_SESSION['error']['ss'] ?? null, + 'viError' => $_SESSION['error']['vi'] ?? null + ); + + // we cannot blanket NUMERIC_CHECK the data as usernames of deleted users are their id which does not support String.lower() + + if ($this->contribute & CONTRIBUTE_CO) + $community['co'] = Util::toJSON(CommunityContent::getComments($this->type, $this->typeId), JSON_UNESCAPED_UNICODE); + + if ($this->contribute & CONTRIBUTE_SS) + $community['ss'] = Util::toJSON(CommunityContent::getScreenshots($this->type, $this->typeId), JSON_UNESCAPED_UNICODE); + + if ($this->contribute & CONTRIBUTE_VI) + $community['vi'] = Util::toJSON(CommunityContent::getVideos($this->type, $this->typeId), JSON_UNESCAPED_UNICODE); + + unset($_SESSION['error']); + + // as comments are not cached, those globals cant be either + $this->extendGlobalData(CommunityContent::getJSGlobals()); + + $this->result->community = $community; + $this->applyGlobals(); + } + + + /**************/ + /* Generators */ + /**************/ + + protected function generate() : void + { + $this->result = new Template\PageTemplate($this->template, $this); + + foreach ($this->scripts as $s) + $this->result->addScript(...$s); + + $this->result->addDataLoader(...$this->dataLoader); + + // static::class so pageStatsHook defined here, can access cacheStats defined in the implementation + $this->result->registerDisplayHook('pageStats', [static::class, 'pageStatsHook']); + + // only adds edit links to the staff menu: precursor to guides? + if (!($this instanceof GuideBaseResponse)) + $this->gPageInfo += array( + 'articleUrl' => $this->articleUrl ?? $this->fullParams, // is actually be the url-param + 'editAccess' => (U_GROUP_ADMIN | U_GROUP_EDITOR | U_GROUP_BUREAU) + ); + + if ($this->breadcrumb) + $this->pageTemplate['breadcrumb'] = $this->breadcrumb; + + if (isset($this->filter)) + $this->pageTemplate['filter'] = $this->filter->query ? 1 : 0; + + $this->addArticle(); + + $this->applyGlobals(); + } + + // we admit this page exists and an error occured on it + public function generateError(?string $altPageName = null) : never + { + $this->result = new Template\PageTemplate('text-page-generic', $this); + + // only use own script defs + foreach (get_class_vars(self::class)['scripts'] as $s) + $this->result->addScript(...$s); + + if (User::isInGroup(U_GROUP_STAFF | U_GROUP_SCREENSHOT | U_GROUP_VIDEO)) + { + $this->result->addScript(SC_CSS_FILE, 'css/staff.css'); + $this->result->addScript(SC_JS_FILE, 'js/staff.js'); + } + + $this->result->registerDisplayHook('pageStats', [self::class, 'pageStatsHook']); + + $this->title[] = Lang::main('errPageTitle'); + $this->h1 = Lang::main('errPageTitle'); + $this->articleUrl = 'page-not-found'; + $this->gPageInfo += array( + 'articleUrl' => 'page-not-found', + 'editAccess' => (U_GROUP_ADMIN | U_GROUP_EDITOR | U_GROUP_BUREAU) + ); + + $this->pageTemplate['pageName'] ??= $altPageName ?? 'page-not-found'; + + $this->addArticle(); + + $this->sumSQLStats(); + + $this->header[] = ['HTTP/1.0 404 Not Found', true, 404]; + + $this->display(true); + exit; + } + + // we do not have this page + public function generateNotFound(string $title = '', string $msg = '') : never + { + $this->result = new Template\PageTemplate('text-page-generic', $this); + + // only use own script defs + foreach (get_class_vars(self::class)['scripts'] as $s) + $this->result->addScript(...$s); + + if (User::isInGroup(U_GROUP_STAFF | U_GROUP_SCREENSHOT | U_GROUP_VIDEO)) + { + $this->result->addScript(SC_CSS_FILE, 'css/staff.css'); + $this->result->addScript(SC_JS_FILE, 'js/staff.js'); + } + + $this->result->registerDisplayHook('pageStats', [self::class, 'pageStatsHook']); + + array_unshift($this->title, Lang::main('nfPageTitle')); + + $this->inputbox = ['inputbox-status', array( + 'head' => isset($this->typeId) ? Util::ucWords($title).' #'.$this->typeId : $title, + 'error' => !$msg && isset($this->typeId) ? Lang::main('pageNotFound', [$title]) : $msg + )]; + + $this->contribute = CONTRIBUTE_NONE; + + if (!empty($this->breadcrumb)) + $this->pageTemplate['breadcrumb'] = $this->breadcrumb; + + $this->sumSQLStats(); + + $this->header[] = ['HTTP/1.0 404 Not Found', true, 404]; + + $this->display(true); + exit; + } + + // display brb gnomes + public function generateMaintenance() : never + { + $this->result = new Template\PageTemplate('maintenance', $this); + + $this->header[] = ['HTTP/1.0 503 Service Temporarily Unavailable', true, 503]; + $this->header[] = ['Retry-After: '.(3 * HOUR)]; + + $this->display(true); + exit; + } + + protected function display(bool $withError = false) : void + { + $this->title = Util::htmlEscape($this->title); + $this->search = Util::htmlEscape($this->search); + // can't escape >h1 here, because CharTitles legitimately add HTML + + $this->addAnnouncements($withError); + if (!$withError) + $this->addCommunityContent(); + + // force jsGlobals from Announcements/CommunityContent into PageTemplate + // as this may be loaded from cache, it will be unlinked from its response + if ($ptJSG = $this->result->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) + $this->result->jsGlobals = $this->jsGlobals; + + if ($this instanceof ICache) + $this->applyOnCacheLoaded($this->result); + + if ($this->result && $this->filterError) + $this->result->setListviewError(); + + $this->sumSQLStats(); + + // Heisenbug: IE11 and FF32 will sometimes (under unknown circumstances) cache 302 redirects and stop + // re-requesting them from the server but load them from local cache, thus breaking menu features. + $this->sendNoCacheHeader(); + foreach ($this->header as $h) + header(...$h); + + $this->result?->render(); + } + + + /**********/ + /* Checks */ + /**********/ + + // has a valid combination of categories + private function isValidPage() : bool + { + if (!$this->category) + return true; + + $c = $this->category; // shorthand + + switch (count($c)) + { + 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)); + case 2: // first param has to be a key. otherwise invalid + if (!isset($this->validCats[$c[0]])) + return false; + + // check if the sub-array is n-imensional + if (is_array($this->validCats[$c[0]]) && count($this->validCats[$c[0]]) == count($this->validCats[$c[0]], COUNT_RECURSIVE)) + return in_array($c[1], $this->validCats[$c[0]]); // second param is value in second level array + else + return isset($this->validCats[$c[0]][$c[1]]); // check if params is key of another array + case 3: // 3 params MUST point to a specific value + return isset($this->validCats[$c[0]][$c[1]]) && in_array($c[2], $this->validCats[$c[0]][$c[1]]); + } + + return false; + } + +} + +?> diff --git a/includes/components/response/textresponse.class.php b/includes/components/response/textresponse.class.php new file mode 100644 index 00000000..1541d239 --- /dev/null +++ b/includes/components/response/textresponse.class.php @@ -0,0 +1,170 @@ +type, // DBType + $this->typeId, // DBTypeId/category + User::$groups, // staff mask + '' // misc (here tooltip) + ); + + if ($this->enhancedTT) + $key[3] = md5(serialize($this->enhancedTT)); + + return $key; + } +} + + +trait TrRss +{ + private array $feedData = []; + + protected function generateRSS(string $title, string $link) : string + { + $root = new SimpleXML(''); + $root->addAttribute('version', '2.0'); + + $channel = $root->addChild('channel'); + + $channel->addChild('title', Cfg::get('NAME_SHORT').' - '.$title); + $channel->addChild('link', Cfg::get('HOST_URL').'/?'.$link); + $channel->addChild('description', Cfg::get('NAME')); + $channel->addChild('language', implode('-', str_split(Lang::getLocale()->json(), 2))); + $channel->addChild('ttl', Cfg::get('TTL_RSS')); + $channel->addChild('lastBuildDate', date(DATE_RSS)); + + foreach ($this->feedData as $row) + { + $item = $channel->addChild('item'); + + foreach ($row as $key => [$isCData, $attrib, $text]) + { + if ($isCData && $text) + $child = $item->addChild($key)->addCData($text); + else + $child = $item->addChild($key, $text); + + foreach ($attrib as $k => $v) + $child->addAttribute($k, $v); + } + } + + // pretty print for debug + if (Cfg::get('DEBUG') >= LOG_LEVEL_INFO) + { + $dom = new \DOMDocument('1.0'); + $dom->formatOutput = true; + $dom->loadXML($root->asXML()); + return $dom->saveXML(); + } + + return $root->asXML(); + } +} + +trait TrCommunityHelper +{ + private function handleCaption(?string $caption) : string + { + if (!$caption) + return ''; + + // trim excessive whitespaces + $caption = trim(preg_replace('/\s{2,}/', ' ', $caption)); + + // shorten to fit db + return substr($caption, 0, 200); + } +} + +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; + + public function __construct(string $rawParam = '') + { + self::$time = microtime(true); + $this->params = explode('.', $rawParam); + // todo - validate params? + + parent::__construct(); + + if (Cfg::get('MAINTENANCE') && !User::isInGroup(U_GROUP_EMPLOYEE)) + $this->generate404(); + } + + // by default ajax has nothing to say + protected function onUserGroupMismatch() : never + { + trigger_error('TextResponse::onUserGroupMismatch - loggedIn: '.($this->requiresLogin ? 'yes' : 'no').'; expected: '.Util::asHex($this->requiredUserGroup).'; is: '.Util::asHex(User::$groups), E_USER_WARNING); + + $this->generate403(); + } + + public function generate404(?string $out = null) : never + { + header('HTTP/1.0 404 Not Found', true, 404); + header($this->contentType); + exit($out); + } + + public function generate403(?string $out = null) : never + { + header('HTTP/1.0 403 Forbidden', true, 403); + header($this->contentType); + exit($out); + } + + protected function display() : void + { + if ($this->redirectTo) + $this->forward($this->redirectTo); + + $out = ($this instanceof ICache) ? $this->applyOnCacheLoaded($this->result) : $this->result; + + $this->sendNoCacheHeader(); + header($this->contentType); + + // NOTE - this may fuck up some javascripts that say they expect ajax, but use the whole string anyway + // so it's limited to tooltips + if (Cfg::get('DEBUG') && User::isInGroup(U_GROUP_STAFF) && $this->result instanceof Tooltip) + { + $this->sumSQLStats(); + + echo "/*\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) . ' - ' . 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 new file mode 100644 index 00000000..be782877 --- /dev/null +++ b/includes/components/screenshotmgr.class.php @@ -0,0 +1,216 @@ + self::MAX_W || $is[1] > self::MAX_H) + self::$error = Lang::screenshot('error', 'selectSS'); + } + else + self::$error = Lang::screenshot('error', 'selectSS'); + + if (!self::$error) + return true; + + self::$fileName = ''; + return false; + } + + public static function createThumbnail(string $fileName) : bool + { + if (!self::$img) + return false; + + return static::resizeAndWrite(self::DIMS_THUMB[0], self::DIMS_THUMB[1], self::PATH_THUMB, $fileName); + } + + public static function createResized(string $fileName) : bool + { + if (!self::$img) + return false; + + return self::resizeAndWrite(self::DIMS_RESIZED[0], self::DIMS_RESIZED[1], self::PATH_RESIZED, $fileName); + } + + + /*************/ + /* Admin Mgr */ + /*************/ + + public static function getScreenshots(int $type = 0, int $typeId = 0, $userId = 0, ?int &$nFound = 0) : array + { + 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 %and + %lmt', + $where, $userId || $type ? PHP_INT_MAX : 100 + ); + + $num = []; + foreach ($screenshots as $s) + $num[$s['type']][$s['typeId']] = ($num[$s['type']][$s['typeId']] ?? 0) + 1; + + $nFound = 0; + + // format data to meet requirements of the js + foreach ($screenshots as $i => &$s) + { + $nFound++; + + $s['date'] = date(Util::$dateFormatInternal, $s['date']); + + $s['name'] = "Screenshot #".$s['id']; // what should we REALLY name it? + + if ($i > 0) + $s['prev'] = $i - 1; + + if (($i + 1) < count($screenshots)) + $s['next'] = $i + 1; + + // order gives priority for 'status' + if (!($s['flags'] & CC_FLAG_APPROVED)) + { + $s['pending'] = 1; + $s['status'] = self::STATUS_PENDING; + } + else + $s['status'] = self::STATUS_APPROVED; + + if ($s['flags'] & CC_FLAG_STICKY) + { + $s['sticky'] = 1; + $s['status'] = self::STATUS_STICKY; + } + + if ($s['flags'] & CC_FLAG_DELETED) + { + $s['deleted'] = 1; + $s['status'] = self::STATUS_DELETED; + } + + // something todo with massSelect .. am i doing this right? + if ($num[$s['type']][$s['typeId']] == 1) + $s['unique'] = 1; + + if (!$s['user']) + unset($s['user']); + } + + return $screenshots; + } + + public static function getPages(?bool $all, ?int &$nFound) : array + { + // i GUESS .. ss_getALL ? everything : pending + $nFound = 0; + 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) + { + $ids = []; + foreach ($pages as $row) + if ($row['type'] == $t) + $ids[] = $row['typeId']; + + if (!$ids) + continue; + + $obj = Type::newList($t, [['id', $ids]]); + if (!$obj || $obj->error) + continue; + + foreach ($pages as &$p) + if ($p['type'] == $t) + if ($obj->getEntry($p['typeId'])) + $p['name'] = $obj->getField('name', true); + } + + foreach ($pages as &$p) + { + if (empty($p['name'])) + { + trigger_error('ScreenshotMgr::getPages - screenshot linked to nonexistent type/typeId combination: '.$p['type'].'/'.$p['typeId'], E_USER_NOTICE); + unset($p); + } + else + { + $nFound += $p['count']; + $p['date'] = date(Util::$dateFormatInternal, $p['date']); + } + } + } + + return $pages; + } +} + +?> diff --git a/includes/components/search.class.php b/includes/components/search.class.php new file mode 100644 index 00000000..5587071c --- /dev/null +++ b/includes/components/search.class.php @@ -0,0 +1,1628 @@ + '_searchCharClass', + self::MOD_RACE => '_searchCharRace', + self::MOD_TITLE => '_searchTitle', + self::MOD_WORLDEVENT => '_searchWorldEvent', + self::MOD_CURRENCY => '_searchCurrency', + self::MOD_ITEMSET => '_searchItemset', + self::MOD_ITEM => '_searchItem', + self::MOD_ABILITY => '_searchAbility', + self::MOD_TALENT => '_searchTalent', + self::MOD_GLYPH => '_searchGlyph', + self::MOD_PROFICIENCY => '_searchProficiency', + self::MOD_PROFESSION => '_searchProfession', + self::MOD_COMPANION => '_searchCompanion', + self::MOD_MOUNT => '_searchMount', + self::MOD_CREATURE => '_searchCreature', + self::MOD_QUEST => '_searchQuest', + self::MOD_ACHIEVEMENT => '_searchAchievement', + self::MOD_STATISTIC => '_searchStatistic', + self::MOD_ZONE => '_searchZone', + self::MOD_OBJECT => '_searchObject', + self::MOD_FACTION => '_searchFaction', + self::MOD_SKILL => '_searchSkill', + self::MOD_PET => '_searchPet', + self::MOD_CREATURE_ABILITY => '_searchCreatureAbility', + self::MOD_SPELL => '_searchSpell', + self::MOD_EMOTE => '_searchEmote', + self::MOD_ENCHANTMENT => '_searchEnchantment', + self::MOD_SOUND => '_searchSound' + ); + + private array $jsgStore = []; + private array $resultStore = []; + private array $included = []; + private array $excluded = []; + 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 array $extraCnd = [], private array $extraOpts = [], private int $maxResults = self::DEFAULT_MAX_RESULTS) + { + $this->tokenizeQuery(); + + $this->cndBase[] = $this->maxResults; + + // Exclude internal wow stuff + if (!User::isInGroup(U_GROUP_EMPLOYEE)) + $this->cndBase[] = [['cuFlags', CUSTOM_EXCLUDE_FOR_LISTVIEW, '&'], 0]; + } + + private function tokenizeQuery() : void + { + if (!$this->query) + return; + + if (Util::checkNumeric($this->query, NUM_CAST_INT)) + { + $this->idSearch = true; + $this->included[] = $this->query; + return; + } + + $allowShort = Lang::getLocale()->isLogographic(); + + foreach (explode(' ', $this->query) as $raw) + { + if ([$like, $fulltext, $ex] = Filter::transformToken($raw, $allowShort)) + { + $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 + $this->invalid[] = $raw; + } + } + + private function createLikeLookup(array $fields = []) : array + { + if ($this->idSearch && $this->included) + return ['id', $this->included]; + + if (!$this->included && !$this->excluded) + return []; + + // default to name-field + if (!$fields) + $fields[] = 'name_loc'.Lang::getLocale()->value; + + $qry = []; + foreach ($fields as $f) + { + $sub = []; + $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, DB::AND); + else + $sub = $sub[0]; + + $qry[] = $sub; + } + + // single cnd? + if (count($qry) > 1) + 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 + return (($this->included || $this->extraOpts)) && $this->moduleMask; + } + + public function perform() : \Generator + { + $shared = []; + foreach (self::MODULES as $idx => $ref) + { + if (!($this->moduleMask & (1 << $idx))) + continue; + + $this->resultStore[$idx] ??= $this->$ref($shared); + + if (!$this->resultStore[$idx]) + continue; + + yield $idx => $this->resultStore[$idx]; + } + } + + public function getJSGlobals() : array + { + return $this->jsgStore; + } + + + /******************/ + /* Search Modules */ + /******************/ + + private function _searchCharClass() : ?array // 0 Classes: $moduleMask & 0x00000001 + { + $cnd = array_merge($this->cndBase, [$this->createLikeLookup()]); + $classes = new CharClassList($cnd, ['calcTotal' => true]); + + $data = $classes->getListviewData(); + if (!$data) + return []; + + if ($this->moduleMask & self::TYPE_REGULAR) + { + $lvData = ['data' => $data]; + + if ($classes->getMatches() > $this->maxResults) + { + // $lvData['note'] = sprintf(Util::$tryNarrowingString, 'LANG.lvnote_', $classes->getMatches(), $this->maxResults); + $lvData['_truncated'] = 1; + } + + return [$lvData, CharClassList::$brickFile]; + } + + if ($this->moduleMask & self::TYPE_OPEN) + { + $result = []; + $osInfo = [Type::CHR_CLASS, $classes->getMatches(), [], [], 'Class']; + + foreach ($classes->iterate() as $id => $__) + { + $result[$id] = $classes->getField('name', true); + $osInfo[2][$id] = 'class_'.strToLower($classes->getField('fileString')); + } + + return [$result, ...$osInfo]; + } + + return null; + } + + private function _searchCharRace() : ?array // 1 Races: $moduleMask & 0x00000002 + { + $cnd = array_merge($this->cndBase, [$this->createLikeLookup()]); + $races = new CharRaceList($cnd, ['calcTotal' => true]); + + $data = $races->getListviewData(); + if (!$data) + return []; + + if ($this->moduleMask & self::TYPE_REGULAR) + { + $lvData = ['data' => $data]; + + if ($races->getMatches() > $this->maxResults) + { + // $lvData['note'] = sprintf(Util::$tryNarrowingString, 'LANG.lvnote_', $races->getMatches(), $this->maxResults); + $lvData['_truncated'] = 1; + } + + return [$lvData, CharRaceList::$brickFile]; + } + + if ($this->moduleMask & self::TYPE_OPEN) + { + $result = []; + $osInfo = [Type::CHR_RACE, $races->getMatches(), [], [], 'Race']; + + foreach ($races->iterate() as $id => $__) + { + $result[$id] = $races->getField('name', true); + $osInfo[2][$id] = 'race_'.strToLower($races->getField('fileString')).'_male'; + } + + return [$result, ...$osInfo]; + } + + return null; + } + + private function _searchTitle() : ?array // 2 Titles: $moduleMask & 0x00000004 + { + $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(); + if (!$data) + return []; + + if ($this->moduleMask & self::TYPE_REGULAR) + { + $lvData = ['data' => $data]; + + if ($titles->getMatches() > $this->maxResults) + { + // $lvData['note'] = sprintf(Util::$tryNarrowingString, 'LANG.lvnote_', $titles->getMatches(), $this->maxResults); + $lvData['_truncated'] = 1; + } + + return [$lvData, TitleList::$brickFile]; + } + + if ($this->moduleMask & self::TYPE_OPEN) + { + $result = []; + $osInfo = [Type::TITLE, $titles->getMatches(), [], [], 'Title']; + + foreach ($titles->iterate() as $id => $__) + { + $result[$id] = $titles->getField('male', true); + $osInfo[2][$id] = $titles->getField('side'); + } + + return [$result, ...$osInfo]; + } + + return null; + } + + private function _searchWorldEvent() : ?array // 3 World Events: $moduleMask & 0x00000008 + { + $cnd = array_merge($this->cndBase, array( + array( + 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]); + + $data = $wEvents->getListviewData(); + if (!$data) + return []; + + if ($this->moduleMask & self::TYPE_REGULAR) + { + Util::mergeJsGlobals($this->jsgStore, $wEvents->getJSGlobals()); + + // as allways: dates are updated in postCache-step + $lvData = ['data' => $data]; + + if ($wEvents->getMatches() > $this->maxResults) + { + // $lvData['note'] = sprintf(Util::$tryNarrowingString, 'LANG.lvnote_', $wEvents->getMatches(), $this->maxResults); + $lvData['_truncated'] = 1; + } + + return [$lvData, WorldEventList::$brickFile]; + } + + if ($this->moduleMask & self::TYPE_OPEN) + { + $result = []; + $osInfo = [Type::WORLDEVENT, $wEvents->getMatches(), [], [], 'World Event']; + + foreach ($wEvents->iterate() as $id => $__) + $result[$id] = $wEvents->getField('name', true); + + return [$result, ...$osInfo]; + } + + return null; + } + + private function _searchCurrency() : ?array // 4 Currencies $moduleMask & 0x0000010 + { + $cnd = array_merge($this->cndBase, [$this->createLikeLookup()]); + $money = new CurrencyList($cnd, ['calcTotal' => true]); + + $data = $money->getListviewData(); + if (!$data) + return []; + + if ($this->moduleMask & self::TYPE_REGULAR) + { + $lvData = ['data' => $data]; + + if ($money->getMatches() > $this->maxResults) + { + $lvData['note'] = sprintf(Util::$tryNarrowingString, 'LANG.lvnote_currenciesfound', $money->getMatches(), $this->maxResults); + $lvData['_truncated'] = 1; + } + + return [$lvData, CurrencyList::$brickFile]; + } + + if ($this->moduleMask & self::TYPE_OPEN) + { + $result = []; + $osInfo = [Type::CURRENCY, $money->getMatches(), [], [], 'Currency']; + + foreach ($money->iterate() as $id => $__) + { + $result[$id] = $money->getField('name', true); + $osInfo[2][$id] = $money->getField('iconString'); + } + + return [$result, ...$osInfo]; + } + + return null; + } + + private function _searchItemset(array &$shared) : ?array// 5 Itemsets $moduleMask & 0x0000020 + { + $cnd = array_merge($this->cndBase, [$this->createLikeLookup()]); + $sets = new ItemsetList($cnd, ['calcTotal' => true]); + + $data = $sets->getListviewData(); + if (!$data) + return []; + + if ($this->moduleMask & self::TYPE_REGULAR) + { + Util::mergeJsGlobals($this->jsgStore, $sets->getJSGlobals(GLOBALINFO_SELF)); + + $lvData = ['data' => $data]; + + if ($sets->getMatches() > $this->maxResults) + { + $lvData['note'] = sprintf(Util::$tryNarrowingString, 'LANG.lvnote_itemsetsfound', $sets->getMatches(), $this->maxResults); + $lvData['_truncated'] = 1; + } + + if (isset($lvData['note'])) + $lvData['note'] .= ' + LANG.dash + $WH.sprintf(LANG.lvnote_filterresults, \'?itemsets&filter=na='.urlencode($this->query).'\')'; + else + $lvData['note'] = '$$WH.sprintf(LANG.lvnote_filterresults, \'?itemsets&filter=na='.urlencode($this->query).'\')'; + + return [$lvData, ItemsetList::$brickFile]; + } + + if ($this->moduleMask & self::TYPE_OPEN) + { + $result = []; + $osInfo = [Type::ITEMSET, $sets->getMatches(), [], [], 'Item Set']; + + foreach ($sets->iterate() as $id => $__) + { + $result[$id] = $sets->getField('name', true); + $osInfo[3][$id] = $sets->getField('quality'); + } + + return [$result, ...$osInfo]; + } + + if ($this->moduleMask & self::TYPE_JSON) + { + $shared['pcsToSet'] = $sets->pieceToSet; + + foreach ($data as &$d) + unset($d['quality'], $d['heroic']); + + return array_values($data); + } + + return null; + } + + private function _searchItem(array &$shared) : ?array // 6 Items $moduleMask & 0x0000040 + { + $miscData = ['calcTotal' => true]; + $lookup = $this->createMatchLookup(); + if (!$lookup) + return null; + + if ($this->moduleMask & self::TYPE_JSON) + { + if (!empty($shared['pcsToSet'])) + { + $cnd = [['i.id', array_keys($shared['pcsToSet'])]]; + $miscData = ['pcsToSet' => $shared['pcsToSet']]; + } + else + { + $cnd = $this->cndBase; + $cnd[] = ['i.class', [ITEM_CLASS_WEAPON, ITEM_CLASS_GEM, ITEM_CLASS_ARMOR]]; + $cnd[] = $lookup; + + if ($this->extraOpts) + $miscData['extraOpts'] = $this->extraOpts; + if ($this->extraCnd) + $cnd = array_merge($cnd, $this->extraCnd); + } + } + else + $cnd = array_merge($this->cndBase, [$lookup]); + + $items = new ItemList($cnd, $miscData); + + $data = $items->getListviewData($this->moduleMask & self::TYPE_JSON ? (ITEMINFO_SUBITEMS | ITEMINFO_JSON) : 0); + if (!$data) + return []; + + if ($this->moduleMask & self::TYPE_REGULAR) + { + Util::mergeJsGlobals($this->jsgStore, $items->getJSGlobals()); + + $lvData = ['data' => $data]; + + if ($items->getMatches() > $this->maxResults) + { + $lvData['note'] = sprintf(Util::$tryNarrowingString, 'LANG.lvnote_itemsfound', $items->getMatches(), $this->maxResults); + $lvData['_truncated'] = 1; + } + + if (isset($lvData['note'])) + $lvData['note'] .= ' + LANG.dash + $WH.sprintf(LANG.lvnote_filterresults, \'?items&filter=na='.urlencode($this->query).'\')'; + else + $lvData['note'] = '$$WH.sprintf(LANG.lvnote_filterresults, \'?items&filter=na='.urlencode($this->query).'\')'; + + return [$lvData, ItemList::$brickFile]; + } + + if ($this->moduleMask & self::TYPE_OPEN) + { + $result = []; + $osInfo = [Type::ITEM, $items->getMatches(), [], [], 'Item']; + + foreach ($items->iterate() as $id => $__) + { + $result[$id] = $items->getField('name', true); + $osInfo[2][$id] = $items->getField('iconString'); + $osInfo[3][$id] = $items->getField('quality'); + } + + return [$result, ...$osInfo]; + } + + if ($this->moduleMask & self::TYPE_JSON) + { + foreach ($data as &$d) + if (!empty($d['subitems'])) + foreach ($d['subitems'] as &$si) + $si['enchantment'] = implode(', ', $si['enchantment']); + + return array_values($data); + } + + return null; + } + + private function _searchAbility() : ?array // 7 Abilities (Player + Pet) $moduleMask & 0x0000080 + { + $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], + $lookup + )); + $abilities = new SpellList($cnd, ['calcTotal' => true]); + + $data = $abilities->getListviewData(); + if (!$data) + return []; + + if ($this->moduleMask & self::TYPE_REGULAR) + { + Util::mergeJsGlobals($this->jsgStore, $abilities->getJSGlobals(GLOBALINFO_SELF | GLOBALINFO_RELATED)); + + $vis = ['level', 'schools']; + if ($abilities->hasSetFields('reagent1', 'reagent2', 'reagent3', 'reagent4', 'reagent5', 'reagent6', 'reagent7', 'reagent8')) + $vis[] = 'reagents'; + + if ($abilities->hasSetFields('reqclass')) + $vis[] = 'classes'; // i'd love to set 'singleclass', but do i want to walk through all abilities to see if each mask contains at most 1 class? + + $lvData = array( + 'data' => $data, + 'id' => 'abilities', + 'name' => '$LANG.tab_abilities', + 'visibleCols' => $vis + ); + + if ($abilities->getMatches() > $this->maxResults) + { + $lvData['note'] = sprintf(Util::$tryNarrowingString, 'LANG.lvnote_abilitiesfound', $abilities->getMatches(), $this->maxResults); + $lvData['_truncated'] = 1; + } + + if (isset($lvData['note'])) + $lvData['note'] .= ' + LANG.dash + $WH.sprintf(LANG.lvnote_filterresults, \'?spells=7&filter=na='.urlencode($this->query).'\')'; + else + $lvData['note'] = '$$WH.sprintf(LANG.lvnote_filterresults, \'?spells=7&filter=na='.urlencode($this->query).'\')'; + + return [$lvData, SpellList::$brickFile]; + } + + if ($this->moduleMask & self::TYPE_OPEN) + { + $result = []; + $osInfo = [Type::SPELL, $abilities->getMatches(), [], [], 'Ability']; + + foreach ($abilities->iterate() as $id => $__) + { + $result[$id] = $abilities->getField('name', true); + $osInfo[2][$id] = $abilities->getField('iconString'); + $osInfo[3][$id] = $abilities->ranks[$id]; + } + + return [$result, ...$osInfo]; + } + + return null; + } + + private function _searchTalent() : ?array // 8 Talents (Player + Pet) $moduleMask & 0x0000100 + { + $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(); + if (!$data) + return []; + + if ($this->moduleMask & self::TYPE_REGULAR) + { + Util::mergeJsGlobals($this->jsgStore, $talents->getJSGlobals()); + + $vis = ['level', 'singleclass', 'schools']; + if ($talents->hasSetFields('reagent1', 'reagent2', 'reagent3', 'reagent4', 'reagent5', 'reagent6', 'reagent7', 'reagent8')) + $vis[] = 'reagents'; + + $lvData = array( + 'data' => $data, + 'id' => 'talents', + 'name' => '$LANG.tab_talents', + 'visibleCols' => $vis + ); + + if ($talents->getMatches() > $this->maxResults) + { + $lvData['note'] = sprintf(Util::$tryNarrowingString, 'LANG.lvnote_talentsfound', $talents->getMatches(), $this->maxResults); + $lvData['_truncated'] = 1; + } + + if (isset($lvData['note'])) + $lvData['note'] .= ' + LANG.dash + $WH.sprintf(LANG.lvnote_filterresults, \'?spells=-2&filter=na='.urlencode($this->query).'\')'; + else + $lvData['note'] = '$$WH.sprintf(LANG.lvnote_filterresults, \'?spells=-2&filter=na='.urlencode($this->query).'\')'; + + return [$lvData, SpellList::$brickFile]; + } + + if ($this->moduleMask & self::TYPE_OPEN) + { + $result = []; + $osInfo = [Type::SPELL, $talents->getMatches(), [], [], 'Talent']; + + foreach ($talents->iterate() as $id => $__) + { + $result[$id] = $talents->getField('name', true); + $osInfo[2][$id] = $talents->getField('iconString'); + $osInfo[3][$id] = $talents->ranks[$id]; + } + + return [$result, ...$osInfo]; + } + + return null; + } + + private function _searchGlyph() : ?array // 9 Glyphs $moduleMask & 0x0000200 + { + $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(); + if (!$data) + return []; + + if ($this->moduleMask & self::TYPE_REGULAR) + { + Util::mergeJsGlobals($this->jsgStore, $glyphs->getJSGlobals()); + + $lvData = array( + 'data' => $data, + 'id' => 'glyphs', + 'name' => '$LANG.tab_glyphs', + 'visibleCols' => ['singleclass', 'glyphtype'] + ); + + if ($glyphs->getMatches() > $this->maxResults) + { + $lvData['note'] = sprintf(Util::$tryNarrowingString, 'LANG.lvnote_glyphsfound', $glyphs->getMatches(), $this->maxResults); + $lvData['_truncated'] = 1; + } + + if (isset($lvData['note'])) + $lvData['note'] .= ' + LANG.dash + $WH.sprintf(LANG.lvnote_filterresults, \'?spells=-13&filter=na='.urlencode($this->query).'\')'; + else + $lvData['note'] = '$$WH.sprintf(LANG.lvnote_filterresults, \'?spells=-13&filter=na='.urlencode($this->query).'\')'; + + return [$lvData, SpellList::$brickFile]; + } + + if ($this->moduleMask & self::TYPE_OPEN) + { + $result = []; + $osInfo = [Type::SPELL, $glyphs->getMatches(), [], [], 'Glyph']; + + foreach ($glyphs->iterate() as $id => $__) + { + $result[$id] = $glyphs->getField('name', true); + $osInfo[2][$id] = $glyphs->getField('iconString'); + } + + return [$result, ...$osInfo]; + } + + return null; + } + + private function _searchProficiency() : ?array // 10 Proficiencies $moduleMask & 0x0000400 + { + $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(); + if (!$data) + return []; + + if ($this->moduleMask & self::TYPE_REGULAR) + { + Util::mergeJsGlobals($this->jsgStore, $prof->getJSGlobals()); + + $lvData = array( + 'data' => $data, + 'id' => 'proficiencies', + 'name' => '$LANG.tab_proficiencies', + 'visibleCols' => ['classes'] + ); + + if ($prof->getMatches() > $this->maxResults) + { + $lvData['note'] = sprintf(Util::$tryNarrowingString, 'LANG.lvnote_spellsfound', $prof->getMatches(), $this->maxResults); + $lvData['_truncated'] = 1; + } + + if (isset($lvData['note'])) + $lvData['note'] .= ' + LANG.dash + $WH.sprintf(LANG.lvnote_filterresults, \'?spells=-11&filter=na='.urlencode($this->query).'\')'; + else + $lvData['note'] = '$$WH.sprintf(LANG.lvnote_filterresults, \'?spells=-11&filter=na='.urlencode($this->query).'\')'; + + return [$lvData, SpellList::$brickFile]; + } + + if ($this->moduleMask & self::TYPE_OPEN) + { + $result = []; + $osInfo = [Type::SPELL, $prof->getMatches(), [], [], 'Proficiency']; + + foreach ($prof->iterate() as $id => $__) + { + $result[$id] = $prof->getField('name', true); + $osInfo[2][$id] = $prof->getField('iconString'); + } + + return [$result, ...$osInfo]; + } + + return null; + } + + private function _searchProfession() : ?array // 11 Professions (Primary + Secondary) $moduleMask & 0x0000800 + { + $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(); + if (!$data) + return []; + + if ($this->moduleMask & self::TYPE_REGULAR) + { + Util::mergeJsGlobals($this->jsgStore, $prof->getJSGlobals(GLOBALINFO_SELF | GLOBALINFO_RELATED)); + + $lvData = array( + 'data' => $data, + 'id' => 'professions', + 'name' => '$LANG.tab_professions', + 'visibleCols' => ['source', 'reagents'] + ); + + if ($prof->getMatches() > $this->maxResults) + { + $lvData['note'] = sprintf(Util::$tryNarrowingString, 'LANG.lvnote_professionfound', $prof->getMatches(), $this->maxResults); + $lvData['_truncated'] = 1; + } + + if (isset($lvData['note'])) + $lvData['note'] .= ' + LANG.dash + $WH.sprintf(LANG.lvnote_filterresults, \'?spells=11&filter=na='.urlencode($this->query).'\')'; + else + $lvData['note'] = '$$WH.sprintf(LANG.lvnote_filterresults, \'?spells=11&filter=na='.urlencode($this->query).'\')'; + + return [$lvData, SpellList::$brickFile]; + } + + if ($this->moduleMask & self::TYPE_OPEN) + { + $result = []; + $osInfo = [Type::SPELL, $prof->getMatches(), [], [], 'Profession']; + + foreach ($prof->iterate() as $id => $__) + { + $result[$id] = $prof->getField('name', true); + $osInfo[2][$id] = $prof->getField('iconString'); + } + + return [$result, ...$osInfo]; + } + + return null; + } + + private function _searchCompanion() : ?array // 12 Companions $moduleMask & 0x0001000 + { + $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(); + if (!$data) + return []; + + if ($this->moduleMask & self::TYPE_REGULAR) + { + Util::mergeJsGlobals($this->jsgStore, $vPets->getJSGlobals()); + + $lvData = array( + 'data' => $data, + 'id' => 'companions', + 'name' => '$LANG.tab_companions', + 'visibleCols' => ['reagents'] + ); + + if ($vPets->getMatches() > $this->maxResults) + { + $lvData['note'] = sprintf(Util::$tryNarrowingString, 'LANG.lvnote_companionsfound', $vPets->getMatches(), $this->maxResults); + $lvData['_truncated'] = 1; + } + + if (isset($lvData['note'])) + $lvData['note'] .= ' + LANG.dash + $WH.sprintf(LANG.lvnote_filterresults, \'?spells=-6&filter=na='.urlencode($this->query).'\')'; + else + $lvData['note'] = '$$WH.sprintf(LANG.lvnote_filterresults, \'?spells=-6&filter=na='.urlencode($this->query).'\')'; + + return [$lvData, SpellList::$brickFile]; + } + + if ($this->moduleMask & self::TYPE_OPEN) + { + $result = []; + $osInfo = [Type::SPELL, $vPets->getMatches(), [], [], 'Companion']; + + foreach ($vPets->iterate() as $id => $__) + { + $result[$id] = $vPets->getField('name', true); + $osInfo[2][$id] = $vPets->getField('iconString'); + } + + return [$result, ...$osInfo]; + } + + return null; + } + + private function _searchMount() : ?array // 13 Mounts $moduleMask & 0x0002000 + { + $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(); + if (!$data) + return []; + + if ($this->moduleMask & self::TYPE_REGULAR) + { + Util::mergeJsGlobals($this->jsgStore, $mounts->getJSGlobals()); + + $lvData = array( + 'data' => $data, + 'id' => 'mounts', + 'name' => '$LANG.tab_mounts', + ); + + if ($mounts->getMatches() > $this->maxResults) + { + $lvData['note'] = sprintf(Util::$tryNarrowingString, 'LANG.lvnote_mountsfound', $mounts->getMatches(), $this->maxResults); + $lvData['_truncated'] = 1; + } + + if (isset($lvData['note'])) + $lvData['note'] .= ' + LANG.dash + $WH.sprintf(LANG.lvnote_filterresults, \'?spells=-5&filter=na='.urlencode($this->query).'\')'; + else + $lvData['note'] = '$$WH.sprintf(LANG.lvnote_filterresults, \'?spells=-5&filter=na='.urlencode($this->query).'\')'; + + return [$lvData, SpellList::$brickFile]; + } + + if ($this->moduleMask & self::TYPE_OPEN) + { + $result = []; + $osInfo = [Type::SPELL, $mounts->getMatches(), [], [], 'Mount']; + + foreach ($mounts->iterate() as $id => $__) + { + $result[$id] = $mounts->getField('name', true); + $osInfo[2][$id] = $mounts->getField('iconString'); + } + + return [$result, ...$osInfo]; + } + + return null; + } + + 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 + $lookup + )); + $npcs = new CreatureList($cnd, ['calcTotal' => true]); + + $data = $npcs->getListviewData(); + if (!$data) + return []; + + if ($this->moduleMask & self::TYPE_REGULAR) + { + $lvData = array( + 'data' => $data, + 'id' => 'npcs', + 'name' => '$LANG.tab_npcs', + ); + + if ($npcs->getMatches() > $this->maxResults) + { + $lvData['note'] = sprintf(Util::$tryNarrowingString, 'LANG.lvnote_npcsfound', $npcs->getMatches(), $this->maxResults); + $lvData['_truncated'] = 1; + } + + if (isset($lvData['note'])) + $lvData['note'] .= ' + LANG.dash + $WH.sprintf(LANG.lvnote_filterresults, \'?npcs&filter=na='.urlencode($this->query).'\')'; + else + $lvData['note'] = '$$WH.sprintf(LANG.lvnote_filterresults, \'?npcs&filter=na='.urlencode($this->query).'\')'; + + return [$lvData, CreatureList::$brickFile]; + } + + if ($this->moduleMask & self::TYPE_OPEN) + { + $result = []; + $osInfo = [Type::NPC, $npcs->getMatches(), [], [], 'NPC']; + + foreach ($npcs->iterate() as $id => $__) + { + $result[$id] = $npcs->getField('name', true); + if ($npcs->isBoss()) + $osInfo[2][$id] = 1; + } + + return [$result, ...$osInfo]; + } + + return null; + } + + 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], + $lookup + )); + $quests = new QuestList($cnd, ['calcTotal' => true]); + + $data = $quests->getListviewData(); + if (!$data) + return []; + + if ($this->moduleMask & self::TYPE_REGULAR) + { + Util::mergeJsGlobals($this->jsgStore, $quests->getJSGlobals()); + + $lvData = ['data' => $data]; + + if ($quests->getMatches() > $this->maxResults) + { + $lvData['note'] = sprintf(Util::$tryNarrowingString, 'LANG.lvnote_questsfound', $quests->getMatches(), $this->maxResults); + $lvData['_truncated'] = 1; + } + + if (isset($lvData['note'])) + $lvData['note'] .= ' + LANG.dash + $WH.sprintf(LANG.lvnote_filterresults, \'?quests&filter=na='.urlencode($this->query).'\')'; + else + $lvData['note'] = '$$WH.sprintf(LANG.lvnote_filterresults, \'?quests&filter=na='.urlencode($this->query).'\')'; + + return [$lvData, QuestList::$brickFile]; + } + + if ($this->moduleMask & self::TYPE_OPEN) + { + $result = []; + $osInfo = [Type::QUEST, $quests->getMatches(), [], [], 'Quest']; + + foreach ($quests->iterate() as $id => $__) + { + $result[$id] = $quests->getField('name', true); + $osInfo[2][$id] = $data[$id]['side']; // why recalculate if already set + } + + return [$result, ...$osInfo]; + } + + return null; + } + + private function _searchAchievement() : ?array // 16 Achievements $moduleMask & 0x0010000 + { + $cnd = array_merge($this->cndBase, array( + [['flags', ACHIEVEMENT_FLAG_COUNTER, '&'], 0], // not a statistic + $this->createLikeLookup() + )); + $acvs = new AchievementList($cnd, ['calcTotal' => true]); + + $data = $acvs->getListviewData(); + if (!$data) + return []; + + if ($this->moduleMask & self::TYPE_REGULAR) + { + Util::mergeJsGlobals($this->jsgStore, $acvs->getJSGlobals()); + + $lvData = array( + 'data' => $data, + 'visibleCols' => ['category'] + ); + + if ($acvs->getMatches() > $this->maxResults) + { + $lvData['note'] = sprintf(Util::$tryNarrowingString, 'LANG.lvnote_achievementsfound', $acvs->getMatches(), $this->maxResults); + $lvData['_truncated'] = 1; + } + + if (isset($lvData['note'])) + $lvData['note'] .= ' + LANG.dash + $WH.sprintf(LANG.lvnote_filterresults, \'?achievements&filter=na='.urlencode($this->query).'\')'; + else + $lvData['note'] = '$$WH.sprintf(LANG.lvnote_filterresults, \'?achievements&filter=na='.urlencode($this->query).'\')'; + + return [$lvData, AchievementList::$brickFile]; + } + + if ($this->moduleMask & self::TYPE_OPEN) + { + $result = []; + $osInfo = [Type::ACHIEVEMENT, $acvs->getMatches(), [], [], 'Achievement']; + + foreach ($acvs->iterate() as $id => $__) + { + $result[$id] = $acvs->getField('name', true); + $osInfo[2][$id] = $acvs->getField('iconString'); + } + + return [$result, ...$osInfo]; + } + + return null; + } + + private function _searchStatistic() : ?array // 17 Statistics $moduleMask & 0x0020000 + { + $cnd = array_merge($this->cndBase, array( + ['flags', ACHIEVEMENT_FLAG_COUNTER, '&'], // is a statistic + $this->createLikeLookup() + )); + $stats = new AchievementList($cnd, ['calcTotal' => true]); + + $data = $stats->getListviewData(); + if (!$data) + return []; + + if ($this->moduleMask & self::TYPE_REGULAR) + { + Util::mergeJsGlobals($this->jsgStore, $stats->getJSGlobals(GLOBALINFO_SELF)); + + $lvData = array( + 'data' => $data, + 'visibleCols' => ['category'], + 'hiddenCols' => ['side', 'points', 'rewards'], + 'name' => '$LANG.tab_statistics', + 'id' => 'statistics' + ); + + if ($stats->getMatches() > $this->maxResults) + { + $lvData['note'] = sprintf(Util::$tryNarrowingString, 'LANG.lvnote_statisticsfound', $stats->getMatches(), $this->maxResults); + $lvData['_truncated'] = 1; + } + + if (isset($lvData['note'])) + $lvData['note'] .= ' + LANG.dash + $WH.sprintf(LANG.lvnote_filterresults, \'?achievements=1&filter=na='.urlencode($this->query).'\')'; + else + $lvData['note'] = '$$WH.sprintf(LANG.lvnote_filterresults, \'?achievements=1&filter=na='.urlencode($this->query).'\')'; + + return [$lvData, AchievementList::$brickFile]; + } + + if ($this->moduleMask & self::TYPE_OPEN) + { + $result = []; + $osInfo = [Type::ACHIEVEMENT, $stats->getMatches(), [], [], 'Statistic']; + + foreach ($stats->iterate() as $id => $__) + { + $result[$id] = $stats->getField('name', true); + $osInfo[2][$id] = $stats->getField('iconString'); + } + + return [$result, ...$osInfo]; + } + + return null; + } + + private function _searchZone() : ?array // 18 Zones $moduleMask & 0x0040000 + { + $cnd = array_merge($this->cndBase, [$this->createLikeLookup()]); + $zones = new ZoneList($cnd, ['calcTotal' => true]); + + $data = $zones->getListviewData(); + if (!$data) + return []; + + if ($this->moduleMask & self::TYPE_REGULAR) + { + Util::mergeJsGlobals($this->jsgStore, $zones->getJSGlobals()); + + $lvData = ['data' => $data]; + + if ($zones->getMatches() > $this->maxResults) + { + $lvData['note'] = sprintf(Util::$tryNarrowingString, 'LANG.lvnote_zonesfound', $zones->getMatches(), $this->maxResults); + $lvData['_truncated'] = 1; + } + + if (isset($lvData['note'])) + $lvData['note'] .= ' + LANG.dash + $WH.sprintf(LANG.lvnote_filterresults, \'?achievements&filter=na='.urlencode($this->query).'\')'; + else + $lvData['note'] = '$$WH.sprintf(LANG.lvnote_filterresults, \'?achievements&filter=na='.urlencode($this->query).'\')'; + + return [$lvData, ZoneList::$brickFile]; + } + + if ($this->moduleMask & self::TYPE_OPEN) + { + $result = []; + $osInfo = [Type::ZONE, $zones->getMatches(), [], [], 'Zone']; + + foreach ($zones->iterate() as $id => $__) + $result[$id] = $zones->getField('name', true); + + return [$result, ...$osInfo]; + } + + return null; + } + + private function _searchObject() : ?array // 19 Objects $moduleMask & 0x0080000 + { + $lookup = $this->createMatchLookup(); + if (!$lookup) + return null; + + $cnd = array_merge($this->cndBase, [$lookup]); + $objects = new GameObjectList($cnd, ['calcTotal' => true]); + + $data = $objects->getListviewData(); + if (!$data) + return []; + + if ($this->moduleMask & self::TYPE_REGULAR) + { + Util::mergeJsGlobals($this->jsgStore, $objects->getJSGlobals()); + + $lvData = ['data' => $data]; + + if ($objects->getMatches() > $this->maxResults) + { + $lvData['note'] = sprintf(Util::$tryNarrowingString, 'LANG.lvnote_objectsfound', $objects->getMatches(), $this->maxResults); + $lvData['_truncated'] = 1; + } + + if (isset($lvData['note'])) + $lvData['note'] .= ' + LANG.dash + $WH.sprintf(LANG.lvnote_filterresults, \'?objects&filter=na='.urlencode($this->query).'\')'; + else + $lvData['note'] = '$$WH.sprintf(LANG.lvnote_filterresults, \'?objects&filter=na='.urlencode($this->query).'\')'; + + return [$lvData, GameObjectList::$brickFile]; + } + + if ($this->moduleMask & self::TYPE_OPEN) + { + $result = []; + $osInfo = [Type::OBJECT, $objects->getMatches(), [], [], 'Object']; + + foreach ($objects->iterate() as $id => $__) + $result[$id] = $objects->getField('name', true); + + return [$result, ...$osInfo]; + } + + return null; + } + + private function _searchFaction() : ?array // 20 Factions $moduleMask & 0x0100000 + { + $cnd = array_merge($this->cndBase, [$this->createLikeLookup()]); + $factions = new FactionList($cnd, ['calcTotal' => true]); + + $data = $factions->getListviewData(); + if (!$data) + return []; + + if ($this->moduleMask & self::TYPE_REGULAR) + { + $lvData = ['data' => $data]; + + if ($factions->getMatches() > $this->maxResults) + { + $lvData['note'] = sprintf(Util::$tryNarrowingString, 'LANG.lvnote_factionsfound', $factions->getMatches(), $this->maxResults); + $lvData['_truncated'] = 1; + } + + return [$lvData, FactionList::$brickFile]; + } + + if ($this->moduleMask & self::TYPE_OPEN) + { + $result = []; + $osInfo = [Type::FACTION, $factions->getMatches(), [], [], 'Faction']; + + foreach ($factions->iterate() as $id => $__) + $result[$id] = $factions->getField('name', true); + + return [$result, ...$osInfo]; + } + + return null; + } + + private function _searchSkill() : ?array // 21 Skills $moduleMask & 0x0200000 + { + $cnd = array_merge($this->cndBase, [$this->createLikeLookup()]); + $skills = new SkillList($cnd, ['calcTotal' => true]); + + $data = $skills->getListviewData(); + if (!$data) + return []; + + if ($this->moduleMask & self::TYPE_REGULAR) + { + $lvData = ['data' => $data]; + + if ($skills->getMatches() > $this->maxResults) + { + $lvData['note'] = sprintf(Util::$tryNarrowingString, 'LANG.lvnote_skillsfound', $skills->getMatches(), $this->maxResults); + $lvData['_truncated'] = 1; + } + + return [$lvData, SkillList::$brickFile]; + } + + if ($this->moduleMask & self::TYPE_OPEN) + { + $result = []; + $osInfo = [Type::SKILL, $skills->getMatches(), [], [], 'Skill']; + + foreach ($skills->iterate() as $id => $__) + { + $result[$id] = $skills->getField('name', true); + $osInfo[2][$id] = $skills->getField('iconString'); + } + + return [$result, ...$osInfo]; + } + + return null; + } + + private function _searchPet() : ?array // 22 Pets $moduleMask & 0x0400000 + { + $cnd = array_merge($this->cndBase, [$this->createLikeLookup()]); + $pets = new PetList($cnd, ['calcTotal' => true]); + + $data = $pets->getListviewData(); + if (!$data) + return []; + + if ($this->moduleMask & self::TYPE_REGULAR) + { + $lvData = array( + 'data' => $data, + 'computeDataFunc' => '$_' + ); + + if ($pets->getMatches() > $this->maxResults) + { + $lvData['note'] = sprintf(Util::$tryNarrowingString, 'LANG.lvnote_petsfound', $pets->getMatches(), $this->maxResults); + $lvData['_truncated'] = 1; + } + + return [$lvData, PetList::$brickFile, 'petFoodCol']; + } + + if ($this->moduleMask & self::TYPE_OPEN) + { + $result = []; + $osInfo = [Type::PET, $pets->getMatches(), [], [], 'Pet']; + + foreach ($pets->iterate() as $id => $__) + { + $result[$id] = $pets->getField('name', true); + $osInfo[2][$id] = $pets->getField('iconString'); + } + + return [$result, ...$osInfo]; + } + + return null; + } + + private function _searchCreatureAbility() : ?array // 23 NPCAbilities $moduleMask & 0x0800000 + { + $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(); + if (!$data) + return []; + + if ($this->moduleMask & self::TYPE_REGULAR) + { + Util::mergeJsGlobals($this->jsgStore, $npcAbilities->getJSGlobals()); + + $lvData = array( + 'data' => $data, + 'id' => 'npc-abilities', + 'name' => '$LANG.tab_npcabilities', + 'visibleCols' => ['level'], + 'hiddenCols' => ['skill'] + ); + + if ($npcAbilities->getMatches() > $this->maxResults) + { + $lvData['note'] = sprintf(Util::$tryNarrowingString, 'LANG.lvnote_spellsfound', $npcAbilities->getMatches(), $this->maxResults); + $lvData['_truncated'] = 1; + } + + if (isset($lvData['note'])) + $lvData['note'] .= ' + LANG.dash + $WH.sprintf(LANG.lvnote_filterresults, \'?spells=-8&filter=na='.urlencode($this->query).'\')'; + else + $lvData['note'] = '$$WH.sprintf(LANG.lvnote_filterresults, \'?spells=-8&filter=na='.urlencode($this->query).'\')'; + + return [$lvData, SpellList::$brickFile]; + } + + if ($this->moduleMask & self::TYPE_OPEN) + { + $result = []; + $osInfo = [Type::SPELL, $npcAbilities->getMatches(), [], [], 'Spell']; + + foreach ($npcAbilities->iterate() as $id => $__) + { + $result[$id] = $npcAbilities->getField('name', true); + $osInfo[2][$id] = $npcAbilities->getField('iconString'); + } + + return [$result, ...$osInfo]; + } + + return null; + } + + 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, '!'], + [ + DB::OR, + ['s.typeCat', [0, -9]], + ['s.cuFlags', SPELL_CU_TRIGGERED, '&'], + ['s.attributes0', 0x80, '&'] + ], + $lookup + )); + $misc = new SpellList($cnd, ['calcTotal' => true]); + + $data = $misc->getListviewData(); + if (!$data) + return []; + + if ($this->moduleMask & self::TYPE_REGULAR) + { + Util::mergeJsGlobals($this->jsgStore, $misc->getJSGlobals(GLOBALINFO_SELF | GLOBALINFO_RELATED)); + + $lvData = array( + 'data' => $data, + 'name' => '$LANG.tab_uncategorizedspells', + 'visibleCols' => ['level'], + 'hiddenCols' => ['skill'] + ); + + if ($misc->getMatches() > $this->maxResults) + { + $lvData['note'] = sprintf(Util::$tryNarrowingString, 'LANG.lvnote_spellsfound', $misc->getMatches(), $this->maxResults); + $lvData['_truncated'] = 1; + } + + if (isset($lvData['note'])) + $lvData['note'] .= ' + LANG.dash + $WH.sprintf(LANG.lvnote_filterresults, \'?spells=0&filter=na='.urlencode($this->query).'\')'; + else + $lvData['note'] = '$$WH.sprintf(LANG.lvnote_filterresults, \'?spells=0&filter=na='.urlencode($this->query).'\')'; + + return [$lvData, SpellList::$brickFile]; + } + + if ($this->moduleMask & self::TYPE_OPEN) + { + $result = []; + $osInfo = [Type::SPELL, $misc->getMatches(), [], [], 'Spell']; + + foreach ($misc->iterate() as $id => $__) + { + $result[$id] = $misc->getField('name', true); + $osInfo[2][$id] = $misc->getField('iconString'); + } + + return [$result, ...$osInfo]; + } + + return null; + } + + private function _searchEmote() : ?array // 25 Emotes $moduleMask & 0x2000000 + { + $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(); + if (!$data) + return []; + + if ($this->moduleMask & self::TYPE_REGULAR) + { + Util::mergeJsGlobals($this->jsgStore, $emote->getJSGlobals()); + + $lvData = array( + 'data' => $data, + 'name' => Util::ucFirst(Lang::game('emotes')) + ); + + if ($emote->getMatches() > $this->maxResults) + { + // $lvData['note'] = sprintf(Util::$tryNarrowingString, 'LANG.lvnote_emotesfound', $emote->getMatches(), $this->maxResults); + $lvData['_truncated'] = 1; + } + + return [$lvData, EmoteList::$brickFile]; + } + + if ($this->moduleMask & self::TYPE_OPEN) + { + $result = []; + $osInfo = [Type::EMOTE, $emote->getMatches(), [], [], 'Emote']; + + foreach ($emote->iterate() as $id => $__) + $result[$id] = $emote->getField('name', true); + + return [$result, ...$osInfo]; + } + + return null; + } + + private function _searchEnchantment() : ?array // 26 Enchantments $moduleMask & 0x4000000 + { + $cnd = array_merge($this->cndBase, [$this->createLikeLookup(['name_loc'.Lang::getLocale()->value])]); + $enchantment = new EnchantmentList($cnd, ['calcTotal' => true]); + + $data = $enchantment->getListviewData(); + if (!$data) + return []; + + if ($this->moduleMask & self::TYPE_REGULAR) + { + Util::mergeJsGlobals($this->jsgStore, $enchantment->getJSGlobals()); + + $lvData = array( + 'data' => $data, + 'name' => Util::ucFirst(Lang::game('enchantments')) + ); + + if (array_filter(array_column($data, 'spells'))) + $lvData['visibleCols'] = ['trigger']; + + if (!$enchantment->hasSetFields('skillLine')) + $lvData['hiddenCols'] = ['skill']; + + if ($enchantment->getMatches() > $this->maxResults) + { + $lvData['note'] = sprintf(Util::$tryNarrowingString, 'LANG.lvnote_enchantmentsfound', $enchantment->getMatches(), $this->maxResults); + $lvData['_truncated'] = 1; + } + + return [$lvData, EnchantmentList::$brickFile]; + } + + if ($this->moduleMask & self::TYPE_OPEN) + { + $result = []; + $osInfo = [Type::ENCHANTMENT, $enchantment->getMatches(), [], [], 'Enchantment']; + + foreach ($enchantment->iterate() as $id => $__) + $result[$id] = $enchantment->getField('name', true); + + return [$result, ...$osInfo]; + } + + return null; + } + + private function _searchSound() : ?array // 27 Sounds $moduleMask & 0x8000000 + { + $cnd = array_merge($this->cndBase, [$this->createLikeLookup(['name'])]); + $sounds = new SoundList($cnd, ['calcTotal' => true]); + + $data = $sounds->getListviewData(); + if (!$data) + return []; + + if ($this->moduleMask & self::TYPE_REGULAR) + { + Util::mergeJsGlobals($this->jsgStore, $sounds->getJSGlobals()); + + $lvData = array( + 'data' => $data, + ); + + if ($sounds->getMatches() > $this->maxResults) + { + $lvData['note'] = sprintf(Util::$tryNarrowingString, 'LANG.lvnote_soundsfound', $sounds->getMatches(), $this->maxResults); + $lvData['_truncated'] = 1; + } + + return [$lvData, SoundList::$brickFile]; + } + + if ($this->moduleMask & self::TYPE_OPEN) + { + $result = []; + $osInfo = [Type::SOUND, $sounds->getMatches(), [], [], 'Sound']; + + foreach ($sounds->iterate() as $id => $__) + $result[$id] = $sounds->getField('name', true); + + return [$result, ...$osInfo]; + } + + return null; + } +} 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 @@ + [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(''); + $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(''); + $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(''); + $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 new file mode 100644 index 00000000..1ce894bd --- /dev/null +++ b/includes/components/videomgr.class.php @@ -0,0 +1,220 @@ +id . PHP_EOL); + fwrite($tmpFile, $videoInfo->title . PHP_EOL); + fwrite($tmpFile, $videoInfo->thumbnail_url . PHP_EOL); + fwrite($tmpFile, $videoInfo->thumbnail_height . PHP_EOL); + fwrite($tmpFile, $videoInfo->thumbnail_width . PHP_EOL); + + return fclose($tmpFile); + } + + public static function loadSuggestion(?\stdClass &$videoInfo, int $destType, int $destTypeId, ?string $uid) : bool + { + self::$tmpFile = sprintf(self::PATH_TEMP, User::$username.'-'.$destType.'-'.$destTypeId.'-'.$uid); + + if (!file_exists(self::$tmpFile)) + return false; + + if ($info = file(self::$tmpFile, FILE_IGNORE_NEW_LINES)) + { + $videoInfo = new \stdClass; + $videoInfo->id = $info[0]; + $videoInfo->title = $info[1]; + $videoInfo->thumbnail_url = $info[2]; + $videoInfo->thumbnail_height = (int)$info[3]; + $videoInfo->thumbnail_width = (int)$info[4]; + + return true; + } + + return false; + } + + public static function dropTempFile() + { + if (!self::$tmpFile || !file_exists(self::$tmpFile)) + return; + + unlink(self::$tmpFile); + } + + + /*************/ + /* Admin Mgr */ + /*************/ + + public static function getVideos(int $type = 0, int $typeId = 0, $userId = 0, ?int &$nFound = 0) : array + { + /* VideoData + * caption: caption + * date: isodate + * height: ytPreviewImgHeight? + * width: ytPreviewImgWidth? + * id: id + * next: idx || null + * prev: idx || null + * name: ytTitle? + * pending: bool + * status: statusCode + * type: dbType + * typeId: typeId + * user: userName + * url: ytPreviewImg? + * videoType: always 1 + * videoId: videoId + * unique: bool || null + */ + + 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 %and + %lmt + ORDER BY `type`, `typeId`, `pos` ASC', + $where, $userId || $type ? PHP_INT_MAX : 100 + ); + + $num = []; + foreach ($videos as $v) + { + if (empty($num[$v['type']][$v['typeId']])) + $num[$v['type']][$v['typeId']] = 1; + else + $num[$v['type']][$v['typeId']]++; + } + + $nFound = 0; + + // format data to meet requirements of the js + foreach ($videos as $i => &$v) + { + $nFound++; + + $v['date'] = date(Util::$dateFormatInternal, $v['date']); + $v['videoType'] = self::TYPE_YOUTUBE; + + if ($i > 0) + $v['prev'] = $i - 1; + + if (($i + 1) < count($videos)) + $v['next'] = $i + 1; + + // order gives priority for 'status' + if (!($v['flags'] & CC_FLAG_APPROVED)) + { + $v['pending'] = 1; + $v['status'] = self::STATUS_PENDING; + } + else + $v['status'] = self::STATUS_APPROVED; + + if ($v['flags'] & CC_FLAG_STICKY) + { + $v['sticky'] = 1; + $v['status'] = self::STATUS_STICKY; + } + + if ($v['flags'] & CC_FLAG_DELETED) + { + $v['deleted'] = 1; + $v['status'] = self::STATUS_DELETED; + } + + // something todo with massSelect .. am i doing this right? + if ($num[$v['type']][$v['typeId']] == 1) + $v['unique'] = 1; + + if (!$v['user']) + unset($v['user']); + } + + return $videos; + } + + public static function getPages(?bool $all, ?int &$nFound) : array + { + // i GUESS .. vi_getALL ? everything : pending + $nFound = 0; + 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) + { + $ids = []; + foreach ($pages as $row) + if ($row['type'] == $t) + $ids[] = $row['typeId']; + + if (!$ids) + continue; + + $obj = Type::newList($t, [['id', $ids]]); + if (!$obj || $obj->error) + continue; + + foreach ($pages as &$p) + if ($p['type'] == $t) + if ($obj->getEntry($p['typeId'])) + $p['name'] = $obj->getField('name', true); + } + + foreach ($pages as &$p) + { + if (empty($p['name'])) + { + trigger_error('VideoMgr::getPages - video linked to nonexistent type/typeId combination: '.$p['type'].'/'.$p['typeId'], E_USER_NOTICE); + unset($p); + } + else + { + $nFound += $p['count']; + $p['date'] = date(Util::$dateFormatInternal, $p['date']); + } + } + } + + return $pages; + } +} + +?> diff --git a/includes/database.class.php b/includes/database.class.php deleted file mode 100644 index de2e2f91..00000000 --- a/includes/database.class.php +++ /dev/null @@ -1,129 +0,0 @@ -error) - die('Failed to connect to database.'); - - $interface->setErrorHandler(['DB', 'errorHandler']); - $interface->query('SET NAMES ?', 'utf8'); - if ($options['prefix']) - $interface->setIdentPrefix($options['prefix']); - - // disable STRICT_TRANS_TABLES and STRICT_ALL_TABLES off. It prevents usage of implicit default values. - if ($idx == DB_AOWOW) - $interface->query("SET SESSION sql_mode = 'NO_ENGINE_SUBSTITUTION'"); - // disable ONLY_FULL_GROUP_BY (Allows for non-aggregated selects in a group-by query) - else - $interface->query("SET SESSION sql_mode = ''"); - - self::$interfaceCache[$idx] = &$interface; - self::$connectionCache[$idx] = true; - } - - public static function errorHandler($message, $data) - { - if (!error_reporting()) - return; - - $error = "DB ERROR:

\n\n
".print_r($data, true)."
"; - - echo CLI ? strip_tags($error) : $error; - exit; - } - - public static function getDB($idx) - { - return self::$interfaceCache[$idx]; - } - - public static function isConnected($idx) - { - return isset(self::$connectionCache[$idx]); - } - - public static function isConnectable($idx) - { - return isset(self::$optionsCache[$idx]); - } - - private static function safeGetDB($idx) - { - if (!self::isConnected($idx)) - self::connect($idx); - - return self::getDB($idx); - } - - /** - * @static - * @return DbSimple_Mysql - */ - public static function Characters($realm) - { - if (!isset(self::$optionsCache[DB_CHARACTERS.$realm])) - die('Connection info not found for live database of realm #'.$realm.'. Aborted.'); - - return self::safeGetDB(DB_CHARACTERS.$realm); - } - - /** - * @static - * @return DbSimple_Mysql - */ - public static function Auth() - { - return self::safeGetDB(DB_AUTH); - } - - /** - * @static - * @return DbSimple_Mysql - */ - public static function World() - { - return self::safeGetDB(DB_WORLD); - } - - /** - * @static - * @return DbSimple_Mysql - */ - public static function Aowow() - { - return self::safeGetDB(DB_AOWOW); - } - - public static function load($idx, $config) - { - self::$optionsCache[$idx] = $config; - } -} - -?> 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 @@ +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 = <<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 . '
' . $msg . '
' . 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 = '
';
+        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 .= '';
+        }
+
+        $out .= '';
+
+        return Util::jsEscape($out).'
TimeQuery
'.++$i.'.'.round($t * 1000, 2).'ms'.$l.'
∑t:' . round(array_sum(array_column(self::$logs, 1)) * 1000, 2) . 'ms
'; + } + + 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/types/achievement.class.php b/includes/dbtypes/achievement.class.php similarity index 51% rename from includes/types/achievement.class.php rename to includes/dbtypes/achievement.class.php index 75ba06aa..5a665136 100644 --- a/includes/types/achievement.class.php +++ b/includes/dbtypes/achievement.class.php @@ -1,31 +1,28 @@ [['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`'] + 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`'] ); - /* - todo: evaluate TC custom-data-tables: a*_criteria_data should be merged on installation - */ - - public function __construct($conditions = [], $miscData = null) + public function __construct(array $conditions = [], array $miscData = []) { parent::__construct($conditions, $miscData); @@ -33,23 +30,17 @@ class AchievementList extends BaseType return; // post processing - $rewards = DB::World()->select(' - 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(arl6.Subject, "") AS subject_loc6, IFNULL(arl8.Subject, "") AS subject_loc8, - ar.Text AS text_loc0, IFNULL(arl2.Text, "") AS text_loc2, IFNULL(arl3.Text, "") AS text_loc3, IFNULL(arl6.Text, "") AS text_loc6, IFNULL(arl8.Text, "") AS text_loc8 - FROM - achievement_reward ar - LEFT JOIN - achievement_reward_locale arl2 ON arl2.ID = ar.ID AND arl2.Locale = "frFR" - LEFT JOIN - achievement_reward_locale arl3 ON arl3.ID = ar.ID AND arl3.Locale = "deDE" - 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)', + $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" + FROM achievement_reward ar + LEFT JOIN achievement_reward_locale arl2 ON arl2.`ID` = ar.`ID` AND arl2.`Locale` = "frFR" + LEFT JOIN achievement_reward_locale arl3 ON arl3.`ID` = ar.`ID` AND arl3.`Locale` = "deDE" + 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 %in', $this->getFoundIDs() ); @@ -61,45 +52,47 @@ class AchievementList extends BaseType { $_curTpl = array_merge($rewards[$_id], $_curTpl); + $_curTpl['mailTemplate'] = $rewards[$_id]['MailTemplateID']; + 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]['mailTemplate']); + // $mailSrc = new LootByContainer(); + // $mailSrc->getByContainer(Loot::MAIL, $rewards[$_id]['MailTemplateID']); // foreach ($mailSrc->iterate() as $loot) - // $_curTpl['rewards'][] = [TYPE_ITEM, $loot['id']]; + // $_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]; + $_curTpl['rewards'][] = [Type::ITEM, $mr]; } } //"rewards":[[11,137],[3,138]] [type, typeId] if (!empty($_curTpl['ItemID'])) - $_curTpl['rewards'][] = [TYPE_ITEM, $_curTpl['ItemID']]; + $_curTpl['rewards'][] = [Type::ITEM, $_curTpl['ItemID']]; if (!empty($_curTpl['itemExtra'])) - $_curTpl['rewards'][] = [TYPE_ITEM, $_curTpl['itemExtra']]; + $_curTpl['rewards'][] = [Type::ITEM, $_curTpl['itemExtra']]; if (!empty($_curTpl['TitleA'])) - $_curTpl['rewards'][] = [TYPE_TITLE, $_curTpl['TitleA']]; + $_curTpl['rewards'][] = [Type::TITLE, $_curTpl['TitleA']]; if (!empty($_curTpl['TitleH'])) if (empty($_curTpl['TitleA']) || $_curTpl['TitleA'] != $_curTpl['TitleH']) - $_curTpl['rewards'][] = [TYPE_TITLE, $_curTpl['TitleH']]; + $_curTpl['rewards'][] = [Type::TITLE, $_curTpl['TitleH']]; // icon $_curTpl['iconString'] = $_curTpl['iconString'] ?: 'trade_engineering'; } } - public function getJSGlobals($addMask = GLOBALINFO_ANY) + public function getJSGlobals(int $addMask = GLOBALINFO_ANY) : array { $data = []; foreach ($this->iterate() as $__) { if ($addMask & GLOBALINFO_SELF) - $data[TYPE_ACHIEVEMENT][$this->id] = ['icon' => $this->curTpl['iconString'], 'name' => $this->getField('name', true)]; + $data[Type::ACHIEVEMENT][$this->id] = ['icon' => $this->curTpl['iconString'], 'name' => $this->getField('name', true)]; if ($addMask & GLOBALINFO_REWARDS) foreach ($this->curTpl['rewards'] as $_) @@ -109,7 +102,7 @@ class AchievementList extends BaseType return $data; } - public function getListviewData($addInfoMask = 0x0) + public function getListviewData(int $addInfoMask = 0x0) : array { $data = []; @@ -142,12 +135,12 @@ class AchievementList extends BaseType } // only for current template - public function getCriteria() + public function getCriteria() : array { 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->id); + $result = DB::Aowow()->selectAssoc('SELECT * FROM ::achievementcriteria WHERE `refAchievementId` = %i ORDER BY `order` ASC', $this->curTpl['refAchievement'] ?: $this->id); if (!$result) return []; @@ -156,7 +149,7 @@ class AchievementList extends BaseType return $this->criteria[$this->id]; } - public function renderTooltip() + public function renderTooltip() : ?string { $criteria = $this->getCriteria(); $tmp = []; @@ -183,16 +176,12 @@ class AchievementList extends BaseType $qty = (int)$crt['value2']; // we could show them, but the tooltips are cluttered - if (($crt['completionFlags'] & ACHIEVEMENT_CRITERIA_FLAG_HIDDEN) && User::$perms <= 0) + if (($crt['completionFlags'] & ACHIEVEMENT_CRITERIA_FLAG_HIDDEN) && User::isInGroup(U_GROUP_STAFF)) continue; $crtName = Util::localizedString($crt, 'name'); switch ($crt['type']) { - // link to title - todo (low): crosslink - case ACHIEVEMENT_CRITERIA_TYPE_EARNED_PVP_TITLE: - $crtName = Util::ucFirst(Lang::game('title')).Lang::main('colon').$crtName; - break; // link to quest case ACHIEVEMENT_CRITERIA_TYPE_COMPLETE_QUEST: if (!$crtName) @@ -222,7 +211,7 @@ class AchievementList extends BaseType break; } - $criteria .= '- '.Util::jsEscape($crtName); + $criteria .= '- '.$crtName; if ($crt['completionFlags'] & ACHIEVEMENT_CRITERIA_FLAG_MONEY_COUNTER) $criteria .= ' '.Lang::nf($crt['value2' ] / 10000).''; @@ -234,13 +223,13 @@ class AchievementList extends BaseType } $x = '
'; - $x .= Util::jsEscape($name); + $x .= $name; $x .= '
'; if ($description || $criteria) $x .= ''; - $info = explode(' - ', $r['comment']); - $key = $r['flags'] & CON_FLAG_PHP ? strtolower($r['key']) : strtoupper($r['key']); - - // name - if (!empty($info[1])) - $buff .= ''; - else - $buff .= ''; - - // value - if ($r['flags'] & CON_FLAG_TYPE_BOOL) - $buff .= ''; - else if ($r['flags'] & CON_FLAG_OPT_LIST && !empty($info[2])) - { - $buff .= ''; - } - else if ($r['flags'] & CON_FLAG_BITMASK && !empty($info[2])) - { - $buff .= ''; - } - else - $buff .= ''; - - // actions - $buff .= ''; - - return $buff; - } - - protected function generateTitle() {} - protected function generatePath() {} -} - -?> diff --git a/pages/arenateam.php b/pages/arenateam.php deleted file mode 100644 index ff2104e1..00000000 --- a/pages/arenateam.php +++ /dev/null @@ -1,139 +0,0 @@ - 'Profiler.css']]; - - public function __construct($pageCall, $pageParam) - { - $params = array_map('urldecode', explode('.', $pageParam)); - if ($params[0]) - $params[0] = Profiler::urlize($params[0]); - if (isset($params[1])) - $params[1] = Profiler::urlize($params[1]); - - parent::__construct($pageCall, $pageParam); - - if (count($params) == 1 && intval($params[0])) - { - $this->subject = new LocalArenaTeamList(array(['at.id', intval($params[0])])); - if ($this->subject->error) - $this->notFound(); - - header('Location: '.$this->subject->getProfileUrl(), true, 302); - } - else if (count($params) == 3) - { - $this->getSubjectFromUrl($pageParam); - if (!$this->subjectName) - $this->notFound(); - - // 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['cuFlags'] & PROFILER_CU_NEEDS_RESYNC) - { - $this->handleIncompleteData($subject['realmGUID']); - return; - } - - $this->subjectGUID = $subject['id']; - $this->subject = new LocalArenaTeamList(array(['id', $subject['id']])); - if ($this->subject->error) - $this->notFound(); - - $this->profile = $params; - $this->name = sprintf(Lang::profiler('arenaRoster'), $this->subject->getField('name')); - } - // 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 ($team = 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))) - { - $team['realm'] = $this->realmId; - $team['cuFlags'] = PROFILER_CU_NEEDS_RESYNC; - - // create entry from realm with basic info - DB::Aowow()->query('INSERT IGNORE INTO ?_profiler_arena_team (?#) VALUES (?a)', array_keys($team), array_values($team)); - - $this->handleIncompleteData($team['realmGUID']); - } - // 3) does not exist at all - else - $this->notFound(); - } - else - $this->notFound(); - } - - protected function generateTitle() - { - $team = !empty($this->subject) ? $this->subject->getField('name') : $this->subjectName; - $team .= ' ('.$this->realm.' - '.Lang::profiler('regions', $this->region).')'; - - array_unshift($this->title, $team, Util::ucFirst(Lang::profiler('profiler'))); - } - - protected function generateContent() - { - if ($this->doResync) - return; - - $this->addJS('?data=realms.weight-presets&locale='.User::$localeId.'&t='.$_SESSION['dataKey']); - - $this->redButtons[BUTTON_RESYNC] = [$this->subjectGUID, 'arena-team']; - - /****************/ - /* Main Content */ - /****************/ - - - // statistic calculations here - - - /**************/ - /* Extra Tabs */ - /**************/ - - // tab: members - $member = new LocalProfileList(array(['atm.arenaTeamId', $this->subjectGUID])); - if (!$member->error) - { - $this->lvTabs[] = ['profile', array( - 'data' => array_values($member->getListviewData(PROFILEINFO_CHARACTER | PROFILEINFO_ARENA)), - 'sort' => [-15], - 'visibleCols' => ['race', 'classs', 'level', 'talents', 'gearscore', 'rating', 'wins', 'losses'], - 'hiddenCols' => ['guild', 'location'] - )]; - } - } - - public function notFound($title = '', $msg = '') - { - return parent::notFound($title ?: Util::ucFirst(Lang::profiler('profiler')), $msg ?: Lang::profiler('notFound', 'arenateam')); - } - - private function handleIncompleteData($teamGuid) - { - //display empty page and queue status - $newId = Profiler::scheduleResync(TYPE_ARENA_TEAM, $this->realmId, $teamGuid); - - $this->doResync = ['arena-team', $newId]; - $this->initialSync(); - } -} - -?> diff --git a/pages/arenateams.php b/pages/arenateams.php deleted file mode 100644 index 3fdddf26..00000000 --- a/pages/arenateams.php +++ /dev/null @@ -1,116 +0,0 @@ -getSubjectFromUrl($pageParam); - - $this->filterObj = new ArenaTeamListFilter(); - - foreach (Profiler::getRealms() as $idx => $r) - { - if ($this->region && $r['region'] != $this->region) - continue; - - if ($this->realm && $r['name'] != $this->realm) - continue; - - $this->sumSubjects += DB::Characters($idx)->selectCell('SELECT count(*) FROM arena_team'); - } - - parent::__construct($pageCall, $pageParam); - - $this->name = Lang::profiler('arenaTeams'); - $this->subCat = $pageParam ? '='.$pageParam : ''; - } - - protected function generateTitle() - { - if ($this->realm) - array_unshift($this->title, $this->realm,/* CFG_BATTLEGROUP,*/ Lang::profiler('regions', $this->region), Lang::profiler('arenaTeams')); - else if ($this->region) - array_unshift($this->title, Lang::profiler('regions', $this->region), Lang::profiler('arenaTeams')); - else - array_unshift($this->title, Lang::profiler('arenaTeams')); - } - - protected function generateContent() - { - $this->addJS('?data=realms&locale='.User::$localeId.'&t='.$_SESSION['dataKey']); - - $conditions = []; - if (!User::isInGroup(U_GROUP_EMPLOYEE)) - $conditions[] = ['at.rating', 1000, '>']; - - if ($_ = $this->filterObj->getConditions()) - $conditions[] = $_; - - // recreate form selection - $this->filter = $this->filterObj->getForm(); - $this->filter['query'] = isset($_GET['filter']) ? $_GET['filter'] : null; - $this->filter['initData'] = ['type' => 'arenateams']; - - $tabData = array( - 'id' => 'arena-teams', - 'hideCount' => 1, - 'sort' => [-16], - 'extraCols' => ['$Listview.extraCols.members'], - 'visibleCols' => ['rank', 'wins', 'losses', 'rating'], - 'hiddenCols' => ['arenateam', 'guild'], - ); - - if (empty($this->filter['sz'])) - $tabData['visibleCols'][] = 'size'; - - $miscParams = []; - if ($this->realm) - $miscParams['sv'] = $this->realm; - if ($this->region) - $miscParams['rg'] = $this->region; - - $teams = new RemoteArenaTeamList($conditions, $miscParams); - if (!$teams->error) - { - $teams->initializeLocalEntries(); - - $dFields = $teams->hasDiffFields(['faction', 'type']); - if (!($dFields & 0x1)) - $tabData['hiddenCols'][] = 'faction'; - - $tabData['data'] = array_values($teams->getListviewData()); - - // create note if search limit was exceeded - if ($this->filter['query'] && $teams->getMatches() > CFG_SQL_LIMIT_DEFAULT) - { - $tabData['note'] = sprintf(Util::$tryFilteringString, 'LANG.lvnote_arenateamsfound2', $this->sumSubjects, $teams->getMatches()); - $tabData['_truncated'] = 1; - } - else if ($teams->getMatches() > CFG_SQL_LIMIT_DEFAULT) - $tabData['note'] = sprintf(Util::$tryFilteringString, 'LANG.lvnote_arenateamsfound', $this->sumSubjects, 0); - - if ($this->filterObj->error) - $tabData['_errors'] = 1; - } - - $this->lvTabs[] = ['profile', $tabData, 'membersCol']; - - Lang::sort('game', 'cl'); - Lang::sort('game', 'ra'); - } -} - -?> diff --git a/pages/class.php b/pages/class.php deleted file mode 100644 index a890119b..00000000 --- a/pages/class.php +++ /dev/null @@ -1,236 +0,0 @@ -typeId = intVal($id); - - $this->subject = new CharClassList(array(['id', $this->typeId])); - if ($this->subject->error) - $this->notFound(Lang::game('class'), Lang::chrClass('notFound')); - - $this->name = $this->subject->getField('name', true); - } - - protected function generatePath() - { - $this->path[] = $this->typeId; - } - - protected function generateTitle() - { - array_unshift($this->title, $this->name, Util::ucFirst(Lang::game('class'))); - } - - protected function generateContent() - { - $this->addJS('?data=zones&locale='.User::$localeId.'&t='.$_SESSION['dataKey']); - - $infobox = Lang::getInfoBoxForFlags($this->subject->getField('cuFlags')); - $_mask = 1 << ($this->typeId - 1); - $tcClassId = [null, 8, 3, 1, 5, 4, 9, 6, 2, 7, null, 0]; // see TalentCalc.js - - - /***********/ - /* Infobox */ - /***********/ - - // hero class - if ($this->subject->getField('flags') & 0x40) - $infobox[] = '[tooltip=tooltip_heroclass]'.Lang::game('heroClass').'[/tooltip]'; - - // resource - if ($this->typeId == 11) // special Druid case - $infobox[] = Lang::game('resources').Lang::main('colon'). - '[tooltip name=powertype1]'.Lang::game('st', 0).', '.Lang::game('st', 31).', '.Lang::game('st', 2).'[/tooltip][span class=tip tooltip=powertype1]'.Util::ucFirst(Lang::spell('powerTypes', 0)).'[/span], '. - '[tooltip name=powertype2]'.Lang::game('st', 5).', '.Lang::game('st', 8).'[/tooltip][span class=tip tooltip=powertype2]'.Util::ucFirst(Lang::spell('powerTypes', 1)).'[/span], '. - '[tooltip name=powertype8]'.Lang::game('st', 1).'[/tooltip][span class=tip tooltip=powertype8]'.Util::ucFirst(Lang::spell('powerTypes', 3)).'[/span]'; - else if ($this->typeId == 6) // special DK case - $infobox[] = Lang::game('resources').Lang::main('colon').'[span]'.Util::ucFirst(Lang::spell('powerTypes', 5)).', '.Util::ucFirst(Lang::spell('powerTypes', $this->subject->getField('powerType'))).'[/span]'; - else // regular case - $infobox[] = Lang::game('resource').Lang::main('colon').'[span]'.Util::ucFirst(Lang::spell('powerTypes', $this->subject->getField('powerType'))).'[/span]'; - - // roles - $roles = []; - for ($i = 0; $i < 4; $i++) - if ($this->subject->getField('roles') & (1 << $i)) - $roles[] = (count($roles) == 2 ? "\n" : '').Lang::game('_roles', $i); - - if ($roles) - $infobox[] = (count($roles) > 1 ? Lang::game('roles') : Lang::game('role')).Lang::main('colon').implode(', ', $roles); - - // specs - $specList = []; - $skills = new SkillList(array(['id', $this->subject->getField('skills')])); - foreach ($skills->iterate() as $k => $__) - $specList[$k] = '[icon name='.$skills->getField('iconString').'][url=?spells=7.'.$this->typeId.'.'.$k.']'.$skills->getField('name', true).'[/url][/icon]'; - - if ($specList) - $infobox[] = Lang::game('specs').Lang::main('colon').'[ul][li]'.implode('[/li][li]', $specList).'[/li][/ul]'; - - - /****************/ - /* Main Content */ - /****************/ - - $this->infobox = '[ul][li]'.implode('[/li][li]', $infobox).'[/li][/ul]'; - $this->expansion = Util::$expansionString[$this->subject->getField('expansion')]; - $this->headIcons = ['class_'.strtolower($this->subject->getField('fileString'))]; - $this->redButtons = array( - BUTTON_LINKS => ['type' => $this->type, 'typeId' => $this->typeId], - BUTTON_WOWHEAD => true, - BUTTON_TALENT => ['href' => '?talent#'.Util::$tcEncoding[$tcClassId[$this->typeId] * 3], 'pet' => false], - BUTTON_FORUM => false // todo (low): CFG_BOARD_URL + X - ); - - - /**************/ - /* Extra Tabs */ - /**************/ - - // Tab: Spells (grouped) - // '$LANG.tab_armorproficiencies', - // '$LANG.tab_weaponskills', - // '$LANG.tab_glyphs', - // '$LANG.tab_abilities', - // '$LANG.tab_talents', - $conditions = array( - ['s.typeCat', [-13, -11, -2, 7]], - [['s.cuFlags', (SPELL_CU_TRIGGERED | CUSTOM_EXCLUDE_FOR_LISTVIEW), '&'], 0], - [ - 'OR', - ['s.reqClassMask', $_mask, '&'], // Glyphs, Proficiencies - ['s.skillLine1', $this->subject->getField('skills')], // Abilities / Talents - ['AND', ['s.skillLine1', 0, '>'], ['s.skillLine2OrMask', $this->subject->getField('skills')]] - ], - [ // last rank or unranked - 'OR', - ['s.cuFlags', SPELL_CU_LAST_RANK, '&'], - ['s.rankNo', 0] - ], - CFG_SQL_LIMIT_NONE - ); - - $genSpells = new SpellList($conditions); - if (!$genSpells->error) - { - $this->extendGlobalData($genSpells->getJSGlobals(GLOBALINFO_SELF | GLOBALINFO_RELATED)); - - $this->lvTabs[] = ['spell', array( - 'data' => array_values($genSpells->getListviewData()), - 'id' => 'spells', - 'name' => '$LANG.tab_spells', - 'visibleCols' => ['level', 'schools', 'type', 'classes'], - 'hiddenCols' => ['reagents', 'skill'], - 'sort' => ['-level', 'type', 'name'], - 'computeDataFunc' => '$Listview.funcBox.initSpellFilter', - 'onAfterCreate' => '$Listview.funcBox.addSpellIndicator' - )]; - } - - // Tab: Items (grouped) - $conditions = array( - ['requiredClass', 0, '>'], - ['requiredClass', $_mask, '&'], - [['requiredClass', CLASS_MASK_ALL, '&'], CLASS_MASK_ALL, '!'], - ['itemset', 0], // hmm, do or dont..? - CFG_SQL_LIMIT_NONE - ); - - $items = new ItemList($conditions); - if (!$items->error) - { - $this->extendGlobalData($items->getJSGlobals()); - - $hiddenCols = null; - if ($items->hasDiffFields(['requiredRace'])) - $hiddenCols = ['side']; - - $this->lvTabs[] = ['item', array( - 'data' => array_values($items->getListviewData()), - 'id' => 'items', - 'name' => '$LANG.tab_items', - 'visibleCols' => ['dps', 'armor', 'slot'], - 'hiddenCols' => $hiddenCols, - 'computeDataFunc' => '$Listview.funcBox.initSubclassFilter', - 'onAfterCreate' => '$Listview.funcBox.addSubclassIndicator', - 'note' => sprintf(Util::$filterResultString, '?items&filter=cr=152;crs='.$this->typeId.';crv=0'), - '_truncated' => 1 - )]; - } - - // Tab: Quests - $conditions = array( - ['reqClassMask', $_mask, '&'], - [['reqClassMask', CLASS_MASK_ALL, '&'], CLASS_MASK_ALL, '!'] - ); - - $quests = new QuestList($conditions); - if (!$quests->error) - { - $this->extendGlobalData($quests->getJSGlobals()); - - $this->lvTabs[] = ['quest', array( - 'data' => array_values($quests->getListviewData()), - 'sort' => ['reqlevel', 'name'] - )]; - } - - // Tab: Itemsets - $sets = new ItemsetList(array(['classMask', $_mask, '&'])); - if (!$sets->error) - { - $this->extendGlobalData($sets->getJSGlobals(GLOBALINFO_SELF)); - - $this->lvTabs[] = ['itemset', array( - 'data' => array_values($sets->getListviewData()), - 'note' => sprintf(Util::$filterResultString, '?itemsets&filter=cl='.$this->typeId), - 'hiddenCols' => ['classes'], - 'sort' => ['-level', 'name'] - )]; - } - - // Tab: Trainer - $conditions = array( - ['npcflag', 0x30, '&'], // is trainer - ['trainerType', 0], // trains class spells - ['trainerClass', $this->typeId] - ); - - $trainer = new CreatureList($conditions); - if (!$trainer->error) - { - $this->lvTabs[] = ['creature', array( - 'data' => array_values($trainer->getListviewData()), - 'id' => 'trainers', - 'name' => '$LANG.tab_trainers' - )]; - } - - // Tab: Races - $races = new CharRaceList(array(['classMask', $_mask, '&'])); - if (!$races->error) - $this->lvTabs[] = ['race', ['data' => array_values($races->getListviewData())]]; - } -} - -?> diff --git a/pages/classes.php b/pages/classes.php deleted file mode 100644 index 9b8a2448..00000000 --- a/pages/classes.php +++ /dev/null @@ -1,41 +0,0 @@ -name = Util::ucFirst(Lang::game('classes')); - } - - protected function generateContent() - { - $classes = new CharClassList(); - if (!$classes->error) - $this->lvTabs[] = ['class', ['data' => array_values($classes->getListviewData())]]; - } - - protected function generateTitle() - { - array_unshift($this->title, Util::ucFirst(Lang::game('classes'))); - } - - protected function generatePath() {} -} - -?> diff --git a/pages/compare.php b/pages/compare.php deleted file mode 100644 index e628b3d5..00000000 --- a/pages/compare.php +++ /dev/null @@ -1,105 +0,0 @@ - 'Summary.css']]; - - protected $summary = []; - protected $cmpItems = []; - - private $compareString = ''; - - public function __construct($pageCall, $__) - { - parent::__construct($pageCall, $__); - - // prefer $_GET over $_COOKIE - if (!empty($_GET['compare'])) - $this->compareString = $_GET['compare']; - else if (!empty($_COOKIE['compare_groups'])) - $this->compareString = urldecode($_COOKIE['compare_groups']); - - $this->name = Lang::main('compareTool'); - } - - protected function generateContent() - { - // add conditional js - $this->addJS('?data=weight-presets.gems.enchants.itemsets&locale='.User::$localeId.'&t='.$_SESSION['dataKey']); - - $this->summary = array( - 'template' => 'compare', - 'id' => 'compare', - 'parent' => 'compare-generic' - ); - - if (!$this->compareString) - return; - - $sets = explode(';', $this->compareString); - $items = $outSet = []; - foreach ($sets as $set) - { - $itemSting = explode(':', $set); - $outString = []; - foreach ($itemSting as $substring) - { - $params = explode('.', $substring); - $items[] = (int)$params[0]; - while (sizeof($params) < 7) - $params[] = 0; - - $outString[] = $params; - } - - $outSet[] = $outString; - } - - $this->summary['groups'] = $outSet; - - $iList = new ItemList(array(['i.id', $items])); - $data = $iList->getListviewData(ITEMINFO_SUBITEMS | ITEMINFO_JSON); - - foreach ($iList->iterate() as $itemId => $__) - { - if (empty($data[$itemId])) - continue; - - if (!empty($data[$itemId]['subitems'])) - foreach ($data[$itemId]['subitems'] as &$si) - $si['enchantment'] = implode(', ', $si['enchantment']); - - $this->cmpItems[$itemId] = [ - 'name_'.User::$localeString => $iList->getField('name', true), - 'quality' => $iList->getField('quality'), - 'icon' => $iList->getField('iconString'), - 'jsonequip' => $data[$itemId] - ]; - } - } - - protected function generateTitle() - { - array_unshift($this->title, $this->name); - } - - protected function generatePath() {} -} - -?> diff --git a/pages/currencies.php b/pages/currencies.php deleted file mode 100644 index 33179b6a..00000000 --- a/pages/currencies.php +++ /dev/null @@ -1,57 +0,0 @@ -getCategoryFromUrl($pageParam); - - parent::__construct($pageCall, $pageParam); - - $this->name = Util::ucFirst(Lang::game('currencies')); - } - - protected function generateContent() - { - $conditions = []; - - if (!User::isInGroup(U_GROUP_EMPLOYEE)) - $conditions[] = [['cuFlags', CUSTOM_EXCLUDE_FOR_LISTVIEW, '&'], 0]; - - if ($this->category) - $conditions[] = ['category', (int)$this->category[0]]; - - $money = new CurrencyList($conditions); - $this->lvTabs[] = ['currency', ['data' => array_values($money->getListviewData())]]; - } - - protected function generateTitle() - { - array_unshift($this->title, $this->name); - if ($this->category) - array_unshift($this->title, Lang::currency('cat', $this->category[0])); - } - - protected function generatePath() - { - if ($this->category) - $this->path[] = $this->category[0]; - } -} - -?> diff --git a/pages/currency.php b/pages/currency.php deleted file mode 100644 index beea3502..00000000 --- a/pages/currency.php +++ /dev/null @@ -1,254 +0,0 @@ -mode == CACHE_TYPE_TOOLTIP && isset($_GET['domain'])) - Util::powerUseLocale($_GET['domain']); - - $this->typeId = intVal($id); - - $this->subject = new CurrencyList(array(['id', $this->typeId])); - if ($this->subject->error) - $this->notFound(); - - $this->name = $this->subject->getField('name', true); - } - - protected function generatePath() - { - $this->path[] = $this->subject->getField('category'); - } - - protected function generateTitle() - { - array_unshift($this->title, $this->subject->getField('name', true), Util::ucFirst(Lang::game('currency'))); - } - - protected function generateContent() - { - $this->addJS('?data=zones&locale='.User::$localeId.'&t='.$_SESSION['dataKey']); - - $_itemId = $this->subject->getField('itemId'); - - /***********/ - /* Infobox */ - /**********/ - - $infobox = Lang::getInfoBoxForFlags(intval($this->subject->getField('cuFlags'))); - - if ($_ = $this->subject->getField('cap')) - $infobox[] = Lang::currency('cap').Lang::main('colon').Lang::nf($_); - - /****************/ - /* Main Content */ - /****************/ - - $this->infobox = $infobox ? '[ul][li]'.implode('[/li][li]', $infobox).'[/li][/ul]' : null; - $this->name = $this->subject->getField('name', true); - $this->headIcons = $this->typeId == 104 ? ['inv_bannerpvp_02', 'inv_bannerpvp_01'] : [$this->subject->getField('iconString')]; - $this->redButtons = array( - BUTTON_WOWHEAD => true, - BUTTON_LINKS => true - ); - - if ($_ = $this->subject->getField('description', true)) - $this->extraText = $_; - - /**************/ - /* Extra Tabs */ - /**************/ - - if ($this->typeId != 103 && $this->typeId != 104) // honor && arena points are not handled as items - { - // tabs: this currency is contained in.. - $lootTabs = new Loot(); - - if ($lootTabs->getByItem($_itemId)) - { - $this->extendGlobalData($lootTabs->jsGlobals); - - foreach ($lootTabs->iterate() as list($file, $tabData)) - $this->lvTabs[] = [$file, $tabData]; - } - - // tab: sold by - $itemObj = new ItemList(array(['id', $_itemId])); - if (!empty($itemObj->getExtendedCost()[$_itemId])) - { - $vendors = $itemObj->getExtendedCost()[$_itemId]; - $this->extendGlobalData($itemObj->getJSGlobals(GLOBALINFO_SELF | GLOBALINFO_RELATED)); - - $soldBy = new CreatureList(array(['id', array_keys($vendors)])); - if (!$soldBy->error) - { - $sbData = $soldBy->getListviewData(); - $extraCols = ['$Listview.extraCols.stock', "\$Listview.funcBox.createSimpleCol('stack', 'stack', '10%', 'stack')", '$Listview.extraCols.cost']; - $holidays = []; - - foreach ($sbData as $k => &$row) - { - $items = []; - $tokens = []; - foreach ($vendors[$k] as $id => $qty) - { - if (is_string($id)) - continue; - - if ($id > 0) - $tokens[] = [$id, $qty]; - else if ($id < 0) - $items[] = [-$id, $qty]; - } - - if ($vendors[$k]['event']) - { - if (count($extraCols) == 3) // not already pushed - $extraCols[] = '$Listview.extraCols.condition'; - - $this->extendGlobalIds(TYPE_WORLDEVENT, $vendors[$k]['event']); - $row['condition'][0][$this->typeId][] = [[CND_ACTIVE_EVENT, $vendors[$k]['event']]]; - } - - $row['stock'] = $vendors[$k]['stock']; - $row['stack'] = $itemObj->getField('buyCount'); - $row['cost'] = array( - $itemObj->getField('buyPrice'), - $items ? $items : null, - $tokens ? $tokens : null - ); - } - - $this->lvTabs[] = ['creature', array( - 'data' => array_values($sbData), - 'name' => '$LANG.tab_soldby', - 'id' => 'sold-by-npc', - 'extraCols' => $extraCols, - 'hiddenCols' => ['level', 'type'] - )]; - } - } - } - - // tab: created by (spell) [for items its handled in Loot::getByContainer()] - if ($this->typeId == 104) - { - $createdBy = new SpellList(array(['effect1Id', 45], ['effect2Id', 45], ['effect3Id', 45], 'OR')); - if (!$createdBy->error) - { - $this->extendGlobalData($createdBy->getJSGlobals(GLOBALINFO_SELF | GLOBALINFO_RELATED)); - - $tabData = array( - 'data' => array_values($createdBy->getListviewData()), - 'name' => '$LANG.tab_createdby', - 'id' => 'created-by', - ); - - if ($createdBy->hasSetFields(['reagent1'])) - $tabData['visibleCols'] = ['reagents']; - - $this->lvTabs[] = ['spell', $tabData]; - } - } - - // tab: currency for - if ($this->typeId == 103) - { - $n = '?items&filter=cr=145;crs=1;crv=0'; - $w = 'reqArenaPoints > 0'; - } - else if ($this->typeId == 104) - { - $n = '?items&filter=cr=144;crs=1;crv=0'; - $w = 'reqHonorPoints > 0'; - } - else - { - $n = in_array($this->typeId, [42, 61, 81, 241, 121, 122, 123, 125, 126, 161, 201, 101, 102, 221, 301, 341]) ? '?items&filter=cr=158;crs='.$_itemId.';crv=0' : null; - $w = 'reqItemId1 = '.$_itemId.' OR reqItemId2 = '.$_itemId.' OR reqItemId3 = '.$_itemId.' OR reqItemId4 = '.$_itemId.' OR reqItemId5 = '.$_itemId; - } - - $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) : []; - if ($boughtBy) - { - $boughtBy = new ItemList(array(['id', $boughtBy])); - if (!$boughtBy->error) - { - $tabData = array( - 'data' => array_values($boughtBy->getListviewData(ITEMINFO_VENDOR, [TYPE_CURRENCY => $this->typeId])), - 'name' => '$LANG.tab_currencyfor', - 'id' => 'currency-for', - 'extraCols' => ["\$Listview.funcBox.createSimpleCol('stack', 'stack', '10%', 'stack')"], - ); - - if ($boughtBy->getMatches() > CFG_SQL_LIMIT_DEFAULT) - $tabData['note'] = sprintf(Util::$filterResultString, $n); - - $this->lvTabs[] = ['item', $tabData]; - - $this->extendGlobalData($boughtBy->getJSGlobals(GLOBALINFO_SELF | GLOBALINFO_RELATED)); - } - } - } - - protected function generateTooltip($asError = false) - { - if ($asError) - return '$WowheadPower.registerCurrency('.$this->typeId.', '.User::$localeId.', {});'; - - $x = '$WowheadPower.registerCurrency('.$this->typeId.', '.User::$localeId.", {\n"; - $x .= "\tname_".User::$localeString.": '".Util::jsEscape($this->subject->getField('name', true))."',\n"; - $x .= "\ticon: '".rawurlencode($this->subject->getField('iconString', true, true))."',\n"; - $x .= "\ttooltip_".User::$localeString.": '".$this->subject->renderTooltip()."'\n"; - $x .= "});"; - - return $x; - } - - public function display($override = '') - { - if ($this->mode != CACHE_TYPE_TOOLTIP) - return parent::display($override); - - if (!$this->loadCache($tt)) - { - $tt = $this->generateTooltip(); - $this->saveCache($tt); - } - - header('Content-type: application/x-javascript; charset=utf-8'); - die($tt); - } - - public function notFound($title = '', $msg = '') - { - if ($this->mode != CACHE_TYPE_TOOLTIP) - return parent::notFound($title ?: Lang::game('currency'), $msg ?: Lang::currency('notFound')); - - header('Content-type: application/x-javascript; charset=utf-8'); - echo $this->generateTooltip(true); - exit(); - } -} - -?> diff --git a/pages/emote.php b/pages/emote.php deleted file mode 100644 index d70a0c80..00000000 --- a/pages/emote.php +++ /dev/null @@ -1,128 +0,0 @@ -typeId = intVal($id); - - $this->subject = new EmoteList(array(['id', $this->typeId])); - if ($this->subject->error) - $this->notFound(Util::ucFirst(Lang::game('emote')), Lang::emote('notFound')); - - $this->name = Util::ucFirst($this->subject->getField('cmd')); - } - - protected function generatePath() { } - - protected function generateTitle() - { - array_unshift($this->title, $this->name, Util::ucFirst(Lang::game('emote'))); - } - - protected function generateContent() - { - /***********/ - /* Infobox */ - /***********/ - - $infobox = Lang::getInfoBoxForFlags($this->subject->getField('cuFlags')); - - // has Animation - if ($this->subject->getField('isAnimated')) - $infobox[] = Lang::emote('isAnimated'); - - /****************/ - /* Main Content */ - /****************/ - - $text = ''; - if ($aliasses = DB::Aowow()->selectCol('SELECT command FROM ?_emotes_aliasses WHERE id = ?d AND locales & ?d', $this->typeId, 1 << User::$localeId)) - { - $text .= '[h3]'.Lang::emote('aliases').'[/h3][ul]'; - foreach ($aliasses as $a) - $text .= '[li]/'.$a.'[/li]'; - - $text .= '[/ul][br][br]'; - } - - $texts = []; - if ($_ = $this->subject->getField('self', true)) - $texts[Lang::emote('self')] = $_; - - if ($_ = $this->subject->getField('target', true)) - $texts[Lang::emote('target')] = $_; - - if ($_ = $this->subject->getField('noTarget', true)) - $texts[Lang::emote('noTarget')] = $_; - - if (!$texts) - $text .= '[div][i class=q0]'.Lang::emote('noText').'[/i][/div]'; - else - foreach ($texts as $h => $t) - $text .= '[pad][b]'.$h.'[/b][ul][li][span class=s4]'.preg_replace('/%\d?\$?s/', '<'.Util::ucFirst(Lang::main('name')).'>', $t).'[/span][/li][/ul]'; - - $this->extraText = $text; - $this->infobox = $infobox ? '[ul][li]'.implode('[/li][li]', $infobox).'[/li][/ul]' : null; - $this->redButtons = array( - BUTTON_LINKS => ['type' => $this->type, 'typeId' => $this->typeId], - BUTTON_WOWHEAD => false - ); - - /**************/ - /* Extra Tabs */ - /**************/ - - // tab: achievement - $condition = array( - ['ac.type', ACHIEVEMENT_CRITERIA_TYPE_DO_EMOTE], - ['ac.value1', $this->typeId], - ); - $acv = new AchievementList($condition); - - $this->lvTabs[] = ['achievement', ['data' => array_values($acv->getListviewData())]]; - - $this->extendGlobalData($acv->getJsGlobals()); - - // tab: sound - if ($em = DB::Aowow()->select('SELECT soundId AS ARRAY_KEY, BIT_OR(1 << (raceId - 1)) AS raceMask, BIT_OR(1 << (gender - 1)) AS gender FROM aowow_emotes_sounds WHERE emoteId = ?d GROUP BY soundId', $this->typeId)) - { - $sounds = new SoundList(array(['id', array_keys($em)])); - if (!$sounds->error) - { - $this->extendGlobalData($sounds->getJSGlobals(GLOBALINFO_SELF)); - $data = $sounds->getListviewData(); - foreach($data as $id => &$d) - { - $d['races'] = $em[$id]['raceMask']; - $d['gender'] = $em[$id]['gender']; - } - - $this->lvTabs[] = ['sound', array( - 'data' => array_values($data), - // gender races - 'extraCols' => ['$Listview.templates.title.columns[1]', '$Listview.templates.classs.columns[1]'] - )]; - } - } - } -} - -?> diff --git a/pages/emotes.php b/pages/emotes.php deleted file mode 100644 index 753dcd62..00000000 --- a/pages/emotes.php +++ /dev/null @@ -1,44 +0,0 @@ -name = Util::ucFirst(Lang::game('emotes')); - } - - protected function generateContent() - { - $tabData = array( - 'data' => array_values((new EmoteList())->getListviewData()), - 'name' => Util::ucFirst(Lang::game('emotes')) - ); - - $this->lvTabs[] = ['emote', $tabData, 'emote']; - } - - protected function generateTitle() - { - array_unshift($this->title, $this->name); - } - - protected function generatePath() { } -} - -?> diff --git a/pages/enchantment.php b/pages/enchantment.php deleted file mode 100644 index 6dcf65f0..00000000 --- a/pages/enchantment.php +++ /dev/null @@ -1,324 +0,0 @@ -typeId = intVal($id); - - $this->subject = new EnchantmentList(array(['id', $this->typeId])); - if ($this->subject->error) - $this->notFound(Util::ucFirst(Lang::game('enchantment')), Lang::enchantment('notFound')); - - $this->extendGlobalData($this->subject->getJSGlobals()); - - $this->name = Util::ucFirst($this->subject->getField('name', true)); - } - - private function getDistinctType() - { - $type = 0; - for ($i = 1; $i < 4; $i++) - { - if ($_ = $this->subject->getField('type'.$i)) - { - if ($type) // already set - return 0; - else - $type = $_; - } - } - - return $type; - } - - protected function generateContent() - { - /***********/ - /* Infobox */ - /***********/ - - $infobox = Lang::getInfoBoxForFlags($this->subject->getField('cuFlags')); - - // reqLevel - if ($_ = $this->subject->getField('requiredLevel')) - $infobox[] = sprintf(Lang::game('reqLevel'), $_); - - // reqskill - if ($_ = $this->subject->getField('skillLine')) - { - $this->extendGlobalIds(TYPE_SKILL, $_); - - $foo = sprintf(Lang::game('requires'), ' [skill='.$_.']'); - if ($_ = $this->subject->getField('skillLevel')) - $foo .= ' ('.$_.')'; - - $infobox[] = $foo; - } - - - /****************/ - /* Main Content */ - /****************/ - - - $this->infobox = $infobox ? '[ul][li]'.implode('[/li][li]', $infobox).'[/li][/ul]' : null; - $this->redButtons = array( - BUTTON_LINKS => ['type' => $this->type, 'typeId' => $this->typeId], - BUTTON_WOWHEAD => false - ); - - $this->effects = []; - // 3 effects - for ($i = 1; $i < 4; $i++) - { - $_ty = $this->subject->getField('type'.$i); - $_qty = $this->subject->getField('amount'.$i); - $_obj = $this->subject->getField('object'.$i); - - switch ($_ty) - { - case 1: - case 3: - case 7: - $sArr = $this->subject->getField('spells')[$i]; - $spl = $this->subject->getRelSpell($sArr[0]); - $this->effects[$i]['name'] = User::isInGroup(U_GROUP_EMPLOYEE) ? sprintf(Util::$dfnString, 'Type: '.$_ty, Lang::item('trigger', $sArr[1])) : Lang::item('trigger', $sArr[1]); - $this->effects[$i]['proc'] = $sArr[3]; - $this->effects[$i]['value'] = $_qty ?: null; - $this->effects[$i]['icon'] = array( - 'name' => !$spl ? Util::ucFirst(Lang::game('spell')).' #'.$sArr[0] : Util::localizedString($spl, 'name'), - 'id' => $sArr[0], - 'count' => $sArr[2] - ); - break; - case 5: - if ($_obj < 2) // [mana, health] are on [0, 1] respectively and are expected on [1, 2] .. - $_obj++; // 0 is weaponDmg .. ehh .. i messed up somewhere - - $this->effects[$i]['tip'] = [$_obj, Game::$itemMods[$_obj]]; - // DO NOT BREAK! - case 2: - case 6: - case 8: - case 4: - $this->effects[$i]['name'] = User::isInGroup(U_GROUP_EMPLOYEE) ? sprintf(Util::$dfnString, 'Type: '.$_ty, Lang::enchantment('types', $_ty)) : Lang::enchantment('types', $_ty); - $this->effects[$i]['value'] = $_qty; - if ($_ty == 4) - $this->effects[$i]['name'] .= Lang::main('colon').'('.(User::isInGroup(U_GROUP_EMPLOYEE) ? sprintf(Util::$dfnString, 'Object: '.$_obj, Lang::getMagicSchools(1 << $_obj)) : Lang::getMagicSchools(1 << $_obj)).')'; - } - } - - // activation conditions - if ($_ = $this->subject->getField('conditionId')) - { - $x = ''; - - if ($gemCnd = DB::Aowow()->selectRow('SELECT * FROM ?_itemenchantmentcondition WHERE id = ?d', $_)) - { - for ($i = 1; $i < 6; $i++) - { - if (!$gemCnd['color'.$i]) - continue; - - $fiColors = function ($idx) - { - $foo = ''; - switch ($idx) - { - case 2: $foo = '0:3:5'; break; // red - case 3: $foo = '2:4:5'; break; // yellow - case 4: $foo = '1:3:4'; break; // blue - } - - return $foo; - }; - - $bLink = $gemCnd['color'.$i] ? ''.Lang::item('gemColors', $gemCnd['color'.$i] - 1).'' : ''; - $cLink = $gemCnd['cmpColor'.$i] ? ''.Lang::item('gemColors', $gemCnd['cmpColor'.$i] - 1).'' : ''; - - switch ($gemCnd['comparator'.$i]) - { - case 2: // requires less than ( || ) gems - case 5: // requires at least than ( || ) gems - $sp = (int)$gemCnd['value'.$i] > 1; - $x .= ''.Lang::achievement('reqNumCrt').' '.Lang::item('gemConditions', $gemCnd['comparator'.$i], [$gemCnd['value'.$i], $bLink]).'
'; - break; - case 3: // requires more than ( || ) gems - $link = ''.Lang::item('gemColors', $gemCnd['cmpColor'.$i] - 1).''; - $x .= ''.Lang::achievement('reqNumCrt').' '.Lang::item('gemConditions', $gemCnd['comparator'.$i], [$bLink, $cLink]).'
'; - break; - } - } - } - - $this->activateCondition = $x; - } - - /**************/ - /* Extra Tabs */ - /**************/ - - // used by gem - $gemList = new ItemList(array(['gemEnchantmentId', $this->typeId])); - if (!$gemList->error) - { - $this->lvTabs[] = ['item', array( - 'data' => array_values($gemList->getListviewData()), - 'name' => '$LANG.tab_usedby + \' \' + LANG.gems', - 'id' => 'used-by-gem', - )]; - - $this->extendGlobalData($gemList->getJsGlobals()); - } - - // used by socket bonus - $socketsList = new ItemList(array(['socketBonus', $this->typeId])); - if (!$socketsList->error) - { - $this->lvTabs[] = ['item', array( - 'data' => array_values($socketsList->getListviewData()), - 'name' => '$LANG.tab_usedby + \' \' + \''.Lang::item('socketBonus').'\'', - 'id' => 'used-by-socketbonus', - )]; - - $this->extendGlobalData($socketsList->getJsGlobals()); - } - - // used by spell - // used by useItem - $cnd = array( - 'OR', - ['AND', ['effect1Id', [53, 54, 156, 92]], ['effect1MiscValue', $this->typeId]], - ['AND', ['effect2Id', [53, 54, 156, 92]], ['effect2MiscValue', $this->typeId]], - ['AND', ['effect3Id', [53, 54, 156, 92]], ['effect3MiscValue', $this->typeId]], - ); - $spellList = new SpellList($cnd); - if (!$spellList->error) - { - $spellData = $spellList->getListviewData(); - $this->extendGlobalData($spellList->getJsGlobals()); - - $spellIds = $spellList->getFoundIDs(); - $conditions = array( - 'OR', // [use, useUndelayed] - ['AND', ['spellTrigger1', [0, 5]], ['spellId1', $spellIds]], - ['AND', ['spellTrigger2', [0, 5]], ['spellId2', $spellIds]], - ['AND', ['spellTrigger3', [0, 5]], ['spellId3', $spellIds]], - ['AND', ['spellTrigger4', [0, 5]], ['spellId4', $spellIds]], - ['AND', ['spellTrigger5', [0, 5]], ['spellId5', $spellIds]] - ); - - $ubItems = new ItemList($conditions); - if (!$ubItems->error) - { - $this->lvTabs[] = ['item', array( - 'data' => array_values($ubItems->getListviewData()), - 'name' => '$LANG.tab_usedby + \' \' + LANG.types[3][0]', - 'id' => 'used-by-item', - )]; - - $this->extendGlobalData($ubItems->getJSGlobals(GLOBALINFO_SELF)); - } - - // remove found spells if they are used by an item - if (!$ubItems->error) - { - foreach ($spellList->iterate() as $sId => $__) - { - // if Perm. Enchantment has a createItem its a Scroll of Enchantment (display both) - for ($i = 1; $i < 4; $i++) - if ($spellList->getField('effect'.$i.'Id') == 53 && $spellList->getField('effect'.$i.'CreateItemId')) - continue 2; - - foreach ($ubItems->iterate() as $__) - { - for ($i = 1; $i < 6; $i++) - { - if ($ubItems->getField('spellId'.$i) == $sId) - { - unset($spellData[$sId]); - break 2; - } - } - } - } - } - - $this->lvTabs[] = ['spell', array( - 'data' => array_values($spellData), - 'name' => '$LANG.tab_usedby + \' \' + LANG.types[6][0]', - 'id' => 'used-by-spell', - )]; - } - - // 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', - $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))) - { - $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_SQL_LIMIT_NONE, ['randomEnchant', array_keys($randIds)])); - if (!$randItems->error) - { - $data = $randItems->getListviewData(); - foreach ($randItems->iterate() as $iId => $__) - { - $re = $randItems->getField('randomEnchant'); - - $data[$iId]['percent'] = $iet[abs($re)]['chance']; - $data[$iId]['count'] = 1; // expected by js or the pct-col becomes unsortable - $data[$iId]['rel'] = 'rand='.$ire[$iet[abs($re)]['ench']]['id']; - $data[$iId]['name'] .= ' '.Util::localizedString($ire[$iet[abs($re)]['ench']], 'name'); - } - - $this->lvTabs[] = ['item', array( - 'data' => array_values($data), - 'id' => 'used-by-rand', - 'name' => '$LANG.tab_usedby + \' \' + \''.Lang::item('_rndEnchants').'\'', - 'extraCols' => ['$Listview.extraCols.percent'] - )]; - - $this->extendGlobalData($randItems->getJSGlobals(GLOBALINFO_SELF)); - } - } - } - } - - protected function generateTitle() - { - array_unshift($this->title, $this->name, Util::ucFirst(Lang::game('enchantment'))); - } - - protected function generatePath() - { - if ($_ = $this->getDistinctType()) - $this->path[] = $_; - } -} - -?> diff --git a/pages/enchantments.php b/pages/enchantments.php deleted file mode 100644 index 5f14e89c..00000000 --- a/pages/enchantments.php +++ /dev/null @@ -1,108 +0,0 @@ -getCategoryFromUrl($pageParam);; - $this->filterObj = new EnchantmentListFilter(false, ['parentCats' => $this->category]); - - parent::__construct($pageCall, $pageParam); - - $this->name = Util::ucFirst(Lang::game('enchantments')); - $this->subCat = $pageParam !== null ? '='.$pageParam : ''; - } - - protected function generateContent() - { - $tabData = array( - 'data' => [], - 'name' => Util::ucFirst(Lang::game('enchantments')) - ); - - $conditions = []; - - if (!User::isInGroup(U_GROUP_EMPLOYEE)) - $conditions[] = [['cuFlags', CUSTOM_EXCLUDE_FOR_LISTVIEW, '&'], 0]; - - if ($_ = $this->filterObj->getConditions()) - $conditions[] = $_; - - $ench = new EnchantmentList($conditions); - - $tabData['data'] = array_values($ench->getListviewData()); - $this->extendGlobalData($ench->getJSGlobals()); - - // recreate form selection - $this->filter = $this->filterObj->getForm(); - $this->filter['query'] = isset($_GET['filter']) ? $_GET['filter'] : NULL; - $this->filter['initData'] = ['init' => 'enchantments']; - - if ($x = $this->filterObj->getSetCriteria()) - $this->filter['initData']['sc'] = $x; - - $xCols = $this->filterObj->getExtraCols(); - foreach (Util::$itemFilter as $fiId => $str) - if (array_column($tabData['data'], $str)) - $xCols[] = $fiId; - - if (array_column($tabData['data'], 'dmg')) - $xCols[] = 34; - - if ($xCols) - $this->filter['initData']['ec'] = array_values(array_unique($xCols)); - - if ($xCols) - $tabData['extraCols'] = '$fi_getExtraCols(fi_extraCols, 0, 0)'; - - if ($ench->getMatches() > CFG_SQL_LIMIT_DEFAULT) - { - $tabData['note'] = sprintf(Util::$tryFilteringString, 'LANG.lvnote_enchantmentsfound', $ench->getMatches(), CFG_SQL_LIMIT_DEFAULT); - $tabData['_truncated'] = 1; - } - - if (array_filter(array_column($tabData['data'], 'spells'))) - $tabData['visibleCols'] = ['trigger']; - - if (!$ench->hasSetFields(['skillLine'])) - $tabData['hiddenCols'] = ['skill']; - - if ($this->filterObj->error) - $tabData['_errors'] = '$1'; - - $this->lvTabs[] = ['enchantment', $tabData, 'enchantment']; - } - - protected function generateTitle() - { - $form = $this->filterObj->getForm('form'); - if (!empty($form['ty']) && intVal($form['ty']) && $form['ty'] > 0 && $form['ty'] < 9) - array_unshift($this->title, Lang::enchantment('types', $form['ty'])); - - array_unshift($this->title, $this->name); - } - - protected function generatePath() - { - $form = $this->filterObj->getForm('form'); - if (isset($form['ty']) && !is_array($form['ty'])) - $this->path[] = $form['ty']; - } -} - -?> diff --git a/pages/event.php b/pages/event.php deleted file mode 100644 index 66b17f75..00000000 --- a/pages/event.php +++ /dev/null @@ -1,375 +0,0 @@ -mode == CACHE_TYPE_TOOLTIP && isset($_GET['domain'])) - Util::powerUseLocale($_GET['domain']); - - $this->typeId = intVal($id); - - $this->subject = new WorldEventList(array(['id', $this->typeId])); - if ($this->subject->error) - $this->notFound(); - - $this->hId = $this->subject->getField('holidayId'); - $this->eId = $this->typeId; - $this->name = $this->subject->getField('name', true); - $this->dates = array( - 'firstDate' => $this->subject->getField('startTime'), - 'lastDate' => $this->subject->getField('endTime'), - 'length' => $this->subject->getField('length'), - 'rec' => $this->subject->getField('occurence') - ); - } - - protected function generatePath() - { - switch ($this->subject->getField('scheduleType')) - { - case '': $this->path[] = 0; break; - case -1: $this->path[] = 1; break; - case 0: - case 1: $this->path[] = 2; break; - case 2: $this->path[] = 3; break; - } - } - - protected function generateTitle() - { - array_unshift($this->title, $this->subject->getField('name', true), Util::ucFirst(Lang::game('event'))); - } - - protected function generateContent() - { - $this->addJS('?data=zones&locale='.User::$localeId.'&t='.$_SESSION['dataKey']); - - /***********/ - /* Infobox */ - /***********/ - - $this->infobox = Lang::getInfoBoxForFlags($this->subject->getField('cuFlags')); - - // boss - if ($_ = $this->subject->getField('bossCreature')) - { - $this->extendGlobalIds(TYPE_NPC, $_); - $this->infobox[] = Lang::npc('rank', 3).Lang::main('colon').'[npc='.$_.']'; - } - - // display internal id to staff - if (User::isInGroup(U_GROUP_STAFF)) - $this->infobox[] = 'Event-Id'.Lang::main('colon').$this->eId; - - /****************/ - /* Main Content */ - /****************/ - - // no entry in ?_articles? use default HolidayDescription - if ($this->hId && empty($this->article)) - $this->article = ['text' => Util::jsEscape($this->subject->getField('description', true)), 'params' => []]; - - $this->headIcons = [$this->subject->getField('iconString')]; - $this->redButtons = array( - BUTTON_WOWHEAD => $this->hId > 0, - BUTTON_LINKS => ['type' => $this->type, 'typeId' => $this->typeId] - ); - - /**************/ - /* Extra Tabs */ - /**************/ - - $hasFilter = in_array($this->hId, [372, 283, 285, 353, 420, 400, 284, 201, 374, 409, 141, 324, 321, 424, 335, 327, 341, 181, 404, 398, 301]); - - // 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->eId)) - { - $creatures = new CreatureList(array(['id', array_keys($npcIds)])); - if (!$creatures->error) - { - $data = $creatures->getListviewData(); - foreach ($data as &$d) - $d['method'] = $npcIds[$d['id']]; - - $tabData = ['data' => array_values($data)]; - - if ($hasFilter) - $tabData['note'] = sprintf(Util::$filterResultString, '?npcs&filter=cr=38;crs='.$this->hId.';crv=0'); - - $this->lvTabs[] = ['creature', $tabData]; - } - } - - // 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->eId)) - { - $objects = new GameObjectList(array(['id', array_keys($objectIds)])); - if (!$objects->error) - { - $data = $objects->getListviewData(); - foreach ($data as &$d) - $d['method'] = $objectIds[$d['id']]; - - $tabData = ['data' => array_values($data)]; - - if ($hasFilter) - $tabData['note'] = sprintf(Util::$filterResultString, '?objects&filter=cr=16;crs='.$this->hId.';crv=0'); - - $this->lvTabs[] = ['object', $tabData]; - } - } - - // tab: achievements - if ($_ = $this->subject->getField('achievementCatOrId')) - { - $condition = $_ > 0 ? [['category', $_]] : [['id', -$_]]; - $acvs = new AchievementList($condition); - if (!$acvs->error) - { - $this->extendGlobalData($acvs->getJSGlobals(GLOBALINFO_SELF | GLOBALINFO_RELATED)); - - $tabData = array( - 'data' => array_values($acvs->getListviewData()), - 'visibleCols' => ['category'] - ); - - if ($hasFilter) - $tabData['note'] = sprintf(Util::$filterResultString, '?achievements&filter=cr=11;crs='.$this->hId.';crv=0'); - - $this->lvTabs[] = ['achievement', $tabData]; - } - } - - $itemCnd = []; - if ($this->hId) - { - $itemCnd = array( - 'OR', - ['eventId', $this->eId], // direct requirement on item - ); - - // tab: quests (by table, go & creature) - $quests = new QuestList(array(['eventId', $this->eId])); - if (!$quests->error) - { - $this->extendGlobalData($quests->getJSGlobals(GLOBALINFO_SELF | GLOBALINFO_REWARDS)); - - $tabData = ['data'=> array_values($quests->getListviewData())]; - - if ($hasFilter) - $tabData['note'] = sprintf(Util::$filterResultString, '?quests&filter=cr=33;crs='.$this->hId.';crv=0'); - - $this->lvTabs[] = ['quest', $tabData]; - - $questItems = []; - foreach (array_column($quests->rewards, TYPE_ITEM) as $arr) - $questItems = array_merge($questItems, $arr); - - foreach (array_column($quests->requires, TYPE_ITEM) as $arr) - $questItems = array_merge($questItems, $arr); - - if ($questItems) - $itemCnd[] = ['id', $questItems]; - } - } - - // items from creature - if ($npcIds && !$creatures->error) - { - // vendor - $cIds = $creatures->getFoundIDs(); - if ($sells = DB::World()->selectCol('SELECT item FROM npc_vendor nv WHERE 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)', $cIds, $cIds)) - $itemCnd[] = ['id', $sells]; - } - - // tab: items - // not checking for loot ... cant distinguish between eventLoot and fillerCrapLoot - if ($itemCnd) - { - $eventItems = new ItemList($itemCnd); - if (!$eventItems->error) - { - $this->extendGlobalData($eventItems->getJSGlobals(GLOBALINFO_SELF)); - - $tabData = ['data'=> array_values($eventItems->getListviewData())]; - - if ($hasFilter) - $tabData['note'] = sprintf(Util::$filterResultString, '?items&filter=cr=160;crs='.$this->hId.';crv=0'); - - $this->lvTabs[] = ['item', $tabData]; - } - } - - // 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->eId, $this->eId, $this->eId)) - { - $list = []; - array_walk($rel, function($v, $k) use (&$list) { - if ($v > 0) - $list[] = $v; - else if ($v === null) - trigger_error('game_event_prerequisite: this event has itself as prerequisite', E_USER_WARNING); - }); - - if ($list) - { - $relEvents = new WorldEventList(array(['id', $list])); - $this->extendGlobalData($relEvents->getJSGlobals()); - $relData = $relEvents->getListviewData(); - foreach ($relEvents->getFoundIDs() as $id) - $relData[$id]['condition'][0][$this->typeId][] = [[-CND_ACTIVE_EVENT, $this->eId]]; - - $this->extendGlobalData($this->subject->getJSGlobals()); - foreach ($rel as $r) - { - if ($r <= 0) - continue; - - $this->extendGlobalIds(TYPE_WORLDEVENT, $r); - - $d = $this->subject->getListviewData(); - $d[$this->eId]['condition'][0][$this->typeId][] = [[-CND_ACTIVE_EVENT, $r]]; - - $relData = array_merge($relData, $d); - } - - $this->lvTabs[] = ['event', array( - 'data' => array_values($relData), - 'id' => 'see-also', - 'name' => '$LANG.tab_seealso', - 'hiddenCols' => ['date'], - 'extraCols' => ['$Listview.extraCols.condition'] - )]; - } - } - } - - protected function postCache() - { - // update dates to now() - $updated = WorldEventList::updateDates($this->dates); - - if ($this->mode == CACHE_TYPE_TOOLTIP) - { - return array( - date(Lang::main('dateFmtLong'), $updated['start']), - date(Lang::main('dateFmtLong'), $updated['end']) - ); - } - else - { - if ($this->hId) - Util::$wowheadLink = 'http://'.Util::$subDomains[User::$localeId].'.wowhead.com/event='.$this->hId; - - /********************/ - /* finalize infobox */ - /********************/ - - // start - if ($updated['start']) - array_push($this->infobox, Lang::event('start').Lang::main('colon').date(Lang::main('dateFmtLong'), $updated['start'])); - - // end - if ($updated['end']) - array_push($this->infobox, Lang::event('end').Lang::main('colon').date(Lang::main('dateFmtLong'), $updated['end'])); - - // occurence - if ($updated['rec'] > 0) - array_push($this->infobox, Lang::event('interval').Lang::main('colon').Util::formatTime($updated['rec'] * 1000)); - - // in progress - if ($updated['start'] < time() && $updated['end'] > time()) - array_push($this->infobox, '[span class=q2]'.Lang::event('inProgress').'[/span]'); - - $this->infobox = '[ul][li]'.implode('[/li][li]', $this->infobox).'[/li][/ul]'; - - /***************************/ - /* finalize related events */ - /***************************/ - - foreach ($this->lvTabs as &$view) - { - if ($view[0] != WorldEventList::$brickFile) - continue; - - foreach ($view[1]['data'] as &$data) - { - $updated = WorldEventList::updateDates($data['_date']); - unset($data['_date']); - $data['startDate'] = $updated['start'] ? date(Util::$dateFormatInternal, $updated['start']) : false; - $data['endDate'] = $updated['end'] ? date(Util::$dateFormatInternal, $updated['end']) : false; - $data['rec'] = $updated['rec']; - } - - } - } - } - - protected function generateTooltip($asError = false) - { - if ($asError) - return '$WowheadPower.registerHoliday('.$this->typeId.', '.User::$localeId.', {});'; - - $x = '$WowheadPower.registerHoliday('.$this->typeId.', '.User::$localeId.", {\n"; - $x .= "\tname_".User::$localeString.": '".Util::jsEscape($this->subject->getField('name', true))."',\n"; - - if ($this->subject->getField('iconString') != 'trade_engineering') - $x .= "\ticon: '".rawurlencode($this->subject->getField('iconString', true, true))."',\n"; - - $x .= "\ttooltip_".User::$localeString.": '".$this->subject->renderTooltip()."'\n"; - $x .= "});"; - - return $x; - } - - public function display($override = '') - { - if ($this->mode != CACHE_TYPE_TOOLTIP) - return parent::display($override); - - if (!$this->loadCache($tt)) - { - $tt = $this->generateTooltip(); - $this->saveCache($tt); - } - - list($start, $end) = $this->postCache(); - - header('Content-type: application/x-javascript; charset=utf-8'); - die(sprintf($tt, $start, $end)); - } - - public function notFound($title = '', $msg = '') - { - if ($this->mode != CACHE_TYPE_TOOLTIP) - return parent::notFound($title ?: Lang::game('event'), $msg ?: Lang::event('notFound')); - - header('Content-type: application/x-javascript; charset=utf-8'); - echo $this->generateTooltip(true); - exit(); - } -} - -?> diff --git a/pages/events.php b/pages/events.php deleted file mode 100644 index 1b9a9330..00000000 --- a/pages/events.php +++ /dev/null @@ -1,106 +0,0 @@ -getCategoryFromUrl($pageParam);; - - parent::__construct($pageCall, $pageParam); - - $this->name = Util::ucFirst(Lang::game('events')); - } - - protected function generateContent() - { - $condition = []; - - if (!User::isInGroup(U_GROUP_EMPLOYEE)) - $condition[] = [['cuFlags', CUSTOM_EXCLUDE_FOR_LISTVIEW, '&'], 0]; - - if ($this->category) - { - switch ($this->category[0]) - { - case 0: $condition[] = ['e.holidayId', 0]; break; - case 1: $condition[] = ['h.scheduleType', -1]; break; - case 2: $condition[] = ['h.scheduleType', [0, 1]]; break; - case 3: $condition[] = ['h.scheduleType', 2]; break; - } - } - - $events = new WorldEventList($condition); - $this->extendGlobalData($events->getJSGlobals()); - - $this->deps = []; - foreach ($events->iterate() as $__) - if ($d = $events->getField('requires')) - $this->deps[$events->id] = $d; - - $data = array_values($events->getListviewData()); - - $this->lvTabs[] = ['event', ['data' => $data]]; - - if ($_ = array_values(array_filter($data, function($x) {return $x['category'] > 0;}))) - { - $this->lvTabs[] = ['calendar', array( - 'data' => $_, - 'hideCount' => 1 - )]; - } - } - - protected function generateTitle() - { - array_unshift($this->title, $this->name); - if ($this->category) - array_unshift($this->title, Lang::event('category')[$this->category[0]]); - } - - protected function generatePath() - { - if ($this->category) - $this->path[] = $this->category[0]; - } - - protected function postCache() - { - // recalculate dates with now() - foreach ($this->lvTabs as &$views) - { - foreach ($views[1]['data'] as &$data) - { - // is a followUp-event - if (!empty($this->deps[$data['id']])) - { - $data['startDate'] = $data['endDate'] = false; - unset($data['_date']); - continue; - } - - $updated = WorldEventList::updateDates($data['_date']); - unset($data['_date']); - $data['startDate'] = $updated['start'] ? date(Util::$dateFormatInternal, $updated['start']) : false; - $data['endDate'] = $updated['end'] ? date(Util::$dateFormatInternal, $updated['end']) : false; - $data['rec'] = $updated['rec']; - } - } - } -} - -?> diff --git a/pages/faction.php b/pages/faction.php deleted file mode 100644 index d52cf613..00000000 --- a/pages/faction.php +++ /dev/null @@ -1,300 +0,0 @@ -typeId = intVal($id); - - $this->subject = new FactionList(array(['id', $this->typeId])); - if ($this->subject->error) - $this->notFound(Lang::game('faction'), Lang::faction('notFound')); - - $this->name = $this->subject->getField('name', true); - } - - protected function generatePath() - { - if ($foo = $this->subject->getField('cat')) - { - if ($bar = $this->subject->getField('cat2')) - $this->path[] = $bar; - - $this->path[] = $foo; - } - } - - protected function generateTitle() - { - array_unshift($this->title, $this->subject->getField('name', true), Util::ucFirst(Lang::game('faction'))); - } - - protected function generateContent() - { - $this->addJS('?data=zones&locale='.User::$localeId.'&t='.$_SESSION['dataKey']); - - /***********/ - /* Infobox */ - /***********/ - $infobox = Lang::getInfoBoxForFlags($this->subject->getField('cuFlags')); - - // Quartermaster if any - if ($ids = $this->subject->getField('qmNpcIds')) - { - $this->extendGlobalIds(TYPE_NPC, $ids); - - $qmStr = Lang::faction('quartermaster').Lang::main('colon'); - - if (count($ids) == 1) - $qmStr .= '[npc='.$ids[0].']'; - else if (count($ids) > 1) - { - $qmStr .= '[ul]'; - foreach ($ids as $id) - $qmStr .= '[li][npc='.$id.'][/li]'; - - $qmStr .= '[/ul]'; - } - - $infobox[] = $qmStr; - } - - // side if any - if ($_ = $this->subject->getField('side')) - $infobox[] = Lang::main('side').Lang::main('colon').'[span class=icon-'.($_ == 1 ? 'alliance' : 'horde').']'.Lang::game('si', $_).'[/span]'; - - /****************/ - /* Main Content */ - /****************/ - - $this->extraText = ''; - $this->infobox = $infobox ? '[ul][li]'.implode('[/li][li]', $infobox).'[/li][/ul]' : null; - $this->redButtons = array( - BUTTON_WOWHEAD => true, - BUTTON_LINKS => ['type' => $this->type, 'typeId' => $this->typeId] - ); - - // Spillover Effects - /* todo (low): also check on reputation_spillover_template (but its data is identical to calculation below - $rst = DB::World()->selectRow('SELECT - 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); - */ - - - $conditions = array( - ['id', $this->typeId, '!'], // not self - ['repIdx', -1, '!'] // only gainable - ); - - if ($p = $this->subject->getField('parentFactionId')) // linked via parent - $conditions[] = ['OR', ['id', $p], ['parentFactionId', $p]]; - else // self as parent - $conditions[] = ['parentFactionId', $this->typeId]; - - $spillover = new FactionList($conditions); - $this->extendGlobalData($spillover->getJSGlobals()); - - $buff = ''; - foreach ($spillover->iterate() as $spillId => $__) - if ($val = ($spillover->getField('spilloverRateIn') * $this->subject->getField('spilloverRateOut') * 100)) - $buff .= '[tr][td][faction='.$spillId.'][/td][td][span class=q'.($val > 0 ? '2]+' : '10]').$val.'%[/span][/td][td]'.Lang::game('rep', $spillover->getField('spilloverMaxRank')).'[/td][/tr]'; - - if ($buff) - $this->extraText .= '[h3 class=clear]'.Lang::faction('spillover').'[/h3][div margin=15px]'.Lang::faction('spilloverDesc').'[/div][table class=grid width=400px][tr][td width=150px][b]'.Util::ucFirst(Lang::game('faction')).'[/b][/td][td width=100px][b]'.Lang::spell('_value').'[/b][/td][td width=150px][b]'.Lang::faction('maxStanding').'[/b][/td][/tr]'.$buff.'[/table]'; - - - // reward rates (ultimately this should be calculated into each reward display) - if ($rates = DB::World()->selectRow('SELECT * FROM reputation_reward_rate WHERE faction = ?d', $this->typeId)) - { - $buff = ''; - foreach ($rates as $k => $v) - { - if ($v == 1) - continue; - - switch ($k) - { - case 'quest_rate': $buff .= '[tr][td]'.Lang::game('quests') .Lang::main('colon').'[/td]'; break; - case 'quest_daily_rate': $buff .= '[tr][td]'.Lang::game('quests').' ('.Lang::quest('daily').')' .Lang::main('colon').'[/td]'; break; - case 'quest_weekly_rate': $buff .= '[tr][td]'.Lang::game('quests').' ('.Lang::quest('weekly').')' .Lang::main('colon').'[/td]'; break; - case 'quest_monthly_rate': $buff .= '[tr][td]'.Lang::game('quests').' ('.Lang::quest('monthly').')' .Lang::main('colon').'[/td]'; break; - case 'quest_repeatable_rate': $buff .= '[tr][td]'.Lang::game('quests').' ('.Lang::quest('repeatable').')'.Lang::main('colon').'[/td]'; break; - case 'creature_rate': $buff .= '[tr][td]'.Lang::game('npcs') .Lang::main('colon').'[/td]'; break; - case 'spell_rate': $buff .= '[tr][td]'.Lang::game('spells') .Lang::main('colon').'[/td]'; break; - default: - continue; - } - - $buff .= '[td width=35px align=right][span class=q'.($v < 1 ? '10]' : '2]+').intVal(($v - 1) * 100).'%[/span][/td][/tr]'; - } - - if ($buff) - $this->extraText .= '[h3 class=clear]'.Lang::faction('customRewRate').'[/h3][table]'.$buff.'[/table]'; - } - - // 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)) - { - $altFac = new FactionList(array(['id', abs($pendant)])); - if (!$altFac->error) - { - $this->transfer = sprintf( - Lang::faction('_transfer'), - $altFac->id, - $altFac->getField('name', true), - $pendant > 0 ? 'alliance' : 'horde', - $pendant > 0 ? Lang::game('si', 1) : Lang::game('si', 2) - ); - } - } - - /**************/ - /* Extra Tabs */ - /**************/ - - // tab: items - $items = new ItemList(array(['requiredFaction', $this->typeId])); - if (!$items->error) - { - $this->extendGlobalData($items->getJSGlobals(GLOBALINFO_SELF)); - - $tabData = array( - 'data' => array_values($items->getListviewData()), - 'extraCols' => '$_', - 'sort' => ['standing', 'name'] - ); - - if ($items->getMatches() > CFG_SQL_LIMIT_DEFAULT) - $tabData['note'] = sprintf(Util::$filterResultString, '?items&filter=cr=17;crs='.$this->typeId.';crv=0'); - - $this->lvTabs[] = ['item', $tabData, 'itemStandingCol']; - } - - // tab: creatures with onKill reputation - if ($this->subject->getField('reputationIndex') != -1) // only if you can actually gain reputation by kills - { - // 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)}) - ) x', - $this->typeId, $spillover->getFoundIDs() ?: DBSIMPLE_SKIP, - $this->typeId, $spillover->getFoundIDs() ?: DBSIMPLE_SKIP - ); - - if ($cRep) - { - $killCreatures = new CreatureList(array(['id', array_keys($cRep)])); - if (!$killCreatures->error) - { - $data = $killCreatures->getListviewData(); - foreach ($data as $id => &$d) - $d['reputation'] = $cRep[$id]; - - $tabData = array( - 'data' => array_values($data), - 'extraCols' => '$_', - 'sort' => ['-reputation', 'name'] - ); - - if ($killCreatures->getMatches() > CFG_SQL_LIMIT_DEFAULT) - $tabData['note'] = sprintf(Util::$filterResultString, '?npcs&filter=cr=42;crs='.$this->typeId.';crv=0'); - - $this->lvTabs[] = ['creature', $tabData, 'npcRepCol']; - } - } - } - - // tab: members - if ($_ = $this->subject->getField('templateIds')) - { - $members = new CreatureList(array(['faction', $_])); - if (!$members->error) - { - $tabData = array( - 'data' => array_values($members->getListviewData()), - 'id' => 'member', - 'name' => '$LANG.tab_members' - ); - - if ($members->getMatches() > CFG_SQL_LIMIT_DEFAULT) - $tabData['note'] = sprintf(Util::$filterResultString, '?npcs&filter=cr=3;crs='.$this->typeId.';crv=0'); - - $this->lvTabs[] = ['creature', $tabData]; - } - } - - // tab: objects - if ($_ = $this->subject->getField('templateIds')) - { - $objects = new GameObjectList(array(['faction', $_])); - if (!$objects->error) - $this->lvTabs[] = ['object', ['data' => array_values($objects->getListviewData())]]; - } - - // 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' - ); - $quests = new QuestList($conditions); - if (!$quests->error) - { - $this->extendGlobalData($quests->getJSGlobals(GLOBALINFO_ANY)); - - $tabData = array( - 'data' => array_values($quests->getListviewData($this->typeId)), - 'extraCols' => '$_' - ); - - if ($quests->getMatches() > CFG_SQL_LIMIT_DEFAULT) - $tabData['note'] = sprintf(Util::$filterResultString, '?quests&filter=cr=1;crs='.$this->typeId.';crv=0'); - - $this->lvTabs[] = ['quest', $tabData, 'questRepCol']; - } - - // tab: achievements - $conditions = array( - ['ac.type', ACHIEVEMENT_CRITERIA_TYPE_GAIN_REPUTATION], - ['ac.value1', $this->typeId] - ); - $acvs = new AchievementList($conditions); - if (!$acvs->error) - { - $this->extendGlobalData($acvs->getJSGlobals(GLOBALINFO_ANY)); - - $this->lvTabs[] = ['achievement', array( - 'data' => array_values($acvs->getListviewData()), - 'id' => 'criteria-of', - 'name' => '$LANG.tab_criteriaof', - 'visibleCols' => ['category'] - )]; - } - } -} - -?> diff --git a/pages/factions.php b/pages/factions.php deleted file mode 100644 index e25b32f9..00000000 --- a/pages/factions.php +++ /dev/null @@ -1,86 +0,0 @@ - [469, 891, 67, 892, 169], - 980 => [936], - 1097 => [1037, 1052, 1117], - 0 => true - ); - - public function __construct($pageCall, $pageParam) - { - $this->getCategoryFromUrl($pageParam);; - - parent::__construct($pageCall, $pageParam); - - $this->name = Util::ucFirst(Lang::game('factions')); - } - - protected function generateContent() - { - $conditions = []; - - if (!User::isInGroup(U_GROUP_EMPLOYEE)) // unlisted factions - $conditions[] = [['cuFlags', CUSTOM_EXCLUDE_FOR_LISTVIEW, '&'], 0]; - - if (isset($this->category[1])) - $conditions[] = ['parentFactionId', $this->category[1]]; - else if (isset($this->category[0])) - { - if ($this->category[0]) - $subs = DB::Aowow()->selectCol('SELECT id FROM ?_factions WHERE parentFactionId = ?d', $this->category[0]); - else - $subs = [0]; - - $conditions[] = ['OR', ['parentFactionId', $subs], ['id', $subs]]; - } - - $data = []; - $factions = new FactionList($conditions); - if (!$factions->error) - $data = array_values($factions->getListviewData()); - - $this->lvTabs[] = ['faction', ['data' => $data]]; - } - - protected function generateTitle() - { - array_unshift($this->title, $this->name); - if ($this->category) - { - switch (count($this->category)) - { - case 1: - $t = Lang::faction('cat', $this->category[0]); - array_unshift($this->title, is_array($t) ? $t[0] : $t); - break; - case 2: - array_unshift($this->title, Lang::faction('cat', $this->category[0], $this->category[1])); - break; - } - } - } - - protected function generatePath() - { - foreach ($this->category as $c) - $this->path[] = $c; - } -} - -?> diff --git a/pages/genericPage.class.php b/pages/genericPage.class.php deleted file mode 100644 index 99036841..00000000 --- a/pages/genericPage.class.php +++ /dev/null @@ -1,1014 +0,0 @@ -mode, $this->type, $this->typeId, $staff, User::$localeId, '-1', '-1']; - - // item special: can modify tooltips - if (isset($this->enhancedTT)) - $key[] = md5(serialize($this->enhancedTT)); - - return implode('_', $key); - } - - protected function applyCCErrors() - { - if (!empty($_SESSION['error']['co'])) - $this->coError = $_SESSION['error']['co']; - - if (!empty($_SESSION['error']['ss'])) - $this->ssError = $_SESSION['error']['ss']; - - if (!empty($_SESSION['error']['vi'])) - $this->viError = $_SESSION['error']['vi']; - - unset($_SESSION['error']); - } -} - - -trait ListPage -{ - protected $category = null; - protected $filter = []; - protected $lvTabs = []; // most pages have this - - private $filterObj = null; - - protected function generateCacheKey($withStaff = true) - { - $staff = intVal($withStaff && User::isInGroup(U_GROUP_EMPLOYEE)); - - // mode, type, typeId, employee-flag, localeId, - $key = [$this->mode, $this->type, '-1', $staff, User::$localeId]; - - //category - $key[] = $this->category ? implode('.', $this->category) : '-1'; - - // filter - $key[] = $this->filterObj ? md5(serialize($this->filterObj)) : '-1'; - - return implode('_', $key); - } -} - -trait TrProfiler -{ - protected $region = ''; - protected $realm = ''; - protected $realmId = 0; - protected $battlegroup = ''; // not implemented, since no pserver supports it - protected $subjectName = ''; - protected $subjectGUID = 0; - protected $sumSubjects = 0; - - protected $doResync = null; - - protected function getSubjectFromUrl($str) - { - if (!$str) - return; - - // cat[0] is always region - // cat[1] is realm or bGroup (must be realm if cat[2] is set) - // cat[2] is arena-team, guild or player - $cat = explode('.', $str, 3); - - $cat = array_map('urldecode', $cat); - - if (count($cat) > 3) - return; - - if ($cat[0] !== 'eu' && $cat[0] !== 'us') - return; - - $this->region = $cat[0]; - - // if ($cat[1] == Profiler::urlize(CFG_BATTLEGROUP)) - // $this->battlegroup = CFG_BATTLEGROUP; - if (isset($cat[1])) - { - foreach (Profiler::getRealms() as $rId => $r) - { - if (Profiler::urlize($r['name']) == $cat[1]) - { - $this->realm = $r['name']; - $this->realmId = $rId; - if (isset($cat[2]) && mb_strlen($cat[2]) >= 3) - $this->subjectName = $cat[2]; // cannot reconstruct original name from urlized form; match against special name field - - break; - } - } - } - } - - protected function initialSync() - { - $this->prepareContent(); - - $this->notFound = array( - 'title' => sprintf(Lang::profiler('firstUseTitle'), $this->subjectName, $this->realm), - 'msg' => '' - ); - $this->hasComContent = false; - Util::arraySumByKey($this->mysql, DB::Aowow()->getStatistics(), DB::World()->getStatistics()); - - if (isset($this->tabId)) - $this->pageTemplate['activeTab'] = $this->tabId; - - $this->display('text-page-generic'); - exit(); - } - - protected function generatePath() - { - if ($this->region) - { - $this->path[] = $this->region; - - if ($this->realm) - $this->path[] = Profiler::urlize($this->realm); - // else - // $this->path[] = Profiler::urlize(CFG_BATTLEGROUP); - } - } -} - -class GenericPage -{ - protected $tpl = ''; - protected $reqUGroup = U_GROUP_NONE; - protected $reqAuth = false; - protected $mode = CACHE_TYPE_NONE; - - protected $jsGlobals = []; - protected $lvData = []; - protected $title = [CFG_NAME]; // for title-Element - protected $name = ''; // for h1-Element - protected $tabId = null; - protected $gDataKey = false; // adds the dataKey to the user vars - protected $js = []; - protected $css = []; - - // private vars don't get cached - private $time = 0; - private $cacheDir = 'cache/template/'; - private $jsgBuffer = []; - private $gPageInfo = []; - private $gUser = []; - private $pageTemplate = []; - private $community = ['co' => [], 'sc' => [], 'vi' => []]; - - private $cacheLoaded = []; - private $skipCache = 0x0; - private $memcached = null; - private $mysql = ['time' => 0, 'count' => 0]; - - private $headerLogo = ''; - private $fullParams = ''; - - private $lvTemplates = array( - 'achievement' => ['template' => 'achievement', 'id' => 'achievements', 'parent' => 'lv-generic', 'data' => [], 'name' => '$LANG.tab_achievements' ], - 'calendar' => ['template' => 'holidaycal', 'id' => 'calendar', 'parent' => 'lv-generic', 'data' => [], 'name' => '$LANG.tab_calendar' ], - 'class' => ['template' => 'classs', 'id' => 'classes', 'parent' => 'lv-generic', 'data' => [], 'name' => '$LANG.tab_classes' ], - 'commentpreview' => ['template' => 'commentpreview', 'id' => 'comments', 'parent' => 'lv-generic', 'data' => [], 'name' => '$LANG.tab_comments' ], - 'creature' => ['template' => 'npc', 'id' => 'npcs', 'parent' => 'lv-generic', 'data' => [], 'name' => '$LANG.tab_npcs' ], - 'currency' => ['template' => 'currency', 'id' => 'currencies', 'parent' => 'lv-generic', 'data' => [], 'name' => '$LANG.tab_currencies' ], - 'emote' => ['template' => 'emote', 'id' => 'emotes', 'parent' => 'lv-generic', 'data' => [] ], - 'enchantment' => ['template' => 'enchantment', 'id' => 'enchantments', 'parent' => 'lv-generic', 'data' => [] ], - 'event' => ['template' => 'holiday', 'id' => 'holidays', 'parent' => 'lv-generic', 'data' => [], 'name' => '$LANG.tab_holidays' ], - 'faction' => ['template' => 'faction', 'id' => 'factions', 'parent' => 'lv-generic', 'data' => [], 'name' => '$LANG.tab_factions' ], - 'genericmodel' => ['template' => 'genericmodel', 'id' => 'same-model-as', 'parent' => 'lv-generic', 'data' => [], 'name' => '$LANG.tab_samemodelas' ], - 'icongallery' => ['template' => 'icongallery', 'id' => 'icons', 'parent' => 'lv-generic', 'data' => [] ], - 'item' => ['template' => 'item', 'id' => 'items', 'parent' => 'lv-generic', 'data' => [], 'name' => '$LANG.tab_items' ], - 'itemset' => ['template' => 'itemset', 'id' => 'itemsets', 'parent' => 'lv-generic', 'data' => [], 'name' => '$LANG.tab_itemsets' ], - 'model' => ['template' => 'model', 'id' => 'gallery', 'parent' => 'lv-generic', 'data' => [], 'name' => '$LANG.tab_gallery' ], - 'object' => ['template' => 'object', 'id' => 'objects', 'parent' => 'lv-generic', 'data' => [], 'name' => '$LANG.tab_objects' ], - 'pet' => ['template' => 'pet', 'id' => 'hunter-pets', 'parent' => 'lv-generic', 'data' => [], 'name' => '$LANG.tab_pets' ], - 'profile' => ['template' => 'profile', 'id' => 'profiles', 'parent' => 'lv-generic', 'data' => [], 'name' => '$LANG.tab_profiles' ], - 'quest' => ['template' => 'quest', 'id' => 'quests', 'parent' => 'lv-generic', 'data' => [], 'name' => '$LANG.tab_quests' ], - 'race' => ['template' => 'race', 'id' => 'races', 'parent' => 'lv-generic', 'data' => [], 'name' => '$LANG.tab_races' ], - 'replypreview' => ['template' => 'replypreview', 'id' => 'comment-replies', 'parent' => 'lv-generic', 'data' => [], 'name' => '$LANG.tab_commentreplies'], - 'reputationhistory' => ['template' => 'reputationhistory', 'id' => 'reputation', 'parent' => 'lv-generic', 'data' => [], 'name' => '$LANG.tab_reputation' ], - 'screenshot' => ['template' => 'screenshot', 'id' => 'screenshots', 'parent' => 'lv-generic', 'data' => [], 'name' => '$LANG.tab_screenshots' ], - 'skill' => ['template' => 'skill', 'id' => 'skills', 'parent' => 'lv-generic', 'data' => [], 'name' => '$LANG.tab_skills' ], - 'sound' => ['template' => 'sound', 'id' => 'sounds', 'parent' => 'lv-generic', 'data' => [], 'name' => '$LANG.types[19][2]' ], - 'spell' => ['template' => 'spell', 'id' => 'spells', 'parent' => 'lv-generic', 'data' => [], 'name' => '$LANG.tab_spells' ], - 'title' => ['template' => 'title', 'id' => 'titles', 'parent' => 'lv-generic', 'data' => [], 'name' => '$LANG.tab_titles' ], - 'topusers' => ['template' => 'topusers', 'id' => 'topusers', 'parent' => 'lv-generic', 'data' => [], 'name' => '$LANG.topusers' ], - 'video' => ['template' => 'video', 'id' => 'videos', 'parent' => 'lv-generic', 'data' => [], 'name' => '$LANG.tab_videos' ], - 'zone' => ['template' => 'zone', 'id' => 'zones', 'parent' => 'lv-generic', 'data' => [], 'name' => '$LANG.tab_zones' ] - ); - - public function __construct($pageCall, $pageParam = null) - { - $this->time = microtime(true); - - $this->fullParams = $pageCall; - if ($pageParam) - $this->fullParams .= '='.$pageParam; - - if (CFG_CACHE_DIR && Util::writeDir(CFG_CACHE_DIR)) - $this->cacheDir = mb_substr(CFG_CACHE_DIR, -1) != '/' ? CFG_CACHE_DIR.'/' : CFG_CACHE_DIR; - - // force page refresh - if (isset($_GET['refresh']) && User::isInGroup(U_GROUP_ADMIN | U_GROUP_BUREAU | U_GROUP_DEV)) - { - if ($_GET['refresh'] == 'filecache') - $this->skipCache = CACHE_MODE_FILECACHE; - else if ($_GET['refresh'] == 'memcached') - $this->skipCache = CACHE_MODE_MEMCACHED; - else if ($_GET['refresh'] == '') - $this->skipCache = CACHE_MODE_FILECACHE | CACHE_MODE_MEMCACHED; - } - - // display modes - if (isset($_GET['power']) && method_exists($this, 'generateTooltip')) - $this->mode = CACHE_TYPE_TOOLTIP; - else if (isset($_GET['xml']) && method_exists($this, 'generateXML')) - $this->mode = CACHE_TYPE_XML; - else - { - // 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())) - $this->headerLogo = Util::defStatic($ahl); - - $this->gUser = User::getUserGlobals(); - $this->pageTemplate['pageName'] = strtolower($pageCall); - - if (!$this->isValidPage()) - $this->error(); - } - - // requires authed user - if ($this->reqAuth && !User::$id) - $this->forwardToSignIn($_SERVER['QUERY_STRING']); - - // restricted access - if ($this->reqUGroup && !User::isInGroup($this->reqUGroup)) - { - if (User::$id) - $this->error(); - else - $this->forwardToSignIn($_SERVER['QUERY_STRING']); - } - - if (CFG_MAINTENANCE && !User::isInGroup(U_GROUP_EMPLOYEE)) - $this->maintenance(); - else if (CFG_MAINTENANCE && User::isInGroup(U_GROUP_EMPLOYEE)) - Util::addNote(U_GROUP_EMPLOYEE, 'Maintenance mode enabled!'); - - // get errors from previous page from session and apply to template - if (method_exists($this, 'applyCCErrors')) - $this->applyCCErrors(); - } - - /**********/ - /* Checks */ - /**********/ - - private function isSaneInclude($path, $file) // "template_exists" - { - if (preg_match('/[^\w\-]/i', str_replace('admin/', '', $file))) - return false; - - if (!is_file($path.$file.'.tpl.php')) - return false; - - return true; - } - - private function isValidPage() // has a valid combination of categories - { - if (!isset($this->category) || empty($this->validCats)) - 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, function ($x) { return 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)); - case 2: // first param has to be a key. otherwise invalid - if (!isset($this->validCats[$c[0]])) - return false; - - // check if the sub-array is n-imensional - if (is_array($this->validCats[$c[0]]) && count($this->validCats[$c[0]]) == count($this->validCats[$c[0]], COUNT_RECURSIVE)) - return in_array($c[1], $this->validCats[$c[0]]); // second param is value in second level array - else - return isset($this->validCats[$c[0]][$c[1]]); // check if params is key of another array - case 3: // 3 params MUST point to a specific value - return isset($this->validCats[$c[0]][$c[1]]) && in_array($c[2], $this->validCats[$c[0]][$c[1]]); - } - - return false; - } - - /****************/ - /* Prepare Page */ - /****************/ - - protected function prepareContent() // get from cache ?: run generators - { - if (!$this->loadCache()) - { - $this->addArticle(); - - $this->generateContent(); - $this->generatePath(); - $this->generateTitle(); - - $this->applyGlobals(); - - $this->saveCache(); - } - - if (isset($this->type) && isset($this->typeId)) - { - $this->gPageInfo = array( // varies slightly for special pages like maps, user-dashboard or profiler - 'type' => $this->type, - 'typeId' => $this->typeId, - 'name' => $this->name - ); - } - else if (!empty($this->articleUrl)) - { - $this->gPageInfo = array( - 'articleUrl' => $this->fullParams, // is actually be the url-param - 'editAccess' => isset($this->editAccess) ? $this->editAccess : (U_GROUP_ADMIN | U_GROUP_EDITOR | U_GROUP_BUREAU) - ); - } - - if (!empty($this->path)) - $this->pageTemplate['breadcrumb'] = $this->path; - - if (!empty($this->filter)) - $this->pageTemplate['filter'] = empty($this->filter['query']) ? 0 : 1; - - if (method_exists($this, 'postCache')) // e.g. update dates for events and such - $this->postCache(); - - // determine contribute tabs - if (isset($this->subject)) - { - $x = get_class($this->subject); - $this->contribute = $x::$contribute; - } - - if (!empty($this->hasComContent)) // get comments, screenshots, videos - { - $this->community = CommunityContent::getAll($this->type, $this->typeId, $jsGlobals); - $this->extendGlobalData($jsGlobals); // as comments are not cached, those globals cant be either - $this->applyGlobals(); - } - - $this->time = microtime(true) - $this->time; - Util::arraySumByKey($this->mysql, DB::Aowow()->getStatistics(), DB::World()->getStatistics()); - } - - public function addJS($name, $unshift = false) - { - if (is_array($name)) - { - foreach ($name as $n) - $this->addJS($n, $unshift); - } - else if (!in_array($name, $this->js)) - { - if ($unshift) - array_unshift($this->js, $name); - else - $this->js[] = $name; - } - } - - public function addCSS($struct, $unshift = false) - { - if (is_array($struct) && empty($struct['path']) && empty($struct['string'])) - { - foreach ($struct as $s) - $this->addCSS($s, $unshift); - } - else if (!in_array($struct, $this->css)) - { - if ($unshift) - array_unshift($this->css, $struct); - else - $this->css[] = $struct; - } - } - - private function addArticle() // get article & static infobox (run before processing jsGlobals) - { - $article = []; - if (!empty($this->type) && isset($this->typeId)) - { - $article = DB::Aowow()->selectRow('SELECT article, quickInfo, locale, editAccess FROM ?_articles WHERE type = ?d AND typeId = ?d AND locale = ?d UNION ALL SELECT article, quickInfo, locale, editAccess FROM ?_articles WHERE type = ?d AND typeId = ?d AND locale = 0 ORDER BY locale DESC LIMIT 1', - $this->type, $this->typeId, User::$localeId, $this->type, $this->typeId - ); - } - else if (!empty($this->articleUrl)) - { - $article = DB::Aowow()->selectRow('SELECT article, quickInfo, locale, editAccess FROM ?_articles WHERE url = ? AND locale = ?d UNION ALL SELECT article, quickInfo, locale, editAccess FROM ?_articles WHERE url = ? AND locale = 0 ORDER BY locale DESC LIMIT 1', - $this->articleUrl, User::$localeId, $this->articleUrl - ); - } - - if ($article) - { - if ($article['article']) - (new Markup($article['article']))->parseGlobalsFromText($this->jsgBuffer); - if ($article['quickInfo']) - (new Markup($article['quickInfo']))->parseGlobalsFromText($this->jsgBuffer); - - $this->article = array( - 'text' => Util::jsEscape(Util::defStatic($article['article'])), - 'params' => [] - ); - - if (!empty($this->type) && isset($this->typeId)) - $this->article['params']['dbpage'] = true; - - // convert U_GROUP_* to MARKUP.CLASS_* (as seen in js-object Markup) - if($article['editAccess'] & (U_GROUP_ADMIN | U_GROUP_VIP | U_GROUP_DEV)) - $this->article['params']['allow'] = '$Markup.CLASS_ADMIN'; - else if($article['editAccess'] & U_GROUP_STAFF) - $this->article['params']['allow'] = '$Markup.CLASS_STAFF'; - else if($article['editAccess'] & U_GROUP_PREMIUM) - $this->article['params']['allow'] = '$Markup.CLASS_PREMIUM'; - else if($article['editAccess'] & U_GROUP_PENDING) - $this->article['params']['allow'] = '$Markup.CLASS_PENDING'; - else - $this->article['params']['allow'] = '$Markup.CLASS_USER'; - - $this->editAccess = $article['editAccess']; - - if (empty($this->infobox) && !empty($article['quickInfo'])) - $this->infobox = $article['quickInfo']; - - if ($article['locale'] != User::$localeId) - $this->article['params']['prepend'] = '
'.Lang::main('englishOnly').'
'; - - if (method_exists($this, 'postArticle')) // e.g. update variables in article - $this->postArticle(); - } - } - - private function addAnnouncements() // get announcements and notes for user - { - if (!isset($this->announcements)) - $this->announcements = []; - - // display occured notices - if ($_ = Util::getNotes()) - { - array_unshift($_, 'One or more errors occured, while generating this page.'); - - $this->announcements[0] = array( - 'parent' => 'announcement-0', - 'id' => 0, - 'mode' => 1, - 'status' => 1, - 'name' => 'internal error', - 'style' => 'color: #ff3333; font-weight: bold; font-size: 14px; padding-left: 40px; background-image: url('.STATIC_URL.'/images/announcements/warn-small.png); background-size: 15px 15px; background-position: 12px center; border: dashed 2px #C03030;', - 'text' => '[span]'.implode("[br]", $_).'[/span]' - ); - } - - // fetch announcements - if ($this->pageTemplate['pageName']) - { - $ann = DB::Aowow()->Select('SELECT ABS(id) AS ARRAY_KEY, a.* FROM ?_announcements a WHERE status = 1 AND (page = ? OR page = "*") AND (groupMask = 0 OR groupMask & ?d)', $this->pageTemplate['pageName'], User::$groups); - foreach ($ann as $k => $v) - { - if ($t = Util::localizedString($v, 'text')) - { - $_ = array( - 'parent' => 'announcement-'.$k, - 'id' => $v['id'], - 'mode' => $v['mode'], - 'status' => $v['status'], - 'name' => $v['name'], - 'text' => Util::defStatic($t) - ); - - if ($v['style']) // may be empty - $_['style'] = Util::defStatic($v['style']); - - $this->announcements[$k] = $_; - } - } - } - } - - protected function getCategoryFromUrl($str) - { - $arr = explode('.', $str); - $params = []; - - foreach ($arr as $v) - if (is_numeric($v)) - $params[] = (int)$v; - - $this->category = $params; - } - - protected function forwardToSignIn($next = '') - { - $next = $next ? '&next='.$next : ''; - header('Location: ?account=signin'.$next, true, 302); - } - - /*******************/ - /* Special Display */ - /*******************/ - - public function notFound($title, $msg = '') // unknown entry - { - array_unshift($this->title, Lang::main('nfPageTitle')); - - $this->notFound = array( - 'title' => isset($this->typeId) ? Util::ucFirst($title).' #'.$this->typeId : $title, - 'msg' => !$msg && isset($this->typeId) ? sprintf(Lang::main('pageNotFound'), $title) : $msg - ); - $this->hasComContent = false; - Util::arraySumByKey($this->mysql, DB::Aowow()->getStatistics(), DB::World()->getStatistics()); - - if (isset($this->tabId)) - $this->pageTemplate['activeTab'] = $this->tabId; - - header('HTTP/1.0 404 Not Found', true, 404); - - $this->display('list-page-generic'); - exit(); - } - - public function error() // unknown page - { - $this->path = null; - $this->tabId = null; - $this->articleUrl = 'page-not-found'; - $this->title[] = Lang::main('errPageTitle'); - $this->name = Lang::main('errPageTitle'); - $this->lvTabs = []; - - $this->addArticle(); - - Util::arraySumByKey($this->mysql, DB::Aowow()->getStatistics(), DB::World()->getStatistics()); - - header('HTTP/1.0 404 Not Found', true, 404); - - $this->display('list-page-generic'); - exit(); - } - - public function maintenance() // display brb gnomes - { - header('HTTP/1.0 503 Service Temporarily Unavailable', true, 503); - header('Retry-After: '.(3 * HOUR)); - - $this->display('maintenance'); - exit(); - } - - /*******************/ - /* General Display */ - /*******************/ - - public function display($override = '') // load given template string or GenericPage::$tpl - { - // Heisenbug: IE11 and FF32 will sometimes (under unknown circumstances) cache 302 redirects and stop - // re-requesting them from the server but load them from local cache, thus breaking menu features. - Util::sendNoCacheHeader(); - - if (isset($this->tabId)) - $this->pageTemplate['activeTab'] = $this->tabId; - - if ($override) - { - $this->addAnnouncements(); - - include('template/pages/'.$override.'.tpl.php'); - die(); - } - else if ($this->tpl) - { - $this->prepareContent(); - - if (!$this->isSaneInclude('template/pages/', $this->tpl)) - { - trigger_error('Error: nonexistant template requested: template/pages/'.$this->tpl.'.tpl.php', E_USER_ERROR); - $this->error(); - } - - $this->addAnnouncements(); - - include('template/pages/'.$this->tpl.'.tpl.php'); - die(); - } - else - $this->error(); - } - - public function writeGlobalVars() // load jsGlobal - { - $buff = ''; - - foreach ($this->jsGlobals as $type => $struct) - { - $buff .= " var _ = ".$struct[0].';'; - - foreach ($struct[1] as $key => $data) - { - foreach ($data as $k => $v) - { - // localizes expected fields - if (in_array($k, ['name', 'namefemale'])) - { - $data[$k.'_'.User::$localeString] = $v; - unset($data[$k]); - } - } - - $buff .= ' _['.(is_numeric($key) ? $key : "'".$key."'")."]=".Util::toJSON($data).';'; - } - - $buff .= "\n"; - - if (!empty($this->typeId) && !empty($struct[2][$this->typeId])) - { - $x = $struct[2][$this->typeId]; - - // spell - if (!empty($x['tooltip'])) // spell + item - $buff .= "\n _[".$x['id'].'].tooltip_'.User::$localeString.' = '.Util::toJSON($x['tooltip']).";\n"; - if (!empty($x['buff'])) // spell - $buff .= " _[".$x['id'].'].buff_'.User::$localeString.' = '.Util::toJSON($x['buff']).";\n"; - if (!empty($x['spells'])) // spell + item - $buff .= " _[".$x['id'].'].spells_'.User::$localeString.' = '.Util::toJSON($x['spells']).";\n"; - if (!empty($x['buffspells'])) // spell - $buff .= " _[".$x['id'].'].buffspells_'.User::$localeString.' = '.Util::toJSON($x['buffspells']).";\n"; - - $buff .= "\n"; - } - } - - return $buff; - } - - public function brick($file, array $localVars = []) // load brick - { - foreach ($localVars as $n => $v) - $$n = $v; - - if (!$this->isSaneInclude('template/bricks/', $file)) - trigger_error('Nonexistant template requested: template/bricks/'.$file.'.tpl.php', E_USER_ERROR); - else - include('template/bricks/'.$file.'.tpl.php'); - } - - public function lvBrick($file) // load listview addIns - { - if (!$this->isSaneInclude('template/listviews/', $file)) - trigger_error('Nonexistant Listview addin requested: template/listviews/'.$file.'.tpl.php', E_USER_ERROR); - else - include('template/listviews/'.$file.'.tpl.php'); - } - - public function localizedBrick($file, $loc = LOCALE_EN) // load brick with more text then vars - { - if (!$this->isSaneInclude('template/localized/', $file.'_'.$loc)) - { - if ($loc == LOCALE_EN || !$this->isSaneInclude('template/localized/', $file.'_'.LOCALE_EN)) - trigger_error('Nonexistant template requested: template/localized/'.$file.'_'.$loc.'.tpl.php', E_USER_ERROR); - else - include('template/localized/'.$file.'_'.LOCALE_EN.'.tpl.php'); - } - else - include('template/localized/'.$file.'_'.$loc.'.tpl.php'); - } - - /**********************/ - /* Prepare js-Globals */ - /**********************/ - - public function extendGlobalIds($type, $data) // add typeIds that should be displayed as jsGlobal on the page - { - if (!$type || !$data) - return false; - - if (!isset($this->jsgBuffer[$type])) - $this->jsgBuffer[$type] = []; - - if (is_array($data)) - { - foreach ($data as $id) - $this->jsgBuffer[$type][] = (int)$id; - } - else if (is_numeric($data)) - $this->jsgBuffer[$type][] = (int)$data; - } - - public function extendGlobalData($data, $extra = null) // add jsGlobals or typeIds (can be mixed in one array: TYPE => [mixeddata]) to display on the page - { - foreach ($data as $type => $globals) - { - if (!is_array($globals) || !$globals) - continue; - - $this->initJSGlobal($type); - - // can be id => data - // or idx => id - // and may be mixed - foreach ($globals as $k => $v) - { - if (is_array($v)) - $this->jsGlobals[$type][1][$k] = $v; - else if (is_numeric($v)) - $this->extendGlobalIds($type, $v); - } - } - - if (is_array($extra) && $extra) - $this->jsGlobals[$type][2] = $extra; - } - - private function initJSGlobal($type) // init store for type - { - $jsg = &$this->jsGlobals; // shortcut - - if (isset($jsg[$type])) - return; - - switch ($type) - { // [varName, [data], [extra]] - case TYPE_NPC: $jsg[TYPE_NPC] = ['g_npcs', [], []]; break; - case TYPE_OBJECT: $jsg[TYPE_OBJECT] = ['g_objects', [], []]; break; - case TYPE_ITEM: $jsg[TYPE_ITEM] = ['g_items', [], []]; break; - case TYPE_ITEMSET: $jsg[TYPE_ITEMSET] = ['g_itemsets', [], []]; break; - case TYPE_QUEST: $jsg[TYPE_QUEST] = ['g_quests', [], []]; break; - case TYPE_SPELL: $jsg[TYPE_SPELL] = ['g_spells', [], []]; break; - case TYPE_ZONE: $jsg[TYPE_ZONE] = ['g_gatheredzones', [], []]; break; - case TYPE_FACTION: $jsg[TYPE_FACTION] = ['g_factions', [], []]; break; - case TYPE_PET: $jsg[TYPE_PET] = ['g_pets', [], []]; break; - case TYPE_ACHIEVEMENT: $jsg[TYPE_ACHIEVEMENT] = ['g_achievements', [], []]; break; - case TYPE_TITLE: $jsg[TYPE_TITLE] = ['g_titles', [], []]; break; - case TYPE_WORLDEVENT: $jsg[TYPE_WORLDEVENT] = ['g_holidays', [], []]; break; - case TYPE_CLASS: $jsg[TYPE_CLASS] = ['g_classes', [], []]; break; - case TYPE_RACE: $jsg[TYPE_RACE] = ['g_races', [], []]; break; - case TYPE_SKILL: $jsg[TYPE_SKILL] = ['g_skills', [], []]; break; - case TYPE_CURRENCY: $jsg[TYPE_CURRENCY] = ['g_gatheredcurrencies', [], []]; break; - case TYPE_SOUND: $jsg[TYPE_SOUND] = ['g_sounds', [], []]; break; - case TYPE_ICON: $jsg[TYPE_ICON] = ['g_icons', [], []]; break; - // well, this is awkward - case TYPE_USER: $jsg[TYPE_USER] = ['g_users', [], []]; break; - case TYPE_EMOTE: $jsg[TYPE_EMOTE] = ['g_emotes', [], []]; break; - case TYPE_ENCHANTMENT: $jsg[TYPE_ENCHANTMENT] = ['g_enchantments', [], []]; break; - } - } - - private function applyGlobals() // lookup jsGlobals from collected typeIds - { - foreach ($this->jsgBuffer as $type => $ids) - { - foreach ($ids as $k => $id) // filter already generated data, maybe we can save a lookup or two - if (isset($this->jsGlobals[$type][1][$id])) - unset($ids[$k]); - - if (!$ids) - continue; - - $this->initJSGlobal($type); - - $cnd = [CFG_SQL_LIMIT_NONE, ['id', array_unique($ids, SORT_NUMERIC)]]; - - switch ($type) - { - case TYPE_NPC: $obj = new CreatureList($cnd); break; - case TYPE_OBJECT: $obj = new GameobjectList($cnd); break; - case TYPE_ITEM: $obj = new ItemList($cnd); break; - case TYPE_ITEMSET: $obj = new ItemsetList($cnd); break; - case TYPE_QUEST: $obj = new QuestList($cnd); break; - case TYPE_SPELL: $obj = new SpellList($cnd); break; - case TYPE_ZONE: $obj = new ZoneList($cnd); break; - case TYPE_FACTION: $obj = new FactionList($cnd); break; - case TYPE_PET: $obj = new PetList($cnd); break; - case TYPE_ACHIEVEMENT: $obj = new AchievementList($cnd); break; - case TYPE_TITLE: $obj = new TitleList($cnd); break; - case TYPE_WORLDEVENT: $obj = new WorldEventList($cnd); break; - case TYPE_CLASS: $obj = new CharClassList($cnd); break; - case TYPE_RACE: $obj = new CharRaceList($cnd); break; - case TYPE_SKILL: $obj = new SkillList($cnd); break; - case TYPE_CURRENCY: $obj = new CurrencyList($cnd); break; - case TYPE_SOUND: $obj = new SoundList($cnd); break; - // "um, eh":, he ums and ehs. - case TYPE_USER: $obj = new UserList($cnd); break; - case TYPE_EMOTE: $obj = new EmoteList($cnd); break; - case TYPE_ENCHANTMENT: $obj = new EnchantmentList($cnd); break; - default: continue; - } - - $this->extendGlobalData($obj->getJSGlobals(GLOBALINFO_SELF)); - - // delete processed ids - $this->jsgBuffer[$type] = []; - } - } - - /*********/ - /* Cache */ - /*********/ - - public function saveCache($saveString = null) // visible properties or given strings are cached - { - if ($this->mode == CACHE_TYPE_NONE) - return false; - - if (!CFG_CACHE_MODE || CFG_DEBUG) - return; - - $noCache = ['coError', 'ssError', 'viError']; - $cKey = $this->generateCacheKey(); - $cache = []; - if (!$saveString) - { - foreach ($this as $key => $val) - { - try - { - // public, protected and an undocumented flag added to properties created on the fly..? - if ((new ReflectionProperty($this, $key))->getModifiers() & 0x1300) - if (!in_array($key, $noCache)) - $cache[$key] = $val; - } - catch (ReflectionException $e) { } // shut up! - } - } - else - $cache = (string)$saveString; - - if (CFG_CACHE_MODE & CACHE_MODE_MEMCACHED) - { - // on &refresh also clear related - if ($this->skipCache == CACHE_MODE_MEMCACHED) - { - $oldMode = $this->mode; - for ($i = 1; $i < 5; $i++) // page (1), tooltips (2), searches (3) and xml (4) - { - $this->mode = $i; - for ($j = 0; $j < 2; $j++) // staff / normal - $this->memcached()->delete($this->generateCacheKey($j)); - } - - $this->mode = $oldMode; - } - - $data = array( - 'timestamp' => time(), - 'revision' => AOWOW_REVISION, - 'isString' => $saveString ? 1 : 0, - 'data' => $cache - ); - - $this->memcached()->set($cKey, $data); - } - - if (CFG_CACHE_MODE & CACHE_MODE_FILECACHE) - { - $data = time()." ".AOWOW_REVISION." ".($saveString ? '1' : '0')."\n"; - $data .= gzcompress($saveString ? $cache : serialize($cache), 9); - - // on &refresh also clear related - if ($this->skipCache == CACHE_MODE_FILECACHE) - { - $oldMode = $this->mode; - for ($i = 1; $i < 5; $i++) // page (1), tooltips (2), searches (3) and xml (4) - { - $this->mode = $i; - for ($j = 0; $j < 2; $j++) // staff / normal - { - $key = $this->generateCacheKey($j); - if (file_exists($this->cacheDir.$key)) - unlink($this->cacheDir.$key); - } - } - - $this->mode = $oldMode; - } - - file_put_contents($this->cacheDir.$cKey, $data); - } - } - - public function loadCache(&$saveString = null) - { - if ($this->mode == CACHE_TYPE_NONE) - return false; - - if (!CFG_CACHE_MODE || CFG_DEBUG) - return false; - - $cKey = $this->generateCacheKey(); - $rev = $type = $cache = $data = null; - - if ((CFG_CACHE_MODE & CACHE_MODE_MEMCACHED) && !($this->skipCache & CACHE_MODE_MEMCACHED)) - { - if ($cache = $this->memcached()->get($cKey)) - { - $type = $cache['isString']; - $data = $cache['data']; - - if ($cache['timestamp'] + CFG_CACHE_DECAY <= time() || $cache['revision'] != AOWOW_REVISION) - $cache = null; - else - $this->cacheLoaded = [CACHE_MODE_MEMCACHED, $cache['timestamp']]; - } - } - - if (!$cache && (CFG_CACHE_MODE & CACHE_MODE_FILECACHE) && !($this->skipCache & CACHE_MODE_FILECACHE)) - { - if (!file_exists($this->cacheDir.$cKey)) - return false; - - $cache = file_get_contents($this->cacheDir.$cKey); - if (!$cache) - return false; - - $cache = explode("\n", $cache, 2); - $data = $cache[1]; - if (substr_count($cache[0], ' ') < 2) - return false; - - list($time, $rev, $type) = explode(' ', $cache[0]); - - if ($time + CFG_CACHE_DECAY <= time() || $rev != AOWOW_REVISION) - $cache = null; - else - { - $this->cacheLoaded = [CACHE_MODE_FILECACHE, $time]; - $data = gzuncompress($data); - } - } - - if (!$cache) - return false; - - if ($type == '0') - { - if (is_string($data)) - $data = unserialize($data); - - foreach ($data as $k => $v) - $this->$k = $v; - - return true; - } - else if ($type == '1') - { - $saveString = $data; - return true; - } - - return false;; - } - - private function memcached() - { - if (!$this->memcached && (CFG_CACHE_MODE & CACHE_MODE_MEMCACHED)) - { - $this->memcached = new Memcached(); - $this->memcached->addServer('localhost', 11211); - } - - return $this->memcached; - } -} - -?> diff --git a/pages/guild.php b/pages/guild.php deleted file mode 100644 index 39a3ee0d..00000000 --- a/pages/guild.php +++ /dev/null @@ -1,143 +0,0 @@ - 'Profiler.css']]; - - public function __construct($pageCall, $pageParam) - { - $params = array_map('urldecode', explode('.', $pageParam)); - if ($params[0]) - $params[0] = Profiler::urlize($params[0]); - if (isset($params[1])) - $params[1] = Profiler::urlize($params[1]); - - parent::__construct($pageCall, $pageParam); - - if (count($params) == 1 && intval($params[0])) - { - $this->subject = new LocalGuildList(array(['g.id', intval($params[0])])); - if ($this->subject->error) - $this->notFound(); - - header('Location: '.$this->subject->getProfileUrl(), true, 302); - } - else if (count($params) == 3) - { - $this->getSubjectFromUrl($pageParam); - if (!$this->subjectName) - $this->notFound(); - - // 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['cuFlags'] & PROFILER_CU_NEEDS_RESYNC) - { - $this->handleIncompleteData($subject['realmGUID']); - return; - } - - $this->subjectGUID = $subject['id']; - $this->subject = new LocalGuildList(array(['id', $subject['id']])); - if ($this->subject->error) - $this->notFound(); - - $this->profile = $params; - $this->name = sprintf(Lang::profiler('guildRoster'), $this->subject->getField('name')); - } - // 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 ($team = DB::Characters($this->realmId)->selectRow('SELECT guildid AS realmGUID, name FROM guild WHERE name = ?', Util::ucFirst($this->subjectName))) - { - $team['realm'] = $this->realmId; - $team['cuFlags'] = PROFILER_CU_NEEDS_RESYNC; - - // create entry from realm with basic info - DB::Aowow()->query('INSERT IGNORE INTO ?_profiler_guild (?#) VALUES (?a)', array_keys($team), array_values($team)); - - $this->handleIncompleteData($team['realmGUID']); - } - // 3) does not exist at all - else - $this->notFound(); - } - else - $this->notFound(); - } - - protected function generateTitle() - { - $team = !empty($this->subject) ? $this->subject->getField('name') : $this->subjectName; - $team .= ' ('.$this->realm.' - '.Lang::profiler('regions', $this->region).')'; - - array_unshift($this->title, $team, Util::ucFirst(Lang::profiler('profiler'))); - } - - protected function generateContent() - { - if ($this->doResync) - return; - - $this->addJS('?data=realms.weight-presets&locale='.User::$localeId.'&t='.$_SESSION['dataKey']); - - $this->redButtons[BUTTON_RESYNC] = [$this->subjectGUID, 'guild']; - - /****************/ - /* Main Content */ - /****************/ - - - // 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->subjectGUID)) - $this->extraHTML = ''; - - - /**************/ - /* Extra Tabs */ - /**************/ - - // tab: members - $member = new LocalProfileList(array(['p.guild', $this->subjectGUID])); - if (!$member->error) - { - $this->lvTabs[] = ['profile', array( - 'data' => array_values($member->getListviewData(PROFILEINFO_CHARACTER | PROFILEINFO_ARENA)), - 'sort' => [-15], - 'visibleCols' => ['race', 'classs', 'level', 'talents', 'gearscore', 'achievementpoints', 'guildrank'], - 'hiddenCols' => ['guild', 'location'] - )]; - } - } - - public function notFound($title = '', $msg = '') - { - return parent::notFound($title ?: Util::ucFirst(Lang::profiler('profiler')), $msg ?: Lang::profiler('notFound', 'guild')); - } - - private function handleIncompleteData($teamGuid) - { - //display empty page and queue status - $newId = Profiler::scheduleResync(TYPE_GUILD, $this->realmId, $teamGuid); - - $this->doResync = ['guild', $newId]; - $this->initialSync(); - } -} - -?> diff --git a/pages/guilds.php b/pages/guilds.php deleted file mode 100644 index 05aae2fa..00000000 --- a/pages/guilds.php +++ /dev/null @@ -1,116 +0,0 @@ -getSubjectFromUrl($pageParam); - - $this->filterObj = new GuildListFilter(); - - foreach (Profiler::getRealms() as $idx => $r) - { - if ($this->region && $r['region'] != $this->region) - continue; - - if ($this->realm && $r['name'] != $this->realm) - continue; - - $this->sumSubjects += DB::Characters($idx)->selectCell('SELECT COUNT(*) FROM guild'); - } - - parent::__construct($pageCall, $pageParam); - - $this->name = Lang::profiler('guilds'); - $this->subCat = $pageParam ? '='.$pageParam : ''; - } - - protected function generateTitle() - { - if ($this->realm) - array_unshift($this->title, $this->realm,/* CFG_BATTLEGROUP,*/ Lang::profiler('regions', $this->region), Lang::profiler('guilds')); - else if ($this->region) - array_unshift($this->title, Lang::profiler('regions', $this->region), Lang::profiler('guilds')); - else - array_unshift($this->title, Lang::profiler('guilds')); - } - - protected function generateContent() - { - $this->addJS('?data=realms&locale='.User::$localeId.'&t='.$_SESSION['dataKey']); - - $conditions = array( - ['c.deleteInfos_Account', null], - ['c.level', MAX_LEVEL, '<='], // prevents JS errors - [['c.extra_flags', Profiler::CHAR_GMFLAGS, '&'], 0] - ); - if ($_ = $this->filterObj->getConditions()) - $conditions[] = $_; - - // recreate form selection - $this->filter = $this->filterObj->getForm(); - $this->filter['query'] = isset($_GET['filter']) ? $_GET['filter'] : null; - $this->filter['initData'] = ['type' => 'guilds']; - - $tabData = array( - 'id' => 'guilds', - 'hideCount' => 1, - 'sort' => [-3], - 'visibleCols' => ['members', 'achievementpoints', 'gearscore'], - 'hiddenCols' => ['guild'], - ); - - $miscParams = []; - if ($this->realm) - $miscParams['sv'] = $this->realm; - if ($this->region) - $miscParams['rg'] = $this->region; - - $guilds = new RemoteGuildList($conditions, $miscParams); - if (!$guilds->error) - { - $guilds->initializeLocalEntries(); - - $dFields = $guilds->hasDiffFields(['faction', 'type']); - if (!($dFields & 0x1)) - $tabData['hiddenCols'][] = 'faction'; - - if (($dFields & 0x2)) - $tabData['visibleCols'][] = 'size'; - - $tabData['data'] = array_values($guilds->getListviewData()); - - // create note if search limit was exceeded - if ($this->filter['query'] && $guilds->getMatches() > CFG_SQL_LIMIT_DEFAULT) - { - $tabData['note'] = sprintf(Util::$tryFilteringString, 'LANG.lvnote_guildsfound2', $this->sumSubjects, $guilds->getMatches()); - $tabData['_truncated'] = 1; - } - else if ($guilds->getMatches() > CFG_SQL_LIMIT_DEFAULT) - $tabData['note'] = sprintf(Util::$tryFilteringString, 'LANG.lvnote_guildsfound', $this->sumSubjects, 0); - - if ($this->filterObj->error) - $tabData['_errors'] = 1; - } - - $this->lvTabs[] = ['profile', $tabData, 'membersCol']; - - Lang::sort('game', 'cl'); - Lang::sort('game', 'ra'); - } -} - -?> diff --git a/pages/home.php b/pages/home.php deleted file mode 100644 index 47feffb4..00000000 --- a/pages/home.php +++ /dev/null @@ -1,62 +0,0 @@ - 'home.css']]; - - protected $featuredBox = []; - protected $oneliner = ''; - - public function __construct() - { - parent::__construct('home'); - } - - protected function generateContent() - { - $this->addCSS(['string' => '.announcement { margin: auto; max-width: 1200px; padding: 0px 15px 15px 15px }']); - - // load oneliner - if ($_ = DB::Aowow()->selectRow('SELECT * FROM ?_home_oneliner WHERE active = 1 LIMIT 1')) - $this->oneliner = Util::jsEscape(Util::localizedString($_, 'text')); - - // load featuredBox (user web server time) - $this->featuredBox = DB::Aowow()->selectRow('SELECT id as ARRAY_KEY, n.* FROM ?_home_featuredbox n WHERE ?d BETWEEN startDate AND endDate ORDER BY id DESC LIMIT 1', time()); - if (!$this->featuredBox) - return; - - $this->featuredBox = Util::defStatic($this->featuredBox); - - $this->featuredBox['text'] = Util::localizedString($this->featuredBox, 'text', true); - - if ($_ = (new Markup($this->featuredBox['text']))->parseGlobalsFromText()) - $this->extendGlobalData($_); - - if (empty($this->featuredBox['boxBG'])) - $this->featuredBox['boxBG'] = STATIC_URL.'/images/'.User::$localeString.'/mainpage-bg-news.jpg'; - - // load overlay links - $this->featuredBox['overlays'] = DB::Aowow()->select('SELECT * FROM ?_home_featuredbox_overlay WHERE featureId = ?d', $this->featuredBox['id']); - foreach ($this->featuredBox['overlays'] as &$o) - { - $o['title'] = Util::localizedString($o, 'title', true); - $o['title'] = Util::defStatic($o['title']); - } - } - - protected function generateTitle() - { - if ($_ = DB::Aowow()->selectRow('SELECT * FROM ?_home_titles WHERE active = 1 AND title_loc?d <> "" ORDER BY RAND() LIMIT 1', User::$localeId)) - $this->title[0] .= Lang::main('colon').Util::localizedString($_, 'title'); - } - - protected function generatePath() {} -} - -?> diff --git a/pages/icon.php b/pages/icon.php deleted file mode 100644 index 2c5d3677..00000000 --- a/pages/icon.php +++ /dev/null @@ -1,116 +0,0 @@ -typeId = intVal($id); - - $this->subject = new IconList(array(['id', $this->typeId])); - if ($this->subject->error) - $this->notFound(Util::ucFirst(Lang::game('icon')), Lang::icon('notFound')); - - $this->extendGlobalData($this->subject->getJSGlobals()); - - $this->name = Util::ucFirst($this->subject->getField('name')); - $this->icon = $this->subject->getField('name', true, true); - } - - protected function generateContent() - { - /****************/ - /* Main Content */ - /****************/ - - $this->redButtons = array( - BUTTON_LINKS => ['type' => $this->type, 'typeId' => $this->typeId], - BUTTON_WOWHEAD => false - ); - - - /**************/ - /* Extra Tabs */ - /**************/ - - // used by: spell - $ubSpells = new SpellList(array(['iconId', $this->typeId])); - if (!$ubSpells->error) - { - $this->extendGlobalData($ubSpells->getJsGlobals()); - $this->lvTabs[] = [SpellList::$brickFile, array( - 'data' => array_values($ubSpells->getListviewData()), - 'id' => 'used-by-spell' - )]; - } - - // used by: item - $ubItems = new ItemList(array(['iconId', $this->typeId])); - if (!$ubItems->error) - { - $this->extendGlobalData($ubItems->getJsGlobals()); - $this->lvTabs[] = [ItemList::$brickFile, array( - 'data' => array_values($ubItems->getListviewData()), - 'id' => 'used-by-item' - )]; - } - - // used by: achievement - $ubAchievements = new AchievementList(array(['iconId', $this->typeId])); - if (!$ubAchievements->error) - { - $this->extendGlobalData($ubAchievements->getJsGlobals()); - $this->lvTabs[] = [AchievementList::$brickFile, array( - 'data' => array_values($ubAchievements->getListviewData()), - 'id' => 'used-by-achievement' - )]; - } - - // used by: currency - $ubCurrencies = new CurrencyList(array(['iconId', $this->typeId])); - if (!$ubCurrencies->error) - { - $this->extendGlobalData($ubCurrencies->getJsGlobals()); - $this->lvTabs[] = [CurrencyList::$brickFile, array( - 'data' => array_values($ubCurrencies->getListviewData()), - 'id' => 'used-by-currency' - )]; - } - - // used by: hunter pet - $ubPets = new PetList(array(['iconId', $this->typeId])); - if (!$ubPets->error) - { - $this->extendGlobalData($ubPets->getJsGlobals()); - $this->lvTabs[] = [PetList::$brickFile, array( - 'data' => array_values($ubPets->getListviewData()), - 'id' => 'used-by-pet' - )]; - } - } - - protected function generateTitle() - { - array_unshift($this->title, $this->name, Util::ucFirst(Lang::game('icon'))); - } - - protected function generatePath() { } -} - -?> diff --git a/pages/icons.php b/pages/icons.php deleted file mode 100644 index c20f7580..00000000 --- a/pages/icons.php +++ /dev/null @@ -1,110 +0,0 @@ -filterObj = new IconListFilter(); - - parent::__construct($pageCall); - - $this->name = Util::ucFirst(Lang::game('icons')); - } - - protected function generateContent() - { - $tabData = array( - 'data' => [], - ); - - $sqlLimit = 600; // fits better onto the grid - - $conditions = [$sqlLimit]; - - if (!User::isInGroup(U_GROUP_EMPLOYEE)) - $conditions[] = [['cuFlags', CUSTOM_EXCLUDE_FOR_LISTVIEW, '&'], 0]; - - if ($_ = $this->filterObj->getConditions()) - $conditions[] = $_; - - $icons = new IconList($conditions); - - $tabData['data'] = array_values($icons->getListviewData()); - $this->extendGlobalData($icons->getJSGlobals()); - - // recreate form selection - $this->filter = $this->filterObj->getForm(); - $this->filter['query'] = isset($_GET['filter']) ? $_GET['filter'] : null; - $this->filter['initData'] = ['init' => 'icons']; - - if ($x = $this->filterObj->getSetCriteria()) - $this->filter['initData']['sc'] = $x; - - if ($icons->getMatches() > $sqlLimit) - { - $tabData['note'] = sprintf(Util::$tryFilteringEntityString, $icons->getMatches(), 'LANG.types[29][3]', $sqlLimit); - $tabData['_truncated'] = 1; - } - - if ($this->filterObj->error) - $tabData['_errors'] = '$1'; - - $this->lvTabs[] = ['icongallery', $tabData]; - } - - protected function generateTitle() - { - $setCrt = $this->filterObj->getSetCriteria(); - $title = $this->name; - if (isset($setCrt['cr']) && count($setCrt['cr']) == 1) - { - switch ($setCrt['cr'][0]) - { - case 1: - $title = Util::ucFirst(Lang::game('item')).' '.$title; - break; - case 2: - $title = Util::ucFirst(Lang::game('spell')).' '.$title; - break; - case 3: - $title = Util::ucFirst(Lang::game('achievement')).' '.$title; - break; - case 6: - $title = Util::ucFirst(Lang::game('currency')).' '.$title; - break; - case 9: - $title = Util::ucFirst(Lang::game('pet')).' '.$title; - break; - case 11: - $title = Util::ucFirst(Lang::game('class')).' '.$title; - break; - } - } - - array_unshift($this->title, $title); - } - - protected function generatePath() - { - $setCrt = $this->filterObj->getSetCriteria(); - if (isset($setCrt['cr']) && count($setCrt['cr']) == 1) - $this->path[] = $setCrt['cr'][0]; - } -} - -?> diff --git a/pages/item.php b/pages/item.php deleted file mode 100644 index fa65ac21..00000000 --- a/pages/item.php +++ /dev/null @@ -1,1194 +0,0 @@ -typeId = intVal($param); - - if ($this->mode == CACHE_TYPE_TOOLTIP) - { - // temp locale - if (isset($_GET['domain'])) - Util::powerUseLocale($_GET['domain']); - - if (isset($_GET['rand'])) - $this->enhancedTT['r'] = $_GET['rand']; - if (isset($_GET['ench'])) - $this->enhancedTT['e'] = $_GET['ench']; - if (isset($_GET['gems'])) - $this->enhancedTT['g'] = explode(':', $_GET['gems']); - if (isset($_GET['sock'])) - $this->enhancedTT['s'] = ''; - } - else if ($this->mode == CACHE_TYPE_XML) - { - // temp locale - if (isset($_GET['domain'])) - Util::powerUseLocale($_GET['domain']); - - // allow lookup by name for xml - if (!is_numeric($param)) - $conditions = [['name_loc'.User::$localeId, urldecode($param)]]; - } - - $this->subject = new ItemList($conditions); - if ($this->subject->error) - $this->notFound(); - - if (!is_numeric($param)) - $this->typeId = $this->subject->id; - - $this->name = $this->subject->getField('name', true); - - if ($this->mode == CACHE_TYPE_PAGE) - { - $jsg = $this->subject->getJSGlobals(GLOBALINFO_EXTRA | GLOBALINFO_SELF, $extra); - $this->extendGlobalData($jsg, $extra); - } - } - - protected function generatePath() - { - $_class = $this->subject->getField('class'); - $_subClass = $this->subject->getField('subClass'); - - if (in_array($_class, [ITEM_CLASS_REAGENT, ITEM_CLASS_GENERIC, ITEM_CLASS_PERMANENT])) - { - $this->path[] = ITEM_CLASS_MISC; // misc. - - if ($_class == ITEM_CLASS_REAGENT) // reagent - $this->path[] = 1; - else // other - $this->path[] = 4; - } - else - { - $this->path[] = $_class; - - if (!in_array($_class, [ITEM_CLASS_MONEY, ITEM_CLASS_QUEST, ITEM_CLASS_KEY])) - $this->path[] = $_subClass; - - if ($_class == ITEM_CLASS_ARMOR && in_array($_subClass, [1, 2, 3, 4])) - { - if ($_ = $this->subject->getField('slot')); - $this->path[] = $_; - } - else if (($_class == ITEM_CLASS_CONSUMABLE && $_subClass == 2) || $_class == ITEM_CLASS_GLYPH) - $this->path[] = $this->subject->getField('subSubClass'); - } - } - - protected function generateTitle() - { - array_unshift($this->title, $this->subject->getField('name', true), Util::ucFirst(Lang::game('item'))); - } - - protected function generateContent() - { - $this->addJS('?data=weight-presets.zones&locale='.User::$localeId.'&t='.$_SESSION['dataKey']); - - $_flags = $this->subject->getField('flags'); - $_slot = $this->subject->getField('slot'); - $_class = $this->subject->getField('class'); - $_subClass = $this->subject->getField('subClass'); - $_bagFamily = $this->subject->getField('bagFamily'); - $_model = $this->subject->getField('displayId'); - $_visSlots = array( - INVTYPE_HEAD, INVTYPE_SHOULDERS, INVTYPE_BODY, INVTYPE_CHEST, INVTYPE_WAIST, INVTYPE_LEGS, INVTYPE_FEET, INVTYPE_WRISTS, - INVTYPE_HANDS, INVTYPE_WEAPON, INVTYPE_SHIELD, INVTYPE_RANGED, INVTYPE_CLOAK, INVTYPE_2HWEAPON, INVTYPE_TABARD, INVTYPE_ROBE, - INVTYPE_WEAPONMAINHAND, INVTYPE_WEAPONOFFHAND, INVTYPE_HOLDABLE, INVTYPE_THROWN, INVTYPE_RANGEDRIGHT - ); - - /***********/ - /* Infobox */ - /***********/ - - $infobox = Lang::getInfoBoxForFlags($this->subject->getField('cuFlags')); - - // itemlevel - if (in_array($_class, [ITEM_CLASS_ARMOR, ITEM_CLASS_WEAPON, ITEM_CLASS_AMMUNITION]) || $this->subject->getField('gemEnchantmentId')) - $infobox[] = Lang::game('level').Lang::main('colon').$this->subject->getField('itemLevel'); - - // account-wide - if ($_flags & ITEM_FLAG_ACCOUNTBOUND) - $infobox[] = Lang::item('accountWide'); - - // side - if ($si = $this->subject->json[$this->typeId]['side']) - if ($si != 3) - $infobox[] = Lang::main('side').Lang::main('colon').'[span class=icon-'.($si == 1 ? 'alliance' : 'horde').']'.Lang::game('si', $si).'[/span]'; - - // consumable / not consumable - if (!$_slot) - { - $hasUse = false; - for ($i = 1; $i < 6; $i++) - { - if ($this->subject->getField('spellId'.$i) <= 0 || in_array($this->subject->getField('spellTrigger'.$i), [1, 2])) - continue; - - $hasUse = true; - - if ($this->subject->getField('spellCharges'.$i) >= 0) - continue; - - $tt = '[tooltip=tooltip_consumedonuse]'.Lang::item('consumable').'[/tooltip]'; - break; - } - - if ($hasUse) - $infobox[] = isset($tt) ? $tt : '[tooltip=tooltip_notconsumedonuse]'.Lang::item('nonConsumable').'[/tooltip]'; - } - - // related holiday - if ($eId = $this->subject->getField('eventId')) - { - $this->extendGlobalIds(TYPE_WORLDEVENT, $eId); - $infobox[] = Lang::game('eventShort').Lang::main('colon').'[event='.$eId.']'; - } - - // tool - if ($tId = $this->subject->getField('totemCategory')) - if ($tName = DB::Aowow()->selectRow('SELECT * FROM ?_totemcategory WHERE id = ?d', $tId)) - $infobox[] = Lang::item('tool').Lang::main('colon').'[url=?items&filter=cr=91;crs='.$tId.';crv=0]'.Util::localizedString($tName, 'name').'[/url]'; - - // extendedCost - if (!empty($this->subject->getExtendedCost([], $_reqRating)[$this->subject->id])) - { - $vendors = $this->subject->getExtendedCost()[$this->subject->id]; - $each = $this->subject->getField('stackable') > 1 ? '[color=q0] ('.Lang::item('each').')[/color]' : null; - $handled = []; - $costList = []; - foreach ($vendors as $npcId => $data) - { - $tokens = []; - $currency = []; - - if (!is_array($data)) - continue; - - foreach ($data as $c => $qty) - { - if (is_string($c)) - { - unset($data[$c]); // unset miscData to prevent having two vendors /w the same cost being cached, because of different stock or rating-requirements - continue; - } - - if ($c < 0) // currency items (and honor or arena) - $currency[] = -$c.','.$qty; - else if ($c > 0) // plain items (item1,count1,item2,count2,...) - $tokens[$c] = $c.','.$qty; - } - - // display every cost-combination only once - if (in_array(md5(serialize($data)), $handled)) - continue; - - $handled[] = md5(serialize($data)); - - $cost = isset($data[0]) ? '[money='.$data[0] : '[money'; - - if ($tokens) - $cost .= ' items='.implode(',', $tokens); - - if ($currency) - $cost .= ' currency='.implode(',', $currency); - - $cost .= ']'; - - $costList[] = $cost; - } - - if (count($costList) == 1) - $infobox[] = Lang::item('cost').Lang::main('colon').$costList[0].$each; - else if (count($costList) > 1) - $infobox[] = Lang::item('cost').$each.Lang::main('colon').'[ul][li]'.implode('[/li][li]', $costList).'[/li][/ul]'; - - if ($_reqRating) - { - $res = []; - $i = 0; - $len = 0; - $parts = explode(' ', str_replace('
', ' ', sprintf(Lang::item('reqRating', $_reqRating[1]), $_reqRating[0]))); - foreach ($parts as $p) - { - $res[$i][] = $p; - $len += (mb_strlen($p) + 1); - - if ($len < 30) - continue; - - $len = 0; - $i++; - } - foreach ($res as &$r) - $r = implode(' ', $r); - - $infobox[] = implode('[br]', $res); - } - } - - // repair cost - if ($_ = $this->subject->getField('repairPrice')) - $infobox[] = Lang::item('repairCost').Lang::main('colon').'[money='.$_.']'; - - // avg auction buyout - if (in_array($this->subject->getField('bonding'), [0, 2, 3])) - if ($_ = Profiler::getBuyoutForItem($this->typeId)) - $infobox[] = '[tooltip=tooltip_buyoutprice]'.Lang::item('buyout.').'[/tooltip]'.Lang::main('colon').'[money='.$_.']'.$each; - - // avg money contained - if ($_flags & ITEM_FLAG_OPENABLE) - if ($_ = intVal(($this->subject->getField('minMoneyLoot') + $this->subject->getField('maxMoneyLoot')) / 2)) - $infobox[] = Lang::item('worth').Lang::main('colon').'[tooltip=tooltip_avgmoneycontained][money='.$_.'][/tooltip]'; - - // if it goes into a slot it may be disenchanted - if ($_slot && $_class != ITEM_CLASS_CONTAINER) - { - if ($this->subject->getField('disenchantId')) - { - $_ = $this->subject->getField('requiredDisenchantSkill'); - if ($_ < 1) // these are some items, that never went live .. extremely rough emulation here - $_ = intVal($this->subject->getField('itemLevel') / 7.5) * 25; - - $infobox[] = Lang::item('disenchantable').' ([tooltip=tooltip_reqenchanting]'.$_.'[/tooltip])'; - } - else - $infobox[] = Lang::item('cantDisenchant'); - } - - if (($_flags & ITEM_FLAG_MILLABLE) && $this->subject->getField('requiredSkill') == 773) - $infobox[] = Lang::item('millable').' ([tooltip=tooltip_reqinscription]'.$this->subject->getField('requiredSkillRank').'[/tooltip])'; - - if (($_flags & ITEM_FLAG_PROSPECTABLE) && $this->subject->getField('requiredSkill') == 755) - $infobox[] = Lang::item('prospectable').' ([tooltip=tooltip_reqjewelcrafting]'.$this->subject->getField('requiredSkillRank').'[/tooltip])'; - - if ($_flags & ITEM_FLAG_DEPRECATED) - $infobox[] = '[tooltip=tooltip_deprecated]'.Lang::item('deprecated').'[/tooltip]'; - - if ($_flags & ITEM_FLAG_NO_EQUIPCD) - $infobox[] = '[tooltip=tooltip_noequipcooldown]'.Lang::item('noEquipCD').'[/tooltip]'; - - if ($_flags & ITEM_FLAG_PARTYLOOT) - $infobox[] = '[tooltip=tooltip_partyloot]'.Lang::item('partyLoot').'[/tooltip]'; - - if ($_flags & ITEM_FLAG_REFUNDABLE) - $infobox[] = '[tooltip=tooltip_refundable]'.Lang::item('refundable').'[/tooltip]'; - - if ($_flags & ITEM_FLAG_SMARTLOOT) - $infobox[] = '[tooltip=tooltip_smartloot]'.Lang::item('smartLoot').'[/tooltip]'; - - if ($_flags & ITEM_FLAG_INDESTRUCTIBLE) - $infobox[] = Lang::item('indestructible'); - - if ($_flags & ITEM_FLAG_USABLE_ARENA) - $infobox[] = Lang::item('useInArena'); - - if ($_flags & ITEM_FLAG_USABLE_SHAPED) - $infobox[] = Lang::item('useInShape'); - - // cant roll need - if ($this->subject->getField('flagsExtra') & 0x0100) - $infobox[] = '[tooltip=tooltip_cannotrollneed]'.Lang::item('noNeedRoll').'[/tooltip]'; - - // fits into keyring - if ($_bagFamily & 0x0100) - $infobox[] = Lang::item('atKeyring'); - - /****************/ - /* Main Content */ - /****************/ - - $_cu = in_array($_class, [ITEM_CLASS_WEAPON, ITEM_CLASS_ARMOR]) || $this->subject->getField('gemEnchantmentId'); - - // pageText - $pageText = []; - if ($this->pageText = Game::getPageText($this->subject->getField('pageTextId'))) - { - $this->addJS('Book.js'); - $this->addCSS(['path' => 'Book.css']); - } - - $this->headIcons = [$this->subject->getField('iconString'), $this->subject->getField('stackable')]; - $this->infobox = $infobox ? '[ul][li]'.implode('[/li][li]', $infobox).'[/li][/ul]' : null; - $this->tooltip = $this->subject->renderTooltip(true); - $this->redButtons = array( - BUTTON_WOWHEAD => true, - BUTTON_VIEW3D => in_array($_slot, $_visSlots) && $_model ? ['displayId' => $this->subject->getField('displayId'), 'slot' => $_slot, 'type' => TYPE_ITEM, 'typeId' => $this->typeId] : false, - BUTTON_COMPARE => $_cu, - BUTTON_EQUIP => in_array($_class, [ITEM_CLASS_WEAPON, ITEM_CLASS_ARMOR]), - BUTTON_UPGRADE => ($_cu ? ['class' => $_class, 'slot' => $_slot] : false), - BUTTON_LINKS => array( - 'linkColor' => 'ff'.Game::$rarityColorStings[$this->subject->getField('quality')], - 'linkId' => 'item:'.$this->typeId.':0:0:0:0:0:0:0:0', - 'linkName' => $this->name, - 'type' => $this->type, - 'typeId' => $this->typeId - ) - ); - - // availablility - $this->unavailable = $this->subject->getField('cuFlags') & CUSTOM_UNAVAILABLE; - - // subItems - $this->subject->initSubItems(); - if (!empty($this->subject->subItems[$this->typeId])) - { - uaSort($this->subject->subItems[$this->typeId], function($a, $b) { return strcmp($a['name'], $b['name']); }); - $this->subItems = array( - 'data' => array_values($this->subject->subItems[$this->typeId]), - 'randIds' => array_keys($this->subject->subItems[$this->typeId]), - 'quality' => $this->subject->getField('quality') - ); - - // merge identical stats and names for normal users (e.g. spellPower of a specific school became generel spellPower with 3.0) - - if (!User::isInGroup(U_GROUP_EMPLOYEE)) - { - for ($i = 1; $i < count($this->subItems['data']); $i++) - { - $prev = &$this->subItems['data'][$i - 1]; - $cur = &$this->subItems['data'][$i]; - if ($prev['jsonequip'] == $cur['jsonequip'] && $prev['name'] == $cur['name']) - { - $prev['chance'] += $cur['chance']; - array_splice($this->subItems['data'], $i , 1); - array_splice($this->subItems['randIds'], $i , 1); - $i = 1; - } - } - } - } - - // 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)) - { - $altItem = new ItemList(array(['id', abs($pendant)])); - if (!$altItem->error) - { - $this->transfer = sprintf( - Lang::item('_transfer'), - $altItem->id, - $altItem->getField('quality'), - $altItem->getField('iconString'), - $altItem->getField('name', true), - $pendant > 0 ? 'alliance' : 'horde', - $pendant > 0 ? Lang::game('si', 1) : Lang::game('si', 2) - ); - } - } - - /**************/ - /* Extra Tabs */ - /**************/ - - // 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)) - { - $perfSpells = new SpellList(array(['id', array_column($perfItem, 'spellId')])); - if (!$perfSpells->error) - { - $lvData = $perfSpells->getListviewData(); - $this->extendGlobalData($perfSpells->getJSGlobals(GLOBALINFO_RELATED)); - - foreach ($lvData as $sId => &$data) - { - $data['percent'] = $perfItem[$sId]['perfectCreateChance']; - $data['condition'][0][$this->typeId] = [[[CND_SPELL, $perfItem[$sId]['requiredSpecialization']]]]; - $this->extendGlobalIDs(TYPE_SPELL, $perfItem[$sId]['requiredSpecialization']); - } - - $this->lvTabs[] = ['spell', array( - 'data' => array_values($lvData), - 'name' => '$LANG.tab_createdby', - 'id' => 'created-by', // should by exclusive with created-by from spell_loot - 'extraCols' => ['$Listview.extraCols.percent', '$Listview.extraCols.condition'] - )]; - } - } - - // tabs: this item is contained in.. - $lootTabs = new Loot(); - $createdBy = []; - if ($lootTabs->getByItem($this->typeId)) - { - $this->extendGlobalData($lootTabs->jsGlobals); - - foreach ($lootTabs->iterate() as $idx => list($file, $tabData)) - { - if (!$tabData['data']) - continue; - - if ($idx == 16) - $createdBy = array_column($tabData['data'], 'id'); - - $this->lvTabs[] = [$file, $tabData]; - } - } - - // 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'], []] - ); - - $reqQuest = []; - foreach ($sourceFor as $sf) - { - $lootTab = new Loot(); - if ($lootTab->getByContainer($sf[0], $sf[1])) - { - $this->extendGlobalData($lootTab->jsGlobals); - $sf[4] = array_merge($sf[4], $lootTab->extraCols); - - foreach ($lootTab->iterate() as $lv) - { - if (!$lv['quest']) - continue; - - $sf[4] = array_merge($sf[4], ['Listview.extraCols.condition']); - - $reqQuest[$lv['id']] = 0; - - $lv['condition'][0][$this->typeId][] = [[CND_QUESTTAKEN, &$reqQuest[$lv['id']]]]; - } - - $tabData = array( - 'data' => array_values($lootTab->getResult()), - 'name' => $sf[2], - 'id' => $sf[3], - ); - - if ($sf[4]) - $tabData['extraCols'] = array_unique($sf[4]); - - if ($sf[5]) - $tabData['hiddenCols'] = array_unique($sf[5]); - - if ($sf[6]) - $tabData['visibleCols'] = array_unique($sf[6]); - - $this->lvTabs[] = ['item', $tabData]; - } - } - - if ($reqIds = array_keys($reqQuest)) // apply quest-conditions as back-reference - { - $conditions = array( - 'OR', - ['reqSourceItemId1', $reqIds], ['reqSourceItemId2', $reqIds], - ['reqSourceItemId3', $reqIds], ['reqSourceItemId4', $reqIds], - ['reqItemId1', $reqIds], ['reqItemId2', $reqIds], ['reqItemId3', $reqIds], - ['reqItemId4', $reqIds], ['reqItemId5', $reqIds], ['reqItemId6', $reqIds] - ); - - $reqQuests = new QuestList($conditions); - $reqQuests->getJSGlobals(GLOBALINFO_SELF); - - foreach ($reqQuests->iterate() as $qId => $__) - { - if (empty($reqQuests->requires[$qId][TYPE_ITEM])) - continue; - - foreach ($reqIds as $rId) - if (in_array($rId, $reqQuests->requires[$qId][TYPE_ITEM])) - $reqQuest[$rId] = $reqQuests->id; - } - } - - // tab: container can contain - if ($this->subject->getField('slots') > 0) - { - $contains = new ItemList(array(['bagFamily', $_bagFamily, '&'], ['slots', 1, '<'], CFG_SQL_LIMIT_NONE)); - if (!$contains->error) - { - $this->extendGlobalData($contains->getJSGlobals(GLOBALINFO_SELF)); - - $hCols = ['side']; - if (!$contains->hasSetFields(['slot'])) - $hCols[] = 'slot'; - - $this->lvTabs[] = ['item', array( - 'data' => array_values($contains->getListviewData()), - 'name' => '$LANG.tab_cancontain', - 'id' => 'can-contain', - 'hiddenCols' => $hCols - )]; - } - } - - // tab: can be contained in (except keys) - else if ($_bagFamily != 0x0100) - { - $contains = new ItemList(array(['bagFamily', $_bagFamily, '&'], ['slots', 0, '>'], CFG_SQL_LIMIT_NONE)); - if (!$contains->error) - { - $this->extendGlobalData($contains->getJSGlobals(GLOBALINFO_SELF)); - - $this->lvTabs[] = ['item', array( - 'data' => array_values($contains->getListviewData()), - 'name' => '$LANG.tab_canbeplacedin', - 'id' => 'can-be-placed-in', - 'hiddenCols' => ['side'] - )]; - } - } - - // tab: criteria of - $conditions = array( - ['ac.type', [ACHIEVEMENT_CRITERIA_TYPE_OWN_ITEM, ACHIEVEMENT_CRITERIA_TYPE_USE_ITEM, ACHIEVEMENT_CRITERIA_TYPE_LOOT_ITEM, ACHIEVEMENT_CRITERIA_TYPE_EQUIP_ITEM]], - ['ac.value1', $this->typeId] - ); - - $criteriaOf = new AchievementList($conditions); - if (!$criteriaOf->error) - { - $this->extendGlobalData($criteriaOf->getJSGlobals(GLOBALINFO_SELF | GLOBALINFO_REWARDS)); - - $tabData = array( - 'data' => array_values($criteriaOf->getListviewData()), - 'name' => '$LANG.tab_criteriaof', - 'id' => 'criteria-of', - 'visibleCols' => ['category'] - ); - - if (!$criteriaOf->hasSetFields(['reward_loc0'])) - $tabData['hiddenCols'] = ['rewards']; - - $this->lvTabs[] = ['achievement', $tabData]; - } - - // tab: reagent for - $conditions = array( - '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] - ); - - $reagent = new SpellList($conditions); - if (!$reagent->error) - { - $this->extendGlobalData($reagent->getJSGlobals(GLOBALINFO_SELF | GLOBALINFO_RELATED)); - - $this->lvTabs[] = ['spell', array( - 'data' => array_values($reagent->getListviewData()), - 'name' => '$LANG.tab_reagentfor', - 'id' => 'reagent-for', - 'visibleCols' => ['reagents'] - )]; - } - - // 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 - ); - - if ($lockIds) - { - // objects - $lockedObj = new GameObjectList(array(['lockId', $lockIds])); - if (!$lockedObj->error) - { - $this->lvTabs[] = ['object', array( - 'data' => array_values($lockedObj->getListviewData()), - 'name' => '$LANG.tab_unlocks', - 'id' => 'unlocks-object' - )]; - } - - // items (generally unused. It's the spell on the item, that unlocks stuff) - $lockedItm = new ItemList(array(['lockId', $lockIds])); - if (!$lockedItm->error) - { - $this->extendGlobalData($lockedItm->getJSGlobals(GLOBALINFO_SELF)); - - $this->lvTabs[] = ['item', array( - 'data' => array_values($lockedItm->getListviewData()), - 'name' => '$LANG.tab_unlocks', - 'id' => 'unlocks-item' - )]; - } - } - - // tab: see also - $conditions = array( - ['id', $this->typeId, '!'], - [ - 'OR', - ['name_loc'.User::$localeId, $this->subject->getField('name', true)], - [ - 'AND', - ['class', $_class], - ['subClass', $_subClass], - ['slot', $_slot], - ['itemLevel', $this->subject->getField('itemLevel') - 15, '>'], - ['itemLevel', $this->subject->getField('itemLevel') + 15, '<'], - ['quality', $this->subject->getField('quality')], - ['requiredClass', $this->subject->getField('requiredClass')] - ] - ] - ); - - $saItems = new ItemList($conditions); - if (!$saItems->error) - { - $this->extendGlobalData($saItems->getJSGlobals(GLOBALINFO_SELF)); - - $this->lvTabs[] = ['item', array( - 'data' => array_values($saItems->getListviewData()), - 'name' => '$LANG.tab_seealso', - 'id' => 'see-also' - )]; - } - - // tab: starts (quest) - if ($qId = $this->subject->getField('startQuest')) - { - $starts = new QuestList(array(['id', $qId])); - if (!$starts->error) - { - $this->extendGlobalData($starts->getJSGlobals(GLOBALINFO_SELF | GLOBALINFO_REWARDS)); - - $this->lvTabs[] = ['quest', array( - 'data' => array_values($starts->getListviewData()), - 'name' => '$LANG.tab_starts', - 'id' => 'starts-quest' - )]; - } - } - - // tab: objective of (quest) - $conditions = array( - 'OR', - ['reqItemId1', $this->typeId], ['reqItemId2', $this->typeId], ['reqItemId3', $this->typeId], - ['reqItemId4', $this->typeId], ['reqItemId5', $this->typeId], ['reqItemId6', $this->typeId] - ); - $objective = new QuestList($conditions); - if (!$objective->error) - { - $this->extendGlobalData($objective->getJSGlobals(GLOBALINFO_SELF | GLOBALINFO_REWARDS)); - - $this->lvTabs[] = ['quest', array( - 'data' => array_values($objective->getListviewData()), - 'name' => '$LANG.tab_objectiveof', - 'id' => 'objective-of-quest' - )]; - } - - // tab: provided for (quest) - $conditions = array( - 'OR', ['sourceItemId', $this->typeId], - ['reqSourceItemId1', $this->typeId], ['reqSourceItemId2', $this->typeId], - ['reqSourceItemId3', $this->typeId], ['reqSourceItemId4', $this->typeId] - ); - $provided = new QuestList($conditions); - if (!$provided->error) - { - $this->extendGlobalData($provided->getJSGlobals(GLOBALINFO_SELF | GLOBALINFO_REWARDS)); - - $this->lvTabs[] = ['quest', array( - 'data' => array_values($provided->getListviewData()), - 'name' => '$LANG.tab_providedfor', - 'id' => 'provided-for-quest' - )]; - } - - // 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[] = ['genericmodel', array( - 'data' => array_values($sameModel->getListviewData(ITEMINFO_MODEL)), - 'name' => '$LANG.tab_samemodelas', - 'id' => 'same-model-as', - 'genericlinktype' => 'item' - )]; - } - } - - // tab: sold by - if (!empty($this->subject->getExtendedCost()[$this->subject->id])) - { - $vendors = $this->subject->getExtendedCost()[$this->subject->id]; - $soldBy = new CreatureList(array(['id', array_keys($vendors)])); - if (!$soldBy->error) - { - $sbData = $soldBy->getListviewData(); - $this->extendGlobalData($soldBy->getJSGlobals(GLOBALINFO_SELF)); - - $extraCols = ['$Listview.extraCols.stock', "\$Listview.funcBox.createSimpleCol('stack', 'stack', '10%', 'stack')", '$Listview.extraCols.cost']; - - $holidays = []; - foreach ($sbData as $k => &$row) - { - $currency = []; - $tokens = []; - foreach ($vendors[$k] as $id => $qty) - { - if (is_string($id)) - continue; - - if ($id > 0) - $tokens[] = [$id, $qty]; - else if ($id < 0) - $currency[] = [-$id, $qty]; - } - - if ($currency) - $this->extendGlobalIds(TYPE_CURRENCY, array_column($currency, 0)); - - if ($tokens) - $this->extendGlobalIds(TYPE_ITEM, array_column($tokens, 0)); - - $row['stock'] = $vendors[$k]['stock']; - $row['cost'] = [empty($vendors[$k][0]) ? 0 : $vendors[$k][0]]; - - if ($e = $vendors[$k]['event']) - { - if (count($extraCols) == 3) - $extraCols[] = '$Listview.extraCols.condition'; - - $this->extendGlobalIds(TYPE_WORLDEVENT, $e); - $row['condition'][0][$this->typeId][] = [[CND_ACTIVE_EVENT, $e]]; - } - - if ($currency || $tokens) // fill idx:3 if required - $row['cost'][] = $currency; - - if ($tokens) - $row['cost'][] = $tokens; - - if ($x = $this->subject->getField('buyPrice')) - $row['buyprice'] = $x; - - if ($x = $this->subject->getField('sellPrice')) - $row['sellprice'] = $x; - - if ($x = $this->subject->getField('buyCount')) - $row['stack'] = $x; - } - - - $this->lvTabs[] = ['creature', array( - 'data' => array_values($sbData), - 'name' => '$LANG.tab_soldby', - 'id' => 'sold-by-npc', - 'extraCols' => $extraCols, - 'hiddenCols' => ['level', 'type'] - )]; - } - } - - // tab: currency for - // some minor trickery: get arenaPoints(43307) and honorPoints(43308) directly - if ($this->typeId == 43307) - { - $n = '?items&filter=cr=145;crs=1;crv=0'; - $w = 'reqArenaPoints > 0'; - } - else if ($this->typeId == 43308) - { - $n = '?items&filter=cr=144;crs=1;crv=0'; - $w = 'reqHonorPoints > 0'; - } - else - { - $n = in_array($this->typeId, [42, 61, 81, 241, 121, 122, 123, 125, 126, 161, 201, 101, 102, 221, 301, 341]) ? '?items&filter=cr=158;crs='.$this->typeId.';crv=0' : null; - $w = 'reqItemId1 = '.$this->typeId.' OR reqItemId2 = '.$this->typeId.' OR reqItemId3 = '.$this->typeId.' OR reqItemId4 = '.$this->typeId.' OR reqItemId5 = '.$this->typeId; - } - - $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; - if ($boughtBy) - { - $boughtBy = new ItemList(array(['id', $boughtBy])); - if (!$boughtBy->error) - { - $iCur = new CurrencyList(array(['itemId', $this->typeId])); - $filter = $iCur->error ? [TYPE_ITEM => $this->typeId] : [TYPE_CURRENCY => $iCur->id]; - - $tabData = array( - 'data' => array_values($boughtBy->getListviewData(ITEMINFO_VENDOR, $filter)), - 'name' => '$LANG.tab_currencyfor', - 'id' => 'currency-for', - 'extraCols' => ["\$Listview.funcBox.createSimpleCol('stack', 'stack', '10%', 'stack')", '$Listview.extraCols.cost'], - ); - - if ($boughtBy->getMatches() > CFG_SQL_LIMIT_DEFAULT && $n) - $tabData['note'] = sprintf(Util::$filterResultString, $n); - - $this->lvTabs[] = ['item', $tabData]; - - $this->extendGlobalData($boughtBy->getJSGlobals(GLOBALINFO_SELF | GLOBALINFO_RELATED)); - } - } - - // tab: teaches - $ids = $indirect = []; - for ($i = 1; $i < 6; $i++) - { - if ($this->subject->getField('spellTrigger'.$i) == 6) - $ids[] = $this->subject->getField('spellId'.$i); - else if ($this->subject->getField('spellTrigger'.$i) == 0 && $this->subject->getField('spellId'.$i) > 0) - $indirect[] = $this->subject->getField('spellId'.$i); - } - - // taught indirectly - if ($indirect) - { - $indirectSpells = new SpellList(array(['id', $indirect])); - foreach ($indirectSpells->iterate() as $__) - if ($_ = $indirectSpells->canTeachSpell()) - foreach ($_ as $idx) - $ids[] = $indirectSpells->getField('effect'.$idx.'TriggerSpell'); - - $ids = array_merge($ids, Game::getTaughtSpells($indirect)); - } - - if ($ids) - { - $taughtSpells = new SpellList(array(['id', $ids])); - if (!$taughtSpells->error) - { - $this->extendGlobalData($taughtSpells->getJSGlobals(GLOBALINFO_SELF | GLOBALINFO_RELATED)); - - $visCols = ['level', 'schools']; - if ($taughtSpells->hasSetFields(['reagent1'])) - $visCols[] = 'reagents'; - - $this->lvTabs[] = ['spell', array( - 'data' => array_values($taughtSpells->getListviewData()), - 'name' => '$LANG.tab_teaches', - 'id' => 'teaches', - 'visibleCols' => $visCols - )]; - } - } - - // tab: Shared cooldown - $cdCats = []; - for ($i = 1; $i < 6; $i++) - if ($this->subject->getField('spellId'.$i) > 0 && $this->subject->getField('spellCategory'.$i) > 0) - $cdCats[] = $this->subject->getField('spellCategory'.$i); - - if ($cdCats) - { - $conditions = array( - ['id', $this->typeId, '!'], - [ - 'OR', - ['spellCategory1', $cdCats], - ['spellCategory2', $cdCats], - ['spellCategory3', $cdCats], - ['spellCategory4', $cdCats], - ['spellCategory5', $cdCats], - ] - ); - $cdItems = new ItemList($conditions); - if (!$cdItems->error) - { - $this->lvTabs[] = ['item', array( - 'data' => array_values($cdItems->getListviewData()), - 'name' => '$LANG.tab_sharedcooldown', - 'id' => 'shared-cooldown' - )]; - - $this->extendGlobalData($cdItems->getJSGlobals(GLOBALINFO_SELF)); - } - } - - - // tab: sounds - $soundIds = []; - if ($_class == ITEM_CLASS_WEAPON) - { - $scm = (1 << $_subClass); - 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); - } - - $fields = ['pickUpSoundId', 'dropDownSoundId', 'sheatheSoundId', 'unsheatheSoundId']; - foreach ($fields as $f) - if ($x = $this->subject->getField($f)) - $soundIds[] = $x; - - if ($x = $this->subject->getField('spellVisualId')) - { - if ($spellSounds = DB::Aowow()->selectRow('SELECT * FROM ?_spell_sounds WHERE id = ?d', $x)) - { - array_shift($spellSounds); // bye 'id'-field - foreach ($spellSounds as $ss) - if ($ss) - $soundIds[] = $ss; - } - } - - if ($soundIds) - { - $sounds = new SoundList(array(['id', $soundIds])); - if (!$sounds->error) - { - $this->extendGlobalData($sounds->getJSGlobals(GLOBALINFO_SELF)); - $this->lvTabs[] = ['sound', ['data' => array_values($sounds->getListviewData())]]; - } - } - - - // // todo - tab: taught by - // use var $createdBy to find source of this spell - // id: 'taught-by-X', - // name: LANG.tab_taughtby - } - - protected function generateTooltip($asError = false) - { - $itemString = $this->typeId; - foreach ($this->enhancedTT as $k => $val) - $itemString .= $k.(is_array($val) ? implode(',', $val) : $val); - - if ($asError) - return '$WowheadPower.registerItem(\''.$itemString.'\', '.User::$localeId.', {})'; - - $x = '$WowheadPower.registerItem(\''.$itemString.'\', '.User::$localeId.", {\n"; - $x .= "\tname_".User::$localeString.": '".Util::jsEscape($this->subject->getField('name', true, false, $this->enhancedTT))."',\n"; - $x .= "\tquality: ".$this->subject->getField('quality').",\n"; - $x .= "\ticon: '".rawurlencode($this->subject->getField('iconString', true, true))."',\n"; - $x .= "\ttooltip_".User::$localeString.": '".Util::jsEscape($this->subject->renderTooltip(false, 0, $this->enhancedTT))."'\n"; - $x .= "});"; - - return $x; - } - - protected function generateXML($asError = false) - { - $root = new SimpleXML(''); - - if ($asError) - $root->addChild('error', 'Item not found!'); - else - { - // item root - $xml = $root->addChild('item'); - $xml->addAttribute('id', $this->typeId); - - // name - $xml->addChild('name')->addCData($this->subject->getField('name', true)); - // itemlevel - $xml->addChild('level', $this->subject->getField('itemLevel')); - // quality - $xml->addChild('quality', Lang::item('quality', $this->subject->getField('quality')))->addAttribute('id', $this->subject->getField('quality')); - // class - $x = Lang::item('cat', $this->subject->getField('class')); - $xml->addChild('class')->addCData(is_array($x) ? $x[0] : $x)->addAttribute('id', $this->subject->getField('class')); - // subclass - $x = $this->subject->getField('class') == 2 ? Lang::spell('weaponSubClass') : Lang::item('cat', $this->subject->getField('class'), 1); - $xml->addChild('subclass')->addCData(is_array($x) ? (is_array($x[$this->subject->getField('subClass')]) ? $x[$this->subject->getField('subClass')][0] : $x[$this->subject->getField('subClass')]) : null)->addAttribute('id', $this->subject->getField('subClass')); - // icon + displayId - $xml->addChild('icon', $this->subject->getField('iconString'))->addAttribute('displayId', $this->subject->getField('displayId')); - // inventorySlot - $xml->addChild('inventorySlot', Lang::item('inventoryType', $this->subject->getField('slot')))->addAttribute('id', $this->subject->getField('slot')); - // tooltip - $xml->addChild('htmlTooltip')->addCData($this->subject->renderTooltip()); - - $this->subject->extendJsonStats(); - - // json - $fields = ['classs', 'displayid', 'dps', 'id', 'level', 'name', 'reqlevel', 'slot', 'slotbak', 'speed', 'subclass']; - $json = []; - foreach ($fields as $f) - { - if (isset($this->subject->json[$this->typeId][$f])) - { - $_ = $this->subject->json[$this->typeId][$f]; - if ($f == 'name') - $_ = (7 - $this->subject->getField('quality')).$_; - - $json[$f] = $_; - } - } - - // itemsource - if ($this->subject->getSources($s, $m)) - { - $json['source'] = $s; - if ($m) - $json['sourcemore'] = $m; - } - - $xml->addChild('json')->addCData(substr(json_encode($json), 1, -1)); - - // jsonEquip missing: avgbuyout - $json = []; - if ($_ = $this->subject->getField('sellPrice')) // sellprice - $json['sellprice'] = $_; - - if ($_ = $this->subject->getField('requiredLevel')) // reqlevel - $json['reqlevel'] = $_; - - if ($_ = $this->subject->getField('requiredSkill')) // reqskill - $json['reqskill'] = $_; - - if ($_ = $this->subject->getField('requiredSkillRank')) // reqskillrank - $json['reqskillrank'] = $_; - - if ($_ = $this->subject->getField('cooldown')) // cooldown - $json['cooldown'] = $_ / 1000; - - foreach ($this->subject->itemMods[$this->typeId] as $mod => $qty) - $json[$mod] = $qty; - - foreach ($this->subject->json[$this->typeId] as $name => $qty) - if (in_array($name, Util::$itemFilter)) - $json[$name] = $qty; - - $xml->addChild('jsonEquip')->addCData(substr(json_encode($json), 1, -1)); - - // jsonUse - if ($onUse = $this->subject->getOnUseStats()) - { - $j = ''; - foreach ($onUse as $idx => $qty) - $j .= ',"'.Game::$itemMods[$idx].'":'.$qty; - - $xml->addChild('jsonUse')->addCData(substr($j, 1)); - } - - // reagents - $cnd = array( - 'OR', - ['AND', ['effect1CreateItemId', $this->typeId], ['OR', ['effect1Id', SpellList::$effects['itemCreate']], ['effect1AuraId', SpellList::$auras['itemCreate']]]], - ['AND', ['effect2CreateItemId', $this->typeId], ['OR', ['effect2Id', SpellList::$effects['itemCreate']], ['effect2AuraId', SpellList::$auras['itemCreate']]]], - ['AND', ['effect3CreateItemId', $this->typeId], ['OR', ['effect3Id', SpellList::$effects['itemCreate']], ['effect3AuraId', SpellList::$auras['itemCreate']]]], - ); - - $spellSource = new SpellList($cnd); - if (!$spellSource->error) - { - $cbNode = $xml->addChild('createdBy'); - - foreach ($spellSource->iterate() as $sId => $__) - { - foreach ($spellSource->canCreateItem() as $idx) - { - if ($spellSource->getField('effect'.$idx.'CreateItemId') != $this->typeId) - continue; - - $splNode = $cbNode->addChild('spell'); - $splNode->addAttribute('id', $sId); - $splNode->addAttribute('name', $spellSource->getField('name', true)); - $splNode->addAttribute('icon', $this->subject->getField('iconString')); - $splNode->addAttribute('minCount', $spellSource->getField('effect'.$idx.'BasePoints') + 1); - $splNode->addAttribute('maxCount', $spellSource->getField('effect'.$idx.'BasePoints') + $spellSource->getField('effect'.$idx.'DieSides')); - - foreach ($spellSource->getReagentsForCurrent() as $rId => $qty) - { - if ($reagent = $spellSource->relItems->getEntry($rId)) - { - $rgtNode = $splNode->addChild('reagent'); - $rgtNode->addAttribute('id', $rId); - $rgtNode->addAttribute('name', Util::localizedString($reagent, 'name')); - $rgtNode->addAttribute('quality', $reagent['quality']); - $rgtNode->addAttribute('icon', $reagent['iconString']); - $rgtNode->addAttribute('count', $qty[1]); - } - } - - break; - } - } - } - - // link - $xml->addChild('link', HOST_URL.'?item='.$this->subject->id); - } - - return $root->asXML(); - } - - public function display($override = '') - { - if ($this->mode == CACHE_TYPE_TOOLTIP) - { - if (!$this->loadCache($tt)) - { - $tt = $this->generateTooltip(); - $this->saveCache($tt); - } - - header('Content-type: application/x-javascript; charset=utf-8'); - die($tt); - } - else if ($this->mode == CACHE_TYPE_XML) - { - if (!$this->loadCache($xml)) - { - $xml = $this->generateXML(); - $this->saveCache($xml); - } - - header('Content-type: text/xml; charset=utf-8'); - die($xml); - } - else - return parent::display($override); - } - - public function notFound($title = '', $msg = '') - { - if ($this->mode == CACHE_TYPE_TOOLTIP) - { - header('Content-type: application/x-javascript; charset=utf-8'); - echo $this->generateTooltip(true); - exit(); - } - else if ($this->mode == CACHE_TYPE_XML) - { - header('Content-type: text/xml; charset=utf-8'); - echo $this->generateXML(true); - exit(); - } - else - return parent::notFound($title ?: Lang::game('item'), $msg ?: Lang::item('notFound')); - } -} - -?> diff --git a/pages/itemset.php b/pages/itemset.php deleted file mode 100644 index fd6b7990..00000000 --- a/pages/itemset.php +++ /dev/null @@ -1,274 +0,0 @@ -mode == CACHE_TYPE_TOOLTIP && isset($_GET['domain'])) - Util::powerUseLocale($_GET['domain']); - - $this->typeId = intVal($id); - - $this->subject = new ItemsetList(array(['id', $this->typeId])); - if ($this->subject->error) - $this->notFound(); - - $this->name = $this->subject->getField('name', true); - $this->extendGlobalData($this->subject->getJSGlobals()); - } - - protected function generatePath() - { - if ($_ = $this->subject->getField('classMask')) - { - $bit = log($_, 2); - if (intVal($bit) != $bit) // bit is float => multiple classes were set => skip out - return; - - $this->path[] = $bit + 1; - } - } - - protected function generateTitle() - { - array_unshift($this->title, $this->name, Util::ucFirst(Lang::game('itemset'))); - } - - protected function generateContent() - { - $_ta = $this->subject->getField('contentGroup'); - $_ty = $this->subject->getField('type'); - $_cnt = count($this->subject->getField('pieces')); - - /***********/ - /* Infobox */ - /***********/ - - $infobox = Lang::getInfoBoxForFlags($this->subject->getField('cuFlags')); - - // unavailable (todo (low): set data) - if ($this->subject->getField('cuFlags') & CUSTOM_UNAVAILABLE) - $infobox[] = Lang::main('unavailable'); - - // worldevent - if ($e = $this->subject->getField('eventId')) - { - $infobox[] = Lang::game('eventShort').Lang::main('colon').'[event='.$e.']'; - $this->extendGlobalIds(TYPE_WORLDEVENT, $e); - } - - // 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; - } - - // class - if ($cl = Lang::getClassString($this->subject->getField('classMask'), $jsg, $qty, false)) - { - $this->extendGlobalIds(TYPE_CLASS, $jsg); - $t = $qty == 1 ? Lang::game('class') : Lang::game('classes'); - $infobox[] = Util::ucFirst($t).Lang::main('colon').$cl; - } - - // required level - if ($lvl = $this->subject->getField('reqLevel')) - $infobox[] = sprintf(Lang::game('reqLevel'), $lvl); - - // type - if ($_ty) - $infobox[] = Lang::game('type').Lang::main('colon').Lang::itemset('types', $_ty); - - // tag - if ($_ta) - $infobox[] = Lang::itemset('_tag').Lang::main('colon').'[url=?itemsets&filter=ta='.$_ta.']'.Lang::itemset('notes', $_ta).'[/url]'; - - /****************/ - /* Main Content */ - /****************/ - - // pieces + Summary - $pieces = []; - $eqList = []; - $compare = []; - - if (!$this->subject->pieceToSet) - $cnd = [0]; - else - $cnd = ['i.id', array_keys($this->subject->pieceToSet)]; - - $iList = new ItemList(array($cnd)); - $data = $iList->getListviewData(ITEMINFO_SUBITEMS | ITEMINFO_JSON); - foreach ($iList->iterate() as $itemId => $__) - { - if (empty($data[$itemId])) - continue; - - $slot = $iList->getField('slot'); - $disp = $iList->getField('displayId'); - if ($slot && $disp) - $eqList[] = [$slot, $disp]; - - $compare[] = $itemId; - - $pieces[$itemId] = array( - 'name_'.User::$localeString => $iList->getField('name', true), - 'quality' => $iList->getField('quality'), - 'icon' => $iList->getField('iconString'), - 'jsonequip' => $data[$itemId] - ); - } - - $skill = ''; - if ($_sk = $this->subject->getField('skillId')) - { - $spellLink = sprintf('%s (%s)', $_sk, Lang::spell('cat', 11, $_sk, 0), $this->subject->getField('skillLevel')); - $skill = ' – '.sprintf(Lang::game('requires'), $spellLink).''; - } - - $this->bonusExt = $skill; - $this->description = $_ta ? sprintf(Lang::itemset('_desc'), $this->name, Lang::itemset('notes', $_ta), $_cnt) : sprintf(Lang::itemset('_descTagless'), $this->name, $_cnt); - $this->unavailable = $this->subject->getField('cuFlags') & CUSTOM_UNAVAILABLE; - $this->infobox = $infobox ? '[ul][li]'.implode('[/li][li]', $infobox).'[/li][/ul]' : null; - $this->pieces = $pieces; - $this->spells = $this->subject->getBonuses(); - $this->expansion = 0; - $this->redButtons = array( - BUTTON_WOWHEAD => $this->typeId > 0, // bool only - BUTTON_LINKS => ['type' => $this->type, 'typeId' => $this->typeId], - BUTTON_VIEW3D => ['type' => TYPE_ITEMSET, 'typeId' => $this->typeId, 'equipList' => $eqList], - BUTTON_COMPARE => ['eqList' => implode(':', $compare), 'qty' => $_cnt] - ); - $this->summary = array( - 'id' => 'itemset', - 'template' => 'itemset', - 'parent' => 'summary-generic', - 'groups' => array_map(function ($v) { return [[$v]]; }, $compare), - 'level' => $this->subject->getField('reqLevel'), - ); - - /**************/ - /* Extra Tabs */ - /**************/ - - // related sets (priority: 1: similar tag + class; 2: has event; 3: no tag + similar type, 4: similar type + profession) - $rel = []; - - if ($_ta && count($this->path) == 3) - { - $rel[] = ['id', $this->typeId, '!']; - $rel[] = ['classMask', 1 << (end($this->path) - 1), '&']; - $rel[] = ['contentGroup', (int)$_ta]; - } - else if ($this->subject->getField('eventId')) - { - $rel[] = ['id', $this->typeId, '!']; - $rel[] = ['eventId', 0, '!']; - } - else if ($this->subject->getField('skillId')) - { - $rel[] = ['id', $this->typeId, '!']; - $rel[] = ['contentGroup', 0]; - $rel[] = ['skillId', 0, '!']; - $rel[] = ['type', $_ty]; - } - else if (!$_ta && $_ty) - { - $rel[] = ['id', $this->typeId, '!']; - $rel[] = ['contentGroup', 0]; - $rel[] = ['type', $_ty]; - $rel[] = ['skillId', 0]; - } - - if ($rel) - { - $relSets = new ItemsetList($rel); - if (!$relSets->error) - { - $tabData = array( - 'data' => array_values($relSets->getListviewData()), - 'id' => 'see-also', - 'name' => '$LANG.tab_seealso' - ); - - if (!$relSets->hasDiffFields(['classMask'])) - $tabData['hiddenCols'] = ['classes']; - - $this->lvTabs[] = ['itemset', $tabData]; - - $this->extendGlobalData($relSets->getJSGlobals()); - } - } - } - - protected function generateTooltip($asError = false) - { - if ($asError) - return '$WowheadPower.registerItemSet('.$this->typeId.', '.User::$localeId.', {});'; - - $x = '$WowheadPower.registerItemSet('.$this->typeId.', '.User::$localeId.", {\n"; - $x .= "\tname_".User::$localeString.": '".Util::jsEscape($this->subject->getField('name', true))."',\n"; - $x .= "\ttooltip_".User::$localeString.": '".$this->subject->renderTooltip()."'\n"; - $x .= "});"; - - return $x; - } - - public function display($override = '') - { - if ($this->mode != CACHE_TYPE_TOOLTIP) - return parent::display($override); - - if (!$this->loadCache($tt)) - { - $tt = $this->generateTooltip(); - $this->saveCache($tt); - } - - header('Content-type: application/x-javascript; charset=utf-8'); - die($tt); - } - - public function notFound($title = '', $msg = '') - { - if ($this->mode != CACHE_TYPE_TOOLTIP) - return parent::notFound($title ?: Lang::game('itemset'), $msg ?: Lang::itemset('notFound')); - - header('Content-type: application/x-javascript; charset=utf-8'); - echo $this->generateTooltip(true); - exit(); - } -} - - - - -?> diff --git a/pages/itemsets.php b/pages/itemsets.php deleted file mode 100644 index 80d9553a..00000000 --- a/pages/itemsets.php +++ /dev/null @@ -1,96 +0,0 @@ -getCategoryFromUrl($pageParam); - $this->filterObj = new ItemsetListFilter(false, ['parentCats' => $this->category]); - - parent::__construct($pageCall, $pageParam); - - $this->name = Util::ucFirst(Lang::game('itemsets')); - } - - protected function generateContent() - { - $this->addJS('?data=weight-presets&locale='.User::$localeId.'&t='.$_SESSION['dataKey']); - - $conditions = []; - - if (!User::isInGroup(U_GROUP_EMPLOYEE)) - $conditions[] = [['cuFlags', CUSTOM_EXCLUDE_FOR_LISTVIEW, '&'], 0]; - - if ($_ = $this->filterObj->getConditions()) - $conditions[] = $_; - - $itemsets = new ItemsetList($conditions); - $this->extendGlobalData($itemsets->getJSGlobals()); - - // recreate form selection - $this->filter = $this->filterObj->getForm(); - $this->filter['query'] = isset($_GET['filter']) ? $_GET['filter'] : NULL; - $this->filter['initData'] = ['init' => 'itemsets']; - - if ($x = $this->filterObj->getSetCriteria()) - $this->filter['initData']['sc'] = $x; - - $xCols = $this->filterObj->getExtraCols(); - if ($xCols) - $this->filter['initData']['ec'] = $xCols; - - $tabData = ['data' => array_values($itemsets->getListviewData())]; - - if ($xCols) - $tabData['extraCols'] = '$fi_getExtraCols(fi_extraCols, 0, 0)'; - - // create note if search limit was exceeded - if ($itemsets->getMatches() > CFG_SQL_LIMIT_DEFAULT) - { - $tabData['note'] = sprintf(Util::$tryFilteringString, 'LANG.lvnote_itemsetsfound', $itemsets->getMatches(), CFG_SQL_LIMIT_DEFAULT); - $tabData['_truncated'] = 1; - } - - if ($this->filterObj->error) - $tabData['_errors'] = 1; - - $this->lvTabs[] = ['itemset', $tabData]; - - // sort for dropdown-menus - Lang::sort('itemset', 'notes', SORT_NATURAL); - Lang::sort('game', 'si'); - } - - protected function generateTitle() - { - array_unshift($this->title, $this->name); - - $form = $this->filterObj->getForm('form'); - if (isset($form['cl'])) - array_unshift($this->title, Lang::game('cl', $form['cl'])); - } - - protected function generatePath() - { - $form = $this->filterObj->getForm('form'); - if (isset($form['cl'])) - $this->path[] = $form['cl']; - } -} - -?> diff --git a/pages/maps.php b/pages/maps.php deleted file mode 100644 index e25dfb37..00000000 --- a/pages/maps.php +++ /dev/null @@ -1,38 +0,0 @@ - 'zone-picker { margin-left: 4px }']]; - - public function __construct($pageCall, $__) - { - parent::__construct($pageCall, $__); - - $this->name = Lang::maps('maps'); - } - - protected function generateContent() - { - // add conditional js - $this->addJS('?data=zones&locale=' . User::$localeId . '&t=' . $_SESSION['dataKey']); - } - - protected function generateTitle() - { - array_unshift($this->title, $this->name); - } - - protected function generatePath() {} -} - -?> diff --git a/pages/more.php b/pages/more.php deleted file mode 100644 index 90a9dcd7..00000000 --- a/pages/more.php +++ /dev/null @@ -1,239 +0,0 @@ - CFG_REP_REQ_COMMENT, // write comments - 2 => 0, // NYI post external links - 4 => 0, // NYI no captcha - 5 => CFG_REP_REQ_SUPERVOTE, // votes count for more - 9 => CFG_REP_REQ_VOTEMORE_BASE, // more votes per day - 10 => CFG_REP_REQ_UPVOTE, // can upvote - 11 => CFG_REP_REQ_DOWNVOTE, // can downvote - 12 => CFG_REP_REQ_REPLY, // can reply - 13 => 0, // avatar border [NYI: checked by js, avatars not in use] - 14 => 0, // avatar border [NYI: checked by js, avatars not in use] - 15 => 0, // avatar border [NYI: checked by js, avatars not in use] - 16 => 0, // avatar border [NYI: checked by js, avatars not in use] - 17 => CFG_REP_REQ_PREMIUM // premium status - ); - - private $validPages = array( // [tabId, path[, subPaths]] - 'whats-new' => [2, [2, 7]], - 'searchbox' => [2, [2, 16]], - 'tooltips' => [2, [2, 10]], - 'faq' => [2, [2, 3]], - 'aboutus' => [2, [2, 0]], - 'searchplugins' => [2, [2, 8]], - 'help' => [2, [2, 13], ['commenting-and-you', 'modelviewer', 'screenshots-tips-tricks', 'stat-weighting', 'talent-calculator', 'item-comparison', 'profiler', 'markup-guide']], - 'reputation' => [1, [3, 10]], - 'privilege' => [1, [3, 10], [1, 2, 4, 5, 9, 10, 11, 12, 13, 14, 15, 16, 17]], - 'privileges' => [1, [3, 10, 0]], - 'top-users' => [1, [3, 11]] - ); - - public function __construct($pageCall, $subPage) - { - parent::__construct($pageCall, $subPage); - - // chack if page is valid - if (isset($this->validPages[$pageCall])) - { - $pageData = $this->validPages[$pageCall]; - - $this->tab = $pageData[0]; - $this->path = $pageData[1]; - $this->page = [$pageCall, $subPage]; - - if ($subPage && isset($pageData[2])) - { - $exists = array_search($subPage, $pageData[2]); - if ($exists === false) - $this->error(); - - if (is_numeric($subPage)) - $this->articleUrl = $pageCall.'='.$subPage; - else - $this->articleUrl = $subPage; - - $this->path[] = $subPage; - $this->name = Lang::main('moreTitles', $pageCall, $subPage); - } - else - { - $this->articleUrl = $pageCall; - $this->name = Lang::main('moreTitles', $pageCall); - } - } - else - $this->error(); - - // order by requirement ASC - asort($this->req2priv); - } - - protected function generateContent() - { - switch ($this->page[0]) - { - case 'reputation': - $this->handleReputationPage(); - return; - case 'privileges': - $this->handlePrivilegesPage(); - return; - case 'privilege': - $this->tpl = 'privilege'; - $this->privReqPoints = sprintf(Lang::privileges('reqPoints'), Lang::nf($this->req2priv[$this->page[1]])); - return; - case 'top-users': - $this->handleTopUsersPage(); - return; - default: - return; - } - } - - protected function postArticle() - { - if ($this->page[0] != 'reputation' && - $this->page[0] != 'privileges' && - $this->page[0] != 'privilege') - return; - - $txt = &$this->article['text']; - $consts = get_defined_constants(true); - foreach ($consts['user'] as $k => $v) - { - if (strstr($k, 'CFG_REP_')) - $txt = str_replace($k, Lang::nf($v), $txt); - else if ($k == 'CFG_USER_MAX_VOTES' || $k == 'CFG_BOARD_URL') - $txt = str_replace($k, $v, $txt); - } - } - - protected function generatePath() { } - - protected function generateTitle() - { - array_unshift($this->title, $this->name); - } - - private function handleReputationPage() - { - if (!User::$id) - return; - - 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', User::$id)) - { - foreach ($repData as &$r) - $r['when'] = date(Util::$dateFormatInternal, $r['when']); - - $this->tabsTitle = Lang::main('yourRepHistory'); - $this->forceTabs = true; - $this->lvTabs[] = ['reputationhistory', array( - 'id' => 'reputation-history', - 'name' => '$LANG.reputationhistory', - 'data' => $repData - )]; - } - } - - private function handlePrivilegesPage() - { - $this->tpl = 'privileges'; - $this->privileges = []; - - foreach ($this->req2priv as $id => $val) - if ($val) - $this->privileges[$id] = array( - User::getReputation() >= $val, - Lang::privileges('_privileges', $id), - $val - ); - } - - private function handleTopUsersPage() - { - $tabs = array( - [0, 'top-users-alltime', '$LANG.alltime_stc' ], - [time() - MONTH, 'top-users-monthly', '$LANG.lastmonth_stc'], - [time() - WEEK, 'top-users-weekly', '$LANG.lastweek_stc' ] - ); - - $nullFields = array( - 'uploads' => 0, - 'posts' => 0, - 'gold' => 0, - 'silver' => 0, - 'copper' => 0 - ); - - foreach ($tabs as list($t, $tabId, $tabName)) - { - // stuff received - $res = DB::Aowow()->select(' - SELECT - a.id AS ARRAY_KEY, - a.displayName AS username, - a.userGroups AS groups, - a.joinDate AS creation, - SUM(r.amount) AS reputation, - SUM(IF(r.`action` = 3, 1, 0)) AS comments, - SUM(IF(r.`action` = 6, 1, 0)) AS screenshots, - SUM(IF(r.`action` = 9, 1, 0)) AS reports - FROM ?_account_reputation r - JOIN ?_account a ON a.id = r.userId - {WHERE r.date > ?d} - GROUP BY a.id - ORDER BY reputation DESC - LIMIT ?d - ', $t ?: DBSIMPLE_SKIP, CFG_SQL_LIMIT_SEARCH); - - $data = []; - if ($res) - { - // stuff given - $votes = DB::Aowow()->selectCol( - 'SELECT sourceB AS ARRAY_KEY, SUM(1) FROM ?_account_reputation WHERE action IN (4, 5) AND sourceB IN (?a) {AND date > ?d} GROUP BY sourceB', - array_keys($res), - $t ?: DBSIMPLE_SKIP - ); - foreach ($res as $uId => &$r) - { - $r['creation'] = date('c', $r['creation']); - $r['votes'] = empty($votes[$uId]) ? 0 : $votes[$uId]; - $r = array_merge($r, $nullFields); - } - - $data = array_values($res); - } - - $this->lvTabs[] = ['topusers', array( - 'hiddenCols' => ['achievements', 'posts', 'uploads'], - 'visibleCols' => ['created'], - 'name' => '$LANG.lastweek_stc', - 'name' => $tabName, - 'id' => $tabId, - 'data' => $data - )]; - } - } -} - -?> diff --git a/pages/npc.php b/pages/npc.php deleted file mode 100644 index b0947a27..00000000 --- a/pages/npc.php +++ /dev/null @@ -1,1046 +0,0 @@ -mode == CACHE_TYPE_TOOLTIP && isset($_GET['domain'])) - Util::powerUseLocale($_GET['domain']); - - $this->typeId = intVal($id); - - $this->subject = new CreatureList(array(['id', $this->typeId])); - if ($this->subject->error) - $this->notFound(); - - $this->name = $this->subject->getField('name', true); - $this->subname = $this->subject->getField('subname', true); - } - - protected function generatePath() - { - $this->path[] = $this->subject->getField('type'); - - if ($_ = $this->subject->getField('family')) - $this->path[] = $_; - } - - protected function generateTitle() - { - array_unshift($this->title, $this->name, Util::ucFirst(Lang::game('npc'))); - } - - protected function generateContent() - { - $this->addJS('?data=zones&locale='.User::$localeId.'&t='.$_SESSION['dataKey']); - - $_typeFlags = $this->subject->getField('typeFlags'); - $_altIds = []; - $_altNPCs = null; - $placeholder = null; - $accessory = []; - - // difficulty entries of self - if ($this->subject->getField('cuFlags') & NPC_CU_DIFFICULTY_DUMMY) - $placeholder = [$this->subject->getField('parentId'), $this->subject->getField('parent', true)]; - else - { - for ($i = 1; $i < 4; $i++) - if ($_ = $this->subject->getField('difficultyEntry'.$i)) - $_altIds[$_] = $i; - - if ($_altIds) - $_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)) - { - $vehicles = new CreatureList(array(['id', $_])); - foreach ($vehicles->iterate() as $id => $__) - $accessory[] = [$id, $vehicles->getField('name', true)]; - } - - // try to determine, if it's spawned in a dungeon or raid (shaky at best, if spawned by script) - $mapType = 0; - if ($maps = DB::Aowow()->selectCol('SELECT DISTINCT areaId from ?_spawns WHERE type = ?d AND typeId = ?d', TYPE_NPC, $this->typeId)) - { - if (count($maps) == 1) // should only exist in one instance - { - switch (DB::Aowow()->selectCell('SELECT `type` FROM ?_zones WHERE id = ?d', $maps[0])) - { - case 2: - case 5: $mapType = 1; break; - case 3: - case 7: - case 8: $mapType = 2; break; - } - } - } - else if ($_altIds) // not spawned, but has difficultyDummies - { - if (count($_altIds) > 1) // 3 or more version -> definitly raid (10/25 + hc) - $mapType = 2; - else // 2 versions; may be Heroic (use this), but may also be 10/25-raid - $mapType = 1; - } - - - /***********/ - /* Infobox */ - /***********/ - - $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)) - { - $this->extendGlobalIds(TYPE_WORLDEVENT, $_); - $ev = []; - foreach ($_ as $i => $e) - $ev[] = ($i % 2 ? '[br]' : ' ') . '[event='.$e.']'; - - $infobox[] = Util::ucFirst(Lang::game('eventShort')).Lang::main('colon').implode(',', $ev); - } - - // Level - if ($this->subject->getField('rank') != NPC_RANK_BOSS) - { - $level = $this->subject->getField('minLevel'); - $maxLvl = $this->subject->getField('maxLevel'); - if ($level < $maxLvl) - $level .= ' - '.$maxLvl; - } - else // Boss Level - $level = '??'; - - $infobox[] = Lang::game('level').Lang::main('colon').$level; - - // Classification - if ($_ = $this->subject->getField('rank')) // != NPC_RANK_NORMAL - { - $str = $this->subject->isBoss() ? '[span class=icon-boss]'.Lang::npc('rank', $_).'[/span]' : Lang::npc('rank', $_); - $infobox[] = Lang::npc('classification').Lang::main('colon').$str; - } - - // Reaction - $_ = function ($r) - { - if ($r == 1) return 2; - if ($r == -1) return 10; - return; - }; - $infobox[] = Lang::npc('react').Lang::main('colon').'[color=q'.$_($this->subject->getField('A')).']A[/color] [color=q'.$_($this->subject->getField('H')).']H[/color]'; - - // Faction - $this->extendGlobalIds(TYPE_FACTION, $this->subject->getField('factionId')); - $infobox[] = Util::ucFirst(Lang::game('faction')).Lang::main('colon').'[faction='.$this->subject->getField('factionId').']'; - - // Tameable - if ($_typeFlags & 0x1) - if ($_ = $this->subject->getField('family')) - $infobox[] = sprintf(Lang::npc('tameable'), '[url=pet='.$_.']'.Lang::game('fa', $_).'[/url]'); - - // Wealth - if ($_ = intVal(($this->subject->getField('minGold') + $this->subject->getField('maxGold')) / 2)) - $infobox[] = Lang::npc('worth').Lang::main('colon').'[tooltip=tooltip_avgmoneydropped][money='.$_.'][/tooltip]'; - - // is Vehicle - if ($this->subject->getField('vehicleId')) - $infobox[] = Lang::npc('vehicle'); - - // AI - if (User::isInGroup(U_GROUP_EMPLOYEE)) - { - if ($_ = $this->subject->getField('scriptName')) - $infobox[] = 'Script'.Lang::main('colon').$_; - else if ($_ = $this->subject->getField('aiName')) - $infobox[] = 'AI'.Lang::main('colon').$_; - } - - if (User::isInGroup(U_GROUP_STAFF)) - { - // Mechanic immune - if ($immuneMask = $this->subject->getField('mechanicImmuneMask')) - { - $buff = []; - for ($i = 0; $i < 31; $i++) - if ($immuneMask & (1 << $i)) - $buff[] = (!fMod(count($buff), 3) ? "\n" : null).'[url=?spells&filter=me='.($i + 1).']'.Lang::game('me', $i + 1).'[/url]'; - - $infobox[] = 'Not affected by mechanic'.Lang::main('colon').implode(', ', $buff); - } - - // extra flags - if ($flagsExtra = $this->subject->getField('flagsExtra')) - { - $buff = []; - if ($flagsExtra & 0x000001) - $buff[] = 'Binds attacker to instance on death'; - if ($flagsExtra & 0x000002) - $buff[] = "[tooltip name=civilian]- does not aggro\n- death costs Honor[/tooltip][span class=tip tooltip=civilian]Civilian[/span]"; - if ($flagsExtra & 0x000004) - $buff[] = 'Cannot parry'; - if ($flagsExtra & 0x000008) - $buff[] = 'Has no parry haste'; - if ($flagsExtra & 0x000010) - $buff[] = 'Cannot block'; - if ($flagsExtra & 0x000020) - $buff[] = 'Cannot deal Crushing Blows'; - if ($flagsExtra & 0x000040) - $buff[] = 'Rewards no experience'; - if ($flagsExtra & 0x000080) - $buff[] = 'Trigger-Creature'; - if ($flagsExtra & 0x000100) - $buff[] = 'Immune to Taunt'; - if ($flagsExtra & 0x008000) - $buff[] = "[tooltip name=guard]- engages PvP-Attacker\n- ignores enemy stealth, invisibility and Feign Death[/tooltip][span class=tip tooltip=guard]Guard[/span]"; - if ($flagsExtra & 0x020000) - $buff[] = 'Cannot deal Critical Hits'; - if ($flagsExtra & 0x040000) - $buff[] = 'Attacker does not gain weapon skill'; - if ($flagsExtra & 0x080000) - $buff[] = 'Taunt has diminishing returns'; - if ($flagsExtra & 0x100000) - $buff[] = 'Is subject to diminishing returns'; - - if ($buff) - $infobox[] = 'Extra Flags'.Lang::main('colon').'[ul][li]'.implode('[/li][li]', $buff).'[/li][/ul]'; - } - } - - // > Stats - $stats = []; - $modes = []; // get difficulty versions if set - $hint = '[tooltip name=%3$s][table cellspacing=10][tr]%1s[/tr][/table][/tooltip][span class=tip tooltip=%3$s]%2s[/span]'; - $modeRow = '[tr][td]%s  [/td][td]%s[/td][/tr]'; - // Health - $health = $this->subject->getBaseStats('health'); - $stats['health'] = Util::ucFirst(Lang::spell('powerTypes', -2)).Lang::main('colon').($health[0] < $health[1] ? Lang::nf($health[0]).' - '.Lang::nf($health[1]) : Lang::nf($health[0])); - - // Mana (may be 0) - $mana = $this->subject->getBaseStats('power'); - $stats['mana'] = $mana[0] ? Lang::spell('powerTypes', 0).Lang::main('colon').($mana[0] < $mana[1] ? Lang::nf($mana[0]).' - '.Lang::nf($mana[1]) : Lang::nf($mana[0])) : null; - - // Armor - $armor = $this->subject->getBaseStats('armor'); - $stats['armor'] = Lang::npc('armor').Lang::main('colon').($armor[0] < $armor[1] ? Lang::nf($armor[0]).' - '.Lang::nf($armor[1]) : Lang::nf($armor[0])); - - // Melee Damage - $melee = $this->subject->getBaseStats('melee'); - if ($_ = $this->subject->getField('dmgSchool')) // magic damage - $stats['melee'] = Lang::npc('melee').Lang::main('colon').Lang::nf($melee[0]).' - '.Lang::nf($melee[1]).' ('.Lang::game('sc', $_).')'; - else // phys. damage - $stats['melee'] = Lang::npc('melee').Lang::main('colon').Lang::nf($melee[0]).' - '.Lang::nf($melee[1]); - - // Ranged Damage - $ranged = $this->subject->getBaseStats('ranged'); - $stats['ranged'] = Lang::npc('ranged').Lang::main('colon').Lang::nf($ranged[0]).' - '.Lang::nf($ranged[1]); - - if (in_array($mapType, [1, 2])) // Dungeon or Raid - { - foreach ($_altIds as $id => $mode) - { - foreach ($_altNPCs->iterate() as $dId => $__) - { - if ($dId != $id) - continue; - - $m = Lang::npc('modes', $mapType, $mode); - - // Health - $health = $_altNPCs->getBaseStats('health'); - $modes['health'][] = sprintf($modeRow, $m, $health[0] < $health[1] ? Lang::nf($health[0]).' - '.Lang::nf($health[1]) : Lang::nf($health[0])); - - // Mana (may be 0) - $mana = $_altNPCs->getBaseStats('power'); - $modes['mana'][] = $mana[0] ? sprintf($modeRow, $m, $mana[0] < $mana[1] ? Lang::nf($mana[0]).' - '.Lang::nf($mana[1]) : Lang::nf($mana[0])) : null; - - // Armor - $armor = $_altNPCs->getBaseStats('armor'); - $modes['armor'][] = sprintf($modeRow, $m, $armor[0] < $armor[1] ? Lang::nf($armor[0]).' - '.Lang::nf($armor[1]) : Lang::nf($armor[0])); - - // Melee Damage - $melee = $_altNPCs->getBaseStats('melee'); - if ($_ = $_altNPCs->getField('dmgSchool')) // magic damage - $modes['melee'][] = sprintf($modeRow, $m, Lang::nf($melee[0]).' - '.Lang::nf($melee[1]).' ('.Lang::game('sc', $_).')'); - else // phys. damage - $modes['melee'][] = sprintf($modeRow, $m, Lang::nf($melee[0]).' - '.Lang::nf($melee[1])); - - // Ranged Damage - $ranged = $_altNPCs->getBaseStats('ranged'); - $modes['ranged'][] = sprintf($modeRow, $m, Lang::nf($ranged[0]).' - '.Lang::nf($ranged[1])); - } - } - } - - if ($modes) - foreach ($stats as $k => $v) - if ($v) - $stats[$k] = sprintf($hint, implode('[/tr][tr]', $modes[$k]), $v, $k); - - // < Stats - if ($stats) - $infobox[] = Lang::npc('stats').($modes ? ' ('.Lang::npc('modes', $mapType, 0).')' : null).Lang::main('colon').'[ul][li]'.implode('[/li][li]', $stats).'[/li][/ul]'; - - - /****************/ - /* Main Content */ - /****************/ - - // get spawns and path - $map = null; - if ($spawns = $this->subject->getSpawns(SPAWNINFO_FULL)) - { - $map = ['data' => ['parent' => 'mapper-generic'], 'mapperData' => &$spawns]; - foreach ($spawns as $areaId => &$areaData) - $map['extra'][$areaId] = ZoneList::getName($areaId); - } - - // consider pooled spawns - - $this->map = $map; - $this->infobox = '[ul][li]'.implode('[/li][li]', $infobox).'[/li][/ul]'; - $this->placeholder = $placeholder; - $this->accessory = $accessory; - $this->quotes = $this->getQuotes(); - $this->reputation = $this->getOnKillRep($_altIds, $mapType); - $this->redButtons = array( - BUTTON_WOWHEAD => true, - BUTTON_LINKS => ['type' => $this->type, 'typeId' => $this->typeId], - BUTTON_VIEW3D => ['type' => TYPE_NPC, 'typeId' => $this->typeId, 'displayId' => $this->subject->getRandomModelId()] - ); - - if ($this->subject->getField('humanoid')) - $this->redButtons[BUTTON_VIEW3D]['humanoid'] = 1; - - - /**************/ - /* Extra Tabs */ - /**************/ - - // tab: SAI - // hmm, how should this look like - - // tab: abilities / tab_controlledabilities (dep: VehicleId) - // SMART_SCRIPT_TYPE_CREATURE = 0; SMART_ACTION_CAST = 11; SMART_ACTION_ADD_AURA = 75; SMART_ACTION_INVOKER_CAST = 85; SMART_ACTION_CROSS_CAST = 86 - $smartSpells = DB::World()->selectCol('SELECT action_param1 FROM smart_scripts WHERE source_type = 0 AND action_type IN (11, 75, 85, 86) AND entryOrGUID = ?d', $this->typeId); - $tplSpells = []; - $conditions = ['OR']; - - for ($i = 1; $i < 9; $i++) - if ($_ = $this->subject->getField('spell'.$i)) - $tplSpells[] = $_; - - if ($tplSpells) - $conditions[] = ['id', $tplSpells]; - - if ($smartSpells) - $conditions[] = ['id', $smartSpells]; - - // Pet-Abilities - if ($_typeFlags & 0x1 && ($_ = $this->subject->getField('family'))) - { - $skill = 0; - $mask = 0x0; - foreach (Game::$skillLineMask[-1] as $idx => $pair) - { - if ($pair[0] != $_) - continue; - - $skill = $pair[1]; - $mask = 1 << $idx; - break; - } - $conditions[] = [ - 'AND', - ['s.typeCat', -3], - [ - 'OR', - ['skillLine1', $skill], - ['AND', ['skillLine1', 0, '>'], ['skillLine2OrMask', $skill]], - ['AND', ['skillLine1', -1], ['skillLine2OrMask', $mask, '&']] - ] - ]; - } - - if (count($conditions) > 1) - { - $abilities = new SpellList($conditions); - if (!$abilities->error) - { - $this->extendGlobalData($abilities->getJSGlobals(GLOBALINFO_SELF | GLOBALINFO_RELATED)); - $controled = $abilities->getListviewData(); - $normal = []; - - foreach ($controled as $id => $values) - { - if (in_array($id, $smartSpells)) - { - $normal[$id] = $values; - unset($controled[$id]); - continue; - } - - // not quite right. All seats should be checked for allowed-to-cast-flag-something - if (!$this->subject->getField('vehicleId') && in_array($id, $tplSpells)) - { - $normal[$id] = $values; - unset($controled[$id]); - } - } - - if ($normal) - $this->lvTabs[] = ['spell', array( - 'data' => array_values($normal), - 'name' => '$LANG.tab_abilities', - 'id' => 'abilities' - )]; - - if ($controled) - $this->lvTabs[] = ['spell', array( - 'data' => array_values($controled), - 'name' => '$LANG.tab_controlledabilities', - 'id' => 'controlled-abilities' - )]; - } - } - - // tab: summoned by - $conditions = array( - 'OR', - ['AND', ['effect1Id', 28], ['effect1MiscValue', $this->typeId]], - ['AND', ['effect2Id', 28], ['effect2MiscValue', $this->typeId]], - ['AND', ['effect3Id', 28], ['effect3MiscValue', $this->typeId]] - ); - - $summoned = new SpellList($conditions); - if (!$summoned->error) - { - $this->extendGlobalData($summoned->getJSGlobals()); - - $this->lvTabs[] = ['spell', array( - 'data' => array_values($summoned->getListviewData()), - 'name' => '$LANG.tab_summonedby', - 'id' => 'summoned-by' - )]; - } - - // tab: teaches - if ($this->subject->getField('npcflag') & NPC_FLAG_TRAINER) - { - $teachQuery = ' - SELECT IFNULL(t2.SpellID, t1.SpellID) AS ARRAY_KEY, - IFNULL(t2.MoneyCost, t1.MoneyCost) AS cost, - IFNULL(t2.ReqSkillLine, t1.ReqSkillLine) AS reqSkillId, - IFNULL(t2.ReqSkillRank, t1.ReqSkillRank) AS reqSkillValue, - IFNULL(t2.ReqLevel, t1.ReqLevel) AS reqLevel - FROM npc_trainer t1 - LEFT JOIN npc_trainer t2 ON t2.ID = IF(t1.SpellID < 0, -t1.SpellID, null) - WHERE t1.ID = ?d - '; - - if ($tSpells = DB::World()->select($teachQuery, $this->typeId)) - { - $teaches = new SpellList(array(['id', array_keys($tSpells)])); - if (!$teaches->error) - { - $this->extendGlobalData($teaches->getJSGlobals(GLOBALINFO_SELF | GLOBALINFO_RELATED)); - $data = $teaches->getListviewData(); - - $extra = []; - foreach ($tSpells as $sId => $train) - { - if (empty($data[$sId])) - continue; - - if ($_ = $train['reqSkillId']) - { - $this->extendGlobalIds(TYPE_SKILL, $_); - if (!isset($extra[0])) - $extra[0] = '$Listview.extraCols.condition'; - - $data[$sId]['condition'][0][$this->typeId][] = [[CND_SKILL, $_, $train['reqSkillValue']]]; - } - - if ($_ = $train['reqLevel']) - { - if (!isset($extra[1])) - $extra[1] = "\$Listview.funcBox.createSimpleCol('reqLevel', LANG.tooltip_reqlevel, '7%', 'reqLevel')"; - - $data[$sId]['reqLevel'] = $_; - } - - if ($_ = $train['cost']) - $data[$sId]['trainingcost'] = $_; - } - - $tabData = array( - 'data' => array_values($data), - 'name' => '$LANG.tab_teaches', - 'id' => 'teaches', - 'visibleCols' => ['trainingcost'] - ); - - if ($extra) - $tabData['extraCols'] = $extra; - - $this->lvTabs[] = ['spell', $tabData]; - } - } - else - trigger_error('NPC '.$this->typeId.' is flagged as trainer, but doesn\'t have any spells set', E_USER_WARNING); - } - - // tab: sells - if ($sells = DB::World()->selectCol('SELECT item FROM npc_vendor nv WHERE entry = ?d UNION SELECT item FROM game_event_npc_vendor genv JOIN creature c ON genv.guid = c.guid WHERE c.id = ?d', $this->typeId, $this->typeId)) - { - $soldItems = new ItemList(array(['id', $sells])); - if (!$soldItems->error) - { - $extraCols = ["\$Listview.funcBox.createSimpleCol('stack', 'stack', '10%', 'stack')", '$Listview.extraCols.cost']; - if ($soldItems->hasSetFields(['condition'])) - $extraCols[] = '$Listview.extraCols.condition'; - - $lvData = $soldItems->getListviewData(ITEMINFO_VENDOR, [TYPE_NPC => [$this->typeId]]); - - $sc = Util::getServerConditions(CND_SRC_NPC_VENDOR, $this->typeId); - if (!empty($sc[0])) - { - $this->extendGlobalData($sc[1]); - - $extraCols[] = '$Listview.extraCols.condition'; - - foreach ($lvData as $id => &$row) - foreach ($sc[0] as $srcType => $cndData) - if (!empty($cndData[$id.':'.$this->typeId])) - $row['condition'][0][$id.':'.$this->typeId] = $cndData[$id.':'.$this->typeId]; - } - - $this->lvTabs[] = ['item', array( - 'data' => array_values($lvData), - 'name' => '$LANG.tab_sells', - 'id' => 'currency-for', - 'extraCols' => array_unique($extraCols) - )]; - - $this->extendGlobalData($soldItems->getJSGlobals(GLOBALINFO_SELF | GLOBALINFO_RELATED)); - } - } - - // tabs: this creature contains.. - $skinTab = ['tab_skinning', 'skinning']; - if ($_typeFlags & NPC_TYPEFLAG_HERBLOOT) - $skinTab = ['tab_herbalism', 'herbalism']; - else if ($_typeFlags & NPC_TYPEFLAG_MININGLOOT) - $skinTab = ['tab_mining', 'mining']; - else if ($_typeFlags & NPC_TYPEFLAG_ENGINEERLOOT) - $skinTab = ['tab_engineering', '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}} - */ - - $sourceFor = array( - [LOOT_CREATURE, $this->subject->getField('lootId'), '$LANG.tab_drops', 'drops', [] ], - [LOOT_PICKPOCKET, $this->subject->getField('pickpocketLootId'), '$LANG.tab_pickpocketing', 'pickpocketing', ['side', 'slot', 'reqlevel']], - [LOOT_SKINNING, $this->subject->getField('skinLootId'), '$LANG.'.$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)' - ); - - if ($_altIds) - { - $sourceFor[0][2] = $mapType == 1 ? $langref[-1] : $langref[1]; - foreach ($_altNPCs->iterate() as $id => $__) - { - $mode = ($_altIds[$id] + 1) * ($mapType == 1 ? -1 : 1); - if ($lootGO = DB::Aowow()->selectRow('SELECT o.id, o.lootId, o.name_loc0, o.name_loc2, o.name_loc3, o.name_loc6, o.name_loc8 FROM ?_loot_link l JOIN ?_objects o ON o.id = l.objectId WHERE l.npcId = ?d', $id)) - array_splice($sourceFor, 1, 0, [[LOOT_GAMEOBJECT, $lootGO['lootId'], $langref[$mode], 'drops-object-'.abs($mode), [], 'note' => '$$WH.sprintf(LANG.lvnote_npcobjectsource, '.$lootGO['id'].', "'.Util::localizedString($lootGO, 'name').'")']]); - if ($lootId = $_altNPCs->getField('lootId')) - array_splice($sourceFor, 1, 0, [[LOOT_CREATURE, $lootId, $langref[$mode], 'drops-'.abs($mode), []]]); - } - } - - if ($lootGOs = DB::Aowow()->select('SELECT o.id, IF(npcId < 0, 1, 0) AS modeDummy, o.lootId, o.name_loc0, o.name_loc2, o.name_loc3, o.name_loc6, o.name_loc8 FROM ?_loot_link l JOIN ?_objects o ON o.id = l.objectId WHERE ABS(l.npcId) = ?d', $this->typeId)) - foreach ($lootGOs as $idx => $lgo) - array_splice($sourceFor, 1, 0, [[LOOT_GAMEOBJECT, $lgo['lootId'], $mapType ? $langref[($mapType == 1 ? -1 : 1) + ($lgo['modeDummy'] ? 1 : 0)] : '$LANG.tab_drops', 'drops-object-'.$idx, [], 'note' => '$$WH.sprintf(LANG.lvnote_npcobjectsource, '.$lgo['id'].', "'.Util::localizedString($lgo, 'name').'")']]); - - $reqQuest = []; - foreach ($sourceFor as $sf) - { - $creatureLoot = new Loot(); - if ($creatureLoot->getByContainer($sf[0], $sf[1])) - { - $extraCols = $creatureLoot->extraCols; - $extraCols[] = '$Listview.extraCols.percent'; - - $this->extendGlobalData($creatureLoot->jsGlobals); - - foreach ($creatureLoot->iterate() as &$lv) - { - if (!$lv['quest']) - continue; - - $extraCols[] = '$Listview.extraCols.condition'; - $reqQuest[$lv['id']] = 0; - $lv['condition'][0][$this->typeId][] = [[CND_QUESTTAKEN, &$reqQuest[$lv['id']]]]; - } - - $tabData = array( - 'data' => array_values($creatureLoot->getResult()), - 'name' => $sf[2], - 'id' => $sf[3], - 'extraCols' => array_unique($extraCols), - 'sort' => ['-percent', 'name'], - ); - - if (!empty($sf['note'])) - $tabData['note'] = $sf['note']; - - if ($sf[4]) - $tabData['hiddenCols'] = $sf[4]; - - $this->lvTabs[] = ['item', $tabData]; - } - } - - if ($reqIds = array_keys($reqQuest)) // apply quest-conditions as back-reference - { - $conditions = array( - 'OR', - ['reqSourceItemId1', $reqIds], ['reqSourceItemId2', $reqIds], - ['reqSourceItemId3', $reqIds], ['reqSourceItemId4', $reqIds], - ['reqItemId1', $reqIds], ['reqItemId2', $reqIds], ['reqItemId3', $reqIds], - ['reqItemId4', $reqIds], ['reqItemId5', $reqIds], ['reqItemId6', $reqIds] - ); - - $reqQuests = new QuestList($conditions); - $this->extendGlobalData($reqQuests->getJSGlobals()); - - foreach ($reqQuests->iterate() as $qId => $__) - { - if (empty($reqQuests->requires[$qId][TYPE_ITEM])) - continue; - - foreach ($reqIds as $rId) - if (in_array($rId, $reqQuests->requires[$qId][TYPE_ITEM])) - $reqQuest[$rId] = $reqQuests->id; - } - } - - // tab: starts quest - // tab: ends quest - $startEnd = new QuestList(array(['qse.type', TYPE_NPC], ['qse.typeId', $this->typeId])); - if (!$startEnd->error) - { - $this->extendGlobalData($startEnd->getJSGlobals()); - $lvData = $startEnd->getListviewData(); - $_ = [[], []]; - - foreach ($startEnd->iterate() as $id => $__) - { - $m = $startEnd->getField('method'); - if ($m & 0x1) - $_[0][] = $lvData[$id]; - if ($m & 0x2) - $_[1][] = $lvData[$id]; - } - - if ($_[0]) - $this->lvTabs[] = ['quest', array( - 'data' => array_values($_[0]), - 'name' => '$LANG.tab_starts', - 'id' => 'starts' - )]; - - if ($_[1]) - $this->lvTabs[] = ['quest', array( - 'data' => array_values($_[1]), - 'name' => '$LANG.tab_ends', - 'id' => 'ends' - )]; - } - - // 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, '>']], - ); - - $objectiveOf = new QuestList($conditions); - if (!$objectiveOf->error) - { - $this->extendGlobalData($objectiveOf->getJSGlobals()); - - $this->lvTabs[] = ['quest', array( - 'data' => array_values($objectiveOf->getListviewData()), - 'name' => '$LANG.tab_objectiveof', - 'id' => 'objective-of' - )]; - } - - // tab: criteria of [ACHIEVEMENT_CRITERIA_TYPE_KILL_CREATURE_TYPE have no data set to check for] - $conditions = array( - ['ac.type', [ACHIEVEMENT_CRITERIA_TYPE_KILL_CREATURE, ACHIEVEMENT_CRITERIA_TYPE_KILLED_BY_CREATURE]], - ['ac.value1', $this->typeId] - ); - - $crtOf = new AchievementList($conditions); - if (!$crtOf->error) - { - $this->extendGlobalData($crtOf->getJSGlobals()); - - $this->lvTabs[] = ['achievement', array( - 'data' => array_values($crtOf->getListviewData()), - 'name' => '$LANG.tab_criteriaof', - 'id' => 'criteria-of' - )]; - } - - // tab: passengers - if ($_ = DB::World()->selectCol('SELECT accessory_entry AS ARRAY_KEY, GROUP_CONCAT(seat_id) FROM vehicle_template_accessory WHERE entry = ?d GROUP BY accessory_entry', $this->typeId)) - { - $passengers = new CreatureList(array(['id', array_keys($_)])); - if (!$passengers->error) - { - $data = $passengers->getListviewData(); - - if (User::isInGroup(U_GROUP_STAFF)) - foreach ($data as $id => &$d) - $d['seat'] = str_replace(',', ', ', $_[$id]); - - $this->extendGlobalData($passengers->getJSGlobals(GLOBALINFO_SELF)); - - $tabData = array( - 'data' => array_values($data), - 'name' => Lang::npc('accessory'), - 'id' => 'accessory' - ); - - if (User::isInGroup(U_GROUP_STAFF)) - $tabData['extraCols'] = ["\$Listview.funcBox.createSimpleCol('seat', '".Lang::npc('seat')."', '10%', 'seat')"]; - - $this->lvTabs[] = ['creature', $tabData]; - } - } - - /* tab sounds: - * activity sounds => CreatureDisplayInfo.dbc => (CreatureModelData.dbc => ) CreatureSoundData.dbc - * AI => smart_scripts - * Dialogue VO => creature_text - * onClick VO => CreatureDisplayInfo.dbc => NPCSounds.dbc - */ - $ssActionLists = DB::World()->select('SELECT action_type, action_param1, action_param2, action_param3, action_param4, action_param5, action_param6 FROM smart_scripts WHERE entryorguid = ?d AND source_type = 0 AND action_type IN (80, 87, 88)', $this->typeId); - $actionListIds = []; - foreach ($ssActionLists as $sal) - { - $iMax = 0; - switch ($sal['action_type']) - { - case 80: $iMax = 1; break; - case 87: $iMax = 6; break; - case 88: $iMax = 2; break; - default: continue; - } - - for ($i = 1; $i <= $iMax; $i++) - if ($sal['action_param'.$i]) - $actionListIds[] = $sal['action_param'.$i]; - } - - // not going for a per guid basis. The infos are nested enough as is. - $smartScripts = DB::World()->selectCol('SELECT action_param1 FROM smart_scripts WHERE action_type = 4 AND ((source_type = 0 AND entryorguid = ?d) { OR (source_type = 9 AND entryorguid IN (?a)) } )', $this->typeId, $actionListIds ?: DBSIMPLE_SKIP); - $this->soundIds = array_merge($this->soundIds, $smartScripts); - - // 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')); - array_shift($activitySounds); // remove id-column - $this->soundIds = array_merge($this->soundIds, array_values($activitySounds)); - - if ($this->soundIds) - { - $sounds = new SoundList(array(['id', $this->soundIds])); - if (!$sounds->error) - { - $data = $sounds->getListviewData(); - foreach ($activitySounds as $activity => $id) - if (isset($data[$id])) - $data[$id]['activity'] = $activity; // no index, js wants a string :( - - $tabData = ['data' => array_values($data)]; - if ($activitySounds) - $tabData['visibleCols'] = ['activity']; - - $this->extendGlobalData($sounds->getJSGlobals(GLOBALINFO_SELF)); - $this->lvTabs[] = ['sound', $tabData]; - } - } - } - - protected function generateTooltip($asError = false) - { - if ($asError) - return '$WowheadPower.registerNpc('.$this->typeId.', '.User::$localeId.', {})'; - - $s = $this->subject->getSpawns(SPAWNINFO_SHORT); - - $x = '$WowheadPower.registerNpc('.$this->typeId.', '.User::$localeId.", {\n"; - $x .= "\tname_".User::$localeString.": '".Util::jsEscape($this->subject->getField('name', true))."',\n"; - $x .= "\ttooltip_".User::$localeString.": '".Util::jsEscape($this->subject->renderTooltip())."',\n"; - $x .= "\tmap: ".($s ? "{zone: ".$s[0].", coords: {".$s[1].":".Util::toJSON($s[2])."}}" : '{}')."\n"; - $x .= "});"; - - return $x; - } - - public function display($override = '') - { - if ($this->mode != CACHE_TYPE_TOOLTIP) - return parent::display($override); - - if (!$this->loadCache($tt)) - { - $tt = $this->generateTooltip(); - $this->saveCache($tt); - } - - header('Content-type: application/x-javascript; charset=utf-8'); - die($tt); - } - - public function notFound($title = '', $msg = '') - { - if ($this->mode != CACHE_TYPE_TOOLTIP) - return parent::notFound($title ?: Lang::game('npc'), $msg ?: Lang::npc('notFound')); - - header('Content-type: application/x-javascript; charset=utf-8'); - echo $this->generateTooltip(true); - exit(); - } - - private function getRepForId($entries, &$spillover) - { - $rows = DB::World()->select(' - 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 - 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', - (array)$entries, (array)$entries - ); - - $factions = new FactionList(array(['id', array_column($rows, 'faction')])); - $result = []; - - foreach ($rows as $row) - { - if (!$factions->getEntry($row['faction'])) - continue; - - $set = array( - 'id' => $row['faction'], - 'qty' => [$row['qty'], 0], - 'name' => $factions->getField('name', true), - 'npc' => $row['npc'], - 'cap' => $row['maxRank'] && $row['maxRank'] < REP_EXALTED ? Lang::game('rep', $row['maxRank']) : null - ); - - $cuRate = DB::World()->selectCell('SELECT creature_rate FROM reputation_reward_rate WHERE creature_rate <> 1 AND faction = ?d', $row['faction']); - if ($cuRate !== null) - $set['qty'][1] = $set['qty'][0] * ($cuRate - 1); - - if ($row['spillover']) - { - $spillover[$factions->getField('cat')] = array( - [ $set['qty'][0] / 2, $set['qty'][1] / 2 ], - $row['maxRank'] - ); - $set['spillover'] = $factions->getField('cat'); - } - - $result[] = $set; - } - - return $result; - } - - private function getOnKillRep($dummyIds, $mapType) - { - $spilledParents = []; - $reputation = []; - - // base NPC - if ($base = $this->getRepForId($this->typeId, $spilledParents)) - $reputation[] = [Lang::npc('modes', 1, 0), $base]; - - // difficulty dummys - if ($dummyIds && ($mapType == 1 || $mapType == 2)) - { - $alt = []; - $rep = $this->getRepForId(array_keys($dummyIds), $spilledParents); - - // order by difficulty - foreach ($rep as $r) - $alt[$dummyIds[$r['npc']]][] = $r; - - // apply by difficulty - foreach ($alt as $mode => $dat) - $reputation[] = [Lang::npc('modes', $mapType, $mode), $dat]; - } - - // get spillover factions and apply - if ($spilledParents) - { - $spilled = new FactionList(array(['parentFactionId', array_keys($spilledParents)])); - - foreach ($reputation as &$sets) - { - foreach ($sets[1] as &$row) - { - if (empty($row['spillover'])) - continue; - - foreach ($spilled->iterate() as $spId => $__) - { - // find parent - if ($spilled->getField('parentFactionId') != $row['spillover']) - continue; - - // don't readd parent - if ($row['id'] == $spId) - continue; - - $spMax = $spilledParents[$row['spillover']][1]; - - $sets[1][] = array( - 'id' => $spId, - 'qty' => $spilledParents[$row['spillover']][0], - 'name' => $spilled->getField('name', true), - 'cap' => $spMax && $spMax < REP_EXALTED ? Lang::game('rep', $spMax) : null - ); - } - } - } - } - - return $reputation; - } - - private function getQuotes() - { - $nQuotes = 0; - $quotes = []; - $quoteSrc = DB::World()->select(' - SELECT - ct.GroupID AS ARRAY_KEY, ct.ID as ARRAY_KEY2, ct.`Type`, - ct.TextRange AS `range`, - IFNULL(bct.`Language`, ct.`Language`) AS lang, - IFNULL(NULLIF(bct.MaleText, ""), IFNULL(NULLIF(bct.FemaleText, ""), IFNULL(ct.`Text`, ""))) AS text_loc0, - {IFNULL(NULLIF(bctl.MaleText, ""), IFNULL(NULLIF(bctl.FemaleText, ""), IFNULL(ctl.Text, ""))) AS text_loc?d,} - IF(bct.SoundId > 0, bct.SoundId, 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', - User::$localeId ?: DBSIMPLE_SKIP, - User::$localeId ? Util::$localeStrings[User::$localeId] : DBSIMPLE_SKIP, - User::$localeId ? Util::$localeStrings[User::$localeId] : DBSIMPLE_SKIP, - $this->typeId - ); - - foreach ($quoteSrc as $text) - { - $group = []; - foreach ($text as $t) - { - if ($t['soundId']) - $this->soundIds[] = $t['soundId']; - - $msg = Util::localizedString($t, 'text'); - if (!$msg) - continue; - - // fixup .. either set %s for emotes or dont >.< - if (in_array($t['Type'], [2, 16]) && strpos($msg, '%s') === false) - $msg = '%s '.$msg; - - // fixup: bad case-insensivity - $msg = str_replace('%S', '%s', $msg); - - $line = array( - 'range' => $t['range'], - 'type' => 2, // [type: 0, 12] say: yellow-ish - 'lang' => !empty($t['lang']) ? Lang::game('languages', $t['lang']) : null, - 'text' => sprintf(Util::parseHtmlText(htmlentities($msg)), $this->name), - ); - - switch ($t['Type']) - { - case 1: // yell: - case 14: $line['type'] = 1; break; // - dark red - case 2: // emote: - case 16: // " - case 3: // boss emote: - case 41: $line['type'] = 4; break; // - orange - case 4: // whisper: - case 15: // " - case 5: // boss whisper: - case 42: $line['type'] = 3; break; // - pink-ish - } - - $nQuotes++; - $group[] = $line; - } - - if ($group) - $quotes[] = $group; - } - - return [$quotes, $nQuotes]; - } -} - - -?> diff --git a/pages/npcs.php b/pages/npcs.php deleted file mode 100644 index 161cc4e6..00000000 --- a/pages/npcs.php +++ /dev/null @@ -1,119 +0,0 @@ -getCategoryFromUrl($pageParam);; - $this->filterObj = new CreatureListFilter(false, ['parentCats' => $this->category]); - - parent::__construct($pageCall, $pageParam); - - $this->name = Util::ucFirst(Lang::game('npcs')); - $this->subCat = $pageParam ? '='.$pageParam : ''; - } - - protected function generateContent() - { - $this->addJS('?data=zones&locale='.User::$localeId.'&t='.$_SESSION['dataKey']); - - $conditions = []; - - if (!User::isInGroup(U_GROUP_EMPLOYEE)) - $conditions[] = [['cuFlags', CUSTOM_EXCLUDE_FOR_LISTVIEW, '&'], 0]; - - if ($this->category) - { - $conditions[] = ['type', $this->category[0]]; - $this->petFamPanel = $this->category[0] == 1; - } - else - $this->petFamPanel = false; - - if ($_ = $this->filterObj->getConditions()) - $conditions[] = $_; - - // beast subtypes are selected via filter - $npcs = new CreatureList($conditions, ['extraOpts' => $this->filterObj->extraOpts]); - - // recreate form selection - $this->filter = $this->filterObj->getForm(); - $this->filter['query'] = isset($_GET['filter']) ? $_GET['filter'] : null; - $this->filter['initData'] = ['init' => 'npcs']; - - $rCols = $this->filterObj->getReputationCols(); - $xCols = $this->filterObj->getExtraCols(); - if ($rCols) - $this->filter['initData']['rc'] = $rCols; - - if ($xCols) - $this->filter['initData']['ec'] = $xCols; - - if ($x = $this->filterObj->getSetCriteria()) - $this->filter['initData']['sc'] = $x; - - $tabData = ['data' => array_values($npcs->getListviewData($rCols ? NPCINFO_REP : 0x0))]; - - if ($rCols) // never use pretty-print - $tabData['extraCols'] = '$fi_getReputationCols('.Util::toJSON($rCols, JSON_NUMERIC_CHECK | JSON_UNESCAPED_UNICODE).')'; - else if ($xCols) - $tabData['extraCols'] = '$fi_getExtraCols(fi_extraCols, 0, 0)'; - - if ($this->category) - $tabData['hiddenCols'] = ['type']; - - // create note if search limit was exceeded - if ($npcs->getMatches() > CFG_SQL_LIMIT_DEFAULT) - { - $tabData['note'] = sprintf(Util::$tryFilteringString, 'LANG.lvnote_npcsfound', $npcs->getMatches(), CFG_SQL_LIMIT_DEFAULT); - $tabData['_truncated'] = 1; - } - - if ($this->filterObj->error) - $tabData['_errors'] = 1; - - $this->lvTabs[] = ['creature', $tabData]; - - // sort for dropdown-menus - Lang::sort('game', 'fa'); - } - - protected function generateTitle() - { - array_unshift($this->title, $this->name); - if ($this->category) - array_unshift($this->title, Lang::npc('cat', $this->category[0])); - - $form = $this->filterObj->getForm(); - if (isset($form['fa']) && !is_array($form['fa'])) - array_unshift($this->title, Lang::game('fa', $form['fa'])); - } - - protected function generatePath() - { - if ($this->category) - $this->path[] = $this->category[0]; - - $form = $this->filterObj->getForm(); - if (isset($form['fa']) && !is_array($form['fa'])) - $this->path[] = $form['fa']; - } -} - -?> diff --git a/pages/object.php b/pages/object.php deleted file mode 100644 index 86203277..00000000 --- a/pages/object.php +++ /dev/null @@ -1,468 +0,0 @@ -mode == CACHE_TYPE_TOOLTIP && isset($_GET['domain'])) - Util::powerUseLocale($_GET['domain']); - - $this->typeId = intVal($id); - - $this->subject = new GameObjectList(array(['id', $this->typeId])); - if ($this->subject->error) - $this->notFound(); - - $this->name = $this->subject->getField('name', true); - } - - protected function generatePath() - { - $this->path[] = $this->subject->getField('typeCat'); - } - - protected function generateTitle() - { - array_unshift($this->title, $this->name, Util::ucFirst(Lang::game('object'))); - } - - protected function generateContent() - { - $this->addJS('?data=zones&locale='.User::$localeId.'&t='.$_SESSION['dataKey']); - - /***********/ - /* Infobox */ - /***********/ - - $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)) - { - $this->extendGlobalIds(TYPE_WORLDEVENT, $_); - $ev = []; - foreach ($_ as $i => $e) - $ev[] = ($i % 2 ? '[br]' : ' ') . '[event='.$e.']'; - - $infobox[] = Util::ucFirst(Lang::game('eventShort')).Lang::main('colon').implode(',', $ev); - } - - // Reaction - $_ = function ($r) - { - if ($r == 1) return 2; - if ($r == -1) return 10; - return; - }; - $infobox[] = Lang::npc('react').Lang::main('colon').'[color=q'.$_($this->subject->getField('A')).']A[/color] [color=q'.$_($this->subject->getField('H')).']H[/color]'; - - // reqSkill - switch ($this->subject->getField('typeCat')) - { - case -3: // Herbalism - $infobox[] = sprintf(Lang::game('requires'), Lang::spell('lockType', 2).' ('.$this->subject->getField('reqSkill').')'); - break; - case -4: // Mining - $infobox[] = sprintf(Lang::game('requires'), Lang::spell('lockType', 3).' ('.$this->subject->getField('reqSkill').')'); - break; - case -5: // Lockpicking - $infobox[] = sprintf(Lang::game('requires'), Lang::spell('lockType', 1).' ('.$this->subject->getField('reqSkill').')'); - break; - default: // requires key .. maybe - { - $locks = Lang::getLocks($this->subject->getField('lockId')); - $l = ''; - foreach ($locks as $idx => $_) - { - if ($idx < 0) - continue; - - $this->extendGlobalIds(TYPE_ITEM, $idx); - $l = Lang::gameObject('key').Lang::main('colon').'[item='.$idx.']'; - } - - // if no propper item is found use a skill - if ($locks) - $infobox[] = $l ? $l : array_pop($locks); - } - } - - // linked trap - if ($_ = $this->subject->getField('linkedTrap')) - { - $this->extendGlobalIds(TYPE_OBJECT, $_); - $infobox[] = Lang::gameObject('trap').Lang::main('colon').'[object='.$_.']'; - } - - // trap for - $trigger = new GameObjectList(array(['linkedTrap', $this->typeId])); - if (!$trigger->error) - { - $this->extendGlobalData($trigger->getJSGlobals()); - $infobox[] = Lang::gameObject('triggeredBy').Lang::main('colon').'[object='.$trigger->id.']'; - } - - // 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')) && $_[0]) - { - $buff = Lang::item('charges').Lang::main('colon').$_[0]; - if ($_[0] < $_[1]) - $buff .= Lang::game('valueDelim').$_[1]; - - // since Veins don't have charges anymore, the timer is questionable - $infobox[] = $_[2] > 1 ? '[tooltip name=restock]'.sprintf(Lang::gameObject('restock'), Util::formatTime($_[2] * 1000)).'[/tooltip][span class=tip tooltip=restock]'.$buff.'[/span]' : $buff; - } - - // meeting stone [minLevel, maxLevel, zone] - if ($this->subject->getField('type') == OBJECT_MEETINGSTONE) - { - if ($_ = $this->subject->getField('mStone')) - { - $this->extendGlobalIds(TYPE_ZONE, $_[2]); - $m = Lang::game('meetingStone').Lang::main('colon').'[zone='.$_[2].']'; - - $l = $_[0]; - if ($_[0] > 1 && $_[1] > $_[0]) - $l .= Lang::game('valueDelim').min($_[1], MAX_LEVEL); - - $infobox[] = $l ? '[tooltip name=meetingstone]'.sprintf(Lang::game('reqLevel'), $l).'[/tooltip][span class=tip tooltip=meetingstone]'.$m.'[/span]' : $m; - } - } - - // capture area [minPlayer, maxPlayer, minTime, maxTime, radius] - if ($this->subject->getField('type') == OBJECT_CAPTURE_POINT) - { - if ($_ = $this->subject->getField('capture')) - { - $buff = Lang::gameObject('capturePoint'); - - if ($_[2] > 1 || $_[0]) - $buff .= Lang::main('colon').'[ul]'; - - if ($_[2] > 1) - $buff .= '[li]'.Lang::game('duration').Lang::main('colon').($_[3] > $_[2] ? Util::FormatTime($_[3] * 1000, true).' - ' : null).Util::FormatTime($_[2] * 1000, true).'[/li]'; - - if ($_[1]) - $buff .= '[li]'.Lang::main('players').Lang::main('colon').$_[0].($_[1] > $_[0] ? ' - '.$_[1] : null).'[/li]'; - - if ($_[4]) - $buff .= '[li]'.sprintf(Lang::spell('range'), $_[4]).'[/li]'; - - if ($_[2] > 1 || $_[0]) - $buff .= '[/ul]'; - } - - $infobox[] = $buff; - } - - // AI - if (User::isInGroup(U_GROUP_EMPLOYEE)) - { - if ($_ = $this->subject->getField('ScriptName')) - $infobox[] = 'Script'.Lang::main('colon').$_; - else if ($_ = $this->subject->getField('AIName')) - $infobox[] = 'AI'.Lang::main('colon').$_; - } - - - /****************/ - /* Main Content */ - /****************/ - - // pageText - if ($this->pageText = Game::getPageText($next = $this->subject->getField('pageTextId'))) - { - $this->addCSS(['path' => 'Book.css']); - $this->addJS('Book.js'); - } - - // get spawns and path - $map = null; - if ($spawns = $this->subject->getSpawns(SPAWNINFO_FULL)) - { - $map = ['data' => ['parent' => 'mapper-generic'], 'mapperData' => &$spawns]; - foreach ($spawns as $areaId => &$areaData) - $map['extra'][$areaId] = ZoneList::getName($areaId); - } - - - // todo (low): consider pooled spawns - - - $relBoss = null; - if ($_ = DB::Aowow()->selectCell('SELECT ABS(npcId) FROM ?_loot_link WHERE objectId = ?d', $this->typeId)) - { - // difficulty dummy - if ($c = DB::Aowow()->selectRow('SELECT id, name_loc0, name_loc2, name_loc3, name_loc6, name_loc8 FROM ?_creature WHERE difficultyEntry1 = ?d OR difficultyEntry2 = ?d OR difficultyEntry3 = ?d', $_, $_, $_)) - $relBoss = [$c['id'], Util::localizedString($c, 'name')]; - // base creature - else if ($c = DB::Aowow()->selectRow('SELECT id, name_loc0, name_loc2, name_loc3, name_loc6, name_loc8 FROM ?_creature WHERE id = ?d', $_)) - $relBoss = [$c['id'], Util::localizedString($c, 'name')]; - } - - $this->infobox = $infobox ? '[ul][li]'.implode('[/li][li]', $infobox).'[/li][/ul]' : null; - $this->map = $map; - $this->relBoss = $relBoss; - $this->redButtons = array( - BUTTON_WOWHEAD => true, - BUTTON_LINKS => ['type' => $this->type, 'typeId' => $this->typeId], - BUTTON_VIEW3D => ['displayId' => $this->subject->getField('displayId'), 'type' => TYPE_OBJECT, 'typeId' => $this->typeId] - ); - - - /**************/ - /* Extra Tabs */ - /**************/ - - // tab: summoned by - $conditions = array( - 'OR', - ['AND', ['effect1Id', [50, 76, 104, 105, 106, 107]], ['effect1MiscValue', $this->typeId]], - ['AND', ['effect2Id', [50, 76, 104, 105, 106, 107]], ['effect2MiscValue', $this->typeId]], - ['AND', ['effect3Id', [50, 76, 104, 105, 106, 107]], ['effect3MiscValue', $this->typeId]] - ); - - $summons = new SpellList($conditions); - if (!$summons->error) - { - $this->extendGlobalData($summons->getJSGlobals(GLOBALINFO_SELF | GLOBALINFO_RELATED)); - - $this->lvTabs[] = ['spell', array( - 'data' => array_values($summons->getListviewData()), - 'id' => 'summoned-by', - 'name' => '$LANG.tab_summonedby' - )]; - } - - // tab: related spells - if ($_ = $this->subject->getField('spells')) - { - $relSpells = new SpellList(array(['id', $_])); - if (!$relSpells->error) - { - $this->extendGlobalData($relSpells->getJSGlobals(GLOBALINFO_SELF | GLOBALINFO_RELATED)); - $data = $relSpells->getListviewData(); - - foreach ($data as $relId => $d) - $data[$relId]['trigger'] = array_search($relId, $_); - - $this->lvTabs[] = ['spell', array( - 'data' => array_values($data), - 'id' => 'spells', - 'name' => '$LANG.tab_spells', - 'hiddenCols' => ['skill'], - 'extraCols' => ["\$Listview.funcBox.createSimpleCol('trigger', 'Condition', '10%', 'trigger')"] - )]; - } - } - - // tab: criteria of - $acvs = new AchievementList(array(['ac.type', [ACHIEVEMENT_CRITERIA_TYPE_USE_GAMEOBJECT, ACHIEVEMENT_CRITERIA_TYPE_FISH_IN_GAMEOBJECT]], ['ac.value1', $this->typeId])); - if (!$acvs->error) - { - $this->extendGlobalData($acvs->getJSGlobals(GLOBALINFO_SELF | GLOBALINFO_RELATED)); - - $this->lvTabs[] = ['achievement', array( - 'data' => array_values($acvs->getListviewData()), - 'id' => 'criteria-of', - 'name' => '$LANG.tab_criteriaof' - )]; - } - - // tab: starts quest - // tab: ends quest - $startEnd = new QuestList(array(['qse.type', TYPE_OBJECT], ['qse.typeId', $this->typeId])); - if (!$startEnd->error) - { - $this->extendGlobalData($startEnd->getJSGlobals()); - $lvData = $startEnd->getListviewData(); - $_ = [[], []]; - - foreach ($startEnd->iterate() as $id => $__) - { - $m = $startEnd->getField('method'); - if ($m & 0x1) - $_[0][] = $lvData[$id]; - if ($m & 0x2) - $_[1][] = $lvData[$id]; - } - - if ($_[0]) - $this->lvTabs[] = ['quest', array( - 'data' => array_values($_[0]), - 'name' => '$LANG.tab_starts', - 'id' => 'starts' - )]; - - if ($_[1]) - $this->lvTabs[] = ['quest', array( - 'data' => array_values($_[1]), - 'name' => '$LANG.tab_ends', - 'id' => 'ends' - )]; - } - - // tab: related quests - if ($_ = $this->subject->getField('reqQuest')) - { - $relQuest = new QuestList(array(['id', $_])); - if (!$relQuest->error) - { - $this->extendGlobalData($relQuest->getJSGlobals()); - - $this->lvTabs[] = ['quest', array( - 'data' => array_values($relQuest->getListviewData()), - 'name' => '$LANG.tab_quests', - 'id' => 'quests' - )]; - } - } - - // tab: contains - $reqQuest = []; - if ($_ = $this->subject->getField('lootId')) - { - $goLoot = new Loot(); - if ($goLoot->getByContainer(LOOT_GAMEOBJECT, $_)) - { - $extraCols = $goLoot->extraCols; - $extraCols[] = '$Listview.extraCols.percent'; - $hiddenCols = ['source', 'side', 'slot', 'reqlevel']; - - $this->extendGlobalData($goLoot->jsGlobals); - - foreach ($goLoot->iterate() as &$lv) - { - if (!empty($hiddenCols)) - foreach ($hiddenCols as $k => $str) - if (!empty($lv[$str])) - unset($hiddenCols[$k]); - - if (!$lv['quest']) - continue; - - $extraCols[] = 'Listview.extraCols.condition'; - $reqQuest[$lv['id']] = 0; - $lv['condition'][0][$this->typeId][] = [[CND_QUESTTAKEN, &$reqQuest[$lv['id']]]]; - } - - $tabData = array( - 'data' => array_values($goLoot->getResult()), - 'id' => 'contains', - 'name' => '$LANG.tab_contains', - 'sort' => ['-percent', 'name'], - 'extraCols' => $extraCols - ); - - if ($hiddenCols) - $tabData['hiddenCols'] = $hiddenCols; - - $this->lvTabs[] = ['item', $tabData]; - } - } - - if ($reqIds = array_keys($reqQuest)) // apply quest-conditions as back-reference - { - $conditions = array( - 'OR', - ['reqSourceItemId1', $reqIds], ['reqSourceItemId2', $reqIds], - ['reqSourceItemId3', $reqIds], ['reqSourceItemId4', $reqIds], - ['reqItemId1', $reqIds], ['reqItemId2', $reqIds], ['reqItemId3', $reqIds], - ['reqItemId4', $reqIds], ['reqItemId5', $reqIds], ['reqItemId6', $reqIds] - ); - - $reqQuests = new QuestList($conditions); - $this->extendGlobalData($reqQuests->getJSGlobals()); - - foreach ($reqQuests->iterate() as $qId => $__) - { - if (empty($reqQuests->requires[$qId][TYPE_ITEM])) - continue; - - foreach ($reqIds as $rId) - if (in_array($rId, $reqQuests->requires[$qId][TYPE_ITEM])) - $reqQuest[$rId] = $reqQuests->id; - } - } - - // tab: Same model as .. whats the fucking point..? - $sameModel = new GameObjectList(array(['displayId', $this->subject->getField('displayId')], ['id', $this->typeId, '!'])); - if (!$sameModel->error) - { - $this->extendGlobalData($sameModel->getJSGlobals()); - - $this->lvTabs[] = ['object', array( - 'data' => array_values($sameModel->getListviewData()), - 'name' => '$LANG.tab_samemodelas', - 'id' => 'same-model-as' - )]; - } - } - - protected function generateTooltip($asError = false) - { - if ($asError) - return '$WowheadPower.registerObject('.$this->typeId.', '.User::$localeId.', {});'; - - $s = $this->subject->getSpawns(SPAWNINFO_SHORT); - - $x = '$WowheadPower.registerObject('.$this->typeId.', '.User::$localeId.", {\n"; - $x .= "\tname_".User::$localeString.": '".Util::jsEscape($this->subject->getField('name', true))."',\n"; - $x .= "\ttooltip_".User::$localeString.": '".Util::jsEscape($this->subject->renderTooltip())."',\n"; - $x .= "\tmap: ".($s ? "{zone: ".$s[0].", coords: {".$s[1].":".Util::toJSON($s[2])."}}" : '{}')."\n"; - $x .= "});"; - - return $x; - } - - public function display($override = '') - { - if ($this->mode != CACHE_TYPE_TOOLTIP) - return parent::display($override); - - if (!$this->loadCache($tt)) - { - $tt = $this->generateTooltip(); - $this->saveCache($tt); - } - - header('Content-type: application/x-javascript; charset=utf-8'); - die($tt); - } - - public function notFound($title = '', $msg = '') - { - if ($this->mode != CACHE_TYPE_TOOLTIP) - return parent::notFound($title ?: Lang::game('object'), $msg ?: Lang::gameObject('notFound')); - - header('Content-type: application/x-javascript; charset=utf-8'); - echo $this->generateTooltip(true); - exit(); - } -} - -?> diff --git a/pages/objects.php b/pages/objects.php deleted file mode 100644 index 27717b14..00000000 --- a/pages/objects.php +++ /dev/null @@ -1,91 +0,0 @@ -getCategoryFromUrl($pageParam);; - $this->filterObj = new GameObjectListFilter(false, ['parentCats' => $this->category]); - - parent::__construct($pageCall, $pageParam); - - $this->name = Util::ucFirst(Lang::game('objects')); - $this->subCat = $pageParam ? '='.$pageParam : ''; - } - - protected function generateContent() - { - $this->addJS('?data=zones&locale='.User::$localeId.'&t='.$_SESSION['dataKey']); - - $conditions = []; - - if (!User::isInGroup(U_GROUP_EMPLOYEE)) - $conditions[] = [['cuFlags', CUSTOM_EXCLUDE_FOR_LISTVIEW, '&'], 0]; - - if ($this->category) - $conditions[] = ['typeCat', (int)$this->category[0]]; - - // recreate form selection - $this->filter = $this->filterObj->getForm(); - $this->filter['query'] = isset($_GET['filter']) ? $_GET['filter'] : null; - $this->filter['initData'] = ['init' => 'objects']; - - if ($x = $this->filterObj->getSetCriteria()) - $this->filter['initData']['sc'] = $x; - - if ($_ = $this->filterObj->getConditions()) - $conditions[] = $_; - - $tabData = ['data' => []]; - $objects = new GameObjectList($conditions, ['extraOpts' => $this->filterObj->extraOpts]); - if (!$objects->error) - { - $tabData['data'] = array_values($objects->getListviewData()); - if ($objects->hasSetFields(['reqSkill'])) - $tabData['visibleCols'] = ['skill']; - - // create note if search limit was exceeded - if ($objects->getMatches() > CFG_SQL_LIMIT_DEFAULT) - { - $tabData['note'] = sprintf(Util::$tryFilteringString, 'LANG.lvnote_objectsfound', $objects->getMatches(), CFG_SQL_LIMIT_DEFAULT); - $tabData['_truncated'] = 1; - } - - if ($this->filterObj->error) - $tabData['_errors'] = 1; - } - - $this->lvTabs[] = ['object', $tabData]; - } - - protected function generateTitle() - { - array_unshift($this->title, $this->name); - if ($this->category) - array_unshift($this->title, Lang::gameObject('cat', $this->category[0])); - } - - protected function generatePath() - { - if ($this->category) - $this->path[] = $this->category[0]; - } -} - -?> diff --git a/pages/pet.php b/pages/pet.php deleted file mode 100644 index daa5edd1..00000000 --- a/pages/pet.php +++ /dev/null @@ -1,185 +0,0 @@ -typeId = intVal($id); - - $this->subject = new PetList(array(['id', $this->typeId])); - if ($this->subject->error) - $this->notFound(Lang::game('pet'), Lang::pet('notFound')); - - $this->name = $this->subject->getField('name', true); - } - - protected function generatePath() - { - $this->path[] = $this->subject->getField('type'); - } - - protected function generateTitle() - { - array_unshift($this->title, $this->name, Util::ucFirst(Lang::game('pet'))); - } - - protected function generateContent() - { - $this->addJS('?data=zones&locale='.User::$localeId.'&t='.$_SESSION['dataKey']); - - /***********/ - /* Infobox */ - /***********/ - - $infobox = Lang::getInfoBoxForFlags($this->subject->getField('cuFlags')); - - // level range - $infobox[] = Lang::game('level').Lang::main('colon').$this->subject->getField('minLevel').' - '.$this->subject->getField('maxLevel'); - - // exotic - if ($this->subject->getField('exotic')) - $infobox[] = '[url=?spell=53270]'.Lang::pet('exotic').'[/url]'; - - /****************/ - /* Main Content */ - /****************/ - - $this->infobox = '[ul][li]'.implode('[/li][li]', $infobox).'[/li][/ul]'; - $this->headIcons = [$this->subject->getField('iconString')]; - $this->expansion = Util::$expansionString[$this->subject->getField('expansion')]; - $this->redButtons = array( - BUTTON_WOWHEAD => true, - BUTTON_LINKS => ['type' => $this->type, 'typeId' => $this->typeId], - BUTTON_TALENT => ['href' => '?petcalc#'.Util::$tcEncoding[(int)($this->typeId / 10)] . Util::$tcEncoding[(2 * ($this->typeId % 10) + ($this->subject->getField('exotic') ? 1 : 0))], 'pet' => true] - ); - - /**************/ - /* Extra Tabs */ - /**************/ - - // tab: tameable & gallery - $condition = array( - ['ct.type', 1], // Beast - ['ct.typeFlags', 0x1, '&'], // tameable - ['ct.family', $this->typeId], // displayed petType - [ - 'OR', // at least neutral to at least one faction - ['ft.A', 1, '<'], - ['ft.H', 1, '<'] - ] - ); - $tng = new CreatureList($condition); - - $this->lvTabs[] = ['creature', array( - 'data' => array_values($tng->getListviewData(NPCINFO_TAMEABLE)), - 'name' => '$LANG.tab_tameable', - 'hiddenCols' => ['type'], - 'visibleCols' => ['skin'], - 'note' => sprintf(Util::$filterResultString, '?npcs=1&filter=fa=38'), - 'id' => 'tameable' - )]; - $this->lvTabs[] = ['model', array( - 'data' => array_values($tng->getListviewData(NPCINFO_MODEL)) - )]; - - // tab: diet - $list = []; - $mask = $this->subject->getField('foodMask'); - for ($i = 1; $i < 9; $i++) - if ($mask & (1 << ($i - 1))) - $list[] = $i; - - $food = new ItemList(array(['i.subClass', [5, 8]], ['i.FoodType', $list], CFG_SQL_LIMIT_NONE)); - $this->extendGlobalData($food->getJSGlobals()); - - $this->lvTabs[] = ['item', array( - 'data' => array_values($food->getListviewData()), - 'name' => '$LANG.diet', - 'hiddenCols' => ['source', 'slot', 'side'], - 'sort' => ['level'], - 'id' => 'diet' - )]; - - // tab: spells - $mask = 0x0; - foreach (Game::$skillLineMask[-1] as $idx => $pair) - { - if ($pair[0] == $this->typeId) - { - $mask = 1 << $idx; - break; - } - } - $conditions = [ - ['s.typeCat', -3], // Pet-Ability - [ - 'OR', - // match: first skillLine - ['skillLine1', $this->subject->getField('skillLineId')], - // match: second skillLine (if not mask) - ['AND', ['skillLine1', 0, '>'], ['skillLine2OrMask', $this->subject->getField('skillLineId')]], - // match: skillLineMask (if mask) - ['AND', ['skillLine1', -1], ['skillLine2OrMask', $mask, '&']] - ] - ]; - - $spells = new SpellList($conditions); - $this->extendGlobalData($spells->getJSGlobals(GLOBALINFO_SELF)); - - $this->lvTabs[] = ['spell', array( - 'data' => array_values($spells->getListviewData()), - 'name' => '$LANG.tab_abilities', - 'visibleCols' => ['schools', 'level'], - 'id' => 'abilities' - )]; - - // tab: talents - $conditions = array( - ['s.typeCat', -7], - [ // last rank or unranked - 'OR', - ['s.cuFlags', SPELL_CU_LAST_RANK, '&'], - ['s.rankNo', 0] - ] - ); - - switch ($this->subject->getField('type')) - { - case 0: $conditions[] = ['s.cuFlags', SPELL_CU_PET_TALENT_TYPE0, '&']; break; - case 1: $conditions[] = ['s.cuFlags', SPELL_CU_PET_TALENT_TYPE1, '&']; break; - case 2: $conditions[] = ['s.cuFlags', SPELL_CU_PET_TALENT_TYPE2, '&']; break; - } - - $talents = new SpellList($conditions); - $this->extendGlobalData($talents->getJSGlobals(GLOBALINFO_SELF)); - - $this->lvTabs[] = ['spell', array( - 'data' => array_values($talents->getListviewData()), - 'visibleCols' => ['tier', 'level'], - 'name' => '$LANG.tab_talents', - 'id' => 'talents', - 'sort' => ['tier', 'name'], - '_petTalents' => 1 - )]; - } -} - -?> diff --git a/pages/pets.php b/pages/pets.php deleted file mode 100644 index a3d1b814..00000000 --- a/pages/pets.php +++ /dev/null @@ -1,71 +0,0 @@ -getCategoryFromUrl($pageParam);; - - parent::__construct($pageCall, $pageParam); - - $this->name = Util::ucFirst(Lang::game('pets')); - } - - protected function generateContent() - { - $conditions = []; - - if (!User::isInGroup(U_GROUP_EMPLOYEE)) - $conditions[] = [['cuFlags', CUSTOM_EXCLUDE_FOR_LISTVIEW, '&'], 0]; - - if ($this->category) - $conditions[] = ['type', (int)$this->category[0]]; - - $data = []; - $pets = new PetList($conditions); - if (!$pets->error) - { - $this->extendGlobalData($pets->getJSGlobals(GLOBALINFO_RELATED)); - - $data = array( - 'data' => array_values($pets->getListviewData()), - 'visibleCols' => ['abilities'], - 'computeDataFunc' => '$_' - ); - - if (!$pets->hasDiffFields(['type'])) - $data['hiddenCols'] = ['type']; - }; - $this->lvTabs[] = ['pet', $data, 'petFoodCol']; - } - - protected function generateTitle() - { - array_unshift($this->title, Util::ucFirst(Lang::game('pets'))); - if ($this->category) - array_unshift($this->title, Lang::pet('cat', $this->category[0])); - } - - protected function generatePath() - { - if ($this->category) - $this->path[] = $this->category[0]; - } -} - -?> diff --git a/pages/profile.php b/pages/profile.php deleted file mode 100644 index 1c1c1d2e..00000000 --- a/pages/profile.php +++ /dev/null @@ -1,229 +0,0 @@ - 'talentcalc.css'], - ['path' => 'Profiler.css'] - ); - - private $isCustom = false; - private $profile = null; - - public function __construct($pageCall, $pageParam) - { - $params = array_map('urldecode', explode('.', $pageParam)); - if ($params[0]) - $params[0] = Profiler::urlize($params[0]); - if (isset($params[1])) - $params[1] = Profiler::urlize($params[1]); - - parent::__construct($pageCall, $pageParam); - - // temp locale - if ($this->mode == CACHE_TYPE_TOOLTIP && isset($_GET['domain'])) - Util::powerUseLocale($_GET['domain']); - - if (count($params) == 1 && intval($params[0])) - { - // redundancy much? - $this->subjectGUID = intval($params[0]); - $this->profile = intval($params[0]); - - $this->subject = new LocalProfileList(array(['id', intval($params[0])])); - if ($this->subject->error) - $this->notFound(); - - if (!User::isInGroup(U_GROUP_ADMIN | U_GROUP_BUREAU)) - { - if (!($this->subject->getField('cuFlags') & PROFILER_CU_PUBLISHED) && $this->subject->getField('user') != User::$id) - $this->notFound(); - - if (($this->subject->getField('cuFlags') & PROFILER_CU_DELETED)) - $this->notFound(); - } - - if ($this->subject->isCustom()) - $this->isCustom = true; - else - header('Location: '.$this->subject->getProfileUrl(), true, 302); - } - else if (count($params) == 3) - { - $this->getSubjectFromUrl($pageParam); - if (!$this->subjectName) - $this->notFound(); - - // names MUST be ucFirst. Since we don't expect partial matches, search this way - $this->profile = $params; - - // 3 possibilities - // 1) already synced to aowow - if ($subject = DB::Aowow()->selectRow('SELECT id, realmGUID, cuFlags FROM ?_profiler_profiles WHERE realm = ?d AND name = ?', $this->realmId, Util::ucFirst($this->subjectName))) - { - if ($subject['cuFlags'] & PROFILER_CU_NEEDS_RESYNC) - { - $this->handleIncompleteData($params, $subject['realmGUID']); - return; - } - - $this->subjectGUID = $subject['id']; - $this->subject = new LocalProfileList(array(['id', $subject['id']])); - if ($this->subject->error) - $this->notFound(); - } - // 2) not yet synced but exists on realm (and not a gm character) - else if ($char = DB::Characters($this->realmId)->selectRow('SELECT c.guid AS realmGUID, c.name, c.race, c.class, c.level, c.gender, IFNULL(g.name, "") AS guild, 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', Util::ucFirst($this->subjectName), MAX_LEVEL, Profiler::CHAR_GMFLAGS)) - { - $char['realm'] = $this->realmId; - $char['cuFlags'] = PROFILER_CU_NEEDS_RESYNC; - - // create entry from realm with enough basic info to disply tooltips - DB::Aowow()->query('INSERT IGNORE INTO ?_profiler_profiles (?#) VALUES (?a)', array_keys($char), array_values($char)); - - $this->handleIncompleteData($params, $char['realmGUID']); - } - // 3) does not exist at all - else - $this->notFound(); - } - else if (($params && $params[0]) || !isset($_GET['new'])) - $this->notFound(); - } - - protected function generateContent() - { - if ($this->doResync) - return; - - // + .titles ? - $this->addJS('?data=enchants.gems.glyphs.itemsets.pets.pet-talents.quick-excludes.realms.statistics.weight-presets.achievements&locale='.User::$localeId.'&t='.$_SESSION['dataKey']); - - // as demanded by the raid activity tracker - $bossIds = array( -/* Halion */ -/* ruby */ 39863, -/* Valanar, Lana'thel, Saurfang, Festergut, Deathwisper, Marrowgar, Putricide, Rotface, Sindragosa, Valithria, Lich King */ -/* icc */ 37970, 37955, 37813, 36626, 36855, 36612, 36678, 36627, 36853, 36789, 36597, -/* Jaraxxus, Anub'arak */ -/* toc */ 34780, 34564, -/* Onyxia */ -/* ony */ 10184 - ); - // some events have no singular creature to point to .. create dummy entries - $dummyNPCs = [TYPE_NPC => array( - 100001 => ['name_'.User::$localeString => Lang::profiler('dummyNPCs', 100001)], - 200001 => ['name_'.User::$localeString => Lang::profiler('dummyNPCs', 200001)], - 200002 => ['name_'.User::$localeString => Lang::profiler('dummyNPCs', 200002)], - 200003 => ['name_'.User::$localeString => Lang::profiler('dummyNPCs', 200003)] - )]; - - $this->extendGlobalIds(TYPE_NPC, $bossIds); - $this->extendGlobalData($dummyNPCs); - } - - protected function generatePath() - { - - } - - protected function generateTitle() - { - array_unshift($this->title, Util::ucFirst(Lang::game('profile'))); - } - - protected function generateTooltip($asError = false) - { - $id = $this->profile; - if (!$this->isCustom) - $id = "'".$this->profile[0].'.'.$this->profile[1].'.'.urlencode($this->profile[2])."'"; - - $x = '$WowheadPower.registerProfile('.$id.', '.User::$localeId.', {'; - if ($asError) - return $x."});"; - - $name = $this->subject->getField('name'); - $guild = $this->subject->getField('guild'); - $guildRank = $this->subject->getField('guildrank'); - $lvl = $this->subject->getField('level'); - $ra = $this->subject->getField('race'); - $cl = $this->subject->getField('class'); - $gender = $this->subject->getField('gender'); - // $desc = $this->subject->getField('description'); - $title = ''; - if ($_ = $this->subject->getField('chosenTitle')) - $title = (new TitleList(array(['bitIdx', $_])))->getField($gender ? 'female' : 'male', true); - - if ($this->isCustom) - $name .= ' (Custom Profile)'; - else if ($title) - $name = sprintf($title, $name); - - $x .= "\n"; - $x .= "\tname_".User::$localeString.": '".Util::jsEscape($name)."',\n"; - $x .= "\ttooltip_".User::$localeString.": '".$this->subject->renderTooltip()."',\n"; - $x .= "\ticon: \$WH.g_getProfileIcon(".$ra.", ".$cl.", ".$gender.", ".$lvl."),\n"; // (race, class, gender, level, iconOrId, 'medium') - $x .= "});"; - - return $x; - } - - public function display($override = '') - { - if ($this->mode != CACHE_TYPE_TOOLTIP) - return parent::display($override); - - // do not cache profile tooltips - header('Content-type: application/x-javascript; charset=utf-8'); - die($this->generateTooltip()); - } - - public function notFound($title = '', $msg = '') - { - if ($this->mode != CACHE_TYPE_TOOLTIP) - return parent::notFound($title ?: Util::ucFirst(Lang::profiler('profiler')), $msg ?: Lang::profiler('notFound', 'profile')); - - header('Content-type: application/x-javascript; charset=utf-8'); - echo $this->generateTooltip(true); - exit(); - } - - private function handleIncompleteData($params, $guid) - { - if ($this->mode == CACHE_TYPE_TOOLTIP) // enable tooltip display with basic data we just added - { - $this->subject = new LocalProfileList(array(['name', Util::ucFirst($this->subjectName)]), ['sv' => $params[1]]); - if ($this->subject->error) - $this->notFound(); - - $this->profile = $params; - } - else // display empty page and queue status - { - $this->mode = CACHE_TYPE_NONE; - - // queue full fetch - $newId = Profiler::scheduleResync(TYPE_PROFILE, $this->realmId, $guid); - - $this->doResync = ['profile', $newId]; - $this->initialSync(); - } - } -} - -?> diff --git a/pages/profiler.php b/pages/profiler.php deleted file mode 100644 index 08a941b8..00000000 --- a/pages/profiler.php +++ /dev/null @@ -1,29 +0,0 @@ - 'Profiler.css']]; - - protected function generateContent() - { - $this->addJS('?data=realms&locale='.User::$localeId.'&t='.$_SESSION['dataKey']); - } - - protected function generatePath() { } - - protected function generateTitle() - { - array_unshift($this->title, Util::ucFirst(Lang::profiler('profiler'))); - } -} - -?> diff --git a/pages/profiles.php b/pages/profiles.php deleted file mode 100644 index a0943620..00000000 --- a/pages/profiles.php +++ /dev/null @@ -1,183 +0,0 @@ - 5-man), 1 guild .. it puts a resync button on the lv... - - protected $tabId = 1; - protected $path = [1, 5, 0]; - protected $tpl = 'profiles'; - protected $js = ['filters.js', 'profile_all.js', 'profile.js']; - protected $css = [['path' => 'Profiler.css']]; - - public function __construct($pageCall, $pageParam) - { - $this->getSubjectFromUrl($pageParam); - - $realms = []; - foreach (Profiler::getRealms() as $idx => $r) - { - if ($this->region && $r['region'] != $this->region) - continue; - - if ($this->realm && $r['name'] != $this->realm) - continue; - - $this->sumSubjects += DB::Characters($idx)->selectCell('SELECT count(*) FROM characters WHERE deleteInfos_Name IS NULL'); - $realms[] = $idx; - } - - $this->filterObj = new ProfileListFilter(false, ['realms' => $realms]); - - parent::__construct($pageCall, $pageParam); - - $this->name = Util::ucFirst(Lang::game('profiles')); - $this->subCat = $pageParam ? '='.$pageParam : ''; - } - - protected function generateTitle() - { - if ($this->realm) - array_unshift($this->title, $this->realm,/* CFG_BATTLEGROUP,*/ Lang::profiler('regions', $this->region), Lang::game('profiles')); - else if ($this->region) - array_unshift($this->title, Lang::profiler('regions', $this->region), Lang::game('profiles')); - else - array_unshift($this->title, Lang::game('profiles')); - } - - protected function generateContent() - { - $this->addJS('?data=weight-presets.realms&locale='.User::$localeId.'&t='.$_SESSION['dataKey']); - - $conditions = []; - - if ($_ = $this->filterObj->getConditions()) - $conditions[] = $_; - - if (!$this->filterObj->useLocalList) - { - $conditions[] = ['deleteInfos_Name', null]; - $conditions[] = ['level', MAX_LEVEL, '<=']; // prevents JS errors - $conditions[] = [['extra_flags', Profiler::CHAR_GMFLAGS, '&'], 0]; - } - - // recreate form selection - $this->filter = $this->filterObj->getForm(); - $this->filter['query'] = isset($_GET['filter']) ? $_GET['filter'] : null; - $this->filter['initData'] = ['init' => 'profiles']; - - if ($x = $this->filterObj->getSetCriteria()) - { - $this->filter['initData']['sc'] = $x; - - if ($r = array_intersect([9, 12, 15, 18], $x['cr'])) - if (count($r) == 1) - $this->roster = (reset($r) - 6) / 3; // 1, 2, 3, or 4 - } - - $tabData = array( - 'id' => 'characters', - 'hideCount' => 1, - 'visibleCols' => ['race', 'classs', 'level', 'talents', 'achievementpoints', 'gearscore'], - 'onBeforeCreate' => '$pr_initRosterListview' // puts a resync button on the lv - ); - - $extraCols = $this->filterObj->getExtraCols(); - if ($extraCols) - { - $xc = []; - foreach ($extraCols as $idx => $col) - if ($idx > 0) - $xc[] = "\$Listview.funcBox.createSimpleCol('Skill' + ".$idx.", g_spell_skills[".$idx."], '7%', 'skill' + ".$idx.")"; - - $tabData['extraCols'] = $xc; - } - - $miscParams = []; - if ($this->realm) - $miscParams['sv'] = $this->realm; - if ($this->region) - $miscParams['rg'] = $this->region; - if ($_ = $this->filterObj->extraOpts) - $miscParams['extraOpts'] = $_; - - if ($this->filterObj->useLocalList) - $profiles = new LocalProfileList($conditions, $miscParams); - else - $profiles = new RemoteProfileList($conditions, $miscParams); - - if (!$profiles->error) - { - // init these chars on our side and get local ids - if (!$this->filterObj->useLocalList) - $profiles->initializeLocalEntries(); - - $addInfoMask = PROFILEINFO_CHARACTER; - - // init roster-listview - // $_GET['roster'] = 1|2|3|4 originally supplemented this somehow .. 2,3,4 arenateam-size (4 => 5-man), 1 guild - if ($this->roster == 1 && !$profiles->hasDiffFields(['guild']) && $profiles->getField('guild')) - { - $tabData['roster'] = $this->roster; - $tabData['visibleCols'][] = 'guildrank'; - $tabData['hiddenCols'][] = 'guild'; - - $this->roster = Lang::profiler('guildRoster', [$profiles->getField('guildname')]); - } - else if ($this->roster && !$profiles->hasDiffFields(['arenateam']) && $profiles->getField('arenateam')) - { - $tabData['roster'] = $this->roster; - $tabData['visibleCols'][] = 'rating'; - - $addInfoMask |= PROFILEINFO_ARENA; - $this->roster = Lang::profiler('arenaRoster', [$profiles->getField('arenateam')]); - } - else - $this->roster = 0; - - $tabData['data'] = array_values($profiles->getListviewData($addInfoMask, array_filter($extraCols, function ($x) { return $x > 0; }, ARRAY_FILTER_USE_KEY))); - - if ($sc = $this->filterObj->getSetCriteria()) - if (in_array(10, $sc['cr']) && !in_array('guildrank', $tabData['visibleCols'])) - $tabData['visibleCols'][] = 'guildrank'; - - // create note if search limit was exceeded - if ($this->filter['query'] && $profiles->getMatches() > CFG_SQL_LIMIT_DEFAULT) - { - $tabData['note'] = sprintf(Util::$tryFilteringString, 'LANG.lvnote_charactersfound2', $this->sumSubjects, $profiles->getMatches()); - $tabData['_truncated'] = 1; - } - else if ($profiles->getMatches() > CFG_SQL_LIMIT_DEFAULT) - $tabData['note'] = sprintf(Util::$tryFilteringString, 'LANG.lvnote_charactersfound', $this->sumSubjects, 0); - - if ($this->filterObj->useLocalList) - { - if (!empty($tabData['note'])) - $tabData['note'] .= ' + "
'.Lang::profiler('complexFilter').'"'; - else - $tabData['note'] = ''.Lang::profiler('complexFilter').''; - } - - if ($this->filterObj->error) - $tabData['_errors'] = '$1'; - } - else - $this->roster = 0; - - - $this->lvTabs[] = ['profile', $tabData]; - - Lang::sort('game', 'cl'); - Lang::sort('game', 'ra'); - } -} - -?> diff --git a/pages/quest.php b/pages/quest.php deleted file mode 100644 index 0d37b261..00000000 --- a/pages/quest.php +++ /dev/null @@ -1,1314 +0,0 @@ - 'Book.css']]; - protected $js = ['ShowOnMap.js']; - - public function __construct($pageCall, $id) - { - parent::__construct($pageCall, $id); - - // temp locale - if ($this->mode == CACHE_TYPE_TOOLTIP && isset($_GET['domain'])) - Util::powerUseLocale($_GET['domain']); - - $this->typeId = intVal($id); - - $this->subject = new QuestList(array(['id', $this->typeId])); - if ($this->subject->error) - $this->notFound(); - - $this->name = $this->subject->getField('name', true); - } - - protected function generatePath() - { - // recreate path - $this->path[] = $this->subject->getField('cat2'); - if ($_ = $this->subject->getField('cat1')) - $this->path[] = $_; - } - - protected function generateTitle() - { - array_unshift($this->title, $this->name, Util::ucFirst(Lang::game('quest'))); - } - - protected function generateContent() - { - $_level = $this->subject->getField('level'); - $_minLevel = $this->subject->getField('minLevel'); - $_flags = $this->subject->getField('flags'); - $_specialFlags = $this->subject->getField('specialFlags'); - $_side = Game::sideByRaceMask($this->subject->getField('reqRaceMask')); - - /***********/ - /* Infobox */ - /***********/ - - $infobox = Lang::getInfoBoxForFlags($this->subject->getField('cuFlags')); - - // event (todo: assign eventData) - if ($_ = $this->subject->getField('eventId')) - { - $this->extendGlobalIds(TYPE_WORLDEVENT, $_); - $infobox[] = Lang::game('eventShort').Lang::main('colon').'[event='.$_.']'; - } - - // level - if ($_level > 0) - $infobox[] = Lang::game('level').Lang::main('colon').$_level; - - // reqlevel - if ($_minLevel) - { - $lvl = $_minLevel; - if ($_ = $this->subject->getField('maxLevel')) - $lvl .= ' - '.$_; - - $infobox[] = sprintf(Lang::game('reqLevel'), $lvl); - } - - // loremaster (i dearly hope those flags cover every case...) - if ($this->subject->getField('zoneOrSortBak') > 0 && !$this->subject->isRepeatable()) - { - $conditions = array( - ['ac.type', ACHIEVEMENT_CRITERIA_TYPE_COMPLETE_QUESTS_IN_ZONE], - ['ac.value1', $this->subject->getField('zoneOrSortBak')], - ['a.faction', $_side, '&'] - ); - $loremaster = new AchievementList($conditions); - $this->extendGlobalData($loremaster->getJSGlobals(GLOBALINFO_SELF)); - - switch ($loremaster->getMatches()) - { - case 0: - break; - case 1: - $infobox[] = Lang::quest('loremaster').Lang::main('colon').'[achievement='.$loremaster->id.']'; - break; - default: - $lm = Lang::quest('loremaster').Lang::main('colon').'[ul]'; - foreach ($loremaster->iterate() as $id => $__) - $lm .= '[li][achievement='.$id.'][/li]'; - - $infobox[] = $lm.'[/ul]'; - break; - } - } - - // type (maybe expand uppon?) - $_ = []; - if ($_flags & QUEST_FLAG_DAILY) - $_[] = '[tooltip=tooltip_dailyquest]'.Lang::quest('daily').'[/tooltip]'; - else if ($_flags & QUEST_FLAG_WEEKLY) - $_[] = Lang::quest('weekly'); - else if ($_specialFlags & QUEST_FLAG_SPECIAL_MONTHLY) - $_[] = Lang::quest('monthly'); - - if ($t = $this->subject->getField('type')) - $_[] = Lang::quest('questInfo', $t); - - if ($_) - $infobox[] = Lang::game('type').Lang::main('colon').implode(' ', $_); - - // side - $_ = Lang::main('side').Lang::main('colon'); - switch ($_side) - { - case 3: $infobox[] = $_.Lang::game('si', 3); break; - case 2: $infobox[] = $_.'[span class=icon-horde]'.Lang::game('si', 2).'[/span]'; break; - case 1: $infobox[] = $_.'[span class=icon-alliance]'.Lang::game('si', 1).'[/span]'; break; - } - - // races - if ($_ = Lang::getRaceString($this->subject->getField('reqRaceMask'), $__, $jsg, $n, false)) - { - $this->extendGlobalIds(TYPE_RACE, $jsg); - $t = $n == 1 ? Lang::game('race') : Lang::game('races'); - $infobox[] = Util::ucFirst($t).Lang::main('colon').$_; - } - - // classes - if ($_ = Lang::getClassString($this->subject->getField('reqClassMask'), $jsg, $n, false)) - { - $this->extendGlobalIds(TYPE_CLASS, $jsg); - $t = $n == 1 ? Lang::game('class') : Lang::game('classes'); - $infobox[] = Util::ucFirst($t).Lang::main('colon').$_; - } - - // profession / skill - if ($_ = $this->subject->getField('reqSkillId')) - { - $this->extendGlobalIds(TYPE_SKILL, $_); - $sk = '[skill='.$_.']'; - if ($_ = $this->subject->getField('reqSkillPoints')) - $sk .= ' ('.$_.')'; - - $infobox[] = Lang::quest('profession').Lang::main('colon').$sk; - } - - // timer - if ($_ = $this->subject->getField('timeLimit')) - $infobox[] = Lang::quest('timer').Lang::main('colon').Util::formatTime($_ * 1000); - - $startEnd = DB::Aowow()->select('SELECT * FROM ?_quests_startend WHERE questId = ?d', $this->typeId); - - // start - $start = '[icon name=quest_start'.($this->subject->isRepeatable() ? '_daily' : '').']'.Lang::event('start').Lang::main('colon').'[/icon]'; - $s = []; - foreach ($startEnd as $se) - { - if ($se['method'] & 0x1) - { - $this->extendGlobalIds($se['type'], $se['typeId']); - $s[] = ($s ? '[span=invisible]'.$start.'[/span] ' : $start.' ') .'['.Util::$typeStrings[$se['type']].'='.$se['typeId'].']'; - } - } - - if ($s) - $infobox[] = implode('[br]', $s); - - // end - $end = '[icon name=quest_end'.($this->subject->isRepeatable() ? '_daily' : '').']'.Lang::event('end').Lang::main('colon').'[/icon]'; - $e = []; - foreach ($startEnd as $se) - { - if ($se['method'] & 0x2) - { - $this->extendGlobalIds($se['type'], $se['typeId']); - $e[] = ($e ? '[span=invisible]'.$end.'[/span] ' : $end.' ') . '['.Util::$typeStrings[$se['type']].'='.$se['typeId'].']'; - } - } - - if ($e) - $infobox[] = implode('[br]', $e); - - // Repeatable - if ($this->subject->isRepeatable()) - $infobox[] = Lang::quest('repeatable'); - - // sharable | not sharable - $infobox[] = $_flags & QUEST_FLAG_SHARABLE ? Lang::quest('sharable') : Lang::quest('notSharable'); - - // Keeps you PvP flagged - if ($this->subject->isPvPEnabled()) - $infobox[] = Lang::quest('keepsPvpFlag'); - - // difficulty (todo (low): formula unclear. seems to be [minLevel,] -4, -2, (level), +3, +(9 to 15)) - if ($_level > 0) - { - $_ = []; - - // red - if ($_minLevel && $_minLevel < $_level - 4) - $_[] = '[color=q10]'.$_minLevel.'[/color]'; - - // orange - if (!$_minLevel || $_minLevel < $_level - 2) - $_[] = '[color=r1]'.(!$_ && $_minLevel > $_level - 4 ? $_minLevel : $_level - 4).'[/color]'; - - // yellow - $_[] = '[color=r2]'.(!$_ && $_minLevel > $_level - 2 ? $_minLevel : $_level - 2).'[/color]'; - - // green - $_[] = '[color=r3]'.($_level + 3).'[/color]'; - - // grey (is about +/-1 level off) - $_[] = '[color=r4]'.($_level + 3 + ceil(12 * $_level / MAX_LEVEL)).'[/color]'; - - if ($_) - $infobox[] = Lang::game('difficulty').Lang::main('colon').implode('[small]  [/small]', $_); - } - - $this->infobox = '[ul][li]'.implode('[/li][li]', $infobox).'[/li][/ul]'; - - /**********/ - /* Series */ - /**********/ - - // Quest Chain (are there cases where quests go in parallel?) - $chain = array( - array( - array( - 'side' => $_side, - 'typeStr' => Util::$typeStrings[TYPE_QUEST], - 'typeId' => $this->typeId, - 'name' => $this->name, - '_next' => $this->subject->getField('nextQuestIdChain') - ) - ) - ); - - $_ = $chain[0][0]; - while ($_) - { - if ($_ = DB::Aowow()->selectRow('SELECT id AS typeId, IF(id = nextQuestIdChain, 1, 0) AS error, name_loc0, name_loc2, name_loc3, name_loc6, name_loc8, reqRaceMask FROM ?_quests WHERE nextQuestIdChain = ?d', $_['typeId'])) - { - if ($_['error']) - { - trigger_error('Quest '.$_['typeId'].' is in a chain with itself'); - break; - } - - $n = Util::localizedString($_, 'name'); - array_unshift($chain, array( - array( - 'side' => Game::sideByRaceMask($_['reqRaceMask']), - 'typeStr' => Util::$typeStrings[TYPE_QUEST], - 'typeId' => $_['typeId'], - 'name' => mb_strlen($n) > 40 ? mb_substr($n, 0, 40).'…' : $n - ) - )); - } - } - - $_ = end($chain)[0]; - while ($_) - { - if ($_ = DB::Aowow()->selectRow('SELECT id AS typeId, IF(id = nextQuestIdChain, 1, 0) AS error, name_loc0, name_loc2, name_loc3, name_loc6, name_loc8, reqRaceMask, nextQuestIdChain AS _next FROM ?_quests WHERE id = ?d', $_['_next'])) - { - if ($_['error']) // error already triggered - break; - - $n = Util::localizedString($_, 'name'); - array_push($chain, array( - array( - 'side' => Game::sideByRaceMask($_['reqRaceMask']), - 'typeStr' => Util::$typeStrings[TYPE_QUEST], - 'typeId' => $_['typeId'], - 'name' => mb_strlen($n) > 40 ? mb_substr($n, 0, 40).'…' : $n, - '_next' => $_['_next'], - ) - )); - } - } - - if (count($chain) > 1) - $this->series[] = [$chain, null]; - - - // todo (low): sensibly merge the following lists into 'series' - $listGen = function($cnd) - { - $chain = []; - $list = new QuestList($cnd); - if ($list->error) - return null; - - foreach ($list->iterate() as $id => $__) - { - $n = $list->getField('name', true); - $chain[] = array(array( - 'side' => Game::sideByRaceMask($list->getField('reqRaceMask')), - 'typeStr' => Util::$typeStrings[TYPE_QUEST], - 'typeId' => $id, - 'name' => mb_strlen($n) > 40 ? mb_substr($n, 0, 40).'…' : $n - )); - } - - return $chain; - }; - - $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, '!']])], - - // Requires one of these quests (Requires one of the quests to choose from) - ['reqOneQ', array(['exclusiveGroup', 0, '>'], ['nextQuestId', $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')])], - - // Closes Quests (Quests that become inaccessible after completing this quest) - ['closesQ', array(['exclusiveGroup', 0, '>'], ['exclusiveGroup', $this->subject->getField('exclusiveGroup')], ['id', $this->typeId, '!'])], - - // During the quest available these quests (Quests that are available only at run time this quest) - ['enablesQ', array(['prevQuestId', -$this->typeId])], - - // Requires an active quest (Quests during the execution of which is available on the quest) - ['enabledByQ', array(['id', -$this->subject->getField('prevQuestId')])] - ); - - foreach ($extraLists as $el) - if ($_ = $listGen($el[1])) - $this->series[] = [$_, sprintf(Util::$dfnString, Lang::quest($el[0].'Desc'), Lang::quest($el[0]))]; - - /*******************/ - /* Objectives List */ - /*******************/ - - $this->objectiveList = []; - $this->providedItem = []; - - // gather ids for lookup - $olItems = $olNPCs = $olGOs = $olFactions = []; - $olItemData = $olNPCData = $olGOData = null; - - // items - $olItems[0] = array( // srcItem on idx:0 - $this->subject->getField('sourceItemId'), - $this->subject->getField('sourceItemCount'), - false - ); - - for ($i = 1; $i < 7; $i++) // reqItem in idx:1-6 - { - $id = $this->subject->getField('reqItemId'.$i); - $qty = $this->subject->getField('reqItemCount'.$i); - if (!$id || !$qty) - continue; - - $olItems[$i] = [$id, $qty, $id == $olItems[0][0]]; - } - - if ($ids = array_column($olItems, 0)) - { - $olItemData = new ItemList(array(['id', $ids])); - $this->extendGlobalData($olItemData->getJSGlobals(GLOBALINFO_SELF)); - - $providedRequired = false; - foreach ($olItems as $i => list($itemId, $qty, $provided)) - { - if (!$i || !$itemId || !in_array($itemId, $olItemData->getFoundIDs())) - continue; - - if ($provided) - $providedRequired = true; - - $this->objectiveList[] = array( - 'typeStr' => Util::$typeStrings[TYPE_ITEM], - 'id' => $itemId, - 'name' => $olItemData->json[$itemId]['name'], - 'qty' => $qty > 1 ? $qty : 0, - 'quality' => 7 - $olItemData->json[$itemId]['quality'], - 'extraText' => $provided ? ' ('.Lang::quest('provided').')' : '' - ); - } - - // if providd item is not required by quest, list it below other requirements - if (!$providedRequired && $olItems[0][0] && in_array($olItems[0][0], $olItemData->getFoundIDs())) - { - $this->providedItem = array( - 'id' => $olItems[0][0], - 'name' => $olItemData->json[$olItems[0][0]]['name'], - 'qty' => $olItems[0][1] > 1 ? $olItems[0][1] : 0, - 'quality' => 7 - $olItemData->json[$olItems[0][0]]['quality'] - ); - } - } - - // creature or GO... - for ($i = 1; $i < 5; $i++) - { - $id = $this->subject->getField('reqNpcOrGo'.$i); - $qty = $this->subject->getField('reqNpcOrGoCount'.$i); - $altTxt = $this->subject->getField('objectiveText'.$i, true); - if ($id > 0 && $qty) - $olNPCs[$id] = [$qty, $altTxt, []]; - else if ($id < 0 && $qty) - $olGOs[-$id] = [$qty, $altTxt]; - } - - // .. creature kills - if ($ids = array_keys($olNPCs)) - { - $olNPCData = new CreatureList(array('OR', ['id', $ids], ['killCredit1', $ids], ['killCredit2', $ids])); - $this->extendGlobalData($olNPCData->getJSGlobals(GLOBALINFO_SELF)); - - // create proxy-references - foreach ($olNPCData->iterate() as $id => $__) - { - if ($p = $olNPCData->getField('KillCredit1')) - if (isset($olNPCs[$p])) - $olNPCs[$p][2][$id] = $olNPCData->getField('name', true); - - if ($p = $olNPCData->getField('KillCredit2')) - if (isset($olNPCs[$p])) - $olNPCs[$p][2][$id] = $olNPCData->getField('name', true); - } - - foreach ($olNPCs as $i => $pair) - { - if (!$i || !in_array($i, $olNPCData->getFoundIDs())) - continue; - - $ol = array( - 'typeStr' => Util::$typeStrings[TYPE_NPC], - 'id' => $i, - 'name' => $pair[1] ?: Util::localizedString($olNPCData->getEntry($i), 'name'), - 'qty' => $pair[0] > 1 ? $pair[0] : 0, - 'extraText' => (($_specialFlags & QUEST_FLAG_SPECIAL_SPELLCAST) || $pair[1]) ? '' : ' '.Lang::achievement('slain'), - 'proxy' => $pair[2] - ); - - if ($pair[2]) // has proxies assigned, add yourself as another proxy - $ol['proxy'][$i] = Util::localizedString($olNPCData->getEntry($i), 'name'); - - $this->objectiveList[] = $ol; - } - } - - // .. GO interactions - if ($ids = array_keys($olGOs)) - { - $olGOData = new GameObjectList(array(['id', $ids])); - $this->extendGlobalData($olGOData->getJSGlobals(GLOBALINFO_SELF)); - - foreach ($olGOs as $i => $pair) - { - if (!$i || !in_array($i, $olGOData->getFoundIDs())) - continue; - - $this->objectiveList[] = array( - 'typeStr' => Util::$typeStrings[TYPE_OBJECT], - 'id' => $i, - 'name' => $pair[1] ?: Util::localizedString($olGOData->getEntry($i), 'name'), - 'qty' => $pair[0] > 1 ? $pair[0] : 0, - 'extraText' => '' - ); - } - } - - // reputation required - for ($i = 1; $i < 3; $i++) - { - $id = $this->subject->getField('reqFactionId'.$i); - $val = $this->subject->getField('reqFactionValue'.$i); - if (!$id) - continue; - - $olFactions[$id] = $val; - } - - if ($ids = array_keys($olFactions)) - { - $olFactionsData = new FactionList(array(['id', $ids])); - $this->extendGlobalData($olFactionsData->getJSGlobals(GLOBALINFO_SELF)); - - foreach ($olFactions as $i => $val) - { - if (!$i || !in_array($i, $olFactionsData->getFoundIDs())) - continue; - - $this->objectiveList[] = array( - 'typeStr' => Util::$typeStrings[TYPE_FACTION], - 'id' => $i, - 'name' => Util::localizedString($olFactionsData->getEntry($i), 'name'), - 'qty' => sprintf(Util::$dfnString, $val.' '.Lang::achievement('points'), Lang::getReputationLevelForPoints($val)), - 'extraText' => '' - ); - } - } - - // granted spell - if ($_ = $this->subject->getField('sourceSpellId')) - { - $this->extendGlobalIds(TYPE_SPELL, $_); - $this->objectiveList[] = array( - 'typeStr' => Util::$typeStrings[TYPE_SPELL], - 'id' => $_, - 'name' => SpellList::getName($_), - 'qty' => 0, - 'extraText' => ' ('.Lang::quest('provided').')' - ); - } - - // required money - if ($this->subject->getField('rewardOrReqMoney') < 0) - $this->objectiveList[] = ['text' => Lang::quest('reqMoney').Lang::main('colon').Util::formatMoney(abs($this->subject->getField('rewardOrReqMoney')))]; - - // required pvp kills - if ($_ = $this->subject->getField('reqPlayerKills')) - $this->objectiveList[] = ['text' => Lang::quest('playerSlain').' ('.$_.')']; - - /**********/ - /* Mapper */ - /**********/ - - $this->addJS('?data=zones&locale='.User::$localeId.'&t='.$_SESSION['dataKey']); - - // gather points of interest - $mapNPCs = $mapGOs = []; // [typeId, start|end|objective, startItemId] - - // todo (med): this double list creation very much sucks ... - $getItemSource = function ($itemId, $method = 0) use (&$mapNPCs, &$mapGOs) - { - $lootTabs = new Loot(); - if ($lootTabs->getByItem($itemId)) - { - /* - todo (med): sanity check: - there are loot templates that are absolute tosh, containing hundrets of random items (e.g. Peacebloom for Quest "The Horde Needs Peacebloom!") - even without these .. consider quests like "A Donation of Runecloth" .. oh my ..... - should we... - .. display only a maximum of sources? - .. filter sources for low drop chance? - - for the moment: - if an item has >10 sources, only display sources with >80% chance - always filter sources with <1% chance - */ - - $nSources = 0; - foreach ($lootTabs->iterate() as list($type, $data)) - if ($type == 'creature' || $type == 'object') - $nSources += count(array_filter($data['data'], function($val) { return $val['percent'] >= 1.0; })); - - foreach ($lootTabs->iterate() as $idx => list($file, $tabData)) - { - if (!$tabData['data']) - continue; - - foreach ($tabData['data'] as $data) - { - if ($data['percent'] < 1.0) - continue; - - if ($nSources > 10 && $data['percent'] < 80.0) - continue; - - switch ($file) - { - case 'creature': - $mapNPCs[] = [$data['id'], $method, $itemId]; - break; - case 'object': - $mapGOs[] = [$data['id'], $method, $itemId]; - break; - default: - break; - } - } - } - } - - // also there's vendors... - // 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 c.id FROM game_event_npc_vendor genv JOIN creature c ON c.guid = genv.guid WHERE genv.item = ?d', - $itemId, $itemId - ); - foreach ($vendors as $v) - $mapNPCs[] = [$v, $method, $itemId]; - }; - - $addObjectiveSpawns = function (array $spawns, callable $processing) use (&$mObjectives) - { - foreach ($spawns as $zoneId => $zoneData) - { - if (!isset($mObjectives[$zoneId])) - $mObjectives[$zoneId] = array( - 'zone' => 'Zone #'.$zoneId, - 'mappable' => 1, - 'levels' => [] - ); - - foreach ($zoneData as $floor => $floorData) - { - if (!isset($mObjectives[$zoneId]['levels'][$floor])) - $mObjectives[$zoneId]['levels'][$floor] = []; - - foreach ($floorData as $objId => $objData) - $mObjectives[$zoneId]['levels'][$floor][] = $processing($objId, $objData); - } - } - }; - - - // POI: start + end - foreach ($startEnd as $se) - { - if ($se['type'] == TYPE_NPC) - $mapNPCs[] = [$se['typeId'], $se['method'], 0]; - else if ($se['type'] == TYPE_OBJECT) - $mapGOs[] = [$se['typeId'], $se['method'], 0]; - else if ($se['type'] == TYPE_ITEM) - $getItemSource($se['typeId'], $se['method']); - } - - $itemObjectives = []; - $mObjectives = []; - $mZones = []; - $objectiveIdx = 0; - - // POI objectives - // also map olItems to objectiveIdx so every container gets the same pin color - foreach ($olItems as $i => list($itemId, $qty, $provided)) - { - if (!$provided && $itemId) - { - $itemObjectives[$itemId] = $objectiveIdx++; - $getItemSource($itemId); - } - } - - // PSA: 'redundant' data is on purpose (e.g. creature required for kill, also dropps item required to collect) - - // external events - $endTextWrapper = '%s'; - if ($_specialFlags & QUEST_FLAG_SPECIAL_EXT_COMPLETE) - { - // areatrigger - if ($atir = DB::World()->selectCell('SELECT id FROM areatrigger_involvedrelation WHERE quest = ?d', $this->typeId)) - { - if ($atsp = DB::AoWoW()->selectRow('SELECT guid, posX, posY, floor, areaId FROM ?_spawns WHERE `type` = ?d AND `typeId` = ?d', TYPE_AREATRIGGER, $atir)) - $mObjectives[$atsp['areaId']] = array( - 'zone' => 'Zone #'.$atsp['areaId'], - 'mappable' => 1, - 'levels' => array ( - $atsp['floor'] => array ( - array ( - 'type' => -1, // TYPE_AREATRIGGER is internal, the javascript doesn't know it - 'point' => 'requirement', - 'name' => $this->subject->parseText('end', false), - 'coord' => [$atsp['posX'], $atsp['posY']], - 'coords' => [[$atsp['posX'], $atsp['posY']]], - 'objective' => $objectiveIdx++ - ) - ) - ) - ); - } - // complete-spell - else if ($endSpell = new SpellList(array('OR', ['AND', ['effect1Id', 16], ['effect1MiscValue', $this->typeId]], ['AND', ['effect2Id', 16], ['effect2MiscValue', $this->typeId]], ['AND', ['effect3Id', 16], ['effect3MiscValue', $this->typeId]]))) - if (!$endSpell->error) - $endTextWrapper = '%s'; - } - - // ..adding creature kill requirements - if ($olNPCData && !$olNPCData->error) - { - $spawns = $olNPCData->getSpawns(SPAWNINFO_QUEST); - $addObjectiveSpawns($spawns, function ($npcId, $npcData) use ($olNPCs, &$objectiveIdx) - { - $npcData['point'] = 'requirement'; // always requirement - foreach ($olNPCs as $proxyNpcId => $npc) - { - if ($npc[1] && $npcId == $proxyNpcId) // overwrite creature name with quest specific text, if set. - $npcData['name'] = $npc[1]; - - if (!empty($npc[2][$npcId])) - $npcData['objective'] = $proxyNpcId; - } - - if (!$npcData['objective']) - $npcData['objective'] = $objectiveIdx++; - - return $npcData; - }); - } - - // ..adding object interaction requirements - if ($olGOData && !$olGOData->error) - { - $spawns = $olGOData->getSpawns(SPAWNINFO_QUEST); - $addObjectiveSpawns($spawns, function ($goId, $goData) use ($olGOs, &$objectiveIdx) - { - foreach ($olGOs as $_goId => $go) - { - if ($go[1] && $goId == $_goId) // overwrite object name with quest specific text, if set. - { - $goData['name'] = $go[1]; - break; - } - } - - $goData['point'] = 'requirement'; // always requirement - $goData['objective'] = $objectiveIdx++; - return $goData; - }); - } - - // .. adding npc from: droping queststart item; dropping item needed to collect; starting quest; ending quest - if ($mapNPCs) - { - $npcs = new CreatureList(array(['id', array_column($mapNPCs, 0)])); - if (!$npcs->error) - { - $startEndDupe = []; // if quest starter/ender is the same creature, we need to add it twice - $spawns = $npcs->getSpawns(SPAWNINFO_QUEST); - $addObjectiveSpawns($spawns, function ($npcId, $npcData) use ($mapNPCs, &$startEndDupe, $itemObjectives) - { - foreach ($mapNPCs as $mn) - { - if ($mn[0] != $npcId) - continue; - - if ($mn[2]) // source for itemId - $npcData['item'] = ItemList::getName($mn[2]); - - switch ($mn[1]) // method - { - case 1: // quest start - $npcData['point'] = $mn[2] ? 'sourcestart' : 'start'; - break; - case 2: // quest end (sourceend doesn't actually make sense .. oh well....) - $npcData['point'] = $mn[2] ? 'sourceend' : 'end'; - break; - case 3: // quest start & end - $npcData['point'] = $mn[2] ? 'sourcestart' : 'start'; - $startEndDupe = $npcData; - $startEndDupe['point'] = $mn[2] ? 'sourceend' : 'end'; - break; - default: // just something to kill for quest - $npcData['point'] = $mn[2] ? 'sourcerequirement' : 'requirement'; - if ($mn[2] && !empty($itemObjectives[$mn[2]])) - $npcData['objective'] = $itemObjectives[$mn[2]]; - } - } - - return $npcData; - }); - - if ($startEndDupe) - foreach ($spawns as $zoneId => $zoneData) - foreach ($zoneData as $floor => $floorData) - foreach ($floorData as $objId => $objData) - if ($objId == $startEndDupe['id']) - { - $mObjectives[$zoneId]['levels'][$floor][] = $startEndDupe; - break 3; - } - } - } - - // .. adding go from: containing queststart item; containing item needed to collect; starting quest; ending quest - if ($mapGOs) - { - $gos = new GameObjectList(array(['id', array_column($mapGOs, 0)])); - if (!$gos->error) - { - $startEndDupe = []; // if quest starter/ender is the same object, we need to add it twice - $spawns = $gos->getSpawns(SPAWNINFO_QUEST); - $addObjectiveSpawns($spawns, function ($goId, $goData) use ($mapGOs, &$startEndDupe, $itemObjectives) - { - foreach ($mapGOs as $mgo) - { - if ($mgo[0] != $goId) - continue; - - if ($mgo[2]) // source for itemId - $goData['item'] = ItemList::getName($mgo[2]); - - switch ($mgo[1]) // method - { - case 1: // quest start - $goData['point'] = $mgo[2] ? 'sourcestart' : 'start'; - break; - case 2: // quest end (sourceend doesn't actually make sense .. oh well....) - $goData['point'] = $mgo[2] ? 'sourceend' : 'end'; - break; - case 3: // quest start & end - $goData['point'] = $mgo[2] ? 'sourcestart' : 'start'; - $startEndDupe = $goData; - $startEndDupe['point'] = $mgo[2] ? 'sourceend' : 'end'; - break; - default: // just something to kill for quest - $goData['point'] = $mgo[2] ? 'sourcerequirement' : 'requirement'; - if ($mgo[2] && !empty($itemObjectives[$mgo[2]])) - $goData['objective'] = $itemObjectives[$mgo[2]]; - } - } - - return $goData; - }); - - if ($startEndDupe) - foreach ($spawns as $zoneId => $zoneData) - foreach ($zoneData as $floor => $floorData) - foreach ($floorData as $objId => $objData) - if ($objId == $startEndDupe['id']) - { - $mObjectives[$zoneId]['levels'][$floor][] = $startEndDupe; - break 3; - } - } - } - - // ..process zone data - if ($mObjectives) - { - $areas = new ZoneList(array(['id', array_keys($mObjectives)])); - if (!$areas->error) - { - $someIDX = 0; // todo (low): UNK value ... map priority, floor, mapId..? values seen: 0 - 3; doesn't seem to affect anything - foreach ($areas->iterate() as $id => $__) - { - $mObjectives[$id]['zone'] = $areas->getField('name', true); - $mZones[] = [$id, ++$someIDX]; - } - } - } - - // has start & end? - $hasStartEnd = 0x0; - foreach ($mObjectives as $levels) - { - foreach ($levels['levels'] as $floor) - { - foreach ($floor as $entry) - { - if ($entry['point'] == 'start' || $entry['point'] == 'sourcestart') - $hasStartEnd |= 0x1; - else if ($entry['point'] == 'end' || $entry['point'] == 'sourceend') - $hasStartEnd |= 0x2; - } - } - } - - $this->map = $mObjectives ? array( - 'mapperData' => [], // always empty - 'data' => array( - 'parent' => 'mapper-generic', - 'objectives' => $mObjectives, - 'zoneparent' => 'mapper-zone-generic', - 'zones' => $mZones, - 'missing' => count($mZones) > 1 || $hasStartEnd != 0x3 ? 1 : 0 // 0 if everything happens in one zone, else 1 - ) - ) : null; - - - /****************/ - /* Main Content */ - /****************/ - - $this->gains = $this->createGains(); - $this->mail = $this->createMail($maTab, $startEnd); - $this->rewards = $this->createRewards($_side); - $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->suggestedPl = $this->subject->getField('suggestedPlayers'); - $this->unavailable = $_flags & QUEST_FLAG_UNAVAILABLE || $this->subject->getField('cuFlags') & CUSTOM_EXCLUDE_FOR_LISTVIEW; - $this->redButtons = array( - BUTTON_WOWHEAD => true, - BUTTON_LINKS => array( - 'linkColor' => 'ffffff00', - 'linkId' => 'quest:'.$this->typeId.':'.$_level, - 'linkName' => $this->name, - 'type' => $this->type, - 'typeId' => $this->typeId - ) - ); - - if ($maTab) - $this->lvTabs[] = $maTab; - - // 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)) - { - $altQuest = new QuestList(array(['id', abs($pendant)])); - if (!$altQuest->error) - { - $this->transfer = sprintf( - Lang::quest('_transfer'), - $altQuest->id, - $altQuest->getField('name', true), - $pendant > 0 ? 'alliance' : 'horde', - $pendant > 0 ? Lang::game('si', 1) : Lang::game('si', 2) - ); - } - } - - /**************/ - /* Extra Tabs */ - /**************/ - - // tab: see also - $seeAlso = new QuestList(array(['name_loc'.User::$localeId, '%'.$this->name.'%'], ['id', $this->typeId, '!'])); - if (!$seeAlso->error) - { - $this->extendGlobalData($seeAlso->getJSGlobals()); - $this->lvTabs[] = ['quest', array( - 'data' => array_values($seeAlso->getListviewData()), - 'name' => '$LANG.tab_seealso', - 'id' => 'see-also' - )]; - } - - // tab: criteria of - $criteriaOf = new AchievementList(array(['ac.type', ACHIEVEMENT_CRITERIA_TYPE_COMPLETE_QUEST], ['ac.value1', $this->typeId])); - if (!$criteriaOf->error) - { - $this->extendGlobalData($criteriaOf->getJSGlobals()); - $this->lvTabs[] = ['achievement', array( - 'data' => array_values($criteriaOf->getListviewData()), - 'name' => '$LANG.tab_criteriaof', - 'id' => 'criteria-of' - )]; - } - - // tab: conditions - $cnd = []; - if ($_ = $this->subject->getField('reqMinRepFaction')) - { - $cnd[CND_SRC_QUEST_ACCEPT][$this->typeId][0][] = [CND_REPUTATION_RANK, $_, 1 << Game::getReputationLevelForPoints($this->subject->getField('reqMinRepValue'))]; - $this->extendGlobalIds(TYPE_FACTION, $_); - } - - if ($_ = $this->subject->getField('reqMaxRepFaction')) - { - $cnd[CND_SRC_QUEST_ACCEPT][$this->typeId][0][] = [-CND_REPUTATION_RANK, $_, 1 << Game::getReputationLevelForPoints($this->subject->getField('reqMaxRepValue'))]; - $this->extendGlobalIds(TYPE_FACTION, $_); - } - - $_ = Util::getServerConditions([CND_SRC_QUEST_ACCEPT, CND_SRC_QUEST_SHOW_MARK], null, $this->typeId); - if (!empty($_[0])) - { - // awkward merger - if (isset($_[0][CND_SRC_QUEST_ACCEPT][$this->typeId][0])) - { - if (isset($cnd[CND_SRC_QUEST_ACCEPT][$this->typeId][0])) - $cnd[CND_SRC_QUEST_ACCEPT][$this->typeId][0] = array_merge($cnd[CND_SRC_QUEST_ACCEPT][$this->typeId][0], $_[0][CND_SRC_QUEST_ACCEPT][$this->typeId][0]); - else - $cnd[CND_SRC_QUEST_ACCEPT] = $_[0][CND_SRC_QUEST_ACCEPT]; - } - - if (isset($_[0][CND_SRC_QUEST_SHOW_MARK])) - $cnd[CND_SRC_QUEST_SHOW_MARK] = $_[0][CND_SRC_QUEST_SHOW_MARK]; - - $this->extendGlobalData($_[1]); - } - - if ($cnd) - { - $tab = ""; - - $this->lvTabs[] = [null, array( - 'data' => $tab, - 'id' => 'conditions', - 'name' => '$LANG.requires' - )]; - } - } - - protected function generateTooltip($asError = false) - { - if ($asError) - return '$WowheadPower.registerQuest('.$this->typeId.', '.User::$localeId.', {});'; - - $x = '$WowheadPower.registerQuest('.$this->typeId.', '.User::$localeId.", {\n"; - $x .= "\tname_".User::$localeString.": '".Util::jsEscape($this->subject->getField('name', true))."',\n"; - $x .= "\ttooltip_".User::$localeString.': \''.$this->subject->renderTooltip()."'"; - if ($this->subject->isDaily()) - $x .= ",\n\tdaily: 1"; - $x .= "\n});"; - - return $x; - } - - public function display($override = '') - { - if ($this->mode != CACHE_TYPE_TOOLTIP) - return parent::display($override); - - if (!$this->loadCache($tt)) - { - $tt = $this->generateTooltip(); - $this->saveCache($tt); - } - - header('Content-type: application/x-javascript; charset=utf-8'); - die($tt); - } - - public function notFound($title = '', $msg = '') - { - if ($this->mode != CACHE_TYPE_TOOLTIP) - return parent::notFound($title ?: Lang::game('quest'), $msg ?: Lang::quest('notFound')); - - header('Content-type: application/x-javascript; charset=utf-8'); - echo $this->generateTooltip(true); - exit(); - } - - private function createRewards($side) - { - $rewards = []; - - // moneyReward / maxLevelCompensation - $comp = $this->subject->getField('rewardMoneyMaxLevel'); - $questMoney = $this->subject->getField('rewardOrReqMoney'); - if ($questMoney > 0) - { - $rewards['money'] = Util::formatMoney($questMoney); - if ($comp > 0) - $rewards['money'] .= ' ' . sprintf(Lang::quest('expConvert'), Util::formatMoney($questMoney + $comp), MAX_LEVEL); - } - else if ($questMoney <= 0 && $questMoney + $comp > 0) - $rewards['money'] = sprintf(Lang::quest('expConvert2'), Util::formatMoney($questMoney + $comp), MAX_LEVEL); - - // itemChoices - if (!empty($this->subject->choices[$this->typeId][TYPE_ITEM])) - { - $c = $this->subject->choices[$this->typeId][TYPE_ITEM]; - $choiceItems = new ItemList(array(['id', array_keys($c)])); - if (!$choiceItems->error) - { - $this->extendGlobalData($choiceItems->getJSGlobals()); - foreach ($choiceItems->Iterate() as $id => $__) - { - $rewards['choice'][] = array( - 'typeStr' => Util::$typeStrings[TYPE_ITEM], - 'id' => $id, - 'name' => $choiceItems->getField('name', true), - 'quality' => $choiceItems->getField('quality'), - 'qty' => $c[$id], - 'globalStr' => 'g_items' - ); - } - } - } - - // itemRewards - if (!empty($this->subject->rewards[$this->typeId][TYPE_ITEM])) - { - $ri = $this->subject->rewards[$this->typeId][TYPE_ITEM]; - $rewItems = new ItemList(array(['id', array_keys($ri)])); - if (!$rewItems->error) - { - $this->extendGlobalData($rewItems->getJSGlobals()); - foreach ($rewItems->Iterate() as $id => $__) - { - $rewards['items'][] = array( - 'typeStr' => Util::$typeStrings[TYPE_ITEM], - 'id' => $id, - 'name' => $rewItems->getField('name', true), - 'quality' => $rewItems->getField('quality'), - 'qty' => $ri[$id], - 'globalStr' => 'g_items' - ); - } - } - } - - if (!empty($this->subject->rewards[$this->typeId][TYPE_CURRENCY])) - { - $rc = $this->subject->rewards[$this->typeId][TYPE_CURRENCY]; - $rewCurr = new CurrencyList(array(['id', array_keys($rc)])); - if (!$rewCurr->error) - { - $this->extendGlobalData($rewCurr->getJSGlobals()); - foreach ($rewCurr->Iterate() as $id => $__) - { - $rewards['items'][] = array( - 'typeStr' => Util::$typeStrings[TYPE_CURRENCY], - 'id' => $id, - 'name' => $rewCurr->getField('name', true), - 'quality' => 1, - 'qty' => $rc[$id] * ($side == 2 ? -1 : 1), // toggles the icon - 'globalStr' => 'g_gatheredcurrencies' - ); - } - } - } - - // spellRewards - $displ = $this->subject->getField('rewardSpell'); - $cast = $this->subject->getField('rewardSpellCast'); - if ($cast <= 0 && $displ > 0) - { - $cast = $displ; - $displ = 0; - } - - if ($cast > 0 || $displ > 0) - { - $rewSpells = new SpellList(array(['id', [$displ, $cast]])); - $this->extendGlobalData($rewSpells->getJSGlobals()); - - if (User::isInGroup(U_GROUP_EMPLOYEE)) // accurately display, what spell is what - { - $extra = null; - if ($_ = $rewSpells->getEntry($displ)) - $extra = sprintf(Lang::quest('spellDisplayed'), $displ, Util::localizedString($_, 'name')); - - if ($_ = $rewSpells->getEntry($cast)) - { - $rewards['spells']['extra'] = $extra; - $rewards['spells']['cast'][] = array( - 'typeStr' => Util::$typeStrings[TYPE_SPELL], - 'id' => $cast, - 'name' => Util::localizedString($_, 'name'), - 'globalStr' => 'g_spells' - ); - } - } - else // if it has effect:learnSpell display the taught spell instead - { - $teach = []; - foreach ($rewSpells->iterate() as $id => $__) - if ($_ = $rewSpells->canTeachSpell()) - foreach ($_ as $idx) - $teach[$rewSpells->getField('effect'.$idx.'TriggerSpell')] = $id; - - if ($_ = $rewSpells->getEntry($displ)) - { - $rewards['spells']['extra'] = null; - $rewards['spells'][$teach ? 'learn' : 'cast'][] = array( - 'typeStr' => Util::$typeStrings[TYPE_SPELL], - 'id' => $displ, - 'name' => Util::localizedString($_, 'name'), - 'globalStr' => 'g_spells' - ); - } - else if (($_ = $rewSpells->getEntry($cast)) && !$teach) - { - $rewards['spells']['extra'] = null; - $rewards['spells']['cast'][] = array( - 'typeStr' => Util::$typeStrings[TYPE_SPELL], - 'id' => $cast, - 'name' => Util::localizedString($_, 'name'), - 'globalStr' => 'g_spells' - ); - } - else - { - $taught = new SpellList(array(['id', array_keys($teach)])); - if (!$taught->error) - { - $this->extendGlobalData($taught->getJSGlobals()); - $rewards['spells']['extra'] = null; - foreach ($taught->iterate() as $id => $__) - { - $rewards['spells']['learn'][] = array( - 'typeStr' => Util::$typeStrings[TYPE_SPELL], - 'id' => $id, - 'name' => $taught->getField('name', true), - 'globalStr' => 'g_spells' - ); - } - } - } - } - } - - return $rewards; - } - - private function createMail(&$attachmentTab, $startEnd) - { - $mail = []; - - if ($_ = $this->subject->getField('rewardMailTemplateId')) - { - $delay = $this->subject->getField('rewardMailDelay'); - $letter = DB::Aowow()->selectRow('SELECT * FROM ?_mailtemplate WHERE id = ?d', $_); - - $mail = array( - 'delay' => $delay ? sprintf(Lang::quest('mailIn'), Util::formatTime($delay * 1000)) : null, - 'sender' => null, - 'text' => $letter ? Util::parseHtmlText(Util::localizedString($letter, 'text')) : null, - 'subject' => Util::parseHtmlText(Util::localizedString($letter, 'subject')) - ); - - foreach ($startEnd as $se) - { - if (!($se['method'] & 0x2) || $se['type'] != TYPE_NPC) - continue; - - if ($ti = CreatureList::getName($se['typeId'])) - { - $mail['sender'] = sprintf(Lang::quest('mailBy'), $se['typeId'], $ti); - break; - } - } - - $extraCols = ['$Listview.extraCols.percent']; - $mailLoot = new Loot(); - - if ($mailLoot->getByContainer(LOOT_MAIL, $_)) - { - $this->extendGlobalData($mailLoot->jsGlobals); - $attachmentTab = ['item', array( - 'data' => array_values($mailLoot->getResult()), - 'name' => Lang::quest('attachment'), - 'id' => 'mail-attachments', - 'extraCols' => array_merge($extraCols, $mailLoot->extraCols), - 'hiddenCols' => ['side', 'slot', 'reqlevel'] - )]; - } - } - - return $mail; - } - - private function createGains() - { - $gains = []; - - // xp - if ($_ = $this->subject->getField('rewardXP')) - $gains['xp'] = $_; - - // talent points - if ($_ = $this->subject->getField('rewardTalents')) - $gains['tp'] = $_; - - // reputation - for ($i = 1; $i < 6; $i++) - { - $fac = $this->subject->getField('rewardFactionId'.$i); - $qty = $this->subject->getField('rewardFactionValue'.$i); - if (!$fac || !$qty) - continue; - - $rep = array( - 'qty' => [$qty, 0], - 'id' => $fac, - 'name' => FactionList::getName($fac) - ); - - if ($cuRates = DB::World()->selectRow('SELECT * FROM reputation_reward_rate WHERE faction = ?d', $fac)) - { - if ($dailyType = $this->subject->isDaily()) - { - if ($dailyType == 1 && $cuRates['quest_daily_rate'] != 1.0) - $rep['qty'][1] = $rep['qty'][0] * ($cuRates['quest_daily_rate'] - 1); - else if ($dailyType == 2 && $cuRates['quest_weekly_rate'] != 1.0) - $rep['qty'][1] = $rep['qty'][0] * ($cuRates['quest_weekly_rate'] - 1); - else if ($dailyType == 3 && $cuRates['quest_monthly_rate'] != 1.0) - $rep['qty'][1] = $rep['qty'][0] * ($cuRates['quest_monthly_rate'] - 1); - } - else if ($this->subject->isRepeatable() && $cuRates['quest_repeatable_rate'] != 1.0) - $rep['qty'][1] = $rep['qty'][0] * ($cuRates['quest_repeatable_rate'] - 1); - else if ($cuRates['quest_rate'] != 1.0) - $rep['qty'][1] = $rep['qty'][0] * ($cuRates['quest_rate'] - 1); - } - - $gains['rep'][] = $rep; - } - - // title - if ($_ = (new TitleList(array(['id', $this->subject->getField('rewardTitleId')])))->getHtmlizedName()) - $gains['title'] = $_; - - return $gains; - } -} - -?> diff --git a/pages/quests.php b/pages/quests.php deleted file mode 100644 index 478c1072..00000000 --- a/pages/quests.php +++ /dev/null @@ -1,110 +0,0 @@ -validCats = Game::$questClasses; // needs reviewing (not allowed to set this as default) - - $this->getCategoryFromUrl($pageParam); - $this->filterObj = new QuestListFilter(false, ['parentCats' => $this->category]); - - parent::__construct($pageCall, $pageParam); - - $this->name = Util::ucFirst(Lang::game('quests')); - $this->subCat = $pageParam ? '='.$pageParam : ''; - } - - protected function generateContent() - { - $conditions = []; - - if (!User::isInGroup(U_GROUP_EMPLOYEE)) - $conditions[] = [['cuFlags', CUSTOM_EXCLUDE_FOR_LISTVIEW, '&'], 0]; - - if (isset($this->category[1])) - $conditions[] = ['zoneOrSort', $this->category[1]]; - else if (isset($this->category[0])) - $conditions[] = ['zoneOrSort', $this->validCats[$this->category[0]]]; - - if ($_ = $this->filterObj->getConditions()) - $conditions[] = $_; - - $quests = new QuestList($conditions, ['extraOpts' => $this->filterObj->extraOpts]); - - $this->extendGlobalData($quests->getJSGlobals()); - - // recreate form selection - $this->filter = $this->filterObj->getForm(); - $this->filter['query'] = isset($_GET['filter']) ? $_GET['filter'] : null; - $this->filter['initData'] = ['init' => 'quests']; - - $rCols = $this->filterObj->getReputationCols(); - $xCols = $this->filterObj->getExtraCols(); - if ($rCols) - $this->filter['initData']['rc'] = $rCols; - - if ($xCols) - $this->filter['initData']['ec'] = $xCols; - - if ($x = $this->filterObj->getSetCriteria()) - $this->filter['initData']['sc'] = $x; - - $tabData = ['data' => array_values($quests->getListviewData())]; - - if ($rCols) - $tabData['extraCols'] = '$fi_getReputationCols('.json_encode($rCols, JSON_NUMERIC_CHECK | JSON_UNESCAPED_UNICODE).')'; - else if ($xCols) - $tabData['extraCols'] = '$fi_getExtraCols(fi_extraCols, 0, 0)'; - - // create note if search limit was exceeded - if ($quests->getMatches() > CFG_SQL_LIMIT_DEFAULT) - { - $tabData['note'] = sprintf(Util::$tryFilteringString, 'LANG.lvnote_questsfound', $quests->getMatches(), CFG_SQL_LIMIT_DEFAULT); - $tabData['_truncated'] = 1; - } - else if (isset($this->category[1]) && $this->category[1] > 0) - $tabData['note'] = '$$WH.sprintf(LANG.lvnote_questgivers, '.$this->category[1].', g_zones['.$this->category[1].'], '.$this->category[1].')'; - - if ($this->filterObj->error) - $tabData['_errors'] = 1; - - $this->lvTabs[] = ['quest', $tabData]; - } - - protected function generateTitle() - { - array_unshift($this->title, $this->name); - - if (isset($this->category[1])) - array_unshift($this->title, Lang::quest('cat', $this->category[0], $this->category[1])); - else if (isset($this->category[0])) - { - $c0 = Lang::quest('cat', $this->category[0]); - array_unshift($this->title, is_array($c0) ? $c0[0] : $c0); - } - } - - protected function generatePath() - { - foreach ($this->category as $c) - $this->path[] = $c; - } -} - -?> diff --git a/pages/race.php b/pages/race.php deleted file mode 100644 index d21d0a33..00000000 --- a/pages/race.php +++ /dev/null @@ -1,208 +0,0 @@ -typeId = intVal($id); - - $this->subject = new CharRaceList(array(['id', $this->typeId])); - if ($this->subject->error) - $this->notFound(Lang::game('race'), Lang::race('notFound')); - - $this->name = $this->subject->getField('name', true); - } - - protected function generatePath() - { - $this->path[] = $this->typeId; - } - - protected function generateTitle() - { - array_unshift($this->title, $this->subject->getField('name', true), Util::ucFirst(Lang::game('class'))); - } - - protected function generateContent() - { - $infobox = []; - $_mask = 1 << ($this->typeId - 1); - $mountVendors = array( // race => [starter, argent tournament] - null, [384, 33307], [3362, 33553], [1261, 33310], - [4730, 33653], [4731, 33555], [3685, 33556], [7955, 33650], - [7952, 33554], null, [16264, 33557], [17584, 33657] - ); - - /***********/ - /* Infobox */ - /***********/ - - $infobox = Lang::getInfoBoxForFlags($this->subject->getField('cuFlags')); - - // side - if ($_ = $this->subject->getField('side')) - $infobox[] = Lang::main('side').Lang::main('colon').'[span class=icon-'.($_ == 2 ? 'horde' : 'alliance').']'.Lang::game('si', $_).'[/span]'; - - // faction - if ($_ = $this->subject->getField('factionId')) - { - $fac = new FactionList(array(['f.id', $_])); - $this->extendGlobalData($fac->getJSGlobals()); - $infobox[] = Util::ucFirst(Lang::game('faction')).Lang::main('colon').'[faction='.$fac->id.']'; - } - - // leader - if ($_ = $this->subject->getField('leader')) - { - $this->extendGlobalIds(TYPE_NPC, $_); - $infobox[] = Lang::race('racialLeader').Lang::main('colon').'[npc='.$_.']'; - } - - // start area - if ($_ = $this->subject->getField('startAreaId')) - { - $this->extendGlobalIds(TYPE_ZONE, $_); - $infobox[] = Lang::race('startZone').Lang::main('colon').'[zone='.$_.']'; - } - - - /****************/ - /* Main Content */ - /****************/ - - $this->infobox = '[ul][li]'.implode('[/li][li]', $infobox).'[/li][/ul]'; - $this->expansion = Util::$expansionString[$this->subject->getField('expansion')]; - $this->headIcons = array( - 'race_'.strtolower($this->subject->getField('fileString')).'_male', - 'race_'.strtolower($this->subject->getField('fileString')).'_female' - ); - $this->redButtons = array( - BUTTON_WOWHEAD => true, - BUTTON_LINKS => ['type' => $this->type, 'typeId' => $this->typeId] - ); - - - /**************/ - /* Extra Tabs */ - /**************/ - - // Classes - $classes = new CharClassList(array(['racemask', $_mask, '&'])); - if (!$classes->error) - { - $this->extendGlobalData($classes->getJSGlobals()); - $this->lvTabs[] = ['class', ['data' => array_values($classes->getListviewData())]]; - } - - // Tongues - $conditions = array( - ['typeCat', -11], // proficiencies - ['reqRaceMask', $_mask, '&'] // only languages are race-restricted - ); - - $tongues = new SpellList($conditions); - if (!$tongues->error) - { - $this->extendGlobalData($tongues->getJSGlobals()); - $this->lvTabs[] = ['spell', array( - 'data' => array_values($tongues->getListviewData()), - 'id' => 'languages', - 'name' => '$LANG.tab_languages', - 'hiddenCols' => ['reagents'] - )]; - } - - // Racials - $conditions = array( - ['typeCat', -4], // racial traits - ['reqRaceMask', $_mask, '&'] - ); - - $racials = new SpellList($conditions); - if (!$racials->error) - { - $this->extendGlobalData($racials->getJSGlobals()); - $this->lvTabs[] = ['spell', array( - 'data' => array_values($racials->getListviewData()), - 'id' => 'racial-traits', - 'name' => '$LANG.tab_racialtraits', - 'hiddenCols' => ['reagents'] - )]; - } - - // Quests - $conditions = array( - ['reqRaceMask', $_mask, '&'], - [['reqRaceMask', RACE_MASK_HORDE, '&'], RACE_MASK_HORDE, '!'], - [['reqRaceMask', RACE_MASK_ALLIANCE, '&'], RACE_MASK_ALLIANCE, '!'] - ); - - $quests = new QuestList($conditions); - if (!$quests->error) - { - $this->extendGlobalData($quests->getJSGlobals()); - $this->lvTabs[] = ['quest', ['data' => array_values($quests->getListviewData())]]; - } - - // Mounts - // ok, this sucks, but i rather hardcode the trainer, than fetch items by namepart - $items = isset($mountVendors[$this->typeId]) ? DB::World()->selectCol('SELECT item FROM npc_vendor WHERE entry IN (?a)', $mountVendors[$this->typeId]) : 0; - - $conditions = array( - ['i.id', $items], - ['i.class', ITEM_CLASS_MISC], - ['i.subClass', 5], // mounts - ); - - $mounts = new ItemList($conditions); - if (!$mounts->error) - { - $this->extendGlobalData($mounts->getJSGlobals()); - $this->lvTabs[] = ['item', array( - 'data' => array_values($mounts->getListviewData()), - 'id' => 'mounts', - 'name' => '$LANG.tab_mounts', - 'hiddenCols' => ['slot', 'type'] - )]; - } - - // Sounds - if ($vo = DB::Aowow()->selectCol('SELECT soundId AS ARRAY_KEY, gender FROM ?_races_sounds WHERE raceId = ?d', $this->typeId)) - { - $sounds = new SoundList(array(['id', array_keys($vo)])); - if (!$sounds->error) - { - $this->extendGlobalData($sounds->getJSGlobals(GLOBALINFO_SELF)); - $data = $sounds->getListviewData(); - foreach ($data as $id => &$d) - $d['gender'] = $vo[$id]; - - $this->lvTabs[] = ['sound', array( - 'data' => array_values($data), - 'extraCols' => ['$Listview.templates.title.columns[1]'] - )]; - } - } - } -} - - -?> diff --git a/pages/races.php b/pages/races.php deleted file mode 100644 index ce1ccac2..00000000 --- a/pages/races.php +++ /dev/null @@ -1,49 +0,0 @@ -name = Util::ucFirst(Lang::game('races')); - } - - protected function generateContent() - { - $conditions = []; - - if (!User::isInGroup(U_GROUP_EMPLOYEE)) - $conditions[] = [['cuFlags', CUSTOM_EXCLUDE_FOR_LISTVIEW, '&'], 0]; - - $data = []; - $races = new CharRaceList($conditions); - if (!$races->error) - $data = array_values($races->getListviewData()); - - $this->lvTabs[] = ['race', ['data' => $data]]; - } - - protected function generateTitle() - { - array_unshift($this->title, Util::ucFirst(Lang::game('races'))); - } - - protected function generatePath() {} -} - -?> diff --git a/pages/screenshot.php b/pages/screenshot.php deleted file mode 100644 index f5a6388f..00000000 --- a/pages/screenshot.php +++ /dev/null @@ -1,340 +0,0 @@ -[_original].jpg - -class ScreenshotPage extends GenericPage -{ - const MAX_W = 488; - const MAX_H = 325; - - protected $tpl = 'screenshot'; - protected $js = ['Cropper.js']; - protected $css = [['path' => 'Cropper.css']]; - protected $reqAuth = true; - protected $tabId = 0; - - private $tmpPath = 'static/uploads/temp/'; - private $pendingPath = 'static/uploads/screenshots/pending/'; - private $destination = null; - private $minSize = CFG_SCREENSHOT_MIN_SIZE; - - protected $validCats = ['add', 'crop', 'complete', 'thankyou']; - protected $destType = 0; - protected $destTypeId = 0; - protected $imgHash = ''; - - public function __construct($pageCall, $pageParam) - { - parent::__construct($pageCall, $pageParam); - - $this->name = Lang::screenshot('submission'); - $this->command = $pageParam; - - if ($this->minSize <= 0) - { - trigger_error('config error: dimensions for uploaded screenshots equal or less than zero. Value forced to 200', E_USER_WARNING); - $this->minSize = 200; - } - - // get screenshot destination - // target delivered as screenshot=&.. (hash is optional) - if (preg_match('/^screenshot=\w+&(-?\d+)\.(-?\d+)(\.(\w{16}))?$/i', $_SERVER['QUERY_STRING'], $m)) - { - // no such type - if (empty(Util::$typeClasses[$m[1]])) - $this->error(); - - // this type cannot receive screenshots - if (!(get_class_vars(Util::$typeClasses[$m[1]])['contribute'] & CONTRIBUTE_SS)) - $this->error(); - - $t = Util::$typeClasses[$m[1]]; - $c = [['id', intVal($m[2])]]; - - $this->destination = new $t($c); - - // no such typeId - if ($this->destination->error) - $this->error(); - - // only accept/expect hash for crop & complete - if (empty($m[4]) && ($this->command == 'crop' || $this->command == 'complete')) - $this->error(); - else if (!empty($m[4]) && ($this->command == 'add' || $this->command == 'thankyou')) - $this->error(); - else if (!empty($m[4])) - $this->imgHash = $m[4]; - - $this->destType = intVal($m[1]); - $this->destTypeId = intVal($m[2]); - } - else - $this->error(); - } - - protected function generateContent() - { - switch ($this->command) - { - case 'add': - if ($this->handleAdd()) - header('Location: ?screenshot=crop&'.$this->destType.'.'.$this->destTypeId.'.'.$this->imgHash, true, 302); - else - header('Location: ?'.Util::$typeStrings[$this->destType].'='.$this->destTypeId.'#submit-a-screenshot', true, 302); - die(); - case 'crop': - $this->handleCrop(); - break; - case 'complete': - if ($_ = $this->handleComplete()) - $this->notFound(Lang::main('nfPageTitle'), sprintf(Lang::main('intError2'), '#'.$_)); - else - header('Location: ?screenshot=thankyou&'.$this->destType.'.'.$this->destTypeId, true, 302); - die(); - case 'thankyou': - $this->tpl = 'list-page-generic'; - $this->handleThankyou(); - break; - } - } - - - /*******************/ - /* command handler */ - /*******************/ - - - private function handleAdd() - { - $this->imgHash = Util::createHash(16); - - if (!User::canUploadScreenshot()) - { - $_SESSION['error']['ss'] = Lang::screenshot('error', 'notAllowed'); - return false; - } - - if ($_ = $this->validateScreenshot($isPNG)) - { - $_SESSION['error']['ss'] = $_; - return false; - } - - $im = $isPNG ? $this->loadFromPNG() : $this->loadFromJPG(); - if (!$im) - { - $_SESSION['error']['ss'] = Lang::main('intError'); - return false; - } - - $oSize = $rSize = [imagesx($im), imagesy($im)]; - $rel = $oSize[0] / $oSize[1]; - - // check for oversize and refit to crop-screen - if ($rel >= 1.5 && $oSize[0] > self::MAX_W) - $rSize = [self::MAX_W, self::MAX_W / $rel]; - else if ($rel < 1.5 && $oSize[1] > self::MAX_H) - $rSize = [self::MAX_H * $rel, self::MAX_H]; - - $name = User::$displayName.'-'.$this->destType.'-'.$this->destTypeId.'-'.$this->imgHash; - - $this->writeImage($im, $oSize, $name.'_original'); // use this image for work - $this->writeImage($im, $rSize, $name); // use this image to display - - return true; - } - - private function handleCrop() - { - $im = imagecreatefromjpeg($this->tmpPath.$this->ssName().'_original.jpg'); - - $oSize = $rSize = [imagesx($im), imagesy($im)]; - $rel = $oSize[0] / $oSize[1]; - - // check for oversize and refit to crop-screen - if ($rel >= 1.5 && $oSize[0] > self::MAX_W) - $rSize = [self::MAX_W, self::MAX_W / $rel]; - else if ($rel < 1.5 && $oSize[1] > self::MAX_H) - $rSize = [self::MAX_H * $rel, self::MAX_H]; - - // r: resized; o: original - // r: x <= 488 && y <= 325 while x proportional to y - // mincrop is optional and specifies the minimum resulting image size - $this->cropper = [ - 'url' => STATIC_URL.'/uploads/temp/'.$this->ssName().'.jpg', - 'parent' => 'ss-container', - 'oWidth' => $oSize[0], - 'rWidth' => $rSize[0], - 'oHeight' => $oSize[1], - 'rHeight' => $rSize[1], - 'type' => $this->destType, // only used to check against NPC: 15384 [OLDWorld Trigger (DO NOT DELETE)] - 'typeId' => $this->destTypeId // i guess this was used to upload arbitrary imagery - ]; - - // minimum dimensions - if (!User::isInGroup(U_GROUP_STAFF)) - $this->cropper['minCrop'] = $this->minSize; - - // target - $this->infobox = sprintf(Lang::screenshot('displayOn'), Util::ucFirst(Lang::game(Util::$typeStrings[$this->destType])), Util::$typeStrings[$this->destType], $this->destTypeId); - $this->extendGlobalIds($this->destType, $this->destTypeId); - } - - private function handleComplete() - { - // check tmp file - $fullPath = $this->tmpPath.$this->ssName().'_original.jpg'; - if (!file_exists($fullPath)) - return 1; - - // check post data - if (empty($_POST) || empty($_POST['coords'])) - return 2; - - $dims = explode(',', $_POST['coords']); - if (count($dims) != 4) - return 3; - - Util::checkNumeric($dims, NUM_REQ_INT); - - // actually crop the image - $srcImg = imagecreatefromjpeg($fullPath); - - $x = (int)(imagesx($srcImg) * $dims[0]); - $y = (int)(imagesy($srcImg) * $dims[1]); - $w = (int)(imagesx($srcImg) * $dims[2]); - $h = (int)(imagesy($srcImg) * $dims[3]); - - $destImg = imagecreatetruecolor($w, $h); - - imagefill($destImg, 0, 0, imagecolorallocate($destImg, 255, 255, 255)); - imagecopy($destImg, $srcImg, 0, 0, $x, $y, $w, $h); - imagedestroy($srcImg); - - // write to db - $newId = DB::Aowow()->query( - 'INSERT INTO ?_screenshots (type, typeId, userIdOwner, date, width, height, caption) VALUES (?d, ?d, ?d, UNIX_TIMESTAMP(), ?d, ?d, ?)', - $this->destType, $this->destTypeId, - User::$id, - $w, $h, - !empty($_POST['screenshotalt']) ? $_POST['screenshotalt'] : '' - ); - - // write to file - if (is_int($newId)) // 0 is valid, NULL or FALSE is not - imagejpeg($destImg, $this->pendingPath.$newId.'.jpg', 100); - else - return 6; - } - - private function handleThankyou() - { - $this->extraHTML = Lang::screenshot('thanks', 'contrib').'

'; - $this->extraHTML .= sprintf(Lang::screenshot('thanks', 'goBack'), Util::$typeStrings[$this->destType], $this->destTypeId)."

\n"; - $this->extraHTML .= ''.Lang::screenshot('thanks', 'note').''; - } - - - /**********/ - /* helper */ - /**********/ - - - private function loadFromPNG() - { - $image = imagecreatefrompng($_FILES['screenshotfile']['tmp_name']); - $bg = imagecreatetruecolor(imagesx($image), imagesy($image)); - - imagefill($bg, 0, 0, imagecolorallocate($bg, 255, 255, 255)); - imagealphablending($bg, true); - imagecopy($bg, $image, 0, 0, 0, 0, imagesx($image), imagesy($image)); - imagedestroy($image); - - return $bg; - } - - private function loadFromJPG() - { - return imagecreatefromjpeg($_FILES['screenshotfile']['tmp_name']); - } - - private function writeImage($im, $dims, $file) - { - if ($res = imagecreatetruecolor($dims[0], $dims[1])) - if (imagecopyresampled($res, $im, 0, 0, 0, 0, $dims[0], $dims[1], imagesx($im), imagesy($im))) - if (imagejpeg($res, $this->tmpPath.$file.'.jpg', 100)) - return true; - - return false; - } - - private function validateScreenshot(&$isPNG = false) - { - // no upload happened or some error occured - if (!$_FILES || empty($_FILES['screenshotfile'])) - return Lang::screenshot('error', 'selectSS'); - - switch ($_FILES['screenshotfile']['error']) - { - case 1: - trigger_error('validateScreenshot - the file exceeds the maximum size of '.ini_get('upload_max_filesize'), E_USER_WARNING); - return Lang::screenshot('error', 'selectSS'); - case 3: - trigger_error('validateScreenshot - upload was interrupted', E_USER_WARNING); - return Lang::screenshot('error', 'selectSS'); - case 4: - trigger_error('validateScreenshot() - no file was received', E_USER_WARNING); - return Lang::screenshot('error', 'selectSS'); - case 6: - trigger_error('validateScreenshot - temporary upload directory is not set', E_USER_WARNING); - return Lang::main('intError'); - case 7: - trigger_error('validateScreenshot - could not write temporary file to disk', E_USER_WARNING); - return Lang::main('intError'); - } - - // points to invalid file (hack attempt) - if (!is_uploaded_file($_FILES['screenshotfile']['tmp_name'])) - { - trigger_error('validateScreenshot - uploaded file not in upload directory', E_USER_WARNING); - return Lang::main('intError'); - } - - // check if file is an image; allow jpeg, png - $finfo = new finfo(FILEINFO_MIME); // fileInfo appends charset information and other nonsense - $mime = $finfo->file($_FILES['screenshotfile']['tmp_name']); - if (preg_match('/^image\/(png|jpe?g)/i', $mime, $m)) - $isPNG = $m[0] == 'image/png'; - else - return Lang::screenshot('error', 'unkFormat'); - - // invalid file - $is = getimagesize($_FILES['screenshotfile']['tmp_name']); - if (!$is) - return Lang::screenshot('error', 'selectSS'); - - // size-missmatch: 4k UHD upper limit; 150px lower limit - if ($is[0] < $this->minSize || $is[1] < $this->minSize) - return Lang::screenshot('error', 'tooSmall'); - else if ($is[0] > 3840 || $is[1] > 2160) - return Lang::screenshot('error', 'selectSS'); - - return null; - } - - private function ssName() - { - return $this->imgHash ? User::$displayName.'-'.$this->destType.'-'.$this->destTypeId.'-'.$this->imgHash : ''; - } - - protected function generatePath() { } - protected function generateTitle() - { - array_unshift($this->title, Lang::screenshot('submission')); - } -} - -?> diff --git a/pages/search.php b/pages/search.php deleted file mode 100644 index 54108eb1..00000000 --- a/pages/search.php +++ /dev/null @@ -1,1379 +0,0 @@ - search by compare or profiler - else if &opensearch - => suggestions when typing into searchboxes - array:[ - str, // search - str[10], // found - [], // unused - [], // unused - [], // unused - [], // unused - [], // unused - str[10][4] // type, typeId, param1 (4:quality, 3,6,9,10,17:icon, 5,11:faction), param2 (3:quality, 6:rank) - ] - else - => listviews -*/ - - -// tabId 0: Database g_initHeader() -class SearchPage extends GenericPage -{ - protected $tpl = 'search'; - protected $tabId = 0; - protected $mode = CACHE_TYPE_SEARCH; - protected $js = ['swfobject.js']; - protected $lvTabs = []; // [file, data, extraInclude, osInfo] // osInfo:[type, appendix, nMatches, param1, param2] - protected $search = ''; // output - protected $invalid = []; - - private $maxResults = CFG_SQL_LIMIT_SEARCH; - private $searchMask = 0x0; - private $query = ''; // lookup - private $included = []; - private $excluded = []; - private $searches = array( - '_searchCharClass', '_searchCharRace', '_searchTitle', '_searchWorldEvent', '_searchCurrency', - '_searchItemset', '_searchItem', '_searchAbility', '_searchTalent', '_searchGlyph', - '_searchProficiency', '_searchProfession', '_searchCompanion', '_searchMount', '_searchCreature', - '_searchQuest', '_searchAchievement', '_searchStatistic', '_searchZone', '_searchObject', - '_searchFaction', '_searchSkill', '_searchPet', '_searchCreatureAbility', '_searchSpell', - '_searchEmote', '_searchEnchantment', '_searchSound' - ); - - public function __construct($pageCall, $pageParam) - { - $this->search = trim(urlDecode($pageParam)); - $this->query = strtr($this->search, '?*', '_%'); - - // restricted access - if ($this->reqUGroup && !User::isInGroup($this->reqUGroup)) - $this->error(); - - // select search mode - if (isset($_GET['json'])) - { - if ($_ = intVal($this->search)) // allow for search by Id - $this->query = $_; - - $type = isset($_GET['type']) ? intVal($_GET['type']) : 0; - - if (!empty($_GET['slots'])) - $this->searchMask |= SEARCH_TYPE_JSON | 0x40; - else if ($type == TYPE_ITEMSET) - $this->searchMask |= SEARCH_TYPE_JSON | 0x60; - else if ($type == TYPE_ITEM) - $this->searchMask |= SEARCH_TYPE_JSON | 0x40; - } - else if (isset($_GET['opensearch'])) - { - $this->maxResults = CFG_SQL_LIMIT_QUICKSEARCH; - $this->searchMask |= SEARCH_TYPE_OPEN | SEARCH_MASK_OPEN; - } - else - $this->searchMask |= SEARCH_TYPE_REGULAR | SEARCH_MASK_ALL; - - // handle maintenance status for js-cases - if (CFG_MAINTENANCE && !User::isInGroup(U_GROUP_EMPLOYEE) && !($this->searchMask & SEARCH_TYPE_REGULAR)) - $this->notFound(); - - parent::__construct($pageCall, $pageParam); // just to set g_user and g_locale - - // fill include, exclude and ignore - $this->tokenizeQuery(); - - // invalid conditions: not enough characters to search OR no types to search - if ((!$this->included || !($this->searchMask & SEARCH_MASK_ALL)) && !CFG_MAINTENANCE && !(($this->searchMask & SEARCH_TYPE_JSON) && intVal($this->search))) - { - $this->mode = CACHE_TYPE_NONE; - $this->notFound(); - } - } - - private function tokenizeQuery() - { - if (!$this->query) - return; - - foreach (explode(' ', $this->query) as $p) - { - if (!$p) // multiple spaces - continue; - else if ($p[0] == '-') - { - if (mb_strlen($p) < 4) - $this->invalid[] = mb_substr($p, 1); - else - $this->excluded[] = mb_substr($p, 1); - } - else if ($p !== '') - { - if (mb_strlen($p) < 3) - $this->invalid[] = $p; - else - $this->included[] = $p; - } - } - } - - protected function generateCacheKey($withStaff = true) - { - $staff = intVal($withStaff && User::isInGroup(U_GROUP_EMPLOYEE)); - - $key = [$this->mode, $this->searchMask, md5($this->query), $staff, User::$localeId]; - - return implode('_', $key); - } - - protected function postCache() - { - if (!empty($this->lvTabs[3])) // has world events - { - // update WorldEvents to date() - foreach ($this->lvTabs[3][1]['data'] as &$d) - { - $updated = WorldEventList::updateDates($d['_date']); - unset($d['_date']); - $d['startDate'] = $updated['start'] ? date(Util::$dateFormatInternal, $updated['start']) : false; - $d['endDate'] = $updated['end'] ? date(Util::$dateFormatInternal, $updated['end']) : false; - $d['rec'] = $updated['rec']; - } - } - - if ($this->searchMask & SEARCH_TYPE_REGULAR) - { - $foundTotal = 0; - foreach ($this->lvTabs as list($file, $tabData, $_, $osInfo)) - $foundTotal += count($tabData['data']); - - if ($foundTotal == 1) // only one match -> redirect to find - { - $tab = array_pop($this->lvTabs); - $type = Util::$typeStrings[$tab[3][0]]; - $typeId = array_pop($tab[1]['data'])['id']; - - header('Location: ?'.$type.'='.$typeId, true, 302); - exit(); - } - } - } - - protected function generateTitle() - { - array_unshift($this->title, $this->search, Lang::main('search')); - } - - protected function generatePath() { } - - protected function generateContent() // just wrap it, so GenericPage can call and cache it - { - if ($this->mode == CACHE_TYPE_NONE) // search is invalid - return; - - $this->addJS('?data=zones&locale='.User::$localeId.'&t='.$_SESSION['dataKey']); - - $this->performSearch(); - } - - public function notFound($title = '', $msg = '') - { - if ($this->searchMask & SEARCH_TYPE_REGULAR) - { - // empty queries go home - if (!$this->query) - { - header('Location: .', true, 302); - die(); - } - - parent::display(); // errors are handled in the search-template itself - } - else if ($this->searchMask & SEARCH_TYPE_OPEN) - $result = $this->generateOpenSearch(true); - else /* if ($this->searchMask & SEARCH_TYPE_JSON) */ - $result = $this->generateJsonSearch(true); - - header("Content-type: application/x-javascript"); - exit($result); - } - - public function display($override = '') - { - if ($override || ($this->searchMask & SEARCH_TYPE_REGULAR)) - return parent::display($override); - else if ($this->searchMask & SEARCH_TYPE_OPEN) - { - if (!$this->loadCache($open)) - { - $this->performSearch(); - $open = $this->generateOpenSearch(); - $this->saveCache($open); - } - header('Content-type: application/x-javascript; charset=utf-8'); - die($open); - } - else /* if ($this->searchMask & SEARCH_TYPE_JSON) */ - { - if (!$this->loadCache($json)) - { - $this->performSearch(); - $json = $this->generateJsonSearch(); - $this->saveCache($json); - } - header('Content-type: application/x-javascript; charset=utf-8'); - die($json); - } - } - - private function generateJsonSearch($asError = false) // !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; - { - $outItems = ''; - $outSets = ''; - - if (!$asError) - { - // items - if (!empty($this->lvTabs[6][1]['data'])) - { - $items = []; - foreach ($this->lvTabs[6][1]['data'] as $k => $v) - $items[] = Util::toJSON($v); - - $outItems = "\t".implode(",\n\t", $items)."\n"; - } - - // item sets - if (!empty($this->lvTabs[5][1]['data'])) - { - $sets = []; - foreach ($this->lvTabs[5][1]['data'] as $k => $v) - { - unset($v['quality']); - if (!$v['heroic']) - unset($v['heroic']); - - $sets[] = Util::toJSON($v); - } - - $outSets = "\t".implode(",\n\t", $sets)."\n"; - } - } - - return '["'.Util::jsEscape($this->search)."\", [\n".$outItems."],[\n".$outSets.']]'; - } - - private function generateOpenSearch($asError = false) - { - // this one is funny: we want 10 results, ideally equally distributed over each type - $foundTotal = 0; - $limit = $this->maxResults; - $result = array( //idx1: names, idx3: resultUrl; idx7: extraInfo - $this->search, - [], [], [], [], [], [], [] - ); - - foreach ($this->lvTabs as list($_, $_, $_, $osInfo)) - $foundTotal += $osInfo[2]; - - if (!$foundTotal || $asError) - return '["'.Util::jsEscape($this->search).'", []]'; - - foreach ($this->lvTabs as list($_, $tabData, $_, $osInfo)) - { - $max = max(1, intVal($limit * $osInfo[2] / $foundTotal)); - $limit -= $max; - - for ($i = 0; $i < $max; $i++) - { - $data = array_shift($tabData['data']); - if (!$data) - break; - - $hasQ = is_numeric($data['name'][0]) || $data['name'][0] == '@'; - $result[1][] = ($hasQ ? mb_substr($data['name'], 1) : $data['name']).$osInfo[1]; - $result[3][] = HOST_URL.'/?'.Util::$typeStrings[$osInfo[0]].'='.$data['id']; - - $extra = [$osInfo[0], $data['id']]; // type, typeId - - if (isset($osInfo[3][$data['id']])) - $extra[] = $osInfo[3][$data['id']]; // param1 - - if (isset($osInfo[4][$data['id']])) - $extra[] = $osInfo[4][$data['id']]; // param2 - - $result[7][] = $extra; - } - - if ($limit <= 0) - break; - } - - return Util::toJSON($result); - } - - private function createLookup(array $fields = []) - { - // default to name-field - if (!$fields) - $fields[] = 'name_loc'.User::$localeId; - - $qry = []; - foreach ($fields as $f) - { - $sub = []; - foreach ($this->included as $i) - $sub[] = [$f, '%'.$i.'%']; - - foreach ($this->excluded as $x) - $sub[] = [$f, '%'.$x.'%', '!']; - - // single cnd? - if (count($sub) > 1) - array_unshift($sub, 'AND'); - else - $sub = $sub[0]; - - $qry[] = $sub; - } - - // single cnd? - if (count($qry) > 1) - array_unshift($qry, 'OR'); - else - $qry = $qry[0]; - - return $qry; - } - - private function performSearch() - { - $cndBase = ['AND', $this->maxResults]; - - // Exclude internal wow stuff - if (!User::isInGroup(U_GROUP_EMPLOYEE)) - $cndBase[] = [['cuFlags', CUSTOM_EXCLUDE_FOR_LISTVIEW, '&'], 0]; - - $shared = []; - foreach ($this->searches as $idx => $ref) - if ($this->searchMask & (1 << $idx)) - if ($_ = $this->$ref($cndBase, $shared)) - $this->lvTabs[$idx] = $_; - } - - private function _searchCharClass($cndBase) // 0 Classes: $searchMask & 0x00000001 - { - $cnd = array_merge($cndBase, [$this->createLookup()]); - $classes = new CharClassList($cnd); - - if ($data = $classes->getListviewData()) - { - $result['data'] = array_values($data); - $osInfo = [TYPE_CLASS, ' (Class)', $classes->getMatches(), []]; - - if ($this->searchMask & SEARCH_TYPE_OPEN) - foreach ($classes->iterate() as $id => $__) - $osInfo[3][$id] = 'class_'.strToLower($classes->getField('fileString')); - - if ($classes->getMatches() > $this->maxResults) - { - // $result['note'] = sprintf(Util::$tryNarrowingString, 'LANG.lvnote_', $classes->getMatches(), $this->maxResults); - $result['_truncated'] = 1; - } - - return ['class', $result, null, $osInfo]; - } - - return false; - } - - private function _searchCharRace($cndBase) // 1 Races: $searchMask & 0x00000002 - { - $cnd = array_merge($cndBase, [$this->createLookup()]); - $races = new CharRaceList($cnd); - - if ($data = $races->getListviewData()) - { - $result['data'] = array_values($data); - $osInfo = [TYPE_RACE, ' (Race)', $races->getMatches(), []]; - - if ($this->searchMask & SEARCH_TYPE_OPEN) - foreach ($races->iterate() as $id => $__) - $osInfo[3][$id] = 'race_'.strToLower($races->getField('fileString')).'_male'; - - if ($races->getMatches() > $this->maxResults) - { - // $result['note'] = sprintf(Util::$tryNarrowingString, 'LANG.lvnote_', $races->getMatches(), $this->maxResults); - $result['_truncated'] = 1; - } - - return ['race', $result, null, $osInfo]; - } - - return false; - } - - private function _searchTitle($cndBase) // 2 Titles: $searchMask & 0x00000004 - { - $cnd = array_merge($cndBase, [$this->createLookup(['male_loc'.User::$localeId, 'female_loc'.User::$localeId])]); - $titles = new TitleList($cnd); - - if ($data = $titles->getListviewData()) - { - $result['data'] = array_values($data); - $osInfo = [TYPE_TITLE, ' (Title)', $titles->getMatches(), []]; - - if ($this->searchMask & SEARCH_TYPE_OPEN) - foreach ($titles->iterate() as $id => $__) - $osInfo[3][$id] = $titles->getField('side'); - - if ($titles->getMatches() > $this->maxResults) - { - // $result['note'] = sprintf(Util::$tryNarrowingString, 'LANG.lvnote_', $titles->getMatches(), $this->maxResults); - $result['_truncated'] = 1; - } - - return ['title', $result, null, $osInfo]; - } - - return false; - } - - private function _searchWorldEvent($cndBase) // 3 World Events: $searchMask & 0x00000008 - { - $cnd = array_merge($cndBase, array( - array( - 'OR', - $this->createLookup(['h.name_loc'.User::$localeId]), - ['AND', $this->createLookup(['e.description']), ['e.holidayId', 0]] - ) - )); - $wEvents = new WorldEventList($cnd); - - if ($data = $wEvents->getListviewData()) - { - if ($this->searchMask & SEARCH_TYPE_REGULAR) - $this->extendGlobalData($wEvents->getJSGlobals()); - - $result['data'] = array_values($data); - $osInfo = [TYPE_WORLDEVENT, ' (World Event)', $wEvents->getMatches()]; - - // as allways: dates are updated in postCache-step - - if ($wEvents->getMatches() > $this->maxResults) - { - // $result['note'] = sprintf(Util::$tryNarrowingString, 'LANG.lvnote_', $wEvents->getMatches(), $this->maxResults); - $result['_truncated'] = 1; - } - - return ['event', $result, null, $osInfo]; - } - - return false; - } - - private function _searchCurrency($cndBase) // 4 Currencies $searchMask & 0x0000010 - { - $cnd = array_merge($cndBase, [$this->createLookup()]); - $money = new CurrencyList($cnd); - - if ($data = $money->getListviewData()) - { - $result['data'] = array_values($data); - $osInfo = [TYPE_CURRENCY, ' (Currency)', $money->getMatches()]; - - if ($this->searchMask & SEARCH_TYPE_OPEN) - foreach ($money->iterate() as $id => $__) - $osInfo[3][$id] = strToLower($money->getField('iconString')); - - if ($money->getMatches() > $this->maxResults) - { - $result['note'] = sprintf(Util::$tryNarrowingString, 'LANG.lvnote_currenciesfound', $money->getMatches(), $this->maxResults); - $result['_truncated'] = 1; - } - - return ['currency', $result, null, $osInfo]; - } - - return false; - } - - private function _searchItemset($cndBase, &$shared) // 5 Itemsets $searchMask & 0x0000020 - { - $cnd = array_merge($cndBase, [is_int($this->query) ? ['id', $this->query] : $this->createLookup()]); - $sets = new ItemsetList($cnd); - - if ($data = $sets->getListviewData()) - { - if ($this->searchMask & SEARCH_TYPE_REGULAR) - $this->extendGlobalData($sets->getJSGlobals(GLOBALINFO_SELF)); - - $result['data'] = array_values($data); - $osInfo = [TYPE_ITEMSET, ' (Item Set)', $sets->getMatches()]; - - if ($this->searchMask & SEARCH_TYPE_OPEN) - foreach ($sets->iterate() as $id => $__) - $osInfo[3][$id] = $sets->getField('quality'); - - $shared['pcsToSet'] = $sets->pieceToSet; - - if ($sets->getMatches() > $this->maxResults) - { - $result['note'] = sprintf(Util::$tryNarrowingString, 'LANG.lvnote_itemsetsfound', $sets->getMatches(), $this->maxResults); - $result['_truncated'] = 1; - } - - if (isset($result['note'])) - $result['note'] .= ' + LANG.dash + $WH.sprintf(LANG.lvnote_filterresults, \'?itemsets&filter=na='.urlencode($this->search).'\')'; - else - $result['note'] = '$$WH.sprintf(LANG.lvnote_filterresults, \'?itemsets&filter=na='.urlencode($this->search).'\')'; - - return ['itemset', $result, null, $osInfo]; - } - - return false; - } - - private function _searchItem($cndBase, &$shared) // 6 Items $searchMask & 0x0000040 - { - $miscData = []; - $cndAdd = empty($this->query) ? [] : (is_int($this->query) ? ['id', $this->query] : $this->createLookup()); - - if (($this->searchMask & SEARCH_TYPE_JSON) && ($this->searchMask & 0x20) && !empty($shared['pcsToSet'])) - { - $cnd = [['i.id', array_keys($shared['pcsToSet'])], CFG_SQL_LIMIT_NONE]; - $miscData = ['pcsToSet' => $shared['pcsToSet']]; - } - else if (($this->searchMask & SEARCH_TYPE_JSON) && ($this->searchMask & 0x40)) - { - $cnd = $cndBase; - $cnd[] = ['i.class', [ITEM_CLASS_WEAPON, ITEM_CLASS_GEM, ITEM_CLASS_ARMOR]]; - $cnd[] = $cndAdd; - - $slots = isset($_GET['slots']) ? explode(':', $_GET['slots']) : []; - array_walk($slots, function(&$v, $k) { $v = intVal($v); }); - if ($_ = array_filter($slots)) - $cnd[] = ['slot', $_]; - - // trick ItemListFilter into evaluating weights - if (isset($_GET['wt']) && isset($_GET['wtv'])) - $_GET['filter'] = 'wt='.$_GET['wt'].';wtv='.$_GET['wtv']; - - $itemFilter = new ItemListFilter(); - if ($_ = $itemFilter->createConditionsForWeights()) - { - $miscData['extraOpts'] = $itemFilter->extraOpts; - $cnd = array_merge($cnd, [$_]); - } - } - else - $cnd = array_merge($cndBase, [$cndAdd]); - - $items = new ItemList($cnd, $miscData); - - if ($data = $items->getListviewData($this->searchMask & SEARCH_TYPE_JSON ? (ITEMINFO_SUBITEMS | ITEMINFO_JSON) : 0)) - { - if ($this->searchMask & SEARCH_TYPE_REGULAR) - $this->extendGlobalData($items->getJSGlobals()); - - foreach ($items->iterate() as $itemId => $__) - if (!empty($data[$itemId]['subitems'])) - foreach ($data[$itemId]['subitems'] as &$si) - $si['enchantment'] = implode(', ', $si['enchantment']); - - $osInfo = [TYPE_ITEM, ' (Item)', $items->getMatches(), [], []]; - $result['data'] = array_values($data); - - if ($this->searchMask & SEARCH_TYPE_OPEN) - { - foreach ($items->iterate() as $id => $__) - { - $osInfo[3][$id] = $items->getField('iconString'); - $osInfo[4][$id] = $items->getField('quality'); - } - } - - if ($items->getMatches() > $this->maxResults) - { - $result['note'] = sprintf(Util::$tryNarrowingString, 'LANG.lvnote_itemsfound', $items->getMatches(), $this->maxResults); - $result['_truncated'] = 1; - } - - if (isset($result['note'])) - $result['note'] .= ' + LANG.dash + $WH.sprintf(LANG.lvnote_filterresults, \'?items&filter=na='.urlencode($this->search).'\')'; - else - $result['note'] = '$$WH.sprintf(LANG.lvnote_filterresults, \'?items&filter=na='.urlencode($this->search).'\')'; - - return ['item', $result, null, $osInfo]; - } - - return false; - } - - private function _searchAbility($cndBase) // 7 Abilities (Player + Pet) $searchMask & 0x0000080 - { - $cnd = array_merge($cndBase, array( // hmm, inclued classMounts..? - ['s.typeCat', [7, -2, -3]], - [['s.cuFlags', (SPELL_CU_TRIGGERED | SPELL_CU_TALENT), '&'], 0], - [['s.attributes0', 0x80, '&'], 0], - $this->createLookup() - )); - $abilities = new SpellList($cnd); - - if ($data = $abilities->getListviewData()) - { - if ($this->searchMask & SEARCH_TYPE_REGULAR) - $this->extendGlobalData($abilities->getJSGlobals(GLOBALINFO_SELF | GLOBALINFO_RELATED)); - - $vis = ['level', 'singleclass', 'schools']; - if ($abilities->hasSetFields(['reagent1'])) - $vis[] = 'reagents'; - - $osInfo = [TYPE_SPELL, ' (Ability)', $abilities->getMatches(), [], []]; - $result = array( - 'data' => array_values($data), - 'id' => 'abilities', - 'name' => '$LANG.tab_abilities', - 'visibleCols' => $vis - ); - - if ($this->searchMask & SEARCH_TYPE_OPEN) - { - foreach ($abilities->iterate() as $id => $__) - { - $osInfo[3][$id] = strToLower($abilities->getField('iconString')); - $osInfo[4][$id] = $abilities->ranks[$id]; - } - } - - if ($abilities->getMatches() > $this->maxResults) - { - $result['note'] = sprintf(Util::$tryNarrowingString, 'LANG.lvnote_abilitiesfound', $abilities->getMatches(), $this->maxResults); - $result['_truncated'] = 1; - } - - if (isset($result['note'])) - $result['note'] .= ' + LANG.dash + $WH.sprintf(LANG.lvnote_filterresults, \'?spells=7&filter=na='.urlencode($this->search).'\')'; - else - $result['note'] = '$$WH.sprintf(LANG.lvnote_filterresults, \'?spells=7&filter=na='.urlencode($this->search).'\')'; - - return ['spell', $result, null, $osInfo]; - } - - return false; - } - - private function _searchTalent($cndBase) // 8 Talents (Player + Pet) $searchMask & 0x0000100 - { - $cnd = array_merge($cndBase, array( - ['s.typeCat', [-7, -2]], - $this->createLookup() - )); - $talents = new SpellList($cnd); - - if ($data = $talents->getListviewData()) - { - if ($this->searchMask & SEARCH_TYPE_REGULAR) - $this->extendGlobalData($talents->getJSGlobals()); - - $vis = ['level', 'singleclass', 'schools']; - if ($talents->hasSetFields(['reagent1'])) - $vis[] = 'reagents'; - - $osInfo = [TYPE_SPELL, ' (Talent)', $talents->getMatches(), [], []]; - $result = array( - 'data' => array_values($data), - 'id' => 'talents', - 'name' => '$LANG.tab_talents', - 'visibleCols' => $vis - ); - - if ($this->searchMask & SEARCH_TYPE_OPEN) - { - foreach ($talents->iterate() as $id => $__) - { - $osInfo[3][$id] = strToLower($talents->getField('iconString')); - $osInfo[4][$id] = $talents->ranks[$talents->id]; - } - } - - if ($talents->getMatches() > $this->maxResults) - { - $result['note'] = sprintf(Util::$tryNarrowingString, 'LANG.lvnote_talentsfound', $talents->getMatches(), $this->maxResults); - $result['_truncated'] = 1; - } - - if (isset($result['note'])) - $result['note'] .= ' + LANG.dash + $WH.sprintf(LANG.lvnote_filterresults, \'?spells=-2&filter=na='.urlencode($this->search).'\')'; - else - $result['note'] = '$$WH.sprintf(LANG.lvnote_filterresults, \'?spells=-2&filter=na='.urlencode($this->search).'\')'; - - return ['spell', $result, null, $osInfo]; - } - - return false; - } - - private function _searchGlyph($cndBase) // 9 Glyphs $searchMask & 0x0000200 - { - $cnd = array_merge($cndBase, array( - ['s.typeCat', -13], - $this->createLookup() - )); - $glyphs = new SpellList($cnd); - - if ($data = $glyphs->getListviewData()) - { - if ($this->searchMask & SEARCH_TYPE_REGULAR) - $this->extendGlobalData($glyphs->getJSGlobals(GLOBALINFO_SELF)); - - $osInfo = [TYPE_SPELL, ' (Glyph)', $glyphs->getMatches(), []]; - $result = array( - 'data' => array_values($data), - 'id' => 'glyphs', - 'name' => '$LANG.tab_glyphs', - 'visibleCols' => ['singleclass', 'glyphtype'] - ); - - if ($this->searchMask & SEARCH_TYPE_OPEN) - foreach ($glyphs->iterate() as $id => $__) - $osInfo[3][$id] = strToLower($glyphs->getField('iconString')); - - if ($glyphs->getMatches() > $this->maxResults) - { - $result['note'] = sprintf(Util::$tryNarrowingString, 'LANG.lvnote_glyphsfound', $glyphs->getMatches(), $this->maxResults); - $result['_truncated'] = 1; - } - - if (isset($result['note'])) - $result['note'] .= ' + LANG.dash + $WH.sprintf(LANG.lvnote_filterresults, \'?spells=-13&filter=na='.urlencode($this->search).'\')'; - else - $result['note'] = '$$WH.sprintf(LANG.lvnote_filterresults, \'?spells=-13&filter=na='.urlencode($this->search).'\')'; - - return ['spell', $result, null, $osInfo]; - } - - return false; - } - - private function _searchProficiency($cndBase) // 10 Proficiencies $searchMask & 0x0000400 - { - $cnd = array_merge($cndBase, array( - ['s.typeCat', -11], - $this->createLookup() - )); - $prof = new SpellList($cnd); - - if ($data = $prof->getListviewData()) - { - if ($this->searchMask & SEARCH_TYPE_REGULAR) - $this->extendGlobalData($prof->getJSGlobals(GLOBALINFO_SELF)); - - $osInfo = [TYPE_SPELL, ' (Proficiency)', $prof->getMatches(), []]; - $result = array( - 'data' => array_values($data), - 'id' => 'proficiencies', - 'name' => '$LANG.tab_proficiencies', - 'visibleCols' => ['classes'] - ); - - if ($this->searchMask & SEARCH_TYPE_OPEN) - foreach ($prof->iterate() as $id => $__) - $osInfo[3][$id] = strToLower($prof->getField('iconString')); - - if ($prof->getMatches() > $this->maxResults) - { - $result['note'] = sprintf(Util::$tryNarrowingString, 'LANG.lvnote_spellsfound', $prof->getMatches(), $this->maxResults); - $result['_truncated'] = 1; - } - - if (isset($result['note'])) - $result['note'] .= ' + LANG.dash + $WH.sprintf(LANG.lvnote_filterresults, \'?spells=-11&filter=na='.urlencode($this->search).'\')'; - else - $result['note'] = '$$WH.sprintf(LANG.lvnote_filterresults, \'?spells=-11&filter=na='.urlencode($this->search).'\')'; - - return ['spell', $result, null, $osInfo]; - } - - return false; - } - - private function _searchProfession($cndBase) // 11 Professions (Primary + Secondary) $searchMask & 0x0000800 - { - $cnd = array_merge($cndBase, array( - ['s.typeCat', [9, 11]], - $this->createLookup() - )); - $prof = new SpellList($cnd); - - if ($data = $prof->getListviewData()) - { - if ($this->searchMask & SEARCH_TYPE_REGULAR) - $this->extendGlobalData($prof->getJSGlobals(GLOBALINFO_SELF | GLOBALINFO_RELATED)); - - $osInfo = [TYPE_SPELL, ' (Profession)', $prof->getMatches()]; - $result = array( - 'data' => array_values($data), - 'id' => 'professions', - 'name' => '$LANG.tab_professions', - 'visibleCols' => ['source', 'reagents'] - ); - - if ($this->searchMask & SEARCH_TYPE_OPEN) - foreach ($prof->iterate() as $id => $__) - $osInfo[3][$id] = strToLower($prof->getField('iconString')); - - if ($prof->getMatches() > $this->maxResults) - { - $result['note'] = sprintf(Util::$tryNarrowingString, 'LANG.lvnote_professionfound', $prof->getMatches(), $this->maxResults); - $result['_truncated'] = 1; - } - - if (isset($result['note'])) - $result['note'] .= ' + LANG.dash + $WH.sprintf(LANG.lvnote_filterresults, \'?spells=11&filter=na='.urlencode($this->search).'\')'; - else - $result['note'] = '$$WH.sprintf(LANG.lvnote_filterresults, \'?spells=11&filter=na='.urlencode($this->search).'\')'; - - return ['spell', $result, null, $osInfo]; - } - - return false; - } - - private function _searchCompanion($cndBase) // 12 Companions $searchMask & 0x0001000 - { - $cnd = array_merge($cndBase, array( - ['s.typeCat', -6], - $this->createLookup() - )); - $vPets = new SpellList($cnd); - - if ($data = $vPets->getListviewData()) - { - if ($this->searchMask & SEARCH_TYPE_REGULAR) - $this->extendGlobalData($vPets->getJSGlobals()); - - $osInfo = [TYPE_SPELL, ' (Companion)', $vPets->getMatches(), []]; - $result = array( - 'data' => array_values($data), - 'id' => 'companions', - 'name' => '$LANG.tab_companions', - 'visibleCols' => ['reagents'] - ); - - if ($this->searchMask & SEARCH_TYPE_OPEN) - foreach ($vPets->iterate() as $id => $__) - $osInfo[3][$id] = strToLower($vPets->getField('iconString')); - - if ($vPets->getMatches() > $this->maxResults) - { - $result['note'] = sprintf(Util::$tryNarrowingString, 'LANG.lvnote_companionsfound', $vPets->getMatches(), $this->maxResults); - $result['_truncated'] = 1; - } - - if (isset($result['note'])) - $result['note'] .= ' + LANG.dash + $WH.sprintf(LANG.lvnote_filterresults, \'?spells=-6&filter=na='.urlencode($this->search).'\')'; - else - $result['note'] = '$$WH.sprintf(LANG.lvnote_filterresults, \'?spells=-6&filter=na='.urlencode($this->search).'\')'; - - return ['spell', $result, null, $osInfo]; - } - - return false; - } - - private function _searchMount($cndBase) // 13 Mounts $searchMask & 0x0002000 - { - $cnd = array_merge($cndBase, array( - ['s.typeCat', -5], - $this->createLookup() - )); - $mounts = new SpellList($cnd); - - if ($data = $mounts->getListviewData()) - { - if ($this->searchMask & SEARCH_TYPE_REGULAR) - $this->extendGlobalData($mounts->getJSGlobals(GLOBALINFO_SELF)); - - $osInfo = [TYPE_SPELL, ' (Mount)', $mounts->getMatches(), []]; - $result = array( - 'data' => array_values($data), - 'id' => 'mounts', - 'name' => '$LANG.tab_mounts', - ); - - if ($this->searchMask & SEARCH_TYPE_OPEN) - foreach ($mounts->iterate() as $id => $__) - $osInfo[3][$id] = strToLower($mounts->getField('iconString')); - - if ($mounts->getMatches() > $this->maxResults) - { - $result['note'] = sprintf(Util::$tryNarrowingString, 'LANG.lvnote_mountsfound', $mounts->getMatches(), $this->maxResults); - $result['_truncated'] = 1; - } - - if (isset($result['note'])) - $result['note'] .= ' + LANG.dash + $WH.sprintf(LANG.lvnote_filterresults, \'?spells=-5&filter=na='.urlencode($this->search).'\')'; - else - $result['note'] = '$$WH.sprintf(LANG.lvnote_filterresults, \'?spells=-5&filter=na='.urlencode($this->search).'\')'; - - return ['spell', $result, null, $osInfo]; - } - - return false; - } - - private function _searchCreature($cndBase) // 14 NPCs $searchMask & 0x0004000 - { - $cnd = array_merge($cndBase, array( - [['flagsExtra', 0x80], 0], // exclude trigger creatures - [['cuFlags', NPC_CU_DIFFICULTY_DUMMY, '&'], 0], // exclude difficulty entries - $this->createLookup() - )); - $npcs = new CreatureList($cnd); - - if ($data = $npcs->getListviewData()) - { - $osInfo = [TYPE_NPC, ' (NPC)', $npcs->getMatches()]; - $result = array( - 'data' => array_values($data), - 'id' => 'npcs', - 'name' => '$LANG.tab_npcs', - ); - - if ($npcs->getMatches() > $this->maxResults) - { - $result['note'] = sprintf(Util::$tryNarrowingString, 'LANG.lvnote_npcsfound', $npcs->getMatches(), $this->maxResults); - $result['_truncated'] = 1; - } - - if (isset($result['note'])) - $result['note'] .= ' + LANG.dash + $WH.sprintf(LANG.lvnote_filterresults, \'?npcs&filter=na='.urlencode($this->search).'\')'; - else - $result['note'] = '$$WH.sprintf(LANG.lvnote_filterresults, \'?npcs&filter=na='.urlencode($this->search).'\')'; - - return ['creature', $result, null, $osInfo]; - } - - return false; - } - - private function _searchQuest($cndBase) // 15 Quests $searchMask & 0x0008000 - { - $cnd = array_merge($cndBase, array( - [['flags', CUSTOM_UNAVAILABLE | CUSTOM_DISABLED, '&'], 0], - $this->createLookup() - )); - $quests = new QuestList($cnd); - - if ($data = $quests->getListviewData()) - { - $osInfo = [TYPE_QUEST, ' (Quest)', $quests->getMatches()]; - $result['data'] = array_values($data); - - if ($this->searchMask & SEARCH_TYPE_REGULAR) - $this->extendGlobalData($quests->getJSGlobals()); - - if ($quests->getMatches() > $this->maxResults) - { - $result['note'] = sprintf(Util::$tryNarrowingString, 'LANG.lvnote_questsfound', $quests->getMatches(), $this->maxResults); - $result['_truncated'] = 1; - } - - if (isset($result['note'])) - $result['note'] .= ' + LANG.dash + $WH.sprintf(LANG.lvnote_filterresults, \'?quests&filter=na='.urlencode($this->search).'\')'; - else - $result['note'] = '$$WH.sprintf(LANG.lvnote_filterresults, \'?quests&filter=na='.urlencode($this->search).'\')'; - - return ['quest', $result, null, $osInfo]; - } - - return false; - } - - private function _searchAchievement($cndBase) // 16 Achievements $searchMask & 0x0010000 - { - $cnd = array_merge($cndBase, array( - [['flags', ACHIEVEMENT_FLAG_COUNTER, '&'], 0], // not a statistic - $this->createLookup() - )); - $acvs = new AchievementList($cnd); - - if ($data = $acvs->getListviewData()) - { - if ($this->searchMask & SEARCH_TYPE_REGULAR) - $this->extendGlobalData($acvs->getJSGlobals()); - - $osInfo = [TYPE_ACHIEVEMENT, ' (Achievement)', $acvs->getMatches(), []]; - $result = array( - 'data' => array_values($data), - 'visibleCols' => ['category'] - ); - - if ($this->searchMask & SEARCH_TYPE_OPEN) - foreach ($acvs->iterate() as $id => $__) - $osInfo[3][$id] = strToLower($acvs->getField('iconString')); - - if ($acvs->getMatches() > $this->maxResults) - { - $result['note'] = sprintf(Util::$tryNarrowingString, 'LANG.lvnote_achievementsfound', $acvs->getMatches(), $this->maxResults); - $result['_truncated'] = 1; - } - - if (isset($result['note'])) - $result['note'] .= ' + LANG.dash + $WH.sprintf(LANG.lvnote_filterresults, \'?achieveemnts&filter=na='.urlencode($this->search).'\')'; - else - $result['note'] = '$$WH.sprintf(LANG.lvnote_filterresults, \'?achievements&filter=na='.urlencode($this->search).'\')'; - - return ['achievement', $result, null, $osInfo]; - } - - return false; - } - - private function _searchStatistic($cndBase) // 17 Statistics $searchMask & 0x0020000 - { - $cnd = array_merge($cndBase, array( - ['flags', ACHIEVEMENT_FLAG_COUNTER, '&'], // is a statistic - $this->createLookup() - )); - $stats = new AchievementList($cnd); - - if ($data = $stats->getListviewData()) - { - if ($this->searchMask & SEARCH_TYPE_REGULAR) - $this->extendGlobalData($stats->getJSGlobals(GLOBALINFO_SELF)); - - $osInfo = [TYPE_ACHIEVEMENT, ' (Statistic)', $stats->getMatches()]; - $result = array( - 'data' => array_values($data), - 'visibleCols' => ['category'], - 'hiddenCols' => ['side', 'points', 'rewards'], - 'name' => '$LANG.tab_statistics', - 'id' => 'statistics' - ); - - if ($stats->getMatches() > $this->maxResults) - { - $result['note'] = sprintf(Util::$tryNarrowingString, 'LANG.lvnote_statisticsfound', $stats->getMatches(), $this->maxResults); - $result['_truncated'] = 1; - } - - if (isset($result['note'])) - $result['note'] .= ' + LANG.dash + $WH.sprintf(LANG.lvnote_filterresults, \'?achievements=1&filter=na='.urlencode($this->search).'\')'; - else - $result['note'] = '$$WH.sprintf(LANG.lvnote_filterresults, \'?achievements=1&filter=na='.urlencode($this->search).'\')'; - - return ['achievement', $result, null, $osInfo]; - } - - return false; - } - - private function _searchZone($cndBase) // 18 Zones $searchMask & 0x0040000 - { - $cnd = array_merge($cndBase, [$this->createLookup()]); - $zones = new ZoneList($cnd); - - if ($data = $zones->getListviewData()) - { - if ($this->searchMask & SEARCH_TYPE_REGULAR) - $this->extendGlobalData($zones->getJSGlobals()); - - $osInfo = [TYPE_ZONE, ' (Zone)', $zones->getMatches()]; - $result['data'] = array_values($data); - - if ($zones->getMatches() > $this->maxResults) - { - $result['note'] = sprintf(Util::$tryNarrowingString, 'LANG.lvnote_zonesfound', $zones->getMatches(), $this->maxResults); - $result['_truncated'] = 1; - } - - return ['zone', $result, null, $osInfo]; - } - - return false; - } - - private function _searchObject($cndBase) // 19 Objects $searchMask & 0x0080000 - { - $cnd = array_merge($cndBase, [$this->createLookup()]); - $objects = new GameObjectList($cnd); - - if ($data = $objects->getListviewData()) - { - if ($this->searchMask & SEARCH_TYPE_REGULAR) - $this->extendGlobalData($objects->getJSGlobals()); - - $osInfo = [TYPE_OBJECT, ' (Object)', $objects->getMatches()]; - $result['data'] = array_values($data); - - if ($objects->getMatches() > $this->maxResults) - { - $result['note'] = sprintf(Util::$tryNarrowingString, 'LANG.lvnote_objectsfound', $objects->getMatches(), $this->maxResults); - $result['_truncated'] = 1; - } - - if (isset($result['note'])) - $result['note'] .= ' + LANG.dash + $WH.sprintf(LANG.lvnote_filterresults, \'?objects&filter=na='.urlencode($this->search).'\')'; - else - $result['note'] = '$$WH.sprintf(LANG.lvnote_filterresults, \'?objects&filter=na='.urlencode($this->search).'\')'; - - return ['object', $result, null, $osInfo]; - } - - return false; - } - - private function _searchFaction($cndBase) // 20 Factions $searchMask & 0x0100000 - { - $cnd = array_merge($cndBase, [$this->createLookup()]); - $factions = new FactionList($cnd); - - if ($data = $factions->getListviewData()) - { - $osInfo = [TYPE_FACTION, ' (Faction)', $factions->getMatches()]; - $result['data'] = array_values($data); - - if ($factions->getMatches() > $this->maxResults) - { - $result['note'] = sprintf(Util::$tryNarrowingString, 'LANG.lvnote_factionsfound', $factions->getMatches(), $this->maxResults); - $result['_truncated'] = 1; - } - - return ['faction', $result, null, $osInfo]; - } - - return false; - } - - private function _searchSkill($cndBase) // 21 Skills $searchMask & 0x0200000 - { - $cnd = array_merge($cndBase, [$this->createLookup()]); - $skills = new SkillList($cnd); - - if ($data = $skills->getListviewData()) - { - $osInfo = [TYPE_SKILL, ' (Skill)', $skills->getMatches(), []]; - $result['data'] = array_values($data); - - if ($this->searchMask & SEARCH_TYPE_OPEN) - foreach ($skills->iterate() as $id => $__) - $osInfo[3][$id] = $skills->getField('iconString'); - - if ($skills->getMatches() > $this->maxResults) - { - $result['note'] = sprintf(Util::$tryNarrowingString, 'LANG.lvnote_skillsfound', $skills->getMatches(), $this->maxResults); - $result['_truncated'] = 1; - } - - return ['skill', $result, null, $osInfo]; - } - - return false; - } - - private function _searchPet($cndBase) // 22 Pets $searchMask & 0x0400000 - { - $cnd = array_merge($cndBase, [$this->createLookup()]); - $pets = new PetList($cnd); - - if ($data = $pets->getListviewData()) - { - $osInfo = [TYPE_PET, ' (Pet)', $pets->getMatches(), []]; - $result = array( - 'data' => array_values($data), - 'computeDataFunc' => '$_' - ); - - if ($this->searchMask & SEARCH_TYPE_OPEN) - foreach ($pets->iterate() as $id => $__) - $osInfo[3][$id] = $pets->getField('iconString'); - - if ($pets->getMatches() > $this->maxResults) - { - $result['note'] = sprintf(Util::$tryNarrowingString, 'LANG.lvnote_petsfound', $pets->getMatches(), $this->maxResults); - $result['_truncated'] = 1; - } - - return ['pet', $result, 'petFoodCol', $osInfo]; - } - - return false; - } - - private function _searchCreatureAbility($cndBase) // 23 NPCAbilities $searchMask & 0x0800000 - { - $cnd = array_merge($cndBase, array( - ['s.typeCat', -8], - $this->createLookup() - )); - $npcAbilities = new SpellList($cnd); - - if ($data = $npcAbilities->getListviewData()) - { - if ($this->searchMask & SEARCH_TYPE_REGULAR) - $this->extendGlobalData($npcAbilities->getJSGlobals(GLOBALINFO_SELF)); - - $osInfo = [TYPE_SPELL, ' (Spell)', $npcAbilities->getMatches(), []]; - $result = array( - 'data' => array_values($data), - 'id' => 'npc-abilities', - 'name' => '$LANG.tab_npcabilities', - 'visibleCols' => ['level'], - 'hiddenCols' => ['skill'] - ); - - if ($this->searchMask & SEARCH_TYPE_OPEN) - foreach ($npcAbilities->iterate() as $id => $__) - $osInfo[3][$id] = strToLower($npcAbilities->getField('iconString')); - - if ($npcAbilities->getMatches() > $this->maxResults) - { - $result['note'] = sprintf(Util::$tryNarrowingString, 'LANG.lvnote_spellsfound', $npcAbilities->getMatches(), $this->maxResults); - $result['_truncated'] = 1; - } - - if (isset($result['note'])) - $result['note'] .= ' + LANG.dash + $WH.sprintf(LANG.lvnote_filterresults, \'?spells=-8&filter=na='.urlencode($this->search).'\')'; - else - $result['note'] = '$$WH.sprintf(LANG.lvnote_filterresults, \'?spells=-8&filter=na='.urlencode($this->search).'\')'; - - return ['spell', $result, null, $osInfo]; - } - - return false; - } - - private function _searchSpell($cndBase) // 24 Spells (Misc + GM + triggered abilities) $searchMask & 0x1000000 - { - $cnd = array_merge($cndBase, array( - [ - 'OR', - ['s.typeCat', [0, -9]], - ['AND', ['s.cuFlags', SPELL_CU_TRIGGERED, '&'], ['s.typeCat', [7, -2]]] - ], - $this->createLookup() - )); - $misc = new SpellList($cnd); - - if ($data = $misc->getListviewData()) - { - if ($this->searchMask & SEARCH_TYPE_REGULAR) - $this->extendGlobalData($misc->getJSGlobals(GLOBALINFO_SELF | GLOBALINFO_RELATED)); - - $osInfo = [TYPE_SPELL, ' (Spell)', $misc->getMatches(), []]; - $result = array( - 'data' => array_values($data), - 'name' => '$LANG.tab_uncategorizedspells', - 'visibleCols' => ['level'], - 'hiddenCols' => ['skill'] - ); - - if ($this->searchMask & SEARCH_TYPE_OPEN) - foreach ($misc->iterate() as $id => $__) - $osInfo[3][$id] = strToLower($misc->getField('iconString')); - - if ($misc->getMatches() > $this->maxResults) - { - $result['note'] = sprintf(Util::$tryNarrowingString, 'LANG.lvnote_spellsfound', $misc->getMatches(), $this->maxResults); - $result['_truncated'] = 1; - } - - if (isset($result['note'])) - $result['note'] .= ' + LANG.dash + $WH.sprintf(LANG.lvnote_filterresults, \'?spells=0&filter=na='.urlencode($this->search).'\')'; - else - $result['note'] = '$$WH.sprintf(LANG.lvnote_filterresults, \'?spells=0&filter=na='.urlencode($this->search).'\')'; - - return ['spell', $result, null, $osInfo]; - } - - return false; - } - - private function _searchEmote($cndBase) // 25 Emotes $searchMask & 0x2000000 - { - $cnd = array_merge($cndBase, [$this->createLookup(['cmd', 'self_loc'.User::$localeId, 'target_loc'.User::$localeId, 'noTarget_loc'.User::$localeId])]); - $emote = new EmoteList($cnd); - - if ($data = $emote->getListviewData()) - { - $osInfo = [TYPE_EMOTE, ' (Emote)', $emote->getMatches()]; - $result = array( - 'data' => array_values($data), - 'name' => Util::ucFirst(Lang::game('emotes')) - ); - - return ['emote', $result, 'emote', $osInfo]; - } - - return false; - } - - private function _searchEnchantment($cndBase) // 26 Enchantments $searchMask & 0x4000000 - { - $cnd = array_merge($cndBase, [$this->createLookup(['name_loc'.User::$localeId])]); - $enchantment = new EnchantmentList($cnd); - - if ($data = $enchantment->getListviewData()) - { - $this->extendGlobalData($enchantment->getJSGlobals()); - - $osInfo = [TYPE_ENCHANTMENT, ' (Enchantment)', $enchantment->getMatches()]; - $result = array( - 'data' => array_values($data), - 'name' => Util::ucFirst(Lang::game('enchantments')) - ); - - if (array_filter(array_column($result['data'], 'spells'))) - $result['visibleCols'] = ['trigger']; - - if (!$enchantment->hasSetFields(['skillLine'])) - $result['hiddenCols'] = ['skill']; - - if ($enchantment->getMatches() > $this->maxResults) - { - $result['note'] = sprintf(Util::$tryNarrowingString, 'LANG.lvnote_enchantmentsfound', $enchantment->getMatches(), $this->maxResults); - $result['_truncated'] = 1; - } - - return ['enchantment', $result, 'enchantment', $osInfo]; - } - - return false; - } - - private function _searchSound($cndBase) // 27 Sounds $searchMask & 0x8000000 - { - $cnd = array_merge($cndBase, [$this->createLookup(['name'])]); - $sounds = new SoundList($cnd); - - if ($data = $sounds->getListviewData()) - { - if ($this->searchMask & SEARCH_TYPE_REGULAR) - $this->extendGlobalData($sounds->getJSGlobals()); - - $osInfo = [TYPE_SOUND, ' (Sound)', $sounds->getMatches()]; - $result['data'] = array_values($data); - - if ($sounds->getMatches() > $this->maxResults) - { - $result['note'] = sprintf(Util::$tryNarrowingString, 'LANG.lvnote_soundsfound', $sounds->getMatches(), $this->maxResults); - $result['_truncated'] = 1; - } - - return ['sound', $result, null, $osInfo]; - } - - return false; - } -} - -?> diff --git a/pages/skill.php b/pages/skill.php deleted file mode 100644 index 595643a2..00000000 --- a/pages/skill.php +++ /dev/null @@ -1,342 +0,0 @@ -typeId = intVal($id); - - $this->subject = new SkillList(array(['id', $this->typeId])); - if ($this->subject->error) - $this->notFound(Lang::game('skill'), Lang::skill('notFound')); - - $this->name = $this->subject->getField('name', true); - $this->cat = $this->subject->getField('typeCat'); - } - - protected function generatePath() - { - $this->path[] = (in_array($this->cat, [9, 11]) || $this->typeId == 762) ? $this->typeId : $this->cat; - } - - protected function generateTitle() - { - array_unshift($this->title, $this->name, Util::ucFirst(Lang::game('skill'))); - } - - protected function generateContent() - { - /****************/ - /* Main Content */ - /****************/ - - $this->headIcons = [$this->subject->getField('iconString')]; - $this->redButtons = array( - BUTTON_WOWHEAD => true, - BUTTON_LINKS => ['type' => $this->type, 'typeId' => $this->typeId] - ); - - if ($_ = $this->subject->getField('description', true)) - $this->extraText = $_; - - /**************/ - /* Extra Tabs */ - /**************/ - - if (in_array($this->cat, [-5, 9, 11])) - { - // 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_SQL_LIMIT_NONE - ); - - $recipes = new SpellList($condition); // also relevant for 3 - if (!$recipes->error) - { - $this->extendGlobalData($recipes->getJSGlobals(GLOBALINFO_SELF | GLOBALINFO_RELATED)); - - $this->lvTabs[] = ['spell', array( - 'data' => array_values($recipes->getListviewData()), - 'id' => 'recipes', - 'name' => '$LANG.tab_recipes', - 'visibleCols' => ['reagents', 'source'], - 'note' => sprintf(Util::$filterResultString, '?spells='.$this->cat.'.'.$this->typeId.'&filter=cr=20;crs=1;crv=0') - )]; - } - - // tab: recipe Items [items] (Books) - $filterRecipe = [null, 165, 197, 202, 164, 185, 171, 129, 333, 356, 755, 773, 186, 182]; - $conditions = array( - ['requiredSkill', $this->typeId], - ['class', ITEM_CLASS_RECIPE], - CFG_SQL_LIMIT_NONE - ); - - $recipeItems = new ItemList($conditions); - if (!$recipeItems->error) - { - $this->extendGlobalData($recipeItems->getJSGlobals(GLOBALINFO_SELF)); - - $tabData = array( - 'data' => array_values($recipeItems->getListviewData()), - 'id' => 'recipe-items', - 'name' => '$LANG.tab_recipeitems', - ); - - if ($_ = array_search($this->typeId, $filterRecipe)) - $tabData['note'] = sprintf(Util::$filterResultString, "?items=9.".$_); - - $this->lvTabs[] = ['item', $tabData]; - } - - // tab: crafted items [items] - $filterItem = [null, 171, 164, 185, 333, 202, 129, 755, 165, 186, 197, null, null, 356, 182, 773]; - $created = []; - foreach ($recipes->iterate() as $__) - if ($idx = $recipes->canCreateItem()) - foreach ($idx as $i) - $created[] = $recipes->getField('effect'.$i.'CreateItemId'); - - if ($created) - { - $created = new ItemList(array(['i.id', $created], CFG_SQL_LIMIT_NONE)); - if (!$created->error) - { - $this->extendGlobalData($created->getJSGlobals(GLOBALINFO_SELF)); - - $tabData = array( - 'data' => array_values($created->getListviewData()), - 'id' => 'crafted-items', - 'name' => '$LANG.tab_crafteditems', - ); - - if ($_ = array_search($this->typeId, $filterItem)) - $tabData['note'] = sprintf(Util::$filterResultString, "?items&filter=cr=86;crs=".$_.";crv=0"); - - $this->lvTabs[] = ['item', $tabData]; - } - } - - // tab: required by [item] - $conditions = array( - ['requiredSkill', $this->typeId], - ['class', ITEM_CLASS_RECIPE, '!'], - CFG_SQL_LIMIT_NONE - ); - - $reqBy = new ItemList($conditions); - if (!$reqBy->error) - { - $this->extendGlobalData($reqBy->getJSGlobals(GLOBALINFO_SELF)); - - $tabData = array( - 'data' => array_values($reqBy->getListviewData()), - 'id' => 'required-by', - 'name' => '$LANG.tab_requiredby', - ); - - if ($_ = array_search($this->typeId, $filterItem)) - $tabData['note'] = sprintf(Util::$filterResultString, "?items&filter=99:168;crs=".$_.":2;crv=0:0"); - - $this->lvTabs[] = ['item', $tabData]; - } - - // tab: required by [itemset] - $conditions = array( - ['skillId', $this->typeId], - CFG_SQL_LIMIT_NONE - ); - - $reqBy = new ItemsetList($conditions); - if (!$reqBy->error) - { - $this->extendGlobalData($reqBy->getJSGlobals(GLOBALINFO_SELF)); - - $this->lvTabs[] = ['itemset', array( - 'data' => array_values($reqBy->getListviewData()), - 'id' => 'required-by-set', - 'name' => '$LANG.tab_requiredby' - )]; - } - } - - // 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_SQL_LIMIT_NONE - ); - - foreach (Game::$skillLineMask as $line1 => $sets) - foreach ($sets as $idx => $set) - if ($set[1] == $this->typeId) - { - $condition[1][] = array('AND', ['s.skillLine1', $line1], ['s.skillLine2OrMask', 1 << $idx, '&']); - break 2; - } - - $spells = new SpellList($condition); - if (!$spells->error) - { - foreach ($spells->iterate() as $__) - { - $reqClass |= $spells->getField('reqClassMask'); - $reqRace |= $spells->getField('reqRaceMask'); - } - - $this->extendGlobalData($spells->getJSGlobals(GLOBALINFO_SELF)); - - $tabData = array( - 'data' => array_values($spells->getListviewData()), - 'visibleCols' => ['source'] - ); - - switch ($this->cat) - { - case -4: - $tabData['note'] = sprintf(Util::$filterResultString, '?spells=-4'); - break; - case 7: - if ($this->typeId != 769) // Internal - $tabData['note'] = sprintf(Util::$filterResultString, '?spells='.$this->cat.'.'.(log($reqClass, 2) + 1).'.'.$this->typeId); // doesn't matter what spell; reqClass should be identical for all Class Spells - break; - case 9: - case 11: - $tabData['note'] = sprintf(Util::$filterResultString, '?spells='.$this->cat.'.'.$this->typeId); - break; - } - - $this->lvTabs[] = ['spell', $tabData]; - } - - // tab: trainers [npcs] - if (in_array($this->cat, [-5, 6, 7, 8, 9, 11])) - { - $list = []; - if (!empty(Game::$trainerTemplates[TYPE_SKILL][$this->typeId])) - $list = DB::World()->selectCol('SELECT DISTINCT ID FROM npc_trainer WHERE SpellID IN (?a) AND ID < 200000', Game::$trainerTemplates[TYPE_SKILL][$this->typeId]); - else - { - $mask = 0; - foreach (Game::$skillLineMask[-3] as $idx => $pair) - if ($pair[1] == $this->typeId) - $mask |= 1 << $idx; - - $spellIds = DB::Aowow()->selectCol( - 'SELECT id FROM ?_spell WHERE typeCat IN (-11, 9) AND (skillLine1 = ?d OR (skillLine1 > 0 AND skillLine2OrMask = ?d) {OR (skillLine1 = -3 AND skillLine2OrMask = ?d)})', - $this->typeId, - $this->typeId, - $mask ?: DBSIMPLE_SKIP - ); - - $list = $spellIds ? DB::World()->selectCol(' - SELECT IF(t1.ID > 200000, t2.ID, t1.ID) - FROM npc_trainer t1 - LEFT JOIN npc_trainer t2 ON t2.SpellID = -t1.ID - WHERE t1.SpellID IN (?a)', - $spellIds - ) : []; - } - - if ($list) - { - $this->addJS('?data=zones&locale='.User::$localeId.'&t='.$_SESSION['dataKey']); - - $trainer = new CreatureList(array(CFG_SQL_LIMIT_NONE, ['ct.id', $list], ['s.guid', NULL, '!'], ['ct.npcflag', 0x10, '&'])); - - if (!$trainer->error) - { - $this->extendGlobalData($trainer->getJSGlobals()); - - $this->lvTabs[] = ['creature', array( - 'data' => array_values($trainer->getListviewData()), - 'id' => 'trainer', - 'name' => '$LANG.tab_trainers', - )]; - } - } - } - - // tab: quests [quests] - if (in_array($this->cat, [9, 11])) // only for professions - { - $sort = 0; - switch ($this->typeId) - { - case 182: $sort = 24; break; // Herbalism - case 356: $sort = 101; break; // Fishing - case 164: $sort = 121; break; // Blacksmithing - case 171: $sort = 181; break; // Alchemy - case 165: $sort = 182; break; // Leatherworking - case 202: $sort = 201; break; // Engineering - case 197: $sort = 264; break; // Tailoring - case 185: $sort = 304; break; // Cooking - case 129: $sort = 324; break; // First Aid - case 773: $sort = 371; break; // Inscription - case 755: $sort = 373; break; // Jewelcrafting - } - - if ($sort) - { - $quests = new QuestList(array(['zoneOrSort', -$sort], CFG_SQL_LIMIT_NONE)); - if (!$quests->error) - { - $this->extendGlobalData($quests->getJSGlobals()); - $this->lvTabs[] = ['quest', ['data' => array_values($quests->getListviewData())]]; - } - } - } - - // tab: related classes (apply classes from [spells]) - $class = []; - for ($i = 0; $i < 11; $i++) - if ($reqClass & (1 << $i)) - $class[] = $i + 1; - - if ($class) - { - $classes = new CharClassList(array(['id', $class])); - if (!$classes->error) - $this->lvTabs[] = ['class', ['data' => array_values($classes->getListviewData())]]; - } - - // tab: related races (apply races from [spells]) - $race = []; - for ($i = 0; $i < 12; $i++) - if ($reqRace & (1 << $i)) - $race[] = $i + 1; - - if ($race) - { - $races = new CharRaceList(array(['id', $race])); - if (!$races->error) - $this->lvTabs[] = ['race', ['data' => array_values($races->getListviewData())]]; - } - } -} - - -?> diff --git a/pages/skills.php b/pages/skills.php deleted file mode 100644 index 377f2471..00000000 --- a/pages/skills.php +++ /dev/null @@ -1,57 +0,0 @@ -getCategoryFromUrl($pageParam);; - - parent::__construct($pageCall, $pageParam); - - $this->name = Util::ucFirst(Lang::game('skills')); - } - - protected function generateContent() - { - $conditions = []; - - if (!User::isInGroup(U_GROUP_EMPLOYEE)) - $conditions[] = [['cuFlags', CUSTOM_EXCLUDE_FOR_LISTVIEW, '&'], 0]; - - if ($this->category) - $conditions[] = ['typeCat', $this->category[0]]; - - $skills = new SkillList($conditions); - - $this->lvTabs[] = ['skill', ['data' => array_values($skills->getListviewData())]]; - } - - protected function generateTitle() - { - if ($this->category) - array_unshift($this->title, Lang::skill('cat', $this->category[0])); - } - - protected function generatePath() - { - if ($this->category) - $this->path[] = $this->category[0]; - } -} - -?> diff --git a/pages/sound.php b/pages/sound.php deleted file mode 100644 index 17ccfdb2..00000000 --- a/pages/sound.php +++ /dev/null @@ -1,369 +0,0 @@ -special = true; - $this->name = Lang::sound('cat', 1000); - $this->cat = 1000; - $this->articleUrl = 'sound&playlist'; - $this->hasComContent = false; - } - // regular case - else - { - $this->typeId = intVal($id); - - $this->subject = new SoundList(array(['id', $this->typeId])); - if ($this->subject->error) - $this->notFound(Lang::game('sound'), Lang::sound('notFound')); - - $this->name = $this->subject->getField('name'); - $this->cat = $this->subject->getField('cat'); - } - } - - protected function generatePath() - { - $this->path[] = $this->cat; - } - - protected function generateTitle() - { - array_unshift($this->title, $this->name, Util::ucFirst(Lang::game('sound'))); - } - - protected function generateContent() - { - if ($this->special) - $this->generatePlaylistContent(); - else - $this->generateDefaultContent(); - } - - private function generatePlaylistContent() - { - - } - - private function generateDefaultContent() - { - /****************/ - /* Main Content */ - /****************/ - - $this->addJS('?data=zones&locale='.User::$localeId.'&t='.$_SESSION['dataKey']); - - // get spawns - $map = null; - if ($spawns = $this->subject->getSpawns(SPAWNINFO_FULL)) - { - $map = ['data' => ['parent' => 'mapper-generic'], 'mapperData' => &$spawns]; - foreach ($spawns as $areaId => &$areaData) - $map['extra'][$areaId] = ZoneList::getName($areaId); - } - - // get full path ingame 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); - - $this->map = $map; - $this->headIcons = [$this->subject->getField('iconString')]; - $this->redButtons = array( - BUTTON_WOWHEAD => true, - BUTTON_PLAYLIST => true, - BUTTON_LINKS => array( - 'type' => TYPE_SOUND, - 'typeId' => $this->typeId, - 'sound' => str_replace('\\', '\\\\', $fullpath) // escape for wow client - ) - ); - - $this->extendGlobalData($this->subject->getJSGlobals()); - - - /**************/ - /* Extra Tabs */ - /**************/ - - // tab: Spells - // 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 - ', $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); - - $cnd = array( - 'OR', - ['AND', ['effect1Id', 132], ['effect1MiscValue', $this->typeId]], - ['AND', ['effect2Id', 132], ['effect2MiscValue', $this->typeId]], - ['AND', ['effect3Id', 132], ['effect3MiscValue', $this->typeId]] - ); - - if ($displayIds) - $cnd[] = ['spellVisualId', $displayIds]; - - $spells = new SpellList($cnd); - if (!$spells->error) - { - $data = $spells->getListviewData(); - $this->extendGlobalData($spells->getJSGlobals(GLOBALINFO_SELF)); - - $this->lvTabs[] = ['spell', array( - 'data' => array_values($data), - )]; - } - - - // tab: Items - $subClasses = []; - if ($subClassMask = DB::Aowow()->selectCell('SELECT subClassMask FROM ?_items_sounds WHERE soundId = ?d', $this->typeId)) - for ($i = 0; $i <= 20; $i++) - if ($subClassMask & (1 << $i)) - $subClasses[] = $i; - - $itemIds = DB::Aowow()->selectCol(' - SELECT - id - FROM - ?_items - WHERE - {spellVisualId IN (?a) OR } - pickUpSoundId = ?d OR - dropDownSoundId = ?d OR - sheatheSoundId = ?d OR - unsheatheSoundId = ?d {OR - ( - IF (soundOverrideSubclass > 0, soundOverrideSubclass, subclass) IN (?a) AND - class = ?d - )} - ', $displayIds ?: DBSIMPLE_SKIP, $this->typeId, $this->typeId, $this->typeId, $this->typeId, $subClasses ?: DBSIMPLE_SKIP, ITEM_CLASS_WEAPON); - if ($itemIds) - { - $items = new ItemList(array(['id', $itemIds])); - if (!$items->error) - { - $this->extendGlobalData($items->getJSGlobals(GLOBALINFO_SELF)); - $this->lvTabs[] = ['item', ['data' => array_values($items->getListviewData())]]; - } - } - - - // 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)) - { - $zones = new ZoneList(array(['id', array_column($zoneIds, 'id')])); - if (!$zones->error) - { - $this->extendGlobalData($zones->getJSGlobals(GLOBALINFO_SELF)); - - $zoneData = $zones->getListviewData(); - $parents = $zones->getAllFields('parentArea'); - $tabData = []; - $pIds = array_filter(array_unique(array_values($parents))); - if ($pIds) - { - $pZones = new ZoneList(array(['id', $pIds])); - if (!$pZones->error) - { - $this->extendGlobalData($pZones->getJSGlobals(GLOBALINFO_SELF)); - - $pData = $pZones->getListviewData(); - foreach ($parents as $child => $parent) - { - if (!$parent || empty($pData[$parent])) - continue; - - if (!isset($pData[$parent]['subzones'])) - $pData[$parent]['subzones'] = []; - - $pData[$parent]['subzones'][] = $child; - unset($parents[$child]); - } - - // these are original parents - foreach ($parents as $parent => $__) - if (empty($pData[$parent])) - $pData[$parent] = $zoneData[$parent]; - - $zoneData = $pData; - } - } - - if (array_filter(array_column($zoneIds, 'worldStateId'))) - { - $tabData['extraCols'] = ['$Listview.extraCols.condition']; - - foreach ($zoneIds as $zData) - if ($zData['worldStateId']) - $zoneData[$zData['id']]['condition'][0][$this->typeId][] = [[CND_WORLD_STATE, $zData['worldStateId'], $zData['worldStateValue']]]; - } - - $tabData['data'] = array_values($zoneData); - $tabData['hiddenCols'] = ['territory']; - - $this->lvTabs[] = ['zone', $tabData]; - } - } - - - // 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)) - { - $races = new CharRaceList(array(['id', $vo])); - if (!$races->error) - { - $this->extendGlobalData($races->getJSGlobals(GLOBALINFO_SELF)); - $this->lvTabs[] = ['race', ['data' => array_values($races->getListviewData())]]; - } - } - - - // tab: Emotes (EmotesTextSound (containing emote audio)) - if ($em = DB::Aowow()->selectCol('SELECT emoteId FROM ?_emotes_sounds WHERE soundId = ?d GROUP BY emoteId', $this->typeId)) - { - $races = new EmoteList(array(['id', $em])); - if (!$races->error) - { - $this->extendGlobalData($races->getJSGlobals(GLOBALINFO_SELF)); - $this->lvTabs[] = ['emote', array( - 'data' => array_values($races->getListviewData()), - 'name' => Util::ucFirst(Lang::game('emotes')) - ), 'emote']; - } - } - - $ssQuery = ' - SELECT - source_type AS ARRAY_KEY, - entryorguid AS ARRAY_KEY2, - 0 - FROM - smart_scripts - WHERE - (action_type = 4 AND action_param1 = ?d AND source_type <> 9) { - OR (action_type = 80 AND (action_param1 IN (?a))) - OR (action_type = 87 AND (action_param1 IN (?a) OR action_param2 IN (?a) OR action_param3 IN (?a) OR action_param4 IN (?a) OR action_param5 IN (?a) OR action_param6 IN (?a))) - OR (action_type = 88 AND (action_param1 IN (?a) OR action_param2 IN (?a))) - } - '; - - $ssActionLists = DB::World()->selectCol('SELECT entryorguid FROM smart_scripts WHERE action_type = 4 AND action_param1 = ?d AND source_type = 9', $this->typeId); - $smartScripts = DB::World()->selectCol($ssQuery, $this->typeId, $ssActionLists ?: DBSIMPLE_SKIP, $ssActionLists, $ssActionLists, $ssActionLists, $ssActionLists, $ssActionLists, $ssActionLists, $ssActionLists, $ssActionLists); - - $creatureIds = DB::World()->selectCol('SELECT ct.CreatureID FROM creature_text ct LEFT JOIN broadcast_text bct ON bct.ID = ct.BroadCastTextId WHERE bct.SoundId = ?d OR ct.Sound = ?d', $this->typeId, $this->typeId); - foreach ($smartScripts as $source => $ids) - { - switch($source) - { - case 0: // npc - // filter for guids (id < 0) - $creatureIds = array_merge($creatureIds, array_keys(array_filter($ids, function($x) { return $x > 0; })) ); - break; - case 1: // gameobject - default: - break; - } - } - - - // tab: NPC (dialogues...?, generic creature sound) - // 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 - ', $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, $this->typeId, $this->typeId, $this->typeId, $this->typeId, $this->typeId, $this->typeId, $this->typeId, $this->typeId, $this->typeId, $this->typeId, $this->typeId); - - // broadcast_text <-> creature_text - if ($creatureIds || $displayIds) - { - $extra = []; - $cnds = [CFG_SQL_LIMIT_NONE, &$extra]; - if (!User::isInGroup(U_GROUP_STAFF)) - $cnds[] = [['cuFlags', CUSTOM_EXCLUDE_FOR_LISTVIEW, '&'], 0]; - - if ($creatureIds) - $extra[] = ['id', $creatureIds]; - - if ($displayIds) - $extra[] = ['displayId1', $displayIds]; - - if (count($extra) > 1) - array_unshift($extra, 'OR'); - else - $extra = array_pop($extra); - - $npcs = new CreatureList($cnds); - if (!$npcs->error) - { - $this->addJS('?data=zones&locale='.User::$localeId.'&t='.$_SESSION['dataKey']); - - $this->extendGlobalData($npcs->getJSGlobals(GLOBALINFO_SELF)); - $this->lvTabs[] = ['creature', ['data' => array_values($npcs->getListviewData())]]; - } - } - } -} - - -?> diff --git a/pages/sounds.php b/pages/sounds.php deleted file mode 100644 index b778c329..00000000 --- a/pages/sounds.php +++ /dev/null @@ -1,81 +0,0 @@ -filterObj = new SoundListFilter(); - - parent::__construct($pageCall, $pageParam); - - $this->name = Util::ucFirst(Lang::game('sounds')); - } - - protected function generateContent() - { - $this->addJs('filters.js'); - - $this->redButtons = array( - BUTTON_WOWHEAD => true, - BUTTON_PLAYLIST => true - ); - - $conditions = []; - if ($_ = $this->filterObj->getConditions()) - $conditions[] = $_; - - $this->filter = $this->filterObj->getForm(); - $this->filter['query'] = isset($_GET['filter']) ? $_GET['filter'] : null; - - $sounds = new SoundList($conditions); - $tabData = []; - if (!$sounds->error) - { - $tabData['data'] = array_values($sounds->getListviewData()); - - // create note if search limit was exceeded; overwriting 'note' is intentional - if ($sounds->getMatches() > CFG_SQL_LIMIT_DEFAULT) - { - $tabData['note'] = sprintf(Util::$tryFilteringString, 'LANG.lvnote_soundsfound', $sounds->getMatches(), CFG_SQL_LIMIT_DEFAULT); - $tabData['_truncated'] = 1; - } - - if ($this->filterObj->error) - $tabData['_errors'] = 1; - } - $this->lvTabs[] = ['sound', $tabData]; - - Lang::sort('sound', 'cat'); - } - - protected function generateTitle() - { - $form = $this->filterObj->getForm(); - if (isset($form['ty']) && count($form['ty']) == 1) - array_unshift($this->title, Lang::sound('cat', $form['ty'][0])); - } - - protected function generatePath() - { - $form = $this->filterObj->getForm(); - if (isset($form['ty']) && count($form['ty']) == 1) - $this->path[] = $form['ty']; - } -} - -?> diff --git a/pages/spell.php b/pages/spell.php deleted file mode 100644 index 486592fe..00000000 --- a/pages/spell.php +++ /dev/null @@ -1,2075 +0,0 @@ -mode == CACHE_TYPE_TOOLTIP && isset($_GET['domain'])) - Util::powerUseLocale($_GET['domain']); - - $this->typeId = intVal($id); - - $this->subject = new SpellList(array(['id', $this->typeId])); - if ($this->subject->error) - $this->notFound(); - - $jsg = $this->subject->getJSGlobals(GLOBALINFO_ANY, $extra); - $this->extendGlobalData($jsg, $extra); - - $this->name = $this->subject->getField('name', true); - - // has difficulty versions of itself - $this->difficulties = DB::Aowow()->selectRow( - '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', - $this->typeId, $this->typeId, $this->typeId, $this->typeId - ); - - // returns self or firstRank - $this->firstRank = DB::Aowow()->selectCell( - 'SELECT IF(s1.RankNo <> 1 AND s2.id, s2.id, s1.id) - 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', - $this->typeId - ); - } - - protected function generatePath() - { - $cat = $this->subject->getField('typeCat'); - $cf = $this->subject->getField('cuFlags'); - - $this->path[] = $cat; - - // reconstruct path - switch ($cat) - { - case -2: - case 7: - case -13: - if ($cl = $this->subject->getField('reqClassMask')) - $this->path[] = log($cl, 2) + 1; - - if ($cat == -13) - $this->path[] = ($cf & (SPELL_CU_GLYPH_MAJOR | SPELL_CU_GLYPH_MINOR)) >> 6; - else - $this->path[] = $this->subject->getField('skillLines')[0]; - - break; - case 9: - case -3: - case 11: - $this->path[] = $this->subject->getField('skillLines')[0]; - - if ($cat == 11) - if ($_ = $this->subject->getField('reqSpellId')) - $this->path[] = $_; - - break; - case -11: - foreach (SpellList::$skillLines as $line => $skills) - if (in_array($this->subject->getField('skillLines')[0], $skills)) - $this->path[] = $line; - break; - case -7: // only spells unique in skillLineAbility will always point to the right skillLine :/ - if ($cf & SPELL_CU_PET_TALENT_TYPE0) - $this->path[] = 411; // Ferocity - else if ($cf & SPELL_CU_PET_TALENT_TYPE1) - $this->path[] = 409; // Tenacity - else if ($cf & SPELL_CU_PET_TALENT_TYPE2) - $this->path[] = 410; // Cunning - } - } - - protected function generateTitle() - { - array_unshift($this->title, $this->name, Util::ucFirst(Lang::game('spell'))); - } - - protected function generateContent() - { - $this->addJS('?data=zones&locale='.User::$localeId.'&t='.$_SESSION['dataKey']); - - $_cat = $this->subject->getField('typeCat'); - - $redButtons = array( - BUTTON_VIEW3D => false, - BUTTON_WOWHEAD => true, - BUTTON_LINKS => array( - 'linkColor' => 'ff71d5ff', - 'linkId' => Util::$typeStrings[TYPE_SPELL].':'.$this->typeId, - 'linkName' => $this->name, - 'type' => $this->type, - 'typeId' => $this->typeId - ) - ); - - /***********/ - /* Infobox */ - /***********/ - - $infobox = Lang::getInfoBoxForFlags($this->subject->getField('cuFlags')); - - // level - if (!in_array($_cat, [-5, -6])) // not mount or vanity pet - { - if ($_ = $this->subject->getField('talentLevel')) - $infobox[] = (in_array($_cat, [-2, 7, -13]) ? sprintf(Lang::game('reqLevel'), $_) : Lang::game('level').Lang::main('colon').$_); - else if ($_ = $this->subject->getField('spellLevel')) - $infobox[] = (in_array($_cat, [-2, 7, -13]) ? sprintf(Lang::game('reqLevel'), $_) : Lang::game('level').Lang::main('colon').$_); - } - - // races - if ($_ = Lang::getRaceString($this->subject->getField('reqRaceMask'), $__, $jsg, $n, false)) - { - if ($_ != Lang::game('ra', 0)) // omit: "both" - { - $this->extendGlobalIds(TYPE_RACE, $jsg); - $t = $n == 1 ? Lang::game('race') : Lang::game('races'); - $infobox[] = Util::ucFirst($t).Lang::main('colon').$_; - } - } - - // classes - if ($_ = Lang::getClassString($this->subject->getField('reqClassMask'), $jsg, $n, false)) - { - $this->extendGlobalIds(TYPE_CLASS, $jsg); - $t = $n == 1 ? Lang::game('class') : Lang::game('classes'); - $infobox[] = Util::ucFirst($t).Lang::main('colon').$_; - } - - // spell focus - if ($_ = $this->subject->getField('spellFocusObject')) - { - $bar = DB::Aowow()->selectRow('SELECT * FROM ?_spellfocusobject WHERE id = ?d', $_); - $focus = new GameObjectList(array(['spellFocusId', $_], 1)); - $infobox[] = Lang::game('requires2').' '.($focus->error ? Util::localizedString($bar, 'name') : '[url=?object='.$focus->id.']'.Util::localizedString($bar, 'name').'[/url]'); - } - - // primary & secondary trades - if (in_array($_cat, [9, 11])) - { - // skill - if ($_ = $this->subject->getField('skillLines')[0]) - { - $rSkill = new SkillList(array(['id', $_])); - if (!$rSkill->error) - { - $this->extendGlobalData($rSkill->getJSGlobals()); - - $bar = sprintf(Lang::game('requires'), ' [skill='.$rSkill->id.']'); - if ($_ = $this->subject->getField('learnedAt')) - $bar .= ' ('.$_.')'; - - $infobox[] = $bar; - } - } - - // specialization - if ($_ = $this->subject->getField('reqSpellId')) - { - $rSpell = new SpellList(array(['id', $_])); - if (!$rSpell->error) - { - $this->extendGlobalData($rSpell->getJSGlobals()); - $infobox[] = Lang::game('requires2').' [spell='.$rSpell->id.'][/li]'; - } - } - - // difficulty - if ($_ = $this->subject->getColorsForCurrent()) - { - $bar = []; - for ($i = 0; $i < 4; $i++) - if ($_[$i]) - $bar[] = '[color=r'.($i + 1).']'.$_[$i].'[/color]'; - - $infobox[] = Lang::game('difficulty').Lang::main('colon').implode(' ', $bar); - } - } - - // accquisition.. 10: starter spell; 7: discovery - if (isset($this->subject->sources[$this->subject->id][10])) - $infobox[] = Lang::spell('starter'); - else if (isset($this->subject->sources[$this->subject->id][7])) - $infobox[] = Lang::spell('discovered'); - - // training cost - if ($cost = $this->subject->getField('trainingCost')) - $infobox[] = Lang::spell('trainingCost').Lang::main('colon').'[money='.$cost.'][/li]'; - - // used in mode - foreach ($this->difficulties as $n => $id) - if ($id == $this->typeId) // "Mode" seems to be multilingual acceptable - $infobox[] = 'Mode'.Lang::main('colon').Lang::game('modes', $n); - - $effects = $this->createEffects($infobox, $redButtons); - - // 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)) - $infobox[] = 'Script'.Lang::main('colon').$_; - - $infobox = $infobox ? '[ul][li]'.implode('[/li][li]', $infobox).'[/li][/ul]' : ''; - - // append glyph symbol if available - $glyphId = 0; - for ($i = 1; $i < 4; $i++) - if ($this->subject->getField('effect'.$i.'Id') == 74) - $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 (file_exists('static/images/wow/Interface/Spellbook/'.$_.'.png')) - $infobox .= '[img src='.STATIC_URL.'/images/wow/Interface/Spellbook/'.$_.'.png border=0 float=center margin=15]'; - - - /****************/ - /* Main Content */ - /****************/ - - $this->reagents = $this->createReagentList(); - $this->scaling = $this->createScalingData(); - $this->items = $this->createRequiredItems(); - $this->tools = $this->createTools(); - $this->effects = $effects; - $this->infobox = $infobox; - $this->powerCost = $this->subject->createPowerCostForCurrent(); - $this->castTime = $this->subject->createCastTimeForCurrent(false, false); - $this->name = $this->subject->getField('name', true); - $this->headIcons = [$this->subject->getField('iconString'), $this->subject->getField('stackAmount')]; - $this->level = $this->subject->getField('spellLevel'); - $this->rangeName = $this->subject->getField('rangeText', true); - $this->range = $this->subject->getField('rangeMaxHostile'); - $this->gcd = Util::formatTime($this->subject->getField('startRecoveryTime')); - $this->gcdCat = null; // todo (low): nyi; find out how this works [n/a; normal; ..] - $this->school = [Util::asHex($this->subject->getField('schoolMask')), Lang::getMagicSchools($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; - $this->redButtons = $redButtons; - - // minRange exists.. prepend - if ($_ = $this->subject->getField('rangeMinHostile')) - $this->range = $_.' - '.$this->range; - - if (!($this->subject->getField('attributes2') & 0x80000)) - $this->stances = Lang::getStances($this->subject->getField('stanceMask')); - - if (($_ = $this->subject->getField('recoveryTime')) && $_ > 0) - $this->cooldown = Util::formatTime($_); - else if (($_ = $this->subject->getField('recoveryCategory')) && $_ > 0) - $this->cooldown = Util::formatTime($_); - - if (($_ = $this->subject->getField('duration')) && $_ > 0) - $this->duration = Util::formatTime($_); - - // 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)) - { - $altSpell = new SpellList(array(['id', abs($pendant)])); - if (!$altSpell->error) - { - $this->transfer = sprintf( - Lang::spell('_transfer'), - $altSpell->id, - 1, // quality - $altSpell->getField('iconString'), - $altSpell->getField('name', true), - $pendant > 0 ? 'alliance' : 'horde', - $pendant > 0 ? Lang::game('si', 1) : Lang::game('si', 2) - ); - } - } - - /**************/ - /* Extra Tabs */ - /**************/ - - $j = [null, 'A', 'B', 'C']; - - // tab: abilities [of shapeshift form] - for ($i = 1; $i < 4; $i++) - { - if ($this->subject->getField('effect'.$i.'AuraId') != 36) - continue; - - $formSpells = DB::Aowow()->selectRow('SELECT spellId1, spellId2, spellId3, spellId4, spellId5, spellId6, spellId7, spellId8 FROM ?_shapeshiftforms WHERE id = ?d', $this->subject->getField('effect'.$i.'MiscValue')); - if (!$formSpells) - continue; - - $abilities = new SpellList(array(['id', $formSpells])); - if (!$abilities->error) - { - $tabData = array( - 'data' => array_values($abilities->getListviewData()), - 'id' => 'controlledabilities', - 'name' => '$LANG.tab_controlledabilities', - 'visibleCols' => ['level'], - ); - - if (!$abilities->hasSetFields(['skillLines'])) - $tabData['hiddenCols'] = ['skill']; - - $this->lvTabs[] = ['spell', $tabData]; - - $this->extendGlobalData($abilities->getJSGlobals(GLOBALINFO_SELF)); - } - } - - // tab: modifies $this - $sub = ['OR']; - $conditions = [ - ['s.typeCat', [0, -9, -8], '!'], // uncategorized (0), GM (-9), NPC-Spell (-8); NPC includes totems, lightwell and others :/ - ['s.spellFamilyId', $this->subject->getField('spellFamilyId')], - &$sub - ]; - - for ($i = 1; $i < 4; $i++) - { - // Flat Mods (107), Pct Mods (108), No Reagent Use (256) .. include dummy..? (4) - if (!in_array($this->subject->getField('effect'.$i.'AuraId'), [107, 108, 256, 286 /*, 4*/])) - continue; - - $m1 = $this->subject->getField('effect1SpellClassMask'.$j[$i]); - $m2 = $this->subject->getField('effect2SpellClassMask'.$j[$i]); - $m3 = $this->subject->getField('effect3SpellClassMask'.$j[$i]); - - if (!$m1 && !$m2 && !$m3) - continue; - - $sub[] = ['s.spellFamilyFlags1', $m1, '&']; - $sub[] = ['s.spellFamilyFlags2', $m2, '&']; - $sub[] = ['s.spellFamilyFlags3', $m3, '&']; - } - - if (count($sub) > 1) - { - $modSpells = new SpellList($conditions); - if (!$modSpells->error) - { - $tabData = array( - 'data' => array_values($modSpells->getListviewData()), - 'id' => 'modifies', - 'name' => '$LANG.tab_modifies', - 'visibleCols' => ['level'], - ); - - if (!$modSpells->hasSetFields(['skillLines'])) - $tabData['hiddenCols'] = ['skill']; - - $this->lvTabs[] = ['spell', $tabData]; - - $this->extendGlobalData($modSpells->getJSGlobals(GLOBALINFO_SELF | GLOBALINFO_RELATED)); - } - } - - // tab: modified by $this - $sub = ['OR']; - $conditions = [ - ['s.spellFamilyId', $this->subject->getField('spellFamilyId')], - &$sub - ]; - - for ($i = 1; $i < 4; $i++) - { - $m1 = $this->subject->getField('spellFamilyFlags1'); - $m2 = $this->subject->getField('spellFamilyFlags2'); - $m3 = $this->subject->getField('spellFamilyFlags3'); - - if (!$m1 && !$m2 && !$m3) - continue; - - $sub[] = array( - 'AND', - ['s.effect'.$i.'AuraId', [107, 108, 256, 286 /*, 4*/]], - [ - 'OR', - ['s.effect1SpellClassMask'.$j[$i], $m1, '&'], - ['s.effect2SpellClassMask'.$j[$i], $m2, '&'], - ['s.effect3SpellClassMask'.$j[$i], $m3, '&'] - ] - ); - } - - if (count($sub) > 1) - { - $modsSpell = new SpellList($conditions); - if (!$modsSpell->error) - { - $tabData = array( - 'data' => array_values($modsSpell->getListviewData()), - 'id' => 'modified-by', - 'name' => '$LANG.tab_modifiedby', - 'visibleCols' => ['level'], - ); - - if (!$modsSpell->hasSetFields(['skillLines'])) - $tabData['hiddenCols'] = ['skill']; - - $this->lvTabs[] = ['spell', $tabData]; - - $this->extendGlobalData($modsSpell->getJSGlobals(GLOBALINFO_SELF | GLOBALINFO_RELATED)); - } - } - - // tab: see also - $conditions = array( - ['s.schoolMask', $this->subject->getField('schoolMask')], - ['s.effect1Id', $this->subject->getField('effect1Id')], - ['s.effect2Id', $this->subject->getField('effect2Id')], - ['s.effect3Id', $this->subject->getField('effect3Id')], - ['s.id', $this->subject->id, '!'], - ['s.name_loc'.User::$localeId, $this->subject->getField('name', true)] - ); - - $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? - { - $saE = ['$Listview.extraCols.mode']; - - foreach ($data as $id => &$d) - { - $d['modes'] = ['mode' => 0]; - - if ($this->difficulties[0] == $id) // b0001000 - { - if (!$this->difficulties[2] && !$this->difficulties[3]) - $d['modes']['mode'] |= 0x2; - else - $d['modes']['mode'] |= 0x8; - } - - 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; - } - } - - $tabData = array( - 'data' => array_values($data), - 'id' => 'see-also', - 'name' => '$LANG.tab_seealso', - 'visibleCols' => ['level'], - ); - - if (!$saSpells->hasSetFields(['skillLines'])) - $tabData['hiddenCols'] = ['skill']; - - if (isset($saE)) - $tabData['extraCols'] = $saE; - - $this->lvTabs[] = ['spell', $tabData]; - - $this->extendGlobalData($saSpells->getJSGlobals(GLOBALINFO_SELF | GLOBALINFO_RELATED)); - } - - // 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)) - { - $conditions = array( - 'OR', - ['AND', ['effect1AuraId', 293], ['effect1MiscValue', $so]], - ['AND', ['effect2AuraId', 293], ['effect2MiscValue', $so]], - ['AND', ['effect3AuraId', 293], ['effect3MiscValue', $so]] - ); - $ubSpells = new SpellList($conditions); - if (!$ubSpells->error) - { - $this->lvTabs[] = ['spell', array( - 'data' => array_values($ubSpells->getListviewData()), - 'id' => 'used-by-spell', - 'name' => '$LANG.tab_usedby' - )]; - - $this->extendGlobalData($ubSpells->getJSGlobals(GLOBALINFO_SELF)); - } - } - - - // 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] - ); - - $ubSets = new ItemsetList($conditions); - if (!$ubSets->error) - { - $this->lvTabs[] = ['itemset', array( - 'data' => array_values($ubSets->getListviewData()), - 'id' => 'used-by-itemset', - 'name' => '$LANG.tab_usedby' - )]; - - $this->extendGlobalData($ubSets->getJSGlobals(GLOBALINFO_SELF | GLOBALINFO_RELATED)); - } - - // tab: used by - item - $conditions = array( - 'OR', // 6: learn spell - ['AND', ['spellTrigger1', 6, '!'], ['spellId1', $this->subject->id]], - ['AND', ['spellTrigger2', 6, '!'], ['spellId2', $this->subject->id]], - ['AND', ['spellTrigger3', 6, '!'], ['spellId3', $this->subject->id]], - ['AND', ['spellTrigger4', 6, '!'], ['spellId4', $this->subject->id]], - ['AND', ['spellTrigger5', 6, '!'], ['spellId5', $this->subject->id]] - ); - - $ubItems = new ItemList($conditions); - if (!$ubItems->error) - { - $this->lvTabs[] = ['item', array( - 'data' => array_values($ubItems->getListviewData()), - 'id' => 'used-by-item', - 'name' => '$LANG.tab_usedby' - )]; - - $this->extendGlobalData($ubItems->getJSGlobals(GLOBALINFO_SELF)); - } - - // tab: used by - object - $conditions = array( - 'OR', - ['onUseSpell', $this->subject->id], ['onSuccessSpell', $this->subject->id], - ['auraSpell', $this->subject->id], ['triggeredSpell', $this->subject->id] - ); - - $ubObjects = new GameObjectList($conditions); - if (!$ubObjects->error) - { - $this->lvTabs[] = ['object', array( - 'data' => array_values($ubObjects->getListviewData()), - 'id' => 'used-by-object', - 'name' => '$LANG.tab_usedby' - )]; - - $this->extendGlobalData($ubObjects->getJSGlobals()); - } - - // tab: criteria of - $conditions = array( - ['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] - ); - $coAchievemnts = new AchievementList($conditions); - if (!$coAchievemnts->error) - { - $this->lvTabs[] = ['achievement', array( - 'data' => array_values($coAchievemnts->getListviewData()), - 'id' => 'criteria-of', - 'name' => '$LANG.tab_criteriaof' - )]; - - $this->extendGlobalData($coAchievemnts->getJSGlobals(GLOBALINFO_SELF | GLOBALINFO_RELATED)); - } - - // 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) - { - $this->extendGlobalData($spellLoot->jsGlobals); - - $lv = $spellLoot->getResult(); - $extraCols = $spellLoot->extraCols; - $extraCols[] = '$Listview.extraCols.percent'; - - 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])) - { - $lv[$bar] = $foo[$bar]; - $lv[$bar]['percent'] = $extraItem['additionalCreateChance']; - $lv[$bar]['condition'][0][$this->typeId][] = [[CND_SPELL, $extraItem['requiredSpecialization']]]; - $this->extendGlobalIds(TYPE_SPELL, $extraItem['requiredSpecialization']); - $extraCols[] = '$Listview.extraCols.condition'; - if ($max = ($extraItem['additionalMaxNum'] - 1)) - $lv[$bar]['stack'] = [1, $max]; - - break; // skill_extra_item_template can only contain 1 item - } - } - } - - $this->lvTabs[] = ['item', array( - 'data' => array_values($lv), - 'name' => '$LANG.tab_contains', - 'id' => 'contains', - 'hiddenCols' => ['side', 'slot', 'source', 'reqlevel'], - 'extraCols' => $extraCols - )]; - } - - // tab: exclusive with - if ($this->firstRank) { - $linkedSpells = DB::World()->selectCol( // dont look too closely ..... please..? - 'SELECT IF(sg2.spell_id < 0, sg2.id, sg2.spell_id) AS ARRAY_KEY, IF(sg2.spell_id < 0, sg2.spell_id, sr.stack_rule) - FROM spell_group sg1 - JOIN spell_group sg2 - ON (sg1.id = sg2.id OR sg1.id = -sg2.spell_id) AND sg1.spell_id != sg2.spell_id - LEFT JOIN spell_group_stack_rules sr - ON sg1.id = sr.group_id - WHERE sg1.spell_id = ?d', - $this->firstRank - ); - - if ($linkedSpells) - { - $extraSpells = []; - foreach ($linkedSpells as $k => $v) - { - if ($v > 0) - continue; - - $extraSpells += DB::World()->selectCol( // recursive case (recursive and regular ids are not mixed in a group) - 'SELECT sg2.spell_id AS ARRAY_KEY, sr.stack_rule - FROM spell_group sg1 - JOIN spell_group sg2 - ON sg2.id = -sg1.spell_id AND sg2.spell_id != ?d - LEFT JOIN spell_group_stack_rules sr - ON sg1.id = sr.group_id - WHERE sg1.id = ?d', - $this->firstRank, - $k - ); - - unset($linkedSpells[$k]); - } - - // todo (high): fixme - querys have erronous edge-cases (see spell: 13218) - if ($groups = $linkedSpells + $extraSpells) - { - $stacks = new SpellList(array(['s.id', array_keys($groups)])); - if (!$stacks->error) - { - $data = $stacks->getListviewData(); - foreach ($data as $k => $d) - $data[$k]['stackRule'] = $groups[$k]; - - if (!$stacks->hasSetFields(['skillLines'])) - $sH = ['skill']; - - $tabData = array( - 'data' => array_values($data), - 'id' => 'spell-group-stack', - 'name' => Lang::spell('stackGroup'), - 'visibleCols' => ['stackRules'] - ); - - if (isset($sH)) - $tabData['hiddenCols'] = $sH; - - $this->lvTabs[] = ['spell', $tabData]; - - $this->extendGlobalData($stacks->getJSGlobals(GLOBALINFO_SELF | GLOBALINFO_RELATED)); - } - } - } - } - - // 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 - FROM spell_linked_spell - WHERE ABS(spell_effect) = ?d OR ABS(spell_trigger) = ?d', - $this->typeId, $this->typeId, $this->typeId - ); - - $related = []; - foreach ($rows as $row) - $related[] = $row['related']; - - if ($related) - $linked = new SpellList(array(['s.id', $related])); - - if (isset($linked) && !$linked->error) - { - $lv = $linked->getListviewData(); - $data = []; - - foreach ($rows as $r) - { - foreach ($lv as $dk => $d) - { - if ($r['related'] != $dk) - continue; - - $lv[$dk]['linked'] = [$r['trigger'], $r['effect'], $r['type']]; - $data[] = $lv[$dk]; - break; - } - } - - $this->lvTabs[] = ['spell', array( - 'data' => array_values($data), - 'id' => 'spell-link', - 'name' => Lang::spell('linkedWith'), - 'hiddenCols' => ['skill', 'name'], - 'visibleCols' => ['linkedTrigger', 'linkedEffect'] - )]; - - $this->extendGlobalData($linked->getJSGlobals(GLOBALINFO_SELF | GLOBALINFO_RELATED)); - } - - - // 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]], - ); - - $trigger = new SpellList($conditions); - if (!$trigger->error) - { - $this->lvTabs[] = ['spell', array( - 'data' => array_values($trigger->getListviewData()), - 'id' => 'triggered-by', - 'name' => '$LANG.tab_triggeredby' - )]; - - $this->extendGlobalData($trigger->getJSGlobals(GLOBALINFO_SELF)); - } - - // tab: used by - creature - // SMART_SCRIPT_TYPE_CREATURE = 0; SMART_ACTION_CAST = 11; SMART_ACTION_ADD_AURA = 75; SMART_ACTION_INVOKER_CAST = 85; SMART_ACTION_CROSS_CAST = 86 - $conditions = array( - '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 ($_ = DB::World()->selectCol('SELECT entryOrGUID FROM smart_scripts WHERE entryorguid > 0 AND source_type = 0 AND action_type IN (11, 75, 85, 86) AND action_param1 = ?d', $this->typeId)) - $conditions[] = ['id', $_]; - - $ubCreature = new CreatureList($conditions); - if (!$ubCreature->error) - { - $this->lvTabs[] = ['creature', array( - 'data' => array_values($ubCreature->getListviewData()), - 'id' => 'used-by-npc', - 'name' => '$LANG.tab_usedby' - )]; - - $this->extendGlobalData($ubCreature->getJSGlobals(GLOBALINFO_SELF)); - } - - // tab: zone - if ($areas = DB::World()->select('SELECT * FROM spell_area WHERE spell = ?d', $this->typeId)) - { - $zones = new ZoneList(array(['id', array_column($areas, 'area')])); - if (!$zones->error) - { - $lvZones = $zones->getListviewData(); - $this->extendGlobalData($zones->getJSGlobals()); - - $lv = []; - $parents = []; - $extra = false; - foreach ($areas as $a) - { - if (empty($lvZones[$a['area']])) - continue; - - $condition = []; - if ($a['aura_spell']) - { - $this->extendGlobalIds(TYPE_SPELL, abs($a['aura_spell'])); - $condition[0][$this->typeId][] = [[$a['aura_spell'] > 0 ? CND_AURA : -CND_AURA, abs($a['aura_spell'])]]; - } - - if ($a['quest_start']) // status for quests needs work - { - $this->extendGlobalIds(TYPE_QUEST, $a['quest_start']); - $group = []; - for ($i = 0; $i < 7; $i++) - { - if (!($a['quest_start_status'] & (1 << $i))) - continue; - - if ($i == 0) - $group[] = [CND_QUEST_NONE, $a['quest_start']]; - else if ($i == 1) - $group[] = [CND_QUEST_COMPLETE, $a['quest_start']]; - else if ($i == 3) - $group[] = [CND_QUESTTAKEN, $a['quest_start']]; - else if ($i == 6) - $group[] = [CND_QUESTREWARDED, $a['quest_start']]; - } - - if ($group) - $condition[0][$this->typeId][] = $group; - } - - if ($a['quest_end'] && $a['quest_end'] != $a['quest_start']) - { - $this->extendGlobalIds(TYPE_QUEST, $a['quest_end']); - $group = []; - for ($i = 0; $i < 7; $i++) - { - if (!($a['quest_end_status'] & (1 << $i))) - continue; - - if ($i == 0) - $group[] = [-CND_QUEST_NONE, $a['quest_end']]; - else if ($i == 1) - $group[] = [-CND_QUEST_COMPLETE, $a['quest_end']]; - else if ($i == 3) - $group[] = [-CND_QUESTTAKEN, $a['quest_end']]; - else if ($i == 6) - $group[] = [-CND_QUESTREWARDED, $a['quest_end']]; - } - - if ($group) - $condition[0][$this->typeId][] = $group; - } - - if ($a['racemask']) - { - $foo = []; - for ($i = 0; $i < 11; $i++) - if ($a['racemask'] & (1 << $i)) - $foo[] = $i + 1; - - $this->extendGlobalIds(TYPE_RACE, $foo); - $condition[0][$this->typeId][] = [[CND_RACE, $a['racemask']]]; - } - - if ($a['gender'] != 2) // 2: both - $condition[0][$this->typeId][] = [[CND_GENDER, $a['gender'] + 1]]; - - $row = $lvZones[$a['area']]; - if ($condition) - { - $extra = true; - $row = array_merge($row, ['condition' => $condition]); - } - - // merge subzones, into one row, if: conditions match && parentZone is shared - if ($p = $zones->getEntry($a['area'])['parentArea']) - { - $parents[] = $p; - $row['parentArea'] = $p; - $row['subzones'] = [$a['area']]; - } - else - $row['parentArea'] = 0; - - $set = false; - foreach ($lv as &$v) - { - if ($v['parentArea'] != $row['parentArea'] && $v['id'] != $row['parentArea']) - continue; - - if (empty($v['condition']) xor empty($row['condition'])) - continue; - - if (!empty($row['condition']) && !empty($v['condition']) && $v['condition'] != $row['condition']) - continue; - - if (!$row['parentArea'] && $v['id'] != $row['parentArea']) - continue; - - $set = true; - $v['subzones'][] = $row['id']; - break; - } - - // add self as potential subzone; IF we are a parentZone without added children, we get filtered in JScript - if (!$set) - { - $row['subzones'] = [$row['id']]; - $lv[] = $row; - } - } - - // overwrite lvData with parent-lvData (condition and subzones are kept) - if ($parents) - { - $parents = (new ZoneList(array(['id', $parents])))->getListviewData(); - foreach ($lv as &$_) - if (isset($parents[$_['parentArea']])) - $_ = array_merge($_, $parents[$_['parentArea']]); - } - - $tabData = ['data' => array_values($lv)]; - - if ($extra) - { - $tabData['extraCols'] = ['$Listview.extraCols.condition']; - $tabData['hiddenCols'] = ['instancetype']; - } - - $this->lvTabs[] = ['zone', $tabData]; - } - } - - // tab: teaches - if ($ids = Game::getTaughtSpells($this->subject)) - { - $teaches = new SpellList(array(['id', $ids])); - if (!$teaches->error) - { - $this->extendGlobalData($teaches->getJSGlobals(GLOBALINFO_SELF | GLOBALINFO_RELATED)); - $vis = ['level', 'schools']; - - foreach ($teaches->iterate() as $__) - { - if (!$teaches->canCreateItem()) - continue; - - $vis[] = 'reagents'; - break; - } - - $tabData = array( - 'data' => array_values($teaches->getListviewData()), - 'id' => 'teaches-spell', - 'name' => '$LANG.tab_teaches', - 'visibleCols' => $vis, - ); - - if (!$teaches->hasSetFields(['skillLines'])) - $tabData['hiddenCols'] = ['skill']; - - $this->lvTabs[] = ['spell', $tabData]; - } - } - - // tab: taught by npc (source:6 => trainer) - if (!empty($this->subject->sources[$this->typeId][6])) - { - $src = $this->subject->sources[$this->typeId][6]; - $list = []; - if (count($src) == 1 && $src[0] == 1) // multiple trainer - { - $list = DB::World()->selectCol(' - SELECT IF(t1.ID > 200000, t2.ID, t1.ID) - FROM npc_trainer t1 - LEFT JOIN npc_trainer t2 ON t2.SpellID = -t1.ID - WHERE t1.SpellID = ?d', - $this->typeId - ); - } - else if ($src) - $list = array_values($src); - - if ($list) - { - $tbTrainer = new CreatureList(array(CFG_SQL_LIMIT_NONE, ['ct.id', $list], ['s.guid', null, '!'], ['ct.npcflag', 0x10, '&'])); - if (!$tbTrainer->error) - { - $this->extendGlobalData($tbTrainer->getJSGlobals()); - $this->lvTabs[] = ['creature', array( - 'data' => array_values($tbTrainer->getListviewData()), - 'id' => 'taught-by-npc', - 'name' => '$LANG.tab_taughtby', - )]; - } - } - } - - // 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]], - ); - - $tbSpell = new SpellList($conditions); - $tbsData = []; - if (!$tbSpell->error) - { - $tbsData = $tbSpell->getListviewData(); - $this->lvTabs[] = ['spell', array( - 'data' => array_values($tbsData), - 'id' => 'taught-by-spell', - 'name' => '$LANG.tab_taughtby' - )]; - - $this->extendGlobalData($tbSpell->getJSGlobals(GLOBALINFO_SELF)); - } - - // tab: taught by quest - $conditions = ['OR', ['sourceSpellId', $this->typeId], ['rewardSpell', $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]; - - $tbQuest = new QuestList($conditions); - if (!$tbQuest->error) - { - $this->lvTabs[] = ['quest', array( - 'data' => array_values($tbQuest->getListviewData()), - 'id' => 'reward-from-quest', - 'name' => '$LANG.tab_rewardfrom' - )]; - - $this->extendGlobalData($tbQuest->getJSGlobals()); - } - - // 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', 6], ['spellId1', $this->subject->id]], - ['AND', ['spellTrigger2', 6], ['spellId2', $this->subject->id]], - ['AND', ['spellTrigger3', 6], ['spellId3', $this->subject->id]], - ['AND', ['spellTrigger4', 6], ['spellId4', $this->subject->id]], - ['AND', ['spellTrigger5', 6], ['spellId5', $this->subject->id]], - ); - - $tbItem = new ItemList($conditions); - if (!$tbItem->error) - { - $this->lvTabs[] = ['item', array( - 'data' => array_values($tbItem->getListviewData()), - 'id' => 'taught-by-item', - 'name' => '$LANG.tab_taughtby' - )]; - - $this->extendGlobalData($tbItem->getJSGlobals(GLOBALINFO_SELF)); - } - - // tab: enchantments - $conditions = array( - 'OR', - ['AND', ['type1', [1, 3, 7]], ['object1', $this->typeId]], - ['AND', ['type2', [1, 3, 7]], ['object2', $this->typeId]], - ['AND', ['type3', [1, 3, 7]], ['object3', $this->typeId]] - ); - $enchList = new EnchantmentList($conditions); - if (!$enchList->error) - { - $this->lvTabs[] = ['enchantment', array( - 'data' => array_values($enchList->getListviewData()), - 'name' => Util::ucFirst(Lang::game('enchantments')) - ), 'enchantment']; - - $this->extendGlobalData($enchList->getJSGlobals()); - } - - // tab: sounds - $activitySounds = DB::Aowow()->selectRow('SELECT * FROM ?_spell_sounds WHERE id = ?d', $this->subject->getField('spellVisualId')); - array_shift($activitySounds); // remove id-column - if ($activitySounds) - { - $sounds = new SoundList(array(['id', $activitySounds])); - if (!$sounds->error) - { - $data = $sounds->getListviewData(); - foreach ($activitySounds as $activity => $id) - if (isset($data[$id])) - $data[$id]['activity'] = $activity; // no index, js wants a string :( - - $tabData = ['data' => array_values($data)]; - if ($activitySounds) - $tabData['visibleCols'] = ['activity']; - - $this->extendGlobalData($sounds->getJSGlobals(GLOBALINFO_SELF)); - $this->lvTabs[] = ['sound', $tabData]; - } - } - - - // find associated NPC, Item and merge results - // taughtbypets (unused..?) - // taughtbyquest (usually the spell casted as quest reward teaches something; exclude those seplls from taughtBySpell) - // taughtbytrainers - // taughtbyitem - - // tab: conditions - $sc = Util::getServerConditions([CND_SRC_SPELL_LOOT_TEMPLATE, CND_SRC_SPELL_IMPLICIT_TARGET, CND_SRC_SPELL, CND_SRC_SPELL_CLICK_EVENT, CND_SRC_VEHICLE_SPELL, CND_SRC_SPELL_PROC], null, $this->typeId); - if (!empty($sc[0])) - { - $this->extendGlobalData($sc[1]); - $tab = ""; - - $this->lvTabs[] = [null, array( - 'data' => $tab, - 'id' => 'conditions', - 'name' => '$LANG.requires' - )]; - } - } - - protected function generateTooltip($asError = false) - { - if ($asError) - die('$WowheadPower.registerSpell('.$this->typeId.', '.User::$localeId.', {});'); - - $x = '$WowheadPower.registerSpell('.$this->typeId.', '.User::$localeId.", {\n"; - $pt = []; - if ($n = $this->subject->getField('name', true)) - $pt[] = "\tname_".User::$localeString.": '".Util::jsEscape($n)."'"; - if ($i = $this->subject->getField('iconString', true, true)) - $pt[] = "\ticon: '".rawurlencode($i)."'"; - if ($tt = $this->subject->renderTooltip()) - { - $pt[] = "\ttooltip_".User::$localeString.": '".Util::jsEscape($tt[0])."'"; - $pt[] = "\tspells_".User::$localeString.": ".Util::toJSON($tt[1]); - } - if ($btt = $this->subject->renderBuff()) - { - $pt[] = "\tbuff_".User::$localeString.": '".Util::jsEscape($btt[0])."'"; - $pt[] = "\tbuffspells_".User::$localeString.": ".Util::toJSON($btt[1]);; - } - $x .= implode(",\n", $pt)."\n});"; - - return $x; - } - - public function display($override = '') - { - if ($this->mode != CACHE_TYPE_TOOLTIP) - return parent::display($override); - - if (!$this->loadCache($tt)) - { - $tt = $this->generateTooltip(); - $this->saveCache($tt); - } - - header('Content-type: application/x-javascript; charset=utf-8'); - die($tt); - } - - public function notFound($title = '', $msg = '') - { - if ($this->mode != CACHE_TYPE_TOOLTIP) - return parent::notFound($title ?: Lang::game('spell'), $msg ?: Lang::spell('notFound')); - - header('Content-type: application/x-javascript; charset=utf-8'); - echo $this->generateTooltip(true); - exit(); - } - - private function appendReagentItem(&$reagentResult, $_iId, $_qty, $_mult, $_level, $_path, $alreadyUsed) - { - if (in_array($_iId, $alreadyUsed)) - return false; - - $item = DB::Aowow()->selectRow(' - SELECT name_loc0, name_loc2, name_loc3, name_loc6, name_loc8, i.id, ic.name AS iconString, quality, - IF ( (spellId1 > 0 AND spellCharges1 < 0) OR - (spellId2 > 0 AND spellCharges2 < 0) OR - (spellId3 > 0 AND spellCharges3 < 0) OR - (spellId4 > 0 AND spellCharges4 < 0) OR - (spellId5 > 0 AND spellCharges5 < 0), 1, 0) AS consumed - FROM ?_items i - LEFT JOIN ?_icons ic ON ic.id = i.iconId - WHERE i.id = ?d', - $_iId - ); - - if (!$item) - return false; - - $this->extendGlobalIds(TYPE_ITEM, $item['id']); - - $_level++; - - if ($item['consumed']) - $_qty++; - - $data = array( - 'type' => TYPE_ITEM, - 'typeId' => $item['id'], - 'typeStr' => Util::$typeStrings[TYPE_ITEM], - 'quality' => $item['quality'], - 'name' => Util::localizedString($item, 'name'), - 'icon' => $item['iconString'], - 'qty' => $_qty * $_mult, - 'path' => $_path.'.'.TYPE_ITEM.'-'.$item['id'], - 'level' => $_level - ); - - $idx = count($reagentResult); - $reagentResult[] = $data; - $alreadyUsed[] = $item['id']; - - if (!$this->appendReagentSpell($reagentResult, $item['id'], $data['qty'], $data['level'], $data['path'], $alreadyUsed)) - $reagentResult[$idx]['final'] = true; - - return true; - } - - private function appendReagentSpell(&$reagentResult, $_iId, $_qty, $_level, $_path, $alreadyUsed) - { - $_level++; - // assume that tradeSpells only use the first index to create items, so this runs somewhat efficiently >.< - $spells = DB::Aowow()->select(' - SELECT reagent1, reagent2, reagent3, reagent4, reagent5, reagent6, reagent7, reagent8, - reagentCount1, reagentCount2, reagentCount3, reagentCount4, reagentCount5, reagentCount6, reagentCount7, reagentCount8, - name_loc0, name_loc2, name_loc3, name_loc6, name_loc8, - 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 = 24)',// OR - // (effect2CreateItemId = ?d AND effect2Id = 24) OR - // (effect3CreateItemId = ?d AND effect3Id = 24)', - $_iId //, $_iId, $_iId - ); - - if (!$spells) - return false; - - $didAppendSomething = false; - foreach ($spells as $sId => $row) - { - if (in_array(-$sId, $alreadyUsed)) - continue; - - $this->extendGlobalIds(TYPE_SPELL, $sId); - - $data = array( - 'type' => TYPE_SPELL, - 'typeId' => $sId, - 'typeStr' => Util::$typeStrings[TYPE_SPELL], - 'name' => Util::localizedString($row, 'name'), - 'icon' => $row['iconString'], - 'qty' => $_qty, - 'path' => $_path.'.'.TYPE_SPELL.'-'.$sId, - 'level' => $_level, - ); - - $reagentResult[] = $data; - $_aU = $alreadyUsed; - $_aU[] = -$sId; - - $hasUnusedReagents = false; - for ($i = 1; $i < 9; $i++) - { - if ($row['reagent'.$i] <= 0 || $row['reagentCount'.$i] <= 0) - continue; - - if ($this->appendReagentItem($reagentResult, $row['reagent'.$i], $row['reagentCount'.$i], $data['qty'], $data['level'], $data['path'], $_aU)) - { - $hasUnusedReagents = true; - $didAppendSomething = true; - } - } - - if (!$hasUnusedReagents) // no reagents were added, remove spell from result set - array_pop($reagentResult); - } - - return $didAppendSomething; - } - - private function createReagentList() - { - $reagentResult = []; - $enhanced = false; - - if ($reagents = $this->subject->getReagentsForCurrent()) - { - foreach ($this->subject->relItems->iterate() as $iId => $__) - { - if (!in_array($iId, array_keys($reagents))) - continue; - - $data = array( - 'type' => TYPE_ITEM, - 'typeId' => $iId, - 'typeStr' => Util::$typeStrings[TYPE_ITEM], - 'quality' => $this->subject->relItems->getField('quality'), - 'name' => $this->subject->relItems->getField('name', true), - 'icon' => $this->subject->relItems->getField('iconString'), - 'qty' => $reagents[$iId][1], - 'path' => TYPE_ITEM.'-'.$iId, // id of the html-element - 'level' => 0 // depths in array, used for indentation - ); - - $idx = count($reagentResult); - $reagentResult[] = $data; - - // start with self and current original item in usedEntries (spell < 0; item > 0) - if ($this->appendReagentSpell($reagentResult, $iId, $data['qty'], 0, $data['path'], [-$this->typeId, $iId])) - $enhanced = true; - else - $reagentResult[$idx]['final'] = true; - } - } - - // increment all indizes (by prepending null and removing it again) - array_unshift($reagentResult, null); - unset($reagentResult[0]); - - return [$enhanced, $reagentResult]; - } - - private function createScalingData() // calculation mostly like seen in TC - { - $scaling = array_merge( - array( - 'directSP' => -1, - 'dotSP' => -1, - 'directAP' => 0, - 'dotAP' => 0 - ), - (array)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 (!$this->subject->isDamagingSpell() && !$this->subject->isHealingSpell()) - return $scaling; - - foreach ($scaling as $k => $v) - { - // only calculate for class/pet spells - if ($v != -1 || !in_array($this->subject->getField('typeCat'), [-2, -3, -7, 7])) - continue; - - - // no known calculation for physical abilities - if ($k == 'directAP' || $k == 'dotAP') - continue; - - // dont use spellPower to scale physical Abilities - if ($this->subject->getField('schoolMask') == 0x1 && ($k == 'directSP' || $k == 'dotSP')) - continue; - - $isDOT = false; - $pMask = $this->subject->periodicEffectsMask(); - - if ($k == 'dotSP' || $k == 'dotAP') - { - if ($pMask) - $isDOT = true; - else - continue; - } - else // if all used effects are periodic, dont calculate direct component - { - $bar = true; - for ($i = 1; $i < 4; $i++) - { - if (!$this->subject->getField('effect'.$i.'Id')) - continue; - - if ($pMask & 1 << ($i - 1)) - continue; - - $bar = false; - } - - if ($bar) - continue; - } - - // Damage over Time spells bonus calculation - $dotFactor = 1.0; - if ($isDOT) - { - $dotDuration = $this->subject->getField('duration'); - // 200% limit - if ($dotDuration > 0) - { - if ($dotDuration > 30000) - $dotDuration = 30000; - if (!$this->subject->isChanneledSpell()) - $dotFactor = $dotDuration / 15000; - } - } - - // Distribute Damage over multiple effects, reduce by AoE - $castingTime = $this->subject->getCastingTimeForBonus($isDOT); - - // 50% for damage and healing spells for leech spells from damage bonus and 0% from healing - for ($j = 1; $j < 4; ++$j) - { - // SPELL_EFFECT_HEALTH_LEECH || SPELL_AURA_PERIODIC_LEECH - if ($this->subject->getField('effectId'.$j) == 9 || $this->subject->getField('effect'.$j.'AuraId') == 53) - { - $castingTime /= 2; - break; - } - } - - if ($this->subject->isHealingSpell()) - $castingTime *= 1.88; - - // SPELL_SCHOOL_MASK_NORMAL - if ($this->subject->getField('schoolMask') != 0x1) - $scaling[$k] = ($castingTime / 3500.0) * $dotFactor; - else - $scaling[$k] = 0; // would be 1 ($dotFactor), but we dont want it to be displayed - } - - return $scaling; - } - - private function createRequiredItems() - { - // parse itemClass & itemSubClassMask - $class = $this->subject->getField('equippedItemClass'); - $subClass = $this->subject->getField('equippedItemSubClassMask'); - $invType = $this->subject->getField('equippedItemInventoryTypeMask'); - - if ($class <= 0) - return; - - $title = ['Class: '.$class, 'SubClass: '.Util::asHex($subClass)]; - $text = Lang::getRequiredItems($class, $subClass, false); - - if ($invType) - { - // remap some duplicated strings 'Off Hand' and 'Shield' are never used simultaneously - if ($invType & (1 << INVTYPE_ROBE)) // Robe => Chest - { - $invType &= ~(1 << INVTYPE_ROBE); - $invType |= (1 << INVTYPE_CHEST); - } - - if ($invType & (1 << INVTYPE_RANGEDRIGHT)) // Ranged2 => Ranged - { - $invType &= ~(1 << INVTYPE_RANGEDRIGHT); - $invType |= (1 << INVTYPE_RANGED); - } - - $_ = []; - $strs = Lang::item('inventoryType'); - foreach ($strs as $k => $str) - if ($invType & 1 << $k && $str) - $_[] = $str; - - $title[] = Lang::item('slot').Lang::main('colon').Util::asHex($invType); - $text .= ' '.Lang::spell('_inSlot').Lang::main('colon').implode(', ', $_); - } - - return [$title, $text]; - } - - private function createTools() - { - $tools = $this->subject->getToolsForCurrent(); - - // prepare Tools - foreach ($tools as &$tool) - { - if (isset($tool['itemId'])) // Tool - $tool['url'] = '?item='.$tool['itemId']; - else // ToolCat - { - $tool['quality'] = ITEM_QUALITY_HEIRLOOM - ITEM_QUALITY_NORMAL; - $tool['url'] = '?items&filter=cr=91;crs='.$tool['id'].';crv=0'; - } - } - - return $tools; - } - - private function createEffects(&$infobox, &$redButtons) - { - // proc data .. maybe use more information..? - $procData = DB::World()->selectRow('SELECT IF(ProcsPerMinute > 0, -ProcsPerMinute, Chance) AS chance, Cooldown AS cooldown FROM spell_proc WHERE ABS(SpellId) = ?d', $this->firstRank); - if (!isset($procData['cooldown'])) - $procData['cooldown'] = 0; - - $effects = []; - $spellIdx = array_unique(array_merge($this->subject->canTriggerSpell(), $this->subject->canTeachSpell())); - $itemIdx = $this->subject->canCreateItem(); - $perfItem = DB::World()->selectRow('SELECT * FROM skill_perfect_item_template WHERE spellId = ?d', $this->typeId); - - // Iterate through all effects: - for ($i = 1; $i < 4; $i++) - { - if ($this->subject->getField('effect'.$i.'Id') <= 0) - continue; - - $effId = (int)$this->subject->getField('effect'.$i.'Id'); - $effMV = (int)$this->subject->getField('effect'.$i.'MiscValue'); - $effMVB = (int)$this->subject->getField('effect'.$i.'MiscValueB'); - $effBP = (int)$this->subject->getField('effect'.$i.'BasePoints'); - $effDS = (int)$this->subject->getField('effect'.$i.'DieSides'); - $effRPPL = $this->subject->getField('effect'.$i.'RealPointsPerLevel'); - $effAura = (int)$this->subject->getField('effect'.$i.'AuraId'); - $foo = &$effects[]; - - // Icons: - // .. from item - if (in_array($i, $itemIdx)) - { - $_ = $this->subject->getField('effect'.$i.'CreateItemId'); - foreach ($this->subject->relItems->iterate() as $itemId => $__) - { - if ($itemId != $_) - continue; - - $foo['icon'] = array( - 'id' => $this->subject->relItems->id, - 'name' => $this->subject->relItems->getField('name', true), - 'quality' => $this->subject->relItems->getField('quality'), - 'count' => $effDS + $effBP, - 'icon' => $this->subject->relItems->getField('iconString') - ); - - break; - } - - // perfect Items - if ($perfItem && $this->subject->relItems->getEntry($perfItem['perfectItemType'])) - { - $cndSpell = new SpellList(array(['id', $perfItem['requiredSpecialization']])); - if (!$cndSpell->error) - { - $foo['perfItem'] = array( - 'icon' => $cndSpell->getField('iconString'), - 'quality' => $this->subject->relItems->getField('quality'), - 'cndSpellId' => $perfItem['requiredSpecialization'], - 'cndSpellName' => $cndSpell->getField('name', true), - 'chance' => $perfItem['perfectCreateChance'], - 'itemId' => $perfItem['perfectItemType'], - 'itemName' => $this->subject->relItems->getField('name', true) - ); - } - } - - if ($effDS > 1) - $foo['icon']['count'] = "'".($effBP + 1).'-'.$foo['icon']['count']."'"; - } - // .. from spell - else if (in_array($i, $spellIdx) || $effId == 133) - { - if ($effId == 155) - $_ = $effMV; - else - $_ = $this->subject->getField('effect'.$i.'TriggerSpell'); - - $trig = new SpellList(array(['s.id', (int)$_])); - - $foo['icon'] = array( - 'id' => $_, - 'name' => $trig->error ? Util::ucFirst(Lang::game('spell')).' #'.$_ : $trig->getField('name', true), - 'count' => 0 - ); - - $this->extendGlobalData($trig->getJSGlobals(GLOBALINFO_SELF | GLOBALINFO_RELATED)); - } - - // Effect Name - $foo['name'] = (User::isInGroup(U_GROUP_EMPLOYEE) ? sprintf(Util::$dfnString, 'EffectId: '.$effId, Lang::spell('effects', $effId)) : Lang::spell('effects', $effId)).Lang::main('colon'); - - if ($this->subject->getField('effect'.$i.'RadiusMax') > 0) - $foo['radius'] = $this->subject->getField('effect'.$i.'RadiusMax'); - - if (!in_array($i, $itemIdx) && !in_array($i, $spellIdx) && !in_array($effAura, [225, 227])) - $foo['value'] = ($effDS && $effDS != 1 ? ($effBP + 1).Lang::game('valueDelim') : null).($effBP + $effDS); - - if ($effRPPL != 0) - $foo['value'] = (isset($foo['value']) ? $foo['value'] : '0').sprintf(Lang::spell('costPerLevel'), $effRPPL); - if ($this->subject->getField('effect'.$i.'Periode') > 0) - $foo['interval'] = Util::formatTime($this->subject->getField('effect'.$i.'Periode')); - - if ($_ = $this->subject->getField('effect'.$i.'Mechanic')) - $foo['mechanic'] = Lang::game('me', $_); - - if (in_array($i, $this->subject->canTriggerSpell()) && !empty($procData['chance'])) - $foo['procData'] = array( - $procData['chance'], - $procData['cooldown'] ? Util::formatTime($procData['cooldown'], true) : null - ); - else if (in_array($i, $this->subject->canTriggerSpell()) && $this->subject->getField('procChance')) - $foo['procData'] = array( - $this->subject->getField('procChance'), - $procData['cooldown'] ? Util::formatTime($procData['cooldown'], true) : null - ); - - // parse masks and indizes - switch ($effId) - { - case 8: // Power Drain - case 30: // Energize - case 137: // Energize Pct - $_ = Lang::spell('powerTypes', $effMV); - if ($_ && User::isInGroup(U_GROUP_EMPLOYEE)) - $_ = sprintf(Util::$dfnString, 'MiscValue'.Lang::main('colon').$effMV, $_); - else if (!$_) - $_ = $effMV; - - if ($effMV == POWER_RAGE || $effMV == POWER_RUNIC_POWER) - $foo['value'] = ($effDS && $effDS != 1 ? (($effBP + 1) / 10).Lang::game('valueDelim') : null).(($effBP + $effDS) / 10); - - $foo['name'] .= ' ('.$_.')'; - break; - case 16: // QuestComplete - if ($_ = QuestList::getName($effMV)) - $foo['name'] .= '('.$_.')'; - else - $foo['name'] .= Util::ucFirst(Lang::game('quest')).' #'.$effMV;; - break; - case 28: // Summon - case 90: // Kill Credit - case 134: // Kill Credit2 - if ($summon = $this->subject->getModelInfo($this->typeId, $i)) - $redButtons[BUTTON_VIEW3D] = ['type' => TYPE_NPC, 'displayId' => $summon['displayId']]; - - $_ = Lang::game('npc').' #'.$effMV; - if ($n = CreatureList::getName($effMV)) - $_ = ' ('.$n.')'; - - $foo['name'] .= $_; - break; - case 33: // Open Lock - $_ = $effMV ? Lang::spell('lockType', $effMV) : $effMV; - if ($_ && User::isInGroup(U_GROUP_EMPLOYEE)) - $_ = sprintf(Util::$dfnString, 'MiscValue'.Lang::main('colon').$effMV, $_); - else if (!$_) - $_ = $effMV; - - $foo['name'] .= ' ('.$_.')'; - break; - case 53: // Enchant Item Perm - case 54: // Enchant Item Temp - case 92: // Enchant Held Item - case 156: // Enchant Item Prismatic - if ($_ = DB::Aowow()->selectRow('SELECT * FROM ?_itemenchantment WHERE id = ?d', $effMV)) - $foo['name'] .= ' ('.Util::localizedString($_, 'name').')'; - else - $foo['name'] .= ' #'.$effMV; - break; - case 38: // Dispel [miscValue => Types] - case 126: // Steal Aura - $_ = Lang::game('dt', $effMV); - if ($_ && User::isInGroup(U_GROUP_EMPLOYEE)) - $_ = sprintf(Util::$dfnString, 'MiscValue'.Lang::main('colon').$effMV, $_); - else if (!$_) - $_ = $effMV; - - $foo['name'] .= ' ('.$_.')'; - break; - case 39: // Learn Language - $_ = Lang::game('languages', $effMV); - if ($_ && User::isInGroup(U_GROUP_EMPLOYEE)) - $_ = sprintf(Util::$dfnString, 'MiscValue'.Lang::main('colon').$effMV, $_); - else if (!$_) - $_ = $effMV; - - $foo['name'] .= ' ('.$_.')'; - break; - case 50: // Trans Door - case 76: // Summon Object (Wild) - // case 86: // Activate Object - case 104: // Summon Object (slot 1) - case 105: // Summon Object (slot 2) - case 106: // Summon Object (slot 3) - case 107: // Summon Object (slot 4) - if ($summon = $this->subject->getModelInfo($this->typeId, $i)) - $redButtons[BUTTON_VIEW3D] = ['type' => TYPE_OBJECT, 'displayId' => $summon['displayId']]; - - $_ = Util::ucFirst(Lang::game('object')).' #'.$effMV; - if ($n = GameobjectList::getName($effMV)) - $_ = ' ('.$n.')'; - - $foo['name'] .= $_; - break; - case 74: // Apply Glyph - if ($_ = DB::Aowow()->selectCell('SELECT spellId FROM ?_glyphproperties WHERE id = ?d', $effMV)) - { - if ($n = SpellList::getName($_)) - $foo['name'] .= '('.$n.')'; - else - $foo['name'] .= Util::ucFirst(Lang::game('spell')).' #'.$effMV; - } - else - $foo['name'] .= ' #'.$effMV;; - break; - case 95: // Skinning - switch ($effMV) - { - case 0: $_ = Lang::game('ct', 1).', '.Lang::game('ct', 2); break; // Beast, Dragonkin - case 1: - case 2: $_ = Lang::game('ct', 4); break; // Elemental (nature based, earth based) - case 3: $_ = Lang::game('ct', 9); break; // Mechanic - default; $_ = ''; - } - if (User::isInGroup(U_GROUP_EMPLOYEE)) - $_ = sprintf(Util::$dfnString, 'MiscValue'.Lang::main('colon').$effMV, $_); - else - $_ = $effMV; - - $foo['name'] .= ' ('.$_.')'; - break; - case 108: // Dispel Mechanic - $_ = Lang::game('me', $effMV); - if ($_ && User::isInGroup(U_GROUP_EMPLOYEE)) - $_ = sprintf(Util::$dfnString, 'MiscValue'.Lang::main('colon').$effMV, $_); - else if (!$_) - $_ = $effMV; - - $foo['name'] .= ' ('.$_.')'; - break; - case 118: // Require Skill - if ($_ = SkillList::getName($effMV)) - $foo['name'] .= '('.$_.')'; - else - $foo['name'] .= Util::ucFirst(Lang::game('skill')).' #'.$effMV;; - break; - case 146: // Activate Rune - $_ = Lang::spell('powerRunes', $effMV); - if ($_ && User::isInGroup(U_GROUP_EMPLOYEE)) - $_ = sprintf(Util::$dfnString, 'MiscValue'.Lang::main('colon').$effMV, $_); - else if (!$_) - $_ = $effMV; - - $foo['name'] .= ' ('.$_.')'; - break; - case 131: // Play Music - case 132: // Play Sound - $foo['markup'] = '[sound='.$effMV.']'; - break; - case 103: // Reputation - $_ = Util::ucFirst(Lang::game('faction')).' #'.$effMV; - if ($n = FactionList::getName($effMV)) - $_ = ' ('.$n.')'; - - // apply custom reward rated - if ($cuRate = DB::World()->selectCell('SELECT spell_rate FROM reputation_reward_rate WHERE spell_rate <> 1 && faction = ?d', $effMV)) - $foo['value'] .= sprintf(Util::$dfnString, Lang::faction('customRewRate'), ' ('.(($cuRate < 1 ? '-' : '+').intVal(($cuRate - 1) * $foo['value'])).')'); - - $foo['name'] .= $_; - - break; - case 123: // Send Taxi - effMV is taxiPathId. We only use paths for flightmasters for now, so spell-triggered paths are not in the table - default: - { - if (($effMV || $effId == 97) && $effId != 155) - $foo['name'] .= ' ('.$effMV.')'; - - break; - } - // Aura - case 6: // Simple - case 27: // AA Persistent - case 35: // AA Party - case 65: // AA Raid - case 119: // AA Pet - case 128: // AA Friend - case 129: // AA Enemy - case 143: // AA Owner - { - if ($effAura > 0 && ($aurName = Lang::spell('auras', $effAura))) - { - $foo['name'] .= User::isInGroup(U_GROUP_EMPLOYEE) ? sprintf(Util::$dfnString, 'AuraId: '.$effAura, $aurName) : $aurName; - - $bar = $effMV; - switch ($effAura) - { - case 17: // Mod Stealth Detection - if ($_ = Lang::spell('stealthType', $effMV)) - $bar = User::isInGroup(U_GROUP_EMPLOYEE) ? sprintf(Util::$dfnString, 'MiscValue'.Lang::main('colon').$effMV, $_) : $_; - - break; - case 19: // Mod Invisibility Detection - if ($_ = Lang::spell('invisibilityType', $effMV)) - $bar = User::isInGroup(U_GROUP_EMPLOYEE) ? sprintf(Util::$dfnString, 'MiscValue'.Lang::main('colon').$effMV, $_) : $_; - - break; - case 24: // Periodic Energize - case 21: // Obsolete Mod Power - case 35: // Mod Increase Power - case 85: // Mod Power Regeneration - case 110: // Mod Power Regeneration Pct - if ($_ = Lang::spell('powerTypes', $effMV)) - $bar = User::isInGroup(U_GROUP_EMPLOYEE) ? sprintf(Util::$dfnString, 'MiscValue'.Lang::main('colon').$effMV, $_) : $_; - - break; - case 29: // Mod Stat - case 80: // Mod Stat % - case 137: // Mod Total Stat % - case 175: // Mod Spell Healing Of Stat Percent - case 212: // Mod Ranged Attack Power Of Stat Percent - case 219: // Mod Mana Regeneration from Stat - case 268: // Mod Attack Power Of Stat Percent - $mask = $effMV == -1 ? 0x1F : 1 << $effMV; - $_ = []; - for ($j = 0; $j < 5; $j++) - if ($mask & (1 << $j)) - $_[] = Lang::game('stats', $j); - - if ($_ = implode(', ', $_)); - $bar = User::isInGroup(U_GROUP_EMPLOYEE) ? sprintf(Util::$dfnString, 'MiscValue'.Lang::main('colon').$effMV, $_) : $_; - - break; - case 36: // Shapeshift - if ($st = $this->subject->getModelInfo($this->typeId, $i)) - { - $redButtons[BUTTON_VIEW3D] = array( - 'type' => TYPE_NPC, - 'displayId' => $st['displayId'] - ); - - if ($st['creatureType'] > 0) - $infobox[] = Lang::game('type').Lang::main('colon').Lang::game('ct', $st['creatureType']); - - if ($_ = $st['displayName']) - $bar = User::isInGroup(U_GROUP_EMPLOYEE) ? sprintf(Util::$dfnString, 'MiscValue'.Lang::main('colon').$effMV, $_) : $_; - } - break; - case 37: // Effect immunity - if ($_ = Lang::spell('effects', $effMV)) - $bar = User::isInGroup(U_GROUP_EMPLOYEE) ? sprintf(Util::$dfnString, 'MiscValue'.Lang::main('colon').$effMV, $_) : $_; - - break; - case 38: // Aura immunity - if ($_ = Lang::spell('auras', $effMV)) - $bar = User::isInGroup(U_GROUP_EMPLOYEE) ? sprintf(Util::$dfnString, 'MiscValue'.Lang::main('colon').$effMV, $_) : $_; - - break; - case 41: // Dispel Immunity - case 178: // Mod Debuff Resistance - case 245: // Mod Aura Duration By Dispel - if ($_ = Lang::game('dt', $effMV)) - $bar = User::isInGroup(U_GROUP_EMPLOYEE) ? sprintf(Util::$dfnString, 'MiscValue'.Lang::main('colon').$effMV, $_) : $_; - - break; - case 44: // Track Creature - if ($_ = Lang::game('ct', $effMV)) - $bar = User::isInGroup(U_GROUP_EMPLOYEE) ? sprintf(Util::$dfnString, 'MiscValue'.Lang::main('colon').$effMV, $_) : $_; - - break; - case 45: // Track Resource - if ($_ = Lang::spell('lockType', $effMV)) - $bar = User::isInGroup(U_GROUP_EMPLOYEE) ? sprintf(Util::$dfnString, 'MiscValue'.Lang::main('colon').$effMV, $_) : $_; - - break; - case 75: // Language - if ($_ = Lang::game('languages', $effMV)) - $bar = User::isInGroup(U_GROUP_EMPLOYEE) ? sprintf(Util::$dfnString, 'MiscValue'.Lang::main('colon').$effMV, $_) : $_; - - break; - case 77: // Mechanic Immunity - case 117: // Mod Mechanic Resistance - case 232: // Mod Mechanic Duration - case 234: // Mod Mechanic Duration (no stack) - case 255: // Mod Mechanic Damage Taken Pct - case 276: // Mod Mechanic Damage Done Percent - if ($_ = Lang::game('me', $effMV)) - $bar = User::isInGroup(U_GROUP_EMPLOYEE) ? sprintf(Util::$dfnString, 'MiscValue'.Lang::main('colon').Util::asHex($effMV), $_) : $_; - - break; - case 147: // Mechanic Immunity Mask - $_ = []; - foreach (Lang::game('me') as $k => $str) - if ($k && ($effMV & (1 << $k - 1))) - $_[] = $str; - - if ($_ = implode(', ', $_)) - $bar = User::isInGroup(U_GROUP_EMPLOYEE) ? sprintf(Util::$dfnString, 'MiscValue'.Lang::main('colon').Util::asHex($effMV), $_) : $_; - - break; - case 10: // Mod Threat - case 13: // Mod Damage Done - case 14: // Mod Damage Taken - case 22: // Mod Resistance - case 39: // School Immunity - case 40: // Damage Immunity - case 50: // Mod Critical Healing Amount - case 57: // Mod Spell Crit Chance - case 69: // School Absorb - case 71: // Mod Spell Crit Chance School - case 72: // Mod Power Cost School Percent - case 73: // Mod Power Cost School Flat - case 74: // Reflect Spell School - case 79: // Mod Damage Done Pct - case 81: // Split Damage Pct - case 83: // Mod Base Resistance - case 87: // Mod Damage Taken Pct - case 97: // Mana Shield - case 101: // Mod Resistance Pct - case 115: // Mod Healing Taken - case 118: // Mod Healing Taken Pct - case 123: // Mod Target Resistance - case 135: // Mod Healing Done - case 136: // Mod Healing Done Pct - case 142: // Mod Base Resistance Pct - case 143: // Mod Resistance Exclusive - case 149: // Reduce Pushback - case 163: // Mod Crit Damage Bonus - case 174: // Mod Spell Damage Of Stat Percent - case 182: // Mod Resistance Of Stat Percent - case 186: // Mod Attacker Spell Hit Chance - case 194: // Mod Target Absorb School - case 195: // Mod Target Ability Absorb School - case 199: // Mod Increases Spell Percent to Hit - case 229: // Mod AoE Damage Avoidance - case 271: // Mod Damage Percent Taken Form Caster - case 310: // Mod Creature AoE Damage Avoidance - case 237: // Mod Spell Damage Of Attack Power - case 238: // Mod Spell Healing Of Attack Power - if ($_ = Lang::getMagicSchools($effMV)) - $bar = User::isInGroup(U_GROUP_EMPLOYEE) ? sprintf(Util::$dfnString, 'MiscValue'.Lang::main('colon').Util::asHex($effMV), $_) : $_; - - break; - case 30: // Mod Skill - case 98: // Mod Skill Value - if ($n = SkillList::getName($effMV)) - $bar = ' ('.$n.')'; - else - $bar = Lang::main('colon').Util::ucFirst(Lang::game('skill')).' #'.$effMV;; - - break; - case 107: // Flat Modifier - case 108: // Pct Modifier - if ($_ = Lang::spell('spellModOp', $effMV)) - $bar = User::isInGroup(U_GROUP_EMPLOYEE) ? sprintf(Util::$dfnString, 'MiscValue'.Lang::main('colon').$effMV, $_) : $_; - - break; - case 132: // Mod Increase Energy Percent - if ($_ = Lang::spell('powerTypes', $effMV)) - $bar = User::isInGroup(U_GROUP_EMPLOYEE) ? sprintf(Util::$dfnString, 'MiscValue'.Lang::main('colon').$effMV, $_) : $_; - - break; - case 189: // Mod Rating - case 220: // Combat Rating From Stat - $_ = []; - foreach (Lang::spell('combatRating') as $k => $str) - if ((1 << $k) & $effMV) - $_[] = $str; - - if ($_ = implode(', ', $_)) - $bar = User::isInGroup(U_GROUP_EMPLOYEE) ? sprintf(Util::$dfnString, 'MiscValue'.Lang::main('colon').Util::asHex($effMV), $_) : $_; - - break; - case 168: // Mod Damage Done Versus - case 59: // Mod Damage Done Versus Creature - case 102: // Mod Melee Attack Power Versus - case 131: // Mod Ranged Attack Power Versus - case 180: // Mod Spell Damage Versus - $_ = []; - foreach (Lang::game('ct') as $k => $str) - if ($k && ($effMV & (1 << $k - 1))) - $_[] = $str; - - if ($_ = implode(', ', $_)) - $bar = User::isInGroup(U_GROUP_EMPLOYEE) ? sprintf(Util::$dfnString, 'MiscValue'.Lang::main('colon').Util::asHex($effMV), $_) : $_; - - break; - case 249: // Convert Rune - if ($_ = Lang::spell('powerRunes', $effMVB)) - $bar = User::isInGroup(U_GROUP_EMPLOYEE) ? sprintf(Util::$dfnString, 'MiscValueB'.Lang::main('colon').$effMVB, $_) : $_; - - break; - case 78: // Mounted - case 56: // Transform - if ($transform = $this->subject->getModelInfo($this->typeId, $i)) - { - $redButtons[BUTTON_VIEW3D] = ['type' => TYPE_NPC, 'displayId' => $transform['displayId']]; - $bar = $transform['typeId'] ? ' ('.$transform['displayName'].')' : ' (#0)'; - } - else - $bar = Lang::main('colon').Lang::game('npc').' #'.$effMV;; - - break; - case 139: // Force Reaction - $foo['value'] = sprintf(Util::$dfnString, $foo['value'], Lang::game('rep', $foo['value'])); - // DO NOT BREAK - case 190: // Mod Faction Reputation Gain - $n = FactionList::getName($effMV); - $bar = ' ('.($n ? ''.$n.'' : Util::ucFirst(Lang::game('faction')).' #'.$effMV).')'; - break; // also breaks for 139 - case 293: // Override Spells - if ($so = DB::Aowow()->selectRow('SELECT spellId1, spellId2, spellId3, spellId4, spellId5 FROM ?_spelloverride WHERE id = ?d', $effMV)) - { - $buff = []; - for ($i = 1; $i < 6; $i++) - { - if ($x = $so['spellId'.$i]) - { - $this->extendGlobalData([TYPE_SPELL => [$x]]); - $buff[] = '[spell='.$x.']'; - } - } - $foo['markup'] = implode(', ', $buff); - } - break; - } - $foo['name'] .= strstr($bar, 'href') || strstr($bar, '#') ? $bar : ($bar ? ' ('.$bar.')' : null); - - if (in_array($effAura, [174, 220, 182])) - $foo['name'] .= ' ['.sprintf(Util::$dfnString, 'MiscValueB'.Lang::main('colon').$effMVB, Lang::game('stats', $effMVB)).']'; - else if ($effMVB > 0) - $foo['name'] .= ' ['.$effMVB.']'; - - } - else if ($effAura > 0) - $foo['name'] .= Lang::main('colon').'Unknown Aura ('.$effAura.')'; - - break; - } - } - - // cases where we dont want 'Value' to be displayed - if (in_array($effAura, [11, 12, 36, 77]) || in_array($effId, [132]) || empty($foo['value'])) - unset($foo['value']); - } - - unset($foo); // clear reference - - return $effects; - } -} - - - -?> diff --git a/pages/talent.php b/pages/talent.php deleted file mode 100644 index 4db7a2b7..00000000 --- a/pages/talent.php +++ /dev/null @@ -1,55 +0,0 @@ - 'talentcalc.css'], - ['path' => 'talent.css'] - ); - - private $isPetCalc = false; - - public function __construct($pageCall, $__) - { - parent::__construct($pageCall, $__); - - $this->isPetCalc = $pageCall == 'petcalc'; - $this->name = $this->isPetCalc ? Lang::main('petCalc') : Lang::main('talentCalc'); - } - - protected function generateContent() - { - // add conditional js & css - $this->addJS(array( - ($this->isPetCalc ? '?data=pet-talents.pets' : '?data=glyphs').'&locale='.User::$localeId.'&t='.$_SESSION['dataKey'], - $this->isPetCalc ? 'petcalc.js' : 'talent.js', - $this->isPetCalc ? 'swfobject.js' : null - )); - $this->addCSS($this->isPetCalc ? ['path' => 'petcalc.css'] : null); - - $this->tcType = $this->isPetCalc ? 'pc' : 'tc'; - } - - protected function generateTitle() - { - array_unshift($this->title, $this->name); - } - - protected function generatePath() - { - $this->path[] = $this->isPetCalc ? 2 : 0; - } -} - -?> diff --git a/pages/title.php b/pages/title.php deleted file mode 100644 index 66f61181..00000000 --- a/pages/title.php +++ /dev/null @@ -1,138 +0,0 @@ -typeId = intVal($id); - - $this->subject = new TitleList(array(['id', $this->typeId])); - if ($this->subject->error) - $this->notFound(Lang::game('title'), Lang::title('notFound')); - - $this->name = $this->subject->getHtmlizedName(); - $this->nameFixed = Util::ucFirst(trim(strtr($this->subject->getField('male', true), ['%s' => '', ',' => '']))); - } - - protected function generatePath() - { - $this->path[] = $this->subject->getField('category'); - } - - protected function generateTitle() - { - array_unshift($this->title, $this->nameFixed, Util::ucFirst(Lang::game('title'))); - } - - protected function generateContent() - { - /***********/ - /* Infobox */ - /***********/ - - $infobox = Lang::getInfoBoxForFlags($this->subject->getField('cuFlags')); - - if ($this->subject->getField('side') == SIDE_ALLIANCE) - $infobox[] = Lang::main('side').Lang::main('colon').'[span class=icon-alliance]'.Lang::game('si', SIDE_ALLIANCE).'[/span]'; - else if ($this->subject->getField('side') == SIDE_HORDE) - $infobox[] = Lang::main('side').Lang::main('colon').'[span class=icon-horde]'.Lang::game('si', SIDE_HORDE).'[/span]'; - else - $infobox[] = Lang::main('side').Lang::main('colon').Lang::game('si', SIDE_BOTH); - - if ($g = $this->subject->getField('gender')) - $infobox[] = Lang::main('gender').Lang::main('colon').'[span class=icon-'.($g == 2 ? 'female' : 'male').']'.Lang::main('sex', $g).'[/span]'; - - if ($eId = $this->subject->getField('eventId')) - { - $this->extendGlobalIds(TYPE_WORLDEVENT, $eId); - $infobox[] = Lang::game('eventShort').Lang::main('colon').'[event='.$eId.']'; - } - - /****************/ - /* Main Content */ - /****************/ - - $this->infobox = $infobox ? '[ul][li]'.implode('[/li][li]', $infobox).'[/li][/ul]' : null; - $this->expansion = Util::$expansionString[$this->subject->getField('expansion')]; - $this->redButtons = array( - BUTTON_WOWHEAD => true, - BUTTON_LINKS => ['type' => $this->type, 'typeId' => $this->typeId] - ); - - // 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)) - { - $altTitle = new TitleList(array(['id', abs($pendant)])); - if (!$altTitle->error) - { - $this->transfer = sprintf( - Lang::title('_transfer'), - $altTitle->id, - $altTitle->getHtmlizedName(), - $pendant > 0 ? 'alliance' : 'horde', - $pendant > 0 ? Lang::game('si', 1) : Lang::game('si', 2) - ); - } - } - - /**************/ - /* Extra Tabs */ - /**************/ - - // tab: quest source - $quests = new QuestList(array(['rewardTitleId', $this->typeId])); - if (!$quests->error) - { - $this->extendGlobalData($quests->getJSGlobals(GLOBALINFO_REWARDS)); - - $this->lvTabs[] = ['quest', array( - 'data' => array_values($quests->getListviewData()), - 'id' => 'reward-from-quest', - 'name' => '$LANG.tab_rewardfrom', - 'hiddenCols' => ['experience', 'money'], - 'visibleCols' => ['category'] - )]; - } - - // tab: achievement source - if ($aIds = DB::World()->selectCol('SELECT ID FROM achievement_reward WHERE TitleA = ?d OR TitleH = ?d', $this->typeId, $this->typeId)) - { - $acvs = new AchievementList(array(['id', $aIds])); - if (!$acvs->error) - { - $this->extendGlobalData($acvs->getJSGlobals()); - - $this->lvTabs[] = ['achievement', array( - 'data' => array_values($acvs->getListviewData()), - 'id' => 'reward-from-achievement', - 'name' => '$LANG.tab_rewardfrom', - 'visibleCols' => ['category'], - 'sort' => ['reqlevel', 'name'] - )]; - } - } - - // tab: criteria of (to be added by TC) - } -} - -?> diff --git a/pages/titles.php b/pages/titles.php deleted file mode 100644 index ed41c6ea..00000000 --- a/pages/titles.php +++ /dev/null @@ -1,69 +0,0 @@ -getCategoryFromUrl($pageParam);; - - parent::__construct($pageCall, $pageParam); - - $this->name = Util::ucFirst(Lang::game('titles')); - } - - protected function generateContent() - { - $conditions = []; - - if (!User::isInGroup(U_GROUP_EMPLOYEE)) // hide unused titles - $conditions[] = [['cuFlags', CUSTOM_EXCLUDE_FOR_LISTVIEW, '&'], 0]; - - if ($this->category) - $conditions[] = ['category', $this->category[0]]; - - $tabData = ['data' => []]; - $titles = new TitleList($conditions); - if (!$titles->error) - { - $tabData['data'] = array_values($titles->getListviewData()); - - if ($titles->hasDiffFields(['category'])) - $tabData['visibleCols'] = ['category']; - - if (!$titles->hasAnySource()) - $tabData['hiddenCols'] = ['source']; - } - - $this->lvTabs[] = ['title', $tabData]; - } - - protected function generateTitle() - { - array_unshift($this->title, Util::ucFirst(Lang::game('titles'))); - if ($this->category) - array_unshift($this->title, Lang::title('cat', $this->category[0])); - } - - protected function generatePath() - { - if ($this->category) - $this->path[] = $this->category[0]; // should be only one parameter anyway - } -} - -?> diff --git a/pages/user.php b/pages/user.php deleted file mode 100644 index 80a73754..00000000 --- a/pages/user.php +++ /dev/null @@ -1,284 +0,0 @@ - 'Profiler.css']]; - protected $mode = CACHE_TYPE_NONE; - - protected $typeId = 0; - protected $pageName = ''; - - public function __construct($pageCall, $pageParam) - { - parent::__construct($pageCall, $pageParam); - - if ($pageParam) - { - // todo: check if account is disabled or something - if ($user = DB::Aowow()->selectRow('SELECT a.id, a.user, a.displayName, a.consecutiveVisits, a.userGroups, a.avatar, a.title, a.description, a.joinDate, a.prevLogin, IFNULL(SUM(ar.amount), 0) AS sumRep FROM ?_account a LEFT JOIN ?_account_reputation ar ON a.id = ar.userId WHERE a.user = ? GROUP BY a.id', $pageParam)) - $this->user = $user; - else - $this->notFound(sprintf(Lang::user('notFound'), $pageParam)); - } - else if (User::$id) - { - header('Location: ?user='.User::$displayName, true, 302); - die(); - } - else - $this->forwardToSignIn('user'); - } - - protected function generateContent() - { - /***********/ - /* Infobox */ - /***********/ - - $infobox = $contrib = $groups = []; - foreach (Lang::account('groups') as $idx => $key) - if ($idx >= 0 && $this->user['userGroups'] & (1 << $idx)) - $groups[] = (!fMod(count($groups) + 1, 3) ? '[br]' : null).Lang::account('groups', $idx); - - $infobox[] = Lang::user('joinDate'). Lang::main('colon').'[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('lastLogin').Lang::main('colon').'[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('userGroups').Lang::main('colon').($groups ? implode(', ', $groups) : Lang::account('groups', -1)); - $infobox[] = Lang::user('consecVisits').Lang::main('colon').$this->user['consecutiveVisits']; - $infobox[] = Util::ucFirst(Lang::main('siteRep')).Lang::main('colon').Lang::nf($this->user['sumRep']); - - // contrib -> [url=http://www.wowhead.com/client]Data uploads: n [small]([tooltip=tooltip_totaldatauploads]xx.y MB[/tooltip])[/small][/url] - - $co = DB::Aowow()->selectRow( - 'SELECT COUNT(DISTINCT c.id) AS sum, SUM(IFNULL(cr.value, 0)) AS nRates FROM ?_comments c LEFT JOIN ?_comments_rates cr ON cr.commentId = c.id AND cr.userId <> 0 WHERE c.replyTo = 0 AND c.userId = ?d', - $this->user['id'] - ); - if ($co['sum']) - $contrib[] = Lang::user('comments').Lang::main('colon').$co['sum'].($co['nRates'] ? ' [small]([tooltip=tooltip_totalratings]'.$co['nRates'].'[/tooltip])[/small]' : null); - - $ss = DB::Aowow()->selectRow('SELECT COUNT(*) AS sum, SUM(IF(status & ?d, 1, 0)) AS nSticky, SUM(IF(status & ?d, 0, 1)) AS nPending FROM ?_screenshots WHERE userIdOwner = ?d AND (status & ?d) = 0', - CC_FLAG_STICKY, - CC_FLAG_APPROVED, - $this->user['id'], - CC_FLAG_DELETED - ); - if ($ss['sum']) - { - $buff = []; - if ($ss['nSticky'] || $ss['nPending']) - { - if ($normal = ($ss['sum'] - $ss['nSticky'] - $ss['nPending'])) - $buff[] = '[tooltip=tooltip_normal]'.$normal.'[/tooltip]'; - - if ($ss['nSticky']) - $buff[] = '[tooltip=tooltip_sticky]'.$ss['nSticky'].'[/tooltip]'; - - if ($ss['nPending']) - $buff[] = '[tooltip=tooltip_pending]'.$ss['nPending'].'[/tooltip]'; - } - - $contrib[] = Lang::user('screenshots').Lang::main('colon').$ss['sum'].($buff ? ' [small]('.implode($buff, ' + ').')[/small]' : null); - } - - $vi = DB::Aowow()->selectRow('SELECT COUNT(id) AS sum, SUM(IF(status & ?d, 1, 0)) AS nSticky, SUM(IF(status & ?d, 0, 1)) AS nPending FROM ?_videos WHERE userIdOwner = ?d AND (status & ?d) = 0', - CC_FLAG_STICKY, - CC_FLAG_APPROVED, - $this->user['id'], - CC_FLAG_DELETED - ); - if ($vi['sum']) - { - $buff = []; - if ($vi['nSticky'] || $vi['nPending']) - { - if ($normal = ($vi['sum'] - $vi['nSticky'] - $vi['nPending'])) - $buff[] = '[tooltip=tooltip_normal]'.$normal.'[/tooltip]'; - - if ($vi['nSticky']) - $buff[] = '[tooltip=tooltip_sticky]'.$vi['nSticky'].'[/tooltip]'; - - if ($vi['nPending']) - $buff[] = '[tooltip=tooltip_pending]'.$vi['nPending'].'[/tooltip]'; - } - - $contrib[] = Lang::user('videos').Lang::main('colon').$vi['sum'].($buff ? ' [small]('.implode($buff, ' + ').')[/small]' : null); - } - - // contrib -> Forum posts: 5769 [small]([tooltip=topics]579[/tooltip] + [tooltip=replies]5190[/tooltip])[/small] - - $this->infobox = '[ul][li]'.implode('[/li][li]', $infobox).'[/li][/ul]'; - - if ($contrib) - $this->contributions = '[ul][li]'.implode('[/li][li]', $contrib).'[/li][/ul]'; - - - /****************/ - /* Main Content */ - /****************/ - - $this->name = $this->user['title'] ? $this->user['displayName'].' <'.$this->user['title'].'>' : sprintf(Lang::user('profileTitle'), $this->user['displayName']); - - /**************/ - /* Extra Tabs */ - /**************/ - - $this->lvTabs = []; - $this->forceTabs = true; - - // [unused] Site Achievements - - // Reputation changelog (params only for comment-events) - 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'])) - { - foreach ($repData as &$r) - $r['when'] = date(Util::$dateFormatInternal, $r['when']); - - $this->lvTabs[] = ['reputationhistory', ['data' => $repData]]; - } - - // Comments - if ($_ = CommunityContent::getCommentPreviews(['user' => $this->user['id'], 'replies' => false], $nFound)) - { - $tabData = array( - 'data' => $_, - 'hiddenCols' => ['author'], - 'onBeforeCreate' => '$Listview.funcBox.beforeUserComments', - '_totalCount' => $nFound - ); - - if ($nFound > CFG_SQL_LIMIT_DEFAULT) - { - $tabData['name'] = '$LANG.tab_latestcomments'; - $tabData['note'] = '$$WH.sprintf(LANG.lvnote_usercomments, '.$nFound.')'; - } - - $this->lvTabs[] = ['commentpreview', $tabData]; - } - - // Comment Replies - if ($_ = CommunityContent::getCommentPreviews(['user' => $this->user['id'], 'replies' => true], $nFound)) - { - $tabData = array( - 'data' => $_, - 'hiddenCols' => ['author'], - 'onBeforeCreate' => '$Listview.funcBox.beforeUserComments', - '_totalCount' => $nFound - ); - - if ($nFound > CFG_SQL_LIMIT_DEFAULT) - { - $tabData['name'] = '$LANG.tab_latestreplies'; - $tabData['note'] = '$$WH.sprintf(LANG.lvnote_userreplies, '.$nFound.')'; - } - - $this->lvTabs[] = ['replypreview', $tabData]; - } - - // Screenshots - if ($_ = CommunityContent::getScreenshots(-$this->user['id'], 0, $nFound)) - { - $tabData = array( - 'data' => $_, - '_totalCount' => $nFound - ); - - if ($nFound > CFG_SQL_LIMIT_DEFAULT) - { - $tabData['name'] = '$LANG.tab_latestscreenshots'; - $tabData['note'] = '$$WH.sprintf(LANG.lvnote_userscreenshots, '.$nFound.')'; - } - - $this->lvTabs[] = ['screenshot', $tabData]; - } - - // Videos - if ($_ = CommunityContent::getVideos(-$this->user['id'], 0, $nFound)) - { - $tabData = array( - 'data' => $_, - '_totalCount' => $nFound - ); - - if ($nFound > CFG_SQL_LIMIT_DEFAULT) - { - $tabData['name'] = '$LANG.tab_latestvideos'; - $tabData['note'] = '$$WH.sprintf(LANG.lvnote_uservideos, '.$nFound.')'; - } - - $this->lvTabs[] = ['video', $tabData]; - } - - // forum -> latest topics [unused] - - // forum -> latest replies [unused] - - $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); - - $profiles = new LocalProfileList($conditions); - if (!$profiles->error) - { - $this->addJS('?data=weight-presets&t='.$_SESSION['dataKey']); - - // Characters - if ($chars = $profiles->getListviewData(PROFILEINFO_CHARACTER)) - $this->user['characterData'] = $chars; - - // Profiles - if ($prof = $profiles->getListviewData(PROFILEINFO_PROFILE)) - $this->user['profileData'] = $prof; - } - - /* - us_addCharactersTab([ - { - id:763, - "name":"Lilywhite", - "achievementpoints":"0", - "guild":"whatever", - "guildrank":"0", - "realm":"draenor", - "realmname":"Draenor", - "battlegroup":"cyclone", - "battlegroupname":"Cyclone", - "region":"us", - "level":"10", - "race":"7", - "gender":"0", - "classs":"1", - "faction":"0", - "gearscore":"0", - "talenttree1":"0", - "talenttree2":"0", - "talenttree3":"0", - "talentspec":0, - "published":1, - "pinned":0 - } - ]); - */ - - } - - protected function generateTitle() - { - array_unshift($this->title, sprintf(Lang::user('profileTitle'), $this->user['displayName'])); - } - - protected function generatePath() { } -} - -?> diff --git a/pages/utility.php b/pages/utility.php deleted file mode 100644 index 7f42c174..00000000 --- a/pages/utility.php +++ /dev/null @@ -1,309 +0,0 @@ - 'latest-videos', 12 => 'most-comments', 13 => 'missing-screenshots' - ); - - private $page = ''; - private $rss = false; - private $feedData = []; - - public function __construct($pageCall, $pageParam) - { - $this->getCategoryFromUrl($pageParam); - - parent::__construct($pageCall, $pageParam); - - $this->page = $pageCall; - $this->rss = isset($_GET['rss']); - - if ($this->page != 'random') - $this->name = Lang::main('utilities', array_search($pageCall, $this->validPages)); - - if ($this->page == 'most-comments') - { - if ($this->category && in_array($this->category[0], [7, 30])) - $this->name .= Lang::main('colon') . sprintf(Lang::main('mostComments', 1), $this->category[0]); - else - $this->name .= Lang::main('colon') . Lang::main('mostComments', 0); - } - - $this->lvTabs = []; - } - - public function display($override = '') - { - if ($this->rss) // this should not be cached - { - header('Content-Type: application/rss+xml; charset=UTF-8'); - die($this->generateRSS()); - } - else - return parent::display($override); - } - - protected function generateContent() - { - /****************/ - /* Main Content */ - /****************/ - - if (in_array(array_search($this->page, $this->validPages), [0, 1, 2, 3, 11, 12])) - $this->h1Links = ''.Lang::main('subscribe').''; - - switch ($this->page) - { - case 'random': - $type = array_rand(array_filter(Util::$typeClasses)); - $typeId = (new Util::$typeClasses[$type](null))->getRandomId(); - - header('Location: ?'.Util::$typeStrings[$type].'='.$typeId, true, 302); - die(); - case 'latest-comments': // rss - $data = CommunityContent::getCommentPreviews(); - - if ($this->rss) - { - foreach ($data as $d) - { - // todo (low): preview should be html-formated - $this->feedData[] = array( - 'title' => [true, [], Util::ucFirst(Lang::game(Util::$typeStrings[$d['type']])).Lang::main('colon').htmlentities($d['subject'])], - 'link' => [false, [], HOST_URL.'/?go-to-comment&id='.$d['id']], - 'description' => [true, [], htmlentities($d['preview'])."

".sprintf(Lang::main('byUserTimeAgo'), $d['user'], Util::formatTime($d['elapsed'] * 1000, true))], - 'pubDate' => [false, [], date(DATE_RSS, time() - $d['elapsed'])], - 'guid' => [false, [], HOST_URL.'/?go-to-comment&id='.$d['id']] - // 'domain' => [false, [], null] - ); - } - } - else - $this->lvTabs[] = ['commentpreview', ['data' => $data]]; - - break; - case 'latest-screenshots': // rss - $data = CommunityContent::getScreenshots(); - - if ($this->rss) - { - foreach ($data as $d) - { - $desc = ''; - if ($d['caption']) - $desc .= '
'.$d['caption']; - $desc .= "

".sprintf(Lang::main('byUserTimeAgo'), $d['user'], Util::formatTime($d['elapsed'] * 1000, true)); - - // enclosure/length => filesize('static/uploads/screenshots/thumb/'.$d['id'].'.jpg') .. always set to this placeholder value though - $this->feedData[] = array( - 'title' => [true, [], Util::ucFirst(Lang::game(Util::$typeStrings[$d['type']])).Lang::main('colon').htmlentities($d['subject'])], - 'link' => [false, [], HOST_URL.'/?'.Util::$typeStrings[$d['type']].'='.$d['typeId'].'#screenshots:id='.$d['id']], - 'description' => [true, [], $desc], - 'pubDate' => [false, [], date(DATE_RSS, time() - $d['elapsed'])], - 'enclosure' => [false, ['url' => STATIC_URL.'/uploads/screenshots/thumb/'.$d['id'].'.jpg', 'length' => 12345, 'type' => 'image/jpeg'], null], - 'guid' => [false, [], HOST_URL.'/?'.Util::$typeStrings[$d['type']].'='.$d['typeId'].'#screenshots:id='.$d['id']], - // 'domain' => [false, [], live|ptr] - ); - } - } - else - $this->lvTabs[] = ['screenshot', ['data' => $data]]; - - break; - case 'latest-videos': // rss - $data = CommunityContent::getVideos(); - - if ($this->rss) - { - foreach ($data as $d) - { - $desc = ''; - if ($d['caption']) - $desc .= '
'.$d['caption']; - $desc .= "

".sprintf(Lang::main('byUserTimeAgo'), $d['user'], Util::formatTime($d['elapsed'] * 1000, true)); - - // is enclosure/length .. is this even relevant..? - $this->feedData[] = array( - 'title' => [true, [], Util::ucFirst(Lang::game(Util::$typeStrings[$d['type']])).Lang::main('colon').htmlentities($row['subject'])], - 'link' => [false, [], HOST_URL.'/?'.Util::$typeStrings[$d['type']].'='.$d['typeId'].'#videos:id='.$d['id']], - 'description' => [true, [], $desc], - 'pubDate' => [false, [], date(DATE_RSS, time() - $row['elapsed'])], - 'enclosure' => [false, ['url' => '//i3.ytimg.com/vi/'.$d['videoId'].'/default.jpg', 'length' => 12345, 'type' => 'image/jpeg'], null], - 'guid' => [false, [], HOST_URL.'/?'.Util::$typeStrings[$d['type']].'='.$d['typeId'].'#videos:id='.$d['id']], - // 'domain' => [false, [], live|ptr] - ); - } - } - else - $this->lvTabs[] = ['video', ['data' => $data]]; - - break; - case 'latest-articles': // rss - $this->lvTabs = []; - break; - case 'latest-additions': // rss - $extraText = ''; - break; - case 'unrated-comments': - $this->lvTabs[] = ['commentpreview', ['data' => []]]; - break; - case 'missing-screenshots': - // limit to 200 entries each (it generates faster, consumes less memory and should be enough options) - $cnd = [[['cuFlags', CUSTOM_HAS_SCREENSHOT, '&'], 0], 200]; - if (!User::isInGroup(U_GROUP_EMPLOYEE)) - $cnd[] = [['cuFlags', CUSTOM_EXCLUDE_FOR_LISTVIEW, '&'], 0]; - - foreach (Util::$typeClasses as $type => $classStr) - { - if (!$classStr) - continue; - - if (!($classStr::$contribute & CONTRIBUTE_SS)) - continue; - - $typeObj = new $classStr($cnd); - if (!$typeObj->error) - { - $this->extendGlobalData($typeObj->getJSGlobals(GLOBALINFO_ANY)); - $this->lvTabs[] = [$typeObj::$brickFile, ['data' => array_values($typeObj->getListviewData())]]; - } - } - break; - case 'most-comments': // rss - if ($this->category && !in_array($this->category[0], [1, 7, 30])) - header('Location: ?most-comments=1'.($this->rss ? '&rss' : null), true, 302); - - $tabBase = array( - 'extraCols' => ["\$Listview.funcBox.createSimpleCol('ncomments', 'tab_comments', '10%', 'ncomments')"], - 'sort' => ['-ncomments'] - ); - - foreach (Util::$typeClasses as $type => $classStr) - { - if (!$classStr) - continue; - - $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) - GROUP BY `type`, `typeId` - LIMIT 100', - CC_FLAG_DELETED, - $type, - (isset($this->category[0]) ? $this->category[0] : 1) * DAY - ); - if (!$comments) - continue; - - $typeClass = new $classStr(array(['id', array_keys($comments)])); - if (!$typeClass->error) - { - $data = $typeClass->getListviewData(); - - if ($this->rss) - { - foreach ($data as $typeId => &$d) - { - $this->feedData[] = array( - 'title' => [true, [], htmlentities(Util::$typeStrings[$type] == 'item' ? mb_substr($d['name'], 1) : $d['name'])], - 'type' => [false, [], Util::$typeStrings[$type]], - 'link' => [false, [], HOST_URL.'/?'.Util::$typeStrings[$type].'='.$d['id']], - 'ncomments' => [false, [], $comments[$typeId]] - ); - } - } - else - { - foreach ($data as $typeId => &$d) - $d['ncomments'] = $comments[$typeId]; - - $this->extendGlobalData($typeClass->getJSGlobals(GLOBALINFO_ANY)); - $this->lvTabs[] = [$typeClass::$brickFile, array_merge($tabBase, ['data' => array_values($data)])]; - } - } - } - - break; - } - - // found nothing => set empty content - // tpl: commentpreview - anything, doesn't matter what - if (!$this->lvTabs && !$this->rss) - $this->lvTabs[] = ['commentpreview', ['data' => []]]; - } - - protected function generateRSS() - { - $this->generateContent(); - - $root = new SimpleXML(''); - $root->addAttribute('version', '2.0'); - - $channel = $root->addChild('channel'); - - $channel->addChild('title', CFG_NAME_SHORT.' - '.$this->name); - $channel->addChild('link', HOST_URL.'/?'.$this->page . ($this->category ? '='.$this->category[0] : null)); - $channel->addChild('description', CFG_NAME); - $channel->addChild('language', implode('-', str_split(User::$localeString, 2))); - $channel->addChild('ttl', CFG_TTL_RSS); - $channel->addChild('lastBuildDate', date(DATE_RSS)); - - foreach ($this->feedData as $row) - { - $item = $channel->addChild('item'); - - foreach ($row as $key => list($isCData, $attrib, $text)) - { - if ($isCData && $text) - $child = $item->addChild($key)->addCData($text); - else - $child = $item->addChild($key, $text); - - foreach ($attrib as $k => $v) - $child->addAttribute($k, $v); - } - } - - return $root->asXML(); - } - - protected function generateTitle() - { - if ($this->page == 'most-comments') - { - if ($this->category && in_array($this->category[0], [7, 30])) - array_unshift($this->title, sprintf(Lang::main('mostComments', 1), $this->category[0])); - else - array_unshift($this->title, Lang::main('mostComments', 0)); - } - - array_unshift($this->title, $this->name); - } - - protected function generatePath() - { - $this->path[] = array_search($this->page, $this->validPages); - - if ($this->page == 'most-comments') - { - if ($this->category && in_array($this->category[0], [7, 30])) - $this->path[] = $this->category[0]; - else - $this->path[] = 1; - } - } -} - -?> diff --git a/pages/zone.php b/pages/zone.php deleted file mode 100644 index 52b121bb..00000000 --- a/pages/zone.php +++ /dev/null @@ -1,796 +0,0 @@ -typeId = intVal($id); - - parent::__construct($pageCall, $id); - - $this->subject = new ZoneList(array(['id', $this->typeId])); - if ($this->subject->error) - $this->notFound(Lang::game('zone'), Lang::zone('notFound')); - - $this->name = $this->subject->getField('name', true); - } - - protected function generateContent() - { - $this->addJS('?data=zones&locale='.User::$localeId.'&t='.$_SESSION['dataKey']); - - /***********/ - /* Infobox */ - /***********/ - - $infobox = Lang::getInfoBoxForFlags($this->subject->getField('cuFlags')); - - // City - if ($this->subject->getField('flags') & 0x8 && !$this->subject->getField('parentArea')) - $infobox[] = Lang::zone('city'); - - // Auto repop - if ($this->subject->getField('flags') & 0x1000 && !$this->subject->getField('parentArea')) - $infobox[] = Lang::zone('autoRez'); - - // Level - if ($_ = $this->subject->getField('levelMin')) - { - if ($_ < $this->subject->getField('levelMax')) - $_ .= ' - '.$this->subject->getField('levelMax'); - - $infobox[] = Lang::game('level').Lang::main('colon').$_; - } - - // required Level - if ($_ = $this->subject->getField('levelReq')) - { - if ($__ = $this->subject->getField('levelReqLFG')) - $buff = sprintf(Lang::zone('reqLevels'), $_, $__); - else - $buff = Lang::main('_reqLevel').Lang::main('colon').$_; - - $infobox[] = $buff; - } - - // Territory - $_ = $this->subject->getField('faction'); - $__ = '%s'; - if ($_ == 0) - $__ = '[span class=icon-alliance]%s[/span]'; - else if ($_ == 1) - $__ = '[span class=icon-horde]%s[/span]'; - else if ($_ == 4) - $__ = '[span class=icon-ffa]%s[/span]'; - - $infobox[] = Lang::zone('territory').Lang::main('colon').sprintf($__, Lang::zone('territories', $_)); - - // Instance Type - $infobox[] = Lang::zone('instanceType').Lang::main('colon').'[span class=icon-instance'.$this->subject->getField('type').']'.Lang::zone('instanceTypes', $this->subject->getField('type')).'[/span]'; - - // Heroic mode - if ($_ = $this->subject->getField('levelHeroic')) - $infobox[] = '[icon preset=heroic]'.sprintf(Lang::zone('hcAvailable'), $_).'[/icon]'; - - // number of players - if ($_ = $this->subject->getField('maxPlayer')) - $infobox[] = Lang::zone('numPlayers').Lang::main('colon').($_ == -2 ? '10/25' : $_); - - // Attunement Quest/Achievements & Keys - if ($attmnt = $this->subject->getField('attunes')) - { - foreach ($attmnt as $type => $ids) - { - $this->extendGlobalIds($type, array_map('abs', $ids)); - foreach ($ids as $id) - { - if ($type == TYPE_ITEM) - $infobox[] = Lang::zone('key', (int)($id < 0)).Lang::main('colon').'[item='.abs($id).']'; - else - $infobox[] = Lang::zone('attunement', (int)($id < 0)).Lang::main('colon').'['.Util::$typeStrings[$type].'='.abs($id).']'; - } - } - } - - // Instances - if ($_ = DB::Aowow()->selectCol('SELECT id FROM ?_zones WHERE parentAreaId = ?d AND (flags & ?d) = 0', $this->typeId, CUSTOM_EXCLUDE_FOR_LISTVIEW)) - { - $this->extendGlobalIds(TYPE_ZONE, $_); - $infobox[] = Lang::maps('Instances').Lang::main('colon')."\n[zone=".implode("], \n[zone=", $_).']'; - } - - // location (if instance) - if ($pa = $this->subject->getField('parentAreaId')) - { - $paO = new ZoneList(array(['id', $pa])); - if (!$paO->error) - { - $pins = str_pad($this->subject->getField('parentX') * 10, 3, '0', STR_PAD_LEFT) . str_pad($this->subject->getField('parentY') * 10, 3, '0', STR_PAD_LEFT); - $infobox[] = Lang::zone('location').Lang::main('colon').'[lightbox=map zone='.$pa.' pins='.$pins.']'.$paO->getField('name', true).'[/lightbox]'; - } - } - -/* has to be defined in an article, i think - - // faction(s) / Reputation Hub / Raid Faction - // [li]Raid faction: [faction=1156][/li] || [li]Factions: [faction=1156]/[faction=1156][/li] - - // final boss - // [li]Final boss: [icon preset=boss][npc=37226][/icon][/li] -*/ - - /****************/ - /* Main Content */ - /****************/ - - $addToSOM = function ($what, $entry) use (&$som) - { - // entry always contains: type, id, name, level, coords[] - if (!isset($som[$what][$entry['name']])) // not found yet - $som[$what][$entry['name']][] = $entry; - else // found .. something.. - { - // check for identical floors - foreach ($som[$what][$entry['name']] as &$byFloor) - { - if ($byFloor['level'] != $entry['level']) - continue; - - // found existing floor, ammending coords - $byFloor['coords'][] = $entry['coords'][0]; - return; - } - - // floor not used yet, create it - $som[$what][$entry['name']][] = $entry; - } - }; - - if ($_ = $this->subject->getField('parentArea')) - { - $this->extraText = sprintf(Lang::zone('zonePartOf'), $_); - $this->extendGlobalIds(TYPE_ZONE, $_); - } - - // we cannot fetch spawns via lists. lists are grouped by entry - $oSpawns = DB::Aowow()->select('SELECT * FROM ?_spawns WHERE areaId = ?d AND type = ?d', $this->typeId, TYPE_OBJECT); - $cSpawns = DB::Aowow()->select('SELECT * FROM ?_spawns WHERE areaId = ?d AND type = ?d', $this->typeId, TYPE_NPC); - - $conditions = [CFG_SQL_LIMIT_NONE, ['s.areaId', $this->typeId]]; - if (!User::isInGroup(U_GROUP_STAFF)) - $conditions[] = [['cuFlags', CUSTOM_EXCLUDE_FOR_LISTVIEW, '&'], 0]; - - $objectSpawns = new GameObjectList($conditions); - $creatureSpawns = new CreatureList($conditions); - - $questsLV = $rewardsLV = []; - - // see if we can actually display a map - $hasMap = file_exists('static/images/wow/maps/'.Util::$localeStrings[User::$localeId].'/normal/'.$this->typeId.'.jpg'); - if (!$hasMap) // try multilayered - $hasMap = file_exists('static/images/wow/maps/'.Util::$localeStrings[User::$localeId].'/normal/'.$this->typeId.'-1.jpg'); - if (!$hasMap) // try english fallback - $hasMap = file_exists('static/images/wow/maps/enus/normal/'.$this->typeId.'.jpg'); - if (!$hasMap) // try english fallback, multilayered - $hasMap = file_exists('static/images/wow/maps/enus/normal/'.$this->typeId.'-1.jpg'); - - if ($hasMap) - { - $som = []; - foreach ($oSpawns as $spawn) - { - $tpl = $objectSpawns->getEntry($spawn['typeId']); - if (!$tpl) - continue; - - $n = Util::localizedString($tpl, 'name'); - - $what = ''; - switch ($tpl['typeCat']) - { - case -3: - $what = 'herb'; - break; - case -4: - $what = 'vein'; - break; - case 9: - $what = 'book'; - break; - case -6: - if ($tpl['spellFocusId'] == 1) - $what = 'anvil'; - else if ($tpl['spellFocusId'] == 3) - $what = 'forge'; - - break; - } - - if ($what) - $addToSOM($what, array( - 'coords' => [[$spawn['posX'], $spawn['posY']]], - 'level' => $spawn['floor'], - 'name' => $n, - 'type' => TYPE_OBJECT, - 'id' => $tpl['id'] - )); - - if ($tpl['startsQuests']) - { - $started = new QuestList(array(['qse.method', 1, '&'], ['qse.type', TYPE_OBJECT], ['qse.typeId', $tpl['id']])); - if ($started->error) - continue; - - // store data for misc tabs - foreach ($started->getListviewData() as $id => $data) - { - if (!empty($started->rewards[$id][TYPE_ITEM])) - $rewardsLV = array_merge($rewardsLV, array_keys($started->rewards[$id][TYPE_ITEM])); - - if (!empty($started->choices[$id][TYPE_ITEM])) - $rewardsLV = array_merge($rewardsLV, array_keys($started->choices[$id][TYPE_ITEM])); - - $questsLV[$id] = $data; - } - - $this->extendGlobalData($started->getJSGlobals()); - - if (($tpl['A'] != -1) & ($_ = $started->getSOMData(SIDE_ALLIANCE))) - $addToSOM('alliancequests', 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)), - 'quests' => array_values($_) - )); - - if (($tpl['H'] != -1) & ($_ = $started->getSOMData(SIDE_HORDE))) - $addToSOM('hordequests', 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)), - 'quests' => array_values($_) - )); - } - } - - $flightNodes = []; - foreach ($cSpawns as $spawn) - { - $tpl = $creatureSpawns->getEntry($spawn['typeId']); - if (!$tpl) - continue; - - $n = Util::localizedString($tpl, 'name'); - $sn = Util::localizedString($tpl, 'subname'); - - $what = ''; - if ($tpl['npcflag'] & NPC_FLAG_REPAIRER) - $what = 'repair'; - else if ($tpl['npcflag'] & NPC_FLAG_AUCTIONEER) - $what = 'auctioneer'; - else if ($tpl['npcflag'] & NPC_FLAG_BANKER) - $what = 'banker'; - else if ($tpl['npcflag'] & NPC_FLAG_BATTLEMASTER) - $what = 'battlemaster'; - else if ($tpl['npcflag'] & NPC_FLAG_INNKEEPER) - $what = 'innkeeper'; - else if ($tpl['npcflag'] & NPC_FLAG_TRAINER) - $what = 'trainer'; - else if ($tpl['npcflag'] & NPC_FLAG_VENDOR) - $what = 'vendor'; - else if ($tpl['npcflag'] & NPC_FLAG_FLIGHT_MASTER) - { - $flightNodes[$tpl['id']] = [$spawn['posX'], $spawn['posY']]; - $what = 'flightmaster'; - } - else if ($tpl['npcflag'] & NPC_FLAG_STABLE_MASTER) - $what = 'stablemaster'; - else if ($tpl['npcflag'] & NPC_FLAG_GUILD_MASTER) - $what = 'guildmaster'; - else if ($tpl['npcflag'] & (NPC_FLAG_SPIRIT_HEALER | NPC_FLAG_SPIRIT_GUIDE)) - $what = 'spirithealer'; - else if ($creatureSpawns->isBoss()) - $what = 'boss'; - else if ($tpl['rank'] == 2 || $tpl['rank'] == 4) - $what = 'rare'; - - if ($what) - $addToSOM($what, array( - 'coords' => [[$spawn['posX'], $spawn['posY']]], - 'level' => $spawn['floor'], - 'name' => $n, - 'type' => TYPE_NPC, - 'id' => $tpl['id'], - 'reacthorde' => $tpl['H'] ?: 1, // no neutral (0) setting - 'reactalliance' => $tpl['A'] ?: 1, - 'description' => $sn - )); - - if ($tpl['startsQuests']) - { - $started = new QuestList(array(['qse.method', 1, '&'], ['qse.type', TYPE_NPC], ['qse.typeId', $tpl['id']])); - if ($started->error) - continue; - - // store data for misc tabs - foreach ($started->getListviewData() as $id => $data) - { - if (!empty($started->rewards[$id][TYPE_ITEM])) - $rewardsLV = array_merge($rewardsLV, array_keys($started->rewards[$id][TYPE_ITEM])); - - if (!empty($started->choices[$id][TYPE_ITEM])) - $rewardsLV = array_merge($rewardsLV, array_keys($started->choices[$id][TYPE_ITEM])); - - $questsLV[$id] = $data; - } - - $this->extendGlobalData($started->getJSGlobals()); - - if (($tpl['A'] != -1) & ($_ = $started->getSOMData(SIDE_ALLIANCE))) - $addToSOM('alliancequests', array( - 'coords' => [[$spawn['posX'], $spawn['posY']]], - 'level' => $spawn['floor'], - 'name' => $n, - 'type' => TYPE_NPC, - 'id' => $tpl['id'], - 'reacthorde' => $tpl['H'], - 'reactalliance' => $tpl['A'], - '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( - 'coords' => [[$spawn['posX'], $spawn['posY']]], - 'level' => $spawn['floor'], - 'name' => $n, - 'type' => TYPE_NPC, - 'id' => $tpl['id'], - 'reacthorde' => $tpl['H'], - 'reactalliance' => $tpl['A'], - 'side' => (($tpl['A'] < 0 ? 0 : SIDE_ALLIANCE) | ($tpl['H'] < 0 ? 0 : SIDE_HORDE)), - 'quests' => array_values($_) - )); - } - } - - // remove unwanted indizes - foreach ($som as $what => &$dataz) - { - if (empty($som[$what])) - continue; - - foreach ($dataz as &$data) - $data = array_values($data); - - if (!in_array($what, ['vein', 'herb', 'rare'])) - { - $foo = []; - foreach ($dataz as $d) - foreach ($d as $_) - $foo[] = $_; - - $dataz = $foo; - } - } - - unset($data); - - // append paths between nodes - if ($flightNodes) - { - // neutral nodes come last as the line is colored by the node it's attached to - usort($som['flightmaster'], function($a, $b) { - $n1 = $a['reactalliance'] == $a['reacthorde']; - $n2 = $b['reactalliance'] == $b['reacthorde']; - - if ($n1 && !$n2) - return 1; - - if (!$n1 && $n2) - return -1; - - return 0; - }); - - $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)); - - foreach ($paths as $k => $path) - { - foreach ($som['flightmaster'] as &$fm) - { - if ($fm['id'] != $path[0] && $fm['id'] != $path[1]) - continue; - - if ($fm['id'] == $path[0]) - $fm['paths'][] = $flightNodes[$path[1]]; - - if ($fm['id'] == $path[1]) - $fm['paths'][] = $flightNodes[$path[0]]; - - unset($paths[$k]); - break; - } - } - } - - // preselect bosses for raids/dungeons - if (in_array($this->subject->getField('type'), [2, 3, 4, 5, 7, 8])) - $som['instance'] = true; - - $this->map = array( - 'data' => ['parent' => 'mapper-generic', 'zone' => $this->typeId], - 'som' => $som - ); - } - else - $this->map = false; - - $this->infobox = $infobox ? '[ul][li]'.implode('[/li][li]', $infobox).'[/li][/ul]' : null; - $this->expansion = Util::$expansionString[$this->subject->getField('expansion')]; - $this->redButtons = array( - BUTTON_WOWHEAD => true, - BUTTON_LINKS => ['type' => $this->type, 'typeId' => $this->typeId] - ); - - /* - - associated with holiday? - */ - - /**************/ - /* Extra Tabs */ - /**************/ - - // tab: NPCs - if ($cSpawns && !$creatureSpawns->error) - { - $tabData = array( - 'data' => array_values($creatureSpawns->getListviewData()), - 'note' => sprintf(Util::$filterResultString, '?npcs&filter=cr=6;crs='.$this->typeId.';crv=0') - ); - - if ($creatureSpawns->getMatches() > CFG_SQL_LIMIT_DEFAULT) - $tabData['_truncated'] = 1; - - $this->extendGlobalData($creatureSpawns->getJSGlobals(GLOBALINFO_SELF)); - - $this->lvTabs[] = ['creature', $tabData]; - } - - // tab: Objects - if ($oSpawns && !$objectSpawns->error) - { - $tabData = array( - 'data' => array_values($objectSpawns->getListviewData()), - 'note' => sprintf(Util::$filterResultString, '?objects&filter=cr=1;crs='.$this->typeId.';crv=0') - ); - - if ($objectSpawns->getMatches() > CFG_SQL_LIMIT_DEFAULT) - $tabData['_truncated'] = 1; - - $this->extendGlobalData($objectSpawns->getJSGlobals(GLOBALINFO_SELF)); - - $this->lvTabs[] = ['object', $tabData]; - } - - // tab: Quests [data collected by SOM-routine] - if ($questsLV) - { - $tabData = ['quest', ['data' => array_values($questsLV)]]; - - foreach (Game::$questClasses as $parent => $children) - { - if (in_array($this->typeId, $children)) - { - $tabData[1]['note'] = '$$WH.sprintf(LANG.lvnote_zonequests, '.$parent.', '.$this->typeId.',"'.$this->subject->getField('name', true).'", '.$this->typeId.')'; - break; - } - } - - $this->lvTabs[] = $tabData; - } - - // tab: item-quest starter - // select every quest starter, that is a drop - $questStartItem = DB::Aowow()->select(' - 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)))', - TYPE_ITEM, $this->typeId, - TYPE_NPC, array_unique(array_column($cSpawns, 'typeId')) ?: [0], - TYPE_OBJECT, array_unique(array_column($oSpawns, 'typeId')) ?: [0] - ); - - if ($questStartItem) - { - $qsiList = new ItemList(array(['id', array_keys($questStartItem)])); - if (!$qsiList->error) - { - $this->lvTabs[] = ['item', array( - 'data' => array_values($qsiList->getListviewData()), - 'name' => '$LANG.tab_startsquest', - 'id' => 'starts-quest' - )]; - - $this->extendGlobalData($qsiList->getJSGlobals(GLOBALINFO_SELF)); - } - } - - // tab: Quest Rewards [ids collected by SOM-routine] - if ($rewardsLV) - { - $rewards = new ItemList(array(['id', array_unique($rewardsLV)])); - if (!$rewards->error) - { - $this->lvTabs[] = ['item', array( - 'data' => array_values($rewards->getListviewData()), - 'name' => '$LANG.tab_questrewards', - 'id' => 'quest-rewards', - 'note' => sprintf(Util::$filterResultString, '?items&filter=cr=126;crs='.$this->typeId.';crv=0') - )]; - - $this->extendGlobalData($rewards->getJSGlobals(GLOBALINFO_SELF)); - } - } - - // tab: achievements - - // tab: fished in zone - $fish = new Loot(); - if ($fish->getByContainer(LOOT_FISHING, $this->typeId)) - { - $this->extendGlobalData($fish->jsGlobals); - $xCols = array_merge(['$Listview.extraCols.percent'], $fish->extraCols); - - foreach ($fish->iterate() as $lv) - { - if (!$lv['quest']) - continue; - - $xCols = array_merge($xCols, ['$Listview.extraCols.condition']); - - $reqQuest[$lv['id']] = 0; - - $lv['condition'][0][$this->typeId][] = [[CND_QUESTTAKEN, &$reqQuest[$lv['id']]]]; - } - - $this->lvTabs[] = ['item', array( - 'data' => array_values($fish->getResult()), - 'name' => '$LANG.tab_fishing', - 'id' => 'fishing', - 'extraCols' => array_unique($xCols), - 'hiddenCols' => ['side'] - )]; - } - - // tab: spells - if ($saData = DB::World()->select('SELECT * FROM spell_area WHERE area = ?d', $this->typeId)) - { - $spells = new SpellList(array(['id', array_column($saData, 'spell')])); - if (!$spells->error) - { - $lvSpells = $spells->getListviewData(); - $this->extendGlobalData($spells->getJSGlobals()); - - $extra = false; - foreach ($saData as $a) - { - if (empty($lvSpells[$a['spell']])) - continue; - - $condition = []; - if ($a['aura_spell']) - { - $this->extendGlobalIds(TYPE_SPELL, abs($a['aura_spell'])); - $condition[0][$this->typeId][] = [[$a['aura_spell'] > 0 ? CND_AURA : -CND_AURA, abs($a['aura_spell'])]]; - } - - if ($a['quest_start']) // status for quests needs work - { - $this->extendGlobalIds(TYPE_QUEST, $a['quest_start']); - $group = []; - for ($i = 0; $i < 7; $i++) - { - if (!($a['quest_start_status'] & (1 << $i))) - continue; - - if ($i == 0) - $group[] = [CND_QUEST_NONE, $a['quest_start']]; - else if ($i == 1) - $group[] = [CND_QUEST_COMPLETE, $a['quest_start']]; - else if ($i == 3) - $group[] = [CND_QUESTTAKEN, $a['quest_start']]; - else if ($i == 6) - $group[] = [CND_QUESTREWARDED, $a['quest_start']]; - } - - if ($group) - $condition[0][$this->typeId][] = $group; - } - - if ($a['quest_end'] && $a['quest_end'] != $a['quest_start']) - { - $this->extendGlobalIds(TYPE_QUEST, $a['quest_end']); - $group = []; - for ($i = 0; $i < 7; $i++) - { - if (!($a['quest_end_status'] & (1 << $i))) - continue; - - if ($i == 0) - $group[] = [-CND_QUEST_NONE, $a['quest_end']]; - else if ($i == 1) - $group[] = [-CND_QUEST_COMPLETE, $a['quest_end']]; - else if ($i == 3) - $group[] = [-CND_QUESTTAKEN, $a['quest_end']]; - else if ($i == 6) - $group[] = [-CND_QUESTREWARDED, $a['quest_end']]; - } - - if ($group) - $condition[0][$this->typeId][] = $group; - } - - if ($a['racemask']) - { - $foo = []; - for ($i = 0; $i < 11; $i++) - if ($a['racemask'] & (1 << $i)) - $foo[] = $i + 1; - - $this->extendGlobalIds(TYPE_RACE, $foo); - $condition[0][$this->typeId][] = [[CND_RACE, $a['racemask']]]; - } - - if ($a['gender'] != 2) // 2: both - $condition[0][$this->typeId][] = [[CND_GENDER, $a['gender'] + 1]]; - - if ($condition) - { - $extra = true; - $lvSpells[$a['spell']] = array_merge($lvSpells[$a['spell']], ['condition' => $condition]); - } - } - - $tabData = array( - 'data' => array_values($lvSpells), - 'hiddenCols' => ['skill'] - ); - - if ($extra) - $tabData['extraCols'] = ['$Listview.extraCols.condition']; - - $this->lvTabs[] = ['spell', $tabData]; - } - } - - // tab: subzones - $subZones = new ZoneList(array(['parentArea', $this->typeId])); - if (!$subZones->error) - { - $this->lvTabs[] = ['zone', array( - 'data' => array_values($subZones->getListviewData()), - 'name' => '$LANG.tab_zones', - 'id' => 'subzones', - 'hiddenCols' => ['territory', 'instancetype'] - )]; - - $this->extendGlobalData($subZones->getJSGlobals(GLOBALINFO_SELF)); - } - - // tab: sound (including subzones; excluding parents) - $areaIds = []; - if (!$subZones->error) - $areaIds = $subZones->getFoundIDs(); - - $areaIds[] = $this->typeId; - - $soundIds = []; - $zoneMusic = DB::Aowow()->select(' - 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 - 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)) - $soundIds = array_merge($soundIds, $sSpawns); - - if ($zoneMusic) - $soundIds = array_merge($soundIds, array_column($zoneMusic, 'soundId')); - - if ($soundIds) - { - $music = new SoundList(array(['id', array_unique($soundIds)])); - if (!$music->error) - { - // tab - $data = $music->getListviewData(); - $tabData = []; - - if (array_filter(array_column($zoneMusic, 'worldStateId'))) - { - $tabData['extraCols'] = ['$Listview.extraCols.condition']; - - foreach ($soundIds as $sId) - if (!empty($zoneMusic[$sId]['worldStateId'])) - $data[$sId]['condition'][0][$this->typeId][] = [[CND_WORLD_STATE, $zoneMusic[$sId]['worldStateId'], $zoneMusic[$sId]['worldStateValue']]]; - } - - $tabData['data'] = array_values($data); - - $this->lvTabs[] = ['sound', $tabData]; - - $this->extendGlobalData($music->getJSGlobals(GLOBALINFO_SELF)); - - // audio controls - // ambience - if ($sounds = array_filter($zoneMusic, function ($x) { return $x['type'] == 1; } )) - foreach ($sounds as $sId => $_) - if (!empty($data[$sId]['files'])) - foreach ($data[$sId]['files'] as $f) - $this->zoneMusic['ambience'][] = $f; - - // music - if ($sounds = array_filter($zoneMusic, function ($x) { return $x['type'] == 2; } )) - foreach ($sounds as $sId => $_) - if (!empty($data[$sId]['files'])) - foreach ($data[$sId]['files'] as $f) - $this->zoneMusic['music'][] = $f; - - // intro - if ($sounds = array_filter($zoneMusic, function ($x) { return $x['type'] == 3; } )) - foreach ($sounds as $sId => $_) - if (!empty($data[$sId]['files'])) - foreach ($data[$sId]['files'] as $f) - $this->zoneMusic['intro'][] = $f; - - } - } - } - - protected function generatePath() - { - $this->path[] = $this->subject->getField('category'); - - if (in_array($this->subject->getField('category'), [2, 3])) - $this->path[] = $this->subject->getField('expansion'); - } - - protected function generateTitle() - { - array_unshift($this->title, $this->name, Util::ucFirst(Lang::game('zone'))); - } - -} - -?> diff --git a/pages/zones.php b/pages/zones.php deleted file mode 100644 index 5ceba164..00000000 --- a/pages/zones.php +++ /dev/null @@ -1,180 +0,0 @@ -getCategoryFromUrl($pageParam);; - - parent::__construct($pageCall, $pageParam); - - $this->name = Util::ucFirst(Lang::game('zones')); - } - - protected function generateContent() - { - $conditions = [CFG_SQL_LIMIT_NONE]; - $visibleCols = []; - $hiddenCols = []; - $mapFile = 0; - $spawnMap = -1; - - if (!User::isInGroup(U_GROUP_EMPLOYEE)) // sub-areas and unused zones - $conditions[] = [['cuFlags', CUSTOM_EXCLUDE_FOR_LISTVIEW, '&'], 0]; - - if ($this->category) - { - $conditions[] = ['z.category', $this->category[0]]; - $hiddenCols[] = 'category'; - - if (isset($this->category[1]) && in_array($this->category[0], [2, 3])) - $conditions[] = ['z.expansion', $this->category[1]]; - - if (empty($this->category[1])) - { - switch ($this->category[0]) - { - case 0: $mapFile = -3; $spawnMap = 0; break; - case 1: $mapFile = -6; $spawnMap = 1; break; - case 8: $mapFile = -2; $spawnMap = 530; break; - case 10: $mapFile = -5; $spawnMap = 571; break; - } - } - - switch ($this->category[0]) - { - case 6: - case 2: - case 3: - array_push($visibleCols, 'level', 'players'); - case 9: - $hiddenCols[] = 'territory'; - break; - } - } - - $zones = new ZoneList($conditions); - - if (!$zones->hasSetFields(['type'])) - $hiddenCols[] = 'instancetype'; - - $tabData = ['data' => array_values($zones->getListviewData())]; - - if ($visibleCols) - $tabData['visibleCols'] = $visibleCols; - - if ($hiddenCols) - $tabData['hiddenCols'] = $hiddenCols; - - $this->map = null; - $this->lvTabs[] = ['zone', $tabData]; - - // create flight map - if ($mapFile) - { - $somData = ['flightmaster' => []]; - $nodes = DB::Aowow()->select('SELECT id AS ARRAY_KEY, tn.* FROM ?_taxinodes tn WHERE mapId = ?d ', $spawnMap); - $paths = DB::Aowow()->select(' - 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 - WHERE tn1.Id = tp.endNodeId AND - tn2.Id = tp.startNodeId AND - (tp.startNodeId IN (?a) OR tp.EndNodeId IN (?a)) - ', array_keys($nodes), array_keys($nodes)); - - foreach ($nodes as $i => $n) - { - $neutral = $n['reactH'] == $n['reactA']; - - $data = array( - 'coords' => [[$n['posX'], $n['posY']]], - 'level' => 0, // floor - 'name' => Util::localizedString($n, 'name'), - 'type' => $n['type'], - 'id' => $n['typeId'], - 'reacthorde' => $n['reactH'], - 'reactalliance' => $n['reactA'], - 'paths' => [] - ); - - foreach ($paths as $j => $p) - { - if ($i != $p['startId'] && $i != $p['endId']) - continue; - - if ($i == $p['startId'] && (!$neutral || $p['neutral'])) - { - $data['paths'][] = [$p['startPosX'], $p['startPosY']]; - unset($paths[$j]); - } - else if ($i == $p['endId'] && (!$neutral || $p['neutral'])) - { - $data['paths'][] = [$p['endPosX'], $p['endPosY']]; - unset($paths[$j]); - } - } - - if (empty($data['paths'])) - unset($data['paths']); - - $somData['flightmaster'][] = $data; - } - - $this->map = array( - 'data' => array( - 'zone' => $mapFile, - 'zoom' => 1, - 'overlay' => true, - 'zoomable' => false, - 'parent' => 'mapper-generic' - ), - 'som' => $somData, - 'mapperData' => [$mapFile => new stdClass()] - ); - } - } - - protected function generateTitle() - { - if ($this->category) - { - if (isset($this->category[1])) - array_unshift($this->title, Lang::game('expansions', $this->category[1])); - - array_unshift($this->title, Lang::zone('cat', $this->category[0])); - } - } - - protected function generatePath() - { - foreach ($this->category as $c) - $this->path[] = $c; - } -} - - -?> diff --git a/prQueue b/prQueue index 4d2c645d..20b12827 100755 --- a/prQueue +++ b/prQueue @@ -1,6 +1,10 @@ +#!/usr/bin/env php 'char', + Type::GUILD => 'guild', + Type::ARENA_TEAM => 'arena team' + }; - 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_CHAR, $realmId, $type, $typeId); - trigger_error('prQueue - unknown '.$what.' guid #'.$typeId.' on realm #'.$realmId.' to sync into profiler.', E_USER_WARNING); - CLI::write('unknown '.$what.' guid #'.$typeId.' on realm #'.$realmId.' to sync into profiler.', CLI::LOG_WARN); + $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); }; -// if (CFG_PROFILER_QUEUE) - wont work because it is not redefined if changed in config -while (DB::Aowow()->selectCell('SELECT value FROM ?_config WHERE `key` = "profiler_queue"')) +while (Cfg::get('PROFILER_ENABLE', true)) { - if (($tDiff = (microtime(true) - $tCycle)) < (CFG_PROFILER_QUEUE_DELAY / 1000)) + $delay = Cfg::get('PROFILER_QUEUE_DELAY') / 1000; + if (($tDiff = (microtime(true) - $tCycle)) < $delay) { - $wait = (CFG_PROFILER_QUEUE_DELAY / 1000) - $tDiff; + $wait = $delay - $tDiff; CLI::write('sleeping '.Lang::nf($wait, 2).'s..'); usleep($wait * 1000 * 1000); } - $tCycle = microtime(true); - - $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,47 +76,64 @@ while (DB::Aowow()->selectCell('SELECT value FROM ?_config WHERE `key` = "profil 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 requestTime = ?d AND realm = ?d AND type = ?d AND typeId = ?d', PR_QUEUE_STATUS_WORKING, time(), $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'])) + case Type::PROFILE: + switch ($result = Profiler::getCharFromRealm($row['realm'], $row['realmGUID'])) { - $error(TYPE_PROFILE, $row['realmGUID'], $row['realm']); - 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'])) + case Type::GUILD: + switch ($result = Profiler::getGuildFromRealm($row['realm'], $row['realmGUID'])) { - $error(TYPE_ARENA_GUILD, $row['realmGUID'], $row['realm']); - 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'])) + case Type::ARENA_TEAM: + switch ($result = Profiler::getArenaTeamFromRealm($row['realm'], $row['realmGUID'])) { - $error(TYPE_ARENA_TEAM, $row['realmGUID'], $row['realm']); - 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); - CLI::write('unknown type #'.$row['type'].' to sync into profiler. Removing from queue...', CLI::LOG_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/db_structure.sql b/setup/db_structure.sql deleted file mode 100644 index d6202737..00000000 --- a/setup/db_structure.sql +++ /dev/null @@ -1,3071 +0,0 @@ --- MySQL dump 10.16 Distrib 10.2.10-MariaDB, for debian-linux-gnu (x86_64) --- --- Host: localhost Database: sarjuuk_aowow --- ------------------------------------------------------ --- Server version 10.2.10-MariaDB-10.2.10+maria~xenial-log - -/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */; -/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */; -/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */; -/*!40101 SET NAMES utf8 */; -/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */; -/*!40103 SET TIME_ZONE='+00:00' */; -/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */; -/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */; -/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */; -/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */; - --- --- Table structure for table `aowow_account` --- - -DROP TABLE IF EXISTS `aowow_account`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!40101 SET character_set_client = utf8 */; -CREATE TABLE `aowow_account` ( - `id` int(10) unsigned NOT NULL AUTO_INCREMENT, - `extId` int(10) unsigned NOT NULL COMMENT 'external user id', - `user` varchar(64) NOT NULL COMMENT 'login', - `passHash` varchar(128) NOT NULL, - `displayName` varchar(64) NOT NULL COMMENT 'nickname', - `email` varchar(64) NOT NULL, - `joinDate` int(10) unsigned NOT NULL COMMENT 'unixtime', - `allowExpire` tinyint(1) unsigned NOT NULL, - `dailyVotes` smallint(5) unsigned NOT NULL DEFAULT 0, - `consecutiveVisits` smallint(5) unsigned NOT NULL DEFAULT 0, - `curIP` varchar(45) NOT NULL, - `prevIP` varchar(45) NOT NULL, - `curLogin` int(15) unsigned NOT NULL COMMENT 'unixtime', - `prevLogin` int(15) unsigned NOT NULL, - `locale` tinyint(4) unsigned NOT NULL DEFAULT 0 COMMENT '0,2,3,6,8', - `userGroups` smallint(5) unsigned NOT NULL DEFAULT 0 COMMENT 'bitmask', - `avatar` varchar(50) NOT NULL DEFAULT '' COMMENT 'icon-string for internal or id for upload', - `title` varchar(50) NOT NULL DEFAULT '' COMMENT 'user can obtain custom titles', - `description` text NOT NULL COMMENT 'markdown formated', - `excludeGroups` smallint(5) unsigned NOT NULL DEFAULT 1 COMMENT 'profiler - completion exclude bitmask', - `userPerms` tinyint(4) unsigned NOT NULL DEFAULT 0 COMMENT 'bool isAdmin', - `status` tinyint(4) unsigned NOT NULL DEFAULT 0 COMMENT 'flag, see defines', - `statusTimer` int(10) unsigned NOT NULL DEFAULT 0, - `token` varchar(40) NOT NULL COMMENT 'creation & recovery', - PRIMARY KEY (`id`), - UNIQUE KEY `user` (`user`) -) ENGINE=InnoDB AUTO_INCREMENT=17 DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Table structure for table `aowow_account_banned` --- - -DROP TABLE IF EXISTS `aowow_account_banned`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!40101 SET character_set_client = utf8 */; -CREATE TABLE `aowow_account_banned` ( - `id` int(16) unsigned NOT NULL, - `userId` int(10) unsigned NOT NULL COMMENT 'affected accountId', - `staffId` int(10) unsigned NOT NULL COMMENT 'executive accountId', - `typeMask` tinyint(4) unsigned NOT NULL COMMENT 'ACC_BAN_*', - `start` int(10) unsigned NOT NULL COMMENT 'unixtime', - `end` int(10) unsigned NOT NULL COMMENT 'automatic unban @ unixtime', - `reason` varchar(255) NOT NULL, - PRIMARY KEY (`id`), - KEY `FK_acc_banned` (`userId`), - CONSTRAINT `FK_acc_banned` FOREIGN KEY (`userId`) REFERENCES `aowow_account` (`id`) ON DELETE CASCADE ON UPDATE CASCADE -) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Table structure for table `aowow_account_bannedips` --- - -DROP TABLE IF EXISTS `aowow_account_bannedips`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!40101 SET character_set_client = utf8 */; -CREATE TABLE `aowow_account_bannedips` ( - `ip` varchar(45) NOT NULL, - `type` tinyint(4) NOT NULL COMMENT '0: onSignin; 1:onSignup', - `count` smallint(6) NOT NULL COMMENT 'nFails', - `unbanDate` int(11) NOT NULL COMMENT 'automatic remove @ unixtime', - PRIMARY KEY (`ip`,`type`) -) ENGINE=MyISAM DEFAULT CHARSET=utf8; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Table structure for table `aowow_account_cookies` --- - -DROP TABLE IF EXISTS `aowow_account_cookies`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!40101 SET character_set_client = utf8 */; -CREATE TABLE `aowow_account_cookies` ( - `userId` int(10) unsigned NOT NULL, - `name` varchar(127) NOT NULL, - `data` text NOT NULL, - PRIMARY KEY (`userId`), - UNIQUE KEY `userId_name` (`userId`,`name`), - CONSTRAINT `FK_acc_cookies` FOREIGN KEY (`userId`) REFERENCES `aowow_account` (`id`) ON DELETE CASCADE ON UPDATE CASCADE -) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Table structure for table `aowow_account_excludes` --- - -DROP TABLE IF EXISTS `aowow_account_excludes`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!40101 SET character_set_client = utf8 */; -CREATE TABLE `aowow_account_excludes` ( - `userId` int(11) unsigned NOT NULL, - `type` smallint(5) unsigned NOT NULL, - `typeId` mediumint(8) unsigned NOT NULL, - `mode` tinyint(2) unsigned NOT NULL COMMENT '1: exclude; 2: include', - UNIQUE KEY `userId_type_typeId` (`userId`,`type`,`typeId`), - KEY `userId` (`userId`), - CONSTRAINT `FK_acc_excludes` FOREIGN KEY (`userId`) REFERENCES `aowow_account` (`id`) ON DELETE CASCADE ON UPDATE CASCADE -) ENGINE=InnoDB DEFAULT CHARSET=utf8; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Table structure for table `aowow_account_profiles` --- - -DROP TABLE IF EXISTS `aowow_account_profiles`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!40101 SET character_set_client = utf8 */; -CREATE TABLE `aowow_account_profiles` ( - `accountId` int(10) unsigned NOT NULL, - `profileId` int(10) unsigned NOT NULL, - `extraFlags` int(10) unsigned NOT NULL, - UNIQUE KEY `accountId_profileId` (`accountId`,`profileId`), - KEY `accountId` (`accountId`), - KEY `profileId` (`profileId`), - CONSTRAINT `FK_account_id` FOREIGN KEY (`accountId`) REFERENCES `aowow_account` (`id`) ON DELETE CASCADE ON UPDATE CASCADE, - CONSTRAINT `FK_profile_id` FOREIGN KEY (`profileId`) REFERENCES `aowow_profiler_profiles` (`id`) ON DELETE CASCADE ON UPDATE CASCADE -) ENGINE=InnoDB DEFAULT CHARSET=utf8; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Table structure for table `aowow_account_reputation` --- - -DROP TABLE IF EXISTS `aowow_account_reputation`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!40101 SET character_set_client = utf8 */; -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, - `sourceA` int(11) unsigned NOT NULL DEFAULT 0 COMMENT 'e.g. upvoting user', - `sourceB` int(11) unsigned NOT NULL DEFAULT 0 COMMENT 'e.g. upvoted commentId', - `date` int(10) unsigned NOT NULL DEFAULT 0, - UNIQUE KEY `userId_action_source` (`userId`,`action`,`sourceA`,`sourceB`), - KEY `userId` (`userId`), - CONSTRAINT `FK_acc_rep` FOREIGN KEY (`userId`) REFERENCES `aowow_account` (`id`) ON DELETE CASCADE ON UPDATE CASCADE -) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT COMMENT='reputation log'; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Table structure for table `aowow_account_weightscale_data` --- - -DROP TABLE IF EXISTS `aowow_account_weightscale_data`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!40101 SET character_set_client = utf8 */; -CREATE TABLE `aowow_account_weightscale_data` ( - `id` int(32) NOT NULL, - `field` varchar(15) NOT NULL, - `val` smallint(6) unsigned NOT NULL, - KEY `id` (`id`), - CONSTRAINT `FK_acc_weightscales` FOREIGN KEY (`id`) REFERENCES `aowow_account_weightscales` (`id`) ON DELETE CASCADE ON UPDATE CASCADE -) ENGINE=InnoDB DEFAULT CHARSET=utf8; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Table structure for table `aowow_account_weightscales` --- - -DROP TABLE IF EXISTS `aowow_account_weightscales`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!40101 SET character_set_client = utf8 */; -CREATE TABLE `aowow_account_weightscales` ( - `id` int(32) NOT NULL AUTO_INCREMENT, - `userId` int(10) unsigned NOT NULL, - `name` varchar(32) NOT NULL, - `class` tinyint(3) unsigned NOT NULL DEFAULT 0, - `icon` varchar(48) NOT NULL DEFAULT '', - PRIMARY KEY (`id`,`userId`), - KEY `FK_acc_weights` (`userId`), - CONSTRAINT `FK_acc_weights` FOREIGN KEY (`userId`) REFERENCES `aowow_account` (`id`) ON DELETE CASCADE ON UPDATE CASCADE -) ENGINE=InnoDB AUTO_INCREMENT=33 DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Table structure for table `aowow_achievement` --- - -DROP TABLE IF EXISTS `aowow_achievement`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!40101 SET character_set_client = utf8 */; -CREATE TABLE `aowow_achievement` ( - `id` smallint(5) unsigned NOT NULL, - `faction` tinyint(3) unsigned NOT NULL, - `map` smallint(6) NOT NULL, - `chainId` tinyint(3) unsigned NOT NULL, - `chainPos` tinyint(3) unsigned NOT NULL, - `category` smallint(6) unsigned NOT NULL, - `parentCat` smallint(6) NOT NULL, - `points` tinyint(3) unsigned NOT NULL, - `orderInGroup` tinyint(3) unsigned NOT NULL, - `iconId` smallint(5) unsigned NOT NULL DEFAULT 0, - `iconIdBak` smallint(5) unsigned NOT NULL DEFAULT 0, - `flags` smallint(5) unsigned NOT NULL, - `reqCriteriaCount` tinyint(3) unsigned NOT NULL, - `refAchievement` smallint(5) unsigned NOT NULL, - `itemExtra` mediumint(8) unsigned NOT NULL, - `cuFlags` int(10) unsigned NOT NULL COMMENT 'see defines.php for flags', - `name_loc0` varchar(78) NOT NULL, - `name_loc2` varchar(79) NOT NULL, - `name_loc3` varchar(86) NOT NULL, - `name_loc6` varchar(78) NOT NULL, - `name_loc8` varchar(76) NOT NULL, - `description_loc0` text NOT NULL, - `description_loc2` text NOT NULL, - `description_loc3` text NOT NULL, - `description_loc6` text NOT NULL, - `description_loc8` text NOT NULL, - `reward_loc0` varchar(74) NOT NULL, - `reward_loc2` varchar(88) NOT NULL, - `reward_loc3` varchar(92) NOT NULL, - `reward_loc6` varchar(83) NOT NULL, - `reward_loc8` varchar(95) NOT NULL, - PRIMARY KEY (`id`), - KEY `iconId` (`iconId`) -) ENGINE=MyISAM DEFAULT CHARSET=utf8; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Table structure for table `aowow_achievementcategory` --- - -DROP TABLE IF EXISTS `aowow_achievementcategory`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!40101 SET character_set_client = utf8 */; -CREATE TABLE `aowow_achievementcategory` ( - `id` int(11) unsigned NOT NULL, - `parentCategory` mediumint(9) NOT NULL, - `name_loc0` varchar(255) NOT NULL, - `name_loc2` varchar(255) NOT NULL, - `name_loc3` varchar(255) NOT NULL, - `name_loc6` varchar(255) NOT NULL, - `name_loc8` varchar(255) NOT NULL, - PRIMARY KEY (`id`), - KEY `idx_achievement` (`parentCategory`) -) ENGINE=MyISAM DEFAULT CHARSET=utf8; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Table structure for table `aowow_achievementcriteria` --- - -DROP TABLE IF EXISTS `aowow_achievementcriteria`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!40101 SET character_set_client = utf8 */; -CREATE TABLE `aowow_achievementcriteria` ( - `id` smallint(6) unsigned NOT NULL, - `refAchievementId` smallint(6) unsigned NOT NULL, - `type` tinyint(3) unsigned NOT NULL, - `value1` int(10) unsigned NOT NULL, - `value2` int(10) unsigned NOT NULL, - `value3` int(10) unsigned NOT NULL, - `value4` int(10) unsigned NOT NULL, - `value5` int(10) unsigned NOT NULL, - `value6` int(10) unsigned NOT NULL, - `name_loc0` varchar(92) NOT NULL, - `name_loc2` varchar(104) NOT NULL, - `name_loc3` varchar(128) NOT NULL, - `name_loc6` varchar(119) NOT NULL, - `name_loc8` varchar(118) NOT NULL, - `completionFlags` tinyint(3) unsigned NOT NULL, - `groupFlags` tinyint(3) unsigned NOT NULL, - `timeLimit` smallint(5) unsigned NOT NULL, - `order` smallint(5) unsigned NOT NULL, - PRIMARY KEY (`id`), - KEY `idx_achievement` (`refAchievementId`) -) ENGINE=MyISAM DEFAULT CHARSET=utf8; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Table structure for table `aowow_announcements` --- - -DROP TABLE IF EXISTS `aowow_announcements`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!40101 SET character_set_client = utf8 */; -CREATE TABLE `aowow_announcements` ( - `id` int(16) NOT NULL AUTO_INCREMENT COMMENT 'iirc negative Ids cant be deleted', - `page` varchar(256) NOT NULL, - `name` varchar(256) NOT NULL, - `groupMask` smallint(5) unsigned NOT NULL, - `style` varchar(256) NOT NULL, - `mode` tinyint(4) unsigned NOT NULL COMMENT '0:pageTop; 1:contentTop', - `status` tinyint(4) unsigned NOT NULL COMMENT '0:disabled; 1:enabled; 2:deleted', - `text_loc0` text NOT NULL, - `text_loc2` text NOT NULL, - `text_loc3` text NOT NULL, - `text_loc6` text NOT NULL, - `text_loc8` text NOT NULL, - PRIMARY KEY (`id`) -) ENGINE=MyISAM AUTO_INCREMENT=5 DEFAULT CHARSET=utf8; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Table structure for table `aowow_articles` --- - -DROP TABLE IF EXISTS `aowow_articles`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!40101 SET character_set_client = utf8 */; -CREATE TABLE `aowow_articles` ( - `type` smallint(5) DEFAULT NULL, - `typeId` mediumint(9) DEFAULT NULL, - `locale` tinyint(4) unsigned NOT NULL, - `url` varchar(50) DEFAULT NULL, - `editAccess` smallint(5) unsigned NOT NULL DEFAULT 2, - `article` text DEFAULT NULL COMMENT 'Markdown formated', - `quickInfo` text DEFAULT NULL COMMENT 'Markdown formated', - UNIQUE KEY `type` (`type`,`typeId`,`locale`), - UNIQUE KEY `locale_url` (`locale`,`url`) -) ENGINE=MyISAM DEFAULT CHARSET=utf8; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Table structure for table `aowow_classes` --- - -DROP TABLE IF EXISTS `aowow_classes`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!40101 SET character_set_client = utf8 */; -CREATE TABLE `aowow_classes` ( - `id` int(16) NOT NULL, - `fileString` varchar(128) NOT NULL, - `name_loc0` varchar(128) NOT NULL, - `name_loc2` varchar(128) NOT NULL, - `name_loc3` varchar(128) NOT NULL, - `name_loc6` varchar(128) NOT NULL, - `name_loc8` varchar(128) NOT NULL, - `powerType` tinyint(4) NOT NULL, - `raceMask` int(16) NOT NULL, - `roles` int(16) NOT NULL, - `skills` varchar(32) NOT NULL, - `flags` mediumint(16) NOT NULL, - `cuFlags` int(10) unsigned NOT NULL, - `weaponTypeMask` int(32) NOT NULL, - `armorTypeMask` int(32) NOT NULL, - `expansion` tinyint(2) NOT NULL, - PRIMARY KEY (`id`) -) ENGINE=MyISAM DEFAULT CHARSET=utf8; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Table structure for table `aowow_comments` --- - -DROP TABLE IF EXISTS `aowow_comments`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!40101 SET character_set_client = utf8 */; -CREATE TABLE `aowow_comments` ( - `id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT 'Comment ID', - `type` smallint(5) unsigned NOT NULL COMMENT 'Type of Page', - `typeId` mediumint(9) NOT NULL COMMENT 'ID Of Page', - `userId` int(10) unsigned DEFAULT NULL COMMENT 'User ID', - `roles` smallint(5) unsigned NOT NULL, - `body` text NOT NULL COMMENT 'Comment text', - `date` int(11) NOT NULL COMMENT 'Comment timestap', - `flags` smallint(6) NOT NULL DEFAULT 0 COMMENT 'deleted, outofdate, sticky', - `replyTo` int(10) unsigned NOT NULL DEFAULT 0 COMMENT 'Reply To, comment ID', - `editUserId` int(10) unsigned NOT NULL DEFAULT 0 COMMENT 'Last Edit User ID', - `editDate` int(10) unsigned NOT NULL DEFAULT 0 COMMENT 'Last Edit Time', - `editCount` tinyint(3) unsigned NOT NULL DEFAULT 0 COMMENT 'Count Of Edits', - `deleteUserId` int(10) unsigned NOT NULL DEFAULT 0, - `deleteDate` int(10) unsigned NOT NULL DEFAULT 0, - `responseUserId` int(10) unsigned NOT NULL DEFAULT 0, - `responseBody` text DEFAULT NULL, - `responseRoles` smallint(5) unsigned NOT NULL DEFAULT 0, - PRIMARY KEY (`id`), - KEY `type_typeId` (`type`,`typeId`), - KEY `FK_acc_co` (`userId`), - CONSTRAINT `FK_acc_co` FOREIGN KEY (`userId`) REFERENCES `aowow_account` (`id`) ON DELETE SET NULL ON UPDATE CASCADE -) ENGINE=InnoDB DEFAULT CHARSET=utf8; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Table structure for table `aowow_comments_rates` --- - -DROP TABLE IF EXISTS `aowow_comments_rates`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!40101 SET character_set_client = utf8 */; -CREATE TABLE `aowow_comments_rates` ( - `commentId` int(10) unsigned NOT NULL DEFAULT 0 COMMENT 'Comment ID', - `userId` int(10) unsigned NOT NULL DEFAULT 0 COMMENT 'User ID', - `value` tinyint(4) NOT NULL DEFAULT 0 COMMENT 'Rating Set', - PRIMARY KEY (`commentId`,`userId`), - UNIQUE KEY `commentId_userId` (`commentId`,`userId`), - KEY `FK_acc_co_rate_user` (`userId`), - CONSTRAINT `FK_acc_co_rate` FOREIGN KEY (`commentId`) REFERENCES `aowow_comments` (`id`) ON DELETE CASCADE ON UPDATE CASCADE, - CONSTRAINT `FK_acc_co_rate_user` FOREIGN KEY (`userId`) REFERENCES `aowow_account` (`id`) ON DELETE NO ACTION ON UPDATE CASCADE -) ENGINE=InnoDB DEFAULT CHARSET=utf8; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Table structure for table `aowow_config` --- - -DROP TABLE IF EXISTS `aowow_config`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!40101 SET character_set_client = utf8 */; -CREATE TABLE `aowow_config` ( - `key` varchar(25) NOT NULL, - `value` varchar(255) NOT NULL, - `cat` tinyint(3) unsigned NOT NULL DEFAULT 5, - `flags` tinyint(3) unsigned NOT NULL DEFAULT 0, - `comment` varchar(255) NOT NULL, - PRIMARY KEY (`key`) -) ENGINE=MyISAM DEFAULT CHARSET=utf8; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Table structure for table `aowow_creature` --- - -DROP TABLE IF EXISTS `aowow_creature`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!40101 SET character_set_client = utf8 */; -CREATE TABLE `aowow_creature` ( - `id` mediumint(8) unsigned NOT NULL DEFAULT 0, - `cuFlags` int(10) unsigned NOT NULL DEFAULT 0, - `difficultyEntry1` mediumint(8) unsigned NOT NULL DEFAULT 0, - `difficultyEntry2` mediumint(8) unsigned NOT NULL DEFAULT 0, - `difficultyEntry3` mediumint(8) unsigned NOT NULL DEFAULT 0, - `KillCredit1` int(10) unsigned NOT NULL DEFAULT 0, - `KillCredit2` int(10) unsigned NOT NULL DEFAULT 0, - `displayId1` mediumint(8) unsigned NOT NULL DEFAULT 0, - `displayId2` mediumint(8) unsigned NOT NULL DEFAULT 0, - `displayId3` mediumint(8) unsigned NOT NULL DEFAULT 0, - `displayId4` mediumint(8) unsigned NOT NULL DEFAULT 0, - `textureString` varchar(50) DEFAULT NULL, - `modelId` mediumint(8) NOT NULL, - `humanoid` tinyint(1) unsigned NOT NULL DEFAULT 0, - `iconString` varchar(50) DEFAULT NULL COMMENT 'first texture of first model for search (up to 11 other skins omitted..)', - `name_loc0` varchar(100) NOT NULL DEFAULT '0', - `name_loc2` varchar(100) DEFAULT NULL, - `name_loc3` varchar(100) DEFAULT NULL, - `name_loc6` varchar(100) DEFAULT NULL, - `name_loc8` varchar(100) DEFAULT NULL, - `subname_loc0` varchar(100) DEFAULT NULL, - `subname_loc2` varchar(100) DEFAULT NULL, - `subname_loc3` varchar(100) DEFAULT NULL, - `subname_loc6` varchar(100) DEFAULT NULL, - `subname_loc8` varchar(100) DEFAULT NULL, - `minLevel` tinyint(3) unsigned NOT NULL DEFAULT 1, - `maxLevel` tinyint(3) unsigned NOT NULL DEFAULT 1, - `exp` smallint(6) NOT NULL DEFAULT 0, - `faction` smallint(5) unsigned NOT NULL DEFAULT 0, - `npcflag` int(10) unsigned NOT NULL DEFAULT 0, - `rank` tinyint(3) unsigned NOT NULL DEFAULT 0, - `dmgSchool` tinyint(4) NOT NULL DEFAULT 0, - `dmgMultiplier` float NOT NULL DEFAULT 1, - `atkSpeed` int(10) unsigned NOT NULL DEFAULT 0, - `rngAtkSpeed` int(10) unsigned NOT NULL DEFAULT 0, - `mleVariance` float NOT NULL DEFAULT 1, - `rngVariance` float NOT NULL DEFAULT 1, - `unitClass` tinyint(3) unsigned NOT NULL DEFAULT 0, - `unitFlags` int(10) unsigned NOT NULL DEFAULT 0, - `unitFlags2` int(10) unsigned NOT NULL DEFAULT 0, - `dynamicFlags` int(10) unsigned NOT NULL DEFAULT 0, - `family` tinyint(4) NOT NULL DEFAULT 0, - `trainerType` tinyint(4) NOT NULL DEFAULT 0, - `trainerSpell` mediumint(8) unsigned NOT NULL DEFAULT 0, - `trainerClass` tinyint(3) unsigned NOT NULL DEFAULT 0, - `trainerRace` tinyint(3) unsigned NOT NULL DEFAULT 0, - `dmgMin` float unsigned NOT NULL DEFAULT 0, - `dmgMax` float unsigned NOT NULL DEFAULT 0, - `mleAtkPwrMin` smallint(5) unsigned NOT NULL DEFAULT 0, - `mleAtkPwrMax` smallint(5) unsigned NOT NULL DEFAULT 0, - `rngAtkPwrMin` smallint(5) unsigned NOT NULL DEFAULT 0, - `rngAtkPwrMax` smallint(5) unsigned NOT NULL DEFAULT 0, - `type` tinyint(3) unsigned NOT NULL DEFAULT 0, - `typeFlags` int(10) unsigned NOT NULL DEFAULT 0, - `lootId` mediumint(8) unsigned NOT NULL DEFAULT 0, - `pickpocketLootId` mediumint(8) unsigned NOT NULL DEFAULT 0, - `skinLootId` mediumint(8) unsigned NOT NULL DEFAULT 0, - `spell1` mediumint(8) unsigned NOT NULL DEFAULT 0, - `spell2` mediumint(8) unsigned NOT NULL DEFAULT 0, - `spell3` mediumint(8) unsigned NOT NULL DEFAULT 0, - `spell4` mediumint(8) unsigned NOT NULL DEFAULT 0, - `spell5` mediumint(8) unsigned NOT NULL DEFAULT 0, - `spell6` mediumint(8) unsigned NOT NULL DEFAULT 0, - `spell7` mediumint(8) unsigned NOT NULL DEFAULT 0, - `spell8` mediumint(8) unsigned NOT NULL DEFAULT 0, - `petSpellDataId` mediumint(8) unsigned NOT NULL DEFAULT 0, - `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, - `manaMax` int(10) unsigned NOT NULL DEFAULT 1, - `armorMin` mediumint(8) unsigned NOT NULL DEFAULT 1, - `armorMax` mediumint(8) unsigned NOT NULL DEFAULT 1, - `racialLeader` tinyint(3) unsigned NOT NULL DEFAULT 0, - `mechanicImmuneMask` int(10) unsigned NOT NULL DEFAULT 0, - `flagsExtra` int(10) unsigned NOT NULL DEFAULT 0, - `scriptName` varchar(50) NOT NULL DEFAULT '', - PRIMARY KEY (`id`), - KEY `idx_name` (`name_loc0`), - KEY `difficultyEntry1` (`difficultyEntry1`), - KEY `difficultyEntry2` (`difficultyEntry2`), - KEY `difficultyEntry3` (`difficultyEntry3`) -) ENGINE=MyISAM DEFAULT CHARSET=utf8; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Table structure for table `aowow_creature_sounds` --- - -DROP TABLE IF EXISTS `aowow_creature_sounds`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!40101 SET character_set_client = utf8 */; -CREATE TABLE `aowow_creature_sounds` ( - `id` smallint(5) unsigned NOT NULL COMMENT 'CreatureDisplayInfo.dbc/id', - `greeting` smallint(5) unsigned NOT NULL, - `farewell` smallint(5) unsigned NOT NULL, - `angry` smallint(5) unsigned NOT NULL, - `exertion` smallint(5) unsigned NOT NULL, - `exertioncritical` smallint(5) unsigned NOT NULL, - `injury` smallint(5) unsigned NOT NULL, - `injurycritical` smallint(5) unsigned NOT NULL, - `death` smallint(5) unsigned NOT NULL, - `stun` smallint(5) unsigned NOT NULL, - `stand` smallint(5) unsigned NOT NULL, - `footstep` smallint(5) unsigned NOT NULL, - `aggro` smallint(5) unsigned NOT NULL, - `wingflap` smallint(5) unsigned NOT NULL, - `wingglide` smallint(5) unsigned NOT NULL, - `alert` smallint(5) unsigned NOT NULL, - `fidget` smallint(5) unsigned NOT NULL, - `customattack` smallint(5) unsigned NOT NULL, - `loop` smallint(5) unsigned NOT NULL, - `jumpstart` smallint(5) unsigned NOT NULL, - `jumpend` smallint(5) unsigned NOT NULL, - `petattack` smallint(5) unsigned NOT NULL, - `petorder` smallint(5) unsigned NOT NULL, - `petdismiss` smallint(5) unsigned NOT NULL, - `birth` smallint(5) unsigned NOT NULL, - `spellcast` smallint(5) unsigned NOT NULL, - `submerge` smallint(5) unsigned NOT NULL, - `submerged` smallint(5) unsigned NOT NULL, - `transform` smallint(5) unsigned NOT NULL, - `transformanimated` smallint(5) unsigned NOT NULL, - PRIMARY KEY (`id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='!ATTENTION!\r\nthe primary key of this table is NOT a creatureId, but displayId\r\n\r\ncolumn names from LANG.sound_activities'; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Table structure for table `aowow_creature_waypoints` --- - -DROP TABLE IF EXISTS `aowow_creature_waypoints`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!40101 SET character_set_client = utf8 */; -CREATE TABLE `aowow_creature_waypoints` ( - `creatureOrPath` int(11) NOT NULL, - `point` tinyint(3) unsigned NOT NULL, - `areaId` smallint(5) unsigned NOT NULL, - `floor` tinyint(3) unsigned NOT NULL, - `posX` float unsigned NOT NULL, - `posY` float unsigned NOT NULL, - `wait` mediumint(8) unsigned NOT NULL, - PRIMARY KEY (`creatureOrPath`,`point`,`areaId`,`floor`) -) ENGINE=MyISAM DEFAULT CHARSET=utf8; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Table structure for table `aowow_currencies` --- - -DROP TABLE IF EXISTS `aowow_currencies`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!40101 SET character_set_client = utf8 */; -CREATE TABLE `aowow_currencies` ( - `id` int(16) NOT NULL, - `category` mediumint(8) NOT NULL, - `cuFlags` int(10) unsigned NOT NULL, - `iconId` smallint(5) unsigned NOT NULL DEFAULT 0, - `itemId` int(16) NOT NULL, - `cap` mediumint(8) unsigned NOT NULL, - `name_loc0` varchar(64) NOT NULL, - `name_loc2` varchar(64) NOT NULL, - `name_loc3` varchar(64) NOT NULL, - `name_loc6` varchar(64) NOT NULL, - `name_loc8` varchar(64) NOT NULL, - `description_loc0` varchar(256) NOT NULL, - `description_loc2` varchar(256) NOT NULL, - `description_loc3` varchar(256) NOT NULL, - `description_loc6` varchar(256) NOT NULL, - `description_loc8` varchar(256) NOT NULL, - PRIMARY KEY (`id`), - KEY `iconId` (`iconId`) -) ENGINE=MyISAM DEFAULT CHARSET=utf8; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Table structure for table `aowow_dbversion` --- - -DROP TABLE IF EXISTS `aowow_dbversion`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!40101 SET character_set_client = utf8 */; -CREATE TABLE `aowow_dbversion` ( - `date` int(10) unsigned NOT NULL DEFAULT 0, - `part` tinyint(3) unsigned NOT NULL DEFAULT 0, - `sql` text DEFAULT NULL, - `build` text DEFAULT NULL -) ENGINE=MyISAM DEFAULT CHARSET=utf8; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Table structure for table `aowow_emotes` --- - -DROP TABLE IF EXISTS `aowow_emotes`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!40101 SET character_set_client = utf8 */; -CREATE TABLE `aowow_emotes` ( - `id` smallint(5) unsigned NOT NULL, - `cmd` varchar(15) NOT NULL, - `isAnimated` tinyint(1) unsigned NOT NULL, - `cuFlags` int(10) unsigned NOT NULL, - `target_loc0` varchar(65) DEFAULT NULL, - `target_loc2` varchar(70) DEFAULT NULL, - `target_loc3` varchar(95) DEFAULT NULL, - `target_loc6` varchar(90) DEFAULT NULL, - `target_loc8` varchar(70) DEFAULT NULL, - `noTarget_loc0` varchar(65) DEFAULT NULL, - `noTarget_loc2` varchar(110) DEFAULT NULL, - `noTarget_loc3` varchar(85) DEFAULT NULL, - `noTarget_loc6` varchar(75) DEFAULT NULL, - `noTarget_loc8` varchar(60) DEFAULT NULL, - `self_loc0` varchar(65) DEFAULT NULL, - `self_loc2` varchar(115) DEFAULT NULL, - `self_loc3` varchar(85) DEFAULT NULL, - `self_loc6` varchar(75) DEFAULT NULL, - `self_loc8` varchar(70) DEFAULT NULL, - PRIMARY KEY (`id`) -) ENGINE=MyISAM DEFAULT CHARSET=utf8; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Table structure for table `aowow_emotes_aliasses` --- - -DROP TABLE IF EXISTS `aowow_emotes_aliasses`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!40101 SET character_set_client = utf8 */; -CREATE TABLE `aowow_emotes_aliasses` ( - `id` smallint(6) unsigned NOT NULL, - `locales` smallint(6) unsigned NOT NULL, - `command` varchar(15) NOT NULL, - UNIQUE KEY `id_command` (`id`,`command`), - KEY `id` (`id`) -) ENGINE=MyISAM DEFAULT CHARSET=utf8; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Table structure for table `aowow_emotes_sounds` --- - -DROP TABLE IF EXISTS `aowow_emotes_sounds`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!40101 SET character_set_client = utf8 */; -CREATE TABLE `aowow_emotes_sounds` ( - `emoteId` smallint(5) unsigned NOT NULL, - `raceId` tinyint(3) unsigned NOT NULL, - `gender` tinyint(1) unsigned NOT NULL, - `soundId` smallint(5) unsigned NOT NULL, - UNIQUE KEY `emoteId_raceId_gender_soundId` (`emoteId`,`raceId`,`gender`,`soundId`), - KEY `emoteId` (`emoteId`), - KEY `raceId` (`raceId`), - KEY `soundId` (`soundId`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Table structure for table `aowow_errors` --- - -DROP TABLE IF EXISTS `aowow_errors`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!40101 SET character_set_client = utf8 */; -CREATE TABLE `aowow_errors` ( - `date` int(10) unsigned DEFAULT NULL, - `version` smallint(5) unsigned NOT NULL, - `phpError` smallint(5) unsigned NOT NULL, - `file` varchar(250) NOT NULL, - `line` smallint(5) unsigned NOT NULL, - `query` varchar(250) NOT NULL, - `userGroups` smallint(5) unsigned NOT NULL, - `message` text DEFAULT NULL, - PRIMARY KEY (`file`,`line`,`phpError`,`version`,`userGroups`) -) ENGINE=MyISAM DEFAULT CHARSET=utf8; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Table structure for table `aowow_events` --- - -DROP TABLE IF EXISTS `aowow_events`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!40101 SET character_set_client = utf8 */; -CREATE TABLE `aowow_events` ( - `id` tinyint(3) unsigned NOT NULL, - `holidayId` mediumint(8) unsigned NOT NULL DEFAULT 0, - `cuFlags` int(10) unsigned NOT NULL DEFAULT 0, - `startTime` bigint(20) NOT NULL, - `endTime` bigint(20) NOT NULL, - `occurence` bigint(20) unsigned NOT NULL, - `length` bigint(20) unsigned NOT NULL, - `requires` varchar(255) DEFAULT NULL, - `description` varchar(255) DEFAULT NULL, - PRIMARY KEY (`id`), - KEY `holidayId` (`holidayId`) -) ENGINE=MyISAM DEFAULT CHARSET=utf8; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Table structure for table `aowow_factions` --- - -DROP TABLE IF EXISTS `aowow_factions`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!40101 SET character_set_client = utf8 */; -CREATE TABLE `aowow_factions` ( - `id` smallint(5) unsigned NOT NULL, - `repIdx` smallint(5) unsigned NOT NULL, - `side` tinyint(1) unsigned NOT NULL, - `expansion` tinyint(1) unsigned NOT NULL, - `qmNpcIds` varchar(12) NOT NULL COMMENT 'space separated', - `templateIds` tinytext NOT NULL COMMENT 'space separated', - `cuFlags` int(10) unsigned NOT NULL, - `parentFactionId` smallint(5) unsigned NOT NULL, - `spilloverRateIn` float(8,2) NOT NULL, - `spilloverRateOut` float(8,2) NOT NULL, - `spilloverMaxRank` tinyint(3) unsigned NOT NULL, - `name_loc0` varchar(35) NOT NULL, - `name_loc2` varchar(49) NOT NULL, - `name_loc3` varchar(40) NOT NULL, - `name_loc6` varchar(50) NOT NULL, - `name_loc8` varchar(47) NOT NULL, - PRIMARY KEY (`id`) -) ENGINE=MyISAM DEFAULT CHARSET=utf8; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Table structure for table `aowow_factiontemplate` --- - -DROP TABLE IF EXISTS `aowow_factiontemplate`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!40101 SET character_set_client = utf8 */; -CREATE TABLE `aowow_factiontemplate` ( - `id` smallint(5) unsigned NOT NULL, - `factionId` smallint(5) unsigned NOT NULL, - `A` tinyint(4) NOT NULL COMMENT 'Aliance: -1 - hostile, 1 - friendly, 0 - neutral', - `H` tinyint(4) NOT NULL COMMENT 'Horde: -1 - hostile, 1 - friendly, 0 - neutral', - PRIMARY KEY (`id`) -) ENGINE=MyISAM DEFAULT CHARSET=utf8; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Table structure for table `aowow_glyphproperties` --- - -DROP TABLE IF EXISTS `aowow_glyphproperties`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!40101 SET character_set_client = utf8 */; -CREATE TABLE `aowow_glyphproperties` ( - `id` smallint(5) unsigned NOT NULL, - `spellId` mediumint(11) unsigned NOT NULL, - `typeFlags` tinyint(3) unsigned NOT NULL, - `iconId` smallint(5) unsigned NOT NULL DEFAULT 0, - `iconIdBak` smallint(5) unsigned NOT NULL DEFAULT 0, - PRIMARY KEY (`id`) -) ENGINE=MyISAM DEFAULT CHARSET=utf8; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Table structure for table `aowow_holidays` --- - -DROP TABLE IF EXISTS `aowow_holidays`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!40101 SET character_set_client = utf8 */; -CREATE TABLE `aowow_holidays` ( - `id` smallint(6) unsigned NOT NULL, - `bossCreature` mediumint(8) unsigned NOT NULL, - `achievementCatOrId` mediumint(9) NOT NULL, - `name_loc0` varchar(36) NOT NULL, - `name_loc2` varchar(42) NOT NULL, - `name_loc3` varchar(36) NOT NULL, - `name_loc6` varchar(49) NOT NULL, - `name_loc8` varchar(29) NOT NULL, - `description_loc0` text DEFAULT NULL, - `description_loc2` text DEFAULT NULL, - `description_loc3` text DEFAULT NULL, - `description_loc6` text DEFAULT NULL, - `description_loc8` text DEFAULT NULL, - `looping` tinyint(2) NOT NULL, - `scheduleType` tinyint(2) NOT NULL, - `textureString` varchar(30) NOT NULL, - `iconString` varchar(51) NOT NULL, - PRIMARY KEY (`id`) -) ENGINE=MyISAM DEFAULT CHARSET=utf8; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Table structure for table `aowow_home_featuredbox` --- - -DROP TABLE IF EXISTS `aowow_home_featuredbox`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!40101 SET character_set_client = utf8 */; -CREATE TABLE `aowow_home_featuredbox` ( - `id` smallint(5) unsigned NOT NULL, - `editorId` int(10) unsigned DEFAULT NULL, - `editDate` int(10) unsigned NOT NULL, - `startDate` int(10) unsigned NOT NULL DEFAULT 0, - `endDate` int(10) unsigned NOT NULL DEFAULT 0, - `extraWide` tinyint(3) unsigned NOT NULL DEFAULT 0, - `boxBG` varchar(150) DEFAULT NULL, - `altHomeLogo` varchar(150) DEFAULT NULL, - `altHeaderLogo` varchar(150) DEFAULT NULL, - `text_loc0` text NOT NULL, - `text_loc2` text NOT NULL, - `text_loc3` text NOT NULL, - `text_loc6` text NOT NULL, - `text_loc8` text NOT NULL, - PRIMARY KEY (`id`), - KEY `FK_acc_hFBox` (`editorId`), - CONSTRAINT `FK_acc_hFBox` FOREIGN KEY (`editorId`) REFERENCES `aowow_account` (`id`) ON DELETE SET NULL ON UPDATE CASCADE -) ENGINE=InnoDB DEFAULT CHARSET=utf8; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Table structure for table `aowow_home_featuredbox_overlay` --- - -DROP TABLE IF EXISTS `aowow_home_featuredbox_overlay`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!40101 SET character_set_client = utf8 */; -CREATE TABLE `aowow_home_featuredbox_overlay` ( - `featureId` smallint(5) unsigned NOT NULL, - `left` smallint(5) unsigned NOT NULL, - `width` smallint(5) unsigned NOT NULL, - `url` varchar(150) NOT NULL, - `title_loc0` varchar(100) NOT NULL DEFAULT '', - `title_loc2` varchar(100) NOT NULL DEFAULT '', - `title_loc3` varchar(100) NOT NULL DEFAULT '', - `title_loc6` varchar(100) NOT NULL DEFAULT '', - `title_loc8` varchar(100) NOT NULL DEFAULT '', - KEY `FK_home_featurebox` (`featureId`), - CONSTRAINT `FK_home_featurebox` FOREIGN KEY (`featureId`) REFERENCES `aowow_home_featuredbox` (`id`) ON DELETE CASCADE ON UPDATE CASCADE -) ENGINE=InnoDB DEFAULT CHARSET=utf8; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Table structure for table `aowow_home_oneliner` --- - -DROP TABLE IF EXISTS `aowow_home_oneliner`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!40101 SET character_set_client = utf8 */; -CREATE TABLE `aowow_home_oneliner` ( - `id` smallint(5) unsigned NOT NULL, - `editorId` int(10) unsigned DEFAULT NULL, - `editDate` int(10) unsigned NOT NULL, - `active` tinyint(1) unsigned NOT NULL, - `text_loc0` varchar(200) NOT NULL, - `text_loc2` varchar(200) NOT NULL, - `text_loc3` varchar(200) NOT NULL, - `text_loc6` varchar(200) NOT NULL, - `text_loc8` varchar(200) NOT NULL, - PRIMARY KEY (`id`), - KEY `FK_acc_hOneliner` (`editorId`), - CONSTRAINT `FK_acc_hOneliner` FOREIGN KEY (`editorId`) REFERENCES `aowow_account` (`id`) ON DELETE SET NULL ON UPDATE CASCADE -) ENGINE=InnoDB DEFAULT CHARSET=utf8; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Table structure for table `aowow_home_titles` --- - -DROP TABLE IF EXISTS `aowow_home_titles`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!40101 SET character_set_client = utf8 */; -CREATE TABLE `aowow_home_titles` ( - `id` smallint(5) unsigned NOT NULL, - `editorId` int(10) unsigned DEFAULT NULL, - `editDate` int(10) unsigned NOT NULL, - `active` tinyint(1) unsigned NOT NULL, - `title_loc0` varchar(100) NOT NULL, - `title_loc2` varchar(100) NOT NULL, - `title_loc3` varchar(100) NOT NULL, - `title_loc6` varchar(100) NOT NULL, - `title_loc8` varchar(100) NOT NULL, - PRIMARY KEY (`id`), - KEY `FK_acc_hTitles` (`editorId`), - CONSTRAINT `FK_acc_hTitles` FOREIGN KEY (`editorId`) REFERENCES `aowow_account` (`id`) ON DELETE SET NULL ON UPDATE CASCADE -) ENGINE=InnoDB DEFAULT CHARSET=utf8; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Table structure for table `aowow_icons` --- - -DROP TABLE IF EXISTS `aowow_icons`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!40101 SET character_set_client = utf8 */; -CREATE TABLE `aowow_icons` ( - `id` smallint(5) unsigned NOT NULL AUTO_INCREMENT, - `cuFlags` int(11) unsigned NOT NULL DEFAULT 0, - `name` varchar(55) NOT NULL DEFAULT '', - PRIMARY KEY (`id`), - KEY `name` (`name`) -) ENGINE=InnoDB AUTO_INCREMENT=5856 DEFAULT CHARSET=utf8; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Table structure for table `aowow_item_stats` --- - -DROP TABLE IF EXISTS `aowow_item_stats`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!40101 SET character_set_client = utf8 */; -CREATE TABLE `aowow_item_stats` ( - `type` smallint(5) unsigned NOT NULL, - `typeId` mediumint(8) unsigned NOT NULL, - `nsockets` tinyint(3) unsigned NOT NULL, - `dmgmin1` smallint(5) unsigned NOT NULL, - `dmgmax1` smallint(5) unsigned NOT NULL, - `speed` float(8,2) NOT NULL, - `dps` float(8,2) NOT NULL, - `mledmgmin` smallint(5) unsigned NOT NULL, - `mledmgmax` smallint(5) unsigned NOT NULL, - `mlespeed` float(8,2) NOT NULL, - `mledps` float(8,2) NOT NULL, - `rgddmgmin` smallint(5) unsigned NOT NULL, - `rgddmgmax` smallint(5) unsigned NOT NULL, - `rgdspeed` float(8,2) NOT NULL, - `rgddps` float(8,2) NOT NULL, - `dmg` float(8,2) NOT NULL, - `damagetype` tinyint(4) NOT NULL, - `mana` smallint(6) NOT NULL, - `health` smallint(6) NOT NULL, - `agi` smallint(6) NOT NULL, - `str` smallint(6) NOT NULL, - `int` smallint(6) NOT NULL, - `spi` smallint(6) NOT NULL, - `sta` smallint(6) NOT NULL, - `energy` smallint(6) NOT NULL, - `rage` smallint(6) NOT NULL, - `focus` smallint(6) NOT NULL, - `runicpwr` smallint(6) NOT NULL, - `defrtng` smallint(6) NOT NULL, - `dodgertng` smallint(6) NOT NULL, - `parryrtng` smallint(6) NOT NULL, - `blockrtng` smallint(6) NOT NULL, - `mlehitrtng` smallint(6) NOT NULL, - `rgdhitrtng` smallint(6) NOT NULL, - `splhitrtng` smallint(6) NOT NULL, - `mlecritstrkrtng` smallint(6) NOT NULL, - `rgdcritstrkrtng` smallint(6) NOT NULL, - `splcritstrkrtng` smallint(6) NOT NULL, - `_mlehitrtng` smallint(6) NOT NULL, - `_rgdhitrtng` smallint(6) NOT NULL, - `_splhitrtng` smallint(6) NOT NULL, - `_mlecritstrkrtng` smallint(6) NOT NULL, - `_rgdcritstrkrtng` smallint(6) NOT NULL, - `_splcritstrkrtng` smallint(6) NOT NULL, - `mlehastertng` smallint(6) NOT NULL, - `rgdhastertng` smallint(6) NOT NULL, - `splhastertng` smallint(6) NOT NULL, - `hitrtng` smallint(6) NOT NULL, - `critstrkrtng` smallint(6) NOT NULL, - `_hitrtng` smallint(6) NOT NULL, - `_critstrkrtng` smallint(6) NOT NULL, - `resirtng` smallint(6) NOT NULL, - `hastertng` smallint(6) NOT NULL, - `exprtng` smallint(6) NOT NULL, - `atkpwr` smallint(6) NOT NULL, - `mleatkpwr` smallint(6) NOT NULL, - `rgdatkpwr` smallint(6) NOT NULL, - `feratkpwr` smallint(6) NOT NULL, - `splheal` smallint(6) NOT NULL, - `spldmg` smallint(6) NOT NULL, - `manargn` smallint(6) NOT NULL, - `armorpenrtng` smallint(6) NOT NULL, - `splpwr` smallint(6) NOT NULL, - `healthrgn` smallint(6) NOT NULL, - `splpen` smallint(6) NOT NULL, - `block` smallint(6) NOT NULL, - `mastrtng` smallint(6) NOT NULL, - `armor` smallint(6) NOT NULL, - `armorbonus` smallint(6) NOT NULL, - `firres` smallint(6) NOT NULL, - `frores` smallint(6) NOT NULL, - `holres` smallint(6) NOT NULL, - `shares` smallint(6) NOT NULL, - `natres` smallint(6) NOT NULL, - `arcres` smallint(6) NOT NULL, - `firsplpwr` smallint(6) NOT NULL, - `frosplpwr` smallint(6) NOT NULL, - `holsplpwr` smallint(6) NOT NULL, - `shasplpwr` smallint(6) NOT NULL, - `natsplpwr` smallint(6) NOT NULL, - `arcsplpwr` smallint(6) NOT NULL, - PRIMARY KEY (`typeId`,`type`) -) ENGINE=MyISAM DEFAULT CHARSET=utf8; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Table structure for table `aowow_itemenchantment` --- - -DROP TABLE IF EXISTS `aowow_itemenchantment`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!40101 SET character_set_client = utf8 */; -CREATE TABLE `aowow_itemenchantment` ( - `id` smallint(5) unsigned NOT NULL, - `charges` tinyint(4) unsigned NOT NULL, - `cuFlags` int(10) unsigned NOT NULL, - `procChance` tinyint(3) unsigned NOT NULL, - `ppmRate` float NOT NULL, - `type1` tinyint(4) unsigned NOT NULL, - `type2` tinyint(4) unsigned NOT NULL, - `type3` tinyint(4) unsigned NOT NULL, - `amount1` smallint(6) NOT NULL, - `amount2` smallint(6) NOT NULL, - `amount3` smallint(6) NOT NULL, - `object1` mediumint(9) unsigned NOT NULL, - `object2` mediumint(9) unsigned NOT NULL, - `object3` smallint(6) unsigned NOT NULL, - `name_loc0` varchar(65) NOT NULL, - `name_loc2` varchar(91) NOT NULL, - `name_loc3` varchar(84) NOT NULL, - `name_loc6` varchar(89) NOT NULL, - `name_loc8` varchar(96) NOT NULL, - `conditionId` tinyint(3) unsigned NOT NULL, - `skillLine` smallint(5) unsigned NOT NULL, - `skillLevel` smallint(5) unsigned NOT NULL, - `requiredLevel` tinyint(3) unsigned NOT NULL, - PRIMARY KEY (`id`) -) ENGINE=MyISAM DEFAULT CHARSET=utf8; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Table structure for table `aowow_itemenchantmentcondition` --- - -DROP TABLE IF EXISTS `aowow_itemenchantmentcondition`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!40101 SET character_set_client = utf8 */; -CREATE TABLE `aowow_itemenchantmentcondition` ( - `id` smallint(6) unsigned NOT NULL, - `color1` tinyint(4) unsigned zerofill NOT NULL, - `color2` tinyint(4) unsigned zerofill NOT NULL, - `color3` tinyint(4) unsigned zerofill NOT NULL, - `color4` tinyint(4) unsigned zerofill NOT NULL, - `color5` tinyint(4) unsigned zerofill NOT NULL, - `comparator1` tinyint(4) unsigned zerofill NOT NULL, - `comparator2` tinyint(4) unsigned zerofill NOT NULL, - `comparator3` tinyint(4) unsigned zerofill NOT NULL, - `comparator4` tinyint(4) unsigned zerofill NOT NULL, - `comparator5` tinyint(4) unsigned zerofill NOT NULL, - `cmpColor1` tinyint(4) unsigned zerofill NOT NULL, - `cmpColor2` tinyint(4) unsigned zerofill NOT NULL, - `cmpColor3` tinyint(4) unsigned zerofill NOT NULL, - `cmpColor4` tinyint(4) unsigned zerofill NOT NULL, - `cmpColor5` tinyint(4) unsigned zerofill NOT NULL, - `value1` tinyint(4) unsigned zerofill NOT NULL, - `value2` tinyint(4) unsigned zerofill NOT NULL, - `value3` tinyint(4) unsigned zerofill NOT NULL, - `value4` tinyint(4) unsigned zerofill NOT NULL, - `value5` tinyint(4) unsigned zerofill NOT NULL, - PRIMARY KEY (`id`) -) ENGINE=MyISAM DEFAULT CHARSET=utf8; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Table structure for table `aowow_itemextendedcost` --- - -DROP TABLE IF EXISTS `aowow_itemextendedcost`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!40101 SET character_set_client = utf8 */; -CREATE TABLE `aowow_itemextendedcost` ( - `id` smallint(5) unsigned NOT NULL, - `reqHonorPoints` mediumint(8) unsigned NOT NULL, - `reqArenaPoints` smallint(5) unsigned NOT NULL, - `reqArenaSlot` tinyint(3) unsigned NOT NULL, - `reqItemId1` mediumint(8) unsigned NOT NULL, - `reqItemId2` mediumint(8) unsigned NOT NULL, - `reqItemId3` mediumint(8) unsigned NOT NULL, - `reqItemId4` mediumint(8) unsigned NOT NULL, - `reqItemId5` mediumint(8) unsigned NOT NULL, - `itemCount1` smallint(5) unsigned NOT NULL, - `itemCount2` smallint(5) unsigned NOT NULL, - `itemCount3` smallint(5) unsigned NOT NULL, - `itemCount4` smallint(5) unsigned NOT NULL, - `itemCount5` smallint(5) unsigned NOT NULL, - `reqPersonalRating` smallint(5) unsigned NOT NULL, - PRIMARY KEY (`id`) -) ENGINE=MyISAM DEFAULT CHARSET=utf8; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Table structure for table `aowow_itemlimitcategory` --- - -DROP TABLE IF EXISTS `aowow_itemlimitcategory`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!40101 SET character_set_client = utf8 */; -CREATE TABLE `aowow_itemlimitcategory` ( - `id` tinyint(3) unsigned NOT NULL, - `name_loc0` varchar(31) NOT NULL, - `name_loc2` varchar(36) NOT NULL, - `name_loc3` varchar(34) NOT NULL, - `name_loc6` varchar(40) NOT NULL, - `name_loc8` varchar(35) NOT NULL, - `count` tinyint(3) unsigned NOT NULL, - `isGem` tinyint(3) unsigned NOT NULL, - PRIMARY KEY (`id`) -) ENGINE=MyISAM DEFAULT CHARSET=utf8; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Table structure for table `aowow_itemrandomenchant` --- - -DROP TABLE IF EXISTS `aowow_itemrandomenchant`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!40101 SET character_set_client = utf8 */; -CREATE TABLE `aowow_itemrandomenchant` ( - `id` smallint(6) NOT NULL, - `name_loc0` varchar(250) NOT NULL, - `name_loc2` varchar(250) NOT NULL, - `name_loc3` varchar(250) NOT NULL, - `name_loc6` varchar(250) NOT NULL, - `name_loc8` varchar(250) NOT NULL, - `nameINT` char(250) NOT NULL, - `enchantId1` smallint(5) unsigned NOT NULL, - `enchantId2` smallint(5) unsigned NOT NULL, - `enchantId3` smallint(5) unsigned NOT NULL, - `enchantId4` smallint(5) unsigned NOT NULL, - `enchantId5` smallint(5) unsigned NOT NULL, - `allocationPct1` smallint(5) unsigned NOT NULL, - `allocationPct2` smallint(5) unsigned NOT NULL, - `allocationPct3` smallint(5) unsigned NOT NULL, - `allocationPct4` smallint(5) unsigned NOT NULL, - `allocationPct5` smallint(5) unsigned NOT NULL, - PRIMARY KEY (`id`) -) ENGINE=MyISAM DEFAULT CHARSET=utf8; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Table structure for table `aowow_itemrandomproppoints` --- - -DROP TABLE IF EXISTS `aowow_itemrandomproppoints`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!40101 SET character_set_client = utf8 */; -CREATE TABLE `aowow_itemrandomproppoints` ( - `id` smallint(5) unsigned NOT NULL, - `epic1` smallint(5) unsigned NOT NULL, - `epic2` smallint(5) unsigned NOT NULL, - `epic3` smallint(5) unsigned NOT NULL, - `epic4` smallint(5) unsigned NOT NULL, - `epic5` smallint(5) unsigned NOT NULL, - `rare1` smallint(5) unsigned NOT NULL, - `rare2` smallint(5) unsigned NOT NULL, - `rare3` smallint(5) unsigned NOT NULL, - `rare4` smallint(5) unsigned NOT NULL, - `rare5` smallint(5) unsigned NOT NULL, - `uncommon1` smallint(5) unsigned NOT NULL, - `uncommon2` smallint(5) unsigned NOT NULL, - `uncommon3` smallint(5) unsigned NOT NULL, - `uncommon4` smallint(5) unsigned NOT NULL, - `uncommon5` smallint(5) unsigned NOT NULL, - PRIMARY KEY (`id`) -) ENGINE=MyISAM DEFAULT CHARSET=utf8; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Table structure for table `aowow_items` --- - -DROP TABLE IF EXISTS `aowow_items`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!40101 SET character_set_client = utf8 */; -CREATE TABLE `aowow_items` ( - `id` mediumint(8) unsigned NOT NULL DEFAULT 0, - `class` tinyint(3) unsigned NOT NULL DEFAULT 0, - `classBak` tinyint(3) NOT NULL, - `subClass` tinyint(3) NOT NULL DEFAULT 0, - `subClassBak` tinyint(3) NOT NULL, - `soundOverrideSubclass` tinyint(3) NOT NULL, - `subSubClass` tinyint(3) NOT NULL, - `name_loc0` varchar(127) NOT NULL DEFAULT '', - `name_loc2` varchar(127) DEFAULT NULL, - `name_loc3` varchar(127) DEFAULT NULL, - `name_loc6` varchar(127) DEFAULT NULL, - `name_loc8` varchar(127) DEFAULT NULL, - `iconId` smallint(5) unsigned NOT NULL DEFAULT 0, - `displayId` mediumint(8) unsigned NOT NULL DEFAULT 0, - `spellVisualId` smallint(5) unsigned NOT NULL DEFAULT 0, - `quality` tinyint(3) unsigned NOT NULL DEFAULT 0, - `flags` bigint(20) NOT NULL DEFAULT 0, - `flagsExtra` int(10) unsigned NOT NULL DEFAULT 0, - `buyCount` tinyint(3) unsigned NOT NULL DEFAULT 1, - `buyPrice` bigint(20) NOT NULL DEFAULT 0, - `sellPrice` int(10) unsigned NOT NULL DEFAULT 0, - `repairPrice` int(10) unsigned NOT NULL, - `slot` tinyint(3) NOT NULL, - `slotBak` tinyint(3) unsigned NOT NULL DEFAULT 0, - `requiredClass` int(11) NOT NULL DEFAULT -1, - `requiredRace` int(11) NOT NULL DEFAULT -1, - `itemLevel` smallint(5) unsigned NOT NULL DEFAULT 0, - `requiredLevel` tinyint(3) unsigned NOT NULL DEFAULT 0, - `requiredSkill` smallint(5) unsigned NOT NULL DEFAULT 0, - `requiredSkillRank` smallint(5) unsigned NOT NULL DEFAULT 0, - `requiredSpell` mediumint(8) unsigned NOT NULL DEFAULT 0, - `requiredHonorRank` mediumint(8) unsigned NOT NULL DEFAULT 0, - `requiredCityRank` mediumint(8) unsigned NOT NULL DEFAULT 0, - `requiredFaction` smallint(5) unsigned NOT NULL DEFAULT 0, - `requiredFactionRank` smallint(5) unsigned NOT NULL DEFAULT 0, - `maxCount` int(11) NOT NULL DEFAULT 0, - `cuFlags` int(10) unsigned NOT NULL, - `model` varchar(50) NOT NULL, - `stackable` int(11) DEFAULT 1, - `slots` tinyint(3) unsigned NOT NULL DEFAULT 0, - `statType1` tinyint(3) unsigned NOT NULL DEFAULT 0, - `statValue1` smallint(6) NOT NULL DEFAULT 0, - `statType2` tinyint(3) unsigned NOT NULL DEFAULT 0, - `statValue2` smallint(6) NOT NULL DEFAULT 0, - `statType3` tinyint(3) unsigned NOT NULL DEFAULT 0, - `statValue3` smallint(6) NOT NULL DEFAULT 0, - `statType4` tinyint(3) unsigned NOT NULL DEFAULT 0, - `statValue4` smallint(6) NOT NULL DEFAULT 0, - `statType5` tinyint(3) unsigned NOT NULL DEFAULT 0, - `statValue5` smallint(6) NOT NULL DEFAULT 0, - `statType6` tinyint(3) unsigned NOT NULL DEFAULT 0, - `statValue6` smallint(6) NOT NULL DEFAULT 0, - `statType7` tinyint(3) unsigned NOT NULL DEFAULT 0, - `statValue7` smallint(6) NOT NULL DEFAULT 0, - `statType8` tinyint(3) unsigned NOT NULL DEFAULT 0, - `statValue8` smallint(6) NOT NULL DEFAULT 0, - `statType9` tinyint(3) unsigned NOT NULL DEFAULT 0, - `statValue9` smallint(6) NOT NULL DEFAULT 0, - `statType10` tinyint(3) unsigned NOT NULL DEFAULT 0, - `statValue10` smallint(6) NOT NULL DEFAULT 0, - `scalingStatDistribution` smallint(6) NOT NULL DEFAULT 0, - `scalingStatValue` int(10) unsigned NOT NULL DEFAULT 0, - `dmgMin1` float NOT NULL DEFAULT 0, - `dmgMax1` float NOT NULL DEFAULT 0, - `dmgType1` tinyint(3) unsigned NOT NULL DEFAULT 0, - `dmgMin2` float NOT NULL DEFAULT 0, - `dmgMax2` float NOT NULL DEFAULT 0, - `dmgType2` tinyint(3) unsigned NOT NULL DEFAULT 0, - `delay` smallint(5) unsigned NOT NULL DEFAULT 1000, - `armor` smallint(5) unsigned NOT NULL DEFAULT 0, - `armorDamageModifier` float NOT NULL DEFAULT 0, - `block` mediumint(8) unsigned NOT NULL DEFAULT 0, - `resHoly` tinyint(3) unsigned NOT NULL DEFAULT 0, - `resFire` tinyint(3) unsigned NOT NULL DEFAULT 0, - `resNature` tinyint(3) unsigned NOT NULL DEFAULT 0, - `resFrost` tinyint(3) unsigned NOT NULL DEFAULT 0, - `resShadow` tinyint(3) unsigned NOT NULL DEFAULT 0, - `resArcane` tinyint(3) unsigned NOT NULL DEFAULT 0, - `ammoType` tinyint(3) unsigned NOT NULL DEFAULT 0, - `rangedModRange` float NOT NULL DEFAULT 0, - `spellId1` mediumint(8) NOT NULL DEFAULT 0, - `spellTrigger1` tinyint(3) unsigned NOT NULL DEFAULT 0, - `spellCharges1` smallint(6) DEFAULT NULL, - `spellppmRate1` float NOT NULL DEFAULT 0, - `spellCooldown1` int(11) NOT NULL DEFAULT -1, - `spellCategory1` smallint(5) unsigned NOT NULL DEFAULT 0, - `spellCategoryCooldown1` int(11) NOT NULL DEFAULT -1, - `spellId2` mediumint(8) NOT NULL DEFAULT 0, - `spellTrigger2` tinyint(3) unsigned NOT NULL DEFAULT 0, - `spellCharges2` smallint(6) DEFAULT NULL, - `spellppmRate2` float NOT NULL DEFAULT 0, - `spellCooldown2` int(11) NOT NULL DEFAULT -1, - `spellCategory2` smallint(5) unsigned NOT NULL DEFAULT 0, - `spellCategoryCooldown2` int(11) NOT NULL DEFAULT -1, - `spellId3` mediumint(8) NOT NULL DEFAULT 0, - `spellTrigger3` tinyint(3) unsigned NOT NULL DEFAULT 0, - `spellCharges3` smallint(6) DEFAULT NULL, - `spellppmRate3` float NOT NULL DEFAULT 0, - `spellCooldown3` int(11) NOT NULL DEFAULT -1, - `spellCategory3` smallint(5) unsigned NOT NULL DEFAULT 0, - `spellCategoryCooldown3` int(11) NOT NULL DEFAULT -1, - `spellId4` mediumint(8) NOT NULL DEFAULT 0, - `spellTrigger4` tinyint(3) unsigned NOT NULL DEFAULT 0, - `spellCharges4` smallint(6) DEFAULT NULL, - `spellppmRate4` float NOT NULL DEFAULT 0, - `spellCooldown4` int(11) NOT NULL DEFAULT -1, - `spellCategory4` smallint(5) unsigned NOT NULL DEFAULT 0, - `spellCategoryCooldown4` int(11) NOT NULL DEFAULT -1, - `spellId5` mediumint(8) NOT NULL DEFAULT 0, - `spellTrigger5` tinyint(3) unsigned NOT NULL DEFAULT 0, - `spellCharges5` smallint(6) DEFAULT NULL, - `spellppmRate5` float NOT NULL DEFAULT 0, - `spellCooldown5` int(11) NOT NULL DEFAULT -1, - `spellCategory5` smallint(5) unsigned NOT NULL DEFAULT 0, - `spellCategoryCooldown5` int(11) NOT NULL DEFAULT -1, - `bonding` tinyint(3) unsigned NOT NULL DEFAULT 0, - `description_loc0` varchar(255) NOT NULL DEFAULT '', - `description_loc2` varchar(255) DEFAULT NULL, - `description_loc3` varchar(255) DEFAULT NULL, - `description_loc6` varchar(255) DEFAULT NULL, - `description_loc8` varchar(255) DEFAULT NULL, - `pageTextId` mediumint(8) unsigned NOT NULL DEFAULT 0, - `languageId` tinyint(3) unsigned NOT NULL DEFAULT 0, - `startQuest` mediumint(8) unsigned NOT NULL DEFAULT 0, - `lockId` mediumint(8) unsigned NOT NULL DEFAULT 0, - `material` tinyint(3) NOT NULL DEFAULT 0, - `randomEnchant` mediumint(8) NOT NULL DEFAULT 0, - `itemset` mediumint(8) unsigned NOT NULL DEFAULT 0, - `durability` smallint(5) unsigned NOT NULL DEFAULT 0, - `area` mediumint(8) unsigned NOT NULL DEFAULT 0, - `map` smallint(6) NOT NULL DEFAULT 0, - `bagFamily` mediumint(8) NOT NULL DEFAULT 0, - `totemCategory` mediumint(8) NOT NULL DEFAULT 0, - `socketColor1` tinyint(4) NOT NULL DEFAULT 0, - `socketContent1` mediumint(8) NOT NULL DEFAULT 0, - `socketColor2` tinyint(4) NOT NULL DEFAULT 0, - `socketContent2` mediumint(8) NOT NULL DEFAULT 0, - `socketColor3` tinyint(4) NOT NULL DEFAULT 0, - `socketContent3` mediumint(8) NOT NULL DEFAULT 0, - `socketBonus` mediumint(8) NOT NULL DEFAULT 0, - `gemColorMask` mediumint(8) NOT NULL DEFAULT 0, - `requiredDisenchantSkill` smallint(6) NOT NULL DEFAULT -1, - `disenchantId` mediumint(8) unsigned NOT NULL DEFAULT 0, - `duration` int(10) unsigned NOT NULL DEFAULT 0, - `itemLimitCategory` smallint(6) NOT NULL DEFAULT 0, - `eventId` smallint(5) unsigned NOT NULL, - `scriptName` varchar(64) NOT NULL DEFAULT '', - `foodType` tinyint(3) unsigned NOT NULL DEFAULT 0, - `gemEnchantmentId` mediumint(8) NOT NULL, - `minMoneyLoot` int(10) unsigned NOT NULL DEFAULT 0, - `maxMoneyLoot` int(10) unsigned NOT NULL DEFAULT 0, - `pickUpSoundId` smallint(5) unsigned NOT NULL DEFAULT 0, - `dropDownSoundId` smallint(5) unsigned NOT NULL DEFAULT 0, - `sheatheSoundId` smallint(5) unsigned NOT NULL DEFAULT 0, - `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`) -) ENGINE=MyISAM DEFAULT CHARSET=utf8; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Table structure for table `aowow_items_sounds` --- - -DROP TABLE IF EXISTS `aowow_items_sounds`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!40101 SET character_set_client = utf8 */; -CREATE TABLE `aowow_items_sounds` ( - `soundId` smallint(5) unsigned NOT NULL, - `subClassMask` mediumint(8) unsigned NOT NULL, - PRIMARY KEY (`soundId`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='actually .. its only weapon related sounds in here'; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Table structure for table `aowow_itemset` --- - -DROP TABLE IF EXISTS `aowow_itemset`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!40101 SET character_set_client = utf8 */; -CREATE TABLE `aowow_itemset` ( - `id` int(16) NOT NULL, - `refSetId` int(11) NOT NULL, - `cuFlags` int(10) unsigned NOT NULL, - `name_loc0` varchar(255) NOT NULL, - `name_loc2` varchar(255) NOT NULL, - `name_loc3` varchar(255) NOT NULL, - `name_loc6` varchar(255) NOT NULL, - `name_loc8` varchar(255) NOT NULL, - `item1` mediumint(11) unsigned NOT NULL, - `item2` mediumint(11) unsigned NOT NULL, - `item3` mediumint(11) unsigned NOT NULL, - `item4` mediumint(11) unsigned NOT NULL, - `item5` mediumint(11) unsigned NOT NULL, - `item6` mediumint(11) unsigned NOT NULL, - `item7` mediumint(11) unsigned NOT NULL, - `item8` mediumint(11) unsigned NOT NULL, - `item9` mediumint(11) unsigned NOT NULL, - `item10` mediumint(11) unsigned NOT NULL, - `spell1` mediumint(11) unsigned NOT NULL, - `spell2` mediumint(11) unsigned NOT NULL, - `spell3` mediumint(11) unsigned NOT NULL, - `spell4` mediumint(11) unsigned NOT NULL, - `spell5` mediumint(11) unsigned NOT NULL, - `spell6` mediumint(11) unsigned NOT NULL, - `spell7` mediumint(11) unsigned NOT NULL, - `spell8` mediumint(11) unsigned NOT NULL, - `bonus1` tinyint(1) unsigned NOT NULL, - `bonus2` tinyint(1) unsigned NOT NULL, - `bonus3` tinyint(1) unsigned NOT NULL, - `bonus4` tinyint(1) unsigned NOT NULL, - `bonus5` tinyint(1) unsigned NOT NULL, - `bonus6` tinyint(1) unsigned NOT NULL, - `bonus7` tinyint(1) unsigned NOT NULL, - `bonus8` tinyint(1) unsigned NOT NULL, - `bonusText_loc0` varchar(256) NOT NULL, - `bonusText_loc2` varchar(256) NOT NULL, - `bonusText_loc3` varchar(256) NOT NULL, - `bonusText_loc6` varchar(256) NOT NULL, - `bonusText_loc8` varchar(256) NOT NULL, - `bonusParsed` varchar(256) NOT NULL COMMENT 'serialized itemMods', - `npieces` tinyint(3) NOT NULL, - `minLevel` smallint(6) NOT NULL, - `maxLevel` smallint(6) NOT NULL, - `reqLevel` smallint(6) NOT NULL, - `classMask` mediumint(9) NOT NULL, - `heroic` tinyint(1) NOT NULL COMMENT 'bool', - `quality` tinyint(4) NOT NULL, - `type` smallint(6) NOT NULL COMMENT 'g_itemset_types', - `contentGroup` smallint(6) NOT NULL COMMENT 'g_itemset_notes', - `eventId` smallint(3) unsigned NOT NULL, - `skillId` smallint(3) unsigned NOT NULL, - `skillLevel` smallint(3) unsigned NOT NULL, - PRIMARY KEY (`id`) -) ENGINE=MyISAM DEFAULT CHARSET=utf8; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Table structure for table `aowow_lock` --- - -DROP TABLE IF EXISTS `aowow_lock`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!40101 SET character_set_client = utf8 */; -CREATE TABLE `aowow_lock` ( - `id` mediumint(8) unsigned NOT NULL, - `type1` tinyint(3) unsigned NOT NULL, - `type2` tinyint(3) unsigned NOT NULL, - `type3` tinyint(3) unsigned NOT NULL, - `type4` tinyint(3) unsigned NOT NULL, - `type5` tinyint(3) unsigned NOT NULL, - `properties1` smallint(5) unsigned NOT NULL, - `properties2` mediumint(8) unsigned NOT NULL, - `properties3` mediumint(8) unsigned NOT NULL, - `properties4` mediumint(8) unsigned NOT NULL, - `properties5` mediumint(8) unsigned NOT NULL, - `reqSkill1` mediumint(8) unsigned NOT NULL, - `reqSkill2` mediumint(8) unsigned NOT NULL, - `reqSkill3` mediumint(8) unsigned NOT NULL, - `reqSkill4` mediumint(8) unsigned NOT NULL, - `reqSkill5` mediumint(8) unsigned NOT NULL, - PRIMARY KEY (`id`) -) ENGINE=MyISAM DEFAULT CHARSET=utf8; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Table structure for table `aowow_loot_link` --- - -DROP TABLE IF EXISTS `aowow_loot_link`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!40101 SET character_set_client = utf8 */; -CREATE TABLE `aowow_loot_link` ( - `npcId` mediumint(8) NOT NULL, - `objectId` mediumint(8) unsigned NOT NULL, - UNIQUE KEY `npcId` (`npcId`), - KEY `objectId` (`objectId`) -) ENGINE=MyISAM DEFAULT CHARSET=utf8; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Table structure for table `aowow_mailtemplate` --- - -DROP TABLE IF EXISTS `aowow_mailtemplate`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!40101 SET character_set_client = utf8 */; -CREATE TABLE `aowow_mailtemplate` ( - `id` smallint(5) unsigned NOT NULL, - `subject_loc0` varchar(128) NOT NULL, - `subject_loc2` varchar(128) NOT NULL, - `subject_loc3` varchar(128) NOT NULL, - `subject_loc6` varchar(128) NOT NULL, - `subject_loc8` varchar(128) NOT NULL, - `text_loc0` text NOT NULL, - `text_loc2` text NOT NULL, - `text_loc3` text NOT NULL, - `text_loc6` text NOT NULL, - `text_loc8` text NOT NULL, - PRIMARY KEY (`id`) -) ENGINE=MyISAM DEFAULT CHARSET=utf8; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Table structure for table `aowow_objects` --- - -DROP TABLE IF EXISTS `aowow_objects`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!40101 SET character_set_client = utf8 */; -CREATE TABLE `aowow_objects` ( - `id` mediumint(8) unsigned NOT NULL DEFAULT 0, - `type` tinyint(3) unsigned NOT NULL DEFAULT 0, - `typeCat` tinyint(3) NOT NULL DEFAULT 0, - `event` smallint(5) unsigned NOT NULL DEFAULT 0, - `displayId` mediumint(8) unsigned NOT NULL DEFAULT 0, - `name_loc0` varchar(100) DEFAULT NULL, - `name_loc2` varchar(100) DEFAULT NULL, - `name_loc3` varchar(100) DEFAULT NULL, - `name_loc6` varchar(100) DEFAULT NULL, - `name_loc8` varchar(100) DEFAULT NULL, - `faction` smallint(5) unsigned NOT NULL DEFAULT 0, - `flags` int(10) unsigned NOT NULL DEFAULT 0, - `cuFlags` int(10) unsigned NOT NULL DEFAULT 0, - `lootId` mediumint(8) unsigned NOT NULL DEFAULT 0, - `lockId` smallint(5) unsigned NOT NULL DEFAULT 0, - `reqSkill` smallint(5) unsigned NOT NULL DEFAULT 0, - `pageTextId` smallint(5) unsigned NOT NULL DEFAULT 0, - `linkedTrap` mediumint(8) unsigned NOT NULL DEFAULT 0, - `reqQuest` smallint(5) unsigned NOT NULL DEFAULT 0, - `spellFocusId` smallint(5) unsigned NOT NULL DEFAULT 0, - `onUseSpell` mediumint(8) unsigned NOT NULL DEFAULT 0, - `onSuccessSpell` mediumint(8) unsigned NOT NULL DEFAULT 0, - `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, - PRIMARY KEY (`id`), - KEY `idx_name` (`name_loc0`) -) ENGINE=MyISAM DEFAULT CHARSET=utf8; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Table structure for table `aowow_pet` --- - -DROP TABLE IF EXISTS `aowow_pet`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!40101 SET character_set_client = utf8 */; -CREATE TABLE `aowow_pet` ( - `id` int(11) NOT NULL, - `category` mediumint(8) NOT NULL, - `cuFlags` int(10) unsigned NOT NULL, - `minLevel` smallint(6) NOT NULL, - `maxLevel` smallint(6) NOT NULL, - `foodMask` int(11) NOT NULL, - `type` tinyint(4) NOT NULL, - `exotic` tinyint(4) NOT NULL, - `expansion` tinyint(4) NOT NULL, - `name_loc0` varchar(64) NOT NULL, - `name_loc2` varchar(64) NOT NULL, - `name_loc3` varchar(64) NOT NULL, - `name_loc6` varchar(64) NOT NULL, - `name_loc8` varchar(64) NOT NULL, - `iconId` smallint(5) unsigned NOT NULL DEFAULT 0, - `skillLineId` mediumint(9) NOT NULL, - `spellId1` mediumint(9) NOT NULL, - `spellId2` mediumint(9) NOT NULL, - `spellId3` mediumint(9) NOT NULL, - `spellId4` mediumint(9) NOT NULL, - `armor` mediumint(9) NOT NULL, - `damage` mediumint(9) NOT NULL, - `health` mediumint(9) NOT NULL, - PRIMARY KEY (`id`), - KEY `iconId` (`iconId`) -) ENGINE=MyISAM DEFAULT CHARSET=utf8; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Table structure for table `aowow_profiler_arena_team` --- - -DROP TABLE IF EXISTS `aowow_profiler_arena_team`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!40101 SET character_set_client = utf8 */; -CREATE TABLE `aowow_profiler_arena_team` ( - `id` int(10) unsigned NOT NULL AUTO_INCREMENT, - `realm` tinyint(3) unsigned NOT NULL, - `realmGUID` int(10) unsigned NOT NULL, - `name` varchar(24) NOT NULL, - `nameUrl` varchar(24) NOT NULL, - `type` tinyint(3) unsigned NOT NULL DEFAULT 0, - `cuFlags` int(11) unsigned NOT NULL, - `rating` smallint(5) unsigned NOT NULL DEFAULT 0, - `seasonGames` smallint(5) unsigned NOT NULL DEFAULT 0, - `seasonWins` smallint(5) unsigned NOT NULL DEFAULT 0, - `weekGames` smallint(5) unsigned NOT NULL DEFAULT 0, - `weekWins` smallint(5) unsigned NOT NULL DEFAULT 0, - `rank` int(10) unsigned NOT NULL DEFAULT 0, - `backgroundColor` int(10) unsigned NOT NULL DEFAULT 0, - `emblemStyle` tinyint(3) unsigned NOT NULL DEFAULT 0, - `emblemColor` int(10) unsigned NOT NULL DEFAULT 0, - `borderStyle` tinyint(3) unsigned NOT NULL DEFAULT 0, - `borderColor` int(10) unsigned NOT NULL DEFAULT 0, - PRIMARY KEY (`id`), - UNIQUE KEY `realm_realmGUID` (`realm`,`realmGUID`), - KEY `name` (`name`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Table structure for table `aowow_profiler_arena_team_member` --- - -DROP TABLE IF EXISTS `aowow_profiler_arena_team_member`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!40101 SET character_set_client = utf8 */; -CREATE TABLE `aowow_profiler_arena_team_member` ( - `arenaTeamId` int(10) unsigned NOT NULL DEFAULT 0, - `profileId` int(10) unsigned NOT NULL DEFAULT 0, - `captain` tinyint(1) unsigned NOT NULL DEFAULT 0, - `weekGames` smallint(5) unsigned NOT NULL DEFAULT 0, - `weekWins` smallint(5) unsigned NOT NULL DEFAULT 0, - `seasonGames` smallint(5) unsigned NOT NULL DEFAULT 0, - `seasonWins` smallint(5) unsigned NOT NULL DEFAULT 0, - `personalRating` smallint(5) unsigned NOT NULL DEFAULT 0, - PRIMARY KEY (`arenaTeamId`,`profileId`), - KEY `guid` (`profileId`), - CONSTRAINT `FK_aowow_profiler_arena_team_member_aowow_profiler_arena_team` FOREIGN KEY (`arenaTeamId`) REFERENCES `aowow_profiler_arena_team` (`id`) ON DELETE CASCADE ON UPDATE CASCADE, - CONSTRAINT `FK_aowow_profiler_arena_team_member_aowow_profiler_profiles` FOREIGN KEY (`profileId`) REFERENCES `aowow_profiler_profiles` (`id`) ON DELETE CASCADE ON UPDATE CASCADE -) ENGINE=InnoDB DEFAULT CHARSET=utf8; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Table structure for table `aowow_profiler_completion` --- - -DROP TABLE IF EXISTS `aowow_profiler_completion`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!40101 SET character_set_client = utf8 */; -CREATE TABLE `aowow_profiler_completion` ( - `id` int(11) unsigned NOT NULL, - `type` smallint(6) unsigned NOT NULL, - `typeId` mediumint(9) NOT NULL, - `cur` int(11) DEFAULT NULL, - `max` int(11) DEFAULT NULL, - KEY `id` (`id`), - KEY `type` (`type`), - KEY `typeId` (`typeId`), - CONSTRAINT `FK_pr_completion` FOREIGN KEY (`id`) REFERENCES `aowow_profiler_profiles` (`id`) ON DELETE CASCADE ON UPDATE CASCADE -) ENGINE=InnoDB DEFAULT CHARSET=utf8; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Table structure for table `aowow_profiler_excludes` --- - -DROP TABLE IF EXISTS `aowow_profiler_excludes`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!40101 SET character_set_client = utf8 */; -CREATE TABLE `aowow_profiler_excludes` ( - `type` smallint(5) unsigned NOT NULL, - `typeId` mediumint(8) unsigned NOT NULL, - `groups` smallint(5) unsigned NOT NULL COMMENT 'see exclude group defines', - `comment` varchar(50) NOT NULL COMMENT 'rebuilding profiler files will delete everything without a comment', - PRIMARY KEY (`type`,`typeId`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Table structure for table `aowow_profiler_guild` --- - -DROP TABLE IF EXISTS `aowow_profiler_guild`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!40101 SET character_set_client = utf8 */; -CREATE TABLE `aowow_profiler_guild` ( - `id` int(10) unsigned NOT NULL AUTO_INCREMENT, - `realm` int(10) unsigned NOT NULL, - `realmGUID` int(10) unsigned NOT NULL, - `cuFlags` int(10) unsigned NOT NULL DEFAULT 0, - `name` varchar(26) NOT NULL, - `nameUrl` varchar(26) NOT NULL, - `emblemStyle` tinyint(3) unsigned NOT NULL DEFAULT 0, - `emblemColor` tinyint(3) unsigned NOT NULL DEFAULT 0, - `borderStyle` tinyint(3) unsigned NOT NULL DEFAULT 0, - `borderColor` tinyint(3) unsigned NOT NULL DEFAULT 0, - `backgroundColor` tinyint(3) unsigned NOT NULL DEFAULT 0, - `info` varchar(500) NOT NULL DEFAULT '', - `createDate` int(10) unsigned NOT NULL DEFAULT 0, - PRIMARY KEY (`id`), - UNIQUE KEY `realm_realmGUID` (`realm`,`realmGUID`), - KEY `name` (`name`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Table structure for table `aowow_profiler_guild_rank` --- - -DROP TABLE IF EXISTS `aowow_profiler_guild_rank`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!40101 SET character_set_client = utf8 */; -CREATE TABLE `aowow_profiler_guild_rank` ( - `guildId` int(10) unsigned NOT NULL DEFAULT 0, - `rank` tinyint(3) unsigned NOT NULL, - `name` varchar(20) NOT NULL DEFAULT '', - PRIMARY KEY (`guildId`,`rank`), - KEY `rank` (`rank`), - CONSTRAINT `FK_aowow_profiler_guild_rank_aowow_profiler_guild` FOREIGN KEY (`guildId`) REFERENCES `aowow_profiler_guild` (`id`) ON DELETE CASCADE ON UPDATE CASCADE -) ENGINE=InnoDB DEFAULT CHARSET=utf8; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Table structure for table `aowow_profiler_items` --- - -DROP TABLE IF EXISTS `aowow_profiler_items`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!40101 SET character_set_client = utf8 */; -CREATE TABLE `aowow_profiler_items` ( - `id` int(11) unsigned DEFAULT NULL, - `slot` tinyint(3) unsigned DEFAULT NULL, - `item` mediumint(8) unsigned DEFAULT NULL, - `subItem` smallint(6) DEFAULT NULL, - `permEnchant` mediumint(8) unsigned DEFAULT NULL, - `tempEnchant` mediumint(8) unsigned DEFAULT NULL, - `extraSocket` tinyint(3) unsigned DEFAULT NULL COMMENT 'not used .. the appropriate gem slot is set to -1 instead', - `gem1` mediumint(8) DEFAULT NULL, - `gem2` mediumint(8) DEFAULT NULL, - `gem3` mediumint(8) DEFAULT NULL, - `gem4` mediumint(8) DEFAULT NULL, - UNIQUE KEY `id_slot` (`id`,`slot`), - KEY `id` (`id`), - KEY `item` (`item`), - CONSTRAINT `FK_pr_items` FOREIGN KEY (`id`) REFERENCES `aowow_profiler_profiles` (`id`) ON DELETE CASCADE ON UPDATE CASCADE -) ENGINE=InnoDB DEFAULT CHARSET=utf8; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Table structure for table `aowow_profiler_pets` --- - -DROP TABLE IF EXISTS `aowow_profiler_pets`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!40101 SET character_set_client = utf8 */; -CREATE TABLE `aowow_profiler_pets` ( - `id` mediumint(8) unsigned NOT NULL AUTO_INCREMENT, - `owner` int(10) unsigned DEFAULT NULL, - `name` varchar(50) DEFAULT NULL, - `family` tinyint(3) unsigned DEFAULT NULL, - `npc` smallint(5) unsigned DEFAULT NULL, - `displayId` smallint(5) unsigned DEFAULT NULL, - `talents` varchar(20) 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 -) ENGINE=InnoDB DEFAULT CHARSET=utf8; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Table structure for table `aowow_profiler_profiles` --- - -DROP TABLE IF EXISTS `aowow_profiler_profiles`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!40101 SET character_set_client = utf8 */; -CREATE TABLE `aowow_profiler_profiles` ( - `id` int(11) unsigned NOT NULL AUTO_INCREMENT, - `realm` tinyint(3) unsigned DEFAULT NULL, - `realmGUID` int(11) unsigned DEFAULT NULL, - `cuFlags` int(11) unsigned NOT NULL DEFAULT 0, - `sourceId` int(11) unsigned DEFAULT NULL, - `sourceName` varchar(50) DEFAULT NULL, - `copy` int(10) unsigned DEFAULT NULL, - `icon` varchar(50) DEFAULT NULL, - `user` int(11) unsigned DEFAULT NULL, - `name` varchar(50) NOT NULL, - `race` tinyint(3) unsigned NOT NULL, - `class` tinyint(3) unsigned NOT NULL, - `level` tinyint(3) unsigned NOT NULL, - `gender` tinyint(3) unsigned NOT NULL, - `guild` int(10) unsigned DEFAULT NULL, - `guildrank` tinyint(3) unsigned DEFAULT NULL COMMENT '0: guild master', - `skincolor` tinyint(3) unsigned NOT NULL, - `hairstyle` tinyint(3) unsigned NOT NULL, - `haircolor` tinyint(3) unsigned NOT NULL, - `facetype` tinyint(3) unsigned NOT NULL, - `features` tinyint(3) unsigned NOT NULL, - `nomodelMask` int(11) unsigned NOT NULL DEFAULT 0, - `title` tinyint(3) unsigned NOT NULL, - `description` text DEFAULT NULL, - `playedtime` int(11) unsigned NOT NULL, - `gearscore` smallint(5) unsigned NOT NULL, - `achievementpoints` smallint(5) unsigned NOT NULL, - `lastupdated` int(11) NOT NULL, - `talenttree1` tinyint(4) unsigned NOT NULL COMMENT 'points spend in 1st tree', - `talenttree2` tinyint(4) unsigned NOT NULL COMMENT 'points spend in 2nd tree', - `talenttree3` tinyint(4) unsigned NOT NULL COMMENT 'points spend in 3rd tree', - `talentbuild1` varchar(105) NOT NULL, - `talentbuild2` varchar(105) NOT NULL, - `glyphs1` varchar(45) NOT NULL, - `glyphs2` varchar(45) NOT NULL, - `activespec` tinyint(1) unsigned NOT NULL, - PRIMARY KEY (`id`), - UNIQUE KEY `realm_realmGUID_name` (`realm`,`realmGUID`,`name`), - KEY `user` (`user`), - KEY `guild` (`guild`), - 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=utf8; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Table structure for table `aowow_profiler_sync` --- - -DROP TABLE IF EXISTS `aowow_profiler_sync`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!40101 SET character_set_client = utf8 */; -CREATE TABLE `aowow_profiler_sync` ( - `realm` tinyint(3) unsigned NOT NULL, - `realmGUID` int(10) unsigned NOT NULL, - `type` smallint(5) unsigned NOT NULL, - `typeId` int(10) unsigned NOT NULL, - `requestTime` int(10) unsigned NOT NULL, - `status` tinyint(3) unsigned NOT NULL, - `errorCode` tinyint(3) unsigned NOT NULL DEFAULT 0, - UNIQUE KEY `realm_realmGUID_type_typeId` (`realm`,`realmGUID`,`type`), - UNIQUE KEY `type_typeId` (`type`,`typeId`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Table structure for table `aowow_quests` --- - -DROP TABLE IF EXISTS `aowow_quests`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!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, - `level` smallint(3) 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, - `suggestedPlayers` tinyint(3) unsigned NOT NULL DEFAULT 0, - `timeLimit` int(10) unsigned NOT NULL DEFAULT 0, - `eventId` smallint(5) unsigned NOT NULL DEFAULT 0, - `prevQuestId` mediumint(8) NOT NULL DEFAULT 0, - `nextQuestId` mediumint(8) NOT NULL DEFAULT 0, - `exclusiveGroup` mediumint(8) NOT NULL DEFAULT 0, - `nextQuestIdChain` mediumint(8) unsigned NOT NULL DEFAULT 0, - `flags` int(10) unsigned NOT NULL DEFAULT 0, - `specialFlags` tinyint(3) unsigned NOT NULL DEFAULT 0, - `cuFlags` int(10) unsigned NOT NULL DEFAULT 0, - `reqClassMask` smallint(5) unsigned NOT NULL DEFAULT 0, - `reqRaceMask` smallint(5) unsigned NOT NULL DEFAULT 0, - `reqSkillId` smallint(5) unsigned NOT NULL DEFAULT 0, - `reqSkillPoints` smallint(5) unsigned NOT NULL DEFAULT 0, - `reqFactionId1` smallint(5) unsigned NOT NULL DEFAULT 0, - `reqFactionId2` smallint(5) unsigned NOT NULL DEFAULT 0, - `reqFactionValue1` mediumint(8) NOT NULL DEFAULT 0, - `reqFactionValue2` mediumint(8) NOT NULL DEFAULT 0, - `reqMinRepFaction` smallint(5) unsigned NOT NULL DEFAULT 0, - `reqMaxRepFaction` smallint(5) unsigned NOT NULL DEFAULT 0, - `reqMinRepValue` mediumint(8) NOT NULL DEFAULT 0, - `reqMaxRepValue` mediumint(8) NOT NULL DEFAULT 0, - `reqPlayerKills` tinyint(3) unsigned NOT NULL DEFAULT 0, - `sourceItemId` mediumint(8) unsigned NOT NULL DEFAULT 0, - `sourceItemCount` tinyint(3) unsigned NOT NULL DEFAULT 0, - `sourceSpellId` mediumint(8) unsigned NOT NULL DEFAULT 0, - `rewardXP` mediumint(8) unsigned NOT NULL DEFAULT 0, - `rewardOrReqMoney` int(11) NOT NULL DEFAULT 0, - `rewardMoneyMaxLevel` int(10) unsigned NOT NULL DEFAULT 0, - `rewardSpell` mediumint(8) unsigned NOT NULL DEFAULT 0, - `rewardSpellCast` int(11) NOT NULL DEFAULT 0, - `rewardHonorPoints` int(11) NOT NULL DEFAULT 0, - `rewardMailTemplateId` mediumint(8) unsigned NOT NULL DEFAULT 0, - `rewardMailDelay` int(11) unsigned NOT NULL DEFAULT 0, - `rewardTitleId` tinyint(3) unsigned NOT NULL DEFAULT 0, - `rewardTalents` tinyint(3) unsigned NOT NULL DEFAULT 0, - `rewardArenaPoints` smallint(5) unsigned NOT NULL DEFAULT 0, - `rewardItemId1` mediumint(8) unsigned NOT NULL DEFAULT 0, - `rewardItemId2` mediumint(8) unsigned NOT NULL DEFAULT 0, - `rewardItemId3` mediumint(8) unsigned NOT NULL DEFAULT 0, - `rewardItemId4` mediumint(8) unsigned NOT NULL DEFAULT 0, - `rewardItemCount1` smallint(5) unsigned NOT NULL DEFAULT 0, - `rewardItemCount2` smallint(5) unsigned NOT NULL DEFAULT 0, - `rewardItemCount3` smallint(5) unsigned NOT NULL DEFAULT 0, - `rewardItemCount4` smallint(5) unsigned NOT NULL DEFAULT 0, - `rewardChoiceItemId1` mediumint(8) unsigned NOT NULL DEFAULT 0, - `rewardChoiceItemId2` mediumint(8) unsigned NOT NULL DEFAULT 0, - `rewardChoiceItemId3` mediumint(8) unsigned NOT NULL DEFAULT 0, - `rewardChoiceItemId4` mediumint(8) unsigned NOT NULL DEFAULT 0, - `rewardChoiceItemId5` mediumint(8) unsigned NOT NULL DEFAULT 0, - `rewardChoiceItemId6` mediumint(8) unsigned NOT NULL DEFAULT 0, - `rewardChoiceItemCount1` smallint(5) unsigned NOT NULL DEFAULT 0, - `rewardChoiceItemCount2` smallint(5) unsigned NOT NULL DEFAULT 0, - `rewardChoiceItemCount3` smallint(5) unsigned NOT NULL DEFAULT 0, - `rewardChoiceItemCount4` smallint(5) unsigned NOT NULL DEFAULT 0, - `rewardChoiceItemCount5` smallint(5) unsigned NOT NULL DEFAULT 0, - `rewardChoiceItemCount6` smallint(5) unsigned NOT NULL DEFAULT 0, - `rewardFactionId1` smallint(5) unsigned NOT NULL DEFAULT 0 COMMENT 'faction id from Faction.dbc in this case', - `rewardFactionId2` smallint(5) unsigned NOT NULL DEFAULT 0 COMMENT 'faction id from Faction.dbc in this case', - `rewardFactionId3` smallint(5) unsigned NOT NULL DEFAULT 0 COMMENT 'faction id from Faction.dbc in this case', - `rewardFactionId4` smallint(5) unsigned NOT NULL DEFAULT 0 COMMENT 'faction id from Faction.dbc in this case', - `rewardFactionId5` smallint(5) unsigned NOT NULL DEFAULT 0 COMMENT 'faction id from Faction.dbc in this case', - `rewardFactionValue1` mediumint(8) NOT NULL DEFAULT 0, - `rewardFactionValue2` mediumint(8) NOT NULL DEFAULT 0, - `rewardFactionValue3` mediumint(8) NOT NULL DEFAULT 0, - `rewardFactionValue4` mediumint(8) NOT NULL DEFAULT 0, - `rewardFactionValue5` mediumint(8) NOT NULL DEFAULT 0, - `name_loc0` text DEFAULT NULL, - `name_loc2` text DEFAULT NULL, - `name_loc3` text DEFAULT NULL, - `name_loc6` text DEFAULT NULL, - `name_loc8` text DEFAULT NULL, - `objectives_loc0` text DEFAULT NULL, - `objectives_loc2` text DEFAULT NULL, - `objectives_loc3` text DEFAULT NULL, - `objectives_loc6` text DEFAULT NULL, - `objectives_loc8` text DEFAULT NULL, - `details_loc0` text DEFAULT NULL, - `details_loc2` text DEFAULT NULL, - `details_loc3` text DEFAULT NULL, - `details_loc6` text DEFAULT NULL, - `details_loc8` text DEFAULT NULL, - `end_loc0` text DEFAULT NULL, - `end_loc2` text DEFAULT NULL, - `end_loc3` text DEFAULT NULL, - `end_loc6` text DEFAULT NULL, - `end_loc8` text DEFAULT NULL, - `offerReward_loc0` text DEFAULT NULL, - `offerReward_loc2` text DEFAULT NULL, - `offerReward_loc3` text DEFAULT NULL, - `offerReward_loc6` text DEFAULT NULL, - `offerReward_loc8` text DEFAULT NULL, - `requestItems_loc0` text DEFAULT NULL, - `requestItems_loc2` text DEFAULT NULL, - `requestItems_loc3` text DEFAULT NULL, - `requestItems_loc6` text DEFAULT NULL, - `requestItems_loc8` text DEFAULT NULL, - `completed_loc0` text DEFAULT NULL, - `completed_loc2` text DEFAULT NULL, - `completed_loc3` text DEFAULT NULL, - `completed_loc6` text DEFAULT NULL, - `completed_loc8` text DEFAULT NULL, - `reqNpcOrGo1` mediumint(8) NOT NULL DEFAULT 0, - `reqNpcOrGo2` mediumint(8) NOT NULL DEFAULT 0, - `reqNpcOrGo3` mediumint(8) NOT NULL DEFAULT 0, - `reqNpcOrGo4` mediumint(8) NOT NULL DEFAULT 0, - `reqNpcOrGoCount1` smallint(5) unsigned NOT NULL DEFAULT 0, - `reqNpcOrGoCount2` smallint(5) unsigned NOT NULL DEFAULT 0, - `reqNpcOrGoCount3` smallint(5) unsigned NOT NULL DEFAULT 0, - `reqNpcOrGoCount4` smallint(5) unsigned NOT NULL DEFAULT 0, - `reqSourceItemId1` mediumint(8) unsigned NOT NULL DEFAULT 0, - `reqSourceItemId2` mediumint(8) unsigned NOT NULL DEFAULT 0, - `reqSourceItemId3` mediumint(8) unsigned NOT NULL DEFAULT 0, - `reqSourceItemId4` mediumint(8) unsigned NOT NULL DEFAULT 0, - `reqSourceItemCount1` smallint(5) unsigned NOT NULL DEFAULT 0, - `reqSourceItemCount2` smallint(5) unsigned NOT NULL DEFAULT 0, - `reqSourceItemCount3` smallint(5) unsigned NOT NULL DEFAULT 0, - `reqSourceItemCount4` smallint(5) unsigned NOT NULL DEFAULT 0, - `reqItemId1` mediumint(8) unsigned NOT NULL DEFAULT 0, - `reqItemId2` mediumint(8) unsigned NOT NULL DEFAULT 0, - `reqItemId3` mediumint(8) unsigned NOT NULL DEFAULT 0, - `reqItemId4` mediumint(8) unsigned NOT NULL DEFAULT 0, - `reqItemId5` mediumint(8) unsigned NOT NULL DEFAULT 0, - `reqItemId6` mediumint(8) unsigned NOT NULL DEFAULT 0, - `reqItemCount1` smallint(5) unsigned NOT NULL DEFAULT 0, - `reqItemCount2` smallint(5) unsigned NOT NULL DEFAULT 0, - `reqItemCount3` smallint(5) unsigned NOT NULL DEFAULT 0, - `reqItemCount4` smallint(5) unsigned NOT NULL DEFAULT 0, - `reqItemCount5` smallint(5) unsigned NOT NULL DEFAULT 0, - `reqItemCount6` smallint(5) unsigned NOT NULL DEFAULT 0, - `objectiveText1_loc0` text DEFAULT NULL, - `objectiveText1_loc2` text DEFAULT NULL, - `objectiveText1_loc3` text DEFAULT NULL, - `objectiveText1_loc6` text DEFAULT NULL, - `objectiveText1_loc8` text DEFAULT NULL, - `objectiveText2_loc0` text DEFAULT NULL, - `objectiveText2_loc2` text DEFAULT NULL, - `objectiveText2_loc3` text DEFAULT NULL, - `objectiveText2_loc6` text DEFAULT NULL, - `objectiveText2_loc8` text DEFAULT NULL, - `objectiveText3_loc0` text DEFAULT NULL, - `objectiveText3_loc2` text DEFAULT NULL, - `objectiveText3_loc3` text DEFAULT NULL, - `objectiveText3_loc6` text DEFAULT NULL, - `objectiveText3_loc8` text DEFAULT NULL, - `objectiveText4_loc0` text DEFAULT NULL, - `objectiveText4_loc2` text DEFAULT NULL, - `objectiveText4_loc3` text DEFAULT NULL, - `objectiveText4_loc6` text DEFAULT NULL, - `objectiveText4_loc8` text DEFAULT NULL, - PRIMARY KEY (`id`), - KEY `nextQuestIdChain` (`nextQuestIdChain`) -) ENGINE=MyISAM DEFAULT CHARSET=utf8; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Table structure for table `aowow_quests_startend` --- - -DROP TABLE IF EXISTS `aowow_quests_startend`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!40101 SET character_set_client = utf8 */; -CREATE TABLE `aowow_quests_startend` ( - `type` tinyint(4) unsigned NOT NULL, - `typeId` mediumint(9) unsigned NOT NULL, - `questId` mediumint(9) unsigned NOT NULL, - `method` tinyint(4) unsigned NOT NULL COMMENT '&0x1: starts; &0x2:ends', - `eventId` smallint(6) unsigned NOT NULL DEFAULT 0, - PRIMARY KEY (`type`,`typeId`,`questId`) -) ENGINE=MyISAM DEFAULT CHARSET=utf8; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Table structure for table `aowow_races` --- - -DROP TABLE IF EXISTS `aowow_races`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!40101 SET character_set_client = utf8 */; -CREATE TABLE `aowow_races` ( - `id` int(16) NOT NULL, - `classMask` bigint(20) NOT NULL, - `flags` bigint(20) NOT NULL, - `cuFlags` int(10) unsigned NOT NULL, - `factionId` bigint(20) NOT NULL, - `startAreaId` bigint(20) NOT NULL, - `leader` bigint(20) NOT NULL, - `baseLanguage` bigint(20) NOT NULL, - `side` int(3) NOT NULL, - `fileString` varchar(64) NOT NULL, - `name_loc0` varchar(64) NOT NULL, - `name_loc2` varchar(64) NOT NULL, - `name_loc3` varchar(64) NOT NULL, - `name_loc6` varchar(64) NOT NULL, - `name_loc8` varchar(64) NOT NULL, - `expansion` int(1) NOT NULL, - PRIMARY KEY (`id`) -) ENGINE=MyISAM DEFAULT CHARSET=utf8; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Table structure for table `aowow_races_sounds` --- - -DROP TABLE IF EXISTS `aowow_races_sounds`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!40101 SET character_set_client = utf8 */; -CREATE TABLE `aowow_races_sounds` ( - `raceId` tinyint(3) unsigned NOT NULL, - `soundId` smallint(5) unsigned NOT NULL, - `gender` tinyint(1) unsigned NOT NULL, - UNIQUE KEY `race_soundId_gender` (`raceId`,`soundId`,`gender`), - KEY `race` (`raceId`), - KEY `soundId` (`soundId`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Table structure for table `aowow_reports` --- - -DROP TABLE IF EXISTS `aowow_reports`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!40101 SET character_set_client = utf8 */; -CREATE TABLE `aowow_reports` ( - `id` mediumint(8) unsigned NOT NULL AUTO_INCREMENT, - `userId` mediumint(8) unsigned NOT NULL, - `assigned` mediumint(8) unsigned NOT NULL DEFAULT 0, - `status` tinyint(3) unsigned NOT NULL DEFAULT 0 COMMENT '0:new; 1:solved; 2:rejected', - `mode` tinyint(3) unsigned NOT NULL, - `reason` tinyint(3) unsigned NOT NULL, - `subject` mediumint(9) NOT NULL DEFAULT 0, - `ip` varchar(50) NOT NULL, - `description` text NOT NULL, - `userAgent` varchar(255) NOT NULL, - `appName` varchar(32) NOT NULL, - `url` varchar(255) NOT NULL, - `relatedUrl` varchar(255) DEFAULT NULL, - `email` varchar(255) DEFAULT NULL, - PRIMARY KEY (`id`), - KEY `userId` (`userId`) -) ENGINE=MyISAM DEFAULT CHARSET=utf8; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Table structure for table `aowow_scalingstatdistribution` --- - -DROP TABLE IF EXISTS `aowow_scalingstatdistribution`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!40101 SET character_set_client = utf8 */; -CREATE TABLE `aowow_scalingstatdistribution` ( - `id` smallint(5) unsigned NOT NULL, - `statMod1` tinyint(4) NOT NULL, - `statMod2` tinyint(4) NOT NULL, - `statMod3` tinyint(4) NOT NULL, - `statMod4` tinyint(4) NOT NULL, - `statMod5` tinyint(4) NOT NULL, - `statMod6` tinyint(4) NOT NULL, - `statMod7` tinyint(4) NOT NULL, - `statMod8` tinyint(4) NOT NULL, - `statMod9` tinyint(4) NOT NULL, - `statMod10` tinyint(4) NOT NULL, - `modifier1` smallint(5) unsigned NOT NULL, - `modifier2` smallint(5) unsigned NOT NULL, - `modifier3` smallint(5) unsigned NOT NULL, - `modifier4` smallint(5) unsigned NOT NULL, - `modifier5` smallint(5) unsigned NOT NULL, - `modifier6` smallint(5) unsigned NOT NULL, - `modifier7` smallint(5) unsigned NOT NULL, - `modifier8` smallint(5) unsigned NOT NULL, - `modifier9` smallint(5) unsigned NOT NULL, - `modifier10` smallint(5) unsigned NOT NULL, - `maxLevel` tinyint(3) unsigned NOT NULL, - PRIMARY KEY (`id`) -) ENGINE=MyISAM DEFAULT CHARSET=utf8; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Table structure for table `aowow_scalingstatvalues` --- - -DROP TABLE IF EXISTS `aowow_scalingstatvalues`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!40101 SET character_set_client = utf8 */; -CREATE TABLE `aowow_scalingstatvalues` ( - `id` tinyint(3) unsigned NOT NULL, - `shoulderMultiplier` tinyint(3) unsigned NOT NULL, - `trinketMultiplier` tinyint(3) unsigned NOT NULL, - `weaponMultiplier` tinyint(3) unsigned NOT NULL, - `rangedMultiplier` tinyint(3) unsigned NOT NULL, - `clothShoulderArmor` tinyint(3) unsigned NOT NULL, - `leatherShoulderArmor` smallint(5) unsigned NOT NULL, - `mailShoulderArmor` smallint(5) unsigned NOT NULL, - `plateShoulderArmor` smallint(5) unsigned NOT NULL, - `weaponDPS1H` tinyint(3) unsigned NOT NULL, - `weaponDPS2H` tinyint(3) unsigned NOT NULL, - `casterDPS1H` tinyint(3) unsigned NOT NULL, - `casterDPS2H` tinyint(3) unsigned NOT NULL, - `rangedDPS` tinyint(3) unsigned NOT NULL, - `wandDPS` tinyint(3) unsigned NOT NULL, - `spellPower` smallint(5) unsigned NOT NULL, - `primBudged` tinyint(3) unsigned NOT NULL, - `tertBudged` tinyint(3) unsigned NOT NULL, - `clothCloakArmor` tinyint(3) unsigned NOT NULL, - `clothChestArmor` smallint(5) unsigned NOT NULL, - `leatherChestArmor` smallint(5) unsigned NOT NULL, - `mailChestArmor` smallint(5) unsigned NOT NULL, - `plateChestArmor` smallint(5) unsigned NOT NULL, - PRIMARY KEY (`id`) -) ENGINE=MyISAM DEFAULT CHARSET=utf8; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Table structure for table `aowow_screenshots` --- - -DROP TABLE IF EXISTS `aowow_screenshots`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!40101 SET character_set_client = utf8 */; -CREATE TABLE `aowow_screenshots` ( - `id` int(16) unsigned NOT NULL AUTO_INCREMENT, - `type` smallint(5) unsigned NOT NULL, - `typeId` mediumint(9) NOT NULL, - `userIdOwner` int(10) unsigned DEFAULT NULL, - `date` int(32) unsigned NOT NULL, - `width` smallint(5) unsigned NOT NULL, - `height` smallint(5) unsigned NOT NULL, - `caption` varchar(250) DEFAULT NULL, - `status` tinyint(3) unsigned NOT NULL COMMENT 'see defines.php - CC_FLAG_*', - `userIdApprove` int(10) unsigned DEFAULT NULL, - `userIdDelete` int(10) unsigned DEFAULT NULL, - PRIMARY KEY (`id`), - KEY `type` (`type`,`typeId`), - KEY `FK_acc_ss` (`userIdOwner`), - CONSTRAINT `FK_acc_ss` FOREIGN KEY (`userIdOwner`) REFERENCES `aowow_account` (`id`) ON DELETE SET NULL ON UPDATE CASCADE -) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Table structure for table `aowow_shapeshiftforms` --- - -DROP TABLE IF EXISTS `aowow_shapeshiftforms`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!40101 SET character_set_client = utf8 */; -CREATE TABLE `aowow_shapeshiftforms` ( - `Id` bigint(20) NOT NULL, - `flags` bigint(20) NOT NULL, - `creatureType` bigint(20) NOT NULL, - `displayIdA` bigint(20) NOT NULL, - `displayIdH` bigint(20) NOT NULL, - `spellId1` bigint(20) NOT NULL, - `spellId2` bigint(20) NOT NULL, - `spellId3` bigint(20) NOT NULL, - `spellId4` bigint(20) NOT NULL, - `spellId5` bigint(20) NOT NULL, - `spellId6` bigint(20) NOT NULL, - `spellId7` bigint(20) NOT NULL, - `spellId8` bigint(20) NOT NULL, - `comment` varchar(30) DEFAULT NULL, - PRIMARY KEY (`Id`) -) ENGINE=MyISAM DEFAULT CHARSET=utf8; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Table structure for table `aowow_skillline` --- - -DROP TABLE IF EXISTS `aowow_skillline`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!40101 SET character_set_client = utf8 */; -CREATE TABLE `aowow_skillline` ( - `Id` smallint(5) unsigned NOT NULL, - `typeCat` tinyint(4) NOT NULL, - `cuFlags` int(10) unsigned NOT NULL, - `categoryId` tinyint(3) unsigned NOT NULL, - `name_loc0` varchar(64) NOT NULL, - `name_loc2` varchar(64) NOT NULL, - `name_loc3` varchar(64) NOT NULL, - `name_loc6` varchar(64) NOT NULL, - `name_loc8` varchar(64) NOT NULL, - `description_loc0` text NOT NULL, - `description_loc2` text NOT NULL, - `description_loc3` text NOT NULL, - `description_loc6` text NOT NULL, - `description_loc8` text NOT NULL, - `iconId` smallint(5) unsigned NOT NULL DEFAULT 0, - `iconIdBak` smallint(5) unsigned NOT NULL DEFAULT 0, - `professionMask` smallint(5) unsigned NOT NULL, - `recipeSubClass` tinyint(3) unsigned NOT NULL, - `specializations` varchar(30) NOT NULL COMMENT 'space-separated spellIds', - PRIMARY KEY (`Id`) -) ENGINE=MyISAM DEFAULT CHARSET=utf8; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Table structure for table `aowow_sounds` --- - -DROP TABLE IF EXISTS `aowow_sounds`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!40101 SET character_set_client = utf8 */; -CREATE TABLE `aowow_sounds` ( - `id` smallint(5) unsigned NOT NULL, - `cat` tinyint(3) unsigned NOT NULL, - `name` varchar(100) NOT NULL, - `cuFlags` int(10) unsigned NOT NULL, - `soundFile1` smallint(5) unsigned DEFAULT NULL, - `soundFile2` smallint(5) unsigned DEFAULT NULL, - `soundFile3` smallint(5) unsigned DEFAULT NULL, - `soundFile4` smallint(5) unsigned DEFAULT NULL, - `soundFile5` smallint(5) unsigned DEFAULT NULL, - `soundFile6` smallint(5) unsigned DEFAULT NULL, - `soundFile7` smallint(5) unsigned DEFAULT NULL, - `soundFile8` smallint(5) unsigned DEFAULT NULL, - `soundFile9` smallint(5) unsigned DEFAULT NULL, - `soundFile10` smallint(5) unsigned DEFAULT NULL, - `flags` mediumint(8) unsigned NOT NULL, - PRIMARY KEY (`id`), - KEY `cat` (`cat`), - KEY `name` (`name`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Table structure for table `aowow_sounds_files` --- - -DROP TABLE IF EXISTS `aowow_sounds_files`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!40101 SET character_set_client = utf8 */; -CREATE TABLE `aowow_sounds_files` ( - `id` smallint(6) NOT NULL COMMENT '<0 not found in client files', - `file` varchar(75) NOT NULL, - `path` varchar(75) NOT NULL COMMENT 'in client', - `type` tinyint(1) unsigned NOT NULL COMMENT '1: ogg; 2: mp3', - PRIMARY KEY (`id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Table structure for table `aowow_source` --- - -DROP TABLE IF EXISTS `aowow_source`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!40101 SET character_set_client = utf8 */; -CREATE TABLE `aowow_source` ( - `type` tinyint(4) unsigned NOT NULL, - `typeId` mediumint(9) unsigned NOT NULL, - `moreType` tinyint(4) unsigned DEFAULT NULL, - `moreTypeId` mediumint(9) unsigned DEFAULT NULL, - `moreZoneId` mediumint(9) unsigned DEFAULT NULL, - `src1` tinyint(1) unsigned DEFAULT NULL COMMENT 'Crafted', - `src2` tinyint(3) unsigned DEFAULT NULL COMMENT 'Drop (npc / object / item) (modeMask)', - `src3` tinyint(3) unsigned DEFAULT NULL COMMENT 'PvP (g_sources_pvp)', - `src4` tinyint(3) unsigned DEFAULT NULL COMMENT 'Quest (side)', - `src5` tinyint(1) unsigned DEFAULT NULL COMMENT 'Vendor', - `src6` tinyint(1) unsigned DEFAULT NULL COMMENT 'Trainer', - `src7` tinyint(1) unsigned DEFAULT NULL COMMENT 'Discovery', - `src8` tinyint(1) unsigned DEFAULT NULL COMMENT 'Redemption', - `src9` tinyint(1) unsigned DEFAULT NULL COMMENT 'Talent', - `src10` tinyint(1) unsigned DEFAULT NULL COMMENT 'Starter', - `src11` tinyint(1) unsigned DEFAULT NULL COMMENT 'Event (special; not holidays) [not used]', - `src12` tinyint(1) unsigned DEFAULT NULL COMMENT 'Achievemement', - `src13` tinyint(3) unsigned DEFAULT NULL COMMENT 'Misc Source (sourceStringId)', - `src14` tinyint(1) unsigned DEFAULT NULL COMMENT 'Black Market [not used]', - `src15` tinyint(1) unsigned DEFAULT NULL COMMENT 'Disenchanted', - `src16` tinyint(1) unsigned DEFAULT NULL COMMENT 'Fished', - `src17` tinyint(1) unsigned DEFAULT NULL COMMENT 'Gathered', - `src18` tinyint(1) unsigned DEFAULT NULL COMMENT 'Milled', - `src19` tinyint(1) unsigned DEFAULT NULL COMMENT 'Mined', - `src20` tinyint(1) unsigned DEFAULT NULL COMMENT 'Prospected', - `src21` tinyint(1) unsigned DEFAULT NULL COMMENT 'Pickpocketed', - `src22` tinyint(1) unsigned DEFAULT NULL COMMENT 'Salvaged', - `src23` tinyint(1) unsigned DEFAULT NULL COMMENT 'Skinned', - `src24` tinyint(1) unsigned DEFAULT NULL COMMENT 'In-Game Store [not used]', - PRIMARY KEY (`type`,`typeId`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Table structure for table `aowow_sourcestrings` --- - -DROP TABLE IF EXISTS `aowow_sourcestrings`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!40101 SET character_set_client = utf8 */; -CREATE TABLE `aowow_sourcestrings` ( - `id` int(16) NOT NULL, - `source_loc0` varchar(128) NOT NULL, - `source_loc2` varchar(128) NOT NULL, - `source_loc3` varchar(128) NOT NULL, - `source_loc6` varchar(128) NOT NULL, - `source_loc8` varchar(128) NOT NULL, - PRIMARY KEY (`id`), - KEY `Id` (`id`) -) ENGINE=MyISAM DEFAULT CHARSET=utf8; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Table structure for table `aowow_spawns` --- - -DROP TABLE IF EXISTS `aowow_spawns`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!40101 SET character_set_client = utf8 */; -CREATE TABLE `aowow_spawns` ( - `guid` int(11) NOT NULL COMMENT '< 0: vehicle accessory', - `type` smallint(5) unsigned NOT NULL, - `typeId` int(10) unsigned NOT NULL, - `respawn` int(10) unsigned NOT NULL COMMENT 'in seconds', - `spawnMask` tinyint(3) unsigned NOT NULL, - `phaseMask` smallint(5) unsigned NOT NULL, - `areaId` smallint(5) unsigned NOT NULL, - `floor` tinyint(3) unsigned NOT NULL, - `posX` float unsigned NOT NULL, - `posY` float unsigned NOT NULL, - `pathId` int(10) unsigned NOT NULL, - PRIMARY KEY (`guid`,`type`,`floor`), - KEY `type_idx` (`typeId`,`type`), - KEY `zone_idx` (`areaId`), - KEY `guid` (`guid`) -) ENGINE=MyISAM DEFAULT CHARSET=utf8; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Table structure for table `aowow_spell` --- - -DROP TABLE IF EXISTS `aowow_spell`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!40101 SET character_set_client = utf8 */; -CREATE TABLE `aowow_spell` ( - `id` mediumint(8) unsigned NOT NULL, - `category` smallint(5) unsigned NOT NULL, - `dispelType` tinyint(3) unsigned NOT NULL, - `mechanic` tinyint(3) unsigned NOT NULL, - `attributes0` int(10) unsigned NOT NULL, - `attributes1` int(10) unsigned NOT NULL, - `attributes2` int(10) unsigned NOT NULL, - `attributes3` int(10) unsigned NOT NULL, - `attributes4` int(10) unsigned NOT NULL, - `attributes5` int(10) unsigned NOT NULL, - `attributes6` int(10) unsigned NOT NULL, - `attributes7` int(10) unsigned NOT NULL, - `cuFlags` int(10) unsigned NOT NULL, - `typeCat` smallint(6) NOT NULL, - `stanceMask` int(10) unsigned NOT NULL, - `stanceMaskNot` int(10) unsigned NOT NULL, - `spellFocusObject` smallint(5) unsigned NOT NULL, - `castTime` mediumint(8) unsigned NOT NULL, - `recoveryTime` int(10) unsigned NOT NULL, - `recoveryCategory` int(10) unsigned NOT NULL, - `startRecoveryTime` mediumint(8) unsigned NOT NULL, - `startRecoveryCategory` smallint(5) unsigned NOT NULL, - `procChance` tinyint(3) unsigned NOT NULL, - `procCharges` tinyint(3) unsigned NOT NULL, - `procCustom` float NOT NULL, - `procCooldown` smallint(6) unsigned NOT NULL, - `maxLevel` tinyint(3) unsigned NOT NULL, - `baseLevel` tinyint(3) unsigned NOT NULL, - `spellLevel` tinyint(3) unsigned NOT NULL, - `talentLevel` tinyint(3) unsigned NOT NULL, - `duration` int(16) NOT NULL DEFAULT 0, - `powerType` tinyint(4) NOT NULL, - `powerCost` smallint(5) unsigned NOT NULL, - `powerCostPerLevel` tinyint(3) unsigned NOT NULL, - `powerCostPercent` tinyint(3) unsigned NOT NULL, - `powerPerSecond` smallint(5) unsigned NOT NULL, - `powerPerSecondPerLevel` tinyint(3) unsigned NOT NULL, - `powerGainRunicPower` smallint(5) unsigned NOT NULL, - `powerCostRunes` smallint(5) unsigned NOT NULL, - `rangeId` smallint(5) unsigned NOT NULL, - `stackAmount` smallint(5) unsigned NOT NULL, - `tool1` mediumint(8) unsigned NOT NULL, - `tool2` mediumint(8) unsigned NOT NULL, - `toolCategory1` tinyint(3) unsigned NOT NULL, - `toolCategory2` tinyint(3) unsigned NOT NULL, - `reagent1` mediumint(8) unsigned NOT NULL, - `reagent2` mediumint(8) unsigned NOT NULL, - `reagent3` mediumint(8) unsigned NOT NULL, - `reagent4` mediumint(8) unsigned NOT NULL, - `reagent5` mediumint(8) unsigned NOT NULL, - `reagent6` mediumint(8) unsigned NOT NULL, - `reagent7` mediumint(8) unsigned NOT NULL, - `reagent8` mediumint(8) unsigned NOT NULL, - `reagentCount1` tinyint(3) unsigned NOT NULL, - `reagentCount2` tinyint(3) unsigned NOT NULL, - `reagentCount3` tinyint(3) unsigned NOT NULL, - `reagentCount4` tinyint(3) unsigned NOT NULL, - `reagentCount5` tinyint(3) unsigned NOT NULL, - `reagentCount6` tinyint(3) unsigned NOT NULL, - `reagentCount7` tinyint(3) unsigned NOT NULL, - `reagentCount8` tinyint(3) unsigned NOT NULL, - `equippedItemClass` tinyint(4) NOT NULL, - `equippedItemSubClassMask` int(11) NOT NULL, - `equippedItemInventoryTypeMask` int(10) unsigned NOT NULL, - `effect1Id` smallint(5) unsigned NOT NULL, - `effect2Id` smallint(5) unsigned NOT NULL, - `effect3Id` smallint(5) unsigned NOT NULL, - `effect1DieSides` mediumint(9) NOT NULL, - `effect2DieSides` mediumint(9) NOT NULL, - `effect3DieSides` mediumint(9) NOT NULL, - `effect1RealPointsPerLevel` float NOT NULL, - `effect2RealPointsPerLevel` float NOT NULL, - `effect3RealPointsPerLevel` float NOT NULL, - `effect1BasePoints` int(11) NOT NULL, - `effect2BasePoints` int(11) NOT NULL, - `effect3BasePoints` int(11) NOT NULL, - `effect1Mechanic` tinyint(3) unsigned NOT NULL, - `effect2Mechanic` tinyint(3) unsigned NOT NULL, - `effect3Mechanic` tinyint(3) unsigned NOT NULL, - `effect1ImplicitTargetA` smallint(6) NOT NULL, - `effect2ImplicitTargetA` smallint(6) NOT NULL, - `effect3ImplicitTargetA` smallint(6) NOT NULL, - `effect1ImplicitTargetB` smallint(6) NOT NULL, - `effect2ImplicitTargetB` smallint(6) NOT NULL, - `effect3ImplicitTargetB` smallint(6) NOT NULL, - `effect1RadiusMin` smallint(5) unsigned NOT NULL, - `effect1RadiusMax` smallint(5) unsigned NOT NULL DEFAULT 0, - `effect2RadiusMin` smallint(5) unsigned NOT NULL, - `effect2RadiusMax` smallint(5) unsigned NOT NULL DEFAULT 0, - `effect3RadiusMin` smallint(5) unsigned NOT NULL, - `effect3RadiusMax` smallint(5) unsigned NOT NULL DEFAULT 0, - `effect1AuraId` smallint(5) unsigned NOT NULL, - `effect2AuraId` smallint(5) unsigned NOT NULL, - `effect3AuraId` smallint(5) unsigned NOT NULL, - `effect1Periode` mediumint(8) unsigned NOT NULL, - `effect2Periode` mediumint(8) unsigned NOT NULL, - `effect3Periode` mediumint(8) unsigned NOT NULL, - `effect1ValueMultiplier` float NOT NULL, - `effect2ValueMultiplier` float NOT NULL, - `effect3ValueMultiplier` float NOT NULL, - `effect1ChainTarget` smallint(5) unsigned NOT NULL, - `effect2ChainTarget` smallint(5) unsigned NOT NULL, - `effect3ChainTarget` smallint(5) unsigned NOT NULL, - `effect1CreateItemId` mediumint(8) unsigned NOT NULL, - `effect2CreateItemId` mediumint(8) unsigned NOT NULL, - `effect3CreateItemId` mediumint(8) unsigned NOT NULL, - `effect1MiscValue` int(11) NOT NULL, - `effect2MiscValue` int(11) NOT NULL, - `effect3MiscValue` int(11) NOT NULL, - `effect1MiscValueB` mediumint(9) NOT NULL, - `effect2MiscValueB` mediumint(9) NOT NULL, - `effect3MiscValueB` mediumint(9) NOT NULL, - `effect1TriggerSpell` mediumint(9) NOT NULL, - `effect2TriggerSpell` mediumint(9) NOT NULL, - `effect3TriggerSpell` mediumint(9) NOT NULL, - `effect1PointsPerComboPoint` mediumint(9) NOT NULL, - `effect2PointsPerComboPoint` mediumint(9) NOT NULL, - `effect3PointsPerComboPoint` mediumint(9) NOT NULL, - `effect1SpellClassMaskA` int(10) unsigned NOT NULL, - `effect2SpellClassMaskA` int(10) unsigned NOT NULL, - `effect3SpellClassMaskA` int(10) unsigned NOT NULL, - `effect1SpellClassMaskB` int(10) unsigned NOT NULL, - `effect2SpellClassMaskB` int(10) unsigned NOT NULL, - `effect3SpellClassMaskB` int(10) unsigned NOT NULL, - `effect1SpellClassMaskC` int(10) unsigned NOT NULL, - `effect2SpellClassMaskC` int(10) unsigned NOT NULL, - `effect3SpellClassMaskC` int(10) unsigned NOT NULL, - `effect1DamageMultiplier` float NOT NULL, - `effect2DamageMultiplier` float NOT NULL, - `effect3DamageMultiplier` float NOT NULL, - `effect1BonusMultiplier` float NOT NULL, - `effect2BonusMultiplier` float NOT NULL, - `effect3BonusMultiplier` float NOT NULL, - `iconId` smallint(5) unsigned NOT NULL DEFAULT 0, - `iconIdBak` smallint(5) unsigned NOT NULL DEFAULT 0, - `iconIdAlt` smallint(5) unsigned NOT NULL DEFAULT 0, - `rankNo` tinyint(3) unsigned NOT NULL, - `spellVisualId` smallint(5) unsigned NOT NULL, - `name_loc0` varchar(85) NOT NULL, - `name_loc2` varchar(85) NOT NULL, - `name_loc3` varchar(85) NOT NULL, - `name_loc6` varchar(91) NOT NULL, - `name_loc8` varchar(50) NOT NULL, - `rank_loc0` varchar(21) NOT NULL, - `rank_loc2` varchar(24) NOT NULL, - `rank_loc3` varchar(22) NOT NULL, - `rank_loc6` varchar(27) NOT NULL, - `rank_loc8` varchar(29) NOT NULL, - `description_loc0` text NOT NULL, - `description_loc2` text NOT NULL, - `description_loc3` text NOT NULL, - `description_loc6` text NOT NULL, - `description_loc8` text NOT NULL, - `buff_loc0` text NOT NULL, - `buff_loc2` text NOT NULL, - `buff_loc3` text NOT NULL, - `buff_loc6` text NOT NULL, - `buff_loc8` text NOT NULL, - `maxTargetLevel` tinyint(3) unsigned NOT NULL, - `spellFamilyId` tinyint(3) unsigned NOT NULL, - `spellFamilyFlags1` int(10) unsigned NOT NULL, - `spellFamilyFlags2` int(10) unsigned NOT NULL, - `spellFamilyFlags3` int(10) unsigned NOT NULL, - `maxAffectedTargets` tinyint(3) unsigned NOT NULL, - `damageClass` tinyint(3) unsigned NOT NULL, - `skillLine1` smallint(6) NOT NULL DEFAULT 0, - `skillLine2OrMask` bigint(32) NOT NULL DEFAULT 0, - `reqRaceMask` smallint(5) unsigned NOT NULL DEFAULT 0, - `reqClassMask` smallint(5) unsigned NOT NULL DEFAULT 0, - `reqSpellId` mediumint(8) unsigned NOT NULL DEFAULT 0, - `reqSkillLevel` smallint(5) unsigned NOT NULL DEFAULT 0, - `learnedAt` smallint(6) unsigned NOT NULL DEFAULT 0, - `skillLevelGrey` smallint(6) unsigned NOT NULL DEFAULT 0, - `skillLevelYellow` smallint(6) unsigned NOT NULL DEFAULT 0, - `schoolMask` tinyint(3) unsigned NOT NULL, - `spellDescriptionVariableId` tinyint(3) unsigned NOT NULL, - `trainingCost` int(10) unsigned NOT NULL, - PRIMARY KEY (`id`), - KEY `category` (`typeCat`), - KEY `spell` (`id`) USING BTREE, - KEY `effects` (`effect1Id`,`effect2Id`,`effect3Id`), - KEY `items` (`effect1CreateItemId`,`effect2CreateItemId`,`effect3CreateItemId`), - KEY `iconId` (`iconId`) -) ENGINE=MyISAM DEFAULT CHARSET=utf8; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Table structure for table `aowow_spell_sounds` --- - -DROP TABLE IF EXISTS `aowow_spell_sounds`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!40101 SET character_set_client = utf8 */; -CREATE TABLE `aowow_spell_sounds` ( - `id` smallint(5) unsigned NOT NULL COMMENT 'SpellVisual.dbc/id', - `animation` smallint(5) unsigned NOT NULL, - `ready` smallint(5) unsigned NOT NULL, - `precast` smallint(5) unsigned NOT NULL, - `cast` smallint(5) unsigned NOT NULL, - `impact` smallint(5) unsigned NOT NULL, - `state` smallint(5) unsigned NOT NULL, - `statedone` smallint(5) unsigned NOT NULL, - `channel` smallint(5) unsigned NOT NULL, - `casterimpact` smallint(5) unsigned NOT NULL, - `targetimpact` smallint(5) unsigned NOT NULL, - `castertargeting` smallint(5) unsigned NOT NULL, - `missiletargeting` smallint(5) unsigned NOT NULL, - `instantarea` smallint(5) unsigned NOT NULL, - `persistentarea` smallint(5) unsigned NOT NULL, - `casterstate` smallint(5) unsigned NOT NULL, - `targetstate` smallint(5) unsigned NOT NULL, - `missile` smallint(5) unsigned NOT NULL COMMENT 'not predicted by js', - `impactarea` smallint(5) unsigned NOT NULL COMMENT 'not predicted by js', - PRIMARY KEY (`id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='!ATTENTION!\r\nthe primary key of this table is NOT a spellId, but spellVisualId\r\n\r\ncolumn names from LANG.sound_activities'; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Table structure for table `aowow_spelldifficulty` --- - -DROP TABLE IF EXISTS `aowow_spelldifficulty`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!40101 SET character_set_client = utf8 */; -CREATE TABLE `aowow_spelldifficulty` ( - `normal10` mediumint(8) unsigned NOT NULL, - `normal25` mediumint(8) unsigned NOT NULL, - `heroic10` mediumint(8) unsigned NOT NULL, - `heroic25` mediumint(8) unsigned NOT NULL, - KEY `normal10` (`normal10`), - KEY `normal25` (`normal25`), - KEY `heroic10` (`heroic10`), - KEY `heroic25` (`heroic25`) -) ENGINE=MyISAM DEFAULT CHARSET=utf8; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Table structure for table `aowow_spellfocusobject` --- - -DROP TABLE IF EXISTS `aowow_spellfocusobject`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!40101 SET character_set_client = utf8 */; -CREATE TABLE `aowow_spellfocusobject` ( - `id` smallint(5) unsigned NOT NULL, - `name_loc0` varchar(83) NOT NULL, - `name_loc2` varchar(89) NOT NULL, - `name_loc3` varchar(95) NOT NULL, - `name_loc6` varchar(90) NOT NULL, - `name_loc8` varchar(91) NOT NULL, - PRIMARY KEY (`id`) -) ENGINE=MyISAM DEFAULT CHARSET=utf8; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Table structure for table `aowow_spelloverride` --- - -DROP TABLE IF EXISTS `aowow_spelloverride`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!40101 SET character_set_client = utf8 */; -CREATE TABLE `aowow_spelloverride` ( - `id` bigint(20) NOT NULL, - `spellId1` bigint(20) NOT NULL, - `spellId2` bigint(20) NOT NULL, - `spellId3` bigint(20) NOT NULL, - `spellId4` bigint(20) NOT NULL, - `spellId5` bigint(20) NOT NULL, - PRIMARY KEY (`id`) -) ENGINE=MyISAM DEFAULT CHARSET=utf8; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Table structure for table `aowow_spellrange` --- - -DROP TABLE IF EXISTS `aowow_spellrange`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!40101 SET character_set_client = utf8 */; -CREATE TABLE `aowow_spellrange` ( - `id` tinyint(3) unsigned NOT NULL, - `rangeMinHostile` tinyint(3) unsigned NOT NULL, - `rangeMinFriend` tinyint(3) unsigned NOT NULL, - `rangeMaxHostile` smallint(5) unsigned NOT NULL, - `rangeMaxFriend` smallint(5) unsigned NOT NULL, - `rangeType` tinyint(3) unsigned NOT NULL, - `name_loc0` varchar(27) NOT NULL, - `name_loc2` varchar(27) NOT NULL, - `name_loc3` varchar(27) NOT NULL, - `name_loc6` varchar(27) NOT NULL, - `name_loc8` varchar(27) NOT NULL, - PRIMARY KEY (`id`) -) ENGINE=MyISAM DEFAULT CHARSET=utf8; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Table structure for table `aowow_spellvariables` --- - -DROP TABLE IF EXISTS `aowow_spellvariables`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!40101 SET character_set_client = utf8 */; -CREATE TABLE `aowow_spellvariables` ( - `id` tinyint(3) unsigned NOT NULL, - `vars` varchar(368) NOT NULL, - PRIMARY KEY (`id`) -) ENGINE=MyISAM DEFAULT CHARSET=utf8; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Table structure for table `aowow_talents` --- - -DROP TABLE IF EXISTS `aowow_talents`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!40101 SET character_set_client = utf8 */; -CREATE TABLE `aowow_talents` ( - `id` smallint(5) unsigned NOT NULL, - `class` tinyint(3) unsigned NOT NULL, - `petTypeMask` tinyint(3) unsigned NOT NULL, - `tab` tinyint(3) unsigned NOT NULL, - `row` tinyint(3) unsigned NOT NULL, - `col` tinyint(3) unsigned NOT NULL, - `spell` mediumint(8) unsigned NOT NULL, - `rank` tinyint(3) unsigned NOT NULL, - PRIMARY KEY (`id`,`rank`), - KEY `spell` (`spell`), - KEY `class` (`class`) -) ENGINE=MyISAM DEFAULT CHARSET=utf8; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Table structure for table `aowow_taxinodes` --- - -DROP TABLE IF EXISTS `aowow_taxinodes`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!40101 SET character_set_client = utf8 */; -CREATE TABLE `aowow_taxinodes` ( - `id` smallint(5) unsigned NOT NULL, - `mapId` smallint(6) unsigned NOT NULL, - `posX` float unsigned NOT NULL, - `posY` float unsigned NOT NULL, - `type` tinyint(4) unsigned NOT NULL COMMENT 'usually NPC (1) but could support GOs (2)', - `typeId` mediumint(9) unsigned NOT NULL, - `reactA` tinyint(4) NOT NULL, - `reactH` tinyint(4) NOT NULL, - `name_loc0` varchar(46) NOT NULL, - `name_loc2` varchar(62) NOT NULL, - `name_loc3` varchar(55) NOT NULL, - `name_loc6` varchar(63) NOT NULL, - `name_loc8` varchar(50) NOT NULL, - PRIMARY KEY (`id`) -) ENGINE=MyISAM DEFAULT CHARSET=utf8; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Table structure for table `aowow_taxipath` --- - -DROP TABLE IF EXISTS `aowow_taxipath`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!40101 SET character_set_client = utf8 */; -CREATE TABLE `aowow_taxipath` ( - `id` smallint(5) unsigned NOT NULL, - `startNodeId` smallint(6) unsigned NOT NULL, - `endNodeId` smallint(6) unsigned NOT NULL, - PRIMARY KEY (`id`) -) ENGINE=MyISAM DEFAULT CHARSET=utf8; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Table structure for table `aowow_titles` --- - -DROP TABLE IF EXISTS `aowow_titles`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!40101 SET character_set_client = utf8 */; -CREATE TABLE `aowow_titles` ( - `id` tinyint(3) unsigned NOT NULL, - `category` tinyint(3) unsigned NOT NULL, - `cuFlags` int(10) unsigned NOT NULL, - `gender` tinyint(3) unsigned NOT NULL, - `side` tinyint(3) unsigned NOT NULL, - `expansion` tinyint(3) unsigned NOT NULL, - `src12Ext` mediumint(9) unsigned NOT NULL, - `eventId` smallint(5) unsigned NOT NULL, - `bitIdx` tinyint(3) unsigned NOT NULL, - `male_loc0` varchar(33) NOT NULL, - `male_loc2` varchar(35) NOT NULL, - `male_loc3` varchar(37) NOT NULL, - `male_loc6` varchar(34) NOT NULL, - `male_loc8` varchar(37) NOT NULL, - `female_loc0` varchar(33) NOT NULL, - `female_loc2` varchar(35) NOT NULL, - `female_loc3` varchar(39) NOT NULL, - `female_loc6` varchar(35) NOT NULL, - `female_loc8` varchar(41) NOT NULL, - PRIMARY KEY (`id`), - KEY `bitIdx` (`bitIdx`) -) ENGINE=MyISAM DEFAULT CHARSET=utf8; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Table structure for table `aowow_totemcategory` --- - -DROP TABLE IF EXISTS `aowow_totemcategory`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!40101 SET character_set_client = utf8 */; -CREATE TABLE `aowow_totemcategory` ( - `id` tinyint(3) unsigned NOT NULL, - `name_loc0` varchar(29) NOT NULL, - `name_loc2` varchar(45) NOT NULL, - `name_loc3` varchar(31) NOT NULL, - `name_loc6` varchar(36) NOT NULL, - `name_loc8` varchar(69) NOT NULL, - `category` tinyint(3) unsigned NOT NULL, - `categoryMask` int(10) unsigned NOT NULL, - PRIMARY KEY (`id`) -) ENGINE=MyISAM DEFAULT CHARSET=utf8; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Table structure for table `aowow_videos` --- - -DROP TABLE IF EXISTS `aowow_videos`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!40101 SET character_set_client = utf8 */; -CREATE TABLE `aowow_videos` ( - `id` int(16) NOT NULL AUTO_INCREMENT, - `type` smallint(5) unsigned NOT NULL, - `typeId` mediumint(9) NOT NULL, - `userIdOwner` int(10) unsigned DEFAULT NULL, - `date` int(32) NOT NULL, - `videoId` varchar(12) NOT NULL, - `caption` text DEFAULT NULL, - `status` int(8) NOT NULL, - `userIdApprove` int(10) unsigned DEFAULT NULL, - `userIdeDelete` int(10) unsigned DEFAULT NULL, - PRIMARY KEY (`id`), - KEY `type` (`type`,`typeId`), - KEY `FK_acc_vi` (`userIdOwner`), - CONSTRAINT `FK_acc_vi` FOREIGN KEY (`userIdOwner`) REFERENCES `aowow_account` (`id`) ON DELETE SET NULL ON UPDATE CASCADE -) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Table structure for table `aowow_zones` --- - -DROP TABLE IF EXISTS `aowow_zones`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!40101 SET character_set_client = utf8 */; -CREATE TABLE `aowow_zones` ( - `id` smallint(5) unsigned NOT NULL COMMENT 'Zone Id', - `mapId` smallint(5) unsigned NOT NULL COMMENT 'Map Identifier', - `mapIdBak` smallint(5) unsigned NOT NULL, - `parentArea` smallint(6) unsigned NOT NULL, - `category` tinyint(4) unsigned NOT NULL, - `flags` int(11) unsigned NOT NULL, - `cuFlags` int(10) unsigned NOT NULL, - `faction` tinyint(2) unsigned NOT NULL, - `expansion` tinyint(2) unsigned NOT NULL, - `type` tinyint(2) unsigned NOT NULL, - `maxPlayer` tinyint(4) NOT NULL, - `itemLevelReqN` smallint(5) unsigned NOT NULL, - `itemLevelReqH` smallint(5) unsigned NOT NULL, - `levelReq` tinyint(3) unsigned NOT NULL, - `levelReqLFG` tinyint(4) unsigned NOT NULL, - `levelHeroic` tinyint(3) unsigned NOT NULL, - `levelMin` tinyint(4) unsigned NOT NULL, - `levelMax` tinyint(4) unsigned NOT NULL, - `attunementsN` text NOT NULL COMMENT 'space separated; type:typeId', - `attunementsH` text NOT NULL COMMENT 'space separated; type:typeId', - `parentAreaId` smallint(5) unsigned NOT NULL, - `parentX` float NOT NULL, - `parentY` float NOT NULL, - `name_loc0` varchar(120) NOT NULL COMMENT 'Map Name', - `name_loc2` varchar(120) NOT NULL, - `name_loc3` varchar(120) NOT NULL, - `name_loc6` varchar(120) NOT NULL, - `name_loc8` varchar(120) NOT NULL, - PRIMARY KEY (`id`) -) ENGINE=MyISAM DEFAULT CHARSET=utf8; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Table structure for table `aowow_zones_sounds` --- - -DROP TABLE IF EXISTS `aowow_zones_sounds`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!40101 SET character_set_client = utf8 */; -CREATE TABLE `aowow_zones_sounds` ( - `id` smallint(5) unsigned NOT NULL, - `ambienceDay` smallint(5) unsigned NOT NULL, - `ambienceNight` smallint(5) unsigned NOT NULL, - `musicDay` smallint(5) unsigned NOT NULL, - `musicNight` smallint(5) unsigned NOT NULL, - `intro` smallint(5) unsigned NOT NULL, - `worldStateId` smallint(5) unsigned NOT NULL, - `worldStateValue` smallint(6) NOT NULL, - KEY `id` (`id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8; -/*!40101 SET character_set_client = @saved_cs_client */; -/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */; - -/*!40101 SET SQL_MODE=@OLD_SQL_MODE */; -/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */; -/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */; -/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */; -/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */; -/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */; -/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */; - --- Dump completed on 2018-03-26 18:57:18 --- MySQL dump 10.16 Distrib 10.2.10-MariaDB, for debian-linux-gnu (x86_64) --- --- Host: localhost Database: aowow --- ------------------------------------------------------ --- Server version 10.2.10-MariaDB-10.2.10+maria~xenial-log - -/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */; -/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */; -/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */; -/*!40101 SET NAMES utf8 */; -/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */; -/*!40103 SET TIME_ZONE='+00:00' */; -/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */; -/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */; -/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */; - --- --- Dumping data for table `aowow_account` --- - -LOCK TABLES `aowow_account` WRITE; -/*!40000 ALTER TABLE `aowow_account` DISABLE KEYS */; -INSERT INTO `aowow_account` VALUES (0,0,'','','AoWoW','',0,0,0,0,'','',0,0,0,0,'','','',1,0,0,0,''); -/*!40000 ALTER TABLE `aowow_account` ENABLE KEYS */; -UNLOCK TABLES; - --- --- Dumping data for table `aowow_account_weightscales` --- - -LOCK TABLES `aowow_account_weightscales` WRITE; -/*!40000 ALTER TABLE `aowow_account_weightscales` DISABLE KEYS */; -INSERT INTO `aowow_account_weightscales` VALUES (1,0,'arms',1,'ability_rogue_eviscerate'),(2,0,'fury',1,'ability_warrior_innerrage'),(3,0,'prot',1,'ability_warrior_defensivestance'),(4,0,'holy',2,'spell_holy_holybolt'),(5,0,'prot',2,'ability_paladin_shieldofthetemplar'),(6,0,'retrib',2,'spell_holy_auraoflight'),(7,0,'beast',3,'ability_hunter_beasttaming'),(8,0,'marks',3,'ability_marksmanship'),(9,0,'surv',3,'ability_hunter_swiftstrike'),(10,0,'assas',4,'ability_rogue_eviscerate'),(11,0,'combat',4,'ability_backstab'),(12,0,'subtle',4,'ability_stealth'),(13,0,'disc',5,'spell_holy_wordfortitude'),(14,0,'holy',5,'spell_holy_guardianspirit'),(15,0,'shadow',5,'spell_shadow_shadowwordpain'),(16,0,'blooddps',6,'spell_deathknight_bloodpresence'),(17,0,'frostdps',6,'spell_deathknight_frostpresence'),(18,0,'frosttank',6,'spell_deathknight_frostpresence'),(19,0,'unholydps',6,'spell_deathknight_unholypresence'),(20,0,'elem',7,'spell_nature_lightning'),(21,0,'enhance',7,'spell_nature_lightningshield'),(22,0,'resto',7,'spell_nature_magicimmunity'),(23,0,'arcane',8,'spell_holy_magicalsentry'),(24,0,'fire',8,'spell_fire_firebolt02'),(25,0,'frost',8,'spell_frost_frostbolt02'),(26,0,'afflic',9,'spell_shadow_deathcoil'),(27,0,'demo',9,'spell_shadow_metamorphosis'),(28,0,'destro',9,'spell_shadow_rainoffire'),(29,0,'balance',11,'spell_nature_starfall'),(30,0,'feraltank',11,'ability_racial_bearform'),(31,0,'resto',11,'spell_nature_healingtouch'),(32,0,'feraldps',11,'ability_druid_catform'); -/*!40000 ALTER TABLE `aowow_account_weightscales` ENABLE KEYS */; -UNLOCK TABLES; - --- --- Dumping data for table `aowow_account_weightscale_data` --- - -LOCK TABLES `aowow_account_weightscale_data` WRITE; -/*!40000 ALTER TABLE `aowow_account_weightscale_data` DISABLE KEYS */; -INSERT INTO `aowow_account_weightscale_data` VALUES (2,'exprtng',100),(2,'str',82),(2,'critstrkrtng',66),(2,'agi',53),(2,'armorpenrtng',52),(2,'hitrtng',48),(2,'hastertng',36),(2,'atkpwr',31),(2,'armor',5),(3,'sta',100),(3,'dodgertng',90),(3,'defrtng',86),(3,'block',81),(3,'agi',67),(3,'parryrtng',67),(3,'blockrtng',48),(3,'str',48),(3,'exprtng',19),(3,'hitrtng',10),(3,'armorpenrtng',10),(3,'critstrkrtng',7),(3,'armor',6),(3,'hastertng',1),(3,'atkpwr',1),(4,'int',100),(4,'manargn',88),(4,'splpwr',58),(4,'critstrkrtng',46),(4,'hastertng',35),(5,'sta',100),(5,'dodgertng',94),(5,'block',86),(5,'defrtng',86),(5,'exprtng',79),(5,'agi',76),(5,'parryrtng',76),(5,'hitrtng',58),(5,'blockrtng',52),(5,'str',50),(5,'armor',6),(5,'atkpwr',6),(5,'splpwr',4),(5,'critstrkrtng',3),(6,'mledps',470),(6,'hitrtng',100),(6,'str',80),(6,'exprtng',66),(6,'critstrkrtng',40),(6,'atkpwr',34),(6,'agi',32),(6,'hastertng',30),(6,'armorpenrtng',22),(6,'splpwr',9),(7,'rgddps',213),(7,'hitrtng',100),(7,'agi',58),(7,'critstrkrtng',40),(7,'int',37),(7,'atkpwr',30),(7,'armorpenrtng',28),(7,'hastertng',21),(8,'rgddps',379),(8,'hitrtng',100),(8,'agi',74),(8,'critstrkrtng',57),(8,'armorpenrtng',40),(8,'int',39),(8,'atkpwr',32),(8,'hastertng',24),(9,'rgddps',181),(9,'hitrtng',100),(9,'agi',76),(9,'critstrkrtng',42),(9,'int',35),(9,'hastertng',31),(9,'atkpwr',29),(9,'armorpenrtng',26),(10,'mledps',170),(10,'agi',100),(10,'exprtng',87),(10,'hitrtng',83),(10,'critstrkrtng',81),(10,'atkpwr',65),(10,'armorpenrtng',65),(10,'hastertng',64),(10,'str',55),(11,'mledps',220),(11,'armorpenrtng',100),(11,'agi',100),(11,'exprtng',82),(11,'hitrtng',80),(11,'critstrkrtng',75),(11,'hastertng',73),(11,'str',55),(11,'atkpwr',50),(12,'mledps',228),(12,'exprtng',100),(12,'agi',100),(12,'hitrtng',80),(12,'armorpenrtng',75),(12,'critstrkrtng',75),(12,'hastertng',75),(12,'str',55),(12,'atkpwr',50),(13,'splpwr',100),(13,'manargn',67),(13,'int',65),(13,'hastertng',59),(13,'critstrkrtng',48),(13,'spi',22),(14,'manargn',100),(14,'int',69),(14,'splpwr',60),(14,'spi',52),(14,'critstrkrtng',38),(14,'hastertng',31),(15,'hitrtng',100),(15,'shasplpwr',76),(15,'splpwr',76),(15,'critstrkrtng',54),(15,'hastertng',50),(15,'spi',16),(15,'int',16),(16,'mledps',360),(16,'armorpenrtng',100),(16,'str',99),(16,'hitrtng',91),(16,'exprtng',90),(16,'critstrkrtng',57),(16,'hastertng',55),(16,'atkpwr',36),(16,'armor',1),(17,'mledps',337),(17,'hitrtng',100),(17,'str',97),(17,'exprtng',81),(17,'armorpenrtng',61),(17,'critstrkrtng',45),(17,'atkpwr',35),(17,'hastertng',28),(17,'armor',1),(18,'mledps',419),(18,'parryrtng',100),(18,'hitrtng',97),(18,'str',96),(18,'defrtng',85),(18,'exprtng',69),(18,'dodgertng',61),(18,'agi',61),(18,'sta',61),(18,'critstrkrtng',49),(18,'atkpwr',41),(18,'armorpenrtng',31),(18,'armor',5),(19,'mledps',209),(19,'str',100),(19,'hitrtng',66),(19,'exprtng',51),(19,'hastertng',48),(19,'critstrkrtng',45),(19,'atkpwr',34),(19,'armorpenrtng',32),(19,'armor',1),(20,'hitrtng',100),(20,'splpwr',60),(20,'hastertng',56),(20,'critstrkrtng',40),(20,'int',11),(21,'mledps',135),(21,'hitrtng',100),(21,'exprtng',84),(21,'agi',55),(21,'int',55),(21,'critstrkrtng',55),(21,'hastertng',42),(21,'str',35),(21,'atkpwr',32),(21,'splpwr',29),(21,'armorpenrtng',26),(22,'manargn',100),(22,'int',85),(22,'splpwr',77),(22,'critstrkrtng',62),(22,'hastertng',35),(23,'hitrtng',100),(23,'hastertng',54),(23,'arcsplpwr',49),(23,'splpwr',49),(23,'critstrkrtng',37),(23,'int',34),(23,'frosplpwr',24),(23,'firsplpwr',24),(23,'spi',14),(24,'hitrtng',100),(24,'hastertng',53),(24,'firsplpwr',46),(24,'splpwr',46),(24,'critstrkrtng',43),(24,'frosplpwr',23),(24,'arcsplpwr',23),(24,'int',13),(25,'hitrtng',100),(25,'hastertng',42),(25,'frosplpwr',39),(25,'splpwr',39),(25,'arcsplpwr',19),(25,'firsplpwr',19),(25,'critstrkrtng',19),(25,'int',6),(26,'hitrtng',100),(26,'shasplpwr',72),(26,'splpwr',72),(26,'hastertng',61),(26,'critstrkrtng',38),(26,'firsplpwr',36),(26,'spi',34),(26,'int',15),(27,'hitrtng',100),(27,'hastertng',50),(27,'firsplpwr',45),(27,'shasplpwr',45),(27,'splpwr',45),(27,'critstrkrtng',31),(27,'spi',29),(27,'int',13),(28,'hitrtng',100),(28,'firsplpwr',47),(28,'splpwr',47),(28,'hastertng',46),(28,'spi',26),(28,'shasplpwr',23),(28,'critstrkrtng',16),(28,'int',13),(29,'hitrtng',100),(29,'splpwr',66),(29,'hastertng',54),(29,'critstrkrtng',43),(29,'spi',22),(29,'int',22),(30,'agi',100),(30,'sta',75),(30,'dodgertng',65),(30,'defrtng',60),(30,'exprtng',16),(30,'str',10),(30,'armor',10),(30,'hitrtng',8),(30,'hastertng',5),(30,'atkpwr',4),(30,'feratkpwr',4),(30,'critstrkrtng',3),(31,'splpwr',100),(31,'manargn',73),(31,'hastertng',57),(31,'int',51),(31,'spi',32),(31,'critstrkrtng',11),(32,'agi',100),(32,'armorpenrtng',90),(32,'str',80),(32,'critstrkrtng',55),(32,'exprtng',50),(32,'hitrtng',50),(32,'feratkpwr',40),(32,'atkpwr',40),(32,'hastertng',35); -/*!40000 ALTER TABLE `aowow_account_weightscale_data` ENABLE KEYS */; -UNLOCK TABLES; - --- --- Dumping data for table `aowow_announcements` --- - -LOCK TABLES `aowow_announcements` WRITE; -/*!40000 ALTER TABLE `aowow_announcements` DISABLE KEYS */; -INSERT INTO `aowow_announcements` VALUES (4,'compare','Help: Item Comparison Tool',0,'padding-left: 55px; background-image: url(STATIC_URL/images/announcements/help-small.png); background-position: 10px center',1,1,'First time? - Don\'t be shy! Just check out our [url=?help=item-comparison]Help page[/url]!','Première visite? - Ne soyez pas intimidé! Vous n\'avez qu\'à lire notre [url=?help=item-comparison]page d\'aide[/url] !','Euer erstes Mal? Nur keine falsche Scheu! Schaut einfach auf unsere [url=?help=item-comparison]Hilfeseite[/url]!','¿Tu primera vez? ¡No seas vergonzoso! !Mira nuestra [url=?help=item-comparison]página de ayuda[/url]!','Впервые? Не стесняйтесь посетить нашу [url=?help=item-comparison]справочную страницу[/url]!'),(3,'profile','Quick Help: Profiler',0,'padding-left: 80px; background-image: url(STATIC_URL/images/announcements/help-large.gif); background-position: 10px center',1,1,'[h3]First Time?[/h3]\n\nThe [b]Profiler[/b] lets you [span class=tip title=\"e.g. See how\'d you look as a different race!\"]edit your character[/span], find gear upgrades, check your gear score, and more!\n\n[ul]\n[li][b]Right-click[/b] slots to change items, add gems/enchants, or find upgrades.[/li]\n[li]Use the [b]Claim character[/b] button to add your own characters to your [url=?user]user page[/url].[/li]\n[li]Save a modified character to your Aowow account by using the [b]Save as[/b] button.[/li]\n[li][b]Statistics[/b] will update in real time as you make tweaks.[/li]\n[/ul]\n\nFor more information, check out our extensive [b][url=?help=profiler]help page[/url][/b]!','','','',''),(2,'profiler','Help: Profiler',0,'padding-left: 80px; background-image: url(STATIC_URL/images/announcements/help-large.gif); background-position: 10px center',1,1,'[h3]First Time?[/h3]\r\n\r\nThe [b]Profiler[/b] tool lets you [span class=tip title=\"e.g. See how\'d you look as a different race, try different gear or talents, and more!\"]edit your character[/span], find gear upgrades, check your gear score, and more!\r\n\r\n[ul]\r\n[li][b]Right-click[/b] slots to change items, add gems/enchants, or find upgrades.[/li]\r\n[li]Use the [b]Claim character[/b] button to add your own characters to your [url=/?user]user page[/url].[/li]\r\n[li]Save a modified character to your Aowow account by using the [b]Save as[/b] button.[/li]\r\n[li][b]Statistics[/b] will update in real time as you make tweaks.[/li]\r\n[/ul]\r\n\r\nFor more information, check out our extensive [url=?help=profiler]help page[/url]!','','','',''); -/*!40000 ALTER TABLE `aowow_announcements` ENABLE KEYS */; -UNLOCK TABLES; - --- --- Dumping data for table `aowow_articles` --- - -LOCK TABLES `aowow_articles` WRITE; -/*!40000 ALTER TABLE `aowow_articles` DISABLE KEYS */; -INSERT INTO `aowow_articles` VALUES (13,4,0,NULL,2,'[b][color=c4]Rogues[/color][/b] are a leather-clad melee class capable of dealing large amounts of damage to their enemies with very fast attacks. They are masters of stealth and assassination, passing by enemies unseen and striking from the shadows, then escaping from combat in the blink of an eye.\r\n\r\nThey are capable of using poisons to cripple their opponents, massively weakening them in battle. Rogues have a powerful arsenal of skills, many of which are strengthened by their ability to stealth and to incapacitate their victims.\r\n[ul]\r\n[li]Rogues can use a wide variety of melee weapons, such as daggers, fist weapons, one-handed maces, one-handed swords and one-handed axes.[/li]\r\n[li]By coating their weapons with [url=items=0.-3&filter=na=poison;ub=4]poison[/url] rogues can severely cripple or weaken their enemies.[/li]\r\n[li]When using [spell=1784] rogues will be unseen except by the most perceptive enemies.[/li]\r\n[/ul]',NULL),(14,1,0,NULL,2,'[b]Overview:[/b] The [b]humans[/b] are the most populous and the youngest race in Azeroth. The humans have become the [i]de facto[/i] leaders of the Alliance, with their youthful ambitions and resilience.\n\n[b]Capital City:[/b] The human seat of power is in the rebuilt city of [zone=1519].\n\n[b]Starting Zone:[/b] Humans begin questing in [zone=12].\n\n[b]Mounts:[/b] [npc=384] sells armoried ponies in Stormwind, and [npc=33307] at the Argent Tournament has a few distinct models.',NULL),(13,1,0,NULL,2,'[b][color=c1]Warriors[/color][/b] are a very powerful class, with the ability to tank or deal significant melee damage. The warrior\'s Protection tree contains many talents to improve their survivability and generate threat versus monsters. Protection warriors are one of the main tanking classes of the game.\n\nThey also have two damage-oriented talent trees - [icon name=ability_rogue_eviscerate][url=spells=7.1.26]Arms[/url][/icon] and [icon name=ability_warrior_innerrage][url=spells=7.1.256]Fury[/url][/icon], the latter of which includes the talent [spell=46917], which allows the warrior to wield two two-handed weapons at the same time! They are capable of strong melee AoE damage with spells such as [spell=845], [spell=1680], [spell=46924]. A warrior fights while in a specific [i]stance[/i], which grants him bonuses and access to different sets of abilities. He will use [spell=71] for tanking, and [spell=2457] or [spell=2458] for melee DPS.\n\n[ul]\n[li]All warriors can buff their raid or group by using a [i]shout[/i], [spell=6673] or [spell=469], and Fury warriors can provide the passive buff [spell=29801] which significantly increases the melee and ranged critical strike chance of his allies.[/li]\n[li]Warriors start out with only [spell=2457] at first, but learn [spell=71] at level 10 and [spell=2458] at level 30.[/li]\n[li]Warriors have numerous useful methods of getting to their target in a hurry! All warriors can use [spell=100] or [spell=20252] to reach an enemy and Protection warriors have [spell=3411], which allows them to intercept a friendly target and protect them from an attack.[/li]\n[/ul]',NULL),(13,2,0,NULL,2,'[b][color=c2]Paladins[/color][/b] bolster their allies with holy auras and blessing to protect their friends from harm and enhance their powers. Wearing heavy armor, they can withstand terrible blows in the thickest battles while healing their wounded allies and resurrecting the slain. In combat, they can wield massive two-handed weapons, stun their foes, destroy undead and demons, and judge their enemies with holy vengeance. Paladins are a defensive class, primarily designed to outlast their opponents.\n\nThe paladin is a mix of a melee fighter and a secondary spell caster. The paladin has a great deal of group utility due to the paladin\'s healing, blessings, and other abilities. Paladins can have one active aura per paladin on each party member and use specific blessings for specific players. Paladins are pretty hard to kill, thanks to their assortment of defensive abilities. They also make excellent tanks using their [spell=25780] ability.\n\n[ul]\n[li]Can effectively heal, tank, and deal damage in melee.[/li]\n[li]Has a wide selection of [url=spells=7.2&filter=na=blessing]Blessings[/url], [url=spells=7.2&filter=na=aura]Auras[/url], and other buffs.[/li]\n[li]Is the only class with access to a true invulnerability spell: [spell=642][/li]\n[/ul]',NULL),(14,2,0,NULL,2,'[b]Overview:[/b] The [b]orcs[/b] were originally a race of noble savages, residing on the world of Draenor. Unfortunately, The Burning Legion made use of them in an attempt to conquer Azeroth—they were infected with the daemonic blood of Mannoroth the Destructor, driven mad, and turned upon both the Draenei and the denizens of Azeroth. After losing the Second War, they were cut off from the corrupting influence of Mannoroth, and began to return to their shamanistic roots. Now, under the leadership of their new Warchief, the orcs are carving out a home for themselves in Azeroth.\n\n[b]Capital City:[/b] The orcs now reside in the city of [zone=1637], named after the deceased Orgrim Doomhammer, former Warchief of the Horde.\n\n[b]Starting Zone:[/b] Orcs begin questing in [zone=14].\n\n[b]Mounts:[/b] [npc=3362] in Orgrimmar sells a variety of wolves; [npc=33553] sells a few distinctive mounts at the Argent Tournament.',NULL),(13,3,0,NULL,2,'[b][color=c3]Hunters[/color][/b] are a very unique class in World of Warcraft. They are the sole non-magical ranged damage-dealers, fighting with bows and guns. Hunters have a number of different kinds of shots and stings, which can be used to debuff an enemy, and are capable of laying traps to deal damage or otherwise slow/incapacitate their enemy.\n\nA hunter will also tame his very own [url=pets]pet[/url] to aid them in combat. While they are not the only class which can use pet minions, the hunter\'s pet is unique in that each species has a particular type of talent tree, which the hunter can use to distribute points into various skills and passive abilities.\n\nIn addition, each species has a unique special ability. Hunters can seek out the most desirable pets based on their appearances or abilities, and if they spec deep enough into the [icon name=ability_hunter_beasttaming][url=spells=7.3.50]Beast Mastery[/url][/icon] tree they gain access to special, \"exotic\" beasts such as [pet=46] or [pet=39]!\n\n[ul]\n[li]Hunters have access to 23 (32 if [icon name=ability_hunter_beasttaming][url=spells=7.3.50]Beast Mastery[/url][/icon]) different [url=pets]species of pets[/url], featuring over 150 different appearances![/li]\n[li]Hunters have a number of survival-oriented skills which they can use to escape or avoid potential danger, such as [spell=5384] and [spell=781].[/li]\n[li][icon name=ability_hunter_swiftstrike][url=spells=7.3.51]Survival[/url][/icon] hunters can spec down the tree into [spell=53292], which allows them to provide the [spell=57669] buff to their party and raid members.[/li]\n[/ul]',NULL),(13,5,0,NULL,2,'[b][color=c5]Priests[/color][/b] are commonly considered one of the standard healing classes in World of Warcraft, as they have two talent specs that can be used to heal quite effectively.\n\nTheir [icon name=spell_holy_holybolt][url=spells=7.5.56]Holy[/url][/icon] tree includes talents which strongly boost the healing done to their allies, including spells that can be used to heal multiple players at once, such as [spell=48089]. The [icon name=spell_holy_wordfortitude][url=spells=7.5.613]Discipline[/url][/icon] tree, while still capable of significant raw healing output, focuses primarily on damage absorption and mitigation through use of [spell=48066] and procced shielding effects. Priests are also capable of very powerful ranged damage with their unique [icon name=spell_shadow_shadowwordpain][url=spells=7.5.78]Shadow[/url][/icon] abilities, and upon entering [spell=15473] will see a significant increase in their shadow damage while losing the ability to cast any Holy spells.\n\n[ul]\n[li]While the [icon name=spell_holy_wordfortitude][url=spells=7.5.613]Discipline[/url][/icon] talent tree is commonly used for healing, it also contains some powerful talents that can boost the priest\'s Holy damage, though [icon name=spell_shadow_shadowwordpain][url=spells=7.5.78]Shadow[/url][/icon] spells and abilities should be used primarily for DPS.[/li]\n[li]Priests provide of the most appreciated buffs in the game - [spell=48161], which grants an indispensable stamina buff to everyone in the raid. They can also buff both [spell=48073] and [spell=48169]![/li]\n[li]Shadow priests are an excellent utility class for any raid, providing the much-loved [spell=57669] buff to boost mana regeneration and can even heal their own party with [spell=15286]![/li]\n[/ul]',NULL),(13,6,0,NULL,2,'Introduced in the Wrath of the Lich King expansion, [b][color=c6]Death Knights[/color][/b] are World of Warcraft\'s first hero class. Death knights start at level 55 in a special, instanced zone unreachable by any other class: Acherus, the Ebon Hold, located in [zone=4298]. Here they will earn their talent points as quest rewards and even get a special summoned mount, the [spell=48778]!\n\nDeath knights have multiple very strong damage dealing options, as each of their talent trees can be specced to perform exceptionally well with a variety of melee abilities, spells and damage-over-time dealing diseases. They are also very capable tank classes, with both their Blood and Frost trees providing unique options - [icon name=spell_deathknight_bloodboil][url=spells=7.6.770]Blood[/url][/icon] dealing more with self-healing abilities and [icon name=spell_frost_frostnova][url=spells=7.6.771]Frost[/url][/icon] providing significant damage mitigation and strong AoE damage.\n\nDeath knights fight with a special buff active called a [i]presence[/i] (similar to a warrior\'s stances) which provides special bonuses to their roles. Death knights utilize a unique power system, with most spells costing either Runes, which are replenished throughout battle, or Runic Power, which can be generated by various abilities.\n\n[ul]\n[li][icon name=spell_deathknight_armyofthedead][url=spells=7.6.772]Unholy[/url][/icon] death knights can spec into [spell=52143], which makes their summoned Ghoul minion a permanent pet to aid in battle![/li]\n[li]The death knight class has its own special weapon enchanting ability called [spell=53428], which replaces the need for conventional weapon enchants.[/li]\n[li]Death knights are a very unique damage-dealing class in that their damage is dealt by both melee abilities [i]and[/i] spells![/li]\n[/ul]',NULL),(13,7,0,NULL,2,'[b][color=c7]Shamans[/color][/b] master elemental and nature magics and bring the most potential buffs to any group in the form of totems. A shaman can summon one totem of each element - earth, fire, air, and water - which appears at the shaman\'s feet and provides a buff to anyone in the shaman\'s party or raid within range of it. Some shaman totems, notably the fire ones, also do damage to opponents. The trick to playing any type of shaman is knowing which totems to cast under which circumstances to maximize the group\'s damage output and survivability.\n\nShamans are primarily spellcasters, although an [icon name=spell_nature_lightningshield][url=spells=7.7.373]Enhancement[/url][/icon] shaman likes to get close and personal and do damage within melee range. An enhancement shaman learns to [spell=30798] weapons and can use [spell=51533] to summon a pair of Spirit Wolves to aid in battle. Despite being primarily melee, [icon name=spell_nature_lightningshield][url=spells=7.7.373]Enhancement[/url][/icon] shamans can still gain some benefit from spellpower and can cast instant [spell=403] or heals with [spell=51530]. \n\n[icon name=spell_nature_lightning][url=spells=7.7.375]Elemental[/url][/icon] shamans stand back and cast fire and lightning spells to deal great amounts of damage. They can push back enemies with [spell=51490] and root all enemies in an area with[spell=51486]. They also bring [icon name=spell_fire_totemofwrath][url=spell=57722]Totem of Wrath[/url][/icon] and [spell=51470] as amazing spellcaster raid buffs. A shaman that choses [icon name=spell_nature_magicimmunity][url=spells=7.7.374]Restoration[/url][/icon] gains improved healing spells and can be a great raid or tank healer. Resto shamans are known for their powerful [spell=1064] ability and for providing a [spell=16190] to help their party\'s mana restoration. They also gain a powerful [spell=974], can use [spell=51886] to remove curses, and have an instant-cast direct heal plus heal over time effect called [spell=61295].\n\n[ul]\n[li]There are over twenty different totems a shaman can learn![/li]\n[li]Shamans can cast [spell=2825] (or [spell=32182]) to boost the entire group\'s damage and healing. This buff is unique and oft sought after for a raid group.[/li]\n[li]A shaman can turn into a [spell=2645] at level 16 and can even make it instant cast with [spell=16287]. This spell can be used in combat, but not indoors.[/li]\n[li]Shamans can only have one elemental shield - [spell=324] or [spell=52127] - on at a time. [spell=974], if the shaman knows it, can be cast on another player.[/li]\n[/ul]',NULL),(13,8,0,NULL,2,'[b][color=c8]Mages[/color][/b] wield the elements of fire, frost, and arcane to destroy or neutralize their enemies. They are a robed class that excels at dealing massive damage from afar, casting elemental bolts at a single target, or raining destruction down upon their enemies in a wide area of effect. Mages can also augment their allies\' spell-casting powers, summon food or drink to restore their friends, and even travel across the world in an instant by opening arcane portals to distant lands.\n\nWhen seeking someone to introduce monsters to a world of pain, the Mage is a good choice. With their elemental and arcane attacks, it\'s a safe bet something they can do won\'t be resisted by your chosen enemy. Damage is the name of the Mage game, and they do it well. Their arsenal includes some powerful buffs, debuffs, stuns, and snares, enabling them to dictate the terms of any fight.\n\n[ul]\n[li]Can [spell=42956] to restore their allies\' health and mana.[/li]\n[li]Are the only class that can create portals to transport other players. They cannot, however, summon players [i]from[/i] a distant location - that\'s a [icon name=class_warlock][color=c9]Warlock\'s[/color][/icon] job![/li]\n[li]Mages who use [item=50045] can have a permanent water elemental pet![/li]\n[/ul]',NULL),(13,9,0,NULL,2,'[b][color=c9]Warlocks[/color][/b] are masters of the demonic arts. Clothed in demonic styled cloth, they excel in using curses, firing bolts of fire or shadow, and summoning demons to help them in combat. Warlocks, while being excellent spell casters, also excel in supporting fellow allies by summoning other players or using ritual magics to conjure stones imbued with the power to heal.\r\n\r\nA warlock has very powerful abilities that, if used correctly, make them a very formidable opponent. Using their curses in combination with direct damage spells, Warlocks wreak havoc and destruction.\r\n\r\n[ul]\r\n[li]Can use a [spell=698] to summon another player to the portals location.[/li]\r\n[li]Are able to conjure [icon name=inv_stone_04][url=item=5509]Healthstones[/url][/icon] that have the ability to heal the user.[/li]\r\n[li]Can use curses on enemies to [url=spell=47865]weaken[/url] them or [url=spell=47864]damage[/url] them.[/li]\r\n[/ul]',NULL),(13,11,0,NULL,2,'[b][color=c11]Druids[/color][/b] are World of Warcraft\'s \"jack of all trades\" class -- that is, capable of performing in a variety of different roles and as such have one of the most varied playstyles. A druid can act as a healer, melee DPS, ranged DPS or a tank, utilizing a variety of [i]shapeshifting[/i] forms. As a druid levels up, he is able to learn new, powerful forms which he can cast to change into different creatures to suit their roles.\n\nAt lower levels, a druid will heal or ranged DPS in his caster form, but at later levels players who spec into the specialized trees will gain access to two special shapeshift forms for each different role.\n\nHealing druids will learn [spell=33891], which reduces the mana cost of their healing spells and grants a passive healing aura to their allies. Their ranged damage-dealing counterparts will learn [spell=24858], increasing their armor and granting a spell critical aura to their allies. There are also two feral form druid forms -- the mighty [spell=5487] (and at later level, [spell=9634]), a tanking-oriented form which provides additional armor and health and grants access to an arsenal of threat-building and damage mitigation abilities, and the rogue-like [spell=768] which is capable of significant melee DPS.\n\n[ul]\n[li]Druids learn their different forms through questing or training. Some shapeshifts are only learned via talents.[/li]\n[li]There are some shapeshifts that all druids can learn. [spell=5487] is obtained at level 10, [spell=1066] and [spell=783] at level 16, [spell=768] at level 20 and [spell=9634] at level 40.[/li]\n[li]Druids even have their own flying travel form! [spell=33943] can be trained at level 60, and [spell=40120] at level 71 provided the player has already trained [spell=34091].[/li]\n[li]Some druid shapeshifts are obtained via talents only - [spell=24858] can be obtained at level 40 when a player specs deep into the [icon name=spell_nature_starfall][url=spells=7.11.574]Balance[/url][/icon] tree, and [spell=33891] at level 50 after speccing deep into [icon name=spell_nature_healingtouch][url=spells=7.11.573]Restoration[/url][/icon].[/li]\n[li]Druids have their own, class-specific teleport ability that allows them to travel to and from [zone=493], which is handy when needing to train![/li]\n[li]Because feral druids do not actually swing weapons while in shapeshift forms, they instead gain a special statistic from any melee weapon they equip called \"feral attack power.\" This stat is a conversion of a weapon\'s DPS (damage per second) into an attack power-granting statistic which affects the cat or bear\'s damage output.[/li]\n[/ul]',NULL),(14,3,0,NULL,2,'[b]Overview:[/b] The [b]dwarves[/b] are a hardy race, hailing from Khaz Modan in the Eastern Kingdoms. Rumor has it they are descended from the Titans. There are three main clans of dwarves vying for power in Ironforge: the Bronzebeards, Wildhammers, and Dark Irons.\n\n[b]Capital City:[/b] The dwarves make their home in their ancestral seat of [zone=1537].\n\n[b]Starting Zone:[/b] Dwarves begin in [zone=1].\n\n[b]Mounts:[/b] [npc=1261] by the Amberstill Ranch sells rams, as well as [npc=33310] at the Argent Tournament.',NULL),(14,4,0,NULL,2,'[b]Overview:[/b] The [b]night elves[/b] are an ancient and mysterious race. They lived in Kalimdor for thousands of years, undisturbed until the world tree was sacrificed to halt the advance of the Burning Legion prior to the events of World of Warcraft.\n\n[b]Capital City:[/b] The night elf capital city is [zone=1657], situated in the branches of the world tree itself.\n\n[b]Starting Zone:[/b] Night Elves begin in [zone=141], learning about the recent political changes in Darnassus.\n\n[b]Mounts:[/b] [npc=4730] in Darnassus sells a variety of nightsabers, as well as [npc=33653] at the Argent Tournament.',NULL),(14,5,0,NULL,2,'[b]Overview:[/b] When the [b]undead[/b] scourge initially swept across Azeroth, they converted a number of members of the Alliance to the undead. When the combined forces of the orcs, elves, trolls, dwarves and humans began to fight back, though, [npc=36597]\'s hold on his forces began to weaken. A small faction of humans, known as the Forsaken, broke free of the Lich King\'s control.\n\nNow, free of the bonds of servitude as well as the troublesome emotions and connections of their human lives, the Forsaken have found a new home—with the Horde.\n\n[b]Capital City:[/b] The Forsaken reside in the [zone=1497], underneath the ruins of the former human city of Lordaeron.\n\n[b]Starting Zone:[/b] [zone=85] is the starting zone for Forsaken players--they are raised as second-generation Forsaken by val\'kyr and experience Sylvanas\' menacing new agenda firsthand.\n\n[b]Mounts:[/b] [npc=4731] in Tirisfal Glades sells numerous undead horses; [npc=33555] at the Argent Tournament sells a few distinct models.',NULL),(14,6,0,NULL,2,'[b]Overview:[/b] The [b]tauren[/b], a race with deep shamanistic roots, are longtime residents of Kalimdor. They have a deep and abiding love of nature, and the vast majority of them worship a deity known as the Earth Mother. \n\n[b]Capital City:[/b] The tauren reside in [zone=1638].\n\n[b]Starting Zone:[/b] Tauren begin questing in [zone=215].\n\n[b]Mounts:[/b] [npc=3685] sells numerous kodo mounts; [npc=33556] at the Argent Tournament sells a few distinctive models.',NULL),(14,7,0,NULL,2,'[b]Overview:[/b] The [b]gnomes[/b] are a quirky race, obsessed with gadgets and technology. They originally come from the city of [zone=721], which was destroyed by [npc=7937] in an attempt to save it from an invading army of troggs.\n\n[b]Capital City:[/b] The gnomes now make their home in [zone=1537]; they have made efforts to retake their beloved former city with [achievement=4786].\n\n[b]Starting Zone:[/b] Gnomes begin in [zone=1], but they have a very different quest sequence from Dwarves, covering Gnomeregan.\n\n[b]Mounts:[/b] [npc=7955] in Dun Morogh sells numerous mechanostriders, as well as [npc=33650] at the Argent Tournament.',NULL),(14,8,0,NULL,2,'[b]Overview:[/b] While there are many different tribes of [b]trolls[/b] scattered across Azeroth, only the [url=?faction=530]Darkspear Tribe[/url] has ever sworn allegiance to the Horde. The trolls originally lived in the Broken Isles, but were overrun by naga and murlocs and driven from their home. The orcs, led by [npc=4949], saved the Darkspear tribe from certain destruction and offered them amnesty among the Horde. In return, the Darkspear tribe swore fealty to the orcish warchief.\n\n[b]Capital City:[/b] The Darkspear Trolls live now in the Horde capital of [zone=1637].\n\n[b]Starting Zone:[/b] Trolls begin questing in [b]Echo Isles[/b].\n\n[b]Mounts:[/b] [npc=7952] in Sen\'jin Village sells numerous raptors; [npc=33554] at the Argent Tournament sells a few distinctive models.',NULL),(14,10,0,NULL,2,'[b]Overview:[/b] The [b]blood elves[/b] are a proud, haughty race, joining the Horde in Burning Crusade. They represent a faction of former high elves, split off from the rest of elven society; they are also survivors of Arthas\' assault on Silvermoon. Blood elves are fully dependent on magic, having revelled in its power for so long that they suffer horrible withdrawal if it were to be taken away.\n\n[b]Capital City:[/b] The blood elves have rebuilt [zone=3487].\n\n[b]Starting Zone:[/b] [zone=3430] is the starting zone for Blood Elves.\n\n[b]Mounts:[/b] [npc=16264] in Eversong Woods sells numerous hawkstriders; [npc=33557] at the Argent Tournament sells a few unique models.',NULL),(14,11,0,NULL,2,'[b]Overview:[/b] The [b]Draenei[/b] are followers of the Naaru and worshipers of the Holy Light. They originally hail from the distant world of Argus, fleeing after Sargeras tried to corrupt them. They then settled on the Orcish homeworld of Draenor, where after a period of peace, they were brutally murdered during Guldan\'s corruption of the Orcs. Finally they settled in Azeroth, to seek aid in their battle against the Burning Legion. Draenei were introduced in the Burning Crusade expansion.\n\n[b]Capital City:[/b] The Draenei have the seat of their power in the ruins of their once-great ship, [zone=3557].\n\n[b]Starting Zone:[/b] [zone=3524] and [zone=3525] cover the attempts of the Draenei to settle on their new island and deal with the inherent corruption present.\n\n[b]Mounts:[/b] [npc=17584] sells a variety of Elekks, as well as [npc=33657] at the Argent Tournament.',NULL),(8,21,0,NULL,2,'[minibox]\n[h2]Steamwheedle Cartel[/h2]\n[b]Booty Bay[/b]\n[faction=577]\n[faction=369]\n[faction=470]\n[/minibox]\n\n\n[b]Booty Bay[/b] is a large pirate town nestled into the cliffs surrounding a beautiful blue lagoon on the southern tip of [zone=33]. The city is entered by traversing through the bleached-white jaws of a giant shark.\n\nRun by the Blackwater Raiders who are closely associated with the Steamwheedle Cartel, the port offers facilities to any traveller passing through, regardless of their faction. Combined with the world renowned Salty Sailor Tavern, [event=15], numerous profession trainers, and vendors that sell everything from pets to diamond rings, it is one of the most popular locations in Azeroth.\n\n[npc=2496], ruler of this city, is hiring all the help he can get against the pesky [faction=87] and other threats of the city. He resides, together with the leader of the Blackwater Raiders, [npc=2487], at the top of the inn of Booty Bay.\n\nDue to the boat route from Booty Bay to Ratchet, players of all level ranges (mostly Horde, if lower level) can be expected to be found going about their business, although frequent visitors will more than likely fit in the 35 - 45 range. The quests available from the locals reflect this range nicely.\n\nThe water there occasionally has floating wreckages and schools of fish. The schools that are found most often are [item=6359], [item=6358], and [item=13422]. Fishing in the floating wreckages will also give you very high chances of fishing out chests and items, making Booty Bay an ideal place for fishing.\n\n[h3]Reputation[/h3]\nMost of the quests to raise reputation with Booty Bay are located in The Cape of Stranglethorn. Having a friendly or higher reputation will make the guards help you in case of initiated violence against you.\n\nIf you are Hated with Booty Bay, you can do the repeatable quest [quest=9259] to get back to Neutral.',NULL),(8,47,0,NULL,2,'[b]Ironforge[/b] is the faction associated with the capital city of the dwarves, [zone=1537]. [npc=2784] rules his kingdom of Khaz Modan from his throne room within the city, and the [npc=7937], leader of the gnomes, has temporarily had to settle down in Tinker Town after the recent fall of the gnome city [zone=133].\n\n[h3]History[/h3]\nIronforge is the ancient home of the dwarves. A marvel to the dwarves\' skill at shaping rock and stone, Ironforge was constructed in the very heart of the mountains, an expansive underground city home to explorers, miners, and warriors. Massive doors of rock protect the city in times of war, and lava from the mountain itself is redirected and distributed for heat, energy and smithing purposes. Before the Dark Iron Clan was banished from the city, eventually leading to the War of the Three Hammers, Ironforge was the commercial and social center of all the dwarven clans. It is now home to the Bronzebeard Clan. Many dwarven strongholds fell during the Second War between the Horde and the Alliance of Lordaeron, but the mighty city of Ironforge, nestled in the wintry peaks of [zone=1] and protected by its great gates, was never breached by the invading Horde.\n\nRelatively recently, Ironforge also became home to the Gnomeregan refugees. After the Third War, the gnomish city of Gnomeregan became overrun by troggs. Since then, a number of gnomes have settled in Ironforge, converting an area of that city to their liking, an area now known as Tinker Town.\n\nIronforge is one of most populated cities in the world, coming after the human city of [zone=1519], and housing 20,000 people.\n\nWhile the Alliance has been weakened by recent events, the dwarves of Ironforge, led by King Magni Bronzebeard, are forging a new future in the world.[h3]Reputation[/h3]\n[npc=14723] has the repeatable cloth reputation quests. As a reward for being exalted with Ironforge, non-dwarf players are able to ride [url=?items=15.5&filter=na=Ram;cr=93:92;crs=2:1;crv=0:0]rams[/url].\n\nSurrounding zones [zone=1], [zone=38] and [zone=11] contain the most quests for gaining reputation with Ironforge.',NULL),(8,54,0,NULL,2,'[b]Gnomeregan Exiles[/b] is the faction of gnomes who fled from their home, [zone=133] in [zone=1]. It was destroyed by the [url=?npcs=7&filter=na=Trogg]Trogg[/url] after a toxic invasion. Now a member of the Alliance, most are located in the Tinkertown section of the neighboring city [zone=1537], including leader [npc=7937].\n\n[h3]History[/h3]\nIt has been speculated that gnomes were formed as robots by the Titans, due to their inquisitive nature and technical skills.\n\nGnomes were an underground race of tinkers, residing in Gnomeregan until the troggs destroyed it. In this war, over 80% of the gnomish population was lost.\n\n[h3]Reputation[/h3]\n[npc=14724] has the repeatable cloth reputation quests. As a reward for being exalted with Ironforge, non-gnome dwarf players are able to ride [url=?items=15.5&filter=na=Mechanostrider;cr=93:92;crs=2:1;crv=0:0]mechanostriders[/url].\nSurrounding zone [zone=1] contain the most quests for gaining reputation with the Gnomeregan Exiles.',NULL),(8,59,0,NULL,2,'The [b]Thorium Brotherhood[/b] are an elite group of craftsmen who can reveal a number of epic recipes if you gain enough faction reputation with them. All players start off at Neutral reputation with them.\n\n[h3]History[/h3]\n\nThe [zone=51] is home to a group of exceptionally stout dwarves who have split from the Dark Iron Clan. On the cliffs overlooking the region called the Cauldron, in the far north of the Searing Gorge, the dwarves of the Thorium Brotherhood have established a base of operations, Thorium Point. From here, they keep a close eye on the Dark Iron dwarves\' activities in the Searing Gorge and beyond. Adventurers seeking out Thorium Point will find that the dwarves of the Thorium Brotherhood hold great rewards for those who aid them in their never ending struggle against their former brethren.\n\nThe Thorium Brotherhood comprises many exceptionally talented craftsmen, and the blacksmiths of the Brotherhood are rumored to be among the finest Azeroth has ever seen. They possess the knowledge required to make the arms and armaments of [npc=11502], the Fire Lord, but lack the manpower to obtain the materials required for the crafting. It is rumored that one member of the Thorium Brotherhood has been empowered to trade the dwarves\' fabled recipes and plans with those who can prove their loyalty to the Brotherhood. Of course, proving one\'s loyalty at some point may include venturing to the heart of the [zone=2717], the domain of Ragnaros, the Fire Lord himself, to supply the dwarves with the rare raw materials found there. A daunting task, no doubt, but gaining access to the Thorium Brotherhood\'s secrets should prove to be a reward well worth the effort.\n\n[h3]Reputation[/h3]\n\n[b]Neutral to Friendly[/b]\n\n[ul]\n[li]Turn in [item=18944], [item=3857] and either [item=4234], [item=3575], or [item=3356] to [npc=14624].[/li][/ul]\n[b]Friendly to Honored[/b]\n\n[ul]\n[li]Turn in [item=18945] to Master Smith Burninante.[/li][/ul]\n[b]Honored to Exalted[/b]\n\n[ul]\n[li]Turn in [item=11370] to [npc=12944].[/li]\n[li]Turn in [item=17012] to Lokhtos Darkbargainer.[/li]\n[li]Turn in [item=17010] to Lokhtos Darkbargainer.[/li]\n[li]Turn in [item=17011] to Lokhtos Darkbargainer.[/li]\n[li]Turn in [item=11382] to Lokhtos Darkbargainer.[/li][/ul]',NULL),(8,68,0,NULL,2,'[b]Undercity[/b] is the faction for the capital city of the Forsaken Undead, [zone=1497], ruled by Sylvanas Windrunner. It is located in [zone=85], at the northern edge of the Eastern Kingdoms. The city proper is located under the ruins of the historical City of Lordaeron. To enter it, you will walk through the ruined outer defenses of Lordaeron and the abandoned throneroom, until you reach one of three elevators guarded by two abominations.\n\n[h3]History[/h3]\nThe Undercity was originally simply a system of sewers, crypts, and catacombs beneath the Capital City of Lordaeron. After the city was destroyed by the Scourge, Arthas had the underground warren expanded and rebuilt. He originally intended for the Undercity to be his seat of power, from which he would rule the Plaguelands. However, shortly after the Third War ended, Arthas was forced to return to Northrend and save the Lich King. In his absence, [npc=10181] and her rebel Undead captured the ruins of the city. Soon after, she discovered the massive underground fortress, and decided to establish it as the main base of operations for the Undead Forsaken.\n\n[h3]Reputation[/h3]\n[npc=14729] has the Undercity repeatable cloth quests used by non-Undead Horde players to obtain the right to ride [url=?items=15.5&filter=na=Skeletal;cr=93:92;crs=2:1;crv=0:0]skeletal horses[/url] at exalted.\n\nSurrounding zones [zone=267], [zone=130], and Tirisfal Glades have the most quests to earn reputation with Undercity.',NULL),(8,69,0,NULL,2,'[b]Darnassus[/b] is the faction associated with [zone=1657], the capital city of the Night Elves. The high priestess, [npc=7999], resides in the Temple of the Moon, surrounded by other sisters of Elune. In the Cenarion Enclave, the [npc=3516] leads the [faction=609], often in direct opposition to his fellow druids in [zone=493] and Tyrande herself.\n\n[h3]History[/h3]\nIn the aftermath of the Third War, the night elves had to adjust to their mortal existence. Such an adjustment was far from easy, and there were many night elves who could not adjust to the prospects of aging, disease and frailty. Seeking to regain their immortality, a number of wayward druids conspired to plant a special tree that would reestablish a link between their spirits and the eternal world.\n\nWith [npc=15362] missing, Fandral Staghelm - the leader of those who wished to plant the new World Tree - became the new Arch-Druid. In no time at all, he and his fellow druids had forged ahead and planted the great tree, [zone=141], off the stormy coasts of northern Kalimdor. Under their care, the tree sprouted up above the clouds. Among the twilight boughs of the colossal tree, the wondrous city of Darnassus took root. However, the tree was not consecrated with nature\'s blessing and soon fell prey to the corruption of the Burning Legion. Now the wildlife and even the limbs of Teldrassil are tainted by a growing darkness.\n\n[h3]Reputation[/h3]\n[npc=14725] has the Darnassus repeatable [quest=7800] used by non-night elven Alliance players to obtain the right to ride [url=?items=15.5&filter=na=Reins+-Winterspring;ra=4;cr=93:92;crs=2:1;crv=0:0]night sabers[/url].[pad]Players who are at or close to level 44 looking to gain the favor of Darnassus should find and complete the quests of [zone=357]. The quests therein are associated with Darnassus and could prove to substantially increase your reputation should they all be completed.',NULL),(8,70,0,NULL,2,'The [b]Syndicate[/b] is a mostly Human criminal organization that operates primarily in the [zone=45] and the [zone=36], although a few small encampments are scattered in the [zone=267]. Their membership numbers around 3,000 persons.\n\nThey have three leaders: [npc=2423] (who took over from his father Aiden Perenolde), descendent of the original Lord of Alterac, who directs the Syndicate\'s actions in the Alterac Mountains from Strahnbrad; [npc=2597] directs Syndicate actions in Arathi Highlands from the main keep in the semi-abandoned fortress of Stromgarde; and Lady Beve Perenolde, daughter of Aiden Perenolde.\n\n[h3]History[/h3]\n\nDuring the Second War the Kingdom of Alterac, led by Lord Perenolde, was discovered to be in league with the Orcish Horde. Perenolde believed that a Horde victory was inevitable, and thus offered aid to the Horde by stirring up rebellions, attacking Alliance bases, and giving them supplies. When this treachery was discovered, the Alliance marched on Alterac and destroyed it. Perenolde and any nobles who went along with his plans were stripped of their titles and land. Many of the nobility managed to escape, however, and began plotting their revenge. Using their still sizable fortunes, the nobility hired a band of thieves and assassins, forming an organization known as the Syndicate.\n\nAt first the Syndicate\'s goal was just to spread chaos and disorder, striking from hidden bases in the Alterac Mountains. With the end of the Third War and the resultant chaos however, the leaders of the Syndicate saw their chance to return Alterac to its former power. They have now gained control of several outposts in the surrounding area including the sacked fortress of Durnholde Keep and a portion of the city of Stromgarde.\n\nThey are enemies of both the Alliance, whom they consider their mortal enemies, and the Horde, whom they consider mere brutes good for nothing but slave labor. As a result, the Syndicate is now hunted by both factions, with the [npc=10181], in particular, placing a bounty on their heads - guaranteeing that all captured Syndicate members will be summarily executed. In addition, [npc=4949] ordered a number of his agents, including [npc=2229], [npc=2239], [npc=2238] and their leader [npc=2316] to launch an investigation into the nature of the Syndicate and its activities, as well as to recover [item=3498], which belonged to a dear friend of his, [npc=18887] - a necklace now worn by Elysa, the mistress of Lord Aliden.\n\n[h3]Reputation[/h3]\n\nThe Syndicate as a faction in World of Warcraft is very odd in comparison to most factions in that the killing of the factions members will not lower your standing with the faction. For most players who are not a rogue, the only way for the Syndicate to appear on their Reputation Menu is to complete the quest [quest=8249], which is available to non-rogues. However, the quest requires [item=16885] ... which only rogues can obtain by pick-pocketing NPCs above level fifty, and those can only be traded to you - making it difficult to arrange such a transaction.\n\nCurrently there is only one known option to increase a player’s reputation with the Syndicate, and that is by killing members of the [faction=349] faction. There are no known rewards for increasing Syndicate reputation, and Ravenholdt-affiliated NPCs only give 1 Syndicate Reputation points, with the exception of [npc=13085], who gives 5 (although the corresponding loss of reputation with Ravenholdt is also five times as great). With all players starting at 32000/36000 hated with the faction, it would require killing 10,000 Ravenholdt NPCs to reach Neutral status with the faction; unfortunately, neutral status is the highest you can reach with the Syndicate, and if not to deter players further, none of the Ravenholdt NPCs drop loot.\n\n[b]WARNING[/b]: If you do decide to kill Ravenholdt NPCs, know that there is currently no way to restore your standings with Ravenholdt, if you do go below Neutral. The reason for the problem is that none of the quests that give Ravenholdt Reputation points will be available because none of the members from Ravenholdt will speak to you. This would mean its a permanent change and you will never be able to interact with any of the NPC loyal to Ravenholdt ever again. Also note that players start at 0/3000 reputation with Ravenholdt, and killing even one of their NPCs at this reputation level will forever prevent you from raising your reputation with them again.',NULL),(8,72,0,NULL,2,'[b]Stormwind[/b] is the faction associated with [zone=1519], the capital of the humans. It is located in the northwestern part of [zone=12]. The child king, [npc=1747], resides in Stormwind Keep, surrounded by his body guards and advisors, [npc=1748] (the regent), and [npc=1749]. The city is named for the occasional sudden squalls created by a ley line pattern in the mountains around the glorious city.\n\n[h3]History[/h3]\nDuring the First War, the Kingdom of Azeroth, including its capital, Stormwind Keep, was utterly destroyed by the Horde and its survivors fled to Lordaeron. After the orcs were defeated at the Dark Portal at the end of the Second War, it was decided that the city would be rebuilt, even surpassing its former grandeur. The nobles of Stormwind assembled a team of the most skilled and ingenious stonemasons and architects they could find. Under their direction, Stormwind was rebuilt in an amazingly short period of time. Now, at the end of the Third War, in the renamed Kingdom of Stormwind, it stands as one of the last bastions of human power left in the world. \n\nWith the fall of the northern kingdoms, Stormwind is by far the most populated city in the world. Boasting a population of two-hundred thousand people (predominantly human), it serves in many ways as the cultural and trade center of the Alliance, even with remote access to the sea. The humans living in the city are generally carefree and artistic, favoring light and colorful clothes, cuisine and art. It is home to the Academy of Arcane Sciences, the only wizarding school in Eastern Kingdoms, as well as SI:7, a rogue intelligence organization.\n\nHowever, the people of Stormwind find it difficult to accept Theramore\'s role as the home of the new Alliance, convinced not only that Stormwind should be the legitimate heir of Lordaeron\'s role in the past, but also that Theramore is doing little against the worsening situation within the Eastern Kingdoms.\n\n[h3]Reputation[/h3]\n[npc=14722] has the repeatable cloth quests to achieve a higher reputation with Stormwind. In return for exalted reputation, non-human players are able to ride horses.\n\nMost quests associated with Stormwind come from the surrounding areas of Elwynn Forest, [zone=40], and [zone=44].',NULL),(8,76,0,NULL,2,'[b]Orgrimmar[/b] is the faction for the capital city [zone=1637] of the orcs and trolls of the [faction=530]. Found at the northern edge of [zone=14], the imposing city is home to the orcish Warchief, [npc=4949].\n\n[h3]History[/h3]\nThrall led the orcs to the continent of Kalimdor, where they founded a new homeland with the help of their tauren brethren. Naming their new land Durotar after Thrall\'s murdered father, the orcs settled down to rebuild their once-glorious society. The demonic curse on their kind ended, the Horde changed from a warlike juggernaut into more of a loose coalition, dedicated to survival and prosperity rather than conquest. Aided by the noble tauren and the cunning trolls of the Darkspear tribe, Thrall and his orcs looked forward to a new era of peace in their own land. \n\nFrom there, they began the creation of the great warrior city, Orgrimmar. Named after the former Warchief, Orgrim Doomhammer, the new city was constructed in a short amount of time, with the aid of goblins, tauren, trolls, and the Mok\'Nathal Rexxar. Despite having some problems with the centaur, harpies, enraged thunder lizards, kobolds, evil orcish warlocks, quilboars, and unfortunately, the Alliance, Orgrimmar prospered in the end and became home to the orcs and Darkspear Trolls.\n\nToday, Orgrimmar lies at the base of a mountain between Durotar and [zone=16]. A warrior city indeed, it is home to countless amounts of orcs, trolls, tauren, and an increasing amount of Forsaken are now joining the city, as well as the Blood Elves who have recently been accepted into the Horde.\n\n[h3]Reputation[/h3]\n[npc=14726] has the Orgrimmar repeatable cloth quests used by non-orcish Horde players to obtain the right to ride [url=?items=15.5&filter=na=Wolf;cr=93:92;crs=2:1;crv=0:0]wolves[/url] at exalted.\n\nSurrounding areas Durotar and [zone=17] have the most quests for gaining reputation with Orgrimmar.',NULL),(8,81,0,NULL,2,'[b]Thunder Bluff[/b] is the faction of the Tauren capital city [zone=1638] located in the northern part of the region of [zone=215]. The whole of the city is built on bluffs several hundred feet above the surrounding landscape, and is accessible by elevators on the southwestern and northeastern sides.\n\n[h3]History[/h3]\nThe great city of Thunder Bluff lies atop a series of mesas that overlook the verdant grasslands of Mulgore. The once nomadic Tauren recently built the city as a center for trade caravans, traveling craftsmen and artisans of every kind. It was established by the mighty chief [npc=3057] after the Tauren, with help from the orcs, drove away the centaurs that originally inhabited Mulgore. Long bridges of rope and wood span the chasms between the mesas, topped with tents, longhouses, colorfully painted totems, and spirit lodges. The Tauren chief watches over the bustling city, ensuring that the united Tauren tribes live in peace and security.\n\n[h3]Reputation[/h3]\n[npc=14728] has the Thunder Bluff repeatable cloth quests used by non-tauren Horde players to obtain the right to ride [url=?items=15.5&filter=na=Kodo;cr=93:92;crs=2:1;crv=0:0]kodos[/url] at exalted.\n\nSurrounding zones Mulgore and [zone=17] have the most quests for gaining reputation with Thunder Bluff.',NULL),(8,87,0,NULL,2,'During the events leading up to and following the Third War, several criminal organizations appeared in Azeroth. The [b]Bloodsail Buccaneers[/b] appear to be one of these organizations, originating from the Bloodsail Hold on Plunder Isle and is where their ruler, Duke Falrevere holds court. They now plot to plunder and cripple the Steamwheedle Cartel controlled port town of [faction=21], currently under the protection of the Blackwater Raiders. It is likely the Bloodsail Buccaneers have come to take advantage of the town’s current loss of its fleet off the coast of the [zone=45], in which two of its ships were destroyed, and the remaining ship forced to find shelter in a cove, where its crew now fights to survive skirmishes with the Daggerspine Naga.\n\nIn preparation of the attack the Bloodsail Buccaneers have taken position in key locations near the town. Currently they have three ships anchored along the coastline south of Booty Bay, clear of the town’s defensive cannons, with camps also being built along the same coast in preparation of the attack. In addition, a scouting party has landed just west of the entrance to the town, reporting all activities, along with a compound being constructed along the road leading towards the town, likely to stop any re-enforcements from coming to help.\n\nBoth the Bloodsail Buccaneers and Blackwater Raiders seek to achieve their goals without having their forces engaged in battle, to this end each side now seek the aid of adventurers sympathetic to their cause.\n\n[h3]Reputation[/h3]\nThere is only one way to increase your reputation with the Bloodsail Buccaneers and that’s to unleash your wrath on any citizen of Booty Bay who can be found through out the Eastern Kingdoms. Below is a list of every citizen of Booty Bay and their reputation value. The amount gained with the Bloodsail Buccaneers is shown for a level 60 non-human. The amount lost for killing a citizen cannot be shown as it depends on your current level with Booty Bay and the importance of the person you kill. In addition to this what ever you lose with Booty Bay you will lose half of that in the other three goblin towns so if you lose 25 points in Booty Bay you will lose 12.5 points in [faction=470].\n\n[ul]\n[li][npc=4624]: 25 rep gained[/li]\n[li][npc=15088]: 25 rep gained[/li]\n[li][npc=2496]: 5 rep gained[/li]\n[li][npc=2636]: 5 rep gained[/li]\n[li][url=?npcs&filter=cr=3;crs=21;crv=0]Many more NPCs[/url]![/li]\n[/ul]\n\nThe fastest way to increase you reputation with the Bloodsail Buccaneers is to kill Booty Bay Bruisers. At first it may seem a simple task as the guards don\'t appear as threatening as the other monsters a player faces within the game. However, the guards are highly equipped to neutralize players of any class, to prevent people from attacking each other while in the town. What gives the Booty Bay Bruiser the advantage is several factors, one of them being their ability to use nets to lock you in place, preventing you from escaping. Another is the fact that they spawn every time you attack a citizen of the city or if you’re under Unfriendly status with Booty Bay the Bruisers can spawn if you enter a building, because of this players can soon find them selves swarmed by Bruisers.\n\nYet, theses are just the minor problems, in comparison to the Bruiser’s strongest ability, once it pulls out its gun its unlikely you will live, if you do not escape fast enough. Each time a guard shoots you, the attack throws you back, much like an Ogre hammer attack; the difference here is that the Bruiser can shoot in quick succession causing chain throw backs. A player can literally be thrown from one side of the town to the other, preventing you from attacking. More often you will find your self being forced into a corner, unable to move and unable to attack with each spell being interrupted by the Bruiser’s attack. Because the Bruisers do not put their guns away once they are out, the best course of action is to run away. \n\nThrough trial and error most people have discovered a safe place to kill Booty Bay Bruisers. If you follow the tunnel leading into the town, the path to your left that leads to the Blacksmith house is the ideal place to kill the guards. Only two guards patrol this path and normally don’t pass each other that closely, allowing both to be dispatched separately. Once they are gone, one can simply enter the first build on the path to cause a guard to spawn if they are below Unfriendly, if not they can simply attack one of the two NPC in the build, both of which are not high in level. Doing this a player should be able to kill 2 to 4 Bruisers before the two patrolling Bruisers re-spawn. On average a player doing this can kill about 30 to 40 Booty Bay Bruisers gaining about 800 reputation points with the pirates. The Bruisers here don’t appear to pull out their guns, but if you find your self in a bad situation, you can jump over the railing running along the path to the waters below, to escape.\n\n[h3]Rewards[/h3]\nBecoming friendly with the Bloodsail Buccaneers will grant you access to the following items:\n\n[ul]\n[li][item=12185] - Summons a [npc=11236][/li]\n[li][item=22742][/li]\n[li][item=22743][/li]\n[li][item=22745][/li]\n[/ul]\n\nYou will need Honored with the Bloodsail Buccaneers for [achievement=2336].',NULL),(8,92,0,NULL,2,'[b]Gelkis[/b] are a tribe of centaur who have made their home in the southmost parts of [zone=405]. They are mortal enemies of the [faction=93], a brother tribe also located in southern Desolace. The founding leader, or Khan, of the Gelkis was [npc=13741], second of the alleged offspring of Zaetar and Theradras. They are presently lead by [npc=5602] and the clan representative [npc=5397]. \n\nThe Gelkis hold no alliance with their brother tribes, but have been known to act both hostile and passive towards members of the Alliance and Horde.\n\n[h3]History[/h3]\nOriginally lead by the Second Khan Gelk, the Magram situated themselves in the southernmost regions of Desolace when the centaur divided into five tribes and have remained there ever since. \n\nWhen the Gelkis tribe spoke out against Khan Magra of the Magram\'s notion that strength was essential and the tribe’s survival depended on their fighting spirit, arguing that Theradras always watches over the centaur and will keep the tribes safe and alive, an eternal feud between the two tribes was born. \n\nAs such the Gelkis are more civilized - or as close as centaur can come to civilized - than their brethren, with an organised social structure and a firm grasp of the Common tongue. While the Magram only respect strength, the Gelkis respect nature and their birthmother Theradras, calling upon her protection and the power of earth to maintain their existence. Though the Magram view this as weak it would seem to be an erroneous view, as Earth Elementals can be sighted in Gelkis Village, putting an end to unwelcome intruders alongside their centaur masters.\n\n[h3]Reputation[/h3]\nOne of the two factions situated in Desolace, you are required to have a certain amount of reputation with the Gelkis in order to start their quests. Reputation for the Gelkis can be gained by killing [url=?npcs=7&filter=na=Magram]Magram monsters[/url]. When killing Magram monsters, you gain 20 reputation with Gelkis and lose 100 with the Magram tribe.',NULL),(8,93,0,NULL,2,'[b]Magram[/b] are a tribe of centaur who have made their home in the southeastern parts of [zone=405]. They are mortal enemies of the [faction=92], a brother tribe also located in southern Desolace. The founding leader, or Khan, of the Magram was [npc=13740], third of the alleged offspring of Zaetar and Theradras. They are presently lead by [npc=5601] and the clan representative [npc=5398]. \n\nThe Magram hold no alliance with their brother tribes, but have been known to act both hostile and passive towards members of the Alliance and Horde.\n\n[h3]History[/h3]\nOriginally lead by the Third Khan Magra, the Magram situated themselves against the mountain ranges of Desolace when the centaur divided into five tribes and have remained there ever since. \n\nBefore the death of Magra, he installed the idea that strength was essential and the tribe’s survival depended on their fighting spirit. When their brother tribe of Gelkis centaur spoke out against this notion, arguing that Theradras always watches over the centaur and will keep the tribes safe and alive, an eternal feud between the two tribes was born. \n\nThe life-long pursuit of strength has carried on through the Khans of Magram to this day, turning them violent and determined. To solidify their title as the strongest the tribe still fights fiercely to weaken or destroy their brother clans, viewing the Kolkar as weak, the Gelkis as nothing more than a nuisance, and the Maraudine as a formidable enemy. \n\nIt can be assumed that the Magram’s culture has developed into revolving around strength worship above all else. When compared to the Gelkis, the Magram hold very primitive forms of speech and social structure. For example, their grasp of common is limited and the position of Khan would likely be sought through a death match of sorts.\n\n[h3]Reputation[/h3]\nOne of the two factions situated in Desolace, you are required to have a certain amount of reputation with the Magram in order to start their quests. Reputation for the Magram can be gained by killing [url=?npcs=7&filter=na=Gelkis]Gelkis monsters[/url]. When killing Gelkis monsters, you gain 20 reputation with Magram and lose 100 with the Gelkis tribe.',NULL),(8,270,0,NULL,2,'[b]Zandalar Tribe[/b] trolls have come to Yojamba Isle in [zone=33] in the effort to recruit help against the resurrected Blood God and his Atal\'ai Priests in [zone=19] and in the [zone=1417].\n\n[h3]History[/h3]\nThe Zandalarians were the earliest known trolls, the first tribe from which all tribes originated. Over time two distinct troll empires emerged - the Amani and the Gurubashi. They existed for thousands of years until the coming of the Night Elves, who warred with them and eventually drove both empires into exile. \n\nFollowing the Great Sundering, the defeated Gurubashi grew ever more desperate to eke out a living. Searching for a means to survive, they enlisted the aid of the savage [npc=14834], also known as the Soulflayer. Hakkar grew into a merciless oppressor who demanded daily sacrifices from his devotees, and so in time the Gurubashi turned on their dark master. The strongest tribes (including the Zandalar) banded together to defeat Hakkar and his loyal troll priests, the Atal\'ai. The united tribes narrowly defeated the Blood God and cast out the Atal\'ai... despite their victory, however, the Gurubashi Empire soon fell. \n\nIn recent years the exiled Atal\'ai priests have discovered that Hakkar\'s physical form can only be summoned within the ancient and once-deserted capital of the Gurubashi Empire, Zul\'Gurub. Unfortunately, the priests have met with success in their quest to call forth Hakkar—reports confirm the presence of the dreaded Soulflayer in the heart of the ruins. \n\nAnd so the Zandalar tribe has arrived on the shores of Azeroth to battle Hakkar once again. But the Blood God has grown increasingly powerful, bending several tribes to his will and even commanding the avatars of the Primal Gods— Bat, Panther, Tiger, Spider and Snake. With the tribes splintered, the Zandalarians have been forced to recruit champions from Azeroth\'s varied and disparate races to battle, and hopefully once again defeat, the Soulflayer.\n\n[h3]Reputation[/h3]\nReputation with the Zandalar Tribe is gained from killing trash and bosses in Zul\'Gurub as well as repeatable and special quests which require instance-dropped items to complete. Each full run of Zul\'Gurub gives approximately 2,500-3,000 reputation.\n\nBefore the Burning Crusade, the main reason for gaining reputation with the tribe were the [url=?items=0.6&filter=na=Zandalar]shoulder[/url], [url=?items=0.6&filter=minrl=60;maxrl=60;cr=18:107;crs=4:0;crv=0:to+a+leg+or+head+slot+item]head and leg[/url] slot item enchants. As well, there were popular alchemy and enchanting recipes that many end-game guilds sought after. All rewarded items from the item set within Zul\'Gurub required a set level of reputation.',NULL),(8,349,0,NULL,2,'[b]Ravenholdt[/b] is a guild of thieves and assassins that welcomes only those of extraordinary prowess into its fold. They are diametrically opposed to the [faction=70], and are a rogue-only faction as all quests are rogue-only quests. The exception is the quest [quest=8249], which is available to non-rogues, but they would require the help of a rogue to get the items for the quest. [b]Ravenholdt Manor[/b], the faction\'s headquarters, is located in [zone=36], but to get there you have to come from the northeast corner of [zone=267].\n\n[h3]Reputation[/h3]\nAll Syndicate [url=?search=Syndicate#npcs]humanoids[/url] give 1-5 reputation points per kill depending on your current level. As well, there are a few quests that increase your reputation, but your primary method to raise your reputation is from the repeatable quests for turning in pickpocketed items.\n\nYou start off at 0/3000 Neutral with Ravenholdt, meaning if you kill any Ravenholdt NPCs before raising your reputation by at least 5, you will become Unfriendly and be unable to raise your reputation any higher ever again. To raise your reputation from Neutral to Friendly, the repeatable quest [quest=6701] is available. You will have to turn in 11-12 [item=17124] and once you are Friendly, this quest is no longer an option. From Neutral to Friendly you can also deliver five [item=16885] for Junkboxes Needed.\n\nTo raise your reputation beyond Friendly, the only choice is the repeatable quest Junkboxes Needed. There is no known faction reward for obtaining Friendly, Honored, Revered or Exalted, except that the guards speak to you with more respect. However, Exalted is required to obtain the Feat of Strength [achievement=2336].',NULL),(8,369,0,NULL,2,'[minibox]\n[h2]Steamwheedle Cartel[/h2]\n[faction=21]\n[faction=577]\n[b]Gadgetzan[/b]\n[faction=470]\n[/minibox]\n\n[b]Gadgetzan[/b] is the faction of the city Gadgetzan, which is home to goblinhood\'s finest engineers, alchemists and merchants and is the only spot of civilization in the entire desert. Rising out of the northern [zone=440] desert like an oasis, Gadgetzan is the headquarters of the Steamwheedle Cartel, the largest of the Goblin Cartels. The Goblins believe in profit above loyalty, thus Gadgetzan is considered neutral territory in the Horde/Alliance conflict.\n\n[h3]History[/h3]\nAlthough the goblins\' neutrality is almost universally acknowledged, there are still those who seek to sow chaos and anarchy. For Gadgetzan, this comes in the form of the Wastewander bandits, a gang of miscreants who have occupied the Waterspring Field and Noonshade Ruins of northeast Tanaris. Few goblins care about ancient ruins (unless they have treasure) – for all they care, the bandits can have the old blocks of stone. \n\nHowever, the Waterspring Field is vital to the goblins\' survival in the desert, providing them with the liquid gold of the desert. Water towers out in the field were constructed under the blazing heat of the desert sun by the backbreaking work of their slaves, and by Az, the goblins aren\'t going to give up their hard earned towers that easily. However, the Bruisers need to stay in town to keep the gnomes\' collective Napoleonic-complex from getting out of hand and to stop the seemingly endless dueling among the various visitors from disrupting business. Therefore, it falls to brave mercenaries from all corners of the world to help the goblins in their time of utmost need.\n\n[h3]Reputation[/h3]\nKilling the [url=?npcs=7&filter=na=Southsea]Southsea[/url] and [url=?npcs=7&filter=na=Wastewander]Wastewander[/url] monsters will increase your reputation with the Steamwheedle Cartel. Having a friendly or higher reputation will make the guards help you in case of initiated violence against you. Having an exalted reputation means that the guards will never attack you even if you initiate attacks on the opposite faction.\n\nMost of the quests associated with the Gadgetzan faction are located in Tanaris.\n\nIf you are Hated with Gadgetzan, you can do the repeatable quest [quest=9268] to obtain Neutral.',NULL),(8,470,0,NULL,2,'[minibox]\n[h2]Steamwheedle Cartel[/h2]\n[faction=21]\n[faction=577]\n[faction=369]\n[b]Ratchet[/b]\n[/minibox]\n\n[b]Ratchet[/b], the faction of the city Rachet on Kalimdor’s central east coast in [zone=17], is run by goblins and shows it. Its streets sprawl in every direction, and the architecture shows no consistency or common vision. It is a city of entertainment and trade, where anything that anyone would ever want to buy — and plenty of things that no one ever wants to buy — is on sale.\n\nRatchet is currently run by a corporate group known as the Steamwheedle Cartel a splinter group from the Venture Company, who first built the port town for trading with [zone=1637]. It is initially a neutral faction to both Horde and Alliance. A ferry conveniently connects Ratchet to Booty Bay.\n\n[h3]History[/h3]\nBuilt from equal parts of industry and decadence, the goblin port city of Ratchet sprawls along nearly a mile of of coastline where the eastern Barrens poke between [zone=14] and the [zone=15] to the sea. Ratchet is the pride of the goblins, a trade city where you can find almost anything your heart desires - and if something is not in stock, you can bet the goblins can order it. Ratchet also had regular ferries that traversed the safe though roundabout route to the island stronghold of Theramore to the south.\n\nRatchet is a city where creatures who were once the butt of jokes now reign supreme. Its streets wander without rhyme or reason through neighborhoods dedicated to one activity: commerce. Ramshackle warehouses stand next to stately stone homes. Fine shops press cheek to jowl with rude huts. Wares of every type imaginable - and some beyond the imagination - are on display in markets and in exclusive boutiques.\n\nGoblins welcome anyone with gold or items of value and a willingness to trade them for their wares and services. Merchants throng the marketplaces each day, selling everything from silks to slaves, and even at night the stores lining the twisting streets and alleys remain open for business. Those with the money can listen to skilled musicians while drinking fine ales and eating food prepared by expert chefs. For those with earthier tastes, the streets along the wharf teem with whorehouses, taprooms, and casinos.\n\nRatchet is the largest port on Kalimdor, with as many ships bringing cargo in as there are ships heading out for other sites around Kalimdor. In addition to legitimate trade vessels, pirate craft receive amnesty while in the port of Ratchet as long as they can pay the stiff docking fees. This situation makes many merchant captains furious, but they cannot hope to stay in business if they boycott Ratchet. Moreover, the Lawkeepers and hired mercenaries prowling the waterfront are eager to deal with anyone looking to cause trouble.\n\n[h3]Reputation[/h3]\nMost of the quests to raise reputation with Ratchet and the Steamwheedle Cartel are located in the Barrens. Having a friendly or higher reputation will make the guards help you in case of initiated violence against you.\n\nIf you are Hated with Rachet, you can do the repeatable quest [quest=9267] to get back to Neutral.',NULL),(8,471,0,NULL,2,'The Wildhammers are a clan of dwarves currently centered in the [zone=47] and [zone=3520]. The faction has been removed in patch 2.0.1.\n\n[h3]History[/h3]\n\nJust prior to the [object=175739], the Wildhammer Clan, ruled by Thane Khardros Wildhammer, inhabited the foothills and crags around the base of Ironforge. The Wildhammer Clan was unsuccessful in wresting control of [zone=1537] from the Bronzebeard and Dark Iron clans. Khardros and his Wildhammer warriors traveled north through the barrier gates of Dun Algaz, and founded their own kingdom within the distant peak of Grim Batol. There, the Wildhammers thrived and rebuilt their stores of treasure.\n\n[npc=9019] and his Dark Irons vowed revenge against Ironforge. Thaurissan and his sorceress wife, Modgud, launched a two-pronged assault against both Ironforge and Grim Batol. As Modgud confronted the enemy warriors, she used her powers to strike fear into their hearts. Shadows moved at her command, and dark things crawled up from the depths of the earth to stalk the Wildhammers in their own halls. Eventually Modgud broke through the gates and laid siege to the fortress itself. The Wildhammers fought desperately, Khardros himself wading through the roiling masses to slay the sorceress queen. With their queen lost, the Dark Irons fled before the fury of the Wildhammers.\n\nOnce the immediate Dark Iron threat was eliminated, the Wildhammers returned home to Grim Batol. However, the death of the Modgud had left an evil stain on the mountain fortress, and the Wildhammers found it uninhabitable. Khardros took his people north towards the lands of Lordaeron. Settling within the mountainous region of the Aerie Peaks and The Hinterlands, and lush forests of Northeron, the Wildhammers crafted the city of Aerie Peak, where the Wildhammers grew closer to nature and even bonded with the mighty gryphons of the area. Over time they started calling their land the Hinterlands. \n\n[b]Modern Wildhammers[/b]\nThe Wildhammer Clan currently makes its home at Aerie Peak in the Hinterlands. The most immediate threat to their security comes from the east in the form of the Witherbark Trolls and Vilebranch Trolls. They are most famous for riding into battle atop Gryphons, while wielding powerful Stormhammers.\nWildhammer dwarves have a number of clans, each ruled by a Thane. The strongest Thane rules Aerie Peak.',NULL),(8,509,0,NULL,2,'[b]The League of Arathor[/b] was originally established by the survivors of the Kingdom of Stromgarde to reclaim the [zone=45] from the hands of the Forsaken Defilers in Hammerfall. Today it is an organization in support of the Alliance, based out of the [zone=3358] in Refuge Pointe. They have taken it upon themselves to help supply the Alliance forces where needed, and their members include all manner of Alliance races - even though they are still predominantly Stromgardian humans.\n\n[h3]Reputation[/h3]\nPlayers can earn reputation in this faction by participating in the Arathi Basin battleground. When you fight in Arathi Basin you earn 10 reputation per 160 resources. On Arathi Basin holiday weekends the required resources is reduced to 150.\n\nYou are granted the player title [title=48] once exalted with League of Arathor and the other two battleground factions, [faction=890] and [faction=730].',NULL),(8,510,0,NULL,2,'[b]The Defilers[/b] seek to foil the [faction=509] in the [zone=3358] battleground. Today it is an organization in support of the Horde, based out of Hammerfall in [zone=45]. They have taken it upon themselves to help supply the Horde forces where needed, and their members include all manner of Horde races - even though they are still predominantly orcs.\n\n[h3]Reputation[/h3]\nReputation is gained through participation in the Arathi Basin battleground. When you fight in Arathi Basin you earn 10 reputation per 160 resources. On Arathi Basin holiday weekends the required resources is reduced to 150.\n\nYou are granted the player title [title=47] once exalted with the Defilers and the other two battleground factions, [faction=889] and [faction=729].',NULL),(8,529,0,NULL,2,'The [b]Argent Dawn[/b] is an organization focused on protecting Azeroth from the threats that seek to destroy it, such as the Burning Legion and the Scourge. Strongholds of the Argent Dawn can be found in the [zone=139] and [zone=28]. It also maintains a presence in [zone=1657] and in the [zone=85], among other less notable areas. Reputation with the Argent Dawn can be used to purchase various profession recipes, misc. consumables, and to mitigate the cost of attunement to [zone=3456]. With the expansion of the Burning Crusade, Argent Dawn reputation has decreased in value.\n\nArgent is Latin for silver, which could explain why the [item=22999] has an icon of a silver sun rising.[h3]History[/h3]After the death of the [npc=16062], the corruption of the Scarlet Crusade became apparent to some of its members, who subsequently left the ranks of the [url=?search=scarlet+crusade#M0z]Scarlet Crusade[/url] and established the Argent Dawn to protect Azeroth from the threat of the Scourge without the blind zealotry present in the Scarlet Crusade.\n\nWhile they share the same goals as the Crusade, the Argent Dawn has opened its ranks to not only other Alliance races besides Humans, but also members of the Horde and even some of the Forsaken. They caution discretion and introspection, and put a lot of emphasis on researching the Scourge and how to combat them.\n\nWith time the Argent Dawn has grown diversified, and like its progenitor — the Scourge — has split again, with an offshoot called the [url=?search=brotherhood+of+the+light]Brotherhood of the Light[/url], a compromise between the Argent Dawn\'s more scholarly approach and the Scarlet Crusade\'s fanaticism.\n\n[h3]Reputation[/h3]\n[b]Scourgestones[/b]\nWhile wearing a trinket granting the Argent Dawn Commission effect, characters can loot [url=?items=12&filter=na=scourgestone]scourgestones[/url] from undead monsters they\'ve killed, and subsequently turn them in in exchange for [item=12844]. These turn-ins require various numbers of [item=12843], [item=12841], and [item=12840]. It should be noted that the token items received from the turn-ins should be saved until after Revered status is reached, as the quest turn-ins will no longer grant reputation after this point.[pad][b]Cauldrons[/b]\nAnother way to gain reputation with the Argent Dawn is through repeatable \"Cauldron\" quests. The Cauldrons are a source of \"undeathness,\" that contribute to the Scourge\'s numbers.[pad][b]Instances[/b]\nLike most factions, the player can run instances to increase his reputation. These instances are [zone=2017] and [zone=2057]. Naturally, these instances also include quests that will raise Argent Dawn reputation, as well as include Scourgestone drops.',NULL),(8,530,0,NULL,2,'[b]Darkspear Trolls[/b], the tribe of exiled trolls that has joined forces with [npc=4949] and the Horde. They now call [zone=1637] their home, which they share with their orc allies. [npc=10540] is their current leader.\n\n[h3]History[/h3]\nAs tribal rivalries erupted throughout the former Gurubashi Empire, the Darkspear Tribe found themselves driven from their homeland in [zone=33]. Having settled in what are believed today to be the Broken Isles, the tribe soon found themselves entangled in a conflict with a band of murlocs. Their fate seemed sealed until the orcish Warchief Thrall and his band of newly freed orcs took shelter on their island home. Controlled by a Sea Witch, a group of rampaging murlocs captured the Darkspears\' leader Sen\'jin, along with Thrall and several other orcs and trolls. Thrall managed to free himself and others, but was ultimately unable to save the trolls\' leader. Although Sen\'jin was sacrificed to the Sea Witch, he was able to reveal a vision he had in which Thrall would lead the Darkspear from the island. \n\nAfter returning to the island, Thrall and his followers managed to fend off further attacks by the Sea Witch and her murloc minions, and set sail for Kalimdor once again. Under the new leadership of [npc=10540], the Darkspear swore allegiance to Thrall\'s Horde and followed him to Kalimdor. Now considered enemies by all other trolls except the Revantusk and the Zandalari, the Darkspear are held in contempt to this day. Yet, the Darkspear have not forgotten being driven from their ancestral homes and this animosity is eagerly returned, especially towards the other jungle trolls. Having reached the orc\'s new homeland, [zone=14], the trolls carved out another home for themselves - this time among the Echo Isles on the eastern shores of the new orc kingdom. \n\nHowever, with the coming of Kul Tiras and its navy, the Darkspear were forced to retreat inland under the onslaught of the misguided commander [npc=177201]. The trolls, fighting alongside their horde brethren, defeated the enemy and reclaimed their new homeland. Shortly thereafter, a witch doctor by the name of [npc=3205] began using dark magic to take the minds of his fellow Darkspear. As his army of mindless followers grew, Vol\'jin ordered the free trolls to evacuate, and Zalazane took control of the Echo Isles. The Darkspear have since settled on the nearby shore, naming their new village after their old leader, Sen\'jin. From Sen\'jin Village they, along with their allies, send forces to battle Zalazane and his enslaved army.\n\n[h3]Reputation[/h3]\n[npc=14727] has the repeatable cloth reputation quests. As a reward for being exalted with the Darkspear Trolls, non-troll Horde players are able to ride [url=?items=15.5&filter=na=Raptor;cr=93:92;crs=2:1;crv=0:0]raptors[/url].\n\nSurrounding zone Durotar contain the most quests for gaining reputation with the Darkspear Trolls. As well, higher level players with the Burning Crusade also have a good amount of quests in [zone=3521].',NULL),(8,576,0,NULL,2,'As the last uncorrupted furbolg tribe (at least in their view), the [b]Timbermaw[/b] seek to preserve their spiritual ways and end the suffering of their brethren.\n\nThe Timbermaw Furbolgs inhabit two areas: [zone=16] and [zone=361]. They are presumed to be the only furbolg tribe to escape demonic corruption, though this may not be true due to the existence of [npc=3897], an uncorrupted furbolg of unknown tribe, and the Stillpine tribe on [zone=3524] in Burning Crusade. However, many other races kill furbolg blindly now, without bothering to see if they are friend or foe. For this reason, the Timbermaw furbolg trust very few.\n\nAdventurers who seek out Timbermaw Hold in northern Felwood and prove themselves as friends of the Timbermaw will learn that the furbolgs value their friends above all else. Though they possess no fine jewels or any worldly riches, the Timbermaw\'s shamanistic tradition is still strong. They know much about the art of crafting armors from animal hides, and they are more than happy to share their healing/resurrection knowledge with friends of their tribe. Besides, any reputation above Unfriendly will also grant you untroubled access to [zone=493] and [zone=618] through their tunnels.\n\n[h3]Reputation[/h3]\nReputation with the Timbermaw Hold faction is mainly gained through quests and killing in Felwood. The members of the Deadwood Tribe, another Furbolg tribe in Felwood, are the Timbermaws\' main enemies.\n\n[ul]\n[li]Killing one [url=?npcs&filter=na=Winterfall]Winterfall[/url] or [url=?npcs&filter=na=Deadwood]Deadwood[/url] Furbolg gives 10 reputation points. Gains stop at revered; Deadwoods give 2 reputation point at honored.[/li]\n[li]Killing either one of the Deadwood Bosses [npc=9464] or [npc=9462], is worth 60 reputation. There is no reputation limit.[/li]\n[li]Killing the elite Winterfall Furbolg, [npc=10738], located in a cave east of [faction=577], awards 50 reputation. There is no reputation limit, and his respawn rate is 6 to 8 minutes.[/li]\n[li]Killing the named rare mob [npc=14342] is worth 50 reputation. He is a rare spawn at Deadwood Village in Felwood and there is no reputation limit for this mob.[/li]\n[li]Killing the named rare mob [npc=10199] is worth 50 reputation. He is a rare spawn at Winterfall Village in Winterspring. Killing him will grant reputation up until Revered.[/li]\n[li]After completing [quest=8460], turning in 5 [item=21377] yields 150 reputation.[/li]\n[li]After completing [quest=8464], you will be able to turn in [item=21383] collected from furbolgs in Winterspring. Turning in 5 beads at [npc=11556] yields 150 reputation.[/li]\n[/ul]',NULL),(NULL,NULL,0,'commenting-and-you',2,'[menu tab=2 path=2,13,0]One of many useful features is the user-submitted comment system. This system allows users to submit their own comments to augment the data provided here. As a rule, we promote the submission of informative comments, but we also like to see the occasional joke. Moderators and users alike will apply positive and negative ratings to comments in an effort to promote the useful ones and purge unnecessary information.\r\n\r\nWith that in mind, below is a guide that can be used to determine how your comment will likely be received by the community. \r\n\r\n[pad]\r\n\r\n[tabs name=comments]\r\n\r\n[tab name=\"Before you post\"]\r\n\r\n[ul]\r\n[li][b]Read existing comments[/b] – Sometimes, the information you have may already have been posted by another user. In this case, if the information is useful, the existing comment should be given a positive rank. Posting information that was already added in a previous comment will likely result in a negative rating.[pad][/li]\r\n[li][b]Verify your facts[/b] – Make sure that what you have to post is true. A friend might tell you that a mob is immune to Frost Nova, but unless you verify that yourself, you could be posting a potentially misleading comment.[pad][/li]\r\n[li][b]Temporary usability[/b] – If you want to correct invalid or missing information on a page, keep in mind that your comment may go from a positive ranking to a negative ranking when the correction occurs. For example, informing the community that a spell is cast by Illidan Stormrage before that data has been collected will be useful at first, but once Aowow learns to parse that information and adds it to the \'Abilities\' tab, your comment becomes redundant. If you do not want to worry about the comment or do not want one of your comments to be rated negatively, consider informing us in the [url=/?forums&board=1.]Site Feedback[/url] forum. The moderation staff will be happy to add a comment to correct invalid or missing information on the page for you. Alternatively, you can delete your comment later when it becomes redundant.[/li]\r\n[/ul]\r\n\r\n[/tab]\r\n\r\n[tab name=\"Comment ratings\"]\r\n\r\n[h3][color=q2]Positive (+1)[/color][/h3]\r\n[ul]\r\n[li][b]Corrections on drop percentages[/b] – There are many instances where drop percentages will be inaccurate. For example, quest items do not drop for people who do not have the quest, so their drop percentages will be low. Also, mobs that periodically do not drop loot when they die won\'t count against the drop percentages, so these mobs may appear to have higher drop rates for some items.[pad][/li]\r\n[li][b]Strategies[/b] – If you have a strategy that can assist other users in completing a quest or defeating a mob, by all means, share![pad][/li]\r\n[li][b]Quest coordinates[/b] – Providing coordinates for the location of quest items or mobs is always useful. When possible, you should provide links to quest targets as well.[pad][/li]\r\n[li][b]Theorycrafting[/b] – We encourage users to post any information they have regarding complex calculations they may have performed to, for example, prove one item has a higher DPS than another given certain abilities.[pad][/li]\r\n[li][b]Just for laughs[/b] – If your comment is one that would be universally funny (i.e. not an inside joke), post away. We like to laugh as much as anyone else. Of course, whether your joke is funny or not is subject to our other users. :)[/li]\r\n[/ul]\r\n\r\n[h3][color=q10]Negative (-1)[/color][/h3]\r\n[ul]\r\n[li][b]Redundant information[/b] – For instance, a comment that says \"Dropped by Ragnaros\" does not add anything to the page as that information can be viewed in the \"Dropped By\" tab of the page in question.[pad][/li]\r\n[li][b]Soloed by:[/b] Unless your comment contains a detailed explanation of how you defeated a mob, these comments do not add anything to the page. Simply stating your level, class, and that you soloed the mob by using a few skills is not enough to be useful.[pad][/li]\r\n[li][b]Dropped in X kills[/b] – Telling users that you were lucky enough to get the crusader enchant in one drop is not considered useful information.[pad][/li]\r\n[li][b]NPC/Object coordinates[/b] – The coordinates for NPC or mobs are already supplied in convenient maps within the interface.[pad][/li]\r\n[li][b]Best X before level Y[/b] – Simply posting that an item is the best twink weapon or the best dagger for a rogue is not helpful unless you can back up that claim with facts.[pad][/li]\r\n[li][b]HUNTAR WEPPON[/b] – While it would be acceptable to explain why you feel a certain class with a certain spec would gain the most benefit from an item, simply stating that you feel the weapon should always go to a hunter in a raid will result in negative moderation.[pad][/li]\r\n[li][b]Confirmed![/b] – Adding a comment that simply indicates that you have confirmed a comment left by someone else clutters the comments. The best way to confirm a comment as correct is to give it a positive ranking. A comment with a high ranking will indicate to users that many people think it is useful data.[/li]\r\n[/ul]\r\n\r\n[/tab]\r\n\r\n[tab name=Deletion]\r\n\r\nAny comment that does not abide by the same [forumrules] will be deleted by a moderator.\r\n\r\n[/tab]\r\n\r\n[/tabs]',NULL),(NULL,NULL,0,'item-comparison',2,'[menu tab=2 path=2,13,5]Can\'t find the answer you were looking for? Just [url=/?aboutus#contact]contact us[/url], or post on our [url=/?forums&board=1]forums[/url]! \r\n\r\n[pad]\r\n\r\n[tabs name=compare]\r\n\r\n[tab name=\"General usage\"]\r\n\r\n[h3]Basic Controls[/h3]\r\n\r\n[ul]\r\n[li][img src=STATIC_URL/images/icons/save.gif border=0] [b]Save[/b] – Saves the comparison so that you may continue browsing the site without losing it. When you click on the [b]Compare[/b] button found throughout the site you will be given the option to add to your saved comparison.[/li]\r\n[li][img src=STATIC_URL/images/icons/refresh.gif border=0] [b]Autosaving[/b] – Indicates that you are viewing your saved comparison, and that any changes you make will automatically be saved. To avoid modifying your saved comparison, you may click on Link to this comparison before making any changes.[/li]\r\n[li][img src=STATIC_URL/images/icons/link.gif border=0] [b]Link to this comparison[/b] – Provides a link to a new page with the current item comparison already there! Useful for showing friends your item comparisons.[/li]\r\n[li][img src=STATIC_URL/images/icons/delete.gif border=0] [b]Clear[/b] – Removes all items, groups, and weights from the comparison tool, giving you a clean slate to work with. [b]This will [u]delete[/u] your saved comparison if used while autosaving.[/b][/li]\r\n[li][img src=STATIC_URL/images/icons/add.gif border=0] [b]Weight scale[/b] – Allows you to add one or more weight scales to the item comparison using your own weights or one of our predefined presets. Each weight scale can have its own name. A saved comparison also contains the weight information, allowing you to store custom weight scales for future use.[/li]\r\n[li][img src=STATIC_URL/images/icons/add.gif border=0] [b]Item[/b] – Opens a live search that displays item suggestions as you type the name of an item. Clicking on a suggestion will add that item to your comparison.[/li]\r\n[li][img src=STATIC_URL/images/icons/add.gif border=0] [b]Item set[/b] – Opens a live search that displays item set suggestions as you type the name of an item set. Clicking on a suggestion will add all of the items in that set to your comparison.[/li]\r\n[/ul]\r\n\r\n[h3]Adding Items[/h3]\r\n[div float=right align=right][img src=STATIC_URL/images/help/item-comparison/addingitems.gif]\r\n[small]Some of the ways to add items to a comparison.[/small][/div]The comparison tool is fully integrated with our site and designed to be as convenient as possible to work with. There are many ways to add items to a comparison depending on what part of the site you are on: \r\n[ul][li]Using the [url=/?compare]item comparison tool[/url] itself, you may add items or item sets using the links in the top right corner as described above.[/li]\r\n[li]Viewing an [url=/?item=35137]item[/url] or [url=/?itemset=-17]item set[/url] page, you may click on the red [b]Compare[/b] button near the Quick Facts box.[/li]\r\n[li]Viewing [url=/?items=4.2&filter=sl=8]search results[/url] or [url=/?npc=34077#sells]any page with a list of items[/url], checkboxes are displayed next to items which can be equipped. You may select one or more items and click the [b]Compare[/b] button at the top of the list.[/li][/ul]\r\n\r\n[i]Note: If you have a comparison saved, and you add items to your comparison from elsewhere on the site, you will be given the option to add them to your saved comparison or create a new one. If you don\'t have a saved comparison, a new comparison will automatically be created and saved with the selected items.[/i]\r\n\r\n[h3]Managing Your Items[/h3]\r\n[div float=right align=right][img src=STATIC_URL/images/help/item-comparison/newgroup.gif]\r\n[small]Creating a new group by dragging an item.[/small][/div]\r\n[ul][li][b]Creating a new group[/b] – [u]Drag an item into the empty column[/u] on the right to create a new group containing that item.[/li]\r\n[li][b]Moving[/b] – To move an item or group, click on the item (or the group\'s control bar) and [u]drag it to the desired position[/u].[/li]\r\n[li][b]Copying[/b] – [u]Holding shift while dragging[/u] an item or group will make a copy of it when it is dropped.[/li]\r\n[li][b]Deleting[/b] – Items and groups can be deleted by [u]dragging them out of the row[/u]. Groups may also be deleted by clicking the X on the right side of the group\'s control bar.[/li]\r\n[li][b]Deleting all but one group[/b] – [u]Holding shift while deleting a group[/u] (see above) will cause all other groups to be deleted instead of that one.[/li]\r\n[li][b]Splitting a group[/b] – Groups of 2 or more items can be split by [u]clicking on [b]Split[/b] in the menu dropdown[/u] on the group\'s control bar. This will create a new group for each item in the current group.[/li]\r\n[li][b]Exporting a group[/b] – [u]Clicking on [b]Export[/b] in the menu dropdown[/u] of the group\'s control bar will take you to a new comparison containing only the current group.[/li]\r\n[li][b]Item Enhancements[/b] - To add gems or enchantments to an item, [u]right-click on the item icon at the top[/u], then select the desired option from the menu. The stats will automatically update—including the set bonuses.[/li]\r\n[/ul]\r\n\r\n[/tab]\r\n\r\n[tab name=\"Advanced features\"]\r\n\r\n[h3]Level Adjustments[/h3]\r\nYou can select your desired character level from the dropdown at the top left. When you do, all the statistics that change according to your level (including combat ratings and heirloom item stats) will automatically adjust to the corresponding value for the level you\'ve entered.\r\n\r\n[h3]Gains[/h3]\r\nAt the bottom of the item comparison is a special row called \'Gains\'. The gains row calculates the minimum values of all stats that appear in any group in the item comparison. It then displays the bonuses each row has [b]above[/b] this minimum.\r\n\r\nFor example, the minimum stamina for any group in [url=/?compare=35031;35030;35029;35028;35027]this comparison[/url] is 50. The gains row displays nothing for the items which have 50 stamina, +23 sta for the item with 73 stamina, and +27 sta for the items with 77 stamina.\r\n\r\nBasically, the gains row removes the shared stats between all groups so that you can focus on what each group brings to the table.\r\n\r\n[h3]Focus Group[/h3]\r\n\r\n[screenshot url=STATIC_URL/images/help/item-comparison/focus2.gif thumb=STATIC_URL/images/help/item-comparison/focus.gif float=right]Comparing arena sets of the first four PvP\r\nseasons using a focus group.[/screenshot]Setting a focus group is done by clicking on the eye icon in the group\'s control bar. Selecting a group as your focus will update the display of the item comparison to show the difference in stats between all other groups and the focus group.\r\n\r\nWhen a focus is set, the focus group is highlighted and each other group has numbers that indicate the stats gained or lost in comparison to the focus group.\r\n\r\n[b][color=q2]Positive[/color][/b] numbers indicate that group has a higher total for a given stat than the focus group, while [b][color=q10]negative[/color][/b] numbers indicate that group has a lower total for a given stat than the focus group. \r\n\r\n[h3]Stat Weighting[/h3]\r\nTo add a weight scale to your comparison, click on the [b]Add a weight scale[/b] link in the top right corner. You may select a weight scale from our predefined presets or create one of your own. Each weight scale may be given a name that will appear in the score tooltips to help differentiate the different scores. You may add as many weight scales as you like.\r\n\r\nTo remove a weight scale, click on the [b]X[/b] next to the appropriate score in any group. To toggle between normalized (default), raw, and percent score mode, click on the score in any group.\r\n\r\nUnlike the weighted item search, these weight scales [b]do not[/b] automatically select gems or include socket bonuses in the score at this time.\r\n\r\n[h3]Viewing a Group in 3D[/h3]\r\nClick on [b]View in 3D[/b] in the menu dropdown of the group\'s control bar to display a 3D model of the items and select the race and gender to display them on. Of course, items which do not have models, such as trinkets and rings, will not be displayed.\r\n\r\n[/tab]\r\n\r\n[/tabs]',NULL),(NULL,NULL,0,'stat-weighting',2,'[menu tab=2 path=2,13,3]Can\'t find the answer you were looking for? Just [url=/?aboutus#contact]contact us[/url], or post on our [url=/?forums&board=1]forums[/url]! \r\n\r\n[pad]\r\n\r\n[tabs name=weights]\r\n\r\n[tab name=FAQ]\r\n\r\n[h3]How do weights work?[/h3]\r\nThe weighting system allows you to give a weight value to attributes that matter to you and applies your ratings to items in your search results. Each weight value is multiplied by an item\'s stat points and then added together to get the item\'s total score. This score is used to sort the results and display the highest scoring items.\r\n\r\nIf you decide that spell damage is worth twice as much as spell crit, you could add the weights as 2 and 1, 100 and 50, or any other numbers with the same ratio.\r\n\r\nPlease note that weights only work for [url=/?items=4]Armor[/url], [url=/?items=2]Weapons[/url], [url=/?items=3]Gems[/url] and [url=/?items=0]Consumables[/url]. \r\n[h3]What is the difference between weights and equivalency?[/h3]\r\nThe equivalency of two attributes describes how much one equals the other. You may find equivalency ratings that say something like 1 agility = 1.5 strength. This is [b]not[/b] the same as weight values; in fact, it\'s the exact opposite! Equivalency describes the ratio of the stats to each other, which can be used to derive the stat weights. In this example, an appropriate set of weights might be agility 3 and strength 2; this works out to agility being [i]1.5 times as valuable[/i] as strength. \r\n[h3]Is there a way to save a template that I have created?[/h3]\r\nThere sure is! You can save your stat weighting scales by going to the \'Preset\' dropdown menu, selecting \'custom,\' and then filling in your own weights. After you\'ve modified them to your liking, you can hit \'Save\' to give them a name so they can be used for future searches as well.\r\n\r\nWeights also carry over from one item list to another if you use the database menu, so going from a [url=/?items=2&filter=wt=51:48:49;wtv=83:67:58]weighted list of weapons[/url] to the [url=/?items=4&filter=wt=51:48:49;wtv=83:67:58]cloth armor listing[/url] will also maintain your current weight scale. \r\n[h3]Is it better to match sockets and gain the socket bonus, or use the best gems?[/h3]\r\nThe weighting system answers this for you automatically. It compares the score of matching gems plus the score of the socket bonus, to the score of the best gems it could put in that item. It will automatically put in the gems that result in the highest net rating, taking socket bonuses into account. When the socket colors are matched, the socket bonus text will be listed below the gems for each item. \r\n\r\n[h3]What are the default weight presets based on?[/h3]\r\nWe\'ve done a great deal of research, tracking down equivalence points for all of the classes. We\'d like to thank all of the hard-working theorycrafters at [url=http://elitistjerks.com/f47/t21302-theorycrafting_think_tank/]Elitist Jerks[/url], [url=http://forums.tkasomething.com/showthread.php?t=9542]TKA Something[/url], [url=http://shadowpanther.net/aep.htm]Shadow Panther[/url], [url=http://druid.wikispaces.com/Healing+Gear+List]The Druid Wiki[/url], [url=http://www.emmerald.net/]Emmerald[/url], [url=http://www.lootrank.com/wow/templates.asp]Lootrank[/url], [url=http://pawnmod.trenchrats.com/index.php]Pawn Mod[/url], and [url=http://www.codeplex.com/Rawr]Rawr[/url], as well as a host of threads on the World of Warcraft forums. They provided the inspiration for the weighted search and a starting point for our preset values.\r\n\r\n[/tab]\r\n\r\n[tab name=\"Helpful tips\"]\r\n\r\n[ul]\r\n[li]You can help us [b]improve[/b] our presets! Email your suggestions to [feedback].[/li]\r\n[li]Don\'t weight stats that your character is [b]already capped on[/b] (e.g. Hit rating). Be sure to tweak the presets as needed![/li]\r\n[li]You can adjust a preset by clicking on the \'show details\' button.[/li]\r\n[li]Once you have generated a weighting you like, you can bookmark that page. Then, if you browse around on other pages using the menus at the top, your weight scale will be applied to that page as well.[/li]\r\n[/ul]\r\n\r\n[/tab]\r\n\r\n[tab name=Why?]\r\n\r\n[h3]Why does it give a higher score to 2H weapons over 1H weapons, when using a 1H + OH is better?[/h3]\r\nThe scores are based off the stat weights of the item by itself. Two-handers rank higher because by themselves they do have better stats than a one-hander with nothing else in the off hand. If you add up the scores of a main hand and off hand item, the total score is what you should use to compare to that of a two-hander. We do not assume a score for your offhand item, as there is no way of knowing what you have or can obtain for that slot unless you do a weighted search for it. \r\n[h3]Why does the preset list X as more important than Y?[/h3]\r\nSome attributes come in unusual value ranges on items, which affects their equivalency to other stats. It does not mean that your should focus on or ignore that stat, but that a single point of it is worth more or less compared to other stats. Stats with high number ranges (armor, weapon damage, penetration, etc) will require smaller weight values, while stats with low number ranges (mana regeneration) will require much larger weight values.\r\n\r\nIn essence, giving mana regeneration a score of 100 and healing a score of 25 does [b]not[/b] say that mana regeneration is more important than healing, simply that each point of mana regeneration is the equivalent of 4 points of healing.\r\n[h3]Why don\'t you have a preset for PvP/Tier 6 Raiding/...? Why doesn\'t your preset give a stat value for X?[/h3]\r\nIf you would like to suggest changes to the existing presets or new presets for other specs or situations, please do so to [feedback]. \r\n[h3]Why doesn\'t the preset limit the items to X, Y, and Z?[/h3]\r\nThe weight presets are for sorting; filters are for limiting the search results. If you want to restrict the items you see, use the appropriate tool - the filter options. The only limit applied by the weight scales is that it will not display items with a score of 0 or less. You should continue to use the existing filtering system if you want to see items of a specific type, slot, source, speed, etc.\r\n[h3]Why does it suggest the gems it does for the sockets?[/h3]\r\nThe suggested gems are based on your weights. If you would like to see a different gem in the sockets, try increasing the weight of the appropriate stat. If you feel the weights in the presets need to be adjusted, please let us know at [feedback].\r\n\r\n[/tab]\r\n\r\n[/tabs]',NULL),(NULL,NULL,0,'screenshots-tips-tricks',2,'[menu tab=2 path=2,13,2]\r\n\r\nWe thrive on user contributions! Quest data, database comments, forum posts - you name it, we love it! One of our favorite methods of contribution is via uploaded [b]screenshots[/b], images depicting various items, NPCs or quest details in the World of Warcraft. Users can submit screenshots to any database page which will then be reviewed by our staff and, upon approval, added to a database page! Taking and uploading screenshots is easy!\r\n\r\n[small]The information below is graciously provided by [url=http://us.blizzard.com/support/article.xml?locale=en_US&articleId=21048]Blizzard Support[/url].[/small]\r\n[h3]Taking Screenshots on Windows[/h3]\r\n[ul]\r\n[li]While in the game, press the Print Screen key on your keyboard.[/li]\r\n[li]You should see a \"Screen Captured\" message.[/li]\r\n[li]The screenshot will appear as a .JPG file in the Screenshots folder, in your main World of Warcraft directory.[/li]\r\n[li]You should be able to double click on the screenshot files to view the screenshots in Windows default image viewer.[/li]\r\n[/ul]\r\n\r\n[b]Extra notes for Windows Vista users[/b]\r\n[ul]\r\n[li]Due to extra security on the system the screenshots will be saved to the following folder:C:\\\\users\\\\*your user name*\\\\AppData\\\\Local\\\\VirtualStore\\\\Program Files\\\\World of Warcraft\\\\Screenshots[/li]\r\n[li]You may also have to turn on the ability to view hidden files as the AppData folder may be hidden.\r\n[ul]\r\n[li]Click the Start/Window button, select Control Panel, Appearance and Personalization, Folder Options.[/li]\r\n[li]Next click on the View tab, under the Advanced settings, click Show hidden files and folders, and click OK to finish.[/li]\r\n[/ul][/li]\r\n[/ul]\r\n\r\n[h3]Taking Screenshots on Mac[/h3]\r\n[ul]\r\n[li]Players can take a screenshot in-game using the keyboard key bound to the Print Screen functionality.[/li]\r\n[li]If you have a keyboard with an F13 key, press the key to take an in-game screenshot. Players without an F13 key on the keyboard can change the default Screen Shot key in the Key Bindings menu.[/li]\r\n[li]You should see a \"Screen Captured\" message.[/li]\r\n[li]The screenshot will appear as a JPEG file in the Screenshots folder, in your main World of Warcraft folder.[/li]\r\n[/ul]\r\n\r\nRemember to turn off your in-game UI using the Alt+Z (or ⌘+V) command! Upon taking your screenshot, you can then go in and use an image editor (such as the free program [url=http://www.getpaint.net]Paint.NET[/url]) to crop your image for faster upload. You can select specific sections of a screenshot to upload (if you are featuring a particular piece of armor, for example) and save the file, then simply upload your pre-cropped image directly! If not, you can easily crop your screenshot after uploading but before submitting using our handy tool.\r\n\r\nTo submit a screenshot, simply navigate to the database entry for which you\'ve taken a screenshot and navigate to the \'Contribute\' section. Select the \'Submit a screenshot\' tab and click \'Choose file\' to locate the file on your system. Remember that only PNG and JPG file types are accepted! Once you have selected the screenshot simply click \"Submit\" and you\'re on your way! You will then be able to crop the image if necessary before your image is finally submitted for review. Upon approval (which may take up to 72 hours) your screenshot will then be featured on the database page, as well as in a \'Screenshots\' tab in your user profile!\r\n\r\n\r\n[h2]Quality Tips[/h2]\r\n\r\n[screenshot url=STATIC_URL/images/help/screenshots/hinterlands.jpg thumb=STATIC_URL/images/help/screenshots/hinterlands2.jpg float=right]The Hinterlands[/screenshot]A good screenshot is like a miniature piece of art. It should showcase the main object, but take into account the details around it. The same 7 elements of art design come into play here, Line, Shape, Form, Space, Texture, Light & Color. We\'ll touch on several of these and how to make use of the in game settings and mechanics to enhance your pictures.\r\n\r\nTurn your resolution and color sampling as high as your computer can handle. Turn on all the image effects and details, but turn down the weather effects to the lowest setting. In general you want all your glow and spell effects maxed to really show the environment to its fullest potential (they actually help with the lighting too!) You may find a shot that you need to play with these settings to enhance, sometimes turning down environmental detail is helpful to remove extra grasses.\r\n\r\nWorld of Warcraft actually has an internal setting for screenshot quality, and by default that quality is set to [b]3/10[/b]. You can turn this up, though, in order to take higher quality screenshots. In order to do so, type this command into your chatbox:\r\n\r\n[code]/console screenshotQuality 10[/code]\r\n\r\nMost of the time taking the pictures from 1st person view works best, so zoom all the way in so that you\'re looking through your character\'s eyes. Occasionally the object might be too big (large NPCs especially) to use this view - if this is the case get as close to them as you can without having your body in the shot and swing the camera around to get the angle that you\'re looking for.\r\n\r\nPay attention to the light - a well lit picture is 10 times better than a dark one. You may even want to do a little color correcting before uploading - increase the brightness and contrast a touch. For instance - it\'s a lot easier to take pictures in sunny Stormwind than deep in the mountains of torch lit Ironforge. Daytime pictures also turn out better than night.\r\n\r\n[h3]Featuring Armor[/h3]\r\n\r\n[screenshot url=STATIC_URL/images/help/screenshots/armor.jpg thumb=STATIC_URL/images/help/screenshots/armor2.jpg float=right]Dreamwalker Spaulders[/screenshot]We want to see the armor! Not Joe Schmoe in the armor. In general you want close ups of the piece itself (except for full set pictures). Don\'t be afraid to submit a 4 inch picture of one glove. Once\'s it\'s cropped and loaded and shrunk down to the thumbnail it will look great!\r\n\r\nUse your best judgment when cropping armor pics, but remember - we want to see details of the armor - not the person or a far away image. Of course, this also applies to weapons or any other piece of equipment!\r\n\r\n[h3]Featuring NPCs[/h3]\r\n\r\n[screenshot url=STATIC_URL/images/help/screenshots/npc.jpg thumb=STATIC_URL/images/help/screenshots/npc2.jpg float=right]Cairne Bloodhoof [/screenshot]Full body shots should be the norm. If you can\'t get a good full shot (e.g. they\'re standing behind a counter) get the waist up shot. There\'s no need to include the on-screen text and titles of NPCs. The website already lists those, so just get in close and take a great shot of the NPC itself.\r\n\r\nGet down on their level - you may need to \"/sit\" or even \"/sleep\" to get a good view of something low to the ground (scorpions, boots, spiders, etc.)\r\n\r\nWhen capturing moving NPCs, try to get as much a head on front shot as you can, being willing to take a few hits while you take picture of a mob attacking you can make for a great shot. If you don\'t want to get your hands dirty, sitting in place for a while and waiting for it to path in front of you is often easier and faster than running around it trying to get your shot.\r\n\r\nTalking to friendly NPCs will usually make them face you - you can then spin around and get the best background for your picture. You may also catch them in an interesting motion or gesture.',NULL),(NULL,NULL,0,'profiler',2,'[menu tab=2 path=2,13,6]Can\'t find the answer you were looking for? Just [url=/?aboutus#contact]contact us[/url], or post on our [url=/?forums&board=1]forums[/url]!\r\n\r\n[pad]\r\n\r\n[tabs name=profiler]\r\n\r\n[tab name=\"Browsing characters\"]\r\n\r\n[div float=right align=right][img src=STATIC_URL/images/help/profiler/menu.gif]\r\n[small]Navigating the menu to your battlegroup and realm.[/small][/div]We maintain a database of [i]millions[/i] of [url=http://www.wowarmory.com/]Armory[/url] characters, guilds, and arena teams that have been imported by our users. You can browse through this extensive list by visiting the main [url=/?profiles]profiles[/url] page and selecting a region, battlegroup, or realm from the menus at the top.\r\n\r\nThis will give you an unfiltered look at the players and guilds in the area you selected, with the most recently updated characters displayed first. You can also enter your characters name in the box at the top to jump directly to that character.\r\n\r\n[h3]Finding My Characters[/h3]\r\n\r\n[ul]\r\n[li]Use the breadcrumb listings at the top to browse to your region, battlegroup, and realm. When you do this, a box will appear in the listing at the top of the page. Enter your character\'s name in this box to be taken directly to your character. You can use the \"Claim Character\", which is located under the Manage Character button, to save a character to your [url=/user=fewyn#characters]user page[/url] for later viewing.[/li]\r\n[/ul]\r\n\r\n[i]Tip: Claimed characters can be made public or private as you choose—so you only show off the characters people want you to see! Basic information for the profiles will remain public, just as it is in the Armory—but any connection to your account will be hidden.[/i]\r\n\r\n[h3]Filters[/h3]\r\nBut that\'s not the only way to find a character! You can also search Profiles using our robust filter system, just the same way that you can search items, NPCs, or spells in game. Characters and guilds can be filtered by name, region, and realm to limit the number of displayed results.\r\n\r\nAdditionally, characters can be filtered by faction, level, race, and class – as well as a number of other unique and useful criteria. For example:\r\n\r\n[ul]\r\n[li][div float=right align=right][img src=STATIC_URL/images/help/profiler/filters.gif]\r\n[small]Searching for characters that match your criteria.[/small][/div]Let\'s see [url=/?profiles=us.draenor&filter=cl=8;ra=11;cr=35;crs=0;crv=450]all the Draenei mages on my server that have their tailoring maxed out[/url].[/li]\r\n[li]Hmm... I wonder if anyone is [url=/?profiles=eu&filter=na=Malgayne]using my name on European servers[/url]?[/li]\r\n[li]How do I compare to [url=/?profiles=us.draenor&filter=cl=2;minle=80;maxle=80;cr=7;crs=1;crv=50]other Retribution-specced paladins on my server[/url]?[/li]\r\n[li]How many [url=/?profiles&filter=cr=23;crs=0;crv=871]Bloodsail Admirals[/url] are there out there?[/li]\r\n[li]Who got caught wearing a [url=/?profiles&filter=cr=21;crs=0;crv=22279]Lovely Black Dress[/url]?[/li]\r\n[li]How many people on my server and faction [url=/?profiles=us.sentinels&filter=si=2;cr=23;crs=0;crv=2904]completed Heroic Ulduar[/url]?[/li]\r\n[/ul]\r\n\r\nWe\'ll be adding more filters as time goes on, so feel free to experiment – and let us know if you think of other ideas!\r\n\r\n[pad][pad][pad]\r\n\r\n[h3]Guild and Arena Team Rosters[/h3]\r\nWhen you click on a character\'s guild or arena team, you will be directed to a roster view listing all the characters that belong to it. The roster view displays additional information, including guild ranks and personal arena team ratings. You can further filter this information using the [b]Create a filter[/b] link, should you want to find characters matching specific criteria. Now its easy to find all of the crafters in your guild!\r\n\r\n[h3][img src=STATIC_URL/images/help/profiler/queue.gif float=right]Resync Queue[/h3]\r\nWhen a character resync is requested, it is added to the queue. The queue is used to make sure everyone\'s characters are updated and processed in the order they were submitted, without overloading the [url=http://us.battle.net/wow/en/]Battle.net Armory\'s API[/url] with requests. Whenever you access a character that does not exist in our database or has not been updated in more than 1 hour, it will automatically be added to the queue.\r\n\r\n[/tab]\r\n\r\n[tab name=\"General usage\"]\r\n\r\nThe profiler has a wealth of information it can display about characters and custom profiles, so it can seem daunting at first! Each of the sections are broken down in detail below.\r\n[h3]Basic Profile Information[/h3]\r\nAt the top of a profile you will see an expanded header with vital information about the profile itself. All profiles have an icon and the character\'s race, class and level; Armory characters display a link to the character\'s guild under the name, while custom profiles display a description set by the user that created it. A link to [b]Edit[/b] this information appears on the bottom line, allowing you to update a profile you created or make a new custom profile from an existing one.\r\n\r\n[ul]\r\n[li][img src=STATIC_URL/images/help/profiler/edit.gif float=right][b]Name [/b]– Give your profile a name! Names must start with a letter, and can only contain letters, numbers, and spaces.[/li]\r\n[li][b]Level[/b] – Select a level for your profile. Profiles must be at least level 10 (55 for Death Knights) and no more than level 85.[/li]\r\n[li][b]Race[/b] – Ever wonder what you\'d look like as a tauren instead of an orc? Choose any race for your profile, and the character model with automatically be updated.[/li]\r\n[li][b]Class[/b] – You can select any class you like, regardless of racial restrictions. See what your stats would be if you were a draenei druid![/li]\r\n[li][b]Gender[/b] – Select male or female to set your character\'s gender.[/li]\r\n[li][b]Icon[/b] – Icons are automatically generated for Armory characters and in game class/race combinations, but you can change the icon to any you like.[/li]\r\n[li][b]Description[/b] – Enter a tag line or brief description for the profile so you and others know what it is about.[/li]\r\n[li][b]Visibility[/b] – Public profiles will be visible on your user page and anyone can view a public profile. Private ones will not be displayed or visible to others.[/li]\r\n[/ul]\r\n[i]Note: If you edit a character in any way, it will become a custom profile. The reputations, achievements, and raid progress information will be removed.[/i]\r\n\r\n[h3]Managing Profiles[/h3]\r\nIn the upper right are a number of useful buttons for managing profiles without having to go back to your user page. Each of the buttons have several options that can be used to manage the character\'s page you are currently on and include the following options.\r\n\r\n[ul]\r\n[li][b]Custom Profile[/b]\r\n[ul][li][b]New[/b] – This is a quick link to creating a new, blank profile from scratch. It will open in a new window so you do not lose your current profile. This option is always available.[/li]\r\n[li][b]Save[/b] – Save any changes you have made to this profile. This option is only available for logged in users on profiles they own.[/li]\r\n[li][b]Save as[/b] – This will let you save your current changes under a new name. It is extremely useful for making copies of profiles! This option is only available for logged in users.[/li][/ul][/li]\r\n[li][b]Manage Character[/b]\r\n[ul][li][b]Resync[/b] – Request that the character be updated from the armory; it will be added to the queue. This option is only available on Armory character pages.[/li]\r\n[li][b]Claim character[/b] – Adds an Armory character to your user page. This is a good thing to do with all your alts. This option is only available for logged in users on Armory character pages.[/li]\r\n[li][b]Remove[/b] - Removes the character from your user page. Use this if you no longer play the character or have long since deleted it.[/li]\r\n[li][b]Pin/Unpin[/b] - Pin one of your characters so you can perform personalized searches throughout the database for missing or completed quests, achievements, recipes and more![/li]\r\n[/ul][/li]\r\n[/ul]\r\n\r\n[h3]From the User Page[/h3]\r\n[img src=STATIC_URL/images/help/profiler/userpage.gif float=right]All of your claimed Armory characters and custom profiles are listed in one convenient place on your user page. From the [b]Characters[/b] tab you can remove one or more claimed characters. The [b]Profiles[/b] tab allows you to create a new profile, delete profiles, or change the visibility settings of profiles. Your private profiles will not be visible to anyone else.\r\n\r\n[i]Tip: When you are logged in, all of your characters and custom profiles can be accessed from the [b]My profiles[/b] menu at the top right of any page![/i][pad]\r\n[h3]Saving Your Work[/h3]\r\nAny profile can be edited, even if you don\'t own it, but you\'ll probably want to save your work when you\'re done! You must have an account with us in order to save a profile. Once you\'ve created an account, you can bookmark any number of Armory characters and save up to 10 custom profiles. Premium users will be able to create even more, so upgrade if 10 just isn\'t enough! You can use the red buttons to save a profile from its page, and manage your existing profiles and characters from your user page. \r\n\r\n[/tab]\r\n\r\n[tab name=\"Inventory and talents\"]\r\n[img src=STATIC_URL/images/help/profiler/character.jpg height=300 float=right]The main tab for a profile is the character inventory, which includes a lot of the same information you would see by looking at your character pane in game. This tab is broken up into four key sections - the character view, quick facts box, statistics, and gear summary.\r\n\r\n[h3]Character View[/h3]\r\nThe first thing you\'ll notice, of course, is your character – as rendered by our custom built modelviewer, in all it\'s three-dimensional glory. You can turn the character with your mouse, and zoom in and out using the A and Z keys, just like the modelviewer elsewhere in the site. [b]We even pull your face, hair, and skin color information from the Armory![/b]\r\n\r\nOn either side of the character are inventory icons which you can right click on for a menu of options:\r\n\r\n[i]Tip: You can remove a gem or enchant by clicking None in the picker window or by right clicking on it in the gear summary.[/i]\r\n\r\n[ul]\r\n[li][img src=STATIC_URL/images/help/profiler/itemmenu.gif float=right][b]Equip... / Replace...[/b] – Selecting this option will give you a quick search box in which you can type an item\'s name. Click on the item or hit return to equip it.\r\nUnequip – Unequips the item, of course. :)[/li]\r\n[li][b]Add / Replace enchant...[/b] – The spell icon on the left shows if the item is enchanted. This opens a customized picker window with all enchants available for the item slot.[/li]\r\n[li][b]Add / Replace gem...[/b] – The icon on the left shows the socket color or socketed gem. Like the enchants, this opens a picker window with valid gems for the socket.[/li]\r\n[li][b]Extra socket[/b] – The check mark on the left indicates if a blacksmithing socket has been added to this item. Click to toggle on or off.[/li]\r\n[li][b]Clear Enhancements[/b] - This will remove all reforges, enchantments, gems and extra sockets from an item. Useful if you want to start fresh with an item.[/li]\r\n[li][b]Display on character[/b] – The checkmark on the left indicates if the item is displayed on the model. Click to toggle on or off – it works for more than just cloaks and helms![/li]\r\n[li][b]Compare[/b] – Adds the item to the [url=/?compare]item comparison tool[/url] and opens it in a new window to compare with other items.[/li]\r\n[li][b]Find upgrades[/b] – Uses our [url=/?help=stat-weighting]weighted search[/url] to find upgrades based on your talent spec.[/li]\r\n[li][b]Who wears this?[/b] – Creates a filtered list of other Armory characters who are also wearing the item.[/li]\r\n[/ul]\r\n\r\n[i]Tip: Items that can take enchantments but have no enchantment, or which have empty sockets, will even have a little notification in the tooltip![/i]\r\n\r\n[img src=STATIC_URL/images/help/profiler/quickfacts.gif float=right][h3]Quick Facts Box[/h3]\r\nOn the right hand side is a handy Quick Facts box that displays basic, defining information about a profile. This box is chock full of useful information, including talent spec, achievement points, and professions.\r\n\r\n[i]Tip: Any raid icon that\'s ringed in [color=c4]gold[/color] is a raid that the character has cleared![/i]\r\n[h3]Statistics[/h3]\r\nYou\'ll also notice that all of a profile\'s statistics are laid out beneath the character view. This is also all information you can get from the Armory (and then some), but we lay it out in a nice, convenient page so you can view it all at once – no more messing with drop down menus. You can also click on a statistic and expand it so you can see its tooltip information right there on the page—or click on the header to expand all the related statistics. Your statistics are updated as you edit any part of a profile, including race, class, level, items, enhancements, or talents – all in real time! [b]Statistic modifications from glyphs and buffs are not presently supported, but will be in the future.[/b]\r\n\r\n[i]Note: These statistics are calculated manually – they are not pulled from the Armory. Statistics calculations are still in beta and will ironed out as we go.[/i]\r\n\r\n[img src=STATIC_URL/images/help/profiler/statistics.gif float=center]\r\n\r\n[h3]Gear Summary[/h3]\r\n[div float=right align=right][img src=STATIC_URL/images/help/profiler/gearsummary.gif]\r\n[small]A warning message is displayed for missing enhancements.[/small][/div]Last on the character inventory tab, but not least, is the gear summary. This is a personalized list of all items worn by the character, with convenient column headers and in line filtering options. Use it to see where most of a character\'s items come from, what is the best and worst piece, and whether or not there are missing gems and enchants. Just in case the empty icons aren\'t clear enough, a warning appears at the top of the list if a character is missing gems, enchants, or blacksmith sockets. This [color=q10]warning[/color] is based on the professions of the character if it is an Armory profile, and otherwise shows you everything missing on custom profiles.\r\n\r\nThe gems and enchants can also be edited from within the gear summary, and have a few additional options not available in the character view. You can remove or replace an enhancement from here, and you can find upgrades using our [url=/?help=stat-weighting]weighted search[/url] – just like items!\r\n\r\n[h3]Talents[/h3]\r\nThe talents tab includes an inline version of our [url=/?talent]talent calculator[/url] with a full display of a character\'s talents. It is locked by default, but you can unlock it to begin editing talents, just as you would normally. There are two extra features in the Profiler\'s talent calculator: you can store and swap between two specs for each character, and export the current talent build to the calculator to link to your friends. When you change your talents (or swap between specs) your gear score and statistics will be updates real time!\r\n\r\n[/tab]\r\n\r\n[tab name=\"Other tabs\"]\r\n\r\n[h3]Reputation[/h3]\r\nThe reputation tab displays the complete faction information of an Armory character, with collapsible headers for each section. Its much easier to read than the tiny faction pane in game! Of course, you can link directly to the faction\'s page to get more information about that faction. \r\n[h3][img src=STATIC_URL/images/help/profiler/achievements.gif float=right]Achievements[/h3]\r\nThe achievements tab lists an Armory character\'s progress in each of the main achievement categories, and has a filterable list of achievements including date completed. All of the normal column and list filters are available, along with some new ones! You can filter the list by earned, in progress or complete achievements – complete are displayed by default – or click on any of the category progress bars to only display achievements from that category.\r\n\r\n[/tab]\r\n\r\n[tab name=Completion_Tracker]\r\n\r\n[img src=STATIC_URL/images/help/profiler/quests.jpg float=right width=450]You can use the Profiler\'s [b]Completion Tracker[/b] feature to keep track of your quests, achievements, pets, mounts, recipes, and more!\r\n\r\n[h3]Getting Started[/h3]\r\n\r\nIn order to start tracking your completion data, all you need to do is visit your character\'s page on the profiler and resync it. This will automatically collect data about your character\'s completed achievements, companion pets, mounts, quests, recipes, reputations and titles.\r\n\r\n[h3][img src=STATIC_URL/images/help/profiler/completion.jpg float=right]Tracking Your Completion Data[/h3]\r\n\r\nOnce you\'ve got your data up on the site, it will be available in the form of five new tabs: [b]mounts[/b], [b]companions[/b], [b]recipes[/b], [b]quests[/b], and [b]titles[/b].\r\n\r\nIf you open the mounts, companions, or titles tabs, you\'ll immediately be greeted by a list of all the entries you\'ve already completed. You can cycle through the different tabs to see the ones you already have, the ones you still have yet to collect, a complete list, or a list of just the ones you\'ve \"excluded\" (more on that shortly). You can also use the \"Search within results\" box to search the list based on a keyword, just like you can with other search results in the database.\r\n\r\nThe recipe, and quest tabs, like the Achievements tab, contain more entries—so you\'ll be presented with a box like the one shown above. From there, all you have to do is click one of the progress bars to see the complete tabbed list in each category.\r\n\r\n[h3]Exclusions[/h3]\r\n\r\nWhen you\'re trying to make sure we check off every quest, achievement, or mount on our list, everyone knows that there are some that you just don\'t want to bother with. To that end, we\'ve created [b]exclusions[/b].\r\n\r\n[img src=STATIC_URL/images/help/profiler/exclusions.jpg float=right]Using exclusions, you can flag certain quests, mounts, achievements, recipes, pets, or titles that \"don\'t count\" toward your completion total. When you exclude (for example) a quest, that quest no longer appears in \"incomplete\" listings, and the total number of quests in that category is reduced by one.\r\n\r\n[b]For example:[/b] There are 632 quests in the \"Eastern Kingdoms\" category. If I were to decide that [quest=367] is for noobs and I don\'t want to count it, then all I have to do is put a check in the box next to the quest and click \"Exclude\". After I do so, the Eastern Kingdoms progress bar will only show [i]631[/i] quests total—the remaining quest will appear in the \"Excluded\" tab but won\'t be counted for anything else.\r\n\r\nIf you want to re-include a quest, just go to the \"Excluded\" tab and then use the checkboxes to restore as many as you like. You can do the same thing for achievements, titles, mounts, pets, or recipes.\r\n\r\nIf you [b]complete[/b] a quest that you have excluded, it will show in the progress bar as a [b]+1[/b]. Example: If there are 31 quests in the \"Miscellaneous\" category, and I\'ve completed 20 quests and excluded 1, the progress bar will show [b]20/30[/b]. If I have completed [i]the quest that I excluded[/i], then the progress bar will show [b]20(+1)/30[/b]. If I then go on to complete ALL the quests in that category (including the one I excluded), the progress bar will show [b]30(+1)/30[/b].\r\n\r\n[b]Exclusion Manager[/b]\r\nThe companions and mounts tabs let you manage your exclusions en masse with the Exclusion Manager. Just click the \"Manage Exclusions\" button on top of the tabs to see a list of convenient categories you might want to exclude. There\'s also a \"reset all\" button here to let you wipe all of your exclusions and start over.\r\n\r\n[b]Note:[/b] The Exclusion Manager is currently only available for companions and mounts.\r\n\r\n[i]Tip: Exclusions are tied to your account, not to a particular character. This is so even when you look at someone else\'s character, you\'re judging them by [/i]your[i] completion standards, not anyone else\'s![/i] \r\n\r\n[/tab]\r\n\r\n[tab name=Calculations]\r\n\r\nMost of the information we display is pretty straightforward. A lot of it, particularly the stats on items, is readily available in our database and on various tooltips. There are some new numbers on profile pages that you may ask, what does this number mean? How was it calculated?\r\n[h3]Base Statistics[/h3]\r\nA character\'s five base statistics are determined primarily by his or her class and level. This base amount has a modifier applied to it depending on the character\'s race. We gathered an extensive amount of data from the armory to come up with these base numbers, using untalented individuals of every race, class, and level combination. Because racial modifiers are consistent, we are able to create statistics for \"fake\" race and class combos using the data we already know. However, the Armory does not give data on characters below level 10 or Death Knights below level 55, so we have no statistic information for these profiles. To simplify things, we have set a minimum level for custom profiles based on the available statistics.\r\n[h3]Gear Score[/h3]\r\nOkay, so a lot of sites have gear scores. Most of them (ours included) are based around the [url=http://www.wowwiki.com/Item_level]item budget[/url] Blizzard uses to determine how much of each stat can be on an item. This budget is calculated using the item\'s level, quality, and slot, and we use the budget as the item\'s gear score. You can view a complete breakdown of an item\'s gear score by mousing over it in the [url=/?help=profiler#profiler-inventory-and-talents]gear summary[/url] at the bottom of the character tab. You can view a breakdown of a profile\'s total gear score by mousing over it in the Quick Facts box, also on the character tab.\r\n\r\nEach gear score is color coded based on the item levels of the gear in reference to the character level. [b][color=q0]Grey[/color][/b] for poor, [b][color=q1]White[/color][/b] for common, [b][color=q2]Green[/color][/b] for uncommon, [b][color=q3]Blue[/color][/b] for rare, [b][color=q4]Purple[/color][/b] for epic and [b][color=q5]Orange[/color][/b] for legendary. For example, a level 70 character wearing high item-level, raiding epics from [zone=3606] and [zone=3959] will have a purple-colored gearscore, as their items are considerably \"epic\" quality for their level. However, the same character at 80, if wearing this same gear, will have the gearscore colored blue as the items are of lower-than-optimal quality for their level.\r\n\r\nThe value of an empty socket was generated using the gear score of appropriate gems for the item in question, and subtracted from the item\'s score. This allows us to score unsocketed items lower than an item without sockets of the same level, quality, and slot. Items with better than expected gems will receive higher scores, and items with lower quality gems (or no gems at all) will receive lower scores.\r\n\r\nThe values of enchants are based off of the level of the enchantment. Endgame enchantments are 20 points, profession perks are 40 points, etc. The numbers go down from there.\r\n\r\nYou may notice that some profiles have different gear scores for the same item. There is an extreme difference in budget between a two-handed or one-handed weapon, which causes a discrepancy in scores between characters who should be fairly equal according to the level of their gear. To address this, the gear score of weapons has been normalized so that a character with appropriate weapon choices has the equivalent score of two two-handed weapons. Appropriate weapons are determined by your class and spec; for example, an enhancement shaman should dual wield one handed weapons, a protection warrior should have a one-hander and shield, etc. For classes which the melee weapons don\'t really matter – like hunters or spellcasters – anything they can use is considered appropriate.\r\n\r\n[i]Note: Gear score does not take into account the stats of the item. It is a measurement of quality of gear, not whether the stats on the gear are suited to the character\'s spec.[/i]\r\n\r\n[h3]Guild Scores[/h3]\r\nGuild gear scores and achievement points are derived using a weighted average of all of the known characters in that guild. Guilds with at least 25 level 80 players receive full benefit of the top 25 characters\' gear scores, while guilds with at least 10 level 80 characters receive a slight penalty, at least 1 level 80 a moderate penalty, and no level 80 characters a severe penalty. This is to prevent small guilds and bank alts from appearing to have higher scores than legitimate raiding guilds. Instead of being based on level, achievement point averages are based around 1,500 points, but the same penalties apply.\r\n\r\n[/tab]\r\n\r\n[/tabs]',NULL),(8,577,0,NULL,2,'[minibox]\n[h2]Steamwheedle Cartel[/h2]\n[faction=21]\n[faction=577]\n[faction=369]\n[b]Everlook[/b]\n[/minibox]\n\n[b]Everlook[/b], the faction of the town Everlook, is a trading post is run by the goblins of the Steamwheedle Cartel. It lies at the crossroads of [zone=618]\'s main trade routes.\n\n[h3]General Information[/h3]\nThis town is the last point of civilization before reaching Hyjal Summit. It is run by goblins as a trading post and is officially neutral to all races and factions. Even so, pilgrims allowed to venture up to the World Tree stop here, but otherwise this is the highest that merchants and explorers may venture without the night elves’ permission. Everlook would offer a commanding view of Kalimdor, if it were not at such a high altitude that clouds constantly shroud the mountain’s lower flanks.\n\nEverlook is the only major goblin outpost in northern Kalimdor, and it serves several purposes. First, it serves as the base of operations for goblin thorium and arcanite miners since Winterspring has some of the few untapped veins of those materials on the continent. Second, it serves as a center of trade between the Alliance and the Horde. While Everlook is hardly as safe as Moonglade, generally the Alliance and the Horde treat each other fairly well there. Additionally, Everlook is a frequent stop-off and resupply point for the faithful who make the pilgrimage through Winterspring to Hyjal Summit.\n\n[h3]Reputation[/h3]\nReputation for Everlook and the Steamwheedle Cartel is mostly gained from quests in Winterspring. Having a friendly or higher reputation will make the guards help you in case of initiated violence against you.',NULL),(NULL,NULL,0,'talent-calculator',2,'[menu tab=2 path=2,13,4]Can\'t find the answer you were looking for? Just [url=/?aboutus#contact]contact us[/url], or post on our [url=/?forums&board=1]forums[/url]! \r\n\r\n[toc]\r\n\r\n[h2]General Usage[/h2]\r\n[ul]\r\n[li][screenshot url=STATIC_URL/images/help/talent-calculator/glyphs.jpg thumb=STATIC_URL/images/help/talent-calculator/glyphs2.jpg width=268 height=218 float=right][/screenshot][b]Selecting a class[/b] - Easily select a class\' talent tree by chosing from the class icon at the top, or from the dropdown menu. Clicking on a class\' name at the top left of the calculator will open that class\' page here on on this site, providing even more detailed information![/li] \r\n[li][b]Adding or removing talent points[/b] - To add points in a talent simply click the appropriate talent. To remove points, you can either right-click (or Shift+click) the talent.[/li]\r\n[li][b]Adding glyphs[/b] - Click on an empty glyph slot to open a picker window from which you can make your selection. To remove a glyph, simply right-click (or Shift+click) that glyph.[/li]\r\n[li][b]Linking to a build[/b] – Simply copy the auto-updating URL from your browser\'s address bar.[/li]\r\n[/ul]\r\n\r\n[h2]Tools + Options[/h2]\r\n[ul]\r\n[li][b]Reset all[/b] - Resets all talents across all trees.[/li]\r\n[li][img src=STATIC_URL/images/help/talent-calculator/options.jpg float=right][b]Reset tree[/b] - Clicking the red X at the top right corner of a talent tree will reset all talents in that particular tree. Other trees will not be reset.[/li]\r\n[li][b]Lock / Unlock[/b] - Locks or unlocks the talent build, preventing (or allowing) changes to be made. Linking to a build will automatically lock talents.[/li]\r\n[li][b]Import[/b] – Displays a pop-up text window where you can enter the URL of a talent build made with [url=http://www.wowarmory.com/talent-calc.xml]Blizzard\'s talent calculator[/url]. Be sure that you first select the \"Link to this build\" option in the Blizzard talent calculator so that the URL will be properly formatted for importing.[/li]\r\n[li][b]Print[/b] - Opens up a new, printer-friendly page with a textual representation of your chosen talents. Nice if you want to paste the talents you\'ve chosen somewhere, and would prefer it written out.[/li]\r\n[li][b]Link[/b] - Locks your chosen talents and creates a link to your build. Use this option to easily create a URL to share your build with others![/li]\r\n[/ul]\r\n\r\n[h2]Useful Tips[/h2]\r\n\r\n[ul]\r\n[li]When the calculator is locked, you can click talents and glyphs to view their corresponding spell or item page.[/li]\r\n[li]If you\'re building a third-party application, you can link to our talent calculator by using Blizzard-style URLs such as:\r\n[code]HOST_URL?talent#hunter-512002015051122431005311500053052002300100000000000000000000000000000000000000000[/code][/li]\r\n[/ul]',NULL),(NULL,NULL,0,'modelviewer',2,'[menu tab=2 path=2,13,1]\r\n\r\n[url=item=35350][img src=STATIC_URL/images/help/modelviewer/ss-viewin3d.gif float=right][/url]Aowow has a model viewer that will let you see the items and NPCs in the game in full 3D!\r\n\r\nYou can use the dropdown menus to select which character model you want to display armor pieces on, and the model viewer will remember your choice.\r\n\r\nThere are two different versions of the model viewer available, one written in Flash, and the other one written in Java. Aowow should remember which version you used last time, and will automatically open that model viewer the next time you click on the \"View in 3D\" button.\r\n\r\nIf you have any issues, please report them [url=/?forums&topic=202524]here[/url]!\r\n\r\n[i]Tip: You can close the box by clicking anywhere outside of the box.[/i]\r\n\r\n[h2]Modes[/h2]\r\n\r\n[tabs name=mode]\r\n\r\n[tab name=Flash]\r\n\r\n[url=item=34092][img src=STATIC_URL/images/help/modelviewer/ss-flash.png float=right][/url]The [b]Flash[/b] viewer is simple, quick to load, and should work on nearly all browsers. The Flash viewer is the default viewer, and all models will automatically load in the Flash Viewer unless you specify otherwise.\r\n\r\nIt requires the latest version of [url=http://www.adobe.com/go/BONRN]Flash[/url] to be installed on your computer.\r\n\r\n[h3]Controls[/h3]\r\n[ul]\r\n[li][b]Rotate[/b] – Click and drag / arrow keys[/li]\r\n[li][b]Zoom[/b] – Mousewheel / A & Z keys[/li]\r\n[/ul]\r\n\r\n[h3]Features[/h3]\r\n[ul]\r\n[li]Motion blur[/li]\r\n[li]Full screen mode[/li]\r\n[/ul]\r\n\r\n[/tab]\r\n\r\n[tab name=Java]\r\n\r\n[url=/?item=35350][img src=STATIC_URL/images/help/modelviewer/ss-java.png float=right][/url]The Java viewer is slower to initialize than the Flash Viewer, but once it\'s initialized it renders in [b]much greater[/b] detail. Most browsers will only need to initialize it once, and subsequent loads will be much faster. Some browsers may ask you to accept a security certificate when you initialize the viewer.\r\n\r\nIt requires the latest version of [url=http://jdl.sun.com/webapps/getjava/BrowserRedirect?locale=en&host=www.java.com]Java[/url] to be installed on your computer.\r\n\r\n[h3]Controls[/h3]\r\n[ul]\r\n[li][b]Rotate[/b] – Click and drag[/li]\r\n[li][b]Zoom[/b] – Mousewheel[/li]\r\n[li][b]Move[/b] – Right-click and drag[/li]\r\n[/ul]\r\n\r\n[h3]Features[/h3]\r\n[ul]\r\n[li]3D acceleration[/li]\r\n[li]Animations on NPCs, character models, small pets, and mounts[/li]\r\n[/ul]\r\n\r\n[/tab]\r\n\r\n[/tabs]\r\n',NULL),(NULL,NULL,0,'tooltips',2,'[menu tab=2 path=2,10]\r\n\r\n[div float=right align=right][url=http://wow.joystiq.com/2010/04/14/breakfast-topic-using-irl-irl/][img src=STATIC_URL/images/help/tooltips/ss-wowcom.png][/url]\r\n[small]Tooltips in action on [url=http://wow.joystiq.com/2010/04/14/breakfast-topic-using-irl-irl/]WoW Insider[/url][/small][/div]\r\n\r\nIt\'s never been easier to add tooltips to your site.\r\n\r\n[ol]\r\n[li]Add this piece of HTML code in the section of your page:\r\n[code][/code][/li]\r\n[li]You are done![/li]\r\n[/ol]\r\n\r\nLinks found on your site will now sport a [b]tooltip[/b] and an [b]icon[/b]. The following pages are supported: achievement, profile, item, npc, object, spell, quest. Icons show up by default, you can customize the colors of your links, and easily rename them!\r\n\r\nYou can check out this [url=STATIC_URL/widgets/power/demo.html]working demo[/url], and see how easy it is!\r\n\r\n[h2]Related[/h2]\r\n\r\n[tabs name=Related]\r\n\r\n[tab name=\"Advanced usage\"]\r\n\r\nOnce you have the [/code]\r\n[/tab]\r\n\r\n[tab name=\"XML feeds\"]\r\n\r\n[h3]Items[/h3]\r\nAlso available are our item XML feeds. Every item in the database has a corresponding XML feed. You can reach those feeds either by ID or by name. For example:\r\n\r\n[ul]\r\n[li]By ID: HOST_URL?item=52021&xml[/li]\r\n[li]By name: HOST_URL?item=iceblade%20arrow&xml[/li]\r\n[/ul]\r\n\r\n[/tab]\r\n\r\n[tab name=\"Other resources\"]\r\n\r\nInterested in using our script in your forum? Check out [url=http://wowhead.com/forums&topic=3464]this thread[/url] for information on implementing it on many popular forum systems (phpBB, vBulletin, etc.) or check out the handy guides written by Wowheads users:\r\n\r\n[ul]\r\n[li][url=http://wowhead.com/forums&topic=3464#p37094]vBulletin[/url][/li]\r\n[li]phpBB: [url=http://wowhead.com/forums&topic=3464#p37492]2.x.x[/url] - [url=http://wowhead.com/forums&topic=3464.6#p58403]2.x.x Mod Version[/url] | [url=http://wowhead.com/forums&topic=14347&p=126922]3.0[/url] [small]by craCkpot[/small] - [url=http://wowhead.com/forums&topic=3464#p37204]3.0[/url] [small]by marcimi[/small] - [url=http://wowhead.com/forums&topic=3464.3#p42858]3.0 Mod Version[/url][/li]\r\n[li][url=http://wowhead.com/forums&topic=3464#p37618]Simple Machines Forum (SMF)[/url][/li]\r\n[li][url=http://wowhead.com/forums&topic=3464.3&p=4080#p40631]Invision Power Board (IPB)[/url][/li]\r\n[li][url=http://wowhead.com/forums&topic=3464.3&p=42952#p42952]WordPress Blog[/url] ([url=http://wowhead.com/forums&topic=3464.4#p43652]Plugin Version[/url])[/li]\r\n[li][url=http://wowhead.com/forums&topic=3464.7&p=63338#p61443]PHP Nuke-Evolution[/url][/li]\r\n[li][url=http://wowhead.com/forums&topic=3464.3#p43232]MyBB[/url][/li]\r\n[li][url=http://wowhead.com/forums&topic=3464.6#p48648]TikiWiki[/url][/li]\r\n[li][url=http://wowhead.com/forums&topic=3464.6#p49640]YaBB[/url][/li]\r\n[li][url=http://wowhead.com/forums&topic=3464.5#p46801]Drupal[/url][/li]\r\n[li][url=http://wowhead.com/forums&topic=3464.3#p42456]PunBB[/url][/li]\r\n[li][url=http://wowhead.com/forums&topic=10938]Dojo[/url][/li]\r\n[/ul]\r\n\r\n[/tab]\r\n\r\n[/tabs]',NULL),(NULL,NULL,0,'searchbox',2,'[menu tab=2 path=2,16]\r\n\r\nThe code below will produce an iframe that contains the Aowow logo and a search box.\r\n\r\n[code]\r\n[/code]\r\n\r\n[h3]Parameters[/h3]\r\n\r\n[ul]\r\n[li][b]aowow_searchbox_format[/b] – String that specifies how big the iframe should be. The following values can be used:\r\n[pad]\r\n[table width=100%]\r\n[tr]\r\n[td width=20% align=center valign=top]\r\n\"160x200\"\r\n[img src=STATIC_URL/images/help/searchbox/preview-160x200.png]\r\n[/td]\r\n[td width=20% align=center valign=top]\r\n\"120x200\"\r\n[img src=STATIC_URL/images/help/searchbox/preview-120x200.png]\r\n[/td]\r\n[td width=20% align=center valign=top]\r\n\"160x120\"\r\n[img src=STATIC_URL/images/help/searchbox/preview-160x120.png]\r\n[/td]\r\n[td width=20% align=center valign=top]\r\n\"150x120\"\r\n[img src=STATIC_URL/images/help/searchbox/preview-150x120.png]\r\n[/td]\r\n[td width=20% align=center valign=top]\r\n\"120x120\"\r\n[img src=STATIC_URL/images/help/searchbox/preview-120x120.png]\r\n[/td]\r\n[/tr]\r\n[/table]\r\n[/li]\r\n[/ul]\r\n\r\n[h3]Tips[/h3]\r\n\r\n[ul]\r\n[li]You can style the iframe (e.g. adding a border) by using the following class name in your CSS code:\r\n[code].aowow-searchbox { ... }[/code][/li]\r\n[/ul]',NULL),(NULL,NULL,0,'searchplugins',2,'[menu tab=2 path=2,8]\r\n\r\n[div float=right align=right][img src=STATIC_URL/images/help/searchplugins/ss-searchsuggestions.png]\r\n[small]Also features search suggestions![/small]\r\n[/div]\r\n\r\nSearch plugins make it easy to search the database right from your browser!\r\n\r\n[toc h3=false]\r\n\r\n[h2][img src=STATIC_URL/images/help/searchplugins/firefox.gif border=0 margin=5 float=left][img src=STATIC_URL/images/help/searchplugins/ie.gif border=0 float=left]Firefox / Internet Explorer[/h2]\r\n\r\n[div clear=left][/div]Click on the button below to install the search plugin in your browser.\r\n\r\n[pad]\r\n\r\n[script]\r\nfunction addPlugin()\r\n{\r\n try {\r\n if(!$.browser.msie && !$.browser.mozilla) {\r\n throw(\'FAIL\');\r\n }\r\n\r\n window.external.AddSearchProvider(\'STATIC_URL/download/searchplugins/aowow.xml\');\r\n }\r\n catch(e)\r\n {\r\n alert(\'This feature is only for Firefox 2+ and Internet Explorer 7+.\');\r\n }\r\n}\r\n[/script]\r\n\r\n[html]Install pluginInstall plugin[/html]\r\n\r\n[div clear=left][/div][pad]\r\n\r\n[h2][img src=STATIC_URL/images/help/searchplugins/opera.gif border=0 float=left]Opera[/h2]\r\n\r\n[div clear=left][/div]\r\n\r\n[ul]\r\n[li]Right-click on the search box on the [url=/]homepage[/url].[/li]\r\n[li]Select \"Create Search\" in the menu.[/li]\r\n[li]Fill the form as follows:\r\n[pad]\r\n[img src=STATIC_URL/images/help/searchplugins/ss-opera.png border=0]\r\n[pad][/li]\r\n[li]Save your changes, and you\'ll be able to perform Aowow searches by typing \"wh\" followed by the search terms in the address bar (e.g. wh sword).[/li]\r\n[/ul]\r\n',NULL),(NULL,NULL,2,'page-not-found',2,'[tooltip name=AO815][b][color=q4]AO-815 Moteur Principal de Stabulation[/color][/b]\n[color=white]Lié lorsque utilisé\nUnique[/color]\n[color=q2]Utilise: Appelle le pouvoir de l\'Interwebs pour\ninvoquer l\'information demandé à Aowow.[/color]\n[color=q]\"En tout cas, c\'est ce que c\'est supposé faire...\"[/color][/tooltip]Quoi? Comment avez-vous... oubliez ça!\n\nIl semblerait que la page demandée n\'ait pas été trouvée. En tout cas, pas dans cette dimension.\n\nPeut-être que quelques réglages au [span class=tip tooltip=AO815][color=q4][u][AO-815 Moteur Principal de Stabulation][/u][/color][/span] pourraient résulter en l\'apparition soudaine de la page![pad][pad]\n\nOu vous pouvez essayer de [url=?aboutus#contact]nous contacter[/url] - la stabilité du AO-815 est discutable et vous ne voudriez pas un autre accident...\n\n[h2]Liens[/h2]\n[ul]\n[li]Retour à la [url=?]page d\'accueil[/url][/li]\n[li][url=?forums&board=1]Forum[/url] de feedback[/li]\n[/ul]',NULL),(NULL,NULL,0,'faq',2,'[small]no questions have been asked yet[/small]\r\n\r\nbesides .. yes, i\'m insane.',NULL),(NULL,NULL,0,'whats-new',2,'[small]this page for example[/small]',NULL),(NULL,NULL,0,'aboutus',2,'[h3]This is [s]Sparta![/s] [u]Aowow[/u][/h3]\r\n\r\nA project for private servers to sensibly display the vast amount of data a private server contains.\r\n\r\nBuilt with TrinityCore in my neck, but i\'m trying to get away from that .. some time.\r\nWith it\'s own data structure it shouldn\'t be too hard to write a converter for MaNGOS, Ascent or whatever software you prefere.\r\n\r\nThe expected version is 3.3.5 (12340), everything else will get messy.',NULL),(NULL,NULL,3,'page-not-found',2,'[tooltip name=AO815][b][color=q4]AO-815 Großkonfabulierungsmaschine[/color][/b]\n[color=white]Bei Benutzung gebunden\nEinzigartig[/color]\n[color=q2]Benutzen: Ersucht die Mächte der Internetze darum,\nAowow die benötigten Informationen zukommen zu lassen.[/color]\n[color=q]\"Das sollte es im Prinzip eigentlich tun...\"[/color][/tooltip]Was? Wie hast du... vergesst es!\n\nAnscheinend konnte die von Euch angeforderte Seite nicht gefunden werden. Wenigstens nicht in dieser Dimension.\n\nVielleicht lassen einige Justierungen an der [span class=tip tooltip=AO815][color=q4][u][AO-815 Großkonfabulierungsmaschine][/u][/color][/span] die Seite plötzlich wieder auftauchen![pad][pad]\n\nOder, Ihr könnt es auch [url=?aboutus#contact]uns melden[/url] - die Stabilität des AO-815 ist umstritten, und wir möchten gern noch so ein Problem vermeiden...\n\n[h2]Links[/h2]\n[ul]\n[li]Zur [url=?]Titelseite[/url] zurückkehren[/li]\n[li][url=?forums&board=1]Forum[/url] für Rückmeldungen[/li]\n[/ul]',NULL),(NULL,NULL,6,'page-not-found',2,'[tooltip name=AO815][b][color=q4]Dispositivo de confabulación suprema AO-815[/color][/b]\n[color=white]Se liga al usar\nÚnico[/color]\n[color=q2]Uso: Clama a los poderes de Internet para\ninvocar información requerida a Aowow.[/color]\n[color=q]\"Al menos, eso es lo que se supone que hace...\"[/color][/tooltip]¿Pero qué? ¿Cómo? .... ¡olvídalo!\n\nParece que la página que buscas no pudo ser encontrada. Al menos, no en esta dimensión.\n\n¡Quizá un par de ajustes al [span class=tip tooltip=AO815][color=q4][u][Dispositivo de confabulación suprema AO-815][/u][/color][/span] puede que hagan que la página aparezca de repente![pad][pad]\n\nO, puedes intentar [url=?aboutus#contact]contactar con nosotros[/url] - la estabilidad del AO-815 es debatible y no queremos otro accidente...\n\n[h2]Enlaces[/h2]\n[ul]\n[li]Volver a la [url=?]página principal[/url].[/li]\n[li]Foro del [url=?forums&board=1]feedback[/url].[/li]\n[/ul]',NULL),(NULL,NULL,0,'page-not-found',2,'[tooltip name=AO815][b][color=q4]AO-815 Major Confabulation Engine[/color][/b]\n[color=white]Binds when used\nUnique[/color]\n[color=q2]Use: Calls on the powers of the Interwebs to\nsummon requested information to Aowow.[/color]\n[color=q]\"At least, that\'s what it\'s supposed to do...\"[/color][/tooltip]What? How did you... nevermind that!\n\nIt appears that the page you have requested cannot be found. At least, not in this dimension.\n\nPerhaps a few tweaks to the [span class=tip tooltip=AO815][color=q4][u][AO-815 Major Confabulation Engine][/u][/color][/span] may result in the page suddenly making an appearance![pad][pad]\n\nOr, you can try [url=?aboutus#contact]contacting us[/url] - the stability of the AO-815 is debatable, and we wouldn\'t want another accident...\n\n[h2]Links[/h2]\n[ul]\n[li]Return to the [url=?]homepage[/url][/li]\n[li]Feedback [url=?forums&board=1]forum[/url][/li]\n[/ul]',NULL),(NULL,NULL,0,'markup-guide',2,'Here we have quite a few nifty markup tags that users can insert into their comments and forum posts to improve the style and easily link to database entries! Many of these tags can easily inserted using the corresponding icon or dropdown menu found above the text box. We\'ve put together this quick reference for all of these handy tags for you guys so you can get on your way to making high quality posts and comments!\n\n[h2]Formatting Tags[/h2]\n[h3]Bold[/h3]\n\\[b]text[/b]\n\n[h3]Line break[/h3]\n\\[br] -> inserts a line break.\n\n[h3]Code[/h3]\n\\[code]text[/code] -> creates a block of text that ignores markup and uses a monospace font.\n\n[h3]Horizontal Rule[/h3]\n\\[hr] -> creates a horizontal rule\n\n[h3]Italics[/h3]\n\\[i]text[/i] -> [i]text[/i]\n\n[h3]Preformatted text[/h3]\n\\[pre]text[/pre] -> shows text with all whitespace preserved in a monospace font, but allows markup\n\n[h3]Strikethrough[/h3]\n\\[s]text[/s] -> [s]text[/s]\n\n[h3]Small text[/h3]\n\\[small]text[/small] -> [small]text[/small]\n\n[h3]Subscript[/h3]\n\\[sub]text[/sub] -> [sub]text[/sub]\n\n[h3]Superscript[/h3]\n\\[sup]text[/sup] -> [sup]text[/sup]\n\n[h3]Underline[/h3]\n\\[u]text[/u] -> [u]text[/u]\n\n[h2]Database Tags[/h2]\n\n\n[b]For all database tags:[/b]\nOptional attributes: site/domain (both work identically, only use one)\nValid options are: www (default), en, de, es, fr, ru.\nThe purpose of these is to link to localized versions of items with the pretty db tags.\n[b]Example:[/b] \\[achievement=3579 domain=ru] -> [achievement=3579 domain=ru] \n\n[h3]Achievements[/h3]\n\\[achievement=3579] -> [achievement=3579]\n\n[h3]Classes[/h3]\n\\[class=11] -> [class=11]\n\n[h3]Events[/h3]\n\\[event=1] -> [event=1]\n\n[h3]Factions[/h3]\n\\[faction=749] -> [faction=749]\n\n[h3]Items[/h3]\n\\[item=12345] -> [item=12345]\n\nTo hide the icon: \\[item=12345 icon=false] -> [item=12345 icon=false]\n\n[h3]Itemsets[/h3]\n\\[itemset=699] -> [itemset=699]\n\n[h3]NPCs[/h3]\n\\[npc=32906] -> [npc=32906]\n\n[h3]Objects[/h3]\n\\[object=1733] -> [object=1733]\n\n[h3]Pets[/h3]\n\\[pet=45] -> [pet=45]\n\n[h3]Quests[/h3]\n\\[quest=7981] -> [quest=7981]\n\n[h3]Races[/h3]\n\\[race=11] -> [race=11]\n\n[b]To specify the gender of the icon:[/b] \\[race=11 gender=1] -> [race=11 gender=1] - 0 is male, 1 is female\n\n[h3]Skills[/h3]\n\\[skill=171] -> [skill=171]\n\n[h3]Spells[/h3]\n\\[spell=52398] -> [spell=52398]\n\\[spell=31565 buff=true] -> [spell=31565 buff=true]\n\n[h3]Statistics[/h3]\n\\[statistic=1076] -> [statistic=1076]\n\n[h3]Zones[/h3]\n\\[zone=3959] -> [zone=3959]\n\n[h2]HTML Tags[/h2]\n\n[h3]Anchor[/h3]\n\\[anchor=text] -> creates an anchor with the name \\\"text\\\" at this point.\n\n[h3]Ordered List[/h3]\n\\[ol]\\[li]list item[/li][/ol] -> [ol][li]list item[/li][/ol]\n\n[h3]Tables[/h3]\n[b]\\[table][/b]\nBorder: \\[table border=2]\nSpacing: \\[table cellspacing=2]\nPadding: \\[table cellpadding=2]\nWidth: \\[table width=500px] - Valid units are px, em, %\n\n[b]\\[tr][/b] - No attributes\n\n[b]\\[td][/b]\nAlign: \\[td align=right] - Valid options are left, right, center, justify\nVertical align: \\[td valign=baseline] - Valid options are top, middle, bottom, baseline\nColumn span: \\[td colspan=2]\nRow span: \\[td rowspan=2]\nWidth: \\[td width=500px] - Valid units are px, em, %\n\n[h3]Unordered List[/h3]\n\\[ul]\\[li]list item[/li][/ul] -> [ul][li]list item[/li][/ul]\n\n[h3]URLs[/h3]\n\\[url=http://www.wowhead.com]Wowhead[/url] -> [url=http://www.wowhead.com]Wowhead[/url]\n\\[url]http://www.wowhead.com[/url] -> [url]http://www.wowhead.com[/url]\n\\[url=http://www.google.com rel=item=12345]Rel link[/url] -> [url=http://www.google.com rel=item=12345]Rel link[/url]',NULL),(8,589,0,NULL,2,'The [b]Wintersaber Trainers[/b] is an Alliance-only faction consisting of only two night elven NPCs that can both be found in [zone=618]. Currently, the only questgiver is [npc=10618], who is located at the top of Frostsaber Rock in Winterspring. Upon reaching exalted with this faction, Rivern will sell a special mount, the [item=13086].\n\nThis faction\'s mount is the only epic mount (100% riding speed) attainable in the game which only requires 75 riding skill (and thus only costs 90 Gold). The faction is noted for having no Horde counterpart and having the longest and most repetitive reputation grind of the entire game. The first quest can be attained at level 58, while the other two are attainable at level 60.\n\n[h3]Reputation[/h3]\nReputation with the Wintersaber Trainers can only be obtained through three repeatable quests. There are no faction items or mobs that reward repuation directly.\n\n[b]Neutral 0 to 1500[/b]\nOnly one repeatable quest will available at first, so until neutral 1500/3000 is reached the [quest=4970] quest should be repeated. Any Shardtooth and Chillvind mob in Winterspring will drop these. This quest should be done solo as the drop rates are low and not shared if others have the quest.\n\n[b]Neutral 1500 to Exalted[/b]\nHalfway through neutral the [quest=5201] quest will be available. This quest requires to kill 10 Winterfall mobs in the Winterfall Village, just east of Everlook. If the quest [quest=8464] has been done with the [faction=576], [item=21383] can drop from the Winterfall mobs. If a player wants both reputations, saving these until revered with Timbermaw Hold will result in a lot of \"free\" reputation.\n\nThis quest can be done in groups for increased speed. Players grinding either Wintersaber Trainers or Timbermaw Hold reputation can often be found in the Winterfall Village. Even with an epic mount, the travel to and from Winterfall Village takes up much time. There are tigers among the route who will daze you, which will result in a demount, this should be avoided (but can be hard as they\'ll catch up with you on a 60% mount). Usually this quest is repeated all the way to exalted, ignoring the third quest. \n\n[b]Honored to Exalted[/b]\nAt honored the third quest [quest=5981] is available. The quest requires the player to kill 8 Frostmaul giants. They are a lot harder than the Winterfall mobs and the travel lengths are quite longer. This quest is usually skipped, and instead Winterfall Intrusion is repeated.\n\nDue to some players grinding Timbermaw Hold reputation, in Winterfall Village among other places, this quest can indeed turn out to be a faster reputation reward than the Winterfall Intrusion one.',NULL),(8,609,0,NULL,2,'The [b]Cenarion Circle[/b] is an organization of druids, both tauren and night elf, named after Cenarius. Its members are dedicated to protecting nature and restoring the damage done to it by malevolent forces.\n\nThe Circle has many posts, but their main home is the town of Nighthaven in the [zone=493]. Druids learn the spell [spell=18960] at level 10, but anyone else will have to make it to [zone=361] and find a way through the Timbermaw Furbolg tunnels.\n\nThe Circle\'s other major presence is in [zone=1377], where they combat the Silithid, the Qiraji, and Twilight\'s Hammer. Valor\'s Rest and Cenarion Hold serve as their bases in the hostile land, and offer many opportunities to adventurers seeking to aid the druids.\n\n[h3]Notable Members[/h3]\n[ul][li][npc=11832], son of Cenarius[/li][li][npc=3516], leader of the night elven druids[/li][li][npc=5769], leader of the tauren druids[/li][/ul]\n\n[h3]Reputation[/h3]\nThere are several ways to gain reputation with the Cenarion Circle. Aside from the available [url=?quests&filter=cr=1;crs=609;crv=0]quests[/url], you may do the following to gain reputation:[ul][li]Raid the [zone=3429]. This is by far the fastest way to gain reputation, as a full clear can net over 2000 reputation.[/li][li]Kill twilight cultists. These stop yielding reputation when you reach the end of friendly for [npc=11880] and [npc=11881], and at the end of honored for [npc=15201].[/li][li]Turn in [item=20404]. These drop off the cultists, and yield 250 reputation for 10 texts.[/li][li]Turn in [item=20513], [item=20514], and [item=20515]. These drop off the minibosses that are summoned at the windstones using the [itemset=492].[/li][li]Perform the [quest=8507]. These are either [url=?search=logistics+task+briefing]Logistics quests[/url], [url=?search=combat+task+briefing]Combat quests[/url], or [url=?search=tactical+task+briefing]Tactical quests[/url]. The badges you earn from these quests may then be turned in for additional reputation, if you chose to forsake the rewards.[/li][li]Collect [object=181598] from the zone and turn it in to your faction NPC.[/li][/ul]',NULL),(8,729,0,NULL,2,'[b]Frostwolf Clan[/b], along with [npc=11946], lived along the [zone=36] practicing shamanism, and having Frost Wolves as their companions. The dwarven expedition known as the [faction=730] have started an expedition in the Frostwolf territory to excavate the valley and mine its veins, a transgression to the orcs who inhabited Alterac. This provoked a slaughter of the first expedition, and started the battle for [zone=2597].\n\n[h3]Reputation[/h3]\nPlayers can earn reputation in this faction by participating in the Alterac Valley battleground by doing various tasks as well as killing members of the opposite faction, the Stormpike Guard.\n\nYou are granted the player title [title=47] once exalted with the Frostwolf Clan and the other two battleground factions, [faction=889] and [faction=510].',NULL),(8,730,0,NULL,2,'[b]Stormpike Guard[/b] is the Alliance faction in the [zone=2597] battleground. They are an expedition of dwarves of the Stormpike Clan, native to the \"valleys of Alterac\" in [zone=36]. The Stormpikes\' search for relics of their past and harvesting of resources in Alterac Valley have led to open war with the the orcs of the [faction=729] dwelling in the southern part of the valley. They were also issued with a \"sovereign imperialistic imperative\" by [npc=2784] to take the valleys of Alterac for [zone=1537]. \n\nThe main Stormpike base is Dun Baldar, where their leader, [npc=11948], resides with his marshals. His second in command, [npc=11949], is found south of Dun Baldar, at Stonehearth Outpost.\n\n[h3]Reputation[/h3]\nPlayers can earn reputation in this faction by participating in the Alterac Valley battleground by doing various tasks as well as killing members of the opposite faction, the Frostwolf Clan.\n\nYou are granted the player title [title=48] once exalted with Stormpike Guard and the other two battleground factions, [faction=890] and [faction=509].',NULL),(8,749,0,NULL,2,'The [b]Hydraxian Waterlords[/b] are elementals that have made their home on the islands east of [zone=16]. Sworn enemies of the armies of [npc=11502]. Historically servants of the Old Gods, the four Elemental Lords served the gods with undying loyalty. The minions of Neptulon the Tidehunter were numerous and mindless. It is not yet known how [npc=13278] broke free of his lord\'s control (if indeed he has), or what is his ultimate goals are, but the Water elementals are the only elementals that do not attack the mortal races with abandonment.\n\nLocated on a remote island in the far east of Azshara, Duke Hydraxis offers some quests. The first two require killing various elementals in [zone=139] and [zone=1377]. Increased faction with the Waterlords opens up additional quests leading into the [zone=2717]. Any items obtained from the Hydraxian Waterlords, are obtained from its various quests.\n\nCompleting the questline allows players to obtain [item=17333] used to douse the runes found near most bosses in Molten Core. This is required to summon [npc=12018], the penultimate boss, and, after his defeat, to summon Ragnaros himself. Since there are seven runes, any raid needs at least seven players that bring a quintessence if they wish to finish the instance. Since most of the questline takes place within Molten Core, any raider can complete this task with little more than some traveling and an [zone=1583] run.\n\n[h3]Reputation[/h3]\nRepuation is gained through slaying the following elemental enemies of the waterlords.[ul][li][npc=11746] - 5 reputation, lasts until honored.[/li][li][npc=11744] - 5 reputation, lasts until honored.[/li][li][npc=7032] - 5 reputation, lasts until honored.[/li][li][npc=9017] - 15 reputation, lasts until revered.[/li][li][npc=14478] - 25 reputation, lasts until revered.[/li][li][npc=9816] - 50 reputation, lasts until revered.[/li][li][npc=11658], [npc=11673], [npc=12101] and [npc=11668] - 20 reputation, lasts until revered.[/li][li][npc=11659] and Lava Pack ([npc=12100], [npc=12076], [npc=11667], [npc=11666]) - 40 reputation, lasts until revered.[/li][li][npc=12118], [npc=11982], [npc=12259], [npc=12057], [npc=12056], [npc=12264], [npc=12098] - 100 reputation, lasts until exalted.[/li][li][npc=11988] - 150 reputation, lasts until the end of exalted.[/li][li][npc=11502] - 200 reputation, lasts until the end of exalted.[/li][/ul]Reaching revered status with the Hydraxian Waterlords allows players to obtain the [item=22754], which replenishes itself and thus eliminates the need to return to Hydraxis to obtain a new quintessence every week.',NULL),(8,809,0,NULL,2,'The [b]Shen\'dralar[/b] are the faction of the Night Elves remaining in [zone=2557]. They are a group of high practitioners of arcane magic in order of their former Queen Azshara, and her followers, the Highborne. They have been living in Eldre\'Thalas (previous name of Dire Maul) since the Great Sundering. They are few, but their knowledge and mystic power are great, referring to things players think are powerful such as [b]Arcanums[/b] and [b]Librams[/b] as mere cantrips.\n\nTheir leader, [npc=11486], was in charge and oversaw the construction of the pylons to contain the great demon [npc=11496] and syphon his demonic power. After many long years though, it began to dwindle so he started killing the remaining night elves to maintain energy. So their spirits come to adventurers and ask them to kill him. There are very few of the original inhabitants left alive.\n\n[h3]Reputation[/h3]\nReputation can be gained by turning repeatedly in the three Librams of Dire Maul ([item=18333], [item=18334], [item=18332]). Turning in the following class books also gives some reputation:[ul][li][item=18357] - Warrior[/li][li][item=18363] - Shaman[/li][li][item=18356] - Rogue[/li][li][item=18360] - Warlock[/li][li][item=18362] - Priest[/li][li][item=18358] - Mage[/li][li][item=18364] - Druid[/li][li][item=18361] - Hunter[/li][li][item=18359] - Paladin[/li][li][item=18401] - Warrior & Paladin[/li][/ul]Both class books and librams give 500 Reputation points each.',NULL),(8,889,0,NULL,2,'[b]Warsong Outriders[/b] is an orcish clan formerly led by [npc=18076], in which the clan was named after. The clan\'s Warsong Outriders form the Horde faction in the [zone=3277] battleground, where they are attempting to defend their logging operations in [zone=331] from the [faction=890].\n\nOne of the strongest and most violent clans, the Warsong Clan was also one of the most distinguished clans on Draenor and was able to evade Alliance expedition forces at every turn. Depicted as Grunts, they have mastered the use of swords and blades and a few of them have even attained the rank of a Blademaster.\n\n[h3]Reputation[/h3]\nReputation is gained through participation in the Warsong Gulch battleground. You gain 35 reputation each time your side captures a flag. This reputation gain is increased to 45 on holiday weekends.\n\nYou are granted the player title Conqueror once exalted with Warsong Outriders and the other two battleground factions, [faction=510] and [faction=729].',NULL),(8,890,0,NULL,2,'[b]Silverwing Sentinels[/b] are the Alliance faction for the [zone=3277] battleground. The night elves, who have begun a massive push to retake the forests of [zone=331] are now focusing their attention on ridding their land of the [faction=889] once and for all. And so, the Silverwing Sentinels have answered the call and sworn that they will not rest until every last orc is defeated and cast out of Warsong Gulch.\n\n[h3]Reputation[/h3]\nReputation is gained through participation in the Warsong Gulch battleground. You gain 35 reputation each time your side captures a flag. This reputation gain is increased to 45 on holiday weekends.\n\nYou are granted the player title [title=48] once exalted with Silverwing Sentinels and the other two battleground factions, [faction=730] and [faction=509].',NULL),(8,909,0,NULL,2,'The [b]Darkmoon Faire[/b] is a mysterious traveling carnival, which roams not only Azeroth but Outland as well. Led by the inimitable [npc=14823], a gnome of dubious heritage and unknown providence, the Faire brings fun, games, prizes, and exotic trinkets of unexpected power to [zone=215], [zone=12], or [zone=3519] each month.\n\nA variety of amusements can be had by the discerning fairegoer, but the most common attraction is the ticket redemption. A variety of merchants at the Faire collect items from around the worlds in exchange for [item=19182]. The tickets can, in turn, be saved up and turned in for prizes of varying worth and power. Several different ticket distributors are posted around the Faire, offering tickets for crafted items made by Leatherworkers, Blacksmiths, or Engineers as well as items gathered in the wild such as [item=11404] and [item=19933]. Tickets can be redeemed for many things, from flowers to hold in the off-hand to necklaces of great power.\n\nMany adventurers seek out the Darkmoon Faire to turn in the mystical [url=?items=15.0&filter=minle=1;cr=107;crs=0;crv=Combine+the+Ace]Darkmoon Cards[/url]. Darkmoon Cards come in eight suits, each of which has cards from Ace to Eight. Combining all cards in a suit produces a deck, which will start a quest to return that deck to the Darkmoon Faire. Each of the eight decks produces a different [url=?items=4.-4&filter=na=Darkmoon+Card]trinket[/url] with a different effect, some of which are quite powerful.\n\nThe Darkmoon Faire\'s usual schedule has it arriving on site on the first Friday of the month. For the weekend, the carnies will be seen setting up the midway, and the Faire will actually start early on the following Monday.',NULL),(8,910,0,NULL,2,'The [b]Brood of Nozdormu[/b] is a faction consisting of the Bronze Dragonflight. Their leader [npc=15192] can be found outside the [b]Caverns of Time[/b], with many of its agents flying in the sky of [zone=1377].\n\nIn order to open the gates of [b]Ahn\'Qiraj[/b], one champion must complete a long quest line for the bronze dragon Anachronos. This reputation is also relevant in the [zone=3428]; to obtain epic quest gear and rings.\n\n[h3]Reputation[/h3]\nPlayers begin at 0/36000 hated, the lowest level of reputation possible.\n\nBrood of Nozdormu reputation can be earned through killing bosses in both Ahn\'Qiraj instances, killing monsters inside the Temple of Ahn\'Qiraj, and doing quests related to the dungeons. You can also farm [item=20384], though this will take a lot longer, and requires one to have obtained the [item=20383] in [zone=2677] for the [item=21175] quest chain.\n\nKilling trash in the Temple of Ahn\'Qiraj can only get you to 2999 / 3000 Neutral, at which point reputation can only be further advanced through quests and handing in [item=21229] and [item=21230]. You may want to save all the insignias until after you are Neutral, since at that point gaining reputation becomes much more difficult.',NULL),(8,911,0,NULL,2,'[b]Silvermoon City[/b] is the capital of the blood elves, located in the northeastern part of the [zone=3430] within the kingdom of Quel\'Thalas. The breathtaking capital city of the blood elves may rival the dwarven capital of [zone=1537] as the world\'s oldest, still standing, capital. Recently rebuilt from the devastating blow dealt by the evil Prince Arthas, the city houses the largest population of blood elves left on Azeroth.[pad]Silvermoon today is only the eastern half of the original city; the western half was almost completely destroyed by the Scourge during the Third War. Falconwing Square, the second blood elf town, is the only part of western Silvermoon remaining in blood elf control. The Dead Scar (the path taken by Arthas Menethil and his undead army on the quest to resurrect Kel\'Thuzad, which carves through all of Eversong Woods) separates the rebuilt Silvermoon from the ruins of the western half. Interestingly, the Ruins of Silvermoon house no undead, instead they contain [url=?npcs&filter=na=wretched;maxle=8]Wretched[/url] and malfunctioning [npc=15638]. As it stands, what remains of Silvermoon City is still bigger than current Horde cities.\n\n[h3]History[/h3]\nThe city of Silvermoon was founded by the high elves after their arrival in Lordaeron thousands of years ago. The city was constructed out of white stone and living plants in the style of the ancient Kaldorei Empire. The city contained the famous Academies of Silvermoon as a center for the learning of Arcane Magic and Sunstrider Spire, a majestic palace home to the Royal family of the high elves. The Convocation of Silvermoon (also known as \"The Silvermoon Council\"), the ruling body of the high elves was also based here. Across a stretch of ocean to the north is the island that contains the Sunwell.[pad]Although Silvermoon itself was left relatively unscathed from the second war, in the third war the Death Knight Arthas led the Scourge into the city, attacking it on his quest to reach the Sunwell. The High Elven King was slain and the majority of the population killed. Scourge forces held the city for a time but abandoned it after the depleting of its resources.[pad]Though the city was attacked by the Scourge, it is not as destroyed as one might think. Though many of its plants are dead, and the occasional dead body is sprawled across the cobblestone, the city was immune to the fire and destruction. Silvermoon now resembles a ghost town, intact, but eerily abandoned. Nevertheless, treasure hunters often frequent Silvermoon to try and find some of the valuable artifacts that the elves left behind before they deserted the city, but the ghosts of Silvermoon\'s past inhabitants prevents anyone from taking anything.\n\n[h3]Reputation[/h3]\nA comprehensive list of quests that grant Silvermoon reputation can be found [url=?quests&filter=maxle=69;cr=1;crs=911;crv=0#00Mz]here[/url].[pad][npc=20612] is the quest giver for the repeatable [item=14047] quest that must be completed by non-blood elf Horde players in order to reach exalted and gain the ability to ride [url=?items=15.5&filter=na=hawkstrider]hawkstriders[/url], the mount of the blood elf race.',NULL),(8,922,0,NULL,2,'[b]Tranquillien[/b] is a joint blood elf and Forsaken town and separate faction in the [zone=3433].\n\n[h3]History[/h3]\nAs the Scourge made their way to the Sunwell, the elves had no choice but to retreat. The town of Tranquillien was abandoned by the retreating elves. The town is now used by the blood elves and the Forsaken as their base of operation to launch attacks aiming to take back the Ghostlands from the Scourge. However, the city is surrounded by the Scourge and even couriers have trouble getting past the enemy to reach the town. The undead forces of Deatholme are the most dangerous threat to the town.\n\n[h3]Reputation[/h3]\nUnlike most starting areas, the town of Tranquillien is its own faction. All quests you do for them will garner at least 1000 reputation apiece. [npc=16528] acts as the Tranquillien quartermaster. Vredigar can be found near the inn and will sell various [span class=q2]uncommon[/span] items, and even a [span class=q3]rare[/span] cloak when you reach exalted! If you complete all of the Tranquillien quests, you should be exalted by approximately level 20.[pad]There are a variety of quests mostly concerning reclaiming overrun villages, investigating undead and helping around. The \"end\" of the quest-revealed lore surrounding Tranquillien culminates with the quest to kill [npc=16329].',NULL),(8,930,0,NULL,2,'[b]Exodar[/b] is the faction associated with [zone=3557], the enchanted capital city of the draenei, built out of the largest husk of their crashed dimensional ship of the same name. It is located in the westernmost part of [zone=3524]. The Exodar faction leader is [npc=17468], who is located near the battlemasters in the Vault of Lights.\n\nThe history of the Exodar is a short one, as the draenei only recently raised it around the husk of their crashed ship, which is still smoking from the impact. The Exodar was once a naaru satellite structure around the dimensional fortress [url=?search=tempest+keep#z0z]Tempest Keep[/url]. The Exodar contains a large amount of technological wonders (due to its origins lying with the Tempest Keep) such as magically enchanted \"wires\" which transport holy energy throughout the ship to power the heating and lighting, as well as augmenting the draeneis\' already considerable powers.\n\n[h3]Reputation[/h3]\nAs with other major factions associated with the main races, Exodar reputation may be gained by doing repeatable cloth turn-in quests, killing the opposing faction in [zone=2597] (the blood elves), and doing the appropriately related quests. At honored, the player can purchase items from Exodar related vendors for 10% less, and at exalted, the player, if not a draenei, can purchase the [url=?items=15.5&filter=na=elekk;cr=93:92;crs=2:1;crv=0:0]various mounts[/url] sold by the Exodar. The cloth turn-in quests are available from [npc=20604] [small][/small].',NULL),(8,932,0,NULL,2,'[b]The Aldor[/b] are an ancient order of draenei priests who revere the naaru, and to this day they assist the naaru known as [faction=935] in their battle against [npc=22917] and the Burning Legion. They are found primarily in [zone=3703] and [zone=3520]. Though they have suffered much at the hands of the blood elves who later became [faction=934], they have put aside open warfare for the sake of the Sha\'tar. The Aldor\'s most holy temple lies on the Aldor Rise, overlooking the city from the west.\n\nMost players will start at neutral with the Aldor. [npc=18166] in Shattrath City will give players an initial quest to become friendly with the Aldor or the Scryers. This choice is reversible if players feel the need. Draenei players will be friendly with the Aldor and hostile with the Scryers, whereas blood elf players will be hostile to the Aldor and friendly to the Scryers.\n\n[npc=19321] and [npc=20807] are located in the Aldor bank on the northern edge of the Terrace of Light. The Shrine of Unending Light on Aldor Rise is home to [npc=20616]Asuur [small][/small] and [npc=21906] [small][/small], who exchange epic armor tokens for [url=?itemsets&filter=ta=12]Tier 4[/url] and [url=?itemsets&filter=ta=13]Tier 5[/url] gear, respectively.\n\n[i]Note: Reputation gains with Aldor correspond with a 10% greater loss of reputation with the Scryers. Most reputation gains with the Aldor will also grant 50% of the reputation gained toward your standing with the Sha\'tar.[/i]\n\n[h3]Reputation[/h3]\n[b]Until Honored[/b]\nPlayers looking to gain the higher reputation ranks (revered, exalted) may wish to save non-repeatable quests until after reaching honored.\n\nTurning in 10 [span class=q1][item=29425][/span] to [npc=18537] in Aldor Rise will grant 250 reputation with Aldor. There is also a repeatable quest for single mark turn-ins which yields 25 rep. These marks drop from low ranking Burning Legion members found in most zones in Outland, including the two camps north of Auchindoun in the Bone Wastes of [zone=3519]. Approximately 240 marks are required to go from friendly to honored. In addition these quests provide Sha\'tar reputation; 125 reputation per 10 or 12.5 reputation per single turn in.\n\nPlayers who also desire [faction=978] or [faction=941] reputation may prefer killing orcs at Kil\'Sorrow Fortress in southeastern [zone=3518], as they yield marks as well as 10 Kurenai or Mag\'har reputation per kill.[pad][b]Until Exalted[/b]\nOnce you reach level 68 you may also turn in [span class=q1][item=30809][/span] at the same rates as Marks of Kil\'jaeden. These drop from high-ranking followers of the Burning Legion. If you wish, you may turn in the higher level marks before honored reputation. In [zone=3522], grinding in Death\'s Door is the most compact group of mobs that drop marks.[pad][b]Fel Armaments[/b]\n[span class=q2][item=29740][/span] may be turned in at any time to [npc=18538]Ishanah [small][/small] inside the Shrine of Unending Light on the Aldor Rise. This will increase your reputation with Aldor by 350 per hand-in. In addition to reputation gains, you will receive [span class=q1][item=29735][/span], which is currency for the purchase of shoulder enchants from Inscriber Saalyn in the Aldor bank.\n\n[h3]Switching to Aldor[/h3]\nTo change your faction from the Scryers to the Aldor to access their crafting recipes (and undo all reputation progress you have made), find [npc=18597], an Aldor in Lower City. She offers a repeatable quest for 8x [span class=q1][item=25802][/span]. Once you are neutral with the Aldor, you may no longer receive this quest.',NULL),(8,933,0,NULL,2,'Led by [npc=19674], [b]The Consortium[/b] are ethereal smugglers, traders and thieves that have come to Outland. Their main base of operations and biggest settlement is the Stormspire, but they can be found at Midrealm Post, the Aeris Landing, within the [zone=3792] of Auchindoun and various other places.\n\nUpon reaching Friendly status, players are officially considered members of the Consortium and given a salary. The salary is a bag of gems at the beginning of every month, given by [npc=18265] at Aeris Landing. Higher reputation with the Consortium yields higher qualities and quantities of jewels each month.\n\n[h3]Reputation[/h3]\n[b]Until Friendly[/b][ul][li]Run Mana-Tombs in [i]normal[/i] mode, ~1200 reputation per run.[/li][li]Turn in [item=25416] at [npc=18265].[/li][li]Turn in [item=25463] at [npc=18333].[/li][/ul][b]Friendly to Honored[/b][ul][li]Run Mana-Tombs in [i]normal[/i] mode, ~1200 reputation per run.[/li][li]Turn in [item=25433] at [npc=18265].[/li][li]Turn in [item=29209] at [npc=19880].[/li][/ul][b]Honored to Exalted[/b][ul][li]Run Mana-Tombs in [i]heroic[/i] mode, ~2400 reputation per run.[/li][li]Complete all available [url=?quests&filter=cr=1;crs=933;crv=0]quests[/url].[/li][li]Turn in [item=25433] at [npc=18265].[/li][li]Turn in [item=29209] at [npc=19880].[/li][/ul]Characters trying to simultaneously earn reputation with the [faction=941] or [faction=978] and the Consortium may want to focus on killing ogres ([url=?npcs&filter=na=boulderfist;cr=6;crs=3518;crv=0]Boulderfist[/url], [url=?npcs&filter=na=Warmaul;cr=6;crs=3518;crv=0]Warmaul[/url]) in Nagrand and saving the Obsidian Warbeads for Consortium turn-ins. The only caveat is the drop rate, which is roughly 33% for the warbeads, while it is 50% on the insignias. If you are level 70 and want a faster grind without concern for Mag\'har/Kurenai reputation, then you may want to grind insignias instead. Then again, the ogres are generally easier to grind, ranging from level 65 to 67. The choice is ultimately up to the player.',NULL),(8,934,0,NULL,2,'[b]The Scryers[/b] are blood elves who reside in [zone=3703] led by [npc=18530]. The group broke away from [npc=19622] and offered to assist the Naaru at Shattrath City. They are at odds with the [faction=932], and compete with them for power within Shattrath and the Naaru\'s favor.[pad]Most players will start at neutral with the Aldor. [npc=18166] in Shattrath City will give players the choice of aligning themselves with the Scryers or Aldor after completing [quest=10211]. This choice is reversible if players feel the need. Blood elf players will be friendly with the Scryers and hostile with the Aldor, whereas draenei players will be hostile to the Scryers and friendly to the Aldor.[pad]The Scryers have both a [npc=19251] trainer and a [npc=19252] trainer. Due to this, the enchanter nestled deep within [zone=1337] is rendered obsolete.[pad][npc=19331] and [npc=20808] are located in the Scryers bank on the southern edge of the Terrace of Light. The Seer\'s Library in the Scryer\'s Tier is home to [npc=20613] [small][/small] and [npc=21905] [small][/small], who exchange epic armor tokens for [url=?itemsets&filter=ta=12]Tier 4[/url] and [url=?itemsets&filter=ta=13]Tier 5[/url] gear, respectively.[pad][i]Note: Reputation gains with Scryers correspond with a 10% greater loss of reputation with the Aldor. Most reputation gains with the Scryers will also grant 50% of the reputation gained toward your standing with the [faction=935].[/i]\n\n[h3]Lore[/h3]\nAfter enduring relentless assaults, the harried Sha\'tar and Aldor guards braced for the next wave as it marched over the horizon. This time, the attack came from the armies of [npc=22917]. A large regiment of blood elves had been sent by Illidan’s ally, Prince Kael\'thas Sunstrider, to lay waste to the city. As the regiment of blood elves crossed the bridge, the Aldor’s exarches and vindicators lined up to defend the Terrace of Light. Then the unexpected happened, the blood elves laid down their weapons in front of the city\'s defenders. Their leader, a blood elf elder known as Voren’thal, stormed into the Terrace of Light and demanded to speak to the naaru [npc=18481]. As the naaru approached him, Voren’thal knelt and uttered the following words: \"I’ve seen you in a vision, naaru. My race’s only hope for survival lies with you. My followers and I are here to serve you.\"[pad]The defection of Voren’thal and his followers was the largest loss ever incurred by Kael’thas’ forces. Many of the strongest and brightest amongst Kael’thas’ scholars and magisters had been swayed by Voren’thal\'s influence. The naaru accepted the defectors who became known as the Scryers.\n\n[h3]Reputation[/h3]\n[b]Until Honored[/b]\nPlayers looking to gain the higher reputation ranks (revered, exalted) may wish to save non-repeatable quests until after reaching honored.[pad]Turning in 10 [span class=q1][item=29426][/span] to [npc=18531] in Scryer\'s Tier will grant 250 reputation with the Scryers. These signets can also be turned in one at a time at the same exchange rate, 25 reputation per signet. These signets drop from low ranking Firewing members found in the northeast section of Terrokar Forest. This repeatable quest becomes unavailable at honored. If no other reputation quests are done, 240 signets are required to go from friendly to honored.[pad][b]Until Exalted[/b]\nOnce you reach level 68, you may also turn in [span class=q1][item=30810][/span]. These drop from high-ranking Sunfury blood elves (found in [zone=3523], [zone=3520], and the [url=?search=tempest+keep+-eye+-kael]Tempest Keep[/url] instances). If you wish, you may turn in the higher level signets before honored reputation, however it is recommended that you save them for after you hit honored. For every 10 signets, you will gain 250 reputation. Once you hit honored it will take approximately 1,320 Sunfury signets to go from honored to exalted if no other reputation is earned.[pad][b]Arcane Tomes[/b]\n[span class=q2][item=29739][/span] may be turned in at any time to Voren\'thal the Seer inside the The Seer\'s Library on the Scryer\'s Tier. This will increase your reputation with the Scryers by 350 per hand-in. If you wish, you may turn in the Arcane Tomes before honored reputation, however it is recommended that you save them for after you hit honored. Once you hit honored it will take approximately 94 Arcane Tomes to go from honored to exalted if no other reputation is earned. In addition to reputation gains, you will receive an [span class=q1][item=29736][/span], which is currency for the purchase of shoulder enchants from Inscriber Veredis, who resides in the Scryers bank.\n\n[h3]Switching to Scryers[/h3]\nTo change your faction from Aldor to Scryers to access their crafting recipes (and undo all reputation progress you have made), find [npc=18596], a Scryers in the Lower City. She offers you a repeatable quest, [quest=10024], that requires you to find eight [span class=q1][item=25744][/span]. Once you are Neutral with the Scryers, you can no longer receive this quest. The quest gives you +250 Scryers reputation and -275 Aldor reputation (in addition, the quest also gives you +125 reputation with The Sha\'tar).',NULL),(8,935,0,NULL,2,'[b]The Sha\'tar[/b], or \"born of light,\" are naaru that aided [faction=932], the order of draenei priests formerly led by [npc=17468], in rebuilding [zone=3703]. The city was destroyed by the Orcs during their rampage across Draenor prior to the First War. Defeat of the Burning Legion is the Sha\'tar\'s ultimate goal; the Sha\'tar are aided in this war by the Aldor and their rivals, the blood elf faction known as [faction=934]. The Aldor and the Scryers fight for the favor of the Sha\'tar so that they may be assisted in their war by the naaru\'s powers. The entity that leads the Sha\'tar is known as [npc=18481]; he can be found upon the Terrace of Light in Shattrath City.\n\nBoth Alliance and Horde players begin as Neutral toward the Sha\'tar. Players can increase their Sha\'tar reputation through various quests, by raising their reputation with the Aldor or Scryers, or by adventuring into [url=?search=Tempest+Keep#z0z]Tempest Keep[/url].\n\n[h3]Reputation[/h3]\n[b]Until Honored[/b]\nReputation can be gained from Scryer/Aldor signet/mark turn-ins. The following will only grant Sha\'tar reputation until you achieve Honored status: [item=29426], [item=30810], and [item=29739] for the Scryers; [item=29425], [item=30809], and [item=29740] for the Aldor. In addition, these will require more turn-ins to produce equable Sha\'tar reputation to the main faction. Note that this reputation gain does not show up in the combat log, but can be verified by looking at your reputation panel.\n\nReputation can also be gained by running Tempest Keep: [zone=3847], [zone=3846] and [zone=3849].\n\n[b]Through Exalted[/b]\nAfter exhausting the reputation rewards from Aldor/Scryer turn-ins and Mechanar runs, players may wish to complete the few Sha\'tar quests available. In addition to the quests, instance runs in Tempest Keep: Botanica, Arcatraz and Mechanar will continue to grant reputation. At this point, it is probably more worthwhile to run these instances in Heroic mode.',NULL),(8,941,0,NULL,2,'The [b]Mag\'har[/b] are a faction of brown-skinned orcs who remain on Outland and have separated themselves from the other remaining orc clans that fell prey to [npc=17257] and joined his army of fel orcs (that are now led by the powerful [npc=16808]). The Mag\'har are settled in the stronghold of Garadar in the beautiful land of [zone=3518], once home to the majority of the orcs along with [zone=3519] and the [zone=3522].[pad]The Mag\'har orcs have never been corrupted by Mannoroth or Magtheridon and thus remained untouched by the bloodlust. Unlike their former clanmates who live in the ruins of their once-mighty holds, the Mag\'har are made up of members of different orc clans who escaped corruption. The current leader of the Mag\'har, venerable [npc=18141], is an old and wise orc, yet she has recently fallen extremely ill. [npc=18063], son of the mighty Grom Hellscream, serves as the Mag\'har\'s military chief, aided by [npc=18106], son of the venerable chieftain of the Bleeding Hollow clan, Kilrogg Deadeye. In addition, there is an NPC within a Mag\'har camp to the west known as [npc=18229].[pad]It is not clear how the Mag\'har managed to retain their original brown skin. Orcish skin turns green when exposed to warlock magic, regardless of the individual\'s beliefs or practices; Garrosh and Jorin would certainly have been exposed, given the positions of their fathers. \n\nHorde players start out at unfriendly with the Mag\'har. Alliance players will always be treated as hostile. The Alliance counterpart to this faction are the [faction=978].\n\n[h3]Questing[/h3]\nQuests for the Mag\'har begin in [zone=3483] with [quest=9400] from [faction=947]. This quest will lead you to a small Mag\'har outpost north of Hellfire Citadel. Once in Nagrand, players will find the main Mag\'har city, Garadar. The city holds most of the remaining quests that will reward Mag\'har reputation.\n\nNote: You MUST have completed the quest chain of \"The Assassin\" up until the quest [quest=9410] (where you become Neutral) in order for you to talk to most people in Garadar.\n\n[h3]Reputation[/h3]\nReputation can be gained from killing [url=?npcs&filter=na=kil%27sorrow;ra=-1;rh=-1]Kil\'sorrow cult members[/url], [url=?npcs&filter=na=Murkblood;ra=-1;rh=-1;cr=6;crs=3518;crv=0]Murkblood Broken[/url], [url=?npcs&filter=na=warmaul+-marker]Warmaul[/url] and [url=?npcs&filter=na=boulderfist;minle=64;ra=-1;rh=-1]Boulderfist[/url] ogres in Nagrand. Players may also turn in 10x [item=25433], which drop from these ogres.[pad]Players seeking [faction=933] reputation may wish to save their warbeads, as Mag\'har reputation is generally easier to obtain.[pad]Players seeking [faction=932] reputation may prefer killing cult members at Kil\'Sorrow Fortress, as they drop [item=29425] for Aldor reputation turn-ins.\n\n[i]Note: These monsters and quests do not have a limit, they grant reputation all the way through exalted![/i]',NULL),(8,942,0,NULL,2,'Upon the reopening of the Dark Portal to Outland, the [faction=609] dispatched an exploratory force, known as the [b]Cenarion Expedition[/b], to explore the uncharted world. Much like the Circle, it is a coalition of night elf and tauren forces. Since the opening of the Dark Portal, the Cenarion Expedition has quickly gained in size and autonomy, achieving enough power to be considered its own faction. The Expedition maintains its primary base at Cenarion Refuge in [zone=3521]; it has also made its presence known on [zone=3483], in [zone=3519], and in the [zone=3522]. Cenarion Refuge is located immediately west of Thornfang Hill.\n\nThe Refuge is located in the Zangarmarsh for the primary reason of studying the rich wildlife located there. However, the Expedition has discovered troubling goings-on in the marsh. Water levels in many parts of Zangarmarsh are decreasing, and some areas such as the Dead Mire have already suffered greatly from this strange phenomenon. It has become known that this decrease in the water levels can be attributed to pumps that have been constructed in the Marsh by the naga. Their purpose is to create a new Well of Eternity for [npc=22917]. However, the Expedition cannot afford direct confrontation with the naga so numerous in the Zangarmarsh and [url=?search=coilfang#c0z]Coilfang Reservoir[/url]. It needs the aid of those willing to assist the druids in their dangerous battle against those who seek to disturb the marsh\'s natural balance. Quite naturally, those heroic enough to fight the naga at Coilfang Reservoir will be well rewarded.\n\n[h3]Reputation[/h3]\n[b]Neutral to Honored[/b]\nKill Naga, while also running [zone=3717] whenever you can; a good instance run will net reputation faster than soloing. Alternatively, the player can begin turning in [item=24401] for a chance at an [item=24407], which can be turned in for 500 reputation. It is suggested that the player save his Uncatalogued Species until after Honored status is achieved, as the quest cannot be continued past that point, while Uncatalogued Species can be used until Exalted.\n\nIf you are an herbalist, and interested in [faction=970] reputation, you may want to grind the [url=?npcs&filter=na=Bog+Lord]Bog Lords[/url] which can be found in the NE, SE, and SW corners of Zangarmarsh. Their bodies can be \"picked\" by herbalists and often yield Unidentified Plant Parts, while every kill yields 15 reputation with Sporeggar.[pad][b]Honored to Revered[/b]\nOnce the player is Honored, running Slave Pens and the [zone=3716] (with the exception of [npc=17770] and some giants), will no longer grant reputation. You should now do any Cenarion Expedition quests in Hellfire Peninsula, Zangarmarsh, Terokkar Forest and the Blade\'s Edge Mountains. It is also the time to turn in any Uncatalogued Species you have found. Doing this should get you part of the way into Revered.\n\nAlternatively, you can finish leveling to 70 and run [zone=3715]. Each run gives just over 1500 reputation if you clear all mobs. Also within the Steamvault lies a repeatable quest, [quest=9764], which begins with [item=24367]. You will then be able to turn in [item=24368], which drop in both Steamvault and Slave Pens, receiving 250 reputation for the first turn-in and 75 reputation each thereafter. This turn-in is available all the way to Exalted.\n\nOnce you are 70 and have upgraded your gear, you can opt to run Slave Pens, Underbog, and Steamvault on Heroic Mode upon purchasing the [item=30623]. While the instances are difficult, they award significant reputation: regular mobs are worth 15 reputation, 2 for non-elites, and 150/250 for bosses. This method works until Exalted.[pad][b]Revered to Exalted[/b]\nContinue with the same strategy as above: finish any remaining quests, run Steamvault, and continue with [item=24368] turn-ins.\n\nIt is also possible to run Slave Pens, Underbog, and Steamvault on Heroic Mode. The reputation gained is not much more than running Steamvault in normal mode, whilst the time investment for heroic dungeons is much higher, possibly resulting in a lower net reputation per hour, however the loot is better and you will receive [item=29434] from the bosses which can be used to purchase high quality epic gear.',NULL),(8,946,0,NULL,2,'A refuge of human, elven, draenei and dwarven explorers, [b]Honor Hold[/b] is the first major town Alliance explorers will encounter while traversing Outland. Vestiges of the Sons of Lothar, veterans of the Alliance that first came into Draenor, have steadfastly held on to this Hellfire outpost. They are now joined by the armies from Stormwind and Ironforge.\n\n[h3]Reputation[/h3]\nHonor Hold reputation is gained through various means in Hellfire Peninsula. Mobs in and around Hellfire Citadel reward Honor Hold reputation, as well as quests picked up in town. Due to the lack of representatives in other areas, there is a large gap between Honored and Exalted during which you may not attain any Honor Hold reputation from questing and killing mobs in Outland once you depart Hellfire Peninsula.\n\n[b]Through friendly[/b]\nMobs in [zone=3562] and [zone=3713] will award reputation through Friendly. One option is to grind reputation via Ramparts and Blood Furnace runs until honored before doing any Honor Hold quests outside the instances, as those continue to yield reputation up to Exalted. You may also want to check out the following outdoor mobs which give reputation if you are Neutral. These mobs will not give reputation once you are Friendly with Honor Hold.[ul][li][npc=19415] [/li][li][npc=16878] [/li][li][npc=16870][/li][li][npc=16867][/li][li][npc=19414] [/li][li][npc=19413] [/li][li][npc=19411] [/li][li][npc=19422][/li][/ul]To make the best use of available resources, you may want to grind reputation with Honor Hold through Hellfire Ramparts and Blood Furnace prior to completing any Honor Hold quests. \n\n[b]PvP[/b]\nPlayers that enjoy PvP can earn Honor Hold reputation through the daily quest [quest=10106]. This quest awards 70 silver and 150 Honor Hold reputation, but can only be completed once a day and counts towards your 25 daily quest limit. Completion of this quest also yields three [span class=q1][item=24579][/span], which are used as currency for various types of items and gear when turned into [npc=17657] and [npc=18266] in Honor Hold as well as the [npc=18581] in Zangarmarsh.\n\n[i]Tip: You can use these marks to purchase [span class=q1][item=24520][/span] from Warrant Officer Tracy Proudwell and increase the amount of reputation (and experience) gained while running these instances.[/i]\n\n[b]Through Exalted[/b]\nFrom here on out there are only two ways to achieve Revered and Exalted status:[ul][li][zone=3714], this instance requires level 68 and the [span class=q1][item=28395][/span] (only one party member needs the key). Mobs in Shattered Halls will yield reputation through Exalted.[/li][li]After achieving Honored status you can purchase the [span class=q1][item=30622][/span] which grants access to the heroic mode of all Hellfire Citadel instances. Mobs in all Heroic mode Hellfire Citadel instances will yield slightly more reputation than those found in non-heroic Shattered Halls, and will continue to yield reputation through Exalted.[/li][/ul]',NULL),(8,947,0,NULL,2,'The expedition sent through the Dark Portal by Thrall has built a stronghold in Hellfire Peninsula. [b]Thrallmar[/b] serves as a base of operations for much of the Horde\'s activities in Outland.\n\n[h3]Reputation[/h3]\nReputation for Thrallmar up to Honored is relatively easy to earn. Even the easiest quests (those that take you from one quest giver to the next up the road, for example) can yield 75 reputation points, while those that require some effort to complete typically yield 250 reputation points or more. Some group quests that involve killing an elite can yield as much as 1000 reputation points.\n\nIf you do the bulk of the Thrallmar quests instead of quickly moving on to the next zone, you might expect to reach Honored after 1 or 2 levels of play. However, once you reach Honored, you hit an earnings barrier that you can only remove when you are level 68 and can start re-earning points in the [zone=3714] dungeon.\n\n[b]Neutral through Friendly[/b]\nReputation from mobs in [zone=3562] and [zone=3713] stops at 5999/6000 friendly. One option is to grind reputation via Ramparts and Blood Furnace runs to 5999/6000 before doing any Thrallmar quests outside the instances, as those continue to yield reputation up to Exalted.\n\nAlso, the level 63 mobs outside Hellfire Citadel (on the path) give you 5 reputation each.\n\n[b]Friendly through Honored[/b]\nPlayers that enjoy PvP can earn Thrallmar reputation through the daily quest [quest=10110]. This quest awards 70 silver and 150 Thrallmar reputation, but can only be completed once a day and counts towards your 25 daily quest limit. Completion of this quest also yields three [item=24581], which are used as currency for various types of items and gear when turned into [npc=18267] and the [npc=18564] in Thrallmar and near Zabra\'jin in [zone=3521] respectively.\n\nBlood Furnace and Ramparts instance runs will be your best bet for this reputation bracket. Be aware though, that they will only take you to the end of Honored. You will need to run Shattered Halls to reach Revered status.\n\n[b]Revered to Exalted[/b]\nFrom this point on, gaining reputation through Exalted requires one of two things:[ul][li]Access to Shattered Halls, one of the wings of Hellfire Citadel, which requires level 68 and either the [span class=q1][item=28395][/span] or a rogue with 350 lockpicking skill.[/li][li]Doing Heroic versions of Hellfire Citadel dungeons, which typically require you to be well geared and level 70.[/li][/ul]Both of these give reputation until you reach Exalted status. A full clear of Shattered Halls nets you about 2000 reputation points, trash mobs generally yield 6 or 12 each, with up to 150 points from bosses. Heroic trash yields 15-25 points, with bosses worth more. \n\n[i]Tip: You can purchase [span class=q1][item=24522][/span] from Battlecryer Blackeye for use during instance runs to speed up the reputation (and experience) gaining process![/i]',NULL),(8,967,0,NULL,2,'[b]The Violet Eye[/b] is a secret sect founded by the Kirin Tor of Dalaran to spy on the Guardian of Tirisfal, [npc=15608], in his tower of [zone=2562]. Though Medivh is dead, the Violet Eye remains in Karazhan, defending against the evil that appears to have taken hold in the absence of its master. \n\nIt is unknown whether Medivh\'s apprentice, [npc=18166], was a member of the Violet Eye, or whether he knew of their activities at the time (though he does seem to be aware of them now).\n\n[h3]Reputation[/h3]\nViolet Eye reputation is gained by killing mobs inside Karazhan and completing Karazhan related quests. Reputation from Karazhan mobs can be gained from neutral standing all the way to exalted. Each trash mob awards around 15 reputation, with the bosses award more.\n\n[npc=18253] begins a fairly long quest chain starting with [quest=9824] and [quest=9825]. This quest line rewards players with [span class=q1][item=24490][/span] and culminates with [quest=9644]. Full completion of this quest line rewards approximately 10,270 reputation.\n\n[h3]Reputation Rewards[/h3]\n[npc=18253] will offer players rings as rewards for reputation level gains in the form of quests. The first such quest is available at neutral standing and may be completed at friendly. You will receive a new and upgraded version of the ring you chose each time you break into a new reputation tier. The rings are sorted into the following 4 categories:[ul][li][quest=10731]: [item=29280], [item=29281], [item=29282] and [item=29283][/li][li][quest=10729]: [item=29284], [item=29285], [item=29286] and [item=29287][/li][li][quest=10732]: [item=29276], [item=29277], [item=29278], and [item=29279][/li][li][quest=10730]: [item=29288], [item=29289], [item=29291] and [item=29290][/li][/ul][npc=16388], a blacksmith located inside Karazhan just after [npc=15550], offers players with high enough reputation the ability to buy epic blacksmithing plans. Players who are honored or above will also be able to repair armor and weapons at this vendor.\n\n[npc=18255], who stands just outside the main gates of Karazhan, will sell an epic jewelcrafting recipe and shoulder enchant to players who have an honored or above standing with The Violet Eye.',NULL),(8,970,0,NULL,2,'The sporelings are a mostly peaceful race of mushroom-men native to Outland. Their home, [b]Sporeggar[/b], is located in the western bogs of [zone=3521].\n\n[h3]Reputation[/h3]\nPlayers both Alliance and Horde start out unfriendly with Sporeggar. There are many ways to increase your reputation at the beginning:[ul][li]Bringing 10 [span class=q1][item=24290][/span] to [npc=17923] to complete [quest=9739][/li][li]Bringing 6 [span class=q1][item=24291][/span] to Fahssn to complete [quest=9743] [i](both of these quests will be available only if you are below friendly)[/i][/li][li]Killing [url=?search=bog+lord+-hungry#z0z]Bog Lords[/url] [i](lasts until the end of honored)[/i][/li][li]Killing [npc=18137] and [npc=18136] [i](lasts until the end of revered)[/i][/li][li]Bringing 10 [span class=q1][item=24245][/span] to [npc=17924] in Sporeggar [i](lasts only during neutral)[/i][/li][/ul]After you hit [b]friendly[/b], a new handful of repeatable quests opens up at the same time Fahssn\'s quests and the Glowcap turnins become unavailable, these include:[ul][li]Killing 12 each of [npc=18088] and [npc=18089] for [npc=17856] to complete [quest=9726][/li][li]Bringing 10 [span class=q1][item=24449][/span] to [npc=17925] to complete [quest=9806][/li][li]Venturing into [zone=3716] to gather 5 [span class=q1][item=24246][/span] for Gzhun\'tt to complete [quest=9715][/li][/ul]These 3 quests are repeatable and will be available to the end of exalted.\n\nPlayers who are exalted with Sporeggar should speak to [npc=17877] for one final quest.',NULL),(8,978,0,NULL,2,'Draenei for \"redeemed.\" These Broken have escaped the grasp of their various slavers in Outland and have made their home at Telaar in southern [zone=3518]. It is there that they seek to rediscover their destiny. They also maintain a small presence at Orebor Harborage, [zone=3521]. Their quartermaster, [npc=20240], is located outside the inn in Telaar, below the flight point.\n\nAlliance players start out at unfriendly with the Kurenai. Horde players will always be treated as hostile. The Horde counterpart to this faction are [faction=941].\n\n[i]Kurenai is Japanese for \"crimson\".[/i]\n\n[h3]Gaining Reputation[/h3]\nReputation can be gained from killing [url=?npcs&filter=na=kil%27sorrow;ra=-1;rh=-1]Kil\'sorrow cult members[/url], [url=?npcs&filter=na=Murkblood;ra=-1;rh=-1;cr=6;crs=3518;crv=0]Murkblood Broken[/url], [url=?npcs&filter=na=warmaul+-marker]Warmaul[/url] and [url=?npcs&filter=na=boulderfist;minle=64;ra=-1;rh=-1]Boulderfist[/url] ogres in Nagrand. Players may also turn in [item=25433] (10), which drop from these ogres.\n\nPlayers seeking [faction=933] reputation may wish to save their warbeads, as Kurenai reputation is generally easier to obtain.\n\nPlayers seeking [faction=932] reputation may prefer killing cult members at Kil\'Sorrow Fortress, as they drop [item=29425] for Aldor reputation turn-ins.\n\n[i]Note: These monsters and quests do not have a limit, they grant reputation all the way through exalted![/i]',NULL),(8,989,0,NULL,2,'The [b]Keepers of Time[/b] are bronze dragons hand-picked by Nozdormu to watch over the Caverns of Time. They are led by [npc=19932] and [npc=19933], who are also acting leaders of the Bronze Dragonflight in Nozdormu\'s absence.\n\n[h3]Reputation[/h3]\nCurrently the only way to gain the favor of the enigmatic bronze dragons is through [zone=2367] and [zone=2366] instance runs. Keepers of Time reputation rewards may be found at the Keepers\' quartermaster, [npc=21643]. The Keepers will require you to be level 66 and complete the short quest [quest=10277] before allowing passage into Old Hillsbrad Foothills to fulfill [npc=17876]\'s destiny to become the Warchief of the Horde.',NULL),(8,990,0,NULL,2,'The [b]Scale of the Sands[/b] is a secretive subgroup of the Bronze Dragonflight, led by [npc=19935], prime mate of [npc=15185]. It is a subgroup of the Bronze Dragonflight. Their leader, Nozdormu, sent these guardian factions to [zone=3606] where they guard the World Tree from another attack by the demons of Darkwhisper Gorge and help restore the time-stream and preserve the future of the world.\n\n[h3]Reputation[/h3]\nBoth bosses and trash monsters give reputation with each kill. [npc=17968], the final boss, awards 1500 reputation while the other four bosses give 375. General trash award 12 reputation, while [npc=17907] give 60. Yielding an average of 7800 per full clear, it would take 5-6 clears to reach exalted.\n\nCurrently some of the best [span class=q4][url=?items=4.-2&filter=na=band+of+the+eternal]rings[/url][/span] for raiding are available via this reputation. In order to recieve the rings, you must complete the previously required attunement quest, [quest=10445]. Each new reputation level awards an upgraded ring.',NULL),(8,1011,0,NULL,2,'The [b]Lower City[/b] of [zone=3703] is the place where the refugees gather and help out in their own ways. When someone helps any of the mixture of races who fled from war, word gets around quickly. Their quartermaster, [npc=21655], is located at the market in the Lower City. The Lower City of Shattrath also contains a very useful Mana Loom or an Alchemy Lab. Many NPCs have extensive knowledge of crafting. The Battlemasters for both sides of all four [zones=6] can also be found here, as well as the World\'s End Tavern.\n\nOther important NPCs include:[ul][li]A neutral Grand Master Leatherworker, [npc=19187].[/li][li]A neutral Grand Master Skinner, [npc=19180].[/li][li]A neutral Grand Master Alchemist, [npc=19052], with an Alchemy Lab, who also gives the quest [quest=10902] (for alchemy specialization).[/li][li]Three specialist tailors who allow you to specialize and buy new epic tailoring recipes for armor sets and special bags (including the 20-slot bag).[ul][li][npc=22212] [small][/small] sells the patterns for the [itemset=553] set.[/li][li][npc=22213] [small][/small] sells the patterns for the [itemset=552] set.[/li][li][npc=22208] [small][/small] sells the patterns for the [itemset=554] set.[/li][/ul][/li][/ul]\n\n[h3]Reputation[/h3]\n[b]Until Honored[/b][ul][li]Run [zone=3790] in [i]normal[/i] mode, ~750 reputation.[/li][li]Run [zone=3791] in [i]normal[/i] mode, ~1250 reputation.[/li][li]Run [zone=3789] in [i]normal[/i] mode, ~2000 reputation.[/li][li]Turn in [item=25719] at [npc=22429].[/li][/ul][i]Note: Players aiming for faction higher than Honored should wait until honored to complete the Lower City quests.[/i]\n\n[b]Honored to Revered[/b][ul][li]Run Shadow Labyrinth in [i]normal[/i] mode, ~2000 reputation.[/li][li]Complete all available [url=?quests&filter=cr=1;crs=1011;crv=0]Lower City quests[/url].[/li][/ul][b]Revered to Exalted[/b][ul][li]Run Auchenai Crypts in [i]heroic[/i] mode, ~750 reputation.[/li][li]Run Sethekk Halls in [i]heroic[/i] mode, ~1250 reputation.[/li][li]Run Shadow Labyrinth in [i]normal[/i] or [i]heroic[/i] mode, ~2000 reputation.[/li][/ul]\n\n[h3]Trivia[/h3]\n[npc=19227], a vendor in Lower City, sells amulets which are very... interesting. He is quite the salesman, with items like [item=27940], which allows you to return to life as long as you return to the place you died. [i]Buyer beware![/i]\n\nAt exalted you can purchase a [item=31778]. Strangely, none of the NPCs in Lower City can be seen wearing one. Perhaps they cannot afford one...',NULL),(8,1012,0,NULL,2,'The [b]Ashtongue Deathsworn[/b] are the elite of the Broken draenei tribe known as the Ashtongue. The Ashtongue tribe is led by the elder sage [npc=21700]; the Deathsworn are [i]officially[/i] aligned with [npc=22917] [small][/small]. The Deathsworn are Akama\'s most trusted lieutenants and are privy to their leader\'s mysterious motivations.\n\nTo discover the Deathsworn as a faction, the player must begin and complete the majority of the quest line which begins with Tablets of Baa\'ri ([quest=10568] / [quest=10683]). Eventually, you will speak with Akama, whereupon you will become Neutral with the Deathsworn.',NULL),(8,1015,0,NULL,2,'The [b]Netherwing[/b] are a faction of dragons located in Outland. The unusual brood was spawned from the eggs of Deathwing\'s black dragonflight, and infused with raw nether-energies. Now, they seek to find their identity beyond the shadows of their father\'s destructive heritage.\n\n[h3]Reputation[/h3]\nPlayers are introduced to the Netherwing faction at 0/36000 hated reputation, and must be exalted to receive a [span class=q4][url=?items=15.-7&filter=na=Netherwing+Drake]Netherwing Drake[/url][/span]. The quest chain and reputation grind is a mostly solo endeavor involving quests that can only be completed once daily, a 5-player group quest on the way to neutral, and daily 3-player group quests after reaching revered. A flying mount is required for this reputation grind, and 300 riding skill is necessary to advance past neutral.\n\n[b]Hated to Neutral[/b]\nLevel 70 players will begin their journey to exalted reputation by picking up the quest chain offered by [npc=22113], a blood elf wandering the surface of the Netherwing Fields, in the southeast corner of [zone=3520]. The quest chain begins with the quest [quest=10804]. Completion of this quest line will provide an instant reputation boost to neutral and the choice of one of [span class=q3][url=?items&filter=qu=3;na=Netherwing+-wand]these[/url][/span] five items.\n\n[h3]Netherwing Reputation After Neutral[/h3]\nAfter completing the Kindness quest chain, Mordenai will be sure you have acquired 300 [spell=34091] skill and have you swear fealty to the Netherwing. This will grant you a Dragonmaw Fel Orc disguise when you enter Netherwing Ledge and allow you to communicate and work for the Dragonmaw stationed there. Mordenai will initially send you to [npc=23139] with a set of fake papers. Completing this quest will unlock the beginning Dragonmaw quests that you\'ll be working on to increase your Netherwing reputation. Most of these quests will have the new \"Daily\" tag added with 2.1. Daily quests differ from regular quests in that they are infinitely repeatable, but you may only complete each daily quest once per day and are restricted to ten total daily quests per day.[pad][i]Note: New quests will be unlocked with each reputation tier, and all daily quests of previous tiers will always be available, even after reaching exalted.[/i]\n\n[b][toggler id=Neutral hidden]Neutral[/toggler][/b]\n[div id=Neutral hidden]After turning in Mordenai\'s [item=32469] to Mor\'ghor to complete [quest=11013], your first group of quests will become available to start you on your way to the next tier of reputation with the Netherwing. Mor\'ghor will point you to the taskmaster to begin your grunt work, and [npc=23141] will reveal himself as a Netherwing ally in disguise and present another group of quests to you. One of which is [quest=11049]. Players will be able to turn in any [item=32506] that have a 1% chance to be found in [object=185881], [object=185877], and on almost all creatures on Netherwing Ledge. It can also be a rare find as a [object=185915] anywhere on Netherwing Ledge and in the Dragonmaw Fortress on the southeast corner of the Shadowmoon Valley mainland. This quest is not labeled as daily, and therefore can be done as many times as you can find eggs and will not hinder your daily quest limit.[pad]Other quests available from the beginning:[ul][li][i][small](Daily)[/small][/i] [quest=11018], [quest=11016], [quest=11017] - These will be available only to players who possess the respective profession to gather each item.[/li][li][i][small](Daily)[/small][/i] [quest=11015] - Simple gathering quest open to all players regardless of profession.[/li][li][i][small](Daily)[/small][/i] [quest=11020] - Yarzill will ask you to collect [item=32502] and use them to poison the peons that are working to gather resources for Dragonmaw.[/li][li][i][small](Daily)[/small][/i] [quest=11035] - You will need to fly to the northeast corner of Netherwing Ledge and position yourself on one of the floating rocks to intercept the [npc=23188] and recover 10 [item=32509].[/li][/ul][/div][pad][b][toggler id=Friendly hidden]Friendly[/toggler][/b]\n[div id=Friendly hidden]Mor\'ghor will award you with an [item=32694] to go with your new rank among the Dragonmaw.[ul][li][quest=11083] - [npc=23166] will task you with quelling the Murkblood Broken that are stationed deeper within the mines.[/li][li][quest=11081] - After finding [item=32726] in a [item=32724], you\'ll begin to reveal what\'s truly happening with the Murkblood in the mine.[/li][li][quest=11054] - [npc=23291] will have you fashion your very own [item=32680] for use in keeping the Dragonmaw peons in line and working at full efficiency.[/li][li][i][small](Daily)[/small][/i] [quest=11076] - The [npc=23149] will ask that you venture into the Netherwing mines and recover the cargo contained in mine carts randomly strewn among the interior of the mine.[/li][li][i][small](Daily)[/small][/i] [npc=23376] - One of the [npc=23376] will inform you that the creatures deeper in the mine are halting production and ask you to thin their numbers.[/li][li][i][small](Daily)[/small][/i] [quest=11055] - This humorous quest starts at Chief Overseer Mudlump after you bring him the required materials. You\'ll be able to fly around Netherwing Ledge and toss the Booterang at any [npc=23311] that can be found anywhere around the crystals of the ledge.[/li][/ul][/div][pad][b][toggler id=Honored hidden]Honored[/toggler][/b]\n[div id=Honored hidden]Mor\'ghor will award you with your new [item=32695], which is now usable anywhere as long as you\'re outside.[ul][li][quest=11063] - This six-part questline will have you in-flight following the other Dragonmaw masters of flight. They will all attempt to knock you off your mount with cleverly-placed air attacks, you must stay within vision range and on your mount until they land or you will fail and need to restart the quest. After defeating the last of the six riders, you\'ll be awarded a [item=32863], which functions exactly like a [item=25653]. The effects of the two trinkets do [b]not[/b] stack.[/li][li][quest=11089] - [npc=23427] will request a set of materials to fashion a special device to destroy his brother and hinder the Legion\'s advances from the Twilight Portal in western [zone=3518].[/li][li][i][small](Daily)[/small][/i] [quest=11086] - Mor\'ghor will send you to the Twilight Portal in Nagrand to kill 20 [url=?npcs&filter=na=deathshadow+-imp+-hound+-agent]Deathshadow Agents[/url]. Beware the overlords, they patrol most of the area and can pack quite a punch.[/li][/ul][/div][pad][b][toggler id=Revered hidden]Revered[/toggler][/b]\n[div id=Revered hidden]Mor\'ghor will award your final trinket upgrade, the [item=32864] after reaching revered.[ul][li]Kill Them All! ([quest=11094]/[quest=11099]) - Mor\'ghor will order you to begin the attack against your chosen faction\'s base of operations in Shadowmoon Valley. Obviously you\'re not going to actually allow the Dragonmaw to attack your allies, so report to the proper leader and unlock your final daily quest for Dragonmaw...[/li][li][i][small](Daily)[/small][/i] The Deadliest Trap Ever Laid ([quest=11097]/[quest=11101]) - Waves of Dragonmaw Skybreakers will attack after preparations are made. Bring allies, as this is a battle of attrition.[/li][/ul][/div][pad][b][toggler id=Exalted hidden]Exalted[/toggler][/b]\n[div id=Exalted hidden]After many days of work, finally the denouement of the Netherwing/Dragonmaw questline. Taskmaster Varkule will direct you to Mor\'ghor one last time, who will inform you that you will be promoted by [npc=22917] himself. Without spoiling the events that ensue, you will end up in Shattrath with your selection of Netherdrake epic mounts. You may choose one here for free, and if you decide on a different color later, you can speak with [npc=23489] back in the Dragonmaw Base Camp to buy another drake for 200 gold.[/div]',NULL),(8,1031,0,NULL,2,'The [b]Sha\'tari Skyguard[/b] are an air wing of the [faction=935] of [zone=3703], defending the capital from attackers in the hills as well as battling against the arakkoa of Terokk in the peaks of Skettis. The Skyguard has two outposts, one in the northern reaches of the Skethyl Mountains and one near [faction=1038]. Players start out at neutral standing with the Skyguard.\n\n[h3]Reputation[/h3]\n[b]Daily Quests[/b][ul][li][quest=11008] - [npc=23048] will grant you a pack of explosives to destroy the eggs that rest atop Skettis structures.[/li][li][quest=11085] - A [npc=23383] can be found atop certain structures, players will escort him out for reputation, gold, and a choice of either 2 [item=28100] or 2 [item=28101].[/li][li][quest=11065] - [npc=23335] will inform you that the Skyguard\'s bombing runs have taken a toll on their mounts and ask you to gather some more Aether Rays to supplement their scout force.[/li][li][quest=11010] - [npc=23120] asks you to destroy the ammo for the Legion\'s flak cannons so the Skyguard Scouts can continue their job.[/li][li][quest=11004] - After collecting 6 [item=32388], [npc=23042] will make a potion that will allow vision of the more powerful arakkoa, such as [npc=23066].\n[i][small]Note: World of Shadows is not a daily quest, but may be repeated as many times as necessary.[/small][/i][/li][/ul][b]Creatures[/b][ul][li][npc=21804] - 5 reputation, up to the end of Revered.[/li][li][url=?npcs&filter=na=skettis+-kaliri+-assassin;minle=70]All Skettis Arakkoa[/url] - 10 reputation, regardless of Skyguard standing.[/li][li][npc=23029] - 30 reputation, regardless of Skyguard standing.[/li][/ul]',NULL),(8,1038,0,NULL,2,'The [b]Ogri\'la[/b] are a faction of ogres in the [zone=3522], where their proximity to [item=32572] has allowed them to evolve past their brutish nature. They are currently fighting a war against both the Black Dragonflight and the Burning Legion, who seek the Apexis Crystals for their own purposes.\n\n[h3]Location[/h3]\nOgri\'la is situated near the western edge of Blade\'s Edge Mountains, between Forge Camp: Terror and Forge Camp: Wrath, just west of Sylvanaar. Ogri\'la is only accessible by flying mount/form. Another alternative is to have a reputation of honored or higher with [faction=1031]. But a player must have a flying mount to reach the Skyguard camp near Skettis.[pad]\n\n[h3]Reputation[/h3]\nReputation with Ogri\'la can only be gained via Quests, and there only repeatable quests are the available [url=?quests&filter=da=ja;cr=1;crs=1038;crv=0]daily quests[/url]. Thus, there is a cap on how much reputation a day a player can gain reputation with Ogri\'la, making it an \"ungrindable\" reputation.\n\n[b]Apexis Shards[/b]\n[item=32569] can be collected in a variety of ways. They can be looted from mobs, gathered from the environment, or they can be rewards from completed quests.[pad][b]Apexis Crystals[/b]\n[item=32572] are dropped from elite demons and dragons in Blade\'s Edge Mountains. In order to summon these mobs, 35 Apexis Shards are needed, and it is recommended that you have a 5 man group to defeat them.\n\n[b]Quests[/b]\nThere are a [url=?quests&filter=cr=1;crs=1038;crv=0]number of quests[/url] that a player can to do earn reputation with the Ogri\'la, as well as several [url=?quests&filter=da=ja;cr=1;crs=1038;crv=0]daily quests[/url]. Many of the daily quests will also grant reputation with the Sha\'tari Skyguard when they are first completed. \n\nIn order to access the main quests at Ogri\'la itself, a player must first complete the 5 group quests from [npc=22941].\n\n[h3]Depleted Items[/h3]\nA number of \"depleted\" items will sometimes drop from mobs. When combined with 50 Apexis Shards, the items [url=?search=Apexis+Crystal+Infusion]upgrade[/url], gaining stats and gem slots. Once the items are upgraded they become Bind on Equip, and can therefore be sold or traded to other players. One thing to note, however, is that although the depleted items may also have stats or effects, they cannot be equipped.',NULL),(NULL,NULL,0,'sound&playlist',2,'Here you can set up a playlist of sounds and music. \n\nJust click the \"Add\" button near an audio control, then return to this page to listen to the list you\'ve created.',NULL),(14,11,2,NULL,2,'[b]Aperçu :[/b] Les [b]Draenei[/b] sont des adeptes de Naaru et adorateurs de la Lumière Sainte. Chassées d’Argus, leur monde natal, les honorables Draeneï durent fuir des siècles durant Sargeras et sa Légion Ardente, après qu’il ait essayé de les corrompre. Les Draeneï ont alors trouvé une lointaine planète où s’établir. Ils appelèrent Draenor ce monde qu’ils partageaient avec les Orcs chamaniques. Une période de paix s’est alors installée.\nLa Légion Ardente fini par retrouver les DraeneÏ et corrompt les Orcs grâce à Guldan. Les Orcs partirent en guerre et exterminèrent les paisibles Draeneï. De rares survivants purent s’enfuir en Azeroth pour chercher de l’aide dans leur combat contre la Légion Ardente.\n\n[b]Capitale :[/b] Les Draeneï ont le siège de leur pouvoir dans les ruines de leur vaisseau : [zone=3557].\n\n[b]Zone de départ :[/b] [zone=3524] et [zone=3525] couvrent les tentatives des Draeneï de s’installer sur leurs nouvelles îles et de faire face à la corruption présente.\n\n[b]Montures :[/b] [npc=17584] vend des variétés d’Elekks, ainsi que [npc=33657] au tournoi d’Argent.',NULL),(14,8,2,NULL,2,'[b]Aperçu :[/b] Les [b]Trolls[/b] Sombrelance vécurent à l\'origine dans les îles Brisées mais furent envahis par les nagas et les murlocs. Chassés de chez eux, la [url=?faction=530]tribu de Sombrelance[/url] se lie finalement d\'amitié avec les orcs qui ont sauvés les Trolls de la destruction. [npc=4949] leur offre l\'amnistie parmi la Horde, en contrepartie, la tribu Sombrelance jura fidélité au chef de guerre orque.\nBien qu\'ils refusent d\'abandonner leur sombre héritage, les féroces Trolls Sombrelance occupent une place d\'honneur au sein de la Horde.\n\n[b]Capitale :[/b] Les Trolls Sombrelance vivent maintenant dans la capitale de la Horde : [zone=1637].\n\n[b]Zone de départ :[/b] Les Trolls commencent leurs quêtes en [zone=14]\n\n[b]Montures :[/b] [npc=7952] au village de Sen\'jin vend de nombreux raptors ; [npc=33554], au tournoi d\'Argent, vend quelques modèles distincts.',NULL),(14,10,2,NULL,2,'[b]Aperçu :[/b] Les [b]Hauts-Elfes[/b], race fière et hautaine, fondèrent jadis Quel’Thalas où ils créèrent une fontaine magique appelée Puits de Soleil. Ils profitèrent de sa puissance mais devinrent peu à peu dépendants de la magie. Si celle-ci devait être enlevée, les Hauts-Elfes soufreraient horriblement. Ils se séparèrent donc du reste de la société elfique.\nDe nombreux siècles plus tard, le fléau mort-vivant détruisit le Puit de Soleil et tua la plupart des Hauts-Elfes. Les survivants de l’assaut d’Arthas sur Lune-d’Argent, qui ont alors pris le nom d’Elfes de Sang, rebâtissent Quel’Thalas et cherchent de nouvelles sources de magie pour calmer leur douloureux manque.\nLes Elfes de Sang rejoignent la Horde à Burning Crusade.\n\n[b]Capitale :[/b] Les Elfes de Sang ont reconstruit [zone=3487].\n\n[b]Zone de départ :[/b] Les Elfes de Sang commencent au [zone=3430].\n\n[b]Montures :[/b] [npc=16264], aux Bois des Chants Eternelles, vend de nombreux faucons pèlerins ; [npc=33557], au tournoi d’Argent, vend quelques modèles uniques.',NULL),(14,7,2,NULL,2,'[b]Aperçu :[/b] Les [b]Gnomes[/b], race excentrique, sont obsédés par les gadgets et la technologie. Malgré leur petite taille, ils ont mis à profit leur grande intelligence pour s\'assurer une place dans l\'Histoire.\nA l\'origine, les Gnomes viennent de la ville de [zone=721], qui était autrefois une merveille technologique mue à la vapeur. Malheureusement, la ville a été détruite par [npc=7937] à la suite d\'une tentative pour sauver la ville d\'une armée massive de Troggs.\nSes bâtisseurs sont désormais des vagabonds qui errent sur les terres des nains, venant en aide à leurs alliés du mieux qu\'il le peuvent.\n\n[b]Capitale :[/b] Aujourd\'hui, les Gnomes font leurs maisons à [zone=1537] malgré les efforts fournis pour reprendre leur bien aimée ancienne ville avec l\'[achievement=4786].\n\n[b]Zone de départ :[/b] Les Gnomes commencent à [zone=1], mais ont une séquence de quêtes très différente des Nains, couvrant Gnomeregan\n\n[b]Montures :[/b] [npc=7955] à Dun Morogh vend de nombreux mécanotrotteurs, ainsi que [npc=33650] au tournoi d\'Argent.',NULL),(14,6,2,NULL,2,'[b]Aperçu :[/b] Les [b]Taurens[/b], race aux racines chamaniques profondes, sont des résidents de longue date de Kalimdor. Ils vouent un amour profond et durable à la nature, la grande majorité d’entre eux adorent une divinité connue sous le nom de la Terre Mère.\nRécemment attaqués par des centaures, les Taurens auraient été exterminés s’ils n’avaient pas rencontré, par hasard, les Orcs qui les aidèrent à repousser leurs ennemis. Afin d’honorer cette dette de sang, les Taurens ont rejoint la Horde, renforçant ainsi l’amitié entre les deux races.\n\n[b]Capitale :[/b] [zone=1638] est le lieu de résidence des Taurens\n\n[b]Zone de départ :[/b] Les Taurens commencent leurs quêtes en [zone=215].\n\n[b]Montures :[/b] [npc=3685] vend de nombreux kodos ; [npc=33556], au tournoi d’Argent, vend quelques modèles distinctifs.',NULL),(14,5,2,NULL,2,'[b]Aperçu :[/b] Les [b]Réprouvés[/b], résultat d’une première attaque du Fléau en Azeroth, sont une métamorphose d’un certain nombre de membres de l’Alliance en mort vivant. Quand les forces combinées des Orcs, des Elfes, des Trolls, des Nains et des Humains se mirent à se défendre, [npc=36597] se mit à affaiblir ses armées en perdant le contrôle de certaines. Libérés de l’emprise du Roi Liche ainsi que des émotions gênantes et des liens de leurs vies humaines, les Réprouvés, menés par la banshee Sylvanas, réclament vengeance contre le fléau.\nLes Humain sont également devenus des ennemis, impitoyables dans leur désir de purger les terres de tous les mort-vivants. \nLes Réprouvés ne se soucient que très peu de leurs alliés. La Horde ne représente à leurs yeux qu’un simple outil qui pourrait servir leurs sombres desseins.\n\n[b]Capitale :[/b] Les Réprouvés résident sous les ruines de l’ancienne ville humaine de Lordaeron : la [zone=1497].\n\n[b]Zone de départ :[/b] Tous les joueurs de Réprouvés commencent dans la [zone=85]. Ils sont élevés par les Val’kyrs comme des réprouvés de seconde génération\n\n[b]Montures :[/b] [npc=4731] vend de nombreux chevaux mort-vivants ; [npc=33555], au tournoi d’Argent, vend quelques modèles distincts.',NULL),(14,4,2,NULL,2,'[b]Aperçu :[/b] Les [b]Elfes de la nuit[/b], race ancienne et mystérieuse, vivaient à Kalimdor pendant des milliers d\'années, ils fondèrent un vaste empire, mais leur usage imprudent de la magie les conduisit à leur perte. Pétris de douleur, ils se retirèrent dans les forêts et demeurèrent ainsi isolés jusqu\'au retour de leur ancien ennemi. Ne disposant d\'aucune alternative, les Elfes de la nuit furent contraints de sacrifié l\'arbre monde afin d\'arrêter l\'avancé de la Légion Ardente. \nIls émergèrent de leur isolement, afin de défendre leur place dans le nouveau monde.\n\n[b]Capitale :[/b] La capitale des Elfes de la nuit est [zone=1657], située dans les branches de l\'arbre monde.\n\n[b]Zone de départ :[/b] Les Elfes de la nuit commencent à [zone=141]\n\n[b]Montures :[/b] [npc=4730], à Darnassus, vent une variété de sabre de nuit, ainsi que [npc=33653] au tournoi d\'Argent.',NULL),(14,3,2,NULL,2,'[b]Aperçu :[/b] Les [b]Nains[/b], race robuste, viennent de Khaz Modan dans les Royaumes de l’Est. Par la passé, les Nains ne s’intéressaient qu’aux richesses extraites des profondeurs de la terre. Lorsque des études semblèrent indiquer que les Nains étaient les descendants d’une race proche des Titans qui leur aurait conféré un héritage enchanté, la curiosité des Nains fut piquée au vif. Décidés à en savoir plus, les Nains commencèrent à rechercher des artefacts perdus et des connaissances disparues. Aujourd’hui, les Nains dirigent des fouilles archéologiques partout dans le monde.\nTrois principaux Clans de Nains sont répartis dans tout Azeroth : Les Barbes de Bronze, Les Marteaux Hardis et les Sombrefers.\n\n[b]Capitale :[/b] Les Nains font leur maison dans leur siège ancestral de [zone=1537].\n\n[b]Zone de départ :[/b] Les Nains commencent à [zone=1].\n\n[b]Montures :[/b] [npc=1261] vend des béliers à la ferme des Amberstill, ainsi que [npc=33310] au tournoi d’Argent.',NULL),(14,1,2,NULL,2,'[b]Aperçu :[/b] Les [b]Humains[/b], race la plus jeune et la plus peuplés d\'Azeroth, maîtrisent les arts du combat, l\'artisanat et la magie avec une efficacité stupéfiante. La valeur et l\'optimisme des Humains les ont conduits à bâtir certains des plus grands royaumes du monde. En cette ère de troubles, après des générations de conflit, l\'Humanité aspire à ranimer sa gloire passée et à se forger un nouvel avenir rayonnant.\nLes Humains, aux talents très variés, sont devenus les chefs de l\'Alliance grâce à leurs ambitions et leurs résiliences. \n \n[b]Capitale :[/b] Le siège du pouvoir Humain est dans la ville reconstruite de [zone=1519].\n \n[b]Zone de départ :[/b] Les Humains commencent leurs quêtes dans la [zone=12].\n \n[b]Montures :[/b] [npc=384] vend des palefrois dans Hurlevent, et [npc=33307], au tournoi d’Argent, vend quelques modèles distincts.',NULL),(14,2,2,NULL,2,'[b]Aperçu :[/b] Les [b]Orcs[/b] étaient, à l\'origine, un peuple pacifique aux croyances chamaniques résidant sur le monde de Draenor. Malheureusement, infectés par le sang démoniaque de Mannoroth le destructeur, les Orcs furent réduit en esclavage par la Légion Ardente, contraint de guerroyer contre les Draenei et de conquérir Azeroth. \nAprès de nombreuse années de joug, les Orcs ont réussi à se libérer de l\'emprise démoniaque et ont conquis leur liberté, pour revenir à leurs racines chamaniques.\nMaintenant, sous la direction de leur nouveau chef de guerre, les Orcs se construisent un nouveau foyer, où ils combattent pour l\'honneur, dans un monde étranger, haïs et calomniés.\n\n[b]Capitale :[/b] Les Orcs résident maintenant dans la ville d\'[zone=1637], du nom du défunt Orgrim Doomhammer, ancien chef de guerre de la Horde.\n\n[b]Zone de départ :[/b] Les Orcs commencent leurs quêtes en [zone=14].\n\n[b]Montures :[/b] [npc=3362], à Orgrimmar, vend une variété de loups ; [npc=33553], au tournoi d\'Argent, vend quelques montures distinctives',NULL),(NULL,NULL,0,'reputation',2,'[b]Reputation[/b] is a rough measurement of how much you participate in the community--it is earned by convincing your peers that you know what you’re talking about. Our community puts just as much work as our developers do into making our site as awesome as it is and reputation is meant as a way for you to track just how much work you\'re putting into us.\r\n\r\nThe primary means of gaining reputation is by posting quality comments on database entries (which are then voted up by other site members) and by general contributions to the site which can include actions like data and screenshot submissions. Whenever you leave a comment on a database entry, your peers can then vote on these comments, and those votes will cause you to gain reputation. You can also earn reputation by voting on other users\' comments and by sending in reports!\r\n\r\nBy being a good-standing and contributing user you will be able to earn both reputation and achievements for many of the same actions!\r\n\r\n[h3]Reputation Gains[/h3]\r\n[div style=\"max-width:400px\"][table class=grid]\r\n[tr][td][url=?account=signup]Registering[/url] an account[/td]\r\n[td align=right class=no-wrap]CFG_REP_REWARD_REGISTER reputation[/td]\r\n[/tr]\r\n[tr][td]Daily visit[/td]\r\n[td align=right class=no-wrap]CFG_REP_REWARD_DAILYVISIT reputation[/td]\r\n[/tr]\r\n[tr][td]Posting a comment[/td]\r\n[td align=right class=no-wrap]CFG_REP_REWARD_COMMENT reputation[/td]\r\n[/tr]\r\n[tr][td]Your comment was voted up (each upvote)[/td]\r\n[td align=right class=no-wrap]CFG_REP_REWARD_UPVOTED reputation[/td]\r\n[/tr]\r\n[tr][td]Submitting a screenshot[/td]\r\n[td align=right class=no-wrap]CFG_REP_REWARD_UPLOAD reputation[/td]\r\n[/tr]\r\n[tr][td]Submitting a guide (approved)[/td]\r\n[td align=right class=no-wrap]CFG_REP_REWARD_ARTICLE reputation[/td]\r\n[/tr]\r\n[tr][td]Filing a report (accepted)[/td]\r\n[td align=right class=no-wrap]CFG_REP_REWARD_GOOD_REPORT reputation[/td]\r\n[/tr]\r\n[/table][/div]\r\n\r\n\r\n[h3]Site Privileges[/h3]\r\nThe higher your reputation level, the more privileges you gain. Earn a high enough reputation to unlock additional rewards, in the form of new privileges around the site!\r\n[pad]\r\n[div style=\"max-width:400px\"][table class=grid]\r\n[tr][td]Post comments[/td]\r\n[td align=right class=no-wrap]CFG_REP_REQ_COMMENT reputation[/td]\r\n[/tr]\r\n[tr][td]Upvote on comments[/td]\r\n[td align=right class=no-wrap]CFG_REP_REQ_UPVOTE reputation[/td]\r\n[/tr]\r\n[tr][td]Downvote on comments[/td]\r\n[td align=right class=no-wrap]CFG_REP_REQ_DOWNVOTE reputation[/td]\r\n[/tr]\r\n[tr][td]More votes per day[/td]\r\n[td align=right class=no-wrap]CFG_REP_REQ_VOTEMORE_BASE reputation[/td]\r\n[/tr]\r\n[tr][td]Comment votes worth more[/td]\r\n[td align=right class=no-wrap]CFG_REP_REQ_SUPERVOTE reputation[/td]\r\n[/tr]\r\n[/table][/div]\r\n[pad]\r\n[url=?privileges]Check out full details on site privileges you can earn![/url]\r\n',NULL),(NULL,NULL,0,'privilege=1',2,'[h3]Reputation required for posting comments?[/h3]\nThe very first privilege you can earn is the ability to post comments. Because this privilege requires only CFG_REP_REQ_COMMENT reputation, it is earned soon upon registering an account (which awards CFG_REP_REWARD_REGISTER reputation)! Keep this in mind if you\'ve recently registered to post on a contest thread.\n\n[h3]How do I post a comment?[/h3]\nOnce you have earned the ability to post comments, it\'s easy to do! Got some interesting information about an item? Strategies for earning an achievement or killing a boss? These are just a few examples of what could make a quality comment here!\n\nSimply visit any database page that you wish to leave a comment on and scroll down to the \'Contribute\' section. In the \'Add your comment\' tab, you can easily write and format your database comment. You can use our handy formatting buttons to improve the visual quality of your post, and easily add database links using the \'Links\' menu and entering database entry IDs. Once you\'re done, simply click the \'Submit\' button below and voila!\n\n[h3]Comment rating and you![/h3]\nAll comments made on database pages are subject to our rating system. This allows users who have reached the appropriate reputation level to upvote and downvote comments based on their quality. Making quality comments will earn you website reputation each time it has been upvoted, but make a poor quality comment and you may end up losing reputation if it is downvoted!\n\nFor more information on commenting, be sure to check out our handy [url=?help=commenting-and-you]Commenting and You[/url] guide in the website help section!',NULL),(NULL,NULL,0,'privilege=2',2,'[h3]Posting External Links[/h3]\nOne of the first privileges allowed to users is the ability to post external links on the site. This will allow you to link to relevant information found on other websites from our database as well as in our forums. You can also add a link to your user profile, such as to your guild website or personal blog. Users without the appropriate reputation level will have their links filtered automatically, to help prevent spammers and malicious links from being posted on our website.\n\n[h3]Posting Policy[/h3]\nPlease be aware that some URLs may still be filtered out by our moderation team, as they made be deemed inappropriate or advertising. If you are uncertain whether or not a link will be considered advertisement, please do not hesitate to contact our Feedback team with any questions!\n',NULL),(NULL,NULL,0,'privilege=4',2,'[h3]No CAPTCHAs[/h3]\nAh, CAPTCHAS. Love \'em or hate \'em, they\'re often a necessary evil for popular websites which allow any sort of user contribution. Here, we use [url=https://www.google.com/recaptcha/intro/index.html]ReCAPTCHA[/url] which helps thwart bots and spammers from abusing our forum and comment systems. Unfortunately, this also creates a minor inconvenience for our more active users, who are still occasionally asked to input a CAPTCHA despite long since establishing themselves as a legitimate member of the community. Well, not anymore! Users who reach the appropriate reputation level will no longer have to enter CAPTCHAs anywhere on the site!\n',NULL),(NULL,NULL,0,'privilege=5',2,'[h3]Comment rating value increase[/h3]\nWhen you have reached a higher reputation level, your contributions to the site will raise in value! As a more trusted member of our community, your comment ratings will now have an increased weight and, as a result, have a greater effect on the total rating of a comment! Your vote contribution are doubled, so each of upvote will count as two votes (and each of your downvotes as two, as well)! This will allow higher reputation users to have more of an effect on considering quality of a comment, raising quality comments higher and lowering poor comments faster.\n',NULL),(NULL,NULL,0,'privilege=9',2,'[h3]More votes per day[/h3]\nWe have a daily cap for comment votes set to CFG_USER_MAX_VOTES.\n\nThis privilege instantly increases the cap by 1, and then increases the cap by an additional 1 point for each CFG_REP_REQ_VOTEMORE_ADD reputation you have above CFG_REP_REQ_VOTEMORE_BASE.\n',NULL),(NULL,NULL,0,'privilege=10',2,'[h3]Upvoting Comments[/h3]\nDid you find a comment particularly insightful or laugh out loud funny? Upvote it then! Upvoting is a way of giving props to those who truly contribute. From small guides to witty jokes, if a comment has enhanced your user experience, you should remember to upvote it.\n\nThe higher amount of upvotes a comment has, the higher up on the page it is. This way the community can help determine what comments are worth reading by sending some upvotes their way.\n\n[h3]Upvoting Policy[/h3]\nYou should not use upvotes to reward your friends or withhold upvotes to punish users you dislike. These are bannable offenses and you will probably lose your ability to upvote if we catch you doing it.\n',NULL),(NULL,NULL,0,'privilege=11',2,'[h3]Downvoting Comments[/h3]\nDid you find a comment that was out of date, irrelevant, or otherwise less than useful? Downvote it then! Downvoting is a way of removing the clutter from the database and ensuring our comments are up to date. Downvotes remove an upvote--and if a comment has too many downvotes, it can even become a negative comment which appear at the end of an article rather than the beginning. \n\n[h3]Downvoting Policy[/h3]\nYou should not use downvotes to punish users you dislike nor should you downvote in quick succession. Try to use downvotes only to help us out, leaving personal bias out of it. If you abuse downvotes either by making too many in a short time frame or targeting a specific user, you may be warned and in some cases banned.\n',NULL),(NULL,NULL,0,'privilege=12',2,'[h3]Replying to a Comment[/h3]\nYou can reply to comments easily and quickly with the new commenting system. All you have to do is leave a reply on an existing comment for this to work.\n\nA reply is best used to illustrate alternatives to a comment, highlight its accuracy, or expand on a joke. For example, if someone says an item drops from a certain boss but you know it does not, you could reply to explain it doesn\'t; it\'s likely people will find your comment helpful so they don\'t waste time trying to get the item from that NPC.\n\nPlease be aware that you should not use comments like forum threads for discussion.\n',NULL),(NULL,NULL,0,'privilege=13',2,'Your avatar on the [url=CFG_BOARD_URL]Forums[/url] has an uncommon-quality green border.',NULL),(NULL,NULL,0,'privilege=14',2,'Your avatar on the [url=CFG_BOARD_URL]Forums[/url] has a rare-quality blue border.',NULL),(NULL,NULL,0,'privilege=15',2,'Your avatar on the [url=CFG_BOARD_URL]Forums[/url] has an epic-quality purple border.',NULL),(NULL,NULL,0,'privilege=16',2,'Your avatar on the [url=CFG_BOARD_URL]Forums[/url] has a legendary-quality orange border.',NULL),(NULL,NULL,0,'privilege=17',2,'[img src=STATIC_URL/images/premium/user-badge.png border=0 float=right]Unlock [url=HOST_URL/?premium]AoWoW Premium[/url] status for free.\n\nAs a Premium user, you can access a variety of perks:\n[ul]\n[li]Images in tooltips[/li]\n[li]Additional avatar borders[/li]\n[li]And much more![/li][/ul]\n\n',NULL),(13,1,2,NULL,2,'[b][color=c1]Les Guerriers[/color][/b] sont une classe très puissante, avec la capacité de taner ou d\'infliger des dégâts de mêlée. Sa caractéristique principale est la force, mais les tanks s\'intéresseront également à l\'Endurance.\n\nCe combattant se bat avec une posture ce qui lui permet l\'accès à différentes capacités et lui accorde des bonus. Il utilisera [spell=71] pour tanker (appris au niveau 10) et [spell=2457] (appris au niveau 1) ou [spell=2458] (appris au niveau 30) pour les dégâts en mêlée.\n\nL\'arbre de protection du Guerrier contient de nombreux talents pour améliorer leur survie et générer des menaces contre les monstres. Les Guerriers de protection sont l\'une des principales classes de tank du jeu. Pour aller au combat, ils peuvent utiliser [spell=100] ou [spell=20252] mais seul le Guerrier protection peut protéger un allié en utilisant [spell=3411].\nIls ont également deux arbres de talent orientés sur les dégâts [icon name=ability_rogue_eviscerate][url=spells=7.1.26]Armes[/url][/icon] et [icon name=ability_warrior_innerrage][url=spells=7.1.256]Fury[/url][/icon], ce dernier comprend le talent [spell=46917], qui permet au Guerrier de manier deux armes à deux mains. Les Guerriers sont capable de faire de gros dégâts de zone avec des sorts tels que [spell=845], [spell=1680] et [spell=46924]. \n\nLe Guerrier porte une armure en plaques et aspire à la perfection dans les combats. Lorsqu\'il inflige ou subit des dégâts, il génère de la rage, utilisée pour alimenter ses attaques spéciales.\n[ul]\n[li] Allié utile, qui peut ajouter des buffs au groupe ou raid avec [spell=6673] et [spell=469], mais seul les Guerriers Fury peuvent fournir un buff passif [spell=29801] qui augmente les coups critiques en mêlée et à distance.[/li]\n[li] L\'avantages uniques des Guerriers, ce sont les 3 postures de combats.[/li]\n[li] Il peut choisir de se spécialiser dans le port d’armes à deux mains, d\'arme à une main, ou dans l\'utilisation du bouclier en plus d\'une arme à une main.[/li]\n[li] Et dispose de plusieurs techniques qui permettent de se déplacer rapidement sur le champ de bataille.[/li]\n[/ul]',NULL),(13,2,2,NULL,2,'[b][color=c2]Les Paladins[/color][/b] sont des combattants qui utilisent la magie du sacré pour soigner les blessures et combattre le mal. Ils sont relativement autonomes et disposent de nombreuses techniques destinées à empêcher les morts. Le paladin peut choisir de se battre, de protégés ou de soigner, il utilisera le mana pour combattre le mal. Ses caractéristiques principales dépendent du rôle choisi.\n\nIl est un mélange d’un combattant en mêlée et d’un lanceur de sorts secondaires. Allié indispensable dans un combat, il renforce leurs amis avec de saintes auras (une aura active par paladin sur chaque membre du raid) et des bénédictions spécifiques pour les protéger du mal et renforcer leurs pouvoirs.\n\nPortant de lourdes armures, ils peuvent résister à des coups terribles dans les batailles les plus dures tout en guérissant leurs alliés blessés et en ressuscitant les morts. Au combat, ils peuvent utiliser des armes à deux mains, paralyser leurs ennemis, détruire des morts vivants et des démons, et les juger avec une sainte vengeance.\nLes paladins sont une classe défensive, principalement conçus pour survivre à leurs adversaires, grâce à leur assortiment de capacités défensives. Ils font aussi d’excellents tanks en utilisant leurs capacités [spell=25780].\n\n[ul]\n[li] Classe pouvant guérir, tanker avec leur précieux bouclier et infliger des dégâts en mêlée.[/li]\n[li] Renforce les alliées avec les [url=spells=7.2&filter=na=aura]Auras[/url], les [url=spells=7.2&filter=na=bénédiction]bénédictions[/url] et d’autres buffs.[/li]\n[li] Seule classe avec un véritable sort d’invulnérabilité [spell=642].[/li]\n[/ul]\n\n[b]Montures de classe :[/b]\n[ul]\n[li] [spell=13819] est un destrier royal que seuls les plus fervents des paladins peuvent appeler à leur service. Niveau 20 - Bonus de Vitesse de 60%. [/li]\n[li] [spell=23214] est un équipier infatigable capable d\'amener son valeureux maître dans tout Azeroth. Niveau 40 - Bonus de vitesse de 100%. [/li]\n[/ul]',NULL),(13,4,2,NULL,2,'[b][color=c4]Les Voleurs[/color][/b] sont une classe de mêlée capable d\'infliger de grandes quantités de dégâts à leurs ennemis avec des attaques rapides en utilisant de l\'énergie comme ressources. Leurs caractéristiques principales sont la puissance d\'attaque et l\'agilité.\n\nLes Voleurs ont un puissant arsenal de compétences, dont beaucoup sont renforcés par leur capacité de furtivité et d\'étourdissement de leurs victimes. Capables d\'utiliser des poisons, ils paralysent leurs adversaires, les affaiblissant massivement dans la bataille. Avec l\'ambidextrie, ils peuvent utiliser une large gamme d\'armes, mais les Voleurs privilégient la dague, qui est la plus représentative de cette classe. \n\nCe sont les maîtres pour se déplacer furtivement autour de leurs ennemis, frapper dans l\'ombre un adversaire pour tenter de l\'achever rapidement puis s\'échapper du combat en un clin d’œil. \nIls endossent donc souvent le rôle d\'assassin ou d\'éclaireur, mais nombre d\'entre eux sont des loups solitaires.\n\n[ul]\n[li] Porte des armures en cuir.[/li]\n[li] Porte une arme dans chaque main.[/li]\n[li] Utilise une grand variété d\'armes de mêlée, comme les poignards, les armes de pugilats, les masses à une main, les épées à une main et les haches à une main.[/li]\n[li] Recouvre leurs armes avec du [url=items=0.-3&filter=na=poison;ub=4]poison[/url] pour gravement affaiblir leurs ennemis.[/li]\n[li] Utilise le [spell=1784] pour n’être visible que par les ennemis les plus perspicaces.[/li]\n[li] Cumule 5 points de combo pour infliger de puissants coups de grâce.[/li]\n[/ul]',NULL),(13,3,2,NULL,2,'[b][color=c3]Les Chasseurs[/color][/b] sont une classe très unique dans le monde de World of Warcraft. C\'est la seule classe non-magique qui fait des dégâts à distance. Ils se battent avec des arcs, des armes à feu ou des arbalètes. Leurs caractéristiques principales sont la puissance d\'attaque et l\'agilité.\n\nLes Chasseurs se sentent chez eux dans la natures et ont une affinité spéciale avec les animaux. Il sait apprivoiser son propre [url=pets]familier[/url] qui l\'aidera à vaincre son ennemi. L\'animal du chasseur est unique, il possède un arbre de talent où le Chasseur peut attribuer des points dans des compétences diverses et des capacités passives. Chaques espèces de familier a une capacité spéciale unique. Le Chasseur peut rechercher les bêtes les plus appréciables en fonction de leurs apparences ou capacités. Seuls certains familiers ne sont accessibles que si le Chasseur choisi dans son arbre de talent [icon name=ability_hunter_beasttaming][url=spells=7.3.50]Maîtrise des bêtes[/url][/icon] qui lui donne accès aux bêtes « exotique » tels que [pet=46] ou [pet=39].\n\nPendant que leurs familiers attaques, les Chasseurs font pleuvoir leurs projectiles sur leurs malheureuses cibles. Ils préfèrent s’évader du corps-à-corps et ralentir leurs ennemis pour s\'éloigner et lancer leurs salves mortelles. Ils sont aussi capable de poser des pièges pour infliger des dégâts, ralentir ou rendre impossible toutes actions de leurs ennemis.\n\nLes Chasseurs portent des armures intermédiaires (cuir/maille) et utilisent le mana pour faire des dégâts.\n[ul]\n[li] Il peut voyager très vite en utilisant [spell=13161] et le partager avec [spell=13159].[/li]\n[li] Ils ont un certain nombre de compétence accès sur la survie qu\'ils peuvent utiliser pour échapper ou éviter un danger potentiel, comme [spell=5384] et [spell=781].[/li]\n[li] Les Chasseurs spécialisés dans la [icon name=ability_hunter_swiftstrike][url=spells=7.3.51]Survie[/url][/icon] peuvent avoir [spell=53292], ce qui leur permet de fournir aux membres du raid le [spell=57669].[/li]\n[/ul]',NULL),(13,5,2,NULL,2,'[b][color=c5]Les Prêtres[/color][/b] sont généralement considérés comme l\'une des classes de soins les plus répandus dans World of Warcraft, car ils ont deux arbres de talents qui peuvent être utilisés pour guérir très efficacement. Les caractéristiques principales sont la puissance des sorts, l\'intelligence et l\'Esprit (s\'il s\'est spécialisé dans les soins).\n\nL\'arbre [icon name=spell_holy_holybolt][url=spells=7.5.56]Sacré[/url][/icon] comprend des talents qui renforcent fortement la guérison faite à leurs alliés, y compris des sorts qui peuvent être utilisés pour guérir plusieurs joueurs à la fois, comme [spell=48089]. \nL\'arbre de talent [icon name=spell_holy_wordfortitude][url=spells=7.5.613]Discipline[/url][/icon] se concentre principalement sur l\'absorption et l\'atténuation des dommages grâce à l\'utilisation de [spell=48066] et réduit les dégâts subis avec [spell=63944].\n\nLes Prêtres disposent d\'une grande palette d\'outils pour soigner, mais ils peuvent également sacrifier leurs soins pour infliger des dégâts grâce à la magie de l\'[icon name=spell_shadow_shadowwordpain][url=spells=7.5.78]Ombre[/url][/icon]. Ils sont alors capables d\'infliger des dégâts importants avec leurs capacités uniques et une fois qu\'ils se mettent en [spell=15473], leurs dégâts d\'ombre augmentent de manière significative tout en perdant la capacité de lancer des sorts du sacré.\n\nIl porte une armure en tissus, soigne les dégâts grâce à la magie du sacré mais inflige des dégâts grâce à la magie de l\'Ombre. Il utilise le mana comme ressource.\n[ul]\n[li] Fournissant les buffs les plus appréciés dans le jeu - [spell=48161], qui donne un buff d\'endurance indispensable à tout raid. Ils peuvent utiliser [spell=48073] et [spell=48169].[/li]\n[li] Les prêtres d\'ombre sont très sollicités dans n\'importe quel raid , fournissant le buff [spell=57669] pour stimuler la régénération de mana et peut même guérir leur propre groupe avec [spell=15286].[/li]\n[/ul]',NULL),(13,8,2,NULL,2,'[b][color=c8]Les Mages[/color][/b] sont les utilisateurs emblématiques de la magie en Azeroth, qui apprennent leur art au cours de leurs recherches et études approfondies. Ils maîtrisent la magie du feu, du givre et des arcanes pour détruire ou neutraliser leurs ennemis. Leurs caractéristiques principales sont la puissance des sorts et l’intelligence.\n\nIls portent des armures légères, mais compensent cette faiblesse par une puissante gamme de sorts offensifs et défensifs. Le mage fait donc des gros dégâts à distance, envoyant des boules élémentaires sur un ennemi isolé mais faisant pleuvoir la destruction sur une armée. En cas d\'attaque, il peut échapper aux combats rapprochés avec [spell=1953] et devient un [spell=45438] quand cela devient trop critique.\n\nLes Mages peuvent également augmenter les pouvoirs de leurs alliés : [spell=23028], les inviter à leurs [spell=43987] et même les faire voyager à travers des [url=spells=7.8.237&filter=na=portail]portails[/url]. Classe indispensable pour voyager en toute tranquillité. Ils utilisent le mana comme ressource. Les Mages :\n[ul]\n[li]Transforment leurs ennemis en créatures inoffensives ou les geler sur place grâce à [spell=122].[/li]\n[li]Utilisent [item=50045] pour avoir un élémentaire d\'eau en familier.[/li]\n[/ul]',NULL),(13,6,2,NULL,2,'[b][color=c6]Les Chevaliers de la mort[/color][/b] sont d\'anciens agent du Fléau, désormais alliés avec la Horde ou l\'Alliance. Cette classe de héros débute le jeu à haut niveau (55). Ses caractéristiques principales sont la force, sans oublier l\'endurance pour les tanks.\n\nTous leurs arbres de talent peuvent être utilisés pour faire des dégâts ou tanker.\n\nLes Chevaliers de la mort qui ont une affinité avec le [icon name=spell_deathknight_bloodboil][url=spells=7.6.770]Sang[/url][/icon] ont une grande capacité d’auto-guérison et peuvent fournir à un allié : [spell=49016] qui l’enrage à la vue du sang du champ de bataille.\nL’arbre de talent [icon name=spell_frost_freezingbreath][url=spells=7.6.771]Givre[/url][/icon] permet une augmentation significative de l’armure et spécialise le Chevalier de la mort dans les dégâts de zone avec [spell=49184]\nLes maîtres des maladies et des invocations sont les chevaliers de la mort [icon name=spell_deathknight_armyofthedead][url=spells=7.6.772]Impie[/url][/icon]. Ils peuvent utiliser leurs talents [spell=52143] et [spell=49206] pour être aidé lors des combats. Ils ont aussi une plus grande résistance à la magie grâce à la [spell=51052].\n\nLe chevalier de la mort utilise des runes comme ressource principale, dont chacun des trois types est utilisé pour différentes techniques.\n[ul]\n[li] Ils se battent avec les présences (semblable aux positions d\'un Guerrier) qui fournit des bonus spéciaux à leurs rôles.[/li]\n[li] Il dispose de plus de capacités à distance que la plupart de classes de corps à corps et privilégie les maladies et les dégâts infligés par ses familiers morts-vivants.[/li]\n[li] La classe de chevalier de la mort a sa propre capacité d\'enchantement d\'arme spéciale appelée [spell=53428], ce qui remplace le besoin d\'enchantements d\'armes classiques.[/li]\n[li] Ont accès à une zone spéciale inscrite inaccessible par toutes les autres classe : Acherus, le fort d’ébène, situé dans [zone=4298]. Où ils gagneront leurs points de talent en tant que récompenses de quêtes dans les premières heures de jeux.[/li]\n[/ul]\n\n[b]Montures de classe :[/b]\n[ul]\n[li] [spell=48778] - Niveau 55 - Bonus de Vitesse de 100%. [/li]\n[li] [spell=54729] - Niveau 60 - Bonus de vitesse : s’adapte à la compétence de monte. [/li]\n[/ul]',NULL),(13,7,2,NULL,2,'[b][color=c7]Les Chamans[/color][/b], maîtres des éléments et de la nature, apportent un grand nombre de buffs à tout un groupe sous forme de totem. Un Chaman peut appeler un totem de chaque élément : terre, feu, eau et air. Ces totems apparaissent à leurs pieds et sont actifs pour toutes les personnes du raid se trouvant dans la zone d’effet du totem. Un bon Chaman sait quels totems sont à lancer et dans quelles circonstances les utiliser, pour maximiser les dégâts du groupe et la survie.\n\nIls sont principalement des lanceurs de sorts, bien qu’un Chaman [icon name=spell_nature_lightningshield][url=spells=7.7.373]Amélioration[/url][/icon] aime se rapprocher des ennemis pour faire de gros dégâts. Il apprend l’[spell=30798] et peut utiliser le sort [spell=51533] pour invoquer 2 Esprits de Loups qui combattent avec lui. Bien qu’il soit principalement de mêlée, le Chaman Amélioration peut bénéficier de la puissance des sorts et lancer instantanément [spell=403] ou des soins avec le talent [spell=51530]. \n\nLes Chamans [icon name=spell_nature_lightning][url=spells=7.7.375]Élémentaires[/url][/icon] se tiennent en retrait pour lancer leurs sorts de feu et de foudre et infliger de grandes quantités de dégâts. Ils peuvent repousser leurs ennemis avec [spell=51490] et aussi les enraciner avec [spell=51486]. Ils apportent le [icon name=spell_fire_totemofwrath][url=spell=57722]Totem de courrou[/url][/icon] et le [spell=51470], buffs très recherchés dans les raids.\n\nLes Chamans qui choisissent [icon name=spell_nature_magicimmunity][url=spells=7.7.374]Restauration[/url][/icon] ont un grand panel de sort de guérison se qui leurs permets de se spécialiser dans le soin mono-cible ou multi-cible. Ils sont reconnus pour leurs puissantes [spell=1064] et pour créer un [spell=16190] qui aide la restauration de mana aux membres de leurs groupes. Ils gagnent aussi un puissant [spell=974], peuvent employer [spell=51886] pour enlever les malédictions, et ont un sort de guérison instantané : [spell=61295] qui soigne aussi au fil du temps.\n\nLes Chamans invoquent la puissance des éléments pour améliorer les dégâts de leurs armes ou sorts. Ils portent des armures moyennes, boucliers et utilisent le mana comme ressources.\n[ul]\n[li] Il peut apprendre plus de 20 totems différents.[/li]\n[li] Peuvent lancer [spell=32182] (ou [spell=2825]) pour amplifier les dégâts et les soins de tout le raid. Un buff unique très recherché.[/li]\n[li] Un chaman peut se transformer en [spell=2645] à partir du niveau 16 et peut même le rendre instantané avec le talent [spell=16287]. Ce sort ne peut être utilisé qu\'en extérieur.[/li]\n[li] Il ne peut avoir qu\'un seul bouclier élémentaire d\'actif sur lui [spell=324] ou [spell=52127]. Le [spell=974], peut-être posé sur un autre joueur.[/li]\n[/ul]',NULL),(13,11,2,NULL,2,'[b][color=c11]Les Druides[/color][/b] sont la « classe à tout faire » de World of Warcraft, c\'est-à-dire, capable de remplir tous les rôles : soigner, faire des dégâts à distance, faire des dégâts de mêlée ou tanker, en utilisant le Changeforme. Le druide offre donc aux joueurs de nombreux styles de jeu. Ses caractéristiques principales dépendent du rôle choisi.\n\nSous sa forme normale, c’est un lanceur de sorts qui peut se battre à distance et se soigner. Mais il peut aussi prendre d’autres formes dont des formes animales :\n\nLorsqu’un druide se transforme en [spell=5487] (et à un niveau plus avancé, [spell=9634]), son mana se change alors en rage, capable de charger sa cible, de la [spell=8983] et de subir des coups de plusieurs adversaires simultanément. C’est une forme orientée vers le tanking qui fournit une armure et de la vie supplémentaire. Il peut esquiver les coups, utiliser [spell=22812] pour augmenter sa résistance.\nQuand il se transforme en [spell=768], son mana se change alors en énergie, pouvant [spell=5215] tout en se déplaçant, d’augmenter parfois ça vitesse de courses de 70% et de bondir derrière ces ennemis pour attaquer avec le talent [spell=49376]. C’est une forme orienté vers les dégâts de mêlée en faisant saigner leur cible avec [spell=49800] ou [spell=62078] lorsque le druide est entouré d’ennemis.\nAvec les talents de druide équilibre, la [spell=24858] est réputé pour faire beaucoup de dégâts à distance notamment avec les sorts [spell=5176] et [spell=48505] qui peuvent être augmenté avec des points de talent. Il émet aussi une aura, qui augmente les coups critiques des sorts, très appréciée en raid.\nSa forme d’[spell=33891] (talent restauration) est conçue pour soigner sur la durée notamment avec les sorts [spell=33763] et [spell=48438]. Il émet une aura, qui augmente les soins de 6%. Il a la particularité d’avoir une grande régénération de mana.\n\nD’autres formes animales secondaires complètent cette liste : sa [spell=783] qui permet au druide d’augmenter sa vitesse de déplacement, sa [spell=1066] qui lui permet de respirer sous l’eau tout en nageant plus vite et sa [spell=33943] (et avec la compétence [spell=34091], la [spell=40120]) lui permet de voler instantanément.\n\n[ul]\n[li] Dans l’arbre de talent Combat farouche, les druides ont une aura [spell=17007] très utile pour tout groupe de raid.[/li]\n[li] Le sort [spell=20484] est utilisable en combat, mais à une recharge de 10 min.[/li]\n[li] Il possède le sort [spell=29166] qui lui permet de régénérer le mana très vite même en combat, sur lui ou tout autre membre.[/li]\n[li] Les Druides ont leur propre capacité de téléportation qui leur permet de voyager vers [zone=493], ce qui est utile lorsqu’ils ont besoin de s’entraîner.[/li]',NULL),(13,9,2,NULL,2,'[b][color=c9]Les Démonistes[/color][/b], vêtue d’armure légère, sont les maîtres des arts démoniaques. Ils possèdent des capacités très puissantes qui, si elles sont utilisées correctement, en font un adversaire formidable. Utilisant leurs malédictions en combinaison avec des sorts de dégâts directs, il cause des ravages et la destruction. Ses caractéristiques principales sont la puissance des sorts et l’intelligence.\n\nLes Démonistes qui ont choisi de se spécialiser dans l’arbre de talent Affliction, excellent dans l’utilisation des malédictions, ils posent sur leurs ennemis [spell=47865] pour les affaiblir ou [spell=47864] pour leurs faire des dégâts. Ils ont la [spell=18271] ce qui augmente les dégâts des sorts d’ombre de 25%.\nLe démonologue appel des démons pour l’aider dans ces combats, il emploie principalement l’[spell=30146]. Il peut aussi se [spell=59672] en démon pour augmenter ses dégâts durant une courte période.\nLe Démonistes destruction utilise des sorts de feu tels que [spell=5740] ou [spell=17962] pour infliger d’importants dégâts directs.\n\nLes Démonistes, tout en étant d’excellent dans les dégâts à distance, soutiennent beaucoup leurs alliés en appelant d’autre joueur avec [spell=698] ou en utilisant des magies rituelles pour conjurer des pierres imbues du pouvoir de guérir : [icon name=inv_stone_04][url=item=5509]Pierre de soin[/url][/icon].\n\n[ul]\n[li] Le démoniste est doté du sort [spell=1454] qui lui permet de sacrifier des points de vie pour régénérer son mana.[/li]\n[li] Le [spell=48020] lui permet une grande mobilité en annulant tous les effets de déplacement, et en s\'éloignant du corps-à-corps.[/li]\n[li] En utilisant le sort [spell=20022], le démoniste permet à la personne sur qui elle a été appliqué de ressusciter.[/li]\n[/ul]\n\n[b]Montures de classe :[/b]\n[ul]\n[li] [spell=5784], leurs yeux ne brûlent plus que d\'une haine inextinguible pour les démonistes qui les ont corrompus - Niveau 20 - Bonus de Vitesse de 60%. [/li]\n[li] [spell=23161] sont des destriers recréés qui ont été corrompus par les énergies infernales, transpirant et soufflant le feu - Niveau 60 - Bonus de vitesse : 100%. [/li]\n[/ul]',NULL),(8,81,2,NULL,2,'[b]Les Pitons du Tonnerre[/b] est la faction de la capitale des Taurens : [zone=1638], située dans la partie nord de la région de [zone=215]. L\'ensemble de la ville est construit sur des falaises à plusieurs centaines de pieds au-dessus du paysage environnant, elle est accessible par des ascenseurs sur les côtés sud-ouest et nord-est.\n\n[h3]Histoire[/h3]\n\nLa grande ville de Pitons du Tonnerre se trouve au sommet d\'une série de mesas qui donnent sur les prairies verdoyantes de Mulgore. Les Taurens, autrefois nomade, ont récemment construit la ville pour dresser un centre de caravanes commerciales avec des artisans itinérants et des artisans de toutes sortes. Elle a été établi par le puissant chef [npc=3057] après que les Taurens, avec l\'aide des Orcs, ont chassé les centaures qui habitaient à l\'origine Mulgore. De longs ponts de corde et de bois font la liaison entre les mesas qui sont surmontées de tentes, de longues maisons, de totems peints aux couleurs vives et de huttes spirituelles. Le chef de Tauren surveille la ville animée, en veillant à ce que les tribus unies de Tauren vivent en paix et en sécurité.\n\n[h3]Réputation[/h3]\n\n[npc=14728] offre une quête répétitive où il faut fournir des étoffes. Une fois exalté aux Pitons du Tonnerre, en récompense, les joueurs peuvent acheter des [url=?items=15.5&filter=na=Kodo;cr=93:92;crs=2:1;crv=0:0]kodos[/url].',NULL),(8,1038,2,NULL,2,'[b]Ogri\'la[/b] est un groupe d\'Ogres localisé dans [zone=3522], où leur proximité avec [item=32572] leur a permis d\'évoluer au-delà de leur nature brutale. Ils sont particulièrement impliqué dans une guerre contre le Dragon noir et la Légion ardente, qui cherchent les cristaux Apogides pour leurs propres fins.\n\n[h3]Localisation[/h3]\nOgri\'la est situé près du bord ouest des Tranchantes, entre le Camp de Forge: Terreur et le Camp de Forge: Courroux, juste à l\'ouest de Sylvanaar. Ogri\'la est seulement accessible en monture volante ou en forme de vol. Une autre alternative est d\'avoir une réputation d\'honoré ou plus élevé avec [faction=1031]. Mais un joueur doit avoir une monture volante pour atteindre le camp Garde Ciel près de Skettis.[pad]\n\n[h3]Reputation[/h3]\nLa reputation avec Ogri\'la ne peut être acquise que par quêtes, et il n\'y a que des quêtes répétables dont les [url=?quests&filter=da=ja;cr=1;crs=1038;crv=0]quêtes journalières[/url]. Il ya un plafond sur la quantité de réputation que l\'on peut obtenir chaque jour pour un joueur avec Ogri\'la, ce qui en fait une réputation \"difficile à farmer\".\n\n[b]Eclats Apogides[/b]\n[item=32569] peuvent être collectées de diverses manières. Ils peuvent être pillés sur le cadavres de monstres, recueillis à partir de l\'environnement, ou ils peuvent être en récompenses de quêtes terminées.[pad]\n[b]Cristaux Apogies[/b]\n[item=32572] se ramassent sur les élites de type Demons ou Dragons dans les Tranchantes. Pour appeler ces mobs, 35 Eclats Apogides sont nécessaires, et il est recommandé que vous ayez un groupe de 5 personnes pour les vaincre.\n\n[b]Quêtes[/b]\nIl y a un certain [url=?quests&filter=cr=1;crs=1038;crv=0]nombre de quêtes[/url] qu\'un joueur peut faire pour gagner de la réputation avec Ogri\'la, ainsi que plusieurs [url=?quests&filter=da=ja;cr=1;crs=1038;crv=0]quêtes quotidiennes[/url]. Beaucoup de quêtes quotidiennes seront également accordée à la réputation de la Garde Ciel Sha\'tari lorsqu\'elles seront complétées. \n\nPour accéder aux principales quêtes d\'Ogri\'la, un joueur doit d\'abord compléter les 5 quêtes de groupe de [npc=22941].\n\n[h3]Éléments épuisés[/h3]\nUn certain nombre d\'éléments apogides tombent parfois de mobs une fois mort. Lorsque vous avez amassé 50 éclats apogides, [url=?search=Apexis+Crystal+Infusion]les objets suivants peuvent être améliorés[/url], obtenant des statistiques supplémentaires et des emplacements de gemmes. Une fois ces objets améliorés, ils deviendront liés si équipés, et peuvent donc être vendus ou échangés avec d\'autres joueurs. Une chose à noter cependant, bien que les éléments épuisés peuvent également avoir des statistiques ou des effets, ils ne peuvent pas être équipés.',NULL),(8,911,2,NULL,2,'[b]Lune d\'Argent[/b] est la capitale des elfes de sang, située dans la partie nord-est de [zone=3430] dans le royaume de Quel\'Thalas. La capitale,des elfes de sang, est à couper le souffle. Elle peut rivaliser avec la capitale naine de [zone=1537], capitale la plus ancienne du monde toujours debout. Récemment reconstruite, la ville abrite la plus grande population d\'elfes de sang en Azeroth. \n\nAujourd\'hui, Lune d\'Argent n\'est que la moitié orientale de la ville d\'origine. La moitié occidentale a été presque entièrement détruite par le fléau pendant la troisième guerre. La place de l’Épervier, est la seule partie occidental de Lune d\'Argent restant sous le contrôle des elfes de sang. La Malebrèche, chemin parcouru par Arthas Menethil et son armée de morts-vivants parties en quête de ressusciter Kel\'Thuzad, traverse tout le Bois des Chants éternels. Il sépare la Lune d\'Argent reconstruite et ces ruines de la moitié occidentale. Fait intéressant, les ruines de Lune d\'Argent ne logent pas de morts-vivants, au lieu de cela, elles contiennent des [url=?npcs&filter=cr=37;crs=6;crv=1502;na=Déshérité;maxle=8]déshérités[/url] et des [npc=15638]. Dans l\'état actuel des choses, Lune d\'Argent est encore la plus grandes des villes Hordeuses.\n\n[h3]Histoire[/h3]\n\nLa ville de Lune d\'Argent a été fondée par les hauts élus après leur arrivée à Lordaeron, il y a des milliers d\'années. La ville a été construite en pierre blanche autour de plantes vivantes dans le style de l\'ancien Empire Kaldorei. La ville contenait les célèbres académies de Lune d\'Argent, centre d\'apprentissage de la magie arcane, et la Flèche de Solfurie, majestueux palais abritant la famille royale des hauts-elfes. Également basé dans la ville, la convocation de Lune d\'Argent, également connu sous le nom de « Le Concile de Lune d\'Argent », était l\'organe dirigeant des hauts-elfes. À travers une étendue d\'océan vers le nord, il y a l\'île qui contient le plateau du puits du Soleil.\n\nBien que Lune d\'Argent ait resorti relativement indemne de la deuxième guerre, dans la troisième guerre, le Chevalier de la mort Arthas a mené le Fléau dans la ville, l\'attaquant au cours de sa quête pour atteindre le puit du Soleil. Le roi High Elven a été tué et la majorité de la population a été exterminée. Les forces de fléau ont tenu la ville pendant un certain temps mais l\'ont abandonné après l\'épuisement de ses ressources. \n\nBien que la ville ait été attaquée par le Fléau, elle n\'est pas aussi détruite qu\'on pourrait le penser. Beaucoup de ses plantes sont mortes, quelques cadavres sont étendu sur le pavé, la ville était à l\'abri du feu et de la destruction. Lune d\'Argent ressemble maintenant à une ville fantôme, intacte, mais étrangement abandonnée. Néanmoins, les chasseurs de trésors fréquentent fréquemment les ruines de Lune d\'Argent pour essayer de trouver certains des artefacts précieux que les elfes ont laissés derrière avant de déserter la ville, mais les fantômes des anciens habitants de Lune d\'Argent les en empêchent.\n\n[h3]Réputation[/h3]\n\n[npc=20612] offre une quête répétitive où il faut fournir des étoffes. Une fois exalté à Lune d\'Argent, en récompense, les joueurs peuvent acheter des [url=?items=15.5&filter=cr=151;crs=6;crv=35513;na=Faucon-pérégrin]Faucon-pérégrins[/url].\n\nLes zones environnantes du Bois des Chants éternels et des terres fantômes contiennent la plupart des quêtes pour gagner de la réputation avec Lune d\'Argent.',NULL),(8,577,2,NULL,2,'[minibox]\n[h2]Cartel Gentepression[/h2]\n[faction=21]\n[b]Long-guet[/b]\n[faction=369]\n[faction=470]\n[/Minibox]\n\n[b]Long-guet[/b], faction de la ville du même nom, est un poste commercial dirigé par les gobelins du Cartel Gentepression. Il se trouve au carrefour des principales routes commerciales du [zone=618].\n\n[h3]Histoire[/h3]\n\nCette ville est le dernier point de la civilisation avant d\'atteindre le Mont Hyjal. Il est géré par les gobelins comme un poste commercial. La ville est officiellement neutre pour toutes les races et factions. Seuls les pèlerins peuvent monter jusqu’à l’Arbre-Monde, point culminant du Mont Hyjal. Long-guet est donc la destination la plus haute que les marchands et les aventuriers peuvent atteindre sans l\'autorisation des Elfes de nuit. Elle offrirait une vue dominante sur Kalimdor, si les nuages qui enveloppent continuellement les flancs de la montagne, disparaissaient.\n\nLong-guet est le seul avant-poste de gobelin majeur dans le nord de Kalimdor. Tout d\'abord, il sert de base aux opérations pour les mineurs de thorium et d\'arcanites puisque le Berceau-de-l’Hiver possède quelques veines inexploitées de ces matériaux. Deuxièmement, il sert de centre d\'échanges entre l\'Alliance et la Horde. Alors que Long-guet est à peine plus sûr que Reflet-de-Lune, généralement, l\'Alliance et la Horde se traitent assez bien là-bas. En outre, Long-guet est un point d\'arrêt et de réapprovisionnement fréquent pour les fidèles qui font le pèlerinage du Berceau-de-l’Hiver au Mont Hyjal.\n\n[h3]Réputation[/h3]\n\nLa réputation de Long-guet et du Cartel Gentepressin provient surtout des quêtes du Berceau-de-l’Hiver. Avec une réputation au minimum amicale, les gardiens vous aident en cas d’attaque initiée contre vous.',NULL),(8,21,2,NULL,2,'[minibox]\n[h2]Cartel Gentepression[/h2]\n[b]Baie-du-Butin[/b]\n[faction=577]\n[faction=369]\n[faction=470]\n[/minibox]\n\n\n[b]Baie-du-Butin[/b] est une grande ville pirate nichée dans les falaises entourant un magnifique lagon bleu, à l’extrémité de [zone=33]. Pour entrer dans la ville, il faut passer au travers les mâchoires blanchis d\'un requin géant.\n\nParcouru par les Écumeurs des Flots noirs qui sont étroitement associés eu Cartel Gentepression, le port offre des opportunités à n\'importe quel voyageur passant par là, indépendamment de leur faction. Combiné à la célèbre « taverne du Loup de mer », le [event=15], de nombreux maîtres de profession et des vendeurs, qui vendent de tout (des animaux de compagnie aux anneaux de diamant), c\'est l\'un des endroits les plus populaires en Azeroth.\n\n[npc=2496], chef de la ville, embauche toute l\'aide qu\'il peut obtenir contre [faction=87] et autres menaces de la ville. Il réside avec le chef des Écumeurs des Flots noirs, [npc=2487], au sommet de l\'auberge de Baie-du-Butin.\n\nEn raison de la liaison par bateau de Baie-du-Butin à Cabestan, les joueurs de tout niveau (surtout de la Horde, si le niveau est faible) peut-être croisés dans le port, bien que les visiteurs les plus fréquents seront dans les niveaux 35-45, car les quêtes disponibles auprès des gens du pays se situent dans cette tranche de niveau.\n\nL\'eau est parsemée de débris flottants et de bancs de poissons. Plusieurs types de poissons se pèchent dans les eaux de la Baie, tels que le [item=6359], le [item=6358], et l\'[item=13422]. La pêche, dans les débris flottants, vous donnera également plus de chance de pêcher des coffres et d\'autres articles, faisant de Baie-du-Butin un endroit idéal pour la pêche.\n\n[h3]Réputation[/h3]\nLa plupart des quêtes pour augmenter la réputation avec Baie-du-Butin sont situés au Cap de Strangleronce. Avec une réputation au minimum amicale, les gardes vous aiderons en cas d’attaque contre vous.\n\nSi vous êtes haï avec Baie-du-butin vous pouvez faire la quête répétable [quest=9259] pour revenir à Neutre.',NULL),(8,470,2,NULL,2,'[minibox]\n[h2]Cartel Gentepression[/h2]\n[faction=21]\n[faction=577]\n[faction=369]\n[b]Cabestan[/b]\n[/Minibox]\n\n[b]Cabestan[/b], faction de la ville du même nom, situé sur la côte est de Kalimdor dans [zone=17]. Elle est dirigée par des gobelins. Ses rues se répandent dans toutes les directions, et l\'architecture ne montre aucune cohérence ni vision commune. C\'est une ville de divertissement et de commerce, où tout ce que vous voudriez acheter est en vente mais aussi beaucoup de chose que personne ne veut jamais. \n\nCabestan est actuellement géré par un groupe d\'entreprises connu sous le nom du Cartel Gentepression, un groupe fragmenté de la KapitalRisk, qui a d\'abord construit la ville portuaire pour la négociation avec [zone=1637]. C\'est d\'abord une faction neutre où Horde et Alliance se côtoient. Un bateau relie commodément Cabestan à Baie-du-butin.\n\n[h3]Histoire[/h3]\n\nConstruit à part égales entre l\'industrie et de la décadence, la ville portuaire gobeline de Cabestan s\'étend sur près d\'un kilomètre de littoral des Tarides de l\'est, entre [zone=14] et [zone=15]. Cabestan est la fierté des gobelins, une ville commerciale où vous pouvez trouver presque tout ce que votre cœur désire, et si quelque chose n\'est pas en stock, vous pouvez parier que les gobelins peuvent le commander. Cabestan est desservie régulièrement par les bateaux qui font la traversé en passant devant la forteresse de Theramore, vers le sud.\n\nCabestan est une ville où les habitants, qui étaient autrefois des truands, règnent maintenant. Ses rues errent sans rime ni raison à travers des quartiers dédiés à une seule activité : le commerce. Des entrepôts délabrés se situent à côté de maisons en pierre majestueuses. Les belles boutiques sont voisines avec des cabanes grossières. Des objets de toutes les formes, et certains au-delà de l\'imagination, sont exposés sur les marchés et les boutiques exclusives.\n\nLes Gobelins accueillent toutes personnes ayant de l\'or, des éléments de valeur et une volonté de les échanger contre leurs marchandises et leurs services. Les marchands traversent la ville tous les jours, vendent tout, de la soie aux esclaves. Même la nuit, les magasins qui bordent les rues et les allées restent ouverts aux entreprises. Ceux qui ont de l\'argent peuvent écouter des musiciens qualifiés, tout en buvant des bières fines et en mangeant des aliments préparés par des grands chefs. Pour ceux qui ont des goûts plus terriens, on retrouve le long des quais des marchants d\'armes, la banque et des casinos.\n\nCabestan est le plus grand port de Kalimdor, beaucoup de navires transportant de la cargaison sortent pour d\'autres sites autour de Kalimdor. En plus des navires commerciaux légitimes, les bâtiments pirates reçoivent une amnistie dans le port de Cabestan tant qu\'ils peuvent payer des droits d\'accostage rigides. Cette situation rend les capitaines marchands furieux, mais ils ne peuvent boycotter Cabestan, sinon c\'est la faillite pour leurs commerces. En outre, les avocats et les mercenaires qui rôdent sur le front de mer sont impatients de faire face à tous ceux qui cherchent à causer des problèmes.\n\n[h3]Réputation[/h3]\n\nLa plupart des quêtes pour élever la réputation avec Cabestan et le Cartel Gentepression sont situées dans les Tarides. Avoir une réputation au minimum amicale, les gardiens aident en cas d\'attaque contre vous.\n\nSi vous êtes détesté auprès de Cabestan, vous pouvez faire la quête répétable [quest=9267] pour revenir à une réputation Neutre.',NULL),(8,369,2,NULL,2,'[minibox]\n[h2]Cartel Gentepression[/h2]\n[faction=21]\n[faction=577]\n[b]Gadgetzan[/b]\n[faction=470]\n[/minibox]\n\n[b]Gadgetzan[/b] est la faction de la ville du même nom, qui abrite les plus grands ingénieurs, alchimistes et marchands gobelins. Seul endroit de civilisation au nord du désert de [zone=440], elle est perçue comme une oasis. Gadgetzan est le siège du Cartel Gentepression, le plus grand cartel gobelin. Les gobelins croient au profit plus qu’à la loyauté, donc Gadgetzan est considéré comme territoire Neutre dans le conflit Horde / Alliance.\n\n[h3]Histoire[/h3]\n\nBien que la neutralité des gobelins soit presque universellement reconnue, il y a encore ceux qui cherchent à semer le chaos et l’anarchie. Pour Gadgetzan, cela vient sous la forme des bandits Bat-le-désert, une bande de mécréants qui occupe le champ des Puisatiers et les ruines d\'Ombre-du-Zénith au Nord-est de Tanaris. Peu de Gobelins se soucient des ruines antiques (à moins qu’ils y aient un trésor), les bandits peuvent avoir les vieux blocs de pierre. \nCependant, le champ des Puisatiers est vital pour la survie des gobelins, leur fournissant l’or liquide du désert. Les tours d\'eau dans le champ ont été construites sous la chaleur ardente du soleil, par le travail de leurs esclaves. Les gobelins ne vont pas abandonner leurs tours durement gagnées, aussi facilement. Mais, ils doivent rester en ville pour arrêter le conflit, en apparence interminable, parmi les différents visiteurs et donc empêcher de perturber les affaires. Par conséquent, ils embauchent de braves mercenaires venant de tous les coins du monde pour les aider.\n\n[h3]Réputation[/h3]\n\nEn tuant les [url=?npcs=7&filter=na=mers+du+Sud]Flibustiers des mers du Sud[/url] et les [url=?npcs=7&filter=na=bat-le-désert]Bandits Bat-le-désert[/url], la réputation avec le cartel Gentepression augmentera. Ayant une réputation au minimum amicale, les gardes vous aideront en cas d\'attaque contre vous. Avoir une réputation exaltée signifie que les gardes ne vous attaqueront jamais même si vous lancez des attaques sur la faction opposée. \n\nLa plupart des quêtes associées à la faction Gadgetzan sont situées à Tanaris. \n\nSi vous êtes détestés avec Gadgetzan, vous pouvez faire la quête répétable [quest=9268] pour obtenir la Neutralité.',NULL),(8,47,2,NULL,2,'[b]Forgefer[/b] est la faction associée à la capitale des nains, [zone=1537]. [npc=2784] règle son royaume de Khaz Modan de sa salle du trône dans la ville, et [npc=7937], chef des gnomes, a temporairement dû s\'établir dans Brikabrok après la récente chute de la ville gnome [zone=133].\n\n[h3]Histoire[/h3]\n\nForgefer est l\'ancienne demeure des nains, une merveille façonnée dans la pierre. Forgefer a été construite au cœur même des montagnes, une ville souterraine qui abrite des explorateurs, des mineurs et des guerriers. Les portes massives de roche protègent la ville en temps de guerre, et la lave de la montagne est redirigée et distribuée à des fins de chaleur, d\'énergie et de forage. \nAvant que le clan de Sombrefer ne soit banni de la ville, menant à la Guerre des Trois Marteaux, Forgefer était le centre commercial et social de tous les clans nains. Il appartient maintenant au Clan Barbe-de-bronze. \nBeaucoup de bastions nains ont chuté pendant la Guerre de Lordaeron, entre la Horde et l\'Alliance, mais la puissante ville de Forgefer, nichée dans les sommets hivernaux de [zone=1] et protégée par ses grandes portes, n\'a jamais été violée par la Horde envahissante.\n\nRelativement récemment, Forgefer est également devenu le foyer des Exilés de Gnomeregan. Après la troisième guerre, la ville gnome fut envahie par Troggs. Depuis lors, un certain nombre de gnomes se sont installés à Forgefer, transformant une zone de cette ville à leur goût, une région connue sous le nom de Brikabrok.\n\nForgefer est l\'une des villes les plus peuplées du monde, venant après la ville humaine de [zone=1519], et abritant 20 000 personnes.\n\nAlors que l\'Alliance a été affaiblie par les événements récents, les nains de Forgefer, dirigés par le roi Magni Barbe-de-bronze, forment un nouveau futur dans le monde. \n\n[h3]Réputation[/h3]\n\n[npc=14723] offre une quête répétitive où il faut fournir des étoffes. Une fois exalté à Forgefer, en récompense, les joueurs peuvent acheter des [url=?items=15.5&filter=cr=93:92:151:151;crs=2:1:6:6;crv=0:0:33977:33976;na=bélier] béliers [/url].\n\nLes zones environnantes [zone=1], [zone=38] et [zone=11] contiennent la plupart des quêtes pour gagner de la réputation auprès de Forgefer.',NULL),(8,54,2,NULL,2,'[b]Les Exilés de Gnomeregan[/b] est la faction des gnomes qui ont fui leur domicile, [zone=133] à [zone=1]. Elle a été détruite par [url=?npcs=7&filter=na=Trogg] les Troggs[/url] après une invasion toxique. Maintenant, membre de l’alliance, la plupart sont situés à Brikabrok, une partie de la ville voisine [zone=1537], y compris le leader [npc=7937].\n\n[h3]Histoire[/h3]\n\nOn a spéculé que les gnomes ont été formés comme des robots par les titans, en raison de leur nature curieuse et de leurs compétences techniques. Ils vivaient autrefois dans la cité de Gnomeregan, sans doute la plus belle ville technologique du monde.\n\nLes gnomes étaient une race souterraine de bricoleurs, jusqu’à ce que les Troggs aient détruit Gnomeregan. Dans cette guerre, plus de 80% de la population gnome a été exterminé.\n\n[h3]Réputation[/h3]\n\n[npc=14724] offre une quêtes répétables où il faut fournir des étoffes. En étant exalté aux Exilés de Gnomeregan, les joueurs sont capables de conduire des [url=?items=15.5&filter=cr=93:92;crs=2:1;crv=0:0;na=mécanotrotteur]mécanotrotteurs[/url].\n[zone=1] contient la plupart des quêtes pour gagner la réputation avec les exiés de Gnomeregan.',NULL),(8,72,2,NULL,2,'[b]Hurlevent[/b] est la faction associée à [zone=1519], la capitale des Humains. Elle est située dans la partie nord-ouest de la [zone=12]. L\'enfant roi, [npc=1747], réside dans le Donjon de Hurlevent, entouré de ses gardes du corps et de ses conseillers, [npc=1748] (le régent) et [npc=1749]. La ville est nommée ainsi à cause des rafales soudaines et occasionnelles créées par la forme spéciale des montagnes autour de la ville glorieuse.\n\n[h3]Histoire[/h3]\n\nPendant la Première Guerre, le Royaume d\'Azeroth, y compris sa capitale, le Donjon de Hurlevent, a été complètement détruit par la Horde. Ses survivants ont fui vers Lordaeron. Après que les orcs ont été vaincus, au Portail des Ténèbres, à la fin de la Deuxième Guerre, il a été décidé que la ville serait reconstruite, dépassant sa grandeur d’antan. Des tailleurs de pierres et des architectes ont pu été rassemblés par les nobles de Hurlevent. Sous la directio de cette équipe, la plus qualifiée et la plus ingénieuse, Hurlevent a été reconstruit dans une période de temps incroyablement courte. Maintenant, à la fin de la troisième guerre, dans le renommé Royaume de Hurlevent. C’est l\'un des derniers bastions du pouvoir humain laissé dans le monde.\n\nAvec la chute des Royaumes du Nord, Hurlevent est de loin la ville la plus peuplée du monde. Avec une population de deux cents mille personnes (principalement humaines), elle sert à bien des égards comme le centre culturel et commercial de l\'Alliance, même avec un accès à la mer. Les humains qui vivent dans la ville sont généralement insouciants et artistiques, favorisant les vêtements légers et colorés, la cuisine et l\'art. Elle abrite l\'Académie des sciences arcanes, la seule école de sorcellerie dans les royaumes de l\'Est, ainsi que le SI:7, une organisation de renseignement.\n\nCependant, les gens de Hurlevent ont du mal à accepter le rôle de Theramore en tant que foyer de la nouvelle Alliance. Ils sont convaincus que Hurlevent devrait être l\'héritière légitime du rôle de la ville de Lordaeron comme par le passé, mais aussi que Theramore est attristé face à l\'aggravation de la situation au sein de Les Royaumes de l\'Est.\n\n[h3]Réputation[/h3]\n\n[npc=14722] propose une quête répétable pour obtenir une réputation plus élevée avec Hurlevent. En contrepartie d\'une réputation exaltée, les joueurs non-humains peuvent monter sur des chevaux.\n\nLa plupart des quêtes associées à Hurlevent viennent des zones environnantes de la forêt d\'Elwynn, [zone=40] et [zone=44].',NULL),(8,930,2,NULL,2,'[b]Exodar[/b] est la faction associée à [zone=3557], la capitale enchantée des Draeneï construit avec la plus grande partie de leur vaisseau qui s’est écrasé. Il est situé dans la partie ouest de l’[zone=3524]. Le chef de la faction Exodar est [npc=17468], qui est situé près des maîtres de combat dans la Voûte des Lumières.\n\n[h3]Histoire[/h3]\n\nLes Draeneï rescapés du crash de leur vaisseau se sont récemment réveillés pour reconstruire l’Exodar, encore fumant de l’impact. L\'Exodar était autrefois une structure de satellite naaru autour de la forteresse dimensionnelle du [url=?search=donjon+tempête]Donjon de la Tempête[/url]. L\'Exodar contient une grande quantité de merveilles technologiques (en raison de ses origines avec le Donjon), comme des «fils» magiquement enchantés qui transmettent de l\'énergie sainte dans tout le navire pour alimenter le chauffage et l\'éclairage, tout en augmentant les pouvoirs, déjà considérable, des Draeneï.\n\n[h3]Réputation[/h3]\n\nComme pour les autres grandes factions associées aux races principales, la réputation de l\'Exodar peut être acquise en faisant la quête répétable de [npc=20604] [small][/small], ou alors, en tuant la faction adverse dans [zone=2597] (les elfes de sang) et en faisant les quêtes appropriées. Avec la réputation, le joueur peut acheter des objets provenant de fournisseurs liés à Exodar pour 10% de moins et, une fois exalté, le joueur peut acheter [url=?Items=15.5&filter=na=elekk;cr=93:92;Crs=2:1;crv=0:0] diverses montures[/url].',NULL),(8,69,2,NULL,2,'[b]Darnassus[/b] est la faction de la ville de [zone=1657], la capitale des Elfes de la nuit. La haute prêtresse, [npc=7999], réside dans le Temples de la Lune, entourée d\'autres sœurs d\'Elune. Dans l\'Enclave Cénarien, l\'[npc=3516] conduit le [faction=609], souvent en opposition directe avec ses autres druides à [zone=493] et Tyrande elle-même.\n\n[h3]Histoire[/h3]\n\nAu lendemain de la troisième guerre, les Elfes de la nuit devaient s\'adapter à leur existence mortelle. Un tel ajustement était loin d\'être facile. Beaucoup d\'Elfes de la nuit ne pouvaient pas s\'adapter aux perspectives de vieillissement, de maladie et de fragilité. En cherchant à retrouver leur immortalité, un certain nombre de druides capricieux conspiraient pour planter un arbre spécial qui rétablirait un lien entre leurs esprits et le monde éternel.\n\nAvec [npc=15362] disparu, Fandral Forteramure, le chef de la conspiration qui souhaitaient planter le nouvel Arbre-Monde, est devenu le nouvel Archidruide. En un rien de temps, lui et ses camarades druides ont pris les devants et ont planté le grand arbre, [zone=141], au large des côtes orageuses du nord de Kalimdor. Avec leur soin, l\'arbre a poussé au-dessus des nuages. Parmi les branches crépusculaires de l\'arbre colossal, la merveilleuse ville de Darnassus a pris racine. Cependant, l\'arbre n\'a pas été béni par la nature et s\'avère être corrompu par la Légion Ardente. Maintenant, la faune et même les membres de Teldrassil sont contaminés par une obscurité croissante.\n\n[h3]Réputation[/h3]\n\n[npc=14725] offre une quête répétable [quest=7800] utilisé par les joueurs de l\'Alliance pour obtenir le droit de monter des [url=?items=15.5&filter=cr=93:92:151;crs=2:1:6;crv=0:0:13086;na=sabre;si=-1]Sabres-de-nuit[/url]. Les joueurs qui sont au minimum niveau 44, cherchant à gagner la faveur de Darnassus, devraient trouver et compléter les quêtes de [zone=357]. Les quêtes sont associées à Darnassus et pourraient accroître considérablement votre réputation.',NULL),(8,809,2,NULL,2,'Les [b]Shen\'dralar[/b] sont la faction des Elfes de nuit restant dans [zone=2557]. Ils sont un groupe qui pratique la magie arcane à son apogée sur les traces de leur ancienne reine Azshara, et de ses partisans, les Bien-nées. Ils vivent à Eldre\'Thalas (nom antérieur de Hache-tripes) depuis la fin de la guerre des Anciens. Ils sont peu nombreux, mais leur connaissance et leur pouvoir mystique sont géniaux.\n\nLeur chef, [npc=11486], était chargé de superviser la construction des pylônes pour contenir le grand démon [npc=11496] et absorber son pouvoir démoniaque. Après de longues et nombreuses années, le pouvoir des pylônes a commencé à diminuer, le prince a entrepris de tuer les elfes de nuit restants pour maintenir l\'énergie. Les esprits des défunts demandent vengeance, mais seuls des aventuriers aguerris peuvent le tuer. Faite-vite, il reste très peu d\'habitants en vie.\n\n[h3]Réputation[/h3]\n\nLa réputation peut être obtenue en rendant à plusieurs reprises les quêtes obtenus avec les trois Librams de Hache-Tripes : [item=18333], [item=18334] et [item=18332]. \nLa réputation peut être obtenue aussi via les livres de classe suivant :\n[ul] \n[li] [item=18357] - Guerrier [/li] \n[li] [item=18363] - Chaman [/li] \n[li] [item=18356] - Voleur [/li] \n[li] [item=18360] - Démoniste [/li] \n[li] [item=18362] - Prêtre [/li]\n[li] [item=18358] - Mage [/li]\n[li] [item=18364] - Druide [/li]\n[li] [item=18361] - Chasseur [/li]\n[li] [item=18359] - Paladin [/li]\n[li] [item=18401] - Guerrier et Paladin [/li] \n[/ul] \nLes livres de classe et les librams donnent 500 points de réputation chacun.',NULL),(8,349,2,NULL,2,'[b]Ravenholdt[/b] est une guilde de voleurs et d\'assassins qui ne reçoit que ceux d\'une extraordinaire prouesse. Ils sont opposés à la [faction=70]. La quête, [quest=8249], est disponible pour les classes non-voleurs, mais elle nécessite l\'aide d\'un voleur pour obtenir les objets pour la quête. Le manoir de Ravenholdt, le siège de la faction, est situé dans [zone=36], mais pour y arriver, vous devez venir du coin nord-est de [zone=267].\n\n[h3]Réputation[/h3]\n\nTous les [url=?Search=Syndicat#npcs]membres du Syndicat [/url] donnent 1-5 points de réputation en fonction de votre niveau actuel. De plus, il existe quelques quêtes qui augmentent votre réputation, mais la méthode principale pour élever votre réputation provient des quêtes répétées pour fournir les objets demandés.\n\nVous commencez à une réputation Neutre (0/3000) avec Ravenholdt, ce qui signifie que si vous tuez un NPC de Ravenholdt avant d\'augmenter votre réputation d\'au moins 5, vous deviendrez hostile et ne pourrez jamais augmenter votre réputation. \nPour augmenter votre réputation de Neutre à Amicale, la quête répétable [quest=6701] est disponible. Vous devrez fournir 11-12 [item=17124] et une fois que vous êtes amical, cette quête n\'est plus disponible. Vous pouvez également fournir cinq [item=16885].\nPour augmenter votre réputation au-delà de Amical, le seul choix est la quête répétable, [quest=8249]. \n\n[h3]Récompense[/h3]\n\nIl n\'y a aucune récompense de faction connue pour obtenir que se soit avec une réputation Amicale, un honoré, révéré ou exalté, sauf que les gardes vous parlent avec plus de respect. \n\nCependant, La réputation Exalté est nécessaire pour obtenir le Haut-Fait : [achievement=2336].',NULL),(8,87,2,NULL,2,'Les [b] Pirates de la Voile Sanglante [/b] semblent être l\'une de ces organisations, qui sont apparues en Azeroth pendant les événements menant à la troisième guerre et à la suite de la troisième guerre. Ils sont originaires du Rivage Cruel, où leur chef, l\'[npc=2546], organise les opérations. Ils ont maintenant l\'intention de paralyser et de piller la ville portuaire de [faction=21], contrôlée par le Cartel Gentepression et sous la protection des Ecumeurs des Flots noirs. Il est probable que les Pirates de le Voile Sanglante sont venus profiter de la perte actuelle de leur flotte, sur la côte de la [zone=45], dans laquelle deux de ses navires ont été détruits. Le navire restant a été obligé de trouver un abri dans une crique où son équipe lutte maintenant pour survivre aux escarmouches des Nagas.\n\nEn préparation de l\'attaque, les Pirates de la Voile Sanglante ont pris position dans des endroits clés près de la ville. À l\'heure actuelle, ils ont trois navires ancrés le long du littoral au sud de Baie-du-Butin, à l\'abri des canons défensifs de la ville. Des camps ont également été construits le long de la même côte en prévision de l\'attaque. En outre, une fête scoute a atterri juste à l\'ouest de l\'entrée de la ville, signalant toutes les activités, ainsi qu\'un camp construit le long de la route menant vers la ville, susceptible d\'empêcher tout renfort.\n\nLes Pirates de la Voile Sanglante cherchent à atteindre leurs objectifs sans avoir leurs forces engagées dans la bataille, à cette fin, chaque côté cherche maintenant l\'aide d\'aventuriers sympathiques à leur cause.\n\n[h3]Réputation [/h3]\n\nIl n\'y a qu\'une seule façon d\'augmenter votre réputation auprès des Pirates de la Voile Sanglante et c\'est de libérer votre colère contre tous les citoyens de Baie-du-Butin. Voici une liste de tous les citoyens de Baie-du-Butin et leur valeur de réputation. \n[ul]\n[li] [npc=4624] : 25 points de réputation gagné [/li]\n[li] [npc=15088] : 25 points de réputation gagné [/li]\n[li] [npc=2496] : 5 points de réputation gagné [/li]\n[li] [npc=2636] : 5 points de réputation gagné [/li]\n[li] [url=?Npcs&filter=cr=3;crs=21;crv=0] Plusieurs autres NPC [/url][/Li]\n[/Ul]\nLe montant gagné avec les Pirates de la Voile Sanglante est indiqué pour un niveau 60 non humain. Le montant perdu pour tuer un citoyen ne peut pas être démontré car il dépend de votre niveau actuel avec Baie-du-Butin et de l\'importance de la personne que vous tuez. En plus de cela, quand vous perdez de la réputation avec Baie-du-Butin, vous perdez la moitié dans les trois autres villes du Cartel Gentepression. Par exemple, si vous perdez 25 points avec Baie-du-Butin, vous perdrez 12,5 points avec [faction=470].\n\nLe moyen le plus rapide d\'augmenter votre réputation avec les Pirates de la Voile Sanglante est de tuer des habitants de Baie-du-Butin. Au début, cela peut sembler une tâche simple car les gardes n\'apparaissent pas aussi menaçants que les autres monstres auxquels un joueur est confronté dans le jeu. Cependant, les gardes sont très équipés pour neutraliser les joueurs de toute classe, afin d\'éviter que les gens ne s\'attaquent les uns les autres dans la ville. \n\nLe Cogneur de Baie-du-butin a l\'avantage avec plusieurs capacités. L’une d’entre elle est l’utilisation de filet pour vous bloquer sur place, vous empêchant de vous échapper. Une autre est le fait qu\'ils appellent d’autres Cogneurs chaque fois que vous attaquiez un citoyen de la ville ou si vous êtes sous un statut hostile avec Baie-du-Butin, les joueurs peuvent bientôt se retrouver rapidement submergés par les Cogneurs.\nLa capacité la plus forte du Cogneur est qu’une fois qu\'il tire son arme, il est peu probable que vous vivez, si vous ne vous échappez pas assez vite. Chaque fois qu\'un Cogneur vous tire dessus, l\'attaque vous retient, tout comme une attaque de marteau d\'Ogre. La différence ici, est que le Cogneur peut tirer rapidement en succession, provoquant des lances de chaîne. Un joueur peut littéralement être jeté d\'un côté de la ville à l\'autre, ce qui vous empêche d\'attaquer. Plus souvent, vous vous retrouverez coincé dans un coin, incapable de bouger et incapable d\'attaquer avec tous les sortilèges interrompues par l\'attaque du Cogneur. Parce que les Cogneurs ne rangent pas leurs armes à feu une fois qu\'elles sont sorties, la meilleure façon d\'agir est de s\'enfuir.\n\nPar essais et erreurs, la plupart des gens ont découvert un endroit sûr pour tuer les Cogneurs de Baie-du-Butin. Si vous suivez le tunnel qui mène à la ville, le chemin de votre gauche qui mène à la maison du Forgeron est l\'endroit idéal pour tuer les gardes. Seuls deux gardes patrouillent sur ce chemin. Une fois qu\'ils sont partis, entrer dans la première construction sur le chemin pour provoquer un rassemblement. Un joueur devrait pouvoir tuer 2 à 4 Cogneurs avant que les deux Cogneurs de patrouille en appellent d’autres. En moyenne, un joueur qui fait cela peut tuer environ 30 à 40 Cogneurs de Baie-du-Butin, gagnant environ 800 points de réputation auprès de la Voile Sanglante. Les Cogneurs ici ne semblent pas sortir leurs armes, mais si vous vous trouvez dans une mauvaise situation, vous pouvez sauter sur la balustrade, courir sur le chemin des eaux, pour vous échapper.\n\nPour augmenter votre réputation au-delà de honoré, seuls deux NPC vous le permettent : \n[ul]\n[li] [npc=9179] : 5 points de réputation toutes les 7 minutes jusqu’à révéré [/li]\n[li] [npc=26081]: 5 points de réputation toutes les 24 heures jusqu’à exalté [/li]\n[/Ul]\n\n[h3]Récompenses[/h3]\n\nDevenir amical avec Les Pirates de la Voile Sanglante, vous donnera accès aux éléments suivants :\n[ul]\n[li] [item=12185] - Invoque un [npc=11236] [/li]\n[li] [item=22742] [/li]\n[li] [item=22743] [/li]\n[li] [item=22745] [/li]\n[/Ul]\nVous aurez besoin d\'être honoré avec la Voile Sanglante pour [achievement=2336].',NULL),(8,70,2,NULL,2,'Le[b] Syndicat [/b] est une organisation criminelle humaine qui opère principalement dans les [zone=45] et les [zone=36], bien que quelques petits campements soient éparpillés dans les [zone=267]. Leur effectif compte environ 3 000 personnes.\n\nIls ont trois chefs : [npc=2423], descendant du premier Lord d\'Alterac, qui dirige les actions du Syndicat dans les montagnes Alterac, [npc=2597] dirige les actions du Syndicat dans les Hautes Terres d\'Arathi à partir de la principale demeure, le Donjon semi-abandonnée de Stromgarde, et Lady Beve Perenolde, fille d\'Aiden Perenolde.\n\n[h3]Histoire[/h3]\n\nPendant la seconde guerre, Lord Perenolde qui dirige le royaume d\'Alterac, a été découvert pour être en liaison avec les orcs de la Horde. Perenolde croyait qu\'une victoire de le Horde était inévitable et offrait ainsi une aide à la Horde en suscitant des rébellions, en attaquant les bases de l\'Alliance et en leur fournissant des armes. Lorsque cette trahison fut découverte, l\'Alliance marchait contre Alterac et la détruisit. Perenolde et tous les nobles qui ont accompagné ses projets ont été dépouillés de leurs titres et de leurs terres. Beaucoup d\'entre eux ont réussi à s\'échapper, mais ont commencé à comploter pour se venger. En utilisant leur fortune encore considérables, la noblesse a engagé une bande de voleurs et d\'assassins, formant une organisation connue sous le nom de Syndicat.\n\nAu début, le but du Syndicat était simplement de répandre le chaos et le désordre, frappant des bases cachées dans les montagnes d\'Alterac. Avec la fin de la troisième guerre et le chaos qui suivie, les dirigeants du Syndicat ont vu leur chance de reprendre Alterac et de retrouver leurs anciens pouvoirs. Ils ont maintenant pris le contrôle de plusieurs avant-postes dans la région environnante, y compris le donjon abandonnée et une partie de la ville de Stromgarde.\n\nIls sont haïe par l\'Alliance, qu\'ils considèrent comme leurs ennemis mortels, et la Horde, qu\'ils considèrent comme des brutes faits pour travailler en esclaves. En conséquence, le Syndicat est maintenant chassé par les deux factions, avec [npc=10181], en particulier, une prime est sur sa tête, tous les membres du Syndicat capturés seront exécutés sommairement. En outre, [npc=4949] a commandé un certain nombre de ses agents, y compris [npc=2229], [npc=2239], [npc=2238] et leur chef [npc=2316] pour lancer une enquête sur la nature du Syndicate et ses activités, ainsi que pour récupérer [item=3498], un collier maintenant porté par Elysa, la maîtresse de Lord Aliden, qui appartenait à un son cher ami, [npc=18887].\n\n[h3]Réputation[/h3]\n\nLe Syndicat, en tant que faction dans World of Warcraft, est très étrange par rapport à la plupart des factions. En effet, que le meurtre des membres de cette faction ne réduira pas votre réputation. Pour la plupart des joueurs, qui ne sont pas voleur, la seule façon d\'afficher le Syndicat dans leur menu de réputation est de compléter la quête [quest=8249]. Cependant, la quête requiert [item=16885] ... que seuls les voleurs peuvent obtenir en volant à la tir des PNJ au-dessus du niveau cinquante ce qui rend difficile d\'organiser une telle transaction.\n\nActuellement, il n\'y a qu\'une seule option connue pour augmenter la réputation d\'un joueur avec le Syndicat, en tuant des membres de la faction [faction=349]. Il n\'y a pas de récompenses connues pour avoir augmenté la réputation du Syndicat. Les PNJ affiliés à Ravenholdt ne donnent que 1 point de réputation, à l\'exception de [npc=13085], qui donne 5 (bien que la perte de réputation correspondante avec Ravenholdt soit aussi cinq fois plus grande ). Tous les joueurs commençent à une réputation détestée de 32000/36000, il faudrait tuer 10 000 PNJ de Ravenholdt pour atteindre le statut neutre avec la faction. Malheureusement, l\'état neutre est le plus élevé que vous puissiez atteindre avec le Syndicat, ce n\'est pas pour dissuader les joueurs, aucun des NPC Ravenholdt ne grimpe la réputation.\n\n[b]AVERTISSEMENT[/b]: Si vous décidez de tuer les PNJ de Ravenholdt, sachez qu\'il n\'y a actuellement aucun moyen de restaurer votre positionnement avec Ravenholdt, si vous passez en dessous de Neutre. La raison du problème est qu\'aucune des quêtes qui donnent des points de réputation de Ravenholdt ne sera disponible car aucun des membres de Ravenholdt ne vous parleront. Cela signifierait qu\'il s\'agit d\'un changement permanent et que vous ne pourrez plus jamais interagir avec l\'un des NPC fidèles à Ravenholdt. Notez également que les joueurs commencent à la réputation de 0/3000 avec Ravenholdt, et le fait de tuer même un de leurs PNJ à ce niveau de réputation vous empêchera pour toujours de rétablir votre réputation avec eux.',NULL),(8,59,2,NULL,2,'[b]La Confrérie du Thorium[/b] est un groupe d\'artisans d\'élite qui vend un certain nombre de recettes épiques, par contre, vous devez obtenir suffisamment de réputation avec eux. Tous les joueurs commencent à la réputation : Neutre.\n\n[h3]Histoire[/h3]\n\nLa [zone=51] abrite un groupe de nains exceptionnellement robustes qui se sont séparés du Clan Sombrefer. Sur les falaises surplombant la région appelée « Le Chaudron », dans le grand nord des Gorges des vents brulants, les nains de la Confrérie du Thorium ont établi une base d\'opérations, la Halte du Thorium. De là, ils surveillent de près les activités des nains de Sombrefer dans les Gorges des vents brûlants. Les aventuriers qui cherchent la Halte du Thorium trouveront que les nains de la Confrérie du Thorium qui donnent de grandes récompenses pour ceux qui les aident dans leur lutte sans fin contre leurs anciens frères.\n\nLa Confrérie du Thorium comprend de nombreux artisans exceptionnellement talentueux, et les forgerons de la Confrérie sont censés être parmi les meilleurs Azeroth. Ils possèdent les connaissances requises pour fabriquer les armes et les armures de [npc=11502], le Seigneur du Feu, mais n\'ont pas de main-d\'œuvre pour obtenir les matériaux nécessaires à l\'artisanat. On raconte qu\'un membre de la Confrérie du Thorium a été habilité à échanger les recettes et les projets fabuleux des nains avec ceux qui peuvent prouver leur fidélité à la Confrérie. Bien sûr, pour prouver sa fidélité, l\'aventurier doit s\'aventurer au coeur de [zone=2717], le domaine de Ragnaros, le Seigneur du Feu lui-même, pour fournir aux nains les matières premières rares trouvées là-bas. Une tâche ardue, sans aucun doute, mais avoir accès aux secrets de la Confrérie du Thorium devrait s\'avérer être une récompense qui vaut bien l\'effort.\n\n[h3]Réputation[/h3]\n\n[b]De Neutre à Amical[/b]\n[ul]\n[li] Fournir : [item=18944], [item=3857] et [item=4234], [item=3575] ou [item=3356] au [npc=14624]. [/Li]\n[/ul]\n[b]De Amical à Honoré[/b]\n[ul]\n[li] Fournir : [item=18945] au [npc=14624]. [/Li] \n[/ul]\n[b]De Honoré à Exalté[/b]\n[ul]\n[li] Fournir : [item=11370] à [npc=12944]. [/Li]\n[li] Fournir : [item=17012] à Lokhtos Sombrescompte. [/Li]\n[li] Fournir : [item=17010] à Lokhtos Sombrescompte. [/Li]\n[li] Fournir : [item=17011] à Lokhtos Sombrescompte. [/Li]\n[li] Fournir : [item=11382] à Lokhtos Sombrescompte. [/Li] \n[/ul]',NULL),(8,68,2,NULL,2,'[b]Fossoyeuse[/b] est la faction pour la capitale du même nom, [zone=1497], régie par Sylvanas Coursevent. La cité est situé dans la [zone=85], au bord nord des Royaumes de l\'Est. La ville proprement dite est sous les ruines de la ville historique de Lordaeron. Pour y entrer, vous traverserez les défenses extérieures en ruines de Lordaeron et la salle du trône abandonnée, jusqu\'à ce que vous atteigniez l\'un des trois ascenseurs gardés par deux abominations.\n\n[h3]Histoire[/h3]\n\nFossoyeuse était à l\'origine un système d\'égouts, de cryptes et de catacombes sous la capitale de Lordaeron. Après que la ville a été détruite par le Fléau, Arthas a reconstruit et agrandit le dédale de souterrain. Initialement, il voulait que Fossoyeuse soit son siège de pouvoir, d\'où il gouvernerait les terres de pestes. Cependant, peu de temps après la fin de la troisième guerre, Arthas a été obligé de retourner à Norfendre et de sauver le Roi Liche. En son absence, [npc=10181] et ses non-morts rebelles ont capturé les ruines de la ville. Peu de temps après, elle a découvert la grande forteresse souterraine et a décidé de l\'établir comme base principale des opérations pour les Réprouvés.\n\n[h3]Réputation[/h3]\n\n[npc=14729] offre une quête répétitive où il faut fournir des étoffes. Une fois exalté à Fossoyeuse, en récompense, les joueurs peuvent acheter des [url=?items=15.5&filter=cr=93:92;crs=2:1;crv=0:0;na=squelette] chevaux squelettiques [/url].\n\nLes zones environnantes [zone=267], [zone=130], et la [zone=85] contiennent la plupart des quêtes pour gagner de la réputation auprès de Fossoyeuse.',NULL),(8,909,2,NULL,2,'La [b]Foire de Sombrelune[/b] est un mystérieux carnaval itinérant, qui parcourt non seulement Azeroth, mais aussi l’Outreterre. Conduite par l\'inimitable [npc=14823], un gnome d\'héritage douteux et de racine inconnue. La Foire amène des jeux, des prix et des bibelots exotiques inattendus, puissants ou non, en [zone=215], à la [zone=12] ou à la [zone=3519] chaque mois.\n\nUne variété de divertissement est proposée par la Foire, mais l\'attraction la plus commune est la rédaction du billet. Plusieurs forains distribuent des [item=19182], répartis dans toute la Foire, ils offrent des bons contre des articles fabriqués par des travailleurs du cuir, des forgerons ou des ingénieurs ainsi que des objets rassemblés dans la nature tels que [item=11404] et [item=19933]. Les bons peuvent être échangés contre de nombreuses choses allant de la [item=19295] à des colliers de grande puissance.\n\nBeaucoup d\'aventuriers recherchent la Foire de Sombrelune pour trouver les mystiques [url=?items=15.0&filter=minle=1;cr=107;crs=0;crv=Combine+the+Ace]carte de Sombrelune[/url]. Les cartes de Sombrelune viennent en huit combinaisons, chacune ayant une suite de l\'As aux Huit. Avec la combinaison de toutes les cartes, la suite est créée qui commencera une quête pour vous envoyer à la foire de Sombrelune. \nChacune des huit suites produit un [url=?items=4.-4&filter=na=carte+sombrelune] bijou [/url] différent avec un effet différent, dont certains sont assez puissants.\n\nLe calendrier habituel de la Foire de Sombrelune arrive sur le site, le premier vendredi du mois et le départ commencera tôt le lundi suivant.',NULL),(8,76,2,NULL,2,'[b]Orgrimmar[/b] est la faction de la capital des orcs : [zone=1637]. Situé au bord nord de [zone=14], la ville imposante abrite le chef de guerre orcs, [npc=4949].\n\n[h3]Histoire[/h3]\n\nThrall a dirigé les orcs vers le continent de Kalimdor, où ils ont fondé une nouvelle patrie avec l\'aide de leurs frères tauren. En nommant leur nouvelle terre, Durotar, nom du père assassiné de Thrall, les orcs se sont installés pour reconstruire leur société autrefois glorieuse. La malédiction démoniaque sur leur race a pris fin, la Horde a décidé de passer d’un discours de conquête avec une coalition lâche à la survie et à la prospérité pour tous. Aidé par les nobles Taurens et les Trolls rusés de la tribu Sombrelance, Thrall et ses orcs attendaient une nouvelle ère de paix dans leur propre pays.\n\nDe là, ils ont commencé la création de la grande ville guerrière, Orgrimmar. Nommé de l\'ancien chef de guerre, Orgrim [color=#ff143c]Doomhammer[/color], la nouvelle ville a été construite en peu de temps, à l\'aide des gobelins, des Taurens, des trolls et de [color=#ff122a]Mok\'Nathal Rexxar[/color]. En dépit d\'avoir des problèmes avec les centaures, les harpies, les lézards de tonnerre enragés, les kobolds, et malheureusement, l\'Alliance, Orgrimmar a prospéré et est devenu le foyer des orcs et des Trolls Sombrelance.\n\nAujourd\'hui, Orgrimmar se trouve à la base d\'une montagne entre Durotar et [zone=16]. Une ville guerrière en effet, elle abrite d\'innombrables quantités d\'Orcs, Trolls, Taurens, et une quantité croissante de Réprouvés rejoignent maintenant la ville, ainsi que les Elfes de Sang qui ont récemment été acceptés dans la Horde.\n\n[h3]Réputation[/h3]\n\n[npc=14726] offre une quête répétitive où il faut fournir des étoffes. Une fois exalté à Orgrimmar, en récompense, les joueurs peuvent acheter des[url=?items=15.5&filter=cr=93:92;crs=2:1;crv=0:0;na=Loup] loups [/url].\n\nLes zones environnantes Durotar et [zone=17] contiennent la plupart des quêtes pour gagner de la réputation avec Orgrimmar.',NULL),(8,530,2,NULL,2,'[b]Les Trolls Sombrelances[/b], tribu de Trolls exilés, ont uni leurs forces avec [npc=4949] et la Horde. Ils appellent maintenant [zone=1637] leur maison, qu\'ils partagent avec leurs alliés Orc. [npc=10540] est leur chef actuel.\n\n[h3]Histoire [/h3]\n\nLorsque les rivalités tribales ont éclaté dans l\'ancien Empire Gurubashi, la tribu Sombrelance s\'est trouvée chassée de sa patrie dans [zone=33]. S\'étant installés dans ce que l\'on croit aujourd\'hui être les îles brisées, la tribu se retrouve bientôt enchevêtrée dans un conflit avec une bande de murlocs. Leur sort semblait scellé jusqu\'à ce que Thrall, chef de guerre Orc, et son armée, nouvellement libérés, s\'emparent de leurs maisons. Contrôlée par une sorcière des mers, un groupe de murlocs a capturé le chef des Sombrelances, Sen\'jin, avec Thrall et plusieurs autres Orcs et Trolls. Thrall a réussi à se libérer avec d\'autres, mais n\'a finalement pas pu sauver le chef des Trolls. Bien que Sen\'jin ait été sacrifié par la sorcière des mers, il a pu révéler une vision qu\'il avait eu, dans laquelle Thrall conduirait les Sombrelances hors des îles.\n\nAprès son retour, Thrall et ses partisans ont réussi à repousser de nouvelles attaques de la sorcière des mers et de ses murlocs, et se sont à nouveau dirigés vers Kalimdor. Sous la direction de [npc=10540], les Sombrelances ont alors juré allégeance à la Horde de Thrall et les ont suivi. Maintenant considérés comme ennemis par toutes les autres tribus Trolls sauf les Vengebroches et les Zandalar, les Sombrelances sont aujourd\'hui méprisés. Pourtant, les Trolls Sombrelances n\'ont pas oublié qu’ils ont été chassés de leurs terres ancestrales et cette animosité gardée est accentuée avec l’impatience, surtout vers les autres tribus Trolls. Après avoir atteint la nouvelle patrie des Orcs, [zone=14], les trolls se sont alors installés sur les rives orientales du royaume Orc, les îles Echo.\n\nCependant, avec l\'arrivée de Kul Tiras et de sa marine, les Sombrelances ont été forcés de reculer à l\'intérieur des terres sous l\'assaut du commandant. Les Trolls, se battant avec la Horde aux côtés de leurs frères, ont vaincu l\'ennemi. Les Trolls ont alors réclamé leur nouvelle patrie. Peu de temps après, un sorcier du nom de [npc=3205] a commencé à utiliser la magie noire pour prendre possession de ses collègues Sombrelances. Au fur et à mesure que son armée de disciples augmentait, Vol\'jin ordonna que les trolls restant évacuent, alors Zalazane prit le contrôle des îles Echo. Les Sombrelances se sont installés sur la rive voisine, en nommant leur nouveau village en hommage à leur ancien chef Sen\'jin. Du village de Sen\'jin, ils envoient, avec leurs alliés, des forces pour combattre Zalazane et son armée asservie.\n\n[h3]Réputation[/h3]\n\n[npc=14727] offre une quête répétitive où il faut fournir des étoffes. Une fois exalté aux Trolls Sombrelances, en récompense, les joueurs peuvent acheter des [url=?items=15.5&filter=na=Raptor;cr=93:92;crs=2:1;crv=0:0] Raptors [/url].\nLa zone environnante, Durotar, contient la plupart des quêtes pour gagner de la réputation avec les Trolls Sombrelances. De plus, les joueurs de niveau supérieur ont également une bonne quantité de quêtes dans [zone=3521].',NULL),(8,92,2,NULL,2,'[b]Les Gelkis[/b] sont une tribu de centaures qui ont construit leur campement dans les parties les plus au sud de [zone=405]. Ce sont les ennemis mortels des [faction=93], une tribu de frère située également dans le sud de Desolace. Le chef fondateur, ou Khan, des Gelkis était [npc=13741], deuxième de la prétendue progéniture de Zaetar et Theradras. Ils sont actuellement dirigés par [npc=5602] et ont pour représentant [npc=5397].\nLes Gelkis ne tiennent aucune alliance avec leurs tribus de frères, mais sont aussi connus pour agir à la fois hostilement et passivement envers les membres de l\'Alliance comme de la Horde.\n\n[h3]Histoire[/h3]\n\nInitialement dirigé par le Second Khan Gelk, les Gelkis se situaient dans les régions les plus au sud de Desolace lorsque la tribu centaure se divisa en cinq.\nLorsque la tribu Gelkis s\'est prononcée contre le Khan Magra, une éternelle querelle entre les Magram et les Gelkis est née.\n\nLes Gelkis considérés comme plus civilisés que leurs frères avec une structure sociale organisée et une compréhension ferme de la langue commune, respectent la nature et leur mère de naissance Theradras. \nAlors que les Magram prônent la force comme essentielle et que la survie de la tribu dépend de leur esprit de combat.\n\nPour alléger ce conflit, Theradras veille toujours sur les centaures et gardera les tribus en sécurité et en vie. Les Gelkis ont alors demandé sa protection et donc le pouvoir de la terre maintien leur existence. \n\nBien que la Magram considère que cela soit faible, il semblerait que ce soit une vue erronée, car des élémentaires peuvent être aperçu dans Village Gelkis, mettant un terme aux intrus indésirables aux côtés de leurs maîtres centaures.\n\n[h3]Réputation[/h3]\n\nC’est une des deux factions situées en Desolace, vous devez avoir une certaine réputation auprès des Gelkis pour commencer leurs quêtes. La réputation pour les Gelkis peut être obtenue en tuant les [url=?Npcs=7&filter=na=Magram]centaures Magram[/url].\n\nVous gagnez 20 points de réputation chez les Gelkis et perds 100 avec la tribu Magram.',NULL),(8,93,2,NULL,2,'[b]Les Magram[/b] sont une tribu de centaures qui construit leur campement dans les parties sud-est de [zone=405]. Ce sont les ennemis mortels de la [faction=92], une tribu de frère située également dans le sud de Desolace. Le chef fondateur, ou Khan, des Magram était [npc=13740], troisième de la prétendue progéniture de Zaetar et Theradras. Ils sont actuellement dirigés par [npc=5601] et ont pour représentant [npc=5398].\nLes Magram ne tiennent aucune alliance avec leurs tribus de frères, mais osont aussi connus pour agir à la fois hostilement et passivement envers les membres de l\'Alliance comme de la Horde.\n\n[h3]Histoire[/h3]\n\nÀ l\'origine menée par le troisième Khan Magra, les Magram se situaient contre les chaînes de montagnes de Desolace lorsque la tribu centaure se divisa en cinq.\nAvant la mort de Magra, il a installé l\'idée que la force était essentielle et que la survie de la tribu dépendait de son esprit de combat. Quand leur frère, la tribu Gelkis, s\'est prononcée contre cette notion, une éternelle querelle entre les deux tribus est née.\n\nLa poursuite de la force a continué à travers les Khans Magram jusqu\'à ce jour, transformant les centaures en des êtres violents et déterminés. Pour solidifier leur titre de plus fort, la tribu lutte encore férocement pour affaiblir ou détruire leurs clans de frères, considérant les Kolkar comme faible, les Gelkis comme une nuisance, et les Maraudon comme un formidable ennemi.\n\nOn peut supposer que la culture Magram s\'est développée autour de la force de culte avant tout. Par rapport aux Gelkis, les Magram tiennent des formes très primitives de la parole et de la structure sociale. Par exemple, leur compréhension commune est limitée et la position de Khan serait vraisemblablement recherchée par un démon de la mort.\n\n[h3]Réputation[/h3]\n\nC\'est une des deux factions situées à Desolace, vous devez avoir une certaine réputation auprès des Magram pour commencer leurs quêtes. La réputation pour les Magram peut être obtenue en tuant [url=?npcs=7&filter=na=Gelkis]les centaures Gelkis[/url]. \n\nVous gagnez 20 points de réputation chez les Magram et perds 100 avec la tribu Gelkis.',NULL),(8,270,2,NULL,2,'Les trolls de la[b] Tribu Zandalar[/b] sont venus à île de Yojamba dans la [zone=33] pour recruter de l\'aide contre le Dieu du sang ressuscité et ses prêtres d\'Atal\'ai dans [zone=19] et [zone=1417].\n\n[h3]Histoire[/h3]\n\nLes Zandalar étaient les premiers trolls connus, tribu d\'où provenaient toutes les tribus. Au fil du temps, deux empires troll distincts ont émergé, l\'Amani et le Gurubashi. Ils existaient pendant des milliers d\'années jusqu\'à l\'avènement des Elfes de la nuit, qui ont combattu avec eux et ont finalement conduit les deux empires à l\'exil.\n\nÀ la suite du Great Sundering, les Gurubashi vaincus sont de plus en plus désespérés. En cherchant un moyen de survivre, ils ont enrôlé l\'aide du sauvage [npc=14834], également appelé Soulflayer. Hakkar s\'est transformé en un oppresseur impitoyable qui a exigé des sacrifices quotidiens de ses sujets, les Gurubashi se sont alors retournés contre leur sombre maître. Les tribus les plus fortes (y compris les Zandalar) se sont regroupées pour vaincre Hakkar et ses fidèles prêtres, les Atal\'ai. Les tribus unies ont vaincu le Dieu des Sang et ont expulsé les Atal\'ai, et malgré leur victoire, l\'Empire Gurubashi tomba peu de temps après.\n\nAu cours des dernières années, les prêtres d\'Atal\'ai ont découvert que la forme physique de Hakkar ne peut être convoquée que dans la capitale ancienne et déserte de l\'Empire Gurubashi, Zul\'Gurub. Malheureusement, au cœur de cette nouvelle quête, les prêtres ont invoqué, avec succès, Hakkar, confirmant la présence du Soulflayer redouté au cœur des ruines.\n\nAinsi, la tribu Zandalar est arrivée sur les rives d\'Azeroth pour combattre encore Hakkar. Mais le dieu du sang est devenu de plus en plus puissant, pliant plusieurs tribus à sa volonté, et même, commandant les avatars des dieux primitifs: chauve-souris, panthère, tigre, araignée et serpent. Avec les tribus trolls éparpillées, les Zandalri ont été forcés de recruter des aventuriers de diverse origine d\'Azeroth pour les rejoindre dans la bataille, et espèrent une fois de plus vaincre, le Soulflayer.\n\n[h3]Réputation[/h3]\n\nLa réputation avec la tribu Zandalar est obtenue en tuant les monstres et boss dans Zul\'Gurub. Des quêtes répétitives et spécifiques sont aussi disponibles, elles requièrent des éléments qui ont été abandonnés dans l’instance. Chaque Zul\'Gurub donne environ 2 500 à 3 000 de réputation.\nAvant la croisade brûlante, la principale raison de monter la réputation avec la tribu était les enchantements [url=?Items=0.6&filter=na=Zandalar]d’épaule[/url], [url=?items=0.6&filter=minrl=60;maxrl=60;cr=18:107;crs=4:0;crv=0:to+a+leg+or+head+slot+item]de tête et de jambe[/url]. De plus, il y avait des pièces d’armure en récompense de quête à faire dans Zul\'Gurub nécessitant un niveau de réputation.',NULL),(8,471,2,NULL,2,'[b]Les Marteaux-hardis[/b] sont un clan de nains actuellement centrés dans [zone=47] et la [zone=3520]. La faction a été supprimée dans le patch 2.0.1.\n\n[h3]Histoire[/h3]\n\nJuste avant le [objet=175739], le clan Marteaux-hardis, dirigé par Thane Khardros Marteaux-hardis, habitait les contreforts et les falaises autour de Forgefer. Le clan Marteaux-hardis a échoué à prendre le contrôle de [zone=1537], des clans Barbe-de-bronze et Sombrefer. Khardros et ses guerriers Marteaux-hardis se sont rendus au nord par les barrières de Dun Algaz et ont fondé leur propre royaume dans le lointain sommet de Grim Batol. Là, les Marteaux-hardis ont prospéré et reconstruit leurs richesses.\n\n[npc=9019] et ses Sombrefer ont juré de se venger de Forgefer. Thaurissan et sa femme sorcière, Modgud, ont lancé un attentat contre Forgefer et Grim Batol. les forces de Modgud ont commencé à franchir les portes de Grim Batol, elle a utilisé ses pouvoirs pour frapper la peur dans leurs cœurs. Les ombres se déplaçaient à son commandement, et des choses sombres se glissaient dans les profondeurs de la terre pour traquer les Marteaux-hardis dans leurs propres retranchements. Finalement, Modgud a franchi les portes et a assiégé la forteresse elle-même. Les Marteaux-hardis se sont battus désespérément, Khardros lui-même s’est lancé dans la bataille pour tuer la sorcière reine. Avec leur reine perdue, les Sombrefer ont fui avant la fureur des Marteaux-hardis.\n\nUne fois que la menace immédiate des Sombrefer a été éliminée, les Marteaux-hardis sont rentrés à Grim Batol. Cependant, la mort du Modgud avait laissé une tache maléfique sur la forteresse de la montagne, et les Marteaux-hardis la trouvaient inhabitable. Khardros a conduit son peuple vers le nord vers les terres de Lordaeron. En s\'installant dans la région montagneuse des Hinterlands, et ces forêts luxuriantes, les Marteaux-hardis ont construit la ville de Nid-de-l’aigle, où les Marteaux-hardis se sont rapprochés de la nature et même liés aux puissants griffons de la région.\n\nLa menace la plus immédiate pour leurs sécurités vient de l\'est sous la forme de deux clans trolls, les Vilebranches et les Fanécorces. Ils sont les plus célèbres pour organiser des batailles contre la ville des Marteaux-hardis, tout en brandissant des armes puissantes.\nLes nains Marteaux-hardis ont un certain nombre de clans, chacun gouverné par un Thane. Le plus fort Thane règne sur Nid-de-l’aigle.',NULL),(8,509,2,NULL,2,'[b]La Ligue d\'Arathor[/b] a été initialement établie par les survivants du Royaume de Stromgarde pour récupérer la [zone=45] des mains des Profanateurs au Trépas d\'Orgrim. Aujourd\'hui, c\'est une organisation à l\'appui de l\'Alliance, basée sur [zone=3358] dans le Refuge de l’Ornière. Ils se sont chargés d\'aider à fournir des forces, pour l\'Alliance, lorsque c’est nécessaire, leurs membres incluent toutes les races de l\'Alliance mais se sont encore principalement des humains stromgardiens.\n\n[h3]Réputation[/h3]\n\nLes joueurs peuvent gagner la réputation dans cette faction en participant au champ de bataille du bassin Arathi. Lorsque vous vous battez dans le bassin d\'Arathi, vous gagnez 10 points de réputation pour 160 ressources. Sur les weekends d’[event=20], les ressources requises sont ramenées à 150.\n\nOn vous accorde le titre, [title=48], une fois exalté avec Ligue d’Arathor et les deux autres factions du champ de bataille, [faction=890] et [faction=730].',NULL),(8,730,2,NULL,2,'[b]Les Gardes Foudrepiques[/b] est la faction de l\'Alliance dans le champ de bataille [zone=2597]. Ils sont une expédition de nains du clan Foudrepique, originaire des « vallées d\'Alterac » dans [zone=36]. La recherche des Foudrepiques pour les reliques de leurs passés et la récolte de ressources dans la vallée d\'Alterac ont conduit à une guerre ouverte avec les Orcs de la [faction=729] habitant dans la partie sud de la vallée. Ils ont également reçu un « ordre de la souveraineté impérialiste » par [npc=2784] pour prendre les vallées d\'Alterac pour [zone=1537].\n\nLa principale base des Foudrepiques est Dun Baldar, où son chef, [npc=11948], réside avec ses maréchaux. Son second commandant, [npc=11949], se trouve au sud de Dun Baldar, à Cœur de pierre.\n\n[h3]Réputation[/h3]\n\nLes joueurs peuvent gagner leurs réputation, dans cette faction, en participant au champ de bataille de la vallée d’Alterac, en faisant diverses tâches et en tuant les membres de la faction adverse, le clan Frostwolf.\n\nOn vous accorde le titre : [title=48] au joueur, une fois qu’il est exalté avec les Gardes Foudrepiques et les deux autres factions des champs de bataille, [faction=890] et [faction=509].',NULL),(8,510,2,NULL,2,'[b]Les Profanateurs[/b] cherchent à feuilleter la [faction=509] dans le champ de bataille, [zone=3358]. Aujourd\'hui, c\'est une organisation à l\'appui de la Horde, basée au Trépas d’Orgrim dans [zone=45]. Ils se sont investis pour aider les forces de la Horde, au besoin, et leurs membres incluent toutes les races de la Horde, même si, se sont encore principalement des Orcs.\n\n[h3]Réputation[/h3]\n\nLes joueurs peuvent gagner la réputation dans cette faction en participant au champ de bataille du bassin Arathi. Lorsque vous vous battez dans le bassin d\'Arathi, vous gagnez 10 points de réputation pour 160 ressources. Sur les weekends d’[event=20], les ressources requises sont ramenées à 150.\n\nOn vous accorde le titre, [title=48], une fois exalté avec les Profanateurs et les deux autres factions du champ de bataille, [faction=889] et [faction=729].',NULL),(8,529,2,NULL,2,'L’[b]Aube d’Argent[/b] est une organisation axée sur la protection d\'Azeroth des menaces qui cherchent à la détruire, comme la Légion Ardente et le Fléau. Les forteresses de l\'Aube d\'Argent se trouvent dans les [zone=139] et les [zone=28]. Elle maintient également une présence dans [zone=1657] et dans les [zone=85], et dans d’autres zones moins remarquables. La réputation avec l’Aube d’Argent peut être utilisée pour acheter divers plans, consommables, et pour atténuer le coût à [zone=3456]. Avec l\'expansion « Burnning Croisade », la réputation de l’Aube d’Argent a diminué en valeur.\n\nLe [item=22999] a pour icône un lever de soleil argenté.\n\n[h3]Histoire[/h3]\n\nAprès la mort du [npc=16062], la corruption de la Croisade Écarlate est devenu évidente pour certains de ses membres, qui ont par la suite abandonné les rangs de la [url=?npcs&filter=na=croisade%20écarlate;ex=on]Croisade Écarlate[/url] et a créé l’Aube d’Argent pour protéger Azeroth de la menace du Fléau sans présence de fanatique dans la Croisade Écarlate.\n\nAlors qu\'ils partagent les mêmes objectifs que la Croisade, l’Aube d’Argent a ouvert ses rangs non seulement aux races de l\'Alliance, mais aussi aux membres de la Horde et même à certains des Réprouvés. Ils mettent en garde contre la discrétion et l\'introspection, et mettent beaucoup l\'accent sur la recherche du Fléau et sur la façon de le combattre.\n\nAvec le temps, l’Aube d’Argent s\'est diversifié, comme le Fléau qui s\'est divisé de nouveau, avec un rejeton appelé la Fraternité de la Lumière, un compromis entre l\'approche plus savante de l’Aube d’Argent et le fanatisme de la Croisade écarlate.\n\n[h3] Réputation [/h3]\n\n[b]Les pierres du Fléau[/b]\nTout en portant un bijou accordant l\'effet « Commission pour l’Aube d’Argent », les personnages peuvent tuer des monstres mort-vivants pour leurs [url=?items=12&filter=cr=151;crs=6;crv=43169;na=pierre%20du%20fléau] pierres du Fléau[/url] et ensuite les transformer en monnaies échange contre [item=12844]. Les quêtes requièrent beaucoup de [item=12843], [item=12841] et [item=12840]. Il convient de noter que les monnaies d’échanges reçus des entités doivent être sauvegardés jusqu\'à ce que le statut de Révéré soit atteint, car les quêtes ne donneront plus de réputation après.\n\nUne autre façon d’augmenter la réputation avec l’Aube d’Argent est de faire la quête répétable « Chaudron ». Les chaudrons sont une source de « production » de membres du Fléau.\n\nComme la plupart des factions, le joueur peut faire des instances pour augmenter sa réputation. Les instances associées sont [zone=2017] et [zone=2057]. Naturellement, ces instances incluent également des quêtes qui augmentent la réputation de l’Aube d’Argent.',NULL),(8,933,2,NULL,2,'[b]Le Consortium[/b],dirigé par [npc=19674], sont des passeurs éthérés, des commerçants et des voleurs qui sont venus en Outreterre. Le principal base d\'opérations et le plus grand rassemblement se trouve à Foudreflèche, mais ils peuvent être trouvés à[color=#ff0537] Midrealm Post[/color], Aeris Landing, près d\'Auchindoun à [zone=3792] et dans d\'autres endroits.\n\nEn arrivant à un statut amical, les joueurs sont officiellement considérés comme membres du Consortium et bénéficient d\'un salaire. Le salaire est un sac de gemmes au début de chaque mois, donné par [npc=18265] chez Aeris Landing. Une plus grande réputation avec le Consortium produit des qualités et quantités supérieures de gemmes chaque mois.\n\n[h3]Réputation[/h3]\n\n[b]Jusqu\'à Amical[/b]\n[ul]\n[li]Faire le donjon Tombe-mana en [i]mode normal[/i] rapporte environs 1 200 points de réputation[/li]\n[li]Donner des [item=25416] à [npc=18265].[/li]\n[li]Donner des [item=25463] à [npc=18333].[/li]\n[/ul]\n\n[b]De amical à honoré[/b]\n[ul]\n[li]Faire Tombe-mana en [i]mode normal[/i] rapporte environs 1 200 point de réputation.[/li]\n[li]Activer les [item=25433] à [npc=18265].[/li]\n[li]Donner des [item=29209] à [npc=19880].[/li]\n[/ul]\n\n[b]De honoré à exalté[/b]\n[ul]\n[li]Faire Tombe-mana en [i]mode héroïque[/i] rapporte environs 2 400 points de réputation.[/li]\n[li]Faire toutes les [url=?Quêtes et filtre=cr=1;crs=933;crv=0]quêtes[/url].[/li]\n[li]Donner des [item=25433] à [npc=18265].[/li]\n[li]Donner des [item=29209] à [npc=19880].[/li]\n[/ul]\n\nToutes personnes qui essayent de gagner simultanément la réputation du Consortium et des [faction=941] ou [faction=978] peuvent se concentrer à tuer des ogres ([url=?npcs&filter=na=rochepoing;cr=6;crs=3518;crv=0]Rochepoing[/url], [url=?npcs&filter=na=cogneguerre;cr=6;crs=3518;crv=0]Cogneguerre[/url]) à Nagrand et rendre les perles de guerre obsidienne au Consortium.\n\nLa seule mise en garde est le taux de loot, soit environ 33% pour les Cogneguerre, alors qu\'il est de 50% pour les insignes. Si vous êtes au niveau 70 et que vous voulez monter cette réputation plus rapidement sans se soucier de la réputation de Mag\'har / Kurenai, vous voudrez peut-être donner des insignes à la place. Ensuite, les ogres sont généralement plus faciles à tuer, allant du niveau 65 à 67. Le choix dépend finalement du joueur.',NULL),(8,932,2,NULL,2,'[b]L\'Aldor[/b] est un ancien ordre de prêtres draeneïs qui vénèrent les naaru, et à ce jour ils assistent les naaru [faction=935] dans leur combat contre [npc=22917] et la Légion Ardente. Ils se trouvent principalement dans la [zone=3520] et [zone=3703]. Bien qu\'ils aient beaucoup souffert des Elfes du sang qui sont devenus [faction=934], ont mis de côté une guerre ouverte contre les Sha\'tar. Le temple le plus saint de l\'Aldor repose sur l’éminence de l\'Aldor, surplombant la ville à l\'ouest.\n\nLa plupart des joueurs commenceront à une réputation neutre auprès de l\'Aldor. [npc=18166] à Shattrath donnera aux joueurs une première quête pour devenir amical avec Aldor ou Les clairvoyants. Ce choix est réversible si les joueurs ressentent le besoin.\nLes joueurs de Draenei seront directement amicaux avec Aldor et hostiles avec les Clairvoyants, alors que les joueurs Elfe du sang seront hostiles à l\'Aldor et amicaux envers les Clairvoyants.\n\n[npc=19321] et [npc=20807] sont situés dans la banque Aldor, sur le bord nord de la terrasse de la lumière. Le sanctuaire de la lumière sans fin sur l’éminence de l\'Aldor abrite [npc=20616] [petit][/small] et [npc=21906] [petit][/small], qui échangent, respectivement, des jetons épiques d\'armure contre des pièces de set de [url=?Itemsets&filter=ta=12]Niveau 4[/url] et de [url=?Itemsets&filter=ta=13]Niveau 5[/url].\n\n[i]Note : Les gains de réputation avec Aldor correspondent à une perte de réputation de 10% plus élevée chez les Clairvoyants. La plupart des gains de réputation avec Aldor accorderont également 50% de la réputation avec le Sha\'tar.[/i]\n\n[h3]Réputation[/h3]\n\n[b]Jusqu\'à honoré[/b]\n\nLes joueurs qui cherchent à gagner les rangs de réputation supérieurs (Révéré, Exalté) peuvent vouloir sauver des quêtes non répétables jusqu\'à ce qu\'ils soient honorés.\n\nDonner 10 [span class=q1][item=29425][/span] à [npc=18537] dans l’éminence de l\'Aldor accordera 250 points de réputation pour l\'Aldor. Il existe également une quête répétable où donner une unique marque accorde 25 points de réputation. Ces marques tombent sur des membres inférieurs de la Légion Ardente trouvés dans la plupart des zones de Outreterre, y compris les deux camps au nord d\'Auchindoun dans les déchets osseux de [zone=3519].\nEnviron 240 marques sont nécessaires pour passer d\'amical à honoré.\nEn outre, ces quêtes fournissent de la réputation de Sha\'tar ; 125 points de réputation pour 10 marques ou 12,5 points de réputation pour une unique marque.\n\nLes joueurs qui souhaitent également faire la réputation des factions [faction=978] ou [faction=941] iront tuer des Orcs à la forteresse de Kil\'Sorrow dans le sud-est de [zone=3518], car ils donnent des marques ainsi que 10 points de réputation auprès des Kurenai ou des Mag\'har.\n\n[b]Jusqu\'à Exalté[/b]\n\nUne fois que vous atteignez le niveau 68, vous pouvez également donner 10 [span class=q1] [item=30809][/span], c\'est le même principe que les marques de Kil\'jaeden mais ceux-ci tombent sur des partisans de haut rang de la Légion Ardente. Si vous le souhaitez, vous pouvez transformer les marques de niveau supérieur avant la réputation honorée. Dans [zone=3522], la porte de la mort dispose du plus grand nombre de membre avec ce grade.\n\n[b]Arme gangrenée[/b]\n\n[span class=q2][item=29740][/span] peut être donné à tout moment à [npc=18538] [small][/small] à l’éminence de l\'Aldor. Cela augmentera votre réputation avec l\'Aldor de 350 par arme gangrenée.\nEn plus des gains de réputation, vous recevrez [span class=q1][item=29735][/span], qui est la condition pour acheter l’enchantement d\'épaule à [npc=20807] dans la banque de l\'Aldor.\n\n[h3]Passer à la réputation de l\'Aldor[/h3]\n\nPour changer votre faction des Claivoyants vers l\'Aldor et donc pour accéder à leurs recettes d\'artisanat (et annuler toutes les réputations que vous avez faites), trouvez [npc=18597], un membre de l\'Aldor dans la ville basse. Elle propose une quête répétable où pour 8x [span class=q1][item=25802][/span] vous montez la réputation Aldor. Une fois que vous êtes neutre, vous ne pourrez plus recevoir cette quête.',NULL),(8,922,2,NULL,2,'[b]Tranquilliens[/b] a été reprise par les Réprouvés et les Elfes de sang puis est devenu une faction des [zone=3433].\n\n[h3]Histoire[/h3]\n\nAlors que l\'armée du Fléau faisait son chemin vers le Puit-du-Soleil, les elfes n\'avaient pas d\'autre choix que de se retirer, Tranquillien fût donc abandonnée. La ville est maintenant utilisée par les Elfes de sang et les Réprouvés comme base d\'opération pour lancer des attaques visant à reprendre les Terres Fantômes. Cependant, la ville est entourée par le fléau, même les courriers ont du mal à traverser l\'ennemi pour atteindre la ville. Les forces mortels de Mortholme sont la menace la plus dangereuse pour la ville.\n\n[h3]Réputation[/h3]\n\nContrairement à la plupart des zones de départ, la ville de Tranquillien a sa propre faction.\nToutes les quêtes que vous effectuez pour eux accumuleront au moins 1000 points de réputation. [npc=16528] agit comme l’intendant des Tranquilliens. Vredigar peut être trouvé près de l\'auberge et vendra divers éléments [span class=q2]commun[/span], et même un manteau [span class=q3]rare[/span] lorsque vous atteignez la réputation exaltée.\n\nSi vous complétez toutes les quêtes des Tranquilliens, vous devriez être exalté.\nIl existe une variété de quêtes concernant principalement la récupération des villages envahis, l\'enquête sur les morts-vivants et l\'aide apportée à la population. La suite de quête prend « fin » avec la quête où il faut tuer [npc=16329].',NULL),(8,910,2,NULL,2,'La [b]Progéniture de Nozdormu[/b] est une faction composée du vol Draconique de bronze. Leur chef, [npc=15192], se trouve à l\'extérieur des [b]Grottes du temps[/b], avec beaucoup de ses agents volant dans le ciel de [zone=1377].\n\nPour ouvrir les portes d’[b]Ahn\'Qiraj[/b], un champion doit compléter une longue ligne de quête pour le dragon de bronze Anachronos. Cette réputation est également présente dans [zone=3428]; Elle permet d’obtenir des équipements et des bagues épiques.\n\n[h3]Réputation [/h3]\n\nLes joueurs commencent leur réputation au plus bas niveau possible, c’est–à-dire 0/36000 de détestés.\n\nLa réputation de la Progéniture de Nozdormu peut être gagnée en tuant des monstres à l\'intérieur du temple d\'Ahn\'Qiraj et en faisant des quêtes liées. Vous pouvez également exploiter [item=20384], cela prend beaucoup plus de temps et nécessite l\'obtention de [item=20383] dans [zone=2677] pour la suite de quête [item=21175].\n\nTuer des monstres dans le temple d\'Ahn\'Qiraj ne permet que d’atteindre une réputation de 2999/3000 de neutre, la réputation ne peut donc être avancée que par des quêtes et la remise de [item=21229] et [item=21230]. \nUn conseil, gardez tous les insignes jusqu\'à ce que vous soyez à une réputation neutre, car à ce moment-là, cela devient beaucoup plus difficile.',NULL),(8,749,2,NULL,2,'Les [b]Hydraxiens[/b] sont des élémentaires qui se sont installés sur les îles à l\'est de [zone=16]. Les ennemis jurés des armées de [npc=11502]. Historiquement serviteurs des Anciens Dieux, les quatre Lords Élémentaires ont servi les dieux avec une loyauté éternelle. Les minions de Neptulon, le chasse-marée, étaient nombreux et insensés. On ne sait pas encore comment le [npc=13278] a libéré le contrôle de son seigneur ou quels sont ses objectifs ultimes, mais les élémentaires d’eau sont les seuls éléments qui n\'attaquent pas les races mortelles.\n\nSitué sur une île éloignée dans l\'extrême est d\'Azshara, le Duke Hydraxis propose des quêtes. Les deux premiers nécessitent de tuer divers élémentaires dans les [zone=139] et en [zone=1377]. Une réputation accrue avec les Hydraxiens ouvre des quêtes supplémentaires menant à [zone=2717]. Tous les objets obtenus auprès des Hydraxiens sont gagnés à partir de différentes missions.\n\nL\'achèvement de la suite de quête permet aux joueurs d\'obtenir [item=17333] utilisé pour endommager les runes trouvées près de la plupart des boss dans Cœur de Magma. Ceci est nécessaire pour convoquer [npc=12018], l\'avant-dernier boss, et, après sa défaite, pour convoquer Ragnaros lui-même. Comme il y a sept runes, tout raid nécessite au moins sept joueurs qui apportent une quintessence s\'ils souhaitent terminer l\'instance. Comme la majeure partie de la suite de quête a lieu au sein de Cœur de Magma, toutes personnes du raid peuvent compléter cette tâche avec un peu plus que quelques voyages et une course au [zone=1583].\n\n[h3] Réputation [/h3]\n\nLa réputation des Hydraxiens est obtenue en tuant les ennemis élémentaires suivants :\n[ul][li] [npc=11746] - 5 points de réputation, jusqu\'à l\'Honoré. [/li]\n[li] [npc=11744] - 5 points de réputation, jusqu\'à Honoré.[/li]\n[li] [npc=7032] - 5 points de réputation, jusqu\'à Honoré.[/li]\n[li] [npc=9017] - 15 points de réputation, jusqu\'à Révéré. [/li]\n[li] [npc=14478] - 25 points de réputation, jusqu\'à Révéré. [/li]\n[li] [npc=9816] - 50 points de réputation, jusqu\'à Révéré. [/li]\n[li] [npc=11658], [npc=11673], [npc=12101] et [npc=11668] - 20 points de réputation, jusqu\'à Révéré. [/li]\n[li] [npc=11659], [npc=12100], [npc=12076], [npc=11667] et [npc=11666] - 40 points de réputation, jusqu\'à Révéré. [/li]\n[li] [npc=12118], [npc=11982], [npc=12259], [npc=12057], [npc=12056], [npc=12264] et [npc=12098] - 100 points de réputation, jusqu\'à Exalté. [/li]\n[li] [npc=11988] - 150 points de réputation, jusqu\'à Exalté. [/li]\n[li] [npc=11502] - 200 points de réputation, jusqu\'à Exalté. [/li][/ul]\n\nLa réputation au statut de Révéré avec les Hydraxiens permet aux joueurs d’obtenir le [item=22754], qui se recharge. Et donc évite la nécessité de retourner à Hydraxis pour obtenir une nouvelle quintessence chaque semaine.',NULL),(8,609,2,NULL,2,'Le [b]Cercle Cénarien [/b] est une organisation de druides, à la fois tauren et elfe de nuit, nommé d\'après Cénarius. Ses membres se consacrent à la protection de la nature et à la restauration de celle-ci suite aux dégâts subis par des forces malveillantes.\n\nLe Cercle a de nombreux sites, mais leur ville principale est la ville de Havre- nuit dans la [zone=493]. Les druides apprennent le sort [sortilège=18960] au niveau 10, mais il est aussi possible d’y arriver par [zone=361] via le tunnel des Grumegueles.\n\nLe cercle Cénarien est aussi beaucoup présent en [zone=1377], où ils combattent les Silithides, les Qirajis et l’armée du crépuscule. Le repos du vaillant et le Fort Cénarien servent de base dans ces terres hostiles et offrent de nombreuses opportunités aux aventuriers qui cherchent à aider les druides.\n\n[h3]Membres notables[/h3]\n\n[ul][li][npc=11832], fils de Cenarius [/li]\n[li][npc=3516], chef des druides - elfes de la nuit [/li]\n[li][npc=5769], chef des druides - Taurens [/li][/ul]\n\n[h3]Réputation[/h3]\n\nIl existe plusieurs façons de se faire connaître avec le cercle Cénarien.\nMise à part les [url=?Quests&filter=cr=1;crs=609;crv=0]quêtes[/url], vous pouvez faire ce qui suit pour gagner en réputation: \n[ul]\n[li]Le raid des [zone=3429] est de loin le moyen le plus rapide de gagner en réputation, car un clean complet peut dépasser 2000 points de réputation. [/li]\n[li] Tuez l’armée du crépuscule. Elle cesse d’augmenter une fois que vous atteignez la réputation Honoré pour [npc=11880] et [npc=11881], et Révéré pour [npc=15201].[/li]\n[li] Trouvez des [item=20404 ]. Ceux-ci se trouvent sur l’armée du crépuscule et produisent 250 points de réputation pour 10 textes.[/li]\n[li] Trouvez des [item=20513], [item=20514] et [item=20515]. Ceux-ci se trouvent sur les mini-boss qui sont convoqués aux pierres de vent en utilisant [itemset=492]. [/li]\n[li] Effectuez la quête : [quest=8507]. Ce sont soit des [url=?search=logistique+Briefing] Quêtes de logistique [/url], des [url=?search=combat+Briefing]quêtes de Combat[/url] ou des [url=?search=tactique+Briefing] Quêtes tactiques [/url]. Les badges que vous gagnez de ces quêtes peuvent être transformés en réputation supplémentaire, si vous choisissez d\'abandonner les récompenses. [/li]\n[li] Collectez les [object=181598] de la zone et rendez les à votre faction.[/li]\n[/ul]',NULL),(8,589,2,NULL,2,'Les [b]Éleveurs de sabres-d\'hiver[/b] est une faction de l\'Alliance composée de deux Elfes de la nuit qui peuvent être trouvés au [zone=618]. À l\'heure actuelle, le seul donneur de quête est [npc=10618], qui est situé au sommet du Rocher des Sabres-d\'hiver au Berceau-de-l’hiver. En atteignant un niveau de réputation exalté avec cette faction, Rivern vendra une monture spéciale, le [item=13086].\n\nLa monture de cette faction est la seule monture épique, ayant une vitesse de 100%, utilisable avec une compétence en équitation de 75. La faction est connue pour ne pas avoir d’équivalant côté Horde et être la plus longue et la plus répétitive des réputations à monter dans l\'ensemble du jeu. La première quête peut être faite au niveau 58, tandis que les deux autres sont réalisables qu’au niveau 60.\n\n[h3]Réputation[/h3]\n\nLa réputation avec les Éleveurs de sabres-d\'hiver ne peut être obtenue que par trois quêtes répétables. Il n\'y a pas d\'objets de faction ni de mobs qui récompensent la réputation directement.\n\n[b]De neutre 0 à 1500[/b]\n\nUne seule quête répétable sera disponible jusqu\'à ce qu’une réputation de 1500/3000 soit atteinte, la quête : [quest=4970] doit donc être répétée. Tous les [url=?npcs&filter=cr=6;crs=618;crv=0;na=Croc%20acéré]Ours[/url] et [url=?npcs&filter=cr=6;crs=618;crv=0;na=Noroît]Noroît[/url] au Berceau-de-l’hivers peuvent looter les objets de quête. Cette quête doit être effectuée en solo, car les taux de loot sont faibles et ne sont pas partageables si d\'autres ont la quête.\n\n[b]De neutre 1500 à exalté [/b]\n\nÀ mi-chemin du neutre, la quête : [quest=5201] sera disponible. Cette quête nécessite de tuer 10 Tombe-hivers dans le village Tombe-hivers, juste à l\'est de Long-guet. Si la quête : [quest=8464] a été effectuée pour [faction=576], les [item=21383] peuvent tomber sur les Tombe-hivers. Si un joueur veut les deux réputations, il préférable qu’il les gardes jusqu’à ce qu’il soit Révéré avec les Grumegueules. Ce qui entraînera beaucoup de réputation \"gratuite\".\n\nCette quête peut se faire en groupes pour aller plus vite. Les joueurs qui augmentent les réputations des Éleveurs de sabres-d\'hiver et des Grumegueules peuvent être trouvés dans le village des Tombe-hivers. Même en épique, le voyage vers le village Tombe-hivers prend beaucoup de temps. Il y a des tigres sur la route qui vous étourdiront, ce qui entraînera un désarçonnement, cela devrait être évité (mais peut être difficile car ils vont vous rattraper sur une monture de 60%). \n\n[b]De honoré à exalté[/b]\n\nA partir d’honoré, la troisième quête : [quest=5981] est disponible. La quête exige que le joueur tue 8 géants. Ils sont beaucoup plus difficiles que les Tombe-hivers et le trajet est assez long. Cette quête est généralement ignorée.\n\nEn raison de certains joueurs qui augmentent la réputation des Grumegueules, dans le village de Tombe-hivers, cette quête peut effectivement se révéler une récompense de réputation plus rapide que [quest=5201].',NULL),(8,576,2,NULL,2,'[b]Les Grumegueules[/b], dernière tribu furbolg non-corrompue (au moins dans leur point de vue), cherchent à conserver leurs voies spirituelles et à mettre fin à la souffrance de leurs frères.\n\nLes Grumegueules habitent deux zones : [zone=16] et [zone=361]. Ils sont présumés être la seule tribu furbolg à échapper à la corruption démoniaque, mais ce n\'est peut-être pas vrai, en raison de l\'existence de [npc=3897], furbolg de tribu inconnue, et la tribu Stillpine sur [zone=3524]. Cependant, de nombreuses autres races tuent les furbolgs aveuglément maintenant, sans savoir si elles sont alliées ou non. Pour cette raison, les Grumegueles ne se montrent pratiquement pas.\n\nLes aventuriers qui recherchent les Grumegueules dans le nord de Gangrebois et s\'aventurent chez eux apprendront qu’il faut mieux être leurs alliés. Bien qu\'ils ne possèdent pas de bijoux fins ou de richesses mondaines, la tradition chamanique des Grumegueules est encore forte. Ils connaissent bien l\'art de fabriquer des armures à partir de peaux d\'animaux, et ils sont plus qu\'heureux de partager leurs connaissances de guérison avec des amis de leur tribu. En outre, à partir d’une réputation inamical, les Grumegueules vous accorderont également un accès sans problème à [zone=493] et [zone=618] dans leurs tunnels.\n\n[h3] Réputation[/h3]\n\nLa réputation avec la faction des Grumegueules est principalement acquise grâce à des quêtes. Les membres de la tribu Mort-bois, une autre tribu de Furbolg à Gangrebois, sont les principaux ennemis des Grumegueules et peuvent être tué pour gagner de la réputation.\n\n[ul]\n[li] Tuer des furbolgs [url=?Npcs&filter=na=Tombe-hivers]Tombe-hivers[/url] ou [url=?Npcs&filter=na=Mort-bois]Mort-bois[/url], donne 10 points de réputation. Les gains s\'arrêtent à révéré. [/li]\n[li] Tuer [npc=9464] ou [npc=9462], donne 60 points de réputation.[/li]\n[li] Tuer [npc=10738], située dans une grotte à l\'est de [faction=577], donne 50 points de réputation. Son taux de réapparition est de 6 à 8 minutes. [/li]\n[li] Tuer [npc=14342], élite rare, donne 50 points de réputation. Il se situe au village des Mort-bois à Gangrebois. Donne de la réputation jusqu’à exalté. [/ Li]\n[li] Tuer [npc=10199], élite rare, donne 50 points de réputation. Il se situe dans le village des Tombe-hivers au Berceau-de-l’Hivers. Donne de la réputation jusqu’à exalté. [/li]\n[li] Après avoir terminé la quête : [quest=8460], avec les [item=21377] ramassés sur les Furbolgs Mort-bois, la réputation augmente de 150 points. [/li]\n[li] Après avoir terminé la quête : [quest=8464], avec les [item=21383] ramassés sur les furbolgs Tombe-hivers, la réputation augmente de 150 points.[/li]\n[/ul]',NULL),(8,890,2,NULL,2,'[b]Les Sentinelles d\'Aile-argent[/b] représente la faction de l\'Alliance sur le champ de bataille [zone=3277]. Les elfes de la nuit, qui ont commencé une avancée massive pour reprendre les forêts de [zone=331], concentrent leur attention sur le débarquement sur leur terre de la [faction=889] une fois pour toutes. Et ainsi, les Sentinelles d\'Aile-argent ont répondu à l\'appel et ont juré qu\'ils ne vont pas se reposer avant que tous les orcs soient vaincus et expulsés du Goulet des Chanteguerres.\n\n[h3]Réputation[/h3]\n\nLes joueurs peuvent gagner leurs réputations, dans cette faction, en participant au champ de bataille du Goulet des Chanteguerres. Vous gagnez 35 points de réputation à chaque fois que votre faction capture un drapeau. Ce gain de réputation est augmenté à 45 les week-ends du champ de bataille.\n\nOn vous accorde le titre : [title=47] une fois qu’il est exalté avec Les Sentinelles d\'Aile-argent et les deux autres factions des champs de bataille, [faction=730] et [faction=509].',NULL),(8,889,2,NULL,2,'[b]Les Voltigeurs Chanteguerre[/b] est un clan orc précédemment dirigé par [npc=18076], d’après lequel le clan a été nommé. Les Voltigeurs Chanteguerre représentent la faction de la Horde sur le champ de bataille [zone=3277], où ils tentent de défendre leurs opérations d\'enregistrement dans [zone=331] de la [faction=890].\n\nC’est l\'un des clans les plus forts et les plus violents, le clan de Chanteguerre était également l\'un des clans les plus distingués de Draenor, ce clan a pu échapper aux forces de l\'expédition de l\'Alliance à chaque tournant. Formés comme Grunts, ils ont maîtrisé l\'utilisation d\'épées et de lames et quelques-uns ont même atteint le rang de Maître-lames.\n\n[h3]Réputation[/h3]\n\nLes joueurs peuvent gagner leurs réputations, dans cette faction, en participant au champ de bataille du Goulet des Chanteguerres. Vous gagnez 35 points de réputation à chaque fois que votre faction capture un drapeau. Ce gain de réputation est augmenté à 45 les week-ends du champ de bataille.\n\nOn vous accorde le titre : [title=47] une fois qu’il est exalté avec Les Voltigeurs Chanteguerre et les deux autres factions des champs de bataille, [faction=510] et [faction=729].',NULL),(8,729,2,NULL,2,'[b]Le Clan Loup-de-givre[/b], ainsi que [npc=11946], ont vécu dans [zone=36] et ont des Loups de givre comme compagnons. Des nains, connue sous le nom de [faction=730], ont commencé une expédition dans le territoire des Loup-de-givre pour creuser la vallée et miner les veines. Une transgression envers les Orcs qui habitaient en Alterac. Cela a provoqué l’extermination de la première expédition et la bataille pour [zone=2597] a commencé.\n\n[h3]Réputation[/h3]\n\nLes joueurs peuvent gagner leurs réputation, dans cette faction, en participant au champ de bataille de la vallée d’Alterac, en effectuant diverses tâches et en tuant les membres de la faction opposée, les Gardes Foudrepiques.\n\nOn vous accorde le titre : [title=47] au joueur une fois qu’il est exalté avec le clan Loup-de-givre et les deux autres factions des champs de bataille, [faction=889] et [faction=510].',NULL),(8,935,2,NULL,2,'[b]Les Sha\'tar[/b], ou \"né de la lumière\", sont des naaru qui ont aidé [faction=932], l\'ordre des prêtres draenei précédemment dirigés par [npc=17468], en reconstruction à [zone=3703]. La ville a été détruite par les Orcs pendant leur fuite à travers Draenor avant la Première Guerre mondiale. \nLa défaite de la Légion ardente est le but ultime des Sha\'tar. Les Sha\'tar sont aidés dans cette guerre par l\'Aldor et leurs rivaux, la faction des elfes du sang connue sous le nom : [faction=934]. \nL\'Aldor et les Clairvoyants se battent pour la faveur du Sha\'tar afin qu\'ils puissent être aidés dans leur guerre pour les pouvoirs des naaru. L\'entité qui dirige le Sha\'tar est connue sous le nom de [npc=18481] ; Il peut être trouvé sur la terrasse de la lumière dans la ville de Shattrath.\n\nLes joueurs de l\'Alliance et de la Horde commencent avec une réputation neutre auprès des Sha\'tar. Les joueurs peuvent augmenter leur réputation, Sha\'tar, à travers diverses quêtes, en élevant leur réputation avec l’Aldor ou les clairvoyants, ou en s\'aventurant dans le [url=?search=donjon+tempête]donjon des tempêtes [/url].\n\n[h3]Réputation[/h3]\n\n[b]Jusqu\'à honoré[/b]\n\nLa réputation peut être obtenue à partir de divers objets. Ce qui suit n\'accordera que de la réputation de Sha\'tar jusqu\'à ce que vous obteniez un statut honoré : \n[li]Pour une réputation envers les Clairvoyants : [item=29426], [item=30810] et [item=29739][/li]\n[li]Pour une réputation envers l\'Aldor : [item=29425], [item=30809] et [item=29740][/li]\n\n[i]Notez que ce gain de réputation ne s\'affiche pas dans le journal de combat, mais peut être vérifié en regardant votre panneau de réputation.[/i]\n\nLa réputation peut également être obtenue en faisant le temple des tempêtes : [zone=3847], [zone=3846] et [zone=3849].\n\n[b]Jusqu’à exalté [/b]\n\nAprès avoir épuisé les récompenses de réputation de Aldor ou des Clairvoyants, les joueurs souhaiteront peut-être compléter les quelques quêtes de Sha\'tar disponibles. En plus des quêtes, les instances qui se trouvent au temple des tempêtes : Botanica, Arcatraz et Mechanar continueront à accorder de la réputation. À ce stade, il est probablement plus utile d\'exécuter ces instances en mode héroïque.',NULL),(8,934,2,NULL,2,'[b]Les Clairvoyants[/b] sont des elfes de sang qui résident dans [zone=3703] dirigé par [npc=18530]. Le groupe s\'est éloigné de [npc=19622] et a offert de leur aide au Naaru de Shattrath. Ils sont en désaccord avec [faction=932], et rivalisent avec eux pour le pouvoir de Shattrath et la faveur du Naaru. \n\nLa plupart des joueurs commenceront avec une réputation neutre auprès des Clairvoyants. [npc=18166] à Shattrath donnera aux joueurs une première quête pour devenir amical avec l’Aldor ou Les Clairvoyants. Ce choix est réversible si les joueurs ressentent le besoin. \nLes joueurs d’elfes de sang seront amicaux avec les Clairvoyants et hostiles avec l\'Aldor, alors que les joueurs draenei seront hostiles aux Clairvoyants et amicaux envers l’Aldor.\n\n[npc=19331] et [npc=20808] sont situés dans la banque des Clairvoyants, sur le bord sud de la terrasse de lumière. La Bibliothèque du Visiteur abrite [npc=20613] [small][/small] et [npc=21905] [small][/small], qui échangent des pièces d\'armure épique contre des pièces de set de[url=?Itemsets&filter=ta=12]Niveau 4[/url] et de [url=?Itemsets&filter=ta=13]Niveau 5[/url].\n\n[i]Note : Les gains de réputation avec les Clairvoyants correspondent à une perte de réputation de 10% plus élevée chez l’Aldor. La plupart des gains de réputation avec les Clairvoyants accorderont également 50% de la réputation avec [faction=935].[/i]\n\n[h3]Tradition [/h3]\n\nAprès avoir subi des assauts implacables de leurs ennemis, les gardes harassés de Sha\'tar et de l’Aldor se sont regroupés pour la prochaine attaque alors qu\'elle marchait sur l\'horizon. Cette fois, l\'attaque provenait des armées de [npc=22917]. Un grand régiment d\'elfes de sang avait été envoyé par l\'allié d\'Illidan, le prince Kael\'thas pour détruit la ville. Alors que le régiment d\'elfes de sang traversait le pont, les exarques et les vindicateurs de l’Aldor se sont alignés pour défendre la Terrasse de Lumière. Alors l\'inattendu arriva, les elfes de sang déposèrent leurs armes devant les défenseurs de la ville.\nLeur chef, un ainé de sang connu sous le nom de Voren\'thal, a exigé de parler au naaru [npc=18481]. À mesure que le naaru s\'approchait de lui, Voren\'thal s\'agenouilla et prononça les mots suivants : « Je vous ai vu dans une vision, naaru. Le seul espoir de survie de ma race est avec vous. Mes disciples et moi-même sommes là pour vous servir ».\nLa défection de Voren\'thal et de ses partisans a été la plus grande perte jamais subie par les forces de Kael\'thas. Beaucoup des plus forts et les plus brillants parmi les savants et les magistrats de Kael\'thas ont été influencés par l\'influence de Voren\'thal. Le naaru a accepté les déflecteurs qui sont devenus connus sous le nom de Clairvoyant.\n\n[h3]Réputation[/h3]\n\n[b]Jusqu\'à honoré[/b]\n\nLes joueurs qui cherchent à gagner les rangs de réputation supérieurs (Révéré, Exalté) peuvent vouloir sauver des quêtes non répétables jusqu\'à ce qu\'ils soient honorés.\n\nDonner 10 [span class=q1][item=29426][/span] à [npc=18531] dans la bibliothèque du Visiteur des Clairvoyants accordera une réputation de 250 points de réputation pour les Clairvoyants. Il existe également une quête répétable où donner une unique chevalière accorde 25 points de réputation. Ces chevalières tombent sur des membres Aile-de feu dans la partie nord-est de la forêt de Terrokar. \nEnviron 240 marques sont nécessaires pour passer d\'amical à honoré.\nEn outre, ces quêtes fournissent de la réputation de Sha\'tar ; 125 points de réputation pour 10 marques ou 12,5 points de réputation pour une unique chevalière.\n\n[b]Jusqu\'à exalté [/b]\n\nUne fois que vous atteignez le niveau 68, vous pouvez également donner 10 [span class=q1][item=30810][/span], c’est le même principe que les chevalières mais ceux-ci tombent sur des elfes de sang Solfurie de haut rang. Si vous le souhaitez, vous pouvez transformer les chevalières de niveau supérieur avant une réputation honorée. Vous les trouverez dans [zone=3523], [zone=3520] et les instances du [url=?Search=tempête+donjon]donjon de la tempêtes[/url].\n\n[b]Tome des Arcanes[/b]\n\n[span class=q2][item=29739][/span] peut être donné à tout moment à [npc=18530] à l\'intérieur la Bibliothèque du Visiteur. Cela augmentera votre réputation avec les Clairvoyants de 350 par Tome des Arcane.\nEn plus des gains de réputation, vous recevrez une [span class=q1][item=29736][/span], qui est la condition pour acheter l\'enchantements d\'épaule à [npc=20808], qui réside dans la banque des Claivoyants.\n\n[h3]Passer à la réputation des Claivoyants[/h3]\n\nPour changer votre faction d\'Aldor vers Claivoyants et donc accéder à leurs recettes d\'artisanat (et annuler toutes les avancées de réputation que vous avez faites), trouvez [npc=18596], membre des Claivroyants dans la ville basse. Elle vous propose une quête répétable, [quest=10024], où pour huit [span class=q1][item=25744][/span] vous montez la réputation Claivoyant. Une fois que vous êtes neutre, vous ne pourrez plus recevoir cette quête.',NULL),(8,942,2,NULL,2,'L’[b]Expédition Cénarienne[/b] a été envoyé par [faction=609], lors de la réouverture de la porte des ténèbres vers l\'Outreterre, pour explorer ce monde inconnu. Tout comme le cercle, il s\'agit d\'une coalition de forces entre les Elfes de la nuit et les Taurens. Depuis l\'ouverture de la porte, l\'expédition Cénarienne a rapidement gagné en taille et en autonomie, obtenant suffisamment de puissance pour être considérée comme une propre et unique faction. L\'expédition maintient sa base principale au refuge Cénarien dans [zone=3521], située immédiatement à l’ouest de la péninsule des flammes infernales. Elle est aussi présente sur [zone=3483], dans [zone=3519], et dans [zone=3522]. \n\nLe Refuge est situé dans le marécage de Zangar afin d’étudier la faune riche située là-bas. Cependant, l\'expédition a révélé des retombées inquiétantes dans le marais. Les niveaux d\'eau dans de nombreuses régions du marécage diminuent, et certaines régions comme Morte-bourbe ont déjà beaucoup souffert de ce phénomène étrange. On sait que cette diminution des niveaux d\'eau peut être attribuée aux pompes qui ont été construites dans le marécage par les naga. Leur but est de créer un nouveau puits d\'éternité pour [npc=22917].\nCependant, l\'expédition ne peut pas se permettre une confrontation directe avec le naga si nombreux dans le marécage de Zangar et le [url=?Search=Glissecroc#c0z]Réservoir de Glissecroc [/url]. Elle a besoin de l\'aide d’aventurier qui veulent soutenir les druides dans leur dangereuse bataille contre les Nagas qui cherchent à perturber l\'équilibre naturel du marais. Naturellement, ceux assez héroïques pour combattre au réservoir de Glissecroc seront bien récompensés.\n\n[h3]Réputation[/h3]\n\n[b]De neutre à honoré[/b]\n\nTuez des Nagas chaque fois que vous le pouvez. Le mieux sera de parcourir les instances, la réputation monte plus rapidement.\nAlternativement, le joueur peut commencer à trouver des [item=24401] pour avoir une chance d’avoir des [item=24407], qui peuvent être transformé en 500 points de réputation. Il est suggéré que le joueur garde ses espèces non cataloguées jusqu\'à ce que son statut honoré soit atteint, car la quête ne peut pas être poursuivie après ce point, alors que les espèces non cataloguées peuvent être utilisées jusqu\'à Exalté.\n\nSi vous êtes un herboriste et que vous êtes intéressé par la réputation [faction=970], vous voudrez peut-être trouver les [url=?Npcs&filter=na=Seigneur+tourbe]Seigneurs-tourbes[/url] qui se trouve dans l’Est, et le coin Sud-ouest du Marécage de Zangar. Leurs corps peuvent être «récoltés» par les herboristes et produisent souvent des végétaux non identifiées, alors que chaque monstre tué donne 15 points de réputation chez Sporeggar. \n\n[b]De honoré à révéré[/b]\n\nUne fois que le joueur est honoré, faire l’enclos aux esclaves et [zone=3716] (à l\'exception de [npc=17770] et de certains géants), n\'accorderont plus de réputation. Vous devriez maintenant faire des quêtes de l\'Expédition Cénarienne dans la péninsule des flammes infernal, le marécage de Zangar, la forêt de Terokkar et les Tranchantes. Il est également temps de transformer toutes les espèces non cataloguées que vous avez trouvées. Faire cela devrait vous faire passer révérer.\n\nAlternativement, vous pouvez, en étant niveau 70, faire [zone=3715]. Chaque donjon donne un peu plus de 1500 points de réputation si vous tuez toutes les mobs.\nDans le Caveau de la vapeur, se trouve, aussi, une quête répétable, [quest=9764], qui commence par [item=24367]. Vous pourrez ensuite donner les [item=24368], qui tombe à la fois dans le caveau de la vapeur et l’enclos aux esclaves, recevant 250 points de réputation pour les premières armes et 75 points de réputation par la suite. Cette quête est disponible jusqu\'à exalté.\n\nUne fois que vous avez le niveau 70 et que vous avez amélioré votre équipement, vous pouvez choisir d\'entrer dans l’enclos des esclaves, le caveau de la vapeur et basse-tourbière en mode héroïque avec l\'achat de la [item=30623]. Ils accordent une réputation importante : les mobs ordinaires valent 15 points de réputation, 2 pour les non élites et 150 à 250 pour les boss. Cette méthode fonctionne jusqu\'à exalté.\n\n[b]De révéré à exalté [/b]\n\nContinuez avec la même stratégie que ci-dessus : terminez toutes les requêtes restantes, faites caveau de la vapeur et continuez avec la quête des [item=24368].\n\nIl est également possible de faire l’enclos des esclaves, Basse-tourbière et caveau de la vapeur en mode héroïque. La réputation acquise n\'est pas beaucoup plus intéressante que le caveau de la vapeur en mode normal, alors que l\'investissement dans le temps pour les donjons héroïques est beaucoup plus élevé, le butin est mieux et vous recevrez [item=29434] sur les boss qui peuvent être utilisés pour acheter des équipements épiques de haute qualité.',NULL),(8,941,2,NULL,2,'Les [b]Mag\'har[/b] sont la faction d\'orcs à peau brune qui sont restées en Outreterre et se sont séparés des autres clans orcs restants qui ont été victimes de [npc=17257] et qui sont maintenant dirigés par le puissant [npc=16808]. Les Mag\'har sont présent dans la forteresse de Garadar dans le magnifique pays de [zone=3518], une fois bien installés, la majorité des orcs sont retournés dans [zone=3519] et [zone=3522].\n\nLes Mag’har n\'ont jamais été corrompus par Mannoroth ou Magtheridon. Contrairement à d’autres anciens clans qui vivent dans les ruines de leurs ancêtres, les Mag\'har sont composés de membres de différents clans d\'orc qui ont échappé à la corruption. Le chef actuel des Mag\'har, la vénérable [npc=18141], est une orc ancienne et sage, mais elle est tombée récemment extrêmement malade. [npc=18063], fils du puissant Grom hurlenfer, sert de chef militaire aux Mag\'har, aidé par [npc=18106], fils du vénérable chef du clan Orbite-Sanglante, Kilrogg Deadeye. En outre, il existe un orc dans un camp de Mag\'har à l\'ouest connu sous le nom [npc=18229].\n\nIl n\'est pas clair comment le Mag\'har a réussi à conserver sa peau marron d\'origine. La peau orque devient verte lorsqu\'elle est exposée à la magie du sorcier, indépendamment des croyances ou des pratiques de l\'individu ; Garrosh et Jorin auraient certainement été exposés, compte tenu de la position hiérarchique de leurs pères.\n\nLes joueurs de la Horde commencent inamical avec le Mag\'har. Les joueurs de l\'Alliance seront toujours traités comme hostiles. La contrepartie de l\'Alliance à cette faction est la faction des : [faction=978].\n\n[h3]Quête[/h3]\n\nLes quêtes pour les Mag\'har commencent dans [zone=3483] avec [quest=9400] de [faction=947]. Cette quête vous mènera à un petit avant-poste Mag\'har au nord de la Citadelle des flammes infernales. Une fois à Nagrand, les joueurs trouveront la principale ville de Mag\'har, Garadar. La ville détient la plupart des quêtes restantes qui récompenseront la réputation de Mag\'har.\n\n[i]Note : Vous DEVEZ compléter la suite de quête de \"l’assassin\" jusqu\'à la quête [quest=9410] (où vous devenez neutre) afin que vous puissiez parler à la plupart des gens de Garadar.[/i]\n\n[h3]Réputation[/h3]\n\nLa réputation peut être obtenue en tuant des [url=?npcs&filter=na=kil%27sorrau;ra=-1;rh=-1]Membres de culte Kil\'sorrau[/url], des [url=?Npcs&filter=na=Bourbesang;ra=-1;rh=-1;cr=6;crs=3518;crv=0]Bourbesang[/url], des [url=?Npcs&filter=na=cogneguerre+-marker]Cogneguerre[/url] et des [url=?Npcs&filter=Na=rochepoing;minle= 64;ra=-1;rh=1]Rochepoing[/url] à Nagrand. Les joueurs peuvent également transformer 10x[item=25433], qui tombent de ces ogres.\n\nLes joueurs qui recherchent la réputation : [faction=933] peuvent vouloir garder leurs perles, car la réputation Mag\'har est généralement plus facile à obtenir. \nLes joueurs qui recherchent la réputation :[faction=932] peuvent préférer tuer les membres du culte à la forteresse de Kil\'Sorrau, car ils donnent aussi des [item=29425] pour la réputation Aldor.\n\n[i]Remarque : Ces monstres et quêtes n\'ont pas de limite, ils accordent une réputation jusqu’à exalté![/i]',NULL),(8,946,2,NULL,2,'Le [b]Bastion de l’Honneur[/b], refuge des explorateurs humains, élu, draenei et nains, est la première grande ville que les explorateurs de l\'Alliance rencontreront en traversant la porte des ténèbres. Les vestiges des fils de Lothar, anciens combattants de l\'Alliance qui sont venus à Draenor, se sont tenus fermement dans cet avant-poste des flammes infernales. Ils sont maintenant rejoints par les armées de Hurlevent et Forgefer.\n\n[h3]Réputation[/h3]\n\nLa réputation du Bastion de l\'Honneur est gagnée par divers moyens dans la péninsule des flammes infernales. Les PNJs, dans et autour, de la citadelle donnent en récompensés de quêtes de l\'honneur et de la réputation. En raison du manque de représentants dans d\'autres endroits d’Outreterre il y a un grand écart entre Honoré et Exalté, au cours duquel il est possible que vous ne puissiez pas obtenir assez de réputation au bastion de l’honneur une fois que vous partez de la péninsule.\n\n[b]Jusqu’à Honoré[/b]\n\nTuer des Pnjs dans [zone=3562] et [zone=3713] attribueront de la réputation. Une option est de faire les donjons jusqu\'à ce que la réputation arrive à honoré avant de faire des quêtes du Bastion de l\'honneur, car les quêtes continuent à donner de la réputation jusqu\'à Exalté.\n\nVous voudrez peut-être tuer les orcs à l’extérieur du bastion qui donnent une réputation si vous êtes Neutre. La réputation donnée s’arrête une fois que vous êtes amicales.\n[ul]\n[li][npc=19415][/li]\n[li][npc=16878][/li]\n[li][npc=16870][/li]\n[li][npc=16867][/li]\n[li][npc=19414][/li]\n[li][npc=19413][/li]\n[li][npc=19411][/li]\n[li][npc=19422][/li]\n[/ul]\n\n[b]PvP[/b]\n\nLes joueurs qui apprécient le PvP peuvent gagner de l\'honneur et de la réputation avec la quête [quest=10106]. Cette quête accorde 70 points d\'honneur et 150 points de réputation au Bastion de l’Honneur, mais ne peut être complétée qu\'une fois par jour et compte pour votre limite de 25 quêtes journalières. L\'achèvement de cette quête fournit également trois [span class=q1][item=24579][/span], qui sont utilisés comme monnaie pour divers types d\'articles lorsqu\'ils sont échangés chez [npc=17657] et [npc=18266] au Bastion de l’Honneur ainsi que [npc=18581] aux marécages de Zangar.\n\n[b]Jusqu’à Exalté[/b]\n\nÀ partir de là, il n\'y a que deux façons d\'atteindre Révéré et Exalté :\n[ul]\n[li][zone=3714], cette instance nécessite le niveau 68 et [span class=q1][item=28395][/span] (Un seul membre du groupe a besoin de la clé). L’instance des salles brisées abrite des PNJs qui donnent de la réputation jusqu’à Exalté.[/li]\n[li]Après avoir obtenu le statut d’honoré, vous pouvez acheter [span class=q1][item=30622][/span] qui accorde l\'accès au mode héroïque des instances de la citadelle des flammes infernales. Faire les donjons en mode Héroique donneront plus de réputation que les salles brisées en mode normale et continueront à donner de la réputation jusqu’à Exalté.[/li]\n[/ul]\n\n[i]Astuce : Vous pouvez utiliser ces marques pour acheter [span class=q1][item=24520][/span] à l\'adjudant Tracy Proudwell et augmenter le montant gagné de réputation (et d’expérience) acquise lors de l\'exécution de ces instances.[/i]',NULL),(8,967,2,NULL,2,'[b]L\'Oeil Pourpre[/b] est une secte secrète fondée par le Kirin Tor de Dalaran pour espionner le gardien de Tirisfal, [npc=15608], dans la tour de [zone=2562]. Bien que Medivh soit mort, l\'œil pourpre reste dans Karazhan, défendant le mal qui semble l’envahir en l\'absence de son maître.\n\nOn ignore si l\'apprenti de Medivh, [npc=18166], était membre de l’Oeil Pourpre, ou s\'il connaissait leurs activités à l\'époque.\n\n[h3]Réputation[/h3]\n\nLa réputation de l’œil pourpre est obtenue en tuant des mobs à l\'intérieur de Karazhan et en complétant les quêtes liées à Karazhan. La réputation grâce aux mobs de Karazhan peut être acquise à partir d\'une position neutre jusqu’à une réputation exalté. Chaque mob apporte une réputation d\'environ 15 points, les boss accordent davantage de réputation.\n\n[npc=18253] propose une chaîne de quête assez longue commençant par [quest=9824] et [quest=9825]. Cette suite de quête se termine par [quest=9644] et récompense les joueurs avec [span class=q1][item=24490][/span]. L\'achèvement complet de cette suite de quête récompense le joueur avec 10 270 point de réputation d\'environ.\n\n[h3]Récompenses de la réputation[/h3]\n\n[npc=18253] offrira aux joueurs des bagues en récompenses pour chaque niveau de réputation sous forme de quêtes. La première de ces quêtes est disponible dès la réputation neutre. Vous recevrez une version nouvelle et améliorée de la bague que vous avez choisi chaque fois que vous entrez dans un nouveau niveau de réputation. Les anneaux sont triés dans les 4 catégories suivantes :\n[ul]\n[li][quest=10731] : [item=29280], [item=29281], [item=29282] et [item=29283][/li]\n[li][quest=10729] : [item=29284], [item=29285], [item=29286] et [item=29287][/li]\n[li][quest=10732] : [item=29276], [item=29277], [item=29278] et [item=29279][/li]\n[li][quest=10730] : [item=29288], [item=29289], [item=29291] et [item=29290][/li]\n[/ul]\n\n[npc=16388], un forgeron situé à l\'intérieur de Karazhan juste après [npc=15550], offre aux joueurs ayant une réputation assez élevée la possibilité d\'acheter des plans de forge épique. Les joueurs honorés ou au-dessus pourront également réparer des armures et des armes chez ce fournisseur.\n\n[npc=18255], qui se trouve juste à l\'extérieur des portes principales de Karazhan, vendra une recette de joaillerie épique et un enchantement d\'épaule aux joueurs qui ont une haute réputation avec l’Oeil Pourpre.',NULL),(8,970,2,NULL,2,'Les[b]Sporeggar[/b] sont une race de champignons essentiellement pacifique originaire d\'Outreterre. Ils vivent dans une ville située dans les tourbières occidentales de [zone=3521].\n\n[h3]Réputation [/h3]\n\nLes joueurs de l\'Alliance et de la Horde commencent amicalement avec Sporeggar. Il existe de nombreuses façons d\'augmenter votre réputation au début : \n[ul]\n[li]Apporter 10 [span class=q1][item=24290][/ span] à [npc=17923] pour compléter [quest=9739][/li]\n[li]Apporter 6 [span class=q1][item=24291][/span] à Fahssn pour compléter [quest=9743][/li]\n[i]Ces deux quêtes ne seront disponibles que si vous avez une réputation au minimum amical[/i]\n[li]Tuer [url=?Search=seigneurs +tourbes+-hungry #z0z]Seigneurs tourbes[/url] [i](jusqu\'à honoré)[/i][/li]\n[li]Tuer [npc=18137] et [npc=18136] [i](jusqu\'à révéré)[/i][/li]\n[li]Apporter 10 [span class=q1][item=24245][/span] à [npc=17924] dans Sporeggar[i] (jusqu’à amical)[/i][/li]\n[/ul]\n\nAprès avoir une réputation [b]amicale[/b], de nouvelles quêtes répétitives s\'ouvrent en même temps que les quêtes de Fahssn, notamment :\n[ul]\n[li]Tuer 12 [npc=18088] et [npc=18089] pour [npc=17856] pour compléter [quest=9726][/li]\n[li]Apporter 10 [span class=q1][item=24449][/span] à [npc=17925] pour compléter [quest=9806][/li]\n[li] S\'aventurer dans [zone=3716] pour rassembler 5 [span class=q1][item=24246][/span] pour terminer [quest=9715][/li]\n[/ul]\nCes 3 quêtes sont répétables et seront disponibles jusqu’à la réputation exalté.\nLes joueurs qui sont exaltés avec Sporeggar devraient parler à [npc=17877] pour une dernière quête.',NULL),(8,978,2,NULL,2,'Les Kurenaï, pour « racheté », ont échappé à l’esclavage en Outreterre et ont fait leur maison à Telaar dans le sud de [zone=3518]. C\'est là qu\'ils cherchent à redécouvrir leur destinée. Ils conservent également une petite présence en [zone=3521]. Leur intendant, [npc=20240], est situé à l\'extérieur de l\'auberge à Telaar, en dessous du point de vol.\n\nLes joueurs de l\'Alliance commencent à faire preuve d\'hostilité avec les Kurenai. Les joueurs de la Horde seront toujours traités comme hostiles. La contrepartie de la Horde à cette faction est [faction=941].\n\n[i]Kurenai est le japonais pour « cramoisi ».[/i]\n\n[h3]Réputation[/h3]\n\nLa réputation peut être obtenue en tuant des [url=?Npcs&filter=na=kil%27sorrau;ra=-1;rh=-1]Membres de culte Kil\'sorrau[/url], des [url=?Npcs&filter=na=Bourbesang;ra=-1;rh=-1;cr=6;crs=3518;crv=0]Bourbesang[/url], des [url=?Npcs&filter=na=cogneguerre+-marker]Cogneguerre[/url] et des [url=?Npcs&filter=Na=rochepoing;minle= 64;ra=-1;rh=1]Rochepoing[/url] à Nagrand. Les joueurs peuvent également transformer 10x [item=25433], qui tombent de ces ogres.\n\nLes joueurs qui cherchent la réputation de la faction [faction=933] peuvent vouloir garder leurs perles, car la réputation de Kurenai est généralement plus facile à obtenir.\n\nLes joueurs qui cherchent la réputation de la faction [faction=932] peuvent préférer tuer les membres du culte à la forteresse de Kil\'Sorrau, alors qu\'ils donnent des [item=29425] pour la réputation de l’Aldor.\n\n[i]Remarque : Ces monstres et quêtes n\'ont pas de limite, ils accordent de la réputation jusqu’à exalté.[/i]',NULL),(8,989,2,NULL,2,'Les [b]Gardiens du Temps[/b] sont des dragons de bronze sélectionnés par Nozdormu pour surveiller les grottes du temps. Ils sont dirigés par [npc=19932] et [npc=19933], qui remplacent également Nozdormu en son absence.\n\n[h3]Réputation[/h3]\n\nActuellement, la seule façon d\'obtenir la faveur des dragons de bronze est de faire les instances : [zone=2367] et [zone=2366]. L’intendant des Gardiens du Temps, [npc=21643], se situe au quartier-intendant dans les grottes du temps. Les Gardiens vous demanderont d\'être au minimum niveau 66 et de compléter la courte quête [quest=10277] avant d\'autoriser le passage dans Les contreforts d’Hautebande d’antan pour accomplir la destinée du Chef de la Horde, [npc=17876].',NULL),(8,990,2,NULL,2,'La [b]Balance des sables[/b] est un sous-groupe secret du vol des Dragons de bronze, dirigé par [npc=19935], premier partenaire de [npc=15185]. Leur chef, Nozdormu, a envoyé ces factions gardiennes à [zone=3606] où ils gardent l\'Arbre Monde d\'une autre attaque par les démons, contribuent à restaurer le temps et à préserver l\'avenir du monde.\n\n[h3]Réputation[/h3]\n\nTuer les boss et monstres du Fléau font monter la réputation. [npc=17968], le boss final, récompense de 1 500 points de réputation tandis que les quatre autres boss donnent 375 points de réputations. La réputation général des montres du Fléau donnent 12 points de réputation, tandis que [npc=17907] donnent 60 points de réputation. En produisant une moyenne de 7 800 points de réputations par raid, 6 raids sont nécessaires pour atteindre la réputation exaltée.\n\nActuellement, la réputation permet d’avoir l’une des meilleurs [span class=q4][url=?Items=4.-2&filter=na=bague+éternel]Bagues[/url][/span] pour les raids. Afin de recevoir ces anneaux, vous devez compléter la quête précédemment requise, [quest=10445]. Chaque nouveau niveau de réputation accorde une bague améliorée.',NULL),(8,1012,2,NULL,2,'Les [b]Ligemorts Cendrelangues[/b] sont l\'élite de la tribu Kurenaï connue sous le nom de Cendrelangue. La tribu Cendrelangue est dirigée par la sage aînée [npc=21700]. Les Ligemorts sont [i]officiellement[/i] alignés avec [npc=22917] [small][/small]. Les Ligemorts sont les lieutenants les plus dignes d\'Akama et sont au courant des motivations mystérieuses de leur chef.\n\nPour découvrir les Ligemorts Centrelangues en tant que faction, le joueur doit commencer et compléter la majorité de la suite de quête qui commence par [quest=10568] ou [quest=10683]. Finalement, vous parlerez avec Akama, après quoi vous deviendrez neutre avec les Ligemorts Cendrelangues.',NULL),(8,947,2,NULL,2,'[b]Thrallmar[/b], expédition envoyée par le Portail des Ténèbres par Thrall, a construit un bastion dans la péninsule des flammes infernales qui sert de base d\'opérations pour une grande partie des activités de la Horde en Outreterre.\n\n[h3]Réputation[/h3]\n\nLa réputation de Thrallmar jusqu\'à l\'honorée est relativement facile à gagner. Même les quêtes les plus faciles (celles qui vous emmènent d\'un fournisseur de quête à la prochaine, par exemple) peuvent produire 75 points de réputation, alors que ceux qui nécessitent plus d’efforts pour compléter ont généralement 250 points de réputation ou plus. Certaines quêtes de groupe impliquant de tuer un élite peuvent donner jusqu\'à 1 000 points de réputation.\n\nSi vous faites la majeure partie des quêtes de Thrallmar au lieu de passer rapidement à la prochaine zone, vous pourriez vous attendre à être honoré après 1 ou 2 niveaux de jeu. En raison du manque de représentants dans d\'autres endroits d’Outreterre il y a un grand écart entre Honoré et Exalté, au cours duquel il est possible que vous ne puissiez pas obtenir assez de réputation à Thrallmar une fois que vous partez de la péninsule. C’est seulement au niveau 68 que vous pouvez commencer à regagner des points dans le donjon [zone=3714].\n\n[b]Jusqu’à Honoré[/b]\n\nTuer des Pnjs dans [zone=3562] et [zone=3713] attribueront de la réputation. Une option est de faire les donjons jusqu\'à ce que la réputation arrive à honoré avant de faire des quêtes de Thrallmar, car les quêtes continuent à donner de la réputation jusqu\'à Exalté.\n\nVous voudrez peut-être tuer les orcs à l’extérieur du bastion qui donnent une réputation si vous êtes Neutre. La réputation donnée s’arrête une fois que vous êtes amicales.\n[ul]\n[li][npc=19415][/li]\n[li][npc=16878][/li]\n[li][npc=16870][/li]\n[li][npc=16867][/li]\n[li][npc=19414][/li]\n[li][npc=19413][/li]\n[li][npc=19411][/li]\n[li][npc=19422][/li]\n[/ul]\n\n[b]PvP[/b]\n\nLes joueurs qui apprécient le PvP peuvent gagner de l\'honneur et de la réputation avec la quête [quest=10110]. Cette quête accorde 70 points d\'honneur et 150 points de réputation à Thrallmar, mais ne peut être complétée qu\'une fois par jour et compte pour votre limite de 25 quêtes journalières. L\'achèvement de cette quête fournit également trois [span class=q1][item=24581][/span], qui sont utilisés comme monnaie pour divers types d\'articles lorsqu\'ils sont échangés chez [npc=18267] et [npc=18564] à Thrallmar et près de Zabra’jin dans [zone=3521].\n\n[b]Jusqu’à Exalté[/b]\n\nÀ partir de là, il n\'y a que deux façons d\'atteindre Révéré et Exalté :\n[ul]\n[li][zone=3714], cette instance nécessite le niveau 68 et [span class=q1][item=28395][/span] (Un seul membre du groupe a besoin de la clé). L’instance des salles brisées abrite des PNJs qui donnent de la réputation jusqu’à Exalté.[/li]\n[li]Après avoir obtenu le statut d’honoré, vous pouvez acheter [span class=q1][item=30637][/span] qui accorde l\'accès au mode héroïque des instances de la citadelle des flammes infernales. Faire les donjons en mode Héroique donneront plus de réputation que les salles brisées en mode normale et continueront à donner de la réputation jusqu’à Exalté.[/li]\n[/ul]\n\n[i]Astuce : Vous pouvez utiliser ces marques pour acheter [span class=q1][item=24522][/span] au Crieur-de-guerre Coquard et augmenter le montant gagné de réputation (et d’expérience) acquise lors de l\'exécution de ces instances.[/i]',NULL),(8,1011,2,NULL,2,'[b]Ville Basse[/b] de [zone=3703] est l\'endroit où les réfugiés se rassemblent et s’aident par leurs propres moyens. Lorsque vous aidez l\'une des races qui ont fui la guerre, la réputation se débrouille rapidement. Leur intendant, [npc=21655], est situé sur le marché dans la ville basse.\n\nLa ville basse de Shattrath contient de nombreux artisans qui possèdent de vastes connaissances :\n[ul]\n[li][npc=19187], [small]< Maître des travailleurs du cuirs >[/ small].[/li]\n[li][npc=19180], [small]< Maître des dépeceurs >[/small].[/li]\n[li][npc=19052], [small]< Maître des alchimistes >[/small]. Il donne la quête [quest=10902] (pour une spécialisation). Un laboratoire d’alchimiste se trouve également à son côté.[/li]\n[li]Trois tailleurs qui vous permettent de se spécialiser et d\'acheter de nouvelles recettes de couture épiques pour des ensembles d\'armures et des sacs spéciaux :\n[ul][li][npc=22212], [small]< Spécialiste de couture de tisse-ombre >[/small] vend des recettes pour [itemset=553][/li]\n[li][npc=22213], [small]< Spécialiste de couture de feu-sorcier >[/small] vend des recettes pour [itemset=552].[/li]\n[li][npc=22208], [small]< Spécialiste de couture d’étoffe lunaire > [/small] vend des recettes pour [itemset=554].[/li][/ul]\n[/ul]\n\nLes maîtres de guerre, Alliance et Horde, des quatre [zones=6] peuvent également être trouvés ici, ainsi que la Tavernes de la Fin du Monde.\n\n[h3]Réputation[/h3]\n\n[b]Jusqu\'à honoré [/b]\n[ul]\n[li]Faire [zone=3790] en [i]mode normal[/i], vous récompense d’environs 750 points de réputation.[/li]\n[li]Faire [zone=3791] en [i]mode normal[/i], vous récompense d’environs 1 250 points de réputation.[/li]\n[li]Faire [zone=3789] en [i]mode normal[/i], vous récompense d’environs 2 000 points de réputation.[/li]\n[li]Fournir 30 x [item=25719] à [npc=22429], vous récompense de 250 points de réputations par quête.[/li]\n[/ul]\n[i]Note : Les joueurs qui visent une faction supérieure à Honorée devraient attendre jusqu\'à d’être honoré avant de compléter les quêtes de la Ville Basse.[/i]\n\n[b]De honoré à exalté[/b]\n[ul]\n[li]Faire de Labyrinthe des ombres en [i]mode normal[/i], vous récompense de 2 000 points de réputation.[/li]\n[li]Terminer toutes les [url=?quests&filter=cr=1;crs=1011;crv=0]quête de la Ville-Basse[/url].[/li]\n[/ul]\n[b]De révéré à exalté[/b]\n[ul]\n[li]Faire les Cryptages Auchenai en [i]mode héroïque[/i], vous récompense d’environs 750 points de réputation.[/li]\n[li]Faire les salles de Sethekk en [i]mode héroïque[/i], vous récompense d’environs 1 250 points de réputation.[/li]\n[li]Faire le Labyrinthe des ombres en [i]mode normal[/i] ou en [i]mode héroïque[/i], vous récompense d’environs 2 000 points de réputation.[/li]\n[/ul]\n\n[h3]Anecdotes[/h3]\n\n[npc=19227], un vendeur dans la ville basse, vend des amulettes qui sont très ... intéressantes. Il vend des articles comme [item=27940], qui vous permettent de revenir à la vie lorsque vous retournez à l\'endroit où vous êtes mort. [i]Buyer se méfiez-vous![/i]\n\nEn tant qu’exalté, vous pouvez acheter un [item=31778]. Curieusement, aucun des habitants de la Ville Basse n’a été vu avec un tel objet. Peut-être qu\'ils ne peuvent pas se le permettre',NULL),(8,1015,2,NULL,2,'L’[b]Aile-du-Néant[/b] est une faction de dragons situés en Outreterre. La couvée inhabituelle a été engendrée par les œufs du vol de dragon noir d’Aile-de-Mort et infusée d\'énergies brutes. Maintenant, ils cherchent à trouver leur identité au-delà de l\'ombre du patrimoine destructeur de leur père.\n\n[h3]Réputation[/h3]\n\nLes joueurs, au commencement, sont haïe à la faction Aile-du-Néant et doivent être exaltés pour recevoir des [span class=q4][url=?Items=15.-7&filter=na=Aile-du-Néant+Drake]Drakes Aile-du-Néant[/url][/spanclass]. La suite de quête de la réputation est une suite qui se fait en solitaire impliquant des quêtes journalières, une quête de groupes (5 joueurs) pour passer Neutre et les quêtes journalières de groupe (3 joueurs) après être passer Révéré.\nUne monture volante est requise pour cette réputation et 300 compétences de monte sont nécessaires pour passer neutre.\n\n[b]De Haïe à Neutre[/b]\n\nLes joueurs de niveau 70 commenceront leur voyage pour une réputation exaltée en choisissant la suite de quête offerte par [npc=22113], un elfe du sang errant la surface des champs d’Aile-du-Néant, dans le coin sud-est de [zone=3520]. La suite de quête commence par [quest=10804]. L\'achèvement de cette suite fournira une réputation instantanée neutre et le choix de l\'un de [span class=q3][url=?Items&filter=qu=18;cr=1;crv=0;na=Aile%20néant;qu=3]ces 5 items[/url][/span].\n\n[h3]Après Neutre [/h3]\n\nAprès avoir terminé la suite de quête, Mordenai s’assurera qui vous ayez acquis 300 compétences [spell=34091] et que vous ayez une réputation neutre auprsè de l’Aile-de-Néant.\nCela vous accordera un déguisement d’Orc Gueule-de-Dragon lorsque vous entrez dans la zone Aile-du-Néant et vous permettra de communiquer et de travailler pour les Gueules-de-Dragon stationné là-bas.\n\nMordenai vous enverra d\'abord à [npc=23139] avec un ensemble de faux papiers. L\'achèvement de cette quête débloque le début des quêtes Gueule-de-Dragon sur lesquelles vous travaillerez pour augmenter votre réputation Aile-du-Néant.\n\nLa plupart de ces quêtes seront journalières (ajoutée à la 2.1). Les quêtes journalières diffèrent des quêtes régulières car elles sont infiniment repérables, mais vous ne pouvez compléter chaque quête journalière qu\'une fois par jour et se limiter à 25 quêtes journalières par jour.\n[i]Remarque : De nouvelles quêtes seront débloquées après chaque niveau de réputation, et toutes les quêtes journalières des niveaux précédents seront toujours disponibles.[/i]\n\n[b][toggler id=Neutralcaché]Neutre[/toggler][/b]\n\n[div id=Neutralcaché] \nAprès avoir donné la [item=32469] à [npc=23139] pour compléter [quest=11013], votre première suite de quêtes sera disponible pour accéder au prochain niveau de réputation avec Aile-du-Néant.\n\nMor\'ghor vous indiquera d’aller voir le maître d\'œuvre afin de commencer votre travail, et [npc=23141] se révélera comme un allié déguisé et vous proposera d’autres quêtes.\nL\'une d\'entre elles est [quest=11049]. Les joueurs pourront trouver, avec un peu de chance (1% de loot), l’[item=32506] sur presque toutes les créatures de l’escarpement d’Aile-du-Néant et sur un [item=185881] ou un [item=185877].\nYarzill voudra aussi une trouvaille rare, l’[item=185915], trouvée n\'importe où sur le rebord d’Aile-du-Néant et dans la forteresse Gueule-de-Dragon, coin sud-est de la vallée de d’Ombrelune. Cette quête n\'est pas étiquetée comme journalière et peut donc être effectuée autant de fois que vous voulez, du moment que vous pouvez trouver des œufs. Cette quête n’est pas comprise dans votre limite de quête journalière.\n\nAutres quêtes disponibles dès le début:\n[ul]\n[li][i][small]Journalière[/small][/i] - [quest=11018], [quest=11016], [quest=11017] – N’est disponible que pour les joueurs qui possèdent la profession adaptée pour rassembler chaque élément.[/li]\n[li][i][small]Journalière[/small][/i] - [quest=11015] - Une quête de collecte simple ouverte à tous les joueurs indépendamment de leur profession.[/li] \n[li][i][small]Journalière[/small][/i] - [quest=11020] - Yarzill vous demandera de collecter des [item=32502]s et de les utiliser afin d’empoisonner les péons qui travaillent pour rassembler des ressources pour Gueule-de-Dragon.[/li]\n[li][i][small]Journalière[/small][/i] - [quest=11035] - Vous devrez voler vers le coin nord-est de l’escarpement d’Aile-du-Néant et vous positionner sur une des roches flottantes pour intercepter le [npc= 23188] et récupérer 10 x [item=32509].[/li]\n[/ul]\n[/div]\n[b][toggler id=Friendlyhidden]Amical[/toggler][/b]\n\n[div id=Friendlyhidden]\nMor\'ghor vous donnera un [item=32694] pour circuler avec votre nouveau rang parmi les Gueules-de-Dragon.\n[ul]\n[li][quest=11083] - [npc=23166] vous enverra tuer des bourbesangs qui sont stationné profondément dans les mines.[/li]\n[li][quest=11081] - Après avoir trouvé les [item=32726] dans un [item=32724], vous révélerez ce qui se passe réellement avec les bourbesangs dans la mine.[/li]\n[li][quest=11054] - [npc=23291] vous donnera vos propres [item=32680] pour garder les pétons Gueules-de-Dragon en ligne et travailler avec efficacité[/li]\n[li][i][small]Journalière[/small][/i] - [quest=11076] - La [npc=23149] vous demandera de vous aventurer dans les mines Ailes-du-Néant et de récupérer la cargaison contenue dans les chars de la mine qui est jetée au hasard dans l\'intérieur de la mine.[/li]\n[li][i][small]Journalière[/small][/i] - [quest=11077] - L\'un des [npc=23376] vous informera que des créatures plus profondes dans la mine interrompent la production et vous demandent de réduire leur nombre.[/li]\n[li][i][small]Journalière[/small][/i] - [quest=11055] - Cette quête humoristique commence chez le [npc=23291] après que vous lui apportiez le matériel requis. Vous pourrez survoler l’escarpement Aile-du-Néant et lancer le Booterang à n\'importe quel [npc=23311] qui s’y trouve autour des cris-taux.[/li]\n[/ul]\n[/div]\n[b][toggler id=Honorécaché]Honoré[/toggler][/b]\n\n[div id=Honorécaché]\nMor\'ghor vous donnera votre nouveau [item=32695], qui est maintenant utilisable n\'importe où, tant que vous êtes à l\'extérieur.\n[ul]\n[li][quest=11063] - Cette quête en six parties est une course aérienne contre les autres maîtres de vol Gueule-de-Dragon. Ils tenteront tous de vous renverser, vous et votre monture, avec des attaques aériennes habilement placées, vous devez rester visible et sur votre monture jusqu\'à leur atterrissage, si vous échouez, vous devez redémarrer la quête. Après avoir vaincu le dernier des six coureurs, vous recevrez un [item=32863], qui fonctionne exactement comme une [item=25653]. Les effets des deux bijoux ne s’additionnent pas.[/li]\n[li][quest=11089] – Le [npc=23427] demandera un ensemble de matériaux pour créer un dispositif spécial pour détruire son frère et entraver les avancées de la légion dans l\'ouest de [zone=3518].[/li]\n[li][i][small]Journalière[/small][/i] - [quest=11086] - Mor\'ghor Vous enverra au Portal de Nagrand pour tuer 20 [url=?npcs=7&filter=na=ombremort] Agents Ombremort[/url]. Attention aux seigneurs, ils patrouillent dans la région et peuvent vous tuer d’coup de poing.[/li]\n[/ul]\n[/div]\n[b][toggler id=Révéréhidden]Révéré[/toggler][/b]\n\n[div id=Révéréhidden]\nMor\'ghor vous donnera votre nouveau [item=32864], le plus haut bijou.\n[ul]\n[li][url=?quests&filter=na=tuez%20les%20tous;minle=70;maxle=70] Tuez-les tous ![/url] - Mor\'ghor vous ordonnera de commencer l\'attaque la base d\'opérations de votre faction dans la vallée de Sombrelune. De toute évidence, vous n\'allez pas autoriser les Gueules-de-Dragon à attaquer vos alliés, alors vous informerez au leader approprié et débloquerez votre dernière quête journalière pour les Gueules-de-Dragon.[/li]\n[li][i][small]Journalière[/small][/i] – [url=?quests&filter=na=le%20plus%20mortel%20des%20pièges]Le plus mortel des pièges[/url] - Les forces Gueules-de-Dragon vont attaquer la base des opérations. Apportez des alliés, car il s\'agit d\'une grande bataille.[/li]\n[/ul]\n[/div]\n[b][toggler id=Exaltécaché]Exalté[/toggler][/b]\n\n[div id=Exaltécaché]\nAprès de nombreux jours de travail, finalement le dénouement de la suite des quêtes Aile-du-Néant / Gueule-de-Dragon, vous dirigera à Mor\'ghor une dernière fois, qui vous informera que vous serez promu par [npc=22917] lui-même.\nSans gâcher les événements qui s\'ensuivent, vous vous retrouverez à Shattrath avec une sélection de montures épiques Aile-du-Néant. Vous pouvez en choisir un gratuitement, et si vous décidez d\'une couleur différente plus tard, vous pouvez acheter un autre drake chez [npc=23489] dans le camp de Gueule-de-Dragon pour 200 or.\n[/div]',NULL),(8,1031,2,NULL,2,'Les [b]Gardes-ciel sha\'tari[/b] sont les gardiens aériens de [zone=3703], défendant la capitale des assaillants dans les collines ainsi que la lutte contre les Arakkoas de Terokk dans les sommets de Skettis. [faction=935] dirigent les gardes-ciel sha’tari.\nIls ont deux avant-postes, l\'un au nord des montages de Skettis et un près d’[faction=1038]. Les joueurs commencent avec une réputation neutre chez les Gardes-ciel sha\'tari.\n\n[h3]Réputation[/h3]\n\n[b]Quêtes journalières[/b]\n[ul]\n[li][quest=11008] - [npc=23048] vous accordera un paquet d\'explosifs pour détruire les oeufs qui reposent au sommet des structures de Skettis. [/li]\n[li][quest=11085] - Le [npc=23383] peut être trouvé au sommet de certaines structures, les joueurs l\'escorteront pour la réputation, l\'or et un choix entre deux potions : [item=28100] ou [item=28101].[/li]\n[li][quest=11065] - [npc=23335] vous informera que les bombardements, de l’avant-poste de la garde-ciel, ont coûté la vie de leurs montures et vous demandent de rassembler des Raies de l’éther pour compléter leurs forces aériennes.[/li]\n[li][quest=11010] - [npc=23120] vous demande de détruire les munitions pour les canons de la Légion afin que les gardes-ciel puissent continuer leur travail.[/li]\n[li][quest=11004] - Après avoir recueilli 6 [item=32388], [npc=23042] fera une potion qui permettra de voir l\'arakkoa le plus puissant, tel que [npc=23066].[i][small] Note : cette quête n\'est pas une quête journalière, mais peut être répété autant de fois que nécessaire. [/small][/i][/li]\n[/ul]\n\n[b]Créatures[/b]\n\n[ul]\n[li][npc=21804] - 5 points de réputation, jusqu\'à la fin de Révéré[/li]\n[li][url=?npcs&filter=na=skettis+-kaliri+-assassin;minle=70] Tous les Arakkoa de Skettis[/url] - 10 points de réputation.[/li]\n[li][npc=23029] - 30 points de réputation.[/li]\n[/ul]',NULL); -/*!40000 ALTER TABLE `aowow_articles` ENABLE KEYS */; -UNLOCK TABLES; - --- --- Dumping data for table `aowow_config` --- - -LOCK TABLES `aowow_config` WRITE; -/*!40000 ALTER TABLE `aowow_config` DISABLE KEYS */; -INSERT INTO `aowow_config` VALUES ('sql_limit_search','500',1,129,'default: 500 - max results for search'),('sql_limit_default','300',1,129,'default: 300 - max results for listviews'),('sql_limit_quicksearch','10',1,129,'default: 10 - max results for suggestions'),('sql_limit_none','0',1,129,'default: 0 - unlimited results (i wouldn\'t change that mate)'),('ttl_rss','60',1,129,'default: 60 - time to live for RSS (in seconds)'),('name','Aowow Database Viewer (ADV)',1,136,' - website title'),('name_short','Aowow',1,136,' - feed title'),('board_url','http://www.wowhead.com/forums?board=',1,136,' - another halfbaked javascript thing..'),('contact_email','feedback@aowow.org',1,136,' - displayed sender for auth-mails, ect'),('battlegroup','Pure Pwnage',1,136,' - pretend, we belong to a battlegroup to satisfy profiler-related Jscripts'),('debug','0',1,132,'default: 0 - disable cache, enable sql-errors, enable error_reporting'),('maintenance','1',1,132,'default: 0 - display brb gnomes and block access for non-staff'),('user_max_votes','50',1,129,'default: 50 - vote limit per day'),('force_ssl','0',1,132,'default: 0 - enforce SSL, if the server is behind a load balancer'),('locales','333',1,161,'default: 0x14D - allowed locales - 0:English, 2:French, 3:German, 6:Spanish, 8:Russian'),('screenshot_min_size','200',1,129,'default: 200 - minimum dimensions of uploaded screenshots in px (yes, it\'s square)'),('site_host','',1,136,' - points js to executable files'),('static_host','',1,136,' - points js to images & scripts'),('cache_decay','25200',2,129,'default: 60 * 60 * 7 - time to keep cache in seconds'),('cache_mode','1',2,161,'default: 1 - set cache method - 0:filecache, 1:memcached'),('cache_dir','',2,136,'default: cache/template - generated pages are saved here (requires CACHE_MODE: filecache)'),('acc_failed_auth_block','900',3,129,'default: 15 * 60 - how long an account is closed after exceeding FAILED_AUTH_COUNT (in seconds)'),('acc_failed_auth_count','5',3,129,'default: 5 - how often invalid passwords are tolerated'),('acc_allow_register','1',3,132,'default: 1 - allow/disallow account creation (requires AUTH_MODE: aowow)'),('acc_auth_mode','0',3,145,'default: 0 - source to auth against - 0:aowow, 1:TC auth-table, 2:external script'),('acc_create_save_decay','604800',3,129,'default: 604800 - time in wich an unconfirmed account cannot be overwritten by new registrations'),('acc_recovery_decay','300',3,129,'default: 300 - time to recover your account and new recovery requests are blocked'),('session_timeout_delay','3600',4,129,'default: 60 * 60 - non-permanent session times out in time() + X'),('session.gc_maxlifetime','604800',4,200,'default: 7*24*60*60 - lifetime of session data'),('session.gc_probability','1',4,200,'default: 0 - probability to remove session data on garbage collection'),('session.gc_divisor','100',4,200,'default: 100 - probability to remove session data on garbage collection'),('session_cache_dir','',4,136,'default: - php sessions are saved here. Leave empty to use php default directory.'),('rep_req_upvote','125',5,129,'default: 125 - required reputation to upvote comments'),('rep_req_downvote','250',5,129,'default: 250 - required reputation to downvote comments'),('rep_req_comment','75',5,129,'default: 75 - required reputation to write a comment'),('rep_req_reply','75',5,129,'default: 75 - required reputation to write a reply'),('rep_req_supervote','2500',5,129,'default: 2500 - required reputation for double vote effect'),('rep_req_votemore_base','2000',5,129,'default: 2000 - gains more votes past this threshold'),('rep_reward_register','100',5,129,'default: 100 - activated an account'),('rep_reward_upvoted','5',5,129,'default: 5 - comment received upvote'),('rep_reward_downvoted','0',5,129,'default: 0 - comment received downvote'),('rep_reward_good_report','10',5,129,'default: 10 - filed an accepted report'),('rep_reward_bad_report','0',5,129,'default: 0 - filed a rejected report'),('rep_reward_dailyvisit','5',5,129,'default: 5 - daily visit'),('rep_reward_user_warned','-50',5,129,'default: -50 - moderator imposed a warning'),('rep_reward_comment','1',5,129,'default: 1 - created a comment (not a reply) '),('rep_req_premium','25000',5,129,'default: 25000 - required reputation for premium status through reputation'),('rep_reward_upload','10',5,129,'default: 10 - suggested / uploaded video / screenshot was approved'),('rep_reward_article','100',5,129,'default: 100 - submitted an approved article/guide'),('rep_reward_user_suspended','-200',5,129,'default: -200 - moderator revoked rights'),('rep_req_votemore_add','250',5,129,'default: 250 - required reputation per additional vote past threshold'),('serialize_precision','5',0,65,' - some derelict code, probably unused'),('memory_limit','1500M',0,200,'default: 1500M - parsing spell.dbc is quite intense'),('default_charset','UTF-8',0,72,'default: UTF-8'),('analytics_user','',6,136,'default: - enter your GA-user here to track site stats'),('profiler_queue','0',7,132,'default: 0 - enable/disable profiler queue'),('profiler_queue_delay','3000',7,129,'default: 3000 - min. delay between queue cycles (in ms)'),('profiler_resync_ping','5000',7,129,'default: 5000 - how often the javascript asks for for updates, when queued (in ms)'),('profiler_resync_delay','3600',7,129,'default: 1*60*60 - how often a character can be refreshed (in sec)'); -/*!40000 ALTER TABLE `aowow_config` ENABLE KEYS */; -UNLOCK TABLES; - --- --- Dumping data for table `aowow_dbversion` --- - -LOCK TABLES `aowow_dbversion` WRITE; -/*!40000 ALTER TABLE `aowow_dbversion` DISABLE KEYS */; -INSERT INTO `aowow_dbversion` VALUES (1521735364,0,NULL,NULL); -/*!40000 ALTER TABLE `aowow_dbversion` ENABLE KEYS */; -UNLOCK TABLES; - --- --- Dumping data for table `aowow_home_featuredbox` --- - -LOCK TABLES `aowow_home_featuredbox` WRITE; -/*!40000 ALTER TABLE `aowow_home_featuredbox` DISABLE KEYS */; -INSERT INTO `aowow_home_featuredbox` VALUES (1,NULL,0,0,0,0,'',NULL,NULL,'[pad]Welcome to [b][span class=q5]AoWoW[/span][/b]!','[pad]Bienvenue à [b][span class=q5]AoWoW[/span][/b]!','[pad]Willkommen bei [b][span class=q5]AoWoW[/span][/b]!','','Добро[pad] пожаловать на [b][span class=q5]AoWoW[/span][/b]!'),(2,NULL,0,0,0,1,'STATIC_URL/images/logos/newsbox-explained.png',NULL,NULL,'[ul]\n[li][i]just demoing the newsbox here..[/i][/li]\n[li][b][url=http://www.example.com]..with urls[/url][/b][/li]\n[li][b]..typeLinks [item=45533][/b][/li]\n[li][b]..also, over there to the right is an overlay-trigger =>[/b][/li]\n[/ul]\n\n[ul]\n[li][tooltip name=demotip]hey, it hints you stuff![/tooltip][b][span class=tip tooltip=demotip]..hover me[/span][/b][/li]\n[/ul]','','','',''); -/*!40000 ALTER TABLE `aowow_home_featuredbox` ENABLE KEYS */; -UNLOCK TABLES; - --- --- Dumping data for table `aowow_home_featuredbox_overlay` --- - -LOCK TABLES `aowow_home_featuredbox_overlay` WRITE; -/*!40000 ALTER TABLE `aowow_home_featuredbox_overlay` DISABLE KEYS */; -INSERT INTO `aowow_home_featuredbox_overlay` VALUES (2,405,100,'http://example.com','example overlay','','','',''); -/*!40000 ALTER TABLE `aowow_home_featuredbox_overlay` ENABLE KEYS */; -UNLOCK TABLES; - --- --- Dumping data for table `aowow_loot_link` --- - -LOCK TABLES `aowow_loot_link` WRITE; -/*!40000 ALTER TABLE `aowow_loot_link` DISABLE KEYS */; -INSERT INTO `aowow_loot_link` VALUES (17537,185168),(18434,185169),(17536,185168),(18432,185169),(19218,184465),(21525,184849),(19710,184465),(21526,184849),(28234,190586),(-28234,193996),(27656,191349),(31561,193603),(26533,190663),(31217,193597),(16064,181366),(30603,193426),(16065,181366),(30601,193426),(30549,181366),(30600,193426),(16063,181366),(30602,193426),(28859,193905),(31734,193967),(32930,195046),(33909,195047),(32865,194313),(33147,194315),(33350,194957),(-33350,194958),(32845,194200),(32846,194201),(32906,194324),(33360,194325),(32871,194821),(33070,194822),(35119,195374),(35518,195375),(34928,195323),(35517,195324),(34705,195709),(36088,195710),(34702,195709),(36082,195710),(34701,195709),(36083,195710),(34657,195709),(36086,195710),(34703,195709),(36087,195710),(35572,195709),(36089,195710),(35569,195709),(36085,195710),(35571,195709),(36090,195710),(35570,195709),(36091,195710),(35617,195709),(36084,195710),(34441,195631),(34442,195632),(34443,195633),(-34443,195635),(34444,195631),(35740,195632),(35741,195633),(-35741,195635),(34445,195631),(35705,195632),(35706,195633),(-35706,195635),(34447,195631),(35683,195632),(35684,195633),(-35684,195635),(34448,195631),(35724,195632),(35725,195633),(-35725,195635),(34449,195631),(35689,195632),(35690,195633),(-35690,195635),(34450,195631),(35695,195632),(35696,195633),(-35696,195635),(34451,195631),(35671,195632),(35672,195633),(-35672,195635),(34453,195631),(35718,195632),(35719,195633),(-35719,195635),(34454,195631),(35711,195632),(35712,195633),(-35712,195635),(34455,195631),(35680,195632),(35681,195633),(-35681,195635),(34456,195631),(35708,195632),(35709,195633),(-35709,195635),(34458,195631),(35692,195632),(35693,195633),(-35693,195635),(34459,195631),(35686,195632),(35687,195633),(-35687,195635),(34460,195631),(35702,195632),(35703,195633),(-35703,195635),(34461,195631),(35743,195632),(35744,195633),(-35744,195635),(34463,195631),(35734,195632),(35735,195633),(-35735,195635),(34465,195631),(35746,195632),(35747,195633),(-35747,195635),(34466,195631),(35665,195632),(35666,195633),(-35666,195635),(34467,195631),(35662,195632),(35663,195633),(-35663,195635),(34468,195631),(35721,195632),(35722,195633),(-35722,195635),(34469,195631),(35714,195632),(35715,195633),(-35715,195635),(34470,195631),(35728,195632),(35729,195633),(-35729,195635),(34471,195631),(35668,195632),(35669,195633),(-35669,195635),(34472,195631),(35699,195632),(35700,195633),(-35700,195635),(34473,195631),(35674,195632),(35675,195633),(-35675,195635),(34474,195631),(35731,195632),(35732,195633),(-35732,195635),(34475,195631),(35737,195632),(35738,195633),(-35738,195635),(37226,201710),(-37226,202336),(36948,202178),(38157,202180),(38639,202177),(38640,202179),(36939,202178),(38156,202180),(38637,202177),(38638,202179); -/*!40000 ALTER TABLE `aowow_loot_link` ENABLE KEYS */; -UNLOCK TABLES; - --- --- Dumping data for table `aowow_sourcestrings` --- - -LOCK TABLES `aowow_sourcestrings` WRITE; -/*!40000 ALTER TABLE `aowow_sourcestrings` DISABLE KEYS */; -INSERT INTO `aowow_sourcestrings` VALUES (1,'Arena Season 1','Saison 1 des combats d\'arène','Arenasaison 1','Temporada de arena 1','Сезон арены 1'),(2,'Arena Season 2','Saison 2 des combats d\'arène','Arenasaison 2','Temporada de arena 2','Сезон арены 2'),(3,'Arena Season 3','Saison 3 des combats d\'arène','Arenasaison 3','Temporada de arena 3','Сезон арены 3'),(4,'Arena Season 4','Saison 4 des combats d\'arène','Arenasaison 4','Temporada de arena 4','Сезон арены 4'),(5,'Arena Season 5','Saison 5 des combats d\'arène','Arenasaison 5','Temporada de arena 5','Сезон арены 5'),(6,'Arena Season 6','Saison 6 des combats d\'arène','Arenasaison 6','Temporada de arena 6','Сезон арены 6'),(7,'Arena Season 7','Saison 7 des combats d\'arène','Arenasaison 7','Temporada de arena 7','Сезон арены 7'),(8,'Arena Season 8','Saison 8 des combats d\'arène','Arenasaison 8','Temporada de arena 8','Сезон арены 8'),(9,'2009 Arena Tournament','Tournoi 2009 des combats d\'arène','2009 Arena-Turnier','Torneo de arena 2009','Турнир арены 2009'); -/*!40000 ALTER TABLE `aowow_sourcestrings` ENABLE KEYS */; -UNLOCK TABLES; - --- --- Dumping data for table `aowow_profiler_excludes` --- - -LOCK TABLES `aowow_profiler_excludes` WRITE; -/*!40000 ALTER TABLE `aowow_profiler_excludes` DISABLE KEYS */; -INSERT INTO `aowow_profiler_excludes` VALUES (6,459,1,'Gray Wolf'),(6,468,1,'White Stallion'),(6,471,1,'Palamino'),(6,472,1,'Pinto'),(6,578,1,'Black Wolf'),(6,579,1,'Red Wolf'),(6,581,1,'Winter Wolf'),(6,3363,1,'Nether Drake'),(6,6896,1,'Black Ram'),(6,6897,1,'Blue Ram'),(6,8980,1,'Skeletal Horse'),(6,10681,1,'Summon Cockatoo'),(6,10686,1,'Summon Prairie Chicken'),(6,10687,1,'Summon White Plymouth Rock'),(6,10699,1,'Summon Bronze Whelpling'),(6,10700,1,'Summon Faeling'),(6,10701,1,'Summon Dart Frog'),(6,10702,1,'Summon Island Frog'),(6,10705,1,'Summon Eagle Owl'),(6,10708,1,'Summon Snowy Owl'),(6,10710,1,'Summon Cottontail Rabbit'),(6,10712,1,'Summon Spotted Rabbit'),(6,10715,1,'Summon Blue Racer'),(6,10718,1,'Green Water Snake'),(6,10719,1,'Ribbon Snake'),(6,10720,1,'Scarlet Snake'),(6,10721,1,'Summon Elven Wisp'),(6,10795,1,'Ivory Raptor'),(6,10798,1,'Obsidian Raptor'),(6,15648,1,'Corrupted Kitten'),(6,15779,1,'White Mechanostrider Mod B'),(6,15780,1,'Green Mechanostrider'),(6,15781,1,'Steel Mechanostrider'),(6,16055,1,'Black Nightsaber'),(6,16056,1,'Ancient Frostsaber'),(6,16058,1,'Primal Leopard'),(6,16059,1,'Tawny Sabercat'),(6,16060,1,'Golden Sabercat'),(6,16080,1,'Red Wolf'),(6,16081,1,'Winter Wolf'),(6,16082,1,'Palomino'),(6,16083,1,'White Stallion'),(6,16084,1,'Mottled Red Raptor'),(6,17450,1,'Ivory Raptor'),(6,17455,1,'Purple Mechanostrider'),(6,17456,1,'Red and Blue Mechanostrider'),(6,17458,1,'Fluorescent Green Mechanostrider'),(6,17459,1,'Icy Blue Mechanostrider Mod A'),(6,17460,1,'Frost Ram'),(6,17461,1,'Black Ram'),(6,17468,1,'Pet Fish'),(6,17469,1,'Pet Stone'),(6,18363,1,'Riding Kodo'),(6,18991,1,'Green Kodo'),(6,18992,1,'Teal Kodo'),(6,19363,1,'Summon Mechanical Yeti'),(6,23220,1,'Swift Dawnsaber'),(6,23428,1,'Albino Snapjaw'),(6,23429,1,'Loggerhead Snapjaw'),(6,23430,1,'Olive Snapjaw'),(6,23431,1,'Leatherback Snapjaw'),(6,23432,1,'Hawksbill Snapjaw'),(6,23530,16,'Tiny Red Dragon'),(6,23531,16,'Tiny Green Dragon'),(6,24985,1,'Summon Baby Murloc (Blue)'),(6,24986,1,'Summon Baby Murloc (Green)'),(6,24987,1,'Summon Baby Murloc (Orange)'),(6,24988,4,'Lurky'),(6,24989,1,'Summon Baby Murloc (Pink)'),(6,24990,1,'Summon Baby Murloc (Purple)'),(6,25849,1,'Baby Shark'),(6,26067,1,'Summon Mechanical Greench'),(6,26391,1,'Tentacle Call'),(6,28828,1,'Nether Drake'),(6,29059,1,'Naxxramas Deathcharger'),(6,30152,1,'White Tiger Cub'),(6,30156,2,'Hippogryph Hatchling'),(6,30174,2,'Riding Turtle'),(6,32298,4,'Netherwhelp'),(6,32345,1,'Peep the Phoenix Mount'),(6,33050,128,'Magical Crawdad'),(6,33057,1,'Summon Mighty Mr. Pinchy'),(6,33630,1,'Blue Mechanostrider'),(6,34407,1,'Great Elite Elekk'),(6,35157,1,'Summon Spotted Rabbit'),(6,37015,1,'Swift Nether Drake'),(6,40319,16,'Lucky'),(6,40405,16,'Lucky'),(6,43688,1,'Amani War Bear'),(6,43810,1,'Frost Wyrm'),(6,44317,1,'Merciless Nether Drake'),(6,44744,1,'Merciless Nether Drake'),(6,45125,2,'Rocket Chicken'),(6,45174,16,'Golden Pig'),(6,45175,16,'Silver Pig'),(6,45890,1,'Scorchling'),(6,47037,1,'Swift War Elekk'),(6,48406,16,'Essence of Competition'),(6,48408,1,'Essence of Competition'),(6,48954,8,'Swift Zhevra'),(6,49322,8,'Swift Zhevra'),(6,49378,1,'Brewfest Riding Kodo'),(6,50869,1,'Brewfest Kodo'),(6,50870,1,'Brewfest Ram'),(6,51851,1,'Vampiric Batling'),(6,51960,1,'Frost Wyrm Mount'),(6,52615,4,'Frosty'),(6,53082,8,'Mini Tyrael'),(6,53768,1,'Haunted'),(6,54187,1,'Clockwork Rocket Bot'),(6,55068,1,'Mr. Chilly'),(6,58983,8,'Big Blizzard Bear'),(6,59572,1,'Black Polar Bear'),(6,59573,1,'Brown Polar Bear'),(6,59802,1,'Grand Ice Mammoth'),(6,59804,1,'Grand Ice Mammoth'),(6,59976,1,'Black Proto-Drake'),(6,60021,1,'Plagued Proto-Drake'),(6,60136,1,'Grand Caravan Mammoth'),(6,60140,1,'Grand Caravan Mammoth'),(6,61309,512,'Magnificent Flying Carpet'),(6,61442,1,'Swift Mooncloth Carpet'),(6,61444,1,'Swift Shadoweave Carpet'),(6,61446,1,'Swift Spellfire Carpete'),(6,61451,512,'Flying Carpet'),(6,61855,1,'Baby Blizzard Bear'),(6,62048,1,'Black Dragonhawk Mount'),(6,62514,1,'Alarming Clockbot'),(6,63318,8,'Murkimus the Gladiator'),(6,64351,1,'XS-001 Constructor Bot'),(6,64656,1,'Blue Skeletal Warhorse'),(6,64731,128,'Sea Turtle'),(6,65682,1,'Warbot'),(6,65917,2,'Magic Rooster'),(6,66030,8,'Grunty'),(6,66122,1,'Magic Rooster - dummy spell'),(6,66123,1,'Magic Rooster - dummy spell'),(6,66124,1,'Magic Rooster - dummy spell'),(6,66520,1,'Jade Tiger'),(6,66907,1,'Argent Warhorse'),(6,67527,16,'Onyx Panther'),(6,68767,2,'Tuskarr Kite'),(6,68810,2,'Spectral Tiger Cub'),(6,69002,1,'Onyxian Whelpling'),(6,69452,8,'Core Hound Pup'),(6,69535,4,'Gryphon Hatchling'),(6,69536,4,'Wind Rider Cub'),(6,69539,1,'Zipao Tiger'),(6,69541,4,'Pandaren Monk'),(6,69677,4,'Lil\' K.T.'),(6,74856,2,'Blazing Hippogryph'),(6,74918,2,'Wooly White Rhino'),(6,75596,512,'Frosty Flying Carpet'),(6,75613,1,'Celestial Dragon'),(6,75614,16,'Celestial Steed'),(6,75906,4,'Lil\' XT'),(6,75936,1,'Murkimus the Gladiator'),(6,75973,8,'X-53 Touring Rocket'),(6,78381,8,'Mini Thor'),(8,87,1024,'Bloodsail Buccaneers - max rank is honored'),(8,92,1024,'Gelkis Clan Centaur - max rank is friendly'),(8,93,1024,'Magram Clan Centaur - max rank is friendly'); -/*!40000 ALTER TABLE `aowow_profiler_excludes` ENABLE KEYS */; -UNLOCK TABLES; -/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */; - -/*!40101 SET SQL_MODE=@OLD_SQL_MODE */; -/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */; -/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */; -/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */; -/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */; -/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */; - --- Dump completed on 2018-03-26 16:36:07 diff --git a/setup/setup.php b/setup/setup.php index 2ad6752f..029eb864 100644 --- a/setup/setup.php +++ b/setup/setup.php @@ -1,5 +1,7 @@ : regenerate tables/files that depend on given world-table\n"; - echo "--update : apply new sql updates fetched from github\n"; - echo "--firstrun : goes through the nessecary hoops of the initial setup.\n"; - echo "additional options\n"; - echo "--log logfile : write ouput to file\n"; - echo "--locales= : limit setup to enUS, frFR, deDE, esES and/or ruRU (does not override config settings)\n"; - echo "--mpqDataDir=path/ : manually point to directory with extracted mpq files; is limited to setup/ (default: setup/mpqData/)\n"; - echo "--delete | -d : delete generated dbc_* tables when script finishes\n"; - echo "--help | -h : contextual help\n"; - die("\n"); -} -else - CLISetup::init(); +if (CLISetup::getOpt('delete')) // generated with TEMPORARY keyword. Manual deletion is not needed + CLI::write('generated dbc_* - tables have been deleted.', CLI::LOG_INFO); -$cmd = array_pop(array_keys($opt)); -$s = []; -$b = []; -switch ($cmd) // we accept only one main parameter -{ - case 'firstrun': - require_once 'setup/tools/clisetup/firstrun.func.php'; - firstrun(); +CLISetup::runInitial(); - finish(); - case 'account': - case 'dbconfig': - case 'siteconfig': - case 'sql': - case 'build': - require_once 'setup/tools/clisetup/'.$cmd.'.func.php'; - $cmd(); - - finish(); - case 'update': - require_once 'setup/tools/clisetup/update.func.php'; - list($s, $b) = update(); // return true if we do not rebuild stuff - if (!$s && !$b) - return; - case 'sync': - require_once 'setup/tools/clisetup/sql.func.php'; - require_once 'setup/tools/clisetup/build.func.php'; - $_s = sql($s); - $_b = build($b); - - if ($s) - { - $_ = array_diff($s, $_s); - DB::Aowow()->query('UPDATE ?_dbversion SET `sql` = ?', $_ ? implode(' ', $_) : ''); - } - - if ($b) - { - $_ = array_diff($b, $_b); - DB::Aowow()->query('UPDATE ?_dbversion SET `build` = ?', $_ ? implode(' ', $_) : ''); - } - - finish(); -} +fwrite(STDOUT, "\n"); +exit; ?> diff --git a/setup/sql/01-db_structure.sql b/setup/sql/01-db_structure.sql new file mode 100644 index 00000000..86c69286 --- /dev/null +++ b/setup/sql/01-db_structure.sql @@ -0,0 +1,3257 @@ +-- MariaDB dump 10.19 Distrib 10.4.32-MariaDB, for Win64 (AMD64) +-- +-- Host: localhost Database: aowow +-- ------------------------------------------------------ +-- Server version 10.4.32-MariaDB + +/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */; +/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */; +/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */; +/*!40101 SET NAMES utf8mb4 */; +/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */; +/*!40103 SET TIME_ZONE='+00:00' */; +/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */; +/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */; +/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */; +/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */; + +-- +-- Table structure for table `aowow_account` +-- + +DROP TABLE IF EXISTS `aowow_account`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `aowow_account` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `extId` int(10) unsigned DEFAULT NULL COMMENT 'external user id', + `login` varchar(64) NOT NULL DEFAULT '' COMMENT 'only used for login', + `passHash` varchar(128) NOT NULL, + `username` varchar(64) NOT NULL COMMENT 'unique; used for for links and display', + `email` varchar(64) DEFAULT NULL COMMENT 'unique; can be used for login if AUTH_SELF and can be NULL if not', + `joinDate` int(10) unsigned NOT NULL COMMENT 'unixtime', + `dailyVotes` smallint(5) unsigned NOT NULL DEFAULT 0, + `consecutiveVisits` smallint(5) unsigned NOT NULL DEFAULT 0, + `curIP` varchar(45) NOT NULL DEFAULT '', + `prevIP` varchar(45) NOT NULL DEFAULT '', + `curLogin` int(10) unsigned NOT NULL DEFAULT 0 COMMENT 'unixtime', + `prevLogin` int(10) unsigned NOT NULL DEFAULT 0, + `locale` tinyint(3) unsigned NOT NULL DEFAULT 0 COMMENT '0,2,3,4,6,8', + `userGroups` smallint(5) unsigned NOT NULL DEFAULT 0 COMMENT 'bitmask', + `debug` tinyint(1) NOT NULL DEFAULT 0 COMMENT 'show ids in lists user option', + `avatar` tinyint(4) DEFAULT 0, + `avatarborder` tinyint(3) unsigned NOT NULL DEFAULT 2, + `wowicon` varchar(55) NOT NULL DEFAULT '' COMMENT 'iconname as avatar', + `title` varchar(50) NOT NULL DEFAULT '' COMMENT 'user can obtain custom titles', + `description` text NOT NULL DEFAULT '', + `excludeGroups` smallint(5) unsigned NOT NULL DEFAULT 1 COMMENT 'profiler - exclude bitmask', + `userPerms` tinyint(3) unsigned NOT NULL DEFAULT 0 COMMENT 'bool isAdmin', + `status` tinyint(3) unsigned NOT NULL DEFAULT 0 COMMENT 'flag, see defines', + `statusTimer` int(10) unsigned NOT NULL DEFAULT 0, + `token` varchar(40) DEFAULT NULL COMMENT 'identification key for changes to account', + `updateValue` varchar(128) DEFAULT NULL COMMENT 'temp store for new passHash / email', + `renameCooldown` int(10) unsigned NOT NULL DEFAULT 0 COMMENT 'timestamp when rename is available again', + PRIMARY KEY (`id`), + UNIQUE KEY `username` (`username`), + UNIQUE KEY `email` (`email`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=COMPACT; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `aowow_account_avatars` +-- + +DROP TABLE IF EXISTS `aowow_account_avatars`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `aowow_account_avatars` ( + `id` mediumint(8) unsigned NOT NULL, + `userId` int(10) unsigned NOT NULL, + `name` varchar(20) NOT NULL, + `size` mediumint(8) unsigned NOT NULL, + `when` int(10) unsigned NOT NULL, + `current` tinyint(3) unsigned NOT NULL DEFAULT 0, + `status` tinyint(3) unsigned NOT NULL DEFAULT 0, + UNIQUE KEY `id` (`id`) USING BTREE, + KEY `userId` (`userId`) USING BTREE, + CONSTRAINT `FK_acc_avatars` FOREIGN KEY (`userId`) REFERENCES `aowow_account` (`id`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=COMPACT; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `aowow_account_banned` +-- + +DROP TABLE IF EXISTS `aowow_account_banned`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `aowow_account_banned` ( + `id` int(10) unsigned NOT NULL, + `userId` int(10) unsigned NOT NULL COMMENT 'affected accountId', + `staffId` int(10) unsigned NOT NULL COMMENT 'executive accountId', + `typeMask` tinyint(3) unsigned NOT NULL COMMENT 'ACC_BAN_*', + `start` int(10) unsigned NOT NULL COMMENT 'unixtime', + `end` int(10) unsigned NOT NULL COMMENT 'automatic unban @ unixtime', + `reason` varchar(255) NOT NULL, + PRIMARY KEY (`id`), + KEY `FK_acc_banned` (`userId`), + CONSTRAINT `FK_acc_banned` FOREIGN KEY (`userId`) REFERENCES `aowow_account` (`id`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=COMPACT; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `aowow_account_bannedips` +-- + +DROP TABLE IF EXISTS `aowow_account_bannedips`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `aowow_account_bannedips` ( + `ip` varchar(45) NOT NULL, + `type` tinyint(4) NOT NULL COMMENT '0: onSignin; 1:onSignup', + `count` smallint(6) NOT NULL COMMENT 'nFails', + `unbanDate` int(11) NOT NULL COMMENT 'automatic remove @ unixtime', + PRIMARY KEY (`ip`,`type`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `aowow_account_cookies` +-- + +DROP TABLE IF EXISTS `aowow_account_cookies`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `aowow_account_cookies` ( + `userId` int(10) unsigned NOT NULL, + `name` varchar(127) NOT NULL, + `data` text NOT NULL, + UNIQUE KEY `userId_name` (`userId`,`name`) USING BTREE, + KEY `userId` (`userId`) USING BTREE, + CONSTRAINT `FK_acc_cookies` FOREIGN KEY (`userId`) REFERENCES `aowow_account` (`id`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=COMPACT; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `aowow_account_excludes` +-- + +DROP TABLE IF EXISTS `aowow_account_excludes`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `aowow_account_excludes` ( + `userId` int(10) unsigned NOT NULL, + `type` smallint(5) unsigned NOT NULL, + `typeId` mediumint(8) unsigned NOT NULL, + `mode` enum('EXCLUDE','INCLUDE') NOT NULL, + UNIQUE KEY `userId_type_typeId` (`userId`,`type`,`typeId`), + KEY `userId` (`userId`), + CONSTRAINT `FK_acc_excludes` FOREIGN KEY (`userId`) REFERENCES `aowow_account` (`id`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `aowow_account_favorites` +-- + +DROP TABLE IF EXISTS `aowow_account_favorites`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `aowow_account_favorites` ( + `userId` int(10) unsigned NOT NULL, + `type` smallint(5) unsigned NOT NULL, + `typeId` mediumint(8) unsigned NOT NULL, + UNIQUE KEY `userId_type_typeId` (`userId`,`type`,`typeId`), + KEY `userId` (`userId`), + CONSTRAINT `FK_acc_favorites` FOREIGN KEY (`userId`) REFERENCES `aowow_account` (`id`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `aowow_account_profiles` +-- + +DROP TABLE IF EXISTS `aowow_account_profiles`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `aowow_account_profiles` ( + `accountId` int(10) unsigned NOT NULL, + `profileId` int(10) unsigned NOT NULL, + `extraFlags` int(10) unsigned NOT NULL DEFAULT 0, + UNIQUE KEY `accountId_profileId` (`accountId`,`profileId`), + KEY `accountId` (`accountId`), + KEY `profileId` (`profileId`), + CONSTRAINT `FK_account_id` FOREIGN KEY (`accountId`) REFERENCES `aowow_account` (`id`) ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT `FK_profile_id` FOREIGN KEY (`profileId`) 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 */; + +-- +-- Table structure for table `aowow_account_reputation` +-- + +DROP TABLE IF EXISTS `aowow_account_reputation`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +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) 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, + UNIQUE KEY `userId_action_source` (`userId`,`action`,`sourceA`,`sourceB`), + KEY `userId` (`userId`), + CONSTRAINT `FK_acc_rep` FOREIGN KEY (`userId`) REFERENCES `aowow_account` (`id`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=COMPACT COMMENT='reputation log'; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `aowow_account_sessions` +-- + +DROP TABLE IF EXISTS `aowow_account_sessions`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `aowow_account_sessions` ( + `userId` int(10) unsigned NOT NULL, + `sessionId` varchar(190) NOT NULL COMMENT 'PHPSESSID', + `created` int(10) unsigned NOT NULL, + `expires` int(10) unsigned NOT NULL COMMENT 'timestamp or 0 (never expires)', + `touched` int(10) unsigned NOT NULL COMMENT 'timestamp - last used', + `deviceInfo` varchar(256) NOT NULL, + `ip` varchar(45) NOT NULL COMMENT 'can change; just last used ip', + `status` enum('ACTIVE','LOGOUT','FORCEDLOGOUT','EXPIRED') NOT NULL, + UNIQUE KEY `sessionId` (`sessionId`) USING BTREE, + KEY `userId` (`userId`) USING BTREE, + CONSTRAINT `FK_acc_sessions` FOREIGN KEY (`userId`) REFERENCES `aowow_account` (`id`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=COMPACT; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `aowow_account_weightscale_data` +-- + +DROP TABLE IF EXISTS `aowow_account_weightscale_data`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `aowow_account_weightscale_data` ( + `id` int(11) NOT NULL, + `field` varchar(15) NOT NULL, + `val` smallint(5) unsigned NOT NULL, + KEY `id` (`id`), + CONSTRAINT `FK_acc_weightscales` FOREIGN KEY (`id`) REFERENCES `aowow_account_weightscales` (`id`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `aowow_account_weightscales` +-- + +DROP TABLE IF EXISTS `aowow_account_weightscales`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `aowow_account_weightscales` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `userId` int(10) unsigned NOT NULL, + `name` varchar(32) NOT NULL, + `class` tinyint(3) unsigned NOT NULL DEFAULT 0, + `orderIdx` tinyint(3) unsigned NOT NULL DEFAULT 0 COMMENT 'check how Profiler handles classes with more than 3 specs before modifying', + `icon` varchar(51) NOT NULL DEFAULT '', + PRIMARY KEY (`id`), + KEY `FK_acc_weights` (`userId`), + CONSTRAINT `FK_acc_weights` FOREIGN KEY (`userId`) REFERENCES `aowow_account` (`id`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=COMPACT; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `aowow_achievement` +-- + +DROP TABLE IF EXISTS `aowow_achievement`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `aowow_achievement` ( + `id` smallint(5) unsigned NOT NULL, + `faction` tinyint(3) unsigned NOT NULL, + `map` smallint(6) NOT NULL, + `chainId` tinyint(3) unsigned NOT NULL DEFAULT 0, + `chainPos` tinyint(3) unsigned NOT NULL DEFAULT 0, + `category` smallint(5) unsigned NOT NULL DEFAULT 0, + `parentCat` smallint(6) NOT NULL DEFAULT 0, + `points` tinyint(3) unsigned NOT NULL DEFAULT 0, + `orderInGroup` tinyint(3) unsigned NOT NULL DEFAULT 0, + `iconId` smallint(5) unsigned NOT NULL DEFAULT 0, + `iconIdBak` mediumint(8) unsigned NOT NULL DEFAULT 0, + `flags` smallint(5) unsigned NOT NULL DEFAULT 0, + `reqCriteriaCount` tinyint(3) unsigned NOT NULL DEFAULT 0, + `refAchievement` smallint(5) unsigned NOT NULL DEFAULT 0, + `itemExtra` mediumint(8) unsigned DEFAULT NULL, + `cuFlags` int(10) unsigned NOT NULL DEFAULT 0 COMMENT 'see defines.php for flags', + `name_loc0` varchar(78) DEFAULT NULL, + `name_loc2` varchar(79) DEFAULT NULL, + `name_loc3` varchar(86) DEFAULT NULL, + `name_loc4` varchar(86) DEFAULT NULL, + `name_loc6` varchar(78) DEFAULT NULL, + `name_loc8` varchar(76) DEFAULT NULL, + `description_loc0` text DEFAULT NULL, + `description_loc2` text DEFAULT NULL, + `description_loc3` text DEFAULT NULL, + `description_loc4` text DEFAULT NULL, + `description_loc6` text DEFAULT NULL, + `description_loc8` text DEFAULT NULL, + `reward_loc0` varchar(74) DEFAULT NULL, + `reward_loc2` varchar(88) DEFAULT NULL, + `reward_loc3` varchar(92) DEFAULT NULL, + `reward_loc4` varchar(92) DEFAULT NULL, + `reward_loc6` varchar(83) DEFAULT NULL, + `reward_loc8` varchar(95) DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `iconId` (`iconId`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `aowow_achievementcategory` +-- + +DROP TABLE IF EXISTS `aowow_achievementcategory`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `aowow_achievementcategory` ( + `id` smallint(5) unsigned NOT NULL DEFAULT 0, + `parentCat` smallint(6) NOT NULL DEFAULT 0, + `parentCat2` smallint(6) NOT NULL DEFAULT 0, + PRIMARY KEY (`id`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `aowow_announcements` +-- + +DROP TABLE IF EXISTS `aowow_announcements`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `aowow_announcements` ( + `id` int(11) NOT NULL AUTO_INCREMENT COMMENT 'iirc negative Ids cant be deleted', + `page` varchar(256) NOT NULL, + `name` varchar(256) NOT NULL, + `groupMask` smallint(5) unsigned NOT NULL, + `style` varchar(256) NOT NULL, + `mode` tinyint(3) unsigned NOT NULL COMMENT '0:pageTop; 1:contentTop', + `status` tinyint(3) unsigned NOT NULL COMMENT '0:disabled; 1:enabled; 2:deleted', + `text_loc0` text DEFAULT NULL, + `text_loc2` text DEFAULT NULL, + `text_loc3` text DEFAULT NULL, + `text_loc4` text DEFAULT NULL, + `text_loc6` text DEFAULT NULL, + `text_loc8` text DEFAULT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `aowow_areatrigger` +-- + +DROP TABLE IF EXISTS `aowow_areatrigger`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `aowow_areatrigger` ( + `id` int(10) unsigned NOT NULL, + `cuFlags` int(10) unsigned NOT NULL DEFAULT 0 COMMENT 'see defines.php for flags', + `type` smallint(5) unsigned NOT NULL, + `mapId` smallint(5) unsigned NOT NULL COMMENT 'world pos. from dbc', + `posX` float NOT NULL COMMENT 'world pos. from dbc', + `posY` float NOT NULL COMMENT 'world pos. from dbc', + `orientation` float NOT NULL, + `name` varchar(100) DEFAULT NULL, + `quest` mediumint(8) unsigned DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `quest` (`quest`), + KEY `type` (`type`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `aowow_articles` +-- + +DROP TABLE IF EXISTS `aowow_articles`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `aowow_articles` ( + `type` smallint(6) DEFAULT NULL, + `typeId` mediumint(9) DEFAULT NULL, + `locale` tinyint(3) unsigned NOT NULL, + `url` varchar(50) DEFAULT NULL, + `rev` tinyint(3) unsigned NOT NULL DEFAULT 0, + `editAccess` smallint(5) unsigned NOT NULL DEFAULT 2, + `article` mediumtext DEFAULT NULL COMMENT 'Markdown formated', + UNIQUE KEY `type` (`type`,`typeId`,`locale`,`rev`), + UNIQUE KEY `url` (`url`,`locale`,`rev`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `aowow_classes` +-- + +DROP TABLE IF EXISTS `aowow_classes`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +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, + `name_loc4` varchar(128) DEFAULT NULL, + `name_loc6` varchar(128) DEFAULT NULL, + `name_loc8` varchar(128) DEFAULT NULL, + `powerType` tinyint(4) NOT NULL DEFAULT 0, + `raceMask` int(11) NOT NULL DEFAULT 0, + `roles` int(11) NOT NULL DEFAULT 0, + `skills` varchar(32) NOT NULL DEFAULT '', + `flags` mediumint(9) NOT NULL DEFAULT 0, + `cuFlags` int(10) unsigned NOT NULL DEFAULT 0 COMMENT 'see defines.php for flags', + `weaponTypeMask` int(11) NOT NULL DEFAULT 0, + `armorTypeMask` int(11) NOT NULL DEFAULT 0, + `expansion` tinyint(4) NOT NULL DEFAULT 0, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `aowow_comments` +-- + +DROP TABLE IF EXISTS `aowow_comments`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `aowow_comments` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT 'Comment ID', + `type` smallint(5) unsigned NOT NULL DEFAULT 0 COMMENT 'Type of Page', + `typeId` mediumint(9) NOT NULL DEFAULT 0 COMMENT 'ID Of Page', + `userId` int(10) unsigned DEFAULT NULL COMMENT 'User ID', + `roles` smallint(5) unsigned NOT NULL, + `body` text NOT NULL COMMENT 'Comment text', + `date` int(11) NOT NULL COMMENT 'Comment timestap', + `flags` smallint(6) NOT NULL DEFAULT 0 COMMENT 'deleted, outofdate, sticky', + `replyTo` int(10) unsigned NOT NULL DEFAULT 0 COMMENT 'Reply To, comment ID', + `editUserId` int(10) unsigned NOT NULL DEFAULT 0 COMMENT 'Last Edit User ID', + `editDate` int(10) unsigned NOT NULL DEFAULT 0 COMMENT 'Last Edit Time', + `editCount` tinyint(3) unsigned NOT NULL DEFAULT 0 COMMENT 'Count Of Edits', + `deleteUserId` int(10) unsigned NOT NULL DEFAULT 0, + `deleteDate` int(10) unsigned NOT NULL DEFAULT 0, + `responseUserId` int(10) unsigned NOT NULL DEFAULT 0, + `responseBody` text DEFAULT NULL, + `responseRoles` smallint(5) unsigned NOT NULL DEFAULT 0, + PRIMARY KEY (`id`), + KEY `type_typeId` (`type`,`typeId`), + KEY `FK_acc_co` (`userId`), + CONSTRAINT `FK_acc_co` FOREIGN KEY (`userId`) REFERENCES `aowow_account` (`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 */; + +-- +-- Table structure for table `aowow_config` +-- + +DROP TABLE IF EXISTS `aowow_config`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `aowow_config` ( + `key` varchar(50) NOT NULL, + `value` varchar(255) NOT NULL, + `default` varchar(255) DEFAULT NULL, + `cat` tinyint(3) unsigned NOT NULL DEFAULT 0, + `flags` smallint(5) unsigned NOT NULL DEFAULT 0, + `comment` varchar(255) NOT NULL DEFAULT '', + PRIMARY KEY (`key`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `aowow_creature` +-- + +DROP TABLE IF EXISTS `aowow_creature`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `aowow_creature` ( + `id` mediumint(8) unsigned NOT NULL DEFAULT 0, + `cuFlags` int(10) unsigned NOT NULL DEFAULT 0 COMMENT 'see defines.php for flags', + `difficultyEntry1` mediumint(8) unsigned NOT NULL DEFAULT 0, + `difficultyEntry2` mediumint(8) unsigned NOT NULL DEFAULT 0, + `difficultyEntry3` mediumint(8) unsigned NOT NULL DEFAULT 0, + `KillCredit1` int(10) unsigned NOT NULL DEFAULT 0, + `KillCredit2` int(10) unsigned NOT NULL DEFAULT 0, + `displayId1` mediumint(8) unsigned NOT NULL DEFAULT 0, + `displayId2` mediumint(8) unsigned NOT NULL DEFAULT 0, + `displayId3` mediumint(8) unsigned NOT NULL DEFAULT 0, + `displayId4` mediumint(8) unsigned NOT NULL DEFAULT 0, + `textureString` varchar(50) DEFAULT NULL, + `modelId` mediumint(9) NOT NULL DEFAULT 0, + `humanoid` tinyint(3) unsigned NOT NULL DEFAULT 0, + `iconString` varchar(50) DEFAULT NULL COMMENT 'first texture of first model for search (up to 11 other skins omitted..)', + `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, + `subname_loc0` varchar(100) DEFAULT NULL, + `subname_loc2` varchar(100) DEFAULT NULL, + `subname_loc3` varchar(100) DEFAULT NULL, + `subname_loc4` varchar(100) DEFAULT NULL, + `subname_loc6` varchar(100) DEFAULT NULL, + `subname_loc8` varchar(100) DEFAULT NULL, + `minLevel` tinyint(3) unsigned NOT NULL DEFAULT 1, + `maxLevel` tinyint(3) unsigned NOT NULL DEFAULT 1, + `exp` smallint(6) NOT NULL DEFAULT 0, + `faction` smallint(5) unsigned NOT NULL DEFAULT 0, + `npcflag` int(10) unsigned NOT NULL DEFAULT 0, + `rank` tinyint(3) unsigned NOT NULL DEFAULT 0, + `dmgSchool` tinyint(4) NOT NULL DEFAULT 0, + `dmgMultiplier` float NOT NULL DEFAULT 1, + `atkSpeed` int(10) unsigned NOT NULL DEFAULT 0, + `rngAtkSpeed` int(10) unsigned NOT NULL DEFAULT 0, + `mleVariance` float NOT NULL DEFAULT 1, + `rngVariance` float NOT NULL DEFAULT 1, + `unitClass` tinyint(3) unsigned NOT NULL DEFAULT 0, + `unitFlags` int(10) unsigned NOT NULL DEFAULT 0, + `unitFlags2` int(10) unsigned NOT NULL DEFAULT 0, + `dynamicFlags` int(10) unsigned NOT NULL DEFAULT 0, + `family` tinyint(4) NOT NULL DEFAULT 0, + `trainerType` tinyint(4) NOT NULL DEFAULT 0, + `trainerRequirement` smallint(5) unsigned NOT NULL DEFAULT 0, + `dmgMin` float unsigned NOT NULL DEFAULT 0, + `dmgMax` float unsigned NOT NULL DEFAULT 0, + `mleAtkPwrMin` smallint(5) unsigned NOT NULL DEFAULT 0, + `mleAtkPwrMax` smallint(5) unsigned NOT NULL DEFAULT 0, + `rngAtkPwrMin` smallint(5) unsigned NOT NULL DEFAULT 0, + `rngAtkPwrMax` smallint(5) unsigned NOT NULL DEFAULT 0, + `type` tinyint(3) unsigned NOT NULL DEFAULT 0, + `typeFlags` int(10) unsigned NOT NULL DEFAULT 0, + `lootId` mediumint(8) unsigned NOT NULL DEFAULT 0, + `pickpocketLootId` mediumint(8) unsigned NOT NULL DEFAULT 0, + `skinLootId` mediumint(8) unsigned NOT NULL DEFAULT 0, + `spell1` mediumint(8) unsigned NOT NULL DEFAULT 0, + `spell2` mediumint(8) unsigned NOT NULL DEFAULT 0, + `spell3` mediumint(8) unsigned NOT NULL DEFAULT 0, + `spell4` mediumint(8) unsigned NOT NULL DEFAULT 0, + `spell5` mediumint(8) unsigned NOT NULL DEFAULT 0, + `spell6` mediumint(8) unsigned NOT NULL DEFAULT 0, + `spell7` mediumint(8) unsigned NOT NULL DEFAULT 0, + `spell8` mediumint(8) unsigned NOT NULL DEFAULT 0, + `petSpellDataId` mediumint(8) unsigned NOT NULL DEFAULT 0, + `vehicleId` mediumint(8) unsigned NOT NULL DEFAULT 0, + `minGold` mediumint(8) unsigned NOT NULL DEFAULT 0, + `maxGold` mediumint(8) unsigned NOT NULL DEFAULT 0, + `healthMin` int(10) unsigned NOT NULL DEFAULT 1, + `healthMax` int(10) unsigned NOT NULL DEFAULT 1, + `manaMin` int(10) unsigned NOT NULL DEFAULT 1, + `manaMax` int(10) unsigned NOT NULL DEFAULT 1, + `armorMin` mediumint(8) unsigned NOT NULL DEFAULT 1, + `armorMax` mediumint(8) unsigned NOT NULL DEFAULT 1, + `resistance1` smallint(6) NOT NULL DEFAULT 0, + `resistance2` smallint(6) NOT NULL DEFAULT 0, + `resistance3` smallint(6) NOT NULL DEFAULT 0, + `resistance4` smallint(6) NOT NULL DEFAULT 0, + `resistance5` smallint(6) NOT NULL DEFAULT 0, + `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, + `ScriptOrAI` varchar(64) DEFAULT NULL, + `StringId` varchar(64) DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `difficultyEntry1` (`difficultyEntry1`), + KEY `difficultyEntry2` (`difficultyEntry2`), + KEY `difficultyEntry3` (`difficultyEntry3`), + KEY `idx_loot` (`lootId`), + KEY `idx_pickpocketloot` (`pickpocketLootId`), + 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 */; + +-- +-- Table structure for table `aowow_creature_sounds` +-- + +DROP TABLE IF EXISTS `aowow_creature_sounds`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `aowow_creature_sounds` ( + `id` smallint(5) unsigned NOT NULL COMMENT 'CreatureDisplayInfo.dbc/id', + `greeting` smallint(5) unsigned NOT NULL DEFAULT 0, + `farewell` smallint(5) unsigned NOT NULL DEFAULT 0, + `angry` smallint(5) unsigned NOT NULL DEFAULT 0, + `exertion` smallint(5) unsigned NOT NULL DEFAULT 0, + `exertioncritical` smallint(5) unsigned NOT NULL DEFAULT 0, + `injury` smallint(5) unsigned NOT NULL DEFAULT 0, + `injurycritical` smallint(5) unsigned NOT NULL DEFAULT 0, + `death` smallint(5) unsigned NOT NULL DEFAULT 0, + `stun` smallint(5) unsigned NOT NULL DEFAULT 0, + `stand` smallint(5) unsigned NOT NULL DEFAULT 0, + `footstep` smallint(5) unsigned NOT NULL DEFAULT 0, + `aggro` smallint(5) unsigned NOT NULL DEFAULT 0, + `wingflap` smallint(5) unsigned NOT NULL DEFAULT 0, + `wingglide` smallint(5) unsigned NOT NULL DEFAULT 0, + `alert` smallint(5) unsigned NOT NULL DEFAULT 0, + `fidget` smallint(5) unsigned NOT NULL DEFAULT 0, + `customattack` smallint(5) unsigned NOT NULL DEFAULT 0, + `loop` smallint(5) unsigned NOT NULL DEFAULT 0, + `jumpstart` smallint(5) unsigned NOT NULL DEFAULT 0, + `jumpend` smallint(5) unsigned NOT NULL DEFAULT 0, + `petattack` smallint(5) unsigned NOT NULL DEFAULT 0, + `petorder` smallint(5) unsigned NOT NULL DEFAULT 0, + `petdismiss` smallint(5) unsigned NOT NULL DEFAULT 0, + `birth` smallint(5) unsigned NOT NULL DEFAULT 0, + `spellcast` smallint(5) unsigned NOT NULL DEFAULT 0, + `submerge` smallint(5) unsigned NOT NULL DEFAULT 0, + `submerged` smallint(5) unsigned NOT NULL DEFAULT 0, + `transform` smallint(5) unsigned NOT NULL DEFAULT 0, + `transformanimated` smallint(5) unsigned NOT NULL DEFAULT 0, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='!ATTENTION!\r\nthe primary key of this table is NOT a creatureId, but displayId\r\n\r\ncolumn names from LANG.sound_activities'; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `aowow_creature_waypoints` +-- + +DROP TABLE IF EXISTS `aowow_creature_waypoints`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `aowow_creature_waypoints` ( + `creatureOrPath` int(11) NOT NULL, + `point` smallint(5) unsigned NOT NULL, + `areaId` smallint(5) unsigned NOT NULL, + `floor` tinyint(4) NOT NULL DEFAULT -1, + `posX` float unsigned NOT NULL, + `posY` float unsigned NOT NULL, + `wait` int(10) unsigned NOT NULL DEFAULT 0, + PRIMARY KEY (`creatureOrPath`,`point`,`areaId`,`floor`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `aowow_currencies` +-- + +DROP TABLE IF EXISTS `aowow_currencies`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `aowow_currencies` ( + `id` int(11) NOT NULL, + `category` mediumint(9) NOT NULL, + `cuFlags` int(10) unsigned NOT NULL DEFAULT 0 COMMENT 'see defines.php for flags', + `iconId` smallint(5) unsigned NOT NULL DEFAULT 0, + `itemId` int(11) NOT NULL DEFAULT 0, + `cap` int(10) unsigned NOT NULL DEFAULT 0, + `name_loc0` varchar(64) DEFAULT NULL, + `name_loc2` varchar(64) DEFAULT NULL, + `name_loc3` varchar(64) DEFAULT NULL, + `name_loc4` varchar(64) DEFAULT NULL, + `name_loc6` varchar(64) DEFAULT NULL, + `name_loc8` varchar(64) DEFAULT NULL, + `description_loc0` varchar(256) DEFAULT NULL, + `description_loc2` varchar(256) DEFAULT NULL, + `description_loc3` varchar(256) DEFAULT NULL, + `description_loc4` varchar(256) DEFAULT NULL, + `description_loc6` varchar(256) DEFAULT NULL, + `description_loc8` varchar(256) DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `iconId` (`iconId`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `aowow_dbversion` +-- + +DROP TABLE IF EXISTS `aowow_dbversion`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `aowow_dbversion` ( + `date` int(10) unsigned NOT NULL DEFAULT 0, + `part` tinyint(3) unsigned NOT NULL DEFAULT 0, + `sql` text DEFAULT NULL, + `build` text DEFAULT NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `aowow_declinedword` +-- + +DROP TABLE IF EXISTS `aowow_declinedword`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `aowow_declinedword` ( + `id` smallint(5) unsigned NOT NULL, + `word` varchar(127) NOT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `aowow_declinedwordcases` +-- + +DROP TABLE IF EXISTS `aowow_declinedwordcases`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `aowow_declinedwordcases` ( + `wordId` smallint(5) unsigned NOT NULL, + `caseIdx` tinyint(3) unsigned NOT NULL, + `word` varchar(131) NOT NULL, + PRIMARY KEY (`wordId`,`caseIdx`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `aowow_emotes` +-- + +DROP TABLE IF EXISTS `aowow_emotes`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `aowow_emotes` ( + `id` smallint(6) NOT NULL, + `cmd` varchar(35) NOT NULL, + `isAnimated` tinyint(3) unsigned NOT NULL DEFAULT 0, + `flags` smallint(5) unsigned NOT NULL DEFAULT 0, + `parentEmote` smallint(6) NOT NULL DEFAULT 0, + `soundId` smallint(6) NOT NULL DEFAULT 0, + `state` tinyint(3) unsigned NOT NULL DEFAULT 0, + `stateParam` tinyint(3) unsigned NOT NULL DEFAULT 0, + `cuFlags` int(10) unsigned NOT NULL DEFAULT 0 COMMENT 'see defines.php for flags', + `extToExt_loc0` varchar(150) DEFAULT NULL, + `extToExt_loc2` varchar(150) DEFAULT NULL, + `extToExt_loc3` varchar(150) DEFAULT NULL, + `extToExt_loc4` varchar(150) DEFAULT NULL, + `extToExt_loc6` varchar(150) DEFAULT NULL, + `extToExt_loc8` varchar(150) DEFAULT NULL, + `extToMe_loc0` varchar(150) DEFAULT NULL, + `extToMe_loc2` varchar(150) DEFAULT NULL, + `extToMe_loc3` varchar(150) DEFAULT NULL, + `extToMe_loc4` varchar(150) DEFAULT NULL, + `extToMe_loc6` varchar(150) DEFAULT NULL, + `extToMe_loc8` varchar(150) DEFAULT NULL, + `meToExt_loc0` varchar(150) DEFAULT NULL, + `meToExt_loc2` varchar(150) DEFAULT NULL, + `meToExt_loc3` varchar(150) DEFAULT NULL, + `meToExt_loc4` varchar(150) DEFAULT NULL, + `meToExt_loc6` varchar(150) DEFAULT NULL, + `meToExt_loc8` varchar(150) DEFAULT NULL, + `extToNone_loc0` varchar(150) DEFAULT NULL, + `extToNone_loc2` varchar(150) DEFAULT NULL, + `extToNone_loc3` varchar(150) DEFAULT NULL, + `extToNone_loc4` varchar(150) DEFAULT NULL, + `extToNone_loc6` varchar(150) DEFAULT NULL, + `extToNone_loc8` varchar(150) DEFAULT NULL, + `meToNone_loc0` varchar(150) DEFAULT NULL, + `meToNone_loc2` varchar(150) DEFAULT NULL, + `meToNone_loc3` varchar(150) DEFAULT NULL, + `meToNone_loc4` varchar(150) DEFAULT NULL, + `meToNone_loc6` varchar(150) DEFAULT NULL, + `meToNone_loc8` varchar(150) DEFAULT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `aowow_emotes_aliasses` +-- + +DROP TABLE IF EXISTS `aowow_emotes_aliasses`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `aowow_emotes_aliasses` ( + `id` smallint(5) unsigned NOT NULL, + `locales` smallint(5) unsigned NOT NULL, + `command` varchar(20) NOT NULL, + UNIQUE KEY `id_command` (`id`,`command`), + KEY `id` (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `aowow_emotes_sounds` +-- + +DROP TABLE IF EXISTS `aowow_emotes_sounds`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `aowow_emotes_sounds` ( + `emoteId` smallint(5) unsigned NOT NULL, + `raceId` tinyint(3) unsigned NOT NULL, + `gender` tinyint(3) unsigned NOT NULL, + `soundId` smallint(5) unsigned NOT NULL, + UNIQUE KEY `emoteId_raceId_gender_soundId` (`emoteId`,`raceId`,`gender`,`soundId`), + KEY `emoteId` (`emoteId`), + KEY `raceId` (`raceId`), + KEY `soundId` (`soundId`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `aowow_errors` +-- + +DROP TABLE IF EXISTS `aowow_errors`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `aowow_errors` ( + `date` int(10) unsigned DEFAULT NULL, + `version` tinyint(3) unsigned NOT NULL, + `phpError` smallint(5) unsigned NOT NULL, + `file` varchar(150) NOT NULL, + `line` smallint(5) unsigned NOT NULL, + `query` varchar(250) NOT NULL, + `post` text NOT NULL, + `userGroups` smallint(5) unsigned NOT NULL, + `message` text DEFAULT NULL, + PRIMARY KEY (`file`,`line`,`phpError`,`version`,`userGroups`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `aowow_events` +-- + +DROP TABLE IF EXISTS `aowow_events`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `aowow_events` ( + `id` smallint(5) unsigned NOT NULL, + `holidayId` mediumint(8) unsigned NOT NULL DEFAULT 0, + `cuFlags` int(10) unsigned NOT NULL DEFAULT 0 COMMENT 'see defines.php for flags', + `startTime` int(11) NOT NULL, + `endTime` int(11) NOT NULL, + `occurence` int(10) unsigned NOT NULL, + `length` int(10) unsigned NOT NULL, + `requires` varchar(255) DEFAULT NULL, + `description` varchar(255) DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `holidayId` (`holidayId`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `aowow_factions` +-- + +DROP TABLE IF EXISTS `aowow_factions`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `aowow_factions` ( + `id` smallint(5) unsigned NOT NULL, + `repIdx` smallint(6) NOT NULL, + `baseRepRaceMask1` mediumint(8) unsigned NOT NULL, + `baseRepRaceMask2` mediumint(8) unsigned NOT NULL, + `baseRepRaceMask3` mediumint(8) unsigned NOT NULL, + `baseRepRaceMask4` mediumint(8) unsigned NOT NULL, + `baseRepClassMask1` mediumint(8) unsigned NOT NULL, + `baseRepClassMask2` mediumint(8) unsigned NOT NULL, + `baseRepClassMask3` mediumint(8) unsigned NOT NULL, + `baseRepClassMask4` mediumint(8) unsigned NOT NULL, + `baseRepValue1` mediumint(9) NOT NULL, + `baseRepValue2` 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', + `templateIds` text NOT NULL COMMENT 'space separated', + `cuFlags` int(10) unsigned NOT NULL DEFAULT 0 COMMENT 'see defines.php for flags', + `parentFactionId` smallint(5) unsigned NOT NULL, + `spilloverRateIn` float(8,2) NOT NULL, + `spilloverRateOut` float(8,2) NOT NULL, + `spilloverMaxRank` tinyint(3) unsigned NOT NULL, + `name_loc0` varchar(35) DEFAULT NULL, + `name_loc2` varchar(49) DEFAULT NULL, + `name_loc3` varchar(40) DEFAULT NULL, + `name_loc4` varchar(40) DEFAULT NULL, + `name_loc6` varchar(50) DEFAULT NULL, + `name_loc8` varchar(47) DEFAULT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `aowow_factiontemplate` +-- + +DROP TABLE IF EXISTS `aowow_factiontemplate`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `aowow_factiontemplate` ( + `id` smallint(5) unsigned NOT NULL, + `factionId` smallint(5) unsigned NOT NULL, + `A` tinyint(4) NOT NULL COMMENT 'Aliance: -1 - hostile, 1 - friendly, 0 - neutral', + `H` tinyint(4) NOT NULL COMMENT 'Horde: -1 - hostile, 1 - friendly, 0 - neutral', + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `aowow_glyphproperties` +-- + +DROP TABLE IF EXISTS `aowow_glyphproperties`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `aowow_glyphproperties` ( + `id` smallint(5) unsigned NOT NULL, + `spellId` mediumint(8) unsigned NOT NULL, + `typeFlags` tinyint(3) unsigned NOT NULL, + `iconId` smallint(5) unsigned NOT NULL DEFAULT 0, + `iconIdBak` 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 */; + +-- +-- Table structure for table `aowow_guides` +-- + +DROP TABLE IF EXISTS `aowow_guides`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `aowow_guides` ( + `id` mediumint(8) unsigned NOT NULL AUTO_INCREMENT, + `category` smallint(5) unsigned NOT NULL DEFAULT 0, + `classId` tinyint(3) unsigned DEFAULT NULL, + `specId` tinyint(4) DEFAULT NULL, + `title` varchar(100) NOT NULL DEFAULT '' COMMENT 'title for menus + lists', + `name` varchar(100) NOT NULL DEFAULT '' COMMENT 'title for the page tiself', + `description` varchar(200) NOT NULL DEFAULT '', + `url` varchar(50) DEFAULT NULL, + `locale` tinyint(3) unsigned NOT NULL DEFAULT 0, + `status` tinyint(3) unsigned NOT NULL DEFAULT 1, + `rev` tinyint(3) unsigned NOT NULL DEFAULT 0, + `cuFlags` int(10) unsigned NOT NULL DEFAULT 0 COMMENT 'see defines.php for flags', + `roles` smallint(5) unsigned NOT NULL DEFAULT 0, + `views` mediumint(8) unsigned NOT NULL DEFAULT 0, + `userId` mediumint(8) unsigned DEFAULT NULL, + `date` int(10) unsigned NOT NULL DEFAULT 0, + `approveUserId` mediumint(8) unsigned DEFAULT NULL, + `approveDate` int(10) unsigned NOT NULL DEFAULT 0, + `deleteUserId` mediumint(8) unsigned DEFAULT NULL, + `deleteData` int(10) unsigned NOT NULL DEFAULT 0, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `aowow_guides_changelog` +-- + +DROP TABLE IF EXISTS `aowow_guides_changelog`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `aowow_guides_changelog` ( + `id` mediumint(8) unsigned NOT NULL, + `rev` tinyint(3) unsigned DEFAULT NULL, + `date` int(10) unsigned NOT NULL, + `userId` mediumint(8) unsigned NOT NULL, + `status` tinyint(3) unsigned NOT NULL DEFAULT 0, + `msg` varchar(200) DEFAULT '', + KEY `id` (`id`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `aowow_holidays` +-- + +DROP TABLE IF EXISTS `aowow_holidays`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `aowow_holidays` ( + `id` smallint(5) unsigned NOT NULL, + `bossCreature` mediumint(8) unsigned NOT NULL DEFAULT 0, + `achievementCatOrId` mediumint(9) NOT NULL DEFAULT 0, + `name_loc0` varchar(36) DEFAULT NULL, + `name_loc2` varchar(42) DEFAULT NULL, + `name_loc3` varchar(36) DEFAULT NULL, + `name_loc4` varchar(36) DEFAULT NULL, + `name_loc6` varchar(49) DEFAULT NULL, + `name_loc8` varchar(29) DEFAULT NULL, + `description_loc0` text DEFAULT NULL, + `description_loc2` text DEFAULT NULL, + `description_loc3` text DEFAULT NULL, + `description_loc4` text DEFAULT NULL, + `description_loc6` text DEFAULT NULL, + `description_loc8` text DEFAULT NULL, + `looping` tinyint(4) NOT NULL, + `scheduleType` tinyint(4) NOT NULL, + `textureString` varchar(30) NOT NULL DEFAULT '', + `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 */; + +-- +-- Table structure for table `aowow_home_featuredbox` +-- + +DROP TABLE IF EXISTS `aowow_home_featuredbox`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `aowow_home_featuredbox` ( + `id` smallint(5) unsigned NOT NULL, + `editorId` int(10) unsigned DEFAULT NULL, + `editDate` int(10) unsigned NOT NULL, + `startDate` int(10) unsigned NOT NULL DEFAULT 0, + `endDate` int(10) unsigned NOT NULL DEFAULT 0, + `extraWide` tinyint(3) unsigned NOT NULL DEFAULT 0, + `boxBG` varchar(150) DEFAULT NULL, + `altHomeLogo` varchar(150) DEFAULT NULL, + `altHeaderLogo` varchar(150) DEFAULT NULL, + `text_loc0` text DEFAULT NULL, + `text_loc2` text DEFAULT NULL, + `text_loc3` text DEFAULT NULL, + `text_loc4` text DEFAULT NULL, + `text_loc6` text DEFAULT NULL, + `text_loc8` text DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `FK_acc_hFBox` (`editorId`), + CONSTRAINT `FK_acc_hFBox` FOREIGN KEY (`editorId`) REFERENCES `aowow_account` (`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 */; + +-- +-- Table structure for table `aowow_home_featuredbox_overlay` +-- + +DROP TABLE IF EXISTS `aowow_home_featuredbox_overlay`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `aowow_home_featuredbox_overlay` ( + `featureId` smallint(5) unsigned NOT NULL, + `left` smallint(5) unsigned NOT NULL, + `width` smallint(5) unsigned NOT NULL, + `url` varchar(150) NOT NULL, + `title_loc0` varchar(100) DEFAULT '', + `title_loc2` varchar(100) DEFAULT '', + `title_loc3` varchar(100) DEFAULT '', + `title_loc4` varchar(100) DEFAULT '', + `title_loc6` varchar(100) DEFAULT '', + `title_loc8` varchar(100) DEFAULT '', + KEY `FK_home_featurebox` (`featureId`), + CONSTRAINT `FK_home_featurebox` FOREIGN KEY (`featureId`) REFERENCES `aowow_home_featuredbox` (`id`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `aowow_home_oneliner` +-- + +DROP TABLE IF EXISTS `aowow_home_oneliner`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `aowow_home_oneliner` ( + `id` smallint(5) unsigned NOT NULL, + `editorId` int(10) unsigned DEFAULT NULL, + `editDate` int(10) unsigned NOT NULL, + `active` tinyint(3) unsigned NOT NULL, + `text_loc0` varchar(200) DEFAULT NULL, + `text_loc2` varchar(200) DEFAULT NULL, + `text_loc3` varchar(200) DEFAULT NULL, + `text_loc4` varchar(200) DEFAULT NULL, + `text_loc6` varchar(200) DEFAULT NULL, + `text_loc8` varchar(200) DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `FK_acc_hOneliner` (`editorId`), + CONSTRAINT `FK_acc_hOneliner` FOREIGN KEY (`editorId`) REFERENCES `aowow_account` (`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 */; + +-- +-- Table structure for table `aowow_home_titles` +-- + +DROP TABLE IF EXISTS `aowow_home_titles`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `aowow_home_titles` ( + `id` smallint(5) unsigned NOT NULL AUTO_INCREMENT, + `editorId` int(10) unsigned DEFAULT NULL, + `editDate` int(10) unsigned NOT NULL, + `active` tinyint(3) unsigned NOT NULL, + `locale` tinyint(3) unsigned NOT NULL, + `title` varchar(100) NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `locale_title` (`locale`,`title`), + KEY `FK_acc_hTitles` (`editorId`), + CONSTRAINT `FK_acc_hTitles` FOREIGN KEY (`editorId`) REFERENCES `aowow_account` (`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 */; + +-- +-- Table structure for table `aowow_icons` +-- + +DROP TABLE IF EXISTS `aowow_icons`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +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 `idx_sourcename` (`name_source`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `aowow_item_stats` +-- + +DROP TABLE IF EXISTS `aowow_item_stats`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `aowow_item_stats` ( + `type` smallint(5) unsigned NOT NULL, + `typeId` mediumint(8) NOT NULL, + `nsockets` tinyint(3) unsigned NOT NULL DEFAULT 0, + `dps` float(8,2) DEFAULT NULL, + `damagetype` tinyint(4) DEFAULT NULL, + `dmgmin1` mediumint(5) unsigned DEFAULT NULL, + `dmgmax1` mediumint(5) unsigned DEFAULT NULL, + `speed` float(8,2) DEFAULT NULL, + `mledps` float(8,2) DEFAULT NULL, + `mledmgmin` mediumint(5) unsigned DEFAULT NULL, + `mledmgmax` mediumint(5) unsigned DEFAULT NULL, + `mlespeed` float(8,2) DEFAULT NULL, + `rgddps` float(8,2) DEFAULT NULL, + `rgddmgmin` mediumint(5) unsigned DEFAULT NULL, + `rgddmgmax` mediumint(5) unsigned DEFAULT NULL, + `rgdspeed` float(8,2) DEFAULT NULL, + `dmg` float(8,2) NOT NULL DEFAULT 0.00, + `mana` mediumint(6) NOT NULL DEFAULT 0, + `health` mediumint(6) NOT NULL DEFAULT 0, + `agi` mediumint(6) NOT NULL DEFAULT 0, + `str` mediumint(6) NOT NULL DEFAULT 0, + `int` mediumint(6) NOT NULL DEFAULT 0, + `spi` mediumint(6) NOT NULL DEFAULT 0, + `sta` mediumint(6) NOT NULL DEFAULT 0, + `energy` mediumint(6) NOT NULL DEFAULT 0, + `rage` mediumint(6) NOT NULL DEFAULT 0, + `focus` mediumint(6) NOT NULL DEFAULT 0, + `runic` mediumint(6) NOT NULL DEFAULT 0, + `defrtng` mediumint(6) NOT NULL DEFAULT 0, + `dodgertng` mediumint(6) NOT NULL DEFAULT 0, + `parryrtng` mediumint(6) NOT NULL DEFAULT 0, + `blockrtng` mediumint(6) NOT NULL DEFAULT 0, + `mlehitrtng` mediumint(6) NOT NULL DEFAULT 0, + `rgdhitrtng` mediumint(6) NOT NULL DEFAULT 0, + `splhitrtng` mediumint(6) NOT NULL DEFAULT 0, + `mlecritstrkrtng` mediumint(6) NOT NULL DEFAULT 0, + `rgdcritstrkrtng` mediumint(6) NOT NULL DEFAULT 0, + `splcritstrkrtng` mediumint(6) NOT NULL DEFAULT 0, + `_mlehitrtng` mediumint(6) NOT NULL DEFAULT 0, + `_rgdhitrtng` mediumint(6) NOT NULL DEFAULT 0, + `_splhitrtng` mediumint(6) NOT NULL DEFAULT 0, + `_mlecritstrkrtng` mediumint(6) NOT NULL DEFAULT 0, + `_rgdcritstrkrtng` mediumint(6) NOT NULL DEFAULT 0, + `_splcritstrkrtng` mediumint(6) NOT NULL DEFAULT 0, + `mlehastertng` mediumint(6) NOT NULL DEFAULT 0, + `rgdhastertng` mediumint(6) NOT NULL DEFAULT 0, + `splhastertng` mediumint(6) NOT NULL DEFAULT 0, + `hitrtng` mediumint(6) NOT NULL DEFAULT 0, + `critstrkrtng` mediumint(6) NOT NULL DEFAULT 0, + `_hitrtng` mediumint(6) NOT NULL DEFAULT 0, + `_critstrkrtng` mediumint(6) NOT NULL DEFAULT 0, + `resirtng` mediumint(6) NOT NULL DEFAULT 0, + `hastertng` mediumint(6) NOT NULL DEFAULT 0, + `exprtng` mediumint(6) NOT NULL DEFAULT 0, + `atkpwr` mediumint(6) NOT NULL DEFAULT 0, + `mleatkpwr` mediumint(6) NOT NULL DEFAULT 0, + `rgdatkpwr` mediumint(6) NOT NULL DEFAULT 0, + `feratkpwr` mediumint(6) NOT NULL DEFAULT 0, + `splheal` mediumint(6) NOT NULL DEFAULT 0, + `spldmg` mediumint(6) NOT NULL DEFAULT 0, + `manargn` mediumint(6) NOT NULL DEFAULT 0, + `armorpenrtng` mediumint(6) NOT NULL DEFAULT 0, + `splpwr` mediumint(6) NOT NULL DEFAULT 0, + `healthrgn` mediumint(6) NOT NULL DEFAULT 0, + `splpen` mediumint(6) NOT NULL DEFAULT 0, + `block` mediumint(6) NOT NULL DEFAULT 0, + `mastrtng` mediumint(6) NOT NULL DEFAULT 0, + `armor` mediumint(6) NOT NULL DEFAULT 0, + `armorbonus` mediumint(6) DEFAULT NULL, + `firres` mediumint(6) NOT NULL DEFAULT 0, + `frores` mediumint(6) NOT NULL DEFAULT 0, + `holres` mediumint(6) NOT NULL DEFAULT 0, + `shares` mediumint(6) NOT NULL DEFAULT 0, + `natres` mediumint(6) NOT NULL DEFAULT 0, + `arcres` mediumint(6) NOT NULL DEFAULT 0, + `firsplpwr` mediumint(6) NOT NULL DEFAULT 0, + `frosplpwr` mediumint(6) NOT NULL DEFAULT 0, + `holsplpwr` mediumint(6) NOT NULL DEFAULT 0, + `shasplpwr` mediumint(6) NOT NULL DEFAULT 0, + `natsplpwr` mediumint(6) NOT NULL DEFAULT 0, + `arcsplpwr` mediumint(6) NOT NULL DEFAULT 0, + PRIMARY KEY (`type`,`typeId`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `aowow_itemenchantment` +-- + +DROP TABLE IF EXISTS `aowow_itemenchantment`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `aowow_itemenchantment` ( + `id` smallint(5) unsigned NOT NULL, + `charges` tinyint(3) unsigned NOT NULL, + `cuFlags` int(10) unsigned NOT NULL DEFAULT 0 COMMENT 'see defines.php for flags', + `procChance` tinyint(3) unsigned NOT NULL, + `ppmRate` float NOT NULL, + `type1` tinyint(3) unsigned NOT NULL, + `type2` tinyint(3) unsigned NOT NULL, + `type3` tinyint(3) unsigned NOT NULL, + `amount1` smallint(6) NOT NULL, + `amount2` smallint(6) NOT NULL, + `amount3` smallint(6) NOT NULL, + `object1` mediumint(8) unsigned NOT NULL, + `object2` mediumint(8) unsigned NOT NULL, + `object3` smallint(5) unsigned NOT NULL, + `name_loc0` varchar(65) DEFAULT NULL, + `name_loc2` varchar(91) DEFAULT NULL, + `name_loc3` varchar(84) DEFAULT NULL, + `name_loc4` varchar(84) DEFAULT NULL, + `name_loc6` varchar(89) DEFAULT NULL, + `name_loc8` varchar(96) DEFAULT NULL, + `conditionId` tinyint(3) unsigned NOT NULL, + `skillLine` smallint(5) unsigned NOT NULL, + `skillLevel` smallint(5) unsigned NOT NULL, + `requiredLevel` tinyint(3) unsigned NOT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `aowow_itemrandomenchant` +-- + +DROP TABLE IF EXISTS `aowow_itemrandomenchant`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `aowow_itemrandomenchant` ( + `id` smallint(6) NOT NULL, + `name_loc0` varchar(250) DEFAULT NULL, + `name_loc2` varchar(250) DEFAULT NULL, + `name_loc3` varchar(250) DEFAULT NULL, + `name_loc4` varchar(250) DEFAULT NULL, + `name_loc6` varchar(250) DEFAULT NULL, + `name_loc8` varchar(250) DEFAULT NULL, + `nameINT` char(250) NOT NULL, + `enchantId1` smallint(5) unsigned NOT NULL, + `enchantId2` smallint(5) unsigned NOT NULL, + `enchantId3` smallint(5) unsigned NOT NULL, + `enchantId4` smallint(5) unsigned NOT NULL, + `enchantId5` smallint(5) unsigned NOT NULL, + `allocationPct1` smallint(5) unsigned NOT NULL, + `allocationPct2` smallint(5) unsigned NOT NULL, + `allocationPct3` smallint(5) unsigned NOT NULL, + `allocationPct4` smallint(5) unsigned NOT NULL, + `allocationPct5` smallint(5) unsigned NOT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `aowow_items` +-- + +DROP TABLE IF EXISTS `aowow_items`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `aowow_items` ( + `id` mediumint(8) unsigned NOT NULL DEFAULT 0, + `class` tinyint(3) unsigned NOT NULL DEFAULT 0, + `classBak` tinyint(4) NOT NULL, + `subClass` tinyint(4) NOT NULL DEFAULT 0, + `subClassBak` tinyint(4) NOT NULL, + `soundOverrideSubclass` tinyint(4) NOT NULL, + `subSubClass` tinyint(4) NOT NULL, + `name_loc0` varchar(127) DEFAULT NULL, + `name_loc2` varchar(127) DEFAULT NULL, + `name_loc3` varchar(127) DEFAULT NULL, + `name_loc4` varchar(127) DEFAULT NULL, + `name_loc6` varchar(127) DEFAULT NULL, + `name_loc8` varchar(127) DEFAULT NULL, + `iconId` smallint(5) unsigned NOT NULL DEFAULT 0, + `displayId` mediumint(8) unsigned NOT NULL DEFAULT 0, + `spellVisualId` smallint(5) unsigned NOT NULL DEFAULT 0, + `quality` tinyint(3) unsigned NOT NULL DEFAULT 0, + `flags` int(10) unsigned NOT NULL DEFAULT 0, + `flagsExtra` int(10) unsigned NOT NULL DEFAULT 0, + `buyCount` tinyint(3) unsigned NOT NULL DEFAULT 1, + `buyPrice` int(11) NOT NULL DEFAULT 0, + `sellPrice` int(10) unsigned NOT NULL DEFAULT 0, + `repairPrice` int(10) unsigned NOT NULL, + `slot` tinyint(4) NOT NULL, + `slotBak` tinyint(3) unsigned NOT NULL DEFAULT 0, + `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, + `requiredSkillRank` smallint(5) unsigned NOT NULL DEFAULT 0, + `requiredSpell` mediumint(8) unsigned NOT NULL DEFAULT 0, + `requiredHonorRank` mediumint(8) unsigned NOT NULL DEFAULT 0, + `requiredCityRank` mediumint(8) unsigned NOT NULL DEFAULT 0, + `requiredFaction` smallint(5) unsigned NOT NULL DEFAULT 0, + `requiredFactionRank` smallint(5) unsigned NOT NULL DEFAULT 0, + `maxCount` int(11) NOT NULL DEFAULT 0, + `cuFlags` int(10) unsigned NOT NULL DEFAULT 0 COMMENT 'see defines.php for flags', + `model` varchar(50) NOT NULL, + `stackable` int(11) DEFAULT 1, + `slots` tinyint(3) unsigned NOT NULL DEFAULT 0, + `statType1` tinyint(3) unsigned NOT NULL DEFAULT 0, + `statValue1` smallint(6) NOT NULL DEFAULT 0, + `statType2` tinyint(3) unsigned NOT NULL DEFAULT 0, + `statValue2` smallint(6) NOT NULL DEFAULT 0, + `statType3` tinyint(3) unsigned NOT NULL DEFAULT 0, + `statValue3` smallint(6) NOT NULL DEFAULT 0, + `statType4` tinyint(3) unsigned NOT NULL DEFAULT 0, + `statValue4` smallint(6) NOT NULL DEFAULT 0, + `statType5` tinyint(3) unsigned NOT NULL DEFAULT 0, + `statValue5` smallint(6) NOT NULL DEFAULT 0, + `statType6` tinyint(3) unsigned NOT NULL DEFAULT 0, + `statValue6` smallint(6) NOT NULL DEFAULT 0, + `statType7` tinyint(3) unsigned NOT NULL DEFAULT 0, + `statValue7` smallint(6) NOT NULL DEFAULT 0, + `statType8` tinyint(3) unsigned NOT NULL DEFAULT 0, + `statValue8` smallint(6) NOT NULL DEFAULT 0, + `statType9` tinyint(3) unsigned NOT NULL DEFAULT 0, + `statValue9` smallint(6) NOT NULL DEFAULT 0, + `statType10` tinyint(3) unsigned NOT NULL DEFAULT 0, + `statValue10` smallint(6) NOT NULL DEFAULT 0, + `scalingStatDistribution` smallint(6) NOT NULL DEFAULT 0, + `scalingStatValue` int(10) unsigned NOT NULL DEFAULT 0, + `dmgMin1` float NOT NULL DEFAULT 0, + `dmgMax1` float NOT NULL DEFAULT 0, + `dmgType1` tinyint(3) unsigned NOT NULL DEFAULT 0, + `dmgMin2` float NOT NULL DEFAULT 0, + `dmgMax2` float NOT NULL DEFAULT 0, + `dmgType2` tinyint(3) unsigned NOT NULL DEFAULT 0, + `delay` smallint(5) unsigned NOT NULL DEFAULT 1000, + `armor` smallint(5) unsigned NOT NULL DEFAULT 0, + `armorDamageModifier` float NOT NULL DEFAULT 0, + `block` mediumint(8) unsigned NOT NULL DEFAULT 0, + `resHoly` tinyint(3) unsigned NOT NULL DEFAULT 0, + `resFire` tinyint(3) unsigned NOT NULL DEFAULT 0, + `resNature` tinyint(3) unsigned NOT NULL DEFAULT 0, + `resFrost` tinyint(3) unsigned NOT NULL DEFAULT 0, + `resShadow` tinyint(3) unsigned NOT NULL DEFAULT 0, + `resArcane` tinyint(3) unsigned NOT NULL DEFAULT 0, + `ammoType` tinyint(3) unsigned NOT NULL DEFAULT 0, + `rangedModRange` float NOT NULL DEFAULT 0, + `spellId1` mediumint(9) NOT NULL DEFAULT 0, + `spellTrigger1` tinyint(3) unsigned NOT NULL DEFAULT 0, + `spellCharges1` smallint(6) DEFAULT NULL, + `spellppmRate1` float NOT NULL DEFAULT 0, + `spellCooldown1` int(11) NOT NULL DEFAULT -1, + `spellCategory1` smallint(5) unsigned NOT NULL DEFAULT 0, + `spellCategoryCooldown1` int(11) NOT NULL DEFAULT -1, + `spellId2` mediumint(9) NOT NULL DEFAULT 0, + `spellTrigger2` tinyint(3) unsigned NOT NULL DEFAULT 0, + `spellCharges2` smallint(6) DEFAULT NULL, + `spellppmRate2` float NOT NULL DEFAULT 0, + `spellCooldown2` int(11) NOT NULL DEFAULT -1, + `spellCategory2` smallint(5) unsigned NOT NULL DEFAULT 0, + `spellCategoryCooldown2` int(11) NOT NULL DEFAULT -1, + `spellId3` mediumint(9) NOT NULL DEFAULT 0, + `spellTrigger3` tinyint(3) unsigned NOT NULL DEFAULT 0, + `spellCharges3` smallint(6) DEFAULT NULL, + `spellppmRate3` float NOT NULL DEFAULT 0, + `spellCooldown3` int(11) NOT NULL DEFAULT -1, + `spellCategory3` smallint(5) unsigned NOT NULL DEFAULT 0, + `spellCategoryCooldown3` int(11) NOT NULL DEFAULT -1, + `spellId4` mediumint(9) NOT NULL DEFAULT 0, + `spellTrigger4` tinyint(3) unsigned NOT NULL DEFAULT 0, + `spellCharges4` smallint(6) DEFAULT NULL, + `spellppmRate4` float NOT NULL DEFAULT 0, + `spellCooldown4` int(11) NOT NULL DEFAULT -1, + `spellCategory4` smallint(5) unsigned NOT NULL DEFAULT 0, + `spellCategoryCooldown4` int(11) NOT NULL DEFAULT -1, + `spellId5` mediumint(9) NOT NULL DEFAULT 0, + `spellTrigger5` tinyint(3) unsigned NOT NULL DEFAULT 0, + `spellCharges5` smallint(6) DEFAULT NULL, + `spellppmRate5` float NOT NULL DEFAULT 0, + `spellCooldown5` int(11) NOT NULL DEFAULT -1, + `spellCategory5` smallint(5) unsigned NOT NULL DEFAULT 0, + `spellCategoryCooldown5` int(11) NOT NULL DEFAULT -1, + `bonding` tinyint(3) unsigned NOT NULL DEFAULT 0, + `description_loc0` varchar(255) DEFAULT NULL, + `description_loc2` varchar(255) DEFAULT NULL, + `description_loc3` varchar(255) DEFAULT NULL, + `description_loc4` varchar(255) DEFAULT NULL, + `description_loc6` varchar(255) DEFAULT NULL, + `description_loc8` varchar(255) DEFAULT NULL, + `pageTextId` mediumint(8) unsigned NOT NULL DEFAULT 0, + `languageId` tinyint(3) unsigned NOT NULL DEFAULT 0, + `startQuest` mediumint(8) unsigned NOT NULL DEFAULT 0, + `lockId` mediumint(8) unsigned NOT NULL DEFAULT 0, + `material` tinyint(4) NOT NULL DEFAULT 0, + `randomEnchant` mediumint(9) NOT NULL DEFAULT 0, + `itemset` mediumint(8) unsigned NOT NULL DEFAULT 0, + `durability` smallint(5) unsigned NOT NULL DEFAULT 0, + `area` mediumint(8) unsigned NOT NULL DEFAULT 0, + `map` smallint(6) NOT NULL DEFAULT 0, + `bagFamily` mediumint(9) NOT NULL DEFAULT 0, + `totemCategory` mediumint(9) NOT NULL DEFAULT 0, + `socketColor1` tinyint(4) NOT NULL DEFAULT 0, + `socketContent1` mediumint(9) NOT NULL DEFAULT 0, + `socketColor2` tinyint(4) NOT NULL DEFAULT 0, + `socketContent2` mediumint(9) NOT NULL DEFAULT 0, + `socketColor3` tinyint(4) NOT NULL DEFAULT 0, + `socketContent3` mediumint(9) NOT NULL DEFAULT 0, + `socketBonus` mediumint(9) NOT NULL DEFAULT 0, + `gemColorMask` mediumint(9) NOT NULL DEFAULT 0, + `requiredDisenchantSkill` smallint(6) NOT NULL DEFAULT -1, + `disenchantId` mediumint(8) unsigned NOT NULL DEFAULT 0, + `duration` int(10) unsigned NOT NULL DEFAULT 0, + `itemLimitCategory` smallint(6) NOT NULL DEFAULT 0, + `eventId` smallint(5) unsigned NOT NULL, + `scriptName` varchar(64) NOT NULL DEFAULT '', + `foodType` tinyint(3) unsigned NOT NULL DEFAULT 0, + `gemEnchantmentId` mediumint(9) NOT NULL, + `minMoneyLoot` int(10) unsigned NOT NULL DEFAULT 0, + `maxMoneyLoot` int(10) unsigned NOT NULL DEFAULT 0, + `pickUpSoundId` smallint(5) unsigned NOT NULL DEFAULT 0, + `dropDownSoundId` smallint(5) unsigned NOT NULL DEFAULT 0, + `sheatheSoundId` smallint(5) unsigned NOT NULL DEFAULT 0, + `unsheatheSoundId` smallint(5) unsigned NOT NULL DEFAULT 0, + `flagsCustom` int(10) unsigned NOT NULL DEFAULT 0, + PRIMARY KEY (`id`), + KEY `items_index` (`class`), + KEY `idx_model` (`displayId`), + KEY `idx_faction` (`requiredFaction`), + 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 */; + +-- +-- Table structure for table `aowow_items_sounds` +-- + +DROP TABLE IF EXISTS `aowow_items_sounds`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `aowow_items_sounds` ( + `soundId` smallint(5) unsigned NOT NULL, + `subClassMask` mediumint(8) unsigned NOT NULL, + PRIMARY KEY (`soundId`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='actually .. its only weapon related sounds in here'; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `aowow_itemset` +-- + +DROP TABLE IF EXISTS `aowow_itemset`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `aowow_itemset` ( + `id` int(11) NOT NULL, + `refSetId` int(11) NOT NULL, + `cuFlags` int(10) unsigned NOT NULL DEFAULT 0 COMMENT 'see defines.php for flags', + `name_loc0` varchar(255) DEFAULT NULL, + `name_loc2` varchar(255) DEFAULT NULL, + `name_loc3` varchar(255) DEFAULT NULL, + `name_loc4` varchar(255) DEFAULT NULL, + `name_loc6` varchar(255) DEFAULT NULL, + `name_loc8` varchar(255) DEFAULT NULL, + `item1` mediumint(8) unsigned NOT NULL DEFAULT 0, + `item2` mediumint(8) unsigned NOT NULL DEFAULT 0, + `item3` mediumint(8) unsigned NOT NULL DEFAULT 0, + `item4` mediumint(8) unsigned NOT NULL DEFAULT 0, + `item5` mediumint(8) unsigned NOT NULL DEFAULT 0, + `item6` mediumint(8) unsigned NOT NULL DEFAULT 0, + `item7` mediumint(8) unsigned NOT NULL DEFAULT 0, + `item8` mediumint(8) unsigned NOT NULL DEFAULT 0, + `item9` mediumint(8) unsigned NOT NULL DEFAULT 0, + `item10` mediumint(8) unsigned NOT NULL DEFAULT 0, + `spell1` mediumint(8) unsigned NOT NULL DEFAULT 0, + `spell2` mediumint(8) unsigned NOT NULL DEFAULT 0, + `spell3` mediumint(8) unsigned NOT NULL DEFAULT 0, + `spell4` mediumint(8) unsigned NOT NULL DEFAULT 0, + `spell5` mediumint(8) unsigned NOT NULL DEFAULT 0, + `spell6` mediumint(8) unsigned NOT NULL DEFAULT 0, + `spell7` mediumint(8) unsigned NOT NULL DEFAULT 0, + `spell8` mediumint(8) unsigned NOT NULL DEFAULT 0, + `bonus1` tinyint(3) unsigned NOT NULL DEFAULT 0, + `bonus2` tinyint(3) unsigned NOT NULL DEFAULT 0, + `bonus3` tinyint(3) unsigned NOT NULL DEFAULT 0, + `bonus4` tinyint(3) unsigned NOT NULL DEFAULT 0, + `bonus5` tinyint(3) unsigned NOT NULL DEFAULT 0, + `bonus6` tinyint(3) unsigned NOT NULL DEFAULT 0, + `bonus7` tinyint(3) unsigned NOT NULL DEFAULT 0, + `bonus8` tinyint(3) unsigned NOT NULL DEFAULT 0, + `bonusText_loc0` text DEFAULT NULL, + `bonusText_loc2` text DEFAULT NULL, + `bonusText_loc3` text DEFAULT NULL, + `bonusText_loc4` text DEFAULT NULL, + `bonusText_loc6` text DEFAULT NULL, + `bonusText_loc8` text DEFAULT NULL, + `npieces` tinyint(4) NOT NULL DEFAULT 0, + `minLevel` smallint(6) NOT NULL DEFAULT 0, + `maxLevel` smallint(6) NOT NULL DEFAULT 0, + `reqLevel` smallint(6) NOT NULL DEFAULT 0, + `classMask` mediumint(9) NOT NULL DEFAULT 0, + `heroic` tinyint(4) NOT NULL DEFAULT 0 COMMENT 'bool', + `quality` tinyint(4) NOT NULL DEFAULT 0, + `type` smallint(6) NOT NULL DEFAULT 0 COMMENT 'g_itemset_types', + `contentGroup` smallint(6) NOT NULL DEFAULT 0 COMMENT 'g_itemset_notes', + `eventId` smallint(5) unsigned NOT NULL DEFAULT 0, + `skillId` smallint(5) unsigned NOT NULL DEFAULT 0, + `skillLevel` 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 */; + +-- +-- Table structure for table `aowow_loot_link` +-- + +DROP TABLE IF EXISTS `aowow_loot_link`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `aowow_loot_link` ( + `npcId` mediumint(8) unsigned NOT NULL, + `objectId` mediumint(8) unsigned NOT NULL, + `difficulty` tinyint(3) unsigned NOT NULL DEFAULT 1, + `priority` tinyint(3) unsigned NOT NULL COMMENT '1: use this npc from group encounter (others 0)', + `encounterId` mediumint(8) unsigned NOT NULL COMMENT 'as title reference', + UNIQUE KEY `npcId_difficulty` (`npcId`,`difficulty`), + KEY `objectId` (`objectId`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `aowow_mails` +-- + +DROP TABLE IF EXISTS `aowow_mails`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `aowow_mails` ( + `id` smallint(6) NOT NULL, + `cuFlags` int(10) unsigned NOT NULL DEFAULT 0 COMMENT 'see defines.php for flags', + `subject_loc0` varchar(128) DEFAULT NULL, + `subject_loc2` varchar(128) DEFAULT NULL, + `subject_loc3` varchar(128) DEFAULT NULL, + `subject_loc4` varchar(128) DEFAULT NULL, + `subject_loc6` varchar(128) DEFAULT NULL, + `subject_loc8` varchar(128) DEFAULT NULL, + `text_loc0` text DEFAULT NULL, + `text_loc2` text DEFAULT NULL, + `text_loc3` text DEFAULT NULL, + `text_loc4` text DEFAULT NULL, + `text_loc6` text DEFAULT NULL, + `text_loc8` text DEFAULT NULL, + `attachment` 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 */; + +-- +-- 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` +-- + +DROP TABLE IF EXISTS `aowow_objects`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `aowow_objects` ( + `id` mediumint(8) unsigned NOT NULL DEFAULT 0, + `type` tinyint(3) unsigned NOT NULL DEFAULT 0, + `typeCat` tinyint(4) NOT NULL DEFAULT 0, + `event` smallint(5) unsigned NOT NULL DEFAULT 0, + `displayId` mediumint(8) unsigned NOT NULL DEFAULT 0, + `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, + `faction` smallint(5) unsigned NOT NULL DEFAULT 0, + `flags` int(10) unsigned NOT NULL DEFAULT 0, + `cuFlags` int(10) unsigned NOT NULL DEFAULT 0 COMMENT 'see defines.php for flags', + `lootId` mediumint(8) unsigned NOT NULL DEFAULT 0, + `lockId` smallint(5) unsigned NOT NULL DEFAULT 0, + `reqSkill` smallint(5) unsigned NOT NULL DEFAULT 0, + `pageTextId` smallint(5) unsigned NOT NULL DEFAULT 0, + `linkedTrap` mediumint(8) unsigned NOT NULL DEFAULT 0, + `reqQuest` mediumint(9) NOT NULL DEFAULT 0, + `spellFocusId` smallint(5) unsigned NOT NULL DEFAULT 0, + `onUseSpell` mediumint(8) unsigned NOT NULL DEFAULT 0, + `onSuccessSpell` mediumint(8) unsigned NOT NULL DEFAULT 0, + `auraSpell` mediumint(8) unsigned NOT NULL DEFAULT 0, + `triggeredSpell` mediumint(8) unsigned NOT NULL DEFAULT 0, + `miscInfo` varchar(128) NOT NULL, + `ScriptOrAI` varchar(64) DEFAULT NULL, + `StringId` varchar(64) DEFAULT NULL, + PRIMARY KEY (`id`), + 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 */; + +-- +-- Table structure for table `aowow_pet` +-- + +DROP TABLE IF EXISTS `aowow_pet`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `aowow_pet` ( + `id` int(11) NOT NULL, + `category` mediumint(9) NOT NULL, + `cuFlags` int(10) unsigned NOT NULL DEFAULT 0 COMMENT 'see defines.php for flags', + `minLevel` smallint(6) NOT NULL, + `maxLevel` smallint(6) NOT NULL, + `foodMask` int(11) NOT NULL, + `type` tinyint(4) NOT NULL, + `exotic` tinyint(4) NOT NULL, + `expansion` tinyint(4) NOT NULL, + `name_loc0` varchar(64) DEFAULT NULL, + `name_loc2` varchar(64) DEFAULT NULL, + `name_loc3` varchar(64) DEFAULT NULL, + `name_loc4` varchar(64) DEFAULT NULL, + `name_loc6` varchar(64) DEFAULT NULL, + `name_loc8` varchar(64) DEFAULT NULL, + `iconId` smallint(5) unsigned NOT NULL DEFAULT 0, + `skillLineId` mediumint(9) NOT NULL, + `spellId1` mediumint(9) NOT NULL, + `spellId2` mediumint(9) NOT NULL, + `spellId3` mediumint(9) NOT NULL, + `spellId4` mediumint(9) NOT NULL, + `armor` mediumint(9) NOT NULL, + `damage` mediumint(9) NOT NULL, + `health` mediumint(9) NOT NULL, + PRIMARY KEY (`id`), + KEY `iconId` (`iconId`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `aowow_profiler_arena_team` +-- + +DROP TABLE IF EXISTS `aowow_profiler_arena_team`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `aowow_profiler_arena_team` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `realm` tinyint(3) unsigned NOT NULL, + `realmGUID` int(10) unsigned NOT NULL, + `name` varchar(24) NOT NULL, + `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, + `weekGames` smallint(5) unsigned NOT NULL DEFAULT 0, + `weekWins` smallint(5) unsigned NOT NULL DEFAULT 0, + `rank` int(10) unsigned NOT NULL DEFAULT 0, + `backgroundColor` int(10) unsigned NOT NULL DEFAULT 0, + `emblemStyle` tinyint(3) unsigned NOT NULL DEFAULT 0, + `emblemColor` int(10) unsigned NOT NULL DEFAULT 0, + `borderStyle` tinyint(3) unsigned NOT NULL DEFAULT 0, + `borderColor` int(10) unsigned NOT NULL DEFAULT 0, + PRIMARY KEY (`id`), + UNIQUE KEY `realm_realmGUID` (`realm`,`realmGUID`), + 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 */; + +-- +-- Table structure for table `aowow_profiler_arena_team_member` +-- + +DROP TABLE IF EXISTS `aowow_profiler_arena_team_member`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `aowow_profiler_arena_team_member` ( + `arenaTeamId` int(10) unsigned NOT NULL DEFAULT 0, + `profileId` int(10) unsigned NOT NULL DEFAULT 0, + `captain` tinyint(3) unsigned NOT NULL DEFAULT 0, + `weekGames` smallint(5) unsigned NOT NULL DEFAULT 0, + `weekWins` smallint(5) unsigned NOT NULL DEFAULT 0, + `seasonGames` smallint(5) unsigned NOT NULL DEFAULT 0, + `seasonWins` smallint(5) unsigned NOT NULL DEFAULT 0, + `personalRating` smallint(5) unsigned NOT NULL DEFAULT 0, + PRIMARY KEY (`arenaTeamId`,`profileId`), + KEY `guid` (`profileId`), + CONSTRAINT `FK_aowow_profiler_arena_team_member_aowow_profiler_arena_team` FOREIGN KEY (`arenaTeamId`) REFERENCES `aowow_profiler_arena_team` (`id`) ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT `FK_aowow_profiler_arena_team_member_aowow_profiler_profiles` FOREIGN KEY (`profileId`) 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 */; + +-- +-- Table structure for table `aowow_profiler_completion_achievements` +-- + +DROP TABLE IF EXISTS `aowow_profiler_completion_achievements`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `aowow_profiler_completion_achievements` ( + `id` int(10) unsigned NOT NULL, + `achievementId` smallint(5) unsigned NOT NULL, + `date` int(10) unsigned DEFAULT NULL, + KEY `id` (`id`), + KEY `typeId` (`achievementId`), + CONSTRAINT `FK_pr_completion_achievements` 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 */; + +-- +-- Table structure for table `aowow_profiler_completion_quests` +-- + +DROP TABLE IF EXISTS `aowow_profiler_completion_quests`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +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 */; + +-- +-- Table structure for table `aowow_profiler_completion_reputation` +-- + +DROP TABLE IF EXISTS `aowow_profiler_completion_reputation`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +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 */; + +-- +-- Table structure for table `aowow_profiler_completion_skills` +-- + +DROP TABLE IF EXISTS `aowow_profiler_completion_skills`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `aowow_profiler_completion_skills` ( + `id` int(10) unsigned NOT NULL, + `skillId` smallint(5) unsigned NOT NULL, + `value` smallint(5) unsigned DEFAULT NULL, + `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 */; + +-- +-- Table structure for table `aowow_profiler_completion_spells` +-- + +DROP TABLE IF EXISTS `aowow_profiler_completion_spells`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +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 */; + +-- +-- Table structure for table `aowow_profiler_completion_statistics` +-- + +DROP TABLE IF EXISTS `aowow_profiler_completion_statistics`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `aowow_profiler_completion_statistics` ( + `id` int(10) unsigned NOT NULL, + `achievementId` smallint(6) NOT NULL, + `date` int(10) unsigned DEFAULT NULL, + `counter` smallint(5) unsigned DEFAULT NULL, + KEY `id` (`id`), + KEY `typeId` (`achievementId`), + CONSTRAINT `FK_pr_completion_statistics` 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 */; + +-- +-- Table structure for table `aowow_profiler_completion_titles` +-- + +DROP TABLE IF EXISTS `aowow_profiler_completion_titles`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +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 */; + +-- +-- Table structure for table `aowow_profiler_excludes` +-- + +DROP TABLE IF EXISTS `aowow_profiler_excludes`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `aowow_profiler_excludes` ( + `type` smallint(5) unsigned NOT NULL, + `typeId` mediumint(8) unsigned NOT NULL, + `groups` smallint(5) unsigned NOT NULL COMMENT 'see exclude group defines', + `comment` varchar(50) NOT NULL COMMENT 'rebuilding profiler files will delete everything without a comment', + PRIMARY KEY (`type`,`typeId`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `aowow_profiler_guild` +-- + +DROP TABLE IF EXISTS `aowow_profiler_guild`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `aowow_profiler_guild` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `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, + `emblemColor` tinyint(3) unsigned NOT NULL DEFAULT 0, + `borderStyle` tinyint(3) unsigned NOT NULL DEFAULT 0, + `borderColor` tinyint(3) unsigned NOT NULL DEFAULT 0, + `backgroundColor` tinyint(3) unsigned NOT NULL DEFAULT 0, + `info` varchar(500) NOT NULL DEFAULT '', + `createDate` int(10) unsigned NOT NULL DEFAULT 0, + PRIMARY KEY (`id`), + UNIQUE KEY `realm_realmGUID` (`realm`,`realmGUID`), + KEY `name` (`name`), + KEY `idx_stub` (`stub`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `aowow_profiler_guild_rank` +-- + +DROP TABLE IF EXISTS `aowow_profiler_guild_rank`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `aowow_profiler_guild_rank` ( + `guildId` int(10) unsigned NOT NULL DEFAULT 0, + `rank` tinyint(3) unsigned NOT NULL, + `name` varchar(20) NOT NULL DEFAULT '', + PRIMARY KEY (`guildId`,`rank`), + KEY `rank` (`rank`), + CONSTRAINT `FK_aowow_profiler_guild_rank_aowow_profiler_guild` FOREIGN KEY (`guildId`) REFERENCES `aowow_profiler_guild` (`id`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `aowow_profiler_items` +-- + +DROP TABLE IF EXISTS `aowow_profiler_items`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `aowow_profiler_items` ( + `id` int(10) unsigned DEFAULT NULL, + `slot` tinyint(3) unsigned DEFAULT NULL, + `item` mediumint(8) unsigned DEFAULT NULL, + `subItem` smallint(6) DEFAULT NULL, + `permEnchant` mediumint(8) unsigned DEFAULT NULL, + `tempEnchant` mediumint(8) unsigned DEFAULT NULL, + `extraSocket` tinyint(3) unsigned DEFAULT NULL COMMENT 'not used .. the appropriate gem slot is set to -1 instead', + `gem1` mediumint(9) DEFAULT NULL, + `gem2` mediumint(9) DEFAULT NULL, + `gem3` mediumint(9) DEFAULT NULL, + `gem4` mediumint(9) DEFAULT NULL, + UNIQUE KEY `id_slot` (`id`,`slot`), + KEY `id` (`id`), + KEY `item` (`item`), + CONSTRAINT `FK_pr_items` 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 */; + +-- +-- Table structure for table `aowow_profiler_pets` +-- + +DROP TABLE IF EXISTS `aowow_profiler_pets`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `aowow_profiler_pets` ( + `id` mediumint(8) unsigned NOT NULL AUTO_INCREMENT, + `owner` int(10) unsigned DEFAULT NULL, + `name` varchar(50) DEFAULT NULL, + `family` tinyint(3) unsigned DEFAULT NULL, + `npc` smallint(5) unsigned DEFAULT NULL, + `displayId` smallint(5) unsigned 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 +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `aowow_profiler_profiles` +-- + +DROP TABLE IF EXISTS `aowow_profiler_profiles`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `aowow_profiler_profiles` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `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, + `icon` varchar(50) DEFAULT NULL, + `user` int(10) unsigned DEFAULT NULL, + `name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL, + `renameItr` tinyint(3) unsigned DEFAULT NULL, + `race` tinyint(3) unsigned NOT NULL, + `class` tinyint(3) unsigned NOT NULL, + `level` tinyint(3) unsigned NOT NULL, + `gender` tinyint(3) unsigned NOT NULL, + `guild` int(10) unsigned DEFAULT NULL, + `guildrank` tinyint(3) unsigned DEFAULT NULL COMMENT '0: guild master', + `skincolor` tinyint(3) unsigned NOT NULL DEFAULT 0, + `hairstyle` tinyint(3) unsigned NOT NULL DEFAULT 0, + `haircolor` tinyint(3) unsigned NOT NULL DEFAULT 0, + `facetype` tinyint(3) unsigned NOT NULL DEFAULT 0, + `features` tinyint(3) unsigned NOT NULL DEFAULT 0, + `nomodelMask` int(10) unsigned NOT NULL DEFAULT 0, + `title` tinyint(3) unsigned NOT NULL DEFAULT 0, + `description` text DEFAULT NULL, + `playedtime` int(10) unsigned NOT NULL DEFAULT 0, + `gearscore` smallint(5) unsigned NOT NULL DEFAULT 0, + `achievementpoints` smallint(5) unsigned NOT NULL DEFAULT 0, + `lastupdated` int(11) NOT NULL DEFAULT 0, + `talenttree1` tinyint(3) unsigned NOT NULL DEFAULT 0 COMMENT 'points spend in 1st tree', + `talenttree2` tinyint(3) unsigned NOT NULL DEFAULT 0 COMMENT 'points spend in 2nd tree', + `talenttree3` tinyint(3) unsigned NOT NULL DEFAULT 0 COMMENT 'points spend in 3rd tree', + `talentbuild1` varchar(105) NOT NULL DEFAULT '', + `talentbuild2` varchar(105) NOT NULL DEFAULT '', + `glyphs1` varchar(45) NOT NULL DEFAULT '', + `glyphs2` varchar(45) NOT NULL DEFAULT '', + `activespec` tinyint(3) unsigned NOT NULL DEFAULT 0, + PRIMARY KEY (`id`), + UNIQUE KEY `realm_realmGUID` (`realm`,`realmGUID`), + 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 */; + +-- +-- Table structure for table `aowow_profiler_sync` +-- + +DROP TABLE IF EXISTS `aowow_profiler_sync`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `aowow_profiler_sync` ( + `realm` tinyint(3) unsigned NOT NULL, + `realmGUID` int(10) unsigned NOT NULL, + `type` smallint(5) unsigned NOT NULL, + `typeId` int(10) unsigned NOT NULL, + `requestTime` int(10) unsigned NOT NULL, + `status` tinyint(3) unsigned NOT NULL, + `errorCode` tinyint(3) unsigned NOT NULL DEFAULT 0, + UNIQUE KEY `realm_realmGUID_type_typeId` (`realm`,`realmGUID`,`type`), + UNIQUE KEY `type_typeId` (`type`,`typeId`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `aowow_quests` +-- + +DROP TABLE IF EXISTS `aowow_quests`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `aowow_quests` ( + `id` mediumint(8) unsigned NOT NULL DEFAULT 0, + `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, + `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, + `prevQuestId` mediumint(9) NOT NULL DEFAULT 0, + `nextQuestId` mediumint(9) NOT NULL DEFAULT 0, + `breadcrumbForQuestId` mediumint(9) NOT NULL DEFAULT 0, + `exclusiveGroup` mediumint(9) NOT NULL DEFAULT 0, + `nextQuestIdChain` mediumint(8) unsigned NOT NULL DEFAULT 0, + `flags` int(10) unsigned NOT NULL DEFAULT 0, + `specialFlags` tinyint(3) unsigned NOT NULL DEFAULT 0, + `cuFlags` int(10) unsigned NOT NULL DEFAULT 0 COMMENT 'see defines.php for flags', + `reqClassMask` smallint(5) unsigned NOT NULL DEFAULT 0, + `reqRaceMask` smallint(5) unsigned NOT NULL DEFAULT 0, + `reqSkillId` smallint(5) unsigned NOT NULL DEFAULT 0, + `reqSkillPoints` smallint(5) unsigned NOT NULL DEFAULT 0, + `reqFactionId1` smallint(5) unsigned NOT NULL DEFAULT 0, + `reqFactionId2` smallint(5) unsigned NOT NULL DEFAULT 0, + `reqFactionValue1` mediumint(9) NOT NULL DEFAULT 0, + `reqFactionValue2` mediumint(9) NOT NULL DEFAULT 0, + `reqMinRepFaction` smallint(5) unsigned NOT NULL DEFAULT 0, + `reqMaxRepFaction` smallint(5) unsigned NOT NULL DEFAULT 0, + `reqMinRepValue` mediumint(9) NOT NULL DEFAULT 0, + `reqMaxRepValue` mediumint(9) NOT NULL DEFAULT 0, + `reqPlayerKills` tinyint(3) unsigned NOT NULL DEFAULT 0, + `sourceItemId` mediumint(8) unsigned NOT NULL DEFAULT 0, + `sourceItemCount` tinyint(3) unsigned NOT NULL DEFAULT 0, + `sourceSpellId` mediumint(8) unsigned NOT NULL DEFAULT 0, + `rewardXP` mediumint(8) unsigned NOT NULL DEFAULT 0, + `rewardOrReqMoney` int(11) NOT NULL DEFAULT 0, + `rewardMoneyMaxLevel` int(10) unsigned NOT NULL DEFAULT 0, + `rewardSpell` mediumint(8) unsigned NOT NULL DEFAULT 0, + `rewardSpellCast` int(11) NOT NULL DEFAULT 0, + `rewardHonorPoints` int(11) NOT NULL DEFAULT 0, + `rewardMailTemplateId` mediumint(8) unsigned NOT NULL DEFAULT 0, + `rewardMailDelay` int(10) unsigned NOT NULL DEFAULT 0, + `rewardTitleId` tinyint(3) unsigned NOT NULL DEFAULT 0, + `rewardTalents` tinyint(3) unsigned NOT NULL DEFAULT 0, + `rewardArenaPoints` smallint(6) NOT NULL DEFAULT 0, + `rewardItemId1` mediumint(8) unsigned NOT NULL DEFAULT 0, + `rewardItemId2` mediumint(8) unsigned NOT NULL DEFAULT 0, + `rewardItemId3` mediumint(8) unsigned NOT NULL DEFAULT 0, + `rewardItemId4` mediumint(8) unsigned NOT NULL DEFAULT 0, + `rewardItemCount1` smallint(5) unsigned NOT NULL DEFAULT 0, + `rewardItemCount2` smallint(5) unsigned NOT NULL DEFAULT 0, + `rewardItemCount3` smallint(5) unsigned NOT NULL DEFAULT 0, + `rewardItemCount4` smallint(5) unsigned NOT NULL DEFAULT 0, + `rewardChoiceItemId1` mediumint(8) unsigned NOT NULL DEFAULT 0, + `rewardChoiceItemId2` mediumint(8) unsigned NOT NULL DEFAULT 0, + `rewardChoiceItemId3` mediumint(8) unsigned NOT NULL DEFAULT 0, + `rewardChoiceItemId4` mediumint(8) unsigned NOT NULL DEFAULT 0, + `rewardChoiceItemId5` mediumint(8) unsigned NOT NULL DEFAULT 0, + `rewardChoiceItemId6` mediumint(8) unsigned NOT NULL DEFAULT 0, + `rewardChoiceItemCount1` smallint(5) unsigned NOT NULL DEFAULT 0, + `rewardChoiceItemCount2` smallint(5) unsigned NOT NULL DEFAULT 0, + `rewardChoiceItemCount3` smallint(5) unsigned NOT NULL DEFAULT 0, + `rewardChoiceItemCount4` smallint(5) unsigned NOT NULL DEFAULT 0, + `rewardChoiceItemCount5` smallint(5) unsigned NOT NULL DEFAULT 0, + `rewardChoiceItemCount6` smallint(5) unsigned NOT NULL DEFAULT 0, + `rewardFactionId1` smallint(5) unsigned NOT NULL DEFAULT 0 COMMENT 'faction id from Faction.dbc in this case', + `rewardFactionId2` smallint(5) unsigned NOT NULL DEFAULT 0 COMMENT 'faction id from Faction.dbc in this case', + `rewardFactionId3` smallint(5) unsigned NOT NULL DEFAULT 0 COMMENT 'faction id from Faction.dbc in this case', + `rewardFactionId4` smallint(5) unsigned NOT NULL DEFAULT 0 COMMENT 'faction id from Faction.dbc in this case', + `rewardFactionId5` smallint(5) unsigned NOT NULL DEFAULT 0 COMMENT 'faction id from Faction.dbc in this case', + `rewardFactionValue1` mediumint(9) NOT NULL DEFAULT 0, + `rewardFactionValue2` mediumint(9) NOT NULL DEFAULT 0, + `rewardFactionValue3` mediumint(9) NOT NULL DEFAULT 0, + `rewardFactionValue4` mediumint(9) NOT NULL DEFAULT 0, + `rewardFactionValue5` mediumint(9) NOT NULL DEFAULT 0, + `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, + `objectives_loc4` text DEFAULT NULL, + `objectives_loc6` text DEFAULT NULL, + `objectives_loc8` text DEFAULT NULL, + `details_loc0` text DEFAULT NULL, + `details_loc2` text DEFAULT NULL, + `details_loc3` text DEFAULT NULL, + `details_loc4` text DEFAULT NULL, + `details_loc6` text DEFAULT NULL, + `details_loc8` text DEFAULT NULL, + `end_loc0` text DEFAULT NULL, + `end_loc2` text DEFAULT NULL, + `end_loc3` text DEFAULT NULL, + `end_loc4` text DEFAULT NULL, + `end_loc6` text DEFAULT NULL, + `end_loc8` text DEFAULT NULL, + `offerReward_loc0` text DEFAULT NULL, + `offerReward_loc2` text DEFAULT NULL, + `offerReward_loc3` text DEFAULT NULL, + `offerReward_loc4` text DEFAULT NULL, + `offerReward_loc6` text DEFAULT NULL, + `offerReward_loc8` text DEFAULT NULL, + `requestItems_loc0` text DEFAULT NULL, + `requestItems_loc2` text DEFAULT NULL, + `requestItems_loc3` text DEFAULT NULL, + `requestItems_loc4` text DEFAULT NULL, + `requestItems_loc6` text DEFAULT NULL, + `requestItems_loc8` text DEFAULT NULL, + `completed_loc0` text DEFAULT NULL, + `completed_loc2` text DEFAULT NULL, + `completed_loc3` text DEFAULT NULL, + `completed_loc4` text DEFAULT NULL, + `completed_loc6` text DEFAULT NULL, + `completed_loc8` text DEFAULT NULL, + `reqNpcOrGo1` mediumint(9) NOT NULL DEFAULT 0, + `reqNpcOrGo2` mediumint(9) NOT NULL DEFAULT 0, + `reqNpcOrGo3` mediumint(9) NOT NULL DEFAULT 0, + `reqNpcOrGo4` mediumint(9) NOT NULL DEFAULT 0, + `reqNpcOrGoCount1` smallint(5) unsigned NOT NULL DEFAULT 0, + `reqNpcOrGoCount2` smallint(5) unsigned NOT NULL DEFAULT 0, + `reqNpcOrGoCount3` smallint(5) unsigned NOT NULL DEFAULT 0, + `reqNpcOrGoCount4` smallint(5) unsigned NOT NULL DEFAULT 0, + `reqSourceItemId1` mediumint(8) unsigned NOT NULL DEFAULT 0, + `reqSourceItemId2` mediumint(8) unsigned NOT NULL DEFAULT 0, + `reqSourceItemId3` mediumint(8) unsigned NOT NULL DEFAULT 0, + `reqSourceItemId4` mediumint(8) unsigned NOT NULL DEFAULT 0, + `reqSourceItemCount1` smallint(5) unsigned NOT NULL DEFAULT 0, + `reqSourceItemCount2` smallint(5) unsigned NOT NULL DEFAULT 0, + `reqSourceItemCount3` smallint(5) unsigned NOT NULL DEFAULT 0, + `reqSourceItemCount4` smallint(5) unsigned NOT NULL DEFAULT 0, + `reqItemId1` mediumint(8) unsigned NOT NULL DEFAULT 0, + `reqItemId2` mediumint(8) unsigned NOT NULL DEFAULT 0, + `reqItemId3` mediumint(8) unsigned NOT NULL DEFAULT 0, + `reqItemId4` mediumint(8) unsigned NOT NULL DEFAULT 0, + `reqItemId5` mediumint(8) unsigned NOT NULL DEFAULT 0, + `reqItemId6` mediumint(8) unsigned NOT NULL DEFAULT 0, + `reqItemCount1` smallint(5) unsigned NOT NULL DEFAULT 0, + `reqItemCount2` smallint(5) unsigned NOT NULL DEFAULT 0, + `reqItemCount3` smallint(5) unsigned NOT NULL DEFAULT 0, + `reqItemCount4` smallint(5) unsigned NOT NULL DEFAULT 0, + `reqItemCount5` smallint(5) unsigned NOT NULL DEFAULT 0, + `reqItemCount6` smallint(5) unsigned NOT NULL DEFAULT 0, + `objectiveText1_loc0` text DEFAULT NULL, + `objectiveText1_loc2` text DEFAULT NULL, + `objectiveText1_loc3` text DEFAULT NULL, + `objectiveText1_loc4` text DEFAULT NULL, + `objectiveText1_loc6` text DEFAULT NULL, + `objectiveText1_loc8` text DEFAULT NULL, + `objectiveText2_loc0` text DEFAULT NULL, + `objectiveText2_loc2` text DEFAULT NULL, + `objectiveText2_loc3` text DEFAULT NULL, + `objectiveText2_loc4` text DEFAULT NULL, + `objectiveText2_loc6` text DEFAULT NULL, + `objectiveText2_loc8` text DEFAULT NULL, + `objectiveText3_loc0` text DEFAULT NULL, + `objectiveText3_loc2` text DEFAULT NULL, + `objectiveText3_loc3` text DEFAULT NULL, + `objectiveText3_loc4` text DEFAULT NULL, + `objectiveText3_loc6` text DEFAULT NULL, + `objectiveText3_loc8` text DEFAULT NULL, + `objectiveText4_loc0` text DEFAULT NULL, + `objectiveText4_loc2` text DEFAULT NULL, + `objectiveText4_loc3` text DEFAULT NULL, + `objectiveText4_loc4` text DEFAULT NULL, + `objectiveText4_loc6` text DEFAULT NULL, + `objectiveText4_loc8` text DEFAULT NULL, + PRIMARY KEY (`id`), + 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 */; + +-- +-- Table structure for table `aowow_quests_startend` +-- + +DROP TABLE IF EXISTS `aowow_quests_startend`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `aowow_quests_startend` ( + `type` tinyint(3) unsigned NOT NULL, + `typeId` mediumint(8) unsigned NOT NULL, + `questId` mediumint(8) unsigned NOT NULL, + `method` tinyint(3) unsigned NOT NULL COMMENT '&0x1: starts; &0x2:ends', + `eventId` smallint(5) unsigned NOT NULL DEFAULT 0, + PRIMARY KEY (`type`,`typeId`,`questId`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `aowow_quickfacts` +-- + +DROP TABLE IF EXISTS `aowow_quickfacts`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `aowow_quickfacts` ( + `type` smallint(5) unsigned NOT NULL, + `typeId` mediumint(9) NOT NULL, + `orderIdx` tinyint(4) NOT NULL COMMENT '<0: prepend to generic list; >0: append to generic list', + `row` varchar(200) NOT NULL COMMENT 'Markdown formated', + UNIQUE KEY `row` (`type`,`typeId`,`orderIdx`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=COMPACT; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `aowow_races` +-- + +DROP TABLE IF EXISTS `aowow_races`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `aowow_races` ( + `id` int(10) unsigned NOT NULL, + `classMask` smallint(5) unsigned NOT NULL, + `flags` tinyint(3) unsigned NOT NULL, + `cuFlags` int(10) unsigned NOT NULL DEFAULT 0 COMMENT 'see defines.php for flags', + `factionId` smallint(6) NOT NULL, + `startAreaId` smallint(6) NOT NULL, + `leader` mediumint(8) unsigned NOT NULL, + `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, + `name_loc4` varchar(64) DEFAULT NULL, + `name_loc6` varchar(64) DEFAULT NULL, + `name_loc8` varchar(64) DEFAULT NULL, + `expansion` int(11) NOT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `aowow_races_sounds` +-- + +DROP TABLE IF EXISTS `aowow_races_sounds`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `aowow_races_sounds` ( + `raceId` tinyint(3) unsigned NOT NULL, + `soundId` smallint(5) unsigned NOT NULL, + `gender` tinyint(3) unsigned NOT NULL, + UNIQUE KEY `race_soundId_gender` (`raceId`,`soundId`,`gender`), + KEY `race` (`raceId`), + KEY `soundId` (`soundId`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `aowow_reports` +-- + +DROP TABLE IF EXISTS `aowow_reports`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `aowow_reports` ( + `id` mediumint(8) unsigned NOT NULL AUTO_INCREMENT, + `userId` mediumint(8) unsigned NOT NULL, + `assigned` mediumint(8) unsigned NOT NULL DEFAULT 0, + `status` tinyint(3) unsigned NOT NULL DEFAULT 0 COMMENT '0:new; 1:solved; 2:rejected', + `createDate` int(10) unsigned NOT NULL, + `mode` tinyint(3) unsigned NOT NULL, + `reason` tinyint(3) unsigned NOT NULL, + `subject` mediumint(9) NOT NULL DEFAULT 0, + `ip` varchar(50) NOT NULL, + `description` text NOT NULL, + `userAgent` varchar(255) NOT NULL, + `appName` varchar(32) NOT NULL, + `url` varchar(255) NOT NULL, + `relatedUrl` varchar(255) DEFAULT NULL, + `email` varchar(255) DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `userId` (`userId`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `aowow_screeneffect_sounds` +-- + +DROP TABLE IF EXISTS `aowow_screeneffect_sounds`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `aowow_screeneffect_sounds` ( + `id` smallint(5) unsigned NOT NULL, + `name` varchar(40) NOT NULL, + `ambienceDay` smallint(5) unsigned NOT NULL, + `ambienceNight` smallint(5) unsigned NOT NULL, + `musicDay` smallint(5) unsigned NOT NULL, + `musicNight` smallint(5) unsigned NOT NULL, + KEY `id` (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `aowow_screenshots` +-- + +DROP TABLE IF EXISTS `aowow_screenshots`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `aowow_screenshots` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `type` smallint(5) unsigned NOT NULL, + `typeId` mediumint(9) NOT NULL, + `userIdOwner` int(10) unsigned DEFAULT NULL, + `date` int(10) unsigned NOT NULL, + `width` smallint(5) unsigned NOT NULL, + `height` smallint(5) unsigned NOT NULL, + `caption` varchar(200) DEFAULT NULL, + `status` tinyint(3) unsigned NOT NULL COMMENT 'see defines.php - CC_FLAG_*', + `userIdApprove` int(10) unsigned DEFAULT NULL, + `userIdDelete` int(10) unsigned DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `type` (`type`,`typeId`), + KEY `FK_acc_ss` (`userIdOwner`), + CONSTRAINT `FK_acc_ss` FOREIGN KEY (`userIdOwner`) REFERENCES `aowow_account` (`id`) ON DELETE SET NULL ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=COMPACT; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `aowow_setup_custom_data` +-- + +DROP TABLE IF EXISTS `aowow_setup_custom_data`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `aowow_setup_custom_data` ( + `command` varchar(100) NOT NULL DEFAULT '', + `entry` int(11) NOT NULL DEFAULT 0 COMMENT 'typeId', + `field` varchar(100) NOT NULL DEFAULT '', + `value` text DEFAULT NULL, + `comment` text DEFAULT NULL, + KEY `aowow_setup_custom_data_command_IDX` (`command`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `aowow_shapeshiftforms` +-- + +DROP TABLE IF EXISTS `aowow_shapeshiftforms`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `aowow_shapeshiftforms` ( + `Id` tinyint(3) unsigned NOT NULL, + `flags` smallint(5) unsigned NOT NULL, + `creatureType` tinyint(4) NOT NULL, + `displayIdA` smallint(5) unsigned NOT NULL, + `displayIdH` smallint(5) unsigned NOT NULL, + `spellId1` mediumint(8) unsigned NOT NULL, + `spellId2` mediumint(8) unsigned NOT NULL, + `spellId3` mediumint(8) unsigned NOT NULL, + `spellId4` mediumint(8) unsigned NOT NULL, + `spellId5` mediumint(8) unsigned NOT NULL, + `spellId6` mediumint(8) unsigned NOT NULL, + `spellId7` mediumint(8) unsigned NOT NULL, + `spellId8` mediumint(8) unsigned NOT NULL, + `comment` varchar(30) DEFAULT NULL, + PRIMARY KEY (`Id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `aowow_skillline` +-- + +DROP TABLE IF EXISTS `aowow_skillline`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `aowow_skillline` ( + `Id` smallint(5) unsigned NOT NULL, + `typeCat` tinyint(4) NOT NULL, + `cuFlags` int(10) unsigned NOT NULL DEFAULT 0 COMMENT 'see defines.php for flags', + `categoryId` tinyint(4) NOT NULL, + `name_loc0` varchar(64) DEFAULT NULL, + `name_loc2` varchar(64) DEFAULT NULL, + `name_loc3` varchar(64) DEFAULT NULL, + `name_loc4` varchar(64) DEFAULT NULL, + `name_loc6` varchar(64) DEFAULT NULL, + `name_loc8` varchar(64) DEFAULT NULL, + `description_loc0` text DEFAULT NULL, + `description_loc2` text DEFAULT NULL, + `description_loc3` text DEFAULT NULL, + `description_loc4` text DEFAULT NULL, + `description_loc6` text DEFAULT NULL, + `description_loc8` text DEFAULT NULL, + `iconId` smallint(5) unsigned NOT NULL DEFAULT 0, + `iconIdBak` smallint(5) unsigned NOT NULL DEFAULT 0, + `professionMask` smallint(5) unsigned NOT NULL, + `recipeSubClass` tinyint(3) unsigned NOT NULL, + `specializations` varchar(30) NOT NULL COMMENT 'space-separated spellIds', + PRIMARY KEY (`Id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `aowow_sounds` +-- + +DROP TABLE IF EXISTS `aowow_sounds`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `aowow_sounds` ( + `id` smallint(5) unsigned NOT NULL, + `cat` tinyint(3) unsigned NOT NULL, + `name` varchar(100) NOT NULL, + `cuFlags` int(10) unsigned NOT NULL DEFAULT 0 COMMENT 'see defines.php for flags', + `soundFile1` smallint(5) unsigned DEFAULT NULL, + `soundFile2` smallint(5) unsigned DEFAULT NULL, + `soundFile3` smallint(5) unsigned DEFAULT NULL, + `soundFile4` smallint(5) unsigned DEFAULT NULL, + `soundFile5` smallint(5) unsigned DEFAULT NULL, + `soundFile6` smallint(5) unsigned DEFAULT NULL, + `soundFile7` smallint(5) unsigned DEFAULT NULL, + `soundFile8` smallint(5) unsigned DEFAULT NULL, + `soundFile9` smallint(5) unsigned DEFAULT NULL, + `soundFile10` smallint(5) unsigned DEFAULT NULL, + `flags` mediumint(8) unsigned NOT NULL, + PRIMARY KEY (`id`), + KEY `cat` (`cat`), + KEY `name` (`name`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `aowow_sounds_files` +-- + +DROP TABLE IF EXISTS `aowow_sounds_files`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `aowow_sounds_files` ( + `id` smallint(6) NOT NULL COMMENT '<0 not found in client files', + `file` varchar(75) NOT NULL, + `path` varchar(75) NOT NULL COMMENT 'in client', + `type` enum('OGG','MP3') NOT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `aowow_source` +-- + +DROP TABLE IF EXISTS `aowow_source`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `aowow_source` ( + `type` tinyint(3) unsigned NOT NULL, + `typeId` mediumint(9) NOT NULL, + `moreType` tinyint(3) unsigned DEFAULT NULL, + `moreTypeId` mediumint(8) unsigned DEFAULT NULL, + `moreZoneId` mediumint(8) unsigned DEFAULT NULL, + `moreMask` mediumint(8) unsigned DEFAULT NULL, + `src1` tinyint(3) unsigned DEFAULT NULL COMMENT 'Crafted', + `src2` tinyint(3) unsigned DEFAULT NULL COMMENT 'Drop (npc / object / item) (modeMask)', + `src3` tinyint(3) unsigned DEFAULT NULL COMMENT 'PvP (g_sources_pvp)', + `src4` tinyint(3) unsigned DEFAULT NULL COMMENT 'Quest (side)', + `src5` tinyint(3) unsigned DEFAULT NULL COMMENT 'Vendor', + `src6` tinyint(3) unsigned DEFAULT NULL COMMENT 'Trainer', + `src7` tinyint(3) unsigned DEFAULT NULL COMMENT 'Discovery', + `src8` tinyint(3) unsigned DEFAULT NULL COMMENT 'Redemption', + `src9` tinyint(3) unsigned DEFAULT NULL COMMENT 'Talent', + `src10` tinyint(3) unsigned DEFAULT NULL COMMENT 'Starter', + `src11` tinyint(3) unsigned DEFAULT NULL COMMENT 'Event (special; not holidays) [not used]', + `src12` tinyint(3) unsigned DEFAULT NULL COMMENT 'Achievemement', + `src13` tinyint(3) unsigned DEFAULT NULL COMMENT 'Misc Source (sourceStringId)', + `src14` tinyint(3) unsigned DEFAULT NULL COMMENT 'Black Market [not used]', + `src15` tinyint(3) unsigned DEFAULT NULL COMMENT 'Disenchanted', + `src16` tinyint(3) unsigned DEFAULT NULL COMMENT 'Fished', + `src17` tinyint(3) unsigned DEFAULT NULL COMMENT 'Gathered', + `src18` tinyint(3) unsigned DEFAULT NULL COMMENT 'Milled', + `src19` tinyint(3) unsigned DEFAULT NULL COMMENT 'Mined', + `src20` tinyint(3) unsigned DEFAULT NULL COMMENT 'Prospected', + `src21` tinyint(3) unsigned DEFAULT NULL COMMENT 'Pickpocketed', + `src22` tinyint(3) unsigned DEFAULT NULL COMMENT 'Salvaged', + `src23` tinyint(3) unsigned DEFAULT NULL COMMENT 'Skinned', + `src24` tinyint(3) unsigned DEFAULT NULL COMMENT 'In-Game Store [not used]', + PRIMARY KEY (`type`,`typeId`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `aowow_spawns` +-- + +DROP TABLE IF EXISTS `aowow_spawns`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `aowow_spawns` ( + `guid` int(11) NOT NULL COMMENT '< 0: vehicle accessory', + `type` smallint(5) unsigned NOT NULL, + `typeId` int(10) unsigned NOT NULL, + `respawn` int(11) NOT NULL DEFAULT 0 COMMENT 'in seconds', + `spawnMask` tinyint(3) unsigned NOT NULL DEFAULT 0, + `phaseMask` smallint(5) unsigned NOT NULL DEFAULT 0, + `areaId` smallint(5) unsigned NOT NULL DEFAULT 0, + `floor` tinyint(3) unsigned NOT NULL DEFAULT 0, + `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`), + KEY `guid` (`guid`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `aowow_spawns_override` +-- + +DROP TABLE IF EXISTS `aowow_spawns_override`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `aowow_spawns_override` ( + `type` smallint(5) unsigned NOT NULL, + `typeGuid` mediumint(9) NOT NULL, + `areaId` mediumint(8) unsigned NOT NULL, + `floor` mediumint(8) unsigned NOT NULL, + `revision` tinyint(3) unsigned NOT NULL COMMENT 'Aowow revision, when this override was applied', + PRIMARY KEY (`type`,`typeGuid`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `aowow_spell` +-- + +DROP TABLE IF EXISTS `aowow_spell`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `aowow_spell` ( + `id` mediumint(8) unsigned NOT NULL, + `category` smallint(5) unsigned NOT NULL, + `dispelType` tinyint(3) unsigned NOT NULL, + `mechanic` tinyint(3) unsigned NOT NULL, + `attributes0` int(10) unsigned NOT NULL, + `attributes1` int(10) unsigned NOT NULL, + `attributes2` int(10) unsigned NOT NULL, + `attributes3` int(10) unsigned NOT NULL, + `attributes4` int(10) unsigned NOT NULL, + `attributes5` int(10) unsigned NOT NULL, + `attributes6` int(10) unsigned NOT NULL, + `attributes7` int(10) unsigned NOT NULL, + `cuFlags` int(10) unsigned NOT NULL DEFAULT 0 COMMENT 'see defines.php for flags', + `typeCat` smallint(6) NOT NULL, + `stanceMask` int(11) NOT NULL, + `stanceMaskNot` int(11) NOT NULL, + `targets` mediumint(8) unsigned NOT NULL, + `spellFocusObject` smallint(5) unsigned NOT NULL, + `castTime` float unsigned NOT NULL, + `recoveryTime` int(10) unsigned NOT NULL, + `recoveryCategory` int(10) unsigned NOT NULL, + `startRecoveryTime` mediumint(8) unsigned NOT NULL, + `startRecoveryCategory` smallint(5) unsigned NOT NULL, + `procChance` tinyint(3) unsigned NOT NULL, + `procCharges` mediumint(8) unsigned NOT NULL, + `procCustom` float NOT NULL, + `procCooldown` smallint(5) unsigned NOT NULL, + `maxLevel` smallint(5) unsigned NOT NULL, + `baseLevel` smallint(5) unsigned NOT NULL, + `spellLevel` smallint(5) unsigned NOT NULL, + `talentLevel` tinyint(3) unsigned NOT NULL, + `duration` int(11) NOT NULL DEFAULT 0, + `powerType` smallint(6) NOT NULL, + `powerCost` smallint(5) unsigned NOT NULL, + `powerCostPerLevel` tinyint(3) unsigned NOT NULL, + `powerCostPercent` tinyint(3) unsigned NOT NULL, + `powerPerSecond` smallint(5) unsigned NOT NULL, + `powerPerSecondPerLevel` tinyint(3) unsigned NOT NULL, + `powerGainRunicPower` smallint(5) unsigned NOT NULL, + `powerCostRunes` smallint(5) unsigned NOT NULL, + `rangeId` smallint(5) unsigned NOT NULL, + `stackAmount` mediumint(8) unsigned NOT NULL, + `tool1` mediumint(8) unsigned NOT NULL, + `tool2` mediumint(8) unsigned NOT NULL, + `toolCategory1` tinyint(3) unsigned NOT NULL, + `toolCategory2` tinyint(3) unsigned NOT NULL, + `reagent1` mediumint(9) NOT NULL, + `reagent2` mediumint(9) NOT NULL, + `reagent3` mediumint(9) NOT NULL, + `reagent4` mediumint(9) NOT NULL, + `reagent5` mediumint(9) NOT NULL, + `reagent6` mediumint(9) NOT NULL, + `reagent7` mediumint(9) NOT NULL, + `reagent8` mediumint(9) NOT NULL, + `reagentCount1` tinyint(4) NOT NULL, + `reagentCount2` tinyint(4) NOT NULL, + `reagentCount3` tinyint(4) NOT NULL, + `reagentCount4` tinyint(4) NOT NULL, + `reagentCount5` tinyint(4) NOT NULL, + `reagentCount6` tinyint(4) NOT NULL, + `reagentCount7` tinyint(4) NOT NULL, + `reagentCount8` tinyint(4) NOT NULL, + `equippedItemClass` tinyint(4) NOT NULL, + `equippedItemSubClassMask` int(11) NOT NULL, + `equippedItemInventoryTypeMask` int(10) unsigned NOT NULL, + `effect1Id` smallint(5) unsigned NOT NULL, + `effect2Id` smallint(5) unsigned NOT NULL, + `effect3Id` smallint(5) unsigned NOT NULL, + `effect1DieSides` int(11) NOT NULL, + `effect2DieSides` int(11) NOT NULL, + `effect3DieSides` int(11) NOT NULL, + `effect1RealPointsPerLevel` float NOT NULL, + `effect2RealPointsPerLevel` float NOT NULL, + `effect3RealPointsPerLevel` float NOT NULL, + `effect1BasePoints` int(11) NOT NULL, + `effect2BasePoints` int(11) NOT NULL, + `effect3BasePoints` int(11) NOT NULL, + `effect1Mechanic` tinyint(3) unsigned NOT NULL, + `effect2Mechanic` tinyint(3) unsigned NOT NULL, + `effect3Mechanic` tinyint(3) unsigned NOT NULL, + `effect1ImplicitTargetA` smallint(6) NOT NULL, + `effect2ImplicitTargetA` smallint(6) NOT NULL, + `effect3ImplicitTargetA` smallint(6) NOT NULL, + `effect1ImplicitTargetB` smallint(6) NOT NULL, + `effect2ImplicitTargetB` smallint(6) NOT NULL, + `effect3ImplicitTargetB` smallint(6) NOT NULL, + `effect1RadiusMin` smallint(5) unsigned NOT NULL, + `effect1RadiusMax` smallint(5) unsigned NOT NULL DEFAULT 0, + `effect2RadiusMin` smallint(5) unsigned NOT NULL, + `effect2RadiusMax` smallint(5) unsigned NOT NULL DEFAULT 0, + `effect3RadiusMin` smallint(5) unsigned NOT NULL, + `effect3RadiusMax` smallint(5) unsigned NOT NULL DEFAULT 0, + `effect1AuraId` smallint(5) unsigned NOT NULL, + `effect2AuraId` smallint(5) unsigned NOT NULL, + `effect3AuraId` smallint(5) unsigned NOT NULL, + `effect1Periode` mediumint(8) unsigned NOT NULL, + `effect2Periode` mediumint(8) unsigned NOT NULL, + `effect3Periode` mediumint(8) unsigned NOT NULL, + `effect1ValueMultiplier` float NOT NULL, + `effect2ValueMultiplier` float NOT NULL, + `effect3ValueMultiplier` float NOT NULL, + `effect1ChainTarget` smallint(5) unsigned NOT NULL, + `effect2ChainTarget` smallint(5) unsigned NOT NULL, + `effect3ChainTarget` smallint(5) unsigned NOT NULL, + `effect1CreateItemId` int(11) NOT NULL, + `effect2CreateItemId` int(11) NOT NULL, + `effect3CreateItemId` int(11) NOT NULL, + `effect1MiscValue` int(11) NOT NULL, + `effect2MiscValue` int(11) NOT NULL, + `effect3MiscValue` int(11) NOT NULL, + `effect1MiscValueB` mediumint(9) NOT NULL, + `effect2MiscValueB` mediumint(9) NOT NULL, + `effect3MiscValueB` mediumint(9) NOT NULL, + `effect1TriggerSpell` mediumint(9) NOT NULL, + `effect2TriggerSpell` mediumint(9) NOT NULL, + `effect3TriggerSpell` mediumint(9) NOT NULL, + `effect1PointsPerComboPoint` mediumint(9) NOT NULL, + `effect2PointsPerComboPoint` mediumint(9) NOT NULL, + `effect3PointsPerComboPoint` mediumint(9) NOT NULL, + `effect1SpellClassMaskA` int(11) NOT NULL, + `effect1SpellClassMaskB` int(11) NOT NULL, + `effect1SpellClassMaskC` int(11) NOT NULL, + `effect2SpellClassMaskA` int(11) NOT NULL, + `effect2SpellClassMaskB` int(11) NOT NULL, + `effect2SpellClassMaskC` int(11) NOT NULL, + `effect3SpellClassMaskA` int(11) NOT NULL, + `effect3SpellClassMaskB` int(11) NOT NULL, + `effect3SpellClassMaskC` int(11) NOT NULL, + `effect1DamageMultiplier` float NOT NULL, + `effect2DamageMultiplier` float NOT NULL, + `effect3DamageMultiplier` float NOT NULL, + `effect1BonusMultiplier` float NOT NULL, + `effect2BonusMultiplier` float NOT NULL, + `effect3BonusMultiplier` float NOT NULL, + `iconId` smallint(5) unsigned NOT NULL DEFAULT 0, + `iconIdBak` smallint(5) unsigned NOT NULL DEFAULT 0, + `iconIdAlt` mediumint(8) unsigned NOT NULL DEFAULT 0, + `rankNo` tinyint(3) unsigned NOT NULL, + `spellVisualId` smallint(5) unsigned NOT NULL, + `name_loc0` varchar(115) DEFAULT NULL, + `name_loc2` varchar(115) DEFAULT NULL, + `name_loc3` varchar(115) DEFAULT NULL, + `name_loc4` varchar(115) DEFAULT NULL, + `name_loc6` varchar(115) DEFAULT NULL, + `name_loc8` varchar(184) DEFAULT NULL, + `rank_loc0` varchar(21) DEFAULT NULL, + `rank_loc2` varchar(25) DEFAULT NULL, + `rank_loc3` varchar(22) DEFAULT NULL, + `rank_loc4` varchar(21) DEFAULT NULL, + `rank_loc6` varchar(29) DEFAULT NULL, + `rank_loc8` varchar(56) DEFAULT NULL, + `description_loc0` text DEFAULT NULL, + `description_loc2` text DEFAULT NULL, + `description_loc3` text DEFAULT NULL, + `description_loc4` text DEFAULT NULL, + `description_loc6` text DEFAULT NULL, + `description_loc8` text DEFAULT NULL, + `buff_loc0` text DEFAULT NULL, + `buff_loc2` text DEFAULT NULL, + `buff_loc3` text DEFAULT NULL, + `buff_loc4` text DEFAULT NULL, + `buff_loc6` text DEFAULT NULL, + `buff_loc8` text DEFAULT NULL, + `maxTargetLevel` tinyint(3) unsigned NOT NULL, + `spellFamilyId` tinyint(3) unsigned NOT NULL, + `spellFamilyFlags1` int(11) NOT NULL, + `spellFamilyFlags2` int(11) NOT NULL, + `spellFamilyFlags3` int(11) NOT NULL, + `maxAffectedTargets` tinyint(3) unsigned NOT NULL, + `damageClass` tinyint(3) unsigned NOT NULL, + `skillLine1` smallint(6) NOT NULL DEFAULT 0, + `skillLine2OrMask` bigint(20) NOT NULL DEFAULT 0, + `reqRaceMask` smallint(5) unsigned NOT NULL DEFAULT 0, + `reqClassMask` smallint(5) unsigned NOT NULL DEFAULT 0, + `reqSpellId` mediumint(8) unsigned NOT NULL DEFAULT 0, + `reqSkillLevel` smallint(5) unsigned NOT NULL DEFAULT 0, + `learnedAt` smallint(5) unsigned NOT NULL DEFAULT 0, + `skillLevelGrey` smallint(5) unsigned NOT NULL DEFAULT 0, + `skillLevelYellow` smallint(5) unsigned NOT NULL DEFAULT 0, + `schoolMask` tinyint(3) unsigned NOT NULL, + `spellDescriptionVariableId` smallint(6) NOT NULL, + `trainingCost` int(10) unsigned NOT NULL, + PRIMARY KEY (`id`), + KEY `category` (`typeCat`), + KEY `spell` (`id`) USING BTREE, + 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 */; + +-- +-- Table structure for table `aowow_spell_sounds` +-- + +DROP TABLE IF EXISTS `aowow_spell_sounds`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `aowow_spell_sounds` ( + `id` smallint(5) unsigned NOT NULL COMMENT 'SpellVisual.dbc/id', + `animation` smallint(5) unsigned NOT NULL DEFAULT 0, + `ready` smallint(5) unsigned NOT NULL DEFAULT 0, + `precast` smallint(5) unsigned NOT NULL DEFAULT 0, + `cast` smallint(5) unsigned NOT NULL DEFAULT 0, + `impact` smallint(5) unsigned NOT NULL DEFAULT 0, + `state` smallint(5) unsigned NOT NULL DEFAULT 0, + `statedone` smallint(5) unsigned NOT NULL DEFAULT 0, + `channel` smallint(5) unsigned NOT NULL DEFAULT 0, + `casterimpact` smallint(5) unsigned NOT NULL DEFAULT 0, + `targetimpact` smallint(5) unsigned NOT NULL DEFAULT 0, + `castertargeting` smallint(5) unsigned NOT NULL DEFAULT 0, + `missiletargeting` smallint(5) unsigned NOT NULL DEFAULT 0, + `instantarea` smallint(5) unsigned NOT NULL DEFAULT 0, + `persistentarea` smallint(5) unsigned NOT NULL DEFAULT 0, + `casterstate` smallint(5) unsigned NOT NULL DEFAULT 0, + `targetstate` smallint(5) unsigned NOT NULL DEFAULT 0, + `missile` smallint(5) unsigned NOT NULL DEFAULT 0 COMMENT 'not predicted by js', + `impactarea` smallint(5) unsigned NOT NULL DEFAULT 0 COMMENT 'not predicted by js', + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='!ATTENTION!\r\nthe primary key of this table is NOT a spellId, but spellVisualId\r\n\r\ncolumn names from LANG.sound_activities'; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `aowow_spelldifficulty` +-- + +DROP TABLE IF EXISTS `aowow_spelldifficulty`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `aowow_spelldifficulty` ( + `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_talents` +-- + +DROP TABLE IF EXISTS `aowow_talents`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `aowow_talents` ( + `id` smallint(5) unsigned NOT NULL, + `class` tinyint(3) unsigned NOT NULL, + `petTypeMask` tinyint(3) unsigned NOT NULL, + `tab` tinyint(3) unsigned NOT NULL, + `row` tinyint(3) unsigned NOT NULL, + `col` tinyint(3) unsigned NOT NULL, + `spell` mediumint(8) unsigned NOT NULL, + `rank` tinyint(3) unsigned NOT NULL, + PRIMARY KEY (`id`,`rank`), + KEY `spell` (`spell`), + KEY `class` (`class`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `aowow_taxinodes` +-- + +DROP TABLE IF EXISTS `aowow_taxinodes`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `aowow_taxinodes` ( + `id` smallint(5) unsigned NOT NULL, + `mapId` smallint(5) 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, + `reactH` tinyint(4) NOT NULL, + `name_loc0` varchar(59) DEFAULT NULL, + `name_loc2` varchar(84) DEFAULT NULL, + `name_loc3` varchar(61) DEFAULT NULL, + `name_loc4` varchar(59) DEFAULT NULL, + `name_loc6` varchar(89) DEFAULT NULL, + `name_loc8` varchar(142) DEFAULT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `aowow_taxipath` +-- + +DROP TABLE IF EXISTS `aowow_taxipath`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `aowow_taxipath` ( + `id` smallint(5) unsigned NOT NULL, + `startNodeId` smallint(5) unsigned NOT NULL, + `endNodeId` smallint(5) unsigned NOT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `aowow_titles` +-- + +DROP TABLE IF EXISTS `aowow_titles`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `aowow_titles` ( + `id` tinyint(3) unsigned NOT NULL, + `category` tinyint(3) unsigned NOT NULL, + `cuFlags` int(10) unsigned NOT NULL DEFAULT 0 COMMENT 'see defines.php for flags', + `gender` tinyint(3) unsigned NOT NULL, + `side` tinyint(3) unsigned NOT NULL, + `expansion` tinyint(3) unsigned NOT NULL, + `src12Ext` mediumint(8) unsigned NOT NULL, + `eventId` smallint(5) unsigned NOT NULL, + `bitIdx` tinyint(3) unsigned NOT NULL, + `male_loc0` varchar(33) DEFAULT NULL, + `male_loc2` varchar(35) DEFAULT NULL, + `male_loc3` varchar(37) DEFAULT NULL, + `male_loc4` varchar(37) DEFAULT NULL, + `male_loc6` varchar(34) DEFAULT NULL, + `male_loc8` varchar(37) DEFAULT NULL, + `female_loc0` varchar(33) DEFAULT NULL, + `female_loc2` varchar(35) DEFAULT NULL, + `female_loc3` varchar(39) DEFAULT NULL, + `female_loc4` varchar(39) DEFAULT NULL, + `female_loc6` varchar(35) DEFAULT NULL, + `female_loc8` varchar(41) DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `bitIdx` (`bitIdx`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `aowow_user_ratings` +-- + +DROP TABLE IF EXISTS `aowow_user_ratings`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `aowow_user_ratings` ( + `type` enum('COMMENT','GUIDE') NOT NULL, + `entry` int(11) NOT NULL DEFAULT 0, + `userId` int(10) unsigned DEFAULT NULL, + `value` tinyint(4) NOT NULL DEFAULT 0 COMMENT 'Rating Set', + UNIQUE KEY `type` (`type`,`entry`,`userId`), + KEY `FK_acc_co_rate_user` (`userId`), + CONSTRAINT `FK_userId` FOREIGN KEY (`userId`) REFERENCES `aowow_account` (`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 */; + +-- +-- Table structure for table `aowow_videos` +-- + +DROP TABLE IF EXISTS `aowow_videos`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `aowow_videos` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `type` smallint(5) unsigned NOT NULL, + `typeId` mediumint(9) NOT NULL, + `userIdOwner` int(10) unsigned DEFAULT NULL, + `date` int(11) NOT NULL, + `videoId` varchar(12) NOT NULL, + `pos` tinyint(3) unsigned NOT NULL, + `url` varchar(64) NOT NULL COMMENT 'preview thumb', + `width` smallint(5) unsigned NOT NULL, + `height` smallint(5) unsigned NOT NULL, + `name` varchar(64) DEFAULT NULL, + `caption` varchar(200) DEFAULT NULL, + `status` int(11) NOT NULL, + `userIdApprove` int(10) unsigned DEFAULT NULL, + `userIdeDelete` int(10) unsigned DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `type` (`type`,`typeId`), + KEY `FK_acc_vi` (`userIdOwner`), + CONSTRAINT `FK_acc_vi` FOREIGN KEY (`userIdOwner`) REFERENCES `aowow_account` (`id`) ON DELETE SET NULL ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=COMPACT; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `aowow_zones` +-- + +DROP TABLE IF EXISTS `aowow_zones`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `aowow_zones` ( + `id` smallint(5) unsigned NOT NULL COMMENT 'Zone Id', + `mapId` smallint(5) unsigned NOT NULL COMMENT 'Map Identifier', + `mapIdBak` smallint(5) unsigned NOT NULL, + `parentArea` smallint(5) unsigned NOT NULL, + `category` smallint(5) unsigned NOT NULL, + `flags` int(10) unsigned NOT NULL, + `cuFlags` int(10) unsigned NOT NULL DEFAULT 0 COMMENT 'see defines.php for flags', + `faction` tinyint(3) unsigned NOT NULL, + `expansion` tinyint(3) unsigned NOT NULL, + `type` tinyint(3) unsigned NOT NULL, + `maxPlayer` tinyint(4) NOT NULL, + `itemLevelReqN` smallint(5) unsigned NOT NULL, + `itemLevelReqH` smallint(5) unsigned NOT NULL, + `levelReq` tinyint(3) unsigned NOT NULL, + `levelReqLFG` tinyint(3) unsigned NOT NULL, + `levelHeroic` tinyint(3) unsigned NOT NULL, + `levelMin` tinyint(3) unsigned NOT NULL, + `levelMax` tinyint(3) unsigned NOT NULL, + `attunementsN` text NOT NULL COMMENT 'space separated; type:typeId', + `attunementsH` text NOT NULL COMMENT 'space separated; type:typeId', + `parentMapId` smallint(5) unsigned NOT NULL, + `parentX` float NOT NULL, + `parentY` float NOT NULL, + `name_loc0` varchar(120) DEFAULT NULL COMMENT 'Map Name', + `name_loc2` varchar(120) DEFAULT NULL, + `name_loc3` varchar(120) DEFAULT NULL, + `name_loc4` varchar(120) DEFAULT NULL, + `name_loc6` varchar(120) DEFAULT NULL, + `name_loc8` varchar(120) DEFAULT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `aowow_zones_sounds` +-- + +DROP TABLE IF EXISTS `aowow_zones_sounds`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `aowow_zones_sounds` ( + `id` smallint(5) unsigned NOT NULL, + `ambienceDay` smallint(5) unsigned NOT NULL, + `ambienceNight` smallint(5) unsigned NOT NULL, + `musicDay` smallint(5) unsigned NOT NULL, + `musicNight` smallint(5) unsigned NOT NULL, + `intro` smallint(5) unsigned NOT NULL, + `worldStateId` smallint(5) unsigned NOT NULL, + `worldStateValue` smallint(6) NOT NULL, + KEY `id` (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +/*!40101 SET SQL_MODE=@OLD_SQL_MODE */; +/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */; +/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */; +/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */; +/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */; +/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */; + +-- Dump completed on 2025-09-22 23:29:16 diff --git a/setup/sql/02-db_initial_data.sql b/setup/sql/02-db_initial_data.sql new file mode 100644 index 00000000..fe750cf3 --- /dev/null +++ b/setup/sql/02-db_initial_data.sql @@ -0,0 +1,168 @@ +-- MariaDB dump 10.19 Distrib 10.4.32-MariaDB, for Win64 (AMD64) +-- +-- Host: localhost Database: aowow +-- ------------------------------------------------------ +-- Server version 10.4.32-MariaDB + +/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */; +/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */; +/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */; +/*!40101 SET NAMES utf8mb4 */; +/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */; +/*!40103 SET TIME_ZONE='+00:00' */; +/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */; +/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */; +/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */; +/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */; + +-- +-- Dumping data for table `aowow_account` +-- + +LOCK TABLES `aowow_account` WRITE; +/*!40000 ALTER TABLE `aowow_account` DISABLE KEYS */; +INSERT INTO `aowow_account` VALUES (0,0,'','','AoWoW',NULL,0,0,0,'','',0,0,0,0,0,'','','',1,0,0,0,'',0,2,'',0); +/*!40000 ALTER TABLE `aowow_account` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Dumping data for table `aowow_account_weightscales` +-- + +LOCK TABLES `aowow_account_weightscales` WRITE; +/*!40000 ALTER TABLE `aowow_account_weightscales` DISABLE KEYS */; +INSERT INTO `aowow_account_weightscales` VALUES (1,0,'arms',1,0,'ability_rogue_eviscerate'),(2,0,'fury',1,1,'ability_warrior_innerrage'),(3,0,'prot',1,2,'ability_warrior_defensivestance'),(4,0,'holy',2,0,'spell_holy_holybolt'),(5,0,'prot',2,1,'ability_paladin_shieldofthetemplar'),(6,0,'retrib',2,2,'spell_holy_auraoflight'),(7,0,'beast',3,0,'ability_hunter_beasttaming'),(8,0,'marks',3,1,'ability_marksmanship'),(9,0,'surv',3,2,'ability_hunter_swiftstrike'),(10,0,'assas',4,0,'ability_rogue_eviscerate'),(11,0,'combat',4,1,'ability_backstab'),(12,0,'subtle',4,2,'ability_stealth'),(13,0,'disc',5,0,'spell_holy_wordfortitude'),(14,0,'holy',5,1,'spell_holy_guardianspirit'),(15,0,'shadow',5,2,'spell_shadow_shadowwordpain'),(16,0,'blooddps',6,0,'spell_deathknight_bloodpresence'),(17,0,'frostdps',6,1,'spell_deathknight_frostpresence'),(18,0,'frosttank',6,2,'spell_deathknight_frostpresence'),(19,0,'unholydps',6,3,'spell_deathknight_unholypresence'),(20,0,'elem',7,0,'spell_nature_lightning'),(21,0,'enhance',7,1,'spell_nature_lightningshield'),(22,0,'resto',7,2,'spell_nature_magicimmunity'),(23,0,'arcane',8,0,'spell_holy_magicalsentry'),(24,0,'fire',8,1,'spell_fire_firebolt02'),(25,0,'frost',8,2,'spell_frost_frostbolt02'),(26,0,'afflic',9,0,'spell_shadow_deathcoil'),(27,0,'demo',9,1,'spell_shadow_metamorphosis'),(28,0,'destro',9,2,'spell_shadow_rainoffire'),(29,0,'balance',11,0,'spell_nature_starfall'),(30,0,'feraltank',11,2,'ability_racial_bearform'),(31,0,'resto',11,3,'spell_nature_healingtouch'),(32,0,'feraldps',11,1,'ability_druid_catform'); +/*!40000 ALTER TABLE `aowow_account_weightscales` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Dumping data for table `aowow_account_weightscale_data` +-- + +LOCK TABLES `aowow_account_weightscale_data` WRITE; +/*!40000 ALTER TABLE `aowow_account_weightscale_data` DISABLE KEYS */; +INSERT INTO `aowow_account_weightscale_data` VALUES (2,'exprtng',100),(2,'str',82),(2,'critstrkrtng',66),(2,'agi',53),(2,'armorpenrtng',52),(2,'hitrtng',48),(2,'hastertng',36),(2,'atkpwr',31),(2,'armor',5),(3,'sta',100),(3,'dodgertng',90),(3,'defrtng',86),(3,'block',81),(3,'agi',67),(3,'parryrtng',67),(3,'blockrtng',48),(3,'str',48),(3,'exprtng',19),(3,'hitrtng',10),(3,'armorpenrtng',10),(3,'critstrkrtng',7),(3,'armor',6),(3,'hastertng',1),(3,'atkpwr',1),(4,'int',100),(4,'manargn',88),(4,'splpwr',58),(4,'critstrkrtng',46),(4,'hastertng',35),(5,'sta',100),(5,'dodgertng',94),(5,'block',86),(5,'defrtng',86),(5,'exprtng',79),(5,'agi',76),(5,'parryrtng',76),(5,'hitrtng',58),(5,'blockrtng',52),(5,'str',50),(5,'armor',6),(5,'atkpwr',6),(5,'splpwr',4),(5,'critstrkrtng',3),(6,'mledps',470),(6,'hitrtng',100),(6,'str',80),(6,'exprtng',66),(6,'critstrkrtng',40),(6,'atkpwr',34),(6,'agi',32),(6,'hastertng',30),(6,'armorpenrtng',22),(6,'splpwr',9),(7,'rgddps',213),(7,'hitrtng',100),(7,'agi',58),(7,'critstrkrtng',40),(7,'int',37),(7,'atkpwr',30),(7,'armorpenrtng',28),(7,'hastertng',21),(8,'rgddps',379),(8,'hitrtng',100),(8,'agi',74),(8,'critstrkrtng',57),(8,'armorpenrtng',40),(8,'int',39),(8,'atkpwr',32),(8,'hastertng',24),(9,'rgddps',181),(9,'hitrtng',100),(9,'agi',76),(9,'critstrkrtng',42),(9,'int',35),(9,'hastertng',31),(9,'atkpwr',29),(9,'armorpenrtng',26),(10,'mledps',170),(10,'agi',100),(10,'exprtng',87),(10,'hitrtng',83),(10,'critstrkrtng',81),(10,'atkpwr',65),(10,'armorpenrtng',65),(10,'hastertng',64),(10,'str',55),(11,'mledps',220),(11,'armorpenrtng',100),(11,'agi',100),(11,'exprtng',82),(11,'hitrtng',80),(11,'critstrkrtng',75),(11,'hastertng',73),(11,'str',55),(11,'atkpwr',50),(12,'mledps',228),(12,'exprtng',100),(12,'agi',100),(12,'hitrtng',80),(12,'armorpenrtng',75),(12,'critstrkrtng',75),(12,'hastertng',75),(12,'str',55),(12,'atkpwr',50),(13,'splpwr',100),(13,'manargn',67),(13,'int',65),(13,'hastertng',59),(13,'critstrkrtng',48),(13,'spi',22),(14,'manargn',100),(14,'int',69),(14,'splpwr',60),(14,'spi',52),(14,'critstrkrtng',38),(14,'hastertng',31),(15,'hitrtng',100),(15,'shasplpwr',76),(15,'splpwr',76),(15,'critstrkrtng',54),(15,'hastertng',50),(15,'spi',16),(15,'int',16),(16,'mledps',360),(16,'armorpenrtng',100),(16,'str',99),(16,'hitrtng',91),(16,'exprtng',90),(16,'critstrkrtng',57),(16,'hastertng',55),(16,'atkpwr',36),(16,'armor',1),(17,'mledps',337),(17,'hitrtng',100),(17,'str',97),(17,'exprtng',81),(17,'armorpenrtng',61),(17,'critstrkrtng',45),(17,'atkpwr',35),(17,'hastertng',28),(17,'armor',1),(18,'mledps',419),(18,'parryrtng',100),(18,'hitrtng',97),(18,'str',96),(18,'defrtng',85),(18,'exprtng',69),(18,'dodgertng',61),(18,'agi',61),(18,'sta',61),(18,'critstrkrtng',49),(18,'atkpwr',41),(18,'armorpenrtng',31),(18,'armor',5),(19,'mledps',209),(19,'str',100),(19,'hitrtng',66),(19,'exprtng',51),(19,'hastertng',48),(19,'critstrkrtng',45),(19,'atkpwr',34),(19,'armorpenrtng',32),(19,'armor',1),(20,'hitrtng',100),(20,'splpwr',60),(20,'hastertng',56),(20,'critstrkrtng',40),(20,'int',11),(21,'mledps',135),(21,'hitrtng',100),(21,'exprtng',84),(21,'agi',55),(21,'int',55),(21,'critstrkrtng',55),(21,'hastertng',42),(21,'str',35),(21,'atkpwr',32),(21,'splpwr',29),(21,'armorpenrtng',26),(22,'manargn',100),(22,'int',85),(22,'splpwr',77),(22,'critstrkrtng',62),(22,'hastertng',35),(23,'hitrtng',100),(23,'hastertng',54),(23,'arcsplpwr',49),(23,'splpwr',49),(23,'critstrkrtng',37),(23,'int',34),(23,'frosplpwr',24),(23,'firsplpwr',24),(23,'spi',14),(24,'hitrtng',100),(24,'hastertng',53),(24,'firsplpwr',46),(24,'splpwr',46),(24,'critstrkrtng',43),(24,'frosplpwr',23),(24,'arcsplpwr',23),(24,'int',13),(25,'hitrtng',100),(25,'hastertng',42),(25,'frosplpwr',39),(25,'splpwr',39),(25,'arcsplpwr',19),(25,'firsplpwr',19),(25,'critstrkrtng',19),(25,'int',6),(26,'hitrtng',100),(26,'shasplpwr',72),(26,'splpwr',72),(26,'hastertng',61),(26,'critstrkrtng',38),(26,'firsplpwr',36),(26,'spi',34),(26,'int',15),(27,'hitrtng',100),(27,'hastertng',50),(27,'firsplpwr',45),(27,'shasplpwr',45),(27,'splpwr',45),(27,'critstrkrtng',31),(27,'spi',29),(27,'int',13),(28,'hitrtng',100),(28,'firsplpwr',47),(28,'splpwr',47),(28,'hastertng',46),(28,'spi',26),(28,'shasplpwr',23),(28,'critstrkrtng',16),(28,'int',13),(29,'hitrtng',100),(29,'splpwr',66),(29,'hastertng',54),(29,'critstrkrtng',43),(29,'spi',22),(29,'int',22),(30,'agi',100),(30,'sta',75),(30,'dodgertng',65),(30,'defrtng',60),(30,'exprtng',16),(30,'str',10),(30,'armor',10),(30,'hitrtng',8),(30,'hastertng',5),(30,'atkpwr',4),(30,'feratkpwr',4),(30,'critstrkrtng',3),(31,'splpwr',100),(31,'manargn',73),(31,'hastertng',57),(31,'int',51),(31,'spi',32),(31,'critstrkrtng',11),(32,'agi',100),(32,'armorpenrtng',90),(32,'str',80),(32,'critstrkrtng',55),(32,'exprtng',50),(32,'hitrtng',50),(32,'feratkpwr',40),(32,'atkpwr',40),(32,'hastertng',35); +/*!40000 ALTER TABLE `aowow_account_weightscale_data` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Dumping data for table `aowow_announcements` +-- + +LOCK TABLES `aowow_announcements` WRITE; +/*!40000 ALTER TABLE `aowow_announcements` DISABLE KEYS */; +INSERT INTO `aowow_announcements` VALUES (4,'compare','Help: Item Comparison Tool',0,'padding-left: 55px; background-image: url(STATIC_URL/images/announcements/help-small.png); background-position: 10px center',1,1,'First time? - Don\'t be shy! Just check out our [url=?help=item-comparison]Help page[/url]!','Première visite? - Ne soyez pas intimidé! Vous n\'avez qu\'à lire notre [url=?help=item-comparison]page d\'aide[/url] !','Euer erstes Mal? Nur keine falsche Scheu! Schaut einfach auf unsere [url=?help=item-comparison]Hilfeseite[/url]!','','¿Tu primera vez? ¡No seas vergonzoso! !Mira nuestra [url=?help=item-comparison]página de ayuda[/url]!','Впервые? Не стесняйтесь посетить нашу [url=?help=item-comparison]справочную страницу[/url]!'),(3,'profile','Help: Profiler',0,'padding-left: 80px; background-image: url(STATIC_URL/images/announcements/help-large.gif); background-position: 10px center',1,1,'[h3]First Time?[/h3]\n\nThe [b]Profiler[/b] tool lets you [span class=tip title=\"e.g. See how\'d you look as a different race, try different gear or talents, and more!\"]edit your character[/span], find gear upgrades, check your gear score, and more!\n\n[ul]\n[li][b]Right-click[/b] slots to change items, add gems/enchants, or find upgrades.[/li]\n[li]Use the [b]Claim character[/b] button to add your own characters to your [url=?user]user page[/url].[/li]\n[li]Save a modified character to your Aowow account by using the [b]Save as[/b] button.[/li]\n[li][b]Statistics[/b] will update in real time as you make tweaks.[/li]\n[/ul]\n\nFor more information, check out our extensive [url=?help=profiler]help page[/url]!','','[h3]Euer erster Besuch?[/h3]\n\nDas [b]Profiler[/b]-Werkzeug erlaubt es euch [span class=tip title=\"z.B. Seht, wie Ihr als anderes Volk aussehen würdet, probiert andere Ausrüstung oder Talente aus, und mehr!\"]euren Charakter zu bearbeiten[/span], besser Ausrüstung zu finden, eure Ausrüstungswertung zu vergleichen, und vieles mehr!\n\n[ul]\n[li][b]Rechts-klickt[/b] Plätze um Gegenstände zu tauschen, Edelsteine/Verzauberungen hinzuzufügen, oder bessere AUsrüstung zu finden.[/li]\n[li]Benutzt [b]Charakter beanspruchen[/b] um eure eigenen Charaktere Eurer [url=?user]Benutzerseite[/url] hinzuzufügen.[/li]\n[li]Speichert einen modifizierten Charakter in Eurem Aowow-Konto, indem Ihr [b]Speichern als[/b] benutzt.[/li]\n[li]Die [b]Statistiken[/b] aktualisieren sich in Echtzeit, während Ihr Änderungen durchführt.[/li]\n[/ul]\n\nWeitere Informationen findet Ihr auf unserer umfangreichen [url=?help=profiler]Hilfeseite[/url]!','','',''),(2,'profiler','Help: Profiler',0,'padding-left: 80px; background-image: url(STATIC_URL/images/announcements/help-large.gif); background-position: 10px center',1,1,'[h3]First Time?[/h3]\n\nThe [b]Profiler[/b] tool lets you [span class=tip title=\"e.g. See how\'d you look as a different race, try different gear or talents, and more!\"]edit your character[/span], find gear upgrades, check your gear score, and more!\n\n[ul]\n[li][b]Right-click[/b] slots to change items, add gems/enchants, or find upgrades.[/li]\n[li]Use the [b]Claim character[/b] button to add your own characters to your [url=?user]user page[/url].[/li]\n[li]Save a modified character to your Aowow account by using the [b]Save as[/b] button.[/li]\n[li][b]Statistics[/b] will update in real time as you make tweaks.[/li]\n[/ul]\n\nFor more information, check out our extensive [url=?help=profiler]help page[/url]!','','[h3]Euer erster Besuch?[/h3]\n\nDas [b]Profiler[/b]-Werkzeug erlaubt es euch [span class=tip title=\"z.B. Seht, wie Ihr als anderes Volk aussehen würdet, probiert andere Ausrüstung oder Talente aus, und mehr!\"]euren Charakter zu bearbeiten[/span], besser Ausrüstung zu finden, eure Ausrüstungswertung zu vergleichen, und vieles mehr!\n\n[ul]\n[li][b]Rechts-klickt[/b] Plätze um Gegenstände zu tauschen, Edelsteine/Verzauberungen hinzuzufügen, oder bessere AUsrüstung zu finden.[/li]\n[li]Benutzt [b]Charakter beanspruchen[/b] um eure eigenen Charaktere Eurer [url=?user]Benutzerseite[/url] hinzuzufügen.[/li]\n[li]Speichert einen modifizierten Charakter in Eurem Aowow-Konto, indem Ihr [b]Speichern als[/b] benutzt.[/li]\n[li]Die [b]Statistiken[/b] aktualisieren sich in Echtzeit, während Ihr Änderungen durchführt.[/li]\n[/ul]\n\nWeitere Informationen findet Ihr auf unserer umfangreichen [url=?help=profiler]Hilfeseite[/url]!','','',''); +/*!40000 ALTER TABLE `aowow_announcements` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Dumping data for table `aowow_config` +-- + +LOCK TABLES `aowow_config` WRITE; +/*!40000 ALTER TABLE `aowow_config` DISABLE KEYS */; +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; + +-- +-- Dumping data for table `aowow_dbversion` +-- + +LOCK TABLES `aowow_dbversion` WRITE; +/*!40000 ALTER TABLE `aowow_dbversion` DISABLE KEYS */; +INSERT INTO `aowow_dbversion` VALUES (1774551740,0,NULL,NULL); +/*!40000 ALTER TABLE `aowow_dbversion` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Dumping data for table `aowow_home_featuredbox` +-- + +LOCK TABLES `aowow_home_featuredbox` WRITE; +/*!40000 ALTER TABLE `aowow_home_featuredbox` DISABLE KEYS */; +INSERT INTO `aowow_home_featuredbox` VALUES (1,NULL,0,0,0,0,'',NULL,NULL,'[pad]Welcome to [b][span class=q5]AoWoW[/span][/b]!','[pad]Bienvenue à [b][span class=q5]AoWoW[/span][/b]!','[pad]Willkommen bei [b][span class=q5]AoWoW[/span][/b]!','','','Добро[pad] пожаловать на [b][span class=q5]AoWoW[/span][/b]!'),(2,NULL,0,0,0,1,'STATIC_URL/images/logos/newsbox-explained.png',NULL,NULL,'[ul]\n[li][i]just demoing the newsbox here..[/i][/li]\n[li][b][url=http://www.example.com]..with urls[/url][/b][/li]\n[li][b]..typeLinks [item=45533][/b][/li]\n[li][b]..also, over there to the right is an overlay-trigger =>[/b][/li]\n[/ul]\n\n[ul]\n[li][tooltip name=demotip]hey, it hints you stuff![/tooltip][b][span class=tip tooltip=demotip]..hover me[/span][/b][/li]\n[/ul]','','','','',''); +/*!40000 ALTER TABLE `aowow_home_featuredbox` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Dumping data for table `aowow_home_featuredbox_overlay` +-- + +LOCK TABLES `aowow_home_featuredbox_overlay` WRITE; +/*!40000 ALTER TABLE `aowow_home_featuredbox_overlay` DISABLE KEYS */; +INSERT INTO `aowow_home_featuredbox_overlay` VALUES (2,405,100,'http://example.com','example overlay','','','','',''); +/*!40000 ALTER TABLE `aowow_home_featuredbox_overlay` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Dumping data for table `aowow_home_titles` +-- + +LOCK TABLES `aowow_home_titles` WRITE; +/*!40000 ALTER TABLE `aowow_home_titles` DISABLE KEYS */; +INSERT INTO `aowow_home_titles` VALUES (1,0,1522321542,1,0,'That\'s a 50 DKP plus!'),(2,0,1522321542,1,0,'We\'ve got what you need!'),(3,0,1522321542,1,0,'You haven\'t found the secret title yet.'),(4,0,1522321542,1,0,'...and knowing is half the battle!'),(5,0,1522321542,1,0,'Good news, everyone!'),(6,0,1522321542,1,0,'+1, Insightful'),(7,0,1522321542,1,0,'More effective than a [Booterang].'),(8,0,1522321542,1,0,'There is no cow level.'),(9,0,1522321542,1,0,'We\'ve got more style than a fashion designer who knows CSS.'),(10,0,1522321542,1,3,'Eure Fertigkeit in WoW hat sich auf 450 erhöht.'),(11,0,1522321542,1,0,'If you use your mouse to search, you won\'t be able to click on Rend.'),(12,0,1522321542,1,2,'Tout est dans l\'élégance.'),(13,0,1522321542,1,2,'Rend les chargements supportables depuis 2006.'),(14,0,1522321542,1,2,'Vous allez revenir.'),(15,0,1522321542,1,2,'Base de données extraordinaire'),(16,0,1522321542,1,2,'Si vous lisez ceci, arrêtez d\'appuyer sur F5.'),(17,0,1522321542,1,3,'Und der Tag ist gerettet.'),(18,0,1522321542,1,3,'Jetzt in allen bekannten Internetzen verfügbar!'),(19,0,1522321542,1,3,'Morgens, halb drei in Nordend'),(20,0,1522321542,1,3,'Macht auch Euren Webbrowser glücklich!'),(21,0,1522321542,1,3,'Hier findet Ihr sogar Mankriks Frau.'),(22,0,1522321542,1,6,'Base de datos extraordinaria de WoW'),(23,0,1522321542,1,6,'La única cosa en la que los ninjas y los piratas estan de acuerdo.'),(24,0,1522321542,1,6,'La elegancia lo es todo.'),(25,0,1522321542,1,6,'Hace feliz a los navegadores.'),(26,0,1522321542,1,8,'Ты ещё вернёшься.'),(27,0,1522321542,1,8,'Осваивание нового босса - 45 золота на ремонт. Персональный эпический предмет - 650 золотых'),(28,0,1522321542,1,8,'Не именной. Поделитесь им с друзьями!'),(29,0,1522321542,1,8,'Если вы здесь впервые, то вам необходимо воспользоваться поиском!'),(30,0,1522321542,1,8,'Приколы Мулгора без чата в Мулгоре.'),(31,0,1522321542,1,2,'Les trois premières lettres veulent tout dire.'),(32,0,1522321542,1,2,'Trouvez la femme de Mankrik grâce à lui.'),(33,0,1522321542,1,6,'Tu habilidad con WoW se ha incrementado a 450.'),(34,0,1522321542,1,6,'Buscando uno más: Tú'),(35,0,1522321542,1,8,'Первые три буквы говорят сами за себя.'),(36,0,1522321542,1,8,'У нас больше стиля, чем у дизайнера, знающего CSS.'),(37,0,1522321542,1,0,'Preventing wipes since 2006.'),(38,0,1522321542,1,0,'Never gonna give you up. Never gonna let you down.'),(39,0,1522321542,1,0,'The closest thing to an F1 key for WoW.'),(40,0,1522321542,1,2,'Non lié. Partagez-le avec vos amis !'),(41,0,1522321542,1,2,'Votre navigateur l\'adore !'),(42,0,1522321542,1,3,'Verhindert Wipes seit 2006.'),(43,0,1522321542,1,6,'+1, Utilidad'),(44,0,1522321542,1,6,'Épico, como tu líder de facción.'),(45,0,1522321542,1,8,'Он такой один...'),(46,0,1522321542,1,8,'Если вы это читаете, то прекратите обновлять страницу.'),(47,0,1522321542,1,0,'If you are reading this, stop pressing F5.'),(48,0,1522321542,1,2,'Chasse les jours pluvieux.'),(49,0,1522321542,1,3,'+1, Hilfreich'),(50,0,1522321542,1,3,'Episch - markant - dreifach verzaubert'),(51,0,1522321542,1,8,'Работает как положено.'),(52,0,1522321542,1,0,'Flagged for awesome.'),(53,0,1522321542,1,0,'Thrall-tested, Jaina-approved.'),(54,0,1522321542,1,8,'Всё дело в элегантности.'),(55,0,1522321542,1,0,'What does it mean?'),(56,0,1522321542,1,0,'YOU ARE NOW PREPARED!'),(57,0,1522321542,1,0,'srsly'),(58,0,1522321542,1,2,'C\'est comme prétendre être malade et aller à la plage, mais pour les bases de données.'),(59,0,1522321542,1,3,'Thrall-getestet, Jaina-genehmigt'),(60,0,1522321542,1,6,'Haciendo las pantallas de carga más soportables desde el 2006'),(61,0,1522321542,1,8,'Создан быть лидером.'),(62,0,1522321542,1,0,'You\'ll say \"Wow\" every time.'),(63,0,1522321542,1,0,'Dataz! We need more dataz!'),(64,0,1522321542,1,0,'Your skill in WoW has increased to 450.'),(65,0,1522321542,1,3,'Eleganz ist alles.'),(66,0,1522321542,1,8,'+1, Полезный'),(67,0,1522321542,1,8,'Ух ты!'),(68,0,1522321542,1,0,'Sometimes there is fire. You need to not be in it.'),(69,0,1522321542,1,0,'Working as intended.'),(70,0,1522321542,1,2,'La seule chose sur laquelle les ninjas et les pirates sont d\'accord.'),(71,0,1522321542,1,3,'Nicht seelengebunden. Teilt es mit Euren Freunden!'),(72,0,1522321542,1,8,'Теперь доступен во всех известных Интернетах!'),(73,0,1522321542,1,8,'Вы получаете добычу: [Легендарное Знание]'),(74,0,1522321542,1,0,'You\'ll be back.'),(75,0,1522321542,1,0,'Epic like your faction leader.'),(76,0,1522321542,1,3,'Manchmal gibt es Feuer. Ihr dürft nicht drin stehen.'),(77,0,1522321542,1,3,'Wer das hier lesen kann, drückt zu oft F5.'),(78,0,1522321542,1,6,'¡Datos! ¡Más Datos!'),(79,0,1522321542,1,8,'НЯМ НЯМ НЯМ'),(80,0,1522321542,1,2,'Testé par Thrall, approuvé par Jaina.'),(81,0,1522321542,1,8,'Сделайте его вашей новой расовой возможностью уже сегодня!'),(82,0,1522321542,1,0,'We do math, so you don\'t have to.'),(83,0,1522321542,1,0,'OM NOM NOM'),(84,0,1522321542,1,0,'Now available on all known internets!'),(85,0,1522321542,1,0,'We brake for dataz.'),(86,0,1522321542,1,3,'Neues von der Obstverkäuferfront'),(87,0,1522321542,1,6,'Las primeras tres palabras lo dicen todo.'),(88,0,1522321542,1,8,'Это как будто сказать всем, что ты болен, а самому пойти на пляж, - только для баз данных.'),(89,0,1522321542,1,8,'Меняем семечки на данные!'),(90,0,1522321542,1,0,'It\'s all about elegance.'),(91,0,1522321542,1,0,'Never underestimate the power of the Scout\'s code.'),(92,0,1522321542,1,6,'Elimina los días lluviosos.'),(93,0,1522321542,1,0,'You just won the game.'),(94,0,1522321542,1,8,'Данные! Нам надо больше данных!'),(95,0,1522321542,1,0,'WoW Database Extraordinaire'),(96,0,1522321542,1,0,'No longer soulbound. Can now be shared with friends!'),(97,0,1522321542,1,0,'The dataz you could be using.'),(98,0,1522321542,1,8,'Превосходен, как лидер вашей фракции.'),(99,0,1522321542,1,6,'¡Regresarás!'); +/*!40000 ALTER TABLE `aowow_home_titles` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Dumping data for table `aowow_loot_link` +-- + +LOCK TABLES `aowow_loot_link` WRITE; +/*!40000 ALTER TABLE `aowow_loot_link` DISABLE KEYS */; +INSERT INTO `aowow_loot_link` VALUES (19710,184465,1,0,0),(19218,184465,1,1,0),(21526,184849,2,0,0),(21525,184849,2,1,0),(17537,185168,1,1,0),(17536,185168,1,0,0),(18434,185169,2,1,0),(18432,185169,2,0,0),(28234,190586,1,0,0),(28234,193996,2,0,0),(26533,190663,1,0,0),(31217,193597,2,0,0),(27656,191349,1,0,0),(31561,193603,2,0,0),(28859,193905,1,0,0),(31734,193967,2,0,0),(32845,194307,1,0,0),(32846,194308,2,0,0),(32845,194200,3,0,0),(32846,194201,4,0,0),(32865,194312,1,0,0),(33147,194314,2,0,0),(32865,194313,3,0,0),(33147,194315,4,0,0),(32906,194324,1,0,0),(33360,194328,2,0,0),(32906,194327,3,0,0),(33360,194331,4,0,0),(32871,194821,1,0,0),(33070,194822,2,0,0),(33350,194789,1,0,0),(33350,194956,2,0,0),(33350,194957,3,0,0),(33350,194958,4,0,0),(32930,195046,1,0,0),(33909,195047,2,0,0),(34928,195323,1,0,0),(35517,195324,2,0,0),(35119,195374,1,0,0),(35518,195375,2,0,0),(37226,201710,1,0,0),(37226,202336,2,0,0),(36789,201959,1,0,0),(38174,202339,2,0,0),(36789,202338,3,0,0),(38174,202340,4,0,0),(38402,202239,1,0,0),(38582,202240,2,0,0),(37813,202238,3,0,0),(38583,202241,4,0,0),(9034,169243,1,0,243),(9035,169243,1,1,243),(9039,169243,1,0,243),(9036,169243,1,0,243),(9037,169243,1,0,243),(9038,169243,1,0,243),(9040,169243,1,0,243),(34657,195709,1,0,334),(34701,195709,1,0,334),(34703,195709,1,0,334),(34702,195709,1,0,334),(34705,195709,1,0,334),(35571,195709,1,0,334),(35617,195709,1,0,334),(35572,195709,1,0,334),(35570,195709,1,0,334),(35569,195709,1,1,334),(36089,195710,2,0,334),(36086,195710,2,0,334),(36087,195710,2,0,334),(36082,195710,2,0,334),(36085,195710,2,1,334),(36088,195710,2,0,334),(36084,195710,2,0,334),(36083,195710,2,0,334),(36091,195710,2,0,334),(36090,195710,2,0,334),(34458,195631,1,0,637),(34465,195631,1,0,637),(34463,195631,1,0,637),(34460,195631,1,0,637),(34459,195631,1,0,637),(34456,195631,1,0,637),(34466,195631,1,0,637),(34467,195631,1,0,637),(34468,195631,1,0,637),(34469,195631,1,0,637),(34470,195631,1,0,637),(34472,195631,1,0,637),(34474,195631,1,0,637),(34473,195631,1,0,637),(34455,195631,1,0,637),(34454,195631,1,0,637),(34453,195631,1,0,637),(34441,195631,1,1,637),(34471,195631,1,0,637),(34475,195631,1,0,637),(34444,195631,1,0,637),(34445,195631,1,0,637),(34447,195631,1,0,637),(34461,195631,1,0,637),(34448,195631,1,0,637),(34449,195631,1,0,637),(34450,195631,1,0,637),(34451,195631,1,0,637),(35686,195632,2,0,637),(35671,195632,2,0,637),(35683,195632,2,0,637),(35680,195632,2,0,637),(35674,195632,2,0,637),(35689,195632,2,0,637),(35721,195632,2,0,637),(35718,195632,2,0,637),(35731,195632,2,0,637),(35714,195632,2,0,637),(35711,195632,2,0,637),(35734,195632,2,0,637),(35737,195632,2,0,637),(35740,195632,2,0,637),(35743,195632,2,0,637),(35746,195632,2,0,637),(35708,195632,2,0,637),(35705,195632,2,0,637),(35702,195632,2,0,637),(35699,195632,2,0,637),(35695,195632,2,0,637),(35692,195632,2,0,637),(35728,195632,2,0,637),(35724,195632,2,0,637),(35668,195632,2,0,637),(34442,195632,2,1,637),(35662,195632,2,0,637),(35665,195632,2,0,637),(35725,195633,3,0,637),(35722,195633,3,0,637),(35719,195633,3,0,637),(35715,195633,3,0,637),(35709,195633,3,0,637),(35706,195633,3,0,637),(35729,195633,3,0,637),(35744,195633,3,0,637),(35732,195633,3,0,637),(35735,195633,3,0,637),(35738,195633,3,0,637),(35741,195633,3,0,637),(35747,195633,3,0,637),(35712,195633,3,0,637),(35703,195633,3,0,637),(35700,195633,3,0,637),(35672,195633,3,0,637),(35690,195633,3,0,637),(35687,195633,3,0,637),(35669,195633,3,0,637),(35684,195633,3,0,637),(35693,195633,3,0,637),(34443,195633,3,1,637),(35681,195633,3,0,637),(35663,195633,3,0,637),(35666,195633,3,0,637),(35696,195633,3,0,637),(35675,195633,3,0,637),(35700,195635,4,0,637),(35749,195635,4,1,637),(35706,195635,4,0,637),(35703,195635,4,0,637),(35709,195635,4,0,637),(35744,195635,4,0,637),(35741,195635,4,0,637),(35735,195635,4,0,637),(35732,195635,4,0,637),(35729,195635,4,0,637),(35725,195635,4,0,637),(35722,195635,4,0,637),(35719,195635,4,0,637),(35715,195635,4,0,637),(35712,195635,4,0,637),(35747,195635,4,0,637),(35696,195635,4,0,637),(35675,195635,4,0,637),(35681,195635,4,0,637),(35663,195635,4,0,637),(35669,195635,4,0,637),(35666,195635,4,0,637),(35738,195635,4,0,637),(35672,195635,4,0,637),(35684,195635,4,0,637),(35687,195635,4,0,637),(35690,195635,4,0,637),(35693,195635,4,0,637),(16064,181366,1,0,692),(16065,181366,1,0,692),(16063,181366,1,0,692),(30549,181366,1,1,692),(30602,193426,2,0,692),(30603,193426,2,0,692),(30601,193426,2,0,692),(30600,193426,2,1,692),(36948,202178,1,0,847),(36939,202178,1,0,847),(38157,202180,2,0,847),(38156,202180,2,0,847),(38639,202177,3,0,847),(38637,202177,3,0,847),(38640,202179,4,0,847),(38638,202179,4,0,847),(25740,187892,0,0,0),(12018,179703,0,0,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` +-- + +LOCK TABLES `aowow_profiler_excludes` WRITE; +/*!40000 ALTER TABLE `aowow_profiler_excludes` DISABLE KEYS */; +INSERT INTO `aowow_profiler_excludes` VALUES (6,459,1,'Gray Wolf'),(6,468,1,'White Stallion'),(6,471,1,'Palamino'),(6,472,1,'Pinto'),(6,578,1,'Black Wolf'),(6,579,1,'Red Wolf'),(6,581,1,'Winter Wolf'),(6,3363,1,'Nether Drake'),(6,6896,1,'Black Ram'),(6,6897,1,'Blue Ram'),(6,8980,1,'Skeletal Horse'),(6,10681,1,'Summon Cockatoo'),(6,10686,1,'Summon Prairie Chicken'),(6,10687,1,'Summon White Plymouth Rock'),(6,10699,1,'Summon Bronze Whelpling'),(6,10700,1,'Summon Faeling'),(6,10701,1,'Summon Dart Frog'),(6,10702,1,'Summon Island Frog'),(6,10705,1,'Summon Eagle Owl'),(6,10708,1,'Summon Snowy Owl'),(6,10710,1,'Summon Cottontail Rabbit'),(6,10712,1,'Summon Spotted Rabbit'),(6,10715,1,'Summon Blue Racer'),(6,10718,1,'Green Water Snake'),(6,10719,1,'Ribbon Snake'),(6,10720,1,'Scarlet Snake'),(6,10721,1,'Summon Elven Wisp'),(6,10795,1,'Ivory Raptor'),(6,10798,1,'Obsidian Raptor'),(6,15648,1,'Corrupted Kitten'),(6,15779,1,'White Mechanostrider Mod B'),(6,15780,1,'Green Mechanostrider'),(6,15781,1,'Steel Mechanostrider'),(6,16055,1,'Black Nightsaber'),(6,16056,1,'Ancient Frostsaber'),(6,16058,1,'Primal Leopard'),(6,16059,1,'Tawny Sabercat'),(6,16060,1,'Golden Sabercat'),(6,16080,1,'Red Wolf'),(6,16081,1,'Winter Wolf'),(6,16082,1,'Palomino'),(6,16083,1,'White Stallion'),(6,16084,1,'Mottled Red Raptor'),(6,17450,1,'Ivory Raptor'),(6,17455,1,'Purple Mechanostrider'),(6,17456,1,'Red and Blue Mechanostrider'),(6,17458,1,'Fluorescent Green Mechanostrider'),(6,17459,1,'Icy Blue Mechanostrider Mod A'),(6,17460,1,'Frost Ram'),(6,17461,1,'Black Ram'),(6,17468,1,'Pet Fish'),(6,17469,1,'Pet Stone'),(6,18363,1,'Riding Kodo'),(6,18991,1,'Green Kodo'),(6,18992,1,'Teal Kodo'),(6,19363,1,'Summon Mechanical Yeti'),(6,23220,1,'Swift Dawnsaber'),(6,23428,1,'Albino Snapjaw'),(6,23429,1,'Loggerhead Snapjaw'),(6,23430,1,'Olive Snapjaw'),(6,23431,1,'Leatherback Snapjaw'),(6,23432,1,'Hawksbill Snapjaw'),(6,23530,16,'Tiny Red Dragon - wrong region'),(6,23531,16,'Tiny Green Dragon - wrong region'),(6,24985,1,'Summon Baby Murloc (Blue)'),(6,24986,1,'Summon Baby Murloc (Green)'),(6,24987,1,'Summon Baby Murloc (Orange)'),(6,24988,4,'Lurky - CE'),(6,24989,1,'Summon Baby Murloc (Pink)'),(6,24990,1,'Summon Baby Murloc (Purple)'),(6,25849,1,'Baby Shark'),(6,26067,1,'Summon Mechanical Greench'),(6,26391,1,'Tentacle Call'),(6,28828,1,'Nether Drake'),(6,29059,1,'Naxxramas Deathcharger'),(6,30152,1,'White Tiger Cub'),(6,30156,2,'Hippogryph Hatchling - TCG loot'),(6,30174,2,'Riding Turtle - TCG loot'),(6,32298,4,'Netherwhelp - CE'),(6,32345,1,'Peep the Phoenix Mount'),(6,33050,128,'Magical Crawdad'),(6,33057,1,'Summon Mighty Mr. Pinchy'),(6,33630,1,'Blue Mechanostrider'),(6,34407,1,'Great Elite Elekk'),(6,35157,1,'Summon Spotted Rabbit'),(6,37015,1,'Swift Nether Drake'),(6,40319,16,'Lucky - wrong region'),(6,40405,16,'Lucky - wrong region'),(6,43688,1,'Amani War Bear'),(6,43810,1,'Frost Wyrm'),(6,44317,1,'Merciless Nether Drake'),(6,44744,1,'Merciless Nether Drake'),(6,45125,2,'Rocket Chicken - TCG loot'),(6,45174,16,'Golden Pig - wrong region'),(6,45175,16,'Silver Pig - wrong region'),(6,45890,1,'Scorchling'),(6,47037,1,'Swift War Elekk'),(6,48406,16,'Essence of Competition - wrong region'),(6,48408,16,'Essence of Competition - wrong region'),(6,48954,8,'Swift Zhevra - promotion'),(6,49322,8,'Swift Zhevra - promotion'),(6,49378,1,'Brewfest Riding Kodo'),(6,50869,1,'Brewfest Kodo'),(6,50870,1,'Brewfest Ram'),(6,51851,1,'Vampiric Batling'),(6,51960,1,'Frost Wyrm Mount'),(6,52615,4,'Frosty - CE'),(6,53082,8,'Mini Tyrael - promotion'),(6,53768,1,'Haunted'),(6,54187,1,'Clockwork Rocket Bot'),(6,55068,1,'Mr. Chilly'),(6,58983,8,'Big Blizzard Bear - promotion'),(6,59572,1,'Black Polar Bear'),(6,59573,1,'Brown Polar Bear'),(6,59802,1,'Grand Ice Mammoth'),(6,59804,1,'Grand Ice Mammoth'),(6,59976,1,'Black Proto-Drake'),(6,60021,1,'Plagued Proto-Drake'),(6,60136,1,'Grand Caravan Mammoth'),(6,60140,1,'Grand Caravan Mammoth'),(6,61442,1,'Swift Mooncloth Carpet'),(6,61444,1,'Swift Shadoweave Carpet'),(6,61446,1,'Swift Spellfire Carpete'),(6,61855,1,'Baby Blizzard Bear'),(6,62048,1,'Black Dragonhawk Mount'),(6,62514,1,'Alarming Clockbot'),(6,63318,8,'Murkimus the Gladiator'),(6,64351,1,'XS-001 Constructor Bot'),(6,64656,1,'Blue Skeletal Warhorse'),(6,64731,128,'Sea Turtle - fishing only'),(6,65682,1,'Warbot'),(6,65917,2,'Magic Rooster - TCG loot'),(6,66030,8,'Grunty - promotion'),(6,66520,1,'Jade Tiger'),(6,66907,1,'Argent Warhorse'),(6,67527,16,'Onyx Panther - wrong region'),(6,68767,2,'Tuskarr Kite - TCG loot'),(6,68810,2,'Spectral Tiger Cub - TCG loot'),(6,69002,1,'Onyxian Whelpling'),(6,69452,8,'Core Hound Pup - promotion'),(6,69535,4,'Gryphon Hatchling - CE'),(6,69536,4,'Wind Rider Cub - CE'),(6,69539,1,'Zipao Tiger'),(6,69541,4,'Pandaren Monk - CE'),(6,69677,4,'Lil\' K.T. - CE'),(6,74856,2,'Blazing Hippogryph - TCG loot'),(6,74918,2,'Wooly White Rhino - TCG loot'),(6,75613,1,'Celestial Dragon'),(6,75614,1,'Celestial Steed - unavailable'),(6,75906,4,'Lil\' XT - CE'),(6,75936,1,'Murkimus the Gladiator'),(6,75973,8,'X-53 Touring Rocket - promotion'),(6,78381,8,'Mini Thor - promotion'),(8,87,1024,'Bloodsail Buccaneers - max rank is honored'),(8,92,1024,'Gelkis Clan Centaur - max rank is friendly'),(8,93,1024,'Magram Clan Centaur - max rank is friendly'),(6,46197,2,'X-51 Nether-Rocket - TCG loot'),(6,46199,2,'X-51 Nether-Rocket X-TREME - TCG loot'),(6,26656,1,'Black Qiraji Battle Tank - unavailable'),(6,43899,1,'Brewfest Ram - unavailable'),(6,49193,1,'Vengeful Nether Drake - unavailable'),(6,58615,1,'Brutal Nether Drake - unavailable'),(6,64927,1,'Deadly Gladiator\'s Frost Wyrm - unavailable'),(6,65439,1,'Furious Gladiator\'s Frost Wyrm - unavailable'),(6,67336,1,'Relentless Gladiator\'s Frost Wyrm - unavailable'),(6,71810,1,'Wrathful Gladiator\'s Frost Wyrm - unavailable'),(11,122,1,'RealmFirst Kel\'T Title - unavailable'),(11,159,1,'RealmFirst Algalon Title - unavailable'),(11,120,1,'RealmFirst Maly Title - unavailable'),(11,170,1,'RealmFirst TotGC Title - unavailable'),(11,139,1,'RealmFirst Sarth Title - unavailable'),(11,158,1,'RealmFirst Yogg Title - unavailable'),(6,28505,8,'Poley - promotion'),(6,28487,1,'Terky - unavailable'),(8,70,1024,'Syndicate - max rank is neutral'),(6,28242,1,'Icebane Breastplate'),(6,28243,1,'Icebane Gauntlets'),(6,28244,1,'Icebane Bracers'),(6,16986,1,'Blood Talon'),(6,16987,1,'Darkspear'),(6,16965,1,'Bleakwood Hew'),(6,8366,1,'Ironforge Chain'),(6,8368,1,'Ironforge Gauntlets'),(6,9942,1,'Mithril Scale Gloves'),(6,2671,1,'Rough Bronze Bracers'),(6,16980,1,'Rune Edge'),(6,16960,1,'Thorium Greatsword'),(6,16967,1,'Inlaid Thorium Hammer'),(6,30342,1,'Red Smoke Flare'),(6,30343,1,'Blue Smoke Flare'),(6,28205,1,'Glacial Gloves'),(6,28207,1,'Glacial Vest'),(6,28208,1,'Glacial Cloak'),(6,28209,1,'Glacial Wrists'),(6,28222,1,'Icy Scale Breastplate'),(6,28223,1,'Icy Scale Gauntlets'),(6,28224,1,'Icy Scale Bracers'),(6,28219,1,'Polar Tunic'),(6,28220,1,'Polar Gloves'),(6,28221,1,'Polar Bracers'),(6,28021,1,'Arcane Dust'),(6,44612,1,'Enchant Gloves - Greater Blasting'),(6,62257,1,'Enchant Weapon - Titanguard'),(6,31461,1,'Heavy Netherweave Net'),(6,56048,1,'Duskweave Boots'),(6,7636,1,'Green Woolen Robe'),(6,8778,1,'Boots of Darkness'),(6,12062,1,'Stormcloth Pants'),(6,12063,1,'Stormcloth Gloves'),(6,12068,1,'Stormcloth Vest'),(6,12083,1,'Stormcloth Headband'),(6,12087,1,'Stormcloth Shoulders'),(6,12090,1,'Stormcloth Boots'); +/*!40000 ALTER TABLE `aowow_profiler_excludes` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Dumping data for table `aowow_quickfacts` +-- + +LOCK TABLES `aowow_quickfacts` WRITE; +/*!40000 ALTER TABLE `aowow_quickfacts` DISABLE KEYS */; +INSERT INTO `aowow_quickfacts` VALUES (7,1,1,'|L:zone:city||L:main:colon|[zone=1537]'),(7,12,1,'|L:zone:city||L:main:colon|[zone=1519]'),(7,14,1,'|L:zone:city||L:main:colon|[zone=1637]'),(7,65,1,'|L:zone:reputationHub|[faction=1091]'),(7,67,1,'|L:zone:reputationHub|[faction=1119]'),(7,85,1,'|L:zone:city||L:main:colon|[zone=1497]'),(7,139,1,'|L:zone:reputationHub|[faction=529]'),(7,141,1,'|L:zone:city||L:main:colon|[zone=1657]'),(7,206,1,'|L:zone:boss|[icon preset=boss][npc=23954][/icon]'),(7,209,1,'|L:zone:boss|[icon preset=boss][npc=4275][/icon]'),(7,210,1,'|L:zone:reputationHub|[faction=1106]\n[faction=1098]'),(7,215,1,'|L:zone:city||L:main:colon|[zone=1638]'),(7,361,1,'|L:zone:reputationHub|[faction=576]'),(7,405,1,'|L:zone:reputationHub|[faction=92]\n[faction=93]'),(7,440,1,'|L:zone:reputationHub|[faction=989]'),(7,491,1,'|L:zone:boss|[icon preset=boss][npc=4421][/icon]'),(7,493,1,'|L:game:class||L:main:colon|[class=11]'),(7,618,1,'|L:zone:reputationHub|[faction=589]'),(7,717,1,'|L:zone:boss|[icon preset=boss][npc=1716][/icon]'),(7,718,1,'|L:zone:boss|[icon preset=boss][npc=5775][/icon]'),(7,719,1,'|L:zone:boss|[icon preset=boss][npc=4829][/icon]'),(7,721,1,'|L:zone:boss|[icon preset=boss][npc=7800][/icon]'),(7,722,1,'|L:zone:boss|[icon preset=boss][npc=7358][/icon]'),(7,796,1,'|L:zone:key:0|[item=7146]'),(7,796,2,'|L:zone:boss|[icon preset=boss][npc=3976][/icon]'),(7,1176,1,'|L:zone:boss|[icon preset=boss][npc=7267][/icon]'),(7,1196,1,'|L:zone:boss|[icon preset=boss][npc=26861][/icon]'),(7,1337,1,'|L:zone:boss|[icon preset=boss][npc=2748][/icon]'),(7,1377,1,'|L:zone:reputationHub|[faction=609]'),(7,1477,1,'|L:zone:boss|[icon preset=boss][npc=5709][/icon]'),(7,1497,1,'|L:zone:location|[zone=85]'),(7,1497,2,'|L:zone:reputationHub|[faction=68]'),(7,1519,1,'|L:zone:location|[zone=12]'),(7,1519,2,'|L:zone:reputationHub|[faction=72]'),(7,1537,1,'|L:zone:location|[zone=1]'),(7,1537,2,'|L:zone:reputationHub|[faction=47]\n[faction=54]'),(7,1581,1,'|L:zone:boss|[icon preset=boss][npc=639][/icon]'),(7,1583,1,'|L:zone:boss|[icon preset=boss][npc=10363][/icon]'),(7,1584,1,'|L:zone:boss|[icon preset=boss][npc=9019][/icon]'),(7,1637,1,'|L:zone:location|[zone=14]'),(7,1637,2,'|L:zone:reputationHub|[faction=76]'),(7,1638,1,'|L:zone:location|[zone=215]'),(7,1638,2,'|L:zone:reputationHub|[faction=81]'),(7,1657,1,'|L:zone:location|[zone=141]'),(7,1657,2,'|L:zone:reputationHub|[faction=69]'),(7,1977,1,'|L:zone:raidFaction|[faction=270]'),(7,1977,2,'|L:zone:boss|[icon preset=boss][npc=14834][/icon]'),(7,2017,1,'|L:zone:boss|[icon preset=boss][npc=10440][/icon]'),(7,2057,1,'|L:zone:key:0|[item=13704]'),(7,2057,2,'|L:zone:boss|[icon preset=boss][npc=1853][/icon]'),(7,2100,1,'|L:zone:boss|[icon preset=boss][npc=12201][/icon]'),(7,2366,1,'|L:zone:faction|[faction=989]'),(7,2366,2,'|L:zone:boss|[icon preset=boss][npc=17881][/icon]'),(7,2367,1,'|L:zone:faction|[faction=989]'),(7,2367,2,'|L:zone:boss|[icon preset=boss][npc=18096][/icon]'),(7,2437,1,'|L:zone:boss|[icon preset=boss][npc=11520][/icon]'),(7,2677,1,'|L:zone:attunement:0|[quest=7761]'),(7,2677,2,'|L:zone:boss|[icon preset=boss][npc=11583][/icon]'),(7,2717,1,'|L:zone:attunement:0|[quest=7487]'),(7,2717,2,'|L:zone:raidFaction|[faction=749]'),(7,2717,3,'|L:zone:boss|[icon preset=boss][npc=11502][/icon]'),(7,3428,1,'|L:zone:raidFaction|[faction=910]'),(7,3428,2,'|L:zone:boss|[icon preset=boss][npc=15727][/icon]'),(7,3429,1,'|L:zone:raidFaction|[faction=609]'),(7,3429,2,'|L:zone:boss|[icon preset=boss][npc=15339][/icon]'),(7,3430,1,'|L:zone:city||L:main:colon|[zone=3487]'),(7,3433,1,'|L:zone:reputationHub|[faction=922]'),(7,3457,1,'|L:zone:attunement:0|[quest=9837]'),(7,3457,2,'|L:zone:key:0|[item=24490]'),(7,3457,3,'|L:zone:raidFaction|[faction=967]'),(7,3457,4,'|L:zone:boss|[icon preset=boss][npc=15690][/icon]'),(7,3483,1,'|L:zone:reputationHub|[icon name=side_alliance][faction=946][/icon]\n[icon name=side_horde][faction=947][/icon]'),(7,3487,1,'|L:zone:location|[zone=3430]'),(7,3487,2,'|L:zone:reputationHub|[faction=911]'),(7,3518,1,'|L:zone:reputationHub|[icon name=side_alliance][faction=978][/icon]\n[icon name=side_horde][faction=941][/icon]'),(7,3519,1,'|L:zone:reputationHub|[faction=1031]'),(7,3519,2,'|L:zone:city||L:main:colon|[zone=3703]'),(7,3520,1,'|L:zone:reputationHub|[faction=1015]'),(7,3521,1,'|L:zone:reputationHub|[faction=942]\n[faction=970]'),(7,3522,1,'|L:zone:reputationHub|[faction=1038]'),(7,3523,1,'|L:zone:reputationHub|[faction=933]'),(7,3557,1,'|L:zone:location|[zone=3524]'),(7,3557,2,'|L:zone:reputationHub|[faction=930]'),(7,3562,1,'|L:zone:faction|[icon name=side_alliance][faction=946][/icon] / [icon name=side_horde][faction=947][/icon]'),(7,3562,2,'|L:zone:boss|[icon preset=boss][npc=17536][/icon]'),(7,3606,1,'|L:zone:raidFaction|[faction=990]'),(7,3606,2,'|L:zone:boss|[icon preset=boss][npc=17968][/icon]'),(7,3607,1,'|L:zone:boss|[icon preset=boss][npc=21212][/icon]'),(7,3703,1,'|L:zone:location|[zone=3519]'),(7,3703,2,'|L:zone:reputationHub|[faction=932]\n[faction=934]\n[faction=1011]'),(7,3711,1,'|L:zone:reputationHub|[faction=1105]\n[faction=1104]'),(7,3713,1,'|L:zone:faction|[icon name=side_alliance][faction=946][/icon] / [icon name=side_horde][faction=947][/icon]'),(7,3713,2,'|L:zone:boss|[icon preset=boss][npc=17377][/icon]'),(7,3714,1,'|L:zone:key:0|[item=28395]'),(7,3714,2,'|L:zone:faction|[icon name=side_alliance][faction=946][/icon] / [icon name=side_horde][faction=947][/icon]'),(7,3714,3,'|L:zone:boss|[icon preset=boss][npc=16808][/icon]'),(7,3715,1,'|L:zone:faction|[faction=942]'),(7,3715,2,'|L:zone:boss|[icon preset=boss][npc=17798][/icon]'),(7,3716,1,'|L:zone:faction|[faction=942]'),(7,3716,2,'|L:zone:boss|[icon preset=boss][npc=17882][/icon]'),(7,3717,1,'|L:zone:faction|[faction=942]'),(7,3717,2,'|L:zone:boss|[icon preset=boss][npc=17942][/icon]'),(7,3789,1,'|L:zone:key:0|[item=27991]'),(7,3789,2,'|L:zone:faction|[faction=1011]'),(7,3789,3,'|L:zone:boss|[icon preset=boss][npc=18708][/icon]'),(7,3790,1,'|L:zone:faction|[faction=1011]'),(7,3790,2,'|L:zone:boss|[icon preset=boss][npc=18373][/icon]'),(7,3791,1,'|L:zone:faction|[faction=1011]'),(7,3791,2,'|L:zone:boss|[icon preset=boss][npc=18473][/icon]'),(7,3792,1,'|L:zone:faction|[faction=933]'),(7,3792,2,'|L:zone:boss|[icon preset=boss][npc=18344][/icon]'),(7,3805,1,'|L:zone:boss|[icon preset=boss][npc=23863][/icon]'),(7,3836,1,'|L:zone:boss|[icon preset=boss][npc=17257][/icon]'),(7,3845,1,'|L:zone:boss|[icon preset=boss][npc=19622][/icon]'),(7,3847,1,'|L:zone:faction|[faction=935]'),(7,3847,2,'|L:zone:boss|[icon preset=boss][npc=17977][/icon]'),(7,3848,1,'|L:zone:key:0|[item=31084]'),(7,3848,2,'|L:zone:faction|[faction=935]'),(7,3848,3,'|L:zone:boss|[icon preset=boss][npc=20912][/icon]'),(7,3849,1,'|L:zone:faction|[faction=935]'),(7,3849,2,'|L:zone:boss|[icon preset=boss][npc=19220][/icon]'),(7,3923,1,'|L:zone:boss|[icon preset=boss][npc=19044][/icon]'),(7,3959,1,'|L:zone:raidFaction|[faction=1012]'),(7,3959,2,'|L:zone:boss|[icon preset=boss][npc=22917][/icon]'),(7,4075,1,'|L:zone:boss|[icon preset=boss][npc=25315][/icon]'),(7,4080,1,'|L:zone:reputationHub|[faction=1077]'),(7,4100,1,'|L:zone:boss|[icon preset=boss][npc=26533][/icon]'),(7,4131,1,'|L:zone:faction|[faction=1077]'),(7,4131,2,'|L:zone:boss|[icon preset=boss][npc=24664][/icon]'),(7,4196,1,'|L:zone:boss|[icon preset=boss][npc=26632][/icon]'),(7,4228,1,'|L:zone:boss|[icon preset=boss][npc=27656][/icon]'),(7,4264,1,'|L:zone:boss|[icon preset=boss][npc=27978][/icon]'),(7,4265,1,'|L:zone:boss|[icon preset=boss][npc=26723][/icon]'),(7,4272,1,'|L:zone:boss|[icon preset=boss][npc=28923][/icon]'),(7,4277,1,'|L:zone:boss|[icon preset=boss][npc=29120][/icon]'),(7,4395,1,'|L:zone:location|[zone=2817]'),(7,4395,2,'|L:zone:reputationHub|[faction=1090]'),(7,4415,1,'|L:zone:boss|[icon preset=boss][npc=31134][/icon]'),(7,4416,1,'|L:zone:boss|[icon preset=boss][npc=29306][/icon]'),(7,4494,1,'|L:zone:boss|[icon preset=boss][npc=29311][/icon]'),(7,4723,1,'|L:zone:boss|[icon preset=boss][npc=35451][/icon]'),(7,4809,1,'|L:zone:boss|[icon preset=boss][npc=36502][/icon]'),(7,4813,1,'|L:zone:boss|[icon preset=boss][npc=36658][/icon]'),(7,4820,1,'|L:zone:boss|[icon preset=boss][npc=36954][/icon]'); +/*!40000 ALTER TABLE `aowow_quickfacts` ENABLE KEYS */; +UNLOCK TABLES; +/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */; + +-- +-- Dumping data for table `aowow_setup_custom_data` +-- + +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',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 */; + +/*!40101 SET SQL_MODE=@OLD_SQL_MODE */; +/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */; +/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */; +/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */; +/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */; +/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */; + +-- Dump completed on 2025-09-22 23:29:16 diff --git a/setup/sql/03-db_initial_articles.sql b/setup/sql/03-db_initial_articles.sql new file mode 100644 index 00000000..5450d9e3 --- /dev/null +++ b/setup/sql/03-db_initial_articles.sql @@ -0,0 +1,35 @@ +-- MariaDB dump 10.19 Distrib 10.4.32-MariaDB, for Win64 (AMD64) +-- +-- Host: localhost Database: aowow +-- ------------------------------------------------------ +-- Server version 10.4.32-MariaDB + +/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */; +/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */; +/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */; +/*!40101 SET NAMES utf8mb4 */; +/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */; +/*!40103 SET TIME_ZONE='+00:00' */; +/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */; +/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */; +/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */; +/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */; + +-- +-- Dumping data for table `aowow_articles` +-- + +LOCK TABLES `aowow_articles` WRITE; +/*!40000 ALTER TABLE `aowow_articles` DISABLE KEYS */; +INSERT INTO `aowow_articles` VALUES (13,4,0,NULL,0,2,'[b][color=c4]Rogues[/color][/b] are a leather-clad melee class capable of dealing large amounts of damage to their enemies with very fast attacks. They are masters of stealth and assassination, passing by enemies unseen and striking from the shadows, then escaping from combat in the blink of an eye.\r\n\r\nThey are capable of using poisons to cripple their opponents, massively weakening them in battle. Rogues have a powerful arsenal of skills, many of which are strengthened by their ability to stealth and to incapacitate their victims.\r\n[ul]\r\n[li]Rogues can use a wide variety of melee weapons, such as daggers, fist weapons, one-handed maces, one-handed swords and one-handed axes.[/li]\r\n[li]By coating their weapons with [url=items=0.-3&filter=na=poison;ub=4]poison[/url] rogues can severely cripple or weaken their enemies.[/li]\r\n[li]When using [spell=1784] rogues will be unseen except by the most perceptive enemies.[/li]\r\n[/ul]'),(14,1,0,NULL,0,2,'[b]Overview:[/b] The [b]humans[/b] are the most populous and the youngest race in Azeroth. The humans have become the [i]de facto[/i] leaders of the Alliance, with their youthful ambitions and resilience.\n\n[b]Capital City:[/b] The human seat of power is in the rebuilt city of [zone=1519].\n\n[b]Starting Zone:[/b] Humans begin questing in [zone=12].\n\n[b]Mounts:[/b] [npc=384] sells armoried ponies in Stormwind, and [npc=33307] at the Argent Tournament has a few distinct models.'),(13,1,0,NULL,0,2,'[b][color=c1]Warriors[/color][/b] are a very powerful class, with the ability to tank or deal significant melee damage. The warrior\'s Protection tree contains many talents to improve their survivability and generate threat versus monsters. Protection warriors are one of the main tanking classes of the game.\n\nThey also have two damage-oriented talent trees - [icon name=ability_rogue_eviscerate][url=spells=7.1.26]Arms[/url][/icon] and [icon name=ability_warrior_innerrage][url=spells=7.1.256]Fury[/url][/icon], the latter of which includes the talent [spell=46917], which allows the warrior to wield two two-handed weapons at the same time! They are capable of strong melee AoE damage with spells such as [spell=845], [spell=1680], [spell=46924]. A warrior fights while in a specific [i]stance[/i], which grants him bonuses and access to different sets of abilities. He will use [spell=71] for tanking, and [spell=2457] or [spell=2458] for melee DPS.\n\n[ul]\n[li]All warriors can buff their raid or group by using a [i]shout[/i], [spell=6673] or [spell=469], and Fury warriors can provide the passive buff [spell=29801] which significantly increases the melee and ranged critical strike chance of his allies.[/li]\n[li]Warriors start out with only [spell=2457] at first, but learn [spell=71] at level 10 and [spell=2458] at level 30.[/li]\n[li]Warriors have numerous useful methods of getting to their target in a hurry! All warriors can use [spell=100] or [spell=20252] to reach an enemy and Protection warriors have [spell=3411], which allows them to intercept a friendly target and protect them from an attack.[/li]\n[/ul]'),(13,2,0,NULL,0,2,'[b][color=c2]Paladins[/color][/b] bolster their allies with holy auras and blessing to protect their friends from harm and enhance their powers. Wearing heavy armor, they can withstand terrible blows in the thickest battles while healing their wounded allies and resurrecting the slain. In combat, they can wield massive two-handed weapons, stun their foes, destroy undead and demons, and judge their enemies with holy vengeance. Paladins are a defensive class, primarily designed to outlast their opponents.\n\nThe paladin is a mix of a melee fighter and a secondary spell caster. The paladin has a great deal of group utility due to the paladin\'s healing, blessings, and other abilities. Paladins can have one active aura per paladin on each party member and use specific blessings for specific players. Paladins are pretty hard to kill, thanks to their assortment of defensive abilities. They also make excellent tanks using their [spell=25780] ability.\n\n[ul]\n[li]Can effectively heal, tank, and deal damage in melee.[/li]\n[li]Has a wide selection of [url=spells=7.2&filter=na=blessing]Blessings[/url], [url=spells=7.2&filter=na=aura]Auras[/url], and other buffs.[/li]\n[li]Is the only class with access to a true invulnerability spell: [spell=642][/li]\n[/ul]'),(14,2,0,NULL,0,2,'[b]Overview:[/b] The [b]orcs[/b] were originally a race of noble savages, residing on the world of Draenor. Unfortunately, The Burning Legion made use of them in an attempt to conquer Azeroth—they were infected with the daemonic blood of Mannoroth the Destructor, driven mad, and turned upon both the Draenei and the denizens of Azeroth. After losing the Second War, they were cut off from the corrupting influence of Mannoroth, and began to return to their shamanistic roots. Now, under the leadership of their new Warchief, the orcs are carving out a home for themselves in Azeroth.\n\n[b]Capital City:[/b] The orcs now reside in the city of [zone=1637], named after the deceased Orgrim Doomhammer, former Warchief of the Horde.\n\n[b]Starting Zone:[/b] Orcs begin questing in [zone=14].\n\n[b]Mounts:[/b] [npc=3362] in Orgrimmar sells a variety of wolves; [npc=33553] sells a few distinctive mounts at the Argent Tournament.'),(13,3,0,NULL,0,2,'[b][color=c3]Hunters[/color][/b] are a very unique class in World of Warcraft. They are the sole non-magical ranged damage-dealers, fighting with bows and guns. Hunters have a number of different kinds of shots and stings, which can be used to debuff an enemy, and are capable of laying traps to deal damage or otherwise slow/incapacitate their enemy.\n\nA hunter will also tame his very own [url=pets]pet[/url] to aid them in combat. While they are not the only class which can use pet minions, the hunter\'s pet is unique in that each species has a particular type of talent tree, which the hunter can use to distribute points into various skills and passive abilities.\n\nIn addition, each species has a unique special ability. Hunters can seek out the most desirable pets based on their appearances or abilities, and if they spec deep enough into the [icon name=ability_hunter_beasttaming][url=spells=7.3.50]Beast Mastery[/url][/icon] tree they gain access to special, \"exotic\" beasts such as [pet=46] or [pet=39]!\n\n[ul]\n[li]Hunters have access to 23 (32 if [icon name=ability_hunter_beasttaming][url=spells=7.3.50]Beast Mastery[/url][/icon]) different [url=pets]species of pets[/url], featuring over 150 different appearances![/li]\n[li]Hunters have a number of survival-oriented skills which they can use to escape or avoid potential danger, such as [spell=5384] and [spell=781].[/li]\n[li][icon name=ability_hunter_swiftstrike][url=spells=7.3.51]Survival[/url][/icon] hunters can spec down the tree into [spell=53292], which allows them to provide the [spell=57669] buff to their party and raid members.[/li]\n[/ul]'),(13,5,0,NULL,0,2,'[b][color=c5]Priests[/color][/b] are commonly considered one of the standard healing classes in World of Warcraft, as they have two talent specs that can be used to heal quite effectively.\n\nTheir [icon name=spell_holy_holybolt][url=spells=7.5.56]Holy[/url][/icon] tree includes talents which strongly boost the healing done to their allies, including spells that can be used to heal multiple players at once, such as [spell=48089]. The [icon name=spell_holy_wordfortitude][url=spells=7.5.613]Discipline[/url][/icon] tree, while still capable of significant raw healing output, focuses primarily on damage absorption and mitigation through use of [spell=48066] and procced shielding effects. Priests are also capable of very powerful ranged damage with their unique [icon name=spell_shadow_shadowwordpain][url=spells=7.5.78]Shadow[/url][/icon] abilities, and upon entering [spell=15473] will see a significant increase in their shadow damage while losing the ability to cast any Holy spells.\n\n[ul]\n[li]While the [icon name=spell_holy_wordfortitude][url=spells=7.5.613]Discipline[/url][/icon] talent tree is commonly used for healing, it also contains some powerful talents that can boost the priest\'s Holy damage, though [icon name=spell_shadow_shadowwordpain][url=spells=7.5.78]Shadow[/url][/icon] spells and abilities should be used primarily for DPS.[/li]\n[li]Priests provide of the most appreciated buffs in the game - [spell=48161], which grants an indispensable stamina buff to everyone in the raid. They can also buff both [spell=48073] and [spell=48169]![/li]\n[li]Shadow priests are an excellent utility class for any raid, providing the much-loved [spell=57669] buff to boost mana regeneration and can even heal their own party with [spell=15286]![/li]\n[/ul]'),(13,6,0,NULL,0,2,'Introduced in the Wrath of the Lich King expansion, [b][color=c6]Death Knights[/color][/b] are World of Warcraft\'s first hero class. Death knights start at level 55 in a special, instanced zone unreachable by any other class: Acherus, the Ebon Hold, located in [zone=4298]. Here they will earn their talent points as quest rewards and even get a special summoned mount, the [spell=48778]!\n\nDeath knights have multiple very strong damage dealing options, as each of their talent trees can be specced to perform exceptionally well with a variety of melee abilities, spells and damage-over-time dealing diseases. They are also very capable tank classes, with both their Blood and Frost trees providing unique options - [icon name=spell_deathknight_bloodboil][url=spells=7.6.770]Blood[/url][/icon] dealing more with self-healing abilities and [icon name=spell_frost_frostnova][url=spells=7.6.771]Frost[/url][/icon] providing significant damage mitigation and strong AoE damage.\n\nDeath knights fight with a special buff active called a [i]presence[/i] (similar to a warrior\'s stances) which provides special bonuses to their roles. Death knights utilize a unique power system, with most spells costing either Runes, which are replenished throughout battle, or Runic Power, which can be generated by various abilities.\n\n[ul]\n[li][icon name=spell_deathknight_armyofthedead][url=spells=7.6.772]Unholy[/url][/icon] death knights can spec into [spell=52143], which makes their summoned Ghoul minion a permanent pet to aid in battle![/li]\n[li]The death knight class has its own special weapon enchanting ability called [spell=53428], which replaces the need for conventional weapon enchants.[/li]\n[li]Death knights are a very unique damage-dealing class in that their damage is dealt by both melee abilities [i]and[/i] spells![/li]\n[/ul]'),(13,7,0,NULL,0,2,'[b][color=c7]Shamans[/color][/b] master elemental and nature magics and bring the most potential buffs to any group in the form of totems. A shaman can summon one totem of each element - earth, fire, air, and water - which appears at the shaman\'s feet and provides a buff to anyone in the shaman\'s party or raid within range of it. Some shaman totems, notably the fire ones, also do damage to opponents. The trick to playing any type of shaman is knowing which totems to cast under which circumstances to maximize the group\'s damage output and survivability.\n\nShamans are primarily spellcasters, although an [icon name=spell_nature_lightningshield][url=spells=7.7.373]Enhancement[/url][/icon] shaman likes to get close and personal and do damage within melee range. An enhancement shaman learns to [spell=30798] weapons and can use [spell=51533] to summon a pair of Spirit Wolves to aid in battle. Despite being primarily melee, [icon name=spell_nature_lightningshield][url=spells=7.7.373]Enhancement[/url][/icon] shamans can still gain some benefit from spellpower and can cast instant [spell=403] or heals with [spell=51530]. \n\n[icon name=spell_nature_lightning][url=spells=7.7.375]Elemental[/url][/icon] shamans stand back and cast fire and lightning spells to deal great amounts of damage. They can push back enemies with [spell=51490] and root all enemies in an area with[spell=51486]. They also bring [icon name=spell_fire_totemofwrath][url=spell=57722]Totem of Wrath[/url][/icon] and [spell=51470] as amazing spellcaster raid buffs. A shaman that choses [icon name=spell_nature_magicimmunity][url=spells=7.7.374]Restoration[/url][/icon] gains improved healing spells and can be a great raid or tank healer. Resto shamans are known for their powerful [spell=1064] ability and for providing a [spell=16190] to help their party\'s mana restoration. They also gain a powerful [spell=974], can use [spell=51886] to remove curses, and have an instant-cast direct heal plus heal over time effect called [spell=61295].\n\n[ul]\n[li]There are over twenty different totems a shaman can learn![/li]\n[li]Shamans can cast [spell=2825] (or [spell=32182]) to boost the entire group\'s damage and healing. This buff is unique and oft sought after for a raid group.[/li]\n[li]A shaman can turn into a [spell=2645] at level 16 and can even make it instant cast with [spell=16287]. This spell can be used in combat, but not indoors.[/li]\n[li]Shamans can only have one elemental shield - [spell=324] or [spell=52127] - on at a time. [spell=974], if the shaman knows it, can be cast on another player.[/li]\n[/ul]'),(13,8,0,NULL,0,2,'[b][color=c8]Mages[/color][/b] wield the elements of fire, frost, and arcane to destroy or neutralize their enemies. They are a robed class that excels at dealing massive damage from afar, casting elemental bolts at a single target, or raining destruction down upon their enemies in a wide area of effect. Mages can also augment their allies\' spell-casting powers, summon food or drink to restore their friends, and even travel across the world in an instant by opening arcane portals to distant lands.\n\nWhen seeking someone to introduce monsters to a world of pain, the Mage is a good choice. With their elemental and arcane attacks, it\'s a safe bet something they can do won\'t be resisted by your chosen enemy. Damage is the name of the Mage game, and they do it well. Their arsenal includes some powerful buffs, debuffs, stuns, and snares, enabling them to dictate the terms of any fight.\n\n[ul]\n[li]Can [spell=42956] to restore their allies\' health and mana.[/li]\n[li]Are the only class that can create portals to transport other players. They cannot, however, summon players [i]from[/i] a distant location - that\'s a [icon name=class_warlock][color=c9]Warlock\'s[/color][/icon] job![/li]\n[li]Mages who use [item=50045] can have a permanent water elemental pet![/li]\n[/ul]'),(13,9,0,NULL,0,2,'[b][color=c9]Warlocks[/color][/b] are masters of the demonic arts. Clothed in demonic styled cloth, they excel in using curses, firing bolts of fire or shadow, and summoning demons to help them in combat. Warlocks, while being excellent spell casters, also excel in supporting fellow allies by summoning other players or using ritual magics to conjure stones imbued with the power to heal.\r\n\r\nA warlock has very powerful abilities that, if used correctly, make them a very formidable opponent. Using their curses in combination with direct damage spells, Warlocks wreak havoc and destruction.\r\n\r\n[ul]\r\n[li]Can use a [spell=698] to summon another player to the portals location.[/li]\r\n[li]Are able to conjure [icon name=inv_stone_04][url=item=5509]Healthstones[/url][/icon] that have the ability to heal the user.[/li]\r\n[li]Can use curses on enemies to [url=spell=47865]weaken[/url] them or [url=spell=47864]damage[/url] them.[/li]\r\n[/ul]'),(13,11,0,NULL,0,2,'[b][color=c11]Druids[/color][/b] are World of Warcraft\'s \"jack of all trades\" class -- that is, capable of performing in a variety of different roles and as such have one of the most varied playstyles. A druid can act as a healer, melee DPS, ranged DPS or a tank, utilizing a variety of [i]shapeshifting[/i] forms. As a druid levels up, he is able to learn new, powerful forms which he can cast to change into different creatures to suit their roles.\n\nAt lower levels, a druid will heal or ranged DPS in his caster form, but at later levels players who spec into the specialized trees will gain access to two special shapeshift forms for each different role.\n\nHealing druids will learn [spell=33891], which reduces the mana cost of their healing spells and grants a passive healing aura to their allies. Their ranged damage-dealing counterparts will learn [spell=24858], increasing their armor and granting a spell critical aura to their allies. There are also two feral form druid forms -- the mighty [spell=5487] (and at later level, [spell=9634]), a tanking-oriented form which provides additional armor and health and grants access to an arsenal of threat-building and damage mitigation abilities, and the rogue-like [spell=768] which is capable of significant melee DPS.\n\n[ul]\n[li]Druids learn their different forms through questing or training. Some shapeshifts are only learned via talents.[/li]\n[li]There are some shapeshifts that all druids can learn. [spell=5487] is obtained at level 10, [spell=1066] and [spell=783] at level 16, [spell=768] at level 20 and [spell=9634] at level 40.[/li]\n[li]Druids even have their own flying travel form! [spell=33943] can be trained at level 60, and [spell=40120] at level 71 provided the player has already trained [spell=34091].[/li]\n[li]Some druid shapeshifts are obtained via talents only - [spell=24858] can be obtained at level 40 when a player specs deep into the [icon name=spell_nature_starfall][url=spells=7.11.574]Balance[/url][/icon] tree, and [spell=33891] at level 50 after speccing deep into [icon name=spell_nature_healingtouch][url=spells=7.11.573]Restoration[/url][/icon].[/li]\n[li]Druids have their own, class-specific teleport ability that allows them to travel to and from [zone=493], which is handy when needing to train![/li]\n[li]Because feral druids do not actually swing weapons while in shapeshift forms, they instead gain a special statistic from any melee weapon they equip called \"feral attack power.\" This stat is a conversion of a weapon\'s DPS (damage per second) into an attack power-granting statistic which affects the cat or bear\'s damage output.[/li]\n[/ul]'),(14,3,0,NULL,0,2,'[b]Overview:[/b] The [b]dwarves[/b] are a hardy race, hailing from Khaz Modan in the Eastern Kingdoms. Rumor has it they are descended from the Titans. There are three main clans of dwarves vying for power in Ironforge: the Bronzebeards, Wildhammers, and Dark Irons.\n\n[b]Capital City:[/b] The dwarves make their home in their ancestral seat of [zone=1537].\n\n[b]Starting Zone:[/b] Dwarves begin in [zone=1].\n\n[b]Mounts:[/b] [npc=1261] by the Amberstill Ranch sells rams, as well as [npc=33310] at the Argent Tournament.'),(14,4,0,NULL,0,2,'[b]Overview:[/b] The [b]night elves[/b] are an ancient and mysterious race. They lived in Kalimdor for thousands of years, undisturbed until the world tree was sacrificed to halt the advance of the Burning Legion prior to the events of World of Warcraft.\n\n[b]Capital City:[/b] The night elf capital city is [zone=1657], situated in the branches of the world tree itself.\n\n[b]Starting Zone:[/b] Night Elves begin in [zone=141], learning about the recent political changes in Darnassus.\n\n[b]Mounts:[/b] [npc=4730] in Darnassus sells a variety of nightsabers, as well as [npc=33653] at the Argent Tournament.'),(14,5,0,NULL,0,2,'[b]Overview:[/b] When the [b]undead[/b] scourge initially swept across Azeroth, they converted a number of members of the Alliance to the undead. When the combined forces of the orcs, elves, trolls, dwarves and humans began to fight back, though, [npc=36597]\'s hold on his forces began to weaken. A small faction of humans, known as the Forsaken, broke free of the Lich King\'s control.\n\nNow, free of the bonds of servitude as well as the troublesome emotions and connections of their human lives, the Forsaken have found a new home—with the Horde.\n\n[b]Capital City:[/b] The Forsaken reside in the [zone=1497], underneath the ruins of the former human city of Lordaeron.\n\n[b]Starting Zone:[/b] [zone=85] is the starting zone for Forsaken players--they are raised as second-generation Forsaken by val\'kyr and experience Sylvanas\' menacing new agenda firsthand.\n\n[b]Mounts:[/b] [npc=4731] in Tirisfal Glades sells numerous undead horses; [npc=33555] at the Argent Tournament sells a few distinct models.'),(14,6,0,NULL,0,2,'[b]Overview:[/b] The [b]tauren[/b], a race with deep shamanistic roots, are longtime residents of Kalimdor. They have a deep and abiding love of nature, and the vast majority of them worship a deity known as the Earth Mother. \n\n[b]Capital City:[/b] The tauren reside in [zone=1638].\n\n[b]Starting Zone:[/b] Tauren begin questing in [zone=215].\n\n[b]Mounts:[/b] [npc=3685] sells numerous kodo mounts; [npc=33556] at the Argent Tournament sells a few distinctive models.'),(14,7,0,NULL,0,2,'[b]Overview:[/b] The [b]gnomes[/b] are a quirky race, obsessed with gadgets and technology. They originally come from the city of [zone=721], which was destroyed by [npc=7937] in an attempt to save it from an invading army of troggs.\n\n[b]Capital City:[/b] The gnomes now make their home in [zone=1537]; they have made efforts to retake their beloved former city with [achievement=4786].\n\n[b]Starting Zone:[/b] Gnomes begin in [zone=1], but they have a very different quest sequence from Dwarves, covering Gnomeregan.\n\n[b]Mounts:[/b] [npc=7955] in Dun Morogh sells numerous mechanostriders, as well as [npc=33650] at the Argent Tournament.'),(14,8,0,NULL,0,2,'[b]Overview:[/b] While there are many different tribes of [b]trolls[/b] scattered across Azeroth, only the [url=?faction=530]Darkspear Tribe[/url] has ever sworn allegiance to the Horde. The trolls originally lived in the Broken Isles, but were overrun by naga and murlocs and driven from their home. The orcs, led by [npc=4949], saved the Darkspear tribe from certain destruction and offered them amnesty among the Horde. In return, the Darkspear tribe swore fealty to the orcish warchief.\n\n[b]Capital City:[/b] The Darkspear Trolls live now in the Horde capital of [zone=1637].\n\n[b]Starting Zone:[/b] Trolls begin questing in [b]Echo Isles[/b].\n\n[b]Mounts:[/b] [npc=7952] in Sen\'jin Village sells numerous raptors; [npc=33554] at the Argent Tournament sells a few distinctive models.'),(14,10,0,NULL,0,2,'[b]Overview:[/b] The [b]blood elves[/b] are a proud, haughty race, joining the Horde in Burning Crusade. They represent a faction of former high elves, split off from the rest of elven society; they are also survivors of Arthas\' assault on Silvermoon. Blood elves are fully dependent on magic, having revelled in its power for so long that they suffer horrible withdrawal if it were to be taken away.\n\n[b]Capital City:[/b] The blood elves have rebuilt [zone=3487].\n\n[b]Starting Zone:[/b] [zone=3430] is the starting zone for Blood Elves.\n\n[b]Mounts:[/b] [npc=16264] in Eversong Woods sells numerous hawkstriders; [npc=33557] at the Argent Tournament sells a few unique models.'),(14,11,0,NULL,0,2,'[b]Overview:[/b] The [b]Draenei[/b] are followers of the Naaru and worshipers of the Holy Light. They originally hail from the distant world of Argus, fleeing after Sargeras tried to corrupt them. They then settled on the Orcish homeworld of Draenor, where after a period of peace, they were brutally murdered during Guldan\'s corruption of the Orcs. Finally they settled in Azeroth, to seek aid in their battle against the Burning Legion. Draenei were introduced in the Burning Crusade expansion.\n\n[b]Capital City:[/b] The Draenei have the seat of their power in the ruins of their once-great ship, [zone=3557].\n\n[b]Starting Zone:[/b] [zone=3524] and [zone=3525] cover the attempts of the Draenei to settle on their new island and deal with the inherent corruption present.\n\n[b]Mounts:[/b] [npc=17584] sells a variety of Elekks, as well as [npc=33657] at the Argent Tournament.'),(8,21,0,NULL,0,2,'[minibox]\n[h2]Steamwheedle Cartel[/h2]\n[b]Booty Bay[/b]\n[faction=577]\n[faction=369]\n[faction=470]\n[/minibox]\n\n\n[b]Booty Bay[/b] is a large pirate town nestled into the cliffs surrounding a beautiful blue lagoon on the southern tip of [zone=33]. The city is entered by traversing through the bleached-white jaws of a giant shark.\n\nRun by the Blackwater Raiders who are closely associated with the Steamwheedle Cartel, the port offers facilities to any traveller passing through, regardless of their faction. Combined with the world renowned Salty Sailor Tavern, [event=15], numerous profession trainers, and vendors that sell everything from pets to diamond rings, it is one of the most popular locations in Azeroth.\n\n[npc=2496], ruler of this city, is hiring all the help he can get against the pesky [faction=87] and other threats of the city. He resides, together with the leader of the Blackwater Raiders, [npc=2487], at the top of the inn of Booty Bay.\n\nDue to the boat route from Booty Bay to Ratchet, players of all level ranges (mostly Horde, if lower level) can be expected to be found going about their business, although frequent visitors will more than likely fit in the 35 - 45 range. The quests available from the locals reflect this range nicely.\n\nThe water there occasionally has floating wreckages and schools of fish. The schools that are found most often are [item=6359], [item=6358], and [item=13422]. Fishing in the floating wreckages will also give you very high chances of fishing out chests and items, making Booty Bay an ideal place for fishing.\n\n[h3]Reputation[/h3]\nMost of the quests to raise reputation with Booty Bay are located in The Cape of Stranglethorn. Having a friendly or higher reputation will make the guards help you in case of initiated violence against you.\n\nIf you are Hated with Booty Bay, you can do the repeatable quest [quest=9259] to get back to Neutral.'),(8,47,0,NULL,0,2,'[b]Ironforge[/b] is the faction associated with the capital city of the dwarves, [zone=1537]. [npc=2784] rules his kingdom of Khaz Modan from his throne room within the city, and the [npc=7937], leader of the gnomes, has temporarily had to settle down in Tinker Town after the recent fall of the gnome city [zone=133].\n\n[h3]History[/h3]\nIronforge is the ancient home of the dwarves. A marvel to the dwarves\' skill at shaping rock and stone, Ironforge was constructed in the very heart of the mountains, an expansive underground city home to explorers, miners, and warriors. Massive doors of rock protect the city in times of war, and lava from the mountain itself is redirected and distributed for heat, energy and smithing purposes. Before the Dark Iron Clan was banished from the city, eventually leading to the War of the Three Hammers, Ironforge was the commercial and social center of all the dwarven clans. It is now home to the Bronzebeard Clan. Many dwarven strongholds fell during the Second War between the Horde and the Alliance of Lordaeron, but the mighty city of Ironforge, nestled in the wintry peaks of [zone=1] and protected by its great gates, was never breached by the invading Horde.\n\nRelatively recently, Ironforge also became home to the Gnomeregan refugees. After the Third War, the gnomish city of Gnomeregan became overrun by troggs. Since then, a number of gnomes have settled in Ironforge, converting an area of that city to their liking, an area now known as Tinker Town.\n\nIronforge is one of most populated cities in the world, coming after the human city of [zone=1519], and housing 20,000 people.\n\nWhile the Alliance has been weakened by recent events, the dwarves of Ironforge, led by King Magni Bronzebeard, are forging a new future in the world.[h3]Reputation[/h3]\n[npc=14723] has the repeatable cloth reputation quests. As a reward for being exalted with Ironforge, non-dwarf players are able to ride [url=?items=15.5&filter=na=Ram;cr=93:92;crs=2:1;crv=0:0]rams[/url].\n\nSurrounding zones [zone=1], [zone=38] and [zone=11] contain the most quests for gaining reputation with Ironforge.'),(8,54,0,NULL,0,2,'[b]Gnomeregan Exiles[/b] is the faction of gnomes who fled from their home, [zone=133] in [zone=1]. It was destroyed by the [url=?npcs=7&filter=na=Trogg]Trogg[/url] after a toxic invasion. Now a member of the Alliance, most are located in the Tinkertown section of the neighboring city [zone=1537], including leader [npc=7937].\n\n[h3]History[/h3]\nIt has been speculated that gnomes were formed as robots by the Titans, due to their inquisitive nature and technical skills.\n\nGnomes were an underground race of tinkers, residing in Gnomeregan until the troggs destroyed it. In this war, over 80% of the gnomish population was lost.\n\n[h3]Reputation[/h3]\n[npc=14724] has the repeatable cloth reputation quests. As a reward for being exalted with Ironforge, non-gnome dwarf players are able to ride [url=?items=15.5&filter=na=Mechanostrider;cr=93:92;crs=2:1;crv=0:0]mechanostriders[/url].\nSurrounding zone [zone=1] contain the most quests for gaining reputation with the Gnomeregan Exiles.'),(8,59,0,NULL,0,2,'The [b]Thorium Brotherhood[/b] are an elite group of craftsmen who can reveal a number of epic recipes if you gain enough faction reputation with them. All players start off at Neutral reputation with them.\n\n[h3]History[/h3]\n\nThe [zone=51] is home to a group of exceptionally stout dwarves who have split from the Dark Iron Clan. On the cliffs overlooking the region called the Cauldron, in the far north of the Searing Gorge, the dwarves of the Thorium Brotherhood have established a base of operations, Thorium Point. From here, they keep a close eye on the Dark Iron dwarves\' activities in the Searing Gorge and beyond. Adventurers seeking out Thorium Point will find that the dwarves of the Thorium Brotherhood hold great rewards for those who aid them in their never ending struggle against their former brethren.\n\nThe Thorium Brotherhood comprises many exceptionally talented craftsmen, and the blacksmiths of the Brotherhood are rumored to be among the finest Azeroth has ever seen. They possess the knowledge required to make the arms and armaments of [npc=11502], the Fire Lord, but lack the manpower to obtain the materials required for the crafting. It is rumored that one member of the Thorium Brotherhood has been empowered to trade the dwarves\' fabled recipes and plans with those who can prove their loyalty to the Brotherhood. Of course, proving one\'s loyalty at some point may include venturing to the heart of the [zone=2717], the domain of Ragnaros, the Fire Lord himself, to supply the dwarves with the rare raw materials found there. A daunting task, no doubt, but gaining access to the Thorium Brotherhood\'s secrets should prove to be a reward well worth the effort.\n\n[h3]Reputation[/h3]\n\n[b]Neutral to Friendly[/b]\n\n[ul]\n[li]Turn in [item=18944], [item=3857] and either [item=4234], [item=3575], or [item=3356] to [npc=14624].[/li][/ul]\n[b]Friendly to Honored[/b]\n\n[ul]\n[li]Turn in [item=18945] to Master Smith Burninante.[/li][/ul]\n[b]Honored to Exalted[/b]\n\n[ul]\n[li]Turn in [item=11370] to [npc=12944].[/li]\n[li]Turn in [item=17012] to Lokhtos Darkbargainer.[/li]\n[li]Turn in [item=17010] to Lokhtos Darkbargainer.[/li]\n[li]Turn in [item=17011] to Lokhtos Darkbargainer.[/li]\n[li]Turn in [item=11382] to Lokhtos Darkbargainer.[/li][/ul]'),(8,68,0,NULL,0,2,'[b]Undercity[/b] is the faction for the capital city of the Forsaken Undead, [zone=1497], ruled by Sylvanas Windrunner. It is located in [zone=85], at the northern edge of the Eastern Kingdoms. The city proper is located under the ruins of the historical City of Lordaeron. To enter it, you will walk through the ruined outer defenses of Lordaeron and the abandoned throneroom, until you reach one of three elevators guarded by two abominations.\n\n[h3]History[/h3]\nThe Undercity was originally simply a system of sewers, crypts, and catacombs beneath the Capital City of Lordaeron. After the city was destroyed by the Scourge, Arthas had the underground warren expanded and rebuilt. He originally intended for the Undercity to be his seat of power, from which he would rule the Plaguelands. However, shortly after the Third War ended, Arthas was forced to return to Northrend and save the Lich King. In his absence, [npc=10181] and her rebel Undead captured the ruins of the city. Soon after, she discovered the massive underground fortress, and decided to establish it as the main base of operations for the Undead Forsaken.\n\n[h3]Reputation[/h3]\n[npc=14729] has the Undercity repeatable cloth quests used by non-Undead Horde players to obtain the right to ride [url=?items=15.5&filter=na=Skeletal;cr=93:92;crs=2:1;crv=0:0]skeletal horses[/url] at exalted.\n\nSurrounding zones [zone=267], [zone=130], and Tirisfal Glades have the most quests to earn reputation with Undercity.'),(8,69,0,NULL,0,2,'[b]Darnassus[/b] is the faction associated with [zone=1657], the capital city of the Night Elves. The high priestess, [npc=7999], resides in the Temple of the Moon, surrounded by other sisters of Elune. In the Cenarion Enclave, the [npc=3516] leads the [faction=609], often in direct opposition to his fellow druids in [zone=493] and Tyrande herself.\n\n[h3]History[/h3]\nIn the aftermath of the Third War, the night elves had to adjust to their mortal existence. Such an adjustment was far from easy, and there were many night elves who could not adjust to the prospects of aging, disease and frailty. Seeking to regain their immortality, a number of wayward druids conspired to plant a special tree that would reestablish a link between their spirits and the eternal world.\n\nWith [npc=15362] missing, Fandral Staghelm - the leader of those who wished to plant the new World Tree - became the new Arch-Druid. In no time at all, he and his fellow druids had forged ahead and planted the great tree, [zone=141], off the stormy coasts of northern Kalimdor. Under their care, the tree sprouted up above the clouds. Among the twilight boughs of the colossal tree, the wondrous city of Darnassus took root. However, the tree was not consecrated with nature\'s blessing and soon fell prey to the corruption of the Burning Legion. Now the wildlife and even the limbs of Teldrassil are tainted by a growing darkness.\n\n[h3]Reputation[/h3]\n[npc=14725] has the Darnassus repeatable [quest=7800] used by non-night elven Alliance players to obtain the right to ride [url=?items=15.5&filter=na=Reins+-Winterspring;ra=4;cr=93:92;crs=2:1;crv=0:0]night sabers[/url].[pad]Players who are at or close to level 44 looking to gain the favor of Darnassus should find and complete the quests of [zone=357]. The quests therein are associated with Darnassus and could prove to substantially increase your reputation should they all be completed.'),(8,70,0,NULL,0,2,'The [b]Syndicate[/b] is a mostly Human criminal organization that operates primarily in the [zone=45] and the [zone=36], although a few small encampments are scattered in the [zone=267]. Their membership numbers around 3,000 persons.\n\nThey have three leaders: [npc=2423] (who took over from his father Aiden Perenolde), descendent of the original Lord of Alterac, who directs the Syndicate\'s actions in the Alterac Mountains from Strahnbrad; [npc=2597] directs Syndicate actions in Arathi Highlands from the main keep in the semi-abandoned fortress of Stromgarde; and Lady Beve Perenolde, daughter of Aiden Perenolde.\n\n[h3]History[/h3]\n\nDuring the Second War the Kingdom of Alterac, led by Lord Perenolde, was discovered to be in league with the Orcish Horde. Perenolde believed that a Horde victory was inevitable, and thus offered aid to the Horde by stirring up rebellions, attacking Alliance bases, and giving them supplies. When this treachery was discovered, the Alliance marched on Alterac and destroyed it. Perenolde and any nobles who went along with his plans were stripped of their titles and land. Many of the nobility managed to escape, however, and began plotting their revenge. Using their still sizable fortunes, the nobility hired a band of thieves and assassins, forming an organization known as the Syndicate.\n\nAt first the Syndicate\'s goal was just to spread chaos and disorder, striking from hidden bases in the Alterac Mountains. With the end of the Third War and the resultant chaos however, the leaders of the Syndicate saw their chance to return Alterac to its former power. They have now gained control of several outposts in the surrounding area including the sacked fortress of Durnholde Keep and a portion of the city of Stromgarde.\n\nThey are enemies of both the Alliance, whom they consider their mortal enemies, and the Horde, whom they consider mere brutes good for nothing but slave labor. As a result, the Syndicate is now hunted by both factions, with the [npc=10181], in particular, placing a bounty on their heads - guaranteeing that all captured Syndicate members will be summarily executed. In addition, [npc=4949] ordered a number of his agents, including [npc=2229], [npc=2239], [npc=2238] and their leader [npc=2316] to launch an investigation into the nature of the Syndicate and its activities, as well as to recover [item=3498], which belonged to a dear friend of his, [npc=18887] - a necklace now worn by Elysa, the mistress of Lord Aliden.\n\n[h3]Reputation[/h3]\n\nThe Syndicate as a faction in World of Warcraft is very odd in comparison to most factions in that the killing of the factions members will not lower your standing with the faction. For most players who are not a rogue, the only way for the Syndicate to appear on their Reputation Menu is to complete the quest [quest=8249], which is available to non-rogues. However, the quest requires [item=16885] ... which only rogues can obtain by pick-pocketing NPCs above level fifty, and those can only be traded to you - making it difficult to arrange such a transaction.\n\nCurrently there is only one known option to increase a player’s reputation with the Syndicate, and that is by killing members of the [faction=349] faction. There are no known rewards for increasing Syndicate reputation, and Ravenholdt-affiliated NPCs only give 1 Syndicate Reputation points, with the exception of [npc=13085], who gives 5 (although the corresponding loss of reputation with Ravenholdt is also five times as great). With all players starting at 32000/36000 hated with the faction, it would require killing 10,000 Ravenholdt NPCs to reach Neutral status with the faction; unfortunately, neutral status is the highest you can reach with the Syndicate, and if not to deter players further, none of the Ravenholdt NPCs drop loot.\n\n[b]WARNING[/b]: If you do decide to kill Ravenholdt NPCs, know that there is currently no way to restore your standings with Ravenholdt, if you do go below Neutral. The reason for the problem is that none of the quests that give Ravenholdt Reputation points will be available because none of the members from Ravenholdt will speak to you. This would mean its a permanent change and you will never be able to interact with any of the NPC loyal to Ravenholdt ever again. Also note that players start at 0/3000 reputation with Ravenholdt, and killing even one of their NPCs at this reputation level will forever prevent you from raising your reputation with them again.'),(8,72,0,NULL,0,2,'[b]Stormwind[/b] is the faction associated with [zone=1519], the capital of the humans. It is located in the northwestern part of [zone=12]. The child king, [npc=1747], resides in Stormwind Keep, surrounded by his body guards and advisors, [npc=1748] (the regent), and [npc=1749]. The city is named for the occasional sudden squalls created by a ley line pattern in the mountains around the glorious city.\n\n[h3]History[/h3]\nDuring the First War, the Kingdom of Azeroth, including its capital, Stormwind Keep, was utterly destroyed by the Horde and its survivors fled to Lordaeron. After the orcs were defeated at the Dark Portal at the end of the Second War, it was decided that the city would be rebuilt, even surpassing its former grandeur. The nobles of Stormwind assembled a team of the most skilled and ingenious stonemasons and architects they could find. Under their direction, Stormwind was rebuilt in an amazingly short period of time. Now, at the end of the Third War, in the renamed Kingdom of Stormwind, it stands as one of the last bastions of human power left in the world. \n\nWith the fall of the northern kingdoms, Stormwind is by far the most populated city in the world. Boasting a population of two-hundred thousand people (predominantly human), it serves in many ways as the cultural and trade center of the Alliance, even with remote access to the sea. The humans living in the city are generally carefree and artistic, favoring light and colorful clothes, cuisine and art. It is home to the Academy of Arcane Sciences, the only wizarding school in Eastern Kingdoms, as well as SI:7, a rogue intelligence organization.\n\nHowever, the people of Stormwind find it difficult to accept Theramore\'s role as the home of the new Alliance, convinced not only that Stormwind should be the legitimate heir of Lordaeron\'s role in the past, but also that Theramore is doing little against the worsening situation within the Eastern Kingdoms.\n\n[h3]Reputation[/h3]\n[npc=14722] has the repeatable cloth quests to achieve a higher reputation with Stormwind. In return for exalted reputation, non-human players are able to ride horses.\n\nMost quests associated with Stormwind come from the surrounding areas of Elwynn Forest, [zone=40], and [zone=44].'),(8,76,0,NULL,0,2,'[b]Orgrimmar[/b] is the faction for the capital city [zone=1637] of the orcs and trolls of the [faction=530]. Found at the northern edge of [zone=14], the imposing city is home to the orcish Warchief, [npc=4949].\n\n[h3]History[/h3]\nThrall led the orcs to the continent of Kalimdor, where they founded a new homeland with the help of their tauren brethren. Naming their new land Durotar after Thrall\'s murdered father, the orcs settled down to rebuild their once-glorious society. The demonic curse on their kind ended, the Horde changed from a warlike juggernaut into more of a loose coalition, dedicated to survival and prosperity rather than conquest. Aided by the noble tauren and the cunning trolls of the Darkspear tribe, Thrall and his orcs looked forward to a new era of peace in their own land. \n\nFrom there, they began the creation of the great warrior city, Orgrimmar. Named after the former Warchief, Orgrim Doomhammer, the new city was constructed in a short amount of time, with the aid of goblins, tauren, trolls, and the Mok\'Nathal Rexxar. Despite having some problems with the centaur, harpies, enraged thunder lizards, kobolds, evil orcish warlocks, quilboars, and unfortunately, the Alliance, Orgrimmar prospered in the end and became home to the orcs and Darkspear Trolls.\n\nToday, Orgrimmar lies at the base of a mountain between Durotar and [zone=16]. A warrior city indeed, it is home to countless amounts of orcs, trolls, tauren, and an increasing amount of Forsaken are now joining the city, as well as the Blood Elves who have recently been accepted into the Horde.\n\n[h3]Reputation[/h3]\n[npc=14726] has the Orgrimmar repeatable cloth quests used by non-orcish Horde players to obtain the right to ride [url=?items=15.5&filter=na=Wolf;cr=93:92;crs=2:1;crv=0:0]wolves[/url] at exalted.\n\nSurrounding areas Durotar and [zone=17] have the most quests for gaining reputation with Orgrimmar.'),(8,81,0,NULL,0,2,'[b]Thunder Bluff[/b] is the faction of the Tauren capital city [zone=1638] located in the northern part of the region of [zone=215]. The whole of the city is built on bluffs several hundred feet above the surrounding landscape, and is accessible by elevators on the southwestern and northeastern sides.\n\n[h3]History[/h3]\nThe great city of Thunder Bluff lies atop a series of mesas that overlook the verdant grasslands of Mulgore. The once nomadic Tauren recently built the city as a center for trade caravans, traveling craftsmen and artisans of every kind. It was established by the mighty chief [npc=3057] after the Tauren, with help from the orcs, drove away the centaurs that originally inhabited Mulgore. Long bridges of rope and wood span the chasms between the mesas, topped with tents, longhouses, colorfully painted totems, and spirit lodges. The Tauren chief watches over the bustling city, ensuring that the united Tauren tribes live in peace and security.\n\n[h3]Reputation[/h3]\n[npc=14728] has the Thunder Bluff repeatable cloth quests used by non-tauren Horde players to obtain the right to ride [url=?items=15.5&filter=na=Kodo;cr=93:92;crs=2:1;crv=0:0]kodos[/url] at exalted.\n\nSurrounding zones Mulgore and [zone=17] have the most quests for gaining reputation with Thunder Bluff.'),(8,87,0,NULL,0,2,'During the events leading up to and following the Third War, several criminal organizations appeared in Azeroth. The [b]Bloodsail Buccaneers[/b] appear to be one of these organizations, originating from the Bloodsail Hold on Plunder Isle and is where their ruler, Duke Falrevere holds court. They now plot to plunder and cripple the Steamwheedle Cartel controlled port town of [faction=21], currently under the protection of the Blackwater Raiders. It is likely the Bloodsail Buccaneers have come to take advantage of the town’s current loss of its fleet off the coast of the [zone=45], in which two of its ships were destroyed, and the remaining ship forced to find shelter in a cove, where its crew now fights to survive skirmishes with the Daggerspine Naga.\n\nIn preparation of the attack the Bloodsail Buccaneers have taken position in key locations near the town. Currently they have three ships anchored along the coastline south of Booty Bay, clear of the town’s defensive cannons, with camps also being built along the same coast in preparation of the attack. In addition, a scouting party has landed just west of the entrance to the town, reporting all activities, along with a compound being constructed along the road leading towards the town, likely to stop any re-enforcements from coming to help.\n\nBoth the Bloodsail Buccaneers and Blackwater Raiders seek to achieve their goals without having their forces engaged in battle, to this end each side now seek the aid of adventurers sympathetic to their cause.\n\n[h3]Reputation[/h3]\nThere is only one way to increase your reputation with the Bloodsail Buccaneers and that’s to unleash your wrath on any citizen of Booty Bay who can be found through out the Eastern Kingdoms. Below is a list of every citizen of Booty Bay and their reputation value. The amount gained with the Bloodsail Buccaneers is shown for a level 60 non-human. The amount lost for killing a citizen cannot be shown as it depends on your current level with Booty Bay and the importance of the person you kill. In addition to this what ever you lose with Booty Bay you will lose half of that in the other three goblin towns so if you lose 25 points in Booty Bay you will lose 12.5 points in [faction=470].\n\n[ul]\n[li][npc=4624]: 25 rep gained[/li]\n[li][npc=15088]: 25 rep gained[/li]\n[li][npc=2496]: 5 rep gained[/li]\n[li][npc=2636]: 5 rep gained[/li]\n[li][url=?npcs&filter=cr=3;crs=21;crv=0]Many more NPCs[/url]![/li]\n[/ul]\n\nThe fastest way to increase you reputation with the Bloodsail Buccaneers is to kill Booty Bay Bruisers. At first it may seem a simple task as the guards don\'t appear as threatening as the other monsters a player faces within the game. However, the guards are highly equipped to neutralize players of any class, to prevent people from attacking each other while in the town. What gives the Booty Bay Bruiser the advantage is several factors, one of them being their ability to use nets to lock you in place, preventing you from escaping. Another is the fact that they spawn every time you attack a citizen of the city or if you’re under Unfriendly status with Booty Bay the Bruisers can spawn if you enter a building, because of this players can soon find them selves swarmed by Bruisers.\n\nYet, theses are just the minor problems, in comparison to the Bruiser’s strongest ability, once it pulls out its gun its unlikely you will live, if you do not escape fast enough. Each time a guard shoots you, the attack throws you back, much like an Ogre hammer attack; the difference here is that the Bruiser can shoot in quick succession causing chain throw backs. A player can literally be thrown from one side of the town to the other, preventing you from attacking. More often you will find your self being forced into a corner, unable to move and unable to attack with each spell being interrupted by the Bruiser’s attack. Because the Bruisers do not put their guns away once they are out, the best course of action is to run away. \n\nThrough trial and error most people have discovered a safe place to kill Booty Bay Bruisers. If you follow the tunnel leading into the town, the path to your left that leads to the Blacksmith house is the ideal place to kill the guards. Only two guards patrol this path and normally don’t pass each other that closely, allowing both to be dispatched separately. Once they are gone, one can simply enter the first build on the path to cause a guard to spawn if they are below Unfriendly, if not they can simply attack one of the two NPC in the build, both of which are not high in level. Doing this a player should be able to kill 2 to 4 Bruisers before the two patrolling Bruisers re-spawn. On average a player doing this can kill about 30 to 40 Booty Bay Bruisers gaining about 800 reputation points with the pirates. The Bruisers here don’t appear to pull out their guns, but if you find your self in a bad situation, you can jump over the railing running along the path to the waters below, to escape.\n\n[h3]Rewards[/h3]\nBecoming friendly with the Bloodsail Buccaneers will grant you access to the following items:\n\n[ul]\n[li][item=12185] - Summons a [npc=11236][/li]\n[li][item=22742][/li]\n[li][item=22743][/li]\n[li][item=22745][/li]\n[/ul]\n\nYou will need Honored with the Bloodsail Buccaneers for [achievement=2336].'),(8,92,0,NULL,0,2,'[b]Gelkis[/b] are a tribe of centaur who have made their home in the southmost parts of [zone=405]. They are mortal enemies of the [faction=93], a brother tribe also located in southern Desolace. The founding leader, or Khan, of the Gelkis was [npc=13741], second of the alleged offspring of Zaetar and Theradras. They are presently lead by [npc=5602] and the clan representative [npc=5397]. \n\nThe Gelkis hold no alliance with their brother tribes, but have been known to act both hostile and passive towards members of the Alliance and Horde.\n\n[h3]History[/h3]\nOriginally lead by the Second Khan Gelk, the Magram situated themselves in the southernmost regions of Desolace when the centaur divided into five tribes and have remained there ever since. \n\nWhen the Gelkis tribe spoke out against Khan Magra of the Magram\'s notion that strength was essential and the tribe’s survival depended on their fighting spirit, arguing that Theradras always watches over the centaur and will keep the tribes safe and alive, an eternal feud between the two tribes was born. \n\nAs such the Gelkis are more civilized - or as close as centaur can come to civilized - than their brethren, with an organised social structure and a firm grasp of the Common tongue. While the Magram only respect strength, the Gelkis respect nature and their birthmother Theradras, calling upon her protection and the power of earth to maintain their existence. Though the Magram view this as weak it would seem to be an erroneous view, as Earth Elementals can be sighted in Gelkis Village, putting an end to unwelcome intruders alongside their centaur masters.\n\n[h3]Reputation[/h3]\nOne of the two factions situated in Desolace, you are required to have a certain amount of reputation with the Gelkis in order to start their quests. Reputation for the Gelkis can be gained by killing [url=?npcs=7&filter=na=Magram]Magram monsters[/url]. When killing Magram monsters, you gain 20 reputation with Gelkis and lose 100 with the Magram tribe.'),(8,93,0,NULL,0,2,'[b]Magram[/b] are a tribe of centaur who have made their home in the southeastern parts of [zone=405]. They are mortal enemies of the [faction=92], a brother tribe also located in southern Desolace. The founding leader, or Khan, of the Magram was [npc=13740], third of the alleged offspring of Zaetar and Theradras. They are presently lead by [npc=5601] and the clan representative [npc=5398]. \n\nThe Magram hold no alliance with their brother tribes, but have been known to act both hostile and passive towards members of the Alliance and Horde.\n\n[h3]History[/h3]\nOriginally lead by the Third Khan Magra, the Magram situated themselves against the mountain ranges of Desolace when the centaur divided into five tribes and have remained there ever since. \n\nBefore the death of Magra, he installed the idea that strength was essential and the tribe’s survival depended on their fighting spirit. When their brother tribe of Gelkis centaur spoke out against this notion, arguing that Theradras always watches over the centaur and will keep the tribes safe and alive, an eternal feud between the two tribes was born. \n\nThe life-long pursuit of strength has carried on through the Khans of Magram to this day, turning them violent and determined. To solidify their title as the strongest the tribe still fights fiercely to weaken or destroy their brother clans, viewing the Kolkar as weak, the Gelkis as nothing more than a nuisance, and the Maraudine as a formidable enemy. \n\nIt can be assumed that the Magram’s culture has developed into revolving around strength worship above all else. When compared to the Gelkis, the Magram hold very primitive forms of speech and social structure. For example, their grasp of common is limited and the position of Khan would likely be sought through a death match of sorts.\n\n[h3]Reputation[/h3]\nOne of the two factions situated in Desolace, you are required to have a certain amount of reputation with the Magram in order to start their quests. Reputation for the Magram can be gained by killing [url=?npcs=7&filter=na=Gelkis]Gelkis monsters[/url]. When killing Gelkis monsters, you gain 20 reputation with Magram and lose 100 with the Gelkis tribe.'),(8,270,0,NULL,0,2,'[b]Zandalar Tribe[/b] trolls have come to Yojamba Isle in [zone=33] in the effort to recruit help against the resurrected Blood God and his Atal\'ai Priests in [zone=19] and in the [zone=1417].\n\n[h3]History[/h3]\nThe Zandalarians were the earliest known trolls, the first tribe from which all tribes originated. Over time two distinct troll empires emerged - the Amani and the Gurubashi. They existed for thousands of years until the coming of the Night Elves, who warred with them and eventually drove both empires into exile. \n\nFollowing the Great Sundering, the defeated Gurubashi grew ever more desperate to eke out a living. Searching for a means to survive, they enlisted the aid of the savage [npc=14834], also known as the Soulflayer. Hakkar grew into a merciless oppressor who demanded daily sacrifices from his devotees, and so in time the Gurubashi turned on their dark master. The strongest tribes (including the Zandalar) banded together to defeat Hakkar and his loyal troll priests, the Atal\'ai. The united tribes narrowly defeated the Blood God and cast out the Atal\'ai... despite their victory, however, the Gurubashi Empire soon fell. \n\nIn recent years the exiled Atal\'ai priests have discovered that Hakkar\'s physical form can only be summoned within the ancient and once-deserted capital of the Gurubashi Empire, Zul\'Gurub. Unfortunately, the priests have met with success in their quest to call forth Hakkar—reports confirm the presence of the dreaded Soulflayer in the heart of the ruins. \n\nAnd so the Zandalar tribe has arrived on the shores of Azeroth to battle Hakkar once again. But the Blood God has grown increasingly powerful, bending several tribes to his will and even commanding the avatars of the Primal Gods— Bat, Panther, Tiger, Spider and Snake. With the tribes splintered, the Zandalarians have been forced to recruit champions from Azeroth\'s varied and disparate races to battle, and hopefully once again defeat, the Soulflayer.\n\n[h3]Reputation[/h3]\nReputation with the Zandalar Tribe is gained from killing trash and bosses in Zul\'Gurub as well as repeatable and special quests which require instance-dropped items to complete. Each full run of Zul\'Gurub gives approximately 2,500-3,000 reputation.\n\nBefore the Burning Crusade, the main reason for gaining reputation with the tribe were the [url=?items=0.6&filter=na=Zandalar]shoulder[/url], [url=?items=0.6&filter=minrl=60;maxrl=60;cr=18:107;crs=4:0;crv=0:to+a+leg+or+head+slot+item]head and leg[/url] slot item enchants. As well, there were popular alchemy and enchanting recipes that many end-game guilds sought after. All rewarded items from the item set within Zul\'Gurub required a set level of reputation.'),(8,349,0,NULL,0,2,'[b]Ravenholdt[/b] is a guild of thieves and assassins that welcomes only those of extraordinary prowess into its fold. They are diametrically opposed to the [faction=70], and are a rogue-only faction as all quests are rogue-only quests. The exception is the quest [quest=8249], which is available to non-rogues, but they would require the help of a rogue to get the items for the quest. [b]Ravenholdt Manor[/b], the faction\'s headquarters, is located in [zone=36], but to get there you have to come from the northeast corner of [zone=267].\n\n[h3]Reputation[/h3]\nAll Syndicate [url=?search=Syndicate#npcs]humanoids[/url] give 1-5 reputation points per kill depending on your current level. As well, there are a few quests that increase your reputation, but your primary method to raise your reputation is from the repeatable quests for turning in pickpocketed items.\n\nYou start off at 0/3000 Neutral with Ravenholdt, meaning if you kill any Ravenholdt NPCs before raising your reputation by at least 5, you will become Unfriendly and be unable to raise your reputation any higher ever again. To raise your reputation from Neutral to Friendly, the repeatable quest [quest=6701] is available. You will have to turn in 11-12 [item=17124] and once you are Friendly, this quest is no longer an option. From Neutral to Friendly you can also deliver five [item=16885] for Junkboxes Needed.\n\nTo raise your reputation beyond Friendly, the only choice is the repeatable quest Junkboxes Needed. There is no known faction reward for obtaining Friendly, Honored, Revered or Exalted, except that the guards speak to you with more respect. However, Exalted is required to obtain the Feat of Strength [achievement=2336].'),(8,369,0,NULL,0,2,'[minibox]\n[h2]Steamwheedle Cartel[/h2]\n[faction=21]\n[faction=577]\n[b]Gadgetzan[/b]\n[faction=470]\n[/minibox]\n\n[b]Gadgetzan[/b] is the faction of the city Gadgetzan, which is home to goblinhood\'s finest engineers, alchemists and merchants and is the only spot of civilization in the entire desert. Rising out of the northern [zone=440] desert like an oasis, Gadgetzan is the headquarters of the Steamwheedle Cartel, the largest of the Goblin Cartels. The Goblins believe in profit above loyalty, thus Gadgetzan is considered neutral territory in the Horde/Alliance conflict.\n\n[h3]History[/h3]\nAlthough the goblins\' neutrality is almost universally acknowledged, there are still those who seek to sow chaos and anarchy. For Gadgetzan, this comes in the form of the Wastewander bandits, a gang of miscreants who have occupied the Waterspring Field and Noonshade Ruins of northeast Tanaris. Few goblins care about ancient ruins (unless they have treasure) – for all they care, the bandits can have the old blocks of stone. \n\nHowever, the Waterspring Field is vital to the goblins\' survival in the desert, providing them with the liquid gold of the desert. Water towers out in the field were constructed under the blazing heat of the desert sun by the backbreaking work of their slaves, and by Az, the goblins aren\'t going to give up their hard earned towers that easily. However, the Bruisers need to stay in town to keep the gnomes\' collective Napoleonic-complex from getting out of hand and to stop the seemingly endless dueling among the various visitors from disrupting business. Therefore, it falls to brave mercenaries from all corners of the world to help the goblins in their time of utmost need.\n\n[h3]Reputation[/h3]\nKilling the [url=?npcs=7&filter=na=Southsea]Southsea[/url] and [url=?npcs=7&filter=na=Wastewander]Wastewander[/url] monsters will increase your reputation with the Steamwheedle Cartel. Having a friendly or higher reputation will make the guards help you in case of initiated violence against you. Having an exalted reputation means that the guards will never attack you even if you initiate attacks on the opposite faction.\n\nMost of the quests associated with the Gadgetzan faction are located in Tanaris.\n\nIf you are Hated with Gadgetzan, you can do the repeatable quest [quest=9268] to obtain Neutral.'),(8,470,0,NULL,0,2,'[minibox]\n[h2]Steamwheedle Cartel[/h2]\n[faction=21]\n[faction=577]\n[faction=369]\n[b]Ratchet[/b]\n[/minibox]\n\n[b]Ratchet[/b], the faction of the city Rachet on Kalimdor’s central east coast in [zone=17], is run by goblins and shows it. Its streets sprawl in every direction, and the architecture shows no consistency or common vision. It is a city of entertainment and trade, where anything that anyone would ever want to buy — and plenty of things that no one ever wants to buy — is on sale.\n\nRatchet is currently run by a corporate group known as the Steamwheedle Cartel a splinter group from the Venture Company, who first built the port town for trading with [zone=1637]. It is initially a neutral faction to both Horde and Alliance. A ferry conveniently connects Ratchet to Booty Bay.\n\n[h3]History[/h3]\nBuilt from equal parts of industry and decadence, the goblin port city of Ratchet sprawls along nearly a mile of of coastline where the eastern Barrens poke between [zone=14] and the [zone=15] to the sea. Ratchet is the pride of the goblins, a trade city where you can find almost anything your heart desires - and if something is not in stock, you can bet the goblins can order it. Ratchet also had regular ferries that traversed the safe though roundabout route to the island stronghold of Theramore to the south.\n\nRatchet is a city where creatures who were once the butt of jokes now reign supreme. Its streets wander without rhyme or reason through neighborhoods dedicated to one activity: commerce. Ramshackle warehouses stand next to stately stone homes. Fine shops press cheek to jowl with rude huts. Wares of every type imaginable - and some beyond the imagination - are on display in markets and in exclusive boutiques.\n\nGoblins welcome anyone with gold or items of value and a willingness to trade them for their wares and services. Merchants throng the marketplaces each day, selling everything from silks to slaves, and even at night the stores lining the twisting streets and alleys remain open for business. Those with the money can listen to skilled musicians while drinking fine ales and eating food prepared by expert chefs. For those with earthier tastes, the streets along the wharf teem with whorehouses, taprooms, and casinos.\n\nRatchet is the largest port on Kalimdor, with as many ships bringing cargo in as there are ships heading out for other sites around Kalimdor. In addition to legitimate trade vessels, pirate craft receive amnesty while in the port of Ratchet as long as they can pay the stiff docking fees. This situation makes many merchant captains furious, but they cannot hope to stay in business if they boycott Ratchet. Moreover, the Lawkeepers and hired mercenaries prowling the waterfront are eager to deal with anyone looking to cause trouble.\n\n[h3]Reputation[/h3]\nMost of the quests to raise reputation with Ratchet and the Steamwheedle Cartel are located in the Barrens. Having a friendly or higher reputation will make the guards help you in case of initiated violence against you.\n\nIf you are Hated with Rachet, you can do the repeatable quest [quest=9267] to get back to Neutral.'),(8,471,0,NULL,0,2,'The Wildhammers are a clan of dwarves currently centered in the [zone=47] and [zone=3520]. The faction has been removed in patch 2.0.1.\n\n[h3]History[/h3]\n\nJust prior to the [object=175739], the Wildhammer Clan, ruled by Thane Khardros Wildhammer, inhabited the foothills and crags around the base of Ironforge. The Wildhammer Clan was unsuccessful in wresting control of [zone=1537] from the Bronzebeard and Dark Iron clans. Khardros and his Wildhammer warriors traveled north through the barrier gates of Dun Algaz, and founded their own kingdom within the distant peak of Grim Batol. There, the Wildhammers thrived and rebuilt their stores of treasure.\n\n[npc=9019] and his Dark Irons vowed revenge against Ironforge. Thaurissan and his sorceress wife, Modgud, launched a two-pronged assault against both Ironforge and Grim Batol. As Modgud confronted the enemy warriors, she used her powers to strike fear into their hearts. Shadows moved at her command, and dark things crawled up from the depths of the earth to stalk the Wildhammers in their own halls. Eventually Modgud broke through the gates and laid siege to the fortress itself. The Wildhammers fought desperately, Khardros himself wading through the roiling masses to slay the sorceress queen. With their queen lost, the Dark Irons fled before the fury of the Wildhammers.\n\nOnce the immediate Dark Iron threat was eliminated, the Wildhammers returned home to Grim Batol. However, the death of the Modgud had left an evil stain on the mountain fortress, and the Wildhammers found it uninhabitable. Khardros took his people north towards the lands of Lordaeron. Settling within the mountainous region of the Aerie Peaks and The Hinterlands, and lush forests of Northeron, the Wildhammers crafted the city of Aerie Peak, where the Wildhammers grew closer to nature and even bonded with the mighty gryphons of the area. Over time they started calling their land the Hinterlands. \n\n[b]Modern Wildhammers[/b]\nThe Wildhammer Clan currently makes its home at Aerie Peak in the Hinterlands. The most immediate threat to their security comes from the east in the form of the Witherbark Trolls and Vilebranch Trolls. They are most famous for riding into battle atop Gryphons, while wielding powerful Stormhammers.\nWildhammer dwarves have a number of clans, each ruled by a Thane. The strongest Thane rules Aerie Peak.'),(8,509,0,NULL,0,2,'[b]The League of Arathor[/b] was originally established by the survivors of the Kingdom of Stromgarde to reclaim the [zone=45] from the hands of the Forsaken Defilers in Hammerfall. Today it is an organization in support of the Alliance, based out of the [zone=3358] in Refuge Pointe. They have taken it upon themselves to help supply the Alliance forces where needed, and their members include all manner of Alliance races - even though they are still predominantly Stromgardian humans.\n\n[h3]Reputation[/h3]\nPlayers can earn reputation in this faction by participating in the Arathi Basin battleground. When you fight in Arathi Basin you earn 10 reputation per 160 resources. On Arathi Basin holiday weekends the required resources is reduced to 150.\n\nYou are granted the player title [title=48] once exalted with League of Arathor and the other two battleground factions, [faction=890] and [faction=730].'),(8,510,0,NULL,0,2,'[b]The Defilers[/b] seek to foil the [faction=509] in the [zone=3358] battleground. Today it is an organization in support of the Horde, based out of Hammerfall in [zone=45]. They have taken it upon themselves to help supply the Horde forces where needed, and their members include all manner of Horde races - even though they are still predominantly orcs.\n\n[h3]Reputation[/h3]\nReputation is gained through participation in the Arathi Basin battleground. When you fight in Arathi Basin you earn 10 reputation per 160 resources. On Arathi Basin holiday weekends the required resources is reduced to 150.\n\nYou are granted the player title [title=47] once exalted with the Defilers and the other two battleground factions, [faction=889] and [faction=729].'),(8,529,0,NULL,0,2,'The [b]Argent Dawn[/b] is an organization focused on protecting Azeroth from the threats that seek to destroy it, such as the Burning Legion and the Scourge. Strongholds of the Argent Dawn can be found in the [zone=139] and [zone=28]. It also maintains a presence in [zone=1657] and in the [zone=85], among other less notable areas. Reputation with the Argent Dawn can be used to purchase various profession recipes, misc. consumables, and to mitigate the cost of attunement to [zone=3456]. With the expansion of the Burning Crusade, Argent Dawn reputation has decreased in value.\n\nArgent is Latin for silver, which could explain why the [item=22999] has an icon of a silver sun rising.[h3]History[/h3]After the death of the [npc=16062], the corruption of the Scarlet Crusade became apparent to some of its members, who subsequently left the ranks of the [url=?search=scarlet+crusade#M0z]Scarlet Crusade[/url] and established the Argent Dawn to protect Azeroth from the threat of the Scourge without the blind zealotry present in the Scarlet Crusade.\n\nWhile they share the same goals as the Crusade, the Argent Dawn has opened its ranks to not only other Alliance races besides Humans, but also members of the Horde and even some of the Forsaken. They caution discretion and introspection, and put a lot of emphasis on researching the Scourge and how to combat them.\n\nWith time the Argent Dawn has grown diversified, and like its progenitor — the Scourge — has split again, with an offshoot called the [url=?search=brotherhood+of+the+light]Brotherhood of the Light[/url], a compromise between the Argent Dawn\'s more scholarly approach and the Scarlet Crusade\'s fanaticism.\n\n[h3]Reputation[/h3]\n[b]Scourgestones[/b]\nWhile wearing a trinket granting the Argent Dawn Commission effect, characters can loot [url=?items=12&filter=na=scourgestone]scourgestones[/url] from undead monsters they\'ve killed, and subsequently turn them in in exchange for [item=12844]. These turn-ins require various numbers of [item=12843], [item=12841], and [item=12840]. It should be noted that the token items received from the turn-ins should be saved until after Revered status is reached, as the quest turn-ins will no longer grant reputation after this point.[pad][b]Cauldrons[/b]\nAnother way to gain reputation with the Argent Dawn is through repeatable \"Cauldron\" quests. The Cauldrons are a source of \"undeathness,\" that contribute to the Scourge\'s numbers.[pad][b]Instances[/b]\nLike most factions, the player can run instances to increase his reputation. These instances are [zone=2017] and [zone=2057]. Naturally, these instances also include quests that will raise Argent Dawn reputation, as well as include Scourgestone drops.'),(8,530,0,NULL,0,2,'[b]Darkspear Trolls[/b], the tribe of exiled trolls that has joined forces with [npc=4949] and the Horde. They now call [zone=1637] their home, which they share with their orc allies. [npc=10540] is their current leader.\n\n[h3]History[/h3]\nAs tribal rivalries erupted throughout the former Gurubashi Empire, the Darkspear Tribe found themselves driven from their homeland in [zone=33]. Having settled in what are believed today to be the Broken Isles, the tribe soon found themselves entangled in a conflict with a band of murlocs. Their fate seemed sealed until the orcish Warchief Thrall and his band of newly freed orcs took shelter on their island home. Controlled by a Sea Witch, a group of rampaging murlocs captured the Darkspears\' leader Sen\'jin, along with Thrall and several other orcs and trolls. Thrall managed to free himself and others, but was ultimately unable to save the trolls\' leader. Although Sen\'jin was sacrificed to the Sea Witch, he was able to reveal a vision he had in which Thrall would lead the Darkspear from the island. \n\nAfter returning to the island, Thrall and his followers managed to fend off further attacks by the Sea Witch and her murloc minions, and set sail for Kalimdor once again. Under the new leadership of [npc=10540], the Darkspear swore allegiance to Thrall\'s Horde and followed him to Kalimdor. Now considered enemies by all other trolls except the Revantusk and the Zandalari, the Darkspear are held in contempt to this day. Yet, the Darkspear have not forgotten being driven from their ancestral homes and this animosity is eagerly returned, especially towards the other jungle trolls. Having reached the orc\'s new homeland, [zone=14], the trolls carved out another home for themselves - this time among the Echo Isles on the eastern shores of the new orc kingdom. \n\nHowever, with the coming of Kul Tiras and its navy, the Darkspear were forced to retreat inland under the onslaught of the misguided commander [npc=177201]. The trolls, fighting alongside their horde brethren, defeated the enemy and reclaimed their new homeland. Shortly thereafter, a witch doctor by the name of [npc=3205] began using dark magic to take the minds of his fellow Darkspear. As his army of mindless followers grew, Vol\'jin ordered the free trolls to evacuate, and Zalazane took control of the Echo Isles. The Darkspear have since settled on the nearby shore, naming their new village after their old leader, Sen\'jin. From Sen\'jin Village they, along with their allies, send forces to battle Zalazane and his enslaved army.\n\n[h3]Reputation[/h3]\n[npc=14727] has the repeatable cloth reputation quests. As a reward for being exalted with the Darkspear Trolls, non-troll Horde players are able to ride [url=?items=15.5&filter=na=Raptor;cr=93:92;crs=2:1;crv=0:0]raptors[/url].\n\nSurrounding zone Durotar contain the most quests for gaining reputation with the Darkspear Trolls. As well, higher level players with the Burning Crusade also have a good amount of quests in [zone=3521].'),(8,576,0,NULL,0,2,'As the last uncorrupted furbolg tribe (at least in their view), the [b]Timbermaw[/b] seek to preserve their spiritual ways and end the suffering of their brethren.\n\nThe Timbermaw Furbolgs inhabit two areas: [zone=16] and [zone=361]. They are presumed to be the only furbolg tribe to escape demonic corruption, though this may not be true due to the existence of [npc=3897], an uncorrupted furbolg of unknown tribe, and the Stillpine tribe on [zone=3524] in Burning Crusade. However, many other races kill furbolg blindly now, without bothering to see if they are friend or foe. For this reason, the Timbermaw furbolg trust very few.\n\nAdventurers who seek out Timbermaw Hold in northern Felwood and prove themselves as friends of the Timbermaw will learn that the furbolgs value their friends above all else. Though they possess no fine jewels or any worldly riches, the Timbermaw\'s shamanistic tradition is still strong. They know much about the art of crafting armors from animal hides, and they are more than happy to share their healing/resurrection knowledge with friends of their tribe. Besides, any reputation above Unfriendly will also grant you untroubled access to [zone=493] and [zone=618] through their tunnels.\n\n[h3]Reputation[/h3]\nReputation with the Timbermaw Hold faction is mainly gained through quests and killing in Felwood. The members of the Deadwood Tribe, another Furbolg tribe in Felwood, are the Timbermaws\' main enemies.\n\n[ul]\n[li]Killing one [url=?npcs&filter=na=Winterfall]Winterfall[/url] or [url=?npcs&filter=na=Deadwood]Deadwood[/url] Furbolg gives 10 reputation points. Gains stop at revered; Deadwoods give 2 reputation point at honored.[/li]\n[li]Killing either one of the Deadwood Bosses [npc=9464] or [npc=9462], is worth 60 reputation. There is no reputation limit.[/li]\n[li]Killing the elite Winterfall Furbolg, [npc=10738], located in a cave east of [faction=577], awards 50 reputation. There is no reputation limit, and his respawn rate is 6 to 8 minutes.[/li]\n[li]Killing the named rare mob [npc=14342] is worth 50 reputation. He is a rare spawn at Deadwood Village in Felwood and there is no reputation limit for this mob.[/li]\n[li]Killing the named rare mob [npc=10199] is worth 50 reputation. He is a rare spawn at Winterfall Village in Winterspring. Killing him will grant reputation up until Revered.[/li]\n[li]After completing [quest=8460], turning in 5 [item=21377] yields 150 reputation.[/li]\n[li]After completing [quest=8464], you will be able to turn in [item=21383] collected from furbolgs in Winterspring. Turning in 5 beads at [npc=11556] yields 150 reputation.[/li]\n[/ul]'),(NULL,NULL,0,'help=commenting-and-you',0,2,'[menu tab=2 path=2,13,0]One of many useful features is the user-submitted comment system. This system allows users to submit their own comments to augment the data provided here. As a rule, we promote the submission of informative comments, but we also like to see the occasional joke. Moderators and users alike will apply positive and negative ratings to comments in an effort to promote the useful ones and purge unnecessary information.\r\n\r\nWith that in mind, below is a guide that can be used to determine how your comment will likely be received by the community. \r\n\r\n[pad]\r\n\r\n[tabs name=comments]\r\n\r\n[tab name=\"Before you post\"]\r\n\r\n[ul]\r\n[li][b]Read existing comments[/b] – Sometimes, the information you have may already have been posted by another user. In this case, if the information is useful, the existing comment should be given a positive rank. Posting information that was already added in a previous comment will likely result in a negative rating.[pad][/li]\r\n[li][b]Verify your facts[/b] – Make sure that what you have to post is true. A friend might tell you that a mob is immune to Frost Nova, but unless you verify that yourself, you could be posting a potentially misleading comment.[pad][/li]\r\n[li][b]Temporary usability[/b] – If you want to correct invalid or missing information on a page, keep in mind that your comment may go from a positive ranking to a negative ranking when the correction occurs. For example, informing the community that a spell is cast by Illidan Stormrage before that data has been collected will be useful at first, but once Aowow learns to parse that information and adds it to the \'Abilities\' tab, your comment becomes redundant. If you do not want to worry about the comment or do not want one of your comments to be rated negatively, consider informing us in the [url=/?forums&board=1.]Site Feedback[/url] forum. The moderation staff will be happy to add a comment to correct invalid or missing information on the page for you. Alternatively, you can delete your comment later when it becomes redundant.[/li]\r\n[/ul]\r\n\r\n[/tab]\r\n\r\n[tab name=\"Comment ratings\"]\r\n\r\n[h3][color=q2]Positive (+1)[/color][/h3]\r\n[ul]\r\n[li][b]Corrections on drop percentages[/b] – There are many instances where drop percentages will be inaccurate. For example, quest items do not drop for people who do not have the quest, so their drop percentages will be low. Also, mobs that periodically do not drop loot when they die won\'t count against the drop percentages, so these mobs may appear to have higher drop rates for some items.[pad][/li]\r\n[li][b]Strategies[/b] – If you have a strategy that can assist other users in completing a quest or defeating a mob, by all means, share![pad][/li]\r\n[li][b]Quest coordinates[/b] – Providing coordinates for the location of quest items or mobs is always useful. When possible, you should provide links to quest targets as well.[pad][/li]\r\n[li][b]Theorycrafting[/b] – We encourage users to post any information they have regarding complex calculations they may have performed to, for example, prove one item has a higher DPS than another given certain abilities.[pad][/li]\r\n[li][b]Just for laughs[/b] – If your comment is one that would be universally funny (i.e. not an inside joke), post away. We like to laugh as much as anyone else. Of course, whether your joke is funny or not is subject to our other users. :)[/li]\r\n[/ul]\r\n\r\n[h3][color=q10]Negative (-1)[/color][/h3]\r\n[ul]\r\n[li][b]Redundant information[/b] – For instance, a comment that says \"Dropped by Ragnaros\" does not add anything to the page as that information can be viewed in the \"Dropped By\" tab of the page in question.[pad][/li]\r\n[li][b]Soloed by:[/b] Unless your comment contains a detailed explanation of how you defeated a mob, these comments do not add anything to the page. Simply stating your level, class, and that you soloed the mob by using a few skills is not enough to be useful.[pad][/li]\r\n[li][b]Dropped in X kills[/b] – Telling users that you were lucky enough to get the crusader enchant in one drop is not considered useful information.[pad][/li]\r\n[li][b]NPC/Object coordinates[/b] – The coordinates for NPC or mobs are already supplied in convenient maps within the interface.[pad][/li]\r\n[li][b]Best X before level Y[/b] – Simply posting that an item is the best twink weapon or the best dagger for a rogue is not helpful unless you can back up that claim with facts.[pad][/li]\r\n[li][b]HUNTAR WEPPON[/b] – While it would be acceptable to explain why you feel a certain class with a certain spec would gain the most benefit from an item, simply stating that you feel the weapon should always go to a hunter in a raid will result in negative moderation.[pad][/li]\r\n[li][b]Confirmed![/b] – Adding a comment that simply indicates that you have confirmed a comment left by someone else clutters the comments. The best way to confirm a comment as correct is to give it a positive ranking. A comment with a high ranking will indicate to users that many people think it is useful data.[/li]\r\n[/ul]\r\n\r\n[/tab]\r\n\r\n[tab name=Deletion]\r\n\r\nAny comment that does not abide by the same [forumrules] will be deleted by a moderator.\r\n\r\n[/tab]\r\n\r\n[/tabs]'),(NULL,NULL,0,'help=item-comparison',0,2,'[menu tab=2 path=2,13,5]Can\'t find the answer you were looking for? Just [url=/?aboutus#contact]contact us[/url], or post on our [url=/?forums&board=1]forums[/url]! \r\n\r\n[pad]\r\n\r\n[tabs name=compare]\r\n\r\n[tab name=\"General usage\"]\r\n\r\n[h3]Basic Controls[/h3]\r\n\r\n[ul]\r\n[li][img src=STATIC_URL/images/icons/save.gif border=0] [b]Save[/b] – Saves the comparison so that you may continue browsing the site without losing it. When you click on the [b]Compare[/b] button found throughout the site you will be given the option to add to your saved comparison.[/li]\r\n[li][img src=STATIC_URL/images/icons/refresh.gif border=0] [b]Autosaving[/b] – Indicates that you are viewing your saved comparison, and that any changes you make will automatically be saved. To avoid modifying your saved comparison, you may click on Link to this comparison before making any changes.[/li]\r\n[li][img src=STATIC_URL/images/icons/link.gif border=0] [b]Link to this comparison[/b] – Provides a link to a new page with the current item comparison already there! Useful for showing friends your item comparisons.[/li]\r\n[li][img src=STATIC_URL/images/icons/delete.gif border=0] [b]Clear[/b] – Removes all items, groups, and weights from the comparison tool, giving you a clean slate to work with. [b]This will [u]delete[/u] your saved comparison if used while autosaving.[/b][/li]\r\n[li][img src=STATIC_URL/images/icons/add.gif border=0] [b]Weight scale[/b] – Allows you to add one or more weight scales to the item comparison using your own weights or one of our predefined presets. Each weight scale can have its own name. A saved comparison also contains the weight information, allowing you to store custom weight scales for future use.[/li]\r\n[li][img src=STATIC_URL/images/icons/add.gif border=0] [b]Item[/b] – Opens a live search that displays item suggestions as you type the name of an item. Clicking on a suggestion will add that item to your comparison.[/li]\r\n[li][img src=STATIC_URL/images/icons/add.gif border=0] [b]Item set[/b] – Opens a live search that displays item set suggestions as you type the name of an item set. Clicking on a suggestion will add all of the items in that set to your comparison.[/li]\r\n[/ul]\r\n\r\n[h3]Adding Items[/h3]\r\n[div float=right align=right][img src=STATIC_URL/images/help/item-comparison/addingitems.gif]\r\n[small]Some of the ways to add items to a comparison.[/small][/div]The comparison tool is fully integrated with our site and designed to be as convenient as possible to work with. There are many ways to add items to a comparison depending on what part of the site you are on: \r\n[ul][li]Using the [url=/?compare]item comparison tool[/url] itself, you may add items or item sets using the links in the top right corner as described above.[/li]\r\n[li]Viewing an [url=/?item=35137]item[/url] or [url=/?itemset=-17]item set[/url] page, you may click on the red [b]Compare[/b] button near the Quick Facts box.[/li]\r\n[li]Viewing [url=/?items=4.2&filter=sl=8]search results[/url] or [url=/?npc=34077#sells]any page with a list of items[/url], checkboxes are displayed next to items which can be equipped. You may select one or more items and click the [b]Compare[/b] button at the top of the list.[/li][/ul]\r\n\r\n[i]Note: If you have a comparison saved, and you add items to your comparison from elsewhere on the site, you will be given the option to add them to your saved comparison or create a new one. If you don\'t have a saved comparison, a new comparison will automatically be created and saved with the selected items.[/i]\r\n\r\n[h3]Managing Your Items[/h3]\r\n[div float=right align=right][img src=STATIC_URL/images/help/item-comparison/newgroup.gif]\r\n[small]Creating a new group by dragging an item.[/small][/div]\r\n[ul][li][b]Creating a new group[/b] – [u]Drag an item into the empty column[/u] on the right to create a new group containing that item.[/li]\r\n[li][b]Moving[/b] – To move an item or group, click on the item (or the group\'s control bar) and [u]drag it to the desired position[/u].[/li]\r\n[li][b]Copying[/b] – [u]Holding shift while dragging[/u] an item or group will make a copy of it when it is dropped.[/li]\r\n[li][b]Deleting[/b] – Items and groups can be deleted by [u]dragging them out of the row[/u]. Groups may also be deleted by clicking the X on the right side of the group\'s control bar.[/li]\r\n[li][b]Deleting all but one group[/b] – [u]Holding shift while deleting a group[/u] (see above) will cause all other groups to be deleted instead of that one.[/li]\r\n[li][b]Splitting a group[/b] – Groups of 2 or more items can be split by [u]clicking on [b]Split[/b] in the menu dropdown[/u] on the group\'s control bar. This will create a new group for each item in the current group.[/li]\r\n[li][b]Exporting a group[/b] – [u]Clicking on [b]Export[/b] in the menu dropdown[/u] of the group\'s control bar will take you to a new comparison containing only the current group.[/li]\r\n[li][b]Item Enhancements[/b] - To add gems or enchantments to an item, [u]right-click on the item icon at the top[/u], then select the desired option from the menu. The stats will automatically update—including the set bonuses.[/li]\r\n[/ul]\r\n\r\n[/tab]\r\n\r\n[tab name=\"Advanced features\"]\r\n\r\n[h3]Level Adjustments[/h3]\r\nYou can select your desired character level from the dropdown at the top left. When you do, all the statistics that change according to your level (including combat ratings and heirloom item stats) will automatically adjust to the corresponding value for the level you\'ve entered.\r\n\r\n[h3]Gains[/h3]\r\nAt the bottom of the item comparison is a special row called \'Gains\'. The gains row calculates the minimum values of all stats that appear in any group in the item comparison. It then displays the bonuses each row has [b]above[/b] this minimum.\r\n\r\nFor example, the minimum stamina for any group in [url=/?compare=35031;35030;35029;35028;35027]this comparison[/url] is 50. The gains row displays nothing for the items which have 50 stamina, +23 sta for the item with 73 stamina, and +27 sta for the items with 77 stamina.\r\n\r\nBasically, the gains row removes the shared stats between all groups so that you can focus on what each group brings to the table.\r\n\r\n[h3]Focus Group[/h3]\r\n\r\n[screenshot url=STATIC_URL/images/help/item-comparison/focus2.gif thumb=STATIC_URL/images/help/item-comparison/focus.gif float=right]Comparing arena sets of the first four PvP\r\nseasons using a focus group.[/screenshot]Setting a focus group is done by clicking on the eye icon in the group\'s control bar. Selecting a group as your focus will update the display of the item comparison to show the difference in stats between all other groups and the focus group.\r\n\r\nWhen a focus is set, the focus group is highlighted and each other group has numbers that indicate the stats gained or lost in comparison to the focus group.\r\n\r\n[b][color=q2]Positive[/color][/b] numbers indicate that group has a higher total for a given stat than the focus group, while [b][color=q10]negative[/color][/b] numbers indicate that group has a lower total for a given stat than the focus group. \r\n\r\n[h3]Stat Weighting[/h3]\r\nTo add a weight scale to your comparison, click on the [b]Add a weight scale[/b] link in the top right corner. You may select a weight scale from our predefined presets or create one of your own. Each weight scale may be given a name that will appear in the score tooltips to help differentiate the different scores. You may add as many weight scales as you like.\r\n\r\nTo remove a weight scale, click on the [b]X[/b] next to the appropriate score in any group. To toggle between normalized (default), raw, and percent score mode, click on the score in any group.\r\n\r\nUnlike the weighted item search, these weight scales [b]do not[/b] automatically select gems or include socket bonuses in the score at this time.\r\n\r\n[h3]Viewing a Group in 3D[/h3]\r\nClick on [b]View in 3D[/b] in the menu dropdown of the group\'s control bar to display a 3D model of the items and select the race and gender to display them on. Of course, items which do not have models, such as trinkets and rings, will not be displayed.\r\n\r\n[/tab]\r\n\r\n[/tabs]'),(NULL,NULL,0,'help=stat-weighting',0,2,'[menu tab=2 path=2,13,3]Can\'t find the answer you were looking for? Just [url=/?aboutus#contact]contact us[/url], or post on our [url=/?forums&board=1]forums[/url]! \r\n\r\n[pad]\r\n\r\n[tabs name=weights]\r\n\r\n[tab name=FAQ]\r\n\r\n[h3]How do weights work?[/h3]\r\nThe weighting system allows you to give a weight value to attributes that matter to you and applies your ratings to items in your search results. Each weight value is multiplied by an item\'s stat points and then added together to get the item\'s total score. This score is used to sort the results and display the highest scoring items.\r\n\r\nIf you decide that spell damage is worth twice as much as spell crit, you could add the weights as 2 and 1, 100 and 50, or any other numbers with the same ratio.\r\n\r\nPlease note that weights only work for [url=/?items=4]Armor[/url], [url=/?items=2]Weapons[/url], [url=/?items=3]Gems[/url] and [url=/?items=0]Consumables[/url]. \r\n[h3]What is the difference between weights and equivalency?[/h3]\r\nThe equivalency of two attributes describes how much one equals the other. You may find equivalency ratings that say something like 1 agility = 1.5 strength. This is [b]not[/b] the same as weight values; in fact, it\'s the exact opposite! Equivalency describes the ratio of the stats to each other, which can be used to derive the stat weights. In this example, an appropriate set of weights might be agility 3 and strength 2; this works out to agility being [i]1.5 times as valuable[/i] as strength. \r\n[h3]Is there a way to save a template that I have created?[/h3]\r\nThere sure is! You can save your stat weighting scales by going to the \'Preset\' dropdown menu, selecting \'custom,\' and then filling in your own weights. After you\'ve modified them to your liking, you can hit \'Save\' to give them a name so they can be used for future searches as well.\r\n\r\nWeights also carry over from one item list to another if you use the database menu, so going from a [url=/?items=2&filter=wt=51:48:49;wtv=83:67:58]weighted list of weapons[/url] to the [url=/?items=4&filter=wt=51:48:49;wtv=83:67:58]cloth armor listing[/url] will also maintain your current weight scale. \r\n[h3]Is it better to match sockets and gain the socket bonus, or use the best gems?[/h3]\r\nThe weighting system answers this for you automatically. It compares the score of matching gems plus the score of the socket bonus, to the score of the best gems it could put in that item. It will automatically put in the gems that result in the highest net rating, taking socket bonuses into account. When the socket colors are matched, the socket bonus text will be listed below the gems for each item. \r\n\r\n[h3]What are the default weight presets based on?[/h3]\r\nWe\'ve done a great deal of research, tracking down equivalence points for all of the classes. We\'d like to thank all of the hard-working theorycrafters at [url=http://elitistjerks.com/f47/t21302-theorycrafting_think_tank/]Elitist Jerks[/url], [url=http://forums.tkasomething.com/showthread.php?t=9542]TKA Something[/url], [url=http://shadowpanther.net/aep.htm]Shadow Panther[/url], [url=http://druid.wikispaces.com/Healing+Gear+List]The Druid Wiki[/url], [url=http://www.emmerald.net/]Emmerald[/url], [url=http://www.lootrank.com/wow/templates.asp]Lootrank[/url], [url=http://pawnmod.trenchrats.com/index.php]Pawn Mod[/url], and [url=http://www.codeplex.com/Rawr]Rawr[/url], as well as a host of threads on the World of Warcraft forums. They provided the inspiration for the weighted search and a starting point for our preset values.\r\n\r\n[/tab]\r\n\r\n[tab name=\"Helpful tips\"]\r\n\r\n[ul]\r\n[li]You can help us [b]improve[/b] our presets! Email your suggestions to [feedback].[/li]\r\n[li]Don\'t weight stats that your character is [b]already capped on[/b] (e.g. Hit rating). Be sure to tweak the presets as needed![/li]\r\n[li]You can adjust a preset by clicking on the \'show details\' button.[/li]\r\n[li]Once you have generated a weighting you like, you can bookmark that page. Then, if you browse around on other pages using the menus at the top, your weight scale will be applied to that page as well.[/li]\r\n[/ul]\r\n\r\n[/tab]\r\n\r\n[tab name=Why?]\r\n\r\n[h3]Why does it give a higher score to 2H weapons over 1H weapons, when using a 1H + OH is better?[/h3]\r\nThe scores are based off the stat weights of the item by itself. Two-handers rank higher because by themselves they do have better stats than a one-hander with nothing else in the off hand. If you add up the scores of a main hand and off hand item, the total score is what you should use to compare to that of a two-hander. We do not assume a score for your offhand item, as there is no way of knowing what you have or can obtain for that slot unless you do a weighted search for it. \r\n[h3]Why does the preset list X as more important than Y?[/h3]\r\nSome attributes come in unusual value ranges on items, which affects their equivalency to other stats. It does not mean that your should focus on or ignore that stat, but that a single point of it is worth more or less compared to other stats. Stats with high number ranges (armor, weapon damage, penetration, etc) will require smaller weight values, while stats with low number ranges (mana regeneration) will require much larger weight values.\r\n\r\nIn essence, giving mana regeneration a score of 100 and healing a score of 25 does [b]not[/b] say that mana regeneration is more important than healing, simply that each point of mana regeneration is the equivalent of 4 points of healing.\r\n[h3]Why don\'t you have a preset for PvP/Tier 6 Raiding/...? Why doesn\'t your preset give a stat value for X?[/h3]\r\nIf you would like to suggest changes to the existing presets or new presets for other specs or situations, please do so to [feedback]. \r\n[h3]Why doesn\'t the preset limit the items to X, Y, and Z?[/h3]\r\nThe weight presets are for sorting; filters are for limiting the search results. If you want to restrict the items you see, use the appropriate tool - the filter options. The only limit applied by the weight scales is that it will not display items with a score of 0 or less. You should continue to use the existing filtering system if you want to see items of a specific type, slot, source, speed, etc.\r\n[h3]Why does it suggest the gems it does for the sockets?[/h3]\r\nThe suggested gems are based on your weights. If you would like to see a different gem in the sockets, try increasing the weight of the appropriate stat. If you feel the weights in the presets need to be adjusted, please let us know at [feedback].\r\n\r\n[/tab]\r\n\r\n[/tabs]'),(NULL,NULL,0,'help=screenshots-tips-tricks',0,2,'[menu tab=2 path=2,13,2]\r\n\r\nWe thrive on user contributions! Quest data, database comments, forum posts - you name it, we love it! One of our favorite methods of contribution is via uploaded [b]screenshots[/b], images depicting various items, NPCs or quest details in the World of Warcraft. Users can submit screenshots to any database page which will then be reviewed by our staff and, upon approval, added to a database page! Taking and uploading screenshots is easy!\r\n\r\n[small]The information below is graciously provided by [url=http://us.blizzard.com/support/article.xml?locale=en_US&articleId=21048]Blizzard Support[/url].[/small]\r\n[h3]Taking Screenshots on Windows[/h3]\r\n[ul]\r\n[li]While in the game, press the Print Screen key on your keyboard.[/li]\r\n[li]You should see a \"Screen Captured\" message.[/li]\r\n[li]The screenshot will appear as a .JPG file in the Screenshots folder, in your main World of Warcraft directory.[/li]\r\n[li]You should be able to double click on the screenshot files to view the screenshots in Windows default image viewer.[/li]\r\n[/ul]\r\n\r\n[b]Extra notes for Windows Vista users[/b]\r\n[ul]\r\n[li]Due to extra security on the system the screenshots will be saved to the following folder:C:\\\\users\\\\*your user name*\\\\AppData\\\\Local\\\\VirtualStore\\\\Program Files\\\\World of Warcraft\\\\Screenshots[/li]\r\n[li]You may also have to turn on the ability to view hidden files as the AppData folder may be hidden.\r\n[ul]\r\n[li]Click the Start/Window button, select Control Panel, Appearance and Personalization, Folder Options.[/li]\r\n[li]Next click on the View tab, under the Advanced settings, click Show hidden files and folders, and click OK to finish.[/li]\r\n[/ul][/li]\r\n[/ul]\r\n\r\n[h3]Taking Screenshots on Mac[/h3]\r\n[ul]\r\n[li]Players can take a screenshot in-game using the keyboard key bound to the Print Screen functionality.[/li]\r\n[li]If you have a keyboard with an F13 key, press the key to take an in-game screenshot. Players without an F13 key on the keyboard can change the default Screen Shot key in the Key Bindings menu.[/li]\r\n[li]You should see a \"Screen Captured\" message.[/li]\r\n[li]The screenshot will appear as a JPEG file in the Screenshots folder, in your main World of Warcraft folder.[/li]\r\n[/ul]\r\n\r\nRemember to turn off your in-game UI using the Alt+Z (or ⌘+V) command! Upon taking your screenshot, you can then go in and use an image editor (such as the free program [url=http://www.getpaint.net]Paint.NET[/url]) to crop your image for faster upload. You can select specific sections of a screenshot to upload (if you are featuring a particular piece of armor, for example) and save the file, then simply upload your pre-cropped image directly! If not, you can easily crop your screenshot after uploading but before submitting using our handy tool.\r\n\r\nTo submit a screenshot, simply navigate to the database entry for which you\'ve taken a screenshot and navigate to the \'Contribute\' section. Select the \'Submit a screenshot\' tab and click \'Choose file\' to locate the file on your system. Remember that only PNG and JPG file types are accepted! Once you have selected the screenshot simply click \"Submit\" and you\'re on your way! You will then be able to crop the image if necessary before your image is finally submitted for review. Upon approval (which may take up to 72 hours) your screenshot will then be featured on the database page, as well as in a \'Screenshots\' tab in your user profile!\r\n\r\n\r\n[h2]Quality Tips[/h2]\r\n\r\n[screenshot url=STATIC_URL/images/help/screenshots/hinterlands.jpg thumb=STATIC_URL/images/help/screenshots/hinterlands2.jpg float=right]The Hinterlands[/screenshot]A good screenshot is like a miniature piece of art. It should showcase the main object, but take into account the details around it. The same 7 elements of art design come into play here, Line, Shape, Form, Space, Texture, Light & Color. We\'ll touch on several of these and how to make use of the in game settings and mechanics to enhance your pictures.\r\n\r\nTurn your resolution and color sampling as high as your computer can handle. Turn on all the image effects and details, but turn down the weather effects to the lowest setting. In general you want all your glow and spell effects maxed to really show the environment to its fullest potential (they actually help with the lighting too!) You may find a shot that you need to play with these settings to enhance, sometimes turning down environmental detail is helpful to remove extra grasses.\r\n\r\nWorld of Warcraft actually has an internal setting for screenshot quality, and by default that quality is set to [b]3/10[/b]. You can turn this up, though, in order to take higher quality screenshots. In order to do so, type this command into your chatbox:\r\n\r\n[code]/console screenshotQuality 10[/code]\r\n\r\nMost of the time taking the pictures from 1st person view works best, so zoom all the way in so that you\'re looking through your character\'s eyes. Occasionally the object might be too big (large NPCs especially) to use this view - if this is the case get as close to them as you can without having your body in the shot and swing the camera around to get the angle that you\'re looking for.\r\n\r\nPay attention to the light - a well lit picture is 10 times better than a dark one. You may even want to do a little color correcting before uploading - increase the brightness and contrast a touch. For instance - it\'s a lot easier to take pictures in sunny Stormwind than deep in the mountains of torch lit Ironforge. Daytime pictures also turn out better than night.\r\n\r\n[h3]Featuring Armor[/h3]\r\n\r\n[screenshot url=STATIC_URL/images/help/screenshots/armor.jpg thumb=STATIC_URL/images/help/screenshots/armor2.jpg float=right]Dreamwalker Spaulders[/screenshot]We want to see the armor! Not Joe Schmoe in the armor. In general you want close ups of the piece itself (except for full set pictures). Don\'t be afraid to submit a 4 inch picture of one glove. Once\'s it\'s cropped and loaded and shrunk down to the thumbnail it will look great!\r\n\r\nUse your best judgment when cropping armor pics, but remember - we want to see details of the armor - not the person or a far away image. Of course, this also applies to weapons or any other piece of equipment!\r\n\r\n[h3]Featuring NPCs[/h3]\r\n\r\n[screenshot url=STATIC_URL/images/help/screenshots/npc.jpg thumb=STATIC_URL/images/help/screenshots/npc2.jpg float=right]Cairne Bloodhoof [/screenshot]Full body shots should be the norm. If you can\'t get a good full shot (e.g. they\'re standing behind a counter) get the waist up shot. There\'s no need to include the on-screen text and titles of NPCs. The website already lists those, so just get in close and take a great shot of the NPC itself.\r\n\r\nGet down on their level - you may need to \"/sit\" or even \"/sleep\" to get a good view of something low to the ground (scorpions, boots, spiders, etc.)\r\n\r\nWhen capturing moving NPCs, try to get as much a head on front shot as you can, being willing to take a few hits while you take picture of a mob attacking you can make for a great shot. If you don\'t want to get your hands dirty, sitting in place for a while and waiting for it to path in front of you is often easier and faster than running around it trying to get your shot.\r\n\r\nTalking to friendly NPCs will usually make them face you - you can then spin around and get the best background for your picture. You may also catch them in an interesting motion or gesture.'),(NULL,NULL,0,'help=profiler',0,2,'[menu tab=2 path=2,13,6]Can\'t find the answer you were looking for? Just [url=/?aboutus#contact]contact us[/url], or post on our [url=/?forums&board=1]forums[/url]!\r\n\r\n[pad]\r\n\r\n[tabs name=profiler]\r\n\r\n[tab name=\"Browsing characters\"]\r\n\r\n[div float=right align=right][img src=STATIC_URL/images/help/profiler/menu.gif]\r\n[small]Navigating the menu to your battlegroup and realm.[/small][/div]We maintain a database of [i]millions[/i] of [url=http://www.wowarmory.com/]Armory[/url] characters, guilds, and arena teams that have been imported by our users. You can browse through this extensive list by visiting the main [url=/?profiles]profiles[/url] page and selecting a region, battlegroup, or realm from the menus at the top.\r\n\r\nThis will give you an unfiltered look at the players and guilds in the area you selected, with the most recently updated characters displayed first. You can also enter your characters name in the box at the top to jump directly to that character.\r\n\r\n[h3]Finding My Characters[/h3]\r\n\r\n[ul]\r\n[li]Use the breadcrumb listings at the top to browse to your region, battlegroup, and realm. When you do this, a box will appear in the listing at the top of the page. Enter your character\'s name in this box to be taken directly to your character. You can use the \"Claim Character\", which is located under the Manage Character button, to save a character to your [url=/user=fewyn#characters]user page[/url] for later viewing.[/li]\r\n[/ul]\r\n\r\n[i]Tip: Claimed characters can be made public or private as you choose—so you only show off the characters people want you to see! Basic information for the profiles will remain public, just as it is in the Armory—but any connection to your account will be hidden.[/i]\r\n\r\n[h3]Filters[/h3]\r\nBut that\'s not the only way to find a character! You can also search Profiles using our robust filter system, just the same way that you can search items, NPCs, or spells in game. Characters and guilds can be filtered by name, region, and realm to limit the number of displayed results.\r\n\r\nAdditionally, characters can be filtered by faction, level, race, and class – as well as a number of other unique and useful criteria. For example:\r\n\r\n[ul]\r\n[li][div float=right align=right][img src=STATIC_URL/images/help/profiler/filters.gif]\r\n[small]Searching for characters that match your criteria.[/small][/div]Let\'s see [url=/?profiles=us.draenor&filter=cl=8;ra=11;cr=35;crs=0;crv=450]all the Draenei mages on my server that have their tailoring maxed out[/url].[/li]\r\n[li]Hmm... I wonder if anyone is [url=/?profiles=eu&filter=na=Malgayne]using my name on European servers[/url]?[/li]\r\n[li]How do I compare to [url=/?profiles=us.draenor&filter=cl=2;minle=80;maxle=80;cr=7;crs=1;crv=50]other Retribution-specced paladins on my server[/url]?[/li]\r\n[li]How many [url=/?profiles&filter=cr=23;crs=0;crv=871]Bloodsail Admirals[/url] are there out there?[/li]\r\n[li]Who got caught wearing a [url=/?profiles&filter=cr=21;crs=0;crv=22279]Lovely Black Dress[/url]?[/li]\r\n[li]How many people on my server and faction [url=/?profiles=us.sentinels&filter=si=2;cr=23;crs=0;crv=2904]completed Heroic Ulduar[/url]?[/li]\r\n[/ul]\r\n\r\nWe\'ll be adding more filters as time goes on, so feel free to experiment – and let us know if you think of other ideas!\r\n\r\n[pad][pad][pad]\r\n\r\n[h3]Guild and Arena Team Rosters[/h3]\r\nWhen you click on a character\'s guild or arena team, you will be directed to a roster view listing all the characters that belong to it. The roster view displays additional information, including guild ranks and personal arena team ratings. You can further filter this information using the [b]Create a filter[/b] link, should you want to find characters matching specific criteria. Now its easy to find all of the crafters in your guild!\r\n\r\n[h3][img src=STATIC_URL/images/help/profiler/queue.gif float=right]Resync Queue[/h3]\r\nWhen a character resync is requested, it is added to the queue. The queue is used to make sure everyone\'s characters are updated and processed in the order they were submitted, without overloading the [url=http://us.battle.net/wow/en/]Battle.net Armory\'s API[/url] with requests. Whenever you access a character that does not exist in our database or has not been updated in more than 1 hour, it will automatically be added to the queue.\r\n\r\n[/tab]\r\n\r\n[tab name=\"General usage\"]\r\n\r\nThe profiler has a wealth of information it can display about characters and custom profiles, so it can seem daunting at first! Each of the sections are broken down in detail below.\r\n[h3]Basic Profile Information[/h3]\r\nAt the top of a profile you will see an expanded header with vital information about the profile itself. All profiles have an icon and the character\'s race, class and level; Armory characters display a link to the character\'s guild under the name, while custom profiles display a description set by the user that created it. A link to [b]Edit[/b] this information appears on the bottom line, allowing you to update a profile you created or make a new custom profile from an existing one.\r\n\r\n[ul]\r\n[li][img src=STATIC_URL/images/help/profiler/edit.gif float=right][b]Name [/b]– Give your profile a name! Names must start with a letter, and can only contain letters, numbers, and spaces.[/li]\r\n[li][b]Level[/b] – Select a level for your profile. Profiles must be at least level 10 (55 for Death Knights) and no more than level 85.[/li]\r\n[li][b]Race[/b] – Ever wonder what you\'d look like as a tauren instead of an orc? Choose any race for your profile, and the character model with automatically be updated.[/li]\r\n[li][b]Class[/b] – You can select any class you like, regardless of racial restrictions. See what your stats would be if you were a draenei druid![/li]\r\n[li][b]Gender[/b] – Select male or female to set your character\'s gender.[/li]\r\n[li][b]Icon[/b] – Icons are automatically generated for Armory characters and in game class/race combinations, but you can change the icon to any you like.[/li]\r\n[li][b]Description[/b] – Enter a tag line or brief description for the profile so you and others know what it is about.[/li]\r\n[li][b]Visibility[/b] – Public profiles will be visible on your user page and anyone can view a public profile. Private ones will not be displayed or visible to others.[/li]\r\n[/ul]\r\n[i]Note: If you edit a character in any way, it will become a custom profile. The reputations, achievements, and raid progress information will be removed.[/i]\r\n\r\n[h3]Managing Profiles[/h3]\r\nIn the upper right are a number of useful buttons for managing profiles without having to go back to your user page. Each of the buttons have several options that can be used to manage the character\'s page you are currently on and include the following options.\r\n\r\n[ul]\r\n[li][b]Custom Profile[/b]\r\n[ul][li][b]New[/b] – This is a quick link to creating a new, blank profile from scratch. It will open in a new window so you do not lose your current profile. This option is always available.[/li]\r\n[li][b]Save[/b] – Save any changes you have made to this profile. This option is only available for logged in users on profiles they own.[/li]\r\n[li][b]Save as[/b] – This will let you save your current changes under a new name. It is extremely useful for making copies of profiles! This option is only available for logged in users.[/li][/ul][/li]\r\n[li][b]Manage Character[/b]\r\n[ul][li][b]Resync[/b] – Request that the character be updated from the armory; it will be added to the queue. This option is only available on Armory character pages.[/li]\r\n[li][b]Claim character[/b] – Adds an Armory character to your user page. This is a good thing to do with all your alts. This option is only available for logged in users on Armory character pages.[/li]\r\n[li][b]Remove[/b] - Removes the character from your user page. Use this if you no longer play the character or have long since deleted it.[/li]\r\n[li][b]Pin/Unpin[/b] - Pin one of your characters so you can perform personalized searches throughout the database for missing or completed quests, achievements, recipes and more![/li]\r\n[/ul][/li]\r\n[/ul]\r\n\r\n[h3]From the User Page[/h3]\r\n[img src=STATIC_URL/images/help/profiler/userpage.gif float=right]All of your claimed Armory characters and custom profiles are listed in one convenient place on your user page. From the [b]Characters[/b] tab you can remove one or more claimed characters. The [b]Profiles[/b] tab allows you to create a new profile, delete profiles, or change the visibility settings of profiles. Your private profiles will not be visible to anyone else.\r\n\r\n[i]Tip: When you are logged in, all of your characters and custom profiles can be accessed from the [b]My profiles[/b] menu at the top right of any page![/i][pad]\r\n[h3]Saving Your Work[/h3]\r\nAny profile can be edited, even if you don\'t own it, but you\'ll probably want to save your work when you\'re done! You must have an account with us in order to save a profile. Once you\'ve created an account, you can bookmark any number of Armory characters and save up to 10 custom profiles. Premium users will be able to create even more, so upgrade if 10 just isn\'t enough! You can use the red buttons to save a profile from its page, and manage your existing profiles and characters from your user page. \r\n\r\n[/tab]\r\n\r\n[tab name=\"Inventory and talents\"]\r\n[img src=STATIC_URL/images/help/profiler/character.jpg height=300 float=right]The main tab for a profile is the character inventory, which includes a lot of the same information you would see by looking at your character pane in game. This tab is broken up into four key sections - the character view, quick facts box, statistics, and gear summary.\r\n\r\n[h3]Character View[/h3]\r\nThe first thing you\'ll notice, of course, is your character – as rendered by our custom built modelviewer, in all it\'s three-dimensional glory. You can turn the character with your mouse, and zoom in and out using the A and Z keys, just like the modelviewer elsewhere in the site. [b]We even pull your face, hair, and skin color information from the Armory![/b]\r\n\r\nOn either side of the character are inventory icons which you can right click on for a menu of options:\r\n\r\n[i]Tip: You can remove a gem or enchant by clicking None in the picker window or by right clicking on it in the gear summary.[/i]\r\n\r\n[ul]\r\n[li][img src=STATIC_URL/images/help/profiler/itemmenu.gif float=right][b]Equip... / Replace...[/b] – Selecting this option will give you a quick search box in which you can type an item\'s name. Click on the item or hit return to equip it.\r\nUnequip – Unequips the item, of course. :)[/li]\r\n[li][b]Add / Replace enchant...[/b] – The spell icon on the left shows if the item is enchanted. This opens a customized picker window with all enchants available for the item slot.[/li]\r\n[li][b]Add / Replace gem...[/b] – The icon on the left shows the socket color or socketed gem. Like the enchants, this opens a picker window with valid gems for the socket.[/li]\r\n[li][b]Extra socket[/b] – The check mark on the left indicates if a blacksmithing socket has been added to this item. Click to toggle on or off.[/li]\r\n[li][b]Clear Enhancements[/b] - This will remove all reforges, enchantments, gems and extra sockets from an item. Useful if you want to start fresh with an item.[/li]\r\n[li][b]Display on character[/b] – The checkmark on the left indicates if the item is displayed on the model. Click to toggle on or off – it works for more than just cloaks and helms![/li]\r\n[li][b]Compare[/b] – Adds the item to the [url=/?compare]item comparison tool[/url] and opens it in a new window to compare with other items.[/li]\r\n[li][b]Find upgrades[/b] – Uses our [url=/?help=stat-weighting]weighted search[/url] to find upgrades based on your talent spec.[/li]\r\n[li][b]Who wears this?[/b] – Creates a filtered list of other Armory characters who are also wearing the item.[/li]\r\n[/ul]\r\n\r\n[i]Tip: Items that can take enchantments but have no enchantment, or which have empty sockets, will even have a little notification in the tooltip![/i]\r\n\r\n[img src=STATIC_URL/images/help/profiler/quickfacts.gif float=right][h3]Quick Facts Box[/h3]\r\nOn the right hand side is a handy Quick Facts box that displays basic, defining information about a profile. This box is chock full of useful information, including talent spec, achievement points, and professions.\r\n\r\n[i]Tip: Any raid icon that\'s ringed in [color=c4]gold[/color] is a raid that the character has cleared![/i]\r\n[h3]Statistics[/h3]\r\nYou\'ll also notice that all of a profile\'s statistics are laid out beneath the character view. This is also all information you can get from the Armory (and then some), but we lay it out in a nice, convenient page so you can view it all at once – no more messing with drop down menus. You can also click on a statistic and expand it so you can see its tooltip information right there on the page—or click on the header to expand all the related statistics. Your statistics are updated as you edit any part of a profile, including race, class, level, items, enhancements, or talents – all in real time! [b]Statistic modifications from glyphs and buffs are not presently supported, but will be in the future.[/b]\r\n\r\n[i]Note: These statistics are calculated manually – they are not pulled from the Armory. Statistics calculations are still in beta and will ironed out as we go.[/i]\r\n\r\n[img src=STATIC_URL/images/help/profiler/statistics.gif float=center]\r\n\r\n[h3]Gear Summary[/h3]\r\n[div float=right align=right][img src=STATIC_URL/images/help/profiler/gearsummary.gif]\r\n[small]A warning message is displayed for missing enhancements.[/small][/div]Last on the character inventory tab, but not least, is the gear summary. This is a personalized list of all items worn by the character, with convenient column headers and in line filtering options. Use it to see where most of a character\'s items come from, what is the best and worst piece, and whether or not there are missing gems and enchants. Just in case the empty icons aren\'t clear enough, a warning appears at the top of the list if a character is missing gems, enchants, or blacksmith sockets. This [color=q10]warning[/color] is based on the professions of the character if it is an Armory profile, and otherwise shows you everything missing on custom profiles.\r\n\r\nThe gems and enchants can also be edited from within the gear summary, and have a few additional options not available in the character view. You can remove or replace an enhancement from here, and you can find upgrades using our [url=/?help=stat-weighting]weighted search[/url] – just like items!\r\n\r\n[h3]Talents[/h3]\r\nThe talents tab includes an inline version of our [url=/?talent]talent calculator[/url] with a full display of a character\'s talents. It is locked by default, but you can unlock it to begin editing talents, just as you would normally. There are two extra features in the Profiler\'s talent calculator: you can store and swap between two specs for each character, and export the current talent build to the calculator to link to your friends. When you change your talents (or swap between specs) your gear score and statistics will be updates real time!\r\n\r\n[/tab]\r\n\r\n[tab name=\"Other tabs\"]\r\n\r\n[h3]Reputation[/h3]\r\nThe reputation tab displays the complete faction information of an Armory character, with collapsible headers for each section. Its much easier to read than the tiny faction pane in game! Of course, you can link directly to the faction\'s page to get more information about that faction. \r\n[h3][img src=STATIC_URL/images/help/profiler/achievements.gif float=right]Achievements[/h3]\r\nThe achievements tab lists an Armory character\'s progress in each of the main achievement categories, and has a filterable list of achievements including date completed. All of the normal column and list filters are available, along with some new ones! You can filter the list by earned, in progress or complete achievements – complete are displayed by default – or click on any of the category progress bars to only display achievements from that category.\r\n\r\n[/tab]\r\n\r\n[tab name=Completion_Tracker]\r\n\r\n[img src=STATIC_URL/images/help/profiler/quests.jpg float=right width=450]You can use the Profiler\'s [b]Completion Tracker[/b] feature to keep track of your quests, achievements, pets, mounts, recipes, and more!\r\n\r\n[h3]Getting Started[/h3]\r\n\r\nIn order to start tracking your completion data, all you need to do is visit your character\'s page on the profiler and resync it. This will automatically collect data about your character\'s completed achievements, companion pets, mounts, quests, recipes, reputations and titles.\r\n\r\n[h3][img src=STATIC_URL/images/help/profiler/completion.jpg float=right]Tracking Your Completion Data[/h3]\r\n\r\nOnce you\'ve got your data up on the site, it will be available in the form of five new tabs: [b]mounts[/b], [b]companions[/b], [b]recipes[/b], [b]quests[/b], and [b]titles[/b].\r\n\r\nIf you open the mounts, companions, or titles tabs, you\'ll immediately be greeted by a list of all the entries you\'ve already completed. You can cycle through the different tabs to see the ones you already have, the ones you still have yet to collect, a complete list, or a list of just the ones you\'ve \"excluded\" (more on that shortly). You can also use the \"Search within results\" box to search the list based on a keyword, just like you can with other search results in the database.\r\n\r\nThe recipe, and quest tabs, like the Achievements tab, contain more entries—so you\'ll be presented with a box like the one shown above. From there, all you have to do is click one of the progress bars to see the complete tabbed list in each category.\r\n\r\n[h3]Exclusions[/h3]\r\n\r\nWhen you\'re trying to make sure we check off every quest, achievement, or mount on our list, everyone knows that there are some that you just don\'t want to bother with. To that end, we\'ve created [b]exclusions[/b].\r\n\r\n[img src=STATIC_URL/images/help/profiler/exclusions.jpg float=right]Using exclusions, you can flag certain quests, mounts, achievements, recipes, pets, or titles that \"don\'t count\" toward your completion total. When you exclude (for example) a quest, that quest no longer appears in \"incomplete\" listings, and the total number of quests in that category is reduced by one.\r\n\r\n[b]For example:[/b] There are 632 quests in the \"Eastern Kingdoms\" category. If I were to decide that [quest=367] is for noobs and I don\'t want to count it, then all I have to do is put a check in the box next to the quest and click \"Exclude\". After I do so, the Eastern Kingdoms progress bar will only show [i]631[/i] quests total—the remaining quest will appear in the \"Excluded\" tab but won\'t be counted for anything else.\r\n\r\nIf you want to re-include a quest, just go to the \"Excluded\" tab and then use the checkboxes to restore as many as you like. You can do the same thing for achievements, titles, mounts, pets, or recipes.\r\n\r\nIf you [b]complete[/b] a quest that you have excluded, it will show in the progress bar as a [b]+1[/b]. Example: If there are 31 quests in the \"Miscellaneous\" category, and I\'ve completed 20 quests and excluded 1, the progress bar will show [b]20/30[/b]. If I have completed [i]the quest that I excluded[/i], then the progress bar will show [b]20(+1)/30[/b]. If I then go on to complete ALL the quests in that category (including the one I excluded), the progress bar will show [b]30(+1)/30[/b].\r\n\r\n[b]Exclusion Manager[/b]\r\nThe companions and mounts tabs let you manage your exclusions en masse with the Exclusion Manager. Just click the \"Manage Exclusions\" button on top of the tabs to see a list of convenient categories you might want to exclude. There\'s also a \"reset all\" button here to let you wipe all of your exclusions and start over.\r\n\r\n[b]Note:[/b] The Exclusion Manager is currently only available for companions and mounts.\r\n\r\n[i]Tip: Exclusions are tied to your account, not to a particular character. This is so even when you look at someone else\'s character, you\'re judging them by [/i]your[i] completion standards, not anyone else\'s![/i] \r\n\r\n[/tab]\r\n\r\n[tab name=Calculations]\r\n\r\nMost of the information we display is pretty straightforward. A lot of it, particularly the stats on items, is readily available in our database and on various tooltips. There are some new numbers on profile pages that you may ask, what does this number mean? How was it calculated?\r\n[h3]Base Statistics[/h3]\r\nA character\'s five base statistics are determined primarily by his or her class and level. This base amount has a modifier applied to it depending on the character\'s race. We gathered an extensive amount of data from the armory to come up with these base numbers, using untalented individuals of every race, class, and level combination. Because racial modifiers are consistent, we are able to create statistics for \"fake\" race and class combos using the data we already know. However, the Armory does not give data on characters below level 10 or Death Knights below level 55, so we have no statistic information for these profiles. To simplify things, we have set a minimum level for custom profiles based on the available statistics.\r\n[h3]Gear Score[/h3]\r\nOkay, so a lot of sites have gear scores. Most of them (ours included) are based around the [url=http://www.wowwiki.com/Item_level]item budget[/url] Blizzard uses to determine how much of each stat can be on an item. This budget is calculated using the item\'s level, quality, and slot, and we use the budget as the item\'s gear score. You can view a complete breakdown of an item\'s gear score by mousing over it in the [url=/?help=profiler#profiler-inventory-and-talents]gear summary[/url] at the bottom of the character tab. You can view a breakdown of a profile\'s total gear score by mousing over it in the Quick Facts box, also on the character tab.\r\n\r\nEach gear score is color coded based on the item levels of the gear in reference to the character level. [b][color=q0]Grey[/color][/b] for poor, [b][color=q1]White[/color][/b] for common, [b][color=q2]Green[/color][/b] for uncommon, [b][color=q3]Blue[/color][/b] for rare, [b][color=q4]Purple[/color][/b] for epic and [b][color=q5]Orange[/color][/b] for legendary. For example, a level 70 character wearing high item-level, raiding epics from [zone=3606] and [zone=3959] will have a purple-colored gearscore, as their items are considerably \"epic\" quality for their level. However, the same character at 80, if wearing this same gear, will have the gearscore colored blue as the items are of lower-than-optimal quality for their level.\r\n\r\nThe value of an empty socket was generated using the gear score of appropriate gems for the item in question, and subtracted from the item\'s score. This allows us to score unsocketed items lower than an item without sockets of the same level, quality, and slot. Items with better than expected gems will receive higher scores, and items with lower quality gems (or no gems at all) will receive lower scores.\r\n\r\nThe values of enchants are based off of the level of the enchantment. Endgame enchantments are 20 points, profession perks are 40 points, etc. The numbers go down from there.\r\n\r\nYou may notice that some profiles have different gear scores for the same item. There is an extreme difference in budget between a two-handed or one-handed weapon, which causes a discrepancy in scores between characters who should be fairly equal according to the level of their gear. To address this, the gear score of weapons has been normalized so that a character with appropriate weapon choices has the equivalent score of two two-handed weapons. Appropriate weapons are determined by your class and spec; for example, an enhancement shaman should dual wield one handed weapons, a protection warrior should have a one-hander and shield, etc. For classes which the melee weapons don\'t really matter – like hunters or spellcasters – anything they can use is considered appropriate.\r\n\r\n[i]Note: Gear score does not take into account the stats of the item. It is a measurement of quality of gear, not whether the stats on the gear are suited to the character\'s spec.[/i]\r\n\r\n[h3]Guild Scores[/h3]\r\nGuild gear scores and achievement points are derived using a weighted average of all of the known characters in that guild. Guilds with at least 25 level 80 players receive full benefit of the top 25 characters\' gear scores, while guilds with at least 10 level 80 characters receive a slight penalty, at least 1 level 80 a moderate penalty, and no level 80 characters a severe penalty. This is to prevent small guilds and bank alts from appearing to have higher scores than legitimate raiding guilds. Instead of being based on level, achievement point averages are based around 1,500 points, but the same penalties apply.\r\n\r\n[/tab]\r\n\r\n[/tabs]'),(8,577,0,NULL,0,2,'[minibox]\n[h2]Steamwheedle Cartel[/h2]\n[faction=21]\n[faction=577]\n[faction=369]\n[b]Everlook[/b]\n[/minibox]\n\n[b]Everlook[/b], the faction of the town Everlook, is a trading post is run by the goblins of the Steamwheedle Cartel. It lies at the crossroads of [zone=618]\'s main trade routes.\n\n[h3]General Information[/h3]\nThis town is the last point of civilization before reaching Hyjal Summit. It is run by goblins as a trading post and is officially neutral to all races and factions. Even so, pilgrims allowed to venture up to the World Tree stop here, but otherwise this is the highest that merchants and explorers may venture without the night elves’ permission. Everlook would offer a commanding view of Kalimdor, if it were not at such a high altitude that clouds constantly shroud the mountain’s lower flanks.\n\nEverlook is the only major goblin outpost in northern Kalimdor, and it serves several purposes. First, it serves as the base of operations for goblin thorium and arcanite miners since Winterspring has some of the few untapped veins of those materials on the continent. Second, it serves as a center of trade between the Alliance and the Horde. While Everlook is hardly as safe as Moonglade, generally the Alliance and the Horde treat each other fairly well there. Additionally, Everlook is a frequent stop-off and resupply point for the faithful who make the pilgrimage through Winterspring to Hyjal Summit.\n\n[h3]Reputation[/h3]\nReputation for Everlook and the Steamwheedle Cartel is mostly gained from quests in Winterspring. Having a friendly or higher reputation will make the guards help you in case of initiated violence against you.'),(NULL,NULL,0,'help=talent-calculator',0,2,'[menu tab=2 path=2,13,4]Can\'t find the answer you were looking for? Just [url=/?aboutus#contact]contact us[/url], or post on our [url=/?forums&board=1]forums[/url]! \r\n\r\n[toc]\r\n\r\n[h2]General Usage[/h2]\r\n[ul]\r\n[li][screenshot url=STATIC_URL/images/help/talent-calculator/glyphs.jpg thumb=STATIC_URL/images/help/talent-calculator/glyphs2.jpg width=268 height=218 float=right][/screenshot][b]Selecting a class[/b] - Easily select a class\' talent tree by chosing from the class icon at the top, or from the dropdown menu. Clicking on a class\' name at the top left of the calculator will open that class\' page here on on this site, providing even more detailed information![/li] \r\n[li][b]Adding or removing talent points[/b] - To add points in a talent simply click the appropriate talent. To remove points, you can either right-click (or Shift+click) the talent.[/li]\r\n[li][b]Adding glyphs[/b] - Click on an empty glyph slot to open a picker window from which you can make your selection. To remove a glyph, simply right-click (or Shift+click) that glyph.[/li]\r\n[li][b]Linking to a build[/b] – Simply copy the auto-updating URL from your browser\'s address bar.[/li]\r\n[/ul]\r\n\r\n[h2]Tools + Options[/h2]\r\n[ul]\r\n[li][b]Reset all[/b] - Resets all talents across all trees.[/li]\r\n[li][img src=STATIC_URL/images/help/talent-calculator/options.jpg float=right][b]Reset tree[/b] - Clicking the red X at the top right corner of a talent tree will reset all talents in that particular tree. Other trees will not be reset.[/li]\r\n[li][b]Lock / Unlock[/b] - Locks or unlocks the talent build, preventing (or allowing) changes to be made. Linking to a build will automatically lock talents.[/li]\r\n[li][b]Import[/b] – Displays a pop-up text window where you can enter the URL of a talent build made with [url=http://www.wowarmory.com/talent-calc.xml]Blizzard\'s talent calculator[/url]. Be sure that you first select the \"Link to this build\" option in the Blizzard talent calculator so that the URL will be properly formatted for importing.[/li]\r\n[li][b]Print[/b] - Opens up a new, printer-friendly page with a textual representation of your chosen talents. Nice if you want to paste the talents you\'ve chosen somewhere, and would prefer it written out.[/li]\r\n[li][b]Link[/b] - Locks your chosen talents and creates a link to your build. Use this option to easily create a URL to share your build with others![/li]\r\n[/ul]\r\n\r\n[h2]Useful Tips[/h2]\r\n\r\n[ul]\r\n[li]When the calculator is locked, you can click talents and glyphs to view their corresponding spell or item page.[/li]\r\n[li]If you\'re building a third-party application, you can link to our talent calculator by using Blizzard-style URLs such as:\r\n[code]HOST_URL?talent#hunter-512002015051122431005311500053052002300100000000000000000000000000000000000000000[/code][/li]\r\n[/ul]'),(NULL,NULL,0,'help=modelviewer',0,2,'[menu tab=2 path=2,13,1]\r\n\r\n[url=item=35350][img src=STATIC_URL/images/help/modelviewer/ss-viewin3d.gif float=right][/url]Aowow has a model viewer that will let you see the items and NPCs in the game in full 3D!\r\n\r\nYou can use the dropdown menus to select which character model you want to display armor pieces on, and the model viewer will remember your choice.\r\n\r\nThere are two different versions of the model viewer available, one written in Flash, and the other one written in Java. Aowow should remember which version you used last time, and will automatically open that model viewer the next time you click on the \"View in 3D\" button.\r\n\r\nIf you have any issues, please report them [url=/?forums&topic=202524]here[/url]!\r\n\r\n[i]Tip: You can close the box by clicking anywhere outside of the box.[/i]\r\n\r\n[h2]Modes[/h2]\r\n\r\n[tabs name=mode]\r\n\r\n[tab name=Flash]\r\n\r\n[url=item=34092][img src=STATIC_URL/images/help/modelviewer/ss-flash.png float=right][/url]The [b]Flash[/b] viewer is simple, quick to load, and should work on nearly all browsers. The Flash viewer is the default viewer, and all models will automatically load in the Flash Viewer unless you specify otherwise.\r\n\r\nIt requires the latest version of [url=http://www.adobe.com/go/BONRN]Flash[/url] to be installed on your computer.\r\n\r\n[h3]Controls[/h3]\r\n[ul]\r\n[li][b]Rotate[/b] – Click and drag / arrow keys[/li]\r\n[li][b]Zoom[/b] – Mousewheel / A & Z keys[/li]\r\n[/ul]\r\n\r\n[h3]Features[/h3]\r\n[ul]\r\n[li]Motion blur[/li]\r\n[li]Full screen mode[/li]\r\n[/ul]\r\n\r\n[/tab]\r\n\r\n[tab name=Java]\r\n\r\n[url=/?item=35350][img src=STATIC_URL/images/help/modelviewer/ss-java.png float=right][/url]The Java viewer is slower to initialize than the Flash Viewer, but once it\'s initialized it renders in [b]much greater[/b] detail. Most browsers will only need to initialize it once, and subsequent loads will be much faster. Some browsers may ask you to accept a security certificate when you initialize the viewer.\r\n\r\nIt requires the latest version of [url=http://jdl.sun.com/webapps/getjava/BrowserRedirect?locale=en&host=www.java.com]Java[/url] to be installed on your computer.\r\n\r\n[h3]Controls[/h3]\r\n[ul]\r\n[li][b]Rotate[/b] – Click and drag[/li]\r\n[li][b]Zoom[/b] – Mousewheel[/li]\r\n[li][b]Move[/b] – Right-click and drag[/li]\r\n[/ul]\r\n\r\n[h3]Features[/h3]\r\n[ul]\r\n[li]3D acceleration[/li]\r\n[li]Animations on NPCs, character models, small pets, and mounts[/li]\r\n[/ul]\r\n\r\n[/tab]\r\n\r\n[/tabs]\r\n'),(NULL,NULL,0,'tooltips',0,2,'[menu tab=2 path=2,10]\r\n\r\n[div float=right align=right][url=http://wow.joystiq.com/2010/04/14/breakfast-topic-using-irl-irl/][img src=STATIC_URL/images/help/tooltips/ss-wowcom.png][/url]\r\n[small]Tooltips in action on [url=http://wow.joystiq.com/2010/04/14/breakfast-topic-using-irl-irl/]WoW Insider[/url][/small][/div]\r\n\r\nIt\'s never been easier to add tooltips to your site.\r\n\r\n[ol]\r\n[li]Add this piece of HTML code in the section of your page:\r\n[code][/code][/li]\r\n[li]You are done![/li]\r\n[/ol]\r\n\r\nLinks found on your site will now sport a [b]tooltip[/b] and an [b]icon[/b]. The following pages are supported: achievement, profile, item, npc, object, spell, quest. Icons show up by default, you can customize the colors of your links, and easily rename them!\r\n\r\nYou can check out this [url=STATIC_URL/widgets/power/demo.html]working demo[/url], and see how easy it is!\r\n\r\n[h2]Related[/h2]\r\n\r\n[tabs name=Related]\r\n\r\n[tab name=\"Advanced usage\"]\r\n\r\nOnce you have the [/code]\r\n[/tab]\r\n\r\n[tab name=\"XML feeds\"]\r\n\r\n[h3]Items[/h3]\r\nAlso available are our item XML feeds. Every item in the database has a corresponding XML feed. You can reach those feeds either by ID or by name. For example:\r\n\r\n[ul]\r\n[li]By ID: HOST_URL?item=52021&xml[/li]\r\n[li]By name: HOST_URL?item=iceblade%20arrow&xml[/li]\r\n[/ul]\r\n\r\n[/tab]\r\n\r\n[tab name=\"Other resources\"]\r\n\r\nInterested in using our script in your forum? Check out [url=http://wowhead.com/forums&topic=3464]this thread[/url] for information on implementing it on many popular forum systems (phpBB, vBulletin, etc.) or check out the handy guides written by Wowheads users:\r\n\r\n[ul]\r\n[li][url=http://wowhead.com/forums&topic=3464#p37094]vBulletin[/url][/li]\r\n[li]phpBB: [url=http://wowhead.com/forums&topic=3464#p37492]2.x.x[/url] - [url=http://wowhead.com/forums&topic=3464.6#p58403]2.x.x Mod Version[/url] | [url=http://wowhead.com/forums&topic=14347&p=126922]3.0[/url] [small]by craCkpot[/small] - [url=http://wowhead.com/forums&topic=3464#p37204]3.0[/url] [small]by marcimi[/small] - [url=http://wowhead.com/forums&topic=3464.3#p42858]3.0 Mod Version[/url][/li]\r\n[li][url=http://wowhead.com/forums&topic=3464#p37618]Simple Machines Forum (SMF)[/url][/li]\r\n[li][url=http://wowhead.com/forums&topic=3464.3&p=4080#p40631]Invision Power Board (IPB)[/url][/li]\r\n[li][url=http://wowhead.com/forums&topic=3464.3&p=42952#p42952]WordPress Blog[/url] ([url=http://wowhead.com/forums&topic=3464.4#p43652]Plugin Version[/url])[/li]\r\n[li][url=http://wowhead.com/forums&topic=3464.7&p=63338#p61443]PHP Nuke-Evolution[/url][/li]\r\n[li][url=http://wowhead.com/forums&topic=3464.3#p43232]MyBB[/url][/li]\r\n[li][url=http://wowhead.com/forums&topic=3464.6#p48648]TikiWiki[/url][/li]\r\n[li][url=http://wowhead.com/forums&topic=3464.6#p49640]YaBB[/url][/li]\r\n[li][url=http://wowhead.com/forums&topic=3464.5#p46801]Drupal[/url][/li]\r\n[li][url=http://wowhead.com/forums&topic=3464.3#p42456]PunBB[/url][/li]\r\n[li][url=http://wowhead.com/forums&topic=10938]Dojo[/url][/li]\r\n[/ul]\r\n\r\n[/tab]\r\n\r\n[/tabs]'),(NULL,NULL,0,'searchbox',0,2,'[menu tab=2 path=2,16]\r\n\r\nThe code below will produce an iframe that contains the Aowow logo and a search box.\r\n\r\n[code]\r\n[/code]\r\n\r\n[h3]Parameters[/h3]\r\n\r\n[ul]\r\n[li][b]aowow_searchbox_format[/b] – String that specifies how big the iframe should be. The following values can be used:\r\n[pad]\r\n[table width=100%]\r\n[tr]\r\n[td width=20% align=center valign=top]\r\n\"160x200\"\r\n[img src=STATIC_URL/images/help/searchbox/preview-160x200.png]\r\n[/td]\r\n[td width=20% align=center valign=top]\r\n\"120x200\"\r\n[img src=STATIC_URL/images/help/searchbox/preview-120x200.png]\r\n[/td]\r\n[td width=20% align=center valign=top]\r\n\"160x120\"\r\n[img src=STATIC_URL/images/help/searchbox/preview-160x120.png]\r\n[/td]\r\n[td width=20% align=center valign=top]\r\n\"150x120\"\r\n[img src=STATIC_URL/images/help/searchbox/preview-150x120.png]\r\n[/td]\r\n[td width=20% align=center valign=top]\r\n\"120x120\"\r\n[img src=STATIC_URL/images/help/searchbox/preview-120x120.png]\r\n[/td]\r\n[/tr]\r\n[/table]\r\n[/li]\r\n[/ul]\r\n\r\n[h3]Tips[/h3]\r\n\r\n[ul]\r\n[li]You can style the iframe (e.g. adding a border) by using the following class name in your CSS code:\r\n[code].aowow-searchbox { ... }[/code][/li]\r\n[/ul]'),(NULL,NULL,0,'searchplugins',0,2,'[menu tab=2 path=2,8]\r\n\r\nSearch plugins make it easy to search the database right from your browser!\r\n\r\n[toc h3=false]\r\n\r\n[h2][img src=STATIC_URL/images/help/searchplugins/firefox.png border=0 margin=5 float=left]Firefox[/h2]\r\n\r\n[div float=right align=right][img border=2 src=STATIC_URL/images/help/searchplugins/os-firefox.png][/div]\r\n\r\n[script]\r\nfunction addPlugin()\r\n{\r\n if (typeof window.external.AddSearchProvider == \"function\")\r\n window.external.AddSearchProvider(\"STATIC_URL/download/searchplugins/aowow.xml\");\r\n else\r\n alert(\"This feature is unavailable.\");\r\n}\r\n[/script]\r\n[pad]\r\nEither\r\n[ul]\r\n[li]Click on the button below to install the search plugin in your browser or[/li]\r\n[li]Right-click your address bar and then clck on \"Add AoWoW\" or[/li]\r\n[li]Click on the [img src=STATIC_URL/images/icons/add.png border=0] on the browser search bar and then on [img src=STATIC_URL/images/icons/add.png border=0] \"Add search engine\"[/li]\r\n[/ul]\r\n\r\n[pad]\r\n[html]Install pluginInstall plugin[/html]\r\n[div clear=both][/div]\r\n\r\n[h2][img src=STATIC_URL/images/help/searchplugins/edge.png border=0 float=left][img src=STATIC_URL/images/help/searchplugins/chrome.png border=0 float=left]MS Edge / Google Chrome[/h2]\r\n\r\n[div float=right align=right][img border=2 src=STATIC_URL/images/help/searchplugins/os-edge.png][/div]\r\n[pad]\r\nFor Chrome-based browsers go to settings and fill in the add search engine form as shown.\r\n[pad]\r\n[div width=500px]\r\n[pre]HOST_URL/?search=%s[img src=STATIC_URL/images/icons/pages.gif float=right][/pre]\r\n[/div]\r\n[script]\r\nsetTimeout(() => $WH.clickToCopy($WH.qs(\"pre > img\"), \"HOST_URL/?search=%s\"), 100);\r\n[/script]\r\n[pad]\r\nSave your changes, and you\'ll be able to perform Aowow searches by typing \"db\" followed by the search terms in the address bar (e.g. db sword).\r\n[div clear=both][/div]\r\n'),(NULL,NULL,2,'page-not-found',0,2,'[tooltip name=AO815][b][color=q4]AO-815 Moteur Principal de Stabulation[/color][/b]\n[color=white]Lié lorsque utilisé\nUnique[/color]\n[color=q2]Utilise: Appelle le pouvoir de l\'Interwebs pour\ninvoquer l\'information demandé à Aowow.[/color]\n[color=q]\"En tout cas, c\'est ce que c\'est supposé faire...\"[/color][/tooltip]Quoi? Comment avez-vous... oubliez ça!\n\nIl semblerait que la page demandée n\'ait pas été trouvée. En tout cas, pas dans cette dimension.\n\nPeut-être que quelques réglages au [span class=tip tooltip=AO815][color=q4][u][AO-815 Moteur Principal de Stabulation][/u][/color][/span] pourraient résulter en l\'apparition soudaine de la page![pad][pad]\n\nOu vous pouvez essayer de [url=?aboutus#contact]nous contacter[/url] - la stabilité du AO-815 est discutable et vous ne voudriez pas un autre accident...\n\n[h2]Liens[/h2]\n[ul]\n[li]Retour à la [url=?]page d\'accueil[/url][/li]\n[li][url=?forums&board=1]Forum[/url] de feedback[/li]\n[/ul]'),(NULL,NULL,0,'faq',0,2,'[small]no questions have been asked yet[/small]\r\n\r\nbesides .. yes, i\'m insane.'),(NULL,NULL,0,'whats-new',0,2,'[small]this page for example[/small]'),(NULL,NULL,0,'aboutus',0,2,'[h3]This is [s]Sparta![/s] [u]Aowow[/u][/h3]\r\n\r\nA project for private servers to sensibly display the vast amount of data a private server contains.\r\n\r\nBuilt with TrinityCore in my neck, but i\'m trying to get away from that .. some time.\r\nWith it\'s own data structure it shouldn\'t be too hard to write a converter for MaNGOS, Ascent or whatever software you prefere.\r\n\r\nThe expected version is 3.3.5 (12340), everything else will get messy.'),(NULL,NULL,3,'page-not-found',0,2,'[tooltip name=AO815][b][color=q4]AO-815 Großkonfabulierungsmaschine[/color][/b]\n[color=white]Bei Benutzung gebunden\nEinzigartig[/color]\n[color=q2]Benutzen: Ersucht die Mächte der Internetze darum,\nAowow die benötigten Informationen zukommen zu lassen.[/color]\n[color=q]\"Das sollte es im Prinzip eigentlich tun...\"[/color][/tooltip]Was? Wie hast du... vergesst es!\n\nAnscheinend konnte die von Euch angeforderte Seite nicht gefunden werden. Wenigstens nicht in dieser Dimension.\n\nVielleicht lassen einige Justierungen an der [span class=tip tooltip=AO815][color=q4][u][AO-815 Großkonfabulierungsmaschine][/u][/color][/span] die Seite plötzlich wieder auftauchen![pad][pad]\n\nOder, Ihr könnt es auch [url=?aboutus#contact]uns melden[/url] - die Stabilität des AO-815 ist umstritten, und wir möchten gern noch so ein Problem vermeiden...\n\n[h2]Links[/h2]\n[ul]\n[li]Zur [url=?]Titelseite[/url] zurückkehren[/li]\n[li][url=?forums&board=1]Forum[/url] für Rückmeldungen[/li]\n[/ul]'),(NULL,NULL,6,'page-not-found',0,2,'[tooltip name=AO815][b][color=q4]Dispositivo de confabulación suprema AO-815[/color][/b]\n[color=white]Se liga al usar\nÚnico[/color]\n[color=q2]Uso: Clama a los poderes de Internet para\ninvocar información requerida a Aowow.[/color]\n[color=q]\"Al menos, eso es lo que se supone que hace...\"[/color][/tooltip]¿Pero qué? ¿Cómo? .... ¡olvídalo!\n\nParece que la página que buscas no pudo ser encontrada. Al menos, no en esta dimensión.\n\n¡Quizá un par de ajustes al [span class=tip tooltip=AO815][color=q4][u][Dispositivo de confabulación suprema AO-815][/u][/color][/span] puede que hagan que la página aparezca de repente![pad][pad]\n\nO, puedes intentar [url=?aboutus#contact]contactar con nosotros[/url] - la estabilidad del AO-815 es debatible y no queremos otro accidente...\n\n[h2]Enlaces[/h2]\n[ul]\n[li]Volver a la [url=?]página principal[/url].[/li]\n[li]Foro del [url=?forums&board=1]feedback[/url].[/li]\n[/ul]'),(NULL,NULL,0,'page-not-found',0,2,'[tooltip name=AO815][b][color=q4]AO-815 Major Confabulation Engine[/color][/b]\n[color=white]Binds when used\nUnique[/color]\n[color=q2]Use: Calls on the powers of the Interwebs to\nsummon requested information to Aowow.[/color]\n[color=q]\"At least, that\'s what it\'s supposed to do...\"[/color][/tooltip]What? How did you... nevermind that!\n\nIt appears that the page you have requested cannot be found. At least, not in this dimension.\n\nPerhaps a few tweaks to the [span class=tip tooltip=AO815][color=q4][u][AO-815 Major Confabulation Engine][/u][/color][/span] may result in the page suddenly making an appearance![pad][pad]\n\nOr, you can try [url=?aboutus#contact]contacting us[/url] - the stability of the AO-815 is debatable, and we wouldn\'t want another accident...\n\n[h2]Links[/h2]\n[ul]\n[li]Return to the [url=?]homepage[/url][/li]\n[li]Feedback [url=?forums&board=1]forum[/url][/li]\n[/ul]'),(NULL,NULL,0,'help=markup-guide',0,2,'[menu tab=2 path=2,13,7]Here we have quite a few nifty markup tags that users can insert into their comments and forum posts to improve the style and easily link to database entries! Many of these tags can easily inserted using the corresponding icon or dropdown menu found above the text box. We\'ve put together this quick reference for all of these handy tags for you guys so you can get on your way to making high quality posts and comments!\n\n[h2]Formatting Tags[/h2]\n[h3]Bold[/h3]\n\\[b]text[/b]\n\n[h3]Line break[/h3]\n\\[br] -> inserts a line break.\n\n[h3]Code[/h3]\n\\[code]text[/code] -> creates a block of text that ignores markup and uses a monospace font.\n\n[h3]Horizontal Rule[/h3]\n\\[hr] -> creates a horizontal rule\n\n[h3]Italics[/h3]\n\\[i]text[/i] -> [i]text[/i]\n\n[h3]Preformatted text[/h3]\n\\[pre]text[/pre] -> shows text with all whitespace preserved in a monospace font, but allows markup\n\n[h3]Strikethrough[/h3]\n\\[s]text[/s] -> [s]text[/s]\n\n[h3]Small text[/h3]\n\\[small]text[/small] -> [small]text[/small]\n\n[h3]Subscript[/h3]\n\\[sub]text[/sub] -> [sub]text[/sub]\n\n[h3]Superscript[/h3]\n\\[sup]text[/sup] -> [sup]text[/sup]\n\n[h3]Underline[/h3]\n\\[u]text[/u] -> [u]text[/u]\n\n[h2]Database Tags[/h2]\n\n\n[b]For all database tags:[/b]\nOptional attributes: site/domain (both work identically, only use one)\nValid options are: en (default), cn, de, es, fr, ru.\nThe purpose of these is to link to localized versions of items with the pretty db tags.\n[b]Example:[/b] \\[achievement=3579 domain=ru] -> [achievement=3579 domain=ru] \n\n[h3]Achievements[/h3]\n\\[achievement=3579] -> [achievement=3579]\n\n[h3]Classes[/h3]\n\\[class=11] -> [class=11]\n\n[h3]Events[/h3]\n\\[event=1] -> [event=1]\n\n[h3]Factions[/h3]\n\\[faction=749] -> [faction=749]\n\n[h3]Items[/h3]\n\\[item=12345] -> [item=12345]\n\nTo hide the icon: \\[item=12345 icon=false] -> [item=12345 icon=false]\n\n[h3]Itemsets[/h3]\n\\[itemset=699] -> [itemset=699]\n\n[h3]NPCs[/h3]\n\\[npc=32906] -> [npc=32906]\n\n[h3]Objects[/h3]\n\\[object=1733] -> [object=1733]\n\n[h3]Pets[/h3]\n\\[pet=45] -> [pet=45]\n\n[h3]Quests[/h3]\n\\[quest=7981] -> [quest=7981]\n\n[h3]Races[/h3]\n\\[race=11] -> [race=11]\n\n[b]To specify the gender of the icon:[/b] \\[race=11 gender=1] -> [race=11 gender=1] - 0 is male, 1 is female\n\n[h3]Skills[/h3]\n\\[skill=171] -> [skill=171]\n\n[h3]Spells[/h3]\n\\[spell=52398] -> [spell=52398]\n\\[spell=31565 buff=true] -> [spell=31565 buff=true]\n\n[h3]Statistics[/h3]\n\\[statistic=1076] -> [statistic=1076]\n\n[h3]Zones[/h3]\n\\[zone=3959] -> [zone=3959]\n\n[h2]HTML Tags[/h2]\n\n[h3]Anchor[/h3]\n\\[anchor=text] -> creates an anchor with the name \\\"text\\\" at this point.\n\n[h3]Ordered List[/h3]\n\\[ol]\\[li]list item[/li][/ol] -> [ol][li]list item[/li][/ol]\n\n[h3]Tables[/h3]\n[b]\\[table][/b]\nBorder: \\[table border=2]\nSpacing: \\[table cellspacing=2]\nPadding: \\[table cellpadding=2]\nWidth: \\[table width=500px] - Valid units are px, em, %\n\n[b]\\[tr][/b] - No attributes\n\n[b]\\[td][/b]\nAlign: \\[td align=right] - Valid options are left, right, center, justify\nVertical align: \\[td valign=baseline] - Valid options are top, middle, bottom, baseline\nColumn span: \\[td colspan=2]\nRow span: \\[td rowspan=2]\nWidth: \\[td width=500px] - Valid units are px, em, %\n\n[h3]Unordered List[/h3]\n\\[ul]\\[li]list item[/li][/ul] -> [ul][li]list item[/li][/ul]\n\n[h3]URLs[/h3]\n\\[url=http://www.wowhead.com]Wowhead[/url] -> [url=http://www.wowhead.com]Wowhead[/url]\n\\[url]http://www.wowhead.com[/url] -> [url]http://www.wowhead.com[/url]\n\\[url=http://www.google.com rel=item=12345]Rel link[/url] -> [url=http://www.google.com rel=item=12345]Rel link[/url]'),(8,589,0,NULL,0,2,'The [b]Wintersaber Trainers[/b] is an Alliance-only faction consisting of only two night elven NPCs that can both be found in [zone=618]. Currently, the only questgiver is [npc=10618], who is located at the top of Frostsaber Rock in Winterspring. Upon reaching exalted with this faction, Rivern will sell a special mount, the [item=13086].\n\nThis faction\'s mount is the only epic mount (100% riding speed) attainable in the game which only requires 75 riding skill (and thus only costs 90 Gold). The faction is noted for having no Horde counterpart and having the longest and most repetitive reputation grind of the entire game. The first quest can be attained at level 58, while the other two are attainable at level 60.\n\n[h3]Reputation[/h3]\nReputation with the Wintersaber Trainers can only be obtained through three repeatable quests. There are no faction items or mobs that reward repuation directly.\n\n[b]Neutral 0 to 1500[/b]\nOnly one repeatable quest will available at first, so until neutral 1500/3000 is reached the [quest=4970] quest should be repeated. Any Shardtooth and Chillvind mob in Winterspring will drop these. This quest should be done solo as the drop rates are low and not shared if others have the quest.\n\n[b]Neutral 1500 to Exalted[/b]\nHalfway through neutral the [quest=5201] quest will be available. This quest requires to kill 10 Winterfall mobs in the Winterfall Village, just east of Everlook. If the quest [quest=8464] has been done with the [faction=576], [item=21383] can drop from the Winterfall mobs. If a player wants both reputations, saving these until revered with Timbermaw Hold will result in a lot of \"free\" reputation.\n\nThis quest can be done in groups for increased speed. Players grinding either Wintersaber Trainers or Timbermaw Hold reputation can often be found in the Winterfall Village. Even with an epic mount, the travel to and from Winterfall Village takes up much time. There are tigers among the route who will daze you, which will result in a demount, this should be avoided (but can be hard as they\'ll catch up with you on a 60% mount). Usually this quest is repeated all the way to exalted, ignoring the third quest. \n\n[b]Honored to Exalted[/b]\nAt honored the third quest [quest=5981] is available. The quest requires the player to kill 8 Frostmaul giants. They are a lot harder than the Winterfall mobs and the travel lengths are quite longer. This quest is usually skipped, and instead Winterfall Intrusion is repeated.\n\nDue to some players grinding Timbermaw Hold reputation, in Winterfall Village among other places, this quest can indeed turn out to be a faster reputation reward than the Winterfall Intrusion one.'),(8,609,0,NULL,0,2,'The [b]Cenarion Circle[/b] is an organization of druids, both tauren and night elf, named after Cenarius. Its members are dedicated to protecting nature and restoring the damage done to it by malevolent forces.\n\nThe Circle has many posts, but their main home is the town of Nighthaven in the [zone=493]. Druids learn the spell [spell=18960] at level 10, but anyone else will have to make it to [zone=361] and find a way through the Timbermaw Furbolg tunnels.\n\nThe Circle\'s other major presence is in [zone=1377], where they combat the Silithid, the Qiraji, and Twilight\'s Hammer. Valor\'s Rest and Cenarion Hold serve as their bases in the hostile land, and offer many opportunities to adventurers seeking to aid the druids.\n\n[h3]Notable Members[/h3]\n[ul][li][npc=11832], son of Cenarius[/li][li][npc=3516], leader of the night elven druids[/li][li][npc=5769], leader of the tauren druids[/li][/ul]\n\n[h3]Reputation[/h3]\nThere are several ways to gain reputation with the Cenarion Circle. Aside from the available [url=?quests&filter=cr=1;crs=609;crv=0]quests[/url], you may do the following to gain reputation:[ul][li]Raid the [zone=3429]. This is by far the fastest way to gain reputation, as a full clear can net over 2000 reputation.[/li][li]Kill twilight cultists. These stop yielding reputation when you reach the end of friendly for [npc=11880] and [npc=11881], and at the end of honored for [npc=15201].[/li][li]Turn in [item=20404]. These drop off the cultists, and yield 250 reputation for 10 texts.[/li][li]Turn in [item=20513], [item=20514], and [item=20515]. These drop off the minibosses that are summoned at the windstones using the [itemset=492].[/li][li]Perform the [quest=8507]. These are either [url=?search=logistics+task+briefing]Logistics quests[/url], [url=?search=combat+task+briefing]Combat quests[/url], or [url=?search=tactical+task+briefing]Tactical quests[/url]. The badges you earn from these quests may then be turned in for additional reputation, if you chose to forsake the rewards.[/li][li]Collect [object=181598] from the zone and turn it in to your faction NPC.[/li][/ul]'),(8,729,0,NULL,0,2,'[b]Frostwolf Clan[/b], along with [npc=11946], lived along the [zone=36] practicing shamanism, and having Frost Wolves as their companions. The dwarven expedition known as the [faction=730] have started an expedition in the Frostwolf territory to excavate the valley and mine its veins, a transgression to the orcs who inhabited Alterac. This provoked a slaughter of the first expedition, and started the battle for [zone=2597].\n\n[h3]Reputation[/h3]\nPlayers can earn reputation in this faction by participating in the Alterac Valley battleground by doing various tasks as well as killing members of the opposite faction, the Stormpike Guard.\n\nYou are granted the player title [title=47] once exalted with the Frostwolf Clan and the other two battleground factions, [faction=889] and [faction=510].'),(8,730,0,NULL,0,2,'[b]Stormpike Guard[/b] is the Alliance faction in the [zone=2597] battleground. They are an expedition of dwarves of the Stormpike Clan, native to the \"valleys of Alterac\" in [zone=36]. The Stormpikes\' search for relics of their past and harvesting of resources in Alterac Valley have led to open war with the the orcs of the [faction=729] dwelling in the southern part of the valley. They were also issued with a \"sovereign imperialistic imperative\" by [npc=2784] to take the valleys of Alterac for [zone=1537]. \n\nThe main Stormpike base is Dun Baldar, where their leader, [npc=11948], resides with his marshals. His second in command, [npc=11949], is found south of Dun Baldar, at Stonehearth Outpost.\n\n[h3]Reputation[/h3]\nPlayers can earn reputation in this faction by participating in the Alterac Valley battleground by doing various tasks as well as killing members of the opposite faction, the Frostwolf Clan.\n\nYou are granted the player title [title=48] once exalted with Stormpike Guard and the other two battleground factions, [faction=890] and [faction=509].'),(8,749,0,NULL,0,2,'The [b]Hydraxian Waterlords[/b] are elementals that have made their home on the islands east of [zone=16]. Sworn enemies of the armies of [npc=11502]. Historically servants of the Old Gods, the four Elemental Lords served the gods with undying loyalty. The minions of Neptulon the Tidehunter were numerous and mindless. It is not yet known how [npc=13278] broke free of his lord\'s control (if indeed he has), or what is his ultimate goals are, but the Water elementals are the only elementals that do not attack the mortal races with abandonment.\n\nLocated on a remote island in the far east of Azshara, Duke Hydraxis offers some quests. The first two require killing various elementals in [zone=139] and [zone=1377]. Increased faction with the Waterlords opens up additional quests leading into the [zone=2717]. Any items obtained from the Hydraxian Waterlords, are obtained from its various quests.\n\nCompleting the questline allows players to obtain [item=17333] used to douse the runes found near most bosses in Molten Core. This is required to summon [npc=12018], the penultimate boss, and, after his defeat, to summon Ragnaros himself. Since there are seven runes, any raid needs at least seven players that bring a quintessence if they wish to finish the instance. Since most of the questline takes place within Molten Core, any raider can complete this task with little more than some traveling and an [zone=1583] run.\n\n[h3]Reputation[/h3]\nRepuation is gained through slaying the following elemental enemies of the waterlords.[ul][li][npc=11746] - 5 reputation, lasts until honored.[/li][li][npc=11744] - 5 reputation, lasts until honored.[/li][li][npc=7032] - 5 reputation, lasts until honored.[/li][li][npc=9017] - 15 reputation, lasts until revered.[/li][li][npc=14478] - 25 reputation, lasts until revered.[/li][li][npc=9816] - 50 reputation, lasts until revered.[/li][li][npc=11658], [npc=11673], [npc=12101] and [npc=11668] - 20 reputation, lasts until revered.[/li][li][npc=11659] and Lava Pack ([npc=12100], [npc=12076], [npc=11667], [npc=11666]) - 40 reputation, lasts until revered.[/li][li][npc=12118], [npc=11982], [npc=12259], [npc=12057], [npc=12056], [npc=12264], [npc=12098] - 100 reputation, lasts until exalted.[/li][li][npc=11988] - 150 reputation, lasts until the end of exalted.[/li][li][npc=11502] - 200 reputation, lasts until the end of exalted.[/li][/ul]Reaching revered status with the Hydraxian Waterlords allows players to obtain the [item=22754], which replenishes itself and thus eliminates the need to return to Hydraxis to obtain a new quintessence every week.'),(8,809,0,NULL,0,2,'The [b]Shen\'dralar[/b] are the faction of the Night Elves remaining in [zone=2557]. They are a group of high practitioners of arcane magic in order of their former Queen Azshara, and her followers, the Highborne. They have been living in Eldre\'Thalas (previous name of Dire Maul) since the Great Sundering. They are few, but their knowledge and mystic power are great, referring to things players think are powerful such as [b]Arcanums[/b] and [b]Librams[/b] as mere cantrips.\n\nTheir leader, [npc=11486], was in charge and oversaw the construction of the pylons to contain the great demon [npc=11496] and syphon his demonic power. After many long years though, it began to dwindle so he started killing the remaining night elves to maintain energy. So their spirits come to adventurers and ask them to kill him. There are very few of the original inhabitants left alive.\n\n[h3]Reputation[/h3]\nReputation can be gained by turning repeatedly in the three Librams of Dire Maul ([item=18333], [item=18334], [item=18332]). Turning in the following class books also gives some reputation:[ul][li][item=18357] - Warrior[/li][li][item=18363] - Shaman[/li][li][item=18356] - Rogue[/li][li][item=18360] - Warlock[/li][li][item=18362] - Priest[/li][li][item=18358] - Mage[/li][li][item=18364] - Druid[/li][li][item=18361] - Hunter[/li][li][item=18359] - Paladin[/li][li][item=18401] - Warrior & Paladin[/li][/ul]Both class books and librams give 500 Reputation points each.'),(8,889,0,NULL,0,2,'[b]Warsong Outriders[/b] is an orcish clan formerly led by [npc=18076], in which the clan was named after. The clan\'s Warsong Outriders form the Horde faction in the [zone=3277] battleground, where they are attempting to defend their logging operations in [zone=331] from the [faction=890].\n\nOne of the strongest and most violent clans, the Warsong Clan was also one of the most distinguished clans on Draenor and was able to evade Alliance expedition forces at every turn. Depicted as Grunts, they have mastered the use of swords and blades and a few of them have even attained the rank of a Blademaster.\n\n[h3]Reputation[/h3]\nReputation is gained through participation in the Warsong Gulch battleground. You gain 35 reputation each time your side captures a flag. This reputation gain is increased to 45 on holiday weekends.\n\nYou are granted the player title Conqueror once exalted with Warsong Outriders and the other two battleground factions, [faction=510] and [faction=729].'),(8,890,0,NULL,0,2,'[b]Silverwing Sentinels[/b] are the Alliance faction for the [zone=3277] battleground. The night elves, who have begun a massive push to retake the forests of [zone=331] are now focusing their attention on ridding their land of the [faction=889] once and for all. And so, the Silverwing Sentinels have answered the call and sworn that they will not rest until every last orc is defeated and cast out of Warsong Gulch.\n\n[h3]Reputation[/h3]\nReputation is gained through participation in the Warsong Gulch battleground. You gain 35 reputation each time your side captures a flag. This reputation gain is increased to 45 on holiday weekends.\n\nYou are granted the player title [title=48] once exalted with Silverwing Sentinels and the other two battleground factions, [faction=730] and [faction=509].'),(8,909,0,NULL,0,2,'The [b]Darkmoon Faire[/b] is a mysterious traveling carnival, which roams not only Azeroth but Outland as well. Led by the inimitable [npc=14823], a gnome of dubious heritage and unknown providence, the Faire brings fun, games, prizes, and exotic trinkets of unexpected power to [zone=215], [zone=12], or [zone=3519] each month.\n\nA variety of amusements can be had by the discerning fairegoer, but the most common attraction is the ticket redemption. A variety of merchants at the Faire collect items from around the worlds in exchange for [item=19182]. The tickets can, in turn, be saved up and turned in for prizes of varying worth and power. Several different ticket distributors are posted around the Faire, offering tickets for crafted items made by Leatherworkers, Blacksmiths, or Engineers as well as items gathered in the wild such as [item=11404] and [item=19933]. Tickets can be redeemed for many things, from flowers to hold in the off-hand to necklaces of great power.\n\nMany adventurers seek out the Darkmoon Faire to turn in the mystical [url=?items=15.0&filter=minle=1;cr=107;crs=0;crv=Combine+the+Ace]Darkmoon Cards[/url]. Darkmoon Cards come in eight suits, each of which has cards from Ace to Eight. Combining all cards in a suit produces a deck, which will start a quest to return that deck to the Darkmoon Faire. Each of the eight decks produces a different [url=?items=4.-4&filter=na=Darkmoon+Card]trinket[/url] with a different effect, some of which are quite powerful.\n\nThe Darkmoon Faire\'s usual schedule has it arriving on site on the first Friday of the month. For the weekend, the carnies will be seen setting up the midway, and the Faire will actually start early on the following Monday.'),(8,910,0,NULL,0,2,'The [b]Brood of Nozdormu[/b] is a faction consisting of the Bronze Dragonflight. Their leader [npc=15192] can be found outside the [b]Caverns of Time[/b], with many of its agents flying in the sky of [zone=1377].\n\nIn order to open the gates of [b]Ahn\'Qiraj[/b], one champion must complete a long quest line for the bronze dragon Anachronos. This reputation is also relevant in the [zone=3428]; to obtain epic quest gear and rings.\n\n[h3]Reputation[/h3]\nPlayers begin at 0/36000 hated, the lowest level of reputation possible.\n\nBrood of Nozdormu reputation can be earned through killing bosses in both Ahn\'Qiraj instances, killing monsters inside the Temple of Ahn\'Qiraj, and doing quests related to the dungeons. You can also farm [item=20384], though this will take a lot longer, and requires one to have obtained the [item=20383] in [zone=2677] for the [item=21175] quest chain.\n\nKilling trash in the Temple of Ahn\'Qiraj can only get you to 2999 / 3000 Neutral, at which point reputation can only be further advanced through quests and handing in [item=21229] and [item=21230]. You may want to save all the insignias until after you are Neutral, since at that point gaining reputation becomes much more difficult.'),(8,911,0,NULL,0,2,'[b]Silvermoon City[/b] is the capital of the blood elves, located in the northeastern part of the [zone=3430] within the kingdom of Quel\'Thalas. The breathtaking capital city of the blood elves may rival the dwarven capital of [zone=1537] as the world\'s oldest, still standing, capital. Recently rebuilt from the devastating blow dealt by the evil Prince Arthas, the city houses the largest population of blood elves left on Azeroth.[pad]Silvermoon today is only the eastern half of the original city; the western half was almost completely destroyed by the Scourge during the Third War. Falconwing Square, the second blood elf town, is the only part of western Silvermoon remaining in blood elf control. The Dead Scar (the path taken by Arthas Menethil and his undead army on the quest to resurrect Kel\'Thuzad, which carves through all of Eversong Woods) separates the rebuilt Silvermoon from the ruins of the western half. Interestingly, the Ruins of Silvermoon house no undead, instead they contain [url=?npcs&filter=na=wretched;maxle=8]Wretched[/url] and malfunctioning [npc=15638]. As it stands, what remains of Silvermoon City is still bigger than current Horde cities.\n\n[h3]History[/h3]\nThe city of Silvermoon was founded by the high elves after their arrival in Lordaeron thousands of years ago. The city was constructed out of white stone and living plants in the style of the ancient Kaldorei Empire. The city contained the famous Academies of Silvermoon as a center for the learning of Arcane Magic and Sunstrider Spire, a majestic palace home to the Royal family of the high elves. The Convocation of Silvermoon (also known as \"The Silvermoon Council\"), the ruling body of the high elves was also based here. Across a stretch of ocean to the north is the island that contains the Sunwell.[pad]Although Silvermoon itself was left relatively unscathed from the second war, in the third war the Death Knight Arthas led the Scourge into the city, attacking it on his quest to reach the Sunwell. The High Elven King was slain and the majority of the population killed. Scourge forces held the city for a time but abandoned it after the depleting of its resources.[pad]Though the city was attacked by the Scourge, it is not as destroyed as one might think. Though many of its plants are dead, and the occasional dead body is sprawled across the cobblestone, the city was immune to the fire and destruction. Silvermoon now resembles a ghost town, intact, but eerily abandoned. Nevertheless, treasure hunters often frequent Silvermoon to try and find some of the valuable artifacts that the elves left behind before they deserted the city, but the ghosts of Silvermoon\'s past inhabitants prevents anyone from taking anything.\n\n[h3]Reputation[/h3]\nA comprehensive list of quests that grant Silvermoon reputation can be found [url=?quests&filter=maxle=69;cr=1;crs=911;crv=0#00Mz]here[/url].[pad][npc=20612] is the quest giver for the repeatable [item=14047] quest that must be completed by non-blood elf Horde players in order to reach exalted and gain the ability to ride [url=?items=15.5&filter=na=hawkstrider]hawkstriders[/url], the mount of the blood elf race.'),(8,922,0,NULL,0,2,'[b]Tranquillien[/b] is a joint blood elf and Forsaken town and separate faction in the [zone=3433].\n\n[h3]History[/h3]\nAs the Scourge made their way to the Sunwell, the elves had no choice but to retreat. The town of Tranquillien was abandoned by the retreating elves. The town is now used by the blood elves and the Forsaken as their base of operation to launch attacks aiming to take back the Ghostlands from the Scourge. However, the city is surrounded by the Scourge and even couriers have trouble getting past the enemy to reach the town. The undead forces of Deatholme are the most dangerous threat to the town.\n\n[h3]Reputation[/h3]\nUnlike most starting areas, the town of Tranquillien is its own faction. All quests you do for them will garner at least 1000 reputation apiece. [npc=16528] acts as the Tranquillien quartermaster. Vredigar can be found near the inn and will sell various [span class=q2]uncommon[/span] items, and even a [span class=q3]rare[/span] cloak when you reach exalted! If you complete all of the Tranquillien quests, you should be exalted by approximately level 20.[pad]There are a variety of quests mostly concerning reclaiming overrun villages, investigating undead and helping around. The \"end\" of the quest-revealed lore surrounding Tranquillien culminates with the quest to kill [npc=16329].'),(8,930,0,NULL,0,2,'[b]Exodar[/b] is the faction associated with [zone=3557], the enchanted capital city of the draenei, built out of the largest husk of their crashed dimensional ship of the same name. It is located in the westernmost part of [zone=3524]. The Exodar faction leader is [npc=17468], who is located near the battlemasters in the Vault of Lights.\n\nThe history of the Exodar is a short one, as the draenei only recently raised it around the husk of their crashed ship, which is still smoking from the impact. The Exodar was once a naaru satellite structure around the dimensional fortress [url=?search=tempest+keep#z0z]Tempest Keep[/url]. The Exodar contains a large amount of technological wonders (due to its origins lying with the Tempest Keep) such as magically enchanted \"wires\" which transport holy energy throughout the ship to power the heating and lighting, as well as augmenting the draeneis\' already considerable powers.\n\n[h3]Reputation[/h3]\nAs with other major factions associated with the main races, Exodar reputation may be gained by doing repeatable cloth turn-in quests, killing the opposing faction in [zone=2597] (the blood elves), and doing the appropriately related quests. At honored, the player can purchase items from Exodar related vendors for 10% less, and at exalted, the player, if not a draenei, can purchase the [url=?items=15.5&filter=na=elekk;cr=93:92;crs=2:1;crv=0:0]various mounts[/url] sold by the Exodar. The cloth turn-in quests are available from [npc=20604] [small][/small].'),(8,932,0,NULL,0,2,'[b]The Aldor[/b] are an ancient order of draenei priests who revere the naaru, and to this day they assist the naaru known as [faction=935] in their battle against [npc=22917] and the Burning Legion. They are found primarily in [zone=3703] and [zone=3520]. Though they have suffered much at the hands of the blood elves who later became [faction=934], they have put aside open warfare for the sake of the Sha\'tar. The Aldor\'s most holy temple lies on the Aldor Rise, overlooking the city from the west.\n\nMost players will start at neutral with the Aldor. [npc=18166] in Shattrath City will give players an initial quest to become friendly with the Aldor or the Scryers. This choice is reversible if players feel the need. Draenei players will be friendly with the Aldor and hostile with the Scryers, whereas blood elf players will be hostile to the Aldor and friendly to the Scryers.\n\n[npc=19321] and [npc=20807] are located in the Aldor bank on the northern edge of the Terrace of Light. The Shrine of Unending Light on Aldor Rise is home to [npc=20616]Asuur [small][/small] and [npc=21906] [small][/small], who exchange epic armor tokens for [url=?itemsets&filter=ta=12]Tier 4[/url] and [url=?itemsets&filter=ta=13]Tier 5[/url] gear, respectively.\n\n[i]Note: Reputation gains with Aldor correspond with a 10% greater loss of reputation with the Scryers. Most reputation gains with the Aldor will also grant 50% of the reputation gained toward your standing with the Sha\'tar.[/i]\n\n[h3]Reputation[/h3]\n[b]Until Honored[/b]\nPlayers looking to gain the higher reputation ranks (revered, exalted) may wish to save non-repeatable quests until after reaching honored.\n\nTurning in 10 [span class=q1][item=29425][/span] to [npc=18537] in Aldor Rise will grant 250 reputation with Aldor. There is also a repeatable quest for single mark turn-ins which yields 25 rep. These marks drop from low ranking Burning Legion members found in most zones in Outland, including the two camps north of Auchindoun in the Bone Wastes of [zone=3519]. Approximately 240 marks are required to go from friendly to honored. In addition these quests provide Sha\'tar reputation; 125 reputation per 10 or 12.5 reputation per single turn in.\n\nPlayers who also desire [faction=978] or [faction=941] reputation may prefer killing orcs at Kil\'Sorrow Fortress in southeastern [zone=3518], as they yield marks as well as 10 Kurenai or Mag\'har reputation per kill.[pad][b]Until Exalted[/b]\nOnce you reach level 68 you may also turn in [span class=q1][item=30809][/span] at the same rates as Marks of Kil\'jaeden. These drop from high-ranking followers of the Burning Legion. If you wish, you may turn in the higher level marks before honored reputation. In [zone=3522], grinding in Death\'s Door is the most compact group of mobs that drop marks.[pad][b]Fel Armaments[/b]\n[span class=q2][item=29740][/span] may be turned in at any time to [npc=18538]Ishanah [small][/small] inside the Shrine of Unending Light on the Aldor Rise. This will increase your reputation with Aldor by 350 per hand-in. In addition to reputation gains, you will receive [span class=q1][item=29735][/span], which is currency for the purchase of shoulder enchants from Inscriber Saalyn in the Aldor bank.\n\n[h3]Switching to Aldor[/h3]\nTo change your faction from the Scryers to the Aldor to access their crafting recipes (and undo all reputation progress you have made), find [npc=18597], an Aldor in Lower City. She offers a repeatable quest for 8x [span class=q1][item=25802][/span]. Once you are neutral with the Aldor, you may no longer receive this quest.'),(8,933,0,NULL,0,2,'Led by [npc=19674], [b]The Consortium[/b] are ethereal smugglers, traders and thieves that have come to Outland. Their main base of operations and biggest settlement is the Stormspire, but they can be found at Midrealm Post, the Aeris Landing, within the [zone=3792] of Auchindoun and various other places.\n\nUpon reaching Friendly status, players are officially considered members of the Consortium and given a salary. The salary is a bag of gems at the beginning of every month, given by [npc=18265] at Aeris Landing. Higher reputation with the Consortium yields higher qualities and quantities of jewels each month.\n\n[h3]Reputation[/h3]\n[b]Until Friendly[/b][ul][li]Run Mana-Tombs in [i]normal[/i] mode, ~1200 reputation per run.[/li][li]Turn in [item=25416] at [npc=18265].[/li][li]Turn in [item=25463] at [npc=18333].[/li][/ul][b]Friendly to Honored[/b][ul][li]Run Mana-Tombs in [i]normal[/i] mode, ~1200 reputation per run.[/li][li]Turn in [item=25433] at [npc=18265].[/li][li]Turn in [item=29209] at [npc=19880].[/li][/ul][b]Honored to Exalted[/b][ul][li]Run Mana-Tombs in [i]heroic[/i] mode, ~2400 reputation per run.[/li][li]Complete all available [url=?quests&filter=cr=1;crs=933;crv=0]quests[/url].[/li][li]Turn in [item=25433] at [npc=18265].[/li][li]Turn in [item=29209] at [npc=19880].[/li][/ul]Characters trying to simultaneously earn reputation with the [faction=941] or [faction=978] and the Consortium may want to focus on killing ogres ([url=?npcs&filter=na=boulderfist;cr=6;crs=3518;crv=0]Boulderfist[/url], [url=?npcs&filter=na=Warmaul;cr=6;crs=3518;crv=0]Warmaul[/url]) in Nagrand and saving the Obsidian Warbeads for Consortium turn-ins. The only caveat is the drop rate, which is roughly 33% for the warbeads, while it is 50% on the insignias. If you are level 70 and want a faster grind without concern for Mag\'har/Kurenai reputation, then you may want to grind insignias instead. Then again, the ogres are generally easier to grind, ranging from level 65 to 67. The choice is ultimately up to the player.'),(8,934,0,NULL,0,2,'[b]The Scryers[/b] are blood elves who reside in [zone=3703] led by [npc=18530]. The group broke away from [npc=19622] and offered to assist the Naaru at Shattrath City. They are at odds with the [faction=932], and compete with them for power within Shattrath and the Naaru\'s favor.[pad]Most players will start at neutral with the Aldor. [npc=18166] in Shattrath City will give players the choice of aligning themselves with the Scryers or Aldor after completing [quest=10211]. This choice is reversible if players feel the need. Blood elf players will be friendly with the Scryers and hostile with the Aldor, whereas draenei players will be hostile to the Scryers and friendly to the Aldor.[pad]The Scryers have both a [npc=19251] trainer and a [npc=19252] trainer. Due to this, the enchanter nestled deep within [zone=1337] is rendered obsolete.[pad][npc=19331] and [npc=20808] are located in the Scryers bank on the southern edge of the Terrace of Light. The Seer\'s Library in the Scryer\'s Tier is home to [npc=20613] [small][/small] and [npc=21905] [small][/small], who exchange epic armor tokens for [url=?itemsets&filter=ta=12]Tier 4[/url] and [url=?itemsets&filter=ta=13]Tier 5[/url] gear, respectively.[pad][i]Note: Reputation gains with Scryers correspond with a 10% greater loss of reputation with the Aldor. Most reputation gains with the Scryers will also grant 50% of the reputation gained toward your standing with the [faction=935].[/i]\n\n[h3]Lore[/h3]\nAfter enduring relentless assaults, the harried Sha\'tar and Aldor guards braced for the next wave as it marched over the horizon. This time, the attack came from the armies of [npc=22917]. A large regiment of blood elves had been sent by Illidan’s ally, Prince Kael\'thas Sunstrider, to lay waste to the city. As the regiment of blood elves crossed the bridge, the Aldor’s exarches and vindicators lined up to defend the Terrace of Light. Then the unexpected happened, the blood elves laid down their weapons in front of the city\'s defenders. Their leader, a blood elf elder known as Voren’thal, stormed into the Terrace of Light and demanded to speak to the naaru [npc=18481]. As the naaru approached him, Voren’thal knelt and uttered the following words: \"I’ve seen you in a vision, naaru. My race’s only hope for survival lies with you. My followers and I are here to serve you.\"[pad]The defection of Voren’thal and his followers was the largest loss ever incurred by Kael’thas’ forces. Many of the strongest and brightest amongst Kael’thas’ scholars and magisters had been swayed by Voren’thal\'s influence. The naaru accepted the defectors who became known as the Scryers.\n\n[h3]Reputation[/h3]\n[b]Until Honored[/b]\nPlayers looking to gain the higher reputation ranks (revered, exalted) may wish to save non-repeatable quests until after reaching honored.[pad]Turning in 10 [span class=q1][item=29426][/span] to [npc=18531] in Scryer\'s Tier will grant 250 reputation with the Scryers. These signets can also be turned in one at a time at the same exchange rate, 25 reputation per signet. These signets drop from low ranking Firewing members found in the northeast section of Terrokar Forest. This repeatable quest becomes unavailable at honored. If no other reputation quests are done, 240 signets are required to go from friendly to honored.[pad][b]Until Exalted[/b]\nOnce you reach level 68, you may also turn in [span class=q1][item=30810][/span]. These drop from high-ranking Sunfury blood elves (found in [zone=3523], [zone=3520], and the [url=?search=tempest+keep+-eye+-kael]Tempest Keep[/url] instances). If you wish, you may turn in the higher level signets before honored reputation, however it is recommended that you save them for after you hit honored. For every 10 signets, you will gain 250 reputation. Once you hit honored it will take approximately 1,320 Sunfury signets to go from honored to exalted if no other reputation is earned.[pad][b]Arcane Tomes[/b]\n[span class=q2][item=29739][/span] may be turned in at any time to Voren\'thal the Seer inside the The Seer\'s Library on the Scryer\'s Tier. This will increase your reputation with the Scryers by 350 per hand-in. If you wish, you may turn in the Arcane Tomes before honored reputation, however it is recommended that you save them for after you hit honored. Once you hit honored it will take approximately 94 Arcane Tomes to go from honored to exalted if no other reputation is earned. In addition to reputation gains, you will receive an [span class=q1][item=29736][/span], which is currency for the purchase of shoulder enchants from Inscriber Veredis, who resides in the Scryers bank.\n\n[h3]Switching to Scryers[/h3]\nTo change your faction from Aldor to Scryers to access their crafting recipes (and undo all reputation progress you have made), find [npc=18596], a Scryers in the Lower City. She offers you a repeatable quest, [quest=10024], that requires you to find eight [span class=q1][item=25744][/span]. Once you are Neutral with the Scryers, you can no longer receive this quest. The quest gives you +250 Scryers reputation and -275 Aldor reputation (in addition, the quest also gives you +125 reputation with The Sha\'tar).'),(8,935,0,NULL,0,2,'[b]The Sha\'tar[/b], or \"born of light,\" are naaru that aided [faction=932], the order of draenei priests formerly led by [npc=17468], in rebuilding [zone=3703]. The city was destroyed by the Orcs during their rampage across Draenor prior to the First War. Defeat of the Burning Legion is the Sha\'tar\'s ultimate goal; the Sha\'tar are aided in this war by the Aldor and their rivals, the blood elf faction known as [faction=934]. The Aldor and the Scryers fight for the favor of the Sha\'tar so that they may be assisted in their war by the naaru\'s powers. The entity that leads the Sha\'tar is known as [npc=18481]; he can be found upon the Terrace of Light in Shattrath City.\n\nBoth Alliance and Horde players begin as Neutral toward the Sha\'tar. Players can increase their Sha\'tar reputation through various quests, by raising their reputation with the Aldor or Scryers, or by adventuring into [url=?search=Tempest+Keep#z0z]Tempest Keep[/url].\n\n[h3]Reputation[/h3]\n[b]Until Honored[/b]\nReputation can be gained from Scryer/Aldor signet/mark turn-ins. The following will only grant Sha\'tar reputation until you achieve Honored status: [item=29426], [item=30810], and [item=29739] for the Scryers; [item=29425], [item=30809], and [item=29740] for the Aldor. In addition, these will require more turn-ins to produce equable Sha\'tar reputation to the main faction. Note that this reputation gain does not show up in the combat log, but can be verified by looking at your reputation panel.\n\nReputation can also be gained by running Tempest Keep: [zone=3847], [zone=3846] and [zone=3849].\n\n[b]Through Exalted[/b]\nAfter exhausting the reputation rewards from Aldor/Scryer turn-ins and Mechanar runs, players may wish to complete the few Sha\'tar quests available. In addition to the quests, instance runs in Tempest Keep: Botanica, Arcatraz and Mechanar will continue to grant reputation. At this point, it is probably more worthwhile to run these instances in Heroic mode.'),(8,941,0,NULL,0,2,'The [b]Mag\'har[/b] are a faction of brown-skinned orcs who remain on Outland and have separated themselves from the other remaining orc clans that fell prey to [npc=17257] and joined his army of fel orcs (that are now led by the powerful [npc=16808]). The Mag\'har are settled in the stronghold of Garadar in the beautiful land of [zone=3518], once home to the majority of the orcs along with [zone=3519] and the [zone=3522].[pad]The Mag\'har orcs have never been corrupted by Mannoroth or Magtheridon and thus remained untouched by the bloodlust. Unlike their former clanmates who live in the ruins of their once-mighty holds, the Mag\'har are made up of members of different orc clans who escaped corruption. The current leader of the Mag\'har, venerable [npc=18141], is an old and wise orc, yet she has recently fallen extremely ill. [npc=18063], son of the mighty Grom Hellscream, serves as the Mag\'har\'s military chief, aided by [npc=18106], son of the venerable chieftain of the Bleeding Hollow clan, Kilrogg Deadeye. In addition, there is an NPC within a Mag\'har camp to the west known as [npc=18229].[pad]It is not clear how the Mag\'har managed to retain their original brown skin. Orcish skin turns green when exposed to warlock magic, regardless of the individual\'s beliefs or practices; Garrosh and Jorin would certainly have been exposed, given the positions of their fathers. \n\nHorde players start out at unfriendly with the Mag\'har. Alliance players will always be treated as hostile. The Alliance counterpart to this faction are the [faction=978].\n\n[h3]Questing[/h3]\nQuests for the Mag\'har begin in [zone=3483] with [quest=9400] from [faction=947]. This quest will lead you to a small Mag\'har outpost north of Hellfire Citadel. Once in Nagrand, players will find the main Mag\'har city, Garadar. The city holds most of the remaining quests that will reward Mag\'har reputation.\n\nNote: You MUST have completed the quest chain of \"The Assassin\" up until the quest [quest=9410] (where you become Neutral) in order for you to talk to most people in Garadar.\n\n[h3]Reputation[/h3]\nReputation can be gained from killing [url=?npcs&filter=na=kil%27sorrow;ra=-1;rh=-1]Kil\'sorrow cult members[/url], [url=?npcs&filter=na=Murkblood;ra=-1;rh=-1;cr=6;crs=3518;crv=0]Murkblood Broken[/url], [url=?npcs&filter=na=warmaul+-marker]Warmaul[/url] and [url=?npcs&filter=na=boulderfist;minle=64;ra=-1;rh=-1]Boulderfist[/url] ogres in Nagrand. Players may also turn in 10x [item=25433], which drop from these ogres.[pad]Players seeking [faction=933] reputation may wish to save their warbeads, as Mag\'har reputation is generally easier to obtain.[pad]Players seeking [faction=932] reputation may prefer killing cult members at Kil\'Sorrow Fortress, as they drop [item=29425] for Aldor reputation turn-ins.\n\n[i]Note: These monsters and quests do not have a limit, they grant reputation all the way through exalted![/i]'),(8,942,0,NULL,0,2,'Upon the reopening of the Dark Portal to Outland, the [faction=609] dispatched an exploratory force, known as the [b]Cenarion Expedition[/b], to explore the uncharted world. Much like the Circle, it is a coalition of night elf and tauren forces. Since the opening of the Dark Portal, the Cenarion Expedition has quickly gained in size and autonomy, achieving enough power to be considered its own faction. The Expedition maintains its primary base at Cenarion Refuge in [zone=3521]; it has also made its presence known on [zone=3483], in [zone=3519], and in the [zone=3522]. Cenarion Refuge is located immediately west of Thornfang Hill.\n\nThe Refuge is located in the Zangarmarsh for the primary reason of studying the rich wildlife located there. However, the Expedition has discovered troubling goings-on in the marsh. Water levels in many parts of Zangarmarsh are decreasing, and some areas such as the Dead Mire have already suffered greatly from this strange phenomenon. It has become known that this decrease in the water levels can be attributed to pumps that have been constructed in the Marsh by the naga. Their purpose is to create a new Well of Eternity for [npc=22917]. However, the Expedition cannot afford direct confrontation with the naga so numerous in the Zangarmarsh and [url=?search=coilfang#c0z]Coilfang Reservoir[/url]. It needs the aid of those willing to assist the druids in their dangerous battle against those who seek to disturb the marsh\'s natural balance. Quite naturally, those heroic enough to fight the naga at Coilfang Reservoir will be well rewarded.\n\n[h3]Reputation[/h3]\n[b]Neutral to Honored[/b]\nKill Naga, while also running [zone=3717] whenever you can; a good instance run will net reputation faster than soloing. Alternatively, the player can begin turning in [item=24401] for a chance at an [item=24407], which can be turned in for 500 reputation. It is suggested that the player save his Uncatalogued Species until after Honored status is achieved, as the quest cannot be continued past that point, while Uncatalogued Species can be used until Exalted.\n\nIf you are an herbalist, and interested in [faction=970] reputation, you may want to grind the [url=?npcs&filter=na=Bog+Lord]Bog Lords[/url] which can be found in the NE, SE, and SW corners of Zangarmarsh. Their bodies can be \"picked\" by herbalists and often yield Unidentified Plant Parts, while every kill yields 15 reputation with Sporeggar.[pad][b]Honored to Revered[/b]\nOnce the player is Honored, running Slave Pens and the [zone=3716] (with the exception of [npc=17770] and some giants), will no longer grant reputation. You should now do any Cenarion Expedition quests in Hellfire Peninsula, Zangarmarsh, Terokkar Forest and the Blade\'s Edge Mountains. It is also the time to turn in any Uncatalogued Species you have found. Doing this should get you part of the way into Revered.\n\nAlternatively, you can finish leveling to 70 and run [zone=3715]. Each run gives just over 1500 reputation if you clear all mobs. Also within the Steamvault lies a repeatable quest, [quest=9764], which begins with [item=24367]. You will then be able to turn in [item=24368], which drop in both Steamvault and Slave Pens, receiving 250 reputation for the first turn-in and 75 reputation each thereafter. This turn-in is available all the way to Exalted.\n\nOnce you are 70 and have upgraded your gear, you can opt to run Slave Pens, Underbog, and Steamvault on Heroic Mode upon purchasing the [item=30623]. While the instances are difficult, they award significant reputation: regular mobs are worth 15 reputation, 2 for non-elites, and 150/250 for bosses. This method works until Exalted.[pad][b]Revered to Exalted[/b]\nContinue with the same strategy as above: finish any remaining quests, run Steamvault, and continue with [item=24368] turn-ins.\n\nIt is also possible to run Slave Pens, Underbog, and Steamvault on Heroic Mode. The reputation gained is not much more than running Steamvault in normal mode, whilst the time investment for heroic dungeons is much higher, possibly resulting in a lower net reputation per hour, however the loot is better and you will receive [item=29434] from the bosses which can be used to purchase high quality epic gear.'),(8,946,0,NULL,0,2,'A refuge of human, elven, draenei and dwarven explorers, [b]Honor Hold[/b] is the first major town Alliance explorers will encounter while traversing Outland. Vestiges of the Sons of Lothar, veterans of the Alliance that first came into Draenor, have steadfastly held on to this Hellfire outpost. They are now joined by the armies from Stormwind and Ironforge.\n\n[h3]Reputation[/h3]\nHonor Hold reputation is gained through various means in Hellfire Peninsula. Mobs in and around Hellfire Citadel reward Honor Hold reputation, as well as quests picked up in town. Due to the lack of representatives in other areas, there is a large gap between Honored and Exalted during which you may not attain any Honor Hold reputation from questing and killing mobs in Outland once you depart Hellfire Peninsula.\n\n[b]Through friendly[/b]\nMobs in [zone=3562] and [zone=3713] will award reputation through Friendly. One option is to grind reputation via Ramparts and Blood Furnace runs until honored before doing any Honor Hold quests outside the instances, as those continue to yield reputation up to Exalted. You may also want to check out the following outdoor mobs which give reputation if you are Neutral. These mobs will not give reputation once you are Friendly with Honor Hold.[ul][li][npc=19415] [/li][li][npc=16878] [/li][li][npc=16870][/li][li][npc=16867][/li][li][npc=19414] [/li][li][npc=19413] [/li][li][npc=19411] [/li][li][npc=19422][/li][/ul]To make the best use of available resources, you may want to grind reputation with Honor Hold through Hellfire Ramparts and Blood Furnace prior to completing any Honor Hold quests. \n\n[b]PvP[/b]\nPlayers that enjoy PvP can earn Honor Hold reputation through the daily quest [quest=10106]. This quest awards 70 silver and 150 Honor Hold reputation, but can only be completed once a day and counts towards your 25 daily quest limit. Completion of this quest also yields three [span class=q1][item=24579][/span], which are used as currency for various types of items and gear when turned into [npc=17657] and [npc=18266] in Honor Hold as well as the [npc=18581] in Zangarmarsh.\n\n[i]Tip: You can use these marks to purchase [span class=q1][item=24520][/span] from Warrant Officer Tracy Proudwell and increase the amount of reputation (and experience) gained while running these instances.[/i]\n\n[b]Through Exalted[/b]\nFrom here on out there are only two ways to achieve Revered and Exalted status:[ul][li][zone=3714], this instance requires level 68 and the [span class=q1][item=28395][/span] (only one party member needs the key). Mobs in Shattered Halls will yield reputation through Exalted.[/li][li]After achieving Honored status you can purchase the [span class=q1][item=30622][/span] which grants access to the heroic mode of all Hellfire Citadel instances. Mobs in all Heroic mode Hellfire Citadel instances will yield slightly more reputation than those found in non-heroic Shattered Halls, and will continue to yield reputation through Exalted.[/li][/ul]'),(8,947,0,NULL,0,2,'The expedition sent through the Dark Portal by Thrall has built a stronghold in Hellfire Peninsula. [b]Thrallmar[/b] serves as a base of operations for much of the Horde\'s activities in Outland.\n\n[h3]Reputation[/h3]\nReputation for Thrallmar up to Honored is relatively easy to earn. Even the easiest quests (those that take you from one quest giver to the next up the road, for example) can yield 75 reputation points, while those that require some effort to complete typically yield 250 reputation points or more. Some group quests that involve killing an elite can yield as much as 1000 reputation points.\n\nIf you do the bulk of the Thrallmar quests instead of quickly moving on to the next zone, you might expect to reach Honored after 1 or 2 levels of play. However, once you reach Honored, you hit an earnings barrier that you can only remove when you are level 68 and can start re-earning points in the [zone=3714] dungeon.\n\n[b]Neutral through Friendly[/b]\nReputation from mobs in [zone=3562] and [zone=3713] stops at 5999/6000 friendly. One option is to grind reputation via Ramparts and Blood Furnace runs to 5999/6000 before doing any Thrallmar quests outside the instances, as those continue to yield reputation up to Exalted.\n\nAlso, the level 63 mobs outside Hellfire Citadel (on the path) give you 5 reputation each.\n\n[b]Friendly through Honored[/b]\nPlayers that enjoy PvP can earn Thrallmar reputation through the daily quest [quest=10110]. This quest awards 70 silver and 150 Thrallmar reputation, but can only be completed once a day and counts towards your 25 daily quest limit. Completion of this quest also yields three [item=24581], which are used as currency for various types of items and gear when turned into [npc=18267] and the [npc=18564] in Thrallmar and near Zabra\'jin in [zone=3521] respectively.\n\nBlood Furnace and Ramparts instance runs will be your best bet for this reputation bracket. Be aware though, that they will only take you to the end of Honored. You will need to run Shattered Halls to reach Revered status.\n\n[b]Revered to Exalted[/b]\nFrom this point on, gaining reputation through Exalted requires one of two things:[ul][li]Access to Shattered Halls, one of the wings of Hellfire Citadel, which requires level 68 and either the [span class=q1][item=28395][/span] or a rogue with 350 lockpicking skill.[/li][li]Doing Heroic versions of Hellfire Citadel dungeons, which typically require you to be well geared and level 70.[/li][/ul]Both of these give reputation until you reach Exalted status. A full clear of Shattered Halls nets you about 2000 reputation points, trash mobs generally yield 6 or 12 each, with up to 150 points from bosses. Heroic trash yields 15-25 points, with bosses worth more. \n\n[i]Tip: You can purchase [span class=q1][item=24522][/span] from Battlecryer Blackeye for use during instance runs to speed up the reputation (and experience) gaining process![/i]'),(8,967,0,NULL,0,2,'[b]The Violet Eye[/b] is a secret sect founded by the Kirin Tor of Dalaran to spy on the Guardian of Tirisfal, [npc=15608], in his tower of [zone=2562]. Though Medivh is dead, the Violet Eye remains in Karazhan, defending against the evil that appears to have taken hold in the absence of its master. \n\nIt is unknown whether Medivh\'s apprentice, [npc=18166], was a member of the Violet Eye, or whether he knew of their activities at the time (though he does seem to be aware of them now).\n\n[h3]Reputation[/h3]\nViolet Eye reputation is gained by killing mobs inside Karazhan and completing Karazhan related quests. Reputation from Karazhan mobs can be gained from neutral standing all the way to exalted. Each trash mob awards around 15 reputation, with the bosses award more.\n\n[npc=18253] begins a fairly long quest chain starting with [quest=9824] and [quest=9825]. This quest line rewards players with [span class=q1][item=24490][/span] and culminates with [quest=9644]. Full completion of this quest line rewards approximately 10,270 reputation.\n\n[h3]Reputation Rewards[/h3]\n[npc=18253] will offer players rings as rewards for reputation level gains in the form of quests. The first such quest is available at neutral standing and may be completed at friendly. You will receive a new and upgraded version of the ring you chose each time you break into a new reputation tier. The rings are sorted into the following 4 categories:[ul][li][quest=10731]: [item=29280], [item=29281], [item=29282] and [item=29283][/li][li][quest=10729]: [item=29284], [item=29285], [item=29286] and [item=29287][/li][li][quest=10732]: [item=29276], [item=29277], [item=29278], and [item=29279][/li][li][quest=10730]: [item=29288], [item=29289], [item=29291] and [item=29290][/li][/ul][npc=16388], a blacksmith located inside Karazhan just after [npc=15550], offers players with high enough reputation the ability to buy epic blacksmithing plans. Players who are honored or above will also be able to repair armor and weapons at this vendor.\n\n[npc=18255], who stands just outside the main gates of Karazhan, will sell an epic jewelcrafting recipe and shoulder enchant to players who have an honored or above standing with The Violet Eye.'),(8,970,0,NULL,0,2,'The sporelings are a mostly peaceful race of mushroom-men native to Outland. Their home, [b]Sporeggar[/b], is located in the western bogs of [zone=3521].\n\n[h3]Reputation[/h3]\nPlayers both Alliance and Horde start out unfriendly with Sporeggar. There are many ways to increase your reputation at the beginning:[ul][li]Bringing 10 [span class=q1][item=24290][/span] to [npc=17923] to complete [quest=9739][/li][li]Bringing 6 [span class=q1][item=24291][/span] to Fahssn to complete [quest=9743] [i](both of these quests will be available only if you are below friendly)[/i][/li][li]Killing [url=?search=bog+lord+-hungry#z0z]Bog Lords[/url] [i](lasts until the end of honored)[/i][/li][li]Killing [npc=18137] and [npc=18136] [i](lasts until the end of revered)[/i][/li][li]Bringing 10 [span class=q1][item=24245][/span] to [npc=17924] in Sporeggar [i](lasts only during neutral)[/i][/li][/ul]After you hit [b]friendly[/b], a new handful of repeatable quests opens up at the same time Fahssn\'s quests and the Glowcap turnins become unavailable, these include:[ul][li]Killing 12 each of [npc=18088] and [npc=18089] for [npc=17856] to complete [quest=9726][/li][li]Bringing 10 [span class=q1][item=24449][/span] to [npc=17925] to complete [quest=9806][/li][li]Venturing into [zone=3716] to gather 5 [span class=q1][item=24246][/span] for Gzhun\'tt to complete [quest=9715][/li][/ul]These 3 quests are repeatable and will be available to the end of exalted.\n\nPlayers who are exalted with Sporeggar should speak to [npc=17877] for one final quest.'),(8,978,0,NULL,0,2,'Draenei for \"redeemed.\" These Broken have escaped the grasp of their various slavers in Outland and have made their home at Telaar in southern [zone=3518]. It is there that they seek to rediscover their destiny. They also maintain a small presence at Orebor Harborage, [zone=3521]. Their quartermaster, [npc=20240], is located outside the inn in Telaar, below the flight point.\n\nAlliance players start out at unfriendly with the Kurenai. Horde players will always be treated as hostile. The Horde counterpart to this faction are [faction=941].\n\n[i]Kurenai is Japanese for \"crimson\".[/i]\n\n[h3]Gaining Reputation[/h3]\nReputation can be gained from killing [url=?npcs&filter=na=kil%27sorrow;ra=-1;rh=-1]Kil\'sorrow cult members[/url], [url=?npcs&filter=na=Murkblood;ra=-1;rh=-1;cr=6;crs=3518;crv=0]Murkblood Broken[/url], [url=?npcs&filter=na=warmaul+-marker]Warmaul[/url] and [url=?npcs&filter=na=boulderfist;minle=64;ra=-1;rh=-1]Boulderfist[/url] ogres in Nagrand. Players may also turn in [item=25433] (10), which drop from these ogres.\n\nPlayers seeking [faction=933] reputation may wish to save their warbeads, as Kurenai reputation is generally easier to obtain.\n\nPlayers seeking [faction=932] reputation may prefer killing cult members at Kil\'Sorrow Fortress, as they drop [item=29425] for Aldor reputation turn-ins.\n\n[i]Note: These monsters and quests do not have a limit, they grant reputation all the way through exalted![/i]'),(8,989,0,NULL,0,2,'The [b]Keepers of Time[/b] are bronze dragons hand-picked by Nozdormu to watch over the Caverns of Time. They are led by [npc=19932] and [npc=19933], who are also acting leaders of the Bronze Dragonflight in Nozdormu\'s absence.\n\n[h3]Reputation[/h3]\nCurrently the only way to gain the favor of the enigmatic bronze dragons is through [zone=2367] and [zone=2366] instance runs. Keepers of Time reputation rewards may be found at the Keepers\' quartermaster, [npc=21643]. The Keepers will require you to be level 66 and complete the short quest [quest=10277] before allowing passage into Old Hillsbrad Foothills to fulfill [npc=17876]\'s destiny to become the Warchief of the Horde.'),(8,990,0,NULL,0,2,'The [b]Scale of the Sands[/b] is a secretive subgroup of the Bronze Dragonflight, led by [npc=19935], prime mate of [npc=15185]. It is a subgroup of the Bronze Dragonflight. Their leader, Nozdormu, sent these guardian factions to [zone=3606] where they guard the World Tree from another attack by the demons of Darkwhisper Gorge and help restore the time-stream and preserve the future of the world.\n\n[h3]Reputation[/h3]\nBoth bosses and trash monsters give reputation with each kill. [npc=17968], the final boss, awards 1500 reputation while the other four bosses give 375. General trash award 12 reputation, while [npc=17907] give 60. Yielding an average of 7800 per full clear, it would take 5-6 clears to reach exalted.\n\nCurrently some of the best [span class=q4][url=?items=4.-2&filter=na=band+of+the+eternal]rings[/url][/span] for raiding are available via this reputation. In order to recieve the rings, you must complete the previously required attunement quest, [quest=10445]. Each new reputation level awards an upgraded ring.'),(8,1011,0,NULL,0,2,'The [b]Lower City[/b] of [zone=3703] is the place where the refugees gather and help out in their own ways. When someone helps any of the mixture of races who fled from war, word gets around quickly. Their quartermaster, [npc=21655], is located at the market in the Lower City. The Lower City of Shattrath also contains a very useful Mana Loom or an Alchemy Lab. Many NPCs have extensive knowledge of crafting. The Battlemasters for both sides of all four [zones=6] can also be found here, as well as the World\'s End Tavern.\n\nOther important NPCs include:[ul][li]A neutral Grand Master Leatherworker, [npc=19187].[/li][li]A neutral Grand Master Skinner, [npc=19180].[/li][li]A neutral Grand Master Alchemist, [npc=19052], with an Alchemy Lab, who also gives the quest [quest=10902] (for alchemy specialization).[/li][li]Three specialist tailors who allow you to specialize and buy new epic tailoring recipes for armor sets and special bags (including the 20-slot bag).[ul][li][npc=22212] [small][/small] sells the patterns for the [itemset=553] set.[/li][li][npc=22213] [small][/small] sells the patterns for the [itemset=552] set.[/li][li][npc=22208] [small][/small] sells the patterns for the [itemset=554] set.[/li][/ul][/li][/ul]\n\n[h3]Reputation[/h3]\n[b]Until Honored[/b][ul][li]Run [zone=3790] in [i]normal[/i] mode, ~750 reputation.[/li][li]Run [zone=3791] in [i]normal[/i] mode, ~1250 reputation.[/li][li]Run [zone=3789] in [i]normal[/i] mode, ~2000 reputation.[/li][li]Turn in [item=25719] at [npc=22429].[/li][/ul][i]Note: Players aiming for faction higher than Honored should wait until honored to complete the Lower City quests.[/i]\n\n[b]Honored to Revered[/b][ul][li]Run Shadow Labyrinth in [i]normal[/i] mode, ~2000 reputation.[/li][li]Complete all available [url=?quests&filter=cr=1;crs=1011;crv=0]Lower City quests[/url].[/li][/ul][b]Revered to Exalted[/b][ul][li]Run Auchenai Crypts in [i]heroic[/i] mode, ~750 reputation.[/li][li]Run Sethekk Halls in [i]heroic[/i] mode, ~1250 reputation.[/li][li]Run Shadow Labyrinth in [i]normal[/i] or [i]heroic[/i] mode, ~2000 reputation.[/li][/ul]\n\n[h3]Trivia[/h3]\n[npc=19227], a vendor in Lower City, sells amulets which are very... interesting. He is quite the salesman, with items like [item=27940], which allows you to return to life as long as you return to the place you died. [i]Buyer beware![/i]\n\nAt exalted you can purchase a [item=31778]. Strangely, none of the NPCs in Lower City can be seen wearing one. Perhaps they cannot afford one...'),(8,1012,0,NULL,0,2,'The [b]Ashtongue Deathsworn[/b] are the elite of the Broken draenei tribe known as the Ashtongue. The Ashtongue tribe is led by the elder sage [npc=21700]; the Deathsworn are [i]officially[/i] aligned with [npc=22917] [small][/small]. The Deathsworn are Akama\'s most trusted lieutenants and are privy to their leader\'s mysterious motivations.\n\nTo discover the Deathsworn as a faction, the player must begin and complete the majority of the quest line which begins with Tablets of Baa\'ri ([quest=10568] / [quest=10683]). Eventually, you will speak with Akama, whereupon you will become Neutral with the Deathsworn.'),(8,1015,0,NULL,0,2,'The [b]Netherwing[/b] are a faction of dragons located in Outland. The unusual brood was spawned from the eggs of Deathwing\'s black dragonflight, and infused with raw nether-energies. Now, they seek to find their identity beyond the shadows of their father\'s destructive heritage.\n\n[h3]Reputation[/h3]\nPlayers are introduced to the Netherwing faction at 0/36000 hated reputation, and must be exalted to receive a [span class=q4][url=?items=15.-7&filter=na=Netherwing+Drake]Netherwing Drake[/url][/span]. The quest chain and reputation grind is a mostly solo endeavor involving quests that can only be completed once daily, a 5-player group quest on the way to neutral, and daily 3-player group quests after reaching revered. A flying mount is required for this reputation grind, and 300 riding skill is necessary to advance past neutral.\n\n[b]Hated to Neutral[/b]\nLevel 70 players will begin their journey to exalted reputation by picking up the quest chain offered by [npc=22113], a blood elf wandering the surface of the Netherwing Fields, in the southeast corner of [zone=3520]. The quest chain begins with the quest [quest=10804]. Completion of this quest line will provide an instant reputation boost to neutral and the choice of one of [span class=q3][url=?items&filter=qu=3;na=Netherwing+-wand]these[/url][/span] five items.\n\n[h3]Netherwing Reputation After Neutral[/h3]\nAfter completing the Kindness quest chain, Mordenai will be sure you have acquired 300 [spell=34091] skill and have you swear fealty to the Netherwing. This will grant you a Dragonmaw Fel Orc disguise when you enter Netherwing Ledge and allow you to communicate and work for the Dragonmaw stationed there. Mordenai will initially send you to [npc=23139] with a set of fake papers. Completing this quest will unlock the beginning Dragonmaw quests that you\'ll be working on to increase your Netherwing reputation. Most of these quests will have the new \"Daily\" tag added with 2.1. Daily quests differ from regular quests in that they are infinitely repeatable, but you may only complete each daily quest once per day and are restricted to ten total daily quests per day.[pad][i]Note: New quests will be unlocked with each reputation tier, and all daily quests of previous tiers will always be available, even after reaching exalted.[/i]\n\n[b][toggler id=Neutral hidden]Neutral[/toggler][/b]\n[div id=Neutral hidden]After turning in Mordenai\'s [item=32469] to Mor\'ghor to complete [quest=11013], your first group of quests will become available to start you on your way to the next tier of reputation with the Netherwing. Mor\'ghor will point you to the taskmaster to begin your grunt work, and [npc=23141] will reveal himself as a Netherwing ally in disguise and present another group of quests to you. One of which is [quest=11049]. Players will be able to turn in any [item=32506] that have a 1% chance to be found in [object=185881], [object=185877], and on almost all creatures on Netherwing Ledge. It can also be a rare find as a [object=185915] anywhere on Netherwing Ledge and in the Dragonmaw Fortress on the southeast corner of the Shadowmoon Valley mainland. This quest is not labeled as daily, and therefore can be done as many times as you can find eggs and will not hinder your daily quest limit.[pad]Other quests available from the beginning:[ul][li][i][small](Daily)[/small][/i] [quest=11018], [quest=11016], [quest=11017] - These will be available only to players who possess the respective profession to gather each item.[/li][li][i][small](Daily)[/small][/i] [quest=11015] - Simple gathering quest open to all players regardless of profession.[/li][li][i][small](Daily)[/small][/i] [quest=11020] - Yarzill will ask you to collect [item=32502] and use them to poison the peons that are working to gather resources for Dragonmaw.[/li][li][i][small](Daily)[/small][/i] [quest=11035] - You will need to fly to the northeast corner of Netherwing Ledge and position yourself on one of the floating rocks to intercept the [npc=23188] and recover 10 [item=32509].[/li][/ul][/div][pad][b][toggler id=Friendly hidden]Friendly[/toggler][/b]\n[div id=Friendly hidden]Mor\'ghor will award you with an [item=32694] to go with your new rank among the Dragonmaw.[ul][li][quest=11083] - [npc=23166] will task you with quelling the Murkblood Broken that are stationed deeper within the mines.[/li][li][quest=11081] - After finding [item=32726] in a [item=32724], you\'ll begin to reveal what\'s truly happening with the Murkblood in the mine.[/li][li][quest=11054] - [npc=23291] will have you fashion your very own [item=32680] for use in keeping the Dragonmaw peons in line and working at full efficiency.[/li][li][i][small](Daily)[/small][/i] [quest=11076] - The [npc=23149] will ask that you venture into the Netherwing mines and recover the cargo contained in mine carts randomly strewn among the interior of the mine.[/li][li][i][small](Daily)[/small][/i] [npc=23376] - One of the [npc=23376] will inform you that the creatures deeper in the mine are halting production and ask you to thin their numbers.[/li][li][i][small](Daily)[/small][/i] [quest=11055] - This humorous quest starts at Chief Overseer Mudlump after you bring him the required materials. You\'ll be able to fly around Netherwing Ledge and toss the Booterang at any [npc=23311] that can be found anywhere around the crystals of the ledge.[/li][/ul][/div][pad][b][toggler id=Honored hidden]Honored[/toggler][/b]\n[div id=Honored hidden]Mor\'ghor will award you with your new [item=32695], which is now usable anywhere as long as you\'re outside.[ul][li][quest=11063] - This six-part questline will have you in-flight following the other Dragonmaw masters of flight. They will all attempt to knock you off your mount with cleverly-placed air attacks, you must stay within vision range and on your mount until they land or you will fail and need to restart the quest. After defeating the last of the six riders, you\'ll be awarded a [item=32863], which functions exactly like a [item=25653]. The effects of the two trinkets do [b]not[/b] stack.[/li][li][quest=11089] - [npc=23427] will request a set of materials to fashion a special device to destroy his brother and hinder the Legion\'s advances from the Twilight Portal in western [zone=3518].[/li][li][i][small](Daily)[/small][/i] [quest=11086] - Mor\'ghor will send you to the Twilight Portal in Nagrand to kill 20 [url=?npcs&filter=na=deathshadow+-imp+-hound+-agent]Deathshadow Agents[/url]. Beware the overlords, they patrol most of the area and can pack quite a punch.[/li][/ul][/div][pad][b][toggler id=Revered hidden]Revered[/toggler][/b]\n[div id=Revered hidden]Mor\'ghor will award your final trinket upgrade, the [item=32864] after reaching revered.[ul][li]Kill Them All! ([quest=11094]/[quest=11099]) - Mor\'ghor will order you to begin the attack against your chosen faction\'s base of operations in Shadowmoon Valley. Obviously you\'re not going to actually allow the Dragonmaw to attack your allies, so report to the proper leader and unlock your final daily quest for Dragonmaw...[/li][li][i][small](Daily)[/small][/i] The Deadliest Trap Ever Laid ([quest=11097]/[quest=11101]) - Waves of Dragonmaw Skybreakers will attack after preparations are made. Bring allies, as this is a battle of attrition.[/li][/ul][/div][pad][b][toggler id=Exalted hidden]Exalted[/toggler][/b]\n[div id=Exalted hidden]After many days of work, finally the denouement of the Netherwing/Dragonmaw questline. Taskmaster Varkule will direct you to Mor\'ghor one last time, who will inform you that you will be promoted by [npc=22917] himself. Without spoiling the events that ensue, you will end up in Shattrath with your selection of Netherdrake epic mounts. You may choose one here for free, and if you decide on a different color later, you can speak with [npc=23489] back in the Dragonmaw Base Camp to buy another drake for 200 gold.[/div]'),(8,1031,0,NULL,0,2,'The [b]Sha\'tari Skyguard[/b] are an air wing of the [faction=935] of [zone=3703], defending the capital from attackers in the hills as well as battling against the arakkoa of Terokk in the peaks of Skettis. The Skyguard has two outposts, one in the northern reaches of the Skethyl Mountains and one near [faction=1038]. Players start out at neutral standing with the Skyguard.\n\n[h3]Reputation[/h3]\n[b]Daily Quests[/b][ul][li][quest=11008] - [npc=23048] will grant you a pack of explosives to destroy the eggs that rest atop Skettis structures.[/li][li][quest=11085] - A [npc=23383] can be found atop certain structures, players will escort him out for reputation, gold, and a choice of either 2 [item=28100] or 2 [item=28101].[/li][li][quest=11065] - [npc=23335] will inform you that the Skyguard\'s bombing runs have taken a toll on their mounts and ask you to gather some more Aether Rays to supplement their scout force.[/li][li][quest=11010] - [npc=23120] asks you to destroy the ammo for the Legion\'s flak cannons so the Skyguard Scouts can continue their job.[/li][li][quest=11004] - After collecting 6 [item=32388], [npc=23042] will make a potion that will allow vision of the more powerful arakkoa, such as [npc=23066].\n[i][small]Note: World of Shadows is not a daily quest, but may be repeated as many times as necessary.[/small][/i][/li][/ul][b]Creatures[/b][ul][li][npc=21804] - 5 reputation, up to the end of Revered.[/li][li][url=?npcs&filter=na=skettis+-kaliri+-assassin;minle=70]All Skettis Arakkoa[/url] - 10 reputation, regardless of Skyguard standing.[/li][li][npc=23029] - 30 reputation, regardless of Skyguard standing.[/li][/ul]'),(8,1038,0,NULL,0,2,'The [b]Ogri\'la[/b] are a faction of ogres in the [zone=3522], where their proximity to [item=32572] has allowed them to evolve past their brutish nature. They are currently fighting a war against both the Black Dragonflight and the Burning Legion, who seek the Apexis Crystals for their own purposes.\n\n[h3]Location[/h3]\nOgri\'la is situated near the western edge of Blade\'s Edge Mountains, between Forge Camp: Terror and Forge Camp: Wrath, just west of Sylvanaar. Ogri\'la is only accessible by flying mount/form. Another alternative is to have a reputation of honored or higher with [faction=1031]. But a player must have a flying mount to reach the Skyguard camp near Skettis.[pad]\n\n[h3]Reputation[/h3]\nReputation with Ogri\'la can only be gained via Quests, and there only repeatable quests are the available [url=?quests&filter=da=ja;cr=1;crs=1038;crv=0]daily quests[/url]. Thus, there is a cap on how much reputation a day a player can gain reputation with Ogri\'la, making it an \"ungrindable\" reputation.\n\n[b]Apexis Shards[/b]\n[item=32569] can be collected in a variety of ways. They can be looted from mobs, gathered from the environment, or they can be rewards from completed quests.[pad][b]Apexis Crystals[/b]\n[item=32572] are dropped from elite demons and dragons in Blade\'s Edge Mountains. In order to summon these mobs, 35 Apexis Shards are needed, and it is recommended that you have a 5 man group to defeat them.\n\n[b]Quests[/b]\nThere are a [url=?quests&filter=cr=1;crs=1038;crv=0]number of quests[/url] that a player can to do earn reputation with the Ogri\'la, as well as several [url=?quests&filter=da=ja;cr=1;crs=1038;crv=0]daily quests[/url]. Many of the daily quests will also grant reputation with the Sha\'tari Skyguard when they are first completed. \n\nIn order to access the main quests at Ogri\'la itself, a player must first complete the 5 group quests from [npc=22941].\n\n[h3]Depleted Items[/h3]\nA number of \"depleted\" items will sometimes drop from mobs. When combined with 50 Apexis Shards, the items [url=?search=Apexis+Crystal+Infusion]upgrade[/url], gaining stats and gem slots. Once the items are upgraded they become Bind on Equip, and can therefore be sold or traded to other players. One thing to note, however, is that although the depleted items may also have stats or effects, they cannot be equipped.'),(NULL,NULL,0,'sound&playlist',0,2,'Here you can set up a playlist of sounds and music. \n\nJust click the \"Add\" button near an audio control, then return to this page to listen to the list you\'ve created.'),(14,11,2,NULL,0,2,'[b]Aperçu :[/b] Les [b]Draenei[/b] sont des adeptes de Naaru et adorateurs de la Lumière Sainte. Chassées d’Argus, leur monde natal, les honorables Draeneï durent fuir des siècles durant Sargeras et sa Légion Ardente, après qu’il ait essayé de les corrompre. Les Draeneï ont alors trouvé une lointaine planète où s’établir. Ils appelèrent Draenor ce monde qu’ils partageaient avec les Orcs chamaniques. Une période de paix s’est alors installée.\nLa Légion Ardente fini par retrouver les DraeneÏ et corrompt les Orcs grâce à Guldan. Les Orcs partirent en guerre et exterminèrent les paisibles Draeneï. De rares survivants purent s’enfuir en Azeroth pour chercher de l’aide dans leur combat contre la Légion Ardente.\n\n[b]Capitale :[/b] Les Draeneï ont le siège de leur pouvoir dans les ruines de leur vaisseau : [zone=3557].\n\n[b]Zone de départ :[/b] [zone=3524] et [zone=3525] couvrent les tentatives des Draeneï de s’installer sur leurs nouvelles îles et de faire face à la corruption présente.\n\n[b]Montures :[/b] [npc=17584] vend des variétés d’Elekks, ainsi que [npc=33657] au tournoi d’Argent.'),(14,8,2,NULL,0,2,'[b]Aperçu :[/b] Les [b]Trolls[/b] Sombrelance vécurent à l\'origine dans les îles Brisées mais furent envahis par les nagas et les murlocs. Chassés de chez eux, la [url=?faction=530]tribu de Sombrelance[/url] se lie finalement d\'amitié avec les orcs qui ont sauvés les Trolls de la destruction. [npc=4949] leur offre l\'amnistie parmi la Horde, en contrepartie, la tribu Sombrelance jura fidélité au chef de guerre orque.\nBien qu\'ils refusent d\'abandonner leur sombre héritage, les féroces Trolls Sombrelance occupent une place d\'honneur au sein de la Horde.\n\n[b]Capitale :[/b] Les Trolls Sombrelance vivent maintenant dans la capitale de la Horde : [zone=1637].\n\n[b]Zone de départ :[/b] Les Trolls commencent leurs quêtes en [zone=14]\n\n[b]Montures :[/b] [npc=7952] au village de Sen\'jin vend de nombreux raptors ; [npc=33554], au tournoi d\'Argent, vend quelques modèles distincts.'),(14,10,2,NULL,0,2,'[b]Aperçu :[/b] Les [b]Hauts-Elfes[/b], race fière et hautaine, fondèrent jadis Quel’Thalas où ils créèrent une fontaine magique appelée Puits de Soleil. Ils profitèrent de sa puissance mais devinrent peu à peu dépendants de la magie. Si celle-ci devait être enlevée, les Hauts-Elfes soufreraient horriblement. Ils se séparèrent donc du reste de la société elfique.\nDe nombreux siècles plus tard, le fléau mort-vivant détruisit le Puit de Soleil et tua la plupart des Hauts-Elfes. Les survivants de l’assaut d’Arthas sur Lune-d’Argent, qui ont alors pris le nom d’Elfes de Sang, rebâtissent Quel’Thalas et cherchent de nouvelles sources de magie pour calmer leur douloureux manque.\nLes Elfes de Sang rejoignent la Horde à Burning Crusade.\n\n[b]Capitale :[/b] Les Elfes de Sang ont reconstruit [zone=3487].\n\n[b]Zone de départ :[/b] Les Elfes de Sang commencent au [zone=3430].\n\n[b]Montures :[/b] [npc=16264], aux Bois des Chants Eternelles, vend de nombreux faucons pèlerins ; [npc=33557], au tournoi d’Argent, vend quelques modèles uniques.'),(14,7,2,NULL,0,2,'[b]Aperçu :[/b] Les [b]Gnomes[/b], race excentrique, sont obsédés par les gadgets et la technologie. Malgré leur petite taille, ils ont mis à profit leur grande intelligence pour s\'assurer une place dans l\'Histoire.\nA l\'origine, les Gnomes viennent de la ville de [zone=721], qui était autrefois une merveille technologique mue à la vapeur. Malheureusement, la ville a été détruite par [npc=7937] à la suite d\'une tentative pour sauver la ville d\'une armée massive de Troggs.\nSes bâtisseurs sont désormais des vagabonds qui errent sur les terres des nains, venant en aide à leurs alliés du mieux qu\'il le peuvent.\n\n[b]Capitale :[/b] Aujourd\'hui, les Gnomes font leurs maisons à [zone=1537] malgré les efforts fournis pour reprendre leur bien aimée ancienne ville avec l\'[achievement=4786].\n\n[b]Zone de départ :[/b] Les Gnomes commencent à [zone=1], mais ont une séquence de quêtes très différente des Nains, couvrant Gnomeregan\n\n[b]Montures :[/b] [npc=7955] à Dun Morogh vend de nombreux mécanotrotteurs, ainsi que [npc=33650] au tournoi d\'Argent.'),(14,6,2,NULL,0,2,'[b]Aperçu :[/b] Les [b]Taurens[/b], race aux racines chamaniques profondes, sont des résidents de longue date de Kalimdor. Ils vouent un amour profond et durable à la nature, la grande majorité d’entre eux adorent une divinité connue sous le nom de la Terre Mère.\nRécemment attaqués par des centaures, les Taurens auraient été exterminés s’ils n’avaient pas rencontré, par hasard, les Orcs qui les aidèrent à repousser leurs ennemis. Afin d’honorer cette dette de sang, les Taurens ont rejoint la Horde, renforçant ainsi l’amitié entre les deux races.\n\n[b]Capitale :[/b] [zone=1638] est le lieu de résidence des Taurens\n\n[b]Zone de départ :[/b] Les Taurens commencent leurs quêtes en [zone=215].\n\n[b]Montures :[/b] [npc=3685] vend de nombreux kodos ; [npc=33556], au tournoi d’Argent, vend quelques modèles distinctifs.'),(14,5,2,NULL,0,2,'[b]Aperçu :[/b] Les [b]Réprouvés[/b], résultat d’une première attaque du Fléau en Azeroth, sont une métamorphose d’un certain nombre de membres de l’Alliance en mort vivant. Quand les forces combinées des Orcs, des Elfes, des Trolls, des Nains et des Humains se mirent à se défendre, [npc=36597] se mit à affaiblir ses armées en perdant le contrôle de certaines. Libérés de l’emprise du Roi Liche ainsi que des émotions gênantes et des liens de leurs vies humaines, les Réprouvés, menés par la banshee Sylvanas, réclament vengeance contre le fléau.\nLes Humain sont également devenus des ennemis, impitoyables dans leur désir de purger les terres de tous les mort-vivants. \nLes Réprouvés ne se soucient que très peu de leurs alliés. La Horde ne représente à leurs yeux qu’un simple outil qui pourrait servir leurs sombres desseins.\n\n[b]Capitale :[/b] Les Réprouvés résident sous les ruines de l’ancienne ville humaine de Lordaeron : la [zone=1497].\n\n[b]Zone de départ :[/b] Tous les joueurs de Réprouvés commencent dans la [zone=85]. Ils sont élevés par les Val’kyrs comme des réprouvés de seconde génération\n\n[b]Montures :[/b] [npc=4731] vend de nombreux chevaux mort-vivants ; [npc=33555], au tournoi d’Argent, vend quelques modèles distincts.'),(14,4,2,NULL,0,2,'[b]Aperçu :[/b] Les [b]Elfes de la nuit[/b], race ancienne et mystérieuse, vivaient à Kalimdor pendant des milliers d\'années, ils fondèrent un vaste empire, mais leur usage imprudent de la magie les conduisit à leur perte. Pétris de douleur, ils se retirèrent dans les forêts et demeurèrent ainsi isolés jusqu\'au retour de leur ancien ennemi. Ne disposant d\'aucune alternative, les Elfes de la nuit furent contraints de sacrifié l\'arbre monde afin d\'arrêter l\'avancé de la Légion Ardente. \nIls émergèrent de leur isolement, afin de défendre leur place dans le nouveau monde.\n\n[b]Capitale :[/b] La capitale des Elfes de la nuit est [zone=1657], située dans les branches de l\'arbre monde.\n\n[b]Zone de départ :[/b] Les Elfes de la nuit commencent à [zone=141]\n\n[b]Montures :[/b] [npc=4730], à Darnassus, vent une variété de sabre de nuit, ainsi que [npc=33653] au tournoi d\'Argent.'),(14,3,2,NULL,0,2,'[b]Aperçu :[/b] Les [b]Nains[/b], race robuste, viennent de Khaz Modan dans les Royaumes de l’Est. Par la passé, les Nains ne s’intéressaient qu’aux richesses extraites des profondeurs de la terre. Lorsque des études semblèrent indiquer que les Nains étaient les descendants d’une race proche des Titans qui leur aurait conféré un héritage enchanté, la curiosité des Nains fut piquée au vif. Décidés à en savoir plus, les Nains commencèrent à rechercher des artefacts perdus et des connaissances disparues. Aujourd’hui, les Nains dirigent des fouilles archéologiques partout dans le monde.\nTrois principaux Clans de Nains sont répartis dans tout Azeroth : Les Barbes de Bronze, Les Marteaux Hardis et les Sombrefers.\n\n[b]Capitale :[/b] Les Nains font leur maison dans leur siège ancestral de [zone=1537].\n\n[b]Zone de départ :[/b] Les Nains commencent à [zone=1].\n\n[b]Montures :[/b] [npc=1261] vend des béliers à la ferme des Amberstill, ainsi que [npc=33310] au tournoi d’Argent.'),(14,1,2,NULL,0,2,'[b]Aperçu :[/b] Les [b]Humains[/b], race la plus jeune et la plus peuplés d\'Azeroth, maîtrisent les arts du combat, l\'artisanat et la magie avec une efficacité stupéfiante. La valeur et l\'optimisme des Humains les ont conduits à bâtir certains des plus grands royaumes du monde. En cette ère de troubles, après des générations de conflit, l\'Humanité aspire à ranimer sa gloire passée et à se forger un nouvel avenir rayonnant.\nLes Humains, aux talents très variés, sont devenus les chefs de l\'Alliance grâce à leurs ambitions et leurs résiliences. \n \n[b]Capitale :[/b] Le siège du pouvoir Humain est dans la ville reconstruite de [zone=1519].\n \n[b]Zone de départ :[/b] Les Humains commencent leurs quêtes dans la [zone=12].\n \n[b]Montures :[/b] [npc=384] vend des palefrois dans Hurlevent, et [npc=33307], au tournoi d’Argent, vend quelques modèles distincts.'),(14,2,2,NULL,0,2,'[b]Aperçu :[/b] Les [b]Orcs[/b] étaient, à l\'origine, un peuple pacifique aux croyances chamaniques résidant sur le monde de Draenor. Malheureusement, infectés par le sang démoniaque de Mannoroth le destructeur, les Orcs furent réduit en esclavage par la Légion Ardente, contraint de guerroyer contre les Draenei et de conquérir Azeroth. \nAprès de nombreuse années de joug, les Orcs ont réussi à se libérer de l\'emprise démoniaque et ont conquis leur liberté, pour revenir à leurs racines chamaniques.\nMaintenant, sous la direction de leur nouveau chef de guerre, les Orcs se construisent un nouveau foyer, où ils combattent pour l\'honneur, dans un monde étranger, haïs et calomniés.\n\n[b]Capitale :[/b] Les Orcs résident maintenant dans la ville d\'[zone=1637], du nom du défunt Orgrim Doomhammer, ancien chef de guerre de la Horde.\n\n[b]Zone de départ :[/b] Les Orcs commencent leurs quêtes en [zone=14].\n\n[b]Montures :[/b] [npc=3362], à Orgrimmar, vend une variété de loups ; [npc=33553], au tournoi d\'Argent, vend quelques montures distinctives'),(NULL,NULL,0,'reputation',0,2,'[b]Reputation[/b] is a rough measurement of how much you participate in the community--it is earned by convincing your peers that you know what you’re talking about. Our community puts just as much work as our developers do into making our site as awesome as it is and reputation is meant as a way for you to track just how much work you\'re putting into us.\r\n\r\nThe primary means of gaining reputation is by posting quality comments on database entries (which are then voted up by other site members) and by general contributions to the site which can include actions like data and screenshot submissions. Whenever you leave a comment on a database entry, your peers can then vote on these comments, and those votes will cause you to gain reputation. You can also earn reputation by voting on other users\' comments and by sending in reports!\r\n\r\nBy being a good-standing and contributing user you will be able to earn both reputation and achievements for many of the same actions!\r\n\r\n[h3]Reputation Gains[/h3]\r\n[div style=\"max-width:400px\"][table class=grid]\r\n[tr][td][url=?account=signup]Registering[/url] an account[/td]\r\n[td align=right class=no-wrap]CFG_REP_REWARD_REGISTER reputation[/td]\r\n[/tr]\r\n[tr][td]Daily visit[/td]\r\n[td align=right class=no-wrap]CFG_REP_REWARD_DAILYVISIT reputation[/td]\r\n[/tr]\r\n[tr][td]Posting a comment[/td]\r\n[td align=right class=no-wrap]CFG_REP_REWARD_COMMENT reputation[/td]\r\n[/tr]\r\n[tr][td]Your comment was voted up (each upvote)[/td]\r\n[td align=right class=no-wrap]CFG_REP_REWARD_UPVOTED reputation[/td]\r\n[/tr]\r\n[tr][td]Submitting a screenshot[/td]\r\n[td align=right class=no-wrap]CFG_REP_REWARD_SUBMIT_SCREENSHOT reputation[/td]\r\n[/tr]\r\n[tr][td]Suggesting a video[/td]\r\n[td align=right class=no-wrap]CFG_REP_REWARD_SUGGEST_VIDEO reputation[/td]\r\n[/tr]\r\n[tr][td]Submitting a guide (approved)[/td]\r\n[td align=right class=no-wrap]CFG_REP_REWARD_ARTICLE reputation[/td]\r\n[/tr]\r\n[tr][td]Filing a report (accepted)[/td]\r\n[td align=right class=no-wrap]CFG_REP_REWARD_GOOD_REPORT reputation[/td]\r\n[/tr]\r\n[/table][/div]\r\n\r\n\r\n[h3]Site Privileges[/h3]\r\nThe higher your reputation level, the more privileges you gain. Earn a high enough reputation to unlock additional rewards, in the form of new privileges around the site!\r\n[pad]\r\n[div style=\"max-width:400px\"][table class=grid]\r\n[tr][td]Post comments[/td]\r\n[td align=right class=no-wrap]CFG_REP_REQ_COMMENT reputation[/td]\r\n[/tr]\r\n[tr][td]Upvote on comments[/td]\r\n[td align=right class=no-wrap]CFG_REP_REQ_UPVOTE reputation[/td]\r\n[/tr]\r\n[tr][td]Downvote on comments[/td]\r\n[td align=right class=no-wrap]CFG_REP_REQ_DOWNVOTE reputation[/td]\r\n[/tr]\r\n[tr][td]More votes per day[/td]\r\n[td align=right class=no-wrap]CFG_REP_REQ_VOTEMORE_BASE reputation[/td]\r\n[/tr]\r\n[tr][td]Comment votes worth more[/td]\r\n[td align=right class=no-wrap]CFG_REP_REQ_SUPERVOTE reputation[/td]\r\n[/tr]\r\n[/table][/div]\r\n[pad]\r\n[url=?privileges]Check out full details on site privileges you can earn![/url]\r\n'),(NULL,NULL,0,'privilege=1',0,2,'[h3]Reputation required for posting comments?[/h3]\nThe very first privilege you can earn is the ability to post comments. Because this privilege requires only CFG_REP_REQ_COMMENT reputation, it is earned soon upon registering an account (which awards CFG_REP_REWARD_REGISTER reputation)! Keep this in mind if you\'ve recently registered to post on a contest thread.\n\n[h3]How do I post a comment?[/h3]\nOnce you have earned the ability to post comments, it\'s easy to do! Got some interesting information about an item? Strategies for earning an achievement or killing a boss? These are just a few examples of what could make a quality comment here!\n\nSimply visit any database page that you wish to leave a comment on and scroll down to the \'Contribute\' section. In the \'Add your comment\' tab, you can easily write and format your database comment. You can use our handy formatting buttons to improve the visual quality of your post, and easily add database links using the \'Links\' menu and entering database entry IDs. Once you\'re done, simply click the \'Submit\' button below and voila!\n\n[h3]Comment rating and you![/h3]\nAll comments made on database pages are subject to our rating system. This allows users who have reached the appropriate reputation level to upvote and downvote comments based on their quality. Making quality comments will earn you website reputation each time it has been upvoted, but make a poor quality comment and you may end up losing reputation if it is downvoted!\n\nFor more information on commenting, be sure to check out our handy [url=?help=commenting-and-you]Commenting and You[/url] guide in the website help section!'),(NULL,NULL,0,'privilege=2',0,2,'[h3]Posting External Links[/h3]\nOne of the first privileges allowed to users is the ability to post external links on the site. This will allow you to link to relevant information found on other websites from our database as well as in our forums. You can also add a link to your user profile, such as to your guild website or personal blog. Users without the appropriate reputation level will have their links filtered automatically, to help prevent spammers and malicious links from being posted on our website.\n\n[h3]Posting Policy[/h3]\nPlease be aware that some URLs may still be filtered out by our moderation team, as they made be deemed inappropriate or advertising. If you are uncertain whether or not a link will be considered advertisement, please do not hesitate to contact our Feedback team with any questions!\n'),(NULL,NULL,0,'privilege=4',0,2,'[h3]No CAPTCHAs[/h3]\nAh, CAPTCHAS. Love \'em or hate \'em, they\'re often a necessary evil for popular websites which allow any sort of user contribution. Here, we use [url=https://www.google.com/recaptcha/intro/index.html]ReCAPTCHA[/url] which helps thwart bots and spammers from abusing our forum and comment systems. Unfortunately, this also creates a minor inconvenience for our more active users, who are still occasionally asked to input a CAPTCHA despite long since establishing themselves as a legitimate member of the community. Well, not anymore! Users who reach the appropriate reputation level will no longer have to enter CAPTCHAs anywhere on the site!\n'),(NULL,NULL,0,'privilege=5',0,2,'[h3]Comment rating value increase[/h3]\nWhen you have reached a higher reputation level, your contributions to the site will raise in value! As a more trusted member of our community, your comment ratings will now have an increased weight and, as a result, have a greater effect on the total rating of a comment! Your vote contribution are doubled, so each of upvote will count as two votes (and each of your downvotes as two, as well)! This will allow higher reputation users to have more of an effect on considering quality of a comment, raising quality comments higher and lowering poor comments faster.\n'),(NULL,NULL,0,'privilege=9',0,2,'[h3]More votes per day[/h3]\nWe have a daily cap for comment votes set to CFG_USER_MAX_VOTES.\n\nThis privilege instantly increases the cap by 1, and then increases the cap by an additional 1 point for each CFG_REP_REQ_VOTEMORE_ADD reputation you have above CFG_REP_REQ_VOTEMORE_BASE.\n'),(NULL,NULL,0,'privilege=10',0,2,'[h3]Upvoting Comments[/h3]\nDid you find a comment particularly insightful or laugh out loud funny? Upvote it then! Upvoting is a way of giving props to those who truly contribute. From small guides to witty jokes, if a comment has enhanced your user experience, you should remember to upvote it.\n\nThe higher amount of upvotes a comment has, the higher up on the page it is. This way the community can help determine what comments are worth reading by sending some upvotes their way.\n\n[h3]Upvoting Policy[/h3]\nYou should not use upvotes to reward your friends or withhold upvotes to punish users you dislike. These are bannable offenses and you will probably lose your ability to upvote if we catch you doing it.\n'),(NULL,NULL,0,'privilege=11',0,2,'[h3]Downvoting Comments[/h3]\nDid you find a comment that was out of date, irrelevant, or otherwise less than useful? Downvote it then! Downvoting is a way of removing the clutter from the database and ensuring our comments are up to date. Downvotes remove an upvote--and if a comment has too many downvotes, it can even become a negative comment which appear at the end of an article rather than the beginning. \n\n[h3]Downvoting Policy[/h3]\nYou should not use downvotes to punish users you dislike nor should you downvote in quick succession. Try to use downvotes only to help us out, leaving personal bias out of it. If you abuse downvotes either by making too many in a short time frame or targeting a specific user, you may be warned and in some cases banned.\n'),(NULL,NULL,0,'privilege=12',0,2,'[h3]Replying to a Comment[/h3]\nYou can reply to comments easily and quickly with the new commenting system. All you have to do is leave a reply on an existing comment for this to work.\n\nA reply is best used to illustrate alternatives to a comment, highlight its accuracy, or expand on a joke. For example, if someone says an item drops from a certain boss but you know it does not, you could reply to explain it doesn\'t; it\'s likely people will find your comment helpful so they don\'t waste time trying to get the item from that NPC.\n\nPlease be aware that you should not use comments like forum threads for discussion.\n'),(NULL,NULL,0,'privilege=13',0,2,'Your avatar on the [url=CFG_BOARD_URL]Forums[/url] has an uncommon-quality green border.'),(NULL,NULL,0,'privilege=14',0,2,'Your avatar on the [url=CFG_BOARD_URL]Forums[/url] has a rare-quality blue border.'),(NULL,NULL,0,'privilege=15',0,2,'Your avatar on the [url=CFG_BOARD_URL]Forums[/url] has an epic-quality purple border.'),(NULL,NULL,0,'privilege=16',0,2,'Your avatar on the [url=CFG_BOARD_URL]Forums[/url] has a legendary-quality orange border.'),(NULL,NULL,0,'privilege=17',0,2,'[img src=STATIC_URL/images/premium/user-badge.png border=0 float=right]Unlock [url=HOST_URL/?premium]AoWoW Premium[/url] status for free.\n\nAs a Premium user, you can access a variety of perks:\n[ul]\n[li]Images in tooltips[/li]\n[li]Additional avatar borders[/li]\n[li]And much more![/li][/ul]\n\n'),(13,1,2,NULL,0,2,'[b][color=c1]Les Guerriers[/color][/b] sont une classe très puissante, avec la capacité de taner ou d\'infliger des dégâts de mêlée. Sa caractéristique principale est la force, mais les tanks s\'intéresseront également à l\'Endurance.\n\nCe combattant se bat avec une posture ce qui lui permet l\'accès à différentes capacités et lui accorde des bonus. Il utilisera [spell=71] pour tanker (appris au niveau 10) et [spell=2457] (appris au niveau 1) ou [spell=2458] (appris au niveau 30) pour les dégâts en mêlée.\n\nL\'arbre de protection du Guerrier contient de nombreux talents pour améliorer leur survie et générer des menaces contre les monstres. Les Guerriers de protection sont l\'une des principales classes de tank du jeu. Pour aller au combat, ils peuvent utiliser [spell=100] ou [spell=20252] mais seul le Guerrier protection peut protéger un allié en utilisant [spell=3411].\nIls ont également deux arbres de talent orientés sur les dégâts [icon name=ability_rogue_eviscerate][url=spells=7.1.26]Armes[/url][/icon] et [icon name=ability_warrior_innerrage][url=spells=7.1.256]Fury[/url][/icon], ce dernier comprend le talent [spell=46917], qui permet au Guerrier de manier deux armes à deux mains. Les Guerriers sont capable de faire de gros dégâts de zone avec des sorts tels que [spell=845], [spell=1680] et [spell=46924]. \n\nLe Guerrier porte une armure en plaques et aspire à la perfection dans les combats. Lorsqu\'il inflige ou subit des dégâts, il génère de la rage, utilisée pour alimenter ses attaques spéciales.\n[ul]\n[li] Allié utile, qui peut ajouter des buffs au groupe ou raid avec [spell=6673] et [spell=469], mais seul les Guerriers Fury peuvent fournir un buff passif [spell=29801] qui augmente les coups critiques en mêlée et à distance.[/li]\n[li] L\'avantages uniques des Guerriers, ce sont les 3 postures de combats.[/li]\n[li] Il peut choisir de se spécialiser dans le port d’armes à deux mains, d\'arme à une main, ou dans l\'utilisation du bouclier en plus d\'une arme à une main.[/li]\n[li] Et dispose de plusieurs techniques qui permettent de se déplacer rapidement sur le champ de bataille.[/li]\n[/ul]'),(13,2,2,NULL,0,2,'[b][color=c2]Les Paladins[/color][/b] sont des combattants qui utilisent la magie du sacré pour soigner les blessures et combattre le mal. Ils sont relativement autonomes et disposent de nombreuses techniques destinées à empêcher les morts. Le paladin peut choisir de se battre, de protégés ou de soigner, il utilisera le mana pour combattre le mal. Ses caractéristiques principales dépendent du rôle choisi.\n\nIl est un mélange d’un combattant en mêlée et d’un lanceur de sorts secondaires. Allié indispensable dans un combat, il renforce leurs amis avec de saintes auras (une aura active par paladin sur chaque membre du raid) et des bénédictions spécifiques pour les protéger du mal et renforcer leurs pouvoirs.\n\nPortant de lourdes armures, ils peuvent résister à des coups terribles dans les batailles les plus dures tout en guérissant leurs alliés blessés et en ressuscitant les morts. Au combat, ils peuvent utiliser des armes à deux mains, paralyser leurs ennemis, détruire des morts vivants et des démons, et les juger avec une sainte vengeance.\nLes paladins sont une classe défensive, principalement conçus pour survivre à leurs adversaires, grâce à leur assortiment de capacités défensives. Ils font aussi d’excellents tanks en utilisant leurs capacités [spell=25780].\n\n[ul]\n[li] Classe pouvant guérir, tanker avec leur précieux bouclier et infliger des dégâts en mêlée.[/li]\n[li] Renforce les alliées avec les [url=spells=7.2&filter=na=aura]Auras[/url], les [url=spells=7.2&filter=na=bénédiction]bénédictions[/url] et d’autres buffs.[/li]\n[li] Seule classe avec un véritable sort d’invulnérabilité [spell=642].[/li]\n[/ul]\n\n[b]Montures de classe :[/b]\n[ul]\n[li] [spell=13819] est un destrier royal que seuls les plus fervents des paladins peuvent appeler à leur service. Niveau 20 - Bonus de Vitesse de 60%. [/li]\n[li] [spell=23214] est un équipier infatigable capable d\'amener son valeureux maître dans tout Azeroth. Niveau 40 - Bonus de vitesse de 100%. [/li]\n[/ul]'),(13,4,2,NULL,0,2,'[b][color=c4]Les Voleurs[/color][/b] sont une classe de mêlée capable d\'infliger de grandes quantités de dégâts à leurs ennemis avec des attaques rapides en utilisant de l\'énergie comme ressources. Leurs caractéristiques principales sont la puissance d\'attaque et l\'agilité.\n\nLes Voleurs ont un puissant arsenal de compétences, dont beaucoup sont renforcés par leur capacité de furtivité et d\'étourdissement de leurs victimes. Capables d\'utiliser des poisons, ils paralysent leurs adversaires, les affaiblissant massivement dans la bataille. Avec l\'ambidextrie, ils peuvent utiliser une large gamme d\'armes, mais les Voleurs privilégient la dague, qui est la plus représentative de cette classe. \n\nCe sont les maîtres pour se déplacer furtivement autour de leurs ennemis, frapper dans l\'ombre un adversaire pour tenter de l\'achever rapidement puis s\'échapper du combat en un clin d’œil. \nIls endossent donc souvent le rôle d\'assassin ou d\'éclaireur, mais nombre d\'entre eux sont des loups solitaires.\n\n[ul]\n[li] Porte des armures en cuir.[/li]\n[li] Porte une arme dans chaque main.[/li]\n[li] Utilise une grand variété d\'armes de mêlée, comme les poignards, les armes de pugilats, les masses à une main, les épées à une main et les haches à une main.[/li]\n[li] Recouvre leurs armes avec du [url=items=0.-3&filter=na=poison;ub=4]poison[/url] pour gravement affaiblir leurs ennemis.[/li]\n[li] Utilise le [spell=1784] pour n’être visible que par les ennemis les plus perspicaces.[/li]\n[li] Cumule 5 points de combo pour infliger de puissants coups de grâce.[/li]\n[/ul]'),(13,3,2,NULL,0,2,'[b][color=c3]Les Chasseurs[/color][/b] sont une classe très unique dans le monde de World of Warcraft. C\'est la seule classe non-magique qui fait des dégâts à distance. Ils se battent avec des arcs, des armes à feu ou des arbalètes. Leurs caractéristiques principales sont la puissance d\'attaque et l\'agilité.\n\nLes Chasseurs se sentent chez eux dans la natures et ont une affinité spéciale avec les animaux. Il sait apprivoiser son propre [url=pets]familier[/url] qui l\'aidera à vaincre son ennemi. L\'animal du chasseur est unique, il possède un arbre de talent où le Chasseur peut attribuer des points dans des compétences diverses et des capacités passives. Chaques espèces de familier a une capacité spéciale unique. Le Chasseur peut rechercher les bêtes les plus appréciables en fonction de leurs apparences ou capacités. Seuls certains familiers ne sont accessibles que si le Chasseur choisi dans son arbre de talent [icon name=ability_hunter_beasttaming][url=spells=7.3.50]Maîtrise des bêtes[/url][/icon] qui lui donne accès aux bêtes « exotique » tels que [pet=46] ou [pet=39].\n\nPendant que leurs familiers attaques, les Chasseurs font pleuvoir leurs projectiles sur leurs malheureuses cibles. Ils préfèrent s’évader du corps-à-corps et ralentir leurs ennemis pour s\'éloigner et lancer leurs salves mortelles. Ils sont aussi capable de poser des pièges pour infliger des dégâts, ralentir ou rendre impossible toutes actions de leurs ennemis.\n\nLes Chasseurs portent des armures intermédiaires (cuir/maille) et utilisent le mana pour faire des dégâts.\n[ul]\n[li] Il peut voyager très vite en utilisant [spell=13161] et le partager avec [spell=13159].[/li]\n[li] Ils ont un certain nombre de compétence accès sur la survie qu\'ils peuvent utiliser pour échapper ou éviter un danger potentiel, comme [spell=5384] et [spell=781].[/li]\n[li] Les Chasseurs spécialisés dans la [icon name=ability_hunter_swiftstrike][url=spells=7.3.51]Survie[/url][/icon] peuvent avoir [spell=53292], ce qui leur permet de fournir aux membres du raid le [spell=57669].[/li]\n[/ul]'),(13,5,2,NULL,0,2,'[b][color=c5]Les Prêtres[/color][/b] sont généralement considérés comme l\'une des classes de soins les plus répandus dans World of Warcraft, car ils ont deux arbres de talents qui peuvent être utilisés pour guérir très efficacement. Les caractéristiques principales sont la puissance des sorts, l\'intelligence et l\'Esprit (s\'il s\'est spécialisé dans les soins).\n\nL\'arbre [icon name=spell_holy_holybolt][url=spells=7.5.56]Sacré[/url][/icon] comprend des talents qui renforcent fortement la guérison faite à leurs alliés, y compris des sorts qui peuvent être utilisés pour guérir plusieurs joueurs à la fois, comme [spell=48089]. \nL\'arbre de talent [icon name=spell_holy_wordfortitude][url=spells=7.5.613]Discipline[/url][/icon] se concentre principalement sur l\'absorption et l\'atténuation des dommages grâce à l\'utilisation de [spell=48066] et réduit les dégâts subis avec [spell=63944].\n\nLes Prêtres disposent d\'une grande palette d\'outils pour soigner, mais ils peuvent également sacrifier leurs soins pour infliger des dégâts grâce à la magie de l\'[icon name=spell_shadow_shadowwordpain][url=spells=7.5.78]Ombre[/url][/icon]. Ils sont alors capables d\'infliger des dégâts importants avec leurs capacités uniques et une fois qu\'ils se mettent en [spell=15473], leurs dégâts d\'ombre augmentent de manière significative tout en perdant la capacité de lancer des sorts du sacré.\n\nIl porte une armure en tissus, soigne les dégâts grâce à la magie du sacré mais inflige des dégâts grâce à la magie de l\'Ombre. Il utilise le mana comme ressource.\n[ul]\n[li] Fournissant les buffs les plus appréciés dans le jeu - [spell=48161], qui donne un buff d\'endurance indispensable à tout raid. Ils peuvent utiliser [spell=48073] et [spell=48169].[/li]\n[li] Les prêtres d\'ombre sont très sollicités dans n\'importe quel raid , fournissant le buff [spell=57669] pour stimuler la régénération de mana et peut même guérir leur propre groupe avec [spell=15286].[/li]\n[/ul]'),(13,8,2,NULL,1,2,'[b][color=c8]Les Mages[/color][/b] sont les utilisateurs emblématiques de la magie en Azeroth, qui apprennent leur art au cours de leurs recherches et études approfondies. Ils maîtrisent la magie du feu, du givre et des arcanes pour détruire ou neutraliser leurs ennemis. Leurs caractéristiques principales sont la puissance des sorts et l’intelligence.\n\nIls portent des armures légères, mais compensent cette faiblesse par une puissante gamme de sorts offensifs et défensifs. Le mage fait donc des gros dégâts à distance, envoyant des boules élémentaires sur un ennemi isolé mais faisant pleuvoir la destruction sur une armée. En cas d\'attaque, il peut échapper aux combats rapprochés avec [spell=1953] et devient un [spell=45438] quand cela devient trop critique.\n\nLes Mages peuvent également augmenter les pouvoirs de leurs alliés : [spell=23028], les inviter à leurs [spell=43987] et même les faire voyager à travers des [url=spells=7.8.237&filter=na=portail]portails[/url]. Classe indispensable pour voyager en toute tranquillité. Ils utilisent le mana comme ressource. Les Mages :\n[ul]\n[li]Transforment leurs ennemis en créatures inoffensives ou les geler sur place grâce à [spell=122].[/li]\n[li]Utilisent [item=50045] pour avoir un élémentaire d\'eau en familier.[/li]\n[/ul]'),(13,6,2,NULL,0,2,'[b][color=c6]Les Chevaliers de la mort[/color][/b] sont d\'anciens agent du Fléau, désormais alliés avec la Horde ou l\'Alliance. Cette classe de héros débute le jeu à haut niveau (55). Ses caractéristiques principales sont la force, sans oublier l\'endurance pour les tanks.\n\nTous leurs arbres de talent peuvent être utilisés pour faire des dégâts ou tanker.\n\nLes Chevaliers de la mort qui ont une affinité avec le [icon name=spell_deathknight_bloodboil][url=spells=7.6.770]Sang[/url][/icon] ont une grande capacité d’auto-guérison et peuvent fournir à un allié : [spell=49016] qui l’enrage à la vue du sang du champ de bataille.\nL’arbre de talent [icon name=spell_frost_freezingbreath][url=spells=7.6.771]Givre[/url][/icon] permet une augmentation significative de l’armure et spécialise le Chevalier de la mort dans les dégâts de zone avec [spell=49184]\nLes maîtres des maladies et des invocations sont les chevaliers de la mort [icon name=spell_deathknight_armyofthedead][url=spells=7.6.772]Impie[/url][/icon]. Ils peuvent utiliser leurs talents [spell=52143] et [spell=49206] pour être aidé lors des combats. Ils ont aussi une plus grande résistance à la magie grâce à la [spell=51052].\n\nLe chevalier de la mort utilise des runes comme ressource principale, dont chacun des trois types est utilisé pour différentes techniques.\n[ul]\n[li] Ils se battent avec les présences (semblable aux positions d\'un Guerrier) qui fournit des bonus spéciaux à leurs rôles.[/li]\n[li] Il dispose de plus de capacités à distance que la plupart de classes de corps à corps et privilégie les maladies et les dégâts infligés par ses familiers morts-vivants.[/li]\n[li] La classe de chevalier de la mort a sa propre capacité d\'enchantement d\'arme spéciale appelée [spell=53428], ce qui remplace le besoin d\'enchantements d\'armes classiques.[/li]\n[li] Ont accès à une zone spéciale inscrite inaccessible par toutes les autres classe : Acherus, le fort d’ébène, situé dans [zone=4298]. Où ils gagneront leurs points de talent en tant que récompenses de quêtes dans les premières heures de jeux.[/li]\n[/ul]\n\n[b]Montures de classe :[/b]\n[ul]\n[li] [spell=48778] - Niveau 55 - Bonus de Vitesse de 100%. [/li]\n[li] [spell=54729] - Niveau 60 - Bonus de vitesse : s’adapte à la compétence de monte. [/li]\n[/ul]'),(13,7,2,NULL,0,2,'[b][color=c7]Les Chamans[/color][/b], maîtres des éléments et de la nature, apportent un grand nombre de buffs à tout un groupe sous forme de totem. Un Chaman peut appeler un totem de chaque élément : terre, feu, eau et air. Ces totems apparaissent à leurs pieds et sont actifs pour toutes les personnes du raid se trouvant dans la zone d’effet du totem. Un bon Chaman sait quels totems sont à lancer et dans quelles circonstances les utiliser, pour maximiser les dégâts du groupe et la survie.\n\nIls sont principalement des lanceurs de sorts, bien qu’un Chaman [icon name=spell_nature_lightningshield][url=spells=7.7.373]Amélioration[/url][/icon] aime se rapprocher des ennemis pour faire de gros dégâts. Il apprend l’[spell=30798] et peut utiliser le sort [spell=51533] pour invoquer 2 Esprits de Loups qui combattent avec lui. Bien qu’il soit principalement de mêlée, le Chaman Amélioration peut bénéficier de la puissance des sorts et lancer instantanément [spell=403] ou des soins avec le talent [spell=51530]. \n\nLes Chamans [icon name=spell_nature_lightning][url=spells=7.7.375]Élémentaires[/url][/icon] se tiennent en retrait pour lancer leurs sorts de feu et de foudre et infliger de grandes quantités de dégâts. Ils peuvent repousser leurs ennemis avec [spell=51490] et aussi les enraciner avec [spell=51486]. Ils apportent le [icon name=spell_fire_totemofwrath][url=spell=57722]Totem de courrou[/url][/icon] et le [spell=51470], buffs très recherchés dans les raids.\n\nLes Chamans qui choisissent [icon name=spell_nature_magicimmunity][url=spells=7.7.374]Restauration[/url][/icon] ont un grand panel de sort de guérison se qui leurs permets de se spécialiser dans le soin mono-cible ou multi-cible. Ils sont reconnus pour leurs puissantes [spell=1064] et pour créer un [spell=16190] qui aide la restauration de mana aux membres de leurs groupes. Ils gagnent aussi un puissant [spell=974], peuvent employer [spell=51886] pour enlever les malédictions, et ont un sort de guérison instantané : [spell=61295] qui soigne aussi au fil du temps.\n\nLes Chamans invoquent la puissance des éléments pour améliorer les dégâts de leurs armes ou sorts. Ils portent des armures moyennes, boucliers et utilisent le mana comme ressources.\n[ul]\n[li] Il peut apprendre plus de 20 totems différents.[/li]\n[li] Peuvent lancer [spell=32182] (ou [spell=2825]) pour amplifier les dégâts et les soins de tout le raid. Un buff unique très recherché.[/li]\n[li] Un chaman peut se transformer en [spell=2645] à partir du niveau 16 et peut même le rendre instantané avec le talent [spell=16287]. Ce sort ne peut être utilisé qu\'en extérieur.[/li]\n[li] Il ne peut avoir qu\'un seul bouclier élémentaire d\'actif sur lui [spell=324] ou [spell=52127]. Le [spell=974], peut-être posé sur un autre joueur.[/li]\n[/ul]'),(13,11,2,NULL,0,2,'[b][color=c11]Les Druides[/color][/b] sont la « classe à tout faire » de World of Warcraft, c\'est-à-dire, capable de remplir tous les rôles : soigner, faire des dégâts à distance, faire des dégâts de mêlée ou tanker, en utilisant le Changeforme. Le druide offre donc aux joueurs de nombreux styles de jeu. Ses caractéristiques principales dépendent du rôle choisi.\n\nSous sa forme normale, c’est un lanceur de sorts qui peut se battre à distance et se soigner. Mais il peut aussi prendre d’autres formes dont des formes animales :\n\nLorsqu’un druide se transforme en [spell=5487] (et à un niveau plus avancé, [spell=9634]), son mana se change alors en rage, capable de charger sa cible, de la [spell=8983] et de subir des coups de plusieurs adversaires simultanément. C’est une forme orientée vers le tanking qui fournit une armure et de la vie supplémentaire. Il peut esquiver les coups, utiliser [spell=22812] pour augmenter sa résistance.\nQuand il se transforme en [spell=768], son mana se change alors en énergie, pouvant [spell=5215] tout en se déplaçant, d’augmenter parfois ça vitesse de courses de 70% et de bondir derrière ces ennemis pour attaquer avec le talent [spell=49376]. C’est une forme orienté vers les dégâts de mêlée en faisant saigner leur cible avec [spell=49800] ou [spell=62078] lorsque le druide est entouré d’ennemis.\nAvec les talents de druide équilibre, la [spell=24858] est réputé pour faire beaucoup de dégâts à distance notamment avec les sorts [spell=5176] et [spell=48505] qui peuvent être augmenté avec des points de talent. Il émet aussi une aura, qui augmente les coups critiques des sorts, très appréciée en raid.\nSa forme d’[spell=33891] (talent restauration) est conçue pour soigner sur la durée notamment avec les sorts [spell=33763] et [spell=48438]. Il émet une aura, qui augmente les soins de 6%. Il a la particularité d’avoir une grande régénération de mana.\n\nD’autres formes animales secondaires complètent cette liste : sa [spell=783] qui permet au druide d’augmenter sa vitesse de déplacement, sa [spell=1066] qui lui permet de respirer sous l’eau tout en nageant plus vite et sa [spell=33943] (et avec la compétence [spell=34091], la [spell=40120]) lui permet de voler instantanément.\n\n[ul]\n[li] Dans l’arbre de talent Combat farouche, les druides ont une aura [spell=17007] très utile pour tout groupe de raid.[/li]\n[li] Le sort [spell=20484] est utilisable en combat, mais à une recharge de 10 min.[/li]\n[li] Il possède le sort [spell=29166] qui lui permet de régénérer le mana très vite même en combat, sur lui ou tout autre membre.[/li]\n[li] Les Druides ont leur propre capacité de téléportation qui leur permet de voyager vers [zone=493], ce qui est utile lorsqu’ils ont besoin de s’entraîner.[/li]'),(13,9,2,NULL,0,2,'[b][color=c9]Les Démonistes[/color][/b], vêtue d’armure légère, sont les maîtres des arts démoniaques. Ils possèdent des capacités très puissantes qui, si elles sont utilisées correctement, en font un adversaire formidable. Utilisant leurs malédictions en combinaison avec des sorts de dégâts directs, il cause des ravages et la destruction. Ses caractéristiques principales sont la puissance des sorts et l’intelligence.\n\nLes Démonistes qui ont choisi de se spécialiser dans l’arbre de talent Affliction, excellent dans l’utilisation des malédictions, ils posent sur leurs ennemis [spell=47865] pour les affaiblir ou [spell=47864] pour leurs faire des dégâts. Ils ont la [spell=18271] ce qui augmente les dégâts des sorts d’ombre de 25%.\nLe démonologue appel des démons pour l’aider dans ces combats, il emploie principalement l’[spell=30146]. Il peut aussi se [spell=59672] en démon pour augmenter ses dégâts durant une courte période.\nLe Démonistes destruction utilise des sorts de feu tels que [spell=5740] ou [spell=17962] pour infliger d’importants dégâts directs.\n\nLes Démonistes, tout en étant d’excellent dans les dégâts à distance, soutiennent beaucoup leurs alliés en appelant d’autre joueur avec [spell=698] ou en utilisant des magies rituelles pour conjurer des pierres imbues du pouvoir de guérir : [icon name=inv_stone_04][url=item=5509]Pierre de soin[/url][/icon].\n\n[ul]\n[li] Le démoniste est doté du sort [spell=1454] qui lui permet de sacrifier des points de vie pour régénérer son mana.[/li]\n[li] Le [spell=48020] lui permet une grande mobilité en annulant tous les effets de déplacement, et en s\'éloignant du corps-à-corps.[/li]\n[li] En utilisant le sort [spell=20022], le démoniste permet à la personne sur qui elle a été appliqué de ressusciter.[/li]\n[/ul]\n\n[b]Montures de classe :[/b]\n[ul]\n[li] [spell=5784], leurs yeux ne brûlent plus que d\'une haine inextinguible pour les démonistes qui les ont corrompus - Niveau 20 - Bonus de Vitesse de 60%. [/li]\n[li] [spell=23161] sont des destriers recréés qui ont été corrompus par les énergies infernales, transpirant et soufflant le feu - Niveau 60 - Bonus de vitesse : 100%. [/li]\n[/ul]'),(8,81,2,NULL,0,2,'[b]Les Pitons du Tonnerre[/b] est la faction de la capitale des Taurens : [zone=1638], située dans la partie nord de la région de [zone=215]. L\'ensemble de la ville est construit sur des falaises à plusieurs centaines de pieds au-dessus du paysage environnant, elle est accessible par des ascenseurs sur les côtés sud-ouest et nord-est.\n\n[h3]Histoire[/h3]\n\nLa grande ville de Pitons du Tonnerre se trouve au sommet d\'une série de mesas qui donnent sur les prairies verdoyantes de Mulgore. Les Taurens, autrefois nomade, ont récemment construit la ville pour dresser un centre de caravanes commerciales avec des artisans itinérants et des artisans de toutes sortes. Elle a été établi par le puissant chef [npc=3057] après que les Taurens, avec l\'aide des Orcs, ont chassé les centaures qui habitaient à l\'origine Mulgore. De longs ponts de corde et de bois font la liaison entre les mesas qui sont surmontées de tentes, de longues maisons, de totems peints aux couleurs vives et de huttes spirituelles. Le chef de Tauren surveille la ville animée, en veillant à ce que les tribus unies de Tauren vivent en paix et en sécurité.\n\n[h3]Réputation[/h3]\n\n[npc=14728] offre une quête répétitive où il faut fournir des étoffes. Une fois exalté aux Pitons du Tonnerre, en récompense, les joueurs peuvent acheter des [url=?items=15.5&filter=na=Kodo;cr=93:92;crs=2:1;crv=0:0]kodos[/url].'),(8,1038,2,NULL,0,2,'[b]Ogri\'la[/b] est un groupe d\'Ogres localisé dans [zone=3522], où leur proximité avec [item=32572] leur a permis d\'évoluer au-delà de leur nature brutale. Ils sont particulièrement impliqué dans une guerre contre le Dragon noir et la Légion ardente, qui cherchent les cristaux Apogides pour leurs propres fins.\n\n[h3]Localisation[/h3]\nOgri\'la est situé près du bord ouest des Tranchantes, entre le Camp de Forge: Terreur et le Camp de Forge: Courroux, juste à l\'ouest de Sylvanaar. Ogri\'la est seulement accessible en monture volante ou en forme de vol. Une autre alternative est d\'avoir une réputation d\'honoré ou plus élevé avec [faction=1031]. Mais un joueur doit avoir une monture volante pour atteindre le camp Garde Ciel près de Skettis.[pad]\n\n[h3]Reputation[/h3]\nLa reputation avec Ogri\'la ne peut être acquise que par quêtes, et il n\'y a que des quêtes répétables dont les [url=?quests&filter=da=ja;cr=1;crs=1038;crv=0]quêtes journalières[/url]. Il ya un plafond sur la quantité de réputation que l\'on peut obtenir chaque jour pour un joueur avec Ogri\'la, ce qui en fait une réputation \"difficile à farmer\".\n\n[b]Eclats Apogides[/b]\n[item=32569] peuvent être collectées de diverses manières. Ils peuvent être pillés sur le cadavres de monstres, recueillis à partir de l\'environnement, ou ils peuvent être en récompenses de quêtes terminées.[pad]\n[b]Cristaux Apogies[/b]\n[item=32572] se ramassent sur les élites de type Demons ou Dragons dans les Tranchantes. Pour appeler ces mobs, 35 Eclats Apogides sont nécessaires, et il est recommandé que vous ayez un groupe de 5 personnes pour les vaincre.\n\n[b]Quêtes[/b]\nIl y a un certain [url=?quests&filter=cr=1;crs=1038;crv=0]nombre de quêtes[/url] qu\'un joueur peut faire pour gagner de la réputation avec Ogri\'la, ainsi que plusieurs [url=?quests&filter=da=ja;cr=1;crs=1038;crv=0]quêtes quotidiennes[/url]. Beaucoup de quêtes quotidiennes seront également accordée à la réputation de la Garde Ciel Sha\'tari lorsqu\'elles seront complétées. \n\nPour accéder aux principales quêtes d\'Ogri\'la, un joueur doit d\'abord compléter les 5 quêtes de groupe de [npc=22941].\n\n[h3]Éléments épuisés[/h3]\nUn certain nombre d\'éléments apogides tombent parfois de mobs une fois mort. Lorsque vous avez amassé 50 éclats apogides, [url=?search=Apexis+Crystal+Infusion]les objets suivants peuvent être améliorés[/url], obtenant des statistiques supplémentaires et des emplacements de gemmes. Une fois ces objets améliorés, ils deviendront liés si équipés, et peuvent donc être vendus ou échangés avec d\'autres joueurs. Une chose à noter cependant, bien que les éléments épuisés peuvent également avoir des statistiques ou des effets, ils ne peuvent pas être équipés.'),(8,911,2,NULL,0,2,'[b]Lune d\'Argent[/b] est la capitale des elfes de sang, située dans la partie nord-est de [zone=3430] dans le royaume de Quel\'Thalas. La capitale,des elfes de sang, est à couper le souffle. Elle peut rivaliser avec la capitale naine de [zone=1537], capitale la plus ancienne du monde toujours debout. Récemment reconstruite, la ville abrite la plus grande population d\'elfes de sang en Azeroth. \n\nAujourd\'hui, Lune d\'Argent n\'est que la moitié orientale de la ville d\'origine. La moitié occidentale a été presque entièrement détruite par le fléau pendant la troisième guerre. La place de lÉpervier, est la seule partie occidental de Lune d\'Argent restant sous le contrôle des elfes de sang. La Malebrèche, chemin parcouru par Arthas Menethil et son armée de morts-vivants parties en quête de ressusciter Kel\'Thuzad, traverse tout le Bois des Chants éternels. Il sépare la Lune d\'Argent reconstruite et ces ruines de la moitié occidentale. Fait intéressant, les ruines de Lune d\'Argent ne logent pas de morts-vivants, au lieu de cela, elles contiennent des [url=?npcs&filter=cr=37;crs=6;crv=1502;na=Déshérité;maxle=8]déshérités[/url] et des [npc=15638]. Dans l\'état actuel des choses, Lune d\'Argent est encore la plus grandes des villes Hordeuses.\n\n[h3]Histoire[/h3]\n\nLa ville de Lune d\'Argent a été fondée par les hauts élus après leur arrivée à Lordaeron, il y a des milliers d\'années. La ville a été construite en pierre blanche autour de plantes vivantes dans le style de l\'ancien Empire Kaldorei. La ville contenait les célèbres académies de Lune d\'Argent, centre d\'apprentissage de la magie arcane, et la Flèche de Solfurie, majestueux palais abritant la famille royale des hauts-elfes. Également basé dans la ville, la convocation de Lune d\'Argent, également connu sous le nom de « Le Concile de Lune d\'Argent », était l\'organe dirigeant des hauts-elfes. À travers une étendue d\'océan vers le nord, il y a l\'île qui contient le plateau du puits du Soleil.\n\nBien que Lune d\'Argent ait resorti relativement indemne de la deuxième guerre, dans la troisième guerre, le Chevalier de la mort Arthas a mené le Fléau dans la ville, l\'attaquant au cours de sa quête pour atteindre le puit du Soleil. Le roi High Elven a été tué et la majorité de la population a été exterminée. Les forces de fléau ont tenu la ville pendant un certain temps mais l\'ont abandonné après l\'épuisement de ses ressources. \n\nBien que la ville ait été attaquée par le Fléau, elle n\'est pas aussi détruite qu\'on pourrait le penser. Beaucoup de ses plantes sont mortes, quelques cadavres sont étendu sur le pavé, la ville était à l\'abri du feu et de la destruction. Lune d\'Argent ressemble maintenant à une ville fantôme, intacte, mais étrangement abandonnée. Néanmoins, les chasseurs de trésors fréquentent fréquemment les ruines de Lune d\'Argent pour essayer de trouver certains des artefacts précieux que les elfes ont laissés derrière avant de déserter la ville, mais les fantômes des anciens habitants de Lune d\'Argent les en empêchent.\n\n[h3]Réputation[/h3]\n\n[npc=20612] offre une quête répétitive où il faut fournir des étoffes. Une fois exalté à Lune d\'Argent, en récompense, les joueurs peuvent acheter des [url=?items=15.5&filter=cr=151;crs=6;crv=35513;na=Faucon-pérégrin]Faucon-pérégrins[/url].\n\nLes zones environnantes du Bois des Chants éternels et des terres fantômes contiennent la plupart des quêtes pour gagner de la réputation avec Lune d\'Argent.'),(8,577,2,NULL,0,2,'[minibox]\n[h2]Cartel Gentepression[/h2]\n[faction=21]\n[b]Long-guet[/b]\n[faction=369]\n[faction=470]\n[/Minibox]\n\n[b]Long-guet[/b], faction de la ville du même nom, est un poste commercial dirigé par les gobelins du Cartel Gentepression. Il se trouve au carrefour des principales routes commerciales du [zone=618].\n\n[h3]Histoire[/h3]\n\nCette ville est le dernier point de la civilisation avant d\'atteindre le Mont Hyjal. Il est géré par les gobelins comme un poste commercial. La ville est officiellement neutre pour toutes les races et factions. Seuls les pèlerins peuvent monter jusquà lArbre-Monde, point culminant du Mont Hyjal. Long-guet est donc la destination la plus haute que les marchands et les aventuriers peuvent atteindre sans l\'autorisation des Elfes de nuit. Elle offrirait une vue dominante sur Kalimdor, si les nuages qui enveloppent continuellement les flancs de la montagne, disparaissaient.\n\nLong-guet est le seul avant-poste de gobelin majeur dans le nord de Kalimdor. Tout d\'abord, il sert de base aux opérations pour les mineurs de thorium et d\'arcanites puisque le Berceau-de-lHiver possède quelques veines inexploitées de ces matériaux. Deuxièmement, il sert de centre d\'échanges entre l\'Alliance et la Horde. Alors que Long-guet est à peine plus sûr que Reflet-de-Lune, généralement, l\'Alliance et la Horde se traitent assez bien là-bas. En outre, Long-guet est un point d\'arrêt et de réapprovisionnement fréquent pour les fidèles qui font le pèlerinage du Berceau-de-lHiver au Mont Hyjal.\n\n[h3]Réputation[/h3]\n\nLa réputation de Long-guet et du Cartel Gentepressin provient surtout des quêtes du Berceau-de-lHiver. Avec une réputation au minimum amicale, les gardiens vous aident en cas dattaque initiée contre vous.'),(8,21,2,NULL,0,2,'[minibox]\n[h2]Cartel Gentepression[/h2]\n[b]Baie-du-Butin[/b]\n[faction=577]\n[faction=369]\n[faction=470]\n[/minibox]\n\n\n[b]Baie-du-Butin[/b] est une grande ville pirate nichée dans les falaises entourant un magnifique lagon bleu, à lextrémité de [zone=33]. Pour entrer dans la ville, il faut passer au travers les mâchoires blanchis d\'un requin géant.\n\nParcouru par les Écumeurs des Flots noirs qui sont étroitement associés eu Cartel Gentepression, le port offre des opportunités à n\'importe quel voyageur passant par là, indépendamment de leur faction. Combiné à la célèbre « taverne du Loup de mer », le [event=15], de nombreux maîtres de profession et des vendeurs, qui vendent de tout (des animaux de compagnie aux anneaux de diamant), c\'est l\'un des endroits les plus populaires en Azeroth.\n\n[npc=2496], chef de la ville, embauche toute l\'aide qu\'il peut obtenir contre [faction=87] et autres menaces de la ville. Il réside avec le chef des Écumeurs des Flots noirs, [npc=2487], au sommet de l\'auberge de Baie-du-Butin.\n\nEn raison de la liaison par bateau de Baie-du-Butin à Cabestan, les joueurs de tout niveau (surtout de la Horde, si le niveau est faible) peut-être croisés dans le port, bien que les visiteurs les plus fréquents seront dans les niveaux 35-45, car les quêtes disponibles auprès des gens du pays se situent dans cette tranche de niveau.\n\nL\'eau est parsemée de débris flottants et de bancs de poissons. Plusieurs types de poissons se pèchent dans les eaux de la Baie, tels que le [item=6359], le [item=6358], et l\'[item=13422]. La pêche, dans les débris flottants, vous donnera également plus de chance de pêcher des coffres et d\'autres articles, faisant de Baie-du-Butin un endroit idéal pour la pêche.\n\n[h3]Réputation[/h3]\nLa plupart des quêtes pour augmenter la réputation avec Baie-du-Butin sont situés au Cap de Strangleronce. Avec une réputation au minimum amicale, les gardes vous aiderons en cas dattaque contre vous.\n\nSi vous êtes haï avec Baie-du-butin vous pouvez faire la quête répétable [quest=9259] pour revenir à Neutre.'),(8,470,2,NULL,0,2,'[minibox]\n[h2]Cartel Gentepression[/h2]\n[faction=21]\n[faction=577]\n[faction=369]\n[b]Cabestan[/b]\n[/Minibox]\n\n[b]Cabestan[/b], faction de la ville du même nom, situé sur la côte est de Kalimdor dans [zone=17]. Elle est dirigée par des gobelins. Ses rues se répandent dans toutes les directions, et l\'architecture ne montre aucune cohérence ni vision commune. C\'est une ville de divertissement et de commerce, où tout ce que vous voudriez acheter est en vente mais aussi beaucoup de chose que personne ne veut jamais. \n\nCabestan est actuellement géré par un groupe d\'entreprises connu sous le nom du Cartel Gentepression, un groupe fragmenté de la KapitalRisk, qui a d\'abord construit la ville portuaire pour la négociation avec [zone=1637]. C\'est d\'abord une faction neutre où Horde et Alliance se côtoient. Un bateau relie commodément Cabestan à Baie-du-butin.\n\n[h3]Histoire[/h3]\n\nConstruit à part égales entre l\'industrie et de la décadence, la ville portuaire gobeline de Cabestan s\'étend sur près d\'un kilomètre de littoral des Tarides de l\'est, entre [zone=14] et [zone=15]. Cabestan est la fierté des gobelins, une ville commerciale où vous pouvez trouver presque tout ce que votre cur désire, et si quelque chose n\'est pas en stock, vous pouvez parier que les gobelins peuvent le commander. Cabestan est desservie régulièrement par les bateaux qui font la traversé en passant devant la forteresse de Theramore, vers le sud.\n\nCabestan est une ville où les habitants, qui étaient autrefois des truands, règnent maintenant. Ses rues errent sans rime ni raison à travers des quartiers dédiés à une seule activité : le commerce. Des entrepôts délabrés se situent à côté de maisons en pierre majestueuses. Les belles boutiques sont voisines avec des cabanes grossières. Des objets de toutes les formes, et certains au-delà de l\'imagination, sont exposés sur les marchés et les boutiques exclusives.\n\nLes Gobelins accueillent toutes personnes ayant de l\'or, des éléments de valeur et une volonté de les échanger contre leurs marchandises et leurs services. Les marchands traversent la ville tous les jours, vendent tout, de la soie aux esclaves. Même la nuit, les magasins qui bordent les rues et les allées restent ouverts aux entreprises. Ceux qui ont de l\'argent peuvent écouter des musiciens qualifiés, tout en buvant des bières fines et en mangeant des aliments préparés par des grands chefs. Pour ceux qui ont des goûts plus terriens, on retrouve le long des quais des marchants d\'armes, la banque et des casinos.\n\nCabestan est le plus grand port de Kalimdor, beaucoup de navires transportant de la cargaison sortent pour d\'autres sites autour de Kalimdor. En plus des navires commerciaux légitimes, les bâtiments pirates reçoivent une amnistie dans le port de Cabestan tant qu\'ils peuvent payer des droits d\'accostage rigides. Cette situation rend les capitaines marchands furieux, mais ils ne peuvent boycotter Cabestan, sinon c\'est la faillite pour leurs commerces. En outre, les avocats et les mercenaires qui rôdent sur le front de mer sont impatients de faire face à tous ceux qui cherchent à causer des problèmes.\n\n[h3]Réputation[/h3]\n\nLa plupart des quêtes pour élever la réputation avec Cabestan et le Cartel Gentepression sont situées dans les Tarides. Avoir une réputation au minimum amicale, les gardiens aident en cas d\'attaque contre vous.\n\nSi vous êtes détesté auprès de Cabestan, vous pouvez faire la quête répétable [quest=9267] pour revenir à une réputation Neutre.'),(8,369,2,NULL,0,2,'[minibox]\n[h2]Cartel Gentepression[/h2]\n[faction=21]\n[faction=577]\n[b]Gadgetzan[/b]\n[faction=470]\n[/minibox]\n\n[b]Gadgetzan[/b] est la faction de la ville du même nom, qui abrite les plus grands ingénieurs, alchimistes et marchands gobelins. Seul endroit de civilisation au nord du désert de [zone=440], elle est perçue comme une oasis. Gadgetzan est le siège du Cartel Gentepression, le plus grand cartel gobelin. Les gobelins croient au profit plus quà la loyauté, donc Gadgetzan est considéré comme territoire Neutre dans le conflit Horde / Alliance.\n\n[h3]Histoire[/h3]\n\nBien que la neutralité des gobelins soit presque universellement reconnue, il y a encore ceux qui cherchent à semer le chaos et lanarchie. Pour Gadgetzan, cela vient sous la forme des bandits Bat-le-désert, une bande de mécréants qui occupe le champ des Puisatiers et les ruines d\'Ombre-du-Zénith au Nord-est de Tanaris. Peu de Gobelins se soucient des ruines antiques (à moins quils y aient un trésor), les bandits peuvent avoir les vieux blocs de pierre. \nCependant, le champ des Puisatiers est vital pour la survie des gobelins, leur fournissant lor liquide du désert. Les tours d\'eau dans le champ ont été construites sous la chaleur ardente du soleil, par le travail de leurs esclaves. Les gobelins ne vont pas abandonner leurs tours durement gagnées, aussi facilement. Mais, ils doivent rester en ville pour arrêter le conflit, en apparence interminable, parmi les différents visiteurs et donc empêcher de perturber les affaires. Par conséquent, ils embauchent de braves mercenaires venant de tous les coins du monde pour les aider.\n\n[h3]Réputation[/h3]\n\nEn tuant les [url=?npcs=7&filter=na=mers+du+Sud]Flibustiers des mers du Sud[/url] et les [url=?npcs=7&filter=na=bat-le-désert]Bandits Bat-le-désert[/url], la réputation avec le cartel Gentepression augmentera. Ayant une réputation au minimum amicale, les gardes vous aideront en cas d\'attaque contre vous. Avoir une réputation exaltée signifie que les gardes ne vous attaqueront jamais même si vous lancez des attaques sur la faction opposée. \n\nLa plupart des quêtes associées à la faction Gadgetzan sont situées à Tanaris. \n\nSi vous êtes détestés avec Gadgetzan, vous pouvez faire la quête répétable [quest=9268] pour obtenir la Neutralité.'),(8,47,2,NULL,0,2,'[b]Forgefer[/b] est la faction associée à la capitale des nains, [zone=1537]. [npc=2784] règle son royaume de Khaz Modan de sa salle du trône dans la ville, et [npc=7937], chef des gnomes, a temporairement dû s\'établir dans Brikabrok après la récente chute de la ville gnome [zone=133].\n\n[h3]Histoire[/h3]\n\nForgefer est l\'ancienne demeure des nains, une merveille façonnée dans la pierre. Forgefer a été construite au cur même des montagnes, une ville souterraine qui abrite des explorateurs, des mineurs et des guerriers. Les portes massives de roche protègent la ville en temps de guerre, et la lave de la montagne est redirigée et distribuée à des fins de chaleur, d\'énergie et de forage. \nAvant que le clan de Sombrefer ne soit banni de la ville, menant à la Guerre des Trois Marteaux, Forgefer était le centre commercial et social de tous les clans nains. Il appartient maintenant au Clan Barbe-de-bronze. \nBeaucoup de bastions nains ont chuté pendant la Guerre de Lordaeron, entre la Horde et l\'Alliance, mais la puissante ville de Forgefer, nichée dans les sommets hivernaux de [zone=1] et protégée par ses grandes portes, n\'a jamais été violée par la Horde envahissante.\n\nRelativement récemment, Forgefer est également devenu le foyer des Exilés de Gnomeregan. Après la troisième guerre, la ville gnome fut envahie par Troggs. Depuis lors, un certain nombre de gnomes se sont installés à Forgefer, transformant une zone de cette ville à leur goût, une région connue sous le nom de Brikabrok.\n\nForgefer est l\'une des villes les plus peuplées du monde, venant après la ville humaine de [zone=1519], et abritant 20 000 personnes.\n\nAlors que l\'Alliance a été affaiblie par les événements récents, les nains de Forgefer, dirigés par le roi Magni Barbe-de-bronze, forment un nouveau futur dans le monde. \n\n[h3]Réputation[/h3]\n\n[npc=14723] offre une quête répétitive où il faut fournir des étoffes. Une fois exalté à Forgefer, en récompense, les joueurs peuvent acheter des [url=?items=15.5&filter=cr=93:92:151:151;crs=2:1:6:6;crv=0:0:33977:33976;na=bélier] béliers [/url].\n\nLes zones environnantes [zone=1], [zone=38] et [zone=11] contiennent la plupart des quêtes pour gagner de la réputation auprès de Forgefer.'),(8,54,2,NULL,0,2,'[b]Les Exilés de Gnomeregan[/b] est la faction des gnomes qui ont fui leur domicile, [zone=133] à [zone=1]. Elle a été détruite par [url=?npcs=7&filter=na=Trogg] les Troggs[/url] après une invasion toxique. Maintenant, membre de lalliance, la plupart sont situés à Brikabrok, une partie de la ville voisine [zone=1537], y compris le leader [npc=7937].\n\n[h3]Histoire[/h3]\n\nOn a spéculé que les gnomes ont été formés comme des robots par les titans, en raison de leur nature curieuse et de leurs compétences techniques. Ils vivaient autrefois dans la cité de Gnomeregan, sans doute la plus belle ville technologique du monde.\n\nLes gnomes étaient une race souterraine de bricoleurs, jusquà ce que les Troggs aient détruit Gnomeregan. Dans cette guerre, plus de 80% de la population gnome a été exterminé.\n\n[h3]Réputation[/h3]\n\n[npc=14724] offre une quêtes répétables où il faut fournir des étoffes. En étant exalté aux Exilés de Gnomeregan, les joueurs sont capables de conduire des [url=?items=15.5&filter=cr=93:92;crs=2:1;crv=0:0;na=mécanotrotteur]mécanotrotteurs[/url].\n[zone=1] contient la plupart des quêtes pour gagner la réputation avec les exiés de Gnomeregan.'),(8,72,2,NULL,0,2,'[b]Hurlevent[/b] est la faction associée à [zone=1519], la capitale des Humains. Elle est située dans la partie nord-ouest de la [zone=12]. L\'enfant roi, [npc=1747], réside dans le Donjon de Hurlevent, entouré de ses gardes du corps et de ses conseillers, [npc=1748] (le régent) et [npc=1749]. La ville est nommée ainsi à cause des rafales soudaines et occasionnelles créées par la forme spéciale des montagnes autour de la ville glorieuse.\n\n[h3]Histoire[/h3]\n\nPendant la Première Guerre, le Royaume d\'Azeroth, y compris sa capitale, le Donjon de Hurlevent, a été complètement détruit par la Horde. Ses survivants ont fui vers Lordaeron. Après que les orcs ont été vaincus, au Portail des Ténèbres, à la fin de la Deuxième Guerre, il a été décidé que la ville serait reconstruite, dépassant sa grandeur dantan. Des tailleurs de pierres et des architectes ont pu été rassemblés par les nobles de Hurlevent. Sous la directio de cette équipe, la plus qualifiée et la plus ingénieuse, Hurlevent a été reconstruit dans une période de temps incroyablement courte. Maintenant, à la fin de la troisième guerre, dans le renommé Royaume de Hurlevent. Cest l\'un des derniers bastions du pouvoir humain laissé dans le monde.\n\nAvec la chute des Royaumes du Nord, Hurlevent est de loin la ville la plus peuplée du monde. Avec une population de deux cents mille personnes (principalement humaines), elle sert à bien des égards comme le centre culturel et commercial de l\'Alliance, même avec un accès à la mer. Les humains qui vivent dans la ville sont généralement insouciants et artistiques, favorisant les vêtements légers et colorés, la cuisine et l\'art. Elle abrite l\'Académie des sciences arcanes, la seule école de sorcellerie dans les royaumes de l\'Est, ainsi que le SI:7, une organisation de renseignement.\n\nCependant, les gens de Hurlevent ont du mal à accepter le rôle de Theramore en tant que foyer de la nouvelle Alliance. Ils sont convaincus que Hurlevent devrait être l\'héritière légitime du rôle de la ville de Lordaeron comme par le passé, mais aussi que Theramore est attristé face à l\'aggravation de la situation au sein de Les Royaumes de l\'Est.\n\n[h3]Réputation[/h3]\n\n[npc=14722] propose une quête répétable pour obtenir une réputation plus élevée avec Hurlevent. En contrepartie d\'une réputation exaltée, les joueurs non-humains peuvent monter sur des chevaux.\n\nLa plupart des quêtes associées à Hurlevent viennent des zones environnantes de la forêt d\'Elwynn, [zone=40] et [zone=44].'),(8,930,2,NULL,0,2,'[b]Exodar[/b] est la faction associée à [zone=3557], la capitale enchantée des Draeneï construit avec la plus grande partie de leur vaisseau qui sest écrasé. Il est situé dans la partie ouest de l[zone=3524]. Le chef de la faction Exodar est [npc=17468], qui est situé près des maîtres de combat dans la Voûte des Lumières.\n\n[h3]Histoire[/h3]\n\nLes Draeneï rescapés du crash de leur vaisseau se sont récemment réveillés pour reconstruire lExodar, encore fumant de limpact. L\'Exodar était autrefois une structure de satellite naaru autour de la forteresse dimensionnelle du [url=?search=donjon+tempête]Donjon de la Tempête[/url]. L\'Exodar contient une grande quantité de merveilles technologiques (en raison de ses origines avec le Donjon), comme des «fils» magiquement enchantés qui transmettent de l\'énergie sainte dans tout le navire pour alimenter le chauffage et l\'éclairage, tout en augmentant les pouvoirs, déjà considérable, des Draeneï.\n\n[h3]Réputation[/h3]\n\nComme pour les autres grandes factions associées aux races principales, la réputation de l\'Exodar peut être acquise en faisant la quête répétable de [npc=20604] [small][/small], ou alors, en tuant la faction adverse dans [zone=2597] (les elfes de sang) et en faisant les quêtes appropriées. Avec la réputation, le joueur peut acheter des objets provenant de fournisseurs liés à Exodar pour 10% de moins et, une fois exalté, le joueur peut acheter [url=?Items=15.5&filter=na=elekk;cr=93:92;Crs=2:1;crv=0:0] diverses montures[/url].'),(8,69,2,NULL,0,2,'[b]Darnassus[/b] est la faction de la ville de [zone=1657], la capitale des Elfes de la nuit. La haute prêtresse, [npc=7999], réside dans le Temples de la Lune, entourée d\'autres surs d\'Elune. Dans l\'Enclave Cénarien, l\'[npc=3516] conduit le [faction=609], souvent en opposition directe avec ses autres druides à [zone=493] et Tyrande elle-même.\n\n[h3]Histoire[/h3]\n\nAu lendemain de la troisième guerre, les Elfes de la nuit devaient s\'adapter à leur existence mortelle. Un tel ajustement était loin d\'être facile. Beaucoup d\'Elfes de la nuit ne pouvaient pas s\'adapter aux perspectives de vieillissement, de maladie et de fragilité. En cherchant à retrouver leur immortalité, un certain nombre de druides capricieux conspiraient pour planter un arbre spécial qui rétablirait un lien entre leurs esprits et le monde éternel.\n\nAvec [npc=15362] disparu, Fandral Forteramure, le chef de la conspiration qui souhaitaient planter le nouvel Arbre-Monde, est devenu le nouvel Archidruide. En un rien de temps, lui et ses camarades druides ont pris les devants et ont planté le grand arbre, [zone=141], au large des côtes orageuses du nord de Kalimdor. Avec leur soin, l\'arbre a poussé au-dessus des nuages. Parmi les branches crépusculaires de l\'arbre colossal, la merveilleuse ville de Darnassus a pris racine. Cependant, l\'arbre n\'a pas été béni par la nature et s\'avère être corrompu par la Légion Ardente. Maintenant, la faune et même les membres de Teldrassil sont contaminés par une obscurité croissante.\n\n[h3]Réputation[/h3]\n\n[npc=14725] offre une quête répétable [quest=7800] utilisé par les joueurs de l\'Alliance pour obtenir le droit de monter des [url=?items=15.5&filter=cr=93:92:151;crs=2:1:6;crv=0:0:13086;na=sabre;si=-1]Sabres-de-nuit[/url]. Les joueurs qui sont au minimum niveau 44, cherchant à gagner la faveur de Darnassus, devraient trouver et compléter les quêtes de [zone=357]. Les quêtes sont associées à Darnassus et pourraient accroître considérablement votre réputation.'),(8,809,2,NULL,0,2,'Les [b]Shen\'dralar[/b] sont la faction des Elfes de nuit restant dans [zone=2557]. Ils sont un groupe qui pratique la magie arcane à son apogée sur les traces de leur ancienne reine Azshara, et de ses partisans, les Bien-nées. Ils vivent à Eldre\'Thalas (nom antérieur de Hache-tripes) depuis la fin de la guerre des Anciens. Ils sont peu nombreux, mais leur connaissance et leur pouvoir mystique sont géniaux.\n\nLeur chef, [npc=11486], était chargé de superviser la construction des pylônes pour contenir le grand démon [npc=11496] et absorber son pouvoir démoniaque. Après de longues et nombreuses années, le pouvoir des pylônes a commencé à diminuer, le prince a entrepris de tuer les elfes de nuit restants pour maintenir l\'énergie. Les esprits des défunts demandent vengeance, mais seuls des aventuriers aguerris peuvent le tuer. Faite-vite, il reste très peu d\'habitants en vie.\n\n[h3]Réputation[/h3]\n\nLa réputation peut être obtenue en rendant à plusieurs reprises les quêtes obtenus avec les trois Librams de Hache-Tripes : [item=18333], [item=18334] et [item=18332]. \nLa réputation peut être obtenue aussi via les livres de classe suivant :\n[ul] \n[li] [item=18357] - Guerrier [/li] \n[li] [item=18363] - Chaman [/li] \n[li] [item=18356] - Voleur [/li] \n[li] [item=18360] - Démoniste [/li] \n[li] [item=18362] - Prêtre [/li]\n[li] [item=18358] - Mage [/li]\n[li] [item=18364] - Druide [/li]\n[li] [item=18361] - Chasseur [/li]\n[li] [item=18359] - Paladin [/li]\n[li] [item=18401] - Guerrier et Paladin [/li] \n[/ul] \nLes livres de classe et les librams donnent 500 points de réputation chacun.'),(8,349,2,NULL,0,2,'[b]Ravenholdt[/b] est une guilde de voleurs et d\'assassins qui ne reçoit que ceux d\'une extraordinaire prouesse. Ils sont opposés à la [faction=70]. La quête, [quest=8249], est disponible pour les classes non-voleurs, mais elle nécessite l\'aide d\'un voleur pour obtenir les objets pour la quête. Le manoir de Ravenholdt, le siège de la faction, est situé dans [zone=36], mais pour y arriver, vous devez venir du coin nord-est de [zone=267].\n\n[h3]Réputation[/h3]\n\nTous les [url=?Search=Syndicat#npcs]membres du Syndicat [/url] donnent 1-5 points de réputation en fonction de votre niveau actuel. De plus, il existe quelques quêtes qui augmentent votre réputation, mais la méthode principale pour élever votre réputation provient des quêtes répétées pour fournir les objets demandés.\n\nVous commencez à une réputation Neutre (0/3000) avec Ravenholdt, ce qui signifie que si vous tuez un NPC de Ravenholdt avant d\'augmenter votre réputation d\'au moins 5, vous deviendrez hostile et ne pourrez jamais augmenter votre réputation. \nPour augmenter votre réputation de Neutre à Amicale, la quête répétable [quest=6701] est disponible. Vous devrez fournir 11-12 [item=17124] et une fois que vous êtes amical, cette quête n\'est plus disponible. Vous pouvez également fournir cinq [item=16885].\nPour augmenter votre réputation au-delà de Amical, le seul choix est la quête répétable, [quest=8249]. \n\n[h3]Récompense[/h3]\n\nIl n\'y a aucune récompense de faction connue pour obtenir que se soit avec une réputation Amicale, un honoré, révéré ou exalté, sauf que les gardes vous parlent avec plus de respect. \n\nCependant, La réputation Exalté est nécessaire pour obtenir le Haut-Fait : [achievement=2336].'),(8,87,2,NULL,0,2,'Les [b] Pirates de la Voile Sanglante [/b] semblent être l\'une de ces organisations, qui sont apparues en Azeroth pendant les événements menant à la troisième guerre et à la suite de la troisième guerre. Ils sont originaires du Rivage Cruel, où leur chef, l\'[npc=2546], organise les opérations. Ils ont maintenant l\'intention de paralyser et de piller la ville portuaire de [faction=21], contrôlée par le Cartel Gentepression et sous la protection des Ecumeurs des Flots noirs. Il est probable que les Pirates de le Voile Sanglante sont venus profiter de la perte actuelle de leur flotte, sur la côte de la [zone=45], dans laquelle deux de ses navires ont été détruits. Le navire restant a été obligé de trouver un abri dans une crique où son équipe lutte maintenant pour survivre aux escarmouches des Nagas.\n\nEn préparation de l\'attaque, les Pirates de la Voile Sanglante ont pris position dans des endroits clés près de la ville. À l\'heure actuelle, ils ont trois navires ancrés le long du littoral au sud de Baie-du-Butin, à l\'abri des canons défensifs de la ville. Des camps ont également été construits le long de la même côte en prévision de l\'attaque. En outre, une fête scoute a atterri juste à l\'ouest de l\'entrée de la ville, signalant toutes les activités, ainsi qu\'un camp construit le long de la route menant vers la ville, susceptible d\'empêcher tout renfort.\n\nLes Pirates de la Voile Sanglante cherchent à atteindre leurs objectifs sans avoir leurs forces engagées dans la bataille, à cette fin, chaque côté cherche maintenant l\'aide d\'aventuriers sympathiques à leur cause.\n\n[h3]Réputation [/h3]\n\nIl n\'y a qu\'une seule façon d\'augmenter votre réputation auprès des Pirates de la Voile Sanglante et c\'est de libérer votre colère contre tous les citoyens de Baie-du-Butin. Voici une liste de tous les citoyens de Baie-du-Butin et leur valeur de réputation. \n[ul]\n[li] [npc=4624] : 25 points de réputation gagné [/li]\n[li] [npc=15088] : 25 points de réputation gagné [/li]\n[li] [npc=2496] : 5 points de réputation gagné [/li]\n[li] [npc=2636] : 5 points de réputation gagné [/li]\n[li] [url=?Npcs&filter=cr=3;crs=21;crv=0] Plusieurs autres NPC [/url][/Li]\n[/Ul]\nLe montant gagné avec les Pirates de la Voile Sanglante est indiqué pour un niveau 60 non humain. Le montant perdu pour tuer un citoyen ne peut pas être démontré car il dépend de votre niveau actuel avec Baie-du-Butin et de l\'importance de la personne que vous tuez. En plus de cela, quand vous perdez de la réputation avec Baie-du-Butin, vous perdez la moitié dans les trois autres villes du Cartel Gentepression. Par exemple, si vous perdez 25 points avec Baie-du-Butin, vous perdrez 12,5 points avec [faction=470].\n\nLe moyen le plus rapide d\'augmenter votre réputation avec les Pirates de la Voile Sanglante est de tuer des habitants de Baie-du-Butin. Au début, cela peut sembler une tâche simple car les gardes n\'apparaissent pas aussi menaçants que les autres monstres auxquels un joueur est confronté dans le jeu. Cependant, les gardes sont très équipés pour neutraliser les joueurs de toute classe, afin d\'éviter que les gens ne s\'attaquent les uns les autres dans la ville. \n\nLe Cogneur de Baie-du-butin a l\'avantage avec plusieurs capacités. Lune dentre elle est lutilisation de filet pour vous bloquer sur place, vous empêchant de vous échapper. Une autre est le fait qu\'ils appellent dautres Cogneurs chaque fois que vous attaquiez un citoyen de la ville ou si vous êtes sous un statut hostile avec Baie-du-Butin, les joueurs peuvent bientôt se retrouver rapidement submergés par les Cogneurs.\nLa capacité la plus forte du Cogneur est quune fois qu\'il tire son arme, il est peu probable que vous vivez, si vous ne vous échappez pas assez vite. Chaque fois qu\'un Cogneur vous tire dessus, l\'attaque vous retient, tout comme une attaque de marteau d\'Ogre. La différence ici, est que le Cogneur peut tirer rapidement en succession, provoquant des lances de chaîne. Un joueur peut littéralement être jeté d\'un côté de la ville à l\'autre, ce qui vous empêche d\'attaquer. Plus souvent, vous vous retrouverez coincé dans un coin, incapable de bouger et incapable d\'attaquer avec tous les sortilèges interrompues par l\'attaque du Cogneur. Parce que les Cogneurs ne rangent pas leurs armes à feu une fois qu\'elles sont sorties, la meilleure façon d\'agir est de s\'enfuir.\n\nPar essais et erreurs, la plupart des gens ont découvert un endroit sûr pour tuer les Cogneurs de Baie-du-Butin. Si vous suivez le tunnel qui mène à la ville, le chemin de votre gauche qui mène à la maison du Forgeron est l\'endroit idéal pour tuer les gardes. Seuls deux gardes patrouillent sur ce chemin. Une fois qu\'ils sont partis, entrer dans la première construction sur le chemin pour provoquer un rassemblement. Un joueur devrait pouvoir tuer 2 à 4 Cogneurs avant que les deux Cogneurs de patrouille en appellent dautres. En moyenne, un joueur qui fait cela peut tuer environ 30 à 40 Cogneurs de Baie-du-Butin, gagnant environ 800 points de réputation auprès de la Voile Sanglante. Les Cogneurs ici ne semblent pas sortir leurs armes, mais si vous vous trouvez dans une mauvaise situation, vous pouvez sauter sur la balustrade, courir sur le chemin des eaux, pour vous échapper.\n\nPour augmenter votre réputation au-delà de honoré, seuls deux NPC vous le permettent : \n[ul]\n[li] [npc=9179] : 5 points de réputation toutes les 7 minutes jusquà révéré [/li]\n[li] [npc=26081]: 5 points de réputation toutes les 24 heures jusquà exalté [/li]\n[/Ul]\n\n[h3]Récompenses[/h3]\n\nDevenir amical avec Les Pirates de la Voile Sanglante, vous donnera accès aux éléments suivants :\n[ul]\n[li] [item=12185] - Invoque un [npc=11236] [/li]\n[li] [item=22742] [/li]\n[li] [item=22743] [/li]\n[li] [item=22745] [/li]\n[/Ul]\nVous aurez besoin d\'être honoré avec la Voile Sanglante pour [achievement=2336].'),(8,70,2,NULL,0,2,'Le[b] Syndicat [/b] est une organisation criminelle humaine qui opère principalement dans les [zone=45] et les [zone=36], bien que quelques petits campements soient éparpillés dans les [zone=267]. Leur effectif compte environ 3 000 personnes.\n\nIls ont trois chefs : [npc=2423], descendant du premier Lord d\'Alterac, qui dirige les actions du Syndicat dans les montagnes Alterac, [npc=2597] dirige les actions du Syndicat dans les Hautes Terres d\'Arathi à partir de la principale demeure, le Donjon semi-abandonnée de Stromgarde, et Lady Beve Perenolde, fille d\'Aiden Perenolde.\n\n[h3]Histoire[/h3]\n\nPendant la seconde guerre, Lord Perenolde qui dirige le royaume d\'Alterac, a été découvert pour être en liaison avec les orcs de la Horde. Perenolde croyait qu\'une victoire de le Horde était inévitable et offrait ainsi une aide à la Horde en suscitant des rébellions, en attaquant les bases de l\'Alliance et en leur fournissant des armes. Lorsque cette trahison fut découverte, l\'Alliance marchait contre Alterac et la détruisit. Perenolde et tous les nobles qui ont accompagné ses projets ont été dépouillés de leurs titres et de leurs terres. Beaucoup d\'entre eux ont réussi à s\'échapper, mais ont commencé à comploter pour se venger. En utilisant leur fortune encore considérables, la noblesse a engagé une bande de voleurs et d\'assassins, formant une organisation connue sous le nom de Syndicat.\n\nAu début, le but du Syndicat était simplement de répandre le chaos et le désordre, frappant des bases cachées dans les montagnes d\'Alterac. Avec la fin de la troisième guerre et le chaos qui suivie, les dirigeants du Syndicat ont vu leur chance de reprendre Alterac et de retrouver leurs anciens pouvoirs. Ils ont maintenant pris le contrôle de plusieurs avant-postes dans la région environnante, y compris le donjon abandonnée et une partie de la ville de Stromgarde.\n\nIls sont haïe par l\'Alliance, qu\'ils considèrent comme leurs ennemis mortels, et la Horde, qu\'ils considèrent comme des brutes faits pour travailler en esclaves. En conséquence, le Syndicat est maintenant chassé par les deux factions, avec [npc=10181], en particulier, une prime est sur sa tête, tous les membres du Syndicat capturés seront exécutés sommairement. En outre, [npc=4949] a commandé un certain nombre de ses agents, y compris [npc=2229], [npc=2239], [npc=2238] et leur chef [npc=2316] pour lancer une enquête sur la nature du Syndicate et ses activités, ainsi que pour récupérer [item=3498], un collier maintenant porté par Elysa, la maîtresse de Lord Aliden, qui appartenait à un son cher ami, [npc=18887].\n\n[h3]Réputation[/h3]\n\nLe Syndicat, en tant que faction dans World of Warcraft, est très étrange par rapport à la plupart des factions. En effet, que le meurtre des membres de cette faction ne réduira pas votre réputation. Pour la plupart des joueurs, qui ne sont pas voleur, la seule façon d\'afficher le Syndicat dans leur menu de réputation est de compléter la quête [quest=8249]. Cependant, la quête requiert [item=16885] ... que seuls les voleurs peuvent obtenir en volant à la tir des PNJ au-dessus du niveau cinquante ce qui rend difficile d\'organiser une telle transaction.\n\nActuellement, il n\'y a qu\'une seule option connue pour augmenter la réputation d\'un joueur avec le Syndicat, en tuant des membres de la faction [faction=349]. Il n\'y a pas de récompenses connues pour avoir augmenté la réputation du Syndicat. Les PNJ affiliés à Ravenholdt ne donnent que 1 point de réputation, à l\'exception de [npc=13085], qui donne 5 (bien que la perte de réputation correspondante avec Ravenholdt soit aussi cinq fois plus grande ). Tous les joueurs commençent à une réputation détestée de 32000/36000, il faudrait tuer 10 000 PNJ de Ravenholdt pour atteindre le statut neutre avec la faction. Malheureusement, l\'état neutre est le plus élevé que vous puissiez atteindre avec le Syndicat, ce n\'est pas pour dissuader les joueurs, aucun des NPC Ravenholdt ne grimpe la réputation.\n\n[b]AVERTISSEMENT[/b]: Si vous décidez de tuer les PNJ de Ravenholdt, sachez qu\'il n\'y a actuellement aucun moyen de restaurer votre positionnement avec Ravenholdt, si vous passez en dessous de Neutre. La raison du problème est qu\'aucune des quêtes qui donnent des points de réputation de Ravenholdt ne sera disponible car aucun des membres de Ravenholdt ne vous parleront. Cela signifierait qu\'il s\'agit d\'un changement permanent et que vous ne pourrez plus jamais interagir avec l\'un des NPC fidèles à Ravenholdt. Notez également que les joueurs commencent à la réputation de 0/3000 avec Ravenholdt, et le fait de tuer même un de leurs PNJ à ce niveau de réputation vous empêchera pour toujours de rétablir votre réputation avec eux.'),(8,59,2,NULL,0,2,'[b]La Confrérie du Thorium[/b] est un groupe d\'artisans d\'élite qui vend un certain nombre de recettes épiques, par contre, vous devez obtenir suffisamment de réputation avec eux. Tous les joueurs commencent à la réputation : Neutre.\n\n[h3]Histoire[/h3]\n\nLa [zone=51] abrite un groupe de nains exceptionnellement robustes qui se sont séparés du Clan Sombrefer. Sur les falaises surplombant la région appelée « Le Chaudron », dans le grand nord des Gorges des vents brulants, les nains de la Confrérie du Thorium ont établi une base d\'opérations, la Halte du Thorium. De là, ils surveillent de près les activités des nains de Sombrefer dans les Gorges des vents brûlants. Les aventuriers qui cherchent la Halte du Thorium trouveront que les nains de la Confrérie du Thorium qui donnent de grandes récompenses pour ceux qui les aident dans leur lutte sans fin contre leurs anciens frères.\n\nLa Confrérie du Thorium comprend de nombreux artisans exceptionnellement talentueux, et les forgerons de la Confrérie sont censés être parmi les meilleurs Azeroth. Ils possèdent les connaissances requises pour fabriquer les armes et les armures de [npc=11502], le Seigneur du Feu, mais n\'ont pas de main-d\'uvre pour obtenir les matériaux nécessaires à l\'artisanat. On raconte qu\'un membre de la Confrérie du Thorium a été habilité à échanger les recettes et les projets fabuleux des nains avec ceux qui peuvent prouver leur fidélité à la Confrérie. Bien sûr, pour prouver sa fidélité, l\'aventurier doit s\'aventurer au coeur de [zone=2717], le domaine de Ragnaros, le Seigneur du Feu lui-même, pour fournir aux nains les matières premières rares trouvées là-bas. Une tâche ardue, sans aucun doute, mais avoir accès aux secrets de la Confrérie du Thorium devrait s\'avérer être une récompense qui vaut bien l\'effort.\n\n[h3]Réputation[/h3]\n\n[b]De Neutre à Amical[/b]\n[ul]\n[li] Fournir : [item=18944], [item=3857] et [item=4234], [item=3575] ou [item=3356] au [npc=14624]. [/Li]\n[/ul]\n[b]De Amical à Honoré[/b]\n[ul]\n[li] Fournir : [item=18945] au [npc=14624]. [/Li] \n[/ul]\n[b]De Honoré à Exalté[/b]\n[ul]\n[li] Fournir : [item=11370] à [npc=12944]. [/Li]\n[li] Fournir : [item=17012] à Lokhtos Sombrescompte. [/Li]\n[li] Fournir : [item=17010] à Lokhtos Sombrescompte. [/Li]\n[li] Fournir : [item=17011] à Lokhtos Sombrescompte. [/Li]\n[li] Fournir : [item=11382] à Lokhtos Sombrescompte. [/Li] \n[/ul]'),(8,68,2,NULL,0,2,'[b]Fossoyeuse[/b] est la faction pour la capitale du même nom, [zone=1497], régie par Sylvanas Coursevent. La cité est situé dans la [zone=85], au bord nord des Royaumes de l\'Est. La ville proprement dite est sous les ruines de la ville historique de Lordaeron. Pour y entrer, vous traverserez les défenses extérieures en ruines de Lordaeron et la salle du trône abandonnée, jusqu\'à ce que vous atteigniez l\'un des trois ascenseurs gardés par deux abominations.\n\n[h3]Histoire[/h3]\n\nFossoyeuse était à l\'origine un système d\'égouts, de cryptes et de catacombes sous la capitale de Lordaeron. Après que la ville a été détruite par le Fléau, Arthas a reconstruit et agrandit le dédale de souterrain. Initialement, il voulait que Fossoyeuse soit son siège de pouvoir, d\'où il gouvernerait les terres de pestes. Cependant, peu de temps après la fin de la troisième guerre, Arthas a été obligé de retourner à Norfendre et de sauver le Roi Liche. En son absence, [npc=10181] et ses non-morts rebelles ont capturé les ruines de la ville. Peu de temps après, elle a découvert la grande forteresse souterraine et a décidé de l\'établir comme base principale des opérations pour les Réprouvés.\n\n[h3]Réputation[/h3]\n\n[npc=14729] offre une quête répétitive où il faut fournir des étoffes. Une fois exalté à Fossoyeuse, en récompense, les joueurs peuvent acheter des [url=?items=15.5&filter=cr=93:92;crs=2:1;crv=0:0;na=squelette] chevaux squelettiques [/url].\n\nLes zones environnantes [zone=267], [zone=130], et la [zone=85] contiennent la plupart des quêtes pour gagner de la réputation auprès de Fossoyeuse.'),(8,909,2,NULL,0,2,'La [b]Foire de Sombrelune[/b] est un mystérieux carnaval itinérant, qui parcourt non seulement Azeroth, mais aussi lOutreterre. Conduite par l\'inimitable [npc=14823], un gnome d\'héritage douteux et de racine inconnue. La Foire amène des jeux, des prix et des bibelots exotiques inattendus, puissants ou non, en [zone=215], à la [zone=12] ou à la [zone=3519] chaque mois.\n\nUne variété de divertissement est proposée par la Foire, mais l\'attraction la plus commune est la rédaction du billet. Plusieurs forains distribuent des [item=19182], répartis dans toute la Foire, ils offrent des bons contre des articles fabriqués par des travailleurs du cuir, des forgerons ou des ingénieurs ainsi que des objets rassemblés dans la nature tels que [item=11404] et [item=19933]. Les bons peuvent être échangés contre de nombreuses choses allant de la [item=19295] à des colliers de grande puissance.\n\nBeaucoup d\'aventuriers recherchent la Foire de Sombrelune pour trouver les mystiques [url=?items=15.0&filter=minle=1;cr=107;crs=0;crv=Combine+the+Ace]carte de Sombrelune[/url]. Les cartes de Sombrelune viennent en huit combinaisons, chacune ayant une suite de l\'As aux Huit. Avec la combinaison de toutes les cartes, la suite est créée qui commencera une quête pour vous envoyer à la foire de Sombrelune. \nChacune des huit suites produit un [url=?items=4.-4&filter=na=carte+sombrelune] bijou [/url] différent avec un effet différent, dont certains sont assez puissants.\n\nLe calendrier habituel de la Foire de Sombrelune arrive sur le site, le premier vendredi du mois et le départ commencera tôt le lundi suivant.'),(8,76,2,NULL,0,2,'[b]Orgrimmar[/b] est la faction de la capital des orcs : [zone=1637]. Situé au bord nord de [zone=14], la ville imposante abrite le chef de guerre orcs, [npc=4949].\n\n[h3]Histoire[/h3]\n\nThrall a dirigé les orcs vers le continent de Kalimdor, où ils ont fondé une nouvelle patrie avec l\'aide de leurs frères tauren. En nommant leur nouvelle terre, Durotar, nom du père assassiné de Thrall, les orcs se sont installés pour reconstruire leur société autrefois glorieuse. La malédiction démoniaque sur leur race a pris fin, la Horde a décidé de passer dun discours de conquête avec une coalition lâche à la survie et à la prospérité pour tous. Aidé par les nobles Taurens et les Trolls rusés de la tribu Sombrelance, Thrall et ses orcs attendaient une nouvelle ère de paix dans leur propre pays.\n\nDe là, ils ont commencé la création de la grande ville guerrière, Orgrimmar. Nommé de l\'ancien chef de guerre, Orgrim [color=#ff143c]Doomhammer[/color], la nouvelle ville a été construite en peu de temps, à l\'aide des gobelins, des Taurens, des trolls et de [color=#ff122a]Mok\'Nathal Rexxar[/color]. En dépit d\'avoir des problèmes avec les centaures, les harpies, les lézards de tonnerre enragés, les kobolds, et malheureusement, l\'Alliance, Orgrimmar a prospéré et est devenu le foyer des orcs et des Trolls Sombrelance.\n\nAujourd\'hui, Orgrimmar se trouve à la base d\'une montagne entre Durotar et [zone=16]. Une ville guerrière en effet, elle abrite d\'innombrables quantités d\'Orcs, Trolls, Taurens, et une quantité croissante de Réprouvés rejoignent maintenant la ville, ainsi que les Elfes de Sang qui ont récemment été acceptés dans la Horde.\n\n[h3]Réputation[/h3]\n\n[npc=14726] offre une quête répétitive où il faut fournir des étoffes. Une fois exalté à Orgrimmar, en récompense, les joueurs peuvent acheter des[url=?items=15.5&filter=cr=93:92;crs=2:1;crv=0:0;na=Loup] loups [/url].\n\nLes zones environnantes Durotar et [zone=17] contiennent la plupart des quêtes pour gagner de la réputation avec Orgrimmar.'),(8,530,2,NULL,0,2,'[b]Les Trolls Sombrelances[/b], tribu de Trolls exilés, ont uni leurs forces avec [npc=4949] et la Horde. Ils appellent maintenant [zone=1637] leur maison, qu\'ils partagent avec leurs alliés Orc. [npc=10540] est leur chef actuel.\n\n[h3]Histoire [/h3]\n\nLorsque les rivalités tribales ont éclaté dans l\'ancien Empire Gurubashi, la tribu Sombrelance s\'est trouvée chassée de sa patrie dans [zone=33]. S\'étant installés dans ce que l\'on croit aujourd\'hui être les îles brisées, la tribu se retrouve bientôt enchevêtrée dans un conflit avec une bande de murlocs. Leur sort semblait scellé jusqu\'à ce que Thrall, chef de guerre Orc, et son armée, nouvellement libérés, s\'emparent de leurs maisons. Contrôlée par une sorcière des mers, un groupe de murlocs a capturé le chef des Sombrelances, Sen\'jin, avec Thrall et plusieurs autres Orcs et Trolls. Thrall a réussi à se libérer avec d\'autres, mais n\'a finalement pas pu sauver le chef des Trolls. Bien que Sen\'jin ait été sacrifié par la sorcière des mers, il a pu révéler une vision qu\'il avait eu, dans laquelle Thrall conduirait les Sombrelances hors des îles.\n\nAprès son retour, Thrall et ses partisans ont réussi à repousser de nouvelles attaques de la sorcière des mers et de ses murlocs, et se sont à nouveau dirigés vers Kalimdor. Sous la direction de [npc=10540], les Sombrelances ont alors juré allégeance à la Horde de Thrall et les ont suivi. Maintenant considérés comme ennemis par toutes les autres tribus Trolls sauf les Vengebroches et les Zandalar, les Sombrelances sont aujourd\'hui méprisés. Pourtant, les Trolls Sombrelances n\'ont pas oublié quils ont été chassés de leurs terres ancestrales et cette animosité gardée est accentuée avec limpatience, surtout vers les autres tribus Trolls. Après avoir atteint la nouvelle patrie des Orcs, [zone=14], les trolls se sont alors installés sur les rives orientales du royaume Orc, les îles Echo.\n\nCependant, avec l\'arrivée de Kul Tiras et de sa marine, les Sombrelances ont été forcés de reculer à l\'intérieur des terres sous l\'assaut du commandant. Les Trolls, se battant avec la Horde aux côtés de leurs frères, ont vaincu l\'ennemi. Les Trolls ont alors réclamé leur nouvelle patrie. Peu de temps après, un sorcier du nom de [npc=3205] a commencé à utiliser la magie noire pour prendre possession de ses collègues Sombrelances. Au fur et à mesure que son armée de disciples augmentait, Vol\'jin ordonna que les trolls restant évacuent, alors Zalazane prit le contrôle des îles Echo. Les Sombrelances se sont installés sur la rive voisine, en nommant leur nouveau village en hommage à leur ancien chef Sen\'jin. Du village de Sen\'jin, ils envoient, avec leurs alliés, des forces pour combattre Zalazane et son armée asservie.\n\n[h3]Réputation[/h3]\n\n[npc=14727] offre une quête répétitive où il faut fournir des étoffes. Une fois exalté aux Trolls Sombrelances, en récompense, les joueurs peuvent acheter des [url=?items=15.5&filter=na=Raptor;cr=93:92;crs=2:1;crv=0:0] Raptors [/url].\nLa zone environnante, Durotar, contient la plupart des quêtes pour gagner de la réputation avec les Trolls Sombrelances. De plus, les joueurs de niveau supérieur ont également une bonne quantité de quêtes dans [zone=3521].'),(8,92,2,NULL,0,2,'[b]Les Gelkis[/b] sont une tribu de centaures qui ont construit leur campement dans les parties les plus au sud de [zone=405]. Ce sont les ennemis mortels des [faction=93], une tribu de frère située également dans le sud de Desolace. Le chef fondateur, ou Khan, des Gelkis était [npc=13741], deuxième de la prétendue progéniture de Zaetar et Theradras. Ils sont actuellement dirigés par [npc=5602] et ont pour représentant [npc=5397].\nLes Gelkis ne tiennent aucune alliance avec leurs tribus de frères, mais sont aussi connus pour agir à la fois hostilement et passivement envers les membres de l\'Alliance comme de la Horde.\n\n[h3]Histoire[/h3]\n\nInitialement dirigé par le Second Khan Gelk, les Gelkis se situaient dans les régions les plus au sud de Desolace lorsque la tribu centaure se divisa en cinq.\nLorsque la tribu Gelkis s\'est prononcée contre le Khan Magra, une éternelle querelle entre les Magram et les Gelkis est née.\n\nLes Gelkis considérés comme plus civilisés que leurs frères avec une structure sociale organisée et une compréhension ferme de la langue commune, respectent la nature et leur mère de naissance Theradras. \nAlors que les Magram prônent la force comme essentielle et que la survie de la tribu dépend de leur esprit de combat.\n\nPour alléger ce conflit, Theradras veille toujours sur les centaures et gardera les tribus en sécurité et en vie. Les Gelkis ont alors demandé sa protection et donc le pouvoir de la terre maintien leur existence. \n\nBien que la Magram considère que cela soit faible, il semblerait que ce soit une vue erronée, car des élémentaires peuvent être aperçu dans Village Gelkis, mettant un terme aux intrus indésirables aux côtés de leurs maîtres centaures.\n\n[h3]Réputation[/h3]\n\nCest une des deux factions situées en Desolace, vous devez avoir une certaine réputation auprès des Gelkis pour commencer leurs quêtes. La réputation pour les Gelkis peut être obtenue en tuant les [url=?Npcs=7&filter=na=Magram]centaures Magram[/url].\n\nVous gagnez 20 points de réputation chez les Gelkis et perds 100 avec la tribu Magram.'),(8,93,2,NULL,0,2,'[b]Les Magram[/b] sont une tribu de centaures qui construit leur campement dans les parties sud-est de [zone=405]. Ce sont les ennemis mortels de la [faction=92], une tribu de frère située également dans le sud de Desolace. Le chef fondateur, ou Khan, des Magram était [npc=13740], troisième de la prétendue progéniture de Zaetar et Theradras. Ils sont actuellement dirigés par [npc=5601] et ont pour représentant [npc=5398].\nLes Magram ne tiennent aucune alliance avec leurs tribus de frères, mais osont aussi connus pour agir à la fois hostilement et passivement envers les membres de l\'Alliance comme de la Horde.\n\n[h3]Histoire[/h3]\n\nÀ l\'origine menée par le troisième Khan Magra, les Magram se situaient contre les chaînes de montagnes de Desolace lorsque la tribu centaure se divisa en cinq.\nAvant la mort de Magra, il a installé l\'idée que la force était essentielle et que la survie de la tribu dépendait de son esprit de combat. Quand leur frère, la tribu Gelkis, s\'est prononcée contre cette notion, une éternelle querelle entre les deux tribus est née.\n\nLa poursuite de la force a continué à travers les Khans Magram jusqu\'à ce jour, transformant les centaures en des êtres violents et déterminés. Pour solidifier leur titre de plus fort, la tribu lutte encore férocement pour affaiblir ou détruire leurs clans de frères, considérant les Kolkar comme faible, les Gelkis comme une nuisance, et les Maraudon comme un formidable ennemi.\n\nOn peut supposer que la culture Magram s\'est développée autour de la force de culte avant tout. Par rapport aux Gelkis, les Magram tiennent des formes très primitives de la parole et de la structure sociale. Par exemple, leur compréhension commune est limitée et la position de Khan serait vraisemblablement recherchée par un démon de la mort.\n\n[h3]Réputation[/h3]\n\nC\'est une des deux factions situées à Desolace, vous devez avoir une certaine réputation auprès des Magram pour commencer leurs quêtes. La réputation pour les Magram peut être obtenue en tuant [url=?npcs=7&filter=na=Gelkis]les centaures Gelkis[/url]. \n\nVous gagnez 20 points de réputation chez les Magram et perds 100 avec la tribu Gelkis.'),(8,270,2,NULL,0,2,'Les trolls de la[b] Tribu Zandalar[/b] sont venus à île de Yojamba dans la [zone=33] pour recruter de l\'aide contre le Dieu du sang ressuscité et ses prêtres d\'Atal\'ai dans [zone=19] et [zone=1417].\n\n[h3]Histoire[/h3]\n\nLes Zandalar étaient les premiers trolls connus, tribu d\'où provenaient toutes les tribus. Au fil du temps, deux empires troll distincts ont émergé, l\'Amani et le Gurubashi. Ils existaient pendant des milliers d\'années jusqu\'à l\'avènement des Elfes de la nuit, qui ont combattu avec eux et ont finalement conduit les deux empires à l\'exil.\n\nÀ la suite du Great Sundering, les Gurubashi vaincus sont de plus en plus désespérés. En cherchant un moyen de survivre, ils ont enrôlé l\'aide du sauvage [npc=14834], également appelé Soulflayer. Hakkar s\'est transformé en un oppresseur impitoyable qui a exigé des sacrifices quotidiens de ses sujets, les Gurubashi se sont alors retournés contre leur sombre maître. Les tribus les plus fortes (y compris les Zandalar) se sont regroupées pour vaincre Hakkar et ses fidèles prêtres, les Atal\'ai. Les tribus unies ont vaincu le Dieu des Sang et ont expulsé les Atal\'ai, et malgré leur victoire, l\'Empire Gurubashi tomba peu de temps après.\n\nAu cours des dernières années, les prêtres d\'Atal\'ai ont découvert que la forme physique de Hakkar ne peut être convoquée que dans la capitale ancienne et déserte de l\'Empire Gurubashi, Zul\'Gurub. Malheureusement, au cur de cette nouvelle quête, les prêtres ont invoqué, avec succès, Hakkar, confirmant la présence du Soulflayer redouté au cur des ruines.\n\nAinsi, la tribu Zandalar est arrivée sur les rives d\'Azeroth pour combattre encore Hakkar. Mais le dieu du sang est devenu de plus en plus puissant, pliant plusieurs tribus à sa volonté, et même, commandant les avatars des dieux primitifs: chauve-souris, panthère, tigre, araignée et serpent. Avec les tribus trolls éparpillées, les Zandalri ont été forcés de recruter des aventuriers de diverse origine d\'Azeroth pour les rejoindre dans la bataille, et espèrent une fois de plus vaincre, le Soulflayer.\n\n[h3]Réputation[/h3]\n\nLa réputation avec la tribu Zandalar est obtenue en tuant les monstres et boss dans Zul\'Gurub. Des quêtes répétitives et spécifiques sont aussi disponibles, elles requièrent des éléments qui ont été abandonnés dans linstance. Chaque Zul\'Gurub donne environ 2 500 à 3 000 de réputation.\nAvant la croisade brûlante, la principale raison de monter la réputation avec la tribu était les enchantements [url=?Items=0.6&filter=na=Zandalar]dépaule[/url], [url=?items=0.6&filter=minrl=60;maxrl=60;cr=18:107;crs=4:0;crv=0:to+a+leg+or+head+slot+item]de tête et de jambe[/url]. De plus, il y avait des pièces darmure en récompense de quête à faire dans Zul\'Gurub nécessitant un niveau de réputation.'),(8,471,2,NULL,0,2,'[b]Les Marteaux-hardis[/b] sont un clan de nains actuellement centrés dans [zone=47] et la [zone=3520]. La faction a été supprimée dans le patch 2.0.1.\n\n[h3]Histoire[/h3]\n\nJuste avant le [objet=175739], le clan Marteaux-hardis, dirigé par Thane Khardros Marteaux-hardis, habitait les contreforts et les falaises autour de Forgefer. Le clan Marteaux-hardis a échoué à prendre le contrôle de [zone=1537], des clans Barbe-de-bronze et Sombrefer. Khardros et ses guerriers Marteaux-hardis se sont rendus au nord par les barrières de Dun Algaz et ont fondé leur propre royaume dans le lointain sommet de Grim Batol. Là, les Marteaux-hardis ont prospéré et reconstruit leurs richesses.\n\n[npc=9019] et ses Sombrefer ont juré de se venger de Forgefer. Thaurissan et sa femme sorcière, Modgud, ont lancé un attentat contre Forgefer et Grim Batol. les forces de Modgud ont commencé à franchir les portes de Grim Batol, elle a utilisé ses pouvoirs pour frapper la peur dans leurs curs. Les ombres se déplaçaient à son commandement, et des choses sombres se glissaient dans les profondeurs de la terre pour traquer les Marteaux-hardis dans leurs propres retranchements. Finalement, Modgud a franchi les portes et a assiégé la forteresse elle-même. Les Marteaux-hardis se sont battus désespérément, Khardros lui-même sest lancé dans la bataille pour tuer la sorcière reine. Avec leur reine perdue, les Sombrefer ont fui avant la fureur des Marteaux-hardis.\n\nUne fois que la menace immédiate des Sombrefer a été éliminée, les Marteaux-hardis sont rentrés à Grim Batol. Cependant, la mort du Modgud avait laissé une tache maléfique sur la forteresse de la montagne, et les Marteaux-hardis la trouvaient inhabitable. Khardros a conduit son peuple vers le nord vers les terres de Lordaeron. En s\'installant dans la région montagneuse des Hinterlands, et ces forêts luxuriantes, les Marteaux-hardis ont construit la ville de Nid-de-laigle, où les Marteaux-hardis se sont rapprochés de la nature et même liés aux puissants griffons de la région.\n\nLa menace la plus immédiate pour leurs sécurités vient de l\'est sous la forme de deux clans trolls, les Vilebranches et les Fanécorces. Ils sont les plus célèbres pour organiser des batailles contre la ville des Marteaux-hardis, tout en brandissant des armes puissantes.\nLes nains Marteaux-hardis ont un certain nombre de clans, chacun gouverné par un Thane. Le plus fort Thane règne sur Nid-de-laigle.'),(8,509,2,NULL,0,2,'[b]La Ligue d\'Arathor[/b] a été initialement établie par les survivants du Royaume de Stromgarde pour récupérer la [zone=45] des mains des Profanateurs au Trépas d\'Orgrim. Aujourd\'hui, c\'est une organisation à l\'appui de l\'Alliance, basée sur [zone=3358] dans le Refuge de lOrnière. Ils se sont chargés d\'aider à fournir des forces, pour l\'Alliance, lorsque cest nécessaire, leurs membres incluent toutes les races de l\'Alliance mais se sont encore principalement des humains stromgardiens.\n\n[h3]Réputation[/h3]\n\nLes joueurs peuvent gagner la réputation dans cette faction en participant au champ de bataille du bassin Arathi. Lorsque vous vous battez dans le bassin d\'Arathi, vous gagnez 10 points de réputation pour 160 ressources. Sur les weekends d[event=20], les ressources requises sont ramenées à 150.\n\nOn vous accorde le titre, [title=48], une fois exalté avec Ligue dArathor et les deux autres factions du champ de bataille, [faction=890] et [faction=730].'),(8,730,2,NULL,0,2,'[b]Les Gardes Foudrepiques[/b] est la faction de l\'Alliance dans le champ de bataille [zone=2597]. Ils sont une expédition de nains du clan Foudrepique, originaire des « vallées d\'Alterac » dans [zone=36]. La recherche des Foudrepiques pour les reliques de leurs passés et la récolte de ressources dans la vallée d\'Alterac ont conduit à une guerre ouverte avec les Orcs de la [faction=729] habitant dans la partie sud de la vallée. Ils ont également reçu un « ordre de la souveraineté impérialiste » par [npc=2784] pour prendre les vallées d\'Alterac pour [zone=1537].\n\nLa principale base des Foudrepiques est Dun Baldar, où son chef, [npc=11948], réside avec ses maréchaux. Son second commandant, [npc=11949], se trouve au sud de Dun Baldar, à Cur de pierre.\n\n[h3]Réputation[/h3]\n\nLes joueurs peuvent gagner leurs réputation, dans cette faction, en participant au champ de bataille de la vallée dAlterac, en faisant diverses tâches et en tuant les membres de la faction adverse, le clan Frostwolf.\n\nOn vous accorde le titre : [title=48] au joueur, une fois quil est exalté avec les Gardes Foudrepiques et les deux autres factions des champs de bataille, [faction=890] et [faction=509].'),(8,510,2,NULL,0,2,'[b]Les Profanateurs[/b] cherchent à feuilleter la [faction=509] dans le champ de bataille, [zone=3358]. Aujourd\'hui, c\'est une organisation à l\'appui de la Horde, basée au Trépas dOrgrim dans [zone=45]. Ils se sont investis pour aider les forces de la Horde, au besoin, et leurs membres incluent toutes les races de la Horde, même si, se sont encore principalement des Orcs.\n\n[h3]Réputation[/h3]\n\nLes joueurs peuvent gagner la réputation dans cette faction en participant au champ de bataille du bassin Arathi. Lorsque vous vous battez dans le bassin d\'Arathi, vous gagnez 10 points de réputation pour 160 ressources. Sur les weekends d[event=20], les ressources requises sont ramenées à 150.\n\nOn vous accorde le titre, [title=48], une fois exalté avec les Profanateurs et les deux autres factions du champ de bataille, [faction=889] et [faction=729].'),(8,529,2,NULL,0,2,'L[b]Aube dArgent[/b] est une organisation axée sur la protection d\'Azeroth des menaces qui cherchent à la détruire, comme la Légion Ardente et le Fléau. Les forteresses de l\'Aube d\'Argent se trouvent dans les [zone=139] et les [zone=28]. Elle maintient également une présence dans [zone=1657] et dans les [zone=85], et dans dautres zones moins remarquables. La réputation avec lAube dArgent peut être utilisée pour acheter divers plans, consommables, et pour atténuer le coût à [zone=3456]. Avec l\'expansion « Burnning Croisade », la réputation de lAube dArgent a diminué en valeur.\n\nLe [item=22999] a pour icône un lever de soleil argenté.\n\n[h3]Histoire[/h3]\n\nAprès la mort du [npc=16062], la corruption de la Croisade Écarlate est devenu évidente pour certains de ses membres, qui ont par la suite abandonné les rangs de la [url=?npcs&filter=na=croisade%20écarlate;ex=on]Croisade Écarlate[/url] et a créé lAube dArgent pour protéger Azeroth de la menace du Fléau sans présence de fanatique dans la Croisade Écarlate.\n\nAlors qu\'ils partagent les mêmes objectifs que la Croisade, lAube dArgent a ouvert ses rangs non seulement aux races de l\'Alliance, mais aussi aux membres de la Horde et même à certains des Réprouvés. Ils mettent en garde contre la discrétion et l\'introspection, et mettent beaucoup l\'accent sur la recherche du Fléau et sur la façon de le combattre.\n\nAvec le temps, lAube dArgent s\'est diversifié, comme le Fléau qui s\'est divisé de nouveau, avec un rejeton appelé la Fraternité de la Lumière, un compromis entre l\'approche plus savante de lAube dArgent et le fanatisme de la Croisade écarlate.\n\n[h3] Réputation [/h3]\n\n[b]Les pierres du Fléau[/b]\nTout en portant un bijou accordant l\'effet « Commission pour lAube dArgent », les personnages peuvent tuer des monstres mort-vivants pour leurs [url=?items=12&filter=cr=151;crs=6;crv=43169;na=pierre%20du%20fléau] pierres du Fléau[/url] et ensuite les transformer en monnaies échange contre [item=12844]. Les quêtes requièrent beaucoup de [item=12843], [item=12841] et [item=12840]. Il convient de noter que les monnaies déchanges reçus des entités doivent être sauvegardés jusqu\'à ce que le statut de Révéré soit atteint, car les quêtes ne donneront plus de réputation après.\n\nUne autre façon daugmenter la réputation avec lAube dArgent est de faire la quête répétable « Chaudron ». Les chaudrons sont une source de « production » de membres du Fléau.\n\nComme la plupart des factions, le joueur peut faire des instances pour augmenter sa réputation. Les instances associées sont [zone=2017] et [zone=2057]. Naturellement, ces instances incluent également des quêtes qui augmentent la réputation de lAube dArgent.'),(8,933,2,NULL,0,2,'[b]Le Consortium[/b],dirigé par [npc=19674], sont des passeurs éthérés, des commerçants et des voleurs qui sont venus en Outreterre. Le principal base d\'opérations et le plus grand rassemblement se trouve à Foudreflèche, mais ils peuvent être trouvés à[color=#ff0537] Midrealm Post[/color], Aeris Landing, près d\'Auchindoun à [zone=3792] et dans d\'autres endroits.\n\nEn arrivant à un statut amical, les joueurs sont officiellement considérés comme membres du Consortium et bénéficient d\'un salaire. Le salaire est un sac de gemmes au début de chaque mois, donné par [npc=18265] chez Aeris Landing. Une plus grande réputation avec le Consortium produit des qualités et quantités supérieures de gemmes chaque mois.\n\n[h3]Réputation[/h3]\n\n[b]Jusqu\'à Amical[/b]\n[ul]\n[li]Faire le donjon Tombe-mana en [i]mode normal[/i] rapporte environs 1 200 points de réputation[/li]\n[li]Donner des [item=25416] à [npc=18265].[/li]\n[li]Donner des [item=25463] à [npc=18333].[/li]\n[/ul]\n\n[b]De amical à honoré[/b]\n[ul]\n[li]Faire Tombe-mana en [i]mode normal[/i] rapporte environs 1 200 point de réputation.[/li]\n[li]Activer les [item=25433] à [npc=18265].[/li]\n[li]Donner des [item=29209] à [npc=19880].[/li]\n[/ul]\n\n[b]De honoré à exalté[/b]\n[ul]\n[li]Faire Tombe-mana en [i]mode héroïque[/i] rapporte environs 2 400 points de réputation.[/li]\n[li]Faire toutes les [url=?Quêtes et filtre=cr=1;crs=933;crv=0]quêtes[/url].[/li]\n[li]Donner des [item=25433] à [npc=18265].[/li]\n[li]Donner des [item=29209] à [npc=19880].[/li]\n[/ul]\n\nToutes personnes qui essayent de gagner simultanément la réputation du Consortium et des [faction=941] ou [faction=978] peuvent se concentrer à tuer des ogres ([url=?npcs&filter=na=rochepoing;cr=6;crs=3518;crv=0]Rochepoing[/url], [url=?npcs&filter=na=cogneguerre;cr=6;crs=3518;crv=0]Cogneguerre[/url]) à Nagrand et rendre les perles de guerre obsidienne au Consortium.\n\nLa seule mise en garde est le taux de loot, soit environ 33% pour les Cogneguerre, alors qu\'il est de 50% pour les insignes. Si vous êtes au niveau 70 et que vous voulez monter cette réputation plus rapidement sans se soucier de la réputation de Mag\'har / Kurenai, vous voudrez peut-être donner des insignes à la place. Ensuite, les ogres sont généralement plus faciles à tuer, allant du niveau 65 à 67. Le choix dépend finalement du joueur.'),(8,932,2,NULL,0,2,'[b]L\'Aldor[/b] est un ancien ordre de prêtres draeneïs qui vénèrent les naaru, et à ce jour ils assistent les naaru [faction=935] dans leur combat contre [npc=22917] et la Légion Ardente. Ils se trouvent principalement dans la [zone=3520] et [zone=3703]. Bien qu\'ils aient beaucoup souffert des Elfes du sang qui sont devenus [faction=934], ont mis de côté une guerre ouverte contre les Sha\'tar. Le temple le plus saint de l\'Aldor repose sur léminence de l\'Aldor, surplombant la ville à l\'ouest.\n\nLa plupart des joueurs commenceront à une réputation neutre auprès de l\'Aldor. [npc=18166] à Shattrath donnera aux joueurs une première quête pour devenir amical avec Aldor ou Les clairvoyants. Ce choix est réversible si les joueurs ressentent le besoin.\nLes joueurs de Draenei seront directement amicaux avec Aldor et hostiles avec les Clairvoyants, alors que les joueurs Elfe du sang seront hostiles à l\'Aldor et amicaux envers les Clairvoyants.\n\n[npc=19321] et [npc=20807] sont situés dans la banque Aldor, sur le bord nord de la terrasse de la lumière. Le sanctuaire de la lumière sans fin sur léminence de l\'Aldor abrite [npc=20616] [petit][/small] et [npc=21906] [petit][/small], qui échangent, respectivement, des jetons épiques d\'armure contre des pièces de set de [url=?Itemsets&filter=ta=12]Niveau 4[/url] et de [url=?Itemsets&filter=ta=13]Niveau 5[/url].\n\n[i]Note : Les gains de réputation avec Aldor correspondent à une perte de réputation de 10% plus élevée chez les Clairvoyants. La plupart des gains de réputation avec Aldor accorderont également 50% de la réputation avec le Sha\'tar.[/i]\n\n[h3]Réputation[/h3]\n\n[b]Jusqu\'à honoré[/b]\n\nLes joueurs qui cherchent à gagner les rangs de réputation supérieurs (Révéré, Exalté) peuvent vouloir sauver des quêtes non répétables jusqu\'à ce qu\'ils soient honorés.\n\nDonner 10 [span class=q1][item=29425][/span] à [npc=18537] dans léminence de l\'Aldor accordera 250 points de réputation pour l\'Aldor. Il existe également une quête répétable où donner une unique marque accorde 25 points de réputation. Ces marques tombent sur des membres inférieurs de la Légion Ardente trouvés dans la plupart des zones de Outreterre, y compris les deux camps au nord d\'Auchindoun dans les déchets osseux de [zone=3519].\nEnviron 240 marques sont nécessaires pour passer d\'amical à honoré.\nEn outre, ces quêtes fournissent de la réputation de Sha\'tar ; 125 points de réputation pour 10 marques ou 12,5 points de réputation pour une unique marque.\n\nLes joueurs qui souhaitent également faire la réputation des factions [faction=978] ou [faction=941] iront tuer des Orcs à la forteresse de Kil\'Sorrow dans le sud-est de [zone=3518], car ils donnent des marques ainsi que 10 points de réputation auprès des Kurenai ou des Mag\'har.\n\n[b]Jusqu\'à Exalté[/b]\n\nUne fois que vous atteignez le niveau 68, vous pouvez également donner 10 [span class=q1] [item=30809][/span], c\'est le même principe que les marques de Kil\'jaeden mais ceux-ci tombent sur des partisans de haut rang de la Légion Ardente. Si vous le souhaitez, vous pouvez transformer les marques de niveau supérieur avant la réputation honorée. Dans [zone=3522], la porte de la mort dispose du plus grand nombre de membre avec ce grade.\n\n[b]Arme gangrenée[/b]\n\n[span class=q2][item=29740][/span] peut être donné à tout moment à [npc=18538] [small][/small] à léminence de l\'Aldor. Cela augmentera votre réputation avec l\'Aldor de 350 par arme gangrenée.\nEn plus des gains de réputation, vous recevrez [span class=q1][item=29735][/span], qui est la condition pour acheter lenchantement d\'épaule à [npc=20807] dans la banque de l\'Aldor.\n\n[h3]Passer à la réputation de l\'Aldor[/h3]\n\nPour changer votre faction des Claivoyants vers l\'Aldor et donc pour accéder à leurs recettes d\'artisanat (et annuler toutes les réputations que vous avez faites), trouvez [npc=18597], un membre de l\'Aldor dans la ville basse. Elle propose une quête répétable où pour 8x [span class=q1][item=25802][/span] vous montez la réputation Aldor. Une fois que vous êtes neutre, vous ne pourrez plus recevoir cette quête.'),(8,922,2,NULL,0,2,'[b]Tranquilliens[/b] a été reprise par les Réprouvés et les Elfes de sang puis est devenu une faction des [zone=3433].\n\n[h3]Histoire[/h3]\n\nAlors que l\'armée du Fléau faisait son chemin vers le Puit-du-Soleil, les elfes n\'avaient pas d\'autre choix que de se retirer, Tranquillien fût donc abandonnée. La ville est maintenant utilisée par les Elfes de sang et les Réprouvés comme base d\'opération pour lancer des attaques visant à reprendre les Terres Fantômes. Cependant, la ville est entourée par le fléau, même les courriers ont du mal à traverser l\'ennemi pour atteindre la ville. Les forces mortels de Mortholme sont la menace la plus dangereuse pour la ville.\n\n[h3]Réputation[/h3]\n\nContrairement à la plupart des zones de départ, la ville de Tranquillien a sa propre faction.\nToutes les quêtes que vous effectuez pour eux accumuleront au moins 1000 points de réputation. [npc=16528] agit comme lintendant des Tranquilliens. Vredigar peut être trouvé près de l\'auberge et vendra divers éléments [span class=q2]commun[/span], et même un manteau [span class=q3]rare[/span] lorsque vous atteignez la réputation exaltée.\n\nSi vous complétez toutes les quêtes des Tranquilliens, vous devriez être exalté.\nIl existe une variété de quêtes concernant principalement la récupération des villages envahis, l\'enquête sur les morts-vivants et l\'aide apportée à la population. La suite de quête prend « fin » avec la quête où il faut tuer [npc=16329].'),(8,910,2,NULL,0,2,'La [b]Progéniture de Nozdormu[/b] est une faction composée du vol Draconique de bronze. Leur chef, [npc=15192], se trouve à l\'extérieur des [b]Grottes du temps[/b], avec beaucoup de ses agents volant dans le ciel de [zone=1377].\n\nPour ouvrir les portes d[b]Ahn\'Qiraj[/b], un champion doit compléter une longue ligne de quête pour le dragon de bronze Anachronos. Cette réputation est également présente dans [zone=3428]; Elle permet dobtenir des équipements et des bagues épiques.\n\n[h3]Réputation [/h3]\n\nLes joueurs commencent leur réputation au plus bas niveau possible, cestà-dire 0/36000 de détestés.\n\nLa réputation de la Progéniture de Nozdormu peut être gagnée en tuant des monstres à l\'intérieur du temple d\'Ahn\'Qiraj et en faisant des quêtes liées. Vous pouvez également exploiter [item=20384], cela prend beaucoup plus de temps et nécessite l\'obtention de [item=20383] dans [zone=2677] pour la suite de quête [item=21175].\n\nTuer des monstres dans le temple d\'Ahn\'Qiraj ne permet que datteindre une réputation de 2999/3000 de neutre, la réputation ne peut donc être avancée que par des quêtes et la remise de [item=21229] et [item=21230]. \nUn conseil, gardez tous les insignes jusqu\'à ce que vous soyez à une réputation neutre, car à ce moment-là, cela devient beaucoup plus difficile.'),(8,749,2,NULL,0,2,'Les [b]Hydraxiens[/b] sont des élémentaires qui se sont installés sur les îles à l\'est de [zone=16]. Les ennemis jurés des armées de [npc=11502]. Historiquement serviteurs des Anciens Dieux, les quatre Lords Élémentaires ont servi les dieux avec une loyauté éternelle. Les minions de Neptulon, le chasse-marée, étaient nombreux et insensés. On ne sait pas encore comment le [npc=13278] a libéré le contrôle de son seigneur ou quels sont ses objectifs ultimes, mais les élémentaires deau sont les seuls éléments qui n\'attaquent pas les races mortelles.\n\nSitué sur une île éloignée dans l\'extrême est d\'Azshara, le Duke Hydraxis propose des quêtes. Les deux premiers nécessitent de tuer divers élémentaires dans les [zone=139] et en [zone=1377]. Une réputation accrue avec les Hydraxiens ouvre des quêtes supplémentaires menant à [zone=2717]. Tous les objets obtenus auprès des Hydraxiens sont gagnés à partir de différentes missions.\n\nL\'achèvement de la suite de quête permet aux joueurs d\'obtenir [item=17333] utilisé pour endommager les runes trouvées près de la plupart des boss dans Cur de Magma. Ceci est nécessaire pour convoquer [npc=12018], l\'avant-dernier boss, et, après sa défaite, pour convoquer Ragnaros lui-même. Comme il y a sept runes, tout raid nécessite au moins sept joueurs qui apportent une quintessence s\'ils souhaitent terminer l\'instance. Comme la majeure partie de la suite de quête a lieu au sein de Cur de Magma, toutes personnes du raid peuvent compléter cette tâche avec un peu plus que quelques voyages et une course au [zone=1583].\n\n[h3] Réputation [/h3]\n\nLa réputation des Hydraxiens est obtenue en tuant les ennemis élémentaires suivants :\n[ul][li] [npc=11746] - 5 points de réputation, jusqu\'à l\'Honoré. [/li]\n[li] [npc=11744] - 5 points de réputation, jusqu\'à Honoré.[/li]\n[li] [npc=7032] - 5 points de réputation, jusqu\'à Honoré.[/li]\n[li] [npc=9017] - 15 points de réputation, jusqu\'à Révéré. [/li]\n[li] [npc=14478] - 25 points de réputation, jusqu\'à Révéré. [/li]\n[li] [npc=9816] - 50 points de réputation, jusqu\'à Révéré. [/li]\n[li] [npc=11658], [npc=11673], [npc=12101] et [npc=11668] - 20 points de réputation, jusqu\'à Révéré. [/li]\n[li] [npc=11659], [npc=12100], [npc=12076], [npc=11667] et [npc=11666] - 40 points de réputation, jusqu\'à Révéré. [/li]\n[li] [npc=12118], [npc=11982], [npc=12259], [npc=12057], [npc=12056], [npc=12264] et [npc=12098] - 100 points de réputation, jusqu\'à Exalté. [/li]\n[li] [npc=11988] - 150 points de réputation, jusqu\'à Exalté. [/li]\n[li] [npc=11502] - 200 points de réputation, jusqu\'à Exalté. [/li][/ul]\n\nLa réputation au statut de Révéré avec les Hydraxiens permet aux joueurs dobtenir le [item=22754], qui se recharge. Et donc évite la nécessité de retourner à Hydraxis pour obtenir une nouvelle quintessence chaque semaine.'),(8,609,2,NULL,0,2,'Le [b]Cercle Cénarien [/b] est une organisation de druides, à la fois tauren et elfe de nuit, nommé d\'après Cénarius. Ses membres se consacrent à la protection de la nature et à la restauration de celle-ci suite aux dégâts subis par des forces malveillantes.\n\nLe Cercle a de nombreux sites, mais leur ville principale est la ville de Havre- nuit dans la [zone=493]. Les druides apprennent le sort [sortilège=18960] au niveau 10, mais il est aussi possible dy arriver par [zone=361] via le tunnel des Grumegueles.\n\nLe cercle Cénarien est aussi beaucoup présent en [zone=1377], où ils combattent les Silithides, les Qirajis et larmée du crépuscule. Le repos du vaillant et le Fort Cénarien servent de base dans ces terres hostiles et offrent de nombreuses opportunités aux aventuriers qui cherchent à aider les druides.\n\n[h3]Membres notables[/h3]\n\n[ul][li][npc=11832], fils de Cenarius [/li]\n[li][npc=3516], chef des druides - elfes de la nuit [/li]\n[li][npc=5769], chef des druides - Taurens [/li][/ul]\n\n[h3]Réputation[/h3]\n\nIl existe plusieurs façons de se faire connaître avec le cercle Cénarien.\nMise à part les [url=?Quests&filter=cr=1;crs=609;crv=0]quêtes[/url], vous pouvez faire ce qui suit pour gagner en réputation: \n[ul]\n[li]Le raid des [zone=3429] est de loin le moyen le plus rapide de gagner en réputation, car un clean complet peut dépasser 2000 points de réputation. [/li]\n[li] Tuez larmée du crépuscule. Elle cesse daugmenter une fois que vous atteignez la réputation Honoré pour [npc=11880] et [npc=11881], et Révéré pour [npc=15201].[/li]\n[li] Trouvez des [item=20404 ]. Ceux-ci se trouvent sur larmée du crépuscule et produisent 250 points de réputation pour 10 textes.[/li]\n[li] Trouvez des [item=20513], [item=20514] et [item=20515]. Ceux-ci se trouvent sur les mini-boss qui sont convoqués aux pierres de vent en utilisant [itemset=492]. [/li]\n[li] Effectuez la quête : [quest=8507]. Ce sont soit des [url=?search=logistique+Briefing] Quêtes de logistique [/url], des [url=?search=combat+Briefing]quêtes de Combat[/url] ou des [url=?search=tactique+Briefing] Quêtes tactiques [/url]. Les badges que vous gagnez de ces quêtes peuvent être transformés en réputation supplémentaire, si vous choisissez d\'abandonner les récompenses. [/li]\n[li] Collectez les [object=181598] de la zone et rendez les à votre faction.[/li]\n[/ul]'),(8,589,2,NULL,0,2,'Les [b]Éleveurs de sabres-d\'hiver[/b] est une faction de l\'Alliance composée de deux Elfes de la nuit qui peuvent être trouvés au [zone=618]. À l\'heure actuelle, le seul donneur de quête est [npc=10618], qui est situé au sommet du Rocher des Sabres-d\'hiver au Berceau-de-lhiver. En atteignant un niveau de réputation exalté avec cette faction, Rivern vendra une monture spéciale, le [item=13086].\n\nLa monture de cette faction est la seule monture épique, ayant une vitesse de 100%, utilisable avec une compétence en équitation de 75. La faction est connue pour ne pas avoir déquivalant côté Horde et être la plus longue et la plus répétitive des réputations à monter dans l\'ensemble du jeu. La première quête peut être faite au niveau 58, tandis que les deux autres sont réalisables quau niveau 60.\n\n[h3]Réputation[/h3]\n\nLa réputation avec les Éleveurs de sabres-d\'hiver ne peut être obtenue que par trois quêtes répétables. Il n\'y a pas d\'objets de faction ni de mobs qui récompensent la réputation directement.\n\n[b]De neutre 0 à 1500[/b]\n\nUne seule quête répétable sera disponible jusqu\'à ce quune réputation de 1500/3000 soit atteinte, la quête : [quest=4970] doit donc être répétée. Tous les [url=?npcs&filter=cr=6;crs=618;crv=0;na=Croc%20acéré]Ours[/url] et [url=?npcs&filter=cr=6;crs=618;crv=0;na=Noroît]Noroît[/url] au Berceau-de-lhivers peuvent looter les objets de quête. Cette quête doit être effectuée en solo, car les taux de loot sont faibles et ne sont pas partageables si d\'autres ont la quête.\n\n[b]De neutre 1500 à exalté [/b]\n\nÀ mi-chemin du neutre, la quête : [quest=5201] sera disponible. Cette quête nécessite de tuer 10 Tombe-hivers dans le village Tombe-hivers, juste à l\'est de Long-guet. Si la quête : [quest=8464] a été effectuée pour [faction=576], les [item=21383] peuvent tomber sur les Tombe-hivers. Si un joueur veut les deux réputations, il préférable quil les gardes jusquà ce quil soit Révéré avec les Grumegueules. Ce qui entraînera beaucoup de réputation \"gratuite\".\n\nCette quête peut se faire en groupes pour aller plus vite. Les joueurs qui augmentent les réputations des Éleveurs de sabres-d\'hiver et des Grumegueules peuvent être trouvés dans le village des Tombe-hivers. Même en épique, le voyage vers le village Tombe-hivers prend beaucoup de temps. Il y a des tigres sur la route qui vous étourdiront, ce qui entraînera un désarçonnement, cela devrait être évité (mais peut être difficile car ils vont vous rattraper sur une monture de 60%). \n\n[b]De honoré à exalté[/b]\n\nA partir dhonoré, la troisième quête : [quest=5981] est disponible. La quête exige que le joueur tue 8 géants. Ils sont beaucoup plus difficiles que les Tombe-hivers et le trajet est assez long. Cette quête est généralement ignorée.\n\nEn raison de certains joueurs qui augmentent la réputation des Grumegueules, dans le village de Tombe-hivers, cette quête peut effectivement se révéler une récompense de réputation plus rapide que [quest=5201].'),(8,576,2,NULL,0,2,'[b]Les Grumegueules[/b], dernière tribu furbolg non-corrompue (au moins dans leur point de vue), cherchent à conserver leurs voies spirituelles et à mettre fin à la souffrance de leurs frères.\n\nLes Grumegueules habitent deux zones : [zone=16] et [zone=361]. Ils sont présumés être la seule tribu furbolg à échapper à la corruption démoniaque, mais ce n\'est peut-être pas vrai, en raison de l\'existence de [npc=3897], furbolg de tribu inconnue, et la tribu Stillpine sur [zone=3524]. Cependant, de nombreuses autres races tuent les furbolgs aveuglément maintenant, sans savoir si elles sont alliées ou non. Pour cette raison, les Grumegueles ne se montrent pratiquement pas.\n\nLes aventuriers qui recherchent les Grumegueules dans le nord de Gangrebois et s\'aventurent chez eux apprendront quil faut mieux être leurs alliés. Bien qu\'ils ne possèdent pas de bijoux fins ou de richesses mondaines, la tradition chamanique des Grumegueules est encore forte. Ils connaissent bien l\'art de fabriquer des armures à partir de peaux d\'animaux, et ils sont plus qu\'heureux de partager leurs connaissances de guérison avec des amis de leur tribu. En outre, à partir dune réputation inamical, les Grumegueules vous accorderont également un accès sans problème à [zone=493] et [zone=618] dans leurs tunnels.\n\n[h3] Réputation[/h3]\n\nLa réputation avec la faction des Grumegueules est principalement acquise grâce à des quêtes. Les membres de la tribu Mort-bois, une autre tribu de Furbolg à Gangrebois, sont les principaux ennemis des Grumegueules et peuvent être tué pour gagner de la réputation.\n\n[ul]\n[li] Tuer des furbolgs [url=?Npcs&filter=na=Tombe-hivers]Tombe-hivers[/url] ou [url=?Npcs&filter=na=Mort-bois]Mort-bois[/url], donne 10 points de réputation. Les gains s\'arrêtent à révéré. [/li]\n[li] Tuer [npc=9464] ou [npc=9462], donne 60 points de réputation.[/li]\n[li] Tuer [npc=10738], située dans une grotte à l\'est de [faction=577], donne 50 points de réputation. Son taux de réapparition est de 6 à 8 minutes. [/li]\n[li] Tuer [npc=14342], élite rare, donne 50 points de réputation. Il se situe au village des Mort-bois à Gangrebois. Donne de la réputation jusquà exalté. [/ Li]\n[li] Tuer [npc=10199], élite rare, donne 50 points de réputation. Il se situe dans le village des Tombe-hivers au Berceau-de-lHivers. Donne de la réputation jusquà exalté. [/li]\n[li] Après avoir terminé la quête : [quest=8460], avec les [item=21377] ramassés sur les Furbolgs Mort-bois, la réputation augmente de 150 points. [/li]\n[li] Après avoir terminé la quête : [quest=8464], avec les [item=21383] ramassés sur les furbolgs Tombe-hivers, la réputation augmente de 150 points.[/li]\n[/ul]'),(8,890,2,NULL,0,2,'[b]Les Sentinelles d\'Aile-argent[/b] représente la faction de l\'Alliance sur le champ de bataille [zone=3277]. Les elfes de la nuit, qui ont commencé une avancée massive pour reprendre les forêts de [zone=331], concentrent leur attention sur le débarquement sur leur terre de la [faction=889] une fois pour toutes. Et ainsi, les Sentinelles d\'Aile-argent ont répondu à l\'appel et ont juré qu\'ils ne vont pas se reposer avant que tous les orcs soient vaincus et expulsés du Goulet des Chanteguerres.\n\n[h3]Réputation[/h3]\n\nLes joueurs peuvent gagner leurs réputations, dans cette faction, en participant au champ de bataille du Goulet des Chanteguerres. Vous gagnez 35 points de réputation à chaque fois que votre faction capture un drapeau. Ce gain de réputation est augmenté à 45 les week-ends du champ de bataille.\n\nOn vous accorde le titre : [title=47] une fois quil est exalté avec Les Sentinelles d\'Aile-argent et les deux autres factions des champs de bataille, [faction=730] et [faction=509].'),(8,889,2,NULL,0,2,'[b]Les Voltigeurs Chanteguerre[/b] est un clan orc précédemment dirigé par [npc=18076], daprès lequel le clan a été nommé. Les Voltigeurs Chanteguerre représentent la faction de la Horde sur le champ de bataille [zone=3277], où ils tentent de défendre leurs opérations d\'enregistrement dans [zone=331] de la [faction=890].\n\nCest l\'un des clans les plus forts et les plus violents, le clan de Chanteguerre était également l\'un des clans les plus distingués de Draenor, ce clan a pu échapper aux forces de l\'expédition de l\'Alliance à chaque tournant. Formés comme Grunts, ils ont maîtrisé l\'utilisation d\'épées et de lames et quelques-uns ont même atteint le rang de Maître-lames.\n\n[h3]Réputation[/h3]\n\nLes joueurs peuvent gagner leurs réputations, dans cette faction, en participant au champ de bataille du Goulet des Chanteguerres. Vous gagnez 35 points de réputation à chaque fois que votre faction capture un drapeau. Ce gain de réputation est augmenté à 45 les week-ends du champ de bataille.\n\nOn vous accorde le titre : [title=47] une fois quil est exalté avec Les Voltigeurs Chanteguerre et les deux autres factions des champs de bataille, [faction=510] et [faction=729].'),(8,729,2,NULL,0,2,'[b]Le Clan Loup-de-givre[/b], ainsi que [npc=11946], ont vécu dans [zone=36] et ont des Loups de givre comme compagnons. Des nains, connue sous le nom de [faction=730], ont commencé une expédition dans le territoire des Loup-de-givre pour creuser la vallée et miner les veines. Une transgression envers les Orcs qui habitaient en Alterac. Cela a provoqué lextermination de la première expédition et la bataille pour [zone=2597] a commencé.\n\n[h3]Réputation[/h3]\n\nLes joueurs peuvent gagner leurs réputation, dans cette faction, en participant au champ de bataille de la vallée dAlterac, en effectuant diverses tâches et en tuant les membres de la faction opposée, les Gardes Foudrepiques.\n\nOn vous accorde le titre : [title=47] au joueur une fois quil est exalté avec le clan Loup-de-givre et les deux autres factions des champs de bataille, [faction=889] et [faction=510].'),(8,935,2,NULL,0,2,'[b]Les Sha\'tar[/b], ou \"né de la lumière\", sont des naaru qui ont aidé [faction=932], l\'ordre des prêtres draenei précédemment dirigés par [npc=17468], en reconstruction à [zone=3703]. La ville a été détruite par les Orcs pendant leur fuite à travers Draenor avant la Première Guerre mondiale. \nLa défaite de la Légion ardente est le but ultime des Sha\'tar. Les Sha\'tar sont aidés dans cette guerre par l\'Aldor et leurs rivaux, la faction des elfes du sang connue sous le nom : [faction=934]. \nL\'Aldor et les Clairvoyants se battent pour la faveur du Sha\'tar afin qu\'ils puissent être aidés dans leur guerre pour les pouvoirs des naaru. L\'entité qui dirige le Sha\'tar est connue sous le nom de [npc=18481] ; Il peut être trouvé sur la terrasse de la lumière dans la ville de Shattrath.\n\nLes joueurs de l\'Alliance et de la Horde commencent avec une réputation neutre auprès des Sha\'tar. Les joueurs peuvent augmenter leur réputation, Sha\'tar, à travers diverses quêtes, en élevant leur réputation avec lAldor ou les clairvoyants, ou en s\'aventurant dans le [url=?search=donjon+tempête]donjon des tempêtes [/url].\n\n[h3]Réputation[/h3]\n\n[b]Jusqu\'à honoré[/b]\n\nLa réputation peut être obtenue à partir de divers objets. Ce qui suit n\'accordera que de la réputation de Sha\'tar jusqu\'à ce que vous obteniez un statut honoré : \n[li]Pour une réputation envers les Clairvoyants : [item=29426], [item=30810] et [item=29739][/li]\n[li]Pour une réputation envers l\'Aldor : [item=29425], [item=30809] et [item=29740][/li]\n\n[i]Notez que ce gain de réputation ne s\'affiche pas dans le journal de combat, mais peut être vérifié en regardant votre panneau de réputation.[/i]\n\nLa réputation peut également être obtenue en faisant le temple des tempêtes : [zone=3847], [zone=3846] et [zone=3849].\n\n[b]Jusquà exalté [/b]\n\nAprès avoir épuisé les récompenses de réputation de Aldor ou des Clairvoyants, les joueurs souhaiteront peut-être compléter les quelques quêtes de Sha\'tar disponibles. En plus des quêtes, les instances qui se trouvent au temple des tempêtes : Botanica, Arcatraz et Mechanar continueront à accorder de la réputation. À ce stade, il est probablement plus utile d\'exécuter ces instances en mode héroïque.'),(8,934,2,NULL,0,2,'[b]Les Clairvoyants[/b] sont des elfes de sang qui résident dans [zone=3703] dirigé par [npc=18530]. Le groupe s\'est éloigné de [npc=19622] et a offert de leur aide au Naaru de Shattrath. Ils sont en désaccord avec [faction=932], et rivalisent avec eux pour le pouvoir de Shattrath et la faveur du Naaru. \n\nLa plupart des joueurs commenceront avec une réputation neutre auprès des Clairvoyants. [npc=18166] à Shattrath donnera aux joueurs une première quête pour devenir amical avec lAldor ou Les Clairvoyants. Ce choix est réversible si les joueurs ressentent le besoin. \nLes joueurs delfes de sang seront amicaux avec les Clairvoyants et hostiles avec l\'Aldor, alors que les joueurs draenei seront hostiles aux Clairvoyants et amicaux envers lAldor.\n\n[npc=19331] et [npc=20808] sont situés dans la banque des Clairvoyants, sur le bord sud de la terrasse de lumière. La Bibliothèque du Visiteur abrite [npc=20613] [small][/small] et [npc=21905] [small][/small], qui échangent des pièces d\'armure épique contre des pièces de set de[url=?Itemsets&filter=ta=12]Niveau 4[/url] et de [url=?Itemsets&filter=ta=13]Niveau 5[/url].\n\n[i]Note : Les gains de réputation avec les Clairvoyants correspondent à une perte de réputation de 10% plus élevée chez lAldor. La plupart des gains de réputation avec les Clairvoyants accorderont également 50% de la réputation avec [faction=935].[/i]\n\n[h3]Tradition [/h3]\n\nAprès avoir subi des assauts implacables de leurs ennemis, les gardes harassés de Sha\'tar et de lAldor se sont regroupés pour la prochaine attaque alors qu\'elle marchait sur l\'horizon. Cette fois, l\'attaque provenait des armées de [npc=22917]. Un grand régiment d\'elfes de sang avait été envoyé par l\'allié d\'Illidan, le prince Kael\'thas pour détruit la ville. Alors que le régiment d\'elfes de sang traversait le pont, les exarques et les vindicateurs de lAldor se sont alignés pour défendre la Terrasse de Lumière. Alors l\'inattendu arriva, les elfes de sang déposèrent leurs armes devant les défenseurs de la ville.\nLeur chef, un ainé de sang connu sous le nom de Voren\'thal, a exigé de parler au naaru [npc=18481]. À mesure que le naaru s\'approchait de lui, Voren\'thal s\'agenouilla et prononça les mots suivants : « Je vous ai vu dans une vision, naaru. Le seul espoir de survie de ma race est avec vous. Mes disciples et moi-même sommes là pour vous servir ».\nLa défection de Voren\'thal et de ses partisans a été la plus grande perte jamais subie par les forces de Kael\'thas. Beaucoup des plus forts et les plus brillants parmi les savants et les magistrats de Kael\'thas ont été influencés par l\'influence de Voren\'thal. Le naaru a accepté les déflecteurs qui sont devenus connus sous le nom de Clairvoyant.\n\n[h3]Réputation[/h3]\n\n[b]Jusqu\'à honoré[/b]\n\nLes joueurs qui cherchent à gagner les rangs de réputation supérieurs (Révéré, Exalté) peuvent vouloir sauver des quêtes non répétables jusqu\'à ce qu\'ils soient honorés.\n\nDonner 10 [span class=q1][item=29426][/span] à [npc=18531] dans la bibliothèque du Visiteur des Clairvoyants accordera une réputation de 250 points de réputation pour les Clairvoyants. Il existe également une quête répétable où donner une unique chevalière accorde 25 points de réputation. Ces chevalières tombent sur des membres Aile-de feu dans la partie nord-est de la forêt de Terrokar. \nEnviron 240 marques sont nécessaires pour passer d\'amical à honoré.\nEn outre, ces quêtes fournissent de la réputation de Sha\'tar ; 125 points de réputation pour 10 marques ou 12,5 points de réputation pour une unique chevalière.\n\n[b]Jusqu\'à exalté [/b]\n\nUne fois que vous atteignez le niveau 68, vous pouvez également donner 10 [span class=q1][item=30810][/span], cest le même principe que les chevalières mais ceux-ci tombent sur des elfes de sang Solfurie de haut rang. Si vous le souhaitez, vous pouvez transformer les chevalières de niveau supérieur avant une réputation honorée. Vous les trouverez dans [zone=3523], [zone=3520] et les instances du [url=?Search=tempête+donjon]donjon de la tempêtes[/url].\n\n[b]Tome des Arcanes[/b]\n\n[span class=q2][item=29739][/span] peut être donné à tout moment à [npc=18530] à l\'intérieur la Bibliothèque du Visiteur. Cela augmentera votre réputation avec les Clairvoyants de 350 par Tome des Arcane.\nEn plus des gains de réputation, vous recevrez une [span class=q1][item=29736][/span], qui est la condition pour acheter l\'enchantements d\'épaule à [npc=20808], qui réside dans la banque des Claivoyants.\n\n[h3]Passer à la réputation des Claivoyants[/h3]\n\nPour changer votre faction d\'Aldor vers Claivoyants et donc accéder à leurs recettes d\'artisanat (et annuler toutes les avancées de réputation que vous avez faites), trouvez [npc=18596], membre des Claivroyants dans la ville basse. Elle vous propose une quête répétable, [quest=10024], où pour huit [span class=q1][item=25744][/span] vous montez la réputation Claivoyant. Une fois que vous êtes neutre, vous ne pourrez plus recevoir cette quête.'),(8,942,2,NULL,0,2,'L[b]Expédition Cénarienne[/b] a été envoyé par [faction=609], lors de la réouverture de la porte des ténèbres vers l\'Outreterre, pour explorer ce monde inconnu. Tout comme le cercle, il s\'agit d\'une coalition de forces entre les Elfes de la nuit et les Taurens. Depuis l\'ouverture de la porte, l\'expédition Cénarienne a rapidement gagné en taille et en autonomie, obtenant suffisamment de puissance pour être considérée comme une propre et unique faction. L\'expédition maintient sa base principale au refuge Cénarien dans [zone=3521], située immédiatement à louest de la péninsule des flammes infernales. Elle est aussi présente sur [zone=3483], dans [zone=3519], et dans [zone=3522]. \n\nLe Refuge est situé dans le marécage de Zangar afin détudier la faune riche située là-bas. Cependant, l\'expédition a révélé des retombées inquiétantes dans le marais. Les niveaux d\'eau dans de nombreuses régions du marécage diminuent, et certaines régions comme Morte-bourbe ont déjà beaucoup souffert de ce phénomène étrange. On sait que cette diminution des niveaux d\'eau peut être attribuée aux pompes qui ont été construites dans le marécage par les naga. Leur but est de créer un nouveau puits d\'éternité pour [npc=22917].\nCependant, l\'expédition ne peut pas se permettre une confrontation directe avec le naga si nombreux dans le marécage de Zangar et le [url=?Search=Glissecroc#c0z]Réservoir de Glissecroc [/url]. Elle a besoin de l\'aide daventurier qui veulent soutenir les druides dans leur dangereuse bataille contre les Nagas qui cherchent à perturber l\'équilibre naturel du marais. Naturellement, ceux assez héroïques pour combattre au réservoir de Glissecroc seront bien récompensés.\n\n[h3]Réputation[/h3]\n\n[b]De neutre à honoré[/b]\n\nTuez des Nagas chaque fois que vous le pouvez. Le mieux sera de parcourir les instances, la réputation monte plus rapidement.\nAlternativement, le joueur peut commencer à trouver des [item=24401] pour avoir une chance davoir des [item=24407], qui peuvent être transformé en 500 points de réputation. Il est suggéré que le joueur garde ses espèces non cataloguées jusqu\'à ce que son statut honoré soit atteint, car la quête ne peut pas être poursuivie après ce point, alors que les espèces non cataloguées peuvent être utilisées jusqu\'à Exalté.\n\nSi vous êtes un herboriste et que vous êtes intéressé par la réputation [faction=970], vous voudrez peut-être trouver les [url=?Npcs&filter=na=Seigneur+tourbe]Seigneurs-tourbes[/url] qui se trouve dans lEst, et le coin Sud-ouest du Marécage de Zangar. Leurs corps peuvent être «récoltés» par les herboristes et produisent souvent des végétaux non identifiées, alors que chaque monstre tué donne 15 points de réputation chez Sporeggar. \n\n[b]De honoré à révéré[/b]\n\nUne fois que le joueur est honoré, faire lenclos aux esclaves et [zone=3716] (à l\'exception de [npc=17770] et de certains géants), n\'accorderont plus de réputation. Vous devriez maintenant faire des quêtes de l\'Expédition Cénarienne dans la péninsule des flammes infernal, le marécage de Zangar, la forêt de Terokkar et les Tranchantes. Il est également temps de transformer toutes les espèces non cataloguées que vous avez trouvées. Faire cela devrait vous faire passer révérer.\n\nAlternativement, vous pouvez, en étant niveau 70, faire [zone=3715]. Chaque donjon donne un peu plus de 1500 points de réputation si vous tuez toutes les mobs.\nDans le Caveau de la vapeur, se trouve, aussi, une quête répétable, [quest=9764], qui commence par [item=24367]. Vous pourrez ensuite donner les [item=24368], qui tombe à la fois dans le caveau de la vapeur et lenclos aux esclaves, recevant 250 points de réputation pour les premières armes et 75 points de réputation par la suite. Cette quête est disponible jusqu\'à exalté.\n\nUne fois que vous avez le niveau 70 et que vous avez amélioré votre équipement, vous pouvez choisir d\'entrer dans lenclos des esclaves, le caveau de la vapeur et basse-tourbière en mode héroïque avec l\'achat de la [item=30623]. Ils accordent une réputation importante : les mobs ordinaires valent 15 points de réputation, 2 pour les non élites et 150 à 250 pour les boss. Cette méthode fonctionne jusqu\'à exalté.\n\n[b]De révéré à exalté [/b]\n\nContinuez avec la même stratégie que ci-dessus : terminez toutes les requêtes restantes, faites caveau de la vapeur et continuez avec la quête des [item=24368].\n\nIl est également possible de faire lenclos des esclaves, Basse-tourbière et caveau de la vapeur en mode héroïque. La réputation acquise n\'est pas beaucoup plus intéressante que le caveau de la vapeur en mode normal, alors que l\'investissement dans le temps pour les donjons héroïques est beaucoup plus élevé, le butin est mieux et vous recevrez [item=29434] sur les boss qui peuvent être utilisés pour acheter des équipements épiques de haute qualité.'),(8,941,2,NULL,0,2,'Les [b]Mag\'har[/b] sont la faction d\'orcs à peau brune qui sont restées en Outreterre et se sont séparés des autres clans orcs restants qui ont été victimes de [npc=17257] et qui sont maintenant dirigés par le puissant [npc=16808]. Les Mag\'har sont présent dans la forteresse de Garadar dans le magnifique pays de [zone=3518], une fois bien installés, la majorité des orcs sont retournés dans [zone=3519] et [zone=3522].\n\nLes Maghar n\'ont jamais été corrompus par Mannoroth ou Magtheridon. Contrairement à dautres anciens clans qui vivent dans les ruines de leurs ancêtres, les Mag\'har sont composés de membres de différents clans d\'orc qui ont échappé à la corruption. Le chef actuel des Mag\'har, la vénérable [npc=18141], est une orc ancienne et sage, mais elle est tombée récemment extrêmement malade. [npc=18063], fils du puissant Grom hurlenfer, sert de chef militaire aux Mag\'har, aidé par [npc=18106], fils du vénérable chef du clan Orbite-Sanglante, Kilrogg Deadeye. En outre, il existe un orc dans un camp de Mag\'har à l\'ouest connu sous le nom [npc=18229].\n\nIl n\'est pas clair comment le Mag\'har a réussi à conserver sa peau marron d\'origine. La peau orque devient verte lorsqu\'elle est exposée à la magie du sorcier, indépendamment des croyances ou des pratiques de l\'individu ; Garrosh et Jorin auraient certainement été exposés, compte tenu de la position hiérarchique de leurs pères.\n\nLes joueurs de la Horde commencent inamical avec le Mag\'har. Les joueurs de l\'Alliance seront toujours traités comme hostiles. La contrepartie de l\'Alliance à cette faction est la faction des : [faction=978].\n\n[h3]Quête[/h3]\n\nLes quêtes pour les Mag\'har commencent dans [zone=3483] avec [quest=9400] de [faction=947]. Cette quête vous mènera à un petit avant-poste Mag\'har au nord de la Citadelle des flammes infernales. Une fois à Nagrand, les joueurs trouveront la principale ville de Mag\'har, Garadar. La ville détient la plupart des quêtes restantes qui récompenseront la réputation de Mag\'har.\n\n[i]Note : Vous DEVEZ compléter la suite de quête de \"lassassin\" jusqu\'à la quête [quest=9410] (où vous devenez neutre) afin que vous puissiez parler à la plupart des gens de Garadar.[/i]\n\n[h3]Réputation[/h3]\n\nLa réputation peut être obtenue en tuant des [url=?npcs&filter=na=kil%27sorrau;ra=-1;rh=-1]Membres de culte Kil\'sorrau[/url], des [url=?Npcs&filter=na=Bourbesang;ra=-1;rh=-1;cr=6;crs=3518;crv=0]Bourbesang[/url], des [url=?Npcs&filter=na=cogneguerre+-marker]Cogneguerre[/url] et des [url=?Npcs&filter=Na=rochepoing;minle= 64;ra=-1;rh=1]Rochepoing[/url] à Nagrand. Les joueurs peuvent également transformer 10x[item=25433], qui tombent de ces ogres.\n\nLes joueurs qui recherchent la réputation : [faction=933] peuvent vouloir garder leurs perles, car la réputation Mag\'har est généralement plus facile à obtenir. \nLes joueurs qui recherchent la réputation :[faction=932] peuvent préférer tuer les membres du culte à la forteresse de Kil\'Sorrau, car ils donnent aussi des [item=29425] pour la réputation Aldor.\n\n[i]Remarque : Ces monstres et quêtes n\'ont pas de limite, ils accordent une réputation jusquà exalté![/i]'),(8,946,2,NULL,0,2,'Le [b]Bastion de lHonneur[/b], refuge des explorateurs humains, élu, draenei et nains, est la première grande ville que les explorateurs de l\'Alliance rencontreront en traversant la porte des ténèbres. Les vestiges des fils de Lothar, anciens combattants de l\'Alliance qui sont venus à Draenor, se sont tenus fermement dans cet avant-poste des flammes infernales. Ils sont maintenant rejoints par les armées de Hurlevent et Forgefer.\n\n[h3]Réputation[/h3]\n\nLa réputation du Bastion de l\'Honneur est gagnée par divers moyens dans la péninsule des flammes infernales. Les PNJs, dans et autour, de la citadelle donnent en récompensés de quêtes de l\'honneur et de la réputation. En raison du manque de représentants dans d\'autres endroits dOutreterre il y a un grand écart entre Honoré et Exalté, au cours duquel il est possible que vous ne puissiez pas obtenir assez de réputation au bastion de lhonneur une fois que vous partez de la péninsule.\n\n[b]Jusquà Honoré[/b]\n\nTuer des Pnjs dans [zone=3562] et [zone=3713] attribueront de la réputation. Une option est de faire les donjons jusqu\'à ce que la réputation arrive à honoré avant de faire des quêtes du Bastion de l\'honneur, car les quêtes continuent à donner de la réputation jusqu\'à Exalté.\n\nVous voudrez peut-être tuer les orcs à lextérieur du bastion qui donnent une réputation si vous êtes Neutre. La réputation donnée sarrête une fois que vous êtes amicales.\n[ul]\n[li][npc=19415][/li]\n[li][npc=16878][/li]\n[li][npc=16870][/li]\n[li][npc=16867][/li]\n[li][npc=19414][/li]\n[li][npc=19413][/li]\n[li][npc=19411][/li]\n[li][npc=19422][/li]\n[/ul]\n\n[b]PvP[/b]\n\nLes joueurs qui apprécient le PvP peuvent gagner de l\'honneur et de la réputation avec la quête [quest=10106]. Cette quête accorde 70 points d\'honneur et 150 points de réputation au Bastion de lHonneur, mais ne peut être complétée qu\'une fois par jour et compte pour votre limite de 25 quêtes journalières. L\'achèvement de cette quête fournit également trois [span class=q1][item=24579][/span], qui sont utilisés comme monnaie pour divers types d\'articles lorsqu\'ils sont échangés chez [npc=17657] et [npc=18266] au Bastion de lHonneur ainsi que [npc=18581] aux marécages de Zangar.\n\n[b]Jusquà Exalté[/b]\n\nÀ partir de là, il n\'y a que deux façons d\'atteindre Révéré et Exalté :\n[ul]\n[li][zone=3714], cette instance nécessite le niveau 68 et [span class=q1][item=28395][/span] (Un seul membre du groupe a besoin de la clé). Linstance des salles brisées abrite des PNJs qui donnent de la réputation jusquà Exalté.[/li]\n[li]Après avoir obtenu le statut dhonoré, vous pouvez acheter [span class=q1][item=30622][/span] qui accorde l\'accès au mode héroïque des instances de la citadelle des flammes infernales. Faire les donjons en mode Héroique donneront plus de réputation que les salles brisées en mode normale et continueront à donner de la réputation jusquà Exalté.[/li]\n[/ul]\n\n[i]Astuce : Vous pouvez utiliser ces marques pour acheter [span class=q1][item=24520][/span] à l\'adjudant Tracy Proudwell et augmenter le montant gagné de réputation (et dexpérience) acquise lors de l\'exécution de ces instances.[/i]'),(8,967,2,NULL,0,2,'[b]L\'Oeil Pourpre[/b] est une secte secrète fondée par le Kirin Tor de Dalaran pour espionner le gardien de Tirisfal, [npc=15608], dans la tour de [zone=2562]. Bien que Medivh soit mort, l\'il pourpre reste dans Karazhan, défendant le mal qui semble lenvahir en l\'absence de son maître.\n\nOn ignore si l\'apprenti de Medivh, [npc=18166], était membre de lOeil Pourpre, ou s\'il connaissait leurs activités à l\'époque.\n\n[h3]Réputation[/h3]\n\nLa réputation de lil pourpre est obtenue en tuant des mobs à l\'intérieur de Karazhan et en complétant les quêtes liées à Karazhan. La réputation grâce aux mobs de Karazhan peut être acquise à partir d\'une position neutre jusquà une réputation exalté. Chaque mob apporte une réputation d\'environ 15 points, les boss accordent davantage de réputation.\n\n[npc=18253] propose une chaîne de quête assez longue commençant par [quest=9824] et [quest=9825]. Cette suite de quête se termine par [quest=9644] et récompense les joueurs avec [span class=q1][item=24490][/span]. L\'achèvement complet de cette suite de quête récompense le joueur avec 10 270 point de réputation d\'environ.\n\n[h3]Récompenses de la réputation[/h3]\n\n[npc=18253] offrira aux joueurs des bagues en récompenses pour chaque niveau de réputation sous forme de quêtes. La première de ces quêtes est disponible dès la réputation neutre. Vous recevrez une version nouvelle et améliorée de la bague que vous avez choisi chaque fois que vous entrez dans un nouveau niveau de réputation. Les anneaux sont triés dans les 4 catégories suivantes :\n[ul]\n[li][quest=10731] : [item=29280], [item=29281], [item=29282] et [item=29283][/li]\n[li][quest=10729] : [item=29284], [item=29285], [item=29286] et [item=29287][/li]\n[li][quest=10732] : [item=29276], [item=29277], [item=29278] et [item=29279][/li]\n[li][quest=10730] : [item=29288], [item=29289], [item=29291] et [item=29290][/li]\n[/ul]\n\n[npc=16388], un forgeron situé à l\'intérieur de Karazhan juste après [npc=15550], offre aux joueurs ayant une réputation assez élevée la possibilité d\'acheter des plans de forge épique. Les joueurs honorés ou au-dessus pourront également réparer des armures et des armes chez ce fournisseur.\n\n[npc=18255], qui se trouve juste à l\'extérieur des portes principales de Karazhan, vendra une recette de joaillerie épique et un enchantement d\'épaule aux joueurs qui ont une haute réputation avec lOeil Pourpre.'),(8,970,2,NULL,0,2,'Les[b]Sporeggar[/b] sont une race de champignons essentiellement pacifique originaire d\'Outreterre. Ils vivent dans une ville située dans les tourbières occidentales de [zone=3521].\n\n[h3]Réputation [/h3]\n\nLes joueurs de l\'Alliance et de la Horde commencent amicalement avec Sporeggar. Il existe de nombreuses façons d\'augmenter votre réputation au début : \n[ul]\n[li]Apporter 10 [span class=q1][item=24290][/ span] à [npc=17923] pour compléter [quest=9739][/li]\n[li]Apporter 6 [span class=q1][item=24291][/span] à Fahssn pour compléter [quest=9743][/li]\n[i]Ces deux quêtes ne seront disponibles que si vous avez une réputation au minimum amical[/i]\n[li]Tuer [url=?Search=seigneurs +tourbes+-hungry #z0z]Seigneurs tourbes[/url] [i](jusqu\'à honoré)[/i][/li]\n[li]Tuer [npc=18137] et [npc=18136] [i](jusqu\'à révéré)[/i][/li]\n[li]Apporter 10 [span class=q1][item=24245][/span] à [npc=17924] dans Sporeggar[i] (jusquà amical)[/i][/li]\n[/ul]\n\nAprès avoir une réputation [b]amicale[/b], de nouvelles quêtes répétitives s\'ouvrent en même temps que les quêtes de Fahssn, notamment :\n[ul]\n[li]Tuer 12 [npc=18088] et [npc=18089] pour [npc=17856] pour compléter [quest=9726][/li]\n[li]Apporter 10 [span class=q1][item=24449][/span] à [npc=17925] pour compléter [quest=9806][/li]\n[li] S\'aventurer dans [zone=3716] pour rassembler 5 [span class=q1][item=24246][/span] pour terminer [quest=9715][/li]\n[/ul]\nCes 3 quêtes sont répétables et seront disponibles jusquà la réputation exalté.\nLes joueurs qui sont exaltés avec Sporeggar devraient parler à [npc=17877] pour une dernière quête.'),(8,978,2,NULL,0,2,'Les Kurenaï, pour « racheté », ont échappé à lesclavage en Outreterre et ont fait leur maison à Telaar dans le sud de [zone=3518]. C\'est là qu\'ils cherchent à redécouvrir leur destinée. Ils conservent également une petite présence en [zone=3521]. Leur intendant, [npc=20240], est situé à l\'extérieur de l\'auberge à Telaar, en dessous du point de vol.\n\nLes joueurs de l\'Alliance commencent à faire preuve d\'hostilité avec les Kurenai. Les joueurs de la Horde seront toujours traités comme hostiles. La contrepartie de la Horde à cette faction est [faction=941].\n\n[i]Kurenai est le japonais pour « cramoisi ».[/i]\n\n[h3]Réputation[/h3]\n\nLa réputation peut être obtenue en tuant des [url=?Npcs&filter=na=kil%27sorrau;ra=-1;rh=-1]Membres de culte Kil\'sorrau[/url], des [url=?Npcs&filter=na=Bourbesang;ra=-1;rh=-1;cr=6;crs=3518;crv=0]Bourbesang[/url], des [url=?Npcs&filter=na=cogneguerre+-marker]Cogneguerre[/url] et des [url=?Npcs&filter=Na=rochepoing;minle= 64;ra=-1;rh=1]Rochepoing[/url] à Nagrand. Les joueurs peuvent également transformer 10x [item=25433], qui tombent de ces ogres.\n\nLes joueurs qui cherchent la réputation de la faction [faction=933] peuvent vouloir garder leurs perles, car la réputation de Kurenai est généralement plus facile à obtenir.\n\nLes joueurs qui cherchent la réputation de la faction [faction=932] peuvent préférer tuer les membres du culte à la forteresse de Kil\'Sorrau, alors qu\'ils donnent des [item=29425] pour la réputation de lAldor.\n\n[i]Remarque : Ces monstres et quêtes n\'ont pas de limite, ils accordent de la réputation jusquà exalté.[/i]'),(8,989,2,NULL,0,2,'Les [b]Gardiens du Temps[/b] sont des dragons de bronze sélectionnés par Nozdormu pour surveiller les grottes du temps. Ils sont dirigés par [npc=19932] et [npc=19933], qui remplacent également Nozdormu en son absence.\n\n[h3]Réputation[/h3]\n\nActuellement, la seule façon d\'obtenir la faveur des dragons de bronze est de faire les instances : [zone=2367] et [zone=2366]. Lintendant des Gardiens du Temps, [npc=21643], se situe au quartier-intendant dans les grottes du temps. Les Gardiens vous demanderont d\'être au minimum niveau 66 et de compléter la courte quête [quest=10277] avant d\'autoriser le passage dans Les contreforts dHautebande dantan pour accomplir la destinée du Chef de la Horde, [npc=17876].'),(8,990,2,NULL,0,2,'La [b]Balance des sables[/b] est un sous-groupe secret du vol des Dragons de bronze, dirigé par [npc=19935], premier partenaire de [npc=15185]. Leur chef, Nozdormu, a envoyé ces factions gardiennes à [zone=3606] où ils gardent l\'Arbre Monde d\'une autre attaque par les démons, contribuent à restaurer le temps et à préserver l\'avenir du monde.\n\n[h3]Réputation[/h3]\n\nTuer les boss et monstres du Fléau font monter la réputation. [npc=17968], le boss final, récompense de 1 500 points de réputation tandis que les quatre autres boss donnent 375 points de réputations. La réputation général des montres du Fléau donnent 12 points de réputation, tandis que [npc=17907] donnent 60 points de réputation. En produisant une moyenne de 7 800 points de réputations par raid, 6 raids sont nécessaires pour atteindre la réputation exaltée.\n\nActuellement, la réputation permet davoir lune des meilleurs [span class=q4][url=?Items=4.-2&filter=na=bague+éternel]Bagues[/url][/span] pour les raids. Afin de recevoir ces anneaux, vous devez compléter la quête précédemment requise, [quest=10445]. Chaque nouveau niveau de réputation accorde une bague améliorée.'),(8,1012,2,NULL,0,2,'Les [b]Ligemorts Cendrelangues[/b] sont l\'élite de la tribu Kurenaï connue sous le nom de Cendrelangue. La tribu Cendrelangue est dirigée par la sage aînée [npc=21700]. Les Ligemorts sont [i]officiellement[/i] alignés avec [npc=22917] [small][/small]. Les Ligemorts sont les lieutenants les plus dignes d\'Akama et sont au courant des motivations mystérieuses de leur chef.\n\nPour découvrir les Ligemorts Centrelangues en tant que faction, le joueur doit commencer et compléter la majorité de la suite de quête qui commence par [quest=10568] ou [quest=10683]. Finalement, vous parlerez avec Akama, après quoi vous deviendrez neutre avec les Ligemorts Cendrelangues.'),(8,947,2,NULL,0,2,'[b]Thrallmar[/b], expédition envoyée par le Portail des Ténèbres par Thrall, a construit un bastion dans la péninsule des flammes infernales qui sert de base d\'opérations pour une grande partie des activités de la Horde en Outreterre.\n\n[h3]Réputation[/h3]\n\nLa réputation de Thrallmar jusqu\'à l\'honorée est relativement facile à gagner. Même les quêtes les plus faciles (celles qui vous emmènent d\'un fournisseur de quête à la prochaine, par exemple) peuvent produire 75 points de réputation, alors que ceux qui nécessitent plus defforts pour compléter ont généralement 250 points de réputation ou plus. Certaines quêtes de groupe impliquant de tuer un élite peuvent donner jusqu\'à 1 000 points de réputation.\n\nSi vous faites la majeure partie des quêtes de Thrallmar au lieu de passer rapidement à la prochaine zone, vous pourriez vous attendre à être honoré après 1 ou 2 niveaux de jeu. En raison du manque de représentants dans d\'autres endroits dOutreterre il y a un grand écart entre Honoré et Exalté, au cours duquel il est possible que vous ne puissiez pas obtenir assez de réputation à Thrallmar une fois que vous partez de la péninsule. Cest seulement au niveau 68 que vous pouvez commencer à regagner des points dans le donjon [zone=3714].\n\n[b]Jusquà Honoré[/b]\n\nTuer des Pnjs dans [zone=3562] et [zone=3713] attribueront de la réputation. Une option est de faire les donjons jusqu\'à ce que la réputation arrive à honoré avant de faire des quêtes de Thrallmar, car les quêtes continuent à donner de la réputation jusqu\'à Exalté.\n\nVous voudrez peut-être tuer les orcs à lextérieur du bastion qui donnent une réputation si vous êtes Neutre. La réputation donnée sarrête une fois que vous êtes amicales.\n[ul]\n[li][npc=19415][/li]\n[li][npc=16878][/li]\n[li][npc=16870][/li]\n[li][npc=16867][/li]\n[li][npc=19414][/li]\n[li][npc=19413][/li]\n[li][npc=19411][/li]\n[li][npc=19422][/li]\n[/ul]\n\n[b]PvP[/b]\n\nLes joueurs qui apprécient le PvP peuvent gagner de l\'honneur et de la réputation avec la quête [quest=10110]. Cette quête accorde 70 points d\'honneur et 150 points de réputation à Thrallmar, mais ne peut être complétée qu\'une fois par jour et compte pour votre limite de 25 quêtes journalières. L\'achèvement de cette quête fournit également trois [span class=q1][item=24581][/span], qui sont utilisés comme monnaie pour divers types d\'articles lorsqu\'ils sont échangés chez [npc=18267] et [npc=18564] à Thrallmar et près de Zabrajin dans [zone=3521].\n\n[b]Jusquà Exalté[/b]\n\nÀ partir de là, il n\'y a que deux façons d\'atteindre Révéré et Exalté :\n[ul]\n[li][zone=3714], cette instance nécessite le niveau 68 et [span class=q1][item=28395][/span] (Un seul membre du groupe a besoin de la clé). Linstance des salles brisées abrite des PNJs qui donnent de la réputation jusquà Exalté.[/li]\n[li]Après avoir obtenu le statut dhonoré, vous pouvez acheter [span class=q1][item=30637][/span] qui accorde l\'accès au mode héroïque des instances de la citadelle des flammes infernales. Faire les donjons en mode Héroique donneront plus de réputation que les salles brisées en mode normale et continueront à donner de la réputation jusquà Exalté.[/li]\n[/ul]\n\n[i]Astuce : Vous pouvez utiliser ces marques pour acheter [span class=q1][item=24522][/span] au Crieur-de-guerre Coquard et augmenter le montant gagné de réputation (et dexpérience) acquise lors de l\'exécution de ces instances.[/i]'),(8,1011,2,NULL,0,2,'[b]Ville Basse[/b] de [zone=3703] est l\'endroit où les réfugiés se rassemblent et saident par leurs propres moyens. Lorsque vous aidez l\'une des races qui ont fui la guerre, la réputation se débrouille rapidement. Leur intendant, [npc=21655], est situé sur le marché dans la ville basse.\n\nLa ville basse de Shattrath contient de nombreux artisans qui possèdent de vastes connaissances :\n[ul]\n[li][npc=19187], [small]< Maître des travailleurs du cuirs >[/ small].[/li]\n[li][npc=19180], [small]< Maître des dépeceurs >[/small].[/li]\n[li][npc=19052], [small]< Maître des alchimistes >[/small]. Il donne la quête [quest=10902] (pour une spécialisation). Un laboratoire dalchimiste se trouve également à son côté.[/li]\n[li]Trois tailleurs qui vous permettent de se spécialiser et d\'acheter de nouvelles recettes de couture épiques pour des ensembles d\'armures et des sacs spéciaux :\n[ul][li][npc=22212], [small]< Spécialiste de couture de tisse-ombre >[/small] vend des recettes pour [itemset=553][/li]\n[li][npc=22213], [small]< Spécialiste de couture de feu-sorcier >[/small] vend des recettes pour [itemset=552].[/li]\n[li][npc=22208], [small]< Spécialiste de couture détoffe lunaire > [/small] vend des recettes pour [itemset=554].[/li][/ul]\n[/ul]\n\nLes maîtres de guerre, Alliance et Horde, des quatre [zones=6] peuvent également être trouvés ici, ainsi que la Tavernes de la Fin du Monde.\n\n[h3]Réputation[/h3]\n\n[b]Jusqu\'à honoré [/b]\n[ul]\n[li]Faire [zone=3790] en [i]mode normal[/i], vous récompense denvirons 750 points de réputation.[/li]\n[li]Faire [zone=3791] en [i]mode normal[/i], vous récompense denvirons 1 250 points de réputation.[/li]\n[li]Faire [zone=3789] en [i]mode normal[/i], vous récompense denvirons 2 000 points de réputation.[/li]\n[li]Fournir 30 x [item=25719] à [npc=22429], vous récompense de 250 points de réputations par quête.[/li]\n[/ul]\n[i]Note : Les joueurs qui visent une faction supérieure à Honorée devraient attendre jusqu\'à dêtre honoré avant de compléter les quêtes de la Ville Basse.[/i]\n\n[b]De honoré à exalté[/b]\n[ul]\n[li]Faire de Labyrinthe des ombres en [i]mode normal[/i], vous récompense de 2 000 points de réputation.[/li]\n[li]Terminer toutes les [url=?quests&filter=cr=1;crs=1011;crv=0]quête de la Ville-Basse[/url].[/li]\n[/ul]\n[b]De révéré à exalté[/b]\n[ul]\n[li]Faire les Cryptages Auchenai en [i]mode héroïque[/i], vous récompense denvirons 750 points de réputation.[/li]\n[li]Faire les salles de Sethekk en [i]mode héroïque[/i], vous récompense denvirons 1 250 points de réputation.[/li]\n[li]Faire le Labyrinthe des ombres en [i]mode normal[/i] ou en [i]mode héroïque[/i], vous récompense denvirons 2 000 points de réputation.[/li]\n[/ul]\n\n[h3]Anecdotes[/h3]\n\n[npc=19227], un vendeur dans la ville basse, vend des amulettes qui sont très ... intéressantes. Il vend des articles comme [item=27940], qui vous permettent de revenir à la vie lorsque vous retournez à l\'endroit où vous êtes mort. [i]Buyer se méfiez-vous![/i]\n\nEn tant quexalté, vous pouvez acheter un [item=31778]. Curieusement, aucun des habitants de la Ville Basse na été vu avec un tel objet. Peut-être qu\'ils ne peuvent pas se le permettre'),(8,1015,2,NULL,0,2,'L[b]Aile-du-Néant[/b] est une faction de dragons situés en Outreterre. La couvée inhabituelle a été engendrée par les ufs du vol de dragon noir dAile-de-Mort et infusée d\'énergies brutes. Maintenant, ils cherchent à trouver leur identité au-delà de l\'ombre du patrimoine destructeur de leur père.\n\n[h3]Réputation[/h3]\n\nLes joueurs, au commencement, sont haïe à la faction Aile-du-Néant et doivent être exaltés pour recevoir des [span class=q4][url=?Items=15.-7&filter=na=Aile-du-Néant+Drake]Drakes Aile-du-Néant[/url][/spanclass]. La suite de quête de la réputation est une suite qui se fait en solitaire impliquant des quêtes journalières, une quête de groupes (5 joueurs) pour passer Neutre et les quêtes journalières de groupe (3 joueurs) après être passer Révéré.\nUne monture volante est requise pour cette réputation et 300 compétences de monte sont nécessaires pour passer neutre.\n\n[b]De Haïe à Neutre[/b]\n\nLes joueurs de niveau 70 commenceront leur voyage pour une réputation exaltée en choisissant la suite de quête offerte par [npc=22113], un elfe du sang errant la surface des champs dAile-du-Néant, dans le coin sud-est de [zone=3520]. La suite de quête commence par [quest=10804]. L\'achèvement de cette suite fournira une réputation instantanée neutre et le choix de l\'un de [span class=q3][url=?Items&filter=qu=18;cr=1;crv=0;na=Aile%20néant;qu=3]ces 5 items[/url][/span].\n\n[h3]Après Neutre [/h3]\n\nAprès avoir terminé la suite de quête, Mordenai sassurera qui vous ayez acquis 300 compétences [spell=34091] et que vous ayez une réputation neutre auprsè de lAile-de-Néant.\nCela vous accordera un déguisement dOrc Gueule-de-Dragon lorsque vous entrez dans la zone Aile-du-Néant et vous permettra de communiquer et de travailler pour les Gueules-de-Dragon stationné là-bas.\n\nMordenai vous enverra d\'abord à [npc=23139] avec un ensemble de faux papiers. L\'achèvement de cette quête débloque le début des quêtes Gueule-de-Dragon sur lesquelles vous travaillerez pour augmenter votre réputation Aile-du-Néant.\n\nLa plupart de ces quêtes seront journalières (ajoutée à la 2.1). Les quêtes journalières diffèrent des quêtes régulières car elles sont infiniment repérables, mais vous ne pouvez compléter chaque quête journalière qu\'une fois par jour et se limiter à 25 quêtes journalières par jour.\n[i]Remarque : De nouvelles quêtes seront débloquées après chaque niveau de réputation, et toutes les quêtes journalières des niveaux précédents seront toujours disponibles.[/i]\n\n[b][toggler id=Neutralcaché]Neutre[/toggler][/b]\n\n[div id=Neutralcaché] \nAprès avoir donné la [item=32469] à [npc=23139] pour compléter [quest=11013], votre première suite de quêtes sera disponible pour accéder au prochain niveau de réputation avec Aile-du-Néant.\n\nMor\'ghor vous indiquera daller voir le maître d\'uvre afin de commencer votre travail, et [npc=23141] se révélera comme un allié déguisé et vous proposera dautres quêtes.\nL\'une d\'entre elles est [quest=11049]. Les joueurs pourront trouver, avec un peu de chance (1% de loot), l[item=32506] sur presque toutes les créatures de lescarpement dAile-du-Néant et sur un [item=185881] ou un [item=185877].\nYarzill voudra aussi une trouvaille rare, l[item=185915], trouvée n\'importe où sur le rebord dAile-du-Néant et dans la forteresse Gueule-de-Dragon, coin sud-est de la vallée de dOmbrelune. Cette quête n\'est pas étiquetée comme journalière et peut donc être effectuée autant de fois que vous voulez, du moment que vous pouvez trouver des ufs. Cette quête nest pas comprise dans votre limite de quête journalière.\n\nAutres quêtes disponibles dès le début:\n[ul]\n[li][i][small]Journalière[/small][/i] - [quest=11018], [quest=11016], [quest=11017] Nest disponible que pour les joueurs qui possèdent la profession adaptée pour rassembler chaque élément.[/li]\n[li][i][small]Journalière[/small][/i] - [quest=11015] - Une quête de collecte simple ouverte à tous les joueurs indépendamment de leur profession.[/li] \n[li][i][small]Journalière[/small][/i] - [quest=11020] - Yarzill vous demandera de collecter des [item=32502]s et de les utiliser afin dempoisonner les péons qui travaillent pour rassembler des ressources pour Gueule-de-Dragon.[/li]\n[li][i][small]Journalière[/small][/i] - [quest=11035] - Vous devrez voler vers le coin nord-est de lescarpement dAile-du-Néant et vous positionner sur une des roches flottantes pour intercepter le [npc= 23188] et récupérer 10 x [item=32509].[/li]\n[/ul]\n[/div]\n[b][toggler id=Friendlyhidden]Amical[/toggler][/b]\n\n[div id=Friendlyhidden]\nMor\'ghor vous donnera un [item=32694] pour circuler avec votre nouveau rang parmi les Gueules-de-Dragon.\n[ul]\n[li][quest=11083] - [npc=23166] vous enverra tuer des bourbesangs qui sont stationné profondément dans les mines.[/li]\n[li][quest=11081] - Après avoir trouvé les [item=32726] dans un [item=32724], vous révélerez ce qui se passe réellement avec les bourbesangs dans la mine.[/li]\n[li][quest=11054] - [npc=23291] vous donnera vos propres [item=32680] pour garder les pétons Gueules-de-Dragon en ligne et travailler avec efficacité[/li]\n[li][i][small]Journalière[/small][/i] - [quest=11076] - La [npc=23149] vous demandera de vous aventurer dans les mines Ailes-du-Néant et de récupérer la cargaison contenue dans les chars de la mine qui est jetée au hasard dans l\'intérieur de la mine.[/li]\n[li][i][small]Journalière[/small][/i] - [quest=11077] - L\'un des [npc=23376] vous informera que des créatures plus profondes dans la mine interrompent la production et vous demandent de réduire leur nombre.[/li]\n[li][i][small]Journalière[/small][/i] - [quest=11055] - Cette quête humoristique commence chez le [npc=23291] après que vous lui apportiez le matériel requis. Vous pourrez survoler lescarpement Aile-du-Néant et lancer le Booterang à n\'importe quel [npc=23311] qui sy trouve autour des cris-taux.[/li]\n[/ul]\n[/div]\n[b][toggler id=Honorécaché]Honoré[/toggler][/b]\n\n[div id=Honorécaché]\nMor\'ghor vous donnera votre nouveau [item=32695], qui est maintenant utilisable n\'importe où, tant que vous êtes à l\'extérieur.\n[ul]\n[li][quest=11063] - Cette quête en six parties est une course aérienne contre les autres maîtres de vol Gueule-de-Dragon. Ils tenteront tous de vous renverser, vous et votre monture, avec des attaques aériennes habilement placées, vous devez rester visible et sur votre monture jusqu\'à leur atterrissage, si vous échouez, vous devez redémarrer la quête. Après avoir vaincu le dernier des six coureurs, vous recevrez un [item=32863], qui fonctionne exactement comme une [item=25653]. Les effets des deux bijoux ne sadditionnent pas.[/li]\n[li][quest=11089] Le [npc=23427] demandera un ensemble de matériaux pour créer un dispositif spécial pour détruire son frère et entraver les avancées de la légion dans l\'ouest de [zone=3518].[/li]\n[li][i][small]Journalière[/small][/i] - [quest=11086] - Mor\'ghor Vous enverra au Portal de Nagrand pour tuer 20 [url=?npcs=7&filter=na=ombremort] Agents Ombremort[/url]. Attention aux seigneurs, ils patrouillent dans la région et peuvent vous tuer dcoup de poing.[/li]\n[/ul]\n[/div]\n[b][toggler id=Révéréhidden]Révéré[/toggler][/b]\n\n[div id=Révéréhidden]\nMor\'ghor vous donnera votre nouveau [item=32864], le plus haut bijou.\n[ul]\n[li][url=?quests&filter=na=tuez%20les%20tous;minle=70;maxle=70] Tuez-les tous ![/url] - Mor\'ghor vous ordonnera de commencer l\'attaque la base d\'opérations de votre faction dans la vallée de Sombrelune. De toute évidence, vous n\'allez pas autoriser les Gueules-de-Dragon à attaquer vos alliés, alors vous informerez au leader approprié et débloquerez votre dernière quête journalière pour les Gueules-de-Dragon.[/li]\n[li][i][small]Journalière[/small][/i] [url=?quests&filter=na=le%20plus%20mortel%20des%20pièges]Le plus mortel des pièges[/url] - Les forces Gueules-de-Dragon vont attaquer la base des opérations. Apportez des alliés, car il s\'agit d\'une grande bataille.[/li]\n[/ul]\n[/div]\n[b][toggler id=Exaltécaché]Exalté[/toggler][/b]\n\n[div id=Exaltécaché]\nAprès de nombreux jours de travail, finalement le dénouement de la suite des quêtes Aile-du-Néant / Gueule-de-Dragon, vous dirigera à Mor\'ghor une dernière fois, qui vous informera que vous serez promu par [npc=22917] lui-même.\nSans gâcher les événements qui s\'ensuivent, vous vous retrouverez à Shattrath avec une sélection de montures épiques Aile-du-Néant. Vous pouvez en choisir un gratuitement, et si vous décidez d\'une couleur différente plus tard, vous pouvez acheter un autre drake chez [npc=23489] dans le camp de Gueule-de-Dragon pour 200 or.\n[/div]'),(8,1031,2,NULL,0,2,'Les [b]Gardes-ciel sha\'tari[/b] sont les gardiens aériens de [zone=3703], défendant la capitale des assaillants dans les collines ainsi que la lutte contre les Arakkoas de Terokk dans les sommets de Skettis. [faction=935] dirigent les gardes-ciel shatari.\nIls ont deux avant-postes, l\'un au nord des montages de Skettis et un près d[faction=1038]. Les joueurs commencent avec une réputation neutre chez les Gardes-ciel sha\'tari.\n\n[h3]Réputation[/h3]\n\n[b]Quêtes journalières[/b]\n[ul]\n[li][quest=11008] - [npc=23048] vous accordera un paquet d\'explosifs pour détruire les oeufs qui reposent au sommet des structures de Skettis. [/li]\n[li][quest=11085] - Le [npc=23383] peut être trouvé au sommet de certaines structures, les joueurs l\'escorteront pour la réputation, l\'or et un choix entre deux potions : [item=28100] ou [item=28101].[/li]\n[li][quest=11065] - [npc=23335] vous informera que les bombardements, de lavant-poste de la garde-ciel, ont coûté la vie de leurs montures et vous demandent de rassembler des Raies de léther pour compléter leurs forces aériennes.[/li]\n[li][quest=11010] - [npc=23120] vous demande de détruire les munitions pour les canons de la Légion afin que les gardes-ciel puissent continuer leur travail.[/li]\n[li][quest=11004] - Après avoir recueilli 6 [item=32388], [npc=23042] fera une potion qui permettra de voir l\'arakkoa le plus puissant, tel que [npc=23066].[i][small] Note : cette quête n\'est pas une quête journalière, mais peut être répété autant de fois que nécessaire. [/small][/i][/li]\n[/ul]\n\n[b]Créatures[/b]\n\n[ul]\n[li][npc=21804] - 5 points de réputation, jusqu\'à la fin de Révéré[/li]\n[li][url=?npcs&filter=na=skettis+-kaliri+-assassin;minle=70] Tous les Arakkoa de Skettis[/url] - 10 points de réputation.[/li]\n[li][npc=23029] - 30 points de réputation.[/li]\n[/ul]'),(NULL,NULL,0,'new',0,2,'Any user can write a guide and then share it with the community. Before a guide will be available to the public, it will be put in a queue where it can be approved or rejected by the staff. We suggest that you make sure your guide is complete before you put it through this process. A complete guide will generally be thorough, 100% accurate for World of Warcraft\'s current build, and include details such as images.\n\n[h3]Tips For Creating Quality Guides[/h3]\n\n[ul][li][b]Use [url=?help=markup-guide]Aowow\'s BBCode[/url].[/b][/li]\n[li][b]Choose the correct category.[/b] Guides placed in the wrong category risk being rejected. Don\'t see your category? Email [feedback]![/li]\n[li][b]Always submit only complete guides.[/b] You can save in-progress ones indefinitely so you won\'t risk losing them.[/li]\n[li][b]Make sure it\'s on a unique topic with unique advice.[/b] If someone has already covered your topic, make sure that your guide offers something different and/or better advice or else it may be downvoted by our community.[/li]\n[li][b]Extremely short guides may be better off as a comment.[/b] Though overall there is no predetermined length for a good guide.[/li]\n[li][b]We do not tolerate plagiarism in any form.[/b] Make sure to include credits to other sources and a hyperlink if you use their images or otherwise.[/li][/ul]'),(NULL,NULL,0,'edit',0,2,'Any user can write a guide and then share it with the community. Before a guide will be available to the public, it will be put in a queue where it can be approved or rejected by the staff. We suggest that you make sure your guide is complete before you put it through this process. A complete guide will generally be thorough, 100% accurate for World of Warcraft\'s current build, and include details such as images.\n\n[h3]Tips For Creating Quality Guides[/h3]\n\n[ul][li][b]Use [url=?help=markup-guide]Aowow\'s BBCode[/url].[/b][/li]\n[li][b]Choose the correct category.[/b] Guides placed in the wrong category risk being rejected. Don\'t see your category? Email [feedback]![/li]\n[li][b]Always submit only complete guides.[/b] You can save in-progress ones indefinitely so you won\'t risk losing them.[/li]\n[li][b]Make sure it\'s on a unique topic with unique advice.[/b] If someone has already covered your topic, make sure that your guide offers something different and/or better advice or else it may be downvoted by our community.[/li]\n[li][b]Extremely short guides may be better off as a comment.[/b] Though overall there is no predetermined length for a good guide.[/li]\n[li][b]We do not tolerate plagiarism in any form.[/b] Make sure to include credits to other sources and a hyperlink if you use their images or otherwise.[/li][/ul]'),(13,1,3,NULL,0,2,'[b][color=c1]Krieger[/color][/b] sind eine sehr mächtige Klasse, die sowohl tanken als auch im Nahkampf erheblichen Schaden anrichten kann. Der [icon name=ability_warrior_defensivestance][url=?spells=7.1.257]Schutz[/url][/icon]-Talentbaum des Kriegers enthält viele Talente, um seine Überlebensfähigkeit zu verbessern und Bedrohung gegenüber Monstern zu erzeugen. Schutz-Krieger sind eine der wichtigsten Tank-Klassen des Spiels.\n\nAußerdem verfügen Krieger über zwei schadensorientierte Talentbäume - [icon name=ability_rogue_eviscerate][url=?spells=7.1.26]Waffen[/url][/icon] und [icon name=ability_warrior_innerrage][url=?spells=7.1.256]Furor[/url][/icon]. Der Furor-Talentbaum enthält das Talent [spell=46917], das es dem Krieger erlaubt, zwei Zweihandwaffen gleichzeitig zu führen! Krieger sind in der Lage, mit Fähigkeiten wie [spell=845], [spell=1680] und [spell=46924] starken Flächenschaden im Nahkampf zu verursachen. Ein Krieger kämpft in einer bestimmten [i]Haltung[/i], die ihm Boni und Zugang zu verschiedenen Fähigkeiten gewährt. Zu Beginn verfügen Krieger nur über die [spell=2457], erlernen aber mit Level 10 [spell=71] und mit Level 30 [spell=2458]. Die Verteidigungshaltung wird zum Tanken, die Kampfhaltung oder Berserkerhaltung für erheblichen Nahkampfschaden verwendet.\n\n[ul][li]Alle Krieger können ihren Schlachtzug oder ihre Gruppe mit einem [spell=6673] oder [spell=469] verstärken. Furor-Krieger besitzen den passiven Stärkungszauber [spell=29801], der die Chance auf kritische Treffer im Nah- und Fernkampf für ihre Verbündeten deutlich erhöht.[/li][li]Krieger haben zahlreiche nützliche Fähigkeiten, um schnell an ihr Ziel zu gelangen! Alle Krieger können [spell=100] oder [spell=20252] benutzen, um einen Gegner zu erreichen. Zudem können sie schnell [spell=3411], um ein befreundetes Ziel vor einem Angriff zu schützen.[/li][/ul]'),(13,2,3,NULL,0,2,'[b][color=c2]Paladine[/color][/b] unterstützen ihre Verbündeten mit heiligen Auren und Segen, um sie vor Schaden zu bewahren und ihre Kräfte zu stärken. Sie tragen Plattenrüstungen und können in den härtesten Schlachten verheerenden Schlägen standhalten, während sie ihre Verwundeten heilen und die Gefallenen wiederbeleben. Im Kampf können sie mächtige Zweihandwaffen führen, ihre Feinde betäuben, Untote und Dämonen vernichten und ihre Feinde mit heiliger Vergeltung richten. Paladine sind eine defensive Klasse, die in erster Linie darauf ausgelegt ist, ihre Gegner zu überdauern.\n\nDer Paladin ist hauptsächlich ein Nahkämpfer und in geringem Maße Zauberer, der aufgrund seiner [url=?spells=7.2&filter=cr=109:12;crs=10:1;crv=0:0]Heilzauber[/url], [url=?spells=7.2&filter=na=Segen]Segen[/url] und anderen Fähigkeiten sehr nützlich für die Gruppe ist. Sie können eine aktive [url=?spells=7.2&filter=na=Aura]Aura[/url] pro Paladin auf alle Gruppen- und Schlachtzugsmitglieder legen und bestimmte Segen für bestimmte Spieler verwenden. Dank ihrer zahlreichen defensiven Fähigkeiten vergessen Paladine einfach unglaublich oft zu sterben. Mit ihrer Fähigkeit [spell=25780] sind sie außerdem ausgezeichnete Tanks.\n\n[ul][li]Paladine können effektiv [icon name=spell_holy_holybolt][url=?spells=7.2.594]heilen[/url][/icon], [icon name=spell_holy_devotionaura][url=?spells=7.2.267]tanken[/url][/icon] und im Nahkampf [icon name=spell_holy_auraoflight][url=?spells=7.2.184]Schaden[/url][/icon] verursachen.[/li][li]Sie besitzen eine große Auswahl an Segen, Auren und anderen Verstärkungszaubern.[/li][li]Der Paladin ist die einzige Klasse mit Zugang zu einem echten Unverwundbarkeitszauber: [spell=642].[/li][/ul]'),(13,3,3,NULL,0,2,'[b][color=c3]Jäger[/color][/b] sind eine besonders einzigartige Klasse in World of Warcraft. Sie sind die einzigen nicht-magischen Fernkämpfer, die mit Bögen und Gewehren kämpfen. Jäger verfügen über verschiedene Arten von Schüssen und Bissen zur Schwächung ihrer Gegner und können [url=?spells=7.3&filter=cr=4;crs=1;crv=0;na=Falle]Fallen[/url] legen, um Schaden zu verursachen oder den Gegner auf andere Weise zu verlangsamen oder kampfunfähig zu machen.\n\nJäger [icon name=ability_hunter_beasttaming][url=?spell=1515]zähmen Wildtiere[/url][/icon], damit diese sie als [url=?pets]Begleiter[/url] im Kampf unterstützen. Zwar sind Jäger nicht die einzige Klasse, die Begleiter einsetzen kann. Ihre Tierbegleiter sind aber insofern einzigartig, als jede Spezies einen [url=?petcalc]eigenen Talentbaum[/url] hat, den der Jäger nutzen kann, um Punkte auf verschiedene Fähigkeiten zu verteilen.\n\nDarüber hinaus hat jede Spezies eine einzigartige Spezialfähigkeit. Jäger können sich die begehrtesten Begleiter aufgrund ihres Aussehens oder ihrer Fähigkeiten aussuchen. Und wenn sie genug Talentpunkte in den Baum der [icon name=ability_hunter_beasttaming][url=?spells=7.3.50]Tierherrschaft[/url][/icon] investieren, können sie besondere, \"exotische\" Bestien zähmen, wie [url=?pet=46]Geisterbestien[/url] oder [url=?pet=39]Teufelssaurier[/url]!\n\n[ul][li]Jäger haben Zugriff auf 25 (32 als [icon name=ability_hunter_beastmastery][url=?spell=53270]Meister der Tierherrschaft[/url][/icon]) verschiedene Arten von Begleitern mit über 150 verschiedenen Erscheinungsbildern![/li][li]Jäger haben eine Reihe von überlebensorientierten Fähigkeiten, die sie einsetzen können, um potentiellen Gefahren zu entkommen oder ihnen auszuweichen, wie z.B. [spell=5384] und [spell=781].[/li][li]Auf das [icon name=ability_hunter_swiftstrike][url=?spells=7.3.51]Überleben[/url][/icon] spezialisierte Jäger können in ihrem Talentbaum Punkte in das Talent [icon name=ability_hunter_huntingparty][url=?spells=-2.3&filter=na=jagdgesellschaft rel=spell=53292]Jagdgesellschaft[/url][/icon] investieren, welches es ihnen ermöglicht, ihre Gruppen- und Schlachtzugsmitglieder mit dem Stärkungszauber [spell=57669] zu versorgen.[/li][/ul]'),(13,4,3,NULL,0,2,'[b][color=c4]Schurken[/color][/b] sind eine Nahkampfklasse, die Lederrüstungen trägt und ihren Feinden mit sehr schnellen Angriffen großen Schaden zufügen kann. Sie sind Meister der Verstohlenheit und des Meuchelns, die sich ungesehen an Feinden vorbeischleichen, aus den Schatten heraus zuschlagen und dann blitzschnell aus dem Kampf verschwinden.\n\nSie sind in der Lage, [url=?items=0.-3&filter=cr=152;crs=4;crv=0;ty=-3#0+1-2]Gifte[/url] einzusetzen, um ihre Gegner zu verkrüppeln und sie so im Kampf massiv zu schwächen. Schurken verfügen über ein mächtiges Arsenal an Fähigkeiten, von denen viele dadurch verstärkt werden, dass sie in [spell=1784] schleichen und ihre Opfer kampfunfähig machen können.\n\nSchurken können sich auf drei unterschiedliche Kampfstile mithilfe ihrer Talentbäume Meucheln, Kampf und Täuschung spezialisieren.\n\nAuf das [icon name=ability_rogue_eviscerate][url=?spells=7.4.253]Meucheln[/url][/icon] spezialisierte Schurken sind [icon name=ability_creature_poison_06][url=?spells=-2&filter=na=meister+der+gifte rel=spell=58410]Meister der Gifte[/url][/icon] und [icon name=ability_rogue_disembowel][url=?spell=57993]vergiften[/url][/icon] ihre Gegner mit schnellen Dolchen, die mit [icon name=ability_rogue_feigndeath][url=?spells=-2.4.253&filter=na=Üble+Gifte rel=spell=16515]üblen[/url][/icon] und [icon name=ability_poisons][url=?spells=-2.4.253&filter=na=Verbesserte+Gifte rel=spell=14117]verbesserten[/url][/icon] Giften versehen sind.\n\nAuf den [icon name=ability_backstab][url=?spells=7.4.38]Kampf[/url][/icon] spezialisierte Schurken können den Umgang mit [icon name=inv_sword_27][url=?spells=-2&filter=na=Niedermetzeln rel=spell=13964]Axt und Schwert[/url][/icon] oder [icon name=inv_mace_01][url=?spells=-2&filter=na=Streitkolben-Spezialisierung;cl=4 rel=spell=13803]Streitkolben[/url][/icon] meistern und haben mithilfe ihrer Talente auch in langwierigen Kämpfen eine verbesserte Energiezufuhr, um zuverlässig ihre Angriffscombos durchzuführen.\n\nAuf die [icon name=ability_stealth][url=?spells=7.4.39]Täuschung[/url][/icon] spezialisierte Schurken besitzen Fähigkeiten, die unvorhergesehene Aktionen ermöglichen. So können sie dank [spell=51713] etwa kurzzeitig Fähigkeiten nutzen, die eigentlich nur aus der Verstohlenheit heraus nutzbar wären, mit [spell=36554] plötzlich hinter einem Gegner auftauchen oder mit [icon name=ability_rogue_cheatdeath][url=?spells=-2.4.39&filter=cr=15;crs=0;crv=ability_rogue_cheatdeath rel=spell=31230]Von der Schippe springen[/url][/icon] einen sicheren Todesstoß überleben.'),(13,5,3,NULL,0,2,'[b][color=c5]Priester[/color][/b] gelten allgemein als eine der Standard-Heilerklassen in World of Warcraft, da sie über zwei Talentspezialisierungen zur effektiven Heilung verfügen.\n\nIhr [icon name=spell_holy_holybolt][url=?spells=7.5.56]Heilig[/url][/icon]-Talentbaum enthält Talente, die die Heilung auf ihre Verbündeten erheblich verstärken - einschließlich Zaubern, mit denen mehrere Spieler gleichzeitig geheilt werden können, wie z.B. [spell=48089].\nDer [icon name=spell_holy_wordfortitude][url=?spells=7.5.613]Disziplin[/url][/icon]-Talentbaum ist zwar auch in der Lage, eine beträchtliche Menge an Heilung zu bewirken, konzentriert sich aber in erster Linie auf die Schadensabsorption und -verminderung durch den Einsatz von [spell=48066] und [icon name=spell_holy_devineaegis][url=?spells=-2.5&filter=cr=15;crs=0;crv=spell_holy_devineaegis rel=spell=47515]Göttliche Aegis[/url][/icon].\nPriester können außerdem mit ihren [icon name=spell_shadow_shadowwordpain][url=?spells=7.5.78]Schatten[/url][/icon]fähigkeiten sehr mächtigen Fernkampfschaden verursachen. Insbesondere wenn sie ihre [spell=15473] annehmen, erhöht sich ihr Schattenschaden erheblich, aber sie verlieren ihre Fähigkeit, Heiligzauber zu wirken.\n\n[ul][li]Der Disziplin-Talentbaum wird in der Regel zur Heilung verwendet, enthält aber auch einige Talente zur Erhöhung des Schadens des Priesters, wobei Schattenzauber und -fähigkeiten in erster Linie zur Verursachung von Fernkampfschaden verwendet werden sollten.[/li][li]Priester verfügen über einen der am meisten geschätzten Stärkungszauber im Spiel - [spell=48161], welcher allen befreundeten Mitspielern eine unverzichtbare Erhöhung ihrer Ausdauer gewährt. Außerdem können sie ihre Mitspieler mit [spell=48073] und [spell=48169] verstärken und mit einzigartigen Hymnen das [icon name=spell_holy_divinehymn][url=?spell=64843]Leben[/url][/icon] und [icon name=spell_holy_symbolofhope][url=?spell=64901]Mana[/url][/icon] ihres Schlachtzug signifikant wiederherstellen![/li][li]Schattenpriester unterstützen, zusätzlich zu ihrem Schaden, jeden Schlachtzug mit dem beliebten Stärkungszauber [spell=57669] zur Erhöhung der Manaregeneration und mit ihrer [spell=15286], die ihre gesamte Gruppe passiv heilt.[/li][/ul]'),(13,6,3,NULL,0,2,'Die [b][color=c6]Todesritter[/color][/b] wurden in der Erweiterung Wrath of the Lich King eingeführt und sind die erste Heldenklasse von World of Warcraft. Todesritter beginnen auf Stufe 55 in einer speziellen, instanzierten Zone, die für andere Klassen unzugänglich ist: [url=?maps=4298:511346]Acherus, die Schwarze Festung[/url] in der Scharlachroten Enklave der Östlichen Pestländer. Hier erhalten sie ihre Talentpunkte durch Questbelohnungen und bekommen sogar ein besonderes beschworenes Reittier: das [spell=48778]!\n\nTodesritter haben mehrere sehr starke Möglichkeiten zur Schadensverursachung, da jeder ihrer Talentbäume es erlaubt mit einer Vielfalt an Nahkampffähigkeiten, Zaubern und Schaden-über-Zeit verursachenden Krankheiten überragende Leistung zu erbringen. Sie sind auch sehr fähige Tanks, wobei sowohl ihr Blut- als auch ihr Frost-Talentbaum einzigartige Optionen bietet. [icon name=spell_deathknight_bloodpresence][url=?spells=7.6.770]Blut[/url][/icon] bietet mehr Selbstheilungsfähigkeiten, [icon name=spell_deathknight_frostpresence][url=?spells=7.6.771]Frost[/url][/icon] bietet erhebliche Schadensminderung und starken Flächenschaden.\n\nTodesritter kämpfen mit einem besonderen Verstärkungszauber, der [url=?spells=7&filter=na=präsenz]Präsenz[/url] genannt wird (ähnlich wie die Haltungen eines Kriegers), der ihnen besondere Boni für ihre Rollen verleiht. Todesritter verwenden ein einzigartiges Ressourcensystem, bei dem die meisten Zauber entweder [url=?spells=7.6&filter=cr=45;crs=10;crv=0#50+1+13+3]Runen[/url] kosten, die während des Kampfes wieder aufgefüllt werden, oder [url=?spells=7.6&filter=cr=45;crs=11;crv=0]Runenmacht[/url], die durch verschiedene Fähigkeiten erzeugt werden kann.\n\n[ul][li]Auf [icon name=spell_deathknight_unholypresence][url=?spells=7.6.772]Unheilig[/url][/icon] spezialisierte Todesritter können sich in [spell=52143] spezialisieren, was ihren beschworenen Ghul-Wächter zu einem permanenten Begleiter macht, der sie im Kampf unterstützt![/li][li]Die Klasse der Todesritter verfügt über eine eigene spezielle Waffenverzauberungsfähigkeit namens [spell=53428], die herkömmliche Waffenverzauberungen überflüssig macht.[/li][li]Todesritter sind eine Schadensklasse, die ihren Schaden sowohl durch Nahkampffähigkeiten als auch durch Zauber verursacht![/li][/ul]'),(13,7,3,NULL,0,2,'[b][color=c7]Schamanen[/color][/b] beherrschen Elementar- und Naturmagie und bringen einer (Schlachtzugs-) Gruppe die größte Vielfalt an potenziellen Stärkungszaubern in Form von [url=?spells=7&filter=na=Totem;cl=7]Totems[/url]. Ein Schamane kann für jedes Element - Erde, Feuer, Luft und Wasser - ein Totem beschwören, welches zu seinen Füßen erscheint und allen Mitgliedern seiner (Schlachtzugs-) Gruppe in Reichweite einen Stärkungszauber verleiht. Einige Totems, insbesondere Feuer-Totems, fügen Gegnern auch Schaden zu. Der Trick beim Spielen jeder Art von Schamanen besteht darin, zu wissen, welche Totems in welcher Situation beschworen werden müssen, um den verursachten Schaden und die Überlebensfähigkeit ihrer Gruppe zu maximieren.\n\nSchamanen sind in erster Linie Zauberer, wobei ein auf [icon name=spell_nature_lightningshield][url=?spells=7.7.373]Verstärkung[/url][/icon] spezialisierter Schamane Schaden in Nahkampfreichweite verursacht. Ein solcher Schamane erlernt das Führen zweier Waffen durch [spell=30798] und kann mit [spell=51533] zwei Schattenwölfe zur Unterstützung im Kampf beschwören. Obwohl sie hauptsächlich im Nahkampf eingesetzt werden, können auf Verstärkung spezialisierte Schamanen dennoch einen gewissen Nutzen aus ihrer Zaubermacht ziehen und spontane [icon name=spell_nature_lightning][url=?spell=49238]Blitzschläge[/url][/icon] oder [url=?spells=7&filter=cr=109:12:14;crs=10:1:5;crv=0:0:60000;cl=7]Heilungen[/url] durch [icon name=spell_shaman_maelstromweapon][url=?spells=-2&filter=na=waffe+des+mahlstroms rel=spell=51532]Waffe des Mahlstroms[/url][/icon] wirken.\n\nAuf [icon name=spell_nature_lightning][url=?spells=7.7.375]Elementarkampf[/url][/icon] spezialisierte Schamanen wirken Feuer- und Blitzzauber auf Distanz und verursachen so großen Schaden. Sie können Gegner durch [spell=59159] zurückstoßen und mit [icon name=spell_shaman_stormearthfire][url=?spells=-2&filter=na=sturm%2C+erde+und+feuer rel=spell=51486]Sturm, Erde und Feuer[/url][/icon] alle Feinde in einem Gebiet festwurzeln. Außerdem gewähren sie durch [spell=57722] und [icon name=spell_shaman_elementaloath][url=?spells=-2&filter=na=Elementarer+Schwur rel=spell=51470]Elementarer Schwur[/url][/icon] begehrte Stärkungszauber für Zauberer ihres Schlachtzugs.\n\nEin auf [icon name=spell_nature_magicimmunity][url=?spells=7.7.374]Wiederherstellung[/url][/icon] spezialisierter Schamane erhält verbesserte Heilzauber und kann ein ausgezeichneter Schlachtzugs- oder Tankheiler sein. Sie sind bekannt für ihre mächtige Fähigkeit [spell=55459] und dafür, dass sie ein [spell=16190] zur Verfügung stellen, welches der Gruppe hilft Mana wiederherzustellen. Sie erhalten außerdem ein mächtiges [spell=49284], können mit [spell=51886] Flüche entfernen und verfügen durch [spell=61301] über einen Spontanheilungseffekt, der zusätzlich eine Heilung über Zeit verursacht.\n\n[ul][li]Es gibt über zwanzig verschiedene Totems, die ein Schamane erlernen kann![/li][li]Schamanen der Horde können [spell=2825] und Schamanen der Allianz [spell=32182] wirken, wodurch der verursachte Schaden und die gewirkte Heilung der gesamten Gruppe erhöht wird. Dieser Stärkungszauber ist einzigartig und in jeder Schlachtzugsgruppe sehr begehrt.[/li][li]Ein Schamane kann sich ab Stufe 16 in einen [spell=2645] verwandeln und dies mit dem Talent [spell=16287] sogar als Spontanzauber wirken. Dieser Zauber kann im Kampf eingesetzt werden, aber nicht in geschlossenen Räumen.[/li][li]Schamanen können immer nur einen Elementarschild - [spell=49281] oder [spell=57960] - gleichzeitig benutzen. Auf Wiederherstellung spezialisierte Schamanen können zudem [spell=49284] auf einen anderen Spieler wirken.[/li][/ul]'),(13,8,3,NULL,0,2,'[b][color=c8]Magier[/color][/b] bändigen die Elemente Feuer, Frost und Arkan, um ihre Feinde zu vernichten oder unter Kontrolle zu halten. Dazu besitzen sie ein Arsenal voller Zauber zu unterschiedlichen Zwecken.\nStärkungszauber, [icon name=ability_mage_conjurefoodrank10][url=?spell=42956]herbeigezauberte Erfrischungen[/url][/icon] oder arkane [url=?spells=7&filter=na=Portal]Portale[/url] zur schnellen Weltreise in ferne Länder machen einen Magier zu einem idealen Weggefährten.\nUnd wenn man eine Klasse sucht, die Gegner in eine Welt des Schmerzes einführt, ist der Magier eine gute Wahl. Ihren Gegnern können Magier mit verschiedensten Schwächungszaubern die Bedingungen eines jeden Kampfes diktieren, mit Elementarblitzen massiven Schaden aus der Ferne anrichten, oder Zerstörung in einem großen Wirkungsbereich niederregnen lassen.\n\nAuf [icon name=spell_holy_magicalsentry][url=?spells=7.8.237]Arkan[/url][/icon] spezialisierte Magier haben das Potenzial, mit [icon name=spell_arcane_blast][url=?spell=42897]Arkanschlägen[/url][/icon] und [icon name=ability_mage_missilebarrage][url=?spells=-2&filter=na=Geschosssalve rel=spell=54490]Salven[/url][/icon] an [icon name=spell_nature_starfall][url=?spell=42846]Arkanen Geschossen[/url][/icon] in kurzer Zeit enormen Schaden zu verursachen. Das Bändigen der reinen arkanen Mächte hat jedoch ihre Kehrseite: einem unerfahrenen Arkanmagier verzehrt es schon nach kurzer Zeit seine gesamten Kräfte.\n\nAuf [icon name=spell_fire_flamebolt][url=?spells=7.8.8]Feuer[/url][/icon] spezialisierte Magier verfallen durch kritische Treffer mit Feuerzaubern in [icon name=ability_mage_hotstreak][url=?spells=-2&filter=na=Kampfeshitze rel=spell=44448]Kampfeshitze[/url][/icon] und äschern so ihre Gegner mit verheerenden [icon name=spell_fire_fireball02][url=?spell=42891]Pyroschlägen[/url][/icon] ein. Zudem verwandeln sie ihre Gegner in [icon name=ability_mage_livingbomb][url=?spell=55360]Lebende Bomben[/url][/icon] und verursachen dadurch explosiven Flächenschaden.\n\n[icon name=spell_frost_frostbolt02][url=?spells=7.8.6]Frost[/url][/icon]magier können ihre Gegner [icon name=ability_mage_deepfreeze][url=?spell=44572]in Eis erstarren[/url][/icon] lassen. Ihre Spezialisierung auf Kälteeffekte erlaubt ihnen eine starke Kontrolle über ihre Gegner und erhöht dadurch ihre Überlebensfähigkeit enorm.\n\n[ul][li]Magier können Erfrischungen herbeizaubern, um die Gesundheit und das Mana ihrer Verbündeten wiederherzustellen.[/li][li]Sie sind die einzige Klasse, die Portale erschaffen kann, um andere Spieler zu transportieren. Sie können jedoch keine Spieler von einem entfernten Ort herbeirufen - das ist die Aufgabe eines [class=9]![/li][li]Der verursachte Fernkampfschaden von Magiern ist einer der höchsten im Spiel und macht sie zu einem unverzichtbaren Verbündeten in jedem Schlachtzug.[/li][/ul]'),(13,9,3,NULL,0,2,'[b][color=c9]Hexenmeister[/color][/b] sind Meister der dämonischen Künste. Gekleidet in Gewänder sind sie Meister im Wirken von [url=?spells=7&filter=cr=12;crs=1;crv=0;na=Fluch+der;cl=9]Flüchen[/url], dem Schleudern von Feuer- oder Schattenblitzen und der Beschwörung von [url=?spells=7&filter=cr=14;crs=6;crv=48018;na=beschwören;cl=9]Dämonen[/url] unter ihre Kontrolle zur Unterstützung im Kampf. Die Kombination ihrer Flüche und direkten Schadenszauber richten Verwüstung und Zerstörung an und machen Hexenmeister zu sehr gefürchteten Gegnern.\n\nNeben Mana als primäre Ressource können Hexenmeister Gegnern Teile ihrer [icon name=spell_shadow_haunting][url=?spell=47855]Seele stehlen[/url][/icon] und dadurch [item=6265] erzeugen. Seelensplitter ermöglichen mächtige rituelle Magie, etwa zur [icon name=spell_shadow_twilight][url=?spell=698]Beschwörung von anderen Spielern[/url][/icon] oder von [icon name=spell_shadow_shadesofdarkness][url=?spell=58887]Gesundheitssteinen[/url][/icon] mit heilenden Kräften. Insbesondere kann jedoch ein Hexenmeister mit ihnen die Seele eines Verbündeten in einem [icon name=spell_shadow_soulgem][url=?spell=47884]Seelenstein[/url][/icon] speichern, sodass dieser im Todesfall sich selbst wiederbeleben kann.\n\n[ul][li]Hexenmeister können durch ein Ritual der Beschwörung ein Portal erschaffen, um einen anderen Spieler an den Ort des Portals zu beschwören.[/li][li]Sie können Gesundheitssteine beschwören, die den Anwender heilen.[/li][li]Die Flüche eines Hexenmeisters können ihre Feinde schwächen oder ihnen Schaden zufügen.[/li][/ul]'),(13,11,3,NULL,0,2,'[b][color=c11]Druiden[/color][/b] sind die \"Alleskönner\"-Klasse in World of Warcraft - das heißt, sie können in einer Vielzahl von verschiedenen Rollen agieren und bieten daher einen der vielfältigsten Spielstile. Durch das [i]Annehmen der Gestalt von verschiedenen Kreaturen[/i] kann der Druide heilen, Schaden im Nah- und Fernkampf verursachen oder als Tank agieren. Mit steigenden Stufen kann der Druide neue, immer mächtigere Gestaltwandlungen erlernen, um sich in eine Kreatur passend zu seiner Rolle zu verwandeln.\n\nAuf niedrigeren Stufen wird ein Druide in seiner humanoiden Gestalt heilen oder im Fernkampf Schaden verursachen. Auf späteren Stufen jedoch erhalten Druiden durch die spezialisierten Talentbäume Zugang zu zwei besonderen Gestalten für jede unterschiedliche Rolle.\n\nAuf [icon name=spell_nature_healingtouch][url=?spells=7.11.573]Wiederherstellung[/url][/icon] spezialisierte Druiden erlernen den [spell=33891], der die Manakosten ihrer Heilzauber reduziert und jegliche Heilung auf ihre Verbündeten verstärkt.\nAuf [icon name=spell_nature_starfall][url=?spells=7.11.574]Gleichgewicht[/url][/icon] spezialisierte Druiden verursachen Schaden im Fernkampf und erlernen die [spell=24858], die ihre Rüstung sowie die Chance auf kritische Treffer mit Zaubern bei ihnen und ihren Verbündeten erhöht.\nEs gibt auch zwei Druidenformen für den [icon name=ability_racial_bearform][url=?spells=7.11.134]Wilden Kampf[/url][/icon]. Zum einen die mächtige [spell=5487] (und [spell=9634] ab einer höheren Stufe) - eine auf das Tanken ausgelegte Gestalt, die zusätzliche Rüstung, Gesundheit und Zugang zu einem Arsenal von Fähigkeiten zur Erhöhung der Bedrohung und Schadensverminderung gewährt. Zum anderen die schurkenähnliche [spell=768], die erheblichen Nahkampfschaden verursachen kann.\n\n[ul][li]Druiden erlernen ihre verschiedenen Gestalten durch das Abschließen von Quests oder durch Training. Einige Gestalten können nur durch Talente erlernt werden.[/li][li]Es gibt einige Gestalten, die alle Druiden erlernen können. Die Bärengestalt erhält man ab Stufe 10, die [spell=1066] und [spell=783] ab Stufe 16, die Katzengestalt ab Stufe 20 und die Terrorbärengestalt ab Stufe 40.[/li][li]Druiden haben sogar ihre eigene fliegende Reisegestalt: die [spell=33943] kann ab Stufe 60 und die [spell=40120] ab Stufe 71 erlernt werden, sofern der Spieler bereits [icon name=spell_nature_swiftness][url=?spell=34093]Gekonntes Reiten[/url][/icon] erlernt hat.[/li][li]Einige Druidengestalten können nur über Talente erlernt werden - die Mondkingestalt kann ab Stufe 40 erlernt werden, wenn ein Spieler viele Talentpunkte im Gleichgewicht-Talentbaum verteilt, und Baum des Lebens ab Stufe 50, wenn er viele Talentpunkte im Wiederherstellung-Talentbaum verteilt.[/li][li]Druiden haben ihre eigene, klassenspezifische [icon name=spell_arcane_teleportmoonglade][url=?spell=18960]Teleportationsfähigkeit[/url][/icon], die es ihnen erlaubt, zur [zone=493] zu reisen - praktisch, wenn sie trainieren müssen![/li][li]Druiden in (Terror-) Bärengestalt oder Katzengestalt schwingen zur Verursachung von Nahkampfschaden keine Waffen. Stattdessen erhalten sie einen speziellen Wert für jede ausgerüstete Nahkampfwaffe: die \"Angriffskraft in Tiergestalt\". Dieser Wert ist eine Umwandlung des \"Schaden pro Sekunde\"-Wertes einer Waffe in einen Wert, der Angriffskraft verleiht und den verursachten Schaden des Druiden in Katzen- oder (Terror-) Bärengestalt beeinflusst.[/li][/ul]'); +/*!40000 ALTER TABLE `aowow_articles` ENABLE KEYS */; +UNLOCK TABLES; + +/*!40101 SET SQL_MODE=@OLD_SQL_MODE */; +/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */; +/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */; +/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */; +/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */; +/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */; + +-- Dump completed on 2025-09-22 23:29:16 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/1648222152_01.sql b/setup/sql/updates/1648222152_01.sql new file mode 100644 index 00000000..e4daa30e --- /dev/null +++ b/setup/sql/updates/1648222152_01.sql @@ -0,0 +1 @@ +ALTER TABLE aowow_mails ADD cuFlags INT UNSIGNED DEFAULT 0 NOT NULL AFTER id; diff --git a/setup/sql/updates/1679660926_01.sql b/setup/sql/updates/1679660926_01.sql new file mode 100644 index 00000000..5b3fe754 --- /dev/null +++ b/setup/sql/updates/1679660926_01.sql @@ -0,0 +1 @@ +UPDATE `aowow_dbversion` SET `build` = CONCAT(IFNULL(`build`, ''), ' talentIcons'); diff --git a/setup/sql/updates/1679679565_01.sql b/setup/sql/updates/1679679565_01.sql new file mode 100644 index 00000000..391f0ab8 --- /dev/null +++ b/setup/sql/updates/1679679565_01.sql @@ -0,0 +1 @@ +UPDATE `aowow_dbversion` SET `sql` = CONCAT(IFNULL(`sql`, ''), ' spell'); diff --git a/setup/sql/updates/1680717296_01.sql b/setup/sql/updates/1680717296_01.sql new file mode 100644 index 00000000..0ef56225 --- /dev/null +++ b/setup/sql/updates/1680717296_01.sql @@ -0,0 +1 @@ +UPDATE `aowow_dbversion` SET `build` = CONCAT(IFNULL(`build`, ''), ' power'); diff --git a/setup/sql/updates/1682012750_01.sql b/setup/sql/updates/1682012750_01.sql new file mode 100644 index 00000000..1f28e1bf --- /dev/null +++ b/setup/sql/updates/1682012750_01.sql @@ -0,0 +1 @@ +UPDATE `aowow_dbversion` SET `build` = CONCAT(IFNULL(`build`, ''), ' simpleImg'); diff --git a/setup/sql/updates/1683672833_01.sql b/setup/sql/updates/1683672833_01.sql new file mode 100644 index 00000000..6d163b57 --- /dev/null +++ b/setup/sql/updates/1683672833_01.sql @@ -0,0 +1 @@ +UPDATE `aowow_dbversion` SET `build` = CONCAT(IFNULL(`build`, ''), ' profiler'); diff --git a/setup/sql/updates/1683928829_01.sql b/setup/sql/updates/1683928829_01.sql new file mode 100644 index 00000000..6d163b57 --- /dev/null +++ b/setup/sql/updates/1683928829_01.sql @@ -0,0 +1 @@ +UPDATE `aowow_dbversion` SET `build` = CONCAT(IFNULL(`build`, ''), ' profiler'); diff --git a/setup/sql/updates/1683979752_01.sql b/setup/sql/updates/1683979752_01.sql new file mode 100644 index 00000000..9d975603 --- /dev/null +++ b/setup/sql/updates/1683979752_01.sql @@ -0,0 +1 @@ +UPDATE `aowow_dbversion` SET `build` = CONCAT(IFNULL(`build`, ''), ' statistics'); diff --git a/setup/sql/updates/1684620409_01.sql b/setup/sql/updates/1684620409_01.sql new file mode 100644 index 00000000..0eb594dd --- /dev/null +++ b/setup/sql/updates/1684620409_01.sql @@ -0,0 +1,2 @@ +ALTER TABLE `aowow_source` + ADD COLUMN `moreMask` mediumint(9) unsigned DEFAULT NULL AFTER `moreZoneId`; diff --git a/setup/sql/updates/1684620409_02.sql b/setup/sql/updates/1684620409_02.sql new file mode 100644 index 00000000..bef0e1a9 --- /dev/null +++ b/setup/sql/updates/1684620409_02.sql @@ -0,0 +1 @@ +UPDATE `aowow_dbversion` SET `sql` = CONCAT(IFNULL(`sql`, ''), ' source'); diff --git a/setup/sql/updates/1684793469_01.sql b/setup/sql/updates/1684793469_01.sql new file mode 100644 index 00000000..e6f2ad91 --- /dev/null +++ b/setup/sql/updates/1684793469_01.sql @@ -0,0 +1,71 @@ +REPLACE INTO `aowow_setup_custom_data` VALUES + ("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"); diff --git a/setup/sql/updates/1684849475_01.sql b/setup/sql/updates/1684849475_01.sql new file mode 100644 index 00000000..ce3abb2d --- /dev/null +++ b/setup/sql/updates/1684849475_01.sql @@ -0,0 +1,2 @@ +ALTER TABLE `aowow_source` + MODIFY `typeId` mediumint(9) signed NOT NULL AFTER `type`; diff --git a/setup/sql/updates/1691940877_01.sql b/setup/sql/updates/1691940877_01.sql new file mode 100644 index 00000000..1d298b91 --- /dev/null +++ b/setup/sql/updates/1691940877_01.sql @@ -0,0 +1,49 @@ +DROP TABLE IF EXISTS `dbc_emotes`; +DROP TABLE IF EXISTS `dbc_emotestext`; +DROP TABLE IF EXISTS `dbc_emotestextdata`; + +DROP TABLE IF EXISTS `aowow_emotes`; +CREATE TABLE `aowow_emotes` ( + `id` SMALLINT(5) SIGNED NOT NULL, + `cmd` VARCHAR(35) COLLATE utf8mb4_unicode_ci NOT NULL, + `isAnimated` TINYINT(1) UNSIGNED NOT NULL, + `flags` SMALLINT(5) UNSIGNED NOT NULL, + `parentEmote` SMALLINT(5) UNSIGNED NOT NULL, + `soundId` SMALLINT(5) SIGNED NOT NULL, + `state` TINYINT(1) UNSIGNED NOT NULL, + `stateParam` TINYINT(1) UNSIGNED NOT NULL, + `cuFlags` INT(10) UNSIGNED NOT NULL, + `extToExt_loc0` VARCHAR(65) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `extToExt_loc2` VARCHAR(113) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `extToExt_loc3` VARCHAR(91) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `extToExt_loc4` VARCHAR(71) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `extToExt_loc6` VARCHAR(89) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `extToExt_loc8` VARCHAR(123) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `extToMe_loc0` VARCHAR(65) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `extToMe_loc2` VARCHAR(113) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `extToMe_loc3` VARCHAR(91) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `extToMe_loc4` VARCHAR(71) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `extToMe_loc6` VARCHAR(89) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `extToMe_loc8` VARCHAR(123) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `meToExt_loc0` VARCHAR(65) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `meToExt_loc2` VARCHAR(113) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `meToExt_loc3` VARCHAR(91) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `meToExt_loc4` VARCHAR(71) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `meToExt_loc6` VARCHAR(89) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `meToExt_loc8` VARCHAR(123) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `extToNone_loc0` VARCHAR(65) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `extToNone_loc2` VARCHAR(113) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `extToNone_loc3` VARCHAR(91) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `extToNone_loc4` VARCHAR(71) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `extToNone_loc6` VARCHAR(89) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `extToNone_loc8` VARCHAR(123) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `meToNone_loc0` VARCHAR(65) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `meToNone_loc2` VARCHAR(113) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `meToNone_loc3` VARCHAR(91) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `meToNone_loc4` VARCHAR(71) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `meToNone_loc6` VARCHAR(89) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `meToNone_loc8` VARCHAR(123) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + PRIMARY KEY (`id`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +UPDATE `aowow_dbversion` SET `sql` = CONCAT(IFNULL(`sql`, ''), ' emotes'); diff --git a/setup/sql/updates/1692289951_01.sql b/setup/sql/updates/1692289951_01.sql new file mode 100644 index 00000000..3528f2b5 --- /dev/null +++ b/setup/sql/updates/1692289951_01.sql @@ -0,0 +1,17 @@ +DROP TABLE IF EXISTS `aowow_declinedword`; +DROP TABLE IF EXISTS `aowow_declinedwordcases`; + +CREATE TABLE `aowow_declinedword` ( + `id` SMALLINT(5) UNSIGNED NOT NULL, + `word` VARCHAR(127) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + PRIMARY KEY (`id`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +CREATE TABLE `aowow_declinedwordcases` ( + `wordId` SMALLINT(5) UNSIGNED NOT NULL, + `caseIdx` TINYINT(1) UNSIGNED NOT NULL, + `word` VARCHAR(131) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + PRIMARY KEY (`wordId`, `caseIdx`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +UPDATE `aowow_dbversion` SET `sql` = CONCAT(IFNULL(`sql`, ''), ' declinedwords'); diff --git a/setup/sql/updates/1693485224_01.sql b/setup/sql/updates/1693485224_01.sql new file mode 100644 index 00000000..3db61fd4 --- /dev/null +++ b/setup/sql/updates/1693485224_01.sql @@ -0,0 +1,12 @@ +DROP TABLE IF EXISTS `aowow_screeneffect_sounds`; +CREATE TABLE `aowow_screeneffect_sounds` ( + `id` SMALLINT(5) unsigned NOT NULL, + `name` VARCHAR(40) COLLATE utf8mb4_unicode_ci NOT NULL, + `ambienceDay` SMALLINT(5) unsigned NOT NULL, + `ambienceNight` SMALLINT(5) unsigned NOT NULL, + `musicDay` SMALLINT(5) unsigned NOT NULL, + `musicNight` SMALLINT(5) unsigned NOT NULL, + KEY `id` (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +UPDATE `aowow_dbversion` SET `sql` = CONCAT(IFNULL(`sql`, ''), ' sounds'); diff --git a/setup/sql/updates/1702576294_01.sql b/setup/sql/updates/1702576294_01.sql new file mode 100644 index 00000000..bef0e1a9 --- /dev/null +++ b/setup/sql/updates/1702576294_01.sql @@ -0,0 +1 @@ +UPDATE `aowow_dbversion` SET `sql` = CONCAT(IFNULL(`sql`, ''), ' source'); diff --git a/setup/sql/updates/1709133536_01.sql b/setup/sql/updates/1709133536_01.sql new file mode 100644 index 00000000..4df33585 --- /dev/null +++ b/setup/sql/updates/1709133536_01.sql @@ -0,0 +1,14 @@ +DELETE FROM `aowow_setup_custom_data` WHERE `command` = 'classes' AND `field` = 'roles'; +INSERT INTO `aowow_setup_custom_data` VALUES + ('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'); + +UPDATE `aowow_dbversion` SET `sql` = CONCAT(IFNULL(`sql`, ''), ' classes'); diff --git a/setup/sql/updates/1709143189_01.sql b/setup/sql/updates/1709143189_01.sql new file mode 100644 index 00000000..bef0e1a9 --- /dev/null +++ b/setup/sql/updates/1709143189_01.sql @@ -0,0 +1 @@ +UPDATE `aowow_dbversion` SET `sql` = CONCAT(IFNULL(`sql`, ''), ' source'); diff --git a/setup/sql/updates/1709154964_01.sql b/setup/sql/updates/1709154964_01.sql new file mode 100644 index 00000000..a75d1bfa --- /dev/null +++ b/setup/sql/updates/1709154964_01.sql @@ -0,0 +1 @@ +UPDATE `aowow_dbversion` SET `sql` = CONCAT(IFNULL(`sql`, ''), ' zones'); diff --git a/setup/sql/updates/1709226658_01.sql b/setup/sql/updates/1709226658_01.sql new file mode 100644 index 00000000..0ef56225 --- /dev/null +++ b/setup/sql/updates/1709226658_01.sql @@ -0,0 +1 @@ +UPDATE `aowow_dbversion` SET `build` = CONCAT(IFNULL(`build`, ''), ' power'); diff --git a/setup/sql/updates/1710542696_01.sql b/setup/sql/updates/1710542696_01.sql new file mode 100644 index 00000000..d6f6f21d --- /dev/null +++ b/setup/sql/updates/1710542696_01.sql @@ -0,0 +1 @@ +UPDATE `aowow_dbversion` SET `sql` = CONCAT(IFNULL(`sql`, ''), ' objects'); diff --git a/setup/sql/updates/1711739612_01.sql b/setup/sql/updates/1711739612_01.sql new file mode 100644 index 00000000..1a319ead --- /dev/null +++ b/setup/sql/updates/1711739612_01.sql @@ -0,0 +1,11 @@ +DELETE FROM aowow_articles WHERE `type` = 13 AND `locale` = 3 AND `typeId` IN (1,2,3,4,5,6,7,8,9,11); +INSERT INTO aowow_articles (`type`, `typeId`, `locale`, `article`) VALUES (13, 1, 3, '[b][color=c1]Krieger[/color][/b] sind eine sehr mächtige Klasse, die sowohl tanken als auch im Nahkampf erheblichen Schaden anrichten kann. Der [icon name=ability_warrior_defensivestance][url=?spells=7.1.257]Schutz[/url][/icon]-Talentbaum des Kriegers enthält viele Talente, um seine Überlebensfähigkeit zu verbessern und Bedrohung gegenüber Monstern zu erzeugen. Schutz-Krieger sind eine der wichtigsten Tank-Klassen des Spiels.\n\nAußerdem verfügen Krieger über zwei schadensorientierte Talentbäume - [icon name=ability_rogue_eviscerate][url=?spells=7.1.26]Waffen[/url][/icon] und [icon name=ability_warrior_innerrage][url=?spells=7.1.256]Furor[/url][/icon]. Der Furor-Talentbaum enthält das Talent [spell=46917], das es dem Krieger erlaubt, zwei Zweihandwaffen gleichzeitig zu führen! Krieger sind in der Lage, mit Fähigkeiten wie [spell=845], [spell=1680] und [spell=46924] starken Flächenschaden im Nahkampf zu verursachen. Ein Krieger kämpft in einer bestimmten [i]Haltung[/i], die ihm Boni und Zugang zu verschiedenen Fähigkeiten gewährt. Zu Beginn verfügen Krieger nur über die [spell=2457], erlernen aber mit Level 10 [spell=71] und mit Level 30 [spell=2458]. Die Verteidigungshaltung wird zum Tanken, die Kampfhaltung oder Berserkerhaltung für erheblichen Nahkampfschaden verwendet.\n\n[ul][li]Alle Krieger können ihren Schlachtzug oder ihre Gruppe mit einem [spell=6673] oder [spell=469] verstärken. Furor-Krieger besitzen den passiven Stärkungszauber [spell=29801], der die Chance auf kritische Treffer im Nah- und Fernkampf für ihre Verbündeten deutlich erhöht.[/li][li]Krieger haben zahlreiche nützliche Fähigkeiten, um schnell an ihr Ziel zu gelangen! Alle Krieger können [spell=100] oder [spell=20252] benutzen, um einen Gegner zu erreichen. Zudem können sie schnell [spell=3411], um ein befreundetes Ziel vor einem Angriff zu schützen.[/li][/ul]'); +INSERT INTO aowow_articles (`type`, `typeId`, `locale`, `article`) VALUES (13, 2, 3, '[b][color=c2]Paladine[/color][/b] unterstützen ihre Verbündeten mit heiligen Auren und Segen, um sie vor Schaden zu bewahren und ihre Kräfte zu stärken. Sie tragen Plattenrüstungen und können in den härtesten Schlachten verheerenden Schlägen standhalten, während sie ihre Verwundeten heilen und die Gefallenen wiederbeleben. Im Kampf können sie mächtige Zweihandwaffen führen, ihre Feinde betäuben, Untote und Dämonen vernichten und ihre Feinde mit heiliger Vergeltung richten. Paladine sind eine defensive Klasse, die in erster Linie darauf ausgelegt ist, ihre Gegner zu überdauern.\n\nDer Paladin ist hauptsächlich ein Nahkämpfer und in geringem Maße Zauberer, der aufgrund seiner [url=?spells=7.2&filter=cr=109:12;crs=10:1;crv=0:0]Heilzauber[/url], [url=?spells=7.2&filter=na=Segen]Segen[/url] und anderen Fähigkeiten sehr nützlich für die Gruppe ist. Sie können eine aktive [url=?spells=7.2&filter=na=Aura]Aura[/url] pro Paladin auf alle Gruppen- und Schlachtzugsmitglieder legen und bestimmte Segen für bestimmte Spieler verwenden. Dank ihrer zahlreichen defensiven Fähigkeiten vergessen Paladine einfach unglaublich oft zu sterben. Mit ihrer Fähigkeit [spell=25780] sind sie außerdem ausgezeichnete Tanks.\n\n[ul][li]Paladine können effektiv [icon name=spell_holy_holybolt][url=?spells=7.2.594]heilen[/url][/icon], [icon name=spell_holy_devotionaura][url=?spells=7.2.267]tanken[/url][/icon] und im Nahkampf [icon name=spell_holy_auraoflight][url=?spells=7.2.184]Schaden[/url][/icon] verursachen.[/li][li]Sie besitzen eine große Auswahl an Segen, Auren und anderen Verstärkungszaubern.[/li][li]Der Paladin ist die einzige Klasse mit Zugang zu einem echten Unverwundbarkeitszauber: [spell=642].[/li][/ul]'); +INSERT INTO aowow_articles (`type`, `typeId`, `locale`, `article`) VALUES (13, 3, 3, '[b][color=c3]Jäger[/color][/b] sind eine besonders einzigartige Klasse in World of Warcraft. Sie sind die einzigen nicht-magischen Fernkämpfer, die mit Bögen und Gewehren kämpfen. Jäger verfügen über verschiedene Arten von Schüssen und Bissen zur Schwächung ihrer Gegner und können [url=?spells=7.3&filter=cr=4;crs=1;crv=0;na=Falle]Fallen[/url] legen, um Schaden zu verursachen oder den Gegner auf andere Weise zu verlangsamen oder kampfunfähig zu machen.\n\nJäger [icon name=ability_hunter_beasttaming][url=?spell=1515]zähmen Wildtiere[/url][/icon], damit diese sie als [url=?pets]Begleiter[/url] im Kampf unterstützen. Zwar sind Jäger nicht die einzige Klasse, die Begleiter einsetzen kann. Ihre Tierbegleiter sind aber insofern einzigartig, als jede Spezies einen [url=?petcalc]eigenen Talentbaum[/url] hat, den der Jäger nutzen kann, um Punkte auf verschiedene Fähigkeiten zu verteilen.\n\nDarüber hinaus hat jede Spezies eine einzigartige Spezialfähigkeit. Jäger können sich die begehrtesten Begleiter aufgrund ihres Aussehens oder ihrer Fähigkeiten aussuchen. Und wenn sie genug Talentpunkte in den Baum der [icon name=ability_hunter_beasttaming][url=?spells=7.3.50]Tierherrschaft[/url][/icon] investieren, können sie besondere, "exotische" Bestien zähmen, wie [url=?pet=46]Geisterbestien[/url] oder [url=?pet=39]Teufelssaurier[/url]!\n\n[ul][li]Jäger haben Zugriff auf 25 (32 als [icon name=ability_hunter_beastmastery][url=?spell=53270]Meister der Tierherrschaft[/url][/icon]) verschiedene Arten von Begleitern mit über 150 verschiedenen Erscheinungsbildern![/li][li]Jäger haben eine Reihe von überlebensorientierten Fähigkeiten, die sie einsetzen können, um potentiellen Gefahren zu entkommen oder ihnen auszuweichen, wie z.B. [spell=5384] und [spell=781].[/li][li]Auf das [icon name=ability_hunter_swiftstrike][url=?spells=7.3.51]Überleben[/url][/icon] spezialisierte Jäger können in ihrem Talentbaum Punkte in das Talent [icon name=ability_hunter_huntingparty][url=?spells=-2.3&filter=na=jagdgesellschaft rel=spell=53292]Jagdgesellschaft[/url][/icon] investieren, welches es ihnen ermöglicht, ihre Gruppen- und Schlachtzugsmitglieder mit dem Stärkungszauber [spell=57669] zu versorgen.[/li][/ul]'); +INSERT INTO aowow_articles (`type`, `typeId`, `locale`, `article`) VALUES (13, 4, 3, '[b][color=c4]Schurken[/color][/b] sind eine Nahkampfklasse, die Lederrüstungen trägt und ihren Feinden mit sehr schnellen Angriffen großen Schaden zufügen kann. Sie sind Meister der Verstohlenheit und des Meuchelns, die sich ungesehen an Feinden vorbeischleichen, aus den Schatten heraus zuschlagen und dann blitzschnell aus dem Kampf verschwinden.\n\nSie sind in der Lage, [url=?items=0.-3&filter=cr=152;crs=4;crv=0;ty=-3#0+1-2]Gifte[/url] einzusetzen, um ihre Gegner zu verkrüppeln und sie so im Kampf massiv zu schwächen. Schurken verfügen über ein mächtiges Arsenal an Fähigkeiten, von denen viele dadurch verstärkt werden, dass sie in [spell=1784] schleichen und ihre Opfer kampfunfähig machen können.\n\nSchurken können sich auf drei unterschiedliche Kampfstile mithilfe ihrer Talentbäume Meucheln, Kampf und Täuschung spezialisieren.\n\nAuf das [icon name=ability_rogue_eviscerate][url=?spells=7.4.253]Meucheln[/url][/icon] spezialisierte Schurken sind [icon name=ability_creature_poison_06][url=?spells=-2&filter=na=meister+der+gifte rel=spell=58410]Meister der Gifte[/url][/icon] und [icon name=ability_rogue_disembowel][url=?spell=57993]vergiften[/url][/icon] ihre Gegner mit schnellen Dolchen, die mit [icon name=ability_rogue_feigndeath][url=?spells=-2.4.253&filter=na=Üble+Gifte rel=spell=16515]üblen[/url][/icon] und [icon name=ability_poisons][url=?spells=-2.4.253&filter=na=Verbesserte+Gifte rel=spell=14117]verbesserten[/url][/icon] Giften versehen sind.\n\nAuf den [icon name=ability_backstab][url=?spells=7.4.38]Kampf[/url][/icon] spezialisierte Schurken können den Umgang mit [icon name=inv_sword_27][url=?spells=-2&filter=na=Niedermetzeln rel=spell=13964]Axt und Schwert[/url][/icon] oder [icon name=inv_mace_01][url=?spells=-2&filter=na=Streitkolben-Spezialisierung;cl=4 rel=spell=13803]Streitkolben[/url][/icon] meistern und haben mithilfe ihrer Talente auch in langwierigen Kämpfen eine verbesserte Energiezufuhr, um zuverlässig ihre Angriffscombos durchzuführen.\n\nAuf die [icon name=ability_stealth][url=?spells=7.4.39]Täuschung[/url][/icon] spezialisierte Schurken besitzen Fähigkeiten, die unvorhergesehene Aktionen ermöglichen. So können sie dank [spell=51713] etwa kurzzeitig Fähigkeiten nutzen, die eigentlich nur aus der Verstohlenheit heraus nutzbar wären, mit [spell=36554] plötzlich hinter einem Gegner auftauchen oder mit [icon name=ability_rogue_cheatdeath][url=?spells=-2.4.39&filter=cr=15;crs=0;crv=ability_rogue_cheatdeath rel=spell=31230]Von der Schippe springen[/url][/icon] einen sicheren Todesstoß überleben.'); +INSERT INTO aowow_articles (`type`, `typeId`, `locale`, `article`) VALUES (13, 5, 3, '[b][color=c5]Priester[/color][/b] gelten allgemein als eine der Standard-Heilerklassen in World of Warcraft, da sie über zwei Talentspezialisierungen zur effektiven Heilung verfügen.\n\nIhr [icon name=spell_holy_holybolt][url=?spells=7.5.56]Heilig[/url][/icon]-Talentbaum enthält Talente, die die Heilung auf ihre Verbündeten erheblich verstärken - einschließlich Zaubern, mit denen mehrere Spieler gleichzeitig geheilt werden können, wie z.B. [spell=48089].\nDer [icon name=spell_holy_wordfortitude][url=?spells=7.5.613]Disziplin[/url][/icon]-Talentbaum ist zwar auch in der Lage, eine beträchtliche Menge an Heilung zu bewirken, konzentriert sich aber in erster Linie auf die Schadensabsorption und -verminderung durch den Einsatz von [spell=48066] und [icon name=spell_holy_devineaegis][url=?spells=-2.5&filter=cr=15;crs=0;crv=spell_holy_devineaegis rel=spell=47515]Göttliche Aegis[/url][/icon].\nPriester können außerdem mit ihren [icon name=spell_shadow_shadowwordpain][url=?spells=7.5.78]Schatten[/url][/icon]fähigkeiten sehr mächtigen Fernkampfschaden verursachen. Insbesondere wenn sie ihre [spell=15473] annehmen, erhöht sich ihr Schattenschaden erheblich, aber sie verlieren ihre Fähigkeit, Heiligzauber zu wirken.\n\n[ul][li]Der Disziplin-Talentbaum wird in der Regel zur Heilung verwendet, enthält aber auch einige Talente zur Erhöhung des Schadens des Priesters, wobei Schattenzauber und -fähigkeiten in erster Linie zur Verursachung von Fernkampfschaden verwendet werden sollten.[/li][li]Priester verfügen über einen der am meisten geschätzten Stärkungszauber im Spiel - [spell=48161], welcher allen befreundeten Mitspielern eine unverzichtbare Erhöhung ihrer Ausdauer gewährt. Außerdem können sie ihre Mitspieler mit [spell=48073] und [spell=48169] verstärken und mit einzigartigen Hymnen das [icon name=spell_holy_divinehymn][url=?spell=64843]Leben[/url][/icon] und [icon name=spell_holy_symbolofhope][url=?spell=64901]Mana[/url][/icon] ihres Schlachtzug signifikant wiederherstellen![/li][li]Schattenpriester unterstützen, zusätzlich zu ihrem Schaden, jeden Schlachtzug mit dem beliebten Stärkungszauber [spell=57669] zur Erhöhung der Manaregeneration und mit ihrer [spell=15286], die ihre gesamte Gruppe passiv heilt.[/li][/ul]'); +INSERT INTO aowow_articles (`type`, `typeId`, `locale`, `article`) VALUES (13, 6, 3, 'Die [b][color=c6]Todesritter[/color][/b] wurden in der Erweiterung Wrath of the Lich King eingeführt und sind die erste Heldenklasse von World of Warcraft. Todesritter beginnen auf Stufe 55 in einer speziellen, instanzierten Zone, die für andere Klassen unzugänglich ist: [url=?maps=4298:511346]Acherus, die Schwarze Festung[/url] in der Scharlachroten Enklave der Östlichen Pestländer. Hier erhalten sie ihre Talentpunkte durch Questbelohnungen und bekommen sogar ein besonderes beschworenes Reittier: das [spell=48778]!\n\nTodesritter haben mehrere sehr starke Möglichkeiten zur Schadensverursachung, da jeder ihrer Talentbäume es erlaubt mit einer Vielfalt an Nahkampffähigkeiten, Zaubern und Schaden-über-Zeit verursachenden Krankheiten überragende Leistung zu erbringen. Sie sind auch sehr fähige Tanks, wobei sowohl ihr Blut- als auch ihr Frost-Talentbaum einzigartige Optionen bietet. [icon name=spell_deathknight_bloodpresence][url=?spells=7.6.770]Blut[/url][/icon] bietet mehr Selbstheilungsfähigkeiten, [icon name=spell_deathknight_frostpresence][url=?spells=7.6.771]Frost[/url][/icon] bietet erhebliche Schadensminderung und starken Flächenschaden.\n\nTodesritter kämpfen mit einem besonderen Verstärkungszauber, der [url=?spells=7&filter=na=präsenz]Präsenz[/url] genannt wird (ähnlich wie die Haltungen eines Kriegers), der ihnen besondere Boni für ihre Rollen verleiht. Todesritter verwenden ein einzigartiges Ressourcensystem, bei dem die meisten Zauber entweder [url=?spells=7.6&filter=cr=45;crs=10;crv=0#50+1+13+3]Runen[/url] kosten, die während des Kampfes wieder aufgefüllt werden, oder [url=?spells=7.6&filter=cr=45;crs=11;crv=0]Runenmacht[/url], die durch verschiedene Fähigkeiten erzeugt werden kann.\n\n[ul][li]Auf [icon name=spell_deathknight_unholypresence][url=?spells=7.6.772]Unheilig[/url][/icon] spezialisierte Todesritter können sich in [spell=52143] spezialisieren, was ihren beschworenen Ghul-Wächter zu einem permanenten Begleiter macht, der sie im Kampf unterstützt![/li][li]Die Klasse der Todesritter verfügt über eine eigene spezielle Waffenverzauberungsfähigkeit namens [spell=53428], die herkömmliche Waffenverzauberungen überflüssig macht.[/li][li]Todesritter sind eine Schadensklasse, die ihren Schaden sowohl durch Nahkampffähigkeiten als auch durch Zauber verursacht![/li][/ul]'); +INSERT INTO aowow_articles (`type`, `typeId`, `locale`, `article`) VALUES (13, 7, 3, '[b][color=c7]Schamanen[/color][/b] beherrschen Elementar- und Naturmagie und bringen einer (Schlachtzugs-) Gruppe die größte Vielfalt an potenziellen Stärkungszaubern in Form von [url=?spells=7&filter=na=Totem;cl=7]Totems[/url]. Ein Schamane kann für jedes Element - Erde, Feuer, Luft und Wasser - ein Totem beschwören, welches zu seinen Füßen erscheint und allen Mitgliedern seiner (Schlachtzugs-) Gruppe in Reichweite einen Stärkungszauber verleiht. Einige Totems, insbesondere Feuer-Totems, fügen Gegnern auch Schaden zu. Der Trick beim Spielen jeder Art von Schamanen besteht darin, zu wissen, welche Totems in welcher Situation beschworen werden müssen, um den verursachten Schaden und die Überlebensfähigkeit ihrer Gruppe zu maximieren.\n\nSchamanen sind in erster Linie Zauberer, wobei ein auf [icon name=spell_nature_lightningshield][url=?spells=7.7.373]Verstärkung[/url][/icon] spezialisierter Schamane Schaden in Nahkampfreichweite verursacht. Ein solcher Schamane erlernt das Führen zweier Waffen durch [spell=30798] und kann mit [spell=51533] zwei Schattenwölfe zur Unterstützung im Kampf beschwören. Obwohl sie hauptsächlich im Nahkampf eingesetzt werden, können auf Verstärkung spezialisierte Schamanen dennoch einen gewissen Nutzen aus ihrer Zaubermacht ziehen und spontane [icon name=spell_nature_lightning][url=?spell=49238]Blitzschläge[/url][/icon] oder [url=?spells=7&filter=cr=109:12:14;crs=10:1:5;crv=0:0:60000;cl=7]Heilungen[/url] durch [icon name=spell_shaman_maelstromweapon][url=?spells=-2&filter=na=waffe+des+mahlstroms rel=spell=51532]Waffe des Mahlstroms[/url][/icon] wirken.\n\nAuf [icon name=spell_nature_lightning][url=?spells=7.7.375]Elementarkampf[/url][/icon] spezialisierte Schamanen wirken Feuer- und Blitzzauber auf Distanz und verursachen so großen Schaden. Sie können Gegner durch [spell=59159] zurückstoßen und mit [icon name=spell_shaman_stormearthfire][url=?spells=-2&filter=na=sturm%2C+erde+und+feuer rel=spell=51486]Sturm, Erde und Feuer[/url][/icon] alle Feinde in einem Gebiet festwurzeln. Außerdem gewähren sie durch [spell=57722] und [icon name=spell_shaman_elementaloath][url=?spells=-2&filter=na=Elementarer+Schwur rel=spell=51470]Elementarer Schwur[/url][/icon] begehrte Stärkungszauber für Zauberer ihres Schlachtzugs.\n\nEin auf [icon name=spell_nature_magicimmunity][url=?spells=7.7.374]Wiederherstellung[/url][/icon] spezialisierter Schamane erhält verbesserte Heilzauber und kann ein ausgezeichneter Schlachtzugs- oder Tankheiler sein. Sie sind bekannt für ihre mächtige Fähigkeit [spell=55459] und dafür, dass sie ein [spell=16190] zur Verfügung stellen, welches der Gruppe hilft Mana wiederherzustellen. Sie erhalten außerdem ein mächtiges [spell=49284], können mit [spell=51886] Flüche entfernen und verfügen durch [spell=61301] über einen Spontanheilungseffekt, der zusätzlich eine Heilung über Zeit verursacht.\n\n[ul][li]Es gibt über zwanzig verschiedene Totems, die ein Schamane erlernen kann![/li][li]Schamanen der Horde können [spell=2825] und Schamanen der Allianz [spell=32182] wirken, wodurch der verursachte Schaden und die gewirkte Heilung der gesamten Gruppe erhöht wird. Dieser Stärkungszauber ist einzigartig und in jeder Schlachtzugsgruppe sehr begehrt.[/li][li]Ein Schamane kann sich ab Stufe 16 in einen [spell=2645] verwandeln und dies mit dem Talent [spell=16287] sogar als Spontanzauber wirken. Dieser Zauber kann im Kampf eingesetzt werden, aber nicht in geschlossenen Räumen.[/li][li]Schamanen können immer nur einen Elementarschild - [spell=49281] oder [spell=57960] - gleichzeitig benutzen. Auf Wiederherstellung spezialisierte Schamanen können zudem [spell=49284] auf einen anderen Spieler wirken.[/li][/ul]'); +INSERT INTO aowow_articles (`type`, `typeId`, `locale`, `article`) VALUES (13, 8, 3, '[b][color=c8]Magier[/color][/b] bändigen die Elemente Feuer, Frost und Arkan, um ihre Feinde zu vernichten oder unter Kontrolle zu halten. Dazu besitzen sie ein Arsenal voller Zauber zu unterschiedlichen Zwecken.\nStärkungszauber, [icon name=ability_mage_conjurefoodrank10][url=?spell=42956]herbeigezauberte Erfrischungen[/url][/icon] oder arkane [url=?spells=7&filter=na=Portal]Portale[/url] zur schnellen Weltreise in ferne Länder machen einen Magier zu einem idealen Weggefährten.\nUnd wenn man eine Klasse sucht, die Gegner in eine Welt des Schmerzes einführt, ist der Magier eine gute Wahl. Ihren Gegnern können Magier mit verschiedensten Schwächungszaubern die Bedingungen eines jeden Kampfes diktieren, mit Elementarblitzen massiven Schaden aus der Ferne anrichten, oder Zerstörung in einem großen Wirkungsbereich niederregnen lassen.\n\nAuf [icon name=spell_holy_magicalsentry][url=?spells=7.8.237]Arkan[/url][/icon] spezialisierte Magier haben das Potenzial, mit [icon name=spell_arcane_blast][url=?spell=42897]Arkanschlägen[/url][/icon] und [icon name=ability_mage_missilebarrage][url=?spells=-2&filter=na=Geschosssalve rel=spell=54490]Salven[/url][/icon] an [icon name=spell_nature_starfall][url=?spell=42846]Arkanen Geschossen[/url][/icon] in kurzer Zeit enormen Schaden zu verursachen. Das Bändigen der reinen arkanen Mächte hat jedoch ihre Kehrseite: einem unerfahrenen Arkanmagier verzehrt es schon nach kurzer Zeit seine gesamten Kräfte.\n\nAuf [icon name=spell_fire_flamebolt][url=?spells=7.8.8]Feuer[/url][/icon] spezialisierte Magier verfallen durch kritische Treffer mit Feuerzaubern in [icon name=ability_mage_hotstreak][url=?spells=-2&filter=na=Kampfeshitze rel=spell=44448]Kampfeshitze[/url][/icon] und äschern so ihre Gegner mit verheerenden [icon name=spell_fire_fireball02][url=?spell=42891]Pyroschlägen[/url][/icon] ein. Zudem verwandeln sie ihre Gegner in [icon name=ability_mage_livingbomb][url=?spell=55360]Lebende Bomben[/url][/icon] und verursachen dadurch explosiven Flächenschaden.\n\n[icon name=spell_frost_frostbolt02][url=?spells=7.8.6]Frost[/url][/icon]magier können ihre Gegner [icon name=ability_mage_deepfreeze][url=?spell=44572]in Eis erstarren[/url][/icon] lassen. Ihre Spezialisierung auf Kälteeffekte erlaubt ihnen eine starke Kontrolle über ihre Gegner und erhöht dadurch ihre Überlebensfähigkeit enorm.\n\n[ul][li]Magier können Erfrischungen herbeizaubern, um die Gesundheit und das Mana ihrer Verbündeten wiederherzustellen.[/li][li]Sie sind die einzige Klasse, die Portale erschaffen kann, um andere Spieler zu transportieren. Sie können jedoch keine Spieler von einem entfernten Ort herbeirufen - das ist die Aufgabe eines [class=9]![/li][li]Der verursachte Fernkampfschaden von Magiern ist einer der höchsten im Spiel und macht sie zu einem unverzichtbaren Verbündeten in jedem Schlachtzug.[/li][/ul]'); +INSERT INTO aowow_articles (`type`, `typeId`, `locale`, `article`) VALUES (13, 9, 3, '[b][color=c9]Hexenmeister[/color][/b] sind Meister der dämonischen Künste. Gekleidet in Gewänder sind sie Meister im Wirken von [url=?spells=7&filter=cr=12;crs=1;crv=0;na=Fluch+der;cl=9]Flüchen[/url], dem Schleudern von Feuer- oder Schattenblitzen und der Beschwörung von [url=?spells=7&filter=cr=14;crs=6;crv=48018;na=beschwören;cl=9]Dämonen[/url] unter ihre Kontrolle zur Unterstützung im Kampf. Die Kombination ihrer Flüche und direkten Schadenszauber richten Verwüstung und Zerstörung an und machen Hexenmeister zu sehr gefürchteten Gegnern.\n\nNeben Mana als primäre Ressource können Hexenmeister Gegnern Teile ihrer [icon name=spell_shadow_haunting][url=?spell=47855]Seele stehlen[/url][/icon] und dadurch [item=6265] erzeugen. Seelensplitter ermöglichen mächtige rituelle Magie, etwa zur [icon name=spell_shadow_twilight][url=?spell=698]Beschwörung von anderen Spielern[/url][/icon] oder von [icon name=spell_shadow_shadesofdarkness][url=?spell=58887]Gesundheitssteinen[/url][/icon] mit heilenden Kräften. Insbesondere kann jedoch ein Hexenmeister mit ihnen die Seele eines Verbündeten in einem [icon name=spell_shadow_soulgem][url=?spell=47884]Seelenstein[/url][/icon] speichern, sodass dieser im Todesfall sich selbst wiederbeleben kann.\n\n[ul][li]Hexenmeister können durch ein Ritual der Beschwörung ein Portal erschaffen, um einen anderen Spieler an den Ort des Portals zu beschwören.[/li][li]Sie können Gesundheitssteine beschwören, die den Anwender heilen.[/li][li]Die Flüche eines Hexenmeisters können ihre Feinde schwächen oder ihnen Schaden zufügen.[/li][/ul]'); +INSERT INTO aowow_articles (`type`, `typeId`, `locale`, `article`) VALUES (13, 11, 3, '[b][color=c11]Druiden[/color][/b] sind die "Alleskönner"-Klasse in World of Warcraft - das heißt, sie können in einer Vielzahl von verschiedenen Rollen agieren und bieten daher einen der vielfältigsten Spielstile. Durch das [i]Annehmen der Gestalt von verschiedenen Kreaturen[/i] kann der Druide heilen, Schaden im Nah- und Fernkampf verursachen oder als Tank agieren. Mit steigenden Stufen kann der Druide neue, immer mächtigere Gestaltwandlungen erlernen, um sich in eine Kreatur passend zu seiner Rolle zu verwandeln.\n\nAuf niedrigeren Stufen wird ein Druide in seiner humanoiden Gestalt heilen oder im Fernkampf Schaden verursachen. Auf späteren Stufen jedoch erhalten Druiden durch die spezialisierten Talentbäume Zugang zu zwei besonderen Gestalten für jede unterschiedliche Rolle.\n\nAuf [icon name=spell_nature_healingtouch][url=?spells=7.11.573]Wiederherstellung[/url][/icon] spezialisierte Druiden erlernen den [spell=33891], der die Manakosten ihrer Heilzauber reduziert und jegliche Heilung auf ihre Verbündeten verstärkt.\nAuf [icon name=spell_nature_starfall][url=?spells=7.11.574]Gleichgewicht[/url][/icon] spezialisierte Druiden verursachen Schaden im Fernkampf und erlernen die [spell=24858], die ihre Rüstung sowie die Chance auf kritische Treffer mit Zaubern bei ihnen und ihren Verbündeten erhöht.\nEs gibt auch zwei Druidenformen für den [icon name=ability_racial_bearform][url=?spells=7.11.134]Wilden Kampf[/url][/icon]. Zum einen die mächtige [spell=5487] (und [spell=9634] ab einer höheren Stufe) - eine auf das Tanken ausgelegte Gestalt, die zusätzliche Rüstung, Gesundheit und Zugang zu einem Arsenal von Fähigkeiten zur Erhöhung der Bedrohung und Schadensverminderung gewährt. Zum anderen die schurkenähnliche [spell=768], die erheblichen Nahkampfschaden verursachen kann.\n\n[ul][li]Druiden erlernen ihre verschiedenen Gestalten durch das Abschließen von Quests oder durch Training. Einige Gestalten können nur durch Talente erlernt werden.[/li][li]Es gibt einige Gestalten, die alle Druiden erlernen können. Die Bärengestalt erhält man ab Stufe 10, die [spell=1066] und [spell=783] ab Stufe 16, die Katzengestalt ab Stufe 20 und die Terrorbärengestalt ab Stufe 40.[/li][li]Druiden haben sogar ihre eigene fliegende Reisegestalt: die [spell=33943] kann ab Stufe 60 und die [spell=40120] ab Stufe 71 erlernt werden, sofern der Spieler bereits [icon name=spell_nature_swiftness][url=?spell=34093]Gekonntes Reiten[/url][/icon] erlernt hat.[/li][li]Einige Druidengestalten können nur über Talente erlernt werden - die Mondkingestalt kann ab Stufe 40 erlernt werden, wenn ein Spieler viele Talentpunkte im Gleichgewicht-Talentbaum verteilt, und Baum des Lebens ab Stufe 50, wenn er viele Talentpunkte im Wiederherstellung-Talentbaum verteilt.[/li][li]Druiden haben ihre eigene, klassenspezifische [icon name=spell_arcane_teleportmoonglade][url=?spell=18960]Teleportationsfähigkeit[/url][/icon], die es ihnen erlaubt, zur [zone=493] zu reisen - praktisch, wenn sie trainieren müssen![/li][li]Druiden in (Terror-) Bärengestalt oder Katzengestalt schwingen zur Verursachung von Nahkampfschaden keine Waffen. Stattdessen erhalten sie einen speziellen Wert für jede ausgerüstete Nahkampfwaffe: die "Angriffskraft in Tiergestalt". Dieser Wert ist eine Umwandlung des "Schaden pro Sekunde"-Wertes einer Waffe in einen Wert, der Angriffskraft verleiht und den verursachten Schaden des Druiden in Katzen- oder (Terror-) Bärengestalt beeinflusst.[/li][/ul]'); diff --git a/setup/sql/updates/1713730806_01.sql b/setup/sql/updates/1713730806_01.sql new file mode 100644 index 00000000..00def1c1 --- /dev/null +++ b/setup/sql/updates/1713730806_01.sql @@ -0,0 +1,24 @@ +UPDATE aowow_dbversion SET `sql` = REPLACE(`sql`, 'searchplugin', ''); +UPDATE aowow_dbversion SET `sql` = REPLACE(`sql`, 'power', ''); +UPDATE aowow_dbversion SET `sql` = REPLACE(`sql`, 'searchboxScript', ''); +UPDATE aowow_dbversion SET `sql` = REPLACE(`sql`, 'demo', ''); +UPDATE aowow_dbversion SET `sql` = REPLACE(`sql`, 'searchboxBody', ''); +UPDATE aowow_dbversion SET `sql` = REPLACE(`sql`, 'realmMenu', ''); +UPDATE aowow_dbversion SET `sql` = REPLACE(`sql`, 'locales', ''); +UPDATE aowow_dbversion SET `sql` = REPLACE(`sql`, 'markup', ''); +UPDATE aowow_dbversion SET `sql` = REPLACE(`sql`, 'itemScaling', ''); +UPDATE aowow_dbversion SET `sql` = REPLACE(`sql`, 'realms', ''); +UPDATE aowow_dbversion SET `sql` = REPLACE(`sql`, 'statistics', ''); +UPDATE aowow_dbversion SET `sql` = REPLACE(`sql`, 'simpleImg', ''); +UPDATE aowow_dbversion SET `sql` = REPLACE(`sql`, 'complexImg', ''); +UPDATE aowow_dbversion SET `sql` = REPLACE(`sql`, 'talentCalc', ''); +UPDATE aowow_dbversion SET `sql` = REPLACE(`sql`, 'pets', ''); +UPDATE aowow_dbversion SET `sql` = REPLACE(`sql`, 'talentIcons', ''); +UPDATE aowow_dbversion SET `sql` = REPLACE(`sql`, 'glyphs', ''); +UPDATE aowow_dbversion SET `sql` = REPLACE(`sql`, 'itemsets', ''); +UPDATE aowow_dbversion SET `sql` = REPLACE(`sql`, 'enchants', ''); +UPDATE aowow_dbversion SET `sql` = REPLACE(`sql`, 'gems', ''); +UPDATE aowow_dbversion SET `sql` = REPLACE(`sql`, 'profiler', ''); +UPDATE aowow_dbversion SET `sql` = REPLACE(`sql`, 'weightPresets', ''); +UPDATE aowow_dbversion SET `sql` = REPLACE(`sql`, 'soundfiles', ''); + diff --git a/setup/sql/updates/1716305876_01.sql b/setup/sql/updates/1716305876_01.sql new file mode 100644 index 00000000..434c71b5 --- /dev/null +++ b/setup/sql/updates/1716305876_01.sql @@ -0,0 +1,7 @@ +SET foreign_key_checks = 0; + +ALTER TABLE `aowow_account_weightscales` + DROP PRIMARY KEY, + ADD PRIMARY KEY (`id`); + +SET foreign_key_checks = 1; diff --git a/setup/sql/updates/1716918678_01.sql b/setup/sql/updates/1716918678_01.sql new file mode 100644 index 00000000..8ffa2635 --- /dev/null +++ b/setup/sql/updates/1716918678_01.sql @@ -0,0 +1,44 @@ +-- undo sunken temple data +DELETE FROM aowow_setup_custom_data WHERE `command` = 'zones' AND `entry` IN (1477, 1417); +-- undo icc unused subzone linking (still has EXCLUDE_FOR_LISTVIEW set) +DELETE FROM aowow_setup_custom_data WHERE `command` = 'zones' AND `field` = 'parentAreaId' AND `value` = 4812; +-- undo Hellfire Citadel recategorization +DELETE FROM aowow_setup_custom_data WHERE `command` = 'quests' AND `field` = 'zoneOrSort' AND `entry` IN (9572, 9575, 11354, 9589, 9590, 9607, 9608, 11362, 9492, 9493, 9494, 9495, 9496, 9497, 9524, 9525, 11363, 11364); +INSERT INTO aowow_setup_custom_data VALUES + ('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'), + -- move quests from generic Hellfire Citadel to... + -- ...Hellfire Ramparts [3562] + ('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"), + -- ...The Blood Furnace [3713] + ('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"), + -- ...The Shattered Halls [3714] + ('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'); + +-- implement SpawnedByDefault +ALTER TABLE aowow_spawns + MODIFY COLUMN `respawn` int signed NOT NULL DEFAULT 0; + +-- rebuild spawns +UPDATE aowow_dbversion + SET `sql` = CONCAT(IFNULL(`sql`, ''), ' spawns quests'); diff --git a/setup/sql/updates/1717076299_01.sql b/setup/sql/updates/1717076299_01.sql new file mode 100644 index 00000000..b272f9de --- /dev/null +++ b/setup/sql/updates/1717076299_01.sql @@ -0,0 +1,72 @@ +ALTER TABLE aowow_config + MODIFY COLUMN `cat` tinyint unsigned NOT NULL DEFAULT 0, + MODIFY COLUMN `flags` smallint unsigned NOT NULL DEFAULT 0, + ADD COLUMN `default` varchar(255) DEFAULT NULL AFTER `value`; + +INSERT IGNORE INTO aowow_config VALUES + ('rep_req_border_unco', 5000, 5000, 5, 129, 'required reputation for uncommon quality avatar border'), + ('rep_req_border_rare', 10000, 10000, 5, 129, 'required reputation for rare quality avatar border'), + ('rep_req_border_epic', 15000, 15000, 5, 129, 'required reputation for epic quality avatar border'), + ('rep_req_border_lege', 25000, 25000, 5, 129, 'required reputation for legendary quality avatar border'); + +UPDATE aowow_config SET `default` = 'UTF-8' WHERE `key` = 'default_charset'; +UPDATE aowow_config SET `comment` = 'website title' WHERE `key` = 'name'; +UPDATE aowow_config SET `comment` = 'feed title' WHERE `key` = 'name_short'; +UPDATE aowow_config SET `comment` = 'another halfbaked javascript thing..' WHERE `key` = 'board_url'; +UPDATE aowow_config SET `comment` = 'displayed sender for auth-mails, ect' WHERE `key` = 'contact_email'; +UPDATE aowow_config SET `comment` = 'pretend, we belong to a battlegroup to satisfy profiler-related javascripts' WHERE `key` = 'battlegroup'; +UPDATE aowow_config SET `comment` = 'points js to executable files', `flags` = `flags` | 768 WHERE `key` = 'site_host'; +UPDATE aowow_config SET `comment` = 'points js to images & scripts', `flags` = `flags` | 768 WHERE `key` = 'static_host'; +UPDATE aowow_config SET `comment` = 'some derelict code, probably unused' WHERE `key` = 'serialize_precision'; +UPDATE aowow_config SET `comment` = 'enter your GA-user here to track site stats' WHERE `key` = 'analytics_user'; +UPDATE aowow_config SET `comment` = 'if auth mode is not self; link to external account creation' WHERE `key` = 'acc_ext_create_url'; +UPDATE aowow_config SET `comment` = 'if auth mode is not self; link to external account recovery' WHERE `key` = 'acc_ext_recover_url'; +UPDATE aowow_config SET `comment` = 'php sessions are saved here. Leave empty to use php default directory.' WHERE `key` = 'session_cache_dir'; +UPDATE aowow_config SET `comment` = 'max results for search', `default` = '500' WHERE `key` = 'sql_limit_search'; +UPDATE aowow_config SET `comment` = 'max results for listviews', `default` = '300' WHERE `key` = 'sql_limit_default'; +UPDATE aowow_config SET `comment` = 'max results for suggestions', `default` = '10' WHERE `key` = 'sql_limit_quicksearch'; +UPDATE aowow_config SET `comment` = 'unlimited results (i wouldn\'t change that mate)', `default` = '0' WHERE `key` = 'sql_limit_none'; +UPDATE aowow_config SET `comment` = 'time to live for RSS (in seconds)', `default` = '60' WHERE `key` = 'ttl_rss'; +UPDATE aowow_config SET `comment` = 'disable cache, enable error_reporting - 0:None, 1:Error, 2:Warning, 3:Info', `default` = '0', `flags` = 145 WHERE `key` = 'debug'; +UPDATE aowow_config SET `comment` = 'display brb gnomes and block access for non-staff', `default` = '0' WHERE `key` = 'maintenance'; +UPDATE aowow_config SET `comment` = 'vote limit per day', `default` = '50' WHERE `key` = 'user_max_votes'; +UPDATE aowow_config SET `comment` = 'enforce SSL, if auto-detect fails', `default` = '0' WHERE `key` = 'force_ssl'; +UPDATE aowow_config SET `comment` = 'allowed locales - 0:English, 2:French, 3:German, 4:Chinese, 6:Spanish, 8:Russian', `default` = '0x15D' WHERE `key` = 'locales'; +UPDATE aowow_config SET `comment` = 'minimum dimensions of uploaded screenshots in px (yes, it\'s square)', `default` = '200' WHERE `key` = 'screenshot_min_size'; +UPDATE aowow_config SET `comment` = 'time to keep cache in seconds', `default` = '60 * 60 * 7' WHERE `key` = 'cache_decay'; +UPDATE aowow_config SET `comment` = 'set cache method - 0:filecache, 1:memcached', `default` = '1' WHERE `key` = 'cache_mode'; +UPDATE aowow_config SET `comment` = 'generated pages are saved here (requires CACHE_MODE: filecache)', `default` = 'cache/template' WHERE `key` = 'cache_dir'; +UPDATE aowow_config SET `comment` = 'how long an account is closed after exceeding FAILED_AUTH_COUNT (in seconds)', `default` = '15 * 60' WHERE `key` = 'acc_failed_auth_block'; +UPDATE aowow_config SET `comment` = 'how often invalid passwords are tolerated', `default` = '5' WHERE `key` = 'acc_failed_auth_count'; +UPDATE aowow_config SET `comment` = 'allow/disallow account creation (requires AUTH_MODE: aowow)', `default` = '1' WHERE `key` = 'acc_allow_register'; +UPDATE aowow_config SET `comment` = 'source to auth against - 0:AoWoW, 1:TC auth-table, 2:External script (config/extAuth.php)', `default` = '0', `flags`= `flags`| 256 WHERE `key` = 'acc_auth_mode'; +UPDATE aowow_config SET `comment` = 'time in wich an unconfirmed account cannot be overwritten by new registrations', `default` = '604800' WHERE `key` = 'acc_create_save_decay'; +UPDATE aowow_config SET `comment` = 'time to recover your account and new recovery requests are blocked', `default` = '300' WHERE `key` = 'acc_recovery_decay'; +UPDATE aowow_config SET `comment` = 'non-permanent session times out in time() + X', `default` = '60 * 60' WHERE `key` = 'session_timeout_delay'; +UPDATE aowow_config SET `comment` = 'lifetime of session data', `default` = '7 * 24 * 60 * 60' WHERE `key` = 'session.gc_maxlifetime'; +UPDATE aowow_config SET `comment` = 'probability to remove session data on garbage collection', `default` = '0' WHERE `key` = 'session.gc_probability'; +UPDATE aowow_config SET `comment` = 'probability to remove session data on garbage collection', `default` = '100' WHERE `key` = 'session.gc_divisor'; +UPDATE aowow_config SET `comment` = 'required reputation to upvote comments', `default` = '125' WHERE `key` = 'rep_req_upvote'; +UPDATE aowow_config SET `comment` = 'required reputation to downvote comments', `default` = '250' WHERE `key` = 'rep_req_downvote'; +UPDATE aowow_config SET `comment` = 'required reputation to write a comment', `default` = '75' WHERE `key` = 'rep_req_comment'; +UPDATE aowow_config SET `comment` = 'required reputation to write a reply', `default` = '75' WHERE `key` = 'rep_req_reply'; +UPDATE aowow_config SET `comment` = 'required reputation for double vote effect', `default` = '2500' WHERE `key` = 'rep_req_supervote'; +UPDATE aowow_config SET `comment` = 'gains more votes past this threshold', `default` = '2000' WHERE `key` = 'rep_req_votemore_base'; +UPDATE aowow_config SET `comment` = 'activated an account', `default` = '100' WHERE `key` = 'rep_reward_register'; +UPDATE aowow_config SET `comment` = 'comment received upvote', `default` = '5' WHERE `key` = 'rep_reward_upvoted'; +UPDATE aowow_config SET `comment` = 'comment received downvote', `default` = '0' WHERE `key` = 'rep_reward_downvoted'; +UPDATE aowow_config SET `comment` = 'filed an accepted report', `default` = '10' WHERE `key` = 'rep_reward_good_report'; +UPDATE aowow_config SET `comment` = 'filed a rejected report', `default` = '0' WHERE `key` = 'rep_reward_bad_report'; +UPDATE aowow_config SET `comment` = 'daily visit', `default` = '5' WHERE `key` = 'rep_reward_dailyvisit'; +UPDATE aowow_config SET `comment` = 'moderator imposed a warning', `default` = '-50' WHERE `key` = 'rep_reward_user_warned'; +UPDATE aowow_config SET `comment` = 'created a comment (not a reply)', `default` = '1' WHERE `key` = 'rep_reward_comment'; +UPDATE aowow_config SET `comment` = 'required reputation for premium status through reputation', `default` = '25000' WHERE `key` = 'rep_req_premium'; +UPDATE aowow_config SET `comment` = 'suggested / uploaded video / screenshot was approved', `default` = '10' WHERE `key` = 'rep_reward_upload'; +UPDATE aowow_config SET `comment` = 'submitted an approved article/guide', `default` = '100' WHERE `key` = 'rep_reward_article'; +UPDATE aowow_config SET `comment` = 'moderator revoked rights', `default` = '-200' WHERE `key` = 'rep_reward_user_suspended'; +UPDATE aowow_config SET `comment` = 'required reputation per additional vote past threshold', `default` = '250' WHERE `key` = 'rep_req_votemore_add'; +UPDATE aowow_config SET `comment` = 'parsing spell.dbc is quite intense', `default` = '1500M' WHERE `key` = 'memory_limit'; +UPDATE aowow_config SET `comment` = 'enable/disable profiler feature', `default` = '0', `flags`= `flags`| 256 WHERE `key` = 'profiler_enable'; +UPDATE aowow_config SET `comment` = 'min. delay between queue cycles (in ms)', `default` = '3000' WHERE `key` = 'profiler_queue_delay'; +UPDATE aowow_config SET `comment` = 'how often the javascript asks for for updates, when queued (in ms)', `default` = '5000' WHERE `key` = 'profiler_resync_ping'; +UPDATE aowow_config SET `comment` = 'how often a character can be refreshed (in sec)', `default` = '1 * 60 * 60' WHERE `key` = 'profiler_resync_delay'; diff --git a/setup/sql/updates/1717354214_01.sql b/setup/sql/updates/1717354214_01.sql new file mode 100644 index 00000000..4d2f93c6 --- /dev/null +++ b/setup/sql/updates/1717354214_01.sql @@ -0,0 +1,4 @@ +ALTER TABLE aowow_creature + ADD KEY `idx_loot` (`lootId`), + ADD KEY `idx_pickpocketloot` (`pickpocketLootId`), + ADD KEY `idx_skinloot` (`skinLootId`); diff --git a/setup/sql/updates/1717513011_01.sql b/setup/sql/updates/1717513011_01.sql new file mode 100644 index 00000000..777d40bd --- /dev/null +++ b/setup/sql/updates/1717513011_01.sql @@ -0,0 +1 @@ +UPDATE aowow_config SET `flags` = `flags`| 0x400 WHERE `key` IN ('locales', 'acc_auth_mode', 'profiler_enable'); diff --git a/setup/sql/updates/1718468660_01.sql b/setup/sql/updates/1718468660_01.sql new file mode 100644 index 00000000..ba6fef73 --- /dev/null +++ b/setup/sql/updates/1718468660_01.sql @@ -0,0 +1,91 @@ +ALTER TABLE `aowow_itemset` + DROP COLUMN `bonusParsed`; + +DROP TABLE IF EXISTS `aowow_item_stats`; +CREATE TABLE `aowow_item_stats` ( + `type` smallint(5) unsigned NOT NULL, + `typeId` mediumint(8) NOT NULL, + `nsockets` tinyint(3) unsigned NULL, + `dps` float(8,2) NULL, + `damagetype` tinyint(4) NULL, + `dmgmin1` mediumint(5) unsigned NULL, + `dmgmax1` mediumint(5) unsigned NULL, + `speed` float(8,2) NULL, + `mledps` float(8,2) NULL, + `mledmgmin` mediumint(5) unsigned NULL, + `mledmgmax` mediumint(5) unsigned NULL, + `mlespeed` float(8,2) NULL, + `rgddps` float(8,2) NULL, + `rgddmgmin` mediumint(5) unsigned NULL, + `rgddmgmax` mediumint(5) unsigned NULL, + `rgdspeed` float(8,2) NULL, + `dmg` float(8,2) NULL, + `mana` mediumint(6) NULL, + `health` mediumint(6) NULL, + `agi` mediumint(6) NULL, + `str` mediumint(6) NULL, + `int` mediumint(6) NULL, + `spi` mediumint(6) NULL, + `sta` mediumint(6) NULL, + `energy` mediumint(6) NULL, + `rage` mediumint(6) NULL, + `focus` mediumint(6) NULL, + `runic` mediumint(6) NULL, + `defrtng` mediumint(6) NULL, + `dodgertng` mediumint(6) NULL, + `parryrtng` mediumint(6) NULL, + `blockrtng` mediumint(6) NULL, + `mlehitrtng` mediumint(6) NULL, + `rgdhitrtng` mediumint(6) NULL, + `splhitrtng` mediumint(6) NULL, + `mlecritstrkrtng` mediumint(6) NULL, + `rgdcritstrkrtng` mediumint(6) NULL, + `splcritstrkrtng` mediumint(6) NULL, + `_mlehitrtng` mediumint(6) NULL, + `_rgdhitrtng` mediumint(6) NULL, + `_splhitrtng` mediumint(6) NULL, + `_mlecritstrkrtng` mediumint(6) NULL, + `_rgdcritstrkrtng` mediumint(6) NULL, + `_splcritstrkrtng` mediumint(6) NULL, + `mlehastertng` mediumint(6) NULL, + `rgdhastertng` mediumint(6) NULL, + `splhastertng` mediumint(6) NULL, + `hitrtng` mediumint(6) NULL, + `critstrkrtng` mediumint(6) NULL, + `_hitrtng` mediumint(6) NULL, + `_critstrkrtng` mediumint(6) NULL, + `resirtng` mediumint(6) NULL, + `hastertng` mediumint(6) NULL, + `exprtng` mediumint(6) NULL, + `atkpwr` mediumint(6) NULL, + `mleatkpwr` mediumint(6) NULL, + `rgdatkpwr` mediumint(6) NULL, + `feratkpwr` mediumint(6) NULL, + `splheal` mediumint(6) NULL, + `spldmg` mediumint(6) NULL, + `manargn` mediumint(6) NULL, + `armorpenrtng` mediumint(6) NULL, + `splpwr` mediumint(6) NULL, + `healthrgn` mediumint(6) NULL, + `splpen` mediumint(6) NULL, + `block` mediumint(6) NULL, + `mastrtng` mediumint(6) NULL, + `armor` mediumint(6) NULL, + `armorbonus` mediumint(6) NULL, + `firres` mediumint(6) NULL, + `frores` mediumint(6) NULL, + `holres` mediumint(6) NULL, + `shares` mediumint(6) NULL, + `natres` mediumint(6) NULL, + `arcres` mediumint(6) NULL, + `firsplpwr` mediumint(6) NULL, + `frosplpwr` mediumint(6) NULL, + `holsplpwr` mediumint(6) NULL, + `shasplpwr` mediumint(6) NULL, + `natsplpwr` mediumint(6) NULL, + `arcsplpwr` mediumint(6) NULL, + PRIMARY KEY (`type`,`typeId`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +UPDATE `aowow_dbversion` + SET `sql` = CONCAT(IFNULL(`sql`, ''), ' item_stats'); diff --git a/setup/sql/updates/1718629021_01.sql b/setup/sql/updates/1718629021_01.sql new file mode 100644 index 00000000..3201b8dd --- /dev/null +++ b/setup/sql/updates/1718629021_01.sql @@ -0,0 +1,20 @@ +DROP TABLE IF EXISTS aowow_areatrigger; +CREATE TABLE aowow_areatrigger ( + `id` int unsigned NOT NULL, + `cuFlags` int unsigned NOT NULL DEFAULT 0 COMMENT 'see defines.php for flags', + `type` smallint unsigned NOT NULL, + `mapId` smallint unsigned NOT NULL COMMENT 'world pos. from dbc', + `posX` float NOT NULL COMMENT 'world pos. from dbc', + `posY` float NOT NULL COMMENT 'world pos. from dbc', + `orientation` float NOT NULL, + `name` varchar(100) NULL DEFAULT NULL, + `quest` mediumint unsigned NULL DEFAULT NULL, + `teleportA` smallint unsigned NULL DEFAULT NULL, + `teleportX` float NULL DEFAULT NULL, + `teleportY` float NULL DEFAULT NULL, + `teleportO` float NULL DEFAULT NULL, + `teleportF` tinyint unsigned NULL DEFAULT NULL, + PRIMARY KEY (`id`), + INDEX `quest` (`quest`), + INDEX `type` (`type`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4 COLLATE='utf8mb4_general_ci' ; diff --git a/setup/sql/updates/1718998554_01.sql b/setup/sql/updates/1718998554_01.sql new file mode 100644 index 00000000..806c663d --- /dev/null +++ b/setup/sql/updates/1718998554_01.sql @@ -0,0 +1 @@ +UPDATE `aowow_dbversion` SET `build` = CONCAT(IFNULL(`build`, ''), ' markup'); diff --git a/setup/sql/updates/1719333848_01.sql b/setup/sql/updates/1719333848_01.sql new file mode 100644 index 00000000..d6462672 --- /dev/null +++ b/setup/sql/updates/1719333848_01.sql @@ -0,0 +1,3 @@ +DROP TABLE IF EXISTS dbc_areatrigger, dbc_soundemitters; + +UPDATE `aowow_dbversion` SET `sql` = CONCAT(IFNULL(`sql`, ''), ' areatrigger soundemitters spawns'); diff --git a/setup/sql/updates/1719445945_01.sql b/setup/sql/updates/1719445945_01.sql new file mode 100644 index 00000000..67b34418 --- /dev/null +++ b/setup/sql/updates/1719445945_01.sql @@ -0,0 +1,25 @@ +DROP TABLE IF EXISTS `aowow_areatrigger`; +CREATE TABLE `aowow_areatrigger` ( + `id` int unsigned NOT NULL, + `cuFlags` int unsigned NOT NULL DEFAULT 0 COMMENT 'see defines.php for flags', + `type` smallint unsigned NOT NULL, + `mapId` smallint unsigned NOT NULL COMMENT 'world pos. from dbc', + `posX` float NOT NULL COMMENT 'world pos. from dbc', + `posY` float NOT NULL COMMENT 'world pos. from dbc', + `orientation` float NOT NULL, + `name` varchar(100) DEFAULT NULL, + `quest` mediumint unsigned DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `quest` (`quest`), + KEY `type` (`type`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; + +ALTER TABLE `aowow_zones` + CHANGE COLUMN `parentAreaId` `parentMapId` smallint unsigned NOT NULL; + +DELETE FROM aowow_setup_custom_data WHERE + `command` = 'zones' AND + `entry` IN (3456, 3845, 3847, 3848, 3849) AND + `field` IN ('parentAreaId', 'parentX', 'parentY'); + +UPDATE `aowow_dbversion` SET `sql` = CONCAT(IFNULL(`sql`, ''), ' areatrigger zones spawns'); diff --git a/setup/sql/updates/1720025031_01.sql b/setup/sql/updates/1720025031_01.sql new file mode 100644 index 00000000..0f2ca5f1 --- /dev/null +++ b/setup/sql/updates/1720025031_01.sql @@ -0,0 +1,60 @@ +ALTER TABLE `aowow_account_bannedips` ENGINE=InnoDB ROW_FORMAT=DEFAULT; +ALTER TABLE `aowow_achievement` ENGINE=InnoDB ROW_FORMAT=DEFAULT; +ALTER TABLE `aowow_achievementcategory` ENGINE=InnoDB ROW_FORMAT=DEFAULT; +ALTER TABLE `aowow_achievementcriteria` ENGINE=InnoDB ROW_FORMAT=DEFAULT; +ALTER TABLE `aowow_announcements` ENGINE=InnoDB ROW_FORMAT=DEFAULT; +ALTER TABLE `aowow_areatrigger` ENGINE=InnoDB ROW_FORMAT=DEFAULT; +ALTER TABLE `aowow_articles` ENGINE=InnoDB ROW_FORMAT=DEFAULT; +ALTER TABLE `aowow_classes` ENGINE=InnoDB ROW_FORMAT=DEFAULT; +ALTER TABLE `aowow_config` ENGINE=InnoDB ROW_FORMAT=DEFAULT; +ALTER TABLE `aowow_creature` ENGINE=InnoDB ROW_FORMAT=DEFAULT; +ALTER TABLE `aowow_creature_waypoints` ENGINE=InnoDB ROW_FORMAT=DEFAULT; +ALTER TABLE `aowow_currencies` ENGINE=InnoDB ROW_FORMAT=DEFAULT; +ALTER TABLE `aowow_dbversion` ENGINE=InnoDB ROW_FORMAT=DEFAULT; +ALTER TABLE `aowow_declinedword` ENGINE=InnoDB ROW_FORMAT=DEFAULT; +ALTER TABLE `aowow_declinedwordcases` ENGINE=InnoDB ROW_FORMAT=DEFAULT; +ALTER TABLE `aowow_emotes` ENGINE=InnoDB ROW_FORMAT=DEFAULT; +ALTER TABLE `aowow_emotes_aliasses` ENGINE=InnoDB ROW_FORMAT=DEFAULT; +ALTER TABLE `aowow_errors` ENGINE=InnoDB ROW_FORMAT=DEFAULT; +ALTER TABLE `aowow_events` ENGINE=InnoDB ROW_FORMAT=DEFAULT; +ALTER TABLE `aowow_factions` ENGINE=InnoDB ROW_FORMAT=DEFAULT; +ALTER TABLE `aowow_factiontemplate` ENGINE=InnoDB ROW_FORMAT=DEFAULT; +ALTER TABLE `aowow_glyphproperties` ENGINE=InnoDB ROW_FORMAT=DEFAULT; +ALTER TABLE `aowow_holidays` ENGINE=InnoDB ROW_FORMAT=DEFAULT; +ALTER TABLE `aowow_item_stats` ENGINE=InnoDB ROW_FORMAT=DEFAULT; +ALTER TABLE `aowow_itemenchantment` ENGINE=InnoDB ROW_FORMAT=DEFAULT; +ALTER TABLE `aowow_itemenchantmentcondition` ENGINE=InnoDB ROW_FORMAT=DEFAULT; +ALTER TABLE `aowow_itemextendedcost` ENGINE=InnoDB ROW_FORMAT=DEFAULT; +ALTER TABLE `aowow_itemlimitcategory` ENGINE=InnoDB ROW_FORMAT=DEFAULT; +ALTER TABLE `aowow_itemrandomenchant` ENGINE=InnoDB ROW_FORMAT=DEFAULT; +ALTER TABLE `aowow_itemrandomproppoints` ENGINE=InnoDB ROW_FORMAT=DEFAULT; +ALTER TABLE `aowow_items` ENGINE=InnoDB ROW_FORMAT=DEFAULT; +ALTER TABLE `aowow_itemset` ENGINE=InnoDB ROW_FORMAT=DEFAULT; +ALTER TABLE `aowow_lock` ENGINE=InnoDB ROW_FORMAT=DEFAULT; +ALTER TABLE `aowow_loot_link` ENGINE=InnoDB ROW_FORMAT=DEFAULT; +ALTER TABLE `aowow_mails` ENGINE=InnoDB ROW_FORMAT=DEFAULT; +ALTER TABLE `aowow_objects` ENGINE=InnoDB ROW_FORMAT=DEFAULT; +ALTER TABLE `aowow_pet` ENGINE=InnoDB ROW_FORMAT=DEFAULT; +ALTER TABLE `aowow_quests` ENGINE=InnoDB ROW_FORMAT=DEFAULT; +ALTER TABLE `aowow_quests_startend` ENGINE=InnoDB ROW_FORMAT=DEFAULT; +ALTER TABLE `aowow_races` ENGINE=InnoDB ROW_FORMAT=DEFAULT; +ALTER TABLE `aowow_reports` ENGINE=InnoDB ROW_FORMAT=DEFAULT; +ALTER TABLE `aowow_scalingstatdistribution` ENGINE=InnoDB ROW_FORMAT=DEFAULT; +ALTER TABLE `aowow_scalingstatvalues` ENGINE=InnoDB ROW_FORMAT=DEFAULT; +ALTER TABLE `aowow_setup_custom_data` ENGINE=InnoDB ROW_FORMAT=DEFAULT; +ALTER TABLE `aowow_shapeshiftforms` ENGINE=InnoDB ROW_FORMAT=DEFAULT; +ALTER TABLE `aowow_skillline` ENGINE=InnoDB ROW_FORMAT=DEFAULT; +ALTER TABLE `aowow_spawns` ENGINE=InnoDB ROW_FORMAT=DEFAULT; +ALTER TABLE `aowow_spawns_override` ENGINE=InnoDB ROW_FORMAT=DEFAULT; +ALTER TABLE `aowow_spell` ENGINE=InnoDB ROW_FORMAT=DEFAULT; +ALTER TABLE `aowow_spelldifficulty` ENGINE=InnoDB ROW_FORMAT=DEFAULT; +ALTER TABLE `aowow_spellfocusobject` ENGINE=InnoDB ROW_FORMAT=DEFAULT; +ALTER TABLE `aowow_spelloverride` ENGINE=InnoDB ROW_FORMAT=DEFAULT; +ALTER TABLE `aowow_spellrange` ENGINE=InnoDB ROW_FORMAT=DEFAULT; +ALTER TABLE `aowow_spellvariables` ENGINE=InnoDB ROW_FORMAT=DEFAULT; +ALTER TABLE `aowow_talents` ENGINE=InnoDB ROW_FORMAT=DEFAULT; +ALTER TABLE `aowow_taxinodes` ENGINE=InnoDB ROW_FORMAT=DEFAULT; +ALTER TABLE `aowow_taxipath` ENGINE=InnoDB ROW_FORMAT=DEFAULT; +ALTER TABLE `aowow_titles` ENGINE=InnoDB ROW_FORMAT=DEFAULT; +ALTER TABLE `aowow_totemcategory` ENGINE=InnoDB ROW_FORMAT=DEFAULT; +ALTER TABLE `aowow_zones` ENGINE=InnoDB ROW_FORMAT=DEFAULT; diff --git a/setup/sql/updates/1720116490_01.sql b/setup/sql/updates/1720116490_01.sql new file mode 100644 index 00000000..10c0988c --- /dev/null +++ b/setup/sql/updates/1720116490_01.sql @@ -0,0 +1,217 @@ +DROP TABLE IF EXISTS `aowow_loot_link`; +CREATE TABLE `aowow_loot_link` ( + `npcId` mediumint(9) unsigned NOT NULL, + `objectId` mediumint(8) unsigned NOT NULL, + `difficulty` tinyint(3) unsigned NOT NULL DEFAULT 1, + `priority` tinyint(3) unsigned NOT NULL COMMENT '1: use this npc from group encounter (others 0)', + `encounterId` mediumint(8) unsigned NOT NULL COMMENT 'as title reference', + UNIQUE KEY `npcId_difficulty` (`npcId`, `difficulty`), + KEY `objectId` (`objectId`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +INSERT INTO `aowow_loot_link` VALUES + (19710,184465,1,0,0), + (19218,184465,1,1,0), + (21526,184849,2,0,0), + (21525,184849,2,1,0), + (17537,185168,1,1,0), + (17536,185168,1,0,0), + (18434,185169,2,1,0), + (18432,185169,2,0,0), + (28234,190586,1,0,0), + (28234,193996,2,0,0), + (26533,190663,1,0,0), + (31217,193597,2,0,0), + (27656,191349,1,0,0), + (31561,193603,2,0,0), + (28859,193905,1,0,0), + (31734,193967,2,0,0), + (32845,194307,1,0,0), + (32846,194308,2,0,0), + (32845,194200,3,0,0), + (32846,194201,4,0,0), + (32865,194312,1,0,0), + (33147,194314,2,0,0), + (32865,194313,3,0,0), + (33147,194315,4,0,0), + (32906,194324,1,0,0), + (33360,194328,2,0,0), + (32906,194327,3,0,0), + (33360,194331,4,0,0), + (32871,194821,1,0,0), + (33070,194822,2,0,0), + (33350,194789,1,0,0), + (33350,194956,2,0,0), + (33350,194957,3,0,0), + (33350,194958,4,0,0), + (32930,195046,1,0,0), + (33909,195047,2,0,0), + (34928,195323,1,0,0), + (35517,195324,2,0,0), + (35119,195374,1,0,0), + (35518,195375,2,0,0), + (37226,201710,1,0,0), + (37226,202336,2,0,0), + (36789,201959,1,0,0), + (38174,202339,2,0,0), + (36789,202338,3,0,0), + (38174,202340,4,0,0), + (38402,202239,1,0,0), + (38582,202240,2,0,0), + (37813,202238,3,0,0), + (38583,202241,4,0,0), + (9034,169243,1,0,243), + (9035,169243,1,1,243), + (9039,169243,1,0,243), + (9036,169243,1,0,243), + (9037,169243,1,0,243), + (9038,169243,1,0,243), + (9040,169243,1,0,243), + (34657,195709,1,0,334), + (34701,195709,1,0,334), + (34703,195709,1,0,334), + (34702,195709,1,0,334), + (34705,195709,1,0,334), + (35571,195709,1,0,334), + (35617,195709,1,0,334), + (35572,195709,1,0,334), + (35570,195709,1,0,334), + (35569,195709,1,1,334), + (36089,195710,2,0,334), + (36086,195710,2,0,334), + (36087,195710,2,0,334), + (36082,195710,2,0,334), + (36085,195710,2,1,334), + (36088,195710,2,0,334), + (36084,195710,2,0,334), + (36083,195710,2,0,334), + (36091,195710,2,0,334), + (36090,195710,2,0,334), + (34458,195631,1,0,637), + (34465,195631,1,0,637), + (34463,195631,1,0,637), + (34460,195631,1,0,637), + (34459,195631,1,0,637), + (34456,195631,1,0,637), + (34466,195631,1,0,637), + (34467,195631,1,0,637), + (34468,195631,1,0,637), + (34469,195631,1,0,637), + (34470,195631,1,0,637), + (34472,195631,1,0,637), + (34474,195631,1,0,637), + (34473,195631,1,0,637), + (34455,195631,1,0,637), + (34454,195631,1,0,637), + (34453,195631,1,0,637), + (34441,195631,1,1,637), + (34471,195631,1,0,637), + (34475,195631,1,0,637), + (34444,195631,1,0,637), + (34445,195631,1,0,637), + (34447,195631,1,0,637), + (34461,195631,1,0,637), + (34448,195631,1,0,637), + (34449,195631,1,0,637), + (34450,195631,1,0,637), + (34451,195631,1,0,637), + (35686,195632,2,0,637), + (35671,195632,2,0,637), + (35683,195632,2,0,637), + (35680,195632,2,0,637), + (35674,195632,2,0,637), + (35689,195632,2,0,637), + (35721,195632,2,0,637), + (35718,195632,2,0,637), + (35731,195632,2,0,637), + (35714,195632,2,0,637), + (35711,195632,2,0,637), + (35734,195632,2,0,637), + (35737,195632,2,0,637), + (35740,195632,2,0,637), + (35743,195632,2,0,637), + (35746,195632,2,0,637), + (35708,195632,2,0,637), + (35705,195632,2,0,637), + (35702,195632,2,0,637), + (35699,195632,2,0,637), + (35695,195632,2,0,637), + (35692,195632,2,0,637), + (35728,195632,2,0,637), + (35724,195632,2,0,637), + (35668,195632,2,0,637), + (34442,195632,2,1,637), + (35662,195632,2,0,637), + (35665,195632,2,0,637), + (35725,195633,3,0,637), + (35722,195633,3,0,637), + (35719,195633,3,0,637), + (35715,195633,3,0,637), + (35709,195633,3,0,637), + (35706,195633,3,0,637), + (35729,195633,3,0,637), + (35744,195633,3,0,637), + (35732,195633,3,0,637), + (35735,195633,3,0,637), + (35738,195633,3,0,637), + (35741,195633,3,0,637), + (35747,195633,3,0,637), + (35712,195633,3,0,637), + (35703,195633,3,0,637), + (35700,195633,3,0,637), + (35672,195633,3,0,637), + (35690,195633,3,0,637), + (35687,195633,3,0,637), + (35669,195633,3,0,637), + (35684,195633,3,0,637), + (35693,195633,3,0,637), + (34443,195633,3,1,637), + (35681,195633,3,0,637), + (35663,195633,3,0,637), + (35666,195633,3,0,637), + (35696,195633,3,0,637), + (35675,195633,3,0,637), + (35700,195635,4,0,637), + (35749,195635,4,1,637), + (35706,195635,4,0,637), + (35703,195635,4,0,637), + (35709,195635,4,0,637), + (35744,195635,4,0,637), + (35741,195635,4,0,637), + (35735,195635,4,0,637), + (35732,195635,4,0,637), + (35729,195635,4,0,637), + (35725,195635,4,0,637), + (35722,195635,4,0,637), + (35719,195635,4,0,637), + (35715,195635,4,0,637), + (35712,195635,4,0,637), + (35747,195635,4,0,637), + (35696,195635,4,0,637), + (35675,195635,4,0,637), + (35681,195635,4,0,637), + (35663,195635,4,0,637), + (35669,195635,4,0,637), + (35666,195635,4,0,637), + (35738,195635,4,0,637), + (35672,195635,4,0,637), + (35684,195635,4,0,637), + (35687,195635,4,0,637), + (35690,195635,4,0,637), + (35693,195635,4,0,637), + (16064,181366,1,0,692), + (16065,181366,1,0,692), + (16063,181366,1,0,692), + (30549,181366,1,1,692), + (30602,193426,2,0,692), + (30603,193426,2,0,692), + (30601,193426,2,0,692), + (30600,193426,2,1,692), + (36948,202178,1,0,847), + (36939,202178,1,0,847), + (38157,202180,2,0,847), + (38156,202180,2,0,847), + (38639,202177,3,0,847), + (38637,202177,3,0,847), + (38640,202179,4,0,847), + (38638,202179,4,0,847); diff --git a/setup/sql/updates/1720385207_01.sql b/setup/sql/updates/1720385207_01.sql new file mode 100644 index 00000000..2f84c768 --- /dev/null +++ b/setup/sql/updates/1720385207_01.sql @@ -0,0 +1,4 @@ +DELETE FROM `aowow_loot_link` WHERE `npcId` IN (25740,26338,12018); +INSERT INTO `aowow_loot_link` VALUES + (25740,187892,0,0,0), + (12018,179703,0,0,0); diff --git a/setup/sql/updates/1720448853_01.sql b/setup/sql/updates/1720448853_01.sql new file mode 100644 index 00000000..6436fe43 --- /dev/null +++ b/setup/sql/updates/1720448853_01.sql @@ -0,0 +1,2 @@ +ALTER TABLE `aowow_setup_custom_data` + MODIFY COLUMN `value` text DEFAULT NULL; diff --git a/setup/sql/updates/1720449931_01.sql b/setup/sql/updates/1720449931_01.sql new file mode 100644 index 00000000..af72739b --- /dev/null +++ b/setup/sql/updates/1720449931_01.sql @@ -0,0 +1 @@ +UPDATE `aowow_dbversion` SET `build` = CONCAT(IFNULL(`build`, ''), ' realmmenu'); diff --git a/setup/sql/updates/1720451578_01.sql b/setup/sql/updates/1720451578_01.sql new file mode 100644 index 00000000..05728e9c --- /dev/null +++ b/setup/sql/updates/1720451578_01.sql @@ -0,0 +1 @@ +UPDATE `aowow_config` SET `flags` = 1441 WHERE `key` = 'locales'; diff --git a/setup/sql/updates/1720455278_01.sql b/setup/sql/updates/1720455278_01.sql new file mode 100644 index 00000000..88b450bb --- /dev/null +++ b/setup/sql/updates/1720455278_01.sql @@ -0,0 +1,73 @@ +SET FOREIGN_KEY_CHECKS=0; + +DROP TABLE IF EXISTS `aowow_profiler_completion_quests`; +CREATE TABLE `aowow_profiler_completion_quests` ( +`id` int unsigned NOT NULL, +`questId` mediumint unsigned NOT NULL, +KEY `id` (`id`), +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; + +DROP TABLE IF EXISTS `aowow_profiler_completion_skills`; +CREATE TABLE `aowow_profiler_completion_skills` ( +`id` int unsigned NOT NULL, +`skillId` smallint unsigned NOT NULL, +`value` smallint unsigned DEFAULT NULL, +`max` smallint unsigned DEFAULT NULL, +KEY `id` (`id`), +KEY `typeId` (`skillId`), +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; + +DROP TABLE IF EXISTS `aowow_profiler_completion_reputation`; +CREATE TABLE `aowow_profiler_completion_reputation` ( +`id` int unsigned NOT NULL, +`factionId` smallint unsigned NOT NULL, +`standing` mediumint DEFAULT NULL, +KEY `id` (`id`), +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; + +DROP TABLE IF EXISTS `aowow_profiler_completion_titles`; +CREATE TABLE `aowow_profiler_completion_titles` ( +`id` int unsigned NOT NULL, +`titleId` tinyint unsigned NOT NULL, +KEY `id` (`id`), +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; + +DROP TABLE IF EXISTS `aowow_profiler_completion_achievements`; +CREATE TABLE `aowow_profiler_completion_achievements` ( +`id` int unsigned NOT NULL, +`achievementId` smallint unsigned NOT NULL, +`date` int unsigned DEFAULT NULL, +KEY `id` (`id`), +KEY `typeId` (`achievementId`), +CONSTRAINT `FK_pr_completion_achievements` FOREIGN KEY (`id`) REFERENCES `aowow_profiler_profiles` (`id`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +DROP TABLE IF EXISTS `aowow_profiler_completion_statistics`; +CREATE TABLE `aowow_profiler_completion_statistics` ( +`id` int unsigned NOT NULL, +`achievementId` smallint NOT NULL, +`date` int unsigned DEFAULT NULL, +`counter` smallint unsigned DEFAULT NULL, -- could be values of INT size, but surely not for bosskill counters, right? ... RIGHT!? +KEY `id` (`id`), +KEY `typeId` (`achievementId`), +CONSTRAINT `FK_pr_completion_statistics` FOREIGN KEY (`id`) REFERENCES `aowow_profiler_profiles` (`id`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +DROP TABLE IF EXISTS `aowow_profiler_completion_spells`; +CREATE TABLE `aowow_profiler_completion_spells` ( +`id` int unsigned NOT NULL, +`spellId` mediumint unsigned NOT NULL, +KEY `id` (`id`), +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; + +-- force profiles to be updated +UPDATE `aowow_profiler_profiles` SET `lastUpdated` = 0; + +DROP TABLE IF EXISTS `aowow_profiler_completion`; + +SET FOREIGN_KEY_CHECKS=1; diff --git a/setup/sql/updates/1720523764_01.sql b/setup/sql/updates/1720523764_01.sql new file mode 100644 index 00000000..2d588c5b --- /dev/null +++ b/setup/sql/updates/1720523764_01.sql @@ -0,0 +1,3 @@ +ALTER TABLE `aowow_comments` + MODIFY COLUMN `type` smallint unsigned NOT NULL DEFAULT 0 COMMENT 'Type of Page', + MODIFY COLUMN `typeId` mediumint NOT NULL DEFAULT 0 COMMENT 'ID Of Page'; diff --git a/setup/sql/updates/1720608489_01.sql b/setup/sql/updates/1720608489_01.sql new file mode 100644 index 00000000..cc47e011 --- /dev/null +++ b/setup/sql/updates/1720608489_01.sql @@ -0,0 +1,25 @@ +DROP TABLE IF EXISTS dbc_spell; + +ALTER TABLE `aowow_spell` + DROP COLUMN `effect1SpellClassMaskA`, + DROP COLUMN `effect2SpellClassMaskA`, + DROP COLUMN `effect3SpellClassMaskA`, + DROP COLUMN `effect1SpellClassMaskB`, + DROP COLUMN `effect2SpellClassMaskB`, + DROP COLUMN `effect3SpellClassMaskB`, + DROP COLUMN `effect1SpellClassMaskC`, + DROP COLUMN `effect2SpellClassMaskC`, + DROP COLUMN `effect3SpellClassMaskC`; + +ALTER TABLE `aowow_spell` + ADD COLUMN `effect1SpellClassMaskA` int NOT NULL AFTER `effect3PointsPerComboPoint`, + ADD COLUMN `effect1SpellClassMaskB` int NOT NULL AFTER `effect1SpellClassMaskA`, + ADD COLUMN `effect1SpellClassMaskC` int NOT NULL AFTER `effect1SpellClassMaskB`, + ADD COLUMN `effect2SpellClassMaskA` int NOT NULL AFTER `effect1SpellClassMaskC`, + ADD COLUMN `effect2SpellClassMaskB` int NOT NULL AFTER `effect2SpellClassMaskA`, + ADD COLUMN `effect2SpellClassMaskC` int NOT NULL AFTER `effect2SpellClassMaskB`, + ADD COLUMN `effect3SpellClassMaskA` int NOT NULL AFTER `effect2SpellClassMaskC`, + ADD COLUMN `effect3SpellClassMaskB` int NOT NULL AFTER `effect3SpellClassMaskA`, + ADD COLUMN `effect3SpellClassMaskC` int NOT NULL AFTER `effect3SpellClassMaskB`; + +UPDATE `aowow_dbversion` SET `sql` = CONCAT(IFNULL(`sql`, ''), ' spell'); diff --git a/setup/sql/updates/1720654746_01.sql b/setup/sql/updates/1720654746_01.sql new file mode 100644 index 00000000..2983169a --- /dev/null +++ b/setup/sql/updates/1720654746_01.sql @@ -0,0 +1 @@ +UPDATE `aowow_dbversion` SET `build` = CONCAT(IFNULL(`build`, ''), ' enchants'); diff --git a/setup/sql/updates/1720969085_01.sql b/setup/sql/updates/1720969085_01.sql new file mode 100644 index 00000000..3d0516f5 --- /dev/null +++ b/setup/sql/updates/1720969085_01.sql @@ -0,0 +1,2 @@ +ALTER TABLE `aowow_errors` + ADD COLUMN `post` text NOT NULL AFTER `query`; diff --git a/setup/sql/updates/1722382255_01.sql b/setup/sql/updates/1722382255_01.sql new file mode 100644 index 00000000..9fa47c05 --- /dev/null +++ b/setup/sql/updates/1722382255_01.sql @@ -0,0 +1 @@ +UPDATE `aowow_dbversion` SET `sql` = CONCAT(IFNULL(`sql`, ''), ' stats'); diff --git a/setup/sql/updates/1724095916_01.sql b/setup/sql/updates/1724095916_01.sql new file mode 100644 index 00000000..9620bdbe --- /dev/null +++ b/setup/sql/updates/1724095916_01.sql @@ -0,0 +1 @@ +UPDATE `aowow_dbversion` SET `sql` = CONCAT(IFNULL(`sql`, ''), ' summonproperties'); diff --git a/setup/sql/updates/1724503538_01.sql b/setup/sql/updates/1724503538_01.sql new file mode 100644 index 00000000..c0c38b25 --- /dev/null +++ b/setup/sql/updates/1724503538_01.sql @@ -0,0 +1 @@ +UPDATE `aowow_dbversion` SET `sql` = CONCAT(IFNULL(`sql`, ''), ' spawns'); diff --git a/setup/sql/updates/1725025019_01.sql b/setup/sql/updates/1725025019_01.sql new file mode 100644 index 00000000..3c565be8 --- /dev/null +++ b/setup/sql/updates/1725025019_01.sql @@ -0,0 +1 @@ +UPDATE `aowow_dbversion` SET `build` = CONCAT(IFNULL(`build`, ''), ' tooltips markup locales'); diff --git a/setup/sql/updates/1725972644_01.sql b/setup/sql/updates/1725972644_01.sql new file mode 100644 index 00000000..fa0f3d04 --- /dev/null +++ b/setup/sql/updates/1725972644_01.sql @@ -0,0 +1,87 @@ +DROP TABLE IF EXISTS `aowow_item_stats`; +CREATE TABLE `aowow_item_stats` ( + `type` smallint(5) unsigned NOT NULL, + `typeId` mediumint(8) NOT NULL, + `nsockets` tinyint(3) unsigned NOT NULL DEFAULT 0, + `dps` float(8,2) NULL, + `damagetype` tinyint(4) NULL, + `dmgmin1` mediumint(5) unsigned NULL, + `dmgmax1` mediumint(5) unsigned NULL, + `speed` float(8,2) NULL, + `mledps` float(8,2) NULL, + `mledmgmin` mediumint(5) unsigned NULL, + `mledmgmax` mediumint(5) unsigned NULL, + `mlespeed` float(8,2) NULL, + `rgddps` float(8,2) NULL, + `rgddmgmin` mediumint(5) unsigned NULL, + `rgddmgmax` mediumint(5) unsigned NULL, + `rgdspeed` float(8,2) NULL, + `dmg` float(8,2) NOT NULL DEFAULT 0, + `mana` mediumint(6) NOT NULL DEFAULT 0, + `health` mediumint(6) NOT NULL DEFAULT 0, + `agi` mediumint(6) NOT NULL DEFAULT 0, + `str` mediumint(6) NOT NULL DEFAULT 0, + `int` mediumint(6) NOT NULL DEFAULT 0, + `spi` mediumint(6) NOT NULL DEFAULT 0, + `sta` mediumint(6) NOT NULL DEFAULT 0, + `energy` mediumint(6) NOT NULL DEFAULT 0, + `rage` mediumint(6) NOT NULL DEFAULT 0, + `focus` mediumint(6) NOT NULL DEFAULT 0, + `runic` mediumint(6) NOT NULL DEFAULT 0, + `defrtng` mediumint(6) NOT NULL DEFAULT 0, + `dodgertng` mediumint(6) NOT NULL DEFAULT 0, + `parryrtng` mediumint(6) NOT NULL DEFAULT 0, + `blockrtng` mediumint(6) NOT NULL DEFAULT 0, + `mlehitrtng` mediumint(6) NOT NULL DEFAULT 0, + `rgdhitrtng` mediumint(6) NOT NULL DEFAULT 0, + `splhitrtng` mediumint(6) NOT NULL DEFAULT 0, + `mlecritstrkrtng` mediumint(6) NOT NULL DEFAULT 0, + `rgdcritstrkrtng` mediumint(6) NOT NULL DEFAULT 0, + `splcritstrkrtng` mediumint(6) NOT NULL DEFAULT 0, + `_mlehitrtng` mediumint(6) NOT NULL DEFAULT 0, + `_rgdhitrtng` mediumint(6) NOT NULL DEFAULT 0, + `_splhitrtng` mediumint(6) NOT NULL DEFAULT 0, + `_mlecritstrkrtng` mediumint(6) NOT NULL DEFAULT 0, + `_rgdcritstrkrtng` mediumint(6) NOT NULL DEFAULT 0, + `_splcritstrkrtng` mediumint(6) NOT NULL DEFAULT 0, + `mlehastertng` mediumint(6) NOT NULL DEFAULT 0, + `rgdhastertng` mediumint(6) NOT NULL DEFAULT 0, + `splhastertng` mediumint(6) NOT NULL DEFAULT 0, + `hitrtng` mediumint(6) NOT NULL DEFAULT 0, + `critstrkrtng` mediumint(6) NOT NULL DEFAULT 0, + `_hitrtng` mediumint(6) NOT NULL DEFAULT 0, + `_critstrkrtng` mediumint(6) NOT NULL DEFAULT 0, + `resirtng` mediumint(6) NOT NULL DEFAULT 0, + `hastertng` mediumint(6) NOT NULL DEFAULT 0, + `exprtng` mediumint(6) NOT NULL DEFAULT 0, + `atkpwr` mediumint(6) NOT NULL DEFAULT 0, + `mleatkpwr` mediumint(6) NOT NULL DEFAULT 0, + `rgdatkpwr` mediumint(6) NOT NULL DEFAULT 0, + `feratkpwr` mediumint(6) NOT NULL DEFAULT 0, + `splheal` mediumint(6) NOT NULL DEFAULT 0, + `spldmg` mediumint(6) NOT NULL DEFAULT 0, + `manargn` mediumint(6) NOT NULL DEFAULT 0, + `armorpenrtng` mediumint(6) NOT NULL DEFAULT 0, + `splpwr` mediumint(6) NOT NULL DEFAULT 0, + `healthrgn` mediumint(6) NOT NULL DEFAULT 0, + `splpen` mediumint(6) NOT NULL DEFAULT 0, + `block` mediumint(6) NOT NULL DEFAULT 0, + `mastrtng` mediumint(6) NOT NULL DEFAULT 0, + `armor` mediumint(6) NOT NULL DEFAULT 0, + `armorbonus` mediumint(6) NULL, + `firres` mediumint(6) NOT NULL DEFAULT 0, + `frores` mediumint(6) NOT NULL DEFAULT 0, + `holres` mediumint(6) NOT NULL DEFAULT 0, + `shares` mediumint(6) NOT NULL DEFAULT 0, + `natres` mediumint(6) NOT NULL DEFAULT 0, + `arcres` mediumint(6) NOT NULL DEFAULT 0, + `firsplpwr` mediumint(6) NOT NULL DEFAULT 0, + `frosplpwr` mediumint(6) NOT NULL DEFAULT 0, + `holsplpwr` mediumint(6) NOT NULL DEFAULT 0, + `shasplpwr` mediumint(6) NOT NULL DEFAULT 0, + `natsplpwr` mediumint(6) NOT NULL DEFAULT 0, + `arcsplpwr` mediumint(6) NOT NULL DEFAULT 0, + PRIMARY KEY (`type`,`typeId`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +UPDATE `aowow_dbversion` SET `sql` = CONCAT(IFNULL(`sql`, ''), ' stats'); diff --git a/setup/sql/updates/1741361137_01.sql b/setup/sql/updates/1741361137_01.sql new file mode 100644 index 00000000..92ad74d7 --- /dev/null +++ b/setup/sql/updates/1741361137_01.sql @@ -0,0 +1 @@ +UPDATE `aowow_dbversion` SET `sql` = CONCAT(IFNULL(`sql`, ''), ' itemset'), `build` = CONCAT(IFNULL(`build`, ''), ' itemsets'); diff --git a/setup/sql/updates/1742667408_01.sql b/setup/sql/updates/1742667408_01.sql new file mode 100644 index 00000000..dc4ff953 --- /dev/null +++ b/setup/sql/updates/1742667408_01.sql @@ -0,0 +1,2 @@ +UPDATE `aowow_setup_custom_data` SET `command` = 'items' WHERE `command` = 'item'; +UPDATE `aowow_dbversion` SET `sql` = CONCAT(IFNULL(`sql`, ''), ' items'); diff --git a/setup/sql/updates/1750613426_01.sql b/setup/sql/updates/1750613426_01.sql new file mode 100644 index 00000000..4aced6c3 --- /dev/null +++ b/setup/sql/updates/1750613426_01.sql @@ -0,0 +1 @@ +UPDATE `aowow_dbversion` SET `sql` = CONCAT(IFNULL(`sql`, ''), ' quests'); diff --git a/setup/sql/updates/1753369288_01.sql b/setup/sql/updates/1753369288_01.sql new file mode 100644 index 00000000..cc7a46fc --- /dev/null +++ b/setup/sql/updates/1753369288_01.sql @@ -0,0 +1,27 @@ +ALTER TABLE aowow_account_weightscales + ADD COLUMN `orderIdx` tinyint unsigned NOT NULL DEFAULT 0 COMMENT 'check how Profiler handles classes with more than 3 specs before modifying' AFTER `class`; + +UPDATE aowow_account_weightscales SET `orderIdx` = 1 WHERE `userId` = 0 AND `class` = 1 AND `name` = 'fury'; +UPDATE aowow_account_weightscales SET `orderIdx` = 2 WHERE `userId` = 0 AND `class` = 1 AND `name` = 'prot'; +UPDATE aowow_account_weightscales SET `orderIdx` = 1 WHERE `userId` = 0 AND `class` = 2 AND `name` = 'prot'; +UPDATE aowow_account_weightscales SET `orderIdx` = 2 WHERE `userId` = 0 AND `class` = 2 AND `name` = 'retrib'; +UPDATE aowow_account_weightscales SET `orderIdx` = 1 WHERE `userId` = 0 AND `class` = 3 AND `name` = 'marks'; +UPDATE aowow_account_weightscales SET `orderIdx` = 2 WHERE `userId` = 0 AND `class` = 3 AND `name` = 'surv'; +UPDATE aowow_account_weightscales SET `orderIdx` = 1 WHERE `userId` = 0 AND `class` = 4 AND `name` = 'combat'; +UPDATE aowow_account_weightscales SET `orderIdx` = 2 WHERE `userId` = 0 AND `class` = 4 AND `name` = 'subtle'; +UPDATE aowow_account_weightscales SET `orderIdx` = 1 WHERE `userId` = 0 AND `class` = 5 AND `name` = 'holy'; +UPDATE aowow_account_weightscales SET `orderIdx` = 2 WHERE `userId` = 0 AND `class` = 5 AND `name` = 'shadow'; +UPDATE aowow_account_weightscales SET `orderIdx` = 1 WHERE `userId` = 0 AND `class` = 6 AND `name` = 'frostdps'; +UPDATE aowow_account_weightscales SET `orderIdx` = 2 WHERE `userId` = 0 AND `class` = 6 AND `name` = 'frosttank'; +UPDATE aowow_account_weightscales SET `orderIdx` = 3 WHERE `userId` = 0 AND `class` = 6 AND `name` = 'unholydps'; +UPDATE aowow_account_weightscales SET `orderIdx` = 1 WHERE `userId` = 0 AND `class` = 7 AND `name` = 'enhance'; +UPDATE aowow_account_weightscales SET `orderIdx` = 2 WHERE `userId` = 0 AND `class` = 7 AND `name` = 'resto'; +UPDATE aowow_account_weightscales SET `orderIdx` = 1 WHERE `userId` = 0 AND `class` = 8 AND `name` = 'fire'; +UPDATE aowow_account_weightscales SET `orderIdx` = 2 WHERE `userId` = 0 AND `class` = 8 AND `name` = 'frost'; +UPDATE aowow_account_weightscales SET `orderIdx` = 1 WHERE `userId` = 0 AND `class` = 9 AND `name` = 'demo'; +UPDATE aowow_account_weightscales SET `orderIdx` = 2 WHERE `userId` = 0 AND `class` = 9 AND `name` = 'destro'; +UPDATE aowow_account_weightscales SET `orderIdx` = 1 WHERE `userId` = 0 AND `class` = 11 AND `name` = 'feraldps'; +UPDATE aowow_account_weightscales SET `orderIdx` = 2 WHERE `userId` = 0 AND `class` = 11 AND `name` = 'feraltank'; +UPDATE aowow_account_weightscales SET `orderIdx` = 3 WHERE `userId` = 0 AND `class` = 11 AND `name` = 'resto'; + +UPDATE `aowow_dbversion` SET `build` = CONCAT(IFNULL(`build`, ''), ' weightpresets'); diff --git a/setup/sql/updates/1753563161_01.sql b/setup/sql/updates/1753563161_01.sql new file mode 100644 index 00000000..cae5801d --- /dev/null +++ b/setup/sql/updates/1753563161_01.sql @@ -0,0 +1 @@ +UPDATE aowow_articles SET `article` = '[menu tab=2 path=2,8]\r\n\r\nSearch plugins make it easy to search the database right from your browser!\r\n\r\n[toc h3=false]\r\n\r\n[h2][img src=STATIC_URL/images/help/searchplugins/firefox.png border=0 margin=5 float=left]Firefox[/h2]\r\n\r\n[div float=right align=right][img border=2 src=STATIC_URL/images/help/searchplugins/os-firefox.png][/div]\r\n\r\n[script]\r\nfunction addPlugin()\r\n{\r\n if (typeof window.external.AddSearchProvider == "function")\r\n window.external.AddSearchProvider("STATIC_URL/download/searchplugins/aowow.xml");\r\n else\r\n alert("This feature is unavailable.");\r\n}\r\n[/script]\r\n[pad]\r\nEither\r\n[ul]\r\n[li]Click on the button below to install the search plugin in your browser or[/li]\r\n[li]Right-click your address bar and then clck on "Add AoWoW" or[/li]\r\n[li]Click on the [img src=STATIC_URL/images/icons/add.png border=0] on the browser search bar and then on [img src=STATIC_URL/images/icons/add.png border=0] "Add search engine"[/li]\r\n[/ul]\r\n\r\n[pad]\r\n[html]Install pluginInstall plugin[/html]\r\n[div clear=both][/div]\r\n\r\n[h2][img src=STATIC_URL/images/help/searchplugins/edge.png border=0 float=left][img src=STATIC_URL/images/help/searchplugins/chrome.png border=0 float=left]MS Edge / Google Chrome[/h2]\r\n\r\n[div float=right align=right][img border=2 src=STATIC_URL/images/help/searchplugins/os-edge.png][/div]\r\n[pad]\r\nFor Chrome-based browsers go to settings and fill in the add search engine form as shown.\r\n[pad]\r\n[div width=500px]\r\n[pre]HOST_URL/?search=%s[img src=STATIC_URL/images/icons/pages.gif float=right][/pre]\r\n[/div]\r\n[script]\r\nsetTimeout(() => $WH.clickToCopy($WH.qs("pre > img"), "HOST_URL/?search=%s"), 100);\r\n[/script]\r\n[pad]\r\nSave your changes, and you\'ll be able to perform Aowow searches by typing "db" followed by the search terms in the address bar (e.g. db sword).\r\n[div clear=both][/div]\r\n' WHERE `url` = 'searchplugins' AND `locale` = 0; diff --git a/setup/sql/updates/1753572319_01.sql b/setup/sql/updates/1753572319_01.sql new file mode 100644 index 00000000..2e45b083 --- /dev/null +++ b/setup/sql/updates/1753572319_01.sql @@ -0,0 +1,14 @@ +ALTER TABLE `aowow_account` + DROP INDEX `user`, + CHANGE COLUMN `user` `login` varchar(64) NOT NULL DEFAULT '' COMMENT 'only used for login', + CHANGE COLUMN `displayName` `username` varchar(64) NOT NULL COMMENT 'unique; used for for links and display', + MODIFY COLUMN `email` varchar(64) DEFAULT NULL COMMENT 'unique; can be used for login if AUTH_SELF and can be NULL if not', + MODIFY COLUMN `token` varchar(40) DEFAULT NULL COMMENT 'identification key for changes to account', + ADD COLUMN `updateValue` varchar(128) DEFAULT NULL COMMENT 'temp store for new passHash / email' AFTER `token`, + ADD CONSTRAINT `username` UNIQUE (`username`); + +UPDATE `aowow_account` + SET `email` = NULL WHERE `email` = ''; + +ALTER TABLE `aowow_account` + ADD CONSTRAINT `email` UNIQUE (`email`); diff --git a/setup/sql/updates/1753574969_01.sql b/setup/sql/updates/1753574969_01.sql new file mode 100644 index 00000000..4d599283 --- /dev/null +++ b/setup/sql/updates/1753574969_01.sql @@ -0,0 +1,17 @@ +DROP TABLE IF EXISTS `aowow_account_sessions`; +CREATE TABLE `aowow_account_sessions` ( + `userId` int unsigned NOT NULL, + `sessionId` varchar(190) NOT NULL COMMENT 'PHPSESSID', -- max size (for utf8mb4) to still be a key + `created` int unsigned NOT NULL, + `expires` int unsigned NOT NULL COMMENT 'timestamp or 0 (never expires)', + `touched` int unsigned NOT NULL COMMENT 'timestamp - last used', + `deviceInfo` varchar(256) NOT NULL, + `ip` varchar(45) NOT NULL COMMENT 'can change; just last used ip', -- think mobile switching between WLAN and mobile data + `status` enum('ACTIVE', 'LOGOUT', 'FORCEDLOGOUT', 'EXPIRED') NOT NULL, + UNIQUE KEY `sessionId` (`sessionId`) USING BTREE, + KEY `userId` (`userId`) USING BTREE, + CONSTRAINT `FK_acc_sessions` FOREIGN KEY (`userId`) REFERENCES `aowow_account` (`id`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=COMPACT; + +ALTER TABLE `aowow_account` + DROP COLUMN `allowExpire`; diff --git a/setup/sql/updates/1753635510_01.sql b/setup/sql/updates/1753635510_01.sql new file mode 100644 index 00000000..77f23fbe --- /dev/null +++ b/setup/sql/updates/1753635510_01.sql @@ -0,0 +1,160 @@ +ALTER TABLE `aowow_articles` + DROP COLUMN `quickInfo`; + +DROP TABLE IF EXISTS `aowow_quickfacts`; +CREATE TABLE `aowow_quickfacts` ( + `type` smallint unsigned NOT NULL, + `typeId` mediumint signed NOT NULL, + `orderIdx` tinyint signed NOT NULL COMMENT '<0: prepend to generic list; >0: append to generic list', + `row` varchar(200) NOT NULL COMMENT 'Markdown formated', + UNIQUE KEY `row` (`type`, `typeId`, `orderIdx`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=COMPACT; + +INSERT INTO `aowow_quickfacts` VALUES + -- Dungeons + (7, 206, 1, '|L:zone:boss|[icon preset=boss][npc=23954][/icon]'), + (7, 209, 1, '|L:zone:boss|[icon preset=boss][npc=4275][/icon]'), + (7, 491, 1, '|L:zone:boss|[icon preset=boss][npc=4421][/icon]'), + (7, 717, 1, '|L:zone:boss|[icon preset=boss][npc=1716][/icon]'), + (7, 718, 1, '|L:zone:boss|[icon preset=boss][npc=5775][/icon]'), + (7, 719, 1, '|L:zone:boss|[icon preset=boss][npc=4829][/icon]'), + (7, 721, 1, '|L:zone:boss|[icon preset=boss][npc=7800][/icon]'), + (7, 722, 1, '|L:zone:boss|[icon preset=boss][npc=7358][/icon]'), + (7, 796, 1, '|L:zone:key:0|[item=7146]'), + (7, 796, 2, '|L:zone:boss|[icon preset=boss][npc=3976][/icon]'), + (7, 1176, 1, '|L:zone:boss|[icon preset=boss][npc=7267][/icon]'), + (7, 1196, 1, '|L:zone:boss|[icon preset=boss][npc=26861][/icon]'), + (7, 1337, 1, '|L:zone:boss|[icon preset=boss][npc=2748][/icon]'), + (7, 1477, 1, '|L:zone:boss|[icon preset=boss][npc=5709][/icon]'), + (7, 1581, 1, '|L:zone:boss|[icon preset=boss][npc=639][/icon]'), + (7, 1583, 1, '|L:zone:boss|[icon preset=boss][npc=10363][/icon]'), + (7, 1584, 1, '|L:zone:boss|[icon preset=boss][npc=9019][/icon]'), + (7, 2017, 1, '|L:zone:boss|[icon preset=boss][npc=10440][/icon]'), + (7, 2057, 1, '|L:zone:key:0|[item=13704]'), + (7, 2057, 2, '|L:zone:boss|[icon preset=boss][npc=1853][/icon]'), + (7, 2100, 1, '|L:zone:boss|[icon preset=boss][npc=12201][/icon]'), + (7, 2366, 1, '|L:zone:faction|[faction=989]'), + (7, 2366, 2, '|L:zone:boss|[icon preset=boss][npc=17881][/icon]'), + (7, 2367, 1, '|L:zone:faction|[faction=989]'), + (7, 2367, 2, '|L:zone:boss|[icon preset=boss][npc=18096][/icon]'), + (7, 2437, 1, '|L:zone:boss|[icon preset=boss][npc=11520][/icon]'), + (7, 3562, 1, '|L:zone:faction|[icon name=side_alliance][faction=946][/icon] / [icon name=side_horde][faction=947][/icon]'), + (7, 3562, 2, '|L:zone:boss|[icon preset=boss][npc=17536][/icon]'), + (7, 3713, 1, '|L:zone:faction|[icon name=side_alliance][faction=946][/icon] / [icon name=side_horde][faction=947][/icon]'), + (7, 3713, 2, '|L:zone:boss|[icon preset=boss][npc=17377][/icon]'), + (7, 3714, 1, '|L:zone:key:0|[item=28395]'), + (7, 3714, 2, '|L:zone:faction|[icon name=side_alliance][faction=946][/icon] / [icon name=side_horde][faction=947][/icon]'), + (7, 3714, 3, '|L:zone:boss|[icon preset=boss][npc=16808][/icon]'), + (7, 3715, 1, '|L:zone:faction|[faction=942]'), + (7, 3715, 2, '|L:zone:boss|[icon preset=boss][npc=17798][/icon]'), + (7, 3716, 1, '|L:zone:faction|[faction=942]'), + (7, 3716, 2, '|L:zone:boss|[icon preset=boss][npc=17882][/icon]'), + (7, 3717, 1, '|L:zone:faction|[faction=942]'), + (7, 3717, 2, '|L:zone:boss|[icon preset=boss][npc=17942][/icon]'), + (7, 3789, 1, '|L:zone:key:0|[item=27991]'), + (7, 3789, 2, '|L:zone:faction|[faction=1011]'), + (7, 3789, 3, '|L:zone:boss|[icon preset=boss][npc=18708][/icon]'), + (7, 3790, 1, '|L:zone:faction|[faction=1011]'), + (7, 3790, 2, '|L:zone:boss|[icon preset=boss][npc=18373][/icon]'), + (7, 3791, 1, '|L:zone:faction|[faction=1011]'), + (7, 3791, 2, '|L:zone:boss|[icon preset=boss][npc=18473][/icon]'), + (7, 3792, 1, '|L:zone:faction|[faction=933]'), + (7, 3792, 2, '|L:zone:boss|[icon preset=boss][npc=18344][/icon]'), + (7, 3847, 1, '|L:zone:faction|[faction=935]'), + (7, 3847, 2, '|L:zone:boss|[icon preset=boss][npc=17977][/icon]'), + (7, 3848, 1, '|L:zone:key:0|[item=31084]'), + (7, 3848, 2, '|L:zone:faction|[faction=935]'), + (7, 3848, 3, '|L:zone:boss|[icon preset=boss][npc=20912][/icon]'), + (7, 3849, 1, '|L:zone:faction|[faction=935]'), + (7, 3849, 2, '|L:zone:boss|[icon preset=boss][npc=19220][/icon]'), + (7, 4100, 1, '|L:zone:boss|[icon preset=boss][npc=26533][/icon]'), + (7, 4131, 1, '|L:zone:faction|[faction=1077]'), + (7, 4131, 2, '|L:zone:boss|[icon preset=boss][npc=24664][/icon]'), + (7, 4196, 1, '|L:zone:boss|[icon preset=boss][npc=26632][/icon]'), + (7, 4228, 1, '|L:zone:boss|[icon preset=boss][npc=27656][/icon]'), + (7, 4264, 1, '|L:zone:boss|[icon preset=boss][npc=27978][/icon]'), + (7, 4265, 1, '|L:zone:boss|[icon preset=boss][npc=26723][/icon]'), + (7, 4272, 1, '|L:zone:boss|[icon preset=boss][npc=28923][/icon]'), + (7, 4277, 1, '|L:zone:boss|[icon preset=boss][npc=29120][/icon]'), + (7, 4415, 1, '|L:zone:boss|[icon preset=boss][npc=31134][/icon]'), + (7, 4416, 1, '|L:zone:boss|[icon preset=boss][npc=29306][/icon]'), + (7, 4494, 1, '|L:zone:boss|[icon preset=boss][npc=29311][/icon]'), + (7, 4723, 1, '|L:zone:boss|[icon preset=boss][npc=35451][/icon]'), + (7, 4809, 1, '|L:zone:boss|[icon preset=boss][npc=36502][/icon]'), + (7, 4813, 1, '|L:zone:boss|[icon preset=boss][npc=36658][/icon]'), + (7, 4820, 1, '|L:zone:boss|[icon preset=boss][npc=36954][/icon]'), + -- Raids + (7, 1977, 1, '|L:zone:raidFaction|[faction=270]'), + (7, 1977, 2, '|L:zone:boss|[icon preset=boss][npc=14834][/icon]'), + (7, 2677, 1, '|L:zone:attunement:0|[quest=7761]'), + (7, 2677, 2, '|L:zone:boss|[icon preset=boss][npc=11583][/icon]'), + (7, 2717, 1, '|L:zone:attunement:0|[quest=7487]'), + (7, 2717, 2, '|L:zone:raidFaction|[faction=749]'), + (7, 2717, 3, '|L:zone:boss|[icon preset=boss][npc=11502][/icon]'), + (7, 3428, 1, '|L:zone:raidFaction|[faction=910]'), + (7, 3428, 2, '|L:zone:boss|[icon preset=boss][npc=15727][/icon]'), + (7, 3429, 1, '|L:zone:raidFaction|[faction=609]'), + (7, 3429, 2, '|L:zone:boss|[icon preset=boss][npc=15339][/icon]'), + (7, 3457, 1, '|L:zone:attunement:0|[quest=9837]'), + (7, 3457, 2, '|L:zone:key:0|[item=24490]'), + (7, 3457, 3, '|L:zone:raidFaction|[faction=967]'), + (7, 3457, 4, '|L:zone:boss|[icon preset=boss][npc=15690][/icon]'), + (7, 3606, 1, '|L:zone:raidFaction|[faction=990]'), + (7, 3606, 2, '|L:zone:boss|[icon preset=boss][npc=17968][/icon]'), + (7, 3607, 1, '|L:zone:boss|[icon preset=boss][npc=21212][/icon]'), + (7, 3805, 1, '|L:zone:boss|[icon preset=boss][npc=23863][/icon]'), + (7, 3836, 1, '|L:zone:boss|[icon preset=boss][npc=17257][/icon]'), + (7, 3845, 1, '|L:zone:boss|[icon preset=boss][npc=19622][/icon]'), + (7, 3923, 1, '|L:zone:boss|[icon preset=boss][npc=19044][/icon]'), + (7, 3959, 1, '|L:zone:raidFaction|[faction=1012]'), + (7, 3959, 2, '|L:zone:boss|[icon preset=boss][npc=22917][/icon]'), + (7, 4075, 1, '|L:zone:boss|[icon preset=boss][npc=25315][/icon]'), + -- Zones + (7, 1, 1, '|L:zone:city||L:main:colon|[zone=1537]'), + (7, 12, 1, '|L:zone:city||L:main:colon|[zone=1519]'), + (7, 14, 1, '|L:zone:city||L:main:colon|[zone=1637]'), + (7, 65, 1, '|L:zone:reputationHub|[faction=1091]'), + (7, 67, 1, '|L:zone:reputationHub|[faction=1119]'), + (7, 85, 1, '|L:zone:city||L:main:colon|[zone=1497]'), + (7, 139, 1, '|L:zone:reputationHub|[faction=529]'), + (7, 141, 1, '|L:zone:city||L:main:colon|[zone=1657]'), + (7, 210, 1, '|L:zone:reputationHub|[faction=1106]\n[faction=1098]'), + (7, 215, 1, '|L:zone:city||L:main:colon|[zone=1638]'), + (7, 361, 1, '|L:zone:reputationHub|[faction=576]'), + (7, 405, 1, '|L:zone:reputationHub|[faction=92]\n[faction=93]'), + (7, 440, 1, '|L:zone:reputationHub|[faction=989]'), + (7, 493, 1, '|L:game:class||L:main:colon|[class=11]'), + (7, 618, 1, '|L:zone:reputationHub|[faction=589]'), + (7, 1377, 1, '|L:zone:reputationHub|[faction=609]'), + (7, 1497, 1, '|L:zone:location|[zone=85]'), + (7, 1497, 2, '|L:zone:reputationHub|[faction=68]'), + (7, 1519, 1, '|L:zone:location|[zone=12]'), + (7, 1519, 2, '|L:zone:reputationHub|[faction=72]'), + (7, 1537, 1, '|L:zone:location|[zone=1]'), + (7, 1537, 2, '|L:zone:reputationHub|[faction=47]\n[faction=54]'), + (7, 1637, 1, '|L:zone:location|[zone=14]'), + (7, 1637, 2, '|L:zone:reputationHub|[faction=76]'), + (7, 1638, 1, '|L:zone:location|[zone=215]'), + (7, 1638, 2, '|L:zone:reputationHub|[faction=81]'), + (7, 1657, 1, '|L:zone:location|[zone=141]'), + (7, 1657, 2, '|L:zone:reputationHub|[faction=69]'), + (7, 3430, 1, '|L:zone:city||L:main:colon|[zone=3487]'), + (7, 3433, 1, '|L:zone:reputationHub|[faction=922]'), + (7, 3483, 1, '|L:zone:reputationHub|[icon name=side_alliance][faction=946][/icon]\n[icon name=side_horde][faction=947][/icon]'), + (7, 3487, 1, '|L:zone:location|[zone=3430]'), + (7, 3487, 2, '|L:zone:reputationHub|[faction=911]'), + (7, 3518, 1, '|L:zone:reputationHub|[icon name=side_alliance][faction=978][/icon]\n[icon name=side_horde][faction=941][/icon]'), + (7, 3519, 1, '|L:zone:reputationHub|[faction=1031]'), + (7, 3519, 2, '|L:zone:city||L:main:colon|[zone=3703]'), + (7, 3520, 1, '|L:zone:reputationHub|[faction=1015]'), + (7, 3521, 1, '|L:zone:reputationHub|[faction=942]\n[faction=970]'), + (7, 3522, 1, '|L:zone:reputationHub|[faction=1038]'), + (7, 3523, 1, '|L:zone:reputationHub|[faction=933]'), + (7, 3557, 1, '|L:zone:location|[zone=3524]'), + (7, 3557, 2, '|L:zone:reputationHub|[faction=930]'), + (7, 3711, 1, '|L:zone:reputationHub|[faction=1105]\n[faction=1104]'), + (7, 3703, 1, '|L:zone:location|[zone=3519]'), + (7, 3703, 2, '|L:zone:reputationHub|[faction=932]\n[faction=934]\n[faction=1011]'), + (7, 4080, 1, '|L:zone:reputationHub|[faction=1077]'), + (7, 4395, 1, '|L:zone:location|[zone=2817]'), + (7, 4395, 2, '|L:zone:reputationHub|[faction=1090]'); diff --git a/setup/sql/updates/1753977720_01.sql b/setup/sql/updates/1753977720_01.sql new file mode 100644 index 00000000..bef0e1a9 --- /dev/null +++ b/setup/sql/updates/1753977720_01.sql @@ -0,0 +1 @@ +UPDATE `aowow_dbversion` SET `sql` = CONCAT(IFNULL(`sql`, ''), ' source'); diff --git a/setup/sql/updates/1758578400_01.sql b/setup/sql/updates/1758578400_01.sql new file mode 100644 index 00000000..66e1f52b --- /dev/null +++ b/setup/sql/updates/1758578400_01.sql @@ -0,0 +1,6 @@ +DELETE FROM `aowow_config` WHERE `key` IN ('rep_req_ext_links', 'gtag_measurement_id'); +INSERT INTO `aowow_config` (`key`, `value`, `default`, `cat`, `flags`, `comment`) VALUES + ('rep_req_ext_links', 150, 150, 5, 129, 'required reputation to link to external sites'), + ('gtag_measurement_id', '', NULL, 6, 136, 'Enter your Google Tag measurement ID here to track site stats'); + +UPDATE `aowow_config` SET `key` = 'ua_measurement_key', `comment` = '[DEPRECATED ?] Enter your Google Universal Analytics key here to track site stats' WHERE `key` = 'analytics_user'; diff --git a/setup/sql/updates/1758578400_02.sql b/setup/sql/updates/1758578400_02.sql new file mode 100644 index 00000000..ca98a880 --- /dev/null +++ b/setup/sql/updates/1758578400_02.sql @@ -0,0 +1 @@ +UPDATE `aowow_articles` SET `url` = CONCAT('help=', `url`) WHERE `url` IN ('commenting-and-you', 'item-comparison', 'modelviewer', 'profiler', 'screenshots-tips-tricks', 'stat-weighting', 'talent-calculator', 'markup-guide'); diff --git a/setup/sql/updates/1758578400_03.sql b/setup/sql/updates/1758578400_03.sql new file mode 100644 index 00000000..a579869f --- /dev/null +++ b/setup/sql/updates/1758578400_03.sql @@ -0,0 +1,2 @@ +UPDATE `aowow_config` SET `key` = 'rep_req_border_legendary' WHERE `key` = 'rep_req_border_lege'; +UPDATE `aowow_config` SET `key` = 'rep_req_border_uncommon' WHERE `key` = 'rep_req_border_unco'; diff --git a/setup/sql/updates/1758578400_04.sql b/setup/sql/updates/1758578400_04.sql new file mode 100644 index 00000000..7d6f7c5b --- /dev/null +++ b/setup/sql/updates/1758578400_04.sql @@ -0,0 +1,17 @@ +ALTER TABLE `aowow_account` + CHANGE COLUMN `avatar` `wowicon` varchar(55) NOT NULL DEFAULT '' COMMENT 'iconname as avatar', + ADD COLUMN `avatar` tinyint(1) NOT NULL DEFAULT 0 COMMENT 'selected avatar mode' AFTER `userGroups`; + +DROP TABLE IF EXISTS `aowow_account_avatars`; +CREATE TABLE `aowow_account_avatars` ( + `id` mediumint unsigned NOT NULL, + `userId` int unsigned NOT NULL, + `name` varchar(20) NOT NULL, + `size` mediumint unsigned NOT NULL, + `when` int unsigned NOT NULL, + `current` tinyint unsigned NOT NULL DEFAULT 0, + `status` tinyint unsigned NOT NULL DEFAULT 0, + UNIQUE KEY `id` (`id`) USING BTREE, + KEY `userId` (`userId`) USING BTREE, + CONSTRAINT `FK_acc_avatars` FOREIGN KEY (`userId`) REFERENCES `aowow_account` (`id`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=COMPACT; diff --git a/setup/sql/updates/1758578400_05.sql b/setup/sql/updates/1758578400_05.sql new file mode 100644 index 00000000..d36973d1 --- /dev/null +++ b/setup/sql/updates/1758578400_05.sql @@ -0,0 +1,3 @@ +DELETE FROM `aowow_config` WHERE `key` = 'screenshot_min_size'; +INSERT INTO `aowow_config` (`key`, `value`, `default`, `cat`, `flags`, `comment`) VALUES + ('screenshot_min_size', 200, 200, 1, 1153, "minimum dimensions of uploaded screenshots in px (yes, it's square, no it cant go below 200)"); diff --git a/setup/sql/updates/1758578400_06.sql b/setup/sql/updates/1758578400_06.sql new file mode 100644 index 00000000..9a8c8a9d --- /dev/null +++ b/setup/sql/updates/1758578400_06.sql @@ -0,0 +1,2 @@ +ALTER TABLE `aowow_screenshots` + MODIFY COLUMN `caption` varchar(200) DEFAULT NULL; diff --git a/setup/sql/updates/1758578400_07.sql b/setup/sql/updates/1758578400_07.sql new file mode 100644 index 00000000..4ba7523f --- /dev/null +++ b/setup/sql/updates/1758578400_07.sql @@ -0,0 +1,8 @@ +-- `key` is too small for our new configs +ALTER TABLE `aowow_config` + MODIFY COLUMN `key` varchar(50) NOT NULL; + +-- split generic upload in ss / vi +UPDATE `aowow_config` SET `key` = 'rep_reward_submit_screenshot', `comment` = 'uploaded screenshot was approved' WHERE `key` = 'rep_reward_upload'; +DELETE FROM `aowow_config` WHERE `key` = 'rep_reward_suggest_video'; +INSERT INTO `aowow_config` VALUES ('rep_reward_suggest_video', '10', '10', 5, 129, 'suggested video was approved'); diff --git a/setup/sql/updates/1758578400_08.sql b/setup/sql/updates/1758578400_08.sql new file mode 100644 index 00000000..f7ad1861 --- /dev/null +++ b/setup/sql/updates/1758578400_08.sql @@ -0,0 +1,8 @@ +-- update video storage +ALTER TABLE `aowow_videos` + ADD COLUMN `pos` tinyint unsigned NOT NULL AFTER `videoId`, + ADD COLUMN `url` varchar(64) NOT NULL COMMENT 'preview thumb' AFTER `pos`, + ADD COLUMN `width` smallint unsigned NOT NULL AFTER `url`, + ADD COLUMN `height` smallint unsigned NOT NULL AFTER `width`, + ADD COLUMN `name` varchar(64) DEFAULT NULL AFTER `height`, + MODIFY COLUMN `caption` varchar(200) DEFAULT NULL; diff --git a/setup/sql/updates/1758578400_09.sql b/setup/sql/updates/1758578400_09.sql new file mode 100644 index 00000000..3e2ddcbb --- /dev/null +++ b/setup/sql/updates/1758578400_09.sql @@ -0,0 +1,4 @@ +-- update article affected by cfg change +UPDATE `aowow_articles` SET + `article` = '[b]Reputation[/b] is a rough measurement of how much you participate in the community--it is earned by convincing your peers that you know what you’re talking about. Our community puts just as much work as our developers do into making our site as awesome as it is and reputation is meant as a way for you to track just how much work you\'re putting into us.\r\n\r\nThe primary means of gaining reputation is by posting quality comments on database entries (which are then voted up by other site members) and by general contributions to the site which can include actions like data and screenshot submissions. Whenever you leave a comment on a database entry, your peers can then vote on these comments, and those votes will cause you to gain reputation. You can also earn reputation by voting on other users\' comments and by sending in reports!\r\n\r\nBy being a good-standing and contributing user you will be able to earn both reputation and achievements for many of the same actions!\r\n\r\n[h3]Reputation Gains[/h3]\r\n[div style=\"max-width:400px\"][table class=grid]\r\n[tr][td][url=?account=signup]Registering[/url] an account[/td]\r\n[td align=right class=no-wrap]CFG_REP_REWARD_REGISTER reputation[/td]\r\n[/tr]\r\n[tr][td]Daily visit[/td]\r\n[td align=right class=no-wrap]CFG_REP_REWARD_DAILYVISIT reputation[/td]\r\n[/tr]\r\n[tr][td]Posting a comment[/td]\r\n[td align=right class=no-wrap]CFG_REP_REWARD_COMMENT reputation[/td]\r\n[/tr]\r\n[tr][td]Your comment was voted up (each upvote)[/td]\r\n[td align=right class=no-wrap]CFG_REP_REWARD_UPVOTED reputation[/td]\r\n[/tr]\r\n[tr][td]Submitting a screenshot[/td]\r\n[td align=right class=no-wrap]CFG_REP_REWARD_SUBMIT_SCREENSHOT reputation[/td]\r\n[/tr]\r\n[tr][td]Suggesting a video[/td]\r\n[td align=right class=no-wrap]CFG_REP_REWARD_SUGGEST_VIDEO reputation[/td]\r\n[/tr]\r\n[tr][td]Submitting a guide (approved)[/td]\r\n[td align=right class=no-wrap]CFG_REP_REWARD_ARTICLE reputation[/td]\r\n[/tr]\r\n[tr][td]Filing a report (accepted)[/td]\r\n[td align=right class=no-wrap]CFG_REP_REWARD_GOOD_REPORT reputation[/td]\r\n[/tr]\r\n[/table][/div]\r\n\r\n\r\n[h3]Site Privileges[/h3]\r\nThe higher your reputation level, the more privileges you gain. Earn a high enough reputation to unlock additional rewards, in the form of new privileges around the site!\r\n[pad]\r\n[div style=\"max-width:400px\"][table class=grid]\r\n[tr][td]Post comments[/td]\r\n[td align=right class=no-wrap]CFG_REP_REQ_COMMENT reputation[/td]\r\n[/tr]\r\n[tr][td]Upvote on comments[/td]\r\n[td align=right class=no-wrap]CFG_REP_REQ_UPVOTE reputation[/td]\r\n[/tr]\r\n[tr][td]Downvote on comments[/td]\r\n[td align=right class=no-wrap]CFG_REP_REQ_DOWNVOTE reputation[/td]\r\n[/tr]\r\n[tr][td]More votes per day[/td]\r\n[td align=right class=no-wrap]CFG_REP_REQ_VOTEMORE_BASE reputation[/td]\r\n[/tr]\r\n[tr][td]Comment votes worth more[/td]\r\n[td align=right class=no-wrap]CFG_REP_REQ_SUPERVOTE reputation[/td]\r\n[/tr]\r\n[/table][/div]\r\n[pad]\r\n[url=?privileges]Check out full details on site privileges you can earn![/url]\r\n' +WHERE `url` = 'reputation' AND `locale` = 0; diff --git a/setup/sql/updates/1758578400_10.sql b/setup/sql/updates/1758578400_10.sql new file mode 100644 index 00000000..5e7cd68d --- /dev/null +++ b/setup/sql/updates/1758578400_10.sql @@ -0,0 +1,2 @@ +-- set on_set_fn check +UPDATE `aowow_config` SET `flags` = `flags` | 1024 WHERE `key` = 'cache_mode'; diff --git a/setup/sql/updates/1758578400_11.sql b/setup/sql/updates/1758578400_11.sql new file mode 100644 index 00000000..e525afa0 --- /dev/null +++ b/setup/sql/updates/1758578400_11.sql @@ -0,0 +1,3 @@ +ALTER TABLE `aowow_account` + ADD COLUMN `debug` tinyint(1) NOT NULL DEFAULT 0 COMMENT 'show ids in lists user option' AFTER `userGroups`, + MODIFY COLUMN `description` text NOT NULL DEFAULT ''; diff --git a/setup/sql/updates/1758578400_12.sql b/setup/sql/updates/1758578400_12.sql new file mode 100644 index 00000000..ad48339e --- /dev/null +++ b/setup/sql/updates/1758578400_12.sql @@ -0,0 +1,11 @@ +ALTER TABLE `aowow_user_ratings` + DROP KEY `FK_acc_co_rate_user`, + DROP FOREIGN KEY `FK_userId`, + DROP PRIMARY KEY; + +ALTER TABLE `aowow_user_ratings` MODIFY `userId` int unsigned NULL; + +ALTER TABLE `aowow_user_ratings` + ADD UNIQUE KEY (`type`,`entry`,`userId`), + ADD KEY `FK_acc_co_rate_user` (`userId`), + ADD CONSTRAINT FK_userId FOREIGN KEY (`userId`) REFERENCES aowow_account(`id`) ON DELETE SET NULL ON UPDATE CASCADE; diff --git a/setup/sql/updates/1758578400_13.sql b/setup/sql/updates/1758578400_13.sql new file mode 100644 index 00000000..c055f944 --- /dev/null +++ b/setup/sql/updates/1758578400_13.sql @@ -0,0 +1,2 @@ +ALTER TABLE `aowow_account` + ADD COLUMN `renameCooldown` int unsigned NOT NULL DEFAULT 0 COMMENT 'timestamp when rename is available again' AFTER `updateValue`; diff --git a/setup/sql/updates/1758578400_14.sql b/setup/sql/updates/1758578400_14.sql new file mode 100644 index 00000000..3445704e --- /dev/null +++ b/setup/sql/updates/1758578400_14.sql @@ -0,0 +1,2 @@ +DELETE FROM `aowow_config` WHERE `key` = 'acc_rename_decay'; +INSERT INTO `aowow_config` VALUES ('acc_rename_decay', 30 * 24 * 60 * 60, '30 * 24 * 60 * 60', 3, 129, 'delay between username changes'); diff --git a/setup/sql/updates/1758578400_15.sql b/setup/sql/updates/1758578400_15.sql new file mode 100644 index 00000000..32ee0455 --- /dev/null +++ b/setup/sql/updates/1758578400_15.sql @@ -0,0 +1,3 @@ +DELETE FROM `aowow_config` WHERE `key` = 'acc_max_avatar_uploads'; +INSERT INTO `aowow_config` (`key`, `value`, `default`, `cat`, `flags`, `comment`) VALUES + ('acc_max_avatar_uploads', 10, 10, 3, 129, 'premium users may upload this many avatars'); diff --git a/setup/sql/updates/1758578400_16.sql b/setup/sql/updates/1758578400_16.sql new file mode 100644 index 00000000..4b2f0d43 --- /dev/null +++ b/setup/sql/updates/1758578400_16.sql @@ -0,0 +1,2 @@ +ALTER TABLE `aowow_account` + ADD COLUMN `avatarborder` tinyint unsigned NOT NULL DEFAULT 2 AFTER `avatar`; diff --git a/setup/sql/updates/1758578400_17.sql b/setup/sql/updates/1758578400_17.sql new file mode 100644 index 00000000..03adf418 --- /dev/null +++ b/setup/sql/updates/1758578400_17.sql @@ -0,0 +1 @@ +UPDATE `aowow_dbversion` SET `build` = CONCAT(IFNULL(`build`, ''), ' globaljs'); 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/updates/1433023200_01.sql b/setup/sql/updates/v1.0/1433023200_01.sql similarity index 100% rename from setup/updates/1433023200_01.sql rename to setup/sql/updates/v1.0/1433023200_01.sql diff --git a/setup/updates/1433023200_02.sql b/setup/sql/updates/v1.0/1433023200_02.sql similarity index 100% rename from setup/updates/1433023200_02.sql rename to setup/sql/updates/v1.0/1433023200_02.sql diff --git a/setup/updates/1433793600_01.sql b/setup/sql/updates/v1.0/1433793600_01.sql similarity index 100% rename from setup/updates/1433793600_01.sql rename to setup/sql/updates/v1.0/1433793600_01.sql diff --git a/setup/updates/1435777200_01.sql b/setup/sql/updates/v1.1/1435777200_01.sql similarity index 100% rename from setup/updates/1435777200_01.sql rename to setup/sql/updates/v1.1/1435777200_01.sql diff --git a/setup/updates/1436392800_01.sql b/setup/sql/updates/v1.1/1436392800_01.sql similarity index 100% rename from setup/updates/1436392800_01.sql rename to setup/sql/updates/v1.1/1436392800_01.sql diff --git a/setup/updates/1436619600_01.sql b/setup/sql/updates/v1.1/1436619600_01.sql similarity index 100% rename from setup/updates/1436619600_01.sql rename to setup/sql/updates/v1.1/1436619600_01.sql diff --git a/setup/updates/1436634000_01.sql b/setup/sql/updates/v1.1/1436634000_01.sql similarity index 100% rename from setup/updates/1436634000_01.sql rename to setup/sql/updates/v1.1/1436634000_01.sql diff --git a/setup/updates/1436634000_02.sql b/setup/sql/updates/v1.1/1436634000_02.sql similarity index 100% rename from setup/updates/1436634000_02.sql rename to setup/sql/updates/v1.1/1436634000_02.sql diff --git a/setup/updates/1436735830_01.sql b/setup/sql/updates/v1.1/1436735830_01.sql similarity index 100% rename from setup/updates/1436735830_01.sql rename to setup/sql/updates/v1.1/1436735830_01.sql diff --git a/setup/updates/1436739821_01.sql b/setup/sql/updates/v1.1/1436739821_01.sql similarity index 100% rename from setup/updates/1436739821_01.sql rename to setup/sql/updates/v1.1/1436739821_01.sql diff --git a/setup/updates/1436903207_01.sql b/setup/sql/updates/v1.1/1436903207_01.sql similarity index 100% rename from setup/updates/1436903207_01.sql rename to setup/sql/updates/v1.1/1436903207_01.sql diff --git a/setup/updates/1437329787_01.sql b/setup/sql/updates/v1.1/1437329787_01.sql similarity index 100% rename from setup/updates/1437329787_01.sql rename to setup/sql/updates/v1.1/1437329787_01.sql diff --git a/setup/updates/1437430574_01.sql b/setup/sql/updates/v1.1/1437430574_01.sql similarity index 100% rename from setup/updates/1437430574_01.sql rename to setup/sql/updates/v1.1/1437430574_01.sql diff --git a/setup/updates/1437472069_01.sql b/setup/sql/updates/v1.1/1437472069_01.sql similarity index 100% rename from setup/updates/1437472069_01.sql rename to setup/sql/updates/v1.1/1437472069_01.sql diff --git a/setup/updates/1438620486_01.sql b/setup/sql/updates/v1.1/1438620486_01.sql similarity index 100% rename from setup/updates/1438620486_01.sql rename to setup/sql/updates/v1.1/1438620486_01.sql diff --git a/setup/updates/1438715648_01.sql b/setup/sql/updates/v1.1/1438715648_01.sql similarity index 100% rename from setup/updates/1438715648_01.sql rename to setup/sql/updates/v1.1/1438715648_01.sql diff --git a/setup/updates/1438878038_01.sql b/setup/sql/updates/v1.1/1438878038_01.sql similarity index 100% rename from setup/updates/1438878038_01.sql rename to setup/sql/updates/v1.1/1438878038_01.sql diff --git a/setup/updates/1438970456_01.sql b/setup/sql/updates/v1.1/1438970456_01.sql similarity index 100% rename from setup/updates/1438970456_01.sql rename to setup/sql/updates/v1.1/1438970456_01.sql diff --git a/setup/updates/1439297934_01.sql b/setup/sql/updates/v1.1/1439297934_01.sql similarity index 100% rename from setup/updates/1439297934_01.sql rename to setup/sql/updates/v1.1/1439297934_01.sql diff --git a/setup/updates/1439469082_01.sql b/setup/sql/updates/v1.1/1439469082_01.sql similarity index 100% rename from setup/updates/1439469082_01.sql rename to setup/sql/updates/v1.1/1439469082_01.sql diff --git a/setup/updates/1439590146_01.sql b/setup/sql/updates/v1.1/1439590146_01.sql similarity index 100% rename from setup/updates/1439590146_01.sql rename to setup/sql/updates/v1.1/1439590146_01.sql diff --git a/setup/updates/1439840492_01.sql b/setup/sql/updates/v1.1/1439840492_01.sql similarity index 100% rename from setup/updates/1439840492_01.sql rename to setup/sql/updates/v1.1/1439840492_01.sql diff --git a/setup/updates/1439909965_01.sql b/setup/sql/updates/v1.1/1439909965_01.sql similarity index 100% rename from setup/updates/1439909965_01.sql rename to setup/sql/updates/v1.1/1439909965_01.sql diff --git a/setup/updates/1439924313_01.sql b/setup/sql/updates/v1.1/1439924313_01.sql similarity index 100% rename from setup/updates/1439924313_01.sql rename to setup/sql/updates/v1.1/1439924313_01.sql diff --git a/setup/updates/1445293761_01.sql b/setup/sql/updates/v1.1/1445293761_01.sql similarity index 100% rename from setup/updates/1445293761_01.sql rename to setup/sql/updates/v1.1/1445293761_01.sql diff --git a/setup/updates/1446293928_01.sql b/setup/sql/updates/v1.1/1446293928_01.sql similarity index 100% rename from setup/updates/1446293928_01.sql rename to setup/sql/updates/v1.1/1446293928_01.sql diff --git a/setup/updates/1446917082_01.sql b/setup/sql/updates/v1.1/1446917082_01.sql similarity index 100% rename from setup/updates/1446917082_01.sql rename to setup/sql/updates/v1.1/1446917082_01.sql diff --git a/setup/updates/1448204650_01.sql b/setup/sql/updates/v1.1/1448204650_01.sql similarity index 100% rename from setup/updates/1448204650_01.sql rename to setup/sql/updates/v1.1/1448204650_01.sql diff --git a/setup/updates/1448727750_01.sql b/setup/sql/updates/v1.1/1448727750_01.sql similarity index 100% rename from setup/updates/1448727750_01.sql rename to setup/sql/updates/v1.1/1448727750_01.sql diff --git a/setup/updates/1452718627_01.sql b/setup/sql/updates/v1.1/1452718627_01.sql similarity index 100% rename from setup/updates/1452718627_01.sql rename to setup/sql/updates/v1.1/1452718627_01.sql diff --git a/setup/updates/1456585695_01.sql b/setup/sql/updates/v1.1/1456585695_01.sql similarity index 100% rename from setup/updates/1456585695_01.sql rename to setup/sql/updates/v1.1/1456585695_01.sql diff --git a/setup/updates/1484926142_01.sql b/setup/sql/updates/v1.1/1484926142_01.sql similarity index 100% rename from setup/updates/1484926142_01.sql rename to setup/sql/updates/v1.1/1484926142_01.sql diff --git a/setup/updates/1486948902_01.sql b/setup/sql/updates/v1.1/1486948902_01.sql similarity index 100% rename from setup/updates/1486948902_01.sql rename to setup/sql/updates/v1.1/1486948902_01.sql diff --git a/setup/updates/1487858459_01.sql b/setup/sql/updates/v1.1/1487858459_01.sql similarity index 100% rename from setup/updates/1487858459_01.sql rename to setup/sql/updates/v1.1/1487858459_01.sql diff --git a/setup/updates/1488061468_01.sql b/setup/sql/updates/v1.1/1488061468_01.sql similarity index 100% rename from setup/updates/1488061468_01.sql rename to setup/sql/updates/v1.1/1488061468_01.sql diff --git a/setup/updates/1488745158_01.sql b/setup/sql/updates/v1.1/1488745158_01.sql similarity index 100% rename from setup/updates/1488745158_01.sql rename to setup/sql/updates/v1.1/1488745158_01.sql diff --git a/setup/updates/1488745158_02.sql b/setup/sql/updates/v1.1/1488745158_02.sql similarity index 100% rename from setup/updates/1488745158_02.sql rename to setup/sql/updates/v1.1/1488745158_02.sql diff --git a/setup/updates/1489291710_01.sql b/setup/sql/updates/v1.1/1489291710_01.sql similarity index 100% rename from setup/updates/1489291710_01.sql rename to setup/sql/updates/v1.1/1489291710_01.sql diff --git a/setup/updates/1489673970_01.sql b/setup/sql/updates/v1.1/1489673970_01.sql similarity index 100% rename from setup/updates/1489673970_01.sql rename to setup/sql/updates/v1.1/1489673970_01.sql diff --git a/setup/updates/1489942886_01.sql b/setup/sql/updates/v1.1/1489942886_01.sql similarity index 100% rename from setup/updates/1489942886_01.sql rename to setup/sql/updates/v1.1/1489942886_01.sql diff --git a/setup/updates/1489942886_02.sql b/setup/sql/updates/v1.1/1489942886_02.sql similarity index 100% rename from setup/updates/1489942886_02.sql rename to setup/sql/updates/v1.1/1489942886_02.sql diff --git a/setup/updates/1489942886_03.sql b/setup/sql/updates/v1.1/1489942886_03.sql similarity index 100% rename from setup/updates/1489942886_03.sql rename to setup/sql/updates/v1.1/1489942886_03.sql diff --git a/setup/updates/1489964225_01.sql b/setup/sql/updates/v1.1/1489964225_01.sql similarity index 100% rename from setup/updates/1489964225_01.sql rename to setup/sql/updates/v1.1/1489964225_01.sql diff --git a/setup/updates/1490028370_01.sql b/setup/sql/updates/v1.1/1490028370_01.sql similarity index 100% rename from setup/updates/1490028370_01.sql rename to setup/sql/updates/v1.1/1490028370_01.sql diff --git a/setup/updates/1490049258_01.sql b/setup/sql/updates/v1.1/1490049258_01.sql similarity index 100% rename from setup/updates/1490049258_01.sql rename to setup/sql/updates/v1.1/1490049258_01.sql diff --git a/setup/updates/1490789225_01.sql b/setup/sql/updates/v1.1/1490789225_01.sql similarity index 100% rename from setup/updates/1490789225_01.sql rename to setup/sql/updates/v1.1/1490789225_01.sql diff --git a/setup/updates/1490815301_01.sql b/setup/sql/updates/v1.1/1490815301_01.sql similarity index 100% rename from setup/updates/1490815301_01.sql rename to setup/sql/updates/v1.1/1490815301_01.sql diff --git a/setup/updates/1490912249_01.sql b/setup/sql/updates/v1.1/1490912249_01.sql similarity index 100% rename from setup/updates/1490912249_01.sql rename to setup/sql/updates/v1.1/1490912249_01.sql diff --git a/setup/updates/1491915058_01.sql b/setup/sql/updates/v1.1/1491915058_01.sql similarity index 100% rename from setup/updates/1491915058_01.sql rename to setup/sql/updates/v1.1/1491915058_01.sql diff --git a/setup/updates/1491915872_01.sql b/setup/sql/updates/v1.1/1491915872_01.sql similarity index 100% rename from setup/updates/1491915872_01.sql rename to setup/sql/updates/v1.1/1491915872_01.sql diff --git a/setup/updates/1493755253_01.sql b/setup/sql/updates/v1.1/1493755253_01.sql similarity index 100% rename from setup/updates/1493755253_01.sql rename to setup/sql/updates/v1.1/1493755253_01.sql diff --git a/setup/updates/1493756026_01.sql b/setup/sql/updates/v1.1/1493756026_01.sql similarity index 100% rename from setup/updates/1493756026_01.sql rename to setup/sql/updates/v1.1/1493756026_01.sql diff --git a/setup/updates/1493857753_01.sql b/setup/sql/updates/v1.1/1493857753_01.sql similarity index 100% rename from setup/updates/1493857753_01.sql rename to setup/sql/updates/v1.1/1493857753_01.sql diff --git a/setup/updates/1493924184_01.sql b/setup/sql/updates/v1.1/1493924184_01.sql similarity index 100% rename from setup/updates/1493924184_01.sql rename to setup/sql/updates/v1.1/1493924184_01.sql diff --git a/setup/updates/1494853933_01.sql b/setup/sql/updates/v1.1/1494853933_01.sql similarity index 100% rename from setup/updates/1494853933_01.sql rename to setup/sql/updates/v1.1/1494853933_01.sql diff --git a/setup/updates/1504448040_01.sql b/setup/sql/updates/v1.1/1504448040_01.sql similarity index 100% rename from setup/updates/1504448040_01.sql rename to setup/sql/updates/v1.1/1504448040_01.sql diff --git a/setup/updates/1521735363_01.sql b/setup/sql/updates/v1.1/1521735363_01.sql similarity index 100% rename from setup/updates/1521735363_01.sql rename to setup/sql/updates/v1.1/1521735363_01.sql diff --git a/setup/updates/1521735363_02.sql b/setup/sql/updates/v1.1/1521735363_02.sql similarity index 100% rename from setup/updates/1521735363_02.sql rename to setup/sql/updates/v1.1/1521735363_02.sql diff --git a/setup/sql/updates/v1.2/1522146994_01.sql b/setup/sql/updates/v1.2/1522146994_01.sql new file mode 100644 index 00000000..fbebd0aa --- /dev/null +++ b/setup/sql/updates/v1.2/1522146994_01.sql @@ -0,0 +1,2 @@ +DELETE FROM `aowow_config` WHERE `key` IN ('profiler_queue', 'profiler_enable'); +INSERT INTO `aowow_config` VALUES ('profiler_enable', '0', 7, 132, 'default: 0 - enable/disable profiler feature'); diff --git a/setup/sql/updates/v1.2/1522230798_01.sql b/setup/sql/updates/v1.2/1522230798_01.sql new file mode 100644 index 00000000..78531b79 --- /dev/null +++ b/setup/sql/updates/v1.2/1522230798_01.sql @@ -0,0 +1,279 @@ +SET FOREIGN_KEY_CHECKS = 0; + +ALTER TABLE `aowow_creature_waypoints` + CHANGE COLUMN `point` `point` SMALLINT UNSIGNED NOT NULL AFTER `creatureOrPath`; + +ALTER TABLE `aowow_errors` + CHANGE COLUMN `file` `file` VARCHAR(150) NOT NULL AFTER `phpError`; + +UPDATE `aowow_dbversion` SET `sql` = CONCAT(IFNULL(`sql`, ''), ' spawns'); + +ALTER TABLE `aowow_account` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_account_banned` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_account_bannedips` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_account_cookies` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_account_excludes` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_account_profiles` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_account_reputation` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_account_weightscale_data` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_account_weightscales` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_achievement` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_achievementcategory` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_achievementcriteria` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_announcements` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_articles` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_classes` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_comments` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_comments_rates` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_config` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_creature` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_creature_sounds` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_creature_waypoints` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_currencies` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_dbversion` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_emotes` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_emotes_aliasses` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_emotes_sounds` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_errors` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_events` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_factions` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_factiontemplate` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_glyphproperties` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_holidays` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_home_featuredbox` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_home_featuredbox_overlay` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_home_oneliner` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_home_titles` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_icons` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_item_stats` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_itemenchantment` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_itemenchantmentcondition` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_itemextendedcost` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_itemlimitcategory` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_itemrandomenchant` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_itemrandomproppoints` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_items` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_items_sounds` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_itemset` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_lock` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_loot_link` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_mailtemplate` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_objects` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_pet` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_profiler_arena_team` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_profiler_arena_team_member` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_profiler_completion` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_profiler_excludes` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_profiler_guild` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_profiler_guild_rank` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_profiler_items` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_profiler_pets` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_profiler_profiles` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_profiler_sync` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_quests` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_quests_startend` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_races` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_races_sounds` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_reports` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_scalingstatdistribution` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_scalingstatvalues` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_screenshots` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_shapeshiftforms` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_skillline` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_sounds` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_sounds_files` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_source` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_sourcestrings` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_spawns` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_spell` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_spell_sounds` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_spelldifficulty` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_spellfocusobject` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_spelloverride` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_spellrange` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_spellvariables` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_talents` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_taxinodes` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_taxipath` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_titles` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_totemcategory` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_videos` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_zones` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_zones_sounds` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_account` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_account_banned` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_account_bannedips` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_account_cookies` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_account_reputation` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_account_weightscale_data` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_account_weightscales` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_achievement` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_achievementcategory` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_achievementcriteria` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_announcements` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_articles` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_characters` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_classes` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_comments` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_comments_rates` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_config` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_creature` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_creature_sounds` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_creature_waypoints` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_currencies` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_dbversion` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_emotes` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_emotes_aliasses` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_emotes_sounds` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_errors` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_events` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_factions` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_factiontemplate` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_glyphproperties` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_holidays` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_home_featuredbox` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_home_featuredbox_overlay` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_home_oneliner` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_home_titles` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_icons` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_item_stats` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_itemenchantment` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_itemenchantmentcondition` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_itemextendedcost` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_itemlimitcategory` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_itemrandomenchant` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_itemrandomproppoints` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_items` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_items_sounds` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_itemset` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_lock` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_loot_link` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_mailtemplate` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_objects` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_pet` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_powerdisplay` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_quests` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_quests_startend` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_races` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_races_sounds` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_reports` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_scalingstatdistribution` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_scalingstatvalues` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_screenshots` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_shapeshiftforms` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_skillline` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_sounds` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_sounds_files` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_source` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_sourcestrings` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_spawns` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_spell` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_spell_sounds` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_spelldifficulty` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_spellfocusobject` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_spelloverride` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_spellrange` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_spellvariables` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_talents` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_taxinodes` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_taxipath` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_titles` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_totemcategory` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_videos` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_zones` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_zones_sounds` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_achievement` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_account` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_account_banned` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_account_bannedips` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_account_cookies` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_account_excludes` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_account_profiles` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_account_reputation` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_account_weightscale_data` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_account_weightscales` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_achievement` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_achievementcategory` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_achievementcriteria` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_announcements` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_articles` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_classes` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_comments` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_comments_rates` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_config` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_creature` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_creature_sounds` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_creature_waypoints` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_currencies` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_dbversion` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_emotes` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_emotes_aliasses` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_emotes_sounds` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_errors` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_events` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_factions` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_factiontemplate` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_glyphproperties` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_holidays` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_home_featuredbox` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_home_featuredbox_overlay` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_home_oneliner` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_home_titles` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_icons` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_item_stats` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_itemenchantment` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_itemenchantmentcondition` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_itemextendedcost` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_itemlimitcategory` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_itemrandomenchant` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_itemrandomproppoints` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_items` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_items_sounds` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_itemset` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_lock` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_loot_link` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_mailtemplate` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_objects` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_pet` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_profiler_arena_team` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_profiler_arena_team_member` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_profiler_completion` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_profiler_excludes` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_profiler_guild` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_profiler_guild_rank` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_profiler_items` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_profiler_pets` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_profiler_profiles` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_profiler_sync` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_quests` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_quests_startend` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_races` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_races_sounds` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_reports` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_scalingstatdistribution` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_scalingstatvalues` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_screenshots` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_shapeshiftforms` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_skillline` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_sounds` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_sounds_files` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_source` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_sourcestrings` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_spawns` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_spell` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_spell_sounds` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_spelldifficulty` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_spellfocusobject` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_spelloverride` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_spellrange` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_spellvariables` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_talents` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_taxinodes` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_taxipath` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_titles` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_totemcategory` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_videos` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_zones` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `aowow_zones_sounds` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +SET FOREIGN_KEY_CHECKS = 1; diff --git a/setup/sql/updates/v1.2/1522321542_01.sql b/setup/sql/updates/v1.2/1522321542_01.sql new file mode 100644 index 00000000..ed40c674 --- /dev/null +++ b/setup/sql/updates/v1.2/1522321542_01.sql @@ -0,0 +1,114 @@ +DROP TABLE IF EXISTS `aowow_home_titles`; +CREATE TABLE `aowow_home_titles` ( + `id` SMALLINT(5) UNSIGNED NOT NULL AUTO_INCREMENT, + `editorId` INT(10) UNSIGNED NULL DEFAULT NULL, + `editDate` INT(10) UNSIGNED NOT NULL, + `active` TINYINT(1) UNSIGNED NOT NULL, + `locale` TINYINT(3) UNSIGNED NOT NULL, + `title` VARCHAR(100) NOT NULL, + PRIMARY KEY (`id`), + UNIQUE INDEX `locale_title` (`locale`, `title`), + INDEX `FK_acc_hTitles` (`editorId`), + CONSTRAINT `FK_acc_hTitles` FOREIGN KEY (`editorId`) REFERENCES `aowow_account` (`id`) ON UPDATE CASCADE ON DELETE SET NULL +) COLLATE='utf8_general_ci' ENGINE=InnoDB; + +INSERT INTO `aowow_home_titles` (editorId, editDate, active, locale, title) VALUES + (0, 1522321542, 1, 0, 'That\'s a 50 DKP plus!'), + (0, 1522321542, 1, 0, 'We\'ve got what you need!'), + (0, 1522321542, 1, 0, 'You haven\'t found the secret title yet.'), + (0, 1522321542, 1, 0, '...and knowing is half the battle!'), + (0, 1522321542, 1, 0, 'Good news, everyone!'), + (0, 1522321542, 1, 0, '+1, Insightful'), + (0, 1522321542, 1, 0, 'More effective than a [Booterang].'), + (0, 1522321542, 1, 0, 'There is no cow level.'), + (0, 1522321542, 1, 0, 'We\'ve got more style than a fashion designer who knows CSS.'), + (0, 1522321542, 1, 3, 'Eure Fertigkeit in WoW hat sich auf 450 erhöht.'), + (0, 1522321542, 1, 0, 'If you use your mouse to search, you won\'t be able to click on Rend.'), + (0, 1522321542, 1, 2, 'Tout est dans l\'élégance.'), + (0, 1522321542, 1, 2, 'Rend les chargements supportables depuis 2006.'), + (0, 1522321542, 1, 2, 'Vous allez revenir.'), + (0, 1522321542, 1, 2, 'Base de données extraordinaire'), + (0, 1522321542, 1, 2, 'Si vous lisez ceci, arrêtez d\'appuyer sur F5.'), + (0, 1522321542, 1, 3, 'Und der Tag ist gerettet.'), + (0, 1522321542, 1, 3, 'Jetzt in allen bekannten Internetzen verfügbar!'), + (0, 1522321542, 1, 3, 'Morgens, halb drei in Nordend'), + (0, 1522321542, 1, 3, 'Macht auch Euren Webbrowser glücklich!'), + (0, 1522321542, 1, 3, 'Hier findet Ihr sogar Mankriks Frau.'), + (0, 1522321542, 1, 6, 'Base de datos extraordinaria de WoW'), + (0, 1522321542, 1, 6, 'La única cosa en la que los ninjas y los piratas estan de acuerdo.'), + (0, 1522321542, 1, 6, 'La elegancia lo es todo.'), + (0, 1522321542, 1, 6, 'Hace feliz a los navegadores.'), + (0, 1522321542, 1, 8, 'Ты ещё вернёшься.'), + (0, 1522321542, 1, 8, 'Осваивание нового босса - 45 золота на ремонт. Персональный эпический предмет - 650 золотых'), + (0, 1522321542, 1, 8, 'Не именной. Поделитесь им с друзьями!'), + (0, 1522321542, 1, 8, 'Если вы здесь впервые, то вам необходимо воспользоваться поиском!'), + (0, 1522321542, 1, 8, 'Приколы Мулгора без чата в Мулгоре.'), + (0, 1522321542, 1, 2, 'Les trois premières lettres veulent tout dire.'), + (0, 1522321542, 1, 2, 'Trouvez la femme de Mankrik grâce à lui.'), + (0, 1522321542, 1, 6, 'Tu habilidad con WoW se ha incrementado a 450.'), + (0, 1522321542, 1, 6, 'Buscando uno más: Tú'), + (0, 1522321542, 1, 8, 'Первые три буквы говорят сами за себя.'), + (0, 1522321542, 1, 8, 'У нас больше стиля, чем у дизайнера, знающего CSS.'), + (0, 1522321542, 1, 0, 'Preventing wipes since 2006.'), + (0, 1522321542, 1, 0, 'Never gonna give you up. Never gonna let you down.'), + (0, 1522321542, 1, 0, 'The closest thing to an F1 key for WoW.'), + (0, 1522321542, 1, 2, 'Non lié. Partagez-le avec vos amis !'), + (0, 1522321542, 1, 2, 'Votre navigateur l\'adore !'), + (0, 1522321542, 1, 3, 'Verhindert Wipes seit 2006.'), + (0, 1522321542, 1, 6, '+1, Utilidad'), + (0, 1522321542, 1, 6, 'Épico, como tu líder de facción.'), + (0, 1522321542, 1, 8, 'Он такой один...'), + (0, 1522321542, 1, 8, 'Если вы это читаете, то прекратите обновлять страницу.'), + (0, 1522321542, 1, 0, 'If you are reading this, stop pressing F5.'), + (0, 1522321542, 1, 2, 'Chasse les jours pluvieux.'), + (0, 1522321542, 1, 3, '+1, Hilfreich'), + (0, 1522321542, 1, 3, 'Episch - markant - dreifach verzaubert'), + (0, 1522321542, 1, 8, 'Работает как положено.'), + (0, 1522321542, 1, 0, 'Flagged for awesome.'), + (0, 1522321542, 1, 0, 'Thrall-tested, Jaina-approved.'), + (0, 1522321542, 1, 8, 'Всё дело в элегантности.'), + (0, 1522321542, 1, 0, 'What does it mean?'), + (0, 1522321542, 1, 0, 'YOU ARE NOW PREPARED!'), + (0, 1522321542, 1, 0, 'srsly'), + (0, 1522321542, 1, 2, 'C\'est comme prétendre être malade et aller à la plage, mais pour les bases de données.'), + (0, 1522321542, 1, 3, 'Thrall-getestet, Jaina-genehmigt'), + (0, 1522321542, 1, 6, 'Haciendo las pantallas de carga más soportables desde el 2006'), + (0, 1522321542, 1, 8, 'Создан быть лидером.'), + (0, 1522321542, 1, 0, 'You\'ll say "Wow" every time.'), + (0, 1522321542, 1, 0, 'Dataz! We need more dataz!'), + (0, 1522321542, 1, 0, 'Your skill in WoW has increased to 450.'), + (0, 1522321542, 1, 3, 'Eleganz ist alles.'), + (0, 1522321542, 1, 8, '+1, Полезный'), + (0, 1522321542, 1, 8, 'Ух ты!'), + (0, 1522321542, 1, 0, 'Sometimes there is fire. You need to not be in it.'), + (0, 1522321542, 1, 0, 'Working as intended.'), + (0, 1522321542, 1, 2, 'La seule chose sur laquelle les ninjas et les pirates sont d\'accord.'), + (0, 1522321542, 1, 3, 'Nicht seelengebunden. Teilt es mit Euren Freunden!'), + (0, 1522321542, 1, 8, 'Теперь доступен во всех известных Интернетах!'), + (0, 1522321542, 1, 8, 'Вы получаете добычу: [Легендарное Знание]'), + (0, 1522321542, 1, 0, 'You\'ll be back.'), + (0, 1522321542, 1, 0, 'Epic like your faction leader.'), + (0, 1522321542, 1, 3, 'Manchmal gibt es Feuer. Ihr dürft nicht drin stehen.'), + (0, 1522321542, 1, 3, 'Wer das hier lesen kann, drückt zu oft F5.'), + (0, 1522321542, 1, 6, '¡Datos! ¡Más Datos!'), + (0, 1522321542, 1, 8, 'НЯМ НЯМ НЯМ'), + (0, 1522321542, 1, 2, 'Testé par Thrall, approuvé par Jaina.'), + (0, 1522321542, 1, 8, 'Сделайте его вашей новой расовой возможностью уже сегодня!'), + (0, 1522321542, 1, 0, 'We do math, so you don\'t have to.'), + (0, 1522321542, 1, 0, 'OM NOM NOM'), + (0, 1522321542, 1, 0, 'Now available on all known internets!'), + (0, 1522321542, 1, 0, 'We brake for dataz.'), + (0, 1522321542, 1, 3, 'Neues von der Obstverkäuferfront'), + (0, 1522321542, 1, 6, 'Las primeras tres palabras lo dicen todo.'), + (0, 1522321542, 1, 8, 'Это как будто сказать всем, что ты болен, а самому пойти на пляж, - только для баз данных.'), + (0, 1522321542, 1, 8, 'Меняем семечки на данные!'), + (0, 1522321542, 1, 0, 'It\'s all about elegance.'), + (0, 1522321542, 1, 0, 'Never underestimate the power of the Scout\'s code.'), + (0, 1522321542, 1, 6, 'Elimina los días lluviosos.'), + (0, 1522321542, 1, 0, 'You just won the game.'), + (0, 1522321542, 1, 8, 'Данные! Нам надо больше данных!'), + (0, 1522321542, 1, 0, 'WoW Database Extraordinaire'), + (0, 1522321542, 1, 0, 'No longer soulbound. Can now be shared with friends!'), + (0, 1522321542, 1, 0, 'The dataz you could be using.'), + (0, 1522321542, 1, 8, 'Превосходен, как лидер вашей фракции.'), + (0, 1522321542, 1, 6, '¡Regresarás!'); diff --git a/setup/sql/updates/v1.2/1522421324_01.sql b/setup/sql/updates/v1.2/1522421324_01.sql new file mode 100644 index 00000000..9a5a7a42 --- /dev/null +++ b/setup/sql/updates/v1.2/1522421324_01.sql @@ -0,0 +1,8 @@ +CREATE TABLE `aowow_account_favorites` ( + `userId` INT(11) UNSIGNED NOT NULL, + `type` SMALLINT(5) UNSIGNED NOT NULL, + `typeId` MEDIUMINT(8) UNSIGNED NOT NULL, + UNIQUE INDEX `userId_type_typeId` (`userId`, `type`, `typeId`), + INDEX `userId` (`userId`), + CONSTRAINT `FK_acc_favorites` FOREIGN KEY (`userId`) REFERENCES `aowow_account` (`id`) ON UPDATE CASCADE ON DELETE CASCADE +) COLLATE='utf8mb4_general_ci' ENGINE=InnoDB; diff --git a/setup/sql/updates/v1.2/1522499480_01.sql b/setup/sql/updates/v1.2/1522499480_01.sql new file mode 100644 index 00000000..84ba2de6 --- /dev/null +++ b/setup/sql/updates/v1.2/1522499480_01.sql @@ -0,0 +1,5 @@ +DELETE FROM `aowow_announcements` WHERE page = 'profile' OR page = 'profiler'; +INSERT INTO `aowow_announcements` (`page`, `name`, `groupMask`, `style`, `mode`, `status`, `text_loc0`, `text_loc2`, `text_loc3`, `text_loc6`, `text_loc8`) VALUES + ('profile', 'Help: Profiler', 0, 'padding-left: 80px; background-image: url(STATIC_URL/images/announcements/help-large.gif); background-position: 10px center', 1, 1, '[h3]First Time?[/h3]\r\n\r\nThe [b]Profiler[/b] tool lets you [span class=tip title="e.g. See how\'d you look as a different race, try different gear or talents, and more!"]edit your character[/span], find gear upgrades, check your gear score, and more!\r\n\r\n[ul]\r\n[li][b]Right-click[/b] slots to change items, add gems/enchants, or find upgrades.[/li]\r\n[li]Use the [b]Claim character[/b] button to add your own characters to your [url=/?user]user page[/url].[/li]\r\n[li]Save a modified character to your Aowow account by using the [b]Save as[/b] button.[/li]\r\n[li][b]Statistics[/b] will update in real time as you make tweaks.[/li]\r\n[/ul]\r\n\r\nFor more information, check out our extensive [url=?help=profiler]help page[/url]!', '', '[h3]Euer erster Besuch?[/h3]\n\nDas [b]Profiler[/b]-Werkzeug erlaubt es euch [span class=tip title="z.B. Seht, wie Ihr als anderes Volk aussehen würdet, probiert andere Ausrüstung oder Talente aus, und mehr!"]euren Charakter zu bearbeiten[/span], besser Ausrüstung zu finden, eure Ausrüstungswertung zu vergleichen, und vieles mehr!\n\n[ul]\n[li][b]Rechts-klickt[/b] Plätze um Gegenstände zu tauschen, Edelsteine/Verzauberungen hinzuzufügen, oder bessere AUsrüstung zu finden.[/li]\n[li]Benutzt [b]Charakter beanspruchen[/b] um eure eigenen Charaktere Eurer [url=?user]Benutzerseite[/url] hinzuzufügen.[/li]\n[li]Speichert einen modifizierten Charakter in Eurem Aowow-Konto, indem Ihr [b]Speichern als[/b] benutzt.[/li]\n[li]Die [b]Statistiken[/b] aktualisieren sich in Echtzeit, während Ihr Änderungen durchführt.[/li]\n[/ul]\n\nWeitere Informationen findet Ihr auf unserer umfangreichen [url=?help=profiler]Hilfeseite[/url]!', '', ''), + ('profiler', 'Help: Profiler', 0, 'padding-left: 80px; background-image: url(STATIC_URL/images/announcements/help-large.gif); background-position: 10px center', 1, 1, '[h3]First Time?[/h3]\r\n\r\nThe [b]Profiler[/b] tool lets you [span class=tip title="e.g. See how\'d you look as a different race, try different gear or talents, and more!"]edit your character[/span], find gear upgrades, check your gear score, and more!\r\n\r\n[ul]\r\n[li][b]Right-click[/b] slots to change items, add gems/enchants, or find upgrades.[/li]\r\n[li]Use the [b]Claim character[/b] button to add your own characters to your [url=/?user]user page[/url].[/li]\r\n[li]Save a modified character to your Aowow account by using the [b]Save as[/b] button.[/li]\r\n[li][b]Statistics[/b] will update in real time as you make tweaks.[/li]\r\n[/ul]\r\n\r\nFor more information, check out our extensive [url=?help=profiler]help page[/url]!', '', '[h3]Euer erster Besuch?[/h3]\n\nDas [b]Profiler[/b]-Werkzeug erlaubt es euch [span class=tip title="z.B. Seht, wie Ihr als anderes Volk aussehen würdet, probiert andere Ausrüstung oder Talente aus, und mehr!"]euren Charakter zu bearbeiten[/span], besser Ausrüstung zu finden, eure Ausrüstungswertung zu vergleichen, und vieles mehr!\n\n[ul]\n[li][b]Rechts-klickt[/b] Plätze um Gegenstände zu tauschen, Edelsteine/Verzauberungen hinzuzufügen, oder bessere AUsrüstung zu finden.[/li]\n[li]Benutzt [b]Charakter beanspruchen[/b] um eure eigenen Charaktere Eurer [url=?user]Benutzerseite[/url] hinzuzufügen.[/li]\n[li]Speichert einen modifizierten Charakter in Eurem Aowow-Konto, indem Ihr [b]Speichern als[/b] benutzt.[/li]\n[li]Die [b]Statistiken[/b] aktualisieren sich in Echtzeit, während Ihr Änderungen durchführt.[/li]\n[/ul]\n\nWeitere Informationen findet Ihr auf unserer umfangreichen [url=?help=profiler]Hilfeseite[/url]!', '', ''); + diff --git a/setup/sql/updates/v1.2/1522673571_01.sql b/setup/sql/updates/v1.2/1522673571_01.sql new file mode 100644 index 00000000..1f39b34a --- /dev/null +++ b/setup/sql/updates/v1.2/1522673571_01.sql @@ -0,0 +1,13 @@ +ALTER TABLE `aowow_factions` + ADD COLUMN `baseRepRaceMask1` SMALLINT(5) UNSIGNED NOT NULL AFTER `repIdx`, + ADD COLUMN `baseRepRaceMask2` SMALLINT(5) UNSIGNED NOT NULL AFTER `baseRepRaceMask1`, + ADD COLUMN `baseRepRaceMask3` SMALLINT(5) UNSIGNED NOT NULL AFTER `baseRepRaceMask2`, + ADD COLUMN `baseRepRaceMask4` SMALLINT(5) UNSIGNED NOT NULL AFTER `baseRepRaceMask3`, + ADD COLUMN `baseRepClassMask1` SMALLINT(5) UNSIGNED NOT NULL AFTER `baseRepRaceMask4`, + ADD COLUMN `baseRepClassMask2` SMALLINT(5) UNSIGNED NOT NULL AFTER `baseRepClassMask1`, + ADD COLUMN `baseRepClassMask3` SMALLINT(5) UNSIGNED NOT NULL AFTER `baseRepClassMask2`, + ADD COLUMN `baseRepClassMask4` SMALLINT(5) UNSIGNED NOT NULL AFTER `baseRepClassMask3`, + ADD COLUMN `baseRepValue1` MEDIUMINT NOT NULL AFTER `baseRepClassMask4`, + ADD COLUMN `baseRepValue2` MEDIUMINT NOT NULL AFTER `baseRepValue1`, + ADD COLUMN `baseRepValue3` MEDIUMINT NOT NULL AFTER `baseRepValue2`, + ADD COLUMN `baseRepValue4` MEDIUMINT NOT NULL AFTER `baseRepValue3`; diff --git a/setup/sql/updates/v1.2/1524389034_01.sql b/setup/sql/updates/v1.2/1524389034_01.sql new file mode 100644 index 00000000..66c71f5b --- /dev/null +++ b/setup/sql/updates/v1.2/1524389034_01.sql @@ -0,0 +1,33 @@ +REPLACE INTO aowow_profiler_excludes (`type`, `typeId`, `groups`, `comment`) VALUES + (6, 46197, 2, 'X-51 Nether-Rocket - TCG loot'), + (6, 46199, 2, 'X-51 Nether-Rocket X-TREME - TCG loot'), + (6, 75614, 1, 'Celestial Steed - unavailable'), + (6, 26656, 1, 'Black Qiraji Battle Tank - unavailable'), + (6, 43899, 1, 'Brewfest Ram - unavailable'), + (6, 58983, 8, 'Big Blizzard Bear - promotion'), + (6, 49193, 1, 'Vengeful Nether Drake - unavailable'), + (6, 58615, 1, 'Brutal Nether Drake - unavailable'), + (6, 64927, 1, 'Deadly Gladiator\'s Frost Wyrm - unavailable'), + (6, 65439, 1, 'Furious Gladiator\'s Frost Wyrm - unavailable'), + (6, 67336, 1, 'Relentless Gladiator\'s Frost Wyrm - unavailable'), + (6, 71810, 1, 'Wrathful Gladiator\'s Frost Wyrm - unavailable'), + (11, 122, 1, 'RealmFirst Kel\'T Title - unavailable'), + (11, 159, 1, 'RealmFirst Algalon Title - unavailable'), + (11, 120, 1, 'RealmFirst Maly Title - unavailable'), + (11, 170, 1, 'RealmFirst TotGC Title - unavailable'), + (11, 139, 1, 'RealmFirst Sarth Title - unavailable'), + (11, 158, 1, 'RealmFirst Yogg Title - unavailable'), + (6, 40405, 16, 'Lucky - wrong region'), + (6, 45174, 16, 'Golden Pig - wrong region'), + (6, 67527, 16, 'Onyx Panther - wrong region'), + (6, 28505, 8, 'Poley - promotion'), + (6, 45175, 16, 'Silver Pig - wrong region'), + (6, 28487, 1, 'Terky - unavailable'), + (6, 23531, 16, 'Tiny Green Dragon - wrong region'), + (6, 23530, 16, 'Tiny Red Dragon - wrong region'), + (8, 70, 1024, 'Syndicate - max rank is neutral'), + (6, 48408, 16, 'Essence of Competition - wrong region'); + +DELETE FROM aowow_profiler_excludes WHERE `type` = 6 AND `typeId` IN (66122, 66123, 66124, 61309, 61451, 75596); + +UPDATE `aowow_dbversion` SET `build` = CONCAT(IFNULL(`build`, ''), ' enchants profiler'); diff --git a/setup/sql/updates/v1.2/1524905453_01.sql b/setup/sql/updates/v1.2/1524905453_01.sql new file mode 100644 index 00000000..46bd12d1 --- /dev/null +++ b/setup/sql/updates/v1.2/1524905453_01.sql @@ -0,0 +1 @@ +UPDATE aowow_dbversion SET `sql` = CONCAT(IFNULL(`sql`, ''), ' item_stats'); diff --git a/setup/sql/updates/v1.2/1524907872_01.sql b/setup/sql/updates/v1.2/1524907872_01.sql new file mode 100644 index 00000000..cb36cb0e --- /dev/null +++ b/setup/sql/updates/v1.2/1524907872_01.sql @@ -0,0 +1 @@ +ALTER TABLE `aowow_profiler_profiles` CHANGE COLUMN `name` `name` VARCHAR(50) NOT NULL COLLATE 'utf8mb4_bin' AFTER `user`; diff --git a/setup/sql/updates/v1.2/1525094404_01.sql b/setup/sql/updates/v1.2/1525094404_01.sql new file mode 100644 index 00000000..e2ec8c33 --- /dev/null +++ b/setup/sql/updates/v1.2/1525094404_01.sql @@ -0,0 +1 @@ +UPDATE aowow_dbversion SET `sql` = CONCAT(IFNULL(`sql`, ''), ' quests'), `build` = CONCAT(IFNULL(`build`, ''), ' profiler'); diff --git a/setup/sql/updates/v1.2/1525726941_01.sql b/setup/sql/updates/v1.2/1525726941_01.sql new file mode 100644 index 00000000..0cc98418 --- /dev/null +++ b/setup/sql/updates/v1.2/1525726941_01.sql @@ -0,0 +1,121 @@ +ALTER TABLE `aowow_achievement` + ADD COLUMN `name_loc4` VARCHAR(86) NOT NULL AFTER `name_loc3`, + ADD COLUMN `description_loc4` TEXT NOT NULL AFTER `description_loc3`, + ADD COLUMN `reward_loc4` VARCHAR(92) NOT NULL AFTER `reward_loc3`; + +ALTER TABLE `aowow_achievementcategory` + ADD COLUMN `name_loc4` VARCHAR(255) NOT NULL AFTER `name_loc3`; + +ALTER TABLE `aowow_achievementcriteria` + ADD COLUMN `name_loc4` VARCHAR(128) NOT NULL AFTER `name_loc3`; + +ALTER TABLE `aowow_announcements` + ADD COLUMN `text_loc4` TEXT NOT NULL AFTER `text_loc3`; + +ALTER TABLE `aowow_classes` + ADD COLUMN `name_loc4` VARCHAR(128) NOT NULL AFTER `name_loc3`; + +ALTER TABLE `aowow_creature` + ADD COLUMN `name_loc4` VARCHAR(100) NULL DEFAULT NULL AFTER `name_loc3`, + ADD COLUMN `subname_loc4` VARCHAR(100) NULL DEFAULT NULL AFTER `subname_loc3`; + +ALTER TABLE `aowow_currencies` + ADD COLUMN `name_loc4` VARCHAR(64) NOT NULL AFTER `name_loc3`, + ADD COLUMN `description_loc4` VARCHAR(256) NOT NULL AFTER `description_loc3`; + +ALTER TABLE `aowow_emotes` + ADD COLUMN `target_loc4` VARCHAR(95) NULL DEFAULT NULL AFTER `target_loc3`, + ADD COLUMN `noTarget_loc4` VARCHAR(85) NULL DEFAULT NULL AFTER `noTarget_loc3`, + ADD COLUMN `self_loc4` VARCHAR(85) NULL DEFAULT NULL AFTER `self_loc3`; + +ALTER TABLE `aowow_factions` + ADD COLUMN `name_loc4` VARCHAR(40) NOT NULL AFTER `name_loc3`; + +ALTER TABLE `aowow_holidays` + ADD COLUMN `name_loc4` VARCHAR(36) NOT NULL AFTER `name_loc3`, + ADD COLUMN `description_loc4` TEXT NULL AFTER `description_loc3`; + +ALTER TABLE `aowow_home_featuredbox` + ADD COLUMN `text_loc4` TEXT NOT NULL AFTER `text_loc3`; + +ALTER TABLE `aowow_home_featuredbox_overlay` + ADD COLUMN `title_loc4` VARCHAR(100) NOT NULL AFTER `title_loc3`; + +ALTER TABLE `aowow_home_oneliner` + ADD COLUMN `text_loc4` VARCHAR(200) NOT NULL AFTER `text_loc3`; + +ALTER TABLE `aowow_itemenchantment` + ADD COLUMN `name_loc4` VARCHAR(100) NOT NULL AFTER `name_loc3`; + +ALTER TABLE `aowow_itemlimitcategory` + ADD COLUMN `name_loc4` VARCHAR(34) NOT NULL AFTER `name_loc3`; + +ALTER TABLE `aowow_itemrandomenchant` + ADD COLUMN `name_loc4` VARCHAR(250) NOT NULL AFTER `name_loc3`; + +ALTER TABLE `aowow_items` + ADD COLUMN `name_loc4` VARCHAR(127) NULL DEFAULT NULL AFTER `name_loc3`, + ADD COLUMN `description_loc4` VARCHAR(255) NULL DEFAULT NULL AFTER `description_loc3`; + +ALTER TABLE `aowow_itemset` + ADD COLUMN `name_loc4` VARCHAR(255) NOT NULL AFTER `name_loc3`, + ADD COLUMN `bonusText_loc4` VARCHAR(256) NOT NULL AFTER `bonusText_loc3`; + +ALTER TABLE `aowow_mailtemplate` + ADD COLUMN `subject_loc4` VARCHAR(128) NOT NULL AFTER `subject_loc3`, + ADD COLUMN `text_loc4` TEXT NOT NULL AFTER `text_loc3`; + +ALTER TABLE `aowow_objects` + ADD COLUMN `name_loc4` VARCHAR(100) NULL DEFAULT NULL AFTER `name_loc3`; + +ALTER TABLE `aowow_pet` + ADD COLUMN `name_loc4` VARCHAR(64) NOT NULL AFTER `name_loc3`; + +ALTER TABLE `aowow_quests` + ADD COLUMN `name_loc4` TEXT NULL AFTER `name_loc3`, + ADD COLUMN `objectives_loc4` TEXT NULL AFTER `objectives_loc3`, + ADD COLUMN `details_loc4` TEXT NULL AFTER `details_loc3`, + ADD COLUMN `end_loc4` TEXT NULL AFTER `end_loc3`, + ADD COLUMN `offerReward_loc4` TEXT NULL AFTER `offerReward_loc3`, + ADD COLUMN `requestItems_loc4` TEXT NULL AFTER `requestItems_loc3`, + ADD COLUMN `completed_loc4` TEXT NULL AFTER `completed_loc3`, + ADD COLUMN `objectiveText1_loc4` TEXT NULL AFTER `objectiveText1_loc3`, + ADD COLUMN `objectiveText2_loc4` TEXT NULL AFTER `objectiveText2_loc3`, + ADD COLUMN `objectiveText3_loc4` TEXT NULL AFTER `objectiveText3_loc3`, + ADD COLUMN `objectiveText4_loc4` TEXT NULL AFTER `objectiveText4_loc3`; + +ALTER TABLE `aowow_races` + ADD COLUMN `name_loc4` VARCHAR(64) NOT NULL AFTER `name_loc3`; + +ALTER TABLE `aowow_skillline` + ADD COLUMN `name_loc4` VARCHAR(64) NOT NULL AFTER `name_loc3`, + ADD COLUMN `description_loc4` TEXT NOT NULL AFTER `description_loc3`; + +ALTER TABLE `aowow_sourcestrings` + ADD COLUMN `source_loc4` VARCHAR(128) NOT NULL AFTER `source_loc3`; + +ALTER TABLE `aowow_spell` + ADD COLUMN `name_loc4` VARCHAR(85) NOT NULL AFTER `name_loc3`, + ADD COLUMN `rank_loc4` VARCHAR(22) NOT NULL AFTER `rank_loc3`, + ADD COLUMN `description_loc4` TEXT NOT NULL AFTER `description_loc3`, + ADD COLUMN `buff_loc4` TEXT NOT NULL AFTER `buff_loc3`; + +ALTER TABLE `aowow_spellfocusobject` + ADD COLUMN `name_loc4` VARCHAR(95) NOT NULL AFTER `name_loc3`; + +ALTER TABLE `aowow_spellrange` + ADD COLUMN `name_loc4` VARCHAR(27) NOT NULL AFTER `name_loc3`; + +ALTER TABLE `aowow_taxinodes` + ADD COLUMN `name_loc4` VARCHAR(55) NOT NULL AFTER `name_loc3`; + +ALTER TABLE `aowow_titles` + ADD COLUMN `male_loc4` VARCHAR(37) NOT NULL AFTER `male_loc3`, + ADD COLUMN `female_loc4` VARCHAR(39) NOT NULL AFTER `female_loc3`; + +ALTER TABLE `aowow_totemcategory` + ADD COLUMN `name_loc4` VARCHAR(31) NOT NULL AFTER `name_loc3`; + +ALTER TABLE `aowow_zones` + ADD COLUMN `name_loc4` VARCHAR(120) NOT NULL AFTER `name_loc3`; + diff --git a/setup/sql/updates/v1.2/1525726941_02.sql b/setup/sql/updates/v1.2/1525726941_02.sql new file mode 100644 index 00000000..784944a3 --- /dev/null +++ b/setup/sql/updates/v1.2/1525726941_02.sql @@ -0,0 +1,34 @@ +-- drop deprecated dbc data +DROP TABLE IF EXISTS `dbc_achievement_category`; +DROP TABLE IF EXISTS `dbc_achievement_criteria`; +DROP TABLE IF EXISTS `dbc_achievement`; +DROP TABLE IF EXISTS `dbc_areatable`; +DROP TABLE IF EXISTS `dbc_chartitles`; +DROP TABLE IF EXISTS `dbc_chrclasses`; +DROP TABLE IF EXISTS `dbc_creaturefamily`; +DROP TABLE IF EXISTS `dbc_emotestexxtdata`; +DROP TABLE IF EXISTS `dbc_faction`; +DROP TABLE IF EXISTS `dbc_holidaydescriptions`; +DROP TABLE IF EXISTS `dbc_holidaynames`; +DROP TABLE IF EXISTS `dbc_itemlimitcategory`; +DROP TABLE IF EXISTS `dbc_itemrandomproperties`; +DROP TABLE IF EXISTS `dbc_itemrandomsuffix`; +DROP TABLE IF EXISTS `dbc_itemset`; +DROP TABLE IF EXISTS `dbc_lfgdungeons`; +DROP TABLE IF EXISTS `dbc_mailtemplate`; +DROP TABLE IF EXISTS `dbc_map`; +DROP TABLE IF EXISTS `dbc_skillline`; +DROP TABLE IF EXISTS `dbc_spell`; +DROP TABLE IF EXISTS `dbc_spellfocusobject`; +DROP TABLE IF EXISTS `dbc_spellitemenchantment`; +DROP TABLE IF EXISTS `dbc_spellrange`; +DROP TABLE IF EXISTS `dbc_spellshapeshiftform`; +DROP TABLE IF EXISTS `dbc_talenttab`; +DROP TABLE IF EXISTS `dbc_taxinodes`; +DROP TABLE IF EXISTS `dbc_totemcategory`; + +-- update config +UPDATE `aowow_config` SET `comment` = 'default: 0x15D - allowed locales - 0:English, 2:French, 3:German, 4:Chinese, 6:Spanish, 8:Russian' WHERE `key` = 'locales'; + +-- rebuild affected files +UPDATE `aowow_dbversion` SET `sql` = CONCAT(IFNULL(`sql`, ''), ' achievementcategory achievementcriteria itemenchantment itemlimitcategory mailtemplate spellfocusobject spellrange totemcategory classes factions holidays itemrandomenchant races shapeshiftforms skillline emotes achievement creature currencies objects pet quests spell taxi titles items zones itemset'), `build` = CONCAT(IFNULL(`build`, ''), ' complexImg locales statistics talentCalc pets glyphs itemsets enchants gems profiler'); diff --git a/setup/sql/updates/v1.2/1527333495_01.sql b/setup/sql/updates/v1.2/1527333495_01.sql new file mode 100644 index 00000000..53f2cc57 --- /dev/null +++ b/setup/sql/updates/v1.2/1527333495_01.sql @@ -0,0 +1,8 @@ +-- clear synced chars to prevent conflicts +DELETE FROM `aowow_profiler_profiles` WHERE `realmGUID` IS NOT NULL; +-- clear queue +DELETE FROM `aowow_profiler_sync`; +-- update unique index +ALTER TABLE `aowow_profiler_profiles` + DROP INDEX `realm_realmGUID_name`, + ADD UNIQUE INDEX `realm_realmGUID` (`realm`, `realmGUID`); diff --git a/setup/sql/updates/v1.2/1527343032_01.sql b/setup/sql/updates/v1.2/1527343032_01.sql new file mode 100644 index 00000000..3bd7bb82 --- /dev/null +++ b/setup/sql/updates/v1.2/1527343032_01.sql @@ -0,0 +1,2 @@ +ALTER TABLE `aowow_reports` + ADD COLUMN `createDate` INT UNSIGNED NOT NULL AFTER `status`; diff --git a/setup/sql/updates/v1.2/1528316365_01.sql b/setup/sql/updates/v1.2/1528316365_01.sql new file mode 100644 index 00000000..5f4bb18b --- /dev/null +++ b/setup/sql/updates/v1.2/1528316365_01.sql @@ -0,0 +1,8 @@ +DROP TABLE IF EXISTS `dbc_spell`; + +ALTER TABLE `aowow_spell` + ADD COLUMN `targets` MEDIUMINT UNSIGNED NOT NULL AFTER `stanceMaskNot`, + CHANGE COLUMN `castTime` `castTime` FLOAT UNSIGNED NOT NULL AFTER `spellFocusObject`, + CHANGE COLUMN `powerType` `powerType` SMALLINT NOT NULL AFTER `duration`; + +UPDATE `aowow_dbversion` SET `sql` = CONCAT(IFNULL(`sql`, ''), ' spell'); diff --git a/setup/sql/updates/v1.2/1531668311_01.sql b/setup/sql/updates/v1.2/1531668311_01.sql new file mode 100644 index 00000000..56a16e66 --- /dev/null +++ b/setup/sql/updates/v1.2/1531668311_01.sql @@ -0,0 +1,20 @@ +DROP TABLE IF EXISTS `dbc_areatrigger`; +DROP TABLE IF EXISTS `aowow_areatrigger`; +CREATE TABLE `aowow_areatrigger` ( + `id` INT(10) UNSIGNED NOT NULL, + `cuFlags` INT(10) UNSIGNED NOT NULL, + `type` SMALLINT(5) UNSIGNED NOT NULL, + `name` VARCHAR(100) NULL DEFAULT NULL, + `orientation` FLOAT NOT NULL, + `quest` MEDIUMINT(8) UNSIGNED NULL DEFAULT NULL, + `teleportA` SMALLINT(5) UNSIGNED NULL DEFAULT NULL, + `teleportX` FLOAT UNSIGNED NULL DEFAULT NULL, + `teleportY` FLOAT UNSIGNED NULL DEFAULT NULL, + `teleportO` FLOAT NULL DEFAULT NULL, + `teleportF` TINYINT(4) UNSIGNED NULL DEFAULT NULL, + PRIMARY KEY (`id`), + INDEX `quest` (`quest`), + INDEX `type` (`type`) +) COLLATE='utf8mb4_general_ci' ENGINE=MyISAM; + +UPDATE `aowow_dbversion` SET `sql` = CONCAT(IFNULL(`sql`, ''), ' areatrigger'); diff --git a/setup/sql/updates/v1.2/1543271379_01.sql b/setup/sql/updates/v1.2/1543271379_01.sql new file mode 100644 index 00000000..6d163b57 --- /dev/null +++ b/setup/sql/updates/v1.2/1543271379_01.sql @@ -0,0 +1 @@ +UPDATE `aowow_dbversion` SET `build` = CONCAT(IFNULL(`build`, ''), ' profiler'); diff --git a/setup/sql/updates/v1.2/1543774778_01.sql b/setup/sql/updates/v1.2/1543774778_01.sql new file mode 100644 index 00000000..65183783 --- /dev/null +++ b/setup/sql/updates/v1.2/1543774778_01.sql @@ -0,0 +1,8 @@ +-- clear synced chars to prevent conflicts +DELETE FROM `aowow_profiler_profiles` WHERE `realmGUID` IS NOT NULL; +-- clear queue +DELETE FROM `aowow_profiler_sync`; +-- update unique index +ALTER TABLE `aowow_profiler_profiles` + ADD COLUMN `renameItr` TINYINT UNSIGNED NOT NULL DEFAULT '0' AFTER `name`, + ADD INDEX `name` (`name`); diff --git a/setup/sql/updates/v1.2/1544826311_01.sql b/setup/sql/updates/v1.2/1544826311_01.sql new file mode 100644 index 00000000..8e335d3b --- /dev/null +++ b/setup/sql/updates/v1.2/1544826311_01.sql @@ -0,0 +1 @@ +UPDATE aowow_dbversion SET `sql` = CONCAT(IFNULL(`sql`, ''), ' creature'); diff --git a/setup/sql/updates/v1.2/1581549222_01.sql b/setup/sql/updates/v1.2/1581549222_01.sql new file mode 100644 index 00000000..177faed6 --- /dev/null +++ b/setup/sql/updates/v1.2/1581549222_01.sql @@ -0,0 +1,22 @@ +DROP TABLE IF EXISTS `aowow_mailtemplate`; +DROP TABLE IF EXISTS `aowow_mails`; + +CREATE TABLE `aowow_mails` ( + `id` smallint(5) NOT NULL, + `subject_loc0` varchar(128) COLLATE utf8mb4_unicode_ci NOT NULL, + `subject_loc2` varchar(128) COLLATE utf8mb4_unicode_ci NOT NULL, + `subject_loc3` varchar(128) COLLATE utf8mb4_unicode_ci NOT NULL, + `subject_loc4` varchar(128) COLLATE utf8mb4_unicode_ci NOT NULL, + `subject_loc6` varchar(128) COLLATE utf8mb4_unicode_ci NOT NULL, + `subject_loc8` varchar(128) COLLATE utf8mb4_unicode_ci NOT NULL, + `text_loc0` text COLLATE utf8mb4_unicode_ci NOT NULL, + `text_loc2` text COLLATE utf8mb4_unicode_ci NOT NULL, + `text_loc3` text COLLATE utf8mb4_unicode_ci NOT NULL, + `text_loc4` text COLLATE utf8mb4_unicode_ci NOT NULL, + `text_loc6` text COLLATE utf8mb4_unicode_ci NOT NULL, + `text_loc8` text COLLATE utf8mb4_unicode_ci NOT NULL, + `attachment` smallint(5) unsigned NOT NULL DEFAULT 0, + PRIMARY KEY (`id`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +UPDATE aowow_dbversion SET `sql` = CONCAT(IFNULL(`sql`, ''), ' mails'); diff --git a/setup/sql/updates/v1.2/1582485391_01.sql b/setup/sql/updates/v1.2/1582485391_01.sql new file mode 100644 index 00000000..57b3b7f3 --- /dev/null +++ b/setup/sql/updates/v1.2/1582485391_01.sql @@ -0,0 +1,4 @@ +ALTER TABLE `aowow_quests` + ADD COLUMN `breadcrumbForQuestId` MEDIUMINT(8) NOT NULL DEFAULT '0' AFTER `nextQuestId`; + +UPDATE `aowow_dbversion` SET `sql` = CONCAT(IFNULL(`sql`, ''), ' quests'); diff --git a/setup/sql/updates/v1.2/1582486388_01.sql b/setup/sql/updates/v1.2/1582486388_01.sql new file mode 100644 index 00000000..452379f6 --- /dev/null +++ b/setup/sql/updates/v1.2/1582486388_01.sql @@ -0,0 +1,6 @@ +ALTER TABLE `aowow_creature` + CHANGE COLUMN `trainerSpell` `trainerRequirement` SMALLINT UNSIGNED NOT NULL DEFAULT '0' AFTER `trainerType`, + DROP COLUMN `trainerClass`, + DROP COLUMN `trainerRace`; + +UPDATE `aowow_dbversion` SET `sql` = CONCAT(IFNULL(`sql`, ''), ' spell, source, creature'); diff --git a/setup/sql/updates/v1.2/1586092309_01.sql b/setup/sql/updates/v1.2/1586092309_01.sql new file mode 100644 index 00000000..670ba855 --- /dev/null +++ b/setup/sql/updates/v1.2/1586092309_01.sql @@ -0,0 +1,16 @@ +REPLACE INTO `aowow_profiler_excludes` VALUES + (6, 28242, 1, "Icebane Breastplate"), + (6, 28243, 1, "Icebane Gauntlets"), + (6, 28244, 1, "Icebane Bracers"), + (6, 16986, 1, "Blood Talon"), + (6, 16987, 1, "Darkspear"), + (6, 16965, 1, "Bleakwood Hew"), + (6, 8366, 1, "Ironforge Chain"), + (6, 8368, 1, "Ironforge Gauntlets"), + (6, 9942, 1, "Mithril Scale Gloves"), + (6, 2671, 1, "Rough Bronze Bracers"), + (6, 16980, 1, "Rune Edge"), + (6, 16960, 1, "Thorium Greatsword"), + (6, 16967, 1, "Inlaid Thorium Hammer"), + (6, 30342, 1, "Red Smoke Flare"), + (6, 30343, 1, "Blue Smoke Flare"); diff --git a/setup/sql/updates/v1.2/1586458384_01.sql b/setup/sql/updates/v1.2/1586458384_01.sql new file mode 100644 index 00000000..1491468c --- /dev/null +++ b/setup/sql/updates/v1.2/1586458384_01.sql @@ -0,0 +1,24 @@ +REPLACE INTO `aowow_profiler_excludes` VALUES + (6, 28205, 1, "Glacial Gloves"), + (6, 28207, 1, "Glacial Vest"), + (6, 28208, 1, "Glacial Cloak"), + (6, 28209, 1, "Glacial Wrists"), + (6, 28222, 1, "Icy Scale Breastplate"), + (6, 28223, 1, "Icy Scale Gauntlets"), + (6, 28224, 1, "Icy Scale Bracers"), + (6, 28219, 1, "Polar Tunic"), + (6, 28220, 1, "Polar Gloves"), + (6, 28221, 1, "Polar Bracers"), + (6, 28021, 1, "Arcane Dust"), + (6, 44612, 1, "Enchant Gloves - Greater Blasting"), + (6, 62257, 1, "Enchant Weapon - Titanguard"), + (6, 31461, 1, "Heavy Netherweave Net"), + (6, 56048, 1, "Duskweave Boots"), + (6, 7636, 1, "Green Woolen Robe"), + (6, 8778, 1, "Boots of Darkness"), + (6, 12062, 1, "Stormcloth Pants"), + (6, 12063, 1, "Stormcloth Gloves"), + (6, 12068, 1, "Stormcloth Vest"), + (6, 12083, 1, "Stormcloth Headband"), + (6, 12087, 1, "Stormcloth Shoulders"), + (6, 12090, 1, "Stormcloth Boots"); diff --git a/setup/sql/updates/v1.2/1587314471_01.sql b/setup/sql/updates/v1.2/1587314471_01.sql new file mode 100644 index 00000000..a3b175a4 --- /dev/null +++ b/setup/sql/updates/v1.2/1587314471_01.sql @@ -0,0 +1 @@ +UPDATE aowow_dbversion SET `sql` = CONCAT(IFNULL(`sql`, ''), ' zones'); diff --git a/setup/sql/updates/v1.2/1588517065_01.sql b/setup/sql/updates/v1.2/1588517065_01.sql new file mode 100644 index 00000000..f891bc57 --- /dev/null +++ b/setup/sql/updates/v1.2/1588517065_01.sql @@ -0,0 +1,208 @@ +DROP TABLE IF EXISTS `aowow_loot_link`; +CREATE TABLE IF NOT EXISTS `aowow_loot_link` ( + `npcId` mediumint(8) NOT NULL COMMENT 'id > 0 normal; id < 0 heroic', + `objectId` mediumint(8) unsigned NOT NULL, + `priority` tinyint(1) unsigned NOT NULL COMMENT '1: use this npc from group encounter (others 0)', + `encounterId` mediumint(8) unsigned NOT NULL COMMENT 'as title reference', + UNIQUE KEY `npcId` (`npcId`), + KEY `objectId` (`objectId`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +INSERT INTO `aowow_loot_link` (`npcId`, `objectId`, `priority`, `encounterId`) VALUES + (17537, 185168, 1, 0), + (18434, 185169, 1, 0), + (17536, 185168, 0, 0), + (18432, 185169, 0, 0), + (19218, 184465, 1, 0), + (21525, 184849, 1, 0), + (19710, 184465, 0, 0), + (21526, 184849, 0, 0), + (28234, 190586, 0, 0), + (-28234, 193996, 0, 0), + (27656, 191349, 0, 0), + (31561, 193603, 0, 0), + (26533, 190663, 0, 0), + (31217, 193597, 0, 0), + (16064, 181366, 0, 692), + (30603, 193426, 0, 692), + (16065, 181366, 0, 692), + (30601, 193426, 0, 692), + (30549, 181366, 1, 692), + (30600, 193426, 1, 692), + (16063, 181366, 0, 692), + (30602, 193426, 0, 692), + (28859, 193905, 0, 0), + (31734, 193967, 0, 0), + (32930, 195046, 0, 0), + (33909, 195047, 0, 0), + (32865, 194313, 0, 0), + (33147, 194315, 0, 0), + (33350, 194957, 0, 0), + (-33350, 194958, 0, 0), + (32845, 194200, 0, 0), + (32846, 194201, 0, 0), + (32906, 194324, 0, 0), + (33360, 194325, 0, 0), + (32871, 194821, 0, 0), + (33070, 194822, 0, 0), + (35119, 195374, 0, 0), + (35518, 195375, 0, 0), + (34928, 195323, 0, 0), + (35517, 195324, 0, 0), + (34705, 195709, 0, 334), + (36088, 195710, 0, 334), + (34702, 195709, 0, 334), + (36082, 195710, 0, 334), + (34701, 195709, 0, 334), + (36083, 195710, 0, 334), + (34657, 195709, 0, 334), + (36086, 195710, 0, 334), + (34703, 195709, 0, 334), + (36087, 195710, 0, 334), + (35572, 195709, 0, 334), + (36089, 195710, 0, 334), + (35569, 195709, 1, 334), + (36085, 195710, 1, 334), + (35571, 195709, 0, 334), + (36090, 195710, 0, 334), + (35570, 195709, 0, 334), + (36091, 195710, 0, 334), + (35617, 195709, 0, 334), + (36084, 195710, 0, 334), + (34441, 195631, 1, 637), + (34442, 195632, 1, 637), + (34443, 195633, 1, 637), + (35749, 195635, 1, 637), + (34444, 195631, 0, 637), + (35740, 195632, 0, 637), + (35741, 195633, 0, 637), + (-35741, 195635, 0, 637), + (34445, 195631, 0, 637), + (35705, 195632, 0, 637), + (35706, 195633, 0, 637), + (-35706, 195635, 0, 637), + (34447, 195631, 0, 637), + (35683, 195632, 0, 637), + (35684, 195633, 0, 637), + (-35684, 195635, 0, 637), + (34448, 195631, 0, 637), + (35724, 195632, 0, 637), + (35725, 195633, 0, 637), + (-35725, 195635, 0, 637), + (34449, 195631, 0, 637), + (35689, 195632, 0, 637), + (35690, 195633, 0, 637), + (-35690, 195635, 0, 637), + (34450, 195631, 0, 637), + (35695, 195632, 0, 637), + (35696, 195633, 0, 637), + (-35696, 195635, 0, 637), + (34451, 195631, 0, 637), + (35671, 195632, 0, 637), + (35672, 195633, 0, 637), + (-35672, 195635, 0, 637), + (34453, 195631, 0, 637), + (35718, 195632, 0, 637), + (35719, 195633, 0, 637), + (-35719, 195635, 0, 637), + (34454, 195631, 0, 637), + (35711, 195632, 0, 637), + (35712, 195633, 0, 637), + (-35712, 195635, 0, 637), + (34455, 195631, 0, 637), + (35680, 195632, 0, 637), + (35681, 195633, 0, 637), + (-35681, 195635, 0, 637), + (34456, 195631, 0, 637), + (35708, 195632, 0, 637), + (35709, 195633, 0, 637), + (-35709, 195635, 0, 637), + (34458, 195631, 0, 637), + (35692, 195632, 0, 637), + (35693, 195633, 0, 637), + (-35693, 195635, 0, 637), + (34459, 195631, 0, 637), + (35686, 195632, 0, 637), + (35687, 195633, 0, 637), + (-35687, 195635, 0, 637), + (34460, 195631, 0, 637), + (35702, 195632, 0, 637), + (35703, 195633, 0, 637), + (-35703, 195635, 0, 637), + (34461, 195631, 0, 637), + (35743, 195632, 0, 637), + (35744, 195633, 0, 637), + (-35744, 195635, 0, 637), + (34463, 195631, 0, 637), + (35734, 195632, 0, 637), + (35735, 195633, 0, 637), + (-35735, 195635, 0, 637), + (34465, 195631, 0, 637), + (35746, 195632, 0, 637), + (35747, 195633, 0, 637), + (-35747, 195635, 0, 637), + (34466, 195631, 0, 637), + (35665, 195632, 0, 637), + (35666, 195633, 0, 637), + (-35666, 195635, 0, 637), + (34467, 195631, 0, 637), + (35662, 195632, 0, 637), + (35663, 195633, 0, 637), + (-35663, 195635, 0, 637), + (34468, 195631, 0, 637), + (35721, 195632, 0, 637), + (35722, 195633, 0, 637), + (-35722, 195635, 0, 637), + (34469, 195631, 0, 637), + (35714, 195632, 0, 637), + (35715, 195633, 0, 637), + (-35715, 195635, 0, 637), + (34470, 195631, 0, 637), + (35728, 195632, 0, 637), + (35729, 195633, 0, 637), + (-35729, 195635, 0, 637), + (34471, 195631, 0, 637), + (35668, 195632, 0, 637), + (35669, 195633, 0, 637), + (-35669, 195635, 0, 637), + (34472, 195631, 0, 637), + (35699, 195632, 0, 637), + (35700, 195633, 0, 637), + (-35700, 195635, 0, 637), + (34473, 195631, 0, 637), + (35674, 195632, 0, 637), + (35675, 195633, 0, 637), + (-35675, 195635, 0, 637), + (34474, 195631, 0, 637), + (35731, 195632, 0, 637), + (35732, 195633, 0, 637), + (-35732, 195635, 0, 637), + (34475, 195631, 0, 637), + (35737, 195632, 0, 637), + (35738, 195633, 0, 637), + (-35738, 195635, 0, 637), + (37226, 201710, 0, 0), + (-37226, 202336, 0, 0), + (36948, 202178, 0, 847), + (38157, 202180, 0, 847), + (38639, 202177, 0, 847), + (38640, 202179, 0, 847), + (36939, 202178, 0, 847), + (38156, 202180, 0, 847), + (38637, 202177, 0, 847), + (38638, 202179, 0, 847), + (9034, 169243, 0, 243), + (9035, 169243, 1, 243), + (9036, 169243, 0, 243), + (9037, 169243, 0, 243), + (9038, 169243, 0, 243), + (9039, 169243, 0, 243), + (9040, 169243, 0, 243), + (37813, 202238, 0, 0), + (38402, 202239, 0, 0), + (38582, 202240, 0, 0), + (38583, 202241, 0, 0), + (36789, 201959, 0, 0), + (-36789, 202338, 0, 0), + (38174, 202339, 0, 0), + (-38174, 202340, 0, 0); diff --git a/setup/sql/updates/v1.2/1590506556_01.sql b/setup/sql/updates/v1.2/1590506556_01.sql new file mode 100644 index 00000000..371db2ed --- /dev/null +++ b/setup/sql/updates/v1.2/1590506556_01.sql @@ -0,0 +1,11 @@ +DROP TABLE IF EXISTS `aowow_spawns_override`; +CREATE TABLE `aowow_spawns_override` ( + `type` smallint(5) unsigned NOT NULL, + `typeGuid` mediumint(9) NOT NULL, + `areaId` mediumint(8) unsigned NOT NULL, + `floor` mediumint(8) unsigned NOT NULL, + `revision` tinyint(3) unsigned NOT NULL COMMENT 'Aowow revision, when this override was applied', + PRIMARY KEY (`type`, `typeGuid`) USING BTREE +) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +UPDATE aowow_dbversion SET `sql` = CONCAT(IFNULL(`sql`, ''), ' spawns'); diff --git a/setup/sql/updates/v1.2/1590572038_01.sql b/setup/sql/updates/v1.2/1590572038_01.sql new file mode 100644 index 00000000..9c6ba9ce --- /dev/null +++ b/setup/sql/updates/v1.2/1590572038_01.sql @@ -0,0 +1 @@ +UPDATE aowow_dbversion SET `sql` = CONCAT(IFNULL(`sql`, ''), ' spawns'); diff --git a/setup/sql/updates/v1.2/1590687329_01.sql b/setup/sql/updates/v1.2/1590687329_01.sql new file mode 100644 index 00000000..3064d01c --- /dev/null +++ b/setup/sql/updates/v1.2/1590687329_01.sql @@ -0,0 +1 @@ +ALTER TABLE `aowow_account_cookies` DROP PRIMARY KEY, ADD INDEX `userId` (`userId`) USING BTREE; diff --git a/setup/sql/updates/v1.2/1591223185_01.sql b/setup/sql/updates/v1.2/1591223185_01.sql new file mode 100644 index 00000000..24ba29c2 --- /dev/null +++ b/setup/sql/updates/v1.2/1591223185_01.sql @@ -0,0 +1,9 @@ +DROP TABLE IF EXISTS `aowow_achievementcategory`; +CREATE TABLE `aowow_achievementcategory` ( + `id` smallint(5) unsigned NOT NULL DEFAULT '0', + `parentCat` smallint(5) unsigned NOT NULL DEFAULT '0', + `parentCat2` smallint(5) unsigned NOT NULL DEFAULT '0', + PRIMARY KEY (`id`) USING BTREE +) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +UPDATE aowow_dbversion SET `sql` = CONCAT(IFNULL(`sql`, ''), ' achievements'); diff --git a/setup/sql/updates/v1.2/1591451737_01.sql b/setup/sql/updates/v1.2/1591451737_01.sql new file mode 100644 index 00000000..82d87a5b --- /dev/null +++ b/setup/sql/updates/v1.2/1591451737_01.sql @@ -0,0 +1 @@ +UPDATE aowow_dbversion SET `build` = CONCAT(IFNULL(`build`, ''), ' itemsets'); diff --git a/setup/sql/updates/v1.2/1608244863_01.sql b/setup/sql/updates/v1.2/1608244863_01.sql new file mode 100644 index 00000000..1bd640ca --- /dev/null +++ b/setup/sql/updates/v1.2/1608244863_01.sql @@ -0,0 +1,9 @@ +ALTER TABLE `aowow_creature` + ADD COLUMN `resistance1` SMALLINT NOT NULL DEFAULT 0 AFTER `armorMax`, + ADD COLUMN `resistance2` SMALLINT NOT NULL DEFAULT 0 AFTER `resistance1`, + ADD COLUMN `resistance3` SMALLINT NOT NULL DEFAULT 0 AFTER `resistance2`, + ADD COLUMN `resistance4` SMALLINT NOT NULL DEFAULT 0 AFTER `resistance3`, + ADD COLUMN `resistance5` SMALLINT NOT NULL DEFAULT 0 AFTER `resistance4`, + ADD COLUMN `resistance6` SMALLINT NOT NULL DEFAULT 0 AFTER `resistance5`; + +UPDATE `aowow_dbversion` SET `sql` = CONCAT(IFNULL(`sql`, ''), ' creature'); diff --git a/setup/sql/updates/v1.2/1609244309_01.sql b/setup/sql/updates/v1.2/1609244309_01.sql new file mode 100644 index 00000000..a4f8b488 --- /dev/null +++ b/setup/sql/updates/v1.2/1609244309_01.sql @@ -0,0 +1,2 @@ +DROP TABLE aowow_itemrandomproppoints; +UPDATE aowow_dbversion SET `sql` = CONCAT(IFNULL(`sql`, ''), ' itemrandomproppoints'); diff --git a/setup/sql/updates/v1.2/1613670956_01.sql b/setup/sql/updates/v1.2/1613670956_01.sql new file mode 100644 index 00000000..0358020b --- /dev/null +++ b/setup/sql/updates/v1.2/1613670956_01.sql @@ -0,0 +1,3 @@ +REPLACE INTO aowow_config VALUES + ('acc_ext_create_url', '', 3, 0x88, 'default: - if auth mode is not self; link to external account creation'), + ('acc_ext_recover_url', '', 3, 0x88, 'default: - if auth mode is not self; link to external account recovery'); diff --git a/setup/sql/updates/v1.2/1645476214_01.sql b/setup/sql/updates/v1.2/1645476214_01.sql new file mode 100644 index 00000000..0a84fdc3 --- /dev/null +++ b/setup/sql/updates/v1.2/1645476214_01.sql @@ -0,0 +1,263 @@ +DROP TABLE IF EXISTS `aowow_setup_custom_data`; + +CREATE TABLE `aowow_setup_custom_data` ( + `command` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '', + `entry` int NOT NULL DEFAULT '0' COMMENT 'typeId', + `field` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '', + `value` varchar(100) COLLATE utf8mb4_general_ci DEFAULT NULL, + `comment` text COLLATE utf8mb4_general_ci, + KEY `aowow_setup_custom_data_command_IDX` (`command`) USING BTREE +) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; + +INSERT INTO aowow_setup_custom_data (command,entry,field,value,comment) 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]'); +INSERT INTO aowow_setup_custom_data (command,entry,field,value,comment) VALUES + ('zones',3849,'parentAreaId','3523','The Mechanar - Parent: Netherstorm [not set in map.dbc]'), + ('zones',3849,'parentX','87.3','The Mechanar - Entrance xPos'), + ('zones',3849,'parentY','51.1','The Mechanar - Entrance yPos'), + ('zones',3847,'parentAreaId','3523','The Botanica - Parent: Netherstorm [not set in map.dbc]'), + ('zones',3847,'parentX','71.7','The Botanica - Entrance xPos'), + ('zones',3847,'parentY','55.1','The Botanica - Entrance yPos'), + ('zones',3848,'parentAreaId','3523','The Arcatraz - Parent: Netherstorm [not set in map.dbc]'), + ('zones',3848,'parentX','74.3','The Arcatraz - Entrance xPos'), + ('zones',3848,'parentY','57.8','The Arcatraz - Entrance yPos'), + ('zones',3845,'parentAreaId','3523','Tempest Keep - Parent: Netherstorm [not set in map.dbc]'); +INSERT INTO aowow_setup_custom_data (command,entry,field,value,comment) VALUES + ('zones',3845,'parentX','73.5','Tempest Keep - Entrance xPos'), + ('zones',3845,'parentY','63.7','Tempest Keep - Entrance yPos'), + ('zones',3456,'parentAreaId','65','Naxxramas - Parent: Netherstorm [not set in map.dbc]'), + ('zones',3456,'parentX','87.3','Naxxramas - Entrance xPos'), + ('zones',3456,'parentY','87.3','Naxxramas - Entrance yPos'), + ('zones',4893,'parentAreaId','4812','The Frost Queen''s Lair - Parent: Icecrown Citadel'), + ('zones',4894,'parentAreaId','4812','Putricide''s Laboratory [..] - Parent: Icecrown Citadel'), + ('zones',4895,'parentAreaId','4812','The Crimson Hall - Parent: Icecrown Citadel'), + ('zones',4896,'parentAreaId','4812','The Frozen Throne - Parent: Icecrown Citadel'), + ('zones',4897,'parentAreaId','4812','The Sanctum of Blood - Parent: Icecrown Citadel'); +INSERT INTO aowow_setup_custom_data (command,entry,field,value,comment) VALUES + ('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'); +INSERT INTO aowow_setup_custom_data (command,entry,field,value,comment) VALUES + ('zones',2817,'levelMin','74','Crystalsong Forest - missing lfgDungeons entry'), + ('zones',1477,'cuFlags','1073741824','The Temple of Atal''Hakkar - 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'); +INSERT INTO aowow_setup_custom_data (command,entry,field,value,comment) VALUES + ('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',8,'roles','4','Mage - 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'); +INSERT INTO aowow_setup_custom_data (command,entry,field,value,comment) VALUES + ('classes',7,'roles','7','Shaman - mleDPS + rngDPS + Heal'), + ('classes',8,'roles','4','Mage - rngDPS'), + ('classes',8,'roles','4','Mage - rngDPS'), + ('classes',8,'roles','4','Mage - 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'); +INSERT INTO aowow_setup_custom_data (command,entry,field,value,comment) VALUES + ('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'); +INSERT INTO aowow_setup_custom_data (command,entry,field,value,comment) VALUES + ('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'); +INSERT INTO aowow_setup_custom_data (command,entry,field,value,comment) VALUES + ('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'); +INSERT INTO aowow_setup_custom_data (command,entry,field,value,comment) VALUES + ('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'); +INSERT INTO aowow_setup_custom_data (command,entry,field,value,comment) VALUES + ('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'); +INSERT INTO aowow_setup_custom_data (command,entry,field,value,comment) VALUES + ('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'); +INSERT INTO aowow_setup_custom_data (command,entry,field,value,comment) VALUES + ('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'); +INSERT INTO aowow_setup_custom_data (command,entry,field,value,comment) VALUES + ('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'); +INSERT INTO aowow_setup_custom_data (command,entry,field,value,comment) VALUES + ('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'); +INSERT INTO aowow_setup_custom_data (command,entry,field,value,comment) VALUES + ('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'); +INSERT INTO aowow_setup_custom_data (command,entry,field,value,comment) VALUES + ('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'); +INSERT INTO aowow_setup_custom_data (command,entry,field,value,comment) VALUES + ('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'); +INSERT INTO aowow_setup_custom_data (command,entry,field,value,comment) VALUES + ('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'); +INSERT INTO aowow_setup_custom_data (command,entry,field,value,comment) VALUES + ('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'); +INSERT INTO aowow_setup_custom_data (command,entry,field,value,comment) VALUES + ('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'); +INSERT INTO aowow_setup_custom_data (command,entry,field,value,comment) VALUES + ('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'), + ('item',33147,'class','9','Formula: Enchant Cloak - Subtlety - Class: Recipes'), + ('item',33147,'subClass','8','Formula: Enchant Cloak - Subtlety - Subclass: Enchanting'); \ No newline at end of file diff --git a/setup/sql/updates/v1.2/1645476214_02.sql b/setup/sql/updates/v1.2/1645476214_02.sql new file mode 100644 index 00000000..a5e32bbd --- /dev/null +++ b/setup/sql/updates/v1.2/1645476214_02.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS `aowow_sourcestrings`; diff --git a/setup/sql/updates/v1.2/1647813287_01.sql b/setup/sql/updates/v1.2/1647813287_01.sql new file mode 100644 index 00000000..806c663d --- /dev/null +++ b/setup/sql/updates/v1.2/1647813287_01.sql @@ -0,0 +1 @@ +UPDATE `aowow_dbversion` SET `build` = CONCAT(IFNULL(`build`, ''), ' markup'); diff --git a/setup/sql/updates/v1.2/1647956309_01.sql b/setup/sql/updates/v1.2/1647956309_01.sql new file mode 100644 index 00000000..790e1b29 --- /dev/null +++ b/setup/sql/updates/v1.2/1647956309_01.sql @@ -0,0 +1,47 @@ +-- create new tables + +DROP TABLE IF EXISTS `aowow_guides`, `aowow_guides_changelog`, `aowow_user_ratings`; + +CREATE TABLE `aowow_guides` ( + `id` mediumint unsigned NOT NULL AUTO_INCREMENT, + `category` smallint unsigned NOT NULL DEFAULT '0', + `classId` tinyint unsigned DEFAULT NULL, + `specId` tinyint DEFAULT NULL, + `title` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'title for menus + lists', + `name` varchar(100) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'title for the page tiself', + `description` varchar(200) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '', + `url` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL, + `locale` tinyint unsigned NOT NULL DEFAULT '0', + `status` tinyint unsigned NOT NULL DEFAULT '1', + `rev` tinyint unsigned NOT NULL DEFAULT '0', + `cuFlags` int unsigned NOT NULL DEFAULT '0', + `roles` smallint unsigned NOT NULL DEFAULT '0', + `views` mediumint unsigned NOT NULL DEFAULT '0', + `userId` mediumint unsigned DEFAULT NULL, + `date` int unsigned NOT NULL DEFAULT '0', + `approveUserId` mediumint unsigned DEFAULT NULL, + `approveDate` int unsigned NOT NULL DEFAULT '0', + `deleteUserId` mediumint unsigned DEFAULT NULL, + `deleteData` int unsigned NOT NULL DEFAULT '0', + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; + +CREATE TABLE `aowow_guides_changelog` ( + `id` mediumint unsigned NOT NULL, + `rev` tinyint unsigned DEFAULT NULL, + `date` int unsigned NOT NULL, + `userId` mediumint unsigned NOT NULL, + `status` tinyint unsigned NOT NULL DEFAULT '0', + `msg` varchar(200) COLLATE utf8mb4_general_ci DEFAULT '', + KEY `id` (`id`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; + +CREATE TABLE `aowow_user_ratings` ( + `type` enum('Comment','Guide') COLLATE utf8mb4_unicode_ci NOT NULL, + `entry` int NOT NULL DEFAULT '0', + `userId` int unsigned NOT NULL DEFAULT '0' COMMENT 'User ID', + `value` tinyint NOT NULL DEFAULT '0' COMMENT 'Rating Set', + PRIMARY KEY (`type`,`entry`,`userId`), + KEY `FK_acc_co_rate_user` (`userId`), + CONSTRAINT `FK_userId` FOREIGN KEY (`userId`) REFERENCES `aowow_account` (`id`) ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; \ No newline at end of file diff --git a/setup/sql/updates/v1.2/1647956309_02.sql b/setup/sql/updates/v1.2/1647956309_02.sql new file mode 100644 index 00000000..108713bd --- /dev/null +++ b/setup/sql/updates/v1.2/1647956309_02.sql @@ -0,0 +1,17 @@ +SET FOREIGN_KEY_CHECKS=0; + +-- move comments over to new table +INSERT INTO `aowow_user_ratings` SELECT 1, `commentId`, `userId`, `value` FROM `aowow_comments_rates`; +-- drop aowow_comment_rates at own discretion + +-- modify aowow_articles +ALTER TABLE `aowow_articles` DROP KEY `type`, DROP KEY `locale_url`; +ALTER TABLE `aowow_articles` ADD `rev` TINYINT UNSIGNED DEFAULT 0 NOT NULL AFTER `url`; +ALTER TABLE `aowow_articles` MODIFY COLUMN `editAccess` SMALLINT(5) UNSIGNED DEFAULT 2 NOT NULL; +ALTER TABLE `aowow_articles` ADD UNIQUE KEY `type_id_locale` (`type`,`typeId`,`locale`,`rev`), ADD UNIQUE KEY `url_locale` (`url`,`locale`,`rev`); + +REPLACE INTO `aowow_articles` (`locale`,`url`,`article`) VALUES + (0,'new','Any user can write a guide and then share it with the community. Before a guide will be available to the public, it will be put in a queue where it can be approved or rejected by the staff. We suggest that you make sure your guide is complete before you put it through this process. A complete guide will generally be thorough, 100% accurate for World of Warcraft''s current build, and include details such as images.\n\n[h3]Tips For Creating Quality Guides[/h3]\n\n[ul][li][b]Use [url=?help=markup-guide]Aowow''s BBCode[/url].[/b][/li]\n[li][b]Choose the correct category.[/b] Guides placed in the wrong category risk being rejected. Don''t see your category? Email [feedback]![/li]\n[li][b]Always submit only complete guides.[/b] You can save in-progress ones indefinitely so you won''t risk losing them.[/li]\n[li][b]Make sure it''s on a unique topic with unique advice.[/b] If someone has already covered your topic, make sure that your guide offers something different and/or better advice or else it may be downvoted by our community.[/li]\n[li][b]Extremely short guides may be better off as a comment.[/b] Though overall there is no predetermined length for a good guide.[/li]\n[li][b]We do not tolerate plagiarism in any form.[/b] Make sure to include credits to other sources and a hyperlink if you use their images or otherwise.[/li][/ul]'), + (0,'edit','Any user can write a guide and then share it with the community. Before a guide will be available to the public, it will be put in a queue where it can be approved or rejected by the staff. We suggest that you make sure your guide is complete before you put it through this process. A complete guide will generally be thorough, 100% accurate for World of Warcraft''s current build, and include details such as images.\n\n[h3]Tips For Creating Quality Guides[/h3]\n\n[ul][li][b]Use [url=?help=markup-guide]Aowow''s BBCode[/url].[/b][/li]\n[li][b]Choose the correct category.[/b] Guides placed in the wrong category risk being rejected. Don''t see your category? Email [feedback]![/li]\n[li][b]Always submit only complete guides.[/b] You can save in-progress ones indefinitely so you won''t risk losing them.[/li]\n[li][b]Make sure it''s on a unique topic with unique advice.[/b] If someone has already covered your topic, make sure that your guide offers something different and/or better advice or else it may be downvoted by our community.[/li]\n[li][b]Extremely short guides may be better off as a comment.[/b] Though overall there is no predetermined length for a good guide.[/li]\n[li][b]We do not tolerate plagiarism in any form.[/b] Make sure to include credits to other sources and a hyperlink if you use their images or otherwise.[/li][/ul]'); + +SET FOREIGN_KEY_CHECKS=1; \ No newline at end of file diff --git a/setup/tools/CLISetup.class.php b/setup/tools/CLISetup.class.php index 36f9fa72..cab2a554 100644 --- a/setup/tools/CLISetup.class.php +++ b/setup/tools/CLISetup.class.php @@ -1,5 +1,7 @@ LOCALE_EN, 'enGB' => LOCALE_EN, 'enUS' => LOCALE_EN, - 'frFR' => LOCALE_FR, - 'deDE' => LOCALE_DE, - 'esES' => LOCALE_ES, 'esMX' => LOCALE_ES, - 'ruRU' => LOCALE_RU + + public const SQL_BATCH = 1000; // max. n items per sql insert + + public const LOCK_OFF = 0; + public const LOCK_ON = 1; + public const LOCK_RESTORE = 2; + + private static $lock = self::LOCK_ON; + + public const ARGV_NONE = 0x00; + public const ARGV_REQUIRED = 0x01; + public const ARGV_OPTIONAL = 0x02; + public const ARGV_PARAM = 0x04; // parameter to another argument + public const ARGV_ARRAY = 0x10; // arg accepts list of values + + public const OPT_GRP_SETUP = 0; + public const OPT_GRP_UTIL = 1; + public const OPT_GRP_MISC = 2; + + private const GLOBALSTRINGS_LUA = '%s%sinterface/framexml/globalstrings.lua'; + + private static $opts = []; + private static $optGroups = ['AoWoW Setup', 'Utility Functions', 'Additional Options']; + private static $optDefs = array( // cmd => [groupId, aliases[], argvFlags, description, appendix] + 'delete' => [self::OPT_GRP_MISC, ['d'], self::ARGV_NONE, 'Delete dbc_* tables generated by this prompt when done. (not recommended)', '' ], + 'log' => [self::OPT_GRP_MISC, [], self::ARGV_REQUIRED, 'Write CLI ouput to file.', '=logfile' ], + 'help' => [self::OPT_GRP_MISC, ['h'], self::ARGV_NONE, 'Display contextual help, if available.', '' ], + '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)', '='], + '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' ], ); - public static function init() + private static $utilScriptRefs = []; + private static $setupScriptRefs = []; + private static $tmpStore = []; + private static $gsFiles = []; + + public static function registerUtility(UtilityScript $us) : void { - if ($_ = getopt('d', ['log::', 'locales::', 'mpqDataDir::', 'delete'])) + if (isset(self::$optDefs[$us::COMMAND]) || isset(self::$utilScriptRefs[$us::COMMAND])) { - // optional logging - if (!empty($_['log'])) - CLI::initLogFile(trim($_['log'])); + CLI::write(' Utility function '.CLI::bold($us::COMMAND).' already defined.', CLI::LOG_ERROR); + return; + } + self::$optDefs[$us::COMMAND] = [$us->optGroup, $us->argvOpts, $us->argvFlags, $us::DESCRIPTION, $us::APPENDIX]; + self::$utilScriptRefs[$us::COMMAND] = $us; + } - // alternative data source (no quotes, use forward slash) - if (!empty($_['mpqDataDir'])) - self::$srcDir = CLI::nicePath($_['mpqDataDir']); - - // optional limit handled locales - if (!empty($_['locales'])) - { - // engb and enus are identical for all intents and purposes - $from = ['engb', 'esmx']; - $to = ['enus', 'eses']; - $_['locales'] = str_ireplace($from, $to, strtolower($_['locales'])); - - self::$locales = array_intersect(Util::$localeStrings, explode(',', $_['locales'])); - } - - if (isset($_['d']) || isset($_['delete'])) - self::$tmpDBC = true; + public static function registerSetup(string $invoker, SetupScript $ss) : void + { + if (isset(self::$optDefs[$invoker]) || isset(self::$utilScriptRefs[$invoker])) + { + CLI::write(' Utility function '.CLI::bold($invoker).' not defined. Can\'t attach Subscript '.CLI::bold($ss->getName()).', invoker is missing. Skipping...', CLI::LOG_ERROR); + return; } - if (!self::$locales) - self::$locales = array_filter(Util::$localeStrings); + if (isset(self::$setupScriptRefs[$invoker][$ss->getName()])) + { + CLI::write(' Subscript function '.CLI::bold($ss->getName()).' already defined for invoker '.CLI::bold($invoker).'. Skipping...', CLI::LOG_ERROR); + return; + } - // restrict actual locales - foreach (self::$locales as $idx => $str) - if (!defined('CFG_LOCALES') || CFG_LOCALES & (1 << $idx)) - self::$localeIds[] = $idx; + if ($childArgs = $ss->getSubCommands()) + { + if ($duplicates = array_intersect(array_keys($childArgs), array_keys(self::$optDefs))) + { + CLI::write(' Subscript function '.CLI::bold($ss->getName()).'\'s child arguments --'.implode(', --', $duplicates).' are already defined. Skipping...', CLI::LOG_ERROR); + return; + } + + $newIdx = count(self::$optGroups); + self::$optGroups[] = '--' . $invoker . '=' . $ss->getName(); + + foreach ($childArgs as $cmd => [$aliases, $argFlags, $description]) + self::$optDefs[$cmd] = [$newIdx, $aliases, $argFlags, $description, '']; + } + + // checks done ... store SetupScript + if (self::checkDependencies($ss)) + { + self::$setupScriptRefs[] = [$invoker, $ss->getName(), $ss]; + + // recheck temp stored dependencies + foreach (self::$tmpStore as $idx => [$invoker, $ts]) + { + if (!self::checkDependencies($ts)) + continue; + + self::$setupScriptRefs[] = [$invoker, $ts->getName(), $ts]; + unset(self::$tmpStore[$idx]); + } + } + else // if dependencies haven't been stored yet, put aside for later use + self::$tmpStore[] = [$invoker, $ss]; + } + + private static function checkDependencies(SetupScript &$ss) : bool + { + if ($ss->isOptional) // optional scripts should no depend on anything + return true; + + [$sDep, $bDep] = $ss->getSelfDependencies(); + + return ((!$sDep || $sDep == array_intersect($sDep, array_column(array_filter(self::$setupScriptRefs, function($x) { return $x[0] == 'sql'; }), 1))) && + (!$bDep || $bDep == array_intersect($bDep, array_column(array_filter(self::$setupScriptRefs, function($x) { return $x[0] == 'build'; }), 1)))); + } + + public static function loadScripts() : void + { + foreach (glob('setup/tools/clisetup/*.us.php') as $file) + include_once $file; + + if (self::$tmpStore) + { + CLI::write('Some SubScripts have unresolved dependencies and have not been loaded', CLI::LOG_ERROR); + CLI::write(); + $tbl = [['Name', '--sql dep.', '--build dep.']]; + foreach (self::$tmpStore as [$_, $ssRef]) + { + [$sDep, $bDep] = $ssRef->getSelfDependencies(); + + $missS = array_intersect($sDep, array_column(array_filter(self::$setupScriptRefs, function($x) { return $x[0] == 'sql'; }), 1)); + $missB = array_intersect($sDep, array_column(array_filter(self::$setupScriptRefs, function($x) { return $x[0] == 'build'; }), 1)); + + array_walk($sDep, function (&$x) use($missS) { $x = in_array($x, $missS) ? $x : CLI::red($x); }); + array_walk($bDep, function (&$x) use($missB) { $x = in_array($x, $missB) ? $x : CLI::red($x); }); + + $tbl[] = [$ssRef->getName(), implode(', ', $sDep), implode(', ', $bDep)]; + } + + CLI::writeTable($tbl); + } + + // link SubScipts back to UtilityScript after all UtilityScripts have been loaded + foreach (self::$utilScriptRefs as $name => $us) + if (in_array(TrSubScripts::class, class_uses($us))) + $us->assignGenerators($name); + + self::evalOpts(); + } + + public static function getSubScripts(string $invoker = '') : \Generator + { + foreach (self::$setupScriptRefs as [$src, $name, $ref]) + if (!$invoker || $src == $invoker) + yield $name => [$src, $ref]; + } + + public static function setLocales() : bool + { + // optional limit handled locales + if (isset(self::$opts['locales'])) + { + $opt = array_map('strtolower', self::$opts['locales']); + foreach (Locale::cases() as $loc) + if ($loc->validate() && array_intersect(array_map('strtolower', $loc->gameDirs()), $opt)) + self::$locales[$loc->value] = $loc; + } + if (!self::$locales) + foreach (Locale::cases() as $loc) + if ($loc->validate()) + self::$locales[$loc->value] = $loc; + + return !!self::$locales; + } + + public static function init() : void + { + self::evalOpts(); + + // optional logging + if (isset(self::$opts['log'])) + CLI::initLogFile(trim(self::$opts['log'])); + + // alternative data source (no quotes, use forward slash) + if (isset(self::$opts['datasrc'])) + self::$srcDir = CLI::nicePath('', self::$opts['datasrc']); + + if (!self::setLocales()) + CLI::write('No valid locale specified. Check your config or --locales parameter, if used', CLI::LOG_ERROR); + + // get site status + if (DB::isConnected(DB_AOWOW)) + self::$lock = Cfg::get('MAINTENANCE'); + else + self::$lock = self::LOCK_ON; + } + + public static function writeCLIHelp(bool $full = false) : void + { + $cmd = self::getOpt(1 << self::OPT_GRP_SETUP | 1 << self::OPT_GRP_UTIL); + if (!$cmd || !self::$utilScriptRefs[$cmd[0]]->writeCLIHelp()) + { + $lines = []; + + foreach (self::$optGroups as $idx => $og) + { + if (!$full && $idx > self::OPT_GRP_SETUP) + continue; + + $lines[] = [$og, '']; + + foreach (self::$optDefs as $opt => [$group, $alias, , $desc, $app]) + { + if ($group != $idx) + continue; + + $cmd = ' --'.$opt; + foreach ($alias as $a) + $cmd .= ' | '.(strlen($a) == 1 ? '-'.$a : '--'.$a); + + $lines[] = [$cmd.$app, $desc]; + } + } + + CLI::writeTable($lines); + CLI::write(); + } + } + + // called from Setup + public static function runInitial() : void + { + global $argc, $argv; // todo .. find better way? argv, argc are effectivley already global + + // get arguments present in argGroup 1 or 2, if set. Pick first. + $cmd = self::getOpt(1 << self::OPT_GRP_SETUP | 1 << self::OPT_GRP_UTIL)[0]; + $us = &self::$utilScriptRefs[$cmd]; + $inOut = [null, null, null, null]; + $allOk = true; + + $i = 0; + if ($us::USE_CLI_ARGS) + foreach ($argv as $n => $arg) + { + if (!$n || ($arg && $arg[0] == '-')) // not parent; not handled by getOpt() + continue; + + $inOut[$i++] = $arg; + + if ($i > 3) + break; + } + + if ($dbError = array_filter($us::REQUIRED_DB, function ($x) { return !DB::isConnected($x); })) + { + CLI::write('Database on index '.implode(', ', $dbError).' not yet set up!', CLI::LOG_ERROR); + CLI::write('Please use '.CLI::bold('"php aowow --db"').' for setup', CLI::LOG_BLANK); + CLI::write(); + return; + } + + if ($us::LOCK_SITE != self::LOCK_OFF) + self::siteLock(self::LOCK_ON); + + if ($us::NOTE_START) + CLI::write($us::NOTE_START); + + if (!$us->run($inOut)) + $allOk = false; + + $error = []; + if ($allOk && !$us->test($error)) + { + if ($us::NOTE_ERROR) + CLI::write($us::NOTE_ERROR, CLI::LOG_ERROR); + + foreach ($error as $e) + CLI::write($e, CLI::LOG_BLANK); + + CLI::write(); + $allOk = false; + } + + if ($allOk) + if ($ff = $us->followupFn) + if (array_filter($inOut)) + self::run($ff, $inOut); + + self::siteLock($us::LOCK_SITE == self::LOCK_RESTORE ? self::LOCK_RESTORE : self::LOCK_OFF); + + // end + if ($us::NOTE_END_OK && $allOk) + CLI::write($us::NOTE_END_OK, CLI::LOG_OK); + else if($us::NOTE_END_FAIL && !$allOk) + CLI::write($us::NOTE_END_FAIL, CLI::LOG_ERROR); + } + + // consecutive calls + public static function run(string $cmd, &$args) : bool + { + if (!isset(self::$utilScriptRefs[$cmd])) + return false; + + $us = &self::$utilScriptRefs[$cmd]; + + if ($dbError = array_filter($us::REQUIRED_DB, function ($x) { return !DB::isConnected($x); })) + { + CLI::write('Database on index '.implode(', ', $dbError).' not yet set up!', CLI::LOG_ERROR); + CLI::write('Please use '.CLI::bold('"php aowow --db"').' for setup', CLI::LOG_BLANK); + CLI::write(); + return false; + } + + if ($us::PROMPT) + { + CLI::write($us::PROMPT, -1, false); + CLI::write(); + + if (!CLI::read(['x' => ['Press any key to continue', true, true]], $_)) // we don't actually care about the input + return false; + } + + $args = array_pad($args, 4, null); + + $success = $us->run($args); + + $error = []; + if ($us::NOTE_ERROR && $success && !$us->test($error)) + { + CLI::write($us::NOTE_ERROR, CLI::LOG_ERROR); + foreach ($error as $e) + CLI::write($e, CLI::LOG_BLANK); + + CLI::write(); + return false; + } + + if ($success) + if ($ff = $us->followupFn) + if (array_filter($args)) + if (!self::run($ff, $args)) + $success = false; + + return $success; + } + + + /**************************/ + /* command line arguments */ + /**************************/ + + public static function evalOpts() : void + { + $short = ''; + $long = []; + $alias = []; + + foreach (self::$optDefs as $opt => [, $aliases, $flags, , ]) + { + foreach ($aliases as $i => $a) + { + if (isset($alias[$a])) + $alias[$a][] = $opt; + else + $alias[$a] = [$opt]; + + if ($flags & self::ARGV_REQUIRED) + $a .= ':'; + else if ($flags & self::ARGV_OPTIONAL) + $a .= '::'; + + if (strlen($aliases[$i]) == 1) + $short .= $a; + else + $long[] = $a; + } + + if ($flags & self::ARGV_REQUIRED) + $opt .= ':'; + else if ($flags & self::ARGV_OPTIONAL) + $opt .= '::'; + + $long[] = $opt; + } + + if ($opts = getopt($short, $long)) + { + foreach ($opts as $o => $v) + { + if (!isset($alias[$o])) + self::$opts[$o] = (self::$optDefs[$o][2] & self::ARGV_ARRAY) ? ($v ? explode(',', $v) : []) : ($v ?: true); + else + foreach ($alias[$o] as $a) + self::$opts[$a] = (self::$optDefs[$a][2] & self::ARGV_ARRAY) ? ($v ? explode(',', $v) : []) : ($v ?: true); + } + } + } + + public static function getOpt(/* string|int */ ...$args) // : bool|array|string + { + if (!$args) + return false; + + $result = []; + + // groupMask case + if (is_int($args[0])) + { + foreach (self::$optDefs as $o => [$group, , , , ]) + if (((1 << $group) & $args[0]) && isset(self::$opts[$o])) + $result[] = $o; + + return $result; + } + + // single key case + if (count($args) == 1) + return self::$opts[$args[0]] ?? false; + + // multiple keys case + foreach ($args as $a) + if (isset(self::$optDefs[$a])) + $result[$a] = self::$opts[$a] ?? false; + + return $result; + } + + + /*******************/ + /* web page access */ + /*******************/ + + private static function siteLock(int $mode = self::LOCK_RESTORE) : void + { + if (DB::isConnected(DB_AOWOW)) + Cfg::set('MAINTENANCE', $mode == self::LOCK_RESTORE ? self::$lock : $mode); } @@ -72,42 +457,37 @@ class CLISetup unix systems will throw a fit if you try to get from one to the other, so lets save the paths from 2) and cast it to lowercase lookups will be done in lowercase. A successfull match will return the real path. */ - private static function buildFileList() + private static function buildFileList() : bool { - CLI::write(); - CLI::write('reading MPQdata from '.self::$srcDir.' to list for first time use...'); + CLI::write('indexing game data from '.self::$srcDir.' for first time use...', CLI::LOG_INFO, true, true); $setupDirs = glob('setup/*'); foreach ($setupDirs as $sd) { - if (mb_substr(self::$srcDir, -1) == '/') - self::$srcDir = mb_substr(self::$srcDir, 0, -1); - - if (mb_substr($sd, -1) == '/') + if (mb_substr($sd, -1) == DIRECTORY_SEPARATOR) $sd = mb_substr($sd, 0, -1); if (Util::lower($sd) == Util::lower(self::$srcDir)) { - self::$srcDir = $sd.'/'; + self::$srcDir = $sd.DIRECTORY_SEPARATOR; break; } } try { - $iterator = new RecursiveDirectoryIterator(self::$srcDir); - $iterator->setFlags(RecursiveDirectoryIterator::SKIP_DOTS); + $iterator = new \RecursiveDirectoryIterator(self::$srcDir); + $iterator->setFlags(\RecursiveDirectoryIterator::SKIP_DOTS); - foreach (new RecursiveIteratorIterator($iterator, RecursiveIteratorIterator::SELF_FIRST) as $path) + foreach (new \RecursiveIteratorIterator($iterator, \RecursiveIteratorIterator::SELF_FIRST) as $path) { - $_ = str_replace('\\', '/', $path->getPathname()); + $_ = CLI::nicePath($path->getPathname()); self::$mpqFiles[strtolower($_)] = $_; } - CLI::write('done'); - CLI::write(); + CLI::write('indexing game data from '.self::$srcDir.' for first time use... done!', CLI::LOG_INFO); } - catch (UnexpectedValueException $e) + catch (\UnexpectedValueException $e) { CLI::write('- mpqData dir '.self::$srcDir.' does not exist', CLI::LOG_ERROR); return false; @@ -116,7 +496,7 @@ class CLISetup return true; } - public static function fileExists(&$file) + public static function fileExists(string &$file) : bool { // read mpq source file structure to tree if (!self::$mpqFiles) @@ -124,10 +504,10 @@ class CLISetup return false; // backslash to forward slash - $_ = strtolower(str_replace('\\', '/', $file)); + $_ = strtolower(CLI::nicePath($file)); // remove trailing slash - if (mb_substr($_, -1, 1) == '/') + if (mb_substr($_, -1, 1) == DIRECTORY_SEPARATOR) $_ = mb_substr($_, 0, -1); if (isset(self::$mpqFiles[$_])) @@ -139,7 +519,7 @@ class CLISetup return false; } - public static function filesInPath($path, $useRegEx = false) + public static function filesInPath(string $path, bool $useRegEx = false) : array { $result = []; @@ -149,7 +529,7 @@ class CLISetup return []; // backslash to forward slash - $_ = strtolower(str_replace('\\', '/', $path)); + $_ = strtolower(CLI::nicePath($path)); foreach (self::$mpqFiles as $lowerFile => $realFile) { @@ -162,48 +542,128 @@ class CLISetup return $result; } + public static function filesInPathLocalized(string $pathPattern, ?bool &$status = true, bool $matchAll = true) : array + { + $result = []; + + foreach (self::$locales as $locId => $loc) + { + foreach ($loc->gameDirs() as $gDir) + { + if ($gDir) // if in subDir add trailing slash + $gDir .= DIRECTORY_SEPARATOR; + + $path = sprintf($pathPattern, $gDir); + if (self::fileExists($path)) + { + $result[$locId] = $path; + break; + } + } + } + + if (!$matchAll && !$result) + $status = false; + + if ($matchAll && array_diff_key(self::$locales, $result)) + $status = false; + + return $result; + } + + public static function loadGlobalStrings() : bool + { + CLI::write('loading required GlobalStrings', CLI::LOG_INFO); + + // try to load globalstrings for all selected locales + foreach (self::$locales as $locId => $loc) + { + if (isset(self::$gsFiles[$locId])) + continue; + + foreach ($loc->gameDirs() as $gDir) + { + if ($gDir) + $gDir .= DIRECTORY_SEPARATOR; + + $gsFile = sprintf(self::GLOBALSTRINGS_LUA, self::$srcDir, $gDir); + if (self::fileExists($gsFile)) + { + self::$gsFiles[$locId] = file($gsFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); + break; + } + } + } + + if ($missing = array_diff_key(self::$locales, self::$gsFiles)) + { + ClI::write('GlobalStrings.lua not found for locale '. Lang::concat($missing, callback: fn($x) => $x->name), CLI::LOG_WARN); + return false; + } + + return true; + } + + public static function searchGlobalStrings(string $pattern) : \Generator + { + if (!self::$gsFiles) + return; + + foreach (self::$gsFiles as $lId => $globalStrings) + foreach ($globalStrings as $gs) + if (preg_match($pattern, $gs, $result)) + yield $lId => $result; + } + /*****************/ /* file handling */ /*****************/ - public static function writeFile($file, $content) + public static function writeFile(string $file, string $content) : bool { if (Util::writeFile($file, $content)) { - CLI::write(sprintf(ERR_NONE, CLI::bold($file)), CLI::LOG_OK); + CLI::write('created file '. CLI::bold($file), CLI::LOG_OK, true, true); return true; } - $e = error_get_last(); - CLI::write($e['message'].' '.CLI::bold($file), CLI::LOG_ERROR); return false; } - public static function writeDir($dir) + public static function writeDir(string $dir, bool &$exist = true) : bool { - if (Util::writeDir($dir)) + if (Util::writeDir($dir, $exist)) + { + if (!$exist) + CLI::write('created dir '. CLI::bold($dir), CLI::LOG_OK, true, true); return true; + } - CLI::write(error_get_last()['message'].' '.CLI::bold($dir), CLI::LOG_ERROR); return false; } - public static function loadDBC($name) + public static function loadDBC( string $name) : bool { - if (DB::Aowow()->selectCell('SHOW TABLES LIKE ?', 'dbc_'.$name) && DB::Aowow()->selectCell('SELECT count(1) FROM ?#', 'dbc_'.$name)) + if (!DB::isConnected(DB_AOWOW)) + { + CLI::write('CLISetup::loadDBC() - not connected to DB. Cannot write results!', CLI::LOG_ERROR); + return false; + } + + 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::$tmpDBC]); + $dbc = new DBCReader($name, ['temporary' => self::getOpt('delete')]); if ($dbc->error) { - CLI::write('SqlGen::generate() - required DBC '.$name.'.dbc not found!', CLI::LOG_ERROR); + CLI::write('CLISetup::loadDBC() - required DBC '.$name.'.dbc not found!', CLI::LOG_ERROR); return false; } if (!$dbc->readFile()) { - CLI::write('SqlGen::generate() - DBC '.$name.'.dbc could not be written to DB!', CLI::LOG_ERROR); + CLI::write('CLISetup::loadDBC() - DBC '.$name.'.dbc could not be written to DB!', CLI::LOG_ERROR); return false; } diff --git a/setup/tools/clisetup/account.func.php b/setup/tools/clisetup/account.func.php deleted file mode 100644 index 3beac4db..00000000 --- a/setup/tools/clisetup/account.func.php +++ /dev/null @@ -1,65 +0,0 @@ - ['Username', false], - 'pass1' => ['Enter Password', true ], - 'pass2' => ['Confirm Password', true ] - ); - - User::useLocale(LOCALE_EN); - Lang::load(Util::$localeStrings[LOCALE_EN]); - - if (CLI::readInput($fields)) - { - CLI::write(); - - if (!User::isValidName($fields['name'], $e)) - CLI::write(Lang::account($e == 1 ? 'errNameLength' : 'errNameChars'), CLI::LOG_ERROR); - else if (!User::isValidPass($fields['pass1'], $e)) - CLI::write(Lang::account($e == 1 ? 'errPassLength' : 'errPassChars'), CLI::LOG_ERROR); - else if ($fields['pass1'] != $fields['pass2']) - CLI::write(Lang::account('passMismatch'), CLI::LOG_ERROR); - else if ($_ = DB::Aowow()->SelectCell('SELECT 1 FROM ?_account WHERE user = ? AND (status <> ?d OR (status = ?d AND statusTimer > UNIX_TIMESTAMP()))', $fields['name'], ACC_STATUS_NEW, ACC_STATUS_NEW)) - CLI::write(Lang::account('nameInUse'), CLI::LOG_ERROR); - else - { - // write to db - $ok = DB::Aowow()->query('REPLACE INTO ?_account (user, passHash, displayName, joindate, email, allowExpire, userGroups, userPerms) VALUES (?, ?, ?, UNIX_TIMESTAMP(), ?, 0, ?d, 1)', - $fields['name'], - User::hashCrypt($fields['pass1']), - Util::ucFirst($fields['name']), - CFG_CONTACT_EMAIL, - U_GROUP_ADMIN - ); - if ($ok) - { - $newId = DB::Aowow()->selectCell('SELECT id FROM ?_account WHERE user = ?', $fields['name']); - Util::gainSiteReputation($newId, SITEREP_ACTION_REGISTER); - - CLI::write("account ".$fields['name']." created successfully", CLI::LOG_OK); - } - else // something went wrong - CLI::write(Lang::main('intError'), CLI::LOG_ERROR); - } - } - else - { - CLI::write(); - CLI::write("account creation aborted", CLI::LOG_INFO); - } -} - -?> diff --git a/setup/tools/clisetup/account.us.php b/setup/tools/clisetup/account.us.php new file mode 100644 index 00000000..905ae979 --- /dev/null +++ b/setup/tools/clisetup/account.us.php @@ -0,0 +1,134 @@ + ['Username', false], + 'pass1' => ['Enter Password', true ], + 'pass2' => ['Confirm Password', true ], + 'email' => ['Email (optional)', false] + ); + + // args: username, password, email, null // iiin + public function run(&$args) : bool + { + Lang::load(Locale::EN); + + $name = $args[0] ?? ''; + $passw = $args[1] ?? ''; + $email = $args[2]; + + if (Util::validateUsername($name)) + unset($this->fields['name']); + else + $name = ''; + + if (Util::validatePassword($passw)) + { + unset($this->fields['pass1']); + unset($this->fields['pass2']); + } + else + $passw = ''; + + if (Util::validateEmail($email)) + unset($this->fields['email']); + else + $email = ''; + + if ($this->fields && CLI::read($this->fields, $uiAccount) && $uiAccount) + { + CLI::write(); + + 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) && $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 && !empty($uiAccount['email']) && Util::validateEmail($uiAccount['email'])) + $email = $uiAccount['email']; + 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) + { + CLI::write(); + CLI::write("[account] admin creation aborted", CLI::LOG_INFO); + CLI::write(); + return true; + } + + 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] ' . (Util::lower($name) == Util::lower($username) ? Lang::account('nameInUse') : Lang::account('mailInUse')), CLI::LOG_ERROR); + CLI::write(); + return false; + } + + 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(%s)', $name); + Util::gainSiteReputation($newId, SITEREP_ACTION_REGISTER); + + CLI::write("[account] admin ".$name." created successfully", CLI::LOG_OK); + CLI::write(); + + return true; + } + + CLI::write('[account] ' . Lang::main('intError'), CLI::LOG_ERROR); + CLI::write(); + + return false; + } + + public function test(?array &$error = []) : bool + { + $error = []; + return !!DB::Aowow()->selectCell('SELECT `id` FROM ::account WHERE `userPerms` = 1'); + } +}); + +?> diff --git a/setup/tools/clisetup/build.func.php b/setup/tools/clisetup/build.func.php deleted file mode 100644 index 105db1b8..00000000 --- a/setup/tools/clisetup/build.func.php +++ /dev/null @@ -1,90 +0,0 @@ - list($file, $destPath, $deps)) - { - $reqDBC = []; - - if (!in_array($name, FileGen::$subScripts)) - continue; - - if (!file_exists(FileGen::$tplPath.$file.'.in')) - { - CLI::write(sprintf(ERR_MISSING_FILE, FileGen::$tplPath.$file.'.in'), CLI::LOG_ERROR); - $allOk = false; - continue; - } - - if (!CLISetup::writeDir($destPath)) - continue; - - $syncIds = []; // todo: fetch what exactly must be regenerated - - $ok = FileGen::generate($name, $syncIds); - if (!$ok) - $allOk = false; - else - $done[] = $name; - - CLI::write(' - subscript \''.$file.'\' returned '.($ok ? 'sucessfully' : 'with errors'), $ok ? CLI::LOG_OK : CLI::LOG_ERROR); - set_time_limit(FileGen::$defaultExecTime); // reset to default for the next script - } - - // files without template - foreach (FileGen::$datasets as $file => $deps) - { - if (!in_array($file, FileGen::$subScripts)) - continue; - - $syncIds = []; // todo: fetch what exactly must be regenerated - - $ok = FileGen::generate($file, $syncIds); - if (!$ok) - $allOk = false; - else - $done[] = $file; - - CLI::write(' - subscript \''.$file.'\' returned '.($ok ? 'sucessfully' : 'with errors'), $ok ? CLI::LOG_OK : CLI::LOG_ERROR); - set_time_limit(FileGen::$defaultExecTime); // reset to default for the next script - } - - // end - CLI::write(); - if ($allOk) - CLI::write('successfully finished file generation', CLI::LOG_OK); - else - CLI::write('finished file generation with errors', CLI::LOG_ERROR); - } - else if ($syncMe) - CLI::write('no valid script names supplied', CLI::LOG_ERROR); - - return $done; -} - -?> diff --git a/setup/tools/clisetup/datagen.us.php b/setup/tools/clisetup/datagen.us.php new file mode 100644 index 00000000..03491690 --- /dev/null +++ b/setup/tools/clisetup/datagen.us.php @@ -0,0 +1,169 @@ +'; + public const NOTE_START = '[sql] begin generation of:'; + public const NOTE_END_OK = 'successfully finished sql generation'; + public const NOTE_END_FAIL = 'finished sql generation with errors'; + + public const REQUIRED_DB = [DB_AOWOW, DB_WORLD]; + + public const LOCK_SITE = CLISetup::LOCK_RESTORE; + + public function __construct() + { + if ($this->inited) + return true; + + $this->defaultExecTime = ini_get('max_execution_time'); + + // register subscripts to CLISetup + foreach (glob('setup/tools/sqlgen/*.ss.php') as $file) + include_once $file; + + $this->inited = true; + return true; + } + + // args: scriptToDo, scriptSuccess, null, null // ionn + public function run(&$args) : bool + { + $todo = &$args['doSql']; + $done = &$args['doneSql']; + + if (!$this->inited) + return false; + + // check passed subscript names; limit to real scriptNames + if (($sqlArgs = CLISetup::getOpt('sql')) !== false) + { + if ($sqlArgs === []) // used --sql without arguments + $todo = array_keys($this->generators); // do everything + else if ($_ = array_intersect(array_keys($this->generators), $sqlArgs)) + $todo = $_; + else + { + CLI::write('[sql] no valid script names supplied', CLI::LOG_ERROR); + return false; + } + + // supplement self::NOTE_START + CLI::write(' - '.Lang::concat($todo), CLI::LOG_BLANK, false); + CLI::write(); + } + else if ($todo) + { + $todo = array_intersect(array_keys($this->generators), is_array($todo) ? $todo : [$todo]); + if (!$todo) + return false; + } + else + return false; + + $allOk = true; + + // start file generation + foreach ($todo as $cmd) + { + $syncIds = []; // todo: fetch what exactly must be regenerated + $success = false; + $scriptRef = &$this->generators[$cmd]; + + CLI::write('[sql] filling aowow_'.$cmd.' with data'); + + if ($scriptRef->fulfillRequirements()) + { + if ($scriptRef->generate($syncIds)) + { + if (method_exists($scriptRef, 'applyCustomData')) + $success = $scriptRef->applyCustomData(); + + $success = true; + } + } + + if (!$success) + $allOk = false; + else + $done[] = $cmd; + + 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; + } + + public function writeCLIHelp() : bool + { + if ($args = CLISetup::getOpt('sql')) + { + $anyHelp = false; + foreach ($args as $cmd) + if (isset($this->generators[$cmd]) && $this->generators[$cmd]->writeCLIHelp()) + $anyHelp = true; + + if ($anyHelp) + return true; + } + + CLI::write(' usage: php aowow --sql= [--datasrc: --locales:]', -1, false); + CLI::write(); + CLI::write(' Regenerates DB table content for a given SetupScript. Dependencies are taken into account by the triggered calls of --sync and --update', -1, false); + CLI::write(); + + ksort($this->generators); + + $lines = [['Command', 'TC dependencies', 'AoWoW dependencies', 'Info']]; + foreach ($this->generators as $cmd => $ssRef) + { + $tcDeps = explode("\n", Lang::breakTextClean(implode(', ', $ssRef->getRemoteDependencies() ), 35, Lang::FMT_RAW)); + $aoDeps = explode("\n", Lang::breakTextClean(implode(', ', $ssRef->getSelfDependencies()[0]), 35, Lang::FMT_RAW)); + + for ($i = 0; $i < max(count($tcDeps), count($aoDeps)); $i++) + $lines[] = array( + $i ? '' : $cmd, + $tcDeps[$i] ?? '', + $aoDeps[$i] ?? '', + $i ? '' : $ssRef->getInfo() + ); + } + + CLI::writeTable($lines); + CLI::write(); + + return true; + } +}); + +?> diff --git a/setup/tools/clisetup/dbc.us.php b/setup/tools/clisetup/dbc.us.php new file mode 100644 index 00000000..de910e6d --- /dev/null +++ b/setup/tools/clisetup/dbc.us.php @@ -0,0 +1,121 @@ + [tablename [wowbuild]]'; + + public const REQUIRED_DB = [DB_AOWOW]; + + public const USE_CLI_ARGS = true; + + // args: tblName, wowbuild, null, null // iinn + public function run(&$args) : bool + { + foreach (CLISetup::getOpt('dbc') as $n) + { + $n = str_ireplace('.dbc', '', trim($n)); + + if (empty($n)) + continue; + + $opts = ['temporary' => false]; + if ($args[0]) + $opts['tableName'] = $args[0]; + + $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); + return false; + } + + if (!$dbc->readFile()) + { + CLI::write('[dbc] DBC '.CLI::bold($n).'.dbc could not be written to DB!', CLI::LOG_ERROR); + return false; + } + + $self = DB::Aowow()->selectCell('SELECT DATABASE()'); + CLI::write('[dbc] DBC '.CLI::bold($n).'.dbc written to '.CLI::bold('`'.($self ?? 'NULL').'`.`'.$dbc->getTableName().'`').'!', CLI::LOG_OK); + } + + return true; + } + + public function writeCLIHelp() : bool + { + CLI::write(' usage: php aowow --dbc= [--locales=] [tablename [wowbuild]]', -1, false); + CLI::write(); + CLI::write(' Extract dbc files from datasrc into sql table. If the dbc file contains a locale block, data from all available locales gets merged into the same table.', -1, false); + CLI::write(); + CLI::write(' Known WoW builds:', -1, false); + + foreach (glob('setup/tools/dbc/*.ini') as $f) + { + $a = $b = []; + preg_match('/(\d+)\.ini$/', $f, $a); + if ($h = fopen($f, 'r')) + { + preg_match('/(\d\.\d.\d\.?\d*)/', fgets($h), $b); + fclose($h); + } + + CLI::write(' '.CLI::bold($a[1]).' > '.$b[1], -1, false); + } + + CLI::write(); + CLI::write(' Known DBC files:', -1, false); + + $defs = DBCReader::getDefinitions(); + $letter = ''; + $buff = []; + + asort($defs); + + foreach ($defs as $d) + { + if (!$letter) + $letter = $d[0]; + else if ($letter != $d[0]) + { + CLI::write(' '.CLI::bold(Util::ucFirst($letter)).':', -1, false); + foreach (explode("\n", Lang::breakTextClean(implode(', ', $buff), 120, Lang::FMT_RAW)) as $line) + CLI::write(' '.$line, -1, false); + + $buff = [$d]; + $letter = $d[0]; + } + else + $buff[] = $d; + } + + CLI::write(' '.CLI::bold(Util::ucFirst($letter)).':', -1, false); + foreach (explode("\n", Lang::breakTextClean(implode(', ', $buff), 120, Lang::FMT_RAW)) as $line) + CLI::write(' '.$line, -1, false); + + CLI::write(); + CLI::write(); + + return true; + } +}); + +?> diff --git a/setup/tools/clisetup/dbconfig.func.php b/setup/tools/clisetup/dbconfig.func.php deleted file mode 100644 index 95b1fe3f..00000000 --- a/setup/tools/clisetup/dbconfig.func.php +++ /dev/null @@ -1,159 +0,0 @@ - ['Server Host', false], - 'user' => ['User', false], - 'pass' => ['Password', true ], - 'db' => ['Database Name', false], - 'prefix' => ['Table prefix', false] - ); - $testDB = function($idx, $name, $dbInfo) - { - $buff = '['.CLI::bold($idx).'] '.str_pad($name, 17); - $errStr = ''; - $defPort = ini_get('mysqli.default_port'); - $port = 0; - - if (strstr($dbInfo['host'], ':')) - list($dbInfo['host'], $port) = explode(':', $dbInfo['host']); - - if ($dbInfo['host']) - { - // test DB - if ($link = @mysqli_connect($dbInfo['host'], $dbInfo['user'], $dbInfo['pass'], $dbInfo['db'], $port ?: $defPort)) - mysqli_close($link); - else - $errStr = '['.mysqli_connect_errno().'] '.mysqli_connect_error(); - - $buff .= $errStr ? CLI::red('ERR ') : CLI::green('OK '); - $buff .= 'mysqli://'.$dbInfo['user'].':'.str_pad('', mb_strlen($dbInfo['pass']), '*').'@'.$dbInfo['host'].($port ? ':'.$port : null).'/'.$dbInfo['db']; - $buff .= ($dbInfo['prefix'] ? ' table prefix: '.$dbInfo['prefix'] : null).' '.$errStr; - } - else - $buff .= ' '.CLI::bold(''); - - return $buff; - }; - - if (file_exists('config/config.php')) - require 'config/config.php'; - - foreach ($databases as $idx => $name) - { - if (empty($AoWoWconf[$name]) && $name != 'characters' ) - $AoWoWconf[$name] = array_combine(array_keys($dbFields), ['', '', '', '', '']); - } - - while (true) - { - CLI::write(); - CLI::write("select a numerical index to use the corresponding entry"); - - $nCharDBs = 0; - foreach ($databases as $idx => $name) - { - if ($idx != 3) - CLI::write($testDB($idx, $name, $AoWoWconf[$name])); - else if (!empty($AoWoWconf[$name])) - foreach ($AoWoWconf[$name] as $charIdx => $dbInfo) - CLI::write($testDB($idx + $nCharDBs++, $name.' ['.$charIdx.']', $AoWoWconf[$name][$charIdx])); - } - - CLI::write("[".CLI::bold(3 + $nCharDBs)."] add an additional Character DB"); - - while (true) - { - $inp = ['idx' => ['', true, '/\d/']]; - if (CLI::readInput($inp, true) && $inp) - { - if ($inp['idx'] >= 0 && $inp['idx'] <= (3 + $nCharDBs)) - { - $curFields = $inp['idx'] ? $dbFields : array_splice($dbFields, 0, 4); - - if ($inp['idx'] == 3 + $nCharDBs) // add new realmDB - $curFields['realmId'] = ['Realm Id', false, '/[1-9][0-9]*/']; - - if (CLI::readInput($curFields)) - { - if ($inp['idx'] == 0 && $curFields) - $curFields['prefix'] = 'aowow_'; - - // auth, world or aowow - if ($inp['idx'] < 3) - $AoWoWconf[$databases[$inp['idx']]] = $curFields ?: array_combine(array_keys($dbFields), ['', '', '', '', '']); - // new char DB - else if ($inp['idx'] == 3 + $nCharDBs) - { - if ($curFields) - { - $_ = $curFields['realmId']; - unset($curFields['realmId']); - $AoWoWconf[$databases[3]][$_] = $curFields; - } - } - // existing char DB - else - { - $i = 0; - foreach ($AoWoWconf[$databases[3]] as $realmId => &$dbInfo) - { - if ($inp['idx'] - 3 != $i++) - continue; - - if ($curFields) - $dbInfo = $curFields; - else - unset($AoWoWconf[$databases[3]][$realmId]); - } - } - - // write config file - $buff = " $charInfo) - $buff .= '$AoWoWconf[\''.$db.'\'][\''.$idx.'\'] = '.var_export($AoWoWconf[$db][$idx], true).";\n\n"; - } - $buff .= "?>\n"; - CLI::write(); - CLISetup::writeFile('config/config.php', $buff); - continue 2; - } - else - { - CLI::write(); - CLI::write("edit canceled! returning to list...", CLI::LOG_INFO); - sleep(1); - continue 2; - } - } - } - else - { - CLI::write(); - CLI::write("leaving db setup...", CLI::LOG_INFO); - break 2; - } - } - } -} - -?> diff --git a/setup/tools/clisetup/dbconfig.us.php b/setup/tools/clisetup/dbconfig.us.php new file mode 100644 index 00000000..e2120989 --- /dev/null +++ b/setup/tools/clisetup/dbconfig.us.php @@ -0,0 +1,335 @@ + ['Server Host', false], + 'user' => ['User', false], + 'pass' => ['Password', true ], + 'db' => ['Database Name', false], + 'prefix' => ['Table prefix', false] + ); + + private $icons = ['Normal[0]', 'PvP', null, null, 'Normal[4]', 'RP', null, 'RP-PvP']; + private $regions = array( + null, 'Development', 'United States', 'Oceanic', 'Latin America', 'Tournament (Americas)', 'Korea', 'Tournament (Korea)', 'English', 'German', 'French', 'Spanish', 'Russian', 'Tournament (EU)', 'Taiwan', 'Tournament (Taiwan)', 'China', + 25 => 'Tournament (China)', 26 => 'Test Server', 27 => 'Tournament (Test Server)', 28 => 'QA Server', 30 => 'Test Server 2' + ); + + // args: null, null, null, null // nnnn + public function run(&$args) : bool + { + if (!$this->config && file_exists(self::CONFIG_FILE)) + { + require self::CONFIG_FILE; + $this->config = $AoWoWconf; + unset($AoWoWconf); + } + + foreach ($this->databases as $idx => $name) + if (empty($this->config[$name]) && $name != 'characters' ) + $this->config[$name] = array_combine(array_keys($this->dbFields), ['', '', '', '', '']); + + while (true) + { + CLI::write("select an index to use the corresponding entry", -1, false); + + $nCharDBs = 0; + $tblRows = []; + foreach ($this->databases as $idx => $name) + { + if ($idx != DB_CHARACTERS) + $tblRows[] = $this->testDB($idx, $name, $this->config[$name]); + else if (!empty($this->config[$name])) + foreach ($this->config[$name] as $charIdx => $dbInfo) + $tblRows[] = $this->testDB($idx + $nCharDBs++, $name.' ['.$charIdx.']', $this->config[$name][$charIdx]); + } + + $tblRows[] = ['['.CLI::bold('N').']', 'new characters DB']; + $tblRows[] = ['['.CLI::bold('S').']', 'show available realms']; + $tblRows[] = ['['.CLI::bold('R').']', 'retest / reload DBs']; + CLI::writeTable($tblRows, false, true); + + while (true) + { + if (CLI::read(['idx' => ['', true, true, '/\d|R|N|S/i']], $uiIndex) && $uiIndex) + { + if (strtoupper($uiIndex['idx']) == 'R') + continue 2; + else if (strtoupper($uiIndex['idx']) == 'S') + { + CLI::write(); + if (!DB::isConnectable(DB_AUTH) || !$this->test()) + 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]) + { + $status = []; + $hasRegion = false; + foreach (Profiler::REGIONS as $n => $valid) + if ($hasRegion = in_array($region, $valid)) + { + if ($n == 'dev') + $status[] = 'Restricted region (staff only)'; + break; + } + + if (!$hasRegion && !$status) + $status[] = 'Unsupported region'; + if ($level > 0) + $status[] = 'GM-Level locked'; + if (DB::isConnectable(DB_CHARACTERS . $id)) + $status[] = 'Already in use'; + + $tbl[] = [$id, $name, $this->icons[$icon] ?? '', $this->regions[$region] ?? '', $level, $status ? CLI::yellow(implode(', ', $status)) : CLI::green('Usable')]; + } + + CLI::writeTable($tbl); + } + else + CLI::write('[db] table `realmlist` is empty.', CLI::LOG_WARN); + + CLI::write(); + + continue 2; + } + else if (($uiIndex['idx'] >= DB_AOWOW && $uiIndex['idx'] < (DB_CHARACTERS + $nCharDBs)) || strtoupper($uiIndex['idx']) == 'N') + { + $curFields = $uiIndex['idx'] ? $this->dbFields : array_slice($this->dbFields, 0, 4); + + if (strtoupper($uiIndex['idx']) == 'N') // add new characters DB + $curFields['realmId'] = ['Realm Id', false, false, '/\d{1,3}/']; + + if (CLI::read($curFields, $uiRealm)) + { + if ($uiIndex['idx'] == DB_AOWOW && $uiRealm) + $uiRealm['prefix'] = 'aowow_'; + + if (strtoupper($uiIndex['idx']) == 'N') // new char DB + { + if ($uiRealm) + { + $_ = $uiRealm['realmId']; + unset($uiRealm['realmId']); + $this->config[$this->databases[DB_CHARACTERS]][$_] = $uiRealm; + } + } + else if ($uiIndex['idx'] < DB_CHARACTERS) // auth, world or aowow + $this->config[$this->databases[$uiIndex['idx']]] = $uiRealm ?: array_combine(array_keys($this->dbFields), ['', '', '', '', '']); + else // existing char DB + { + $i = 0; + foreach ($this->config[$this->databases[DB_CHARACTERS]] as $realmId => &$dbInfo) + { + if ($uiIndex['idx'] - DB_CHARACTERS != $i++) + continue; + + if ($uiRealm) + $dbInfo = $uiRealm; + else + unset($this->config[$this->databases[3]][$realmId]); + } + } + + // write config file + $buff = "databases as $db) + { + if ($db != 'characters') + $buff .= '$AoWoWconf[\''.$db.'\'] = '.var_export($this->config[$db], true).";\n\n"; + else if (isset($this->config[$db])) + foreach ($this->config[$db] as $idx => $charInfo) + $buff .= '$AoWoWconf[\''.$db.'\'][\''.$idx.'\'] = '.var_export($this->config[$db][$idx], true).";\n\n"; + } + $buff .= "?>\n"; + CLI::write(); + CLISetup::writeFile(self::CONFIG_FILE, $buff); + continue 2; + } + else + { + CLI::write("[db] edit canceled! returning to list...", CLI::LOG_INFO); + CLI::write(); + sleep(1); + continue 2; + } + } + } + else + { + CLI::write("[db] leaving db config...", CLI::LOG_INFO); + CLI::write(); + break 2; + } + } + } + + return true; + } + + public function test(?array &$error = []) : bool + { + if (!$this->config) + { + require self::CONFIG_FILE; + $this->config = $AoWoWconf; + unset($AoWoWconf); + } + + $error = []; + foreach (['aowow', 'world', 'auth'] as $idx => $what) + { + if ($what == 'auth' && (empty($this->config['auth']) || empty($this->config['auth']['host']))) + continue; + + // init proper access for further setup + if (DB::test($this->config[$what], $err)) + { + DB::load($idx, $this->config[$what]); + switch ($idx) + { + case DB_AOWOW: + 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 %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).')'; + break; + default: + // no further checks at this time + } + } + else + $error[] = ' * '.$what.': '.$err; + } + + return empty($error); + } + + private function testDB($idx, $name, $dbInfo) + { + $buff = ['['.CLI::bold($idx).']', $name]; + + if ($dbInfo['host']) + { + $result = CLI::green('OK'); + $note = ''; + + if (DB::test($dbInfo, $note)) + { + DB::load($idx, $dbInfo); + + $ok = false; + switch ($idx) + { + case DB_AOWOW: + if (DB::Aowow()->selectCell('SHOW TABLES LIKE %s', 'aowow_dbversion')) + { + if ($date = DB::Aowow()->selectCell('SELECT `date` FROM ::dbversion')) + { + $note = 'AoWoW DB version @ ' . date(Util::$dateFormatInternal, $date); + $ok = true; + } + else + $note = CLI::yellow('AoWoW DB version empty! Import of DB dump failed?'); + } + else + $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 %s', 'version')) + { + [$vString, $vNo] = DB::World()->selectRow('SELECT `db_version` AS "0", `cache_id` AS "1" FROM `version`'); + if (strpos($vString, 'TDB') === 0) + { + if ($vNo < TDB_WORLD_MINIMUM_VER) + $note = CLI::yellow('DB test found TrinityDB version older than rev. ').CLI::bold(TDB_WORLD_MINIMUM_VER).CLI::yellow('. Please update to at least rev. ').CLI::bold(TDB_WORLD_MINIMUM_VER); + else if ($vNo > TDB_WORLD_EXPECTED_VER) + $note = CLI::yellow('DB test found TrinityDB version newer than rev. ').CLI::bold(TDB_WORLD_EXPECTED_VER).CLI::yellow('. Be advised! DB structure may diverge!'); + else + { + $note = 'TrinityDB version @ ' . $vString; + $ok = true; + } + } + else if (strpos($vString, 'ACDB') === 0) + $note = CLI::yellow('DB test found AzerothCore DB version. AzerothCore DB structure is not supported!'); + else + $note = CLI::yellow('DB test found unexpected vendor in expected version table. Uhh.. Good Luck..!?'); + } + 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?'); + break; + default: + $ok = true; // no tests right now + } + + if (!$ok) + $result = CLI::yellow('WARN'); + } + else + $result = CLI::red('ERR'); + + $buff[] = $result; + $buff[] = 'mysqli://'.$dbInfo['user'].':'.($dbInfo['pass'] ? '**********' : '').'@'.$dbInfo['host'].'/'.$dbInfo['db']; + $buff[] = $dbInfo['prefix'] ? 'pre.: '.$dbInfo['prefix'] : ''; + $buff[] = $note; + } + else if ($idx == DB_AUTH) + $buff[] = CLI::bold(''); + else + $buff[] = CLI::bold(''); + + return $buff; + } + + public function writeCLIHelp() : bool + { + 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(); + + return true; + } +}); + +?> diff --git a/setup/tools/clisetup/filegen.us.php b/setup/tools/clisetup/filegen.us.php new file mode 100644 index 00000000..426a0769 --- /dev/null +++ b/setup/tools/clisetup/filegen.us.php @@ -0,0 +1,184 @@ +'; + public const NOTE_START = '[build] begin generation of:'; + public const NOTE_END_OK = 'successfully finished file generation'; + public const NOTE_END_FAIL = 'finished file generation with errors'; + + public const REQUIRED_DB = [DB_AOWOW, DB_WORLD]; + + public const LOCK_SITE = CLISetup::LOCK_RESTORE; + + private $uploadDirs = array( // stuff that should be writable by www-data and isn't directly created by setup steps + 'static/uploads/screenshots/normal/', + 'static/uploads/screenshots/pending/', + 'static/uploads/screenshots/resized/', + 'static/uploads/screenshots/temp/', + 'static/uploads/screenshots/thumb/', + 'static/uploads/temp/', + 'static/uploads/guide/images/', + 'static/uploads/avatars/' + ); + + public function __construct() + { + if ($this->inited) + return true; + + $this->defaultExecTime = ini_get('max_execution_time'); + + // register subscripts to CLISetup + foreach (glob('setup/tools/filegen/*.ss.php') as $file) + include_once $file; + + $this->inited = true; + return true; + } + + // args: scriptToDo, scriptSuccess, null, null // ionn + public function run(&$args) : bool + { + $todo = &$args['doBuild']; + $done = &$args['doneBuild']; + + if (!$this->inited) + return false; + + + // check passed subscript names; limit to real scriptNames + if (($buildArgs = CLISetup::getOpt('build')) !== false) + { + if ($buildArgs === []) // used --build without arguments + $todo = array_keys($this->generators); // do everything + else if ($_ = array_intersect(array_keys($this->generators), $buildArgs)) + $todo = $_; + else + { + CLI::write('[build] no valid script names supplied', CLI::LOG_ERROR); + return false; + } + + // supplement self::NOTE_START + CLI::write(' - '.Lang::concat($todo), CLI::LOG_BLANK, false); + CLI::write(); + } + else if ($todo) + { + $todo = array_intersect(array_keys($this->generators), is_array($todo) ? $todo : [$todo]); + if (!$todo) + return false; + } + else + return false; + + // create user upload dir structure + foreach ($this->uploadDirs as $ud) + { + if (CLISetup::writeDir($ud)) + continue; + + CLI::write('[build] could not create directory: '.CLI::bold($ud), CLI::LOG_ERROR); + return false; + } + + $done = []; + $allOk = true; + + // start file generation + foreach ($todo as $cmd) + { + $success = false; + $scriptRef = &$this->generators[$cmd]; + + CLI::write('[build] gathering data for '.$cmd); + + if ($scriptRef->fulfillRequirements()) + $success = $scriptRef->generate(); + + if (!$success) + $allOk = false; + else + $done[] = $cmd; + + CLI::write('[build] 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; + } + + public function writeCLIHelp() : bool + { + if ($args = CLISetup::getOpt('build')) + { + $anyHelp = false; + foreach ($args as $cmd) + if (isset($this->generators[$cmd]) && $this->generators[$cmd]->writeCLIHelp()) + $anyHelp = true; + + if ($anyHelp) + return true; + } + + CLI::write(' usage: php aowow --build= [--datasrc: --locales:]', -1, false); + CLI::write(); + CLI::write(' Compiles files for a given SetupScript. Existing files are kept by default. Dependencies are taken into account by the triggered calls of --sync --update', -1, false); + CLI::write(); + + ksort($this->generators); + + $lines = [['Command', 'TC dependencies', 'AoWoW dependencies', 'Info']]; + foreach ($this->generators as $cmd => $ssRef) + { + $tcDeps = explode("\n", Lang::breakTextClean(implode(', ', $ssRef->getRemoteDependencies() ), 35, Lang::FMT_RAW)); + $aoDeps = explode("\n", Lang::breakTextClean(implode(', ', $ssRef->getSelfDependencies()[0]), 35, Lang::FMT_RAW)); + + for ($i = 0; $i < max(count($tcDeps), count($aoDeps)); $i++) + $lines[] = array( + $i ? '' : $cmd, + $tcDeps[$i] ?? '', + $aoDeps[$i] ?? '', + $i ? '' : $ssRef->getInfo() + ); + } + + CLI::writeTable($lines); + CLI::write(); + + return true; + } +}); + +?> diff --git a/setup/tools/clisetup/firstrun.func.php b/setup/tools/clisetup/firstrun.func.php deleted file mode 100644 index 852dd9f0..00000000 --- a/setup/tools/clisetup/firstrun.func.php +++ /dev/null @@ -1,324 +0,0 @@ - '\/', '.' => '\.'])."/i", $res['Location'], $n)) - { - $protocol = $n[1]; - $host = $n[2]; - } - - return false; - } - - $rCode = 0; - return false; - }; - - $res = DB::Aowow()->selectCol('SELECT `key` AS ARRAY_KEY, value FROM ?_config WHERE `key` IN ("site_host", "static_host", "force_ssl")'); - $prot = $res['force_ssl'] ? 'https://' : 'http://'; - $cases = array( - 'site_host' => [$prot, $res['site_host'], '/README.md'], - 'static_host' => [$prot, $res['static_host'], '/css/aowow.css'] - ); - - foreach ($cases as $conf => list($protocol, $host, $testFile)) - { - if ($host) - { - if (!$test($protocol, $host, $testFile, $resp)) - { - if ($resp == 301 || $resp == 302) - { - CLI::write('self test received status '.CLI::bold($resp).' (page moved) for '.$conf.', pointing to: '.$protocol.$host.$testFile, CLI::LOG_WARN); - $inp = ['x' => ['should '.CLI::bold($conf).' be set to '.CLI::bold($host).' and force_ssl be updated?', true, '/y|n/i']]; - if (!CLI::readInput($inp, true) || !$inp || strtolower($inp['x']) == 'n') - $error[] = ' * could not access '.$protocol.$host.$testFile.' ['.$resp.']'; - else - { - DB::Aowow()->query('UPDATE ?_config SET `value` = ? WHERE `key` = ?', $host, $conf); - DB::Aowow()->query('UPDATE ?_config SET `value` = ?d WHERE `key` = "force_ssl"', intVal($protocol == 'https://')); - } - - CLI::write(); - } - else - $error[] = ' * could not access '.$protocol.$host.$testFile.' ['.$resp.']'; - } - } - else - $error[] = ' * '.strtoupper($conf).' is empty'; - } - - return empty($error); - } - - function testAcc(&$error) - { - $error = []; - return !!DB::Aowow()->selectCell('SELECT id FROM ?_account WHERE userPerms = 1'); - } - - function endSetup() - { - return DB::Aowow()->query('UPDATE ?_config SET value = 0 WHERE `key` = "maintenance"'); - } - - /********************/ - /* get current step */ - /********************/ - - $startStep = 0; - if (file_exists('cache/firstrun')) - { - $rows = file('cache/firstrun'); - if ((int)$rows[0] == AOWOW_REVISION) - $startStep = (int)$rows[1]; - } - - if ($startStep) - { - - CLI::write('Found firstrun progression info. (Halted on subscript '.($steps[$startStep][1] ?: $steps[$startStep][0]).')', CLI::LOG_INFO); - $inp = ['x' => ['continue setup? (y/n)', true, '/y|n/i']]; - $msg = ''; - if (!CLI::readInput($inp, true) || !$inp || strtolower($inp['x']) == 'n') - { - $msg = 'Starting setup from scratch...'; - $startStep = 0; - } - else - $msg = 'Resuming setup from step '.$startStep.'...'; - - CLI::write(); - CLI::write($msg); - sleep(1); - } - - /*******/ - /* run */ - /*******/ - - foreach ($steps as $idx => $step) - { - if ($startStep > $idx) - continue; - - if (!strpos($step[0], '::') && !is_callable($step[0])) - require_once 'setup/tools/clisetup/'.$step[0].'.func.php'; - - if ($step[3]) - { - CLI::write($step[3]); - $inp = ['x' => ['Press any key to continue', true]]; - - if (!CLI::readInput($inp, true)) // we don't actually care about the input - return; - } - - while (true) - { - $res = call_user_func($step[0], $step[1]); - - // check script result - if ($step[2]) - { - if (!$step[2]($errors)) - { - CLI::write($step[4], CLI::LOG_ERROR); - foreach ($errors as $e) - CLI::write($e); - } - else - { - $saveProgress($idx); - break; - } - } - else if ($res !== false) - { - $saveProgress($idx); - break; - } - - $inp = ['x' => ['['.CLI::bold('c').']ontinue anyway? ['.CLI::bold('r').']etry? ['.CLI::bold('a').']bort?', true, '/c|r|a/i']]; - if (CLI::readInput($inp, true) && $inp) - { - CLI::write(); - switch(strtolower($inp['x'])) - { - case 'c': - $saveProgress($idx); - break 2; - case 'r': - break; - case 'a': - return; - } - } - else - { - CLI::write(); - return; - } - } - } - - unlink('cache/firstrun'); - CLI::write('setup finished', CLI::LOG_OK); -} - -?> diff --git a/setup/tools/clisetup/setup.us.php b/setup/tools/clisetup/setup.us.php new file mode 100644 index 00000000..5b8c4762 --- /dev/null +++ b/setup/tools/clisetup/setup.us.php @@ -0,0 +1,208 @@ + [], 'doBuild' => []]; // ref to pass commands from 'update' to 'sync' + private $steps = array( + // [staticUS, $name, [...args]] + ['database', '', []], + ['configure', '', []], + // sql- and build- stuff here + ['update', '', []], + ['sync', '', []], + ['account', '', []] + ); + + private const STEP_FILE = 'cache/setup/firstrun'; + + private function getSavedStartStep() : int + { + if (file_exists(self::STEP_FILE)) + { + $rows = file(self::STEP_FILE); + if ((int)$rows[0] == AOWOW_REVISION) + return (int)$rows[1]; + } + + return 0; + } + + // Note! Must be loaded after all SetupScripts have been registered + public function __construct() + { + /****************/ + /* define steps */ + /****************/ + + # link required steps with param + $this->steps[2][2] = &$this->dynArgs; // update + $this->steps[3][2] = &$this->dynArgs; // sync + + # from /sqlgen + /filegen .. already sorted by CLISetup + foreach (CLISetup::getSubScripts() as $name => [$invoker, $ssRef]) + { + if ($ssRef->isOptional) + continue; + + if ($invoker == 'sql') + array_splice($this->steps, -3, 0, [[$invoker, $name, ['doSql' => $name]]]); + else if ($invoker == 'build') + array_splice($this->steps, -3, 0, [[$invoker, $name, ['doBuild' => $name]]]); + } + } + + // args: null, null, null, null // nnnn + public function run(&$args) : bool + { + /******************/ + /* get start step */ + /******************/ + + $startStep = 0; + if (($cliStartStep = CLISetup::getOpt('step')) !== false) + { + $startStep = ((int)$cliStartStep) - 1; + if ($startStep < 0 || $startStep >= count($this->steps)) + { + 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; + } + + CLI::write(); + 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()); + + + /*******/ + /* run */ + /*******/ + + foreach ($this->steps as $idx => [$usName, , $param]) + { + if ($startStep > $idx) + continue; + + while (true) + { + CLI::write('[setup] step '.($idx + 1).' / '.count($this->steps)); + if (CLISetup::run($usName, $param)) + { + $this->saveProgress($idx); + break; + } + + if (CLI::read(['x' => ['['.CLI::bold('c').']ontinue anyway? ['.CLI::bold('r').']etry? ['.CLI::bold('a').']bort?', true, true, '/c|r|a/i']], $uiCRA) && $uiCRA) + { + CLI::write(); + switch (strtolower($uiCRA['x'])) + { + case 'c': + $this->saveProgress($idx); + break 2; + case 'r': + break; + case 'a': + return false; + } + } + else + { + CLI::write(); + return false; + } + } + } + + unlink(self::STEP_FILE); + + return true; + } + + public function writeCLIHelp() : bool + { + CLI::write(' usage: php aowow --setup [--locales: --datasrc:] [--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 (($startStep = $this->getSavedStartStep()) !== 0) + { + CLI::write(); + 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(); + CLI::write(); + + return true; + } + + + /**********/ + /* helper */ + /**********/ + + private function saveProgress (int $nStep) : void + { + if ($h = fopen(self::STEP_FILE, 'w')) + { + fwrite($h, AOWOW_REVISION."\n".($nStep + 1)."\n"); + fclose($h); + } + else + CLI::write(' * UtilScript::setup - Could not access step file', CLI::LOG_ERROR); + } +}); + +?> diff --git a/setup/tools/clisetup/siteconfig.func.php b/setup/tools/clisetup/siteconfig.func.php deleted file mode 100644 index aad8a67f..00000000 --- a/setup/tools/clisetup/siteconfig.func.php +++ /dev/null @@ -1,424 +0,0 @@ - -f')); - break; - case 'profiler_queue': - $fn = function($x) { - if (!$x) - return true; - - $ok = Profiler::queueStart($msg); - if ($msg) - CLI::write($msg, CLI::LOG_ERROR); - - return $ok; - }; - break; - default: // nothing to do, everything is fine - return true; - } - - return $fn ? $fn($val) : true; - }; - - while (true) - { - CLI::write(); - CLI::write('select a numerical index to use the corresponding entry'); - - $sumNum = 0; - $cfgList = []; - $hasEmpty = false; - $mainBuff = []; - $miscBuff = []; // catg 'misc' should come last - - foreach (Util::$configCats as $idx => $cat) - { - if ($idx) - $mainBuff[] = '===== '.$cat.' ====='; - else - $miscBuff[] = '===== '.$cat.' ====='; - - $results = DB::Aowow()->select('SELECT *, (flags & ?d) AS php FROM ?_config WHERE `cat` = ?d ORDER BY `key` ASC', CON_FLAG_PHP, $idx); - - foreach ($results as $num => $data) - { - if (!($data['flags'] & CON_FLAG_PHP) && $data['value'] === '' && in_array($data['key'], $reqKeys)) - $hasEmpty = true; - - $cfgList[$sumNum + $num] = $data; - - $php = $data['flags'] & CON_FLAG_PHP; - $buff = "[".CLI::bold($sumNum + $num)."] ".(($sumNum + $num) > 9 ? '' : ' ').($php ? ' PHP ' : ' AOWOW '); - $buff .= str_pad($php ? strtolower($data['key']) : strtoupper($data['key']), 35); - if ($data['value'] === '') - $buff .= in_array($data['key'], $reqKeys) ? CLI::red('') : ''; - else - { - $info = explode(' - ', $data['comment']); - - if ($data['flags'] & CON_FLAG_TYPE_BOOL) - $buff .= '[bool] '.($data['value'] ? '' : ''); - else if ($data['flags'] & CON_FLAG_OPT_LIST && !empty($info[2])) - { - $buff .= "[opt] "; - foreach (explode(', ', $info[2]) as $option) - { - $opt = explode(':', $option); - $buff .= '['.($data['value'] == $opt[0] ? 'x' : ' ').']'.$opt[1].' '; - } - } - else if ($data['flags'] & CON_FLAG_BITMASK && !empty($info[2])) - { - $buff .= "[mask] "; - foreach (explode(', ', $info[2]) as $option) - { - $opt = explode(':', $option); - $buff .= '['.($data['value'] & (1 << $opt[0]) ? 'x' : ' ').']'.$opt[1].' '; - } - } - else if ($data['flags'] & CON_FLAG_TYPE_STRING) - $buff .= "[str] ".$data['value']; - else if ($data['flags'] & CON_FLAG_TYPE_FLOAT) - $buff .= "[float] ".floatVal($data['value']); - else /* if ($data['flags'] & CON_FLAG_TYPE_INT) */ - $buff .= "[int] ".intVal($data['value']); - } - - if ($idx) - $mainBuff[] = $buff; - else - $miscBuff[] = $buff; - - } - - $sumNum += count($results); - } - - foreach ($mainBuff as $b) - CLI::write($b); - - foreach ($miscBuff as $b) - CLI::write($b); - - CLI::write(str_pad("[".CLI::bold($sumNum)."]", 21)."add another php configuration"); - - if ($hasEmpty) - { - CLI::write(); - CLI::write("please configure the required empty setings", CLI::LOG_WARN); - } - - $inp = ['idx' => ['', false, '/\d/']]; - if (CLI::readInput($inp) && $inp && $inp['idx'] !== '') - { - // add new php setting - if ($inp['idx'] == $sumNum) - { - CLI::write(); - CLI::write("Adding additional php configuration."); - - while (true) - { - $setting = array( - 'key' => ['option name', false, '/[\w_\.\-]/i'], - 'val' => ['value', ] - ); - if (CLI::readInput($setting) && $setting) - { - CLI::write(); - - $key = strtolower($setting['key']); - if (ini_get($key) === false || ini_set($key, $setting['val']) === false) - { - CLI::write("this configuration option cannot be set", CLI::LOG_ERROR); - sleep(1); - } - else if (DB::Aowow()->selectCell('SELECT 1 FROM ?_config WHERE `flags` & ?d AND `key` = ?', CON_FLAG_PHP, $key)) - { - CLI::write("this configuration option is already in use", CLI::LOG_ERROR); - sleep(1); - } - else - { - DB::Aowow()->query('INSERT IGNORE INTO ?_config (`key`, `value`, `cat`, `flags`) VALUES (?, ?, 0, ?d)', $key, $setting['val'], CON_FLAG_TYPE_STRING | CON_FLAG_PHP); - CLI::write("new php configuration added", CLI::LOG_OK); - sleep(1); - } - - break; - } - else - { - CLI::write(); - CLI::write("edit canceled! returning to list...", CLI::LOG_INFO); - sleep(1); - break; - } - } - } - // edit existing setting - else if ($inp['idx'] >= 0 && $inp['idx'] < $sumNum) - { - $conf = $cfgList[$inp['idx']]; - $info = explode(' - ', $conf['comment']); - $key = strtolower($conf['key']); - $buff = ''; - - CLI::write(); - $buff .= $conf['flags'] & CON_FLAG_PHP ? " PHP: " : "AOWOW: "; - $buff .= $conf['flags'] & CON_FLAG_PHP ? $key : strtoupper('cfg_'.$conf['key']); - - if (!empty($info[1])) - $buff .= " - ".$info[1]; - - CLI::write($buff); - - $buff = "VALUE: "; - - if ($conf['flags'] & CON_FLAG_TYPE_BOOL) - $buff .= $conf['value'] ? '' : ''; - else if ($conf['flags'] & CON_FLAG_OPT_LIST && !empty($info[2])) - { - foreach (explode(', ', $info[2]) as $option) - { - $opt = explode(':', $option); - $buff .= '['.($conf['value'] == $opt[0] ? 'x' : ' ').'] '.$opt[1].' '; - } - } - else if ($conf['flags'] & CON_FLAG_BITMASK && !empty($info[2])) - { - foreach (explode(', ', $info[2]) as $option) - { - $opt = explode(':', $option); - $buff .= '['.($conf['value'] & (1 << $opt[0]) ? 'x' : ' ').'] '.$opt[1].' '; - } - } - else if ($conf['flags'] & CON_FLAG_TYPE_STRING) - $buff .= $conf['value']; - else if ($conf['flags'] & CON_FLAG_TYPE_FLOAT) - $buff .= floatVal($conf['value']); - else /* if ($conf['flags'] & CON_FLAG_TYPE_INT) */ - $buff .= intVal($conf['value']); - - CLI::write($buff); - CLI::write(); - CLI::write("[".CLI::bold('E')."]dit"); - - if (!($conf['flags'] & CON_FLAG_PERSISTENT)) - CLI::write("[".CLI::bold('D')."]elete"); - - if (strstr($info[0], 'default:')) - CLI::write("[".CLI::bold('R')."]estore Default - ".trim(explode('default:', $info[0])[1])); - - while (true) - { - $action = ['idx' => ['', true, '/[edr]/i']]; - if (CLI::readInput($action, true) && $action) - { - switch (strtoupper($action['idx'])) - { - case 'E': // edit value - $pattern = false; - $single = false; - $value = ['idx' => ['Select new value', false, &$pattern]]; - - if ($conf['flags'] & CON_FLAG_OPT_LIST) - { - $_valid = []; - foreach (explode(', ', $info[2]) as $option) - { - $opt = explode(':', $option); - $_valid[] = $opt[0]; - CLI::write('['.CLI::bold($opt[0]).'] '.$opt[1]); - } - $single = true; - $pattern = '/\d/'; - $validate = function ($v) use($_valid) { return in_array($v, $_valid); }; - } - else if ($conf['flags'] & CON_FLAG_BITMASK) - { - CLI::write('Bitmask: sum fields to select multiple options'); - $_valid = 0x0; - foreach (explode(', ', $info[2]) as $option) - { - $opt = explode(':', $option); - $_valid |= (1 << $opt[0]); - CLI::write('['.CLI::bold(1 << $opt[0]).']'.str_pad('', 4-strlen(1 << $opt[0])).$opt[1]); - } - $pattern = '/\d+/'; - $validate = function ($v) use($_valid) { $v = $v & $_valid; return $v; }; - } - else if ($conf['flags'] & CON_FLAG_TYPE_BOOL) - { - CLI::write('['.CLI::bold(0).'] Disabled'); - CLI::write('['.CLI::bold(1).'] Enabled'); - - $single = true; - $pattern = '/[01]/'; - $validate = function ($v) { return true; }; - } - else if ($conf['flags'] & CON_FLAG_TYPE_INT) - $validate = function ($v) { return preg_match('/^-?\d+$/i', $v); }; - else if ($conf['flags'] & CON_FLAG_TYPE_FLOAT) - $validate = function ($v) { return preg_match('/^-?\d*(,|.)?\d+$/i', $v); }; - else // string - $validate = function ($v) { return true; }; - - - while (true) - { - $use = $value; - if (CLI::readInput($use, $single)) - { - CLI::write(); - - if (!$validate($use ? $use['idx'] : '')) - { - CLI::write("value not in range", CLI::LOG_ERROR); - sleep(1); - continue; - } - else - { - $oldVal = DB::Aowow()->selectCell('SELECT `value` FROM ?_config WHERE `key` = ?', $key); - DB::Aowow()->query('UPDATE ?_config SET `value` = ? WHERE `key` = ?', $use['idx'], $key); - - // postChange returned false => reset value - if (!$onChange($key, $use['idx'])) - { - DB::Aowow()->query('UPDATE ?_config SET `value` = ? WHERE `key` = ?', $oldVal, $key); - sleep(1); - break; - } - - CLI::write("setting updated", CLI::LOG_OK); - sleep(1); - break 3; - } - } - else - { - CLI::write("edit canceled! returning to selection...", CLI::LOG_INFO); - sleep(1); - break; - } - } - - break 2; - case 'R': // restore default - if (!strstr($info[0], 'default:')) - continue 2; - - // @eval .. some dafault values are supplied as bitmask or the likes - $val = trim(explode('default:', $info[0])[1]); - if (!($conf['flags'] & CON_FLAG_TYPE_STRING)) - $val = @eval('return ('.$val.');'); - if (DB::Aowow()->query('UPDATE ?_config SET `value` = ? WHERE `key` = ?', $val, $key)) - { - CLI::write("default value restored", CLI::LOG_OK); - $onChange($key, $val); - sleep(1); - } - break 2; - case 'D': // delete config pair - if ($conf['flags'] & CON_FLAG_PERSISTENT) - continue 2; - - if (DB::Aowow()->query('DELETE FROM ?_config WHERE `key` = ? AND (`flags` & ?d) = 0', $key, CON_FLAG_PERSISTENT)) - { - CLI::write("php setting deleted ['".$conf['key']."': '".$conf['value']."']", CLI::LOG_OK); - sleep(1); - } - break 2; - } - } - else - { - CLI::write(); - CLI::write('edit canceled! returning to list...', CLI::LOG_INFO); - sleep(1); - break; - } - } - } - else - { - CLI::write(); - CLI::write('invalid selection', CLI::LOG_ERROR); - sleep(1); - } - } - else - { - CLI::write(); - CLI::write('site configuration aborted', CLI::LOG_INFO); - break; - } - - // propagate changes to static files - if ($updScripts && (!class_exists('FileGen') || FileGen::getMode() != FileGen::MODE_FIRSTRUN)) - { - require_once 'setup/tools/clisetup/build.func.php'; - CLI::write(); - CLI::write('regenerating affected static content', CLI::LOG_INFO); - CLI::write(); - sleep(1); - - if ($_ = array_diff($updScripts, build($updScripts))) - { - CLI::write(' - the following updates returned with errors, please recheck those - '.implode(', ', $_), CLI::LOG_ERROR); - sleep(1); - } - - sleep(1); - $updScripts = []; - } - } -} - -?> diff --git a/setup/tools/clisetup/siteconfig.us.php b/setup/tools/clisetup/siteconfig.us.php new file mode 100644 index 00000000..25b8247a --- /dev/null +++ b/setup/tools/clisetup/siteconfig.us.php @@ -0,0 +1,478 @@ + cfgName [newValue]]'; + public const DESCRIPTION = 'Configure site variables.'; + public const PROMPT = 'SITE_HOST and STATIC_HOST *must* be set. Also enable FORCE_SSL if needed. You may also want to change other variables such as NAME, NAME_SHORT or LOCALES.'; + public const NOTE_ERROR = 'could not access:'; + + public const REQUIRED_DB = [DB_AOWOW]; + + public const USE_CLI_ARGS = true; + + private const HTTP_STATUS_OK = 200; + private const HTTP_STATUS_MOVED_PERM = 301; + private const HTTP_STATUS_MOVED_TEMP = 302; + + private $updScripts = []; + + // args: action, configName, configValue, pendingUpdates[] // iiio + public function run(&$args) : bool + { + $action = $args[0] ?? ''; + $name = $args[1] ?? ''; + $value = $args[2] ?? ''; + + $result = true; + switch (strtoupper($action)) + { + case 'E': + $result = $this->doEdit($name, $args[2]); + break; + case 'R': + $result = $this->doRestore($name); + break; + case 'N': + $result = $this->doNew($name, $value); + break; + case 'D': + $result = $this->doDelete($name); + break; + default: + $this->showConfigList(); + } + + $args['doBuild'] = $this->updScripts; // push files to rebuild one level up + + return $result; + } + + private function showConfigList() : void + { + while (true) + { + CLI::write('select a numerical index or name to use the corresponding entry', -1, false); + CLI::write(); + + $sumNum = 0; + $cfgList = []; + $hasEmpty = false; + $listBuff = []; + + foreach (Cfg::$categories as $idx => $cat) + { + $listBuff[] = '===== '.$cat.' ====='; + + foreach (Cfg::forCategory($idx) as $key => [$value, $flags, $catg, $default, $comment]) + { + $isPhp = $flags & Cfg::FLAG_PHP; + + if ($value === '' && ($flags & Cfg::FLAG_REQUIRED)) + $hasEmpty = true; + + $cfgList[$sumNum] = strtolower($key); + + $row = '['.CLI::bold($sumNum).'] '.(($sumNum) > 9 ? '' : ' ').($isPhp ? ' PHP ' : ' AOWOW '); + $row .= str_pad($isPhp ? strtolower($key) : strtoupper($key), 35); + + $opts = explode(' - ', $comment); + $row .= $this->formatValue($value, $flags, $opts[1] ?? ''); + + $listBuff[] = $row; + $sumNum++; + } + } + + foreach ($listBuff as $b) + CLI::write($b, -1, false); + + CLI::write(str_pad('['.CLI::bold($sumNum).']', 21).'add another php configuration', -1, false); + CLI::write(); + + if ($hasEmpty) + { + CLI::write('please configure the required empty settings', CLI::LOG_WARN); + CLI::write(); + } + + if (CLI::read(['idx' => ['', false, false, Cfg::PATTERN_CONF_KEY_CHAR]], $uiIndex) && $uiIndex && $uiIndex['idx'] !== '') + { + $idx = array_search(strtolower($uiIndex['idx']), $cfgList); + if ($idx === false) + $idx = intVal($uiIndex['idx']); + + // add new php setting + if ($idx == $sumNum) + $this->showNewConfig(); + // edit existing setting + else if ($idx >= 0 && $idx < $sumNum) + $this->showEditConfig($cfgList[$idx] ?? ''); + else + CLI::write('invalid selection', CLI::LOG_ERROR); + + CLI::write(); + sleep(1); + } + else + { + CLI::write('leaving site configuration...', CLI::LOG_INFO); + CLI::write(); + break; + } + } + } + + private function showNewConfig() : void + { + CLI::write('Adding additional php configuration.'); + CLI::write(); + + $setting = array( + 'key' => ['option name', false, false, Cfg::PATTERN_CONF_KEY_CHAR], + 'val' => ['value'] + ); + if (CLI::read($setting, $uiSetting) && $uiSetting) + $this->doNew($uiSetting['key'], $uiSetting['val']); + else + CLI::write('edit canceled! returning to list...', CLI::LOG_INFO); + } + + private function showEditConfig(string $key) : void + { + [$value, $flags, , $default, $comment] = Cfg::get($key, false, true); + $info = explode(' - ', $comment); + $buff = ''; + + $buff .= $flags & Cfg::FLAG_PHP ? 'PHP: ' : 'AOWOW: '; + $buff .= $flags & Cfg::FLAG_PHP ? $key : 'Cfg::'.strtoupper($key); + + if (!empty($info[0])) + $buff .= ' - '.$info[0]; + + CLI::write($buff); + CLI::write(); + + CLI::write('VALUE: '.$this->formatValue($value, $flags, $info[1] ?? '')); + CLI::write(); + CLI::write('['.CLI::bold('E').']dit'); + + if (!($flags & Cfg::FLAG_PERSISTENT)) + CLI::write('['.CLI::bold('D').']elete'); + + if ($default) + CLI::write('['.CLI::bold('R').']estore Default - '.$default); + + CLI::write(); + + while (true) + { + CLI::write(); + sleep(1); + + if (CLI::read(['idx' => ['', true, true, '/[edr]/i']], $uiEDR) && $uiEDR) + { + switch (strtoupper($uiEDR['idx'])) + { + case 'E': // edit value + if (!$this->doEdit($key)) + continue 2; + break 2; + case 'R': // restore default + if (!$this->doRestore($key)) + continue 2; + break 2; + case 'D': // delete config pair + if (!$this->doDelete($key)) + continue 2; + break 2; + } + } + else + { + CLI::write('edit canceled! returning to list...', CLI::LOG_INFO); + break; + } + } + } + + private function doEdit(string $key, ?string $newVal = null) : bool + { + [, $flags, , , $comment] = Cfg::get($key, false, true); + $info = explode(' - ', $comment); + + $pattern = '/.*/'; + $single = false; + $typeHint = []; + + if ($flags & Cfg::FLAG_OPT_LIST) + { + foreach (explode(', ', $info[1]) as $option) + { + [$val, $name] = explode(':', $option); + $typeHint[] = '['.CLI::bold($val).'] '.$name; + } + $single = true; + $pattern = '/^\d$/'; + } + else if ($flags & Cfg::FLAG_BITMASK) + { + foreach (explode(', ', $info[1]) as $option) + { + [$val, $name] = explode(':', $option); + $typeHint[] = '['.CLI::bold(1 << $val).']'.str_pad('', 6 - strlen(1 << $val)).$name; + } + $typeHint[] = 'Bitmask: sum fields to select multiple options'; + $pattern = '/^\d+$/'; + } + else if ($flags & Cfg::FLAG_TYPE_BOOL) + { + $typeHint[] = '['.CLI::bold(0).'] Disabled'; + $typeHint[] = '['.CLI::bold(1).'] Enabled'; + + $single = true; + $pattern = '/^[01]$/'; + } + else if ($flags & Cfg::FLAG_TYPE_INT) + $pattern = '/^-?\d+$/'; + else if ($flags & Cfg::FLAG_TYPE_FLOAT) + $pattern = '/^-?\d*(,|.)?\d+$/i'; + + while (true) + { + if (!isset($newVal)) + foreach ($typeHint as $th) + CLI::write($th); + + if ((isset($newVal) && preg_match($pattern, $newVal)) || CLI::read(['idx' => ['Select new value', false, $single, $pattern]], $uiValue)) + { + CLI::write(); + + $val = $newVal ?? $uiValue['idx'] ?? ''; + unset($newVal); // we loop infinitely if this is set and invalid + + if ($err = Cfg::set($key, $val, $this->updScripts)) + { + CLI::write($err, CLI::LOG_ERROR); + continue; + } + else + { + CLI::write('setting updated', CLI::LOG_OK); + return true; + } + } + else + { + CLI::write('edit canceled! returning to selection...', CLI::LOG_INFO); + return false; + } + } + } + + private function doRestore(string $key) : bool + { + if ($err = Cfg::reset($key, $this->updScripts)) + { + CLI::write($err, CLI::LOG_ERROR); + return false; + } + + CLI::write('default value restored', CLI::LOG_OK); + return true; + } + + private function doNew(string $key, string $val) : bool + { + if ($err = Cfg::add($key, $val)) + { + CLI::write($err, CLI::LOG_ERROR); + return false; + } + + CLI::write('new php configuration added', CLI::LOG_OK); + return true; + } + + private function doDelete(string $key) : bool + { + if ($err = Cfg::delete($key)) + { + CLI::write($err, CLI::LOG_ERROR); + return false; + } + + CLI::write('php setting deleted: '.$key, CLI::LOG_OK); + return true; + } + + + /******************/ + /* Unit formating */ + /******************/ + + private function toOptList(string $options, $curVal, bool $bitmask = false) : string + { + $result = ''; + foreach (explode(', ', $options) as $opt) + { + [$val, $name] = explode(':', $opt); + $equal = $bitmask ? ($curVal & (1 << $val)) : $curVal == $val; + + $result .= '['.($equal ? 'x' : ' ').']'.$name.' '; + } + + return substr($result, 0, -1); + } + + private function formatValue($value, int $flags, string $opts) : string + { + if ($flags & Cfg::FLAG_TYPE_BOOL) + return '[bool] '.($value ? '' : ''); + + if ($flags & Cfg::FLAG_OPT_LIST) + return '[opt] '.$this->toOptList($opts, $value, false); + + if ($flags & Cfg::FLAG_BITMASK) + return '[mask] '.$this->toOptList($opts, $value, true); + + if ($flags & Cfg::FLAG_TYPE_FLOAT) + return '[float] '.floatVal($value); + + if ($flags & Cfg::FLAG_TYPE_INT) + return '[int] '.intVal($value); + + // if ($flags & Cfg::FLAG_TYPE_STRING) + if ($value === '') + return '[str] '.(($flags & Cfg::FLAG_REQUIRED) ? CLI::red('') : CLI::grey('')); + else + return '[str] "'.$value.'"'; + } + + + /****************/ + /* Help display */ + /****************/ + + public function writeCLIHelp(string ...$ss) : bool + { + CLI::write(' usage: php aowow --configure [action cfgName [newValue]]', -1, false); + CLI::write(); + CLI::write(' Configures site and php variables. If incomplete parameters are passed an interactive prompt will open.', -1, false); + CLI::write(); + CLI::write(' action:', -1, false); + CLI::write(' E - Edit variable named '.CLI::bold('cfgName').'. Optionally directly pass a new value.', -1, false); + CLI::write(' R - Restore default value of a variable '.CLI::bold('cfgName').'.', -1, false); + CLI::write(' N - Create a new php config value. Must be a valid php directive. Optionally directly pass a new value.', -1, false); + CLI::write(' D - Delete an existing php config value.', -1, false); + CLI::write(); + CLI::write(); + + return true; + } + + + /***********/ + /* DB test */ + /***********/ + + public function test(?array &$error = []) : bool + { + $error = []; + + if (!DB::isConnected(DB_AOWOW)) + { + $error[] = ' * not connected to DB'; + return false; + } + + $prot = Cfg::get('FORCE_SSL') ? 'https://' : 'http://'; + $cases = array( + 'site_host' => [$prot, Cfg::get('SITE_HOST'), '/index.php'], + 'static_host' => [$prot, Cfg::get('STATIC_HOST'), '/css/aowow.css'] + ); + + foreach ($cases as $conf => [$protocol, $host, $testFile]) + { + if ($host) + { + $resp = 0; + if (!$this->testCase($protocol, $host, $testFile, $resp)) + { + if ($resp == self::HTTP_STATUS_MOVED_PERM || $resp == self::HTTP_STATUS_MOVED_TEMP) + { + CLI::write('self test received status '.CLI::bold($resp).' (page moved) for '.$conf.', pointing to: '.$protocol.$host.$testFile, CLI::LOG_WARN); + if (!CLI::read(['x' => ['should '.CLI::bold($conf).' be set to '.CLI::bold($host).' and force_ssl be updated? (y/n)', true, true, '/y|n/i']], $uiN) || !$uiN || strtolower($uiN['x']) == 'n') + $error[] = ' * '.$protocol.$host.$testFile.' ['.$resp.']'; + else + { + Cfg::set($conf, $host); + Cfg::set('FORCE_SSL', $protocol == 'https://'); + } + + CLI::write(); + } + else + $error[] = ' * '.$protocol.$host.$testFile.' ['.$resp.']'; + } + } + else + $error[] = ' * '.strtoupper($conf).' is empty'; + } + + return empty($error); + } + + private function testCase(&$protocol, &$host, $testFile, &$status) : bool + { + // https://stackoverflow.com/questions/14279095/allow-self-signed-certificates-for-https-wrapper + $ctx = stream_context_create(array( + 'ssl' => ['verify_peer' => false, + 'allow_self_signed' => true] + )); + + $res = get_headers($protocol.$host.$testFile, true, $ctx); + + if (!$res || !preg_match('/HTTP\/[0-9\.]+\s+([0-9]+)/', $res[0], $m)) + return false; + + $status = $m[1]; + + if ($status == self::HTTP_STATUS_OK) + return true; + + if ($status == self::HTTP_STATUS_MOVED_PERM || $status == self::HTTP_STATUS_MOVED_TEMP) + { + if (!empty($res['Location']) && preg_match('/(https?:\/\/)(.*)'.strtr($testFile, ['/' => '\/', '.' => '\.']).'/i', is_array($res['Location']) ? $res['Location'][0] : $res['Location'], $n)) + { + $protocol = $n[1]; + $host = $n[2]; + } + + return false; + } + + $status = 0; + return false; + } +}); + +?> diff --git a/setup/tools/clisetup/sql.func.php b/setup/tools/clisetup/sql.func.php deleted file mode 100644 index 735a910c..00000000 --- a/setup/tools/clisetup/sql.func.php +++ /dev/null @@ -1,56 +0,0 @@ - diff --git a/setup/tools/clisetup/sync.us.php b/setup/tools/clisetup/sync.us.php new file mode 100644 index 00000000..38d20d81 --- /dev/null +++ b/setup/tools/clisetup/sync.us.php @@ -0,0 +1,106 @@ +'; + + public const REQUIRED_DB = [DB_AOWOW, DB_WORLD]; + + // sqlToDo, buildToDo, null, null // iinn + public function run(&$args) : bool + { + $s = &$args['doSql']; + $b = &$args['doBuild']; + + // called manually + if ($s === null && $b === null) + { + [$s, $b] = $this->handleCLIOpt(); + if (!$s && !$b && !CLISetup::getOpt('setup')) + { + CLI::write('[sync] no valid table names supplied', CLI::LOG_ERROR); + return false; + } + } + + if ($s) + { + $io = ['doSql' => $s, 'doneSql' => []]; + CLISetup::run('sql', $io); + 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()->qry('UPDATE ::dbversion SET `build` = %s', implode(' ', array_diff($io['doBuild'], $io['doneBuild']))); + } + + return true; + } + + private function handleCLIOpt() : array + { + $sql = []; + $build = []; + + $sync = CLISetup::getOpt('sync'); + if (!$sync) + return [$sql, $build]; + + foreach (CLISetup::getSubScripts() as $name => [$invoker, $ssRef]) + if (array_intersect($ssRef->getRemoteDependencies(), $sync)) + $$invoker[] = $name; + + do + { + $n = count($sql); + foreach (CLISetup::getSubScripts('sql') as $name => [, $ssRef]) + if (!in_array($name, $sql) && array_intersect($ssRef->getSelfDependencies()[0], $sql)) + $sql[] = $name; + } + while ($n != count($sql)); + + if ($sql) + foreach (CLISetup::getSubScripts('build') as $name => [, $ssRef]) + if (array_intersect($ssRef->getSelfDependencies()[0], $sql)) + $build[] = $name; + + return [array_unique($sql), array_unique($build)]; + } + + public function writeCLIHelp() : bool + { + CLI::write(' usage: php aowow --sync= [--locales: --datasrc: --force -f]', -1, false); + CLI::write(); + CLI::write(' Truncates and recreates AoWoW tables and static data files that depend on the given TC world table. Use this command after you updated your world database.', -1, false); + CLI::write(); + CLI::write(' e.g.: "php aowow --sync=creature_queststarter" causes the table aowow_quests_startend to be recreated.', -1, false); + CLI::write(' Also quest-related profiler files will be recreated as they depend on aowow_quests_startend and thus indirectly on creature_queststarter.', -1, false); + CLI::write(); + CLI::write(); + + return true; + } +}) + +?> diff --git a/setup/tools/clisetup/update.func.php b/setup/tools/clisetup/update.func.php deleted file mode 100644 index 56a62aa6..00000000 --- a/setup/tools/clisetup/update.func.php +++ /dev/null @@ -1,78 +0,0 @@ -selectRow('SELECT `date`, `part` FROM ?_dbversion')); - - CLI::write('checking sql updates'); - - $nFiles = 0; - foreach (glob('setup/updates/*.sql') as $file) - { - $pi = pathinfo($file); - list($fDate, $fPart) = explode('_', $pi['filename']); - - $fData = intVal($fDate); - - if ($date && $fDate < $date) - continue; - else if ($part && $date && $fDate == $date && $fPart <= $part) - continue; - - $nFiles++; - - $updQuery = ''; - $nQuerys = 0; - foreach (file($file) as $line) - { - // skip comments - if (substr($line, 0, 2) == '--' || $line == '') - continue; - - $updQuery .= $line; - - // semicolon at the end -> end of query - if (substr(trim($line), -1, 1) == ';') - { - if (DB::Aowow()->query($updQuery)) - $nQuerys++; - - $updQuery = ''; - } - } - - DB::Aowow()->query('UPDATE ?_dbversion SET `date`= ?d, `part` = ?d', $fDate, $fPart); - CLI::write(' -> '.date('d.m.Y', $fDate).' #'.$fPart.': '.$nQuerys.' queries applied', CLI::LOG_OK); - } - - CLI::write($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 - list($sql, $build) = array_values(DB::Aowow()->selectRow('SELECT `sql`, `build` FROM ?_dbversion')); - - sleep(1); - - $sql = trim($sql) ? array_unique(explode(' ', trim($sql))) : []; - $build = trim($build) ? array_unique(explode(' ', trim($build))) : []; - - if ($sql) - CLI::write('The following table(s) require syncing: '.implode(', ', $sql)); - - if ($build) - CLI::write('The following file(s) require syncing: '.implode(', ', $build)); - - return [$sql, $build]; -} - -?> diff --git a/setup/tools/clisetup/update.us.php b/setup/tools/clisetup/update.us.php new file mode 100644 index 00000000..9b4cc185 --- /dev/null +++ b/setup/tools/clisetup/update.us.php @@ -0,0 +1,128 @@ +date, $this->part] = array_values(DB::Aowow()->selectRow('SELECT `date`, `part` FROM ::dbversion')); + } + + // args: null, null, sqlToDo, buildToDo // nnoo + public function run(&$args) : bool + { + $sql = &$args['doSql']; + $build = &$args['doBuild']; + + CLI::write('[update] checking for sql updates...'); + + $nFiles = 0; + foreach (glob('setup/sql/updates/*.sql') as $file) + { + $pi = pathinfo($file); + + // invalid file + if (!preg_match('/(\d{10})_(\d{2})/', $pi['filename'], $m)) + continue; + + $fDate = intVal($m[1]); + $fPart = intVal($m[2]); + + if ($this->date && $fDate < $this->date) + continue; + else if ($this->part && $this->date && $fDate == $this->date && $fPart <= $this->part) + continue; + + $nFiles++; + + $updQuery = ''; + $nQuerys = 0; + foreach (file($file) as $line) + { + // skip comments + if (substr($line, 0, 2) == '--' || $line == '') + continue; + + $updQuery .= $line; + + // semicolon at the end -> end of query + if (substr(trim($line), -1, 1) == ';') + { + if (DB::Aowow()->qry($updQuery)) + $nQuerys++; + + $updQuery = ''; + } + } + + 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 = 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)))) : []; + + sleep(1); + + if ($sql) + CLI::write('[update] The following sql scripts have been scheduled: '.implode(', ', $sql)); + + if ($build) + CLI::write('[update] The following build scripts have been scheduled: '.implode(', ', $build)); + + return true; + } + + public function writeCLIHelp() : bool + { + CLI::write(' usage: php aowow --update', -1, false); + 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); + + if ($this->date) + { + CLI::write(); + CLI::write(' Last Update: '.date(Util::$dateFormatInternal, $this->date).' (Part #'.$this->part.')', -1, false); + } + + CLI::write(); + CLI::write(); + + return true; + } +}); + +?> diff --git a/setup/tools/dbc.class.php b/setup/tools/dbc.class.php deleted file mode 100644 index 5738a454..00000000 --- a/setup/tools/dbc.class.php +++ /dev/null @@ -1,624 +0,0 @@ - - - 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 . -*/ - -if (!defined('AOWOW_REVISION')) - die('illegal access'); - -if (!CLI) - die('not in cli mode'); - - -/* - Supported format characters: - x - not used/unknown, 4 bytes - X - not used/unknown, 1 byte - s - char* - f - float, 4 bytes (rounded to 4 digits after comma) - u - unsigned int, 4 bytes - i - signed int, 4 bytes - b - unsigned char, 1 byte - d - sorted by this field, not included in array - n - same, but field included in array -*/ -class DBC -{ - private $_formats = array( // locales block for copy pasta: sxssxxsxsxxxxxxxx | xxxxxxxxxxxxxxxxx - 'achievement' => 'niiisxssxxsxsxxxxxxxxsxssxxsxsxxxxxxxxiiiiisxssxxsxsxxxxxxxxii', - 'achievement_category' => 'nisxssxxsxsxxxxxxxxx', - 'achievement_criteria' => 'niiiiiiiisxssxxsxsxxxxxxxxiixii', - 'areatable' => 'niixixxiiixsxssxxsxsxxxxxxxxixxxxxxx', - 'areatrigger' => 'niffxxxxxx', - 'battlemasterlist' => 'niixxxxxxixxxxxxxxxxxxxxxxxxixii', - 'charbaseinfo' => 'bb', - 'charstartoutfit' => 'nbbbXiiiiiiiiiiiiiiiiiiiixxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx', - 'chartitles' => 'nxsxssxxsxsxxxxxxxxsxssxxsxsxxxxxxxxi', - 'chrclasses' => 'nxixsxssxxsxsxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxsxixi', - 'chrraces' => 'niixxxxixxxsxisxssxxsxsxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxi', - 'creaturedisplayinfo' => 'niiixxssssxxixxx', - 'creaturedisplayinfoextra' => 'nxxxxxxxxxxxxxxxxxxxs', - 'creaturefamily' => 'nxxxxixiiisxssxxsxsxxxxxxxxs', - 'creaturemodeldata' => 'nxxxxxxxxxxxxixxxxxxxxxxxxxx', - 'creaturesounddata' => 'niiiixiiiiiiiiixxxxixxxxixiiiiixxiiiix', - 'currencytypes' => 'niix', - 'dungeonmap' => 'niiffffi', - 'durabilitycosts' => 'niiiiiiiiixiiiiiiiiiiixiiiixix', - 'durabilityquality' => 'nf', - 'emotes' => 'nxixxxx', - 'emotestext' => 'nsiixxxixixxxxxxxxx', - 'emotestextdata' => 'nsxssxxsxsxxxxxxxx', - 'emotestextsound' => 'niiii', - 'faction' => 'nixxxxxxxxxxxxixxxiffixsxssxxsxsxxxxxxxxxxxxxxxxxxxxxxxxx', - 'factiontemplate' => 'nixiiiiiiiiiii', - 'gemproperties' => 'nixxi', - 'glyphproperties' => 'niii', - 'gtchancetomeleecrit' => 'f', - 'gtchancetomeleecritbase' => 'f', - 'gtchancetospellcrit' => 'f', - 'gtchancetospellcritbase' => 'f', - 'gtcombatratings' => 'f', - 'gtoctclasscombatratingscalar' => 'nf', - 'gtoctregenhp' => 'f', - 'gtregenmpperspt' => 'f', - 'gtregenhpperspt' => 'f', - 'holidays' => 'nxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxixxxxxxxxxxiisxix', - 'holidaydescriptions' => 'nsxssxxsxsxxxxxxxx', - 'holidaynames' => 'nsxssxxsxsxxxxxxxx', - 'itemdisplayinfo' => 'nssxxsxxxxxiixxxxxxxxxxxx', - 'itemgroupsounds' => 'niixx', - 'itemextendedcost' => 'niiiiiiiiiiiiiix', - 'itemlimitcategory' => 'nsxssxxsxsxxxxxxxxii', - 'itemrandomproperties' => 'nsiiiiisxssxxsxsxxxxxxxx', - 'itemrandomsuffix' => 'nsxssxxsxsxxxxxxxxsiiiiiiiiii', - 'itemset' => 'nsxssxxsxsxxxxxxxxxxxxxxxxxxxxxxxxxiiiiiiiiiiiiiiiiii', - 'itemsubclass' => 'iixxxxxxxixxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx', - 'lfgdungeons' => 'nsxssxxsxsxxxxxxxxiiiiiiixiixixixxxxxxxxxxxxxxxxx', - 'lock' => 'niiiiixxxiiiiixxxiiiiixxxxxxxxxxx', - 'mailtemplate' => 'nsxssxxsxsxxxxxxxxsxssxxsxsxxxxxxxx', - 'map' => 'nsixisxssxxsxsxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxiffxixi', - 'mapdifficulty' => 'niixxxxxxxxxxxxxxxxxxis', - 'material' => 'nxxii', - 'npcsounds' => 'niiix', - 'overridespelldata' => 'niiiixixxxxx', - 'powerdisplay' => 'nisbbb', - 'questfactionreward' => 'niiiiiiiiii', - 'questxp' => 'niiiiiiiiii', - 'randproppoints' => 'niiiiiiiiiiiiiii', - 'scalingstatdistribution' => 'niiiiiiiiiiiiiiiiiiiii', - 'scalingstatvalues' => 'xniiiiiiiiiiiiiiiiiiiiii', - 'skillline' => 'nixsxssxxsxsxxxxxxxxsxssxxsxsxxxxxxxxixxxxxxxxxxxxxxxxxx', - 'skilllineability' => 'niiiixxixiiixx', - 'skillraceclassinfo' => 'niiiiixx', - 'soundambience' => 'nii', - 'soundemitters' => 'nffxxxxiix', - 'soundentries' => 'nisssssssssssxxxxxxxxxxsxixxxx', - 'spell' => 'niiiuuuuuuuuixixxxixxxxxxxxxiiixxxxiiiiiiiiiiiixxiiiiiiiiiiiiiiiiiiiiiiiiiiiifffiiiiiiiiiiiiiiiiiiiiifffiiiiiiiiiiiiiiifffiiiiiiiiiiiiixsxssxxsxsxxxxxxxxsxssxxsxsxxxxxxxxsxssxxsxsxxxxxxxxsxssxxsxsxxxxxxxxiiiiiiiiiixxfffxxxiixiixifffii', - 'spellcasttimes' => 'nixx', - 'spelldescriptionvariables' => 'ns', - 'spelldifficulty' => 'xiiii', - 'spellduration' => 'nixx', - 'spellfocusobject' => 'nsxssxxsxsxxxxxxxx', - 'spellicon' => 'ns', - 'spellitemenchantment' => 'niiiiiiixxxiiisxssxxsxsxxxxxxxxxxxiiii', - 'spellitemenchantmentcondition' => 'nbbbbbxxxxxbbbbbbbbbbiiiiiXXXXX', - 'spellradius' => 'nfxf', - 'spellrange' => 'nffffisxssxxsxsxxxxxxxxxxxxxxxxxxxxxxxxx', - 'spellrunecost' => 'niiii', - 'spellshapeshiftform' => 'nxsxssxxsxsxxxxxxxxiixxiixxiiiiiiii', - 'spellvisual' => 'niiiiiixxxxiixiixxxxxxiiiixxxxxx', - 'spellvisualkit' => 'nxxxxxxxxxxxxxxixxxxxxxxxxxxxxxxxxxxxx', - 'talent' => 'niiiiiiiixxxxixxixxixii', - 'talenttab' => 'nsxssxxsxsxxxxxxxxiiiiis', - 'taxinodes' => 'niffxsxssxxsxsxxxxxxxxxx', - 'taxipath' => 'niix', - 'taxipathnode' => 'niiiffxxxxx', - 'totemcategory' => 'nsxssxxsxsxxxxxxxxiu', - 'vocaluisounds' => 'nxiiixx', - 'weaponimpactsounds' => 'nixiiiiiiiiiiiiiiiiiiii', - 'weaponswingsounds2' => 'nixi', - 'worldmaparea' => 'niisffffxix', // 4.x - niisffffxixxxx - 'worldmapoverlay' => 'niixxxxxsiiiixxxx', // 4.x - niixxxsiiiixxxx - 'worldmaptransforms' => 'niffffiffi', - 'worldstatezonesounds' => 'iiiiiiix', - 'zoneintromusictable' => 'nxixx', - 'zonemusic' => 'nxxxxxii' - ); - - private $_fields = array( - 'achievement' => 'id,faction,map,previous,name_loc0,name_loc2,name_loc3,name_loc6,name_loc8,description_loc0,description_loc2,description_loc3,description_loc6,description_loc8,category,points,orderInGroup,flags,iconId,reward_loc0,reward_loc2,reward_loc3,reward_loc6,reward_loc8,reqCriteriaCount,refAchievement', - 'achievement_category' => 'id,parentCategory,name_loc0,name_loc2,name_loc3,name_loc6,name_loc8', - 'achievement_criteria' => 'id,refAchievementId,type,value1,value2,value3,value4,value5,value6,name_loc0,name_loc2,name_loc3,name_loc6,name_loc8,completionFlags,groupFlags,timeLimit,order', - 'areatable' => 'id,mapId,areaTable,flags,soundAmbience,zoneMusic,zoneIntroMusic,name_loc0,name_loc2,name_loc3,name_loc6,name_loc8,factionGroupMask', - 'areatrigger' => 'id,mapId,posY,posX', - 'battlemasterlist' => 'id,mapId,moreMapId,areaType,maxPlayers,minLevel,maxLevel', - 'charbaseinfo' => 'raceId,classId', - 'charstartoutfit' => 'id,raceId,classId,gender,item1,item2,item3,item4,item5,item6,item7,item8,item9,item10,item11,item12,item13,item14,item15,item16,item17,item18,item19,item20', - 'chartitles' => 'id,male_loc0,male_loc2,male_loc3,male_loc6,male_loc8,female_loc0,female_loc2,female_loc3,female_loc6,female_loc8,bitIdx', - 'chrclasses' => 'id,powerType,name_loc0,name_loc2,name_loc3,name_loc6,name_loc8,fileString,flags,expansion', - 'chrraces' => 'id,flags,factionId,baseLanguage,fileString,side,name_loc0,name_loc2,name_loc3,name_loc6,name_loc8,expansion', - 'creaturedisplayinfo' => 'id,modelId,creatureSoundId,extraInfoId,skin1,skin2,skin3,iconString,npcSoundId', - 'creaturedisplayinfoextra' => 'id,textureString', - 'creaturefamily' => 'id,skillLine1,petFoodMask,petTalentType,categoryEnumID,name_loc0,name_loc2,name_loc3,name_lo6,name_loc8,iconString', - 'creaturemodeldata' => 'id,creatureSoundId', - 'creaturesounddata' => 'id,exertion,exertionCritical,injury,injuryCritical,death,stun,stand,footstepTerrainId,aggro,wingFlap,wingGlide,alert,fidget,customAttack,loop,jumpStart,jumpEnd,petAttack,petOrder,petDismiss,birth,spellcast,submerge,submerged', - 'currencytypes' => 'id,itemId,category', - 'dungeonmap' => 'id,mapId,floor,minY,maxY,minX,maxX,areaId', - 'durabilitycosts' => 'id,w0,w1,w2,w3,w4,w5,w6,w7,w8,w10,w11,w12,w13,w14,w15,w16,w17,w18,w19,w20,a1,a2,a3,a4,a6', - 'durabilityquality' => 'id,mod', - 'emotes' => 'id,animationId', - 'emotestext' => 'id,command,emoteId,targetId,noTargetId,selfId', - 'emotestextsound' => 'id,emotesTextId,raceId,gender,soundId', - 'emotestextdata' => 'id,text_loc0,text_loc2,text_loc3,text_loc6,text_loc8', - 'faction' => 'id,repIdx,repFlags1,parentFaction,spilloverRateIn,spilloverRateOut,spilloverMaxRank,name_loc0,name_loc2,name_loc3,name_loc6,name_loc8', - 'factiontemplate' => 'id,factionId,ourMask,friendlyMask,hostileMask,enemyFactionId1,enemyFactionId2,enemyFactionId3,enemyFactionId4,friendFactionId1,friendFactionId2,friendFactionId3,friendFactionId4', - 'gemproperties' => 'id,enchantmentId,colorMask', - 'glyphproperties' => 'id,spellId,typeFlags,iconId', - 'gtchancetomeleecrit' => 'chance', - 'gtchancetomeleecritbase' => 'chance', - 'gtchancetospellcrit' => 'chance', - 'gtchancetospellcritbase' => 'chance', - 'gtcombatratings' => 'ratio', - 'gtoctclasscombatratingscalar' => 'idx,ratio', - 'gtoctregenhp' => 'ratio', - 'gtregenmpperspt' => 'ratio', - 'gtregenhpperspt' => 'ratio', - 'holidays' => 'id,looping,nameId,descriptionId,textureString,scheduleType', - 'holidaydescriptions' => 'id,description_loc0,description_loc2,description_loc3,description_loc6,description_loc8', - 'holidaynames' => 'id,name_loc0,name_loc2,name_loc3,name_loc6,name_loc8', - 'itemdisplayinfo' => 'id,leftModelName,rightModelName,inventoryIcon1,spellVisualId,groupSoundId', - 'itemgroupsounds' => 'id,pickUpSoundId,dropDownSoundId', - 'itemextendedcost' => 'id,reqHonorPoints,reqArenaPoints,reqArenaSlot,reqItemId1,reqItemId2,reqItemId3,reqItemId4,reqItemId5,itemCount1,itemCount2,itemCount3,itemCount4,itemCount5,reqPersonalRating', - 'itemlimitcategory' => 'id,name_loc0,name_loc2,name_loc3,name_loc6,name_loc8,count,isGem', - 'itemrandomproperties' => 'id,nameINT,enchantId1,enchantId2,enchantId3,enchantId4,enchantId5,name_loc0,name_loc2,name_loc3,name_loc6,name_loc8', - 'itemrandomsuffix' => 'id,name_loc0,name_loc2,name_loc3,name_loc6,name_loc8,nameINT,enchantId1,enchantId2,enchantId3,enchantId4,enchantId5,allocationPct1,allocationPct2,allocationPct3,allocationPct4,allocationPct5', - 'itemset' => 'id,name_loc0,name_loc2,name_loc3,name_loc6,name_loc8,spellId1,spellId2,spellId3,spellId4,spellId5,spellId6,spellId7,spellId8,itemCount1,itemCount2,itemCount3,itemCount4,itemCount5,itemCount6,itemCount7,itemCount8,reqSkillId,reqSkillLevel', - 'itemsubclass' => 'class,subClass,weaponSize', - 'lfgdungeons' => 'id,name_loc0,name_loc2,name_loc3,name_loc6,name_loc8,levelMin,levelMax,targetLevel,targetLevelMin,targetLevelMax,mapId,difficulty,type,faction,expansion,groupId', - 'lock' => 'id,type1,type2,type3,type4,type5,properties1,properties2,properties3,properties4,properties5,reqSkill1,reqSkill2,reqSkill3,reqSkill4,reqSkill5', - 'mailtemplate' => 'id,subject_loc0,subject_loc2,subject_loc3,subject_loc6,subject_loc8,text_loc0,text_loc2,text_loc3,text_loc6,text_loc8', - 'map' => 'id,nameINT,areaType,isBG,name_loc0,name_loc2,name_loc3,name_loc6,name_loc8,parentMapId,parentX,parentY,expansion,maxPlayers', - 'mapdifficulty' => 'id,mapId,difficulty,nPlayer,nPlayerString', - 'material' => 'id,sheatheSoundId,unsheatheSoundId', - 'npcsounds' => 'id,greetSoundId,byeSoundId,angrySoundId', - 'overridespelldata' => 'id,spellId1,spellId2,spellId3,spellId4,spellId5', - 'powerdisplay' => 'id,realType,globalString,r,g,b', - 'questfactionreward' => 'id,field1,field2,field3,field4,field5,field6,field7,field8,field9,field10', - 'questxp' => 'id,field1,field2,field3,field4,field5,field6,field7,field8,field9,field10', - 'randproppoints' => 'id,epic1,epic2,epic3,epic4,epic5,rare1,rare2,rare3,rare4,rare5,uncommon1,uncommon2,uncommon3,uncommon4,uncommon5', - 'scalingstatdistribution' => 'id,statMod1,statMod2,statMod3,statMod4,statMod5,statMod6,statMod7,statMod8,statMod9,statMod10,modifier1,modifier2,modifier3,modifier4,modifier5,modifier6,modifier7,modifier8,modifier9,modifier10,maxLevel', - 'scalingstatvalues' => 'id,shoulderMultiplier,trinketMultiplier,weaponMultiplier,rangedMultiplier,clothShoulderArmor,leatherShoulderArmor,mailShoulderArmor,plateShoulderArmor,weaponDPS1H,weaponDPS2H,casterDPS1H,casterDPS2H,rangedDPS,wandDPS,spellPower,primBudged,tertBudged,clothCloakArmor,clothChestArmor,leatherChestArmor,mailChestArmor,plateChestArmor', - 'skillline' => 'id,categoryId,name_loc0,name_loc2,name_loc3,name_loc6,name_loc8,description_loc0,description_loc2,description_loc3,description_loc6,description_loc8,iconId', - 'skilllineability' => 'id,skillLineId,spellId,reqRaceMask,reqClassMask,reqSkillLevel,acquireMethod,skillLevelGrey,skillLevelYellow', - 'skillraceclassinfo' => 'id,skillLine,raceMask,classMask,flags,reqLevel', - 'soundambience' => 'id,soundIdDay,soundIdNight', - 'soundemitters' => 'id,posY,posX,soundId,mapId', - 'soundentries' => 'id,type,name,file1,file2,file3,file4,file5,file6,file7,file8,file9,file10,path,flags', - 'spell' => 'id,category,dispelType,mechanic,attributes0,attributes1,attributes2,attributes3,attributes4,attributes5,attributes6,attributes7,stanceMask,stanceMaskNot,spellFocus,castTimeId,recoveryTime,recoveryTimeCategory,procChance,procCharges,maxLevel,baseLevel,spellLevel,durationId,powerType,powerCost,powerCostPerLevel,powerPerSecond,powerPerSecondPerLevel,rangeId,stackAmount,tool1,tool2,reagent1,reagent2,reagent3,reagent4,reagent5,reagent6,reagent7,reagent8,reagentCount1,reagentCount2,reagentCount3,reagentCount4,reagentCount5,reagentCount6,reagentCount7,reagentCount8,equippedItemClass,equippedItemSubClassMask,equippedItemInventoryTypeMask,effect1Id,effect2Id,effect3Id,effect1DieSides,effect2DieSides,effect3DieSides,effect1RealPointsPerLevel,effect2RealPointsPerLevel,effect3RealPointsPerLevel,effect1BasePoints,effect2BasePoints,effect3BasePoints,effect1Mechanic,effect2Mechanic,effect3Mechanic,effect1ImplicitTargetA,effect2ImplicitTargetA,effect3ImplicitTargetA,effect1ImplicitTargetB,effect2ImplicitTargetB,effect3ImplicitTargetB,effect1RadiusId,effect2RadiusId,effect3RadiusId,effect1AuraId,effect2AuraId,effect3AuraId,effect1Periode,effect2Periode,effect3Periode,effect1ValueMultiplier,effect2ValueMultiplier,effect3ValueMultiplier,effect1ChainTarget,effect2ChainTarget,effect3ChainTarget,effect1CreateItemId,effect2CreateItemId,effect3CreateItemId,effect1MiscValue,effect2MiscValue,effect3MiscValue,effect1MiscValueB,effect2MiscValueB,effect3MiscValueB,effect1TriggerSpell,effect2TriggerSpell,effect3TriggerSpell,effect1PointsPerComboPoint,effect2PointsPerComboPoint,effect3PointsPerComboPoint,effect1SpellClassMaskA,effect2SpellClassMaskA,effect3SpellClassMaskA,effect1SpellClassMaskB,effect2SpellClassMaskB,effect3SpellClassMaskB,effect1SpellClassMaskC,effect2SpellClassMaskC,effect3SpellClassMaskC,spellVisualId1,spellVisualId2,iconId,iconIdActive,name_loc0,name_loc2,name_loc3,name_loc6,name_loc8,rank_loc0,rank_loc2,rank_loc3,rank_loc6,rank_loc8,description_loc0,description_loc2,description_loc3,description_loc6,description_loc8,buff_loc0,buff_loc2,buff_loc3,buff_loc6,buff_loc8,powerCostPercent,startRecoveryCategory,startRecoveryTime,maxTargetLevel,spellFamilyId,spellFamilyFlags1,spellFamilyFlags2,spellFamilyFlags3,maxAffectedTargets,damageClass,effect1DamageMultiplier,effect2DamageMultiplier,effect3DamageMultiplier,toolCategory1,toolCategory2,schoolMask,runeCostId,powerDisplayId,effect1BonusMultiplier,effect2BonusMultiplier,effect3BonusMultiplier,spellDescriptionVariable,spellDifficulty', - 'spellcasttimes' => 'id,baseTime', - 'spelldescriptionvariables' => 'id,vars', - 'spellduration' => 'id,baseTime', - 'spelldifficulty' => 'normal10,normal25,heroic10,heroic25', - 'spellfocusobject' => 'id,name_loc0,name_loc2,name_loc3,name_loc6,name_loc8', - 'spellicon' => 'id,iconPath', - 'spellitemenchantment' => 'id,charges,type1,type2,type3,amount1,amount2,amount3,object1,object2,object3,name_loc0,name_loc2,name_loc3,name_loc6,name_loc8,conditionId,skillLine,skillLevel,requiredLevel', - 'spellitemenchantmentcondition' => 'id,color1,color2,color3,color4,color5,comparator1,comparator2,comparator3,comparator4,comparator5,cmpColor1,cmpColor2,cmpColor3,cmpColor4,cmpColor5,value1,value2,value3,value4,value5', - 'spellradius' => 'id,radiusMin,radiusMax', - 'spellrange' => 'id,rangeMinHostile,rangeMinFriend,rangeMaxHostile,rangeMaxFriend,rangeType,name_loc0,name_loc2,name_loc3,name_loc6,name_loc8', - 'spellrunecost' => 'id,costBlood,costUnholy,costFrost,runicPowerGain', - 'spellshapeshiftform' => 'id,name_loc0,name_loc2,name_loc3,name_loc6,name_loc8,flags,creatureType,displayIdA,displayIdH,spellId1,spellId2,spellId3,spellId4,spellId5,spellId6,spellId7,spellId8', - 'spellvisual' => 'id,precastKitId,castKitId,impactKitId,stateKitId,statedoneKitId,channelKitId,missileSoundId,animationSoundId,casterImpactKitId,targetImpactKitId,missileTargetingKitId,instantAreaKitId,impactAreaKitId,persistentAreaKitId', - 'spellvisualkit' => 'id,soundId', - 'talent' => 'id,tabId,row,column,rank1,rank2,rank3,rank4,rank5,reqTalent,reqRank,talentSpell,petCategory1,petCategory2', - 'talenttab' => 'id,name_loc0,name_loc2,name_loc3,name_loc6,name_loc8,iconId,raceMask,classMask,creatureFamilyMask,tabNumber,textureFile', - 'taxinodes' => 'id,mapId,posX,posY,name_loc0,name_loc2,name_loc3,name_loc6,name_loc8', - 'taxipath' => 'id,startNodeId,endNodeId', - 'taxipathnode' => 'id,pathId,nodeIdx,mapId,posX,posY', - 'totemcategory' => 'id,name_loc0,name_loc2,name_loc3,name_loc6,name_loc8,category,categoryMask', - 'vocaluisounds' => 'id,raceId,soundIdMale,soundIdFemale', - 'weaponimpactsounds' => 'id,subClass,hit1,hit2,hit3,hit4,hit5,hit6,hit7,hit8,hit9,hit10,crit1,crit2,crit3,crit4,crit5,crit6,crit7,crit8,crit9,crit10', - 'weaponswingsounds2' => 'id,weaponSize,soundId', - 'worldmaparea' => 'id,mapId,areaId,nameINT,left,right,top,bottom,defaultDungeonMapId', - 'worldmapoverlay' => 'id,worldMapAreaId,areaTableId,textureString,w,h,x,y', - 'worldmaptransforms' => 'id,sourceMapId,minX,minY,maxX,maxY,targetMapId,offsetX,offsetY,dungeonMapId', - 'worldstatezonesounds' => 'stateId,value,areaId,wmoAreaId,zoneIntroMusicId,zoneMusicId,soundAmbienceId', - 'zoneintromusictable' => 'id,soundId', - 'zonemusic' => 'id,soundIdDay,soundIdNight' - ); - - private $isGameTable = false; - private $localized = false; - private $tempTable = true; - private $tableName = ''; - - private $dataBuffer = []; - private $bufferSize = 500; - - private $fileRefs = []; - - public $error = true; - public $fields = []; - public $format = ''; - public $file = ''; - - - public function __construct($file, $opts = []) - { - $file = strtolower($file); - if (empty($this->_fields[$file]) || empty($this->_formats[$file])) - { - CLI::write('no structure known for '.$file.'.dbc, aborting.', CLI::LOG_ERROR); - return; - } - - $this->fields = explode(',', $this->_fields[$file]); - $this->format = $this->_formats[$file]; - $this->file = $file; - $this->localized = !!strstr($this->format, 'sxssxxsxsxxxxxxxx'); - - if (count($this->fields) != strlen(str_ireplace('x', '', $this->format))) - { - CLI::write('known field types ['.count($this->fields).'] and names ['.strlen(str_ireplace('x', '', $this->format)).'] do not match for '.$file.'.dbc, aborting.', CLI::LOG_ERROR); - return; - } - - 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 = $this->format == 'f' && substr($file, 0, 2) == 'gt'; - - $foundMask = 0x0; - foreach (CLISetup::$expectedPaths as $locStr => $locId) - { - if (!in_array($locId, CLISetup::$localeIds)) - continue; - - if ($foundMask & (1 << $locId)) - continue; - - $fullPath = CLI::nicePath($this->file.'.dbc', CLISetup::$srcDir, $locStr, 'DBFilesClient'); - if (!CLISetup::fileExists($fullPath)) - continue; - - $this->curFile = $fullPath; - if ($this->validateFile($locId)) - $foundMask |= (1 << $locId); - } - - 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 differenct 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(); - - CLI::write(' - reading '.($this->localized ? 'and merging ' : '').$this->file.'.dbc for locales '.implode(', ', array_keys($this->fileRefs))); - - if (!$this->read()) - { - CLI::write(' - DBC::read() returned with error', CLI::LOG_ERROR); - return false; - } - - return true; - } - - private function endClean() - { - foreach ($this->fileRefs as &$ref) - fclose($ref[0]); - - $this->dataBuffer = null; - } - - private function readHeader(&$handle = null) - { - if (!is_resource($handle)) - $handle = fopen($this->curFile, 'rb'); - - if (!$handle) - return false; - - if (fread($handle, 4) != 'WDBC') - { - CLI::write('file '.$this->curFile.' has incorrect magic bytes', CLI::LOG_ERROR); - fclose($handle); - return false; - } - - return unpack('VrecordCount/VfieldCount/VrecordSize/VstringSize', fread($handle, 16)); - } - - private function validateFile($locId) - { - $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'] != strlen($this->format)) - { - CLI::write('incorrect format string ('.$this->format.') specified for file '.$this->curFile.' fieldCount='.$header['fieldCount'], CLI::LOG_ERROR); - fclose($handle); - return false; - } - - $this->fileRefs[$locId] = [$handle, $this->curFile, $header]; - - return true; - } - - private function createTable() - { - if ($this->error) - return; - - $n = 0; - $pKey = ''; - $query = 'CREATE '.($this->tempTable ? 'TEMPORARY' : '').' TABLE `'.$this->tableName.'` ('; - - if ($this->isGameTable) - { - $query .= '`idx` BIGINT(20) NOT NULL, '; - $pKey = 'idx'; - } - - foreach (str_split($this->format) as $idx => $f) - { - switch ($f) - { - case 'f': - $query .= '`'.$this->fields[$n].'` FLOAT NOT NULL, '; - break; - case 's': - $query .= '`'.$this->fields[$n].'` TEXT NOT NULL, '; - break; - case 'i': - case 'n': - case 'b': - case 'u': - $query .= '`'.$this->fields[$n].'` BIGINT(20) NOT NULL, '; - break; - default: // 'x', 'X', 'd' - continue 2; - } - - if ($f == 'n') - $pKey = $this->fields[$n]; - - $n++; - } - - if ($pKey) - $query .= 'PRIMARY KEY (`'.$pKey.'`) '; - else - $query = substr($query, 0, -2); - - $query .= ') COLLATE=\'utf8_general_ci\' ENGINE=MyISAM'; - - DB::Aowow()->query('DROP TABLE IF EXISTS ?#', $this->tableName); - DB::Aowow()->query($query); - } - - private function writeToDB() - { - if (!$this->dataBuffer || $this->error) - return; - - // make inserts more manageable - $fields = $this->fields; - - if ($this->isGameTable) - array_unshift($fields, 'idx'); - - DB::Aowow()->query('INSERT INTO ?# (?#) VALUES (?a)', $this->tableName, $fields, $this->dataBuffer); - $this->dataBuffer = []; - } - - private function read() - { - // l - signed long (always 32 bit, machine byte order) - // V - unsigned long (always 32 bit, little endian byte order) - $unpackStr = ''; - $unpackFmt = array( - 'x' => 'x/x/x/x', - 'X' => 'x', - 's' => 'V', - 'f' => 'f', - 'i' => 'l', // not sure if 'l' or 'V' should be used here - 'u' => 'V', - 'b' => 'C', - 'd' => 'x4', - 'n' => 'V' - ); - - // Check that record size also matches - $recSize = 0; - for ($i = 0; $i < strlen($this->format); $i++) - { - $ch = $this->format[$i]; - if ($ch == 'X' || $ch == 'b') - $recSize += 1; - else - $recSize += 4; - - if (!isset($unpackFmt[$ch])) - { - CLI::write('unknown format parameter \''.$ch.'\' in format string', CLI::LOG_ERROR); - return false; - } - - $unpackStr .= '/'.$unpackFmt[$ch]; - - if ($ch != 'X' && $ch != 'x') - $unpackStr .= 'f'.$i; - } - - $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 - $strings = []; - $rSize = $header['recordSize']; - $rCount = $header['recordCount']; - $fCount = strlen($this->format); - $strBlock = 4 + 16 + $header['recordSize'] * $header['recordCount']; - - for ($i = 0; $i < $rCount; $i++) - { - $row = []; - $idx = $i; - - // add 'virtual' enumerator for gt*-dbcs - if ($this->isGameTable) - $row[-1] = $i; - - foreach ($this->fileRefs as $locId => list($handle, $fullPath, $header)) - { - $rec = unpack($unpackStr, fread($handle, $header['recordSize'])); - - $n = -1; - for ($j = 0; $j < $fCount; $j++) - { - if (!isset($rec['f'.$j])) - continue; - - if (!empty($row[$j])) - continue; - - $n++; - - switch ($this->format[$j]) - { - 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[$j] = $str; - break; - case 'f': - $row[$j] = round($rec['f'.$j], 8); - break; - case 'n': // DO NOT BREAK! - $idx = $rec['f'.$j]; - default: // nothing special .. 'i', 'u' and the likes - $row[$j] = $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 new file mode 100644 index 00000000..808fa661 --- /dev/null +++ b/setup/tools/dbc/12340.ini @@ -0,0 +1,1538 @@ +; DBC structure - 3.3.5.12340 +; +; x - not used/unknown, 4 bytes +; X - not used/unknown, 1 byte +; s - string block index, 4 bytes +; S - string block index, 4 bytes - localized; autofill; not used in 3.3.5 +; f - float, 4 bytes (rounded to 4 digits after comma) +; u - unsigned int, 4 bytes +; i - signed int, 4 bytes +; b - unsigned char, 1 byte +; d - sorted by this field, not included in array +; n - same, but field included in array +; +; LOC - used locale strings macro [sxsssxsxsxxxxxxxx] +; X_LOC - unused locale strings macro [xxxxxxxxxxxxxxxxx] + +[achievement] +id = n +faction = i +map = i +previous = i +name = LOC +description = LOC +category = i +points = i +orderInGroup = i +flags = i +iconId = i +reward = LOC +reqCriteriaCount = i +refAchievement = i + +[achievement_category] +id = n +parentCategory = i +UNUSED2 = X_LOC +UNUSED3 = x + +[achievement_criteria] +id = n +refAchievementId = i +type = I +value1 = I +value2 = i +value3 = i +value4 = i +value5 = i +value6 = i +name = LOC +completionFlags = i +groupFlags = i +UNUSED12 = x +timeLimit = i +order = i + +[areatable] +id = n +mapId = i +areaTable = i +areaBit = x +flags = i +soundProviderPref = x +soundProviderPrefWater = x +soundAmbience = i +zoneMusic = i +zoneIntroMusic = i +explorationLevel = x +name = LOC +factionGroupMask = i +liquidType1 = x +liquidType2 = x +liquidType3 = x +liquidType4 = x +minElevation = x +ambientMultiplier = x +lightId = x + +[areatrigger] +id = n +mapId = i +posX = f +posY = f +UNUSED4 = x +UNUSED5 = x +UNUSED6 = x +UNUSED7 = x +UNUSED8 = x +orientation = f + +[battlemasterlist] +id = n +mapId = i +moreMapId = i +UNUSED3 = x +UNUSED4 = x +UNUSED5 = x +UNUSED6 = x +UNUSED7 = x +UNUSED8 = x +areaType = i +UNUSED10 = X_LOC +UNUSED11 = x +maxPlayers = i +UNUSED13 = x +minLevel = i +maxLevel = i + +[charbaseinfo] +raceId = b +classId = b + +[charstartoutfit] +id = n +raceId = b +classId = b +gender = b +UNUSED4 = X +item1 = i +item2 = i +item3 = i +item4 = i +item5 = i +item6 = i +item7 = i +item8 = i +item9 = i +item10 = i +item11 = i +item12 = i +item13 = i +item14 = i +item15 = i +item16 = i +item17 = i +item18 = i +item19 = i +item20 = i +UNUSED25 = X_LOC +UNUSED26 = X_LOC +UNUSED27 = X_LOC +UNUSED28 = x + +[chartitles] +id = n +UNUSED1 = x +male = LOC +female = LOC +bitIdx = i + +[chrclasses] +id = n +UNUSED1 = x +powerType = i +UNUSED3 = x +name = LOC +UNUSED5 = X_LOC +UNUSED6 = X_LOC +fileString = s +UNUSED8 = x +flags = i +UNUSED10 = x +expansion = i + +[chrraces] +id = n +flags = i +factionId = i +UNUSED3 = x +UNUSED4 = x +UNUSED5 = x +UNUSED6 = x +baseLanguage = i +UNUSED8 = x +UNUSED9 = x +UNUSED10 = x +fileString = s +UNUSED12 = x +side = i +name = LOC +UNUSED15 = X_LOC +UNUSED16 = X_LOC +UNUSED17 = x +UNUSED18 = x +UNUSED19 = x +expansion = i + +[creaturedisplayinfo] +id = n +modelId = i +creatureSoundId = i +extraInfoId = i +UNUSED4 = x +UNUSED5 = x +skin1 = s +skin2 = s +skin3 = s +iconString = s +UNUSED10 = x +UNUSED11 = x +npcSoundId = i +UNUSED13 = x +UNUSED14 = x +UNUSED15 = x + +[creaturedisplayinfoextra] +id = n +UNUSED1 = X_LOC +UNUSED2 = x +UNUSED3 = x +textureString = s + +[creaturefamily] +id = n +UNUSED1 = x +UNUSED2 = x +UNUSED3 = x +UNUSED4 = x +skillLine1 = i +UNUSED6 = x +petFoodMask = i +petTalentType = i +categoryEnumID = i +name = LOC +iconString = s + +[creaturemodeldata] +id = n +UNUSED1 = x +UNUSED2 = x +UNUSED3 = x +UNUSED4 = x +UNUSED5 = x +UNUSED6 = x +UNUSED7 = x +UNUSED8 = x +UNUSED9 = x +UNUSED10 = x +UNUSED11 = x +UNUSED12 = x +creatureSoundId = i +UNUSED14 = x +UNUSED15 = x +UNUSED16 = x +UNUSED17 = x +UNUSED18 = x +UNUSED19 = x +UNUSED20 = x +UNUSED21 = x +UNUSED22 = x +UNUSED23 = x +UNUSED24 = x +UNUSED25 = x +UNUSED26 = x +UNUSED27 = x + +[creaturesounddata] +id = n +exertion = i +exertionCritical = i +injury = i +injuryCritical = i +UNUSED5 = x +death = i +stun = i +stand = i +footstepTerrainId = i +aggro = i +wingFlap = i +wingGlide = i +alert = i +fidget = i +UNUSED15 = x +UNUSED16 = x +UNUSED17 = x +UNUSED18 = x +customAttack = i +UNUSED20 = x +UNUSED21 = x +UNUSED22 = x +UNUSED23 = x +loop = i +UNUSED25 = x +jumpStart = i +jumpEnd = i +petAttack = i +petOrder = i +petDismiss = i +UNUSED31 = x +UNUSED32 = x +birth = i +spellcast = i +submerge = i +submerged = i +UNUSED37 = x + +[currencytypes] +id = n +itemId = i +category = i +UNUSED3 = x + +[declinedword] +id = n +word = s + +[declinedwordcases] +id = n +wordId = i +caseIdx = i +word = s + +[dungeonmap] +id = n +mapId = i +floor = i +minY = f +maxY = f +minX = f +maxX = f +worldMapAreaId = i + +[durabilitycosts] +id = n +w0 = i +w1 = i +w2 = i +w3 = i +w4 = i +w5 = i +w6 = i +w7 = i +w8 = i +UNUSED10 = x +w10 = i +w11 = i +w12 = i +w13 = i +w14 = i +w15 = i +w16 = i +w17 = i +w18 = i +w19 = i +w20 = i +UNUSED22 = x +a1 = i +a2 = i +a3 = i +a4 = i +UNUSED27 = x +a6 = i +UNUSED29 = x + +[durabilityquality] +id = n +mod = f + +[dungeonencounter] +id = n +map = i +mode = i +order = i +bit = i +name = LOC +UNUSED6 = x + +[emotes] +id = n +name = s +animationId = i +flags = i +state = i +stateParam = i +soundId = i + +[emotestext] +id = n +command = s +emoteId = i +etd0 = i +etd1 = i +etd2 = i +UNUSED6 = x +etd4 = i +UNUSED8 = x +etd6 = i +UNUSED10 = x +etd8 = i +etd9 = i +UNUSED13 = x +UNUSED14 = x +etd12 = i +UNUSED16 = x +UNUSED17 = x +UNUSED18 = x + +[emotestextsound] +id = n +emotesTextId = i +raceId = i +gender = i +soundId = i + +[emotestextdata] +id = n +text = LOC + +[faction] +id = n +repIdx = i +baseRepRaceMask1 = i +baseRepRaceMask2 = i +baseRepRaceMask3 = i +baseRepRaceMask4 = i +baseRepClassMask1 = i +baseRepClassMask2 = i +baseRepClassMask3 = i +baseRepClassMask4 = i +baseRepValue1 = i +baseRepValue2 = i +baseRepValue3 = i +baseRepValue4 = i +repFlags1 = i +UNUSED15 = x +UNUSED16 = x +UNUSED17 = x +parentFaction = i +spilloverRateIn = f +spilloverRateOut = f +spilloverMaxRank = i +UNUSED22 = x +name = LOC +UNUSED24 = X_LOC + +[factiontemplate] +id = n +factionId = i +UNUSED2 = x +ourMask = i +friendlyMask = i +hostileMask = i +enemyFactionId1 = i +enemyFactionId2 = i +enemyFactionId3 = i +enemyFactionId4 = i +friendFactionId1 = i +friendFactionId2 = i +friendFactionId3 = i +friendFactionId4 = i + +[gemproperties] +id = n +enchantmentId = i +UNUSED2 = x +UNUSED3 = x +colorMask = i + +[glyphproperties] +id = n +spellId = i +typeFlags = i +iconId = i + +[gtchancetomeleecrit] +chance = f + +[gtchancetomeleecritbase] +chance = f + +[gtchancetospellcrit] +chance = f + +[gtchancetospellcritbase] +chance = f + +[gtcombatratings] +ratio = f + +[gtnpcmanacostscaler] +factor = f + +[gtoctclasscombatratingscalar] +idx = n +ratio = f + +[gtoctregenhp] +ratio = f + +[gtregenmpperspt] +ratio = f + +[gtregenhpperspt] +ratio = f + +[holidays] +id = n +UNUSED1 = X_LOC +UNUSED2 = X_LOC +UNUSED3 = x +UNUSED4 = x +UNUSED5 = x +looping = i +UNUSED7 = x +UNUSED8 = x +UNUSED9 = x +UNUSED10 = x +UNUSED11 = x +UNUSED12 = x +UNUSED13 = x +UNUSED14 = x +UNUSED15 = x +UNUSED16 = x +nameId = i +descriptionId = i +textureString = s +UNUSED20 = x +scheduleType = i +UNUSED22 = x + +[holidaydescriptions] +id = n +description = LOC + +[holidaynames] +id = n +name = LOC + +[item] +id = n +classId = i +subClassId = i +soundOverride = i +material = i +displayInfoId = i +inventoryType = i +sheatheType = i + +[itemdisplayinfo] +id = n +leftModelName = s +rightModelName = s +UNUSED3 = x +UNUSED4 = x +inventoryIcon1 = s +UNUSED6 = x +UNUSED7 = x +UNUSED8 = x +UNUSED9 = x +UNUSED10 = x +spellVisualId = i +groupSoundId = i +UNUSED13 = x +UNUSED14 = x +UNUSED15 = x +UNUSED16 = x +UNUSED17 = x +UNUSED18 = x +UNUSED19 = x +UNUSED20 = x +UNUSED21 = x +UNUSED22 = x +UNUSED23 = x +UNUSED24 = x + +[itemgroupsounds] +id = n +pickUpSoundId = i +dropDownSoundId = i +UNUSED3 = x +UNUSED4 = x + +[itemextendedcost] +id = n +reqHonorPoints = i +reqArenaPoints = i +reqArenaSlot = i +reqItemId1 = i +reqItemId2 = i +reqItemId3 = i +reqItemId4 = i +reqItemId5 = i +itemCount1 = i +itemCount2 = i +itemCount3 = i +itemCount4 = i +itemCount5 = i +reqPersonalRating = i +UNUSED15 = x + +[itemlimitcategory] +id = n +name = LOC +count = i +isGem = i + +[itemrandomproperties] +id = n +nameINT = s +enchantId1 = i +enchantId2 = i +enchantId3 = i +enchantId4 = i +enchantId5 = i +name = LOC + +[itemrandomsuffix] +id = n +name = LOC +nameINT = s +enchantId1 = i +enchantId2 = i +enchantId3 = i +enchantId4 = i +enchantId5 = i +alLOCationPct1 = i +alLOCationPct2 = i +alLOCationPct3 = i +alLOCationPct4 = i +alLOCationPct5 = i + +[itemset] +id = n +name = LOC +UNUSED2 = X_LOC +spellId1 = i +spellId2 = i +spellId3 = i +spellId4 = i +spellId5 = i +spellId6 = i +spellId7 = i +spellId8 = i +itemCount1 = i +itemCount2 = i +itemCount3 = i +itemCount4 = i +itemCount5 = i +itemCount6 = i +itemCount7 = i +itemCount8 = i +reqSkillId = i +reqSkillLevel = i + +[itemsubclass] +class = i +subClass = i +UNUSED2 = x +UNUSED3 = x +UNUSED4 = x +UNUSED5 = x +UNUSED6 = x +UNUSED7 = x +UNUSED8 = x +weaponSize = i +UNUSED10 = X_LOC +UNUSED11 = X_LOC + +[lfgdungeons] +id = n +name = LOC +levelMin = i +levelMax = i +targetLevel = i +targetLevelMin = i +targetLevelMax = i +mapId = i +difficulty = i +UNUSED9 = x +type = i +faction = i +UNUSED12 = x +expansion = i +UNUSED14 = x +groupId = i +UNUSED16 = X_LOC + +[lock] +id = n +type1 = i +type2 = i +type3 = i +type4 = i +type5 = i +UNUSED6 = x +UNUSED7 = x +UNUSED8 = x +properties1 = i +properties2 = i +properties3 = i +properties4 = i +properties5 = i +UNUSED14 = x +UNUSED15 = x +UNUSED16 = x +reqSkill1 = i +reqSkill2 = i +reqSkill3 = i +reqSkill4 = i +reqSkill5 = i +UNUSED22 = x +UNUSED23 = x +UNUSED24 = x +UNUSED25 = x +UNUSED26 = x +UNUSED27 = x +UNUSED28 = x +UNUSED29 = x +UNUSED30 = x +UNUSED31 = x +UNUSED32 = x + +[locktype] +id = n +name = LOC +state = LOC +process = LOC +strref = s + +[mailtemplate] +id = n +subject = LOC +text = LOC + +[map] +id = n +nameINT = s +areaType = i +UNUSED3 = x +isBG = i +name = LOC +UNUSED6 = X_LOC +UNUSED7 = X_LOC +UNUSED8 = x +UNUSED9 = x +UNUSED10 = x +parentMapId = i +parentX = f +parentY = f +UNUSED14 = x +expansion = i +UNUSED16 = x +maxPlayers = i + +[mapdifficulty] +id = n +mapId = i +difficulty = i +UNUSED3 = X_LOC +UNUSED4 = x +nPlayer = i +nPlayerString = s + +[material] +id = n +UNUSED1 = x +UNUSED2 = x +sheatheSoundId = i +unsheatheSoundId = i + +[npcsounds] +id = n +greetSoundId = i +byeSoundId = i +angrySoundId = i +UNUSED4 = x + +[overridespelldata] +id = n +spellId1 = i +spellId2 = i +spellId3 = i +spellId4 = i +UNUSED5 = x +spellId5 = i +UNUSED7 = x +UNUSED8 = x +UNUSED9 = x +UNUSED10 = x +UNUSED11 = x + +[powerdisplay] +id = n +realType = i +globalString = s +r = b +g = b +b = b + +[questfactionreward] +id = n +field1 = i +field2 = i +field3 = i +field4 = i +field5 = i +field6 = i +field7 = i +field8 = i +field9 = i +field10 = i + +[questsort] +id = n +name = LOC + +[questxp] +id = n +field1 = i +field2 = i +field3 = i +field4 = i +field5 = i +field6 = i +field7 = i +field8 = i +field9 = i +field10 = i + +[randproppoints] +id = n +epic1 = i +epic2 = i +epic3 = i +epic4 = i +epic5 = i +rare1 = i +rare2 = i +rare3 = i +rare4 = i +rare5 = i +uncommon1 = i +uncommon2 = i +uncommon3 = i +uncommon4 = i +uncommon5 = i + +[scalingstatdistribution] +id = n +statMod1 = i +statMod2 = i +statMod3 = i +statMod4 = i +statMod5 = i +statMod6 = i +statMod7 = i +statMod8 = i +statMod9 = i +statMod10 = i +modifier1 = i +modifier2 = i +modifier3 = i +modifier4 = i +modifier5 = i +modifier6 = i +modifier7 = i +modifier8 = i +modifier9 = i +modifier10 = i +maxLevel = i + +[scalingstatvalues] +UNUSED0 = x +id = n +shoulderMultiplier = i +trinketMultiplier = i +weaponMultiplier = i +rangedMultiplier = i +clothShoulderArmor = i +leatherShoulderArmor = i +mailShoulderArmor = i +plateShoulderArmor = i +weaponDPS1H = i +weaponDPS2H = i +casterDPS1H = i +casterDPS2H = i +rangedDPS = i +wandDPS = i +spellPower = i +primBudged = i +tertBudged = i +clothCloakArmor = i +clothChestArmor = i +leatherChestArmor = i +mailChestArmor = i +plateChestArmor = i + +[screeneffect] +id = n +name = s +UNUSED2 = x +UNUSED3 = x +UNUSED4 = x +UNUSED5 = x +UNUSED6 = x +UNUSED7 = x +soundAmbienceId = i +zoneMusicId = i + +[skillline] +id = n +categoryId = i +UNUSED2 = x +name = LOC +description = LOC +iconId = i +UNUSED6 = X_LOC +UNUSED7 = x + +[skilllineability] +id = n +skillLineId = i +spellId = i +reqRaceMask = i +reqClassMask = i +UNUSED5 = x +UNUSED6 = x +reqSkillLevel = i +UNUSED8 = x +acquireMethod = i +skillLevelGrey = i +skillLevelYellow = i +UNUSED12 = x +UNUSED13 = x + +[skilllinecategory] +id = n +name = LOC +index = i + +[skillraceclassinfo] +id = n +skillLine = i +raceMask = i +classMask = i +flags = i +reqLevel = i +UNUSED6 = x +UNUSED7 = x + +[soundambience] +id = n +soundIdDay = i +soundIdNight = i + +[soundemitters] +id = n +posX = f +posY = f +UNUSED3 = x +UNUSED4 = x +UNUSED5 = x +UNUSED6 = x +soundId = i +mapId = i +UNUSED9 = x + +[soundentries] +id = n +type = i +name = s +file1 = s +file2 = s +file3 = s +file4 = s +file5 = s +file6 = s +file7 = s +file8 = s +file9 = s +file10 = s +UNUSED13 = x +UNUSED14 = x +UNUSED15 = x +UNUSED16 = x +UNUSED17 = x +UNUSED18 = x +UNUSED19 = x +UNUSED20 = x +UNUSED21 = x +UNUSED22 = x +path = s +UNUSED24 = x +flags = i +UNUSED26 = x +UNUSED27 = x +UNUSED28 = x +UNUSED29 = x + +[spell] +id = n +category = i +dispelType = i +mechanic = i +attributes0 = u +attributes1 = u +attributes2 = u +attributes3 = u +attributes4 = u +attributes5 = u +attributes6 = u +attributes7 = u +stanceMask = i +UNUSED13 = x +stanceMaskNot = i +UNUSED15 = x +targets = i +UNUSED17 = x +spellFocus = i +UNUSED19 = x +UNUSED20 = x +UNUSED21 = x +UNUSED22 = x +UNUSED23 = x +UNUSED24 = x +UNUSED25 = x +UNUSED26 = x +UNUSED27 = x +castTimeId = i +recoveryTime = i +recoveryTimeCategory = i +UNUSED31 = x +UNUSED32 = x +UNUSED33 = x +UNUSED34 = x +procChance = i +procCharges = i +maxLevel = i +baseLevel = i +spellLevel = i +durationId = i +powerType = i +powerCost = i +powerCostPerLevel = i +powerPerSecond = i +powerPerSecondPerLevel = i +rangeId = i +UNUSED47 = x +UNUSED48 = x +stackAmount = i +tool1 = i +tool2 = i +reagent1 = i +reagent2 = i +reagent3 = i +reagent4 = i +reagent5 = i +reagent6 = i +reagent7 = i +reagent8 = i +reagentCount1 = i +reagentCount2 = i +reagentCount3 = i +reagentCount4 = i +reagentCount5 = i +reagentCount6 = i +reagentCount7 = i +reagentCount8 = i +equippedItemClass = i +equippedItemSubClassMask = i +equippedItemInventoryTypeMask = i +effect1Id = i +effect2Id = i +effect3Id = i +effect1DieSides = i +effect2DieSides = i +effect3DieSides = i +effect1RealPointsPerLevel = f +effect2RealPointsPerLevel = f +effect3RealPointsPerLevel = f +effect1BasePoints = i +effect2BasePoints = i +effect3BasePoints = i +effect1Mechanic = i +effect2Mechanic = i +effect3Mechanic = i +effect1ImplicitTargetA = i +effect2ImplicitTargetA = i +effect3ImplicitTargetA = i +effect1ImplicitTargetB = i +effect2ImplicitTargetB = i +effect3ImplicitTargetB = i +effect1RadiusId = i +effect2RadiusId = i +effect3RadiusId = i +effect1AuraId = i +effect2AuraId = i +effect3AuraId = i +effect1Periode = i +effect2Periode = i +effect3Periode = i +effect1ValueMultiplier = f +effect2ValueMultiplier = f +effect3ValueMultiplier = f +effect1ChainTarget = i +effect2ChainTarget = i +effect3ChainTarget = i +effect1CreateItemId = i +effect2CreateItemId = i +effect3CreateItemId = i +effect1MiscValue = i +effect2MiscValue = i +effect3MiscValue = i +effect1MiscValueB = i +effect2MiscValueB = i +effect3MiscValueB = i +effect1TriggerSpell = i +effect2TriggerSpell = i +effect3TriggerSpell = i +effect1PointsPerComboPoint = f +effect2PointsPerComboPoint = f +effect3PointsPerComboPoint = f +effect1SpellClassMaskA = i +effect1SpellClassMaskB = i +effect1SpellClassMaskC = i +effect2SpellClassMaskA = i +effect2SpellClassMaskB = i +effect2SpellClassMaskC = i +effect3SpellClassMaskA = i +effect3SpellClassMaskB = i +effect3SpellClassMaskC = i +spellVisualId1 = i +spellVisualId2 = i +iconId = i +iconIdActive = i +UNUSED135 = x +name = LOC +rank = LOC +description = LOC +buff = LOC +powerCostPercent = i +startRecoveryCategory = i +startRecoveryTime = i +maxTargetLevel = i +spellFamilyId = i +spellFamilyFlags1 = i +spellFamilyFlags2 = i +spellFamilyFlags3 = i +maxAffectedTargets = i +damageClass = i +UNUSED150 = x +UNUSED151 = x +effect1DamageMultiplier = f +effect2DamageMultiplier = f +effect3DamageMultiplier = f +UNUSED155 = x +UNUSED156 = x +UNUSED157 = x +toolCategory1 = i +toolCategory2 = i +UNUSED160 = x +schoolMask = i +runeCostId = i +UNUSED163 = x +powerDisplayId = i +effect1BonusMultiplier = f +effect2BonusMultiplier = f +effect3BonusMultiplier = f +spellDescriptionVariable = i +spellDifficulty = i + +[spellcasttimes] +id = n +baseTime = i +UNUSED2 = x +UNUSED3 = x + +[spelldescriptionvariables] +id = n +vars = s + +[spellduration] +id = n +baseTime = i +UNUSED2 = x +UNUSED3 = x + +[spelldifficulty] +UNUSED0 = x +normal10 = i +normal25 = i +heroic10 = i +heroic25 = i + +[spellfocusobject] +id = n +name = LOC + +[spellicon] +id = n +iconPath = s + +[spellitemenchantment] +id = n +charges = i +type1 = i +type2 = i +type3 = i +amount1 = i +amount2 = i +amount3 = i +UNUSED8 = x +UNUSED9 = x +UNUSED10 = x +object1 = i +object2 = i +object3 = i +name = LOC +UNUSED15 = x +UNUSED16 = x +UNUSED17 = x +conditionId = i +skillLine = i +skillLevel = i +requiredLevel = i + +[spellitemenchantmentcondition] +id = n +color1 = b +color2 = b +color3 = b +color4 = b +color5 = b +UNUSED6 = x +UNUSED7 = x +UNUSED8 = x +UNUSED9 = x +UNUSED10 = x +comparator1 = b +comparator2 = b +comparator3 = b +comparator4 = b +comparator5 = b +cmpColor1 = b +cmpColor2 = b +cmpColor3 = b +cmpColor4 = b +cmpColor5 = b +value1 = i +value2 = i +value3 = i +value4 = i +value5 = i +UNUSED26 = X +UNUSED27 = X +UNUSED28 = X +UNUSED29 = X +UNUSED30 = X + +[spellradius] +id = n +radiusMin = f +UNUSED2 = x +radiusMax = f + +[spellrange] +id = n +rangeMinHostile = f +rangeMinFriend = f +rangeMaxHostile = f +rangeMaxFriend = f +rangeType = i +name = LOC +UNUSED7 = X_LOC + +[spellrunecost] +id = n +costBlood = i +costUnholy = i +costFrost = i +runicPowerGain = i + +[spellshapeshiftform] +id = n +UNUSED1 = x +name = LOC +flags = i +creatureType = i +UNUSED5 = x +UNUSED6 = x +displayIdA = i +displayIdH = i +UNUSED9 = x +UNUSED10 = x +spellId1 = i +spellId2 = i +spellId3 = i +spellId4 = i +spellId5 = i +spellId6 = i +spellId7 = i +spellId8 = i + +[spellvisual] +id = n +precastKitId = i +castKitId = i +impactKitId = i +stateKitId = i +statedoneKitId = i +channelKitId = i +UNUSED7 = x +UNUSED8 = x +UNUSED9 = x +UNUSED10 = x +missileSoundId = i +animationSoundId = i +UNUSED13 = x +casterImpactKitId = i +targetImpactKitId = i +UNUSED16 = x +UNUSED17 = x +UNUSED18 = x +UNUSED19 = x +UNUSED20 = x +UNUSED21 = x +missileTargetingKitId = i +instantAreaKitId = i +impactAreaKitId = i +persistentAreaKitId = i +UNUSED26 = x +UNUSED27 = x +UNUSED28 = x +UNUSED29 = x +UNUSED30 = x +UNUSED31 = x + +[spellvisualkit] +id = n +UNUSED1 = x +UNUSED2 = x +UNUSED3 = x +UNUSED4 = x +UNUSED5 = x +UNUSED6 = x +UNUSED7 = x +UNUSED8 = x +UNUSED9 = x +UNUSED10 = x +UNUSED11 = x +UNUSED12 = x +UNUSED13 = x +UNUSED14 = x +soundId = i +UNUSED16 = X_LOC +UNUSED17 = x +UNUSED18 = x +UNUSED19 = x +UNUSED20 = x +UNUSED21 = x + +[summonproperties] +id = n +control = u +faction = x +title = x +slot = u +flags = x + +[talent] +id = n +tabId = i +row = i +column = i +rank1 = i +rank2 = i +rank3 = i +rank4 = i +rank5 = i +UNUSED9 = x +UNUSED10 = x +UNUSED11 = x +UNUSED12 = x +reqTalent = i +UNUSED14 = x +UNUSED15 = x +reqRank = i +UNUSED17 = x +UNUSED18 = x +talentSpell = i +UNUSED20 = x +petCategory1 = i +petCategory2 = i + +[talenttab] +id = n +name = LOC +iconId = i +raceMask = i +classMask = i +creatureFamilyMask = i +tabNumber = i +textureFile = s + +[taxinodes] +id = n +mapId = i +posX = f +posY = f +UNUSED4 = x +name = LOC +UNUSED6 = x +UNUSED7 = x + +[taxipath] +id = n +startNodeId = i +endNodeId = i +UNUSED3 = x + +[taxipathnode] +id = n +pathId = i +nodeIdx = i +mapId = i +posX = f +posY = f +UNUSED6 = x +UNUSED7 = x +UNUSED8 = x +UNUSED9 = x +UNUSED10 = x + +[totemcategory] +id = n +name = LOC +category = i +categoryMask = u + +[vocaluisounds] +id = n +UNUSED1 = x +raceId = i +soundIdMale = i +soundIdFemale = i +UNUSED5 = x +UNUSED6 = x + +[weaponimpactsounds] +id = n +subClass = i +UNUSED2 = x +hit1 = i +hit2 = i +hit3 = i +hit4 = i +hit5 = i +hit6 = i +hit7 = i +hit8 = i +hit9 = i +hit10 = i +crit1 = i +crit2 = i +crit3 = i +crit4 = i +crit5 = i +crit6 = i +crit7 = i +crit8 = i +crit9 = i +crit10 = i + +[weaponswingsounds2] +id = n +weaponSize = i +UNUSED2 = x +soundId = i + +[worldmaparea] +id = n +mapId = i +areaId = i +nameINT = s +left = f +right = f +top = f +bottom = f +displayMapId = x +defaultDungeonMapId = i +parentWorldMapId = x + +[worldmapoverlay] +id = n +worldMapAreaId = i +areaTableId = i +UNUSED3 = x +UNUSED4 = x +UNUSED5 = x +UNUSED6 = x +UNUSED7 = x +textureString = s +w = i +h = i +x = i +y = i +UNUSED13 = x +UNUSED14 = x +UNUSED15 = x +UNUSED16 = x + +[worldmaptransforms] +id = n +sourceMapId = i +minX = f +minY = f +maxX = f +maxY = f +targetMapId = i +offsetX = f +offsetY = f +dungeonMapId = i + +[worldstatezonesounds] +stateId = i +value = i +areaId = i +wmoAreaId = i +zoneIntroMusicId = i +zoneMusicId = i +soundAmbienceId = i +UNUSED7 = x + +[zoneintromusictable] +id = n +UNUSED1 = x +soundId = i +UNUSED3 = x +UNUSED4 = x + +[zonemusic] +id = n +UNUSED1 = x +UNUSED2 = x +UNUSED3 = x +UNUSED4 = x +UNUSED5 = x +soundIdDay = i +soundIdNight = i diff --git a/setup/tools/dbc/13623.ini b/setup/tools/dbc/13623.ini new file mode 100644 index 00000000..da23181f --- /dev/null +++ b/setup/tools/dbc/13623.ini @@ -0,0 +1,89 @@ +; DBC structure - 4.0.6.13623 +; +; x - not used/unknown, 4 bytes +; X - not used/unknown, 1 byte +; s - string block index, 4 bytes +; S - string block index, 4 bytes - localized; autofill +; f - float, 4 bytes (rounded to 4 digits after comma) +; u - unsigned int, 4 bytes +; i - signed int, 4 bytes +; b - unsigned char, 1 byte +; d - sorted by this field, not included in array +; n - same, but field included in array +; +; LOC - used locale strings macro [sxsssxsxsxxxxxxxx] +; X_LOC - unused locale strings macro [xxxxxxxxxxxxxxxxx] + +[areatable] +id = n +mapId = i +areaTable = i +areaBit = x +flags = i +UNK1 = i +soundProviderPref = x +soundProviderPrefWater = x +soundAmbience = i +zoneMusic = i +nameINT = s +zoneIntroMusic = i +explorationLevel = x +name = S +factionGroupMask = i +liquidType1 = x +liquidType2 = x +liquidType3 = x +liquidType4 = x +minElevation = x +ambientMultiplier = x +lightId = x +mountFLags = x +uwIntroSound = x +uwZoneMusic = x +uwAmbience = x +worldPvpId = x +pvpCombatWorldStateId = x + +[dungeonmap] +id = n +mapId = i +floor = i +minY = f ; maxY ? +maxY = f ; maxX ? +minX = f ; minY ? +maxX = f ; minX ? +worldMapAreaId = i + +[worldmaparea] +id = n +mapId = i +areaId = i +nameINT = s +left = f +right = f +top = f +bottom = f +displayMapId = x +defaultDungeonMapId = i +parentWorldMapId = x +flags = i +levelMin = x +levelMax = x + +[worldmapoverlay] +id = n +worldMapAreaId = i +areaTableId = i +UNUSED3 = x +UNUSED4 = x +UNUSED5 = x +textureString = s +w = i +h = i +x = i +y = i +UNUSED11 = x +UNUSED12 = x +UNUSED13 = x +UNUSED14 = x +playerConditionId = x diff --git a/setup/tools/dbc/15595.ini b/setup/tools/dbc/15595.ini new file mode 100644 index 00000000..9a11de6e --- /dev/null +++ b/setup/tools/dbc/15595.ini @@ -0,0 +1,2177 @@ +; DBC structure - 4.3.4.15595 +; +; x - not used/unknown, 4 bytes +; X - not used/unknown, 1 byte +; s - string block index, 4 bytes +; S - string block index, 4 bytes - localized; autofill +; f - float, 4 bytes (rounded to 4 digits after comma) +; u - unsigned int, 4 bytes +; i - signed int, 4 bytes +; b - unsigned char, 1 byte +; d - sorted by this field, not included in array +; n - same, but field included in array +; +; LOC - used locale strings macro [sxsssxsxsxxxxxxxx] +; X_LOC - unused locale strings macro [xxxxxxxxxxxxxxxxx] + +[areatable] +id = n +mapId = i +areaTable = i +areaBit = x +flags = i +soundProviderPref = x +soundProviderPrefWater = x +soundAmbience = i +zoneMusic = i +introSound = x +; nameINT = s +; zoneIntroMusic = i +explorationLevel = x +name = S +factionGroupMask = i +liquidType1 = x +liquidType2 = x +liquidType3 = x +liquidType4 = x +minElevation = x +ambientMultiplier = x +lightId = x +mountFLags = x +uwIntroSound = x +uwZoneMusic = x +uwAmbience = x +worldPvpId = x +pvpCombatWorldStateId = x + +[dungeonmap] +id = n +mapId = i +floor = i +minX = f +maxX = f +minY = f +maxY = f +worldMapAreaId = i + +[worldmaparea] +id = n +mapId = i +areaId = i +nameINT = s +left = f +right = f +top = f +bottom = f +displayMapId = x +defaultDungeonMapId = i +parentWorldMapId = x +flags = i +levelMin = x +levelMax = x + +[worldmapoverlay] +id = n +worldMapAreaId = i +areaTableId = i +areaTableId2 = x +areaTableId3 = x +areaTableId4 = x +textureString = s +w = i +h = i +x = i +y = i +hitRectTop = x +hitRectLeft = x +hitRectBottom = x +hitRectRight = x + +; from TrnityCore - Cataclysm Preservation Project +; Achievementfmt = "niixsxiixixxii"; +; AnimKitfmt = "nxx"; +; AchievementCriteriafmt = "niiiliiiisiiiiiiiiiiiii"; +; AreaTableEntryfmt = "niiiiiiiiiisiiiiiffiiiiiii"; +; AreaGroupEntryfmt = "niiiiiii"; +; AreaPOIEntryfmt = "nxiiiiiiiiixffixixxixx"; +; AreaTriggerEntryfmt = "nifffiiifffff"; +; ArmorLocationfmt = "nfffff"; +; AuctionHouseEntryfmt = "niiix"; +; BankBagSlotPricesEntryfmt = "ni"; +; BannedAddOnsfmt = "nxxxxxxxxxx"; +; BarberShopStyleEntryfmt = "nixxxiii"; +; BattlemasterListEntryfmt = "niiiiiiiiixsiiiixxxx"; +; CharStartOutfitEntryfmt = "dbbbXiiiiiiiiiiiiiiiiiiiiiiiixxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxii"; +; CharSectionsEntryfmt = "diiixxxiii"; +; CharTitlesEntryfmt = "nxssix"; +; ChatChannelsEntryfmt = "nixsx"; +; ChrClassesEntryfmt = "nixsxxxixiiiii"; +; ChrRacesEntryfmt = "niixiixixxxxixsxxxxxixxx"; +; ChrClassesXPowerTypesfmt = "nii"; +; CinematicCameraEntryfmt = "nsiffff"; +; CinematicSequencesEntryfmt = "nxiiiiiiii"; +; CreatureDisplayInfofmt = "nixifxxxxxxxxxxxx"; +; CreatureDisplayInfoExtrafmt = "diixxxxxxxxxxxxxxxxxx"; +; CreatureModelDatafmt = "nisxfxxxxxxxxxxffxxxxxxxxxxxxxf"; +; CreatureFamilyfmt = "nfifiiiiixsx"; +; CreatureSpellDatafmt = "niiiixxxx"; +; CreatureTypefmt = "nxx"; +; CurrencyTypesfmt = "nixxxxiiiix"; +; DestructibleModelDatafmt = "ixxixxxixxxixxxixxxxxxxx"; +; DungeonEncounterfmt = "niixisxx"; +; DurabilityCostsfmt = "niiiiiiiiiiiiiiiiiiiiiiiiiiiii"; +; DurabilityQualityfmt = "nf"; +; EmotesEntryfmt = "nxxiiixx"; +; EmotesTextEntryfmt = "nxixxxxxxxxxxxxxxxx"; +; EmotesTextSoundEntryfmt = "niiii"; +; FactionEntryfmt = "niiiiiiiiiiiiiiiiiiffiisxi"; +; FactionTemplateEntryfmt = "niiiiiiiiiiiii"; +; GameObjectArtKitfmt = "nxxxxxxx"; +; GameObjectDisplayInfofmt = "nsxxxxxxxxxxffffffxxx"; +; GemPropertiesEntryfmt = "nixxii"; +; GlyphPropertiesfmt = "niii"; +; GlyphSlotfmt = "nii"; +; GtBarberShopCostBasefmt = "xf"; +; GtCombatRatingsfmt = "xf"; +; GtOCTHpPerStaminafmt = "df"; +; GtChanceToMeleeCritBasefmt = "xf"; +; GtChanceToMeleeCritfmt = "xf"; +; GtChanceToSpellCritBasefmt = "xf"; +; GtChanceToSpellCritfmt = "xf"; +; GtNPCManaCostScalerfmt = "xf"; +; GtOCTClassCombatRatingScalarfmt = "df"; +; GtOCTRegenHPfmt = "f"; +; GtOCTRegenMPfmt = "f"; +; GtRegenMPPerSptfmt = "xf"; +; GtSpellScalingfmt = "df"; +; GtOCTBaseHPByClassfmt = "df"; +; GtOCTBaseMPByClassfmt = "df"; +; GuildPerkSpellsfmt = "dii"; +; Holidaysfmt = "niiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiixxsiix"; +; ImportPriceArmorfmt = "nffff"; +; ImportPriceQualityfmt = "nf"; +; ImportPriceShieldfmt = "nf"; +; ImportPriceWeaponfmt = "nf"; +; ItemPriceBasefmt = "diff"; +; ItemReforgefmt = "nifif"; +; ItemBagFamilyfmt = "nx"; +; ItemArmorQualityfmt = "nfffffffi"; +; ItemArmorShieldfmt = "nifffffff"; +; ItemArmorTotalfmt = "niffff"; +; ItemClassfmt = "dixxfx"; +; ItemDamagefmt = "nfffffffi"; +; ItemDisenchantLootfmt = "niiiiii"; +; ItemDisplayTemplateEntryfmt = "nxxxxxxxxxxixxxxxxxxxxx"; +; ItemLimitCategoryEntryfmt = "nxii"; +; ItemRandomPropertiesfmt = "nxiiiiis"; +; ItemRandomSuffixfmt = "nsxiiiiiiiiii"; +; ItemSetEntryfmt = "dsiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiii"; +; LFGDungeonEntryfmt = "nsiiiiiiiiixxixixiiii"; +; LFGDungeonsGroupingMapfmt = "niii"; +; LightEntryfmt = "nifffxxxxxxxxxx"; +; LiquidTypefmt = "nxxixixxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"; +; LockEntryfmt = "niiiiiiiiiiiiiiiiiiiiiiiixxxxxxxx"; +; PhaseEntryfmt = "nsi"; +; PhaseGroupfmt = "nii"; +; MailTemplateEntryfmt = "nxs"; +; MapEntryfmt = "nsiiiisissififfiiiii"; +; MapDifficultyEntryfmt = "diisiix"; +; MovieEntryfmt = "nxxx"; +; NamesProfanityEntryfmt = "dsi"; +; NamesReservedEntryfmt = "dsi"; +; MountCapabilityfmt = "niiiiiii"; +; MountTypefmt = "niiiiiiiiiiiiiiiiiiiiiiii"; +; NameGenfmt = "dsii"; +; NumTalentsAtLevelfmt = "df"; +; OverrideSpellDatafmt = "niiiiiiiiiixx"; +; QuestFactionRewardfmt = "niiiiiiiiii"; +; QuestPOIBlobfmt = "niii"; +; QuestPOIPointfmt = "niii"; +; QuestSortEntryfmt = "nx"; +; QuestXPfmt = "niiiiiiiiii"; +; PlayerConditionfmt = "ns"; +; PowerDisplayfmt = "nixxxx"; +; PvPDifficultyfmt = "diiiii"; +; RandomPropertiesPointsfmt = "niiiiiiiiiiiiiii"; +; ResearchBranchEntryfmt = "nsiisi"; +; ResearchFieldEntryfmt = "nsi"; +; ResearchProjectEntryfmt = "nssiiiisi"; +; ResearchSiteEntryfmt = "niiss"; +; ScalingStatDistributionfmt = "niiiiiiiiiiiiiiiiiiiiii"; +; ScalingStatValuesfmt = "iniiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiii"; +; SkillLinefmt = "nisxixi"; +; SkillLineAbilityfmt = "niiiixxiiiiiii"; +; SkillRaceClassInfofmt = "diiiiixix"; +; SkillTiersfmt = "nxxxxxxxxxxxxxxxxiiiiiiiiiiiiiiii"; +; SoundEntriesfmt = "nxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"; +; SpellCastTimefmt = "nixx"; +; SpellCategoriesEntryfmt = "diiiiii"; +; SpellCategoryfmt = "niix"; +; SpellDifficultyfmt = "niiii"; +; SpellDurationfmt = "niii"; +; SpellEffectEntryfmt = "nifiiiffiiiiiifiifiiiiiiiix"; +; SpellEntryfmt = "niiiiiiiiiiiiiiifiiiissxxiixxifiiiiiiixiiiiiiiii"; +; SpellFocusObjectfmt = "nx"; +; SpellItemEnchantmentfmt = "nxiiiiiixxxiiisiiiiiiix"; +; SpellItemEnchantmentConditionfmt = "nbbbXxxxxxxbbbXXbbbxiiixxXXXXXX"; +; SpellRadiusfmt = "nfff"; +; SpellRangefmt = "nffffixx"; +; SpellReagentsEntryfmt = "diiiiiiiiiiiiiiii"; +; SpellScalingEntryfmt = "diiiiffffffffffi"; +; SpellTotemsEntryfmt = "niiii"; +; SpellTargetRestrictionsEntryfmt = "nfiiii"; +; SpellPowerEntryfmt = "diiiixxf"; +; SpellInterruptsEntryfmt = "diiiii"; +; SpellEquippedItemsEntryfmt = "diii"; +; SpellAuraOptionsEntryfmt = "niiii"; +; SpellAuraRestrictionsEntryfmt = "diiiiiiii"; +; SpellCastingRequirementsEntryfmt = "niiiiii"; +; SpellClassOptionsEntryfmt = "dxiiiix"; +; SpellCooldownsEntryfmt = "diii"; +; SpellLevelsEntryfmt = "diii"; +; SpellRuneCostfmt = "niiii"; +; SpellShapeshiftEntryfmt = "niiiix"; +; SpellShapeshiftFormfmt = "nxxiixiiiiiiiiiiiiiix"; +; SpellVisualfmt = "dxxxxxxiixxxxxxxxxxxxxxxxxxxxxxxi"; +; SpellVisualKitfmt = "niiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiii"; +; StableSlotPricesfmt = "ni"; +; SummonPropertiesfmt = "niiiii"; +; TalentEntryfmt = "niiiiiiiiiiiiiixxxx"; +; TalentTabEntryfmt = "nxxiiixxxii"; +; TalentTreePrimarySpellsfmt = "diix"; +; TaxiNodesEntryfmt = "nifffsiiiff"; +; TaxiPathEntryfmt = "niii"; +; TaxiPathNodeEntryfmt = "diiifffiiii"; +; TotemCategoryEntryfmt = "nxii"; +; UnitPowerBarfmt = "niiixffxxxxxxxxxxxxxxxxxxxx"; +; TransportAnimationfmt = "diifffx"; +; TransportRotationfmt = "diiffff"; +; VehicleEntryfmt = "niffffiiiiiiiifffffffffffffffssssfifiiii"; +; VehicleSeatEntryfmt = "niiffffffffffiiiiiifffffffiiifffiiiiiiiffiiiiiffffffffffffiiiiiiii"; +; WMOAreaTableEntryfmt = "niiixxxxxiixxxx"; +; WorldSafeLocsEntryfmt = "nifffx"; + +; struct AchievementEntry +; { +; uint32 ID; // 0 +; int32 Faction; // 1 -1=all, 0=horde, 1=alliance +; int32 MapID; // 2 -1=none +; //uint32 Supercedes; // 3 its Achievement parent (can`t start while parent uncomplete, use its Criteria if don`t have own, use its progress on begin) +; char* Title; // 4 +; //char* Description; // 5 +; uint32 Category; // 6 +; uint32 Points; // 7 reward points +; //uint32 Ui_order; // 8 +; uint32 Flags; // 9 +; //uint32 IconID; // 10 icon (from SpellIcon.dbc) +; //char* Reward; // 11 +; uint32 MinimumCriteria; // 12 - need this count of completed criterias (own or referenced achievement criterias) +; uint32 SharesCriteria; // 13 - referenced achievement (counting of all completed criterias) +; }; + +; struct AchievementCategoryEntry +; { +; uint32 ID; // 0 +; uint32 Parent; // 1 -1 for main category +; //char* Name; // 2 +; //uint32 Ui_order; // 3 +; }; + +; struct AchievementCriteriaEntry +; { +; uint32 ID; // 0 +; uint32 AchievementID; // 1 +; uint32 Type; // 2 +; uint64 Quantity; // 4 +; uint32 StartEvent; // 5 +; int32 StartAsset; // 6 +; uint32 FailEvent; // 7 +; int32 FailAsset; // 8 +; char* Description; // 9 +; uint32 Flags; // 10 +; uint32 TimerStartEvent; // 11 +; uint32 TimerAsset; // 12 +; uint32 TimerTime; // 13 +; uint32 OrderIndex; // 14 +; uint32 RequiredWorldStateID; // 15 +; int32 RequiredWorldStateValue; // 16 +; uint32 AdditionalConditionType[3]; // 17-19 +; uint32 AdditionalConditionValue[3]; // 20-22 +; }; + +; struct AnimKitEntry +; { +; uint32 ID; // 0 +; //uint32 OneShotDuration; // 1 +; //uint32 OneShotStopAnimKitID; // 2 +; }; + +; struct AreaGroupEntry +; { +; uint32 ID; // 0 +; uint32 AreaID[6]; // 1-6 +; uint32 NextAreaID; // 7 index of next group +; }; + +; struct AreaPOIEntry +; { +; uint32 ID; // 0 +; // uint32 Importance // 1 +; uint32 Icon[9]; // 2-10 +; // uint32 FactionID // 11 +; DBCPosition2D Pos; // 12 - 13 +; uint32 ContinentID; // 14 +; // uint32 Flags; // 15 +; uint32 AreaID; // 16 +; //char* Name; // 17 +; //char* Description; // 18 +; uint32 WorldStateID; // 19 +; //uint32 WorldMapLink; // 20 +; //uint32 PortLocID; // 21 +; }; + +; struct AreaTriggerEntry +; { +; uint32 ID; // 0 +; uint32 ContinentID; // 1 +; DBCPosition3D Pos; // 2 - 4 +; uint32 PhaseUseFlags; // 5 +; uint32 PhaseID; // 6 +; uint32 PhaseGroupID; // 7 +; float Radius; // 8 +; float Box_length; // 9 +; float Box_width; // 10 +; float Box_height; // 11 +; float Box_yaw; // 12 +; }; + +; struct ArmorLocationEntry +; { +; uint32 ID; // 0 +; float Value[5]; // 1-5 multiplier for armor types (cloth...plate, no armor?) +; }; + +; struct AuctionHouseEntry +; { +; uint32 ID; // 0 index +; uint32 FactionID; // 1 id of faction.dbc for player factions associated with city +; uint32 DepositRate; // 2 1/3 from real +; uint32 ConsignmentRate; // 3 +; //char* Name; // 4 +; }; + +; struct BankBagSlotPricesEntry +; { +; uint32 ID; // 0 +; uint32 Cost; // 1 +; }; + +; struct BannedAddOnsEntry +; { +; uint32 ID; // 0 +; // uint32 NameMD5[4]; // 1 - 4 +; // uint32 VersionMD5[4]; // 5 - 8 +; // uint32 LastModified; // 9 +; // uint32 Flags; // 10 +; }; + +; struct BarberShopStyleEntry +; { +; uint32 ID; // 0 +; uint32 Type; // 1 value 0 -> hair, value 2 -> facialhair +; //char* DisplayName; // 2 +; //uint32 Description; // 3 +; //float Cost_Modifier; // 4 +; uint32 Race; // 5 +; uint32 Sex; // 6 +; uint32 Data; // 7 +; }; + +; struct BattlemasterListEntry +; { +; uint32 ID; // 0 +; int32 MapID[8]; // 1-8 +; uint32 InstanceType; // 9 map type (3 - BG, 4 - arena) +; //uint32 GroupsAllowed; // 10 (0 or 1) +; char* Name; // 11 +; uint32 MaxGroupSize; // 12 maxGroupSize, used for checking if queue as group +; uint32 HolidayWorldState; // 13 new 3.1 +; uint32 MinLevel; // 14, min level (sync with PvPDifficulty.dbc content) +; uint32 MaxLevel; // 15, max level (sync with PvPDifficulty.dbc content) +; //uint32 RatedPlayers; // 16 4.0.1 +; //uint32 MinPlayers; // 17 - 4.0.6.13596 +; //uint32 MaxPlayers; // 18 4.0.1 +; //uint32 Flags; // 19 4.0.3, value 2 for Rated Battlegrounds +; }; + +; struct CharStartOutfitEntry +; { +; //uint32 ID; // 0 +; uint8 RaceID; // 1 +; uint8 ClassID; // 2 +; uint8 SexID; // 3 +; //uint8 OutfitID; // 4 +; int32 ItemID[24]; // 5-28 +; //int32 DisplayItemID[24]; // 29-52 not required at server side +; //int32 InventoryType[24]; // 53-76 not required at server side +; uint32 PetDisplayID; // 77 Pet Model ID for starting pet +; uint32 PetFamilyID; // 78 Pet Family Entry for starting pet +; }; + +; struct CharSectionsEntry +; { +; //uint32 ID // 0 +; uint32 RaceID; // 1 +; uint32 SexID; // 2 +; uint32 BaseSection; // 3 +; //char* TextureName[3]; // 4 - 7 +; uint32 Flags; // 8 +; uint32 VariationIndex; // 9 +; uint32 ColorIndex; // 10 +; }; + +; struct CharTitlesEntry +; { +; uint32 ID; // 0 title ids, for example in Quest::GetCharTitleId() +; //uint32 Condition_ID; // 1 +; char* Name; // 2 +; char* Name1; // 3 +; uint32 Mask_ID; // 4 used in PLAYER_CHOSEN_TITLE and 1<[21] + ArmorSubClassCost<32>[8] +; }; + +; struct DurabilityQualityEntry +; { +; uint32 ID; // 0 +; float Data; // 1 +; }; + +; struct EmotesEntry +; { +; uint32 ID; // 0 +; //char* EmoteSlashCommand; // 1, internal name +; //uint32 AnimID; // 2, ref to animationData +; uint32 EmoteFlags; // 3, bitmask, may be unit_flags +; uint32 EmoteSpecProc; // 4, Can be 0, 1 or 2 (determine how emote are shown) +; uint32 EmoteSpecProcParam; // 5, uncomfirmed, may be enum UnitStandStateType +; //uint32 EventSoundID; // 6, ref to soundEntries +; //uint32 SpellVisualKitID // 7 +; }; + +; struct EmotesTextEntry +; { +; uint32 ID; // 0 +; // char* Name; // 1 +; uint32 EmoteID; // 2 +; // uint32 EmoteText[16]; // 3 - 18 +; }; + +; struct EmotesTextSoundEntry +; { +; uint32 ID; // 0 +; uint32 EmotesTextID; // 1 +; uint32 RaceID; // 2 +; uint32 SexID; // 3 0 male / 1 female +; uint32 SoundID; // 4 +; }; + +; struct FactionEntry +; { +; uint32 ID; // 0 +; int32 ReputationIndex; // 1 +; uint32 ReputationRaceMask[4]; // 2 - 5 +; uint32 ReputationClassMask[4]; // 6 - 9 +; int32 ReputationBase[4]; // 10 - 13 +; uint32 ReputationFlags[4]; // 14 - 17 +; uint32 ParentFactionID; // 18 +; float ParentFactionMod[2]; // 19 - 20 Faction gains incoming rep * spilloverRateIn and Faction outputs rep * spilloverRateOut as spillover reputation +; uint32 ParentFactionCap[2]; // 21 - 22 The highest rank the faction will profit from incoming spillover and It does not seem to be the max standing at which a faction outputs spillover ...so no idea +; char* Name; // 23 +; // char* Description; // 24 +; uint32 Expansion; // 25 +; }; + +; struct FactionTemplateEntry +; { +; uint32 ID; // 0 +; uint32 Faction; // 1 +; uint32 Flags; // 2 +; uint32 FactionGroup; // 3 +; uint32 FriendGroup; // 4 +; uint32 EnemyGroup; // 5 +; uint32 Enemies[4]; // 6 +; uint32 Friend[4]; // 10 +; }; + +; struct GameObjectArtKitEntry +; { +; uint32 ID; // 0 +; //char* TextureVariation[3] // 1-3 +; //char* AttachModel[4] // 4-8 +; }; + +; struct GameObjectDisplayInfoEntry +; { +; uint32 ID; // 0 +; char* ModelName; // 1 +; // uint32 Sound[10]; // 2 - 11 +; DBCPosition3D GeoBoxMin; // 12 - 14 +; DBCPosition3D GeoBoxMax; // 15 - 17 +; // uint32 ObjectEffectPackageID; // 18 +; // float OverrideLootEffectScale; // 19 +; // float OverrideNameScale; // 20 +; }; + +; struct GemPropertiesEntry +; { +; uint32 ID; // 0 +; uint32 Enchant_ID; // 1 +; // uint32 Maxcount_inv; // 2 +; // uint32 Maxcount_item; // 3 +; uint32 Type; // 4 +; uint32 Min_item_level; // 5 +; }; + +; struct GlyphPropertiesEntry +; { +; uint32 ID; // 0 +; uint32 SpellID; // 1 +; uint32 GlyphSlotFlags; // 2 +; uint32 SpellIconID; // 3 GlyphIconId (SpellIcon.dbc) +; }; + +; struct GlyphSlotEntry +; { +; uint32 ID; // 0 +; uint32 Type; // 1 +; uint32 Tooltip; // 2 +; }; + +; // All Gt* DBC store data for 100 levels, some by 100 per class/race +; #define GT_MAX_LEVEL 100 +; // gtOCTClassCombatRatingScalar.dbc stores data for 32 ratings, look at MAX_COMBAT_RATING for real used amount +; #define GT_MAX_RATING 32 +; +; struct GtBarberShopCostBaseEntry +; { +; //uint32 level; +; float cost; +; }; + +; struct GtCombatRatingsEntry +; { +; //uint32 level; +; float ratio; +; }; + +; struct GtChanceToMeleeCritBaseEntry +; { +; //uint32 level; +; float base; +; }; + +; struct GtChanceToMeleeCritEntry +; { +; //uint32 level; +; float ratio; +; }; + +; struct GtChanceToSpellCritBaseEntry +; { +; float base; +; }; + +; struct GtNPCManaCostScalerEntry +; { +; float ratio; +; }; + +; struct GtChanceToSpellCritEntry +; { +; float ratio; +; }; + +; struct GtOCTClassCombatRatingScalarEntry +; { +; float ratio; +; }; + +; struct GtOCTRegenHPEntry +; { +; float ratio; +; }; + +; struct GtOCTRegenMPEntry +; { +; float ratio; +; }; + +; struct gtOCTHpPerStaminaEntry +; { +; float ratio; +; }; + +; struct GtRegenHPPerSptEntry +; { +; float ratio; +; }; + +; struct GtRegenMPPerSptEntry +; { +; float ratio; +; }; + +; struct GtSpellScalingEntry +; { +; float value; +; }; + +; struct GtOCTBaseHPByClassEntry +; { +; float ratio; +; }; + +; struct GtOCTBaseMPByClassEntry +; { +; float ratio; +; }; + +; struct GuildPerkSpellsEntry +; { +; // uint32 ID; // 0 +; uint32 GuildLevel; // 1 +; uint32 SpellID; // 2 +; }; + +; /* not used +; struct HolidayDescriptionsEntry +; { +; uint32 ID; // 0 +; //char* Description // 1 +; }; +*/ + +; struct HolidayNamesEntry +; { +; uint32 ID; // 0 +; //char* Name // 1 +; }; + +; struct HolidaysEntry +; { +; uint32 ID; // 0 +; uint32 Duration[10]; // 1-10 +; uint32 Date[26]; // 11-36 (dates in unix time starting at January, 1, 2000) +; uint32 Region; // 37 (wow region) +; uint32 Looping; // 38 +; uint32 CalendarFlags[10]; // 39-48 +; //uint32 HolidayNameID; // 49 (HolidayNames.dbc) +; //uint32 HolidayDescriptionID; // 50 (HolidayDescriptions.dbc) +; char* TextureFilename; // 51 +; uint32 Priority; // 52 +; int32 CalendarFilterType; // 53 (-1 = Fishing Contest, 0 = Unk, 1 = Darkmoon Festival, 2 = Yearly holiday) +; //uint32 Flags; // 54 (0 = Darkmoon Faire, Fishing Contest and Wotlk Launch, rest is 1) +; }; + +; // ImportPriceArmor.dbc +; struct ImportPriceArmorEntry +; { +; uint32 ID; // 0 Id/InventoryType +; float ClothModifier; // 1 Price factor cloth +; float LeatherModifier; // 2 Price factor leather +; float ChainModifier; // 3 Price factor mail +; float PlateModifier; // 4 Price factor plate +; }; + +; // ImportPriceQuality.dbc +; struct ImportPriceQualityEntry +; { +; uint32 QualityId; // 1 Quality Id (+1?) +; float Factor; // 2 Price factor +; }; + +; // ImportPriceShield.dbc +; struct ImportPriceShieldEntry +; { +; uint32 ID; // 1 +; float Data; // 2 Price factor +; }; + +; // ImportPriceWeapon.dbc +; struct ImportPriceWeaponEntry +; { +; uint32 ID; // 1 Unk id (mainhand - 0, offhand - 1, weapon - 2, 2hweapon - 3, ranged/rangedright/relic - 4) +; float Data; // 2 Price factor +; }; + +; // ItemPriceBase.dbc +; struct ItemPriceBaseEntry +; { +; // uint32 ID; // 0 +; uint32 ItemLevel; // 1 Item level (1 - 1000) +; float Armor; // 2 Price factor for armor +; float Weapon; // 3 Price factor for weapons +; }; + +; struct ItemReforgeEntry +; { +; uint32 ID; // 0 +; uint32 Source_stat; // 1 +; float Source_multiplier; // 2 +; uint32 Target_stat; // 3 +; float Target_multiplier; // 4 +; }; + +; // common struct for: +; // ItemDamageAmmo.dbc +; // ItemDamageOneHand.dbc +; // ItemDamageOneHandCaster.dbc +; // ItemDamageRanged.dbc +; // ItemDamageThrown.dbc +; // ItemDamageTwoHand.dbc +; // ItemDamageTwoHandCaster.dbc +; // ItemDamageWand.dbc +; struct ItemDamageEntry +; { +; uint32 ID; // 0 item level +; float Quality[7]; // 1-7 multiplier for item quality +; uint32 ItemLevel; // 8 item level +; }; + +; struct ItemArmorQualityEntry +; { +; uint32 ID; // 0 +; float Qualitymod[7]; // 1-7 multiplier for item quality +; uint32 ItemLevel; // 8 item level +; }; + +; struct ItemArmorShieldEntry +; { +; uint32 ID; // 0 +; uint32 ItemLevel; // 1 item level +; float Quality[7]; // 2-8 multiplier for item quality +; }; + +; struct ItemArmorTotalEntry +; { +; uint32 ID; // 0 +; uint32 ItemLevel; // 1 item level +; float Value[4]; // 2-5 multiplier for armor types (cloth...plate) +; }; + +; // ItemClass.dbc +; struct ItemClassEntry +; { +; // uint32 ID; // 0 +; uint32 ClassID; // 1 item class id +; //uint32 SubclassMapID; // 2 +; //uint32 Flags; // 3 1 for weapon, 0 for everything else +; float PriceModifier; // 4 used to calculate certain prices +; //char* ClassName; // 5 class name +; }; + +; struct ItemBagFamilyEntry +; { +; uint32 ID; // 0 +; //char* Name; // 1 +; }; + +; struct ItemDisplayInfoEntry +; { +; uint32 ID; // 0 +; // char* ModelName[2]; // 1 - 2 +; // char* ModelTexture[2]; // 3 - 4 +; // char* InventoryIcon[2]; // 5 - 6 +; // uint32 GeosetGroup[2]; // 7 - 8 +; // uint32 Flags; // 9 +; // uint32 SpellVisualID; // 10 +; // uint32 GroupSoundIndex; // 11 +; // uint32 HelmetGeosetVisID[2]; // 12 - 13 +; // char* Texture[8] // 14 - 21 +; // uint32 ItemVisual; // 22 +; // uint32 ParticleColorID; // 23 +; }; + +; struct ItemDisenchantLootEntry +; { +; uint32 ID; // 0 +; uint32 Class; // 1 +; int32 Subclass; // 2 +; uint32 Quality; // 3 +; uint32 MinLevel; // 4 +; uint32 MaxLevel; // 5 +; uint32 SkillRequired; // 6 +; }; + +; struct ItemLimitCategoryEntry +; { +; uint32 ID; // 0 +; //char* Name; // 1 +; uint32 Quantity; // 2 max allowed equipped as item or in gem slot +; uint32 Flags; // 3 0 = have, 1 = equip (enum ItemLimitCategoryMode) +; }; + +; struct ItemRandomPropertiesEntry +; { +; uint32 ID; // 0 +; //char* Name // 1 +; uint32 Enchantment[5]; // 2 - 6 +; char* Name; // 7 +; }; + +; struct ItemRandomSuffixEntry +; { +; uint32 ID; // 0 +; char* Name; // 1 +; // char* InternalName // 2 +; uint32 Enchantment[5]; // 3 - 7 +; uint32 AllocationPct[5]; // 8 - 12 +; }; + +; struct ItemSetEntry +; { +; //uint32 ID // 0 +; char* Name; // 1 +; uint32 ItemID[17]; // 2-18 +; uint32 SetSpellID[8]; // 19-26 +; uint32 SetThreshold[8]; // 27-34 +; uint32 RequiredSkill; // 35 +; uint32 RequiredSkillRank; // 36 +; }; + +; struct LFGDungeonEntry +; { +; uint32 ID; // 0 +; char* Name; // 1 +; uint32 MinLevel; // 2 +; uint32 Maxlevel; // 3 +; uint32 Target_level; // 4 +; uint32 Target_level_min; // 5 +; uint32 Target_level_max; // 6 +; int32 MapID; // 7 +; uint32 DifficultyID; // 8 +; uint32 Flags; // 9 +; uint32 TypeID; // 10 +; //uint32 Faction; // 11 +; //char* TextureFilename; // 12 +; uint32 ExpansionLevel; // 13 +; //uint32 Order_index; // 14 +; uint32 Group_ID; // 15 +; //char* Description; // 16 Description +; uint32 Random_ID; // 17 RandomDungeonID assigned for this dungeon +; uint32 Count_tank; // 18 +; uint32 Count_healer; // 19 +; uint32 Count_damage; // 20 +; }; + +; struct LFGDungeonsGroupingMapEntry +; { +; uint32 ID; // 0 +; uint32 LfgDungeonsID; // 1 +; uint32 Random_lfgDungeonsID; // 2 +; uint32 Group_ID; // 3 +; }; + +; struct LightEntry +; { +; uint32 ID; // 0 +; uint32 ContinentID; // 1 +; float X; // 2 +; float Y; // 3 +; float Z; // 4 +; // float FalloffStart; // 5 +; // float FalloffEnd; // 6 +; // uint32 LightParamsID[8]; // 7 - 14 +; }; + +; struct LiquidTypeEntry +; { +; uint32 ID; // 1 +; //char* Name; // 2 +; //uint32 Flags; // 3 +; uint32 SoundBank; // 4 +; //uint32 SoundID; // 5 +; uint32 SpellID; // 6 +; //float MaxDarkenDepth; // 7 +; //float FogDarkenIntensity; // 8 +; //float AmbDarkenIntensity; // 9 +; //float DirDarkenIntensity; // 10 +; //uint32 LightID; // 11 +; //float ParticleScale; // 12 +; //uint32 ParticleMovement; // 13 +; //uint32 ParticleTexSlots; // 14 +; //uint32 MaterialID; // 15 +; //char* Texture[6]; // 16 - 20 +; //uint32 Color[2]; // 21 - 22 +; //float Float[18]; // 23 - 40 +; //uint32 Int[4]; // 41 - 44 +; }; + +; struct LockEntry +; { +; uint32 ID; // 0 +; uint32 Type[8]; // 1-8 +; uint32 Index[8]; // 9-16 +; uint32 Skill[8]; // 17-24 +; //uint32 Action[8]; // 25-32 +; }; + +; struct PhaseEntry +; { +; uint32 ID; // 0 +; char* Name; // 1 +; uint32 Flags; // 2 +; }; + +; struct PhaseGroupEntry +; { +; uint32 ID; // 1 +; uint32 PhaseID; // 2 +; uint32 PhaseGroupID; // 3 +; }; + +; struct MailTemplateEntry +; { +; uint32 ID; // 0 +; //char* Subject; // 1 +; char* Body; // 2 +; }; + +; struct MapEntry +; { +; uint32 ID; // 0 +; char const* Directory; // 1 +; uint32 MapType; // 2 +; uint32 Flags; // 3 +; uint32 InstanceType; // 4 +; uint32 PVP; // 5 0 or 1 for battlegrounds (not arenas) +; char const* MapName; // 6 +; uint32 AreaTableID; // 7 +; char const* MapDescription0; // 8 Horde +; char const* MapDescription1; // 9 Alliance +; uint32 LoadingScreenID; // 10 (LoadingScreens.dbc) +; float MinimapIconScale; // 11 +; int32 CorpseMapID; // 12 map_id of entrance map in ghost mode (continent always and in most cases = normal entrance) +; DBCPosition2D Corpse; // 13 - 14 entrance coordinates in ghost mode (in most cases = normal entrance) +; uint32 TimeOfDayOverride; // 15 +; uint32 ExpansionID; // 16 +; uint32 RaidOffset; // 17 +; uint32 MaxPlayers; // 18 +; int32 ParentMapID; // 19 +; }; + +; struct MapDifficultyEntry +; { +; //uint32 ID; // 0 +; uint32 MapID; // 1 +; uint32 Difficulty; // 2 (for arenas: arena slot) +; char* Message; // 3 (text showed when transfer to map failed) +; uint32 RaidDuration; // 4 in secs, 0 if no fixed reset time +; uint32 MaxPlayers; // 5 some heroic versions have 0 when expected same amount as in normal version +; //char* Difficultystring; // 6 +; }; + +; struct MountCapabilityEntry +; { +; uint32 ID; // 1 +; uint32 Flags; // 2 +; uint32 ReqRidingSkill; // 3 +; uint32 ReqAreaID; // 4 +; uint32 ReqSpellAuraID; // 5 +; uint32 ReqSpellKnownID; // 6 +; uint32 ModSpellAuraID; // 7 +; int32 ReqMapID; // 8 +; }; + +; struct MountTypeEntry +; { +; uint32 ID; +; uint32 Capability[24]; +; }; + +; struct MovieEntry +; { +; uint32 ID; // 0 index +; //char* Filename; // 1 +; //uint32 Volume; // 2 +; //uint32 KeyID; // 3 +; }; + +; struct NameGenEntry +; { +; //uint32 ID; // 1 +; char* Name; // 2 +; uint32 RaceID; // 3 +; uint32 Sex; // 4 +; }; + +; struct NumTalentsAtLevelEntry +; { +; //uint32 Level; // 0 index +; float NumberOfTalents; // 1 talent count +; }; + +; struct NamesProfanityEntry +; { +; // uint32 ID; // 0 +; char const* Name; // 1 +; int32 Language; // 2 +; }; + +; struct NamesReservedEntry +; { +; // uint32 ID; // 0 +; char const* Name; // 1 +; int32 Language; // 2 +; }; + +; struct OverrideSpellDataEntry +; { +; uint32 ID; // 0 +; uint32 Spells[10]; // 1-10 +; // uint32 Flags; // 11 +; // char* PlayerActionbar; // 12 +; }; + +; struct PlayerConditionEntry +; { +; uint32 ID; // 0 +; char* FailureDescription; // 1 +; }; + +; struct PowerDisplayEntry +; { +; uint32 ID; // 0 +; uint32 ActualType; // 1 +; // char* GlobalStringBaseTag; // 2 +; // uint8 Red; // 3 +; // uint8 Green; // 4 +; // uint8 Blue; // 5 +; // uint8 Padding // 6 +; }; + +; struct PvPDifficultyEntry +; { +; //uint32 ID; // 0 +; uint32 MapID; // 1 +; uint32 RangeIndex; // 2 +; uint32 MinLevel; // 3 +; uint32 MaxLevel; // 4 +; uint32 Difficulty; // 5 +; }; + +; struct QuestSortEntry +; { +; uint32 ID; // 0 +; //char* SortName; // 1 +; }; + +; struct QuestXPEntry +; { +; uint32 ID; // 0 +; uint32 Difficulty[10]; // 1 - 10 +; }; + +; struct QuestFactionRewEntry +; { +; uint32 ID; // 0 +; int32 Difficulty[10]; // 1 - 11 +; }; + +; struct QuestPOIBlobEntry +; { +; uint32 ID; // 0 +; uint32 NumPoints; // 1 +; uint32 MapID; // 2 +; uint32 WorldMapAreaID; // 3 +; }; + +; struct QuestPOIPointEntry +; { +; uint32 ID; // 0 +; int32 X; // 1 +; int32 Y; // 2 +; uint32 QuestPOIBlobID; // 3 +; }; + +; struct RandomPropertiesPointsEntry +; { +; //uint32 Id; // 0 hidden key +; uint32 itemLevel; // 1 +; uint32 EpicPropertiesPoints[5]; // 2-6 +; uint32 RarePropertiesPoints[5]; // 7-11 +; uint32 UncommonPropertiesPoints[5]; // 12-16 +; }; + +; // ResearchBranch.dbc +; struct ResearchBranchEntry +; { +; uint32 ID; // 0 +; char* Name; // 1 +; uint32 ResearchFieldID; // 2 +; uint32 CurrencyID; // 3 +; char* Texture; // 4 +; uint32 ItemID; // 5 +; }; + +; // ResearchField.dbc +; struct ResearchFieldEntry +; { +; uint32 ID; // 0 +; char* Name; // 1 +; uint32 Slot; // 2 +; }; + +; // ResearchProject.dbc +; struct ResearchProjectEntry +; { +; uint32 ID; // 0 +; char* Name; // 1 +; char* Description; // 2 +; uint32 Rarity; // 3 +; uint32 ResearchBranchID; // 4 +; uint32 SpellID; // 5 +; uint32 NumSockets; // 6 +; char* Texture; // 7 +; uint32 RequiredWeight; // 8 +; }; + +; // ResearchSite.dbc +; struct ResearchSiteEntry +; { +; uint32 ID; // 1 +; uint32 MapID; // 2 +; uint32 QuestPOIBlobID; // 3 +; char* Name; // 4 +; char* AreaPOIIconEnum; // 5 +; }; + +; struct ScalingStatDistributionEntry +; { +; uint32 ID; // 0 +; int32 StatID[10]; // 1 - 10 +; uint32 Bonus[10]; // 11 - 20 +; uint32 Minlevel; // 21 +; uint32 Maxlevel; // 22 +; }; + +; struct ScalingStatValuesEntry +; { +; uint32 ID; // 0 +; uint32 Charlevel; // 1 +; uint32 dpsMod[6]; // 2 - 7 DPS mod for level +; uint32 Spellpower; // 8 spell power for level +; uint32 StatMultiplier[5]; // 9 - 13 Multiplier for ScalingStatDistribution +; uint32 Armor[8][4]; // 14 - 46 Armor for level +; uint32 CloakArmor; // 47 armor for cloak +; }; + +; struct SkillLineCategoryEntry +; { +; uint32 id; // 0 +; char* name; // 1 +; uint32 displayOrder; // 2 +; }; + +; struct SkillLineEntry +; { +; uint32 ID; // 0 +; int32 CategoryID; // 1 +; char* DisplayName; // 3 +; //char* Description; // 4 +; uint32 SpellIconID; // 5 +; //char* AlternateVerb; // 6 +; uint32 CanLink; // 7 (prof. with recipe) +; }; + +; struct SkillLineAbilityEntry +; { +; uint32 ID; // 0 +; uint32 SkillLine; // 1 +; uint32 Spell; // 2 +; uint32 RaceMask; // 3 +; uint32 ClassMask; // 4 +; //uint32 ExcludeRace; // 5 +; //uint32 ExcludeClass; // 6 +; uint32 MinSkillLineRank; // 7 +; uint32 SupercededBySpell; // 8 +; uint32 AcquireMethod; // 9 +; uint32 TrivialSkillLineRankHigh; // 10 +; uint32 TrivialSkillLineRankLow; // 11 +; uint32 NumSkillUps; // 12 +; uint32 UniqueBit; // 13 +; }; + +; struct SkillRaceClassInfoEntry +; { +; //uint32 ID; // 0 +; uint32 SkillID; // 1 +; uint32 RaceMask; // 2 +; uint32 ClassMask; // 3 +; uint32 Flags; // 4 +; uint32 Availability; // 5 +; //uint32 MinLevel; // 6 +; uint32 SkillTierID; // 7 +; //uint32 SkillCostIndex; // 8 +; }; + +; struct SkillTiersEntry +; { +; uint32 ID; // 0 +; //uint32 Cost[16]; // 1-16 +; uint32 Value[16]; // 17-32 +; }; + +; struct SoundEntriesEntry +; { +; uint32 ID; // 0 +; //uint32 SoundType; // 1 +; //char* Name; // 2 +; //char* File[10]; // 3 - 12 +; //uint32 Freq[10]; // 13 - 22 +; //char* DirectoryBase; // 23 +; //float VolumeFloat; // 24 +; //uint32 Flags; // 25 +; //float MinDistance; // 26 +; //float DistanceCutoff; // 27 +; //uint32 EAXDef; // 28 +; //uint32 SoundEntriesAdvancedID; // 29 +; //float Volumevariationplus; // 30 +; //float Volumevariationminus; // 31 +; //float Pitchvariationplus; // 32 +; //float Pitchvariationminus; // 33 +; //float PitchAdjust; // 34 +; }; + +; // SpellEffect.dbc +; struct SpellEffectEntry +; { +; uint32 ID; // 0 +; uint32 Effect; // 1 +; float EffectAmplitude; // 2 +; uint32 EffectAura; // 3 +; uint32 EffectAuraPeriod; // 4 +; int32 EffectBasePoints; // 5 +; float EffectBonusCoefficient; // 6 +; float EffectChainAmplitude; // 7 +; uint32 EffectChainTargets; // 8 +; int32 EffectDieSides; // 9 +; uint32 EffectItemType; // 10 +; uint32 EffectMechanic; // 11 +; int32 EffectMiscValue; // 12 +; int32 EffectMiscValueB; // 13 +; float EffectPointsPerResource; // 14 +; uint32 EffectRadiusIndex; // 15 +; uint32 EffectRadiusMaxIndex; // 16 +; float EffectRealPointsPerLevel; // 17 +; flag96 EffectSpellClassMask; // 18 - 20 +; uint32 EffectTriggerSpell; // 21 +; uint32 EffectImplicitTargetA; // 22 +; uint32 EffectImplicitTargetB; // 23 +; uint32 SpellID; // 24 +; uint32 EffectIndex; // 25 +; //uint32 EffectAttributes // 26 +; }; + +; // SpellAuraOptions.dbc +; struct SpellAuraOptionsEntry +; { +; uint32 ID; // 0 +; uint32 CumulativeAura; // 1 +; uint32 ProcChance; // 2 +; uint32 ProcCharges; // 3 +; uint32 ProcTypeMask; // 4 +; }; + +; // SpellAuraRestrictions.dbc/ +; struct SpellAuraRestrictionsEntry +; { +; //uint32 ID; // 0 +; uint32 CasterAuraState; // 1 +; uint32 TargetAuraState; // 2 +; uint32 ExcludeCasterAuraState; // 3 +; uint32 ExcludeTargetAuraState; // 4 +; uint32 CasterAuraSpell; // 5 +; uint32 TargetAuraSpell; // 6 +; uint32 ExcludeCasterAuraSpell; // 7 +; uint32 ExcludeTargetAuraSpell; // 8 +; }; + +; // SpellCastingRequirements.dbc +; struct SpellCastingRequirementsEntry +; { +; uint32 ID; // 0 +; uint32 FacingCasterFlags; // 1 +; uint32 MinFactionID; // 2 +; uint32 MinReputation; // 3 +; int32 RequiredAreasID; // 4 +; uint32 RequiredAuraVision; // 5 +; uint32 RequiresSpellFocus; // 6 +; }; + +; // SpellTotems.dbc +; struct SpellTotemsEntry +; { +; uint32 ID; // 0 +; uint32 RequiredTotemCategoryID[2]; // 1 +; uint32 Totem[2]; // 2 +; }; + +; // Spell.dbc +; struct SpellEntry +; { +; uint32 ID; // 0 +; uint32 Attributes; // 1 +; uint32 AttributesEx; // 2 +; uint32 AttributesEx2; // 3 +; uint32 AttributesEx3; // 4 +; uint32 AttributesEx4; // 5 +; uint32 AttributesEx5; // 6 +; uint32 AttributesEx6; // 7 +; uint32 AttributesEx7; // 8 +; uint32 AttributesEx8; // 9 +; uint32 AttributesEx9; // 1 +; uint32 AttributesEx10; // 1 +; uint32 CastingTimeIndex; // 1 +; uint32 DurationIndex; // 1 +; uint32 PowerType; // 1 +; uint32 RangeIndex; // 1 +; float Speed; // 1 +; uint32 SpellVisualID[2]; // 17 - 18 +; uint32 SpellIconID; // 19 +; uint32 ActiveIconID; // 20 +; char* Name; // 21 +; char* NameSubtext; // 22 +; //char* Description; // 23 +; //char* AuraDescription; // 24 +; uint32 SchoolMask; // 25 +; uint32 RuneCostID; // 26 +; //uint32 SpellMissileID; // 27 +; //uint32 DescriptionVariablesID; // 28 +; uint32 Difficulty; // 29 +; float BonusCoefficient; // 30 +; uint32 ScalingID; // 31 +; uint32 AuraOptionsID; // 32 +; uint32 AuraRestrictionsID; // 33 +; uint32 CastingRequirementsID; // 34 +; uint32 CategoriesID; // 35 +; uint32 ClassOptionsID; // 36 +; uint32 CooldownsID; // 37 +; //uint32 unkIndex7; // 38 +; uint32 EquippedItemsID; // 39 +; uint32 InterruptsID; // 40 +; uint32 LevelsID; // 41 +; uint32 PowerDisplayID; // 42 +; uint32 ReagentsID; // 43 +; uint32 ShapeshiftID; // 44 +; uint32 TargetRestrictionsID; // 45 +; uint32 TotemsID; // 46 +; uint32 RequiredProjectID; // 47 +; }; + +; // SpellCategories.dbc +; struct SpellCategoriesEntry +; { +; //uint32 ID; // 0 +; uint32 Category; // 1 +; uint32 DefenseType; // 2 +; uint32 DispelType; // 3 +; uint32 Mechanic; // 4 +; uint32 PreventionType; // 5 +; uint32 StartRecoveryCategory; // 6 +; }; + +; struct SpellCastTimesEntry +; { +; uint32 ID; // 0 +; int32 Base; // 1 +; //int32 PerLevel; // 2 +; //int32 Minimum; // 3 +; }; + +; struct SpellCategoryEntry +; { +; uint32 ID; // 0 +; uint32 Flags; // 1 +; uint32 UsesPerWeek; // 2 +; // char* Name; // 3 +; }; + +; struct SpellDifficultyEntry +; { +; uint32 ID; // 0 +; int32 DifficultySpellID[4]; // 1 - 4 instance modes: 10N, 25N, 10H, 25H or Normal/Heroic if only 1-2 is set, if 3-4 is 0 then Mode-2 +; }; + +; struct SpellFocusObjectEntry +; { +; uint32 ID; // 0 +; //char* Name; // 1 +; }; + +; struct SpellRadiusEntry +; { +; uint32 ID; // 0 +; float RadiusMin; // 1 +; float RadiusPerLevel; // 2 +; float RadiusMax; // 3 +; }; + +; struct SpellRangeEntry +; { +; uint32 ID; // 1 +; float RangeMin[2]; // 2 - 3 +; float RangeMax[2]; // 4 - 5 +; uint32 Flags; // 6 +; //char* DisplayName; // 7 +; //char* DisplayNameShort; // 8 +; }; + +; // SpellEquippedItems.dbc +; struct SpellEquippedItemsEntry +; { +; //uint32 ID; // 1 +; int32 EquippedItemClass; // 2 +; int32 EquippedItemInvTypes; // 3 +; int32 EquippedItemSubclass; // 4 +; }; + +; // SpellCooldowns.dbc +; struct SpellCooldownsEntry +; { +; //uint32 ID; // 0 +; uint32 CategoryRecoveryTime; // 1 +; uint32 RecoveryTime; // 2 +; uint32 StartRecoveryTime; // 3 +; }; + +; // SpellClassOptions.dbc +; struct SpellClassOptionsEntry +; { +; //uint32 ID; // 0 +; //uint32 ModalNextSpell; // 1 +; flag96 SpellFamilyMask; // 2-4 +; uint32 SpellClassSet; // 5 +; //char* Description; // 6 +; }; + +; // SpellInterrupts.dbc +; struct SpellInterruptsEntry +; { +; //uint32 ID; // 0 +; uint32 AuraInterruptFlags[2]; // 1 - 2 +; uint32 ChannelInterruptFlags[2]; // 3 - 4 +; uint32 InterruptFlags; // 5 +; }; + +; // SpellLevels.dbc +; struct SpellLevelsEntry +; { +; //uint32 ID; // 0 +; uint32 BaseLevel; // 1 +; uint32 MaxLevel; // 2 +; uint32 SpellLevel; // 3 +; }; + +; // SpellPower.dbc +; struct SpellPowerEntry +; { +; //uint32 ID; // 0 +; uint32 ManaCost; // 1 +; uint32 ManaCostPerLevel; // 2 +; uint32 PowerCostPct; // 3 +; uint32 ManaPerSecond; // 4 +; //uint32 PowerDisplayID; // 5 +; //uint32 AltPowerBarID; // 6 +; float PowerCostPct2; // 7 +; }; + +; struct SpellRuneCostEntry +; { +; uint32 ID; // 0 +; uint32 RuneCost[3]; // 1 - 3 (0 = blood, 1 = frost, 2 = unholy) +; uint32 RunicPower; // 4 +; }; + +; struct SpellShapeshiftFormEntry +; { +; uint32 ID; // 0 +; //uint32 BonusActionBar; // 1 unused +; //char* Name; // 2 unused +; uint32 Flags; // 3 +; int32 CreatureType; // 4 <= 0 humanoid, other normal creature types +; //uint32 AttackIconID; // 5 unused, related to next field +; uint32 CombatRoundTime; // 6 +; uint32 CreatureDisplayID[4]; // 7 - 10 +; uint32 PresetSpellID[8]; // 11 - 18 spells which appear in the bar after shapeshifting +; uint32 MountTypeID; // 19 +; //uint32 ExitSoundEntriesID; // 20 +; }; + +; // SpellShapeshift.dbc +; struct SpellShapeshiftEntry +; { +; uint32 ID; // 0 +; uint32 ShapeshiftExclude[2]; // 1 +; uint32 ShapeshiftMask[2]; // 3 +; // uint32 StanceBarOrder; // 5 +; }; + +; // SpellTargetRestrictions.dbc +; struct SpellTargetRestrictionsEntry +; { +; uint32 ID; // 0 +; float ConeAngle; // 1 +; uint32 MaxTargets; // 2 +; uint32 MaxTargetLevel; // 3 +; uint32 TargetCreatureType; // 4 +; uint32 Targets; // 5 +; }; + +; // SpellReagents.dbc +; struct SpellReagentsEntry +; { +; //uint32 ID; // 0 +; int32 Reagents[8]; // 1 - 8 +; uint32 ReagentCount[8]; // 9 - 16 +; }; + +; // SpellScaling.dbc +; struct SpellScalingEntry +; { +; //uint32 ID; // 0 m_ID +; int32 CastTimeMin; // 1 +; int32 CastTimeMax; // 2 +; int32 CastTimeMaxLevel; // 3 +; int32 Class; // 4 (index * 100) + charLevel - 1 => gtSpellScaling.dbc +; float Coefficient[3]; // 5 - 7 +; float Variance[3]; // 8 - 10 +; float ComboPointsCoefficient[3]; // 11 - 13 +; float NerfFactor; // 14 some coefficient, mostly 1.0f +; int32 NerfMaxLevel; // 15 some level +; }; + +; struct SpellDurationEntry +; { +; uint32 ID; // 0 +; int32 Duration; // 1 +; int32 DurationPerLevel; // 2 +; int32 MaxDuration; // 3 +; }; + +; struct SpellItemEnchantmentEntry +; { +; uint32 ID; // 0 +; //uint32 Charges; // 1 +; uint32 Effect[3]; // 2 - 4 +; uint32 EffectPointsMin[3]; // 5 - 7 +; //uint32 EffectPointsMax[3];// 8 - 10 +; uint32 EffectArg[3]; // 11 - 13 +; char* Name; // 14 +; uint32 ItemVisual; // 15 +; uint32 Flags; // 16 +; uint32 Src_itemID; // 17 +; uint32 Condition_ID; // 18 +; uint32 RequiredSkillID; // 19 +; uint32 RequiredSkillRank; // 20 +; uint32 MinLevel; // 21 +; //uint32 ItemLevel; // 22 +; }; + +; struct SpellItemEnchantmentConditionEntry +; { +; uint32 ID; // 0 m_ID +; uint8 Color[3]; // 1-3 m_lt_operandType[5] +; //uint8 unk1; // 4 +; //uint32 unk2[6]; // 5-10 +; uint8 Comparator[3]; // 11-13 m_operator[5] +; //uint8 unk3[2]; // 14-15 +; uint8 CompareColor[3]; // 16-18 m_rt_operandType[5] +; //uint32 unk4; // 19 +; uint32 Value[3]; // 20-22 m_rt_operand[5] +; //uint32 unk5[2]; // 23-24 +; //uint8 unk6[6]; // 25-30 +; }; + +; struct SpellVisualEntry +; { +; //uint32 ID; +; //uint32 PrecastKit; +; //uint32 CastKit; +; //uint32 ImpactKit; +; //uint32 StateKit; +; //uint32 StateDoneKit; +; //uint32 ChannelKit; +; uint32 HasMissile; +; int32 MissileModel; +; //uint32 MissilePathType; +; //uint32 MissileDestinationAttachment; +; //uint32 MissileSound; +; //uint32 AnimEventSoundID; +; //uint32 Flags; +; //uint32 CasterImpactKit; +; //uint32 TargetImpactKit; +; //int32 MissileAttachment; +; //uint32 MissileFollowGroundHeight; +; //uint32 MissileFollowGroundDropSpeed; +; //uint32 MissileFollowGroundApprach; +; //uint32 MissileFollowGroundFlags; +; //uint32 MissileMotionId; +; //uint32 MissileTargetingKit; +; //uint32 InstantAreaKit; +; //uint32 ImpactAreaKit; +; //uint32 PersistentAreaKit; +; //DBCPosition3D MissileCastOffset; +; //DBCPosition3D MissileImpactOffset; +; uint32 AlternativeVisualID; +; }; + +; struct SpellVisualKitEntry +; { +; uint32 ID; +; uint32 StartAnimID; +; uint32 AnimID; +; uint32 AnimKitID; +; uint32 HeadEffect; +; uint32 ChestEffect; +; uint32 BaseEffect; +; uint32 LeftHandEffect; +; uint32 RightHandEffect; +; uint32 BreathEffect; +; uint32 LeftWeaponEffect; +; uint32 RightWeaponEffect; +; uint32 SpecialEffect[3]; +; uint32 WorldEffect; +; uint32 SoundID; +; uint32 ShakeID; +; uint32 CharProc[4]; +; uint32 CharParamZero[4]; +; uint32 CharParamOne[4]; +; uint32 CharParamTwo[4]; +; uint32 CharParamThree[4]; +; uint32 Flags; +; }; + +; struct SummonPropertiesEntry +; { +; uint32 ID; // 0 +; uint32 Control; // 1, 0 - can't be controlled?, 1 - something guardian?, 2 - pet?, 3 - something controllable?, 4 - taxi/mount? +; uint32 Faction; // 2, 14 rows > 0 +; int32 Title; // 3, see enum +; int32 Slot; // 4, 0-6 +; uint32 Flags; // 5 +; }; + +; struct TalentEntry +; { +; uint32 ID; // 0 +; uint32 TabID; // 1 index in TalentTab.dbc (TalentTabEntry) +; uint32 TierID; // 2 +; uint32 ColumnIndex; // 3 +; uint32 SpellRank[MAX_TALENT_RANK]; // 4-8 +; uint32 PrereqTalent[3]; // 9 - 11 (Talent.dbc) +; uint32 PrereqRank[3]; // 12 - 14 part of prev field +; //uint32 Flags; // 15 also need disable higest ranks on reset talent tree +; //uint32 RequiredSpellID; // 16 +; //uint64 CategoryMask[2]; // 17 - 18 its a 64 bit mask for pet 1 << m_categoryEnumID in CreatureFamily.dbc +; }; + +; struct TalentTabEntry +; { +; uint32 ID; // 0 +; //char* Name; // 1 +; //unit32 SpellIconID; // 2 +; uint32 ClassMask; // 3 +; uint32 CategoryEnumID; // 4 +; uint32 OrderIndex; // 5 +; //char* BackgroundFile; // 6 +; //char* Description; // 7 +; //uint32 RoleMask; // 8 +; uint32 MasterySpellID[2]; // 9 - 10 passive mastery bonus spells +; }; + +; struct TalentTreePrimarySpellsEntry +; { +; //uint32 ID; // 0 index +; uint32 TalentTabID; // 1 entry from TalentTab.dbc +; uint32 SpellID; // 2 spell id to learn +; //uint32 Flags; // 3 some kind of flags +; }; + +; struct TaxiNodesEntry +; { +; uint32 ID; // 0 +; uint32 ContinentID; // 1 +; DBCPosition3D Pos; // 2 - 4 +; char* Name; // 5 +; uint32 MountCreatureID[2]; // 6 - 7 +; uint32 Flags; // 8 +; DBCPosition2D MapOffset; // 9 - 10 +; }; + +; struct TaxiPathEntry +; { +; uint32 ID; // 0 +; uint32 FromTaxiNode; // 1 +; uint32 ToTaxiNode; // 2 +; uint32 Cost; // 3 +; }; + +; struct TaxiPathNodeEntry +; { +; //uint32 ID; // 0 +; uint32 PathID; // 1 +; uint32 NodeIndex; // 2 +; uint32 ContinentID; // 3 +; DBCPosition3D Loc; // 4 - 6 +; uint32 Flags; // 7 +; uint32 Delay; // 8 +; uint32 ArrivalEventID; // 9 +; uint32 DepartureEventID; // 10 +; }; + +; struct TotemCategoryEntry +; { +; uint32 ID; // 0 +; //char* Name; // 1 +; uint32 TotemCategoryType; // 2 (one for specialization) +; uint32 TotemCategoryMask; // 3 (compatibility mask for same type: different for totems, compatible from high to low for rods) +; }; + +; struct UnitPowerBarEntry +; { +; uint32 ID; // 1 +; uint32 MinPower; // 2 +; uint32 MaxPower; // 3 +; uint32 StartPower; // 4 +; //uint32 CenterPower; // 5 +; float RegenerationPeace; // 6 +; float RegenerationCombat; // 7 +; //uint32 BarType; // 8 +; //uint32 FileDataID[6]; // 9 - 14 +; //uint32 Color[6]; // 15 - 20 +; //uint32 Flags; // 21 +; //char* Name; // 22 +; //char* Cost; // 23 +; //char* OutOfError; // 24 +; //char* ToolTip; // 25 +; //float StartInset; // 26 +; //float EndInset; // 27 +; }; + +; struct TransportAnimationEntry +; { +; //uint32 ID; // 1 +; uint32 TransportID; // 2 +; uint32 TimeIndex; // 3 +; DBCPosition3D Pos; // 4 - 6 +; //uint32 SequenceID; // 7 +; }; + +; struct TransportRotationEntry +; { +; //uint32 ID; // 1 +; uint32 GameObjectsID; // 2 +; uint32 TimeIndex; // 3 +; float X; // 4 +; float Y; // 5 +; float Z; // 6 +; float W; // 7 +; }; + +; struct VehicleEntry +; { +; uint32 ID; // 0 +; uint32 Flags; // 1 +; float TurnSpeed; // 2 +; float PitchSpeed; // 3 +; float PitchMin; // 4 +; float PitchMax; // 5 +; uint32 SeatID[8]; // 6 - 13 +; float MouseLookOffsetPitch; // 14 +; float CameraFadeDistScalarMin; // 15 +; float CameraFadeDistScalarMax; // 16 +; float CameraPitchOffset; // 17 +; float FacingLimitRight; // 18 +; float FacingLimitLeft; // 19 +; float MsslTrgtTurnLingering; // 20 +; float MsslTrgtPitchLingering; // 21 +; float MsslTrgtMouseLingering; // 22 +; float MsslTrgtEndOpacity; // 23 +; float MsslTrgtArcSpeed; // 24 +; float MsslTrgtArcRepeat; // 25 +; float MsslTrgtArcWidth; // 26 +; float MsslTrgtImpactRadius[2]; // 27 - 28 +; char* MsslTrgtArcTexture; // 29 +; char* MsslTrgtImpactTexture; // 30 +; char* MsslTrgtImpactModel[2]; // 31 - 32 +; float CameraYawOffset; // 33 +; uint32 UiLocomotionType; // 34 +; float MsslTrgtImpactTexRadius; // 35 +; uint32 VehicleUIIndicatorID; // 36 +; uint32 PowerDisplayID[3]; // 37 +; }; + +; struct VehicleSeatEntry +; { +; uint32 ID; // 0 +; uint32 Flags; // 1 +; int32 AttachmentID; // 2 +; DBCPosition3D AttachmentOffset; // 3 - 5 +; float EnterPreDelay; // 6 +; float EnterSpeed; // 7 +; float EnterGravity; // 8 +; float EnterMinDuration; // 9 +; float EnterMaxDuration; // 10 +; float EnterMinArcHeight; // 11 +; float EnterMaxArcHeight; // 12 +; int32 EnterAnimStart; // 13 +; int32 EnterAnimLoop; // 14 +; int32 RideAnimStart; // 15 +; int32 RideAnimLoop; // 16 +; int32 RideUpperAnimStart; // 17 +; int32 RideUpperAnimLoop; // 18 +; float ExitPreDelay; // 19 +; float ExitSpeed; // 20 +; float ExitGravity; // 21 +; float ExitMinDuration; // 22 +; float ExitMaxDuration; // 23 +; float ExitMinArcHeight; // 24 +; float ExitMaxArcHeight; // 25 +; int32 ExitAnimStart; // 26 +; int32 ExitAnimLoop; // 27 +; int32 ExitAnimEnd; // 28 +; float PassengerYaw; // 29 +; float PassengerPitch; // 30 +; float PassengerRoll; // 31 +; int32 PassengerAttachmentID; // 32 +; int32 VehicleEnterAnim; // 33 +; int32 VehicleExitAnim; // 34 +; int32 VehicleRideAnimLoop; // 35 +; int32 VehicleEnterAnimBone; // 36 +; int32 VehicleExitAnimBone; // 37 +; int32 VehicleRideAnimLoopBone; // 38 +; float VehicleEnterAnimDelay; // 39 +; float VehicleExitAnimDelay; // 40 +; uint32 VehicleAbilityDisplay; // 41 +; uint32 EnterUISoundID; // 42 +; uint32 ExitUISoundID; // 43 +; int32 UISkin; // 44 +; uint32 FlagsB; // 45 +; float CameraEnteringDelay; // 46 +; float CameraEnteringDuration; // 47 +; float CameraExitingDelay; // 48 +; float CameraExitingDuration; // 49 +; DBCPosition3D CameraOffset; // 50 - 52 +; float CameraPosChaseRate; // 53 +; float CameraFacingChaseRate; // 54 +; float CameraEnteringZoom; // 55 +; float CameraSeatZoomMin; // 56 +; float CameraSeatZoomMax; // 57 +; uint32 EnterAnimKitID; // 58 +; uint32 RideAnimKitID; // 59 +; uint32 ExitAnimKitID; // 60 +; uint32 VehicleEnterAnimKitID; // 61 +; uint32 VehicleRideAnimKitID; // 62 +; uint32 VehicleExitAnimKitID; // 63 +; uint32 CameraModeID; // 64 +; uint32 FlagsC; // 65 +; }; + +; struct WMOAreaTableEntry +; { +; uint32 ID; // 0 index +; int32 WMOID; // 1 used in root WMO +; int32 NameSetID; // 2 used in adt file +; int32 WMOGroupID; // 3 used in group WMO +; //uint32 SoundProviderPref; // 4 +; //uint32 SoundProviderPrefUnderwater; // 5 +; //uint32 AmbienceID; // 6 +; //uint32 ZoneMusic; // 7 +; //uint32 IntroSound; // 8 +; uint32 Flags; // 9 used for indoor/outdoor determination +; uint32 AreaTableID; // 10 link to AreaTableEntry.ID +; //char* AreaName; // 11 +; //uint32 UwIntroSound; // 12 +; //uint32 UwZoneMusic; // 13 +; //uint32 UwAmbience; // 14 +; }; + +; struct WorldSafeLocsEntry +; { +; uint32 ID; // 0 +; uint32 Continent; // 1 +; DBCPosition3D Loc; // 2 - 4 +; //char* AreaName; // 5 +; }; + +; struct WorldStateSounds +; { +; uint32 ID; // 0 Worldstate +; uint32 unk; // 1 +; uint32 areaTable; // 2 +; uint32 WMOAreaTable; // 3 +; uint32 zoneIntroMusicTable; // 4 +; uint32 zoneIntroMusic; // 5 +; uint32 zoneMusic; // 6 +; uint32 soundAmbience; // 7 +; uint32 soundProviderPreferences; // 8 +; }; + +; struct WorldStateUI +; { +; uint32 ID; // 0 +; uint32 map_id; // 1 Can be -1 to show up everywhere. +; uint32 zone; // 2 Can be zero for "everywhere". +; uint32 phaseMask; // 3 Phase this WorldState is avaliable in +; uint32 icon; // 4 The icon that is used in the interface. +; char* textureFilename; // 5 +; char* text; // 6-21 The worldstate text +; char* description; // 22-38 Text shown when hovering mouse on icon +; uint32 worldstateID; // 39 This is the actual ID used +; uint32 type; // 40 0 = unknown, 1 = unknown, 2 = not shown in ui, 3 = wintergrasp +; uint32 unk1; // 41 +; uint32 unk2; // 43 +; uint32 unk3; // 44-58 +; uint32 unk4; // 59-61 Used for some progress bars. +; uint32 unk7; // 62 Unused in 3.3.5a +; }; 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 @@ + + + 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 . +*/ + +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.class.php b/setup/tools/fileGen.class.php deleted file mode 100644 index a72651bb..00000000 --- a/setup/tools/fileGen.class.php +++ /dev/null @@ -1,271 +0,0 @@ - ['aowow.xml', 'static/download/searchplugins/', []], - 'power' => ['power.js', 'static/widgets/', []], - 'searchboxScript' => ['searchbox.js', 'static/widgets/', []], - 'demo' => ['demo.html', 'static/widgets/power/', []], - 'searchboxBody' => ['searchbox.html', 'static/widgets/searchbox/', []], - 'realmMenu' => ['profile_all.js', 'static/js/', ['realmlist']], - 'locales' => ['locale.js', 'static/js/', []], - 'itemScaling' => ['item-scaling', 'datasets/', []] - ); - public static $datasets = array( // name => [AowowDeps, TCDeps] - 'realms' => [null, ['realmlist']], - 'statistics' => [null, ['player_levelstats', 'player_classlevelstats']], - 'simpleImg' => [null, null], - 'complexImg' => [null, null], - 'talentCalc' => [null, null], - 'pets' => [['spawns', 'creature'], null], - 'talentIcons' => [null, null], - 'glyphs' => [['items', 'spell'], null], - 'itemsets' => [['itemset', 'spell'], null], - 'enchants' => [['items', 'spell', 'itemenchantment'], null], - 'gems' => [['items', 'spell', 'itemenchantment'], null], - 'profiler' => [['quests', 'quests_startend', 'spell', 'currencies', 'achievement', 'titles'], null], - 'weightPresets' => [null, null], - 'soundfiles' => [['sounds'], null] - ); - - public static $defaultExecTime = 30; - - private static $reqDirs = array( - 'static/uploads/screenshots/normal', - 'static/uploads/screenshots/pending', - 'static/uploads/screenshots/resized', - 'static/uploads/screenshots/temp', - 'static/uploads/screenshots/thumb', - 'static/uploads/temp/', - 'static/download/searchplugins/', - 'static/wowsounds/' - ); - - public static $txtConstants = array( - 'CFG_NAME' => CFG_NAME, - 'CFG_NAME_SHORT' => CFG_NAME_SHORT, - 'HOST_URL' => HOST_URL, - 'STATIC_URL' => STATIC_URL - ); - - public static function init($mode = self::MODE_NORMAL, array $updScripts = []) - { - self::$defaultExecTime = ini_get('max_execution_time'); - $doScripts = null; - - if (getopt(self::$shortOpts, self::$longOpts) || $mode == self::MODE_FIRSTRUN) - self::handleCLIOpts($doScripts); - else if ($mode != self::MODE_UPDATE) - { - self::printCLIHelp(); - exit; - } - - // check passed subscript names; limit to real scriptNames - self::$subScripts = array_merge(array_keys(self::$tplFiles), array_keys(self::$datasets)); - if ($doScripts || $updScripts) - self::$subScripts = array_intersect($doScripts ?: $updScripts, self::$subScripts); - else if ($doScripts === null) - self::$subScripts = []; - - if (!CLISetup::$localeIds /* todo: && this script has localized text */) - { - CLI::write('No valid locale specified. Check your config or --locales parameter, if used', CLI::LOG_ERROR); - exit; - } - - // create directory structure - CLI::write('FileGen::init() - creating required directories'); - $pathOk = 0; - foreach (self::$reqDirs as $rd) - if (CLISetup::writeDir($rd)) - $pathOk++; - - CLI::write('created '.$pathOk.' extra paths'.($pathOk == count(self::$reqDirs) ? '' : ' with errors')); - CLI::write(); - - self::$mode = $mode; - } - - private static function handleCLIOpts(&$doScripts) - { - $_ = getopt(self::$shortOpts, self::$longOpts); - - if ((isset($_['help']) || isset($_['h'])) && empty($_['build'])) - { - self::printCLIHelp(); - exit; - } - - // required subScripts - if (!empty($_['sync'])) - { - $sync = explode(',', $_['sync']); - foreach (self::$tplFiles as $name => $info) - if (!empty($info[2]) && array_intersect($sync, $info[2])) - $doScripts[] = $name; - - foreach (self::$datasets as $name => $info) - { - // recursive deps from SqlGen - if (!empty($info[0]) && array_intersect(SqlGen::$subScripts, $info[0])) - $doScripts[] = $name; - else if (!empty($info[1]) && array_intersect($sync, $info[1])) - $doScripts[] = $name; - } - - $doScripts = $doScripts ? array_unique($doScripts) : null; - } - else if (!empty($_['build'])) - $doScripts = explode(',', $_['build']); - - // optional, overwrite existing files - if (isset($_['f'])) - self::$cliOpts['force'] = true; - - if (isset($_['h'])) - self::$cliOpts['help'] = true; - - // mostly build-instructions from longOpts - foreach (self::$longOpts as $opt) - if (!strstr($opt, ':') && isset($_[$opt])) - self::$cliOpts[$opt] = true; - } - - public static function hasOpt(/* ...$opt */) - { - $result = 0x0; - foreach (func_get_args() as $idx => $arg) - { - if (!is_string($arg)) - continue; - - if (isset(self::$cliOpts[$arg])) - $result |= (1 << $idx); - } - - return $result; - } - - public static function printCLIHelp() - { - echo "\nusage: php aowow --build= [-h --help] [-f --force]\n\n"; - echo "--build : available subScripts:\n"; - foreach (array_merge(array_keys(self::$tplFiles), array_keys(self::$datasets)) as $s) - { - echo " * ".str_pad($s, 20).str_pad(isset(self::$tplFiles[$s]) ? self::$tplFiles[$s][1].self::$tplFiles[$s][0] : 'static data file', 45). - (!empty(self::$tplFiles[$s][2]) ? ' - TC deps: '.implode(', ', self::$tplFiles[$s][2]) : (!empty(self::$datasets[$s][1]) ? ' - TC deps: '.implode(', ', self::$datasets[$s][1]) : '')). - (!empty(self::$datasets[$s][0]) ? ' - Aowow deps: '.implode(', ', self::$datasets[$s][0]) : '')."\n"; - } - - echo "-h --help : shows this info\n"; - echo "-f --force : enforces overwriting existing files\n"; - } - - public static function generate($key, array $updateIds = []) - { - $success = false; - $reqDBC = []; - - if (file_exists('setup/tools/filegen/'.$key.'.func.php')) - require_once 'setup/tools/filegen/'.$key.'.func.php'; - else if (empty(self::$tplFiles[$key])) - { - CLI::write(sprintf(ERR_MISSING_INCL, $key, 'setup/tools/filegen/'.$key.'.func.php', CLI::LOG_ERROR)); - return false; - } - - CLI::write('FileGen::generate() - gathering data for '.$key); - - if (!empty(self::$tplFiles[$key])) - { - list($file, $destPath, $deps) = self::$tplFiles[$key]; - - if ($content = file_get_contents(FileGen::$tplPath.$file.'.in')) - { - // replace constants - $content = strtr($content, FileGen::$txtConstants); - - // check for required auxiliary DBC files - foreach ($reqDBC as $req) - if (!CLISetup::loadDBC($req)) - continue; - - // must generate content - // PH format: /*setup:*/ - $funcOK = true; - if (preg_match_all('/\/\*setup:([\w\-_]+)\*\//i', $content, $m)) - { - foreach ($m[1] as $func) - { - if (function_exists($func)) - $content = str_replace('/*setup:'.$func.'*/', $func(), $content); - else - { - $funcOK = false; - CLI::write('No function for was registered for placeholder '.$func.'().', CLI::LOG_ERROR); - if (!array_reduce(get_included_files(), function ($inArray, $itr) use ($func) { return $inArray || false !== strpos($itr, $func); }, false)) - CLI::write('Also, expected include setup/tools/filegen/'.$name.'.func.php was not found.'); - } - } - } - - if ($content && $funcOK) - if (CLISetup::writeFile($destPath.$file, $content)) - $success = true; - } - else - CLI::write(sprintf(ERR_READ_FILE, CLI::bold(FileGen::$tplPath.$file.'.in')), CLI::LOG_ERROR); - } - else if (!empty(self::$datasets[$key])) - { - if (function_exists($key)) - { - // check for required auxiliary DBC files - foreach ($reqDBC as $req) - if (!CLISetup::loadDBC($req)) - return false; - - $success = $key($updateIds); - } - else - CLI::write(' - subscript \''.$key.'\' not defined in included file', CLI::LOG_ERROR); - } - - set_time_limit(FileGen::$defaultExecTime); // reset to default for the next script - - return $success; - } - - public static function getMode() - { - return self::$mode; - } -} - -?> diff --git a/setup/tools/filegen/complexImg.func.php b/setup/tools/filegen/complexImg.func.php deleted file mode 100644 index 241c0952..00000000 --- a/setup/tools/filegen/complexImg.func.php +++ /dev/null @@ -1,754 +0,0 @@ - - - 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 . -*/ - -if (!defined('AOWOW_REVISION')) - die('illegal access'); - -if (!CLI) - die('not in cli mode'); - - // note: for the sake of simplicity, this function handles all images, that must be stitched together (which are mostly maps) - - $reqDBC = ['talenttab', 'chrclasses', 'worldmapoverlay', 'worldmaparea']; - - function complexImg() - { - if (isset(FileGen::$cliOpts['help'])) - { - echo "\n"; - echo "available options for subScript 'complexImg':\n"; // modeMask - echo "--talentbgs (backgrounds for talent calculator)\n"; // 0x01 - echo "--maps (generates worldmaps)\n"; // 0x02 - echo "--spawn-maps (creates alphaMasks of each zone to check spawns against)\n"; // 0x04 - echo "--artwork (optional: imagery from /glues/credits (not used, skipped by default))\n"; // 0x08 - echo "--area-maps (optional: renders maps with highlighted subZones for each area)\n"; // 0x10 - - return true; - } - - $mapWidth = 1002; - $mapHeight = 668; - $threshold = 95; // alpha threshold to define subZones: set it too low and you have unspawnable areas inside a zone; set it too high and the border regions overlap - $runTime = ini_get('max_execution_time'); - $locStr = null; - $imgPath = CLISetup::$srcDir.'%sInterface/'; - $destDir = 'static/images/wow/'; - $success = true; - $modeMask = 0x7; // talentBGs, regular maps, spawn-related alphaMaps - $paths = array( - 0x16 => ['WorldMap/', true, null], - 0x01 => ['TalentFrame/', false, null], - 0x08 => ['Glues/Credits/',false, null] - ); - - $createAlphaImage = function($w, $h) - { - $img = imagecreatetruecolor($w, $h); - - imagesavealpha($img, true); - imagealphablending($img, false); - - $bgColor = imagecolorallocatealpha($img, 0, 0, 0, 127); - imagefilledrectangle($img, 0, 0, imagesx($img) - 1, imagesy($img) - 1, $bgColor); - - imagecolortransparent($img, $bgColor); - imagealphablending($img, true); - - imagecolordeallocate($img, $bgColor); - - return $img; - }; - - // prefer manually converted PNG files (as the imagecreatefromblp-script has issues with some formats) - // alpha channel issues observed with locale deDE Hilsbrad and Elwynn - maps - // see: https://github.com/Kanma/BLPConverter - $loadImageFile = function($path) - { - $result = null; - - $file = $path.'.png'; - if (CLISetup::fileExists($file)) - { - CLI::write('manually converted png file present for '.$path.'.', CLI::LOG_INFO); - $result = imagecreatefrompng($file); - } - - if (!$result) - { - $file = $path.'.blp'; - if (CLISetup::fileExists($file)) - $result = imagecreatefromblp($file); - } - - return $result; - }; - - $assembleImage = function($baseName, $order, $w, $h) use ($loadImageFile) - { - $dest = imagecreatetruecolor($w, $h); - imagesavealpha($dest, true); - imagealphablending($dest, false); - - $_h = $h; - foreach ($order as $y => $row) - { - $_w = $w; - foreach ($row as $x => $suffix) - { - $src = $loadImageFile($baseName.$suffix); - if (!$src) - { - CLI::write(' - complexImg: tile '.$baseName.$suffix.'.blp missing.', CLI::LOG_ERROR); - unset($dest); - return null; - } - - imagecopyresampled($dest, $src, 256 * $x, 256 * $y, 0, 0, min($_w, 256), min($_h, 256), min($_w, 256), min($_h, 256)); - $_w -= 256; - - unset($src); - } - $_h -= 256; - } - - return $dest; - }; - - $writeImage = function($name, $ext, $src, $w, $h, $done) - { - $ok = false; - $dest = imagecreatetruecolor($w, $h); - imagesavealpha($dest, true); - imagealphablending($dest, false); - imagecopyresampled($dest, $src, 0, 0, 0, 0, $w, $h, imagesx($src), imagesy($src)); - - switch ($ext) - { - case 'jpg': - $ok = imagejpeg($dest, $name.'.'.$ext, 85); - break; - case 'png': - $ok = imagepng($dest, $name.'.'.$ext); - break; - default: - CLI::write($done.' - unsupported file fromat: '.$ext, CLI::LOG_WARN); - } - - imagedestroy($dest); - - if ($ok) - { - chmod($name.'.'.$ext, Util::FILE_ACCESS); - CLI::write($done.' - image '.$name.'.'.$ext.' written', CLI::LOG_OK); - } - else - CLI::write($done.' - could not create image '.$name.'.'.$ext, CLI::LOG_ERROR); - - return $ok; - }; - - $createSpawnMap = function($img, $zoneId) use ($mapHeight, $mapWidth, $threshold) - { - CLI::write(' - creating spawn map'); - - $tmp = imagecreate(1000, 1000); - $cbg = imagecolorallocate($tmp, 255, 255, 255); - $cfg = imagecolorallocate($tmp, 0, 0, 0); - - for ($y = 0; $y < 1000; $y++) - { - for ($x = 0; $x < 1000; $x++) - { - $a = imagecolorat($img, ($x * $mapWidth) / 1000, ($y * $mapHeight) / 1000) >> 24; - imagesetpixel($tmp, $x, $y, $a < $threshold ? $cfg : $cbg); - } - } - - imagepng($tmp, 'setup/generated/alphaMaps/' . $zoneId . '.png'); - - imagecolordeallocate($tmp, $cbg); - imagecolordeallocate($tmp, $cfg); - imagedestroy($tmp); - }; - - $checkSourceDirs = function($sub) use ($imgPath, &$paths, $modeMask) - { - $hasMissing = false; - foreach ($paths as $idx => list($subDir, $isLocalized, $realPath)) - { - if ($realPath && !$isLocalized) - continue; - - $p = sprintf($imgPath, $sub).$subDir; - if (CLISetup::fileExists($p)) - { - if ($isLocalized) - $paths[$idx][2][substr($sub, 0, -1)] = $p; - else - $paths[$idx][2] = $p; - } - else - $hasMissing = true; - } - - return !$hasMissing; - }; - - // do not change order of params! - if ($_ = FileGen::hasOpt('talentbgs', 'maps', 'spawn-maps', 'artwork', 'area-maps')) - $modeMask = $_; - - foreach ($paths as $mode => $__) - if (!($mode & $modeMask)) - unset($paths[$mode]); - - foreach (CLISetup::$expectedPaths as $xp => $locId) - { - if (!in_array($locId, CLISetup::$localeIds)) - continue; - - if ($xp) // if in subDir add trailing slash - $xp .= '/'; - - $checkSourceDirs($xp); // do not break; maps are localized - } - - $locList = []; - foreach (CLISetup::$expectedPaths as $xp => $locId) - if (in_array($locId, CLISetup::$localeIds)) - $locList[] = $xp; - - CLI::write('required resources overview:', CLI::LOG_INFO); - foreach ($paths as list($path, $isLocalized, $realPath)) - { - if (!$realPath) - CLI::write(CLI::red('MISSING').' - '.str_pad($path, 14).' @ '.sprintf($imgPath, '['.implode(',', $locList).']/').$path); - else if ($isLocalized) - { - $foundLoc = []; - foreach (CLISetup::$localeIds as $locId) - foreach (CLISetup::$expectedPaths as $xp => $lId) - if ($locId == $lId && isset($realPath[$xp]) && !isset($foundLoc[$locId])) - $foundLoc[$locId] = $xp; - - if ($diff = array_diff(CLISetup::$localeIds, array_keys($foundLoc))) - { - $buff = []; - foreach ($diff as $d) - $buff[] = CLI::yellow(Util::$localeStrings[$d]); - foreach ($foundLoc as $str) - $buff[] = CLI::green($str); - - CLI::write(CLI::yellow('PARTIAL').' - '.str_pad($path, 14).' @ '.sprintf($imgPath, '['.implode(',', $buff).']/').$path); - } - else - CLI::write(CLI::green(' FOUND ').' - '.str_pad($path, 14).' @ '.sprintf($imgPath, '['.implode(',', $foundLoc).']/').$path); - } - else - CLI::write(CLI::green(' FOUND ').' - '.str_pad($path, 14).' @ '.$realPath); - } - - CLI::write(); - - // if no subdir had sufficient data, diaf - if (count(array_filter(array_column($paths, 2))) != count($paths)) - { - CLI::write('one or more required directories are missing:', CLI::LOG_ERROR); - foreach ($missing as $m) - CLI::write(' - '.$m, CLI::LOG_ERROR); - - return; - } - else - sleep(1); - - /**************/ - /* TalentTabs */ - /**************/ - - if ($modeMask & 0x01) - { - if (CLISetup::writeDir($destDir.'hunterpettalents/') && CLISetup::writeDir($destDir.'talents/backgrounds/')) - { - // [classMask, creatureFamilyMask, tabNr, textureStr] - - $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 = (LOG(2, tt.classMask) + 1)'); - $order = array( - ['-TopLeft', '-TopRight'], - ['-BottomLeft', '-BottomRight'] - ); - - if ($tTabs) - { - $sum = 0; - $total = count($tTabs); - CLI::write('Processing '.$total.' files from TalentFrame/ ...'); - - foreach ($tTabs as $tt) - { - ini_set('max_execution_time', 30); // max 30sec per image (loading takes the most time) - $sum++; - $done = ' - '.str_pad($sum.'/'.$total, 8).str_pad('('.number_format($sum * 100 / $total, 2).'%)', 9); - - if ($tt['creatureFamilyMask']) // is PetCalc - { - $size = [244, 364]; - $name = $destDir.'hunterpettalents/bg_'.(log($tt['creatureFamilyMask'], 2) + 1); - } - else - { - $size = [204, 554]; - $name = $destDir.'talents/backgrounds/'.strtolower($tt['fileString']).'_'.($tt['tabNumber'] + 1); - } - - if (!isset(FileGen::$cliOpts['force']) && file_exists($name.'.jpg')) - { - CLI::write($done.' - file '.$name.'.jpg was already processed'); - continue; - } - - $im = $assembleImage($paths[0x1][2].'/'.$tt['textureFile'], $order, 256 + 44, 256 + 75); - if (!$im) - { - CLI::write(' - could not assemble file '.$tt['textureFile'], CLI::LOG_ERROR); - continue; - } - - if (!$writeImage($name, 'jpg', $im, $size[0], $size[1], $done)) - $success = false; - } - } - else - $success = false; - - ini_set('max_execution_time', $runTime); - } - else - $success = false; - } - - /************/ - /* Worldmap */ - /************/ - - if ($modeMask & 0x16) - { - $mapDirs = array( - ['maps/%snormal/', 'jpg', 488, 325], - ['maps/%soriginal/', 'jpg', 0, 0], // 1002, 668 - ['maps/%ssmall/', 'jpg', 224, 163], - ['maps/%szoom/', 'jpg', 772, 515] - ); - - // as the js expects them - $baseLevelFix = array( - // WotLK maps - // Halls of Stone; The Nexus; Violet Hold; Gundrak; Obsidian Sanctum; Eye of Eternity; Vault of Archavon; Trial of the Champion; The Forge of Souls; Pit of Saron; Halls of Reflection - 4264 => 1, 4265 => 1, 4415 => 1, 4416 => 1, 4493 => 0, 4500 => 1, 4603 => 1, 4723 => 1, 4809 => 1, 4813 => 1, 4820 => 1, - // Cata maps for WotLK instances - // TheStockade; TheBloodFurnace; Ragefire; TheUnderbog; TheBotanica; WailingCaverns; TheSlavePens; TheShatteredHalls; HellfireRamparts; RazorfenDowns; RazorfenKraul; ManaTombs - // ShadowLabyrinth; TheTempleOfAtalHakkar (simplified layout); BlackTemple; TempestKeep; MoltenCore; GruulsLair; CoilfangReservoir; MagtheridonsLair; OnyxiasLair; SunwellPlateau; - 717 => 1, 3713 => 1, 2437 => 1, 3716 => 1, 3847 => 1, 718 => 1, 3717 => 1, 3714 => 1, 3562 => 1, 722 => 1, 491 => 1, 3792 => 1, - 3789 => 1, 1477 => 1, 3959 => 0, 3845 => 1, 2717 => 1, 3923 => 1, 3607 => 1, 3836 => 1, 2159 => 1, 4075 => 0 - ); - - $wmo = DB::Aowow()->select('SELECT *, worldMapAreaId AS ARRAY_KEY, id AS ARRAY_KEY2 FROM dbc_worldmapoverlay WHERE textureString <> ""'); - $wma = DB::Aowow()->select('SELECT * FROM dbc_worldmaparea'); - if (!$wma || !$wmo) - { - $success = false; - CLI::write(' - could not read required dbc files: WorldMapArea.dbc ['.count($wma).' entries]; WorldMapOverlay.dbc ['.count($wmo).' entries]', CLI::LOG_ERROR); - return; - } - - // fixups... - foreach ($wma as &$a) - { - if ($a['areaId']) - continue; - - switch ($a['id']) - { - case 13: $a['areaId'] = -6; break; // Kalimdor - case 14: $a['areaId'] = -3; break; // Eastern Kingdoms - case 466: $a['areaId'] = -2; break; // Outland - case 485: $a['areaId'] = -5; break; // Northrend - } - } - array_unshift($wma, ['id' => -1, 'areaId' => -1, 'nameINT' => 'World'], ['id' => -4, 'areaId' => -4, 'nameINT' => 'Cosmic']); - - $sumMaps = count(CLISetup::$localeIds) * count($wma); - - CLI::write('Processing '.$sumMaps.' files from WorldMap/ ...'); - - foreach (CLISetup::$localeIds as $progressLoc => $l) - { - // create destination directories - $dirError = false; - foreach ($mapDirs as $md) - if (!CLISetup::writeDir($destDir . sprintf($md[0], strtolower(Util::$localeStrings[$l]).'/'))) - $dirError = true; - - if ($modeMask & 0x04) - if (!CLISetup::writeDir('setup/generated/alphaMaps')) - $dirError = true; - - if ($dirError) - { - $success = false; - CLI::write(' - complexImg: could not create map directories for locale '.$l.'. skipping...', CLI::LOG_ERROR); - continue; - } - - - // source for mapFiles - $mapSrcDir = null; - $locDirs = array_reverse(array_filter(CLISetup::$expectedPaths, function($var) use ($l) { return !$var || $var == $l; }), true); - foreach ($locDirs as $mapLoc => $__) - { - if(!isset($paths[0x16][2][$mapLoc])) - continue; - - $p = sprintf($imgPath, $mapLoc.'/').$paths[0x16][0]; - if (CLISetup::fileExists($p)) - { - CLI::write(' - using files from '.($mapLoc ?: '/').' for locale '.Util::$localeStrings[$l], CLI::LOG_INFO); - $mapSrcDir = $p.'/'; - break; - } - } - - if ($mapSrcDir === null) - { - $success = false; - CLI::write(' - no suitable localized map files found for locale '.$l, CLI::LOG_ERROR); - continue; - } - - - foreach ($wma as $progressArea => $areaEntry) - { - $curMap = $progressArea + count($wma) * $progressLoc; - $progress = ' - ' . str_pad($curMap.'/'.($sumMaps), 10) . str_pad('('.number_format($curMap * 100 / $sumMaps, 2).'%)', 9); - - $wmaId = $areaEntry['id']; - $zoneId = $areaEntry['areaId']; - $textureStr = $areaEntry['nameINT']; - - $path = $mapSrcDir.$textureStr; - if (!CLISetup::fileExists($path)) - { - $success = false; - CLI::write('worldmap file '.$path.' missing for selected locale '.Util::$localeStrings[$l], CLI::LOG_ERROR); - continue; - } - - $fmt = array( - [1, 2, 3, 4], - [5, 6, 7, 8], - [9, 10, 11, 12] - ); - - CLI::write($textureStr . " [" . $zoneId . "]"); - - $overlay = $createAlphaImage($mapWidth, $mapHeight); - - // zone has overlays (is in open world; is not multiLeveled) - if (isset($wmo[$wmaId])) - { - CLI::write(' - area has '.count($wmo[$wmaId]).' overlays'); - - foreach ($wmo[$wmaId] as &$row) - { - $i = 1; - $y = 0; - while ($y < $row['h']) - { - $x = 0; - while ($x < $row['w']) - { - $img = $loadImageFile($path . '/' . $row['textureString'] . $i); - if (!$img) - { - CLI::write(' - complexImg: tile '.$path.'/'.$row['textureString'].$i.'.blp missing.', CLI::LOG_ERROR); - break 2; - } - - imagecopy($overlay, $img, $row['x'] + $x, $row['y'] + $y, 0, 0, imagesx($img), imagesy($img)); - - // prepare subzone image - if ($modeMask & 0x10) - { - if (!isset($row['maskimage'])) - { - $row['maskimage'] = $createAlphaImage($row['w'], $row['h']); - $row['maskcolor'] = imagecolorallocatealpha($row['maskimage'], 255, 64, 192, 64); - } - - for ($my = 0; $my < imagesy($img); $my++) - for ($mx = 0; $mx < imagesx($img); $mx++) - if ((imagecolorat($img, $mx, $my) >> 24) < $threshold) - imagesetpixel($row['maskimage'], $x + $mx, $y + $my, $row['maskcolor']); - } - - imagedestroy($img); - - $x += 256; - $i++; - } - $y += 256; - } - } - - // create spawn-maps if wanted - if ($modeMask & 0x04) - $createSpawnMap($overlay, $zoneId); - } - - // check, if the current zone is multiLeveled - // if there are also files present without layer-suffix assume them as layer: 0 - $multiLeveled = false; - $multiLevel = 0; - do - { - if (!CLISetup::filesInPath('/'.$textureStr.'\/'.$textureStr.($multiLevel + 1).'_\d\.blp/i', true)) - break; - - $multiLevel++; - $multiLeveled = true; - } - while ($multiLevel < 18); // Karazhan has 17 frickin floors - - // check if we can create base map anyway - $file = $path.'/'.$textureStr.'1.blp'; - $hasBaseMap = CLISetup::fileExists($file); - - CLI::write(' - area has '.($multiLeveled ? $multiLevel . ' levels' : 'only base level')); - - $map = null; - for ($i = 0; $i <= $multiLevel; $i++) - { - ini_set('max_execution_time', 120); // max 120sec per image - - $file = $path.'/'.$textureStr; - - if (!$i && !$hasBaseMap) - continue; - - // if $multiLeveled also suffix -0 to baseMap if it exists - if ($i && $multiLeveled) - $file .= $i.'_'; - - $doSkip = 0x0; - $outFile = []; - - foreach ($mapDirs as $idx => $info) - { - $outFile[$idx] = $destDir . sprintf($info[0], strtolower(Util::$localeStrings[$l]).'/') . $zoneId; - - $floor = $i; - if ($zoneId == 4100) // ToCStratholme: map order fix - $floor += 1; - - if ($multiLeveled && !(isset($baseLevelFix[$zoneId]) && $i == $baseLevelFix[$zoneId])) - $outFile[$idx] .= '-'.$floor; - - if (!isset(FileGen::$cliOpts['force']) && file_exists($outFile[$idx].'.'.$info[1])) - { - CLI::write($progress.' - file '.$outFile[$idx].'.'.$info[1].' was already processed'); - $doSkip |= (1 << $idx); - } - } - - if ($doSkip == 0xF) - continue; - - $map = $assembleImage($file, $fmt, $mapWidth, $mapHeight); - if (!$map) - { - $success = false; - CLI::write(' - could not create image resource for map '.$zoneId.($multiLevel ? ' level '.$i : '')); - continue; - } - - if (!$multiLeveled) - { - imagecopymerge($map, $overlay, 0, 0, 0, 0, imagesx($overlay), imagesy($overlay), 100); - imagedestroy($overlay); - } - - // create map - if ($modeMask & 0x02) - { - foreach ($mapDirs as $idx => $info) - { - if ($doSkip & (1 << $idx)) - continue; - - if (!$writeImage($outFile[$idx], $info[1], $map, $info[2] ?: $mapWidth, $info[3] ?: $mapHeight, $progress)) - $success = false; - } - } - } - - // also create subzone-maps - if ($map && isset($wmo[$wmaId]) && $modeMask & 0x10) - { - foreach ($wmo[$wmaId] as &$row) - { - $doSkip = 0x0; - $outFile = []; - - foreach ($mapDirs as $idx => $info) - { - $outFile[$idx] = $destDir . sprintf($info[0], strtolower(Util::$localeStrings[$l]).'/') . $row['areaTableId']; - if (!isset(FileGen::$cliOpts['force']) && file_exists($outFile[$idx].'.'.$info[1])) - { - CLI::write($progress.' - file '.$outFile[$idx].'.'.$info[1].' was already processed'); - $doSkip |= (1 << $idx); - } - } - - if ($doSkip == 0xF) - continue; - - $subZone = imagecreatetruecolor($mapWidth, $mapHeight); - imagecopy($subZone, $map, 0, 0, 0, 0, imagesx($map), imagesy($map)); - imagecopy($subZone, $row['maskimage'], $row['x'], $row['y'], 0, 0, imagesx($row['maskimage']), imagesy($row['maskimage'])); - - foreach ($mapDirs as $idx => $info) - { - if ($doSkip & (1 << $idx)) - continue; - - if (!$writeImage($outFile[$idx], $info[1], $subZone, $info[2] ?: $mapWidth, $info[3] ?: $mapHeight, $progress)) - $success = false; - } - - imagedestroy($subZone); - } - } - - if ($map) - imagedestroy($map); - - // this takes a while; ping mysql just in case - DB::Aowow()->selectCell('SELECT 1'); - } - } - } - - /***********/ - /* Credits */ - /***********/ - - if ($modeMask & 0x08) // optional tidbits (not used by default) - { - if (CLISetup::writeDir($destDir.'Interface/Glues/Credits/')) - { - // tile ordering - $order = array( - 1 => array( - [1] - ), - 2 => array( - [1], - [2] - ), - 4 => array( - [1, 2], - [3, 4] - ), - 6 => array( - [1, 2, 3], - [4, 5, 6] - ), - 8 => array( - [1, 2, 3, 4], - [5, 6, 7, 8] - ) - ); - - $imgGroups = []; - $files = CLISetup::filesInPath('/'.str_replace('/', '\\/', $paths[0x8][2]).'/i', true); - foreach ($files as $f) - { - if (preg_match('/([^\/]+)(\d).blp/i', $f, $m)) - { - if ($m[1] && $m[2]) - { - if (!isset($imgGroups[$m[1]])) - $imgGroups[$m[1]] = $m[2]; - else if ($imgGroups[$m[1]] < $m[2]) - $imgGroups[$m[1]] = $m[2]; - } - } - } - - // errör-korrekt - $imgGroups['Desolace'] = 4; - $imgGroups['BloodElf_Female'] = 6; - - $total = count($imgGroups); - $sum = 0; - - CLI::write('Processing '.$total.' files from Glues/Credits/...'); - - foreach ($imgGroups as $file => $fmt) - { - ini_set('max_execution_time', 30); // max 30sec per image (loading takes the most time) - - $sum++; - $done = ' - '.str_pad($sum.'/'.$total, 8).str_pad('('.number_format($sum * 100 / $total, 2).'%)', 9); - $name = $destDir.'Interface/Glues/Credits/'.$file; - - if (!isset(FileGen::$cliOpts['force']) && file_exists($name.'.png')) - { - CLI::write($done.' - file '.$name.'.png was already processed'); - continue; - } - - if (!isset($order[$fmt])) - { - CLI::write(' - pattern for file '.$name.' not set. skipping', CLI::LOG_WARN); - continue; - } - - $im = $assembleImage($paths[0x8][2].'/'.$file, $order[$fmt], count($order[$fmt][0]) * 256, count($order[$fmt]) * 256); - if (!$im) - { - CLI::write(' - could not assemble file '.$name, CLI::LOG_ERROR); - continue; - } - - if (!$writeImage($name, 'png', $im, count($order[$fmt][0]) * 256, count($order[$fmt]) * 256, $done)) - $success = false; - } - - ini_set('max_execution_time', $runTime); - } - else - $success = false; - } - - return $success; - } - -?> diff --git a/setup/tools/filegen/demo.ss.php b/setup/tools/filegen/demo.ss.php new file mode 100644 index 00000000..30470685 --- /dev/null +++ b/setup/tools/filegen/demo.ss.php @@ -0,0 +1,24 @@ + [[], CLISetup::ARGV_PARAM, 'Fills powered tooltip demo page (static/widgets/power/demo.html) with site variables.'] + ); + + protected $fileTemplateSrc = ['demo.html.in']; + protected $fileTemplateDest = ['static/widgets/power/demo.html']; +}); + +?> diff --git a/setup/tools/filegen/enchants.func.php b/setup/tools/filegen/enchants.ss.php similarity index 63% rename from setup/tools/filegen/enchants.func.php rename to setup/tools/filegen/enchants.ss.php index 26dc7f00..bc6521fc 100644 --- a/setup/tools/filegen/enchants.func.php +++ b/setup/tools/filegen/enchants.ss.php @@ -1,5 +1,7 @@ [[], CLISetup::ARGV_PARAM, 'Compiles enchantment effects to file for the item comparison tool and profiler tool.'] + ); - function enchants() + protected $setupAfter = [['items', 'spell', 'itemenchantment', 'icons', 'source'], []]; + protected $requiredDirs = ['datasets/']; + protected $localized = true; + + public function generate() : bool { - // from g_item_slots: 13:"One-Hand", 26:"Ranged", 17:"Two-Hand", - $slotPointer = [13, 17, 26, 26, 13, 17, 17, 13, 17, null, 17, null, null, 13, null, 13, null, null, null, null, 17]; - $castItems = []; - $successs = true; - $enchantSpells = DB::Aowow()->select(' - SELECT - s.id AS ARRAY_KEY, - effect1MiscValue, - equippedItemClass, equippedItemInventoryTypeMask, equippedItemSubClassMask, - skillLine1, - IFNULL(i.name, "inv_misc_questionmark") AS iconString, - name_loc0, name_loc2, name_loc3, name_loc6, name_loc8 - FROM - ?_spell s - LEFT JOIN - ?_icons i ON i.id = s.iconId - WHERE - effect1Id = ?d AND - name_loc0 NOT LIKE "QA%"' - , 53); // enchantItemPermanent && !qualityAssurance + // from g_item_slots: 13:"One-Hand", 15:"Ranged", 17:"Two-Hand", + $slotPointer = [13, 17, 15, 15, 13, 17, 17, 13, 17, null, 17, null, null, 13, null, 13, null, null, null, null, 17]; - // check directory-structure - foreach (Util::$localeStrings as $dir) - if (!CLISetup::writeDir('datasets/'.$dir)) - $success = false; + $castItems = []; + $enchantSpells = DB::Aowow()->selectAssoc( + 'SELECT s.`id` AS ARRAY_KEY, + `effect1MiscValue`, + `equippedItemClass`, `equippedItemInventoryTypeMask`, `equippedItemSubClassMask`, + `skillLine1`, + 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` = %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_SQL_LIMIT_NONE)); + $enchantments = new EnchantmentList(array(['id', $enchIds])); if ($enchantments->error) { - CLI::write('Required table ?_itemenchantment seems to be empty! Leaving enchants()...', 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_SQL_LIMIT_NONE)); - - foreach (CLISetup::$localeIds as $lId) + $castItems = new ItemList(array(['spellId1', array_keys($enchantSpells)], ['src.typeId', null, '!'])); + if ($castItems->error) { - User::useLocale($lId); - Lang::load(Util::$localeStrings[$lId]); + CLI::write('[enchants] Required table ::items seems to be empty!', CLI::LOG_ERROR); + CLI::write(); + return false; + } + + foreach (CLISetup::$locales as $loc) + { + Lang::load($loc); $enchantsOut = []; foreach ($enchantSpells as $esId => $es) @@ -106,20 +111,20 @@ if (!CLI) $eId = $es['effect1MiscValue']; if (!$enchantments->getEntry($eId)) { - CLI::write(' * could not find enchantment #'.$eId.' referenced by spell #'.$esId, CLI::LOG_WARN); + CLI::write('[enchants] * could not find enchantment #'.$eId.' referenced by spell #'.$esId, CLI::LOG_WARN); continue; } // slots have to be recalculated $slot = 0; - if ($es['equippedItemClass'] == 4) // armor + if ($es['equippedItemClass'] == ITEM_CLASS_ARMOR) { if ($invType = $es['equippedItemInventoryTypeMask']) $slot = $invType >> 1; else /* if (equippedItemSubClassMask == 64) */ // shields have it their own way <_< - $slot = (1 << (14 - 1)); + $slot = (1 << (INVTYPE_SHIELD - 1)); } - else if ($es['equippedItemClass'] == 2) // weapon + else if ($es['equippedItemClass'] == ITEM_CLASS_WEAPON) { foreach ($slotPointer as $i => $sp) { @@ -129,7 +134,7 @@ if (!CLI) if ((1 << $i) & $es['equippedItemSubClassMask']) { if ($sp == 13) // also mainHand & offHand *siiigh* - $slot |= ((1 << (21 - 1)) | (1 << (22 - 1))); + $slot |= ((1 << (INVTYPE_WEAPONMAINHAND - 1)) | (1 << (INVTYPE_WEAPONOFFHAND - 1))); $slot |= (1 << ($sp - 1)); } @@ -145,7 +150,7 @@ if (!CLI) 'skill' => -1, // modified if skill 'slots' => [], // determined per spell but set per item 'enchantment' => $enchantments->getField('name', true), - 'jsonequip' => $enchantments->getStatGain(), + 'jsonequip' => $enchantments->getStatGainForCurrent(), 'temp' => 0, // always 0 'classes' => 0, // modified by item 'gearscore' => 0 // set later @@ -174,10 +179,14 @@ if (!CLI) $cI = &$castItems; // this construct is a bit .. unwieldy foreach ($cI->iterate() as $__) { + if ($enchantments->getField('skillLevel')) + if ($s = Util::getEnchantmentScore(0, -1, true, $eId)) + $ench['gearscore'] = $s; + if ($cI->getField('spellId1') != $esId) continue; - $isScroll = substr($cI->getField('name_loc0'), 0, 17) == 'Scroll of Enchant'; + $isScroll = $cI->getField('class') == ITEM_CLASS_CONSUMABLE && $cI->getField('subClass') == ITEM_SUBCLASS_ITEM_ENHANCEMENT && $cI->getField('pickUpSoundId') == 1192; if ($s = Util::getEnchantmentScore($cI->getField('itemLevel'), $isScroll ? -1 : $cI->getField('quality'), !!$enchantments->getField('skillLevel'), $eId)) if ($s > $ench['gearscore']) @@ -194,10 +203,10 @@ if (!CLI) 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'])) @@ -246,13 +255,14 @@ if (!CLI) $ench[$k] = $v[0]; $toFile = "var g_enchants = ".Util::toJSON($enchantsOut).";"; - $file = 'datasets/'.User::$localeString.'/enchants'; + $file = 'datasets/'.$loc->json().'/enchants'; if (!CLISetup::writeFile($file, $toFile)) - $success = false; + $this->success = false; } - return $successs; + return $this->success; } +}); ?> diff --git a/setup/tools/filegen/gems.func.php b/setup/tools/filegen/gems.func.php deleted file mode 100644 index ad3fa090..00000000 --- a/setup/tools/filegen/gems.func.php +++ /dev/null @@ -1,102 +0,0 @@ -Select( - 'SELECT i.id AS itemId, - i.name_loc0, i.name_loc2, i.name_loc3, i.name_loc6, i.name_loc8, - IF (i.id < 36000 OR i.itemLevel < 70, 1 , 2) 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 - WHERE i.gemEnchantmentId <> 0 - ORDER BY i.id DESC'); - $success = true; - - // check directory-structure - foreach (Util::$localeStrings as $dir) - if (!CLISetup::writeDir('datasets/'.$dir)) - $success = false; - - $enchIds = []; - foreach ($gems as $pop) - $enchIds[] = $pop['enchId']; - - $enchantments = new EnchantmentList(array(['id', $enchIds], CFG_SQL_LIMIT_NONE)); - if ($enchantments->error) - { - CLI::write('Required table ?_itemenchantment seems to be empty! Leaving gems()...', CLI::LOG_ERROR); - CLI::write(); - return false; - } - - foreach (CLISetup::$localeIds as $lId) - { - set_time_limit(5); - - User::useLocale($lId); - Lang::load(Util::$localeStrings[$lId]); - - $gemsOut = []; - foreach ($gems as $pop) - { - if (!$enchantments->getEntry($pop['enchId'])) - { - CLI::write(' * could not find enchantment #'.$pop['enchId'].' referenced by item #'.$gem['itemId'], CLI::LOG_WARN); - continue; - } - - $gemsOut[$pop['itemId']] = array( - 'name' => Util::localizedString($pop, 'name'), - 'quality' => $pop['quality'], - 'icon' => strToLower($pop['icon']), - 'enchantment' => $enchantments->getField('name', true), - 'jsonequip' => $enchantments->getStatGain(), - 'colors' => $pop['colors'], - 'expansion' => $pop['expansion'], - 'gearscore' => Util::getGemScore($pop['itemLevel'], $pop['quality'], $pop['requiredSkill'] == 755, $pop['itemId']) - ); - } - - $toFile = "var g_gems = ".Util::toJSON($gemsOut).";"; - $file = 'datasets/'.User::$localeString.'/gems'; - - if (!CLISetup::writeFile($file, $toFile)) - $success = false; - } - - return $success; - } - -?> diff --git a/setup/tools/filegen/gems.ss.php b/setup/tools/filegen/gems.ss.php new file mode 100644 index 00000000..6ab27d67 --- /dev/null +++ b/setup/tools/filegen/gems.ss.php @@ -0,0 +1,102 @@ + [[], CLISetup::ARGV_PARAM, 'Compiles gems to file for the item comparison tool and profiler tool.'] + ); + + protected $setupAfter = [['items', 'itemenchantment', 'icons'], []]; + protected $requiredDirs = ['datasets/']; + protected $localized = true; + + public function generate() : bool + { + // sketchy, but should work + // id < 36'000 || ilevel < 70 ? BC : WOTLK + $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, %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` + WHERE i.`gemEnchantmentId` <> 0 + ORDER BY i.`id` DESC', + EXP_BC, EXP_WOTLK + ); + + $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(); + return false; + } + + foreach (CLISetup::$locales as $loc) + { + set_time_limit(5); + + Lang::load($loc); + + $gemsOut = []; + foreach ($gems as $g) + { + if (!$enchantments->getEntry($g['enchId'])) + { + CLI::write('[gems] * could not find enchantment #'.$g['enchId'].' referenced by item #'.$g['itemId'], CLI::LOG_WARN); + continue; + } + + $gemsOut[$g['itemId']] = array( + 'name' => Util::localizedString($g, 'name'), + 'quality' => $g['quality'], + 'icon' => strToLower($g['icon']), + 'enchantment' => $enchantments->getField('name', true), + 'jsonequip' => $enchantments->getStatGainForCurrent(), + 'colors' => $g['colors'], + 'expansion' => $g['expansion'], + 'gearscore' => Util::getGemScore($g['itemLevel'], $g['quality'], $g['requiredSkill'] == SKILL_JEWELCRAFTING, $g['itemId']) + ); + } + + $toFile = "var g_gems = ".Util::toJSON($gemsOut).";"; + $file = 'datasets/'.$loc->json().'/gems'; + + if (!CLISetup::writeFile($file, $toFile)) + $this->success = false; + } + + return $this->success; + } +}); + +?> diff --git a/setup/tools/filegen/global-js.ss.php b/setup/tools/filegen/global-js.ss.php new file mode 100644 index 00000000..28fd1d55 --- /dev/null +++ b/setup/tools/filegen/global-js.ss.php @@ -0,0 +1,80 @@ + [[], CLISetup::ARGV_PARAM, 'Compiles the global javascript file (static/js/global.js).'] + ); + + protected $fileTemplateDest = ['static/js/global.js']; + protected $fileTemplateSrc = ['global.js']; + + private bool $numFmt = false; + + private function locales() : string + { + $result = []; + + foreach (CLISetup::$locales as $loc) + $result[$loc->value] = array( + 'id' => '$LOCALE_' . strtoupper($loc->json()), + 'name' => $loc->json(), + 'domain' => $loc->domain(), + 'description' => $loc->title() + ); + + return Util::toJSON($result); + } +}); + +?> diff --git a/setup/tools/filegen/glyphs.func.php b/setup/tools/filegen/glyphs.func.php deleted file mode 100644 index f39914d2..00000000 --- a/setup/tools/filegen/glyphs.func.php +++ /dev/null @@ -1,91 +0,0 @@ -Select( - 'SELECT i.id AS itemId, - i.*, - IF (g.typeFlags & 0x1, 2, 1) AS type, - i.subclass AS classs, - i.requiredLevel AS level, - s1.id AS glyphSpell, - ic.name AS icon, - 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 = 16'); - - // check directory-structure - foreach (Util::$localeStrings as $dir) - if (!CLISetup::writeDir('datasets/'.$dir)) - $success = false; - - $glyphSpells = new SpellList(array(['s.id', array_keys($glyphList)], CFG_SQL_LIMIT_NONE)); - - foreach (CLISetup::$localeIds as $lId) - { - set_time_limit(30); - - User::useLocale($lId); - Lang::load(Util::$localeStrings[$lId]); - - $glyphsOut = []; - foreach ($glyphSpells->iterate() as $__) - { - $pop = $glyphList[$glyphSpells->id]; - - if (!$pop['glyphEffect']) - continue; - - if ($glyphSpells->getField('effect1Id') != 6 && $glyphSpells->getField('effect2Id') != 6 && $glyphSpells->getField('effect3Id') != 6) - continue; - - $glyphsOut[$pop['itemId']] = array( - 'name' => Util::localizedString($pop, 'name'), - 'description' => $glyphSpells->parseText()[0], - 'icon' => $pop['icon'], - 'type' => $pop['type'], - 'classs' => $pop['classs'], - 'skill' => $pop['skillId'], - 'level' => $pop['level'] - ); - } - - $toFile = "var g_glyphs = ".Util::toJSON($glyphsOut).";"; - $file = 'datasets/'.User::$localeString.'/glyphs'; - - if (!CLISetup::writeFile($file, $toFile)) - $success = false; - } - - return $success; - } -?> diff --git a/setup/tools/filegen/glyphs.ss.php b/setup/tools/filegen/glyphs.ss.php new file mode 100644 index 00000000..f746276a --- /dev/null +++ b/setup/tools/filegen/glyphs.ss.php @@ -0,0 +1,96 @@ + [[], CLISetup::ARGV_PARAM, 'Compiles glyphs to file for the talent calculator tool.'] + ); + + protected $setupAfter = [['items', 'spell', 'glyphproperties', 'icons'], []]; + protected $requiredDirs = ['datasets/']; + protected $localized = true; + + public function generate() : bool + { + $glyphList = DB::Aowow()->selectAssoc( + 'SELECT i.`id` AS "itemId", + i.*, + IF (g.`typeFlags` & 0x1, 2, 1) AS "type", + i.`subclass` AS "classs", + i.`requiredLevel` AS "level", + s1.`id` AS "glyphSpell", + ic.`name` AS "icon", + 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 = %i', + ITEM_CLASS_GLYPH); + + $glyphSpells = new SpellList(array(['s.id', array_keys($glyphList)])); + + foreach (CLISetup::$locales as $loc) + { + set_time_limit(30); + + Lang::load($loc); + + $glyphsOut = []; + foreach ($glyphSpells->iterate() as $__) + { + $pop = $glyphList[$glyphSpells->id]; + + if (!$pop['glyphEffect']) + continue; + + if ($glyphSpells->getField('effect1Id') != SPELL_EFFECT_APPLY_AURA && $glyphSpells->getField('effect2Id') != SPELL_EFFECT_APPLY_AURA && $glyphSpells->getField('effect3Id') != SPELL_EFFECT_APPLY_AURA) + continue; + + $glyphsOut[$pop['itemId']] = array( + 'name' => Util::localizedString($pop, 'name'), + 'description' => $glyphSpells->parseText()[0], + 'icon' => $pop['icon'], + 'type' => $pop['type'], + 'classs' => $pop['classs'], + 'skill' => $pop['skillId'], + 'level' => $pop['level'] + ); + } + + $toFile = "var g_glyphs = ".Util::toJSON($glyphsOut).";"; + $file = 'datasets/'.$loc->json().'/glyphs'; + + if (!CLISetup::writeFile($file, $toFile)) + $this->success = false; + } + + return $this->success; + } +}); + +?> diff --git a/setup/tools/filegen/img-artwork.ss.php b/setup/tools/filegen/img-artwork.ss.php new file mode 100644 index 00000000..ff011d0a --- /dev/null +++ b/setup/tools/filegen/img-artwork.ss.php @@ -0,0 +1,132 @@ + [[], CLISetup::ARGV_PARAM, 'Generate images from /glues/credits (not used on page)'], + ); + + public $isOptional = true; + + private const TILEORDER = array( + 1 => [ [1] ], + 2 => [ [1], + [2] ], + 4 => [ [1, 2], + [3, 4] ], + 6 => [ [1, 2, 3], + [4, 5, 6] ], + 8 => [ [1, 2, 3, 4], + [5, 6, 7, 8] ], + 9 => [ [1, 2, 3], + [4, 5, 6], + [7, 8, 9] ] + ); + + // src, resourcePath, localized, [tileOrder], [[dest, destW, destH]] + private $genSteps = array( + ['Glues/Credits/', null, false, self::TILEORDER, [['cache/Artworks/', 0, 0]]] + ); + + public function __construct() + { + $this->imgPath = CLISetup::$srcDir.$this->imgPath; + $this->maxExecTime = ini_get('max_execution_time'); + + foreach ($this->genSteps[0][self::GEN_IDX_DEST_INFO] as $dir) + $this->requiredDirs[] = $dir[0]; + } + + public function generate() : bool + { + if (!$this->checkSourceDirs()) + { + CLI::write('one or more source directories are missing:', CLI::LOG_ERROR); + $this->success = false; + return false; + } + + sleep(2); + + [, $realPath, , $tileOrder, $outInfo] = $this->genSteps[0]; + + $sum = 0; + $imgGroups = []; + $files = CLISetup::filesInPath('/'.str_replace('/', '\\/', $realPath).'/i', true); + $fileTpl = $outInfo[0][0].'%s.png'; + + foreach ($files as $f) + { + if (preg_match('/([^\/]+)(\d).blp/i', $f, $m)) + { + if (!$m[1] || !$m[2]) + continue; + + if (!isset($imgGroups[$m[1]])) + $imgGroups[$m[1]] = $m[2]; + else if ($imgGroups[$m[1]] < $m[2]) + $imgGroups[$m[1]] = $m[2]; + } + } + + // errör-korrekt + if (isset($imgGroups['Desolace'])) + $imgGroups['Desolace'] = 4; + + $total = count($imgGroups); + + CLI::write('Processing '.$total.' files from '.$realPath.' ...'); + + foreach ($imgGroups as $name => $fmt) + { + ini_set('max_execution_time', $this->maxExecTime); + + $sum++; + $this->status = ' - '.str_pad($sum.'/'.$total, 8).str_pad('('.number_format($sum * 100 / $total, 2).'%)', 9); + $file = sprintf($fileTpl, $name); + + if (!CLISetup::getOpt('force') && file_exists($file)) + { + CLI::write($this->status.' - file '.$file.' was already processed', CLI::LOG_BLANK, true, true); + continue; + } + + if (!isset($tileOrder[$fmt])) + { + CLI::write(' - pattern for file '.$name.' not set. skipping', CLI::LOG_WARN); + $this->success = false; + continue; + } + + $order = $tileOrder[$fmt]; + + $im = $this->assembleImage($realPath.'/'.$name, $order, count($order[0]) * 256, count($order) * 256); + if (!$im) + { + CLI::write(' - could not assemble file '.$name, CLI::LOG_ERROR); + $this->success = false; + continue; + } + + if (!$this->writeImageFile($im, $file, count($order[0]) * 256, count($order) * 256)) + $this->success = false; + } + + ini_set('max_execution_time', $this->maxExecTime); + + return $this->success; + } +}); + +?> diff --git a/setup/tools/filegen/img-maps.ss.php b/setup/tools/filegen/img-maps.ss.php new file mode 100644 index 00000000..380129cf --- /dev/null +++ b/setup/tools/filegen/img-maps.ss.php @@ -0,0 +1,866 @@ + [[ ], CLISetup::ARGV_PARAM, 'Generate zone and continental maps and the corresponding \'zones\' datasets.' ], +/* 1 */ 'spawnmaps' => [['1'], CLISetup::ARGV_OPTIONAL, 'Fallback to generate alpha masks for each zone to match creature and gameobject spawn points.'], +/* 2 */ 'subzones' => [['2'], CLISetup::ARGV_OPTIONAL, 'Generate additional area maps with highlighting for subzones (optional; skipped by default)' ], +/* 4 */ 'skip-zones' => [['3'], CLISetup::ARGV_OPTIONAL, 'Prevent default output of zone maps.' ] + ); + + protected $useGlobalStrings = true; + protected $dbcSourceFiles = ['worldmapoverlay', 'worldmaparea', 'dungeonmap']; + protected $requiredDirs = ['datasets/']; + + private const M_MAPS = (1 << 0); + private const M_SPAWNS = (1 << 1); + private const M_SUBZONES = (1 << 2); + + private $modeMask = self::M_SPAWNS | self::M_MAPS; + + private const SPAWNMAP_WH = 1000; // it is square + private const MAP_W = 1002; + private const MAP_H = 668; + private const A_THRESHOLD = 95; // alpha threshold to define subZones: set it too low and you have unspawnable areas inside a zone; set it too high and the border regions overlap + private const COLOR_WHITE = [255, 255, 255]; // rgb + private const COLOR_BLACK = [ 0, 0, 0]; // rgb + private const COLOR_SUBZONE = [ 0, 230, 255, 74]; // rgba - note: rgb is 0-255, a is 0-127 + + private const AREA_FLAG_DEFAULT_FLOOR_TERRAIN = 0x004; // Default Dungeon Floor is Terrain + private const AREA_FLAG_NO_DEFAULT_FLOOR = 0x100; // Don't use Default Dungeon Floor (typically 1) + + private const CONTINENTS = [0, 1, 530, 571]; // Map.dbc/id + + private const DEST_DIRS = array( + ['static/images/wow/maps/%snormal/', 488, 325], + ['static/images/wow/maps/%soriginal/', 0, 0], // 1002, 668 + ['static/images/wow/maps/%ssmall/', 224, 149], + ['static/images/wow/maps/%szoom/', 772, 515] + ); + + private const TILEORDER = array( + [1, 2, 3, 4], + [5, 6, 7, 8], + [9, 10, 11, 12] + ); + + private const MAP_FILE_PATTERN = '/((\w{4})\/interface\/worldmap(?:\/microdungeon\/([^\/]+))?\/([^\/]+)\/)(\4)(?:(\d{1,2})_)?(\d{1,2})\.(?:blp|png)/i'; + + // src, resourcePath, localized, [tileOrder], [[dest, destW, destH]] + private $genSteps = array( + self::M_MAPS => ['WorldMap/', null, true, self::TILEORDER, self::DEST_DIRS ], + self::M_SPAWNS => ['WorldMap/', null, true, self::TILEORDER, [['cache/alphaMaps/', 0, 0]]], + self::M_SUBZONES => ['WorldMap/', null, true, self::TILEORDER, self::DEST_DIRS ] + ); + + private $progress = 0; + private $wmOverlays = []; + private $dmFloorData = []; + private $wmAreas = []; + private $multiLevelZones = []; + private $mapFiles = []; // [nameINT][floorIdx][loc][tileIdx] => filePath + private $microDungeons = []; + + public function __construct() + { + $this->imgPath = CLISetup::$srcDir.$this->imgPath; + $this->maxExecTime = ini_get('max_execution_time'); + + // init directories + foreach ($this->genSteps as [, , , , $outInfo]) + { + foreach ($outInfo as $dir) + { + if (strpos($dir[0], '%s') === false) + $this->requiredDirs[] = $dir[0]; + else + foreach (CLISetup::$locales as $loc) + $this->requiredDirs[] = sprintf($dir[0], $loc->json().DIRECTORY_SEPARATOR); + } + } + } + + public function generate() : bool + { + // find out what to generate + $opts = array_slice(array_keys($this->info), 1); + $getO = CLISetup::getOpt(...$opts); + $mask = 0x0; + + if ($getO['spawnmaps']) + $mask |= self::M_SPAWNS; + if ($getO['subzones']) + $mask |= self::M_SUBZONES; + if (!$getO['skip-zones']) + $mask |= self::M_MAPS; + + // unless manually prompted drop spawnmap generation if 90% of spawns have core generated area info + $npcPct = DB::World()->selectCell('SELECT SUM(IF(`zoneId` > 0, 1, 0)) / COUNT(*) FROM creature') ?? 0; + $goPct = DB::World()->selectCell('SELECT SUM(IF(`zoneId` > 0, 1, 0)) / COUNT(*) FROM gameobject') ?? 0; + + if (!($mask & self::M_SPAWNS) && $npcPct > 0.9 && $goPct > 0.9) + $this->modeMask &= ~self::M_SPAWNS; + + $this->modeMask = $mask ?: $this->modeMask; + + if (!$this->modeMask) // why would you do this..? + return true; + + // removed unused genSteps + foreach ($this->genSteps as $idx => $_) + if (!($idx & $this->modeMask)) + unset($this->genSteps[$idx]); + + if (!$this->checkSourceDirs()) + { + CLI::write('[img-maps] One or more source directories are missing.', CLI::LOG_ERROR); + $this->success = false; + return false; + } + + sleep(2); + + if ($this->prepare()) + { + $this->buildMaps(); + $this->buildZonesFile(); + } + + return $this->success; + } + + private function buildMapsFUTURE() : void + { + $sumFloors = array_sum(array_column($this->dmFloorData, 1)); + $sumAreas = count($this->wmAreas); + $sumMaps = count(CLISetup::$locales) * ($sumAreas + $sumFloors); + + CLI::write('[img-maps] Processing '.$sumAreas.' zone maps and '.$sumFloors.' dungeon maps from Interface/WorldMap/ for locale: '.Lang::concat(CLISetup::$locales, callback: fn($x) => $x->name)); + + /* todo: retrain brain and generate maps by given files and GlobalStrings. Then assign dbc data to them not the other way round like it is now. + foreach ($this->mapFiles as $name => [$floors, $isMultilevel]) + { + // skip redundant data of a microDungeons + if (in_array($name, $this->microDungeons)) + continue; + + $this->wmAreas = $this->wmAreas[$name] ?? []; + if (!$this->wmAreas) + { + CLI::write('[img-maps] no WMA data for map file '.CLI::bold($name), CLI::LOG_WARN); + continue; + } + + $wmaId = $this->wmAreas['id']; + $zoneId = $this->wmAreas['areaId']; + $mapId = $this->wmAreas['mapId']; + $flags = $this->wmAreas['flags'] ?? 0; // flags added in 4.x + + if ($isMultilevel) + $this->multiLevelZones[$zoneId] = []; + + // TODO + // - Ahn'Kahet (4494) has a secondary map file, that is not referenced in DungeonMap.dbc but looks nice. Lets manually reference it. + // if (isset($floorData[4494])) + // $floorData[4494][1] = 2; + if ($zoneId == 206) + var_dump($floors); + + + foreach ($floors as $locId => [$floorData, $basePath]) + { + ksort($floorData); + + $resOverlay = null; + if (!$isMultilevel) + $resOverlay = $this->generateOverlay($wmaId, $name, $basePath); + + // create spawn-maps if wanted + if ($resOverlay && $this->modeMask & self::M_SPAWNS) + { + $outFile = $this->genSteps[self::M_SPAWNS][self::GEN_IDX_DEST_INFO][0][0] . $zoneId . '.png'; + if (!$this->buildSpawnMap($resOverlay, $outFile)) + $this->success = false; + } + + foreach ($floorData as $floorIdx => $tileData) + { + $outFile = $zoneId; + + // naming of the base floor file is a bit wonky. unsure when the -0 suffix should be implicit or explicit + // just note, that the floor names from GlobalStrings.lua always have a '0' as base level suffix + + if (!$floorIdx && $isMultilevel && !($flags & self::AREA_FLAG_DEFAULT_FLOOR_TERRAIN)) + CLI::write('[img-maps] zone '.$name.' is multilevel and has base level map file, but is not flagged for use', CLI::LOG_INFO); + + if ($isMultilevel && !$floorIdx) + { + if (in_array($mapId, self::CONTINENTS)) + $outFile .= '-0'; + else if ($this->wmAreas['defaultDungeonMapId'] < 0) + $outFile .= '-0'; + // else + // implicit -0 + } + else if ($isMultilevel) + $outFile .= '-'.$floorIdx; + + if ($isMultilevel) + $this->multiLevelZones[$zoneId][$floorIdx] = $outFile; + + + foreach ($tileData as $tileIdx => $filePath) + { + + } + } + } + + if ($isMultilevel) + $this->multiLevelZones[$zoneId] = array_values($this->multiLevelZones[$zoneId]); + + if ($this->modeMask & self::M_SUBZONES) + { + // get subzones for mapFile from wmaData and apply overlays + } + } + */ + + $progressLoc = -1; + foreach (CLISetup::$locales as $l => $loc) + { + $progressLoc++; + + // source for mapFiles + $mapSrcDir = ''; + if ($this->modeMask & self::M_SPAWNS) + $mapSrcDir = $this->genSteps[self::M_SPAWNS][1][$l] ?? ''; + if (!$mapSrcDir && $this->modeMask & self::M_SUBZONES) + $mapSrcDir = $this->genSteps[self::M_SUBZONES][1][$l] ?? ''; + if (!$mapSrcDir) + $mapSrcDir = $this->genSteps[self::M_MAPS][1][$l] ?? ''; + if (!$mapSrcDir) + { + $this->success = false; + CLI::write(' - no suitable localized map files found for locale '.$l, CLI::LOG_ERROR); + continue; + } + + foreach ($this->wmAreas as $progressArea => $areaEntry) + { + $curMap = $progressArea + count($this->wmAreas) * $progressLoc; + $this->status = ' - ' . str_pad($curMap.'/'.($sumMaps), 10) . str_pad('('.number_format($curMap * 100 / $sumMaps, 2).'%)', 9); + + $wmaId = $areaEntry['id']; + $zoneId = $areaEntry['areaId']; + $mapId = $areaEntry['mapId']; + $textureStr = $areaEntry['nameINT']; + $flags = $areaEntry['flags'] ?? 0; // flags added in 4.x + + [$floorStr, $nFloors] = $this->dmFloorData[in_array($mapId, self::CONTINENTS) ? -$wmaId : $mapId] ?? ['', 0]; + + if ($nFloors && !isset($this->multiLevelZones)) + $this->multiLevelZones[$zoneId] = []; + + CLI::write( + str_pad('['.$areaEntry['areaId'].']', 7) . + str_pad($areaEntry['nameINT'], 22) . + str_pad('Overlays: '.count($this->wmOverlays[$areaEntry['id']] ?? []), 14) . + str_pad('Dungeon Maps: '.($nFloors + ((($flags ?? 0) & self::AREA_FLAG_DEFAULT_FLOOR_TERRAIN) ? 1 : 0)), 18) + ); + + $srcPath = $mapSrcDir.DIRECTORY_SEPARATOR.$textureStr; + if (!CLISetup::fileExists($srcPath)) + { + $this->success = false; + CLI::write('worldmap file '.$srcPath.' missing for selected locale '.$loc->name, CLI::LOG_ERROR); + continue; + } + + $resOverlay = null; + + // zone has overlays (is in open world; is not multilevel) + if (isset($this->wmOverlays[$wmaId])) + { + $resOverlay = $this->generateOverlay($wmaId, $srcPath); + + // create spawn-maps if wanted + if ($this->modeMask & self::M_SPAWNS) + $this->buildSpawnMap($resOverlay, $zoneId); + } + + if (!($this->modeMask & self::M_MAPS)) + continue; + + // check, if the current zone is multiLeveled + $floors = [0]; + if ($floorStr) + $floors = array_merge($floors, explode(' ', $floorStr)); + + // - Ahn'Kahet (4494) has a secondary map file, that is not referenced in DungeonMap.dbc but looks nice. Lets manually reference it. + if ($zoneId == 4494) + $floors[] = 2; + + $resMap = null; + foreach ($floors as $floorIdx) + { + ini_set('max_execution_time', $this->maxExecTime); + + $file = $srcPath.DIRECTORY_SEPARATOR.$textureStr; + + // todo: Dalaran [4395] has no level 0 but is not skipped here + if (!$floorIdx && !($flags & self::AREA_FLAG_DEFAULT_FLOOR_TERRAIN) && !in_array($mapId, self::CONTINENTS)) + continue; + + if ($nFloors && ($floorIdx || $flags & self::AREA_FLAG_DEFAULT_FLOOR_TERRAIN)) + $this->multiLevelZones[$zoneId][$floorIdx] = $zoneId . '-' . $floorIdx; + + if ($floorIdx) + $file .= $floorIdx . '_'; + + $doSkip = 0x0; + $outFile = []; + + foreach (self::DEST_DIRS as $sizeIdx => [$path, $width, $height]) + { + $outFile[$sizeIdx] = sprintf($path, $loc->json().DIRECTORY_SEPARATOR) . $zoneId; + + /* dataset 'zones' requires that ... + * 3959 - Black Temple: starts with empty floor suffix + * 4075 - Sunwell: starts with empty floor suffix + * 4723 - Map 650 CoT: 5-man reuses raid map (649) but only the upper floor. Check DungeonMap.dbc + */ + + if ($nFloors && ($floorIdx || $flags & self::AREA_FLAG_DEFAULT_FLOOR_TERRAIN)) + $outFile[$sizeIdx] .= '-'.$floorIdx; + + $outFile[$sizeIdx] .= '.jpg'; + + if (!CLISetup::getOpt('force') && file_exists($outFile[$sizeIdx])) + { + CLI::write($this->status.' - file '.$outFile[$sizeIdx].' was already processed', CLI::LOG_BLANK, true, true); + $doSkip |= (1 << $sizeIdx); + } + } + + if ($doSkip == 0xF) + continue; + + $resMap = $this->assembleImage($file, self::TILEORDER, self::MAP_W, self::MAP_H); + if (!$resMap) + { + CLI::write(' - could not create image resource for zone '.$zoneId.($nFloors ? ' floor '.$floorIdx : ''), CLI::LOG_ERROR); + $this->success = false; + continue; + } + + if ($resOverlay && !$floorIdx) + { + imagecopymerge($resMap, $resOverlay, 0, 0, 0, 0, imagesx($resOverlay), imagesy($resOverlay), 100); + imagedestroy($resOverlay); + } + + // create map + if ($this->modeMask & self::M_MAPS) + { + foreach (self::DEST_DIRS as $sizeIdx => [, $width, $height]) + { + if ($doSkip & (1 << $sizeIdx)) + continue; + + if (!$this->writeImageFile($resMap, $outFile[$sizeIdx], $width ?: self::MAP_W, $height ?: self::MAP_H)) + $this->success = false; + } + } + } + + // also create subzone-maps + if ($resMap && isset($this->wmOverlays[$wmaId]) && $this->modeMask & self::M_SUBZONES) + $this->buildSubZones($resMap, $wmaId, $loc); + + if ($resMap) + imagedestroy($resMap); + + // this takes a while; ping mysql just in case + DB::Aowow()->selectCell('SELECT 1'); + } + } + } + + private function prepare() : bool + { + $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); + $this->success = false; + return false; + } + + // DM fixups... + // unpack + sort floors + array_walk($this->dmFloorData, function (&$x) { $x[0] = explode(' ', $x[0]); sort($x[0]); }); + + // move Dalaran from Howling Fjord to .. well .. Dalaran + $this->dmFloorData[-4395] = $this->dmFloorData[-495]; + unset($this->dmFloorData[-495]); + + // "custom" - show second level of Ahn'Kahet not shown but present in-game + $this->dmFloorData[619][0][] = 2; + $this->dmFloorData[619][1]++; + + // WMA fixups... + foreach ($this->wmAreas as &$a) + { + // flags added in 4.x but required for 3.3.5. Where are they? Derived from defaultDungeonMapId (also refered to as defaultDungeonFloor) being < 0 ? + // no idea, hardcode this shit + switch ($a['areaId']) + { // i deem the missing '-0' a mistake > v < this will not be perpetuated + case 4273: // Ulduar > base + 5 > 4273: ['4273-0', '4273-1', '4273-2', '4273-3', '4273-4', '4273-5'] + case 4075: // SunwellPlateau > base + 1 > 4075: ['4075', '4075-1'], + case 3959: // BlackTemple > base + 7 > 3959: ['3959', '3959-1', '3959-2', '3959-3', '3959-4', '3959-5', '3959-6', '3959-7'], + $a['flags'] = self::AREA_FLAG_DEFAULT_FLOOR_TERRAIN; + break; + case 4100: // CoTStratholme > base + 1 > 4100: ['4100-1', '4100-2'], + $a['flags'] = self::AREA_FLAG_DEFAULT_FLOOR_TERRAIN | self::AREA_FLAG_NO_DEFAULT_FLOOR; + break; + default: + $a['flags'] = $a['flags'] ?? 0; // flags added in 4.x + } + + if ($a['areaId']) + continue; + + switch ($a['id']) + { + case 13: $a['areaId'] = -6; break; // Kalimdor + case 14: $a['areaId'] = -3; break; // Eastern Kingdoms + case 466: $a['areaId'] = -2; break; // Outland + case 485: $a['areaId'] = -5; break; // Northrend + } + } + $this->wmAreas[-1] = ['id' => -1, 'areaId' => -1, 'flags' => 0x0, 'mapId' => 0, 'nameINT' => 'World']; + $this->wmAreas[-4] = ['id' => -4, 'areaId' => -4, 'flags' => 0x0, 'mapId' => 0, 'nameINT' => 'Cosmic']; + + ksort($this->wmAreas); // just so we can sift through the log more easily + + /* + i should be walking through interface/worldmap first and THEN check the worldmaparea / dungeonmap from the file pattern + floorIdx is optional and per map. (e.g. continents share their floors and yes continents can have dungeon maps) + + > /interface/worldmap//(_).blp + > /interface/worldmap/microdungeon///(_).blp + + microdungeons (5.x+?) may be redundant with regluar map files. + + e.g.: + > enGB/interface/worldmap/microdungeon/durotar/burningbladecoven/burningbladecoven8_12.blp + + from nameInt "durotar" we get wmaId = 4, areaTableId = 14 and mapId = 1 (floorIdx = 8 from file string) + with mapId and floor (and wmaId) we get the coordinates from dungeonmap.dbc + + thus the map file name is: -.png > 14-8.png + and the floor is named: DUNGEON_FLOOR_ > DUNGEON_FLOOR_DUROTAR8 (Aquelarre del Filo Ardiente) *nyak nyak nyak* + + + note: some map file may have no floorIdx but the tileIdx is still separated by an underscore. Those files should be ignored. + + */ + /* FUTURE + foreach (CLISetup::filesInPath(self::MAP_FILE_PATTERN, true) as $file) + { + if (!preg_match(self::MAP_FILE_PATTERN, $file, $m)) + continue; + + [, $basePath, $locStr, $mdParent, $nameINT, $nameINT, $floorIdx, $tileIdx] = $m; + + $loc = CLISetup::$expectedPaths[strtolower(substr($locStr, 0, 2)).strtoupper(substr($locStr, 2))] ?? LOCALE_EN; + + if ($mdParent) + $this->microDungeons[] = strtolower($nameINT); + + $key = strtolower($mdParent ?: $nameINT); + + $this->mapFiles[$key][0][$loc][0][$floorIdx ?: 0][$tileIdx] = $file; + $this->mapFiles[$key][0][$loc][1] = $basePath; + $this->mapFiles[$key][1] = ($this->mapFiles[$key][1] ?? false) ?: (($floorIdx ?: 0) > 1); + } + */ + + return true; + } + + private function buildMaps() : void + { + $sumFloors = array_sum(array_column($this->dmFloorData, 1)); + $sumAreas = count($this->wmAreas); + $sumMaps = count(CLISetup::$locales) * ($sumAreas + $sumFloors); + + CLI::write('[img-maps] Processing '.$sumAreas.' zone maps and '.$sumFloors.' dungeon maps from Interface/WorldMap/ for locale: '.Lang::concat(CLISetup::$locales, callback: fn($x) => CLI::bold($x->name))); + + foreach (CLISetup::$locales as $l => $loc) + { + // source for mapFiles + $mapSrcDir = ''; + if ($this->modeMask & self::M_SPAWNS) + $mapSrcDir = $this->genSteps[self::M_SPAWNS][1][$l] ?? ''; + if (!$mapSrcDir && $this->modeMask & self::M_SUBZONES) + $mapSrcDir = $this->genSteps[self::M_SUBZONES][1][$l] ?? ''; + if (!$mapSrcDir) + $mapSrcDir = $this->genSteps[self::M_MAPS][1][$l] ?? ''; + if (!$mapSrcDir) + { + CLI::write('[img-maps] - No suitable localized map files found for locale '.CLI::bold($loc->name).'.', CLI::LOG_ERROR); + $this->success = false; + continue; + } + + foreach ($this->wmAreas as $areaEntry) + { + $resOverlay = null; + $resMap = null; + + $wmaId = $areaEntry['id']; + $zoneId = $areaEntry['areaId']; + $mapId = $areaEntry['mapId']; + $textureStr = $areaEntry['nameINT']; + $flags = $areaEntry['flags']; + + [$dmFloors, $nFloors] = $this->dmFloorData[in_array($mapId, self::CONTINENTS) ? -$zoneId : $mapId] ?? [[0], 0]; + + $this->progress += ($nFloors ?: 1) + ($flags & self::AREA_FLAG_DEFAULT_FLOOR_TERRAIN ? 1 : 0); + $this->status = ' - ' . str_pad($this->progress.'/'.($sumMaps), 10) . str_pad('('.number_format($this->progress * 100 / $sumMaps, 2).'%)', 9); + + // includes base level... + if ($flags & self::AREA_FLAG_DEFAULT_FLOOR_TERRAIN) + { + array_unshift($dmFloors, 0); // 0 => 0, 1 => 1, etc. + $nFloors++; + + // .. which is not set in dbc 0 => 1, 1 => 2, etc. + if ($flags & self::AREA_FLAG_NO_DEFAULT_FLOOR) + $dmFloors = array_combine($dmFloors, array_map(fn($x) => ++$x, $dmFloors)); + } + else if ($dmFloors != [0]) // 1 => 1, 2 => 2, etc. + $dmFloors = array_combine($dmFloors, $dmFloors); + + CLI::write( + '['.$loc->json().'] ' . + str_pad('['.$areaEntry['areaId'].']', 7) . + str_pad($areaEntry['nameINT'], 22) . + str_pad('Overlays: '.count($this->wmOverlays[$areaEntry['id']] ?? []), 14) . + str_pad('Dungeon Maps: '.$nFloors, 18) + ); + + $srcPath = $mapSrcDir.DIRECTORY_SEPARATOR.$textureStr; + if (!CLISetup::fileExists($srcPath)) + { + CLI::write('[img-maps] - WorldMap file path '.$srcPath.' missing for selected locale '.CLI::bold($loc->name), CLI::LOG_ERROR); + $this->success = false; + continue; + } + + $srcPath .= DIRECTORY_SEPARATOR; + + // zone has overlays (is in open world; is not multilevel) + if (isset($this->wmOverlays[$wmaId]) && ($this->modeMask & (self::M_MAPS | self::M_SPAWNS | self::M_SUBZONES))) + { + $resOverlay = $this->generateOverlay($wmaId, $srcPath); + + // create spawn-maps if wanted + if ($resOverlay && ($this->modeMask & self::M_SPAWNS)) + $this->buildSpawnMap($resOverlay, $zoneId); + } + + // check if we can create base map anyway + $png = $srcPath.$textureStr.'1.png'; + $blp = $srcPath.$textureStr.'1.blp'; + $hasBaseMap = CLISetup::fileExists($blp) || CLISetup::fileExists($png); + + foreach ($dmFloors as $srcFloorIdx => $outFloorIdx) + { + ini_set('max_execution_time', $this->maxExecTime); + + $doSkip = 0x0; + $outPaths = []; + $srcFile = $srcPath.$textureStr; + $outFile = $zoneId; + + if (!$srcFloorIdx && !$hasBaseMap) + { + CLI::write('[img-maps] - Zone has no base floor, but is referenced with base floor in dmFloors.', CLI::LOG_WARN); + continue; + } + + if ($srcFloorIdx) + $srcFile .= $srcFloorIdx.'_'; + + if ($nFloors > 1) + if ($outFloorIdx || $flags & self::AREA_FLAG_DEFAULT_FLOOR_TERRAIN) + $outFile .= '-'.$outFloorIdx; + + if ($nFloors > 1) + $this->multiLevelZones[$zoneId][$outFile] = $outFile; + + if (!($this->modeMask & (self::M_MAPS | self::M_SUBZONES))) + continue; + + foreach (self::DEST_DIRS as $sizeIdx => [$path, $width, $height]) + { + $outPaths[$sizeIdx] = sprintf($path, strtolower($loc->json()).DIRECTORY_SEPARATOR) . $outFile . '.jpg'; + + if (!CLISetup::getOpt('force') && file_exists($outPaths[$sizeIdx])) + { + CLI::write($this->status.' - file '.$outPaths[$sizeIdx].' was already processed', CLI::LOG_BLANK, true, true); + $doSkip |= (1 << $sizeIdx); + } + } + + // can't skip map creation if we are to generate subzones later. although they may already exist and get skipped anyway *shrug* + if ($doSkip == 0xF && !($this->modeMask & self::M_SUBZONES)) + continue; + + $resMap = $this->assembleImage($srcFile, self::TILEORDER, self::MAP_W, self::MAP_H); + if (!$resMap) + { + CLI::write('[img-maps] - Could not create image resource for '.($nFloors ? 'floor '.$srcFloorIdx : 'base level'), CLI::LOG_ERROR); + $this->success = false; + continue; + } + + if ($resOverlay && !$nFloors) + { + imagecopymerge($resMap, $resOverlay, 0, 0, 0, 0, imagesx($resOverlay), imagesy($resOverlay), 100); + imagedestroy($resOverlay); + } + + // create map + if ($this->modeMask & self::M_MAPS) + { + foreach (self::DEST_DIRS as $sizeIdx => [, $width, $height]) + { + if ($doSkip & (1 << $sizeIdx)) + continue; + + if (!$this->writeImageFile($resMap, $outPaths[$sizeIdx], $width ?: self::MAP_W, $height ?: self::MAP_H)) + $this->success = false; + } + } + } + + // also create subzone-maps + if ($resMap && isset($this->wmOverlays[$wmaId]) && $this->modeMask & self::M_SUBZONES) + $this->buildSubZones($resMap, $wmaId, $loc); + + if ($resMap) + imagedestroy($resMap); + + // this takes a while; ping mysql just in case + DB::Aowow()->selectCell('SELECT 1'); + } + } + } + + private function buildZonesFile() : void + { + $areaNames = array_combine( + array_column($this->wmAreas, 'areaId'), + array_map(fn($x) => strtoupper($x), array_column($this->wmAreas, 'nameINT')) + ); + + if ($this->multiLevelZones) + { + ksort($this->multiLevelZones); + $this->multiLevelZones = array_map('array_values', $this->multiLevelZones); + } + else + { + CLI::write('[img-maps] No data fetched from either WorldMapArea.dbc or DungeonMap.dbc. Multilevel zones will not display.', CLI::LOG_ERROR); + $this->success = false; + } + + $zoneAreas = []; + // careful: nameINT may end in a number and have > 9 floors attached. see: KARAZHAN17, ULDUAR771 + foreach (CLISetup::searchGlobalStrings('/^DUNGEON_FLOOR_([a-z_]+(?:\d\d)?)(\d{1,2})\s=\s\"(.+)\";$/i') as $lId => [$_, $nameINT, $floor, $nameLOC]) + { + // yes, multiple zones can point to the same map files + if ($zoneIds = array_keys($areaNames, $nameINT)) + { + foreach ($zoneIds as $zId) + if (isset($this->multiLevelZones[$zId])) + $zoneAreas[$lId][$zId][$floor] = $nameLOC; + } + else + CLI::write('[img-maps] ['.$nameINT.'] from GlobalStrings.lua not found in WorldMapArea.dbc', CLI::LOG_WARN); + } + + foreach (CLISetup::$locales as $lId => $loc) + { + Lang::load($loc); + + // "custom" - show second level of Ahn'Kahet not shown but present in-game + if (isset($zoneAreas[$lId][4494])) + $zoneAreas[$lId][4494][2] = Lang::maps('floorN', [2]); + + foreach ($zoneAreas[$lId] as $zoneId => $floorData) + { + $nStrings = count($floorData); + $nFloors = count($this->multiLevelZones[$zoneId] ?? []); + if ($nStrings == $nFloors) + continue; + + // todo: just note for now, try to compensate later? + CLI::write('[img-maps] ['.$loc->json().'] '.str_pad('['.$zoneId.']', 7).'floor count mismatch between GlobalStrings: '.$nStrings.' and image files: '.$nFloors, CLI::LOG_WARN); + } + + ksort($zoneAreas[$lId]); + + $zoneAreas[$lId] = array_map('array_values', $zoneAreas[$lId]); + + // don't convert numbers to int in json + $toFile = "Mapper.multiLevelZones = ".Util::toJSON($this->multiLevelZones, 0x0).";\n\n"; + $toFile .= "var g_zone_areas = ".Util::toJSON($zoneAreas[$lId]).";"; + $file = 'datasets/'.$loc->json().'/zones'; + + if (!CLISetup::writeFile($file, $toFile)) + $this->success = false; + } + } + + private function buildSpawnMap(\GdImage $resOverlay, int $zoneId) : void + { + $outFile = $this->genSteps[self::M_SPAWNS][self::GEN_IDX_DEST_INFO][0][0] . $zoneId . '.png'; + + if (!CLISetup::getOpt('force') && file_exists($outFile)) + { + CLI::write($this->status.' - file '.$outFile.' was already processed', CLI::LOG_BLANK, true, true); + return; + } + + $tmp = imagecreate(self::SPAWNMAP_WH, self::SPAWNMAP_WH); + $cbg = imagecolorallocate($tmp, ...self::COLOR_WHITE); + $cfg = imagecolorallocate($tmp, ...self::COLOR_BLACK); + + for ($y = 0; $y < self::SPAWNMAP_WH; $y++) + { + for ($x = 0; $x < self::SPAWNMAP_WH; $x++) + { + $a = imagecolorat($resOverlay, ($x * self::MAP_W) / self::SPAWNMAP_WH, ($y * self::MAP_H) / self::SPAWNMAP_WH) >> 24; + imagesetpixel($tmp, $x, $y, $a < self::A_THRESHOLD ? $cfg : $cbg); + } + } + + imagecolordeallocate($tmp, $cbg); + imagecolordeallocate($tmp, $cfg); + + if (!$this->writeImageFile($tmp, $outFile, self::SPAWNMAP_WH, self::SPAWNMAP_WH)) + $this->success = false; + } + + private function buildSubZones(\GdImage $resMap, int $wmaId, Locale $loc) : void + { + foreach ($this->wmOverlays[$wmaId] as &$row) + { + $doSkip = 0x0; + $outFile = []; + + foreach (self::DEST_DIRS as $sizeIdx => [$path, , ]) + { + $outFile[$sizeIdx] = sprintf($path, $loc->json() . DIRECTORY_SEPARATOR) . $row['areaTableId'].'.jpg'; + if (!CLISetup::getOpt('force') && file_exists($outFile[$sizeIdx])) + { + CLI::write($this->status.' - file '.$outFile[$sizeIdx].' was already processed', CLI::LOG_BLANK, true, true); + $doSkip |= (1 << $sizeIdx); + } + } + + if ($doSkip == 0xF) + continue; + + $subZone = imagecreatetruecolor(self::MAP_W, self::MAP_H); + imagecopy($subZone, $resMap, 0, 0, 0, 0, imagesx($resMap), imagesy($resMap)); + imagecopy($subZone, $row['maskimage'], $row['x'], $row['y'], 0, 0, imagesx($row['maskimage']), imagesy($row['maskimage'])); + + foreach (self::DEST_DIRS as $sizeIdx => [, $width, $height]) + { + if ($doSkip & (1 << $sizeIdx)) + continue; + + if (!$this->writeImageFile($subZone, $outFile[$sizeIdx], $width ?: self::MAP_W, $height ?: self::MAP_H)) + $this->success = false; + } + + imagedestroy($subZone); + } + } + + private function generateOverlay(int $wmaId, string $basePath) : ?\GdImage + { + if (!isset($this->wmOverlays[$wmaId])) + return null; + + $resOverlay = $this->createAlphaImage(self::MAP_W, self::MAP_H); + + foreach ($this->wmOverlays[$wmaId] as &$row) + { + $i = 1; + $y = 0; + while ($y < $row['h']) + { + $x = 0; + while ($x < $row['w']) + { + $img = $this->loadImageFile($basePath . $row['textureString'] . $i, $noSrcFile); + if (!$img) + { + if ($noSrcFile) + CLI::write('[img-maps] - overlay tile ' . $basePath . $row['textureString'] . $i . '.blp missing.', CLI::LOG_ERROR); + + break 2; + } + + imagecopy($resOverlay, $img, $row['x'] + $x, $row['y'] + $y, 0, 0, imagesx($img), imagesy($img)); + + // prepare subzone image + if ($this->modeMask & self::M_SUBZONES) + { + if (!isset($row['maskimage'])) + { + $row['maskimage'] = $this->createAlphaImage($row['w'], $row['h']); + $row['maskcolor'] = imagecolorallocatealpha($row['maskimage'], ...self::COLOR_SUBZONE); + } + + for ($my = 0; $my < imagesy($img); $my++) + for ($mx = 0; $mx < imagesx($img); $mx++) + if ((imagecolorat($img, $mx, $my) >> 24) < self::A_THRESHOLD) + imagesetpixel($row['maskimage'], $x + $mx, $y + $my, $row['maskcolor']); + } + + imagedestroy($img); + + $x += 256; + $i++; + } + $y += 256; + } + } + + return $resOverlay; + } +}); + +?> diff --git a/setup/tools/filegen/img-talentcalc.ss.php b/setup/tools/filegen/img-talentcalc.ss.php new file mode 100644 index 00000000..c32221c5 --- /dev/null +++ b/setup/tools/filegen/img-talentcalc.ss.php @@ -0,0 +1,112 @@ + [[], CLISetup::ARGV_PARAM, 'Generate backgrounds for the talent calculator.'], + ); + + protected $dbcSourceFiles = ['talenttab', 'chrclasses']; + + private const DEST_DIRS = array( + ['static/images/wow/hunterpettalents/', 0, 0], + ['static/images/wow/talents/backgrounds/', 0, 0] + ); + + private const TILEORDER = array( + ['-TopLeft', '-TopRight'], + ['-BottomLeft', '-BottomRight'] + ); + + // src, resourcePath, localized, [tileOrder], [[dest, destW, destH]] + private $genSteps = array( + ['TalentFrame/', null, false, self::TILEORDER, self::DEST_DIRS] + ); + + public function __construct() + { + $this->imgPath = CLISetup::$srcDir.$this->imgPath; + $this->maxExecTime = ini_get('max_execution_time'); + + // init directories + foreach (self::DEST_DIRS as $dir) + $this->requiredDirs[] = $dir[0]; + } + + public function generate() : bool + { + if (!$this->checkSourceDirs()) + { + CLI::write('one or more source directories are missing:', CLI::LOG_ERROR); + $this->success = false; + return false; + } + + sleep(2); + + $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); + $this->success = false; + return false; + } + + $sum = 0; + $total = count($tTabs); + [, $realPath, , $tileOrder, $outInfo] = $this->genSteps[0]; + + CLI::write('Processing '.$total.' files from '.$realPath.' ...'); + foreach ($tTabs as $tt) + { + ini_set('max_execution_time', $this->maxExecTime); + $sum++; + $this->status = ' - '.str_pad($sum.'/'.$total, 8).str_pad('('.number_format($sum * 100 / $total, 2).'%)', 9); + + if ($tt['creatureFamilyMask']) // is PetCalc + { + $size = [244, 364]; + $outFile = sprintf($outInfo[0][0].'bg_%d.jpg', log($tt['creatureFamilyMask'], 2) + 1); + } + else + { + $size = [204, 554]; + $outFile = sprintf($outInfo[1][0].'%s_%d.jpg', strtolower($tt['fileString']), $tt['tabNumber'] + 1); + } + + if (!CLISetup::getOpt('force') && file_exists($outFile)) + { + CLI::write($this->status.' - file '.$outFile.' was already processed', CLI::LOG_BLANK, true, true); + continue; + } + + $im = $this->assembleImage($realPath.'/'.$tt['textureFile'], $tileOrder, 256 + 44, 256 + 75); + if (!$im) + { + CLI::write(' - could not assemble file '.$tt['textureFile'], CLI::LOG_ERROR); + $this->success = false; + continue; + } + + if (!$this->writeImageFile($im, $outFile, $size[0], $size[1])) + $this->success = false; + } + + ini_set('max_execution_time', $this->maxExecTime); + + return $this->success; + } +}); + +?> diff --git a/setup/tools/filegen/itemScaling.func.php b/setup/tools/filegen/itemScaling.func.php deleted file mode 100644 index cbca24f9..00000000 --- a/setup/tools/filegen/itemScaling.func.php +++ /dev/null @@ -1,115 +0,0 @@ - $row) - { - foreach ($row as &$r) - $r = str_pad($r, 5, " ", STR_PAD_LEFT); - - $buff[] = str_pad($id, 7, " ", STR_PAD_LEFT).": [".implode(', ', $row)."]"; - } - - return "{\r\n".implode(",\r\n", $buff)."\r\n}"; - } - - function itemScalingRB() - { - $ratings = array( - 12 => 1, // ITEM_MOD_DEFENSE_SKILL_RATING => CR_DEFENSE_SKILL - 13 => 2, // ITEM_MOD_DODGE_RATING => CR_DODGE - 14 => 3, // ITEM_MOD_PARRY_RATING => CR_PARRY - 15 => 4, // ITEM_MOD_BLOCK_RATING => CR_BLOCK - 16 => 5, // ITEM_MOD_HIT_MELEE_RATING => CR_HIT_MELEE - 17 => 6, // ITEM_MOD_HIT_RANGED_RATING => CR_HIT_RANGED - 18 => 7, // ITEM_MOD_HIT_SPELL_RATING => CR_HIT_SPELL - 19 => 8, // ITEM_MOD_CRIT_MELEE_RATING => CR_CRIT_MELEE - 20 => 9, // ITEM_MOD_CRIT_RANGED_RATING => CR_CRIT_RANGED - 21 => 10, // ITEM_MOD_CRIT_SPELL_RATING => CR_CRIT_SPELL - 22 => 11, // ITEM_MOD_HIT_TAKEN_MELEE_RATING => CR_HIT_TAKEN_MELEE - 23 => 12, // ITEM_MOD_HIT_TAKEN_RANGED_RATING => CR_HIT_TAKEN_RANGED - 24 => 13, // ITEM_MOD_HIT_TAKEN_SPELL_RATING => CR_HIT_TAKEN_SPELL - 25 => 14, // ITEM_MOD_CRIT_TAKEN_MELEE_RATING => CR_CRIT_TAKEN_MELEE [may be forced 0] - 26 => 15, // ITEM_MOD_CRIT_TAKEN_RANGED_RATING => CR_CRIT_TAKEN_RANGED [may be forced 0] - 27 => 16, // ITEM_MOD_CRIT_TAKEN_SPELL_RATING => CR_CRIT_TAKEN_SPELL [may be forced 0] - 28 => 17, // ITEM_MOD_HASTE_MELEE_RATING => CR_HASTE_MELEE - 29 => 18, // ITEM_MOD_HASTE_RANGED_RATING => CR_HASTE_RANGED - 30 => 19, // ITEM_MOD_HASTE_SPELL_RATING => CR_HASTE_SPELL - 31 => 5, // ITEM_MOD_HIT_RATING => [backRef] - 32 => 8, // ITEM_MOD_CRIT_RATING => [backRef] - 33 => 11, // ITEM_MOD_HIT_TAKEN_RATING => [backRef] [may be forced 0] - 34 => 14, // ITEM_MOD_CRIT_TAKEN_RATING => [backRef] [may be forced 0] - 35 => 14, // ITEM_MOD_RESILIENCE_RATING => [backRef] - 36 => 17, // ITEM_MOD_HASTE_RATING => [backRef] - 37 => 23, // ITEM_MOD_EXPERTISE_RATING => CR_EXPERTISE - 44 => 24 // ITEM_MOD_ARMOR_PENETRATION_RATING => CR_ARMOR_PENETRATION - ); - - $data = $ratings; - - $offsets = array_map(function ($v) { // LookupEntry(cr*GT_MAX_LEVEL+level-1) - return $v * 100 + 60 - 1; - }, $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); - - $offsets = array_map(function ($v) { // LookupEntry((getClass()-1)*GT_MAX_RATING+cr+1) - return (CLASS_WARRIOR - 1) * 32 + $v + 1; - }, $ratings); - $mods = DB::Aowow()->selectCol('SELECT idx - 1 AS ARRAY_KEY, ratio FROM dbc_gtoctclasscombatratingscalar WHERE idx IN (?a)', $offsets); - - foreach ($data as $itemMod => &$val) - $val = CFG_DEBUG ? $base[$val].' / '.$mods[$val] : $base[$val] / $mods[$val]; - - if (!CFG_DEBUG) - return Util::toJSON($data); - - $buff = []; - foreach ($data as $k => $v) - $buff[] = $k.': '.$v; - - return "{\r\n ".implode(",\r\n ", $buff)."\r\n}"; - } - - function itemScalingSV() - { - /* so the javascript expects a slightly different structure, than the dbc provides .. f*** it - e.g. - dbc - 80: 97 97 56 41 210 395 878 570 120 156 86 112 108 220 343 131 73 140 280 527 1171 2093 - expected - 80: 97 97 56 131 41 210 395 878 1570 120 156 86 112 108 220 343 0 0 73 140 280 527 1171 2093 - */ - $fields = Util::$ssdMaskFields; - array_walk($fields, function(&$v, $k) { - $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'); - foreach ($data as &$d) - $d = array_values($d); // strip indizes - - return CFG_DEBUG ? debugify($data) : Util::toJSON($data); - } - - function itemScalingSD() - { - $data = DB::Aowow()->select('SELECT *, id AS ARRAY_KEY FROM dbc_scalingstatdistribution'); - foreach ($data as &$row) - { - $row = array_values($row); - array_splice($row, 0, 1); - } - - return CFG_DEBUG ? debugify($data) : Util::toJSON($data); - } - -?> diff --git a/setup/tools/filegen/itemscaling.ss.php b/setup/tools/filegen/itemscaling.ss.php new file mode 100644 index 00000000..3ddf0b3a --- /dev/null +++ b/setup/tools/filegen/itemscaling.ss.php @@ -0,0 +1,138 @@ + [[], CLISetup::ARGV_PARAM, 'Compiles item scaling data to file to make heirloom tooltips interactive.'] + ); + + protected $fileTemplateDest = ['datasets/item-scaling']; + protected $fileTemplateSrc = ['item-scaling.in']; + + protected $dbcSourceFiles = ['scalingstatdistribution', 'scalingstatvalues', 'gtoctclasscombatratingscalar', 'gtcombatratings']; + + private function debugify(array $data) : string + { + $buff = []; + foreach ($data as $id => $row) + { + foreach ($row as &$r) + $r = str_pad($r, 5, " ", STR_PAD_LEFT); + + $buff[] = str_pad($id, 7, " ", STR_PAD_LEFT).": [".implode(', ', $row)."]"; + } + + return "{\r\n".implode(",\r\n", $buff)."\r\n}"; + } + + private function itemScalingRB() : string + { + // data and format observed via wayback machine. Not entirely sure about the redunacy within the combat ratings though. + $ratings = array( + Stat::DEFENSE_RTG => CR_DEFENSE_SKILL, + Stat::DODGE_RTG => CR_DODGE, + Stat::PARRY_RTG => CR_PARRY, + Stat::BLOCK_RTG => CR_BLOCK, + Stat::MELEE_HIT_RTG => CR_HIT_MELEE, + Stat::RANGED_HIT_RTG => CR_HIT_RANGED, + Stat::SPELL_HIT_RTG => CR_HIT_SPELL, + Stat::MELEE_CRIT_RTG => CR_CRIT_MELEE, + Stat::RANGED_CRIT_RTG => CR_CRIT_RANGED, + Stat::SPELL_CRIT_RTG => CR_CRIT_SPELL, + Stat::MELEE_HIT_TAKEN_RTG => CR_HIT_TAKEN_MELEE, + Stat::RANGED_HIT_TAKEN_RTG => CR_HIT_TAKEN_RANGED, + Stat::SPELL_HIT_TAKEN_RTG => CR_HIT_TAKEN_SPELL, + Stat::MELEE_CRIT_TAKEN_RTG => CR_CRIT_TAKEN_MELEE, // may be forced 0 + Stat::RANGED_CRIT_TAKEN_RTG => CR_CRIT_TAKEN_RANGED, // may be forced 0 + Stat::SPELL_CRIT_TAKEN_RTG => CR_CRIT_TAKEN_SPELL, // may be forced 0 + Stat::MELEE_HASTE_RTG => CR_HASTE_MELEE, + Stat::RANGED_HASTE_RTG => CR_HASTE_RANGED, + Stat::SPELL_HASTE_RTG => CR_HASTE_SPELL, + Stat::HIT_RTG => CR_HIT_MELEE, + Stat::CRIT_RTG => CR_CRIT_MELEE, + Stat::HIT_TAKEN_RTG => CR_HIT_TAKEN_MELEE, // may be forced 0 + Stat::CRIT_TAKEN_RTG => CR_CRIT_TAKEN_MELEE, // may be forced 0 + Stat::RESILIENCE_RTG => CR_CRIT_TAKEN_MELEE, + Stat::HASTE_RTG => CR_HASTE_MELEE, + Stat::EXPERTISE_RTG => CR_EXPERTISE, + Stat::ARMOR_PENETRATION_RTG => CR_ARMOR_PENETRATION + ); + + $data = $ratings; + + $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 %in', $offsets); + + /* non-1 scaler in 3.3.5.12340 + | ratingId | classId | ratio | + | 17 | 2 | 1.3 | + | 17 | 6 | 1.3 | + | 17 | 7 | 1.3 | + | 17 | 11 | 1.3 | + | 24 | < all > | 1.1 | + */ + + $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 %in', $offsets); + + foreach ($data as &$val) + $val = Cfg::get('DEBUG') ? $base[$val].' / '.$mods[$val] : $base[$val] / $mods[$val]; + + if (!Cfg::get('DEBUG')) + return Util::toJSON($data); + + $buff = []; + foreach ($data as $k => $v) + $buff[] = $k.': '.$v; + + return "{\r\n ".implode(",\r\n ", $buff)."\r\n}"; + } + + private function itemScalingSV() : string + { + /* so the javascript expects a slightly different structure, than the dbc provides .. f*** it + e.g. + dbc - 80: 97 97 56 41 210 395 878 570 120 156 86 112 108 220 343 131 73 140 280 527 1171 2093 + expected - 80: 97 97 56 131 41 210 395 878 1570 120 156 86 112 108 220 343 0 0 73 140 280 527 1171 2093 + */ + $fields = Util::$ssdMaskFields; + array_walk($fields, function(&$v, $k) { + $v = $v ?: '0 AS idx'.$k; // NULL => 0 (plus some index so we can have 2x 0) + }); + + $data = DB::Aowow()->selectAssoc('SELECT id AS ARRAY_KEY, '.implode(', ', $fields).' FROM dbc_scalingstatvalues'); + foreach ($data as &$d) + $d = array_values($d); // strip indizes + + return Cfg::get('DEBUG') ? $this->debugify($data) : Util::toJSON($data); + } + + private function itemScalingSD() : string + { + $data = DB::Aowow()->selectAssoc('SELECT *, id AS ARRAY_KEY FROM dbc_scalingstatdistribution'); + foreach ($data as &$row) + { + $row = array_values($row); + array_splice($row, 0, 1); + } + + return Cfg::get('DEBUG') ? $this->debugify($data) : Util::toJSON($data); + } +}) + +?> diff --git a/setup/tools/filegen/itemsets.func.php b/setup/tools/filegen/itemsets.func.php deleted file mode 100644 index e73ff379..00000000 --- a/setup/tools/filegen/itemsets.func.php +++ /dev/null @@ -1,134 +0,0 @@ -Select('SELECT * FROM ?_itemset ORDER BY refSetId DESC'); - $jsonBonus = []; - - // check directory-structure - foreach (Util::$localeStrings as $dir) - if (!CLISetup::writeDir('datasets/'.$dir)) - $success = false; - - foreach (CLISetup::$localeIds as $lId) - { - User::useLocale($lId); - Lang::load(Util::$localeStrings[$lId]); - - $itemsetOut = []; - foreach ($setList as $set) - { - set_time_limit(15); - - $setOut = array( - 'id' => $set['id'], - 'name' => (7 - $set['quality']).Util::jsEscape(Util::localizedString($set, 'name')), - 'pieces' => [], - 'heroic' => !!$set['heroic'], // should be bool - 'maxlevel' => $set['maxLevel'], - 'minlevel' => $set['minLevel'], - 'type' => $set['type'], - 'setbonus' => [] - ); - - if ($set['classMask']) - { - $setOut['reqclass'] = $set['classMask']; - $setOut['classes'] = []; - - for ($i = 0; $i < 12; $i++) - if ($set['classMask'] & (1 << $i)) - $setOut['classes'][] = $i + 1; - } - - if ($set['contentGroup']) - $setOut['note'] = $set['contentGroup']; - - if ($set['id'] < 0) - $setOut['idbak'] = $set['refSetId']; - - for ($i = 1; $i < 11; $i++) - if ($set['item'.$i]) - $setOut['pieces'][] = $set['item'.$i]; - - for ($i = 1; $i < 9; $i++) - { - if (!$set['bonus'.$i] || !$set['spell'.$i]) - continue; - - // costy and locale-independant -> cache - if (!isset($jsonBonus[$set['spell'.$i]])) - $jsonBonus[$set['spell'.$i]] = (new SpellList(array(['s.id', (int)$set['spell'.$i]])))->getStatGain()[$set['spell'.$i]]; - - if (!isset($setOut['setbonus'][$set['bonus'.$i]])) - $setOut['setbonus'][$set['bonus'.$i]] = $jsonBonus[$set['spell'.$i]]; - else - foreach ($jsonBonus[$set['spell'.$i]] as $k => $v) - @$setOut['setbonus'][$set['bonus'.$i]][$k] += $v; - } - - foreach ($setOut['setbonus'] as $k => $v) - { - if (empty($v)) - unset($setOut['setbonus'][$k]); - else - { - foreach ($v as $sk => $sv) - { - if ($str = Game::$itemMods[$sk]) - { - $setOut['setbonus'][$k][$str] = $sv; - unset($setOut['setbonus'][$k][$sk]); - } - } - } - } - - if (empty($setOut['setbonus'])) - unset($setOut['setbonus']); - - $itemsetOut[$setOut['id']] = $setOut; - } - - $toFile = "var g_itemsets = ".Util::toJSON($itemsetOut).";"; - $file = 'datasets/'.User::$localeString.'/itemsets'; - - if (!CLISetup::writeFile($file, $toFile)) - $success = false; - } - - return $success; - } -?> diff --git a/setup/tools/filegen/itemsets.ss.php b/setup/tools/filegen/itemsets.ss.php new file mode 100644 index 00000000..3b367648 --- /dev/null +++ b/setup/tools/filegen/itemsets.ss.php @@ -0,0 +1,130 @@ + [[], CLISetup::ARGV_PARAM, 'Compiles available item sets used throughout the page to file.'] + ); + + protected $setupAfter = [['itemset', 'spell'], []]; + protected $requiredDirs = ['datasets/']; + protected $localized = true; + + public function generate() : bool + { + $setList = DB::Aowow()->selectAssoc('SELECT * FROM ::itemset ORDER BY `refSetId` DESC'); + $jsonBonus = []; + + foreach (CLISetup::$locales as $loc) + { + Lang::load($loc); + + $itemsetOut = []; + foreach ($setList as $set) + { + set_time_limit(15); + + $setOut = array( + 'id' => $set['id'], + 'idbak' => $set['refSetId'], + 'name' => (ITEM_QUALITY_HEIRLOOM - $set['quality']).Util::jsEscape(Util::localizedString($set, 'name')), + 'pieces' => [], + 'heroic' => !!$set['heroic'], // should be bool + 'maxlevel' => $set['maxLevel'], + 'minlevel' => $set['minLevel'], + 'type' => $set['type'], + 'setbonus' => [] + ); + + if ($set['classMask']) + { + $setOut['reqclass'] = $set['classMask']; + $setOut['classes'] = ChrClass::fromMask($set['classMask']); + } + + if ($set['contentGroup']) + $setOut['note'] = $set['contentGroup']; + + for ($i = 1; $i < 11; $i++) + if ($set['item'.$i]) + $setOut['pieces'][] = $set['item'.$i]; + + $_spells = []; + for ($i = 1; $i < 9; $i++) + { + if (!$set['bonus'.$i] || isset($jsonBonus[$set['spell'.$i]])) + continue; + + $_spells[] = $set['spell'.$i]; + } + + // costy and locale-independent -> cache + if ($_spells) + $jsonBonus += (new SpellList(array(['s.id', $_spells])))->getStatGain(); + + $setbonus = []; + for ($i = 1; $i < 9; $i++) + { + $itemQty = $set['bonus'.$i]; + $itemSpl = $set['spell'.$i]; + if (!$itemQty || !$itemSpl || !isset($jsonBonus[$itemSpl])) + continue; + + if ($x = $jsonBonus[$itemSpl]->toJson(Stat::FLAG_ITEM, false)) + { + if (!isset($setbonus[$itemQty])) + $setbonus[$itemQty] = []; + + Util::arraySumByKey($setbonus[$itemQty], $x); + } + } + + if ($setbonus) + $setOut['setbonus'] = $setbonus; + + $itemsetOut[$setOut['id']] = $setOut; + } + + $toFile = "var g_itemsets = ".Util::toJSON($itemsetOut).";"; + $file = 'datasets/'.$loc->json().'/itemsets'; + + if (!CLISetup::writeFile($file, $toFile)) + $this->success = false; + } + + return $this->success; + } +}); + +?> diff --git a/setup/tools/filegen/locales.func.php b/setup/tools/filegen/locales.func.php deleted file mode 100644 index 69a07859..00000000 --- a/setup/tools/filegen/locales.func.php +++ /dev/null @@ -1,56 +0,0 @@ - " 0: { // English\r\n" . - " id: LOCALE_ENUS,\r\n" . - " name: 'enus',\r\n" . - " domain: 'www',\r\n" . - " description: 'English'\r\n" . - " }", - LOCALE_FR => " 2: { // French\r\n" . - " id: LOCALE_FRFR,\r\n" . - " name: 'frfr',\r\n" . - " domain: 'fr',\r\n" . - " description: 'Fran' + String.fromCharCode(231) + 'ais'\r\n" . - " }", - LOCALE_DE => " 3: { // German\r\n" . - " id: LOCALE_DEDE,\r\n" . - " name: 'dede',\r\n" . - " domain: 'de',\r\n" . - " description: 'Deutsch'\r\n" . - " }", - LOCALE_ES => " 6: { // Spanish\r\n" . - " id: LOCALE_ESES,\r\n" . - " name: 'eses',\r\n" . - " domain: 'es',\r\n" . - " description: 'Espa' + String.fromCharCode(241) + 'ol'\r\n" . - " }", - LOCALE_RU => " 8: { // Russian\r\n" . - " id: LOCALE_RURU,\r\n" . - " name: 'ruru',\r\n" . - " domain: 'ru',\r\n" . - " description: String.fromCharCode(1056, 1091, 1089, 1089, 1082, 1080, 1081)\r\n" . - " }", - ); - - foreach (CLISetup::$localeIds as $l) - if (isset($available[$l])) - $result[] = $available[$l]; - - return implode(",\r\n", $result); - } - -?> diff --git a/setup/tools/filegen/pets.func.php b/setup/tools/filegen/pets.func.php deleted file mode 100644 index ad1712ec..00000000 --- a/setup/tools/filegen/pets.func.php +++ /dev/null @@ -1,97 +0,0 @@ -Select( - 'SELECT cr.id, - cr.name_loc0, cr.name_loc2, cr.name_loc3, cr.name_loc6, cr.name_loc8, - cr.minLevel, - cr.maxLevel, - ft.A, - ft.H, - cr.rank AS classification, - cr.family, - cr.displayId1 AS displayId, - cr.textureString AS skin, - LOWER(SUBSTRING_INDEX(cf.iconString, "\\\\", -1)) AS icon, - cf.petTalentType AS type - 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 & 0x2) = 0 - ORDER BY cr.id ASC'); - - // check directory-structure - foreach (Util::$localeStrings as $dir) - if (!CLISetup::writeDir('datasets/'.$dir)) - $success = false; - - foreach (CLISetup::$localeIds as $lId) - { - User::useLocale($lId); - Lang::load(Util::$localeStrings[$lId]); - - $petsOut = []; - foreach ($petList as $pet) - { - // get locations - // again: caching will save you time and nerves - if (!isset($locations[$pet['id']])) - $locations[$pet['id']] = DB::Aowow()->SelectCol('SELECT DISTINCT areaId FROM ?_spawns WHERE type = ?d AND typeId = ?d', TYPE_NPC, $pet['id']); - - $petsOut[$pet['id']] = array( - 'id' => $pet['id'], - 'name' => Util::localizedString($pet, 'name'), - 'minlevel' => $pet['minLevel'], - 'maxlevel' => $pet['maxLevel'], - 'location' => $locations[$pet['id']], - 'react' => [$pet['A'], $pet['H']], - 'classification' => $pet['classification'], - 'family' => $pet['family'], - 'displayId' => $pet['displayId'], - 'skin' => $pet['skin'], - 'icon' => $pet['icon'], - 'type' => $pet['type'] - ); - } - - $toFile = "var g_pets = ".Util::toJSON($petsOut).";"; - $file = 'datasets/'.User::$localeString.'/pets'; - - if (!CLISetup::writeFile($file, $toFile)) - $success = false; - } - - return $success; - } -?> diff --git a/setup/tools/filegen/pets.ss.php b/setup/tools/filegen/pets.ss.php new file mode 100644 index 00000000..d5f223e4 --- /dev/null +++ b/setup/tools/filegen/pets.ss.php @@ -0,0 +1,98 @@ + [[], CLISetup::ARGV_PARAM, 'Compiles tameable hunter pets to file for the talent calculator tool.'] + ); + + protected $dbcSourceFiles = ['creaturefamily']; + protected $setupAfter = [['creature', 'factions', 'spawns'], []]; + protected $requiredDirs = ['datasets/']; + protected $localized = true; + + public function generate() : bool + { + $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`, + ft.`A`, ft.`H`, + cr.`rank` AS "classification", + cr.`family`, + cr.`displayId1` AS "displayId", + cr.`textureString` AS "skin", + LOWER(SUBSTRING_INDEX(cf.`iconString`, "\\", -1)) AS "icon", + cf.`petTalentType` AS "type" + 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` & %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` = %i AND `typeId` IN %in GROUP BY `typeId`, `areaId`', Type::NPC, array_column($petList, 'id')); + + foreach (CLISetup::$locales as $loc) + { + Lang::load($loc); + + $petsOut = []; + foreach ($petList as $pet) + { + $petsOut[$pet['id']] = array( + 'id' => $pet['id'], + 'name' => Util::localizedString($pet, 'name'), + 'minlevel' => $pet['minLevel'], + 'maxlevel' => $pet['maxLevel'], + 'location' => $locations[$pet['id']] ?? [], + 'react' => [$pet['A'], $pet['H']], + 'classification' => $pet['classification'], + 'family' => $pet['family'], + 'displayId' => $pet['displayId'], + 'skin' => $pet['skin'], + 'icon' => $pet['icon'], + 'type' => $pet['type'] + ); + } + + $toFile = "var g_pets = ".Util::toJSON($petsOut).";"; + $file = 'datasets/'.$loc->json().'/pets'; + + if (!CLISetup::writeFile($file, $toFile)) + $this->success = false; + } + + return $this->success; + } +}); + +?> diff --git a/setup/tools/filegen/profiler.func.php b/setup/tools/filegen/profiler.func.php deleted file mode 100644 index 171314a5..00000000 --- a/setup/tools/filegen/profiler.func.php +++ /dev/null @@ -1,392 +0,0 @@ - $type, 'typeId' => $typeId, 'groups' => $groups, 'comment' => $comment]; - else - { - $exclusions[$k]['groups'] |= $groups; - if ($comment) - $exclusions[$k]['comment'] .= '; '.$comment; - } - }; - - - /**********/ - /* Quests */ - /**********/ - $scripts[] = function() use ($exAdd) - { - $success = true; - $condition = [ - CFG_SQL_LIMIT_NONE, - 'AND', - [['cuFlags', CUSTOM_EXCLUDE_FOR_LISTVIEW], 0], - [['flags', QUEST_FLAG_DAILY | QUEST_FLAG_WEEKLY | QUEST_FLAG_REPEATABLE | QUEST_FLAG_AUTO_REWARDED, '&'], 0], - [['specialFlags', QUEST_FLAG_SPECIAL_REPEATABLE | QUEST_FLAG_SPECIAL_DUNGEON_FINDER | QUEST_FLAG_SPECIAL_MONTHLY, '&'], 0] - ]; - $questz = new QuestList($condition); - - // get quests for exclusion - foreach ($questz->iterate() as $id => $__) - { - switch ($questz->getField('reqSkillId')) - { - case 356: - $exAdd(TYPE_QUEST, $id, PR_EXCLUDE_GROUP_REQ_FISHING); - break; - case 202: - $exAdd(TYPE_QUEST, $id, PR_EXCLUDE_GROUP_REQ_ENGINEERING); - break; - case 197: - $exAdd(TYPE_QUEST, $id, PR_EXCLUDE_GROUP_REQ_TAILORING); - break; - } - } - - $_ = []; - $currencies = array_column($questz->rewards, TYPE_CURRENCY); - foreach ($currencies as $curr) - foreach ($curr as $cId => $qty) - $_[] = $cId; - - $relCurr = new CurrencyList(array(['id', $_])); - - foreach (CLISetup::$localeIds as $l) - { - set_time_limit(20); - - User::useLocale($l); - Lang::load(Util::$localeStrings[$l]); - - $buff = "var _ = g_gatheredcurrencies;\n"; - foreach ($relCurr->getListviewData() as $id => $data) - $buff .= '_['.$id.'] = '.Util::toJSON($data).";\n"; - - $buff .= "\n\nvar _ = g_quests;\n"; - foreach ($questz->getListviewData() as $id => $data) - $buff .= '_['.$id.'] = '.Util::toJSON($data).";\n"; - - $buff .= "\ng_quest_catorder = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10];\n"; - - if (!CLISetup::writeFile('datasets/'.User::$localeString.'/p-quests', $buff)) - $success = false; - } - - return $success; - }; - - /**********/ - /* Titles */ - /**********/ - $scripts[] = function() use ($exAdd) - { - $success = true; - $condition = array( - CFG_SQL_LIMIT_NONE, - [['cuFlags', CUSTOM_EXCLUDE_FOR_LISTVIEW, '&'], 0], - ); - $titlez = new TitleList($condition); - - // get titles for exclusion - foreach ($titlez->iterate() as $id => $__) - if (empty($titlez->sources[$id][4]) && empty($titlez->sources[$id][12])) - $exAdd(TYPE_TITLE, $id, PR_EXCLUDE_GROUP_UNAVAILABLE); - - foreach (CLISetup::$localeIds as $l) - { - set_time_limit(5); - - User::useLocale($l); - Lang::load(Util::$localeStrings[$l]); - - foreach ([0, 1] as $g) // gender - { - $buff = "var _ = g_titles;\n"; - foreach ($titlez->getListviewData() as $id => $data) - { - $data['name'] = Util::localizedString($titlez->getEntry($id), $g ? 'female' : 'male'); - unset($data['namefemale']); - $buff .= '_['.$id.'] = '.Util::toJSON($data).";\n"; - } - - if (!CLISetup::writeFile('datasets/'.User::$localeString.'/p-titles-'.$g, $buff)) - $success = false; - } - } - - return $success; - }; - - /**********/ - /* Mounts */ - /**********/ - $scripts[] = function() use ($exAdd) - { - $success = true; - $condition = array( - CFG_SQL_LIMIT_NONE, - [['cuFlags', CUSTOM_EXCLUDE_FOR_LISTVIEW, '&'], 0], - ['typeCat', -5] - ); - $mountz = new SpellList($condition); - - foreach (CLISetup::$localeIds as $l) - { - set_time_limit(5); - - User::useLocale($l); - Lang::load(Util::$localeStrings[$l]); - - $buff = "var _ = g_spells;\n"; - foreach ($mountz->getListviewData(ITEMINFO_MODEL) as $id => $data) - { - $data['quality'] = $data['name'][0]; - $data['name'] = mb_substr($data['name'], 1); - $buff .= '_['.$id.'] = '.Util::toJSON($data).";\n"; - } - - if (!CLISetup::writeFile('datasets/'.User::$localeString.'/p-mounts', $buff)) - $success = false; - } - - return $success; - }; - - /**************/ - /* Companions */ - /**************/ - $scripts[] = function() use ($exAdd) - { - $success = true; - $condition = array( - CFG_SQL_LIMIT_NONE, - [['cuFlags', CUSTOM_EXCLUDE_FOR_LISTVIEW, '&'], 0], - ['typeCat', -6] - ); - $companionz = new SpellList($condition); - - foreach (CLISetup::$localeIds as $l) - { - set_time_limit(5); - - User::useLocale($l); - Lang::load(Util::$localeStrings[$l]); - - $buff = "var _ = g_spells;\n"; - foreach ($companionz->getListviewData(ITEMINFO_MODEL) as $id => $data) - { - $data['quality'] = $data['name'][0]; - $data['name'] = mb_substr($data['name'], 1); - $buff .= '_['.$id.'] = '.Util::toJSON($data).";\n"; - } - - if (!CLISetup::writeFile('datasets/'.User::$localeString.'/p-companions', $buff)) - $success = false; - } - - return $success; - }; - - /************/ - /* Factions */ - /************/ - $scripts[] = function() use ($exAdd) - { - $success = true; - $condition = array( // todo (med): exclude non-gaining reputation-header - CFG_SQL_LIMIT_NONE, - [['cuFlags', CUSTOM_EXCLUDE_FOR_LISTVIEW, '&'], 0] - ); - $factionz = new FactionList($condition); - - foreach (CLISetup::$localeIds as $l) - { - set_time_limit(5); - - User::useLocale($l); - Lang::load(Util::$localeStrings[$l]); - - $buff = "var _ = g_factions;\n"; - foreach ($factionz->getListviewData() as $id => $data) - $buff .= '_['.$id.'] = '.Util::toJSON($data).";\n"; - - $buff .= "\ng_faction_order = [0, 469, 891, 1037, 1118, 67, 1052, 892, 936, 1117, 169, 980, 1097];\n"; - - if (!CLISetup::writeFile('datasets/'.User::$localeString.'/p-factions', $buff)) - $success = false; - } - - return $success; - }; - - /***********/ - /* Recipes */ - /***********/ - $scripts[] = function() use ($exAdd) - { - // special case: secondary skills are always requested, so put them in one single file (185, 129, 356); it also contains g_skill_order - $skills = [171, 164, 333, 202, 182, 773, 755, 165, 186, 393, 197, [185, 129, 356]]; - $success = true; - $baseCnd = array( - CFG_SQL_LIMIT_NONE, - [['cuFlags', CUSTOM_EXCLUDE_FOR_LISTVIEW, '&'], 0], - ['effect1Id', [6, 45, 57, 127, 33, 158, 99, 28, 95], '!'], // aura, tradeSkill, Tracking, Prospecting, Decipher, Milling, Disenchant, Summon (Engineering), Skinning - ['effect2Id', [118, 60], '!'], // not the skill itself - ['OR', ['typeCat', 9], ['typeCat', 11]] - ); - - foreach ($skills as $s) - { - $file = is_array($s) ? 'sec' : (string)$s; - $cnd = array_merge($baseCnd, [['skillLine1', $s]]); - $recipez = new SpellList($cnd); - $created = ''; - foreach ($recipez->iterate() as $__) - { - foreach ($recipez->canCreateItem() as $idx) - { - $id = $recipez->getField('effect'.$idx.'CreateItemId'); - $created .= "g_items.add(".$id.", {'icon':'".$recipez->relItems->getEntry($id)['iconString']."'});\n"; - } - } - - foreach (CLISetup::$localeIds as $l) - { - set_time_limit(10); - - User::useLocale($l); - Lang::load(Util::$localeStrings[$l]); - - $buff = ''; - foreach ($recipez->getListviewData() as $id => $data) - $buff .= '_['.$id.'] = '.Util::toJSON($data).";\n"; - - if (!$buff) - { - // this behaviour is intended, do not create an error - CLI::write('profiler - file datasets/'.User::$localeString.'/p-recipes-'.$file.' has no content => skipping', CLI::LOG_WARN); - continue; - } - - $buff = $created."\nvar _ = g_spells;\n".$buff; - - if (is_array($s)) - $buff .= "\ng_skill_order = [171, 164, 333, 202, 182, 773, 755, 165, 186, 393, 197, 185, 129, 356];\n"; - - if (!CLISetup::writeFile('datasets/'.User::$localeString.'/p-recipes-'.$file, $buff)) - $success = false; - } - } - - return $success; - }; - - /****************/ - /* Achievements */ - /****************/ - $scripts[] = function() use ($exAdd) - { - $success = true; - $condition = array( - CFG_SQL_LIMIT_NONE, - [['cuFlags', CUSTOM_EXCLUDE_FOR_LISTVIEW, '&'], 0], - [['flags', 1, '&'], 0], // no statistics - ); - $achievez = new AchievementList($condition); - - foreach (CLISetup::$localeIds as $l) - { - set_time_limit(5); - - User::useLocale($l); - Lang::load(Util::$localeStrings[$l]); - - $sumPoints = 0; - $buff = "var _ = g_achievements;\n"; - foreach ($achievez->getListviewData(ACHIEVEMENTINFO_PROFILE) as $id => $data) - { - $sumPoints += $data['points']; - $buff .= '_['.$id.'] = '.Util::toJSON($data).";\n"; - } - - // categories to sort by - $buff .= "\ng_achievement_catorder = [92, 14863, 97, 169, 170, 171, 172, 14802, 14804, 14803, 14801, 95, 161, 156, 165, 14806, 14921, 96, 201, 160, 14923, 14808, 14805, 14778, 14865, 14777, 14779, 155, 14862, 14861, 14864, 14866, 158, 162, 14780, 168, 14881, 187, 14901, 163, 14922, 159, 14941, 14961, 14962, 14981, 15003, 15002, 15001, 15041, 15042, 81]"; - // sum points - $buff .= "\ng_achievement_points = [".$sumPoints."];\n"; - - if (!CLISetup::writeFile('datasets/'.User::$localeString.'/achievements', $buff)) - $success = false; - } - - return $success; - }; - - /******************/ - /* Quick Excludes */ - /******************/ - $scripts[] = function() use (&$exclusions) - { - $s = count($exclusions); - $i = $n = 0; - CLI::write('applying '.$s.' baseline exclusions'); - DB::Aowow()->query('DELETE FROM ?_profiler_excludes WHERE comment = ""'); - foreach ($exclusions as $ex) - { - DB::Aowow()->query('REPLACE INTO ?_profiler_excludes (?#) VALUES (?a)', array_keys($ex), array_values($ex)); - if ($i >= 500) - { - $i = 0; - CLI::write(' * '.$n.' / '.$s.' ('.Lang::nf(100 * $n / $s, 1).'%)'); - } - $i++; - $n++; - } - - // excludes; type => [excludeGroupBit => [typeIds]] - $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, function ($x) use ($i) { return $x & (1 << $i); } ))) - $excludes[$type][$i + 1] = $ids; - - $buff = "g_excludes = ".Util::toJSON($excludes ?: (new Stdclass)).";\n"; - - return CLISetup::writeFile('datasets/quick-excludes', $buff); - }; - - // check directory-structure - foreach (Util::$localeStrings as $dir) - if (!CLISetup::writeDir('datasets/'.$dir)) - $success = false; - - // run scripts - foreach ($scripts as $func) - if (!$func()) - $success = false; - - return $success; - } - -?> diff --git a/setup/tools/filegen/profiler.ss.php b/setup/tools/filegen/profiler.ss.php new file mode 100644 index 00000000..835e229e --- /dev/null +++ b/setup/tools/filegen/profiler.ss.php @@ -0,0 +1,433 @@ + [[ ], CLISetup::ARGV_PARAM, 'Generates data dumps and completion exclusion filters for the profiler tool.'], + 'quests' => [['1'], CLISetup::ARGV_OPTIONAL, '...available quests by category'], + 'titles' => [['2'], CLISetup::ARGV_OPTIONAL, '...available titles by gender'], + 'mounts' => [['3'], CLISetup::ARGV_OPTIONAL, '...available mounts'], + 'companions' => [['4'], CLISetup::ARGV_OPTIONAL, '...available companions'], + 'factions' => [['5'], CLISetup::ARGV_OPTIONAL, '...available factions'], + 'recipes' => [['6'], CLISetup::ARGV_OPTIONAL, '...available recipes by skill'], + 'achievements' => [['7'], CLISetup::ARGV_OPTIONAL, '...available achievements'], + 'quickexcludes' => [['9'], CLISetup::ARGV_OPTIONAL, '...unobtainable items, mutually exclusive recipes, factions, etc.'], + ); + + protected $localized = true; + protected $requiredDirs = ['datasets/']; + protected $worldDependency = ['player_factionchange_spells', 'conditions']; + protected $setupAfter = [['quests', 'quests_startend', 'items', 'currencies', 'titles', 'spell', 'factions', 'achievement'], []]; + + private $spellFactions = []; + private $exclusions = []; + private $opts = []; + + public function generate() : bool + { + $anyOpt = array_keys($this->info); + $this->opts = CLISetup::getOpt(...$anyOpt); + + $this->opts = array_filter($this->opts); + if (!$this->opts) // none were set -> use default (all) + $this->opts = array_fill_keys(array_slice($anyOpt, 1), true); + + + $this->spellFactions = DB::World()->selectCol('SELECT `alliance_id` AS ARRAY_KEY, 1 FROM player_factionchange_spells UNION SELECT `horde_id` AS ARRAY_KEY, 2 FROM player_factionchange_spells'); + + + foreach ($this->opts as $fn => $_) + $this->$fn(); + + return $this->success; + } + + private function quests() : void + { + $questorder = []; + $questtotal = []; + $condition = [ + DB::AND, + [['cuFlags', CUSTOM_EXCLUDE_FOR_LISTVIEW | CUSTOM_UNAVAILABLE | CUSTOM_DISABLED, '&'], 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] + ]; + + foreach (Game::QUEST_CLASSES as $cat2 => $cat) + { + if ($cat2 < 0) + continue; + + $cond = array_merge($condition, [['questSortId', $cat]]); + $questz = new QuestList($cond); + if ($questz->error) + continue; + + $questorder[] = $cat2; + $questtotal[$cat2] = []; + + // get quests for exclusion + foreach ($questz->iterate() as $id => $__) + { + $this->sumTotal($questtotal[$cat2], $questz->getField('reqRaceMask') ?: -1, $questz->getField('reqClassMask') ?: -1); + if ($skillEx = $this->getExcludeForSkill($questz->getField('reqSkillId'))) + $this->addExclusion(Type::QUEST, $id, $skillEx); + } + + $_ = []; + $currencies = array_column($questz->rewards, Type::CURRENCY); + foreach ($currencies as $curr) + foreach ($curr as $cId => $qty) + $_[] = $cId; + + $relCurr = new CurrencyList(array(['id', $_])); + + foreach (CLISetup::$locales as $loc) + { + set_time_limit(20); + + Lang::load($loc); + + if (!$relCurr->error) + { + $buff = "var _ = g_gatheredcurrencies;\n"; + foreach ($relCurr->getListviewData() as $id => $data) + $buff .= '_['.$id.'] = '.Util::toJSON($data).";\n"; + } + + $buff .= "var _ = g_quests;\n"; + foreach ($questz->getListviewData() as $id => $data) + $buff .= '_['.$id.'] = '.Util::toJSON($data).";\n"; + + if (!CLISetup::writeFile('datasets/'.$loc->json().'/p-quests-'.$cat2, $buff)) + $this->success = false; + } + } + + $buff = "g_quest_catorder = ".Util::toJSON($questorder).";\n"; + $buff .= "g_quest_catorder_total = {};\n"; + foreach ($questtotal as $cat => $totals) + $buff .= "g_quest_catorder_total[".$cat."] = ".Util::toJSON($totals).";\n"; + + if (!CLISetup::writeFile('datasets/p-quests', $buff)) + $this->success = false; + } + + private function titles(): void + { + $titlez = new TitleList(array([['cuFlags', CUSTOM_EXCLUDE_FOR_LISTVIEW, '&'], 0])); + + // get titles for exclusion + foreach ($titlez->iterate() as $id => $__) + if (empty($titlez->sources[$id][SRC_QUEST]) && empty($titlez->sources[$id][SRC_ACHIEVEMENT])) + $this->addExclusion(Type::TITLE, $id, PR_EXCLUDE_GROUP_UNAVAILABLE); + + foreach (CLISetup::$locales as $loc) + { + set_time_limit(5); + + Lang::load($loc); + + foreach ([GENDER_MALE, GENDER_FEMALE] as $g) + { + $buff = "var _ = g_titles;\n"; + foreach ($titlez->getListviewData() as $id => $data) + { + $data['name'] = Util::localizedString($titlez->getEntry($id), $g ? 'female' : 'male'); + unset($data['namefemale']); + $buff .= '_['.$id.'] = '.Util::toJSON($data).";\n"; + } + + if (!CLISetup::writeFile('datasets/'.$loc->json().'/p-titles-'.$g, $buff)) + $this->success = false; + } + } + } + + private function mounts() : void + { + $condition = array( + [['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` = %i AND `ConditionTypeOrReference` = %i AND `SourceEntry` IN %in', Conditions::SRC_SPELL, Conditions::SKILL, $mountz->getFoundIDs()); + + // get mounts for exclusion + foreach ($conditionSet as $mount => $skill) + if ($skillEx = $this->getExcludeForSkill($skill)) + $this->addExclusion(Type::SPELL, $mount, $skillEx); + + foreach ($mountz->iterate() as $id => $_) + if (!$mountz->getSources()) + $this->addExclusion(Type::SPELL, $id, PR_EXCLUDE_GROUP_UNAVAILABLE); + + foreach (CLISetup::$locales as $loc) + { + set_time_limit(5); + + Lang::load($loc); + + $buff = "var _ = g_spells;\n"; + foreach ($mountz->getListviewData(ITEMINFO_MODEL) as $id => $data) + { + // two cases where the spell is unrestricted but the castitem has class restriction (too lazy to formulate ruleset) + if ($id == 66906) // Argent Charger + $data['reqclass'] = ChrClass::PALADIN->toMask(); + else if ($id == 54729) // Winged Steed of the Ebon Blade + $data['reqclass'] = ChrClass::DEATHKNIGHT->toMask(); + + rsort($data['skill']); // riding (777) expected at pos 0 + + $data['side'] = $this->spellFactions[$id] ?? SIDE_BOTH; + $data['quality'] = $data['name'][0]; + $data['name'] = mb_substr($data['name'], 1); + $buff .= '_['.$id.'] = '.Util::toJSON($data).";\n"; + } + + if (!CLISetup::writeFile('datasets/'.$loc->json().'/p-mounts', $buff)) + $this->success = false; + } + } + + private function companions() : void + { + $condition = array( + [['cuFlags', CUSTOM_EXCLUDE_FOR_LISTVIEW, '&'], 0], + ['typeCat', -6] + ); + $companionz = new SpellList($condition); + $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()) + $this->addExclusion(Type::SPELL, $id, PR_EXCLUDE_GROUP_UNAVAILABLE); + + foreach (CLISetup::$locales as $loc) + { + set_time_limit(5); + + Lang::load($loc); + + $buff = "var _ = g_spells;\n"; + foreach ($companionz->getListviewData(ITEMINFO_MODEL) as $id => $data) + { + if (!in_array($id, $legit)) + continue; + + $data['side'] = $this->spellFactions[$id] ?? SIDE_BOTH; + $data['quality'] = $data['name'][0]; + $data['name'] = mb_substr($data['name'], 1); + $buff .= '_['.$id.'] = '.Util::toJSON($data).";\n"; + } + + if (!CLISetup::writeFile('datasets/'.$loc->json().'/p-companions', $buff)) + $this->success = false; + } + } + + private function factions() : void + { + $factionz = new FactionList(array([['cuFlags', CUSTOM_EXCLUDE_FOR_LISTVIEW, '&'], 0])); + + foreach (CLISetup::$locales as $loc) + { + set_time_limit(5); + + Lang::load($loc); + + $buff = "var _ = g_factions;\n"; + foreach ($factionz->getListviewData() as $id => $data) + $buff .= '_['.$id.'] = '.Util::toJSON($data).";\n"; + + $buff .= "\ng_faction_order = [0, 469, 891, 1037, 1118, 67, 1052, 892, 936, 1117, 169, 980, 1097];\n"; + + if (!CLISetup::writeFile('datasets/'.$loc->json().'/p-factions', $buff)) + $this->success = false; + } + } + + private function recipes() : void + { + // special case: secondary skills are always requested, so put them in one single file (185, 129, 356); it also contains g_skill_order + $skills = array( + SKILL_ALCHEMY, SKILL_BLACKSMITHING, SKILL_ENCHANTING, SKILL_ENGINEERING, SKILL_HERBALISM, + SKILL_INSCRIPTION, SKILL_JEWELCRAFTING, SKILL_LEATHERWORKING, SKILL_MINING, SKILL_SKINNING, + SKILL_TAILORING, [SKILL_COOKING, SKILL_FIRST_AID, SKILL_FISHING] + ); + + $baseCnd = array( + [['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], '!'], + [DB::OR, ['typeCat', 9], ['typeCat', 11]] + ); + + foreach ($skills as $s) + { + $file = is_array($s) ? 'sec' : (string)$s; + $cnd = array_merge($baseCnd, [['skillLine1', $s]]); + $recipez = new SpellList($cnd); + $created = ''; + foreach ($recipez->iterate() as $id => $_) + { + if (!$recipez->getSources()) + $this->addExclusion(Type::SPELL, $id, PR_EXCLUDE_GROUP_UNAVAILABLE); + + foreach ($recipez->canCreateItem() as $idx) + { + $id = $recipez->getField('effect'.$idx.'CreateItemId'); + $created .= "g_items.add(".$id.", {'icon':'".$recipez->relItems->getEntry($id)['iconString']."'});\n"; + } + } + + foreach (CLISetup::$locales as $loc) + { + set_time_limit(10); + + Lang::load($loc); + + $buff = ''; + foreach ($recipez->getListviewData() as $id => $data) + { + $data['side'] = $this->spellFactions[$id] ?? SIDE_BOTH; + $buff .= '_['.$id.'] = '.Util::toJSON($data).";\n"; + } + + if (!$buff) + { + // this behaviour is intended, do not create an error + CLI::write('[profiler] - file datasets/'.$loc->json().'/p-recipes-'.$file.' has no content => skipping', CLI::LOG_INFO); + continue; + } + + $buff = $created."\nvar _ = g_spells;\n".$buff; + + if (is_array($s)) + $buff .= "\ng_skill_order = [171, 164, 333, 202, 182, 773, 755, 165, 186, 393, 197, 185, 129, 356];\n"; + + if (!CLISetup::writeFile('datasets/'.$loc->json().'/p-recipes-'.$file, $buff)) + $this->success = false; + } + } + } + + private function achievements() : void + { + $condition = array( + [['cuFlags', CUSTOM_EXCLUDE_FOR_LISTVIEW, '&'], 0], + [['flags', 1, '&'], 0], // no statistics + ); + $achievez = new AchievementList($condition); + + foreach (CLISetup::$locales as $loc) + { + set_time_limit(5); + + Lang::load($loc); + + $sumPoints = 0; + $buff = "var _ = g_achievements;\n"; + foreach ($achievez->getListviewData(ACHIEVEMENTINFO_PROFILE) as $id => $data) + { + 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 + $buff .= "\ng_achievement_catorder = [92, 14863, 97, 169, 170, 171, 172, 14802, 14804, 14803, 14801, 95, 161, 156, 165, 14806, 14921, 96, 201, 160, 14923, 14808, 14805, 14778, 14865, 14777, 14779, 155, 14862, 14861, 14864, 14866, 158, 162, 14780, 168, 14881, 187, 14901, 163, 14922, 159, 14941, 14961, 14962, 14981, 15003, 15002, 15001, 15041, 15042, 81]"; + // sum points + $buff .= "\ng_achievement_points = [".$sumPoints."];\n"; + + if (!CLISetup::writeFile('datasets/'.$loc->json().'/achievements', $buff)) + $this->success = false; + } + } + + private function quickexcludes() : void + { + set_time_limit(2); + + CLI::write('[profiler] applying '.count($this->exclusions).' baseline exclusions'); + DB::Aowow()->qry('DELETE FROM ::profiler_excludes WHERE `comment` = ""'); + + foreach ($this->exclusions as $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'); + 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)))) + $excludes[$type][$i + 1] = $ids; + + $buff = "g_excludes = ".Util::toJSON($excludes ?: (new \StdClass)).";\n"; + + if (!CLISetup::writeFile('datasets/quick-excludes', $buff)) + $this->success = false; + } + + private function getExcludeForSkill(int $skillId) : int + { + return match ($skillId) + { + SKILL_FISHING => PR_EXCLUDE_GROUP_REQ_FISHING, + SKILL_ENGINEERING => PR_EXCLUDE_GROUP_REQ_ENGINEERING, + SKILL_TAILORING => PR_EXCLUDE_GROUP_REQ_TAILORING, + default => 0 + }; + } + + private function addExclusion(int $type, int $typeId, int $groups, string $comment = '') : void + { + $k = $type.'-'.$typeId; + + if (!isset($this->exclusions[$k])) + $this->exclusions[$k] = ['type' => $type, 'typeId' => $typeId, 'groups' => $groups, 'comment' => $comment]; + else + { + $this->exclusions[$k]['groups'] |= $groups; + if ($comment) + $this->exclusions[$k]['comment'] .= '; '.$comment; + } + } + + private function sumTotal(array &$sumArr, int $raceMask = -1, int $classMask= -1) : void + { + foreach (ChrRace::cases() as $ra) + { + if (!$ra->matches($raceMask)) + continue; + + foreach (ChrClass::cases() as $cl) + { + if (!$cl->matches($classMask)) + continue; + + if (!isset($sumArr[$ra->value][$cl->value])) + $sumArr[$ra->value][$cl->value] = 1; + else + $sumArr[$ra->value][$cl->value]++; + } + } + } +}); + +?> diff --git a/setup/tools/filegen/realmMenu.func.php b/setup/tools/filegen/realmMenu.func.php deleted file mode 100644 index b415d992..00000000 --- a/setup/tools/filegen/realmMenu.func.php +++ /dev/null @@ -1,78 +0,0 @@ - diff --git a/setup/tools/filegen/realmmenu.ss.php b/setup/tools/filegen/realmmenu.ss.php new file mode 100644 index 00000000..ca4ae692 --- /dev/null +++ b/setup/tools/filegen/realmmenu.ss.php @@ -0,0 +1,89 @@ + [[], CLISetup::ARGV_PARAM, 'Generates \'profile_all.js\'-file that extends the profiler menus.'] + ); + + protected $fileTemplateSrc = ['profile_all.js.in']; + protected $fileTemplateDest = ['static/js/profile_all.js']; + protected $worldDependency = ['realmlist']; + + private function realmMenu() : string + { + $subs = []; + $set = 0x0; + $menu = [ + // skip usage of battlegroup + // ['us', Lang::profiler('regions', 'us'), null,[[Profiler::urlize(Cfg::get('BATTLEGROUP')), Cfg::get('BATTLEGROUP'), null, &$subUS]]], + // ['eu', Lang::profiler('regions', 'eu'), null,[[Profiler::urlize(Cfg::get('BATTLEGROUP')), Cfg::get('BATTLEGROUP'), null, &$subEU]]] + ]; + + foreach (Util::$regions as $idx => $n) + $subs[$idx] = []; + + if (!DB::isConnectable(DB_AUTH)) + CLI::write('[realmmenu] Auth DB not set up .. realm menu will be empty', CLI::LOG_WARN); + else + foreach (Profiler::getRealms() as $row) + { + $idx = array_search($row['region'], Util::$regions); + if ($idx === false) + continue; + + $set |= (1 << $idx); + $subs[$idx][] = [Profiler::urlize($row['name'], true), $row['name'], null, null, $row['access'] ? ['requiredAccess' => $row['access']] : null]; + } + + if (!$set) + CLI::write('[realmmenu] no viable realms found .. realm menu will be empty', CLI::LOG_WARN); + + // why is this file not localized!? + Lang::load(Locale::EN); + + foreach (Util::$regions as $idx => $n) + if ($set & (1 << $idx)) + $menu[] = [$n, Lang::profiler('regions', $n), null, &$subs[$idx]]; + + return Util::toJSON($menu); + } +}); + +?> diff --git a/setup/tools/filegen/realms.func.php b/setup/tools/filegen/realms.func.php deleted file mode 100644 index 44453f19..00000000 --- a/setup/tools/filegen/realms.func.php +++ /dev/null @@ -1,44 +0,0 @@ - 'www.wowarmory.com', - eu => 'eu.wowarmory.com' - }; - */ - - /* Examples - 1 => { - name:'Eldre\'Thalas', - battlegroup:'Reckoning', - region:'us' - }, - */ - - function realms() - { - $realms = Profiler::getRealms(); - if (!$realms) - CLI::write(' - realms: Auth-DB not set up .. static data g_realms will be empty', CLI::LOG_WARN); - // else - // foreach ($realms as &$r) - // $r['battlegroup'] = CFG_BATTLEGROUP; - - $toFile = "var g_realms = ".Util::toJSON($realms).";"; - $file = 'datasets/realms'; - - return CLISetup::writeFile($file, $toFile); - } - -?> diff --git a/setup/tools/filegen/realms.ss.php b/setup/tools/filegen/realms.ss.php new file mode 100644 index 00000000..f8ab9bde --- /dev/null +++ b/setup/tools/filegen/realms.ss.php @@ -0,0 +1,58 @@ + 'www.wowarmory.com', + eu => 'eu.wowarmory.com' + }; +*/ + +/* Examples + 1 => { + name:'Eldre\'Thalas', + battlegroup:'Reckoning', + region:'us' + }, +*/ + +CLISetup::registerSetup("build", new class extends SetupScript +{ + protected $info = array( + 'realms' => [[], CLISetup::ARGV_PARAM, 'Generates \'realms\'-file to be referenced by the profiler tool.'] + ); + + protected $worldDependency = ['realmlist']; + protected $requiredDirs = ['datasets/']; + + public function generate() : bool + { + $realms = []; + + if (!DB::isConnectable(DB_AUTH)) + CLI::write('[realms] Auth DB not set up .. static data g_realms will be empty', CLI::LOG_WARN); + else if (!($realms = Profiler::getRealms())) + CLI::write('[realms] no viable realms found .. static data g_realms will be empty', CLI::LOG_WARN); + // else + // foreach ($realms as &$r) + // $r['battlegroup'] = Cfg::get('BATTLEGROUP'); + + // remove access column + array_walk($realms, function (&$x) { unset($x['access']); }); + + $toFile = "var g_realms = ".Util::toJSON($realms).";"; + $file = 'datasets/realms'; + + return CLISetup::writeFile($file, $toFile); + } +}); + +?> 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 @@ + [[], 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/searchbox.ss.php b/setup/tools/filegen/searchbox.ss.php new file mode 100644 index 00000000..91d7c2ee --- /dev/null +++ b/setup/tools/filegen/searchbox.ss.php @@ -0,0 +1,24 @@ + [[], CLISetup::ARGV_PARAM, 'Fills search widget files (static/widgets/searchbox*) with site variables.'] + ); + + protected $fileTemplateSrc = ['searchbox.js.in', 'searchbox.html.in']; + protected $fileTemplateDest = ['static/widgets/searchbox.js', 'static/widgets/searchbox/searchbox.html']; +}); + +?> diff --git a/setup/tools/filegen/searchplugin.ss.php b/setup/tools/filegen/searchplugin.ss.php new file mode 100644 index 00000000..170c7557 --- /dev/null +++ b/setup/tools/filegen/searchplugin.ss.php @@ -0,0 +1,25 @@ + [[], CLISetup::ARGV_PARAM, 'Fills browser opensearch plugin (static/download/searchplugins/aowow.xml) with site variables.'] + ); + + protected $fileTemplateSrc = ['aowow.xml.in']; + protected $fileTemplateDest = ['static/download/searchplugins/aowow.xml']; + protected $requiredDirs = ['static/download/searchplugins/']; +}); + +?> diff --git a/setup/tools/filegen/simpleImg.func.php b/setup/tools/filegen/simpleImg.func.php deleted file mode 100644 index 9298bd7c..00000000 --- a/setup/tools/filegen/simpleImg.func.php +++ /dev/null @@ -1,484 +0,0 @@ - ['Icons/', $iconDirs, '/.*\.blp$', true, 0, null], - 1 => ['Spellbook/', [['Interface/Spellbook/', '.png', 0, 0, 0]], '/UI-Glyph-Rune-?\d+.blp$', true, 0, null], - 2 => ['PaperDoll/', array_slice($iconDirs, 0, 3), '/UI-(Backpack|PaperDoll)-.*\.blp$', true, 0, null], - 3 => ['GLUES/CHARACTERCREATE/UI-CharacterCreate-Races.blp', $iconDirs, '', true, 64, null], - 4 => ['GLUES/CHARACTERCREATE/UI-CharacterCreate-CLASSES.blp', $iconDirs, '', true, 64, null], - 5 => ['GLUES/CHARACTERCREATE/UI-CharacterCreate-Factions.blp', $iconDirs, '', true, 64, null], - // 6 => ['Minimap/OBJECTICONS.BLP', [['icons/tiny/', '.gif', 0, 16, 2]], '', true, 32, null], - 7 => ['FlavorImages/', [['Interface/FlavorImages/', '.png', 0, 0, 0]], '/.*\.blp$', false, 0, null], - 8 => ['Pictures/', [['Interface/Pictures/', '.png', 0, 0, 0]], '/.*\.blp$', false, 0, null], - 9 => ['PvPRankBadges/', [['Interface/PvPRankBadges/', '.png', 0, 0, 0]], '/.*\.blp$', false, 0, null], - 10 => ['Calendar/Holidays/', $calendarDirs, '/.*(start|[ayhs])\.blp$', true, 0, null], - 11 => ['GLUES/LOADINGSCREENS/', $loadScreenDirs, '/lo.*\.blp$', false, 0, null] - ); - // textures are composed of 64x64 icons - // numeric indexed arrays mimick the position on the texture - $cuNames = array( - 2 => array( - 'ui-paperdoll-slot-chest' => 'inventoryslot_chest', - 'ui-backpack-emptyslot' => 'inventoryslot_empty', - 'ui-paperdoll-slot-feet' => 'inventoryslot_feet', - 'ui-paperdoll-slot-finger' => 'inventoryslot_finger', - 'ui-paperdoll-slot-hands' => 'inventoryslot_hands', - 'ui-paperdoll-slot-head' => 'inventoryslot_head', - 'ui-paperdoll-slot-legs' => 'inventoryslot_legs', - 'ui-paperdoll-slot-mainhand' => 'inventoryslot_mainhand', - 'ui-paperdoll-slot-neck' => 'inventoryslot_neck', - 'ui-paperdoll-slot-secondaryhand' => 'inventoryslot_offhand', - 'ui-paperdoll-slot-ranged' => 'inventoryslot_ranged', - 'ui-paperdoll-slot-relic' => 'inventoryslot_relic', - 'ui-paperdoll-slot-shirt' => 'inventoryslot_shirt', - 'ui-paperdoll-slot-shoulder' => 'inventoryslot_shoulder', - 'ui-paperdoll-slot-tabard' => 'inventoryslot_tabard', - 'ui-paperdoll-slot-trinket' => 'inventoryslot_trinket', - 'ui-paperdoll-slot-waist' => 'inventoryslot_waist', - 'ui-paperdoll-slot-wrists' => 'inventoryslot_wrists' - ), - 3 => array( // uses nameINT from ChrRaces.dbc - ['race_human_male', 'race_dwarf_male', 'race_gnome_male', 'race_nightelf_male', 'race_draenei_male' ], - ['race_tauren_male', 'race_scourge_male', 'race_troll_male', 'race_orc_male', 'race_bloodelf_male' ], - ['race_human_female', 'race_dwarf_female', 'race_gnome_female', 'race_nightelf_female', 'race_draenei_female' ], - ['race_tauren_female', 'race_scourge_female', 'race_troll_female', 'race_orc_female', 'race_bloodelf_female'] - ), - 4 => array( // uses nameINT from ChrClasses.dbc - ['class_warrior', 'class_mage', 'class_rogue', 'class_druid' ], - ['class_hunter', 'class_shaman', 'class_priest', 'class_warlock'], - ['class_paladin', 'class_deathknight' ] - ), - 5 => array( - ['faction_alliance', 'faction_horde'] - ), - 6 => array( - [], - [null, 'quest_start', 'quest_end', 'quest_start_daily', 'quest_end_daily'] - ), - 10 => array( // really should have read holidays.dbc... - 'calendar_winterveilstart' => 'calendar_winterveilstart', - 'calendar_noblegardenstart' => 'calendar_noblegardenstart', - 'calendar_childrensweekstart' => 'calendar_childrensweekstart', - 'calendar_fishingextravaganza' => 'calendar_fishingextravaganzastart', - 'calendar_harvestfestivalstart' => 'calendar_harvestfestivalstart', - 'calendar_hallowsendstart' => 'calendar_hallowsendstart', - 'calendar_lunarfestivalstart' => 'calendar_lunarfestivalstart', - 'calendar_loveintheairstart' => 'calendar_loveintheairstart', - 'calendar_midsummerstart' => 'calendar_midsummerstart', - 'calendar_brewfeststart' => 'calendar_brewfeststart', - 'calendar_darkmoonfaireelwynnstart' => 'calendar_darkmoonfaireelwynnstart', - 'calendar_darkmoonfairemulgorestart' => 'calendar_darkmoonfairemulgorestart', - 'calendar_darkmoonfaireterokkarstart' => 'calendar_darkmoonfaireterokkarstart', - 'calendar_piratesday' => 'calendar_piratesdaystart', - 'calendar_wotlklaunch' => 'calendar_wotlklaunchstart', - 'calendar_dayofthedeadstart' => 'calendar_dayofthedeadstart', - 'calendar_fireworks' => 'calendar_fireworksstart' - ) - ); - - $writeImage = function($name, $ext, $src, $srcDims, $destDims, $done) - { - $ok = false; - $dest = imagecreatetruecolor($destDims['w'], $destDims['h']); - - imagesavealpha($dest, true); - if ($ext == '.png') - imagealphablending($dest, false); - - imagecopyresampled($dest, $src, $destDims['x'], $destDims['x'], $srcDims['x'], $srcDims['y'], $destDims['w'], $destDims['h'], $srcDims['w'], $srcDims['h']); - - switch ($ext) - { - case '.jpg': - $ok = imagejpeg($dest, $name.$ext, 85); - break; - case '.gif': - $ok = imagegif($dest, $name.$ext); - break; - case '.png': - $ok = imagepng($dest, $name.$ext); - break; - default: - CLI::write($done.' - unsupported file fromat: '.$ext, CLI::LOG_WARN); - } - - imagedestroy($dest); - - if ($ok) - { - chmod($name.$ext, Util::FILE_ACCESS); - CLI::write($done.' - image '.$name.$ext.' written', CLI::LOG_OK); - } - else - CLI::write($done.' - could not create image '.$name.$ext, CLI::LOG_ERROR); - - return $ok; - }; - - $checkSourceDirs = function($sub) use ($imgPath, &$paths) - { - $hasMissing = false; - foreach ($paths as $pathIdx => list($subDir, , , , , $realPath)) - { - if ($realPath) - continue; - - $p = sprintf($imgPath, $sub).$subDir; - if (CLISetup::fileExists($p)) - $paths[$pathIdx][5] = $p; - else - $hasMissing = true; - } - - return !$hasMissing; - }; - - if (isset(FileGen::$cliOpts['icons'])) - array_push($groups, 0, 2, 3, 4, 5, 10); - if (isset(FileGen::$cliOpts['glyphs'])) - $groups[] = 1; - if (isset(FileGen::$cliOpts['pagetexts'])) - array_push($groups, 7, 8, 9); - if (isset(FileGen::$cliOpts['loadingscreens'])) - $groups[] = 11; - - // filter by pasaed options - if (!$groups) // by default do not generate loadingscreens - unset($paths[11]); - else - foreach (array_keys($paths) as $k) - if (!in_array($k, $groups)) - unset($paths[$k]); - - foreach (CLISetup::$expectedPaths as $xp => $locId) - { - if (!in_array($locId, CLISetup::$localeIds)) - continue; - - if ($xp) // if in subDir add trailing slash - $xp .= '/'; - - if ($checkSourceDirs($xp)) - break; - } - - $locList = []; - foreach (CLISetup::$expectedPaths as $xp => $locId) - if (in_array($locId, CLISetup::$localeIds)) - $locList[] = $xp; - - CLI::write('required resources overview:', CLI::LOG_INFO); - foreach ($paths as list($path, , , , , $realPath)) - { - if ($realPath) - CLI::write(CLI::green(' FOUND ').' - '.str_pad($path, 53).' @ '.$realPath); - else - CLI::write(CLI::red('MISSING').' - '.str_pad($path, 53).' @ '.sprintf($imgPath, '['.implode(',', $locList).']/').$path); - } - - CLI::write(); - - // if no subdir had sufficient data, diaf - if (count(array_filter(array_column($paths, 5))) != count($paths)) - { - CLI::write('one or more required directories are missing:', CLI::LOG_ERROR); - foreach ($missing as $m) - CLI::write(' - '.$m, CLI::LOG_ERROR); - - return; - } - else - sleep(1); - - // init directories - foreach (array_column($paths, 1) as $subDirs) - foreach ($subDirs as $sd) - if (!CLISetup::writeDir($destDir.$sd[0])) - $success = false; - - // ok, departure from std::procedure here - // scan ItemDisplayInfo.dbc and SpellIcon.dbc for expected images and save them to an array - // load all icon paths into another array and xor these two - // excess entries for the directory are fine, excess entries for the dbc's are not - $dbcEntries = []; - - if (isset($paths[0]) || isset($paths[1])) // generates icons or glyphs - { - if (isset($paths[0]) && !isset($paths[1])) - $siRows = DB::Aowow()->selectCol('SELECT iconPath FROM dbc_spellicon WHERE iconPath NOT LIKE "%glyph-rune%"'); - else if (!isset($paths[0]) && isset($paths[1])) - $siRows = DB::Aowow()->selectCol('SELECT iconPath FROM dbc_spellicon WHERE iconPath LIKE "%glyph-rune%"'); - else - $siRows = DB::Aowow()->selectCol('SELECT iconPath FROM dbc_spellicon'); - - foreach ($siRows as $icon) - { - if (stristr($icon, $paths[0][0])) // Icons/ - $dbcEntries[] = strtolower($paths[0][5].substr(strrchr($icon, '\\'), 1)); - else if (stristr($icon, $paths[1][0])) // Spellbook/ - $dbcEntries[] = strtolower($paths[1][5].substr(strrchr($icon, '\\'), 1)); - } - } - - if (isset($paths[0])) - { - $itemIcons = DB::Aowow()->selectCol('SELECT inventoryIcon1 FROM dbc_itemdisplayinfo WHERE inventoryIcon1 <> ""'); - foreach ($itemIcons as $icon) - $dbcEntries[] = strtolower($paths[0][5].'/'.$icon.'.blp'); - - $eventIcons = DB::Aowow()->selectCol('SELECT textureString FROM dbc_holidays WHERE textureString <> ""'); - foreach ($eventIcons as $icon) - $dbcEntries[] = strtolower($paths[10][5].'/'.$icon.'Start.blp'); - } - - // case-insensitive array_unique *vomits silently into a corner* - $dbcEntries = array_intersect_key($dbcEntries, array_unique($dbcEntries)); - - $allPaths = []; - foreach ($paths as $i => list($inPath, $outInfo, $pattern, $isIcon, $tileSize, $path)) - { - $search = $path.$pattern; - if ($pattern) - $search = '/'.str_replace('/', '\\/', $search).'/i'; - - $files = CLISetup::filesInPath($search, !!$pattern); - $allPaths = array_merge($allPaths, $files); - - CLI::write('processing '.count($files).' files in '.$path.'...'); - - $j = 0; - foreach ($files as $f) - { - ini_set('max_execution_time', 30); // max 30sec per image (loading takes the most time) - - $src = null; - $img = explode('.', array_pop(explode('/', $f))); - array_pop($img); // there are a hand full of images with multiple file endings or random dots in the name - $img = implode('.', $img); - - // file not from dbc -> name from array or skip file - if (!empty($cuNames[$i])) - { - if (!empty($cuNames[$i][strtolower($img)])) - $img = $cuNames[$i][strtolower($img)]; - else if (!$tileSize) - { - $j += count($outInfo); - CLI::write('skipping extraneous file '.$img.' (+'.count($outInfo).')'); - continue; - } - } - - $nFiles = count($outInfo) * ($tileSize ? array_sum(array_map('count', $cuNames[$i])) : count($files)); - - foreach ($outInfo as list($dest, $ext, $srcSize, $destSize, $borderOffset)) - { - if ($tileSize) - { - foreach ($cuNames[$i] as $y => $row) - { - foreach ($row as $x => $name) - { - $j++; - $img = $isIcon ? strtolower($name) : $name; - $done = ' - '.str_pad($j.'/'.$nFiles, 12).str_pad('('.number_format($j * 100 / $nFiles, 2).'%)', 9); - - if (!isset(FileGen::$cliOpts['force']) && file_exists($destDir.$dest.$img.$ext)) - { - CLI::write($done.' - file '.$dest.$img.$ext.' was already processed'); - continue; - } - - if (!$src) - $src = $loadImageFile($f); - - if (!$src) // error should be created by imagecreatefromblp - continue; - - /* - ready for some major bullshitery? well, here it comes anyway! - the class-icon tile [idx: 4] isn't 64x64 but 63x64 .. the right side border is 1px short - so if we don't watch out, the icons start to shift over and show the borderi - also the icon border is displayced by 1px - */ - $from = array( - 'x' => $borderOffset + 1 + ($tileSize - ($i == 4 ? 1 : 0)) * $x, - 'y' => $borderOffset + 1 + $tileSize * $y, - 'w' => ($tileSize - ($i == 4 ? 1 : 0)) - $borderOffset * 2, - 'h' => $tileSize - $borderOffset * 2 - ); - $to = array( - 'x' => 0, - 'y' => 0, - 'w' => $destSize, - 'h' => $destSize - ); - - if (!$writeImage($destDir.$dest.$img, $ext, $src, $from, $to, $done)) - $success = false; - } - } - - // custom handle for combined icon 'quest_startend' - /* not used due to alphaChannel issues - if ($tileSize == 32) - { - $dest = imagecreatetruecolor(19, 16); - imagesavealpha($dest, true); - imagealphablending($dest, true); - - // excalmationmark, questionmark - imagecopyresampled($dest, $src, 0, 1, 32 + 5, 32 + 2, 8, 15, 18, 30); - imagecopyresampled($dest, $src, 5, 0, 64 + 1, 32 + 1, 10, 16, 18, 28); - - if (imagegif($dest, $destDir.$dest.'quest_startend.gif')) - CLI::write(' extra - image '.$destDir.$dest.'quest_startend.gif written', CLI::LOG_OK); - else - { - CLI::write(' extra - could not create image '.$destDir.$dest.'quest_startend.gif', CLI::LOG_ERROR); - $success = false; - } - - imagedestroy($dest); - } - */ - } - else - { - // icon -> lowercase - if ($isIcon) - $img = strtolower($img); - - $j++; - $done = ' - '.str_pad($j.'/'.$nFiles, 12).str_pad('('.number_format($j * 100 / $nFiles, 2).'%)', 9); - - if (!isset(FileGen::$cliOpts['force']) && file_exists($destDir.$dest.$img.$ext)) - { - CLI::write($done.' - file '.$dest.$img.$ext.' was already processed'); - continue; - } - - if (!$src) - $src = $loadImageFile($f); - - if (!$src) // error should be created by imagecreatefromblp - continue; - - $from = array( - 'x' => $borderOffset, - 'y' => $borderOffset, - 'w' => ($srcSize ?: imagesx($src)) - $borderOffset * 2, - 'h' => ($srcSize ?: imagesy($src)) - $borderOffset * 2 - ); - $to = array( - 'x' => 0, - 'y' => 0, - 'w' => $destSize ?: imagesx($src), - 'h' => $destSize ?: imagesy($src) - ); - - if (!$writeImage($destDir.$dest.$img, $ext, $src, $from, $to, $done)) - $success = false; - } - } - - unset($src); - } - } - - // reset execTime - ini_set('max_execution_time', FileGen::$defaultExecTime); - - if ($missing = array_diff(array_map('strtolower', $dbcEntries), array_map('strtolower', $allPaths))) - { - // hide affected icons from listviews - $iconNames = array_map(function($path) { - preg_match('/\/([^\/]+)\.blp$/i', $path, $m); - return $m ? $m[1] : null; - }, $missing); - - DB::Aowow()->query('UPDATE ?_icons SET cuFlags = cuFlags | ?d WHERE name IN (?a)', CUSTOM_EXCLUDE_FOR_LISTVIEW, $iconNames); - - asort($missing); - CLI::write('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) - CLI::write(' - '.$m); - } - - return $success; - } diff --git a/setup/tools/filegen/simpleimg.ss.php b/setup/tools/filegen/simpleimg.ss.php new file mode 100644 index 00000000..6e30aedb --- /dev/null +++ b/setup/tools/filegen/simpleimg.ss.php @@ -0,0 +1,391 @@ + [[ ], CLISetup::ARGV_PARAM, 'Converts and resizes BLP2 images smaller than 255x255 into required formats (mostly icons)'], + 'icons' => [['1'], CLISetup::ARGV_OPTIONAL, 'Generate icons for spells, items, classes, races, ect.'], + 'glyphs' => [['2'], CLISetup::ARGV_OPTIONAL, 'Generate decorative glyph symbols displayed on related item and spell pages.'], + 'pagetexts' => [['3'], CLISetup::ARGV_OPTIONAL, 'Generate images contained in text on readable items and gameobjects.'], + 'loadingscreens' => [['4'], CLISetup::ARGV_OPTIONAL, 'Generate loading screen images (not used on page; skipped by default)'] + ); + + protected $dbcSourceFiles = ['holidays', 'spellicon', 'itemdisplayinfo']; + protected $setupAfter = [['icons'], []]; + + private const ICON_DIRS = array( + ['static/images/wow/icons/large/', 'jpg', 0, ICON_SIZE_LARGE, 4], + ['static/images/wow/icons/medium/', 'jpg', 0, ICON_SIZE_MEDIUM, 4], + ['static/images/wow/icons/small/', 'jpg', 0, ICON_SIZE_SMALL, 4], + ['static/images/wow/icons/tiny/', 'gif', 0, ICON_SIZE_TINY, 4] + ); + + private $genSteps = array( + // srcPath, realPath, localized, [pattern, isIcon, tileSize], [[dest, ext, srcSize, destSize, borderOffset]] + 0 => ['Icons/', null, false, ['.*\.(blp|png)$', true, 0], self::ICON_DIRS, ], + 1 => ['Spellbook/', null, false, ['UI-Glyph-Rune-?\d+.(blp|png)$', false, 0], [['static/images/wow/Interface/Spellbook/', 'png', 0, 0, 0]]], + 2 => ['PaperDoll/', null, false, ['UI-(Backpack|PaperDoll)-.*\.(blp|png)$', true, 0], self::ICON_DIRS, ], + 3 => ['GLUES/CHARACTERCREATE/', null, false, ['UI-CharacterCreate-Races\.(blp|png)', true, 64], self::ICON_DIRS, ], + 4 => ['GLUES/CHARACTERCREATE/', null, false, ['UI-CharacterCreate-CLASSES\.(blp|png)', true, 64], self::ICON_DIRS, ], + 5 => ['GLUES/CHARACTERCREATE/', null, false, ['UI-CharacterCreate-Factions\.(blp|png)', true, 64], self::ICON_DIRS, ], + // 6 => ['Minimap/' , null, false, ['OBJECTICONS.(BLP|png)', true, 32], [['static/images/wow/icons/tiny/', 'gif', 0, 16, 2]]], + 7 => ['FlavorImages/', null, false, ['.*\.(blp|png)$', false, 0], [['static/images/wow/Interface/FlavorImages/', 'png', 0, 0, 0]]], + 8 => ['Pictures/', null, false, ['.*\.(blp|png)$', false, 0], [['static/images/wow/Interface/Pictures/', 'png', 0, 0, 0]]], + 9 => ['PvPRankBadges/', null, false, ['.*\.(blp|png)$', false, 0], [['static/images/wow/Interface/PvPRankBadges/', 'png', 0, 0, 0]]], + 10 => ['Calendar/Holidays/', null, false, ['.*(start|[ayhs])\.(blp|png)$', true, 0], self::ICON_DIRS, ], + 11 => ['GLUES/LOADINGSCREENS/', null, false, ['lo.*\.(blp|png)$', false, 0], [['cache/loadingscreens/', 'png', 0, 0, 0]]], + 12 => ['PVPFrame/', null, false, ['PVP-(ArenaPoints|Currency).*\.(blp|png)$', true, 0], self::ICON_DIRS, ] + ); + + // textures are composed of 64x64 icons + // numeric indexed arrays mimick the position on the texture + private $cuNames = array( + 2 => array( + 'ui-paperdoll-slot-chest' => 'inventoryslot_chest', + 'ui-backpack-emptyslot' => 'inventoryslot_empty', + 'ui-paperdoll-slot-feet' => 'inventoryslot_feet', + 'ui-paperdoll-slot-finger' => 'inventoryslot_finger', + 'ui-paperdoll-slot-hands' => 'inventoryslot_hands', + 'ui-paperdoll-slot-head' => 'inventoryslot_head', + 'ui-paperdoll-slot-legs' => 'inventoryslot_legs', + 'ui-paperdoll-slot-mainhand' => 'inventoryslot_mainhand', + 'ui-paperdoll-slot-neck' => 'inventoryslot_neck', + 'ui-paperdoll-slot-secondaryhand' => 'inventoryslot_offhand', + 'ui-paperdoll-slot-ranged' => 'inventoryslot_ranged', + 'ui-paperdoll-slot-relic' => 'inventoryslot_relic', + 'ui-paperdoll-slot-shirt' => 'inventoryslot_shirt', + 'ui-paperdoll-slot-shoulder' => 'inventoryslot_shoulder', + 'ui-paperdoll-slot-tabard' => 'inventoryslot_tabard', + 'ui-paperdoll-slot-trinket' => 'inventoryslot_trinket', + 'ui-paperdoll-slot-waist' => 'inventoryslot_waist', + 'ui-paperdoll-slot-wrists' => 'inventoryslot_wrists' + ), + 3 => array( // uses nameINT from ChrRaces.dbc + ['race_human_male', 'race_dwarf_male', 'race_gnome_male', 'race_nightelf_male', 'race_draenei_male' ], + ['race_tauren_male', 'race_scourge_male', 'race_troll_male', 'race_orc_male', 'race_bloodelf_male' ], + ['race_human_female', 'race_dwarf_female', 'race_gnome_female', 'race_nightelf_female', 'race_draenei_female' ], + ['race_tauren_female', 'race_scourge_female', 'race_troll_female', 'race_orc_female', 'race_bloodelf_female'] + ), + 4 => array( // uses nameINT from ChrClasses.dbc + ['class_warrior', 'class_mage', 'class_rogue', 'class_druid' ], + ['class_hunter', 'class_shaman', 'class_priest', 'class_warlock'], + ['class_paladin', 'class_deathknight' ] + ), + 5 => array( + ['faction_alliance', 'faction_horde'] + ), + 6 => array( + [], + [null, 'quest_start', 'quest_end', 'quest_start_daily', 'quest_end_daily'] + ), + 10 => array( // really should have read holidays.dbc... + 'calendar_winterveilstart' => 'calendar_winterveilstart', + 'calendar_noblegardenstart' => 'calendar_noblegardenstart', + 'calendar_childrensweekstart' => 'calendar_childrensweekstart', + 'calendar_fishingextravaganza' => 'calendar_fishingextravaganzastart', + 'calendar_harvestfestivalstart' => 'calendar_harvestfestivalstart', + 'calendar_hallowsendstart' => 'calendar_hallowsendstart', + 'calendar_lunarfestivalstart' => 'calendar_lunarfestivalstart', + 'calendar_loveintheairstart' => 'calendar_loveintheairstart', + 'calendar_midsummerstart' => 'calendar_midsummerstart', + 'calendar_brewfeststart' => 'calendar_brewfeststart', + 'calendar_darkmoonfaireelwynnstart' => 'calendar_darkmoonfaireelwynnstart', + 'calendar_darkmoonfairemulgorestart' => 'calendar_darkmoonfairemulgorestart', + 'calendar_darkmoonfaireterokkarstart' => 'calendar_darkmoonfaireterokkarstart', + 'calendar_piratesday' => 'calendar_piratesdaystart', + 'calendar_wotlklaunch' => 'calendar_wotlklaunchstart', + 'calendar_dayofthedeadstart' => 'calendar_dayofthedeadstart', + 'calendar_fireworks' => 'calendar_fireworksstart' + ) + ); + + public function __construct() + { + $this->imgPath = CLISetup::$srcDir.$this->imgPath; + $this->maxExecTime = ini_get('max_execution_time'); + + // init directories to be checked when registered + foreach (array_column($this->genSteps, self::GEN_IDX_DEST_INFO) as $subDirs) + foreach ($subDirs as $sd) + $this->requiredDirs[] = $sd[0]; + + // fix genSteps 2 [icons] - no tiny inventory backgrounds + $this->genSteps[2][self::GEN_IDX_DEST_INFO] = array_slice($this->genSteps[2][self::GEN_IDX_DEST_INFO], 0, 3); + + // fix genSteps 12 [pvp money icons] - smaller border offset for pvp currency icons + array_walk($this->genSteps[12][self::GEN_IDX_DEST_INFO], function(&$x) { $x[4] = 2; }); + + // fix genSteps 10 [holoday icons] - img src size is 90px + array_walk($this->genSteps[10][self::GEN_IDX_DEST_INFO], function(&$x) { $x[2] = 90; }); + } + + public function generate() : bool + { + // find out what to generate + $groups = []; + if (CLISetup::getOpt('icons')) + array_push($groups, 0, 2, 3, 4, 5, 10, 12); + if (CLISetup::getOpt('glyphs')) + $groups[] = 1; + if (CLISetup::getOpt('pagetexts')) + array_push($groups, 7, 8, 9); + if (CLISetup::getOpt('loadingscreens')) + $groups[] = 11; + + if (!$groups) // by default do not generate loadingscreens + $groups = [0, 1, 2, 3, 4, 5, 7, 8, 9, 10, 12]; + + // removed unused generators and reset realPaths (in case of retry from failed attempt) + foreach ($this->genSteps as $idx => $_) + { + if (!in_array($idx, $groups)) + unset($this->genSteps[$idx]); + else + $this->genSteps[$idx][self::GEN_IDX_SRC_REAL] = null; + } + + if (!$this->checkSourceDirs()) + { + CLI::write('[simpleimg] one or more required directories are missing:', CLI::LOG_ERROR); + return false; + } + + sleep(2); + + $allPaths = []; + foreach ($this->genSteps as $i => [, $path, , [$pattern, $isIcon, $tileSize], $outInfo]) + { + $search = CLI::nicePath('', $path); + if ($pattern) + $search = '/'.strtr($search, ['\\' => '\\\\', '/' => '\\/']).$pattern.'/i'; + + $files = CLISetup::filesInPath($search, !!$pattern); + $allPaths = array_merge($allPaths, array_map(function ($x) { return substr($x, 0, -4); }, $files)); + + if (!$files) + { + CLI::write('[simpleimg] source directory "'.CLI::bold($search).'" does not contain files matching "'.CLI::bold($pattern), CLI::LOG_ERROR); + $this->success = false; + continue; + } + + CLI::write('[simpleimg] processing '.count($files).' files in '.$path.'...'); + + $j = 0; + foreach ($files as $f) + { + ini_set('max_execution_time', $this->maxExecTime); + + $src = null; + $na = explode(DIRECTORY_SEPARATOR, $f); + $img = explode('.', array_pop($na)); + array_pop($img); // there are a hand full of images with multiple file endings or random dots in the name + $img = implode('.', $img); + + if (!empty($this->cuNames[$i])) // file not from dbc -> name from array or skip file + { + if (!empty($this->cuNames[$i][strtolower($img)])) + $img = $this->cuNames[$i][strtolower($img)]; + else if (!$tileSize) + { + $j += count($outInfo); + CLI::write('[simpleimg] skipping extraneous file '.$img.' (+'.count($outInfo).')'); + continue; + } + } + + $nFiles = count($outInfo) * ($tileSize ? array_sum(array_map('count', $this->cuNames[$i])) : count($files)); + + foreach ($outInfo as [$dest, $ext, $srcSize, $destSize, $borderOffset]) + { + if ($tileSize) + { + foreach ($this->cuNames[$i] as $y => $row) + { + foreach ($row as $x => $name) + { + $j++; + $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); + + if (!CLISetup::getOpt('force') && file_exists($outFile)) + { + CLI::write('[simpleimg] '.$this->status.' - file '.$outFile.' was already processed', CLI::LOG_BLANK, true, true); + continue; + } + + if (!$src) + $src = $this->loadImageFile($f, $noSrcFile); + + if (!$src) // error should be created by imagecreatefromblp + { + if (!$noSrcFile) // there are a couple of void file references in dbc, so this can't be a hard error. + $this->success = false; + + continue; + } + + /* + ready for some major bullshitery? well, here it comes anyway! + the class-icon tile [idx: 4] isn't 64x64 but 63x64 .. the right side border is 1px short + so if we don't watch out, the icons start to shift over and show the border + also the icon border is displaced by 1px + */ + $from = array( + 'x' => $borderOffset + 1 + ($tileSize - ($i == 4 ? 1 : 0)) * $x, + 'y' => $borderOffset + 1 + $tileSize * $y, + 'w' => ($tileSize - ($i == 4 ? 1 : 0)) - $borderOffset * 2, + 'h' => $tileSize - $borderOffset * 2 + ); + $to = array( + 'x' => 0, + 'y' => 0, + 'w' => $destSize, + 'h' => $destSize + ); + + if (!$this->writeImageFile($src, $outFile, $from, $to)) + $this->success = false; + } + } + + // custom handle for combined icon 'quest_startend' + /* not used due to alphaChannel issues + if ($tileSize == 32) + { + $dest = imagecreatetruecolor(19, 16); + imagesavealpha($dest, true); + imagealphablending($dest, true); + + // excalmationmark, questionmark + imagecopyresampled($dest, $src, 0, 1, 32 + 5, 32 + 2, 8, 15, 18, 30); + imagecopyresampled($dest, $src, 5, 0, 64 + 1, 32 + 1, 10, 16, 18, 28); + + if (imagegif($dest, $dest.'quest_startend.gif')) + CLI::write(' extra - image '.$dest.'quest_startend.gif written', CLI::LOG_OK); + else + { + CLI::write(' extra - could not create image '.$dest.'quest_startend.gif', CLI::LOG_ERROR); + $this->success = false; + } + + imagedestroy($dest); + } + */ + } + else + { + $j++; + $this->status = ' - '.str_pad($j.'/'.$nFiles, 12).str_pad('('.number_format($j * 100 / $nFiles, 2).'%)', 9); + $outFile = CLI::nicePath(($isIcon ? $this->fixIconName($img) : $img).'.'.$ext, $dest); + + if (!CLISetup::getOpt('force') && file_exists($outFile)) + { + CLI::write('[simpleimg] '.$this->status.' - file '.$outFile.' was already processed', CLI::LOG_BLANK, true, true); + continue; + } + + $src = $this->loadImageFile($f, $noSrcFile); + if (!$src) // error should be created by imagecreatefromblp + { + if (!$noSrcFile) // there are a couple of void file references in dbc, so this can't be a hard error. + $this->success = false; + + continue; + } + + $from = array( + 'x' => $borderOffset, + 'y' => $borderOffset, + 'w' => ($srcSize ?: imagesx($src)) - $borderOffset * 2, + 'h' => ($srcSize ?: imagesy($src)) - $borderOffset * 2 + ); + $to = array( + 'x' => 0, + 'y' => 0, + 'w' => $destSize ?: imagesx($src), + 'h' => $destSize ?: imagesy($src) + ); + + if (!$this->writeImageFile($src, $outFile, $from, $to)) + $this->success = false; + } + } + + unset($src); + } + } + + // scan ItemDisplayInfo.dbc and SpellIcon.dbc for expected images and save them to an array + // load all icon paths into another array and xor these two + // excess entries for the directory are fine, excess entries for the dbc's are not + $dbcEntries = []; + $gens = array_keys($this->genSteps); + + if (in_array(0, $gens)) // generates icons + { + if ($siRows = DB::Aowow()->selectCol('SELECT `iconPath` FROM dbc_spellicon WHERE `iconPath` NOT LIKE "%glyph-rune%"')) + foreach ($siRows as $icon) + if (stristr($icon, $this->genSteps[0][self::GEN_IDX_SRC_PATH])) // Icons/ + $dbcEntries[] = strtolower($this->genSteps[0][self::GEN_IDX_SRC_REAL].substr(strrchr($icon, '\\'), 1)); + + if ($itemIcons = DB::Aowow()->selectCol('SELECT `inventoryIcon1` FROM dbc_itemdisplayinfo WHERE `inventoryIcon1` <> ""')) + foreach ($itemIcons as $icon) + $dbcEntries[] = strtolower($this->genSteps[0][self::GEN_IDX_SRC_REAL].DIRECTORY_SEPARATOR.$icon); + } + + if (in_array(1, $gens)) // generates glyphs + if ($siRows = DB::Aowow()->selectCol('SELECT `iconPath` FROM dbc_spellicon WHERE `iconPath` LIKE "%glyph-rune%"')) + foreach ($siRows as $icon) + if (stristr($icon, $this->genSteps[1][self::GEN_IDX_SRC_PATH])) // Spellbook/ + $dbcEntries[] = strtolower($this->genSteps[1][self::GEN_IDX_SRC_REAL].substr(strrchr($icon, '\\'), 1)); + + if (in_array(10, $gens)) // generates holiday icons + if ($eventIcons = DB::Aowow()->selectCol('SELECT `textureString` FROM dbc_holidays WHERE `textureString` <> ""')) + foreach ($eventIcons as $icon) + $dbcEntries[] = strtolower($this->genSteps[10][self::GEN_IDX_SRC_REAL].DIRECTORY_SEPARATOR.$icon.'start'); + + // case-insensitive array_unique *vomits silently into a corner* + $dbcEntries = array_intersect_key($dbcEntries, array_unique($dbcEntries)); + + if ($missing = array_diff(array_map('strtolower', $dbcEntries), array_map('strtolower', $allPaths))) + { + // hide affected icons from listviews + $iconNames = array_map(function($path) { + preg_match('/\/([^\/]+)\.blp$/i', $path, $m); + return $m ? $m[1] : null; + }, $missing); + + 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) + CLI::write(' - '.$m); + } + + return $this->success; + } + + private function fixIconName(string $name) : string + { + return preg_replace('/[^a-z0-9_-]/', '-', strtolower($name)); + } +}); + +?> diff --git a/setup/tools/filegen/soundfiles.func.php b/setup/tools/filegen/soundfiles.func.php deleted file mode 100644 index 2380fed2..00000000 --- a/setup/tools/filegen/soundfiles.func.php +++ /dev/null @@ -1,61 +0,0 @@ -selectCol('SELECT ABS(id) AS ARRAY_KEY, CONCAT(path, "/", `file`) FROM ?_sounds_files'); - $nFiles = count($files); - $itr = $i = 0; - $step = 1000; - foreach ($files as $fileId => $filePath) - { - $i++; - $itr++; - if ($i == $step) - { - $i = 0; - CLI::write(' - '.$itr.'/'.$nFiles.' ('.(intVal(100 * $itr / $nFiles).'%) done')); - DB::Aowow()->selectCell('SELECT 1'); // keep mysql busy or it may go away - } - - // expect converted files as file.wav_ or file.mp3_ - $filePath .= '_'; - - // just use the first locale available .. there is no support for multiple audio files anyway - foreach (CLISetup::$expectedPaths as $locStr => $__) - { - // get your paths straight! - $p = CLI::nicePath($filePath, CLISetup::$srcDir, $locStr); - - if (CLISetup::fileExists($p)) - { - // copy over to static/wowsounds/ - if (!copy($p, 'static/wowsounds/'.$fileId)) - { - $ok = false; - CLI::write(' - could not copy '.CLI::bold($p).' into '.CLI::bold('static/wowsounds/'.$fileId), CLI::LOG_ERROR); - break 2; - } - - continue 2; - } - } - - CLI::write(' - did not find file: '.CLI::bold(CLI::nicePath($filePath, CLISetup::$srcDir, '')), CLI::LOG_WARN); - // flag as unusable in DB - DB::Aowow()->query('UPDATE ?_sounds_files SET id = ?d WHERE ABS(id) = ?d', -$fileId, $fileId); - } - - return $ok; - } - -?> - diff --git a/setup/tools/filegen/soundfiles.ss.php b/setup/tools/filegen/soundfiles.ss.php new file mode 100644 index 00000000..36ea6b4e --- /dev/null +++ b/setup/tools/filegen/soundfiles.ss.php @@ -0,0 +1,73 @@ + [[], CLISetup::ARGV_PARAM, 'Links converted sound files to database and moves them to destination.'] + ); + + protected $requiredDirs = ['static/wowsounds/']; + protected $setupAfter = [['sounds'], []]; + + public function generate() : bool + { + // ALL 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; + $time = new Timer(500); + + foreach ($files as $fileId => $filePath) + { + $sum++; + if ($time->update()) + { + CLI::write(sprintf('[soundfiles] * %'.$qtLen.'d / %d (%4.1f%%)', $sum, $nFiles, round(100 * $sum / $nFiles, 1)), CLI::LOG_BLANK, true, true); + DB::Aowow()->selectCell('SELECT 1'); // keep mysql busy or it may go away + } + + // expect converted files as file.wav_ or file.mp3_ + $filePath .= '_'; + + // just use the first locale available .. there is no support for multiple audio files for now + foreach (CLISetup::$locales as $loc) + { + foreach ($loc->gameDirs() as $dir) + { + // get your paths straight! + $p = CLI::nicePath($filePath, CLISetup::$srcDir, $dir); + + if (!CLISetup::fileExists($p)) + continue; + + // copy over to static/wowsounds/ + if (copy($p, 'static/wowsounds/'.$fileId)) + continue 3; + + $this->success = false; + CLI::write('[soundfiles] - could not copy '.CLI::bold($p).' into '.CLI::bold('static/wowsounds/'.$fileId), CLI::LOG_ERROR); + $time->reset(); + } + } + + 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()->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 @@ + [[], 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.func.php b/setup/tools/filegen/statistics.func.php deleted file mode 100644 index b2ae6b9d..00000000 --- a/setup/tools/filegen/statistics.func.php +++ /dev/null @@ -1,210 +0,0 @@ - [[-20, 2, 0, 3], [-10, 0, 1, 1], null, 0.9560, 3.6640, 88.129021, 5, 47.003525, 5, [], []], - 2 => [[-20, 2, 0, 3], [-10, 0, 1, 0], null, 0.9560, 3.4943, 88.129021, 5, 47.003525, 5, [], []], - 3 => [[-20, 1, 1, 2], [-10, 0, 1, 2], null, 0.9880, -4.0873, 145.560408, 5, 145.560408, 0, [], []], - 4 => [[-20, 1, 1, 2], [-10, 0, 1, 1], null, 0.9880, 2.0957, 145.560408, 5, 145.560408, 0, [], []], - 5 => [[-10, 1, 0, 0], [-10, 0, 1, 0], null, 0.9830, 3.4178, 150.375940, 0, 0.0, 0, [], []], - 6 => [[-20, 2, 0, 3], [-10, 0, 1, 0], null, 0.9560, 3.6640, 88.129021, 5, 47.003525, 0, [], ['parryrtng' => [0.25, 'percentOf', 'str']]], // Forcefull Deflection (49410) - 7 => [[-20, 1, 1, 2], [-10, 0, 1, 0], null, 0.9880, 2.1080, 145.560408, 0, 145.560408, 5, [], []], - 8 => [[-10, 1, 0, 0], [-10, 0, 1, 0], null, 0.9830, 3.6587, 150.375940, 0, 0.0, 0, [], []], - 9 => [[-10, 1, 0, 0], [-10, 0, 1, 0], null, 0.9830, 2.4211, 150.375940, 0, 0.0, 0, [], []], - 11 => [[-20, 2, 0, 0], [-10, 0, 1, 0], null, 0.9720, 5.6097, 116.890707, 0, 0.0, 0, [], []] - ); - - 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)); - - return $dataz; - }; - - $race = function() - { - // where did i get this data again..? - // { str, agi, sta, int, spi, raceMod1, raceMod2 } - $raceData = array( - 1 => [20, 20, 20, 20, 20, [], []], - 2 => [23, 17, 22, 17, 23, [], []], - 3 => [22, 16, 23, 19, 19, [], []], - 4 => [17, 25, 19, 20, 20, [], []], - 5 => [19, 18, 21, 18, 25, [], []], - 6 => [25, 15, 22, 15, 22, [], []], - 7 => [15, 23, 19, 24, 20, [], []], - 8 => [21, 22, 21, 16, 21, [], []], - 10 => [17, 22, 18, 24, 19, [], []], - 11 => [21, 17, 19, 21, 22, [], []] - ); - - $racials = new SpellList(array(['typeCat', -4], ['reqClassMask', 0])); - $allMods = $racials->getProfilerMods(); - foreach ($allMods as $spellId => $mods) - { - if (!$mods) - continue; - - // if there is ever a case where a racial is shared between races i don't want to know about it! - $raceId = log($racials->getEntry($spellId)['reqRaceMask'], 2) + 1; - if (!isset($raceData[$raceId])) - continue; - - foreach ($mods as $jsonStat => $mod) - { - if (empty($raceData[$raceId][5][$jsonStat])) - $raceData[$raceId][5][$jsonStat] = $mod; - else - $raceData[$raceId][6][$jsonStat] = $mod; - } - } - - return $raceData; - }; - - $combo = function() - { - $result = []; - $critToDodge = array( - 1 => 0.85/1.15, 2 => 1.00/1.15, 3 => 1.11/1.15, - 4 => 2.00/1.15, 5 => 1.00/1.15, 6 => 0.85/1.15, - 7 => 1.60/1.15, 8 => 1.00/1.15, 9 => 0.97/1.15, 11 => 2.00/1.15 - ); - - // TrinityCore claims, DodgePerAgi per level and class can be constituted from critPerAgi (and level (and class)) - // who am i to argue - // rebase stats to a specific race. chosen human as all stats are 20 - // level:{ str, agi, sta, int, spi, hp, mana, mleCrt%Agi, splCrt%Int, dodge%Agi, HealthRegenModToBaseStat, HealthRegenModToBonusStat } - - foreach ($critToDodge as $class => $mod) - { - // humans can't be hunter, shaman, druids (use tauren here) - if (in_array($class, [3, 7, 11])) - $offset = [25, 15, 22, 15, 22]; - else - $offset = [20, 20, 20, 20, 20]; - - $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 - 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', - (($class - 1) * 100) - 1, // class-offset - $mod, - (($class - 1) * 100) + 0, // lvl 1 - (($class - 1) * 100) + 79 // lvl 80 - ); - - $rows = DB::World()->select(' - SELECT - pls.level AS ARRAY_KEY, - pls.str - ?d, pls.agi - ?d, pls.sta - ?d, pls.inte - ?d, pls.spi - ?d, - 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 ORDER BY pls.level ASC', - $offset[0], $offset[1], $offset[2], $offset[3], $offset[4], - in_array($class, [3, 7, 11]) ? 6 : 1, - $class - ); - - $result[$class] = []; - foreach ($rows as $lvl => $row) - $result[$class][$lvl] = array_values(array_merge($row, $gtData[$lvl])); - } - - return $result; - }; - - $level = function() - { - // base mana regeneration per level - // identical across classes (just use one, that acutally has mana (offset: 100)) - // content of gtRegenMPPerSpt.dbc - - return DB::Aowow()->selectCol('SELECT idx-99 AS ARRAY_KEY, ratio FROM dbc_gtregenmpperspt WHERE idx >= 100 AND idx < 100 + ?d', MAX_LEVEL); - }; - - $skills = function() - { - // profession perks ... too lazy to formulate a search algorithm for two occurences - return array( - 186 => array( // mining / toughness - 75 => ['sta' => 3], - 150 => ['sta' => 5], - 225 => ['sta' => 7], - 300 => ['sta' => 10], - 375 => ['sta' => 30], - 450 => ['sta' => 60], - ), - 393 => array( // skinning / master of anatomy - 75 => ['critstrkrtng' => 3], - 150 => ['critstrkrtng' => 6], - 225 => ['critstrkrtng' => 9], - 300 => ['critstrkrtng' => 12], - 375 => ['critstrkrtng' => 20], - 450 => ['critstrkrtng' => 40], - ) - ); - }; - - $sub = ['classs', 'race', 'combo', 'level', 'skills']; - $out = []; - $success = true; - - foreach ($sub as $s) - { - $res = $$s(); - $out[$s] = $res; - if (!$res) - CLI::write('statistics - generator $'.$s.'() returned empty', CLI::LOG_WARN); - } - - $toFile = 'g_statistics = '.preg_replace('/"\$([^$"]+)"/', '\1', Util::toJSON($out)).';'; - - if (!CLISetup::writeFile('datasets/statistics', $toFile)) - $success = false; - - return $success; - } - -?> \ No newline at end of file diff --git a/setup/tools/filegen/statistics.ss.php b/setup/tools/filegen/statistics.ss.php new file mode 100644 index 00000000..3b0afb13 --- /dev/null +++ b/setup/tools/filegen/statistics.ss.php @@ -0,0 +1,211 @@ + [[], CLISetup::ARGV_PARAM, 'Compiles player stats into file for the character profiler tool.'] + ); + + protected $worldDependency = ['player_levelstats', 'player_classlevelstats']; + protected $dbcSourceFiles = ['gtchancetomeleecrit', 'gtchancetomeleecritbase', 'gtchancetospellcrit', 'gtchancetospellcritbase', 'gtoctregenhp', 'gtregenmpperspt', 'gtregenhpperspt']; + protected $requiredDirs = ['datasets/']; + + public function generate() : bool + { + $sub = ['classs', 'race', 'combo', 'level', 'skills']; + $out = []; + + foreach ($sub as $s) + { + if ($out[$s] = $this->$s()) + continue; + + CLI::write('[statistics] generator '.$s.'() returned empty', CLI::LOG_WARN); + $this->success = false; + } + + $toFile = 'g_statistics = '.preg_replace('/"\$([^$"]+)"/', '\1', Util::toJSON($out)).';'; + + if (!CLISetup::writeFile('datasets/statistics', $toFile)) + $this->success = false; + + return $this->success; + } + + // constants and mods taken from TrinityCore (Player.cpp, StatSystem.cpp) + private function classs() : array + { + /* content per Index + mleatkpwr[base, strMultiplier, agiMultiplier, levelMultiplier] + rngatkpwr[base, strMultiplier, agiMultiplier, levelMultiplier] + baseCritPct[phys, spell] + diminishingConstant + baseDodgePct + DodgeCap + baseParryPct + ParryCap + baseBlockPct + classMod1 applies mod directly only one class having something worth mentioning: DK + classMod2 applies mod directly so what were they originally used for..? + */ + + $dataz = array( + 1 => [[-20, 2, 0, 3], [-10, 0, 1, 1], null, 0.9560, 3.6640, 88.129021, 5, 47.003525, 5, [], []], + 2 => [[-20, 2, 0, 3], [-10, 0, 1, 0], null, 0.9560, 3.4943, 88.129021, 5, 47.003525, 5, [], []], + 3 => [[-20, 1, 1, 2], [-10, 0, 1, 2], null, 0.9880, -4.0873, 145.560408, 5, 145.560408, 0, [], []], + 4 => [[-20, 1, 1, 2], [-10, 0, 1, 1], null, 0.9880, 2.0957, 145.560408, 5, 145.560408, 0, [], []], + 5 => [[-10, 1, 0, 0], [-10, 0, 1, 0], null, 0.9830, 3.4178, 150.375940, 0, 0.0, 0, [], []], + 6 => [[-20, 2, 0, 3], [-10, 0, 1, 0], null, 0.9560, 3.6640, 88.129021, 5, 47.003525, 0, [], ['parryrtng' => [0.25, 'percentOf', 'str']]], // Forcefull Deflection (49410) + 7 => [[-20, 1, 1, 2], [-10, 0, 1, 0], null, 0.9880, 2.1080, 145.560408, 0, 145.560408, 5, [], []], + 8 => [[-10, 1, 0, 0], [-10, 0, 1, 0], null, 0.9830, 3.6587, 150.375940, 0, 0.0, 0, [], []], + 9 => [[-10, 1, 0, 0], [-10, 0, 1, 0], null, 0.9830, 2.4211, 150.375940, 0, 0.0, 0, [], []], + 11 => [[-20, 2, 0, 0], [-10, 0, 1, 0], null, 0.9720, 5.6097, 116.890707, 0, 0.0, 0, [], []] + ); + + 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 = %i', $class - 1)); + + return $dataz; + } + + // { str, agi, sta, int, spi, raceMod1, raceMod2 } + private function race() : array + { + $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 + [[], []]); + + $racials = new SpellList(array(['typeCat', -4], ['reqClassMask', 0])); + $allMods = $racials->getProfilerMods(); + foreach ($allMods as $spellId => $mods) + { + if (!$mods) + continue; + + // if there is ever a case where a racial is shared between races i don't want to know about it! + $raceId = log($racials->getEntry($spellId)['reqRaceMask'], 2) + 1; + if (!isset($raceData[$raceId])) + continue; + + foreach ($mods as $jsonStat => $mod) + { + if (empty($raceData[$raceId][5][$jsonStat])) + $raceData[$raceId][5][$jsonStat] = $mod; + else + $raceData[$raceId][6][$jsonStat] = $mod; + } + } + + return $raceData; + } + + // TrinityCore claims, DodgePerAgi per level and class can be constituted from critPerAgi (and level (and class)) + // who am i to argue + // rebase stats to a specific race. chosen human as all stats are 20 and tauren for hunter, shaman and druid + // level:{ str, agi, sta, int, spi, hp, mana, mleCrt%Agi, splCrt%Int, dodge%Agi, HealthRegenModToBaseStat, HealthRegenModToBonusStat } + private function combo() : array + { + $result = []; + $critToDodge = array( + 1 => 0.85/1.15, 2 => 1.00/1.15, 3 => 1.11/1.15, + 4 => 2.00/1.15, 5 => 1.00/1.15, 6 => 0.85/1.15, + 7 => 1.60/1.15, 8 => 1.00/1.15, 9 => 0.97/1.15, 11 => 2.00/1.15 + ); + + foreach ($critToDodge as $class => $mod) + { + // humans can't be hunter, shaman, druids (use tauren here) + if (in_array($class, [3, 7, 11])) + $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` = 6')); + 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()->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 %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()->selectAssoc( + 'SELECT pls.level AS ARRAY_KEY, + 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 = %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, + $class + ); + + $result[$class] = []; + foreach ($rows as $lvl => $row) + $result[$class][$lvl] = array_values(array_merge($row, $gtData[$lvl])); + } + + return $result; + } + + // base mana regeneration per level + // identical across classes (just use one, that acutally has mana (offset: 100)) + // 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 + %i', MAX_LEVEL); + } + + // profession perks ... too lazy to formulate a search algorithm for two occurences + private function skills() : array + { + // 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 + // JOIN dbc_spell s ON s.id = sla.spellId + // WHERE sla.acquiremethod = 1 AND // learn on skillup + // sk.categoryId = 11 AND // primary profession + // s.effect1Id = 6 AND // ApplyAura + // s.effect1AuraId IN (29, 189) // ModStatFlat, ModRating (need more?) + // '); + + return array( + SKILL_MINING => array( // mining / toughness + 75 => ['sta' => 3], + 150 => ['sta' => 5], + 225 => ['sta' => 7], + 300 => ['sta' => 10], + 375 => ['sta' => 30], + 450 => ['sta' => 60], + ), + SKILL_SKINNING => array( // skinning / master of anatomy + 75 => ['critstrkrtng' => 3], + 150 => ['critstrkrtng' => 6], + 225 => ['critstrkrtng' => 9], + 300 => ['critstrkrtng' => 12], + 375 => ['critstrkrtng' => 20], + 450 => ['critstrkrtng' => 40], + ) + ); + } +}); + +?> diff --git a/setup/tools/filegen/talentCalc.func.php b/setup/tools/filegen/talentCalc.func.php deleted file mode 100644 index ee1aee4b..00000000 --- a/setup/tools/filegen/talentCalc.func.php +++ /dev/null @@ -1,211 +0,0 @@ -getProfilerMods(); - - $buildTree = function ($class) use (&$petFamIcons, &$tSpells, $spellMods) - { - $petCategories = []; - - $mask = $class ? 1 << ($class - 1) : 0; - - // All "tabs" of a given class talent - $tabs = DB::Aowow()->select('SELECT * FROM dbc_talenttab WHERE classMask = ?d ORDER BY `tabNumber`, `creatureFamilyMask`', $mask); - $result = []; - - for ($tabIdx = 0; $tabIdx < count($tabs); $tabIdx++) - { - $talents = DB::Aowow()->select('SELECT t.id AS tId, t.*, s.name_loc0, s.name_loc2, s.name_loc3, s.name_loc6, s.name_loc8, 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`', $tabs[$tabIdx]['id']); - $result[$tabIdx] = array( - 'n' => Util::localizedString($tabs[$tabIdx], 'name'), - 't' => [] - ); - - if (!$class) - { - $petFamId = log($tabs[$tabIdx]['creatureFamilyMask'], 2); - $result[$tabIdx]['icon'] = $petFamIcons[$petFamId]; - $petCategories = DB::Aowow()->SelectCol('SELECT id AS ARRAY_KEY, categoryEnumID FROM dbc_creaturefamily WHERE petTalentType = ?d', $petFamId); - $result[$tabIdx]['f'] = array_keys($petCategories); - } - - // talent dependencies go here - $depLinks = []; - $tNums = []; - - for ($talentIdx = 0; $talentIdx < count($talents); $talentIdx++) - { - $tNums[$talents[$talentIdx]['tId']] = $talentIdx; - - $d = []; - $s = []; - $i = $talents[$talentIdx]['tId']; - $n = Util::localizedString($talents[$talentIdx], 'name'); - $x = $talents[$talentIdx]['column']; - $y = $talents[$talentIdx]['row']; - $r = null; - $t = []; - $j = []; - $icon = $talents[$talentIdx]['iconString']; - $m = $talents[$talentIdx]['rank2'] == 0 ? 1 : ( - $talents[$talentIdx]['rank3'] == 0 ? 2 : ( - $talents[$talentIdx]['rank4'] == 0 ? 3 : ( - $talents[$talentIdx]['rank5'] == 0 ? 4 : 5 - ) - ) - ); - - // duplet handling - $f = []; - foreach ($petCategories as $k => $v) - { - // cant handle 64bit integer .. split - if ($v >= 32 && ((1 << ($v - 32)) & $talents[$talentIdx]['petCategory2'])) - $f[] = $k; - else if ($v < 32 && ((1 << $v) & $talents[$talentIdx]['petCategory1'])) - $f[] = $k; - } - - for ($itr = 0; $itr <= ($m - 1); $itr++) - { - if (!$tSpells->getEntry($talents[$talentIdx]['rank'.($itr + 1)])) - continue; - - $d[] = $tSpells->parseText()[0]; - $s[] = $talents[$talentIdx]['rank'.($itr + 1)]; - if (isset($spellMods[$talents[$talentIdx]['rank'.($itr + 1)]])) - $j[] = $spellMods[$talents[$talentIdx]['rank'.($itr + 1)]]; - else - $j[] = null; - - if ($talents[$talentIdx]['talentSpell']) - $t[] = $tSpells->getTalentHeadForCurrent(); - } - - if ($talents[$talentIdx]['reqTalent']) - { - // we didn't encounter the required talent yet => create reference - if (!isset($tNums[$talents[$talentIdx]['reqTalent']])) - $depLinks[$talents[$talentIdx]['reqTalent']] = $talentIdx; - - $r = @[$tNums[$talents[$talentIdx]['reqTalent']], $talents[$talentIdx]['reqRank'] + 1]; - } - - $result[$tabIdx]['t'][$talentIdx] = array( - 'i' => $i, - 'n' => $n, - 'm' => $m, - 'd' => $d, - 's' => $s, - 'x' => $x, - 'y' => $y, - 'j' => $j - ); - - if (isset($r)) - $result[$tabIdx]['t'][$talentIdx]['r'] = $r; - - if (!empty($t)) - $result[$tabIdx]['t'][$talentIdx]['t'] = $t; - - if (!empty($f)) - $result[$tabIdx]['t'][$talentIdx]['f'] = $f; - - if ($class) - $result[$tabIdx]['t'][$talentIdx]['iconname'] = $icon; - - // If this talent is a reference, add it to the array of talent dependencies - if (isset($depLinks[$talents[$talentIdx]['tId']])) - { - $result[$tabIdx]['t'][$depLinks[$talents[$talentIdx]['tId']]]['r'][0] = $talentIdx; - unset($depLinks[$talents[$talentIdx]['tId']]); - } - } - - // Remove all dependencies for which the talent has not been found - foreach ($depLinks as $dep_link) - unset($result[$tabIdx]['t'][$dep_link]['r']); - } - - return $result; - }; - - // my neighbour is noisy as fuck and my head hurts, so .. - $petFamIcons = ['Ability_Druid_KingoftheJungle', 'Ability_Druid_DemoralizingRoar', 'Ability_EyeOfTheOwl']; // .. i've no idea where to fetch these from - $classes = [CLASS_WARRIOR, CLASS_PALADIN, CLASS_HUNTER, CLASS_ROGUE, CLASS_PRIEST, CLASS_DEATHKNIGHT, CLASS_SHAMAN, CLASS_MAGE, CLASS_WARLOCK, CLASS_DRUID]; - $petIcons = ''; - - // check directory-structure - foreach (Util::$localeStrings as $dir) - if (!CLISetup::writeDir('datasets/'.$dir)) - $success = false; - - $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'); - $tSpells = new SpellList(array(['s.id', $tSpellIds], CFG_SQL_LIMIT_NONE)); - - foreach (CLISetup::$localeIds as $lId) - { - User::useLocale($lId); - Lang::load(Util::$localeStrings[$lId]); - - // TalentCalc - foreach ($classes as $cMask) - { - set_time_limit(20); - - $cId = log($cMask, 2) + 1; - $file = 'datasets/'.User::$localeString.'/talents-'.$cId; - $toFile = '$WowheadTalentCalculator.registerClass('.$cId.', '.Util::toJSON($buildTree($cId)).')'; - - if (!CLISetup::writeFile($file, $toFile)) - $success = false; - } - - // PetCalc - if (empty($petIcons)) - { - $pets = 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($pets); - } - - $toFile = "var g_pet_icons = ".$petIcons.";\n\n"; - $toFile .= 'var g_pet_talents = '.Util::toJSON($buildTree(0)).';'; - $file = 'datasets/'.User::$localeString.'/pet-talents'; - - if (!CLISetup::writeFile($file, $toFile)) - $success = false; - } - - return $success; - } -?> diff --git a/setup/tools/filegen/talentIcons.func.php b/setup/tools/filegen/talentIcons.func.php deleted file mode 100644 index 168137b2..00000000 --- a/setup/tools/filegen/talentIcons.func.php +++ /dev/null @@ -1,102 +0,0 @@ - $v) - { - if (!$v) - continue; - - set_time_limit(10); - - for ($tree = 0; $tree < 3; $tree++) - { - $what = $k ? 'classMask' : 'creatureFamilyMask'; - $set = $k ? 1 << ($k - 1) : 1 << $tree; - $subset = $k ? $tree : 0; - $path = $k ? 'talents/icons' : 'hunterpettalents'; - $outFile = 'static/images/wow/'.$path.'/'.$v.'_'.($tree + 1).'.jpg'; - $icons = DB::Aowow()->SelectCol($query, $what, $set, $subset); - - if (empty($icons)) - { - CLI::write('talentIcons - query for '.$v.' tree: '.$k.' returned empty', CLI::LOG_ERROR); - $success = false; - continue; - } - - if ($res = imageCreateTrueColor(count($icons) * $dims, 2 * $dims)) - { - for ($i = 0; $i < count($icons); $i++) - { - $imgFile = 'static/images/wow/icons/medium/'.strtolower($icons[$i]).'.jpg'; - if (!file_exists($imgFile)) - { - CLI::write('talentIcons - raw image '.CLI::bold($imgFile). ' not found', CLI::LOG_ERROR); - $success = false; - break; - } - - $im = imagecreatefromjpeg($imgFile); - - // colored - imagecopymerge($res, $im, $i * $dims, 0, 0, 0, imageSX($im), imageSY($im), 100); - - // grayscale - if (imageistruecolor($im)) - imagetruecolortopalette($im, false, 256); - - for ($j = 0; $j < imagecolorstotal($im); $j++) - { - $color = imagecolorsforindex($im, $j); - $gray = round(0.299 * $color['red'] + 0.587 * $color['green'] + 0.114 * $color['blue']); - imagecolorset($im, $j, $gray, $gray, $gray); - } - imagecopymerge($res, $im, $i * $dims, $dims, 0, 0, imageSX($im), imageSY($im), 100); - } - - if (@imagejpeg($res, $outFile)) - CLI::write(sprintf(ERR_NONE, CLI::bold($outFile)), CLI::LOG_OK); - else - { - $success = false; - CLI::write('talentIcons - '.CLI::bold($outFile.'.jpg').' could not be written', CLI::LOG_ERROR); - } - } - else - { - $success = false; - CLI::write('talentIcons - image resource not created', CLI::LOG_ERROR); - continue; - } - } - } - - return $success; - } - -?> diff --git a/setup/tools/filegen/talentcalc.ss.php b/setup/tools/filegen/talentcalc.ss.php new file mode 100644 index 00000000..1207fdf4 --- /dev/null +++ b/setup/tools/filegen/talentcalc.ss.php @@ -0,0 +1,199 @@ + [[], CLISetup::ARGV_PARAM, 'Compiles talent tree data to file for the talent calculator tool.'] + ); + + protected $dbcSourceFiles = ['talenttab', 'talent', 'spell', 'creaturefamily', 'spellicon']; + protected $setupAfter = [['spell'], []]; + protected $requiredDirs = ['datasets/']; + protected $localized = true; + + private $petFamIcons = []; + private $tSpells = null; + private $spellMods = []; + + public function generate() : bool + { + // target direcotries are missing + if (!$this->success) + return false; + + // 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])))->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)')); + + $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])); + + foreach (CLISetup::$locales as $loc) + { + Lang::load($loc); + + // TalentCalc + foreach (ChrClass::cases() as $class) + { + set_time_limit(20); + + $file = 'datasets/'.$loc->json().'/talents-'.$class->value; + $toFile = '$WowheadTalentCalculator.registerClass('.$class->value.', '.Util::toJSON($this->buildTree($class->toMask())).')'; + + if (!CLISetup::writeFile($file, $toFile)) + $this->success = false; + } + + // PetCalc + $toFile = "var g_pet_icons = ".$petIcons.";\n\n"; + $toFile .= 'var g_pet_talents = '.Util::toJSON($this->buildTree(0)).';'; + $file = 'datasets/'.$loc->json().'/pet-talents'; + + if (!CLISetup::writeFile($file, $toFile)) + $this->success = false; + } + + return $this->success; + } + + private function buildTree(int $classMask) : array + { + $petCategories = []; + + // All "tabs" of a given class talent + $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()->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" + FROM dbc_talent t, dbc_spell s, dbc_spellicon si + 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'] + ); + + $result[$tabIdx] = array( + 'n' => Util::localizedString($tabs[$tabIdx], 'name'), + 't' => [] + ); + + if (!$classMask) + { + $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` = %i', $petFamId); + $result[$tabIdx]['f'] = array_keys($petCategories); + } + + // talent dependencies go here + $depLinks = []; + $tNums = []; + + for ($talentIdx = 0; $talentIdx < count($talents); $talentIdx++) + { + $tNums[$talents[$talentIdx]['tId']] = $talentIdx; + + $talent = array( + 'i' => $talents[$talentIdx]['tId'], // talent id + 'n' => Util::localizedString($talents[$talentIdx], 'name'), // talent name + 'm' => $talents[$talentIdx]['maxRank'], // maxRank + 'd' => [], // [descriptions] + 's' => [], // [spellIds] + 'x' => $talents[$talentIdx]['column'], // col # + 'y' => $talents[$talentIdx]['row'], // row # + 'j' => [] // [spellMods] that are applied when used in profiler + // 'r' => [] // [reqTalentId, reqRank] (can be omitted) + // 't' => [] // talentspell tooltip (can be omitted) + // 'f' => [] // [petFamilyIds] (can be omitted) + ); + + if ($classMask) + $talent['iconname'] = $talents[$talentIdx]['iconString']; + + for ($itr = 0; $itr <= ($talent['m'] - 1); $itr++) + { + $spell = $talents[$talentIdx]['rank'.($itr + 1)]; + if (!$this->tSpells->getEntry($spell)) + continue; + + $talent['d'][] = $this->tSpells->parseText()[0]; + $talent['s'][] = $talents[$talentIdx]['rank'.($itr + 1)]; + + if ($classMask && isset($this->spellMods[$spell])) + if ($mod = $this->spellMods[$spell]) + $talent['j'][] = $mod; + + if ($talents[$talentIdx]['talentSpell']) + $talent['t'][] = $this->tSpells->getTalentHeadForCurrent(); + } + + foreach ($petCategories as $k => $v) + { + // cant handle 64bit integer .. split + if ($v >= 32 && ((1 << ($v - 32)) & $talents[$talentIdx]['petCategory2'])) + $talent['f'][] = $k; + else if ($v < 32 && ((1 << $v) & $talents[$talentIdx]['petCategory1'])) + $talent['f'][] = $k; + } + + if ($talents[$talentIdx]['reqTalent']) + { + // we didn't encounter the required talent yet => create reference + if (!isset($tNums[$talents[$talentIdx]['reqTalent']])) + $depLinks[$talents[$talentIdx]['reqTalent']] = $talentIdx; + + $talent['r'] = [$tNums[$talents[$talentIdx]['reqTalent']] ?? 0, $talents[$talentIdx]['reqRank'] + 1]; + } + + $result[$tabIdx]['t'][$talentIdx] = $talent; + + // If this talent is a reference, add it to the array of talent dependencies + if (isset($depLinks[$talents[$talentIdx]['tId']])) + { + $result[$tabIdx]['t'][$depLinks[$talents[$talentIdx]['tId']]]['r'][0] = $talentIdx; + unset($depLinks[$talents[$talentIdx]['tId']]); + } + } + + // Remove all dependencies for which the talent has not been found + foreach ($depLinks as $dep_link) + unset($result[$tabIdx]['t'][$dep_link]['r']); + } + + return $result; + } +}); + +?> diff --git a/setup/tools/filegen/talenticons.ss.php b/setup/tools/filegen/talenticons.ss.php new file mode 100644 index 00000000..efbdfdbe --- /dev/null +++ b/setup/tools/filegen/talenticons.ss.php @@ -0,0 +1,135 @@ + [[], CLISetup::ARGV_PARAM, 'Generates icon textures for the talent calculator tool.'] + ); + + protected $dbcSourceFiles = ['talenttab', 'talent', 'spell']; + protected $setupAfter = [['icons', 'spell'], ['simpleimg']]; + protected $requiredDirs = ['static/images/wow/talents/icons', 'static/images/wow/hunterpettalents']; + + private const ICON_SIZE = 36; // px + + public function generate() : bool + { + /***************/ + /* Hunter Pets */ + /***************/ + + for ($tabIdx = 0; $tabIdx < 3; $tabIdx++) + { + $outFile = 'static/images/wow/hunterpettalents/icons_'.($tabIdx + 1).'.jpg'; + + if ($tex = $this->compileTexture('creatureFamilyMask', 1 << $tabIdx, 0)) + { + if (!imagejpeg($tex, $outFile)) + { + CLI::write('[talenticons] - '.CLI::bold($outFile.'.jpg').' could not be written', CLI::LOG_ERROR); + $this->success = false; + } + else + CLI::write('[talenticons] created file '.CLI::bold($outFile), CLI::LOG_OK, true, true); + } + else + $this->success = false; + } + + + /***********/ + /* Players */ + /***********/ + + foreach (ChrClass::cases() as $class) + { + set_time_limit(10); + + for ($tabIdx = 0; $tabIdx < 3; $tabIdx++) + { + $outFile = 'static/images/wow/talents/icons/'.$class->json().'_'.($tabIdx + 1).'.jpg'; + + if ($tex = $this->compileTexture('classMask', $class->toMask(), $tabIdx)) + { + if (!imagejpeg($tex, $outFile)) + { + CLI::write('[talenticons] - '.CLI::bold($outFile.'.jpg').' could not be written', CLI::LOG_ERROR); + $this->success = false; + } + else + CLI::write('[talenticons] created file '.CLI::bold($outFile), CLI::LOG_OK, true, true); + } + else + $this->success = false; + } + } + + return $this->success; + } + + private function compileTexture(string $ttField, int $searchMask, int $tabIdx) : ?\GdImage + { + $icons = DB::Aowow()->SelectCol( + '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.%n = %i AND tt.`tabNumber` = %i + ORDER BY t.`row`, t.`column`, t.`id` ASC', + $ttField, $searchMask, $tabIdx); + + if (empty($icons)) + { + CLI::write('[talenticons] - query for '.$ttField.': '.$searchMask.' on idx: '.$tabIdx.' returned empty', CLI::LOG_ERROR); + return null; + } + + $res = imageCreateTrueColor(count($icons) * self::ICON_SIZE, 2 * self::ICON_SIZE); + if (!$res) + { + CLI::write('[talenticons] - image resource not created', CLI::LOG_ERROR); + return null; + } + + for ($i = 0; $i < count($icons); $i++) + { + $imgFile = 'static/images/wow/icons/medium/'.strtolower($icons[$i]).'.jpg'; + if (!file_exists($imgFile)) + { + CLI::write('[talenticons] - raw image '.CLI::bold($imgFile). ' not found', CLI::LOG_ERROR); + return null; + } + + $im = imagecreatefromjpeg($imgFile); + + // colored + imagecopymerge($res, $im, $i * self::ICON_SIZE, 0, 0, 0, imageSX($im), imageSY($im), 100); + + // grayscale + if (imageistruecolor($im)) + imagetruecolortopalette($im, false, 256); + + for ($j = 0; $j < imagecolorstotal($im); $j++) + { + $color = imagecolorsforindex($im, $j); + $gray = round(0.299 * $color['red'] + 0.587 * $color['green'] + 0.114 * $color['blue']); + imagecolorset($im, $j, $gray, $gray, $gray); + } + imagecopymerge($res, $im, $i * self::ICON_SIZE, self::ICON_SIZE, 0, 0, imageSX($im), imageSY($im), 100); + } + + return $res; + } +}); + +?> diff --git a/setup/tools/filegen/templates/global.js/0_user.js b/setup/tools/filegen/templates/global.js/0_user.js new file mode 100644 index 00000000..cb968339 --- /dev/null +++ b/setup/tools/filegen/templates/global.js/0_user.js @@ -0,0 +1,88 @@ +// Needed for IE because it's dumb + +'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 +*/ + +// IMPORTANT: If you update/change the permission groups below make sure to also update them in User.inc.php! + +/*********/ +/* ROLES */ +/*********/ + +var U_GROUP_TESTER = 0x1; +var U_GROUP_ADMIN = 0x2; +var U_GROUP_EDITOR = 0x4; +var U_GROUP_MOD = 0x8; +var U_GROUP_BUREAU = 0x10; +var U_GROUP_DEV = 0x20; +var U_GROUP_VIP = 0x40; +var U_GROUP_BLOGGER = 0x80; +var U_GROUP_PREMIUM = 0x100; +var U_GROUP_LOCALIZER = 0x200; +var U_GROUP_SALESAGENT = 0x400; +var U_GROUP_SCREENSHOT = 0x800; +var U_GROUP_VIDEO = 0x1000; +var U_GROUP_APIONLY = 0x2000; +var U_GROUP_PENDING = 0x4000; + + +/******************/ +/* ROLE SHORTCUTS */ +/******************/ + +var U_GROUP_STAFF = U_GROUP_ADMIN | U_GROUP_EDITOR | U_GROUP_MOD | U_GROUP_BUREAU | U_GROUP_DEV | U_GROUP_BLOGGER | U_GROUP_LOCALIZER | U_GROUP_SALESAGENT; +var U_GROUP_EMPLOYEE = U_GROUP_ADMIN | U_GROUP_BUREAU | U_GROUP_DEV; +var U_GROUP_GREEN_TEXT = U_GROUP_MOD | U_GROUP_BUREAU | U_GROUP_DEV; +var U_GROUP_PREMIUMISH = U_GROUP_PREMIUM | U_GROUP_EDITOR; +var U_GROUP_MODERATOR = U_GROUP_ADMIN | U_GROUP_MOD | U_GROUP_BUREAU; +var U_GROUP_COMMENTS_MODERATOR = U_GROUP_BUREAU | U_GROUP_MODERATOR | U_GROUP_LOCALIZER; +var U_GROUP_PREMIUM_PERMISSIONS = U_GROUP_PREMIUM | U_GROUP_STAFF | U_GROUP_VIP; + +var g_users = {}; +var g_favorites = []; +var g_customColors = {}; + +function g_isUsernameValid(username) { + return (username.match(/[^a-z0-9]/i) == null && username.length >= 4 && username.length <= 16); +} + +var User = new function() { + var self = this; + + /**********/ + /* PUBLIC */ + /**********/ + + self.hasPermissions = function(roles) + { + if(!roles) + return true; + + return !!(g_user.roles & roles); + } + + /**********/ + /* PRIVATE */ + /**********/ + +}; diff --git a/setup/tools/filegen/templates/global.js/ajax.js b/setup/tools/filegen/templates/global.js/ajax.js new file mode 100644 index 00000000..22761baa --- /dev/null +++ b/setup/tools/filegen/templates/global.js/ajax.js @@ -0,0 +1,51 @@ +function Ajax(url, opt) +{ + if (!url) + return; + + var _; + + try { _ = new XMLHttpRequest() } catch (e) + { + try { _ = new ActiveXObject("Msxml2.XMLHTTP") } catch (e) + { + try { _ = new ActiveXObject("Microsoft.XMLHTTP") } catch (e) + { + if (window.createRequest) + _ = window.createRequest(); + else + { + alert(LANG.message_ajaxnotsupported); + return; + } + } + } + } + + this.request = _; + + $WH.cO(this, opt); + this.method = this.method || (this.params && 'POST') || 'GET'; + + _.open(this.method, url, this.async == null ? true : this.async); + _.onreadystatechange = Ajax.onReadyStateChange.bind(this); + + if (this.method.toUpperCase() == 'POST') + _.setRequestHeader('Content-Type', (this.contentType || 'application/x-www-form-urlencoded') + '; charset=' + (this.encoding || 'UTF-8')); + + _.send(this.params); +} + +Ajax.onReadyStateChange = function() +{ + if (this.request.readyState == 4) + { + if (this.request.status == 0 || (this.request.status >= 200 && this.request.status < 300)) + this.onSuccess != null && this.onSuccess(this.request, this); + else + this.onFailure != null && this.onFailure(this.request, this); + + if (this.onComplete != null) + this.onComplete(this.request, this); + } +}; diff --git a/setup/tools/filegen/templates/global.js/animations.js b/setup/tools/filegen/templates/global.js/animations.js new file mode 100644 index 00000000..da37f6fe --- /dev/null +++ b/setup/tools/filegen/templates/global.js/animations.js @@ -0,0 +1,123 @@ +/* + * jQuery Color Animations + * Copyright 2007 John Resig + * Released under the MIT and GPL licenses. + */ + +(function(jQuery){ + + // We override the animation for all of these color styles + jQuery.each(['backgroundColor', 'borderBottomColor', 'borderLeftColor', 'borderRightColor', 'borderTopColor', 'color', 'outlineColor'], function(i,attr){ + jQuery.fx.step[attr] = function(fx){ + if ( fx.state == 0 ) { + fx.start = getColor( fx.elem, attr ); + fx.end = getRGB( fx.end ); + } + + fx.elem.style[attr] = "rgb(" + [ + Math.max(Math.min( parseInt((fx.pos * (fx.end[0] - fx.start[0])) + fx.start[0]), 255), 0), + Math.max(Math.min( parseInt((fx.pos * (fx.end[1] - fx.start[1])) + fx.start[1]), 255), 0), + Math.max(Math.min( parseInt((fx.pos * (fx.end[2] - fx.start[2])) + fx.start[2]), 255), 0) + ].join(",") + ")"; + } + }); + + // Color Conversion functions from highlightFade + // By Blair Mitchelmore + // http://jquery.offput.ca/highlightFade/ + + // Parse strings looking for color tuples [255,255,255] + function getRGB(color) { + var result; + + // Check if we're already dealing with an array of colors + if ( color && color.constructor == Array && color.length == 3 ) + return color; + + // Look for rgb(num,num,num) + if (result = /rgb\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*\)/.exec(color)) + return [parseInt(result[1]), parseInt(result[2]), parseInt(result[3])]; + + // Look for rgb(num%,num%,num%) + if (result = /rgb\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*\)/.exec(color)) + return [parseFloat(result[1])*2.55, parseFloat(result[2])*2.55, parseFloat(result[3])*2.55]; + + // Look for #a0b1c2 + if (result = /#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})/.exec(color)) + return [parseInt(result[1],16), parseInt(result[2],16), parseInt(result[3],16)]; + + // Look for #fff + if (result = /#([a-fA-F0-9])([a-fA-F0-9])([a-fA-F0-9])/.exec(color)) + return [parseInt(result[1]+result[1],16), parseInt(result[2]+result[2],16), parseInt(result[3]+result[3],16)]; + + // Otherwise, we're most likely dealing with a named color + return colors[jQuery.trim(color).toLowerCase()]; + } + + function getColor(elem, attr) { + var color; + + do { + color = jQuery.curCSS(elem, attr); + + // Keep going until we find an element that has color, or we hit the body + if ( color != '' && color != 'transparent' || jQuery.nodeName(elem, "body") ) + break; + + attr = "backgroundColor"; + } while ( elem = elem.parentNode ); + + return getRGB(color); + } + + // Some named colors to work with + // From Interface by Stefan Petre + // http://interface.eyecon.ro/ + + var colors = { + aqua:[0,255,255], + azure:[240,255,255], + beige:[245,245,220], + black:[0,0,0], + blue:[0,0,255], + brown:[165,42,42], + cyan:[0,255,255], + darkblue:[0,0,139], + darkcyan:[0,139,139], + darkgrey:[169,169,169], + darkgreen:[0,100,0], + darkkhaki:[189,183,107], + darkmagenta:[139,0,139], + darkolivegreen:[85,107,47], + darkorange:[255,140,0], + darkorchid:[153,50,204], + darkred:[139,0,0], + darksalmon:[233,150,122], + darkviolet:[148,0,211], + fuchsia:[255,0,255], + gold:[255,215,0], + green:[0,128,0], + indigo:[75,0,130], + khaki:[240,230,140], + lightblue:[173,216,230], + lightcyan:[224,255,255], + lightgreen:[144,238,144], + lightgrey:[211,211,211], + lightpink:[255,182,193], + lightyellow:[255,255,224], + lime:[0,255,0], + magenta:[255,0,255], + maroon:[128,0,0], + navy:[0,0,128], + olive:[128,128,0], + orange:[255,165,0], + pink:[255,192,203], + purple:[128,0,128], + violet:[128,0,128], + red:[255,0,0], + silver:[192,192,192], + white:[255,255,255], + yellow:[255,255,0] + }; + +})(jQuery); diff --git a/setup/tools/filegen/templates/global.js/announcement.js b/setup/tools/filegen/templates/global.js/announcement.js new file mode 100644 index 00000000..82628760 --- /dev/null +++ b/setup/tools/filegen/templates/global.js/announcement.js @@ -0,0 +1,177 @@ +var Announcement = function(opt) +{ + if (!opt) + opt = {}; + + $WH.cO(this, opt); + + if (this.parent) + this.parentDiv = $WH.ge(this.parent); + else + return; + + if (g_user.id > 0 && (!g_cookiesEnabled() || g_getWowheadCookie('announcement-' + this.id) == 'closed')) + return; + + this.initialize(); +}; + +Announcement.prototype = { + initialize: function() + { + // aowow - animation fix + // this.parentDiv.style.display = 'none'; + this.parentDiv.style.opacity = '0'; + + if (this.mode === undefined || this.mode == 1) + this.parentDiv.className = 'announcement announcement-contenttop'; + else + this.parentDiv.className = 'announcement announcement-pagetop'; + + var div = this.innerDiv = $WH.ce('div'); + div.className = 'announcement-inner text'; + this.setStyle(this.style); + + var a = null; + var id = parseInt(this.id); + + if (g_user && (g_user.roles & (U_GROUP_ADMIN|U_GROUP_BUREAU)) > 0 && Math.abs(id) > 0) + { + if (id < 0) + { + a = $WH.ce('a'); + a.style.cssFloat = a.style.styleFloat = 'right'; + a.href = '?admin=announcements&id=' + Math.abs(id) + '&status=2'; + a.onclick = function() { return confirm('Are you sure you want to delete ' + this.name + '?'); }; + $WH.ae(a, $WH.ct('Delete')); + var small = $WH.ce('small'); + $WH.ae(small, a); + $WH.ae(div, small); + + a = $WH.ce('a'); + a.style.cssFloat = a.style.styleFloat = 'right'; + a.style.marginRight = '10px'; + a.href = '?admin=announcements&id=' + Math.abs(id) + '&status=' + (this.status == 1 ? 0 : 1); + a.onclick = function() { return confirm('Are you sure you want to delete ' + this.name + '?'); }; + $WH.ae(a, $WH.ct((this.status == 1 ? 'Disable' : 'Enable'))); + var small = $WH.ce('small'); + $WH.ae(small, a); + $WH.ae(div, small); + } + + a = $WH.ce('a'); + a.style.cssFloat = a.style.styleFloat = 'right'; + a.style.marginRight = '22px'; + a.href = '?admin=announcements&id=' + Math.abs(id) + '&edit'; + $WH.ae(a, $WH.ct('Edit announcement')); + var small = $WH.ce('small'); + $WH.ae(small, a); + $WH.ae(div, small); + } + + var markupDiv = $WH.ce('div'); + markupDiv.id = this.parent + '-markup'; + $WH.ae(div, markupDiv); + + if (id >= 0) + { + a = $WH.ce('a'); + + a.id = 'closeannouncement'; + a.href = 'javascript:;'; + a.className = 'announcement-close'; + if (this.nocookie) + a.onclick = this.hide.bind(this); + else + a.onclick = this.markRead.bind(this); + + $WH.ae(div, a); + g_addTooltip(a, LANG.close); + } + + $WH.ae(div, $WH.ce('div', { style: { clear: 'both' } })); + + $WH.ae(this.parentDiv, div); + + this.setText(this.text); + + setTimeout(this.show.bind(this), 500); // Delay to avoid visual lag + }, + + show: function() + { + // $(this.parentDiv).animate({ + // opacity: 'show', + // height: 'show' + // },{ + // duration: 333 + // }); + + // 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'; + + $WH.Track.nonInteractiveEvent({ + category: 'Announcements', + action: 'Show', + label: '' + this.name + }); + }, + + hide: function() + { + // $(this.parentDiv).animate({ + // opacity: 'hide', + // height: 'hide' + // },{ + // duration: 200 + // }); + + // aowow - animation fix - jQuery.animate hard snaps into place after half the time passed + this.parentDiv.style.opacity = '0'; + this.parentDiv.style.height = '0px'; + setTimeout(function() { + this.parentDiv.style.display = 'none'; + }.bind(this), 400); + }, + + markRead: function() + { + $WH.Track.interactiveEvent({ + category: 'Announcements', + action: 'Close', + label: '' + this.name + }); + g_setWowheadCookie('announcement-' + this.id, 'closed'); + this.hide(); + }, + + setStyle: function(style) + { + this.style = style; + this.innerDiv.setAttribute('style', style); + }, + + setText: function(text) + { + this.text = text; + Markup.printHtml(this.text, this.parent + '-markup'); + + 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/audio.js b/setup/tools/filegen/templates/global.js/audio.js new file mode 100644 index 00000000..106a93c4 --- /dev/null +++ b/setup/tools/filegen/templates/global.js/audio.js @@ -0,0 +1,377 @@ +var g_audiocontrols = { + __windowloaded: false, +}; +var g_audioplaylist = {}; + +// aowow - why is window.JSON here, wedged between the audio controls. It's only used for SearchBrowseButtons (and sourced by Listview) +if (!window.JSON) { + window.JSON = { + parse: function (sJSON) { + return eval("(" + sJSON + ")"); + }, + + stringify: function (obj) { + if (obj instanceof Object) + { + var str = ''; + if (obj.constructor === Array) + { + for (var i = 0; i < obj.length; str += this.stringify(obj[i]) + ',', i++) {} + return '[' + str.substr(0, str.length - 1) + ']'; + } + if (obj.toString !== Object.prototype.toString) + return '"' + obj.toString().replace(/"/g, '\\$&') + '"'; + + for (var e in obj) + str += '"' + e.replace(/"/g, '\\$&') + '":' + this.stringify(obj[e]) + ','; + + return '{' + str.substr(0, str.length - 1) + '}'; + } + + return typeof obj === 'string' ? '"' + obj.replace(/"/g, '\\$&') + '"' : String(obj); + } + } +} + +AudioControls = function () +{ + var fileIdx = -1; + var canPlay = false; + var looping = false; + var fullPlayer = false; + var autoStart = false; + var controls = {}; + var playlist = []; + var url = ''; + + function updatePlayer(_self, itr, doPlay) + { + var elAudio = $WH.ce('audio'); + elAudio.preload = 'none'; + elAudio.controls = 'true'; + $(elAudio).click(function (s) { s.stopPropagation() }); + elAudio.style.marginTop = '5px'; + + controls.audio.parentNode.replaceChild(elAudio, controls.audio); + controls.audio = elAudio; + $WH.aE(controls.audio, 'ended', setNextTrack.bind(_self)); + + if (doPlay) + { + elAudio.preload = 'auto'; + autoStart = true; + $WH.aE(controls.audio, 'canplaythrough', autoplay.bind(this)); + } + + if (!canPlay) + controls.table.style.visibility = 'visible'; + + var file; + do + { + fileIdx += itr; + if (fileIdx > playlist.length - 1) + { + fileIdx = 0; + if (!canPlay) + { + var div = $WH.ce('div'); + // div.className = 'minibox'; Aowow custom + div.className = 'minibox minibox-left'; + $WH.st(div, $WH.sprintf(LANG.message_browsernoaudio, file.type)); + controls.table.parentNode.replaceChild(div, controls.table); + return + } + } + + if (fileIdx < 0) + fileIdx = playlist.length - 1; + + file = playlist[fileIdx]; + } + while (controls.audio.canPlayType(file.type) == ''); + + var elSource = $WH.ce('source'); + elSource.src = file.url; + elSource.type = file.type; + $WH.ae(controls.audio, elSource); + + if (controls.hasOwnProperty('title')) + { + if (url) + { + $WH.ee(controls.title); + var a = $WH.ce('a'); + a.href = url; + $WH.st(a, '"' + file.title + '"'); + $WH.ae(controls.title, a); + } + else + $WH.st(controls.title, '"' + file.title + '"'); + } + + if (controls.hasOwnProperty('trackdisplay')) + $WH.st(controls.trackdisplay, '' + (fileIdx + 1) + ' / ' + playlist.length); + + if (!canPlay) + { + canPlay = true; + for (var i = fileIdx + 1; i <= playlist.length - 1; i++) + { + if (controls.audio.canPlayType(playlist[i].type)) + { + $(controls.controlsdiv).children('a').removeClass('button-red-disabled'); + break; + } + } + } + + if (controls.hasOwnProperty('addbutton')) + { + $(controls.addbutton).removeClass('button-red-disabled'); + // $WH.st(controls.addbutton, LANG.add); Aowow: doesnt work with RedButtons + RedButton.setText(controls.addbutton, LANG.add); + } + } + + function autoplay() + { + if (!autoStart) + return; + + autoStart = false; + controls.audio.play(); + } + + this.init = function (files, parent, opt) + { + if (!$WH.is_array(files)) + return; + + if (files.length == 0) + return; + + if ((parent.id == '') || g_audiocontrols.hasOwnProperty(parent.id)) + { + var i = 0; + while (g_audiocontrols.hasOwnProperty('auto-audiocontrols-' + (++i))) {} + parent.id = 'auto-audiocontrols-' + i; + } + + g_audiocontrols[parent.id] = this; + + if (typeof opt == 'undefined') + opt = {}; + + looping = !!opt.loop; + if (opt.hasOwnProperty('url')) + url = opt.url; + + playlist = files; + controls.div = parent; + + if (!opt.listview) + { + var tbl = $WH.ce('table', { className: 'audio-controls' }); + controls.table = tbl; + controls.table.style.visibility = 'hidden'; + $WH.ae(controls.div, tbl); + + var tr = $WH.ce('tr'); + $WH.ae(tbl, tr); + + var td = $WH.ce('td'); + $WH.ae(tr, td); + + controls.audio = $WH.ce('div'); + $WH.ae(td, controls.audio); + + controls.title = $WH.ce('div', { className: 'audio-controls-title' }); + $WH.ae(td, controls.title); + + controls.controlsdiv = $WH.ce('div', { className: 'audio-controls-pagination' }); + $WH.ae(td, controls.controlsdiv); + + var prevBtn = createButton(LANG.previous, true); + $WH.ae(controls.controlsdiv, prevBtn); + $WH.aE(prevBtn, 'click', this.btnPrevTrack.bind(this)); + + controls.trackdisplay = $WH.ce('div', { className: 'audio-controls-pagination-track' }); + $WH.ae(controls.controlsdiv, controls.trackdisplay); + + var nextBtn = createButton(LANG.next, true); + $WH.ae(controls.controlsdiv, nextBtn); + $WH.aE(nextBtn, 'click', this.btnNextTrack.bind(this)) + } + else + { + fullPlayer = true; + var div = $WH.ce('div'); + controls.table = div; + $WH.ae(controls.div, div); + + controls.audio = $WH.ce('div'); + $WH.ae(div, controls.audio); + + controls.trackdisplay = opt.trackdisplay; + controls.controlsdiv = $WH.ce('span'); + $WH.ae(div, controls.controlsdiv); + } + + if (g_audioplaylist.isEnabled() && !opt.fromplaylist) + { + var addBtn = createButton(LANG.add); + $WH.ae(controls.controlsdiv, addBtn); + $WH.aE(addBtn, 'click', this.btnAddToPlaylist.bind(this, addBtn)); + controls.addbutton = addBtn; + + if (fullPlayer) + addBtn.style.verticalAlign = '50%'; + } + + if (g_audiocontrols.__windowloaded) + this.btnNextTrack(); + }; + + function setNextTrack() + { + updatePlayer(this, 1, (looping || (fileIdx < (playlist.length - 1)))); + } + + this.btnNextTrack = function () + { + updatePlayer(this, 1, (canPlay && (controls.audio.readyState > 1) && (!controls.audio.paused))); + }; + + this.btnPrevTrack = function () + { + updatePlayer(this, -1, (canPlay && (controls.audio.readyState > 1) && (!controls.audio.paused))); + }; + + this.btnAddToPlaylist = function (_self) + { + if (fullPlayer) + { + for (var i = 0; i < playlist.length; i++) + g_audioplaylist.addSound(playlist[i]); + } + else + g_audioplaylist.addSound(playlist[fileIdx]); + + _self.className += ' button-red-disabled'; + // $WH.st(_self, LANG.added); // Aowow doesn't work with RedButtons + RedButton.setText(_self, LANG.added); + }; + + this.isPlaying = function () + { + return !controls.audio.paused; + }; + + this.removeSelf = function () + { + controls.table.parentNode.removeChild(controls.table); + delete g_audiocontrols[controls.div]; + }; + + function createButton(text, disabled) + { + return $WH.g_createButton(text, null, { + disabled: disabled, + // 'float': false, Aowow - adapted style + // style: 'margin:0 12px; display:inline-block' + style: 'margin:0 12px; display:inline-block; float:inherit; ' + }); + } +}; + +$WH.aE(window, 'load', function () +{ + g_audiocontrols.__windowloaded = true; + for (var i in g_audiocontrols) + if (i.substr(0, 2) != '__') + g_audiocontrols[i].btnNextTrack(); +}); + +AudioPlaylist = function () +{ + var enabled = false; + var playlist = []; + var player, container; + + this.init = function () + { + if (!$WH.localStorage.isSupported()) + return; + + enabled = true; + + var tracks; + if (tracks = $WH.localStorage.get('AudioPlaylist')) + playlist = JSON.parse(tracks); + }; + + this.savePlaylist = function () + { + if (!enabled) + return false; + + $WH.localStorage.set('AudioPlaylist', JSON.stringify(playlist)); + }; + + this.isEnabled = function () + { + return enabled; + }; + + this.addSound = function (track) + { + if (!enabled) + return false; + + this.init(); + playlist.push(track); + this.savePlaylist(); + }; + + this.deleteSound = function (idx) + { + if (idx < 0) + playlist = []; + else + playlist.splice(idx, 1); + + this.savePlaylist(); + + if (!player.isPlaying()) + { + player.removeSelf(); + this.setAudioControls(container); + } + + if (playlist.length == 0) + $WH.Tooltip.hide(); + }; + + this.getList = function () + { + var buf = []; + for (var i = 0; i < playlist.length; i++) + buf.push(playlist[i].title); + + return buf; + }; + + this.setAudioControls = function (parent) + { + if (!enabled) + return false; + + container = parent; + player = new AudioControls(); + player.init(playlist, container, { loop: true, fromplaylist: true }); + }; +}; + +g_audioplaylist = (new AudioPlaylist); +g_audioplaylist.init(); diff --git a/setup/tools/filegen/templates/global.js/clicktocopy.js b/setup/tools/filegen/templates/global.js/clicktocopy.js new file mode 100644 index 00000000..d8c20bfd --- /dev/null +++ b/setup/tools/filegen/templates/global.js/clicktocopy.js @@ -0,0 +1,122 @@ +$WH.clickToCopy = function (el, textOrFn, opt) +{ + opt = opt || {}; + + $WH.aE(el, 'click', $WH.clickToCopy.copy.bind(null, el, textOrFn, opt)); + // $WH.preventSelectStart(el); + + el.classList.add('click-to-copy'); + + if (opt.modifyTooltip) + { + el._fixTooltip = function (e) { + return e + '
' + $WH.ce('span', { className: 'q2', innerHTML: $WH.clickToCopy.getTooltip(false, opt) }).outerHTML; + }; + + opt.overrideOtherTooltips = false; + } + + // aowow - fitted to old system + // $WH.Tooltips.attach( + $WH.Tooltip.simple( + el, + $WH.clickToCopy.getTooltip.bind(null, false, opt), + undefined, + // { + /* byCursor: ! */ opt.attachToElement, + // stopPropagation: opt.overrideOtherTooltips + // } + ); +}; + +$WH.clickToCopy.copy = function (el, textOrFn, opt, ev) +{ + ev.preventDefault(); + ev.stopPropagation(); + + if (textOrFn === undefined) + { + if (!el.childNodes[0] || !el.childNodes[0].textContent) + { + let text = 'Could not find text to copy.'; + // $WH.error(text, el); + + if (opt.attachToElement) + $WH.Tooltip.show(el, text, 'q10'); + else + $WH.Tooltip.showAtCursor(ev, text, 'q10'); + + return; + } + + textOrFn = el.childNodes[0].textContent; + } + else if (typeof textOrFn === 'function') + textOrFn = textOrFn(); + + $WH.copyToClipboard(textOrFn); + + if (opt.attachToElement) + $WH.Tooltip.show(el, $WH.clickToCopy.getTooltip(true, opt)); + else + $WH.Tooltip.showAtCursor(ev, $WH.clickToCopy.getTooltip(true, opt)); +}; + +$WH.clickToCopy.getTooltip = function (clicked, opt) +{ + let txt = ''; + let attr = undefined; + + if (clicked) + { + txt = ' ' + LANG.copied; + attr = { className: 'q1 icon-tick' }; + } + else + txt = LANG.clickToCopy; + + let tt = $WH.ce('div', attr, $WH.ct(txt)); + + if (opt.prefix) + { + tt.style.marginTop = '10px'; + let prefix = typeof opt.prefix === 'function' ? opt.prefix() : opt.prefix; + return prefix + tt.outerHTML; + } + + return tt.outerHTML; +}; + +$WH.copyToClipboard = function (text, t) +{ + if (!$WH.copyToClipboard.hiddenInput) + { + $WH.copyToClipboard.hiddenInput = $WH.ce('textarea', { className: 'hidden-element' }); + $WH.ae(document.body, $WH.copyToClipboard.hiddenInput); + } + + $WH.copyToClipboard.hiddenInput.value = text; + + let isEmpty = $WH.copyToClipboard.hiddenInput.value === ''; + if (isEmpty) + $WH.copyToClipboard.hiddenInput.value = LANG.nothingToCopy_tip; + + $WH.copyToClipboard.hiddenInput.focus(); + $WH.copyToClipboard.hiddenInput.select(); + + if (!document.execCommand('copy')) + prompt(null, text); + + $WH.copyToClipboard.hiddenInput.blur(); + + if (t) + { + if (isEmpty) + $WH.Tooltips.showFadingTooltipAtCursor(LANG.nothingToCopy_tip, t, 'q10'); + else + { + let e = $WH.ce('span', { className: 'q1 icon-tick' }, $WH.ct(' ' + LANG.copied)); + $WH.Tooltips.showFadingTooltipAtCursor(e.outerHTML, t); + } + } +}; diff --git a/setup/tools/filegen/templates/global.js/comments.js b/setup/tools/filegen/templates/global.js/comments.js new file mode 100644 index 00000000..352a6be2 --- /dev/null +++ b/setup/tools/filegen/templates/global.js/comments.js @@ -0,0 +1,459 @@ +/* Note: comment replies are called "comments" because part of this code was taken from another project of mine. */ + +function SetupReplies(post, comment) +{ + SetupAddEditComment(post, comment, false); + SetupShowMoreComments(post, comment); + + post.find('.comment-reply-row').each(function () { SetupRepliesControls($(this), comment); }); + post.find('.comment-reply-row').hover(function () { $(this).find('span').attr('data-hover', 'true'); }, function () { $(this).find('span').attr('data-hover', 'false'); }); +} + +function SetupAddEditComment(post, comment, edit) +{ + /* Variables that will be set by Initialize() */ + var Form = null; + var Body = null; + var AddButton = null; + var TextCounter = null; + var AjaxLoader = null; + var FormContainer = null; + var DialogTableRowContainer = null; + + /* Constants */ + var MIN_LENGTH = 15; + var MAX_LENGTH = 600; + + /* State keeping booleans */ + var Initialized = false; + var Active = false; + var Flashing = false; + var Submitting = false; + + /* Shortcuts */ + var CommentsTable = post.find('.comment-replies > table'); + var AddCommentLink = post.find('.add-reply'); + var CommentsCount = comment.replies.length; + + if(edit) + Open(); + else + AddCommentLink.click(function () { Open(); }); + + function Initialize() + { + if (Initialized) + return; + + Initialized = true; + + var row = $('
'); + + if(edit) + row.addClass('comment-reply-row').addClass('reply-edit-row'); + + row.html('' + + ''); + + /* Set up the various variables for the controls we just created */ + Body = row.find('.comment-form textarea'); + AddButton = row.find('.comment-form input[type=submit]'); + TextCounter = row.find('.comment-form span.text-counter'); + Form = row.find('.comment-form form'); + AjaxLoader = row.find('.comment-form .ajax-loader'); + FormContainer = row.find('.comment-form'); + + /* Intercept submits */ + Form.submit(function () { Submit(); return false; }); + + UpdateTextCounter(); + + /* This is kinda a mess.. Every browser seems to implement keyup, keydown and keypress differently. + * - keyup: We need to use keyup to update the text counter for the simple reason we want to update it only when the user stops typing. + * - keydown: We need to use keydown to detect the ESC key because it's the only one that works in all browsers for ESC + * - keypress: We need to use keypress to detect Enter because it's the only one that 1) Works 2) Allows us to prevent a new line from being entered in the textarea + * I find it very funny that in each scenario there is only one of the 3 that works, and that that one is always different from the others. + */ + + Body.keyup(function (e) { UpdateTextCounter(); }); + Body.keydown(function (e) { if (e.keyCode == 27) { Close(); return false; } }); // ESC + Body.keypress(function (e) { if (e.keyCode == 13) { Submit(); return false; } }); // ENTER + + if(edit) + { + post.after(row); + post.hide(); + Form.find('textarea').text(comment.replies[post.attr('data-idx')].body); + } + else + CommentsTable.append(row); + + DialogTableRowContainer = row; + Form.find('textarea').focus(); + } + + function Open() + { + if (!Initialized) + Initialize(); + + Active = true; + + if(!edit) + { + AddCommentLink.hide(); + post.find('.comment-replies').show(); + FormContainer.show(); + FormContainer.find('textarea').focus(); + } + } + + function Close() + { + Active = false; + + if(edit) + { + if(DialogTableRowContainer) + DialogTableRowContainer.remove(); + post.show(); + return; + } + + AddCommentLink.show(); + FormContainer.hide(); + + if (CommentsCount == 0) + post.find('.comment-replies').hide(); + } + + function Submit() + { + if (!Active || Submitting) + return; + + if (Body.val().length < MIN_LENGTH || Body.val().length > MAX_LENGTH) + { + /* Flash the char counter to attract the attention of the user. */ + if (!Flashing) + { + Flashing = true; + TextCounter.animate({ opacity: '0.0' }, 150); + TextCounter.animate({ opacity: '1.0' }, 150, null, function() { Flashing = false; }); + } + + return false; + } + + SetSubmitState(); + $.ajax({ + type: 'POST', + url: edit ? '?comment=edit-reply' : '?comment=add-reply', + data: { commentId: comment.id, replyId: (edit ? post.attr('data-replyid') : 0), body: Body.val() }, + success: function (newReplies) { OnSubmitSuccess(newReplies); }, + dataType: 'json', + error: function (jqXHR) { OnSubmitFailure(jqXHR.responseText); } + }); + return true; + } + + function SetSubmitState() + { + Submitting = true; + AjaxLoader.show(); + AddButton.attr('disabled', 'disabled'); + FormContainer.find('.message-box').remove(); + } + + function ClearSubmitState() + { + Submitting = false; + AjaxLoader.hide(); + AddButton.removeAttr('disabled'); + } + + function OnSubmitSuccess(newReplies) + { + comment.replies = newReplies; + Listview.templates.comment.updateReplies(comment); + } + + function OnSubmitFailure(error) + { + ClearSubmitState(); + MessageBox(FormContainer, error); + } + + function UpdateTextCounter() + { + var text = '(error)'; + var cssClass = 'q0'; + var chars = Body.val().replace(/(\s+)/g, ' ').replace(/^\s*/, '').replace(/\s*$/, '').length; + var charsLeft = MAX_LENGTH - chars; + + if (chars == 0) + text = $WH.sprintf(LANG.replylength1_format, MIN_LENGTH); + else if (chars < MIN_LENGTH) + text = $WH.sprintf(LANG.replylength2_format, MIN_LENGTH - chars); + else + { + text = $WH.sprintf(charsLeft == 1 ? LANG.replylength4_format : LANG.replylength3_format, charsLeft); + + if (charsLeft < 120) + cssClass = 'q10'; + else if (charsLeft < 240) + cssClass = 'q5'; + else if (charsLeft < 360) + cssClass = 'q11'; + } + + TextCounter.html(text).attr('class', cssClass); + } +} + +function SetupShowMoreComments(post, comment) +{ + var ShowMoreCommentsLink = post.find('.show-more-replies'); + var CommentCell = post.find('.comment-replies'); + + ShowMoreCommentsLink.click(function () { ShowMoreComments(); }); + + function ShowMoreComments() + { + /* Replace link with ajax loader */ + ShowMoreCommentsLink.hide(); + CommentCell.append(CreateAjaxLoader()); + + $.ajax({ + type: 'GET', + url: '?comment=show-replies', + data: { id: comment.id }, + success: function (replies) { comment.replies = replies; Listview.templates.comment.updateReplies(comment); }, + dataType: 'json', + error: function () { OnFetchFail(); } + }); + } + + function OnFetchFail() + { + ShowMoreCommentsLink.show(); + CommentCell.find('.ajax-loader').remove(); + + MessageBox(CommentCell, "There was an error fetching the comments. Try refreshing the page."); + } +} + +function SetupRepliesControls(post, comment) +{ + var CommentId = post.attr('data-replyid'); + var VoteUpControl = post.find('.reply-upvote'); + var VoteDownControl = post.find('.reply-downvote'); + var FlagControl = post.find('.reply-report'); + var CommentScoreText = post.find('.reply-rating'); + var CommentActions = post.find('.reply-controls'); + var DeleteButton = post.find('.reply-delete'); + var EditButton = post.find('.reply-edit'); + var Voting = false; + var Deleting = false; + // aowow - detach functionality is custom + var Detaching = false; + var DetachButton = post.find('.reply-detach'); + var Container = comment.repliesCell; + + EditButton.click(function() { + SetupAddEditComment(post, comment, true); + }); + + FlagControl.click(function () + { + if (Voting || !confirm(LANG.replyreportwarning_tip)) + return; + + Voting = true; + $.ajax({ + type: 'POST', + url: '?comment=flag-reply', + data: { id: CommentId }, + success: function () { OnFlagSuccessful(); }, + error: function (jqXHR) { OnError(jqXHR.responseText); } + }); + }); + + VoteUpControl.click(function () + { + if (VoteUpControl.attr('data-hasvoted') == 'true' || VoteUpControl.attr('data-canvote') != 'true' || Voting) + return; + + Voting = true; + $.ajax({ + type: 'POST', + url: '?comment=upvote-reply', + data: { id: CommentId }, + success: function () { OnVoteSuccessful(1); }, + error: function (jqXHR) { OnError(jqXHR.responseText); } + }); + }); + + VoteDownControl.click(function () + { + if (VoteDownControl.attr('data-hasvoted') == 'true' || VoteDownControl.attr('data-canvote') != 'true' || Voting) + return; + + Voting = true; + $.ajax({ + type: 'POST', + url: '?comment=downvote-reply', + data: { id: CommentId }, + success: function () { OnVoteSuccessful(-1); }, + error: function (jqXHR) { OnError(jqXHR.responseText); } + }); + }); + + DetachButton.click(function () + { + if (Detaching) { + MessageBox(CommentActions, LANG.message_cantdetachcomment); + return; + } + + if (!confirm(LANG.confirm_detachcomment)) { + return; + } + + Detaching = true; + $.ajax({ + type: 'POST', + url: '?comment=detach-reply', + data: { id: CommentId }, + success: function () { OnDetachSuccessful(); }, + error: function (jqXHR) { OnError(jqXHR.responseText); } + }); + }); + + DeleteButton.click(function () + { + if (Deleting) + return; + + if (!confirm(LANG.deletereplyconfirmation_tip)) + return; + + Deleting = true; + $.ajax({ + type: 'POST', + url: '?comment=delete-reply', + data: { id: CommentId }, + success: function () { OnDeleteSuccessful(); }, + error: function (jqXHR) { OnError(jqXHR.responseText); } + }); + }); + + function OnVoteSuccessful(ratingChange) + { + var rating = parseInt(CommentScoreText.text()); + + rating += ratingChange; + + CommentScoreText.text(rating); + + if(ratingChange > 0) + VoteUpControl.attr('data-hasvoted', 'true'); + else + VoteDownControl.attr('data-hasvoted', 'true'); + + VoteUpControl.attr('data-canvote', 'false'); + VoteDownControl.attr('data-canvote', 'false'); + + if(ratingChange > 0) + FlagControl.remove(); + Voting = false; + } + + function OnFlagSuccessful() + { + Voting = false; + FlagControl.remove(); + } + + function OnDetachSuccessful() + { + post.remove(); + MessageBox(Container, LANG.message_commentdetached); + Detaching = false; + } + + function OnDeleteSuccessful() + { + post.remove(); + Deleting = false; + } + + function OnError(text) + { + Voting = false; + Deleting = false; + Detaching = false; + + if (!text) + text = LANG.genericerror; + + MessageBox(CommentActions, text); + } +} + +/* +Global comment-related functions +*/ + +function co_addYourComment() +{ + tabsContribute.focus(0); + var ta = $WH.gE(document.forms['addcomment'], 'textarea')[0]; + ta.focus(); +} + +function co_validateForm(f) +{ + var ta = $WH.gE(f, 'textarea')[0]; + + // prevent locale comments on guide pages + var locale = Locale.getId(); + // aowow - disabled + // if(locale != LOCALE_ENUS && $(f).attr('action') && ($(f).attr('action').replace(/^.*type=([0-9]*).*$/i, '$1')) == 100) + if (false) + { + alert(LANG.message_cantpostlcomment_tip); + return false; + } + + if (g_user.permissions & 1) { + return true; + } + + if (Listview.funcBox.coValidate(ta)) { + return true; + } + + return false; +} + +// Display a warning if a user attempts to leave the page and he has started writing a message +$(document).ready(function() +{ + g_setupChangeWarning($("form[name=addcomment]"), [$("textarea[name=commentbody]")], LANG.message_startedpost); +}); diff --git a/setup/tools/filegen/templates/global.js/conditionList.js b/setup/tools/filegen/templates/global.js/conditionList.js new file mode 100644 index 00000000..f899593f --- /dev/null +++ b/setup/tools/filegen/templates/global.js/conditionList.js @@ -0,0 +1,327 @@ +/* aowow - custom: TrinityCore Conditions */ +var ConditionList = new function() { + var self = this, + _conditions = null; + + self.createCell = function(conditions) + { + if (!conditions) + return null; + + _conditions = conditions; + + return _createCell(); + }; + + self.createTab = function(conditions) + { + if (!conditions) + return null; + + _conditions = conditions; + + return _createTab(); + }; + + function _makeList(mask, src, tpl) + { + var arr = Listview.funcBox.assocBinFlags(mask, src).sort(), + buff = ''; + + for (var i = 0, len = arr.length; i < len; ++i) + { + if (len > 1 && i == len - 1) + buff += LANG.or; + else if (i > 0) + buff += LANG.comma; + + buff += $WH.sprintf(tpl, arr[i], src[arr[i]]); + } + + return buff; + } + + function _parseEntry(entry, targets, target) + { + var str = '', + negate = false, + strIdx = 0, + param = []; + + [strIdx, ...param] = entry; + + negate = strIdx < 0; + strIdx = Math.abs(strIdx); + + if (!g_conditions[strIdx]) + return 'unknown condition index #' + strIdx; + + switch (strIdx) + { + case 5: + var standings = {}; + for (let i in g_reputation_standings) + standings[i * 1 + 1] = g_reputation_standings[i]; + + param[1] = _makeList(entry[2], standings, '$2'); + break; + + case 6: + if (entry[1] == 1) + param[0] = $WH.sprintf('[span class=icon-alliance]$1[/span]', g_sides[1]); + else if (entry[1] == 2) + param[0] = $WH.sprintf('[span class=icon-horde]$1[/span]', g_sides[2]); + else + param[0] = $WH.sprintf('[span class=icon-alliance]$1[/span]$2[span class=icon-horde]$3[/span]', g_sides[1], LANG.or, g_sides[2]); + break; + + case 10: + param[0] = g_drunk_states[entry[1]] ?? 'UNK DRUNK STATE'; + break; + + case 13: + param[2] = g_instance_info[entry[3]] ?? 'UNK INSTANCE INFO'; + break; + + case 15: + param[0] = _makeList(entry[1], g_chr_classes, '[class=$1]'); + break; + + case 16: + param[0] = _makeList(entry[1], g_chr_races, '[race=$1]'); + break; + + case 20: + if (entry[1] == 0) + param[0] = $WH.sprintf('[span class=icon-$1]$2[/span]', g_file_genders[0], LANG.male); + else if (entry[1] == 1) + param[0] = $WH.sprintf('[span class=icon-$1]$2[/span]', g_file_genders[1], LANG.female); + else + param[0] = g_npc_types[10]; // not specified + break; + + case 21: + var states = {}; + for (let i in g_unit_states) + states[i * 1 + 1] = g_unit_states[i]; + + param[0] = _makeList(entry[1], states, '$2'); + break; + + case 22: + if (entry[2]) + param[0] = '[zone=' + entry[2] + ']'; + else + param[0] = g_zone_categories[entry[1]] ?? 'UNK ZONE'; + break; + + case 24: + param[0] = g_npc_types[entry[1]] ?? 'UNK NPC TYPE'; + break; + + case 26: + var idx = 0, buff = []; + while (entry[1] >= (1 << idx)) { + if (!(entry[1] & (1 << idx++))) + continue; + + buff.push(idx); + } + param[0] = buff ? buff.join(LANG.comma) : ''; + break; + + case 27: + case 37: + case 38: + param[1] = g_operators[entry[2]]; + break; + + case 31: + if (entry[2] && entry[1] == 3) + param[0] = '[npc=' + entry[2] + ']'; + else if (entry[2] && entry[1] == 5) + param[0] = '[object=' + entry[2] + ']'; + else + param[0] = g_world_object_types[entry[1]] ?? 'UNK TYPEID'; + break; + + case 32: + var objectTypes = {}; + for (let i in g_world_object_types) + objectTypes[i * 1 + 1] = g_world_object_types[i]; + + param[0] = _makeList(entry[1], objectTypes, '$2'); + break; + + case 33: + param[0] = targets[entry[1]]; + param[1] = g_relation_types[entry[2]] ?? 'UNK RELATION'; + param[2] = targets[target]; + break; + + case 34: + param[0] = targets[entry[1]]; + + var standings = {}; + for (let i in g_reputation_standings) + standings[i * 1 + 1] = g_reputation_standings[i]; + param[1] = _makeList(entry[2], standings, '$2'); + break; + + case 35: + param[0] = targets[entry[1]]; + param[2] = g_operators[entry[3]]; + break; + + case 42: + if (!entry[1]) + param[0] = g_stand_states[entry[2]] ?? 'UNK STAND_STATE'; + else if (entry[1] == 1) + param[0] = g_stand_states[entry[2] ? 1 : 0]; + else + param[0] = ''; + break; + + case 47: + var quest_states = {}; + for (let i in g_quest_states) + quest_states[i * 1 + 1] = g_quest_states[i]; + + param[1] = _makeList(entry[2], quest_states, '$2'); + break; + } + + str = g_conditions[strIdx]; + + // fill in params + return $WH.sprintfa(str, param[0], param[1], param[2], param[3]) + // resolve NegativeCondition + .replace(/\$N([^:]*):([^;]*);/g, '$' + (negate > 0 ? 2 : 1)) + // resolve vars + .replace(/\$C(\d+)([^:]*):([^;]*);/g, (_, i, y, n) => (i > 0 ? y : n)); + } + + function _createTab() + { + var buff = ''; + + // tabs for conditionsTypes + for (g in _conditions) + { + if (!g_condition_sources[g]) + continue; + + let k = 0; + for (h in _conditions[g]) + { + var srcGroup, srcEntry, srcId, target, + targets, desc, + nGroups = Object.keys(_conditions[g][h]).length, + curGroup = 1; + + [srcGroup, srcEntry, srcId, target] = h.split(':').map((x) => parseInt(x)); + [targets, desc] = g_condition_sources[g]; + + // resolve targeting + let src = desc.replace(/\$T([^:]*):([^;]*);/, (_, t1, t2) => (target ? t2 : t1).replace('%', targets[target])); + let rand = $WH.rs(); + + buff += '[h3][toggler' + (k ? '=hidden' : '') + ' id=' + rand + ']' + $WH.sprintfa(src, srcGroup, srcEntry, srcId) + '[/toggler][/h3][div' + (k++ ? '=hidden' : '') + ' id=' + rand + ']'; + + if (nGroups > 1) + { + buff += LANG.note_condition_group + '[br][br]'; + buff += '[table class=grid]'; + } + + // table for elseGroups + for (i in _conditions[g][h]) + { + var group = _conditions[g][h][i], + nEntries = Object.keys(_conditions[g][h][i]).length; + + if (nGroups <= 1 && nEntries > 1) + buff += '[div style="padding-left:15px"]' + LANG.note_condition + '[/div]'; + if (nGroups > 1) + buff += '[tr][td width=70px valign=middle align=center]' + LANG.group + ' ' + (curGroup++) + LANG.colon + '[/td][td]'; + + // individual conditions + buff += '[ol]'; + for (j in group) + buff += '[li]' + _parseEntry(group[j], targets, target) + '[/li]'; + buff += '[/ol]'; + + if (nGroups > 1) + buff += '[/td][/tr]'; + } + + if (nGroups > 1) + buff += '[/tr][/table]'; + + buff += '[/div]'; + } + } + + return buff; + } + + function _createCell() + { + var rows = []; + + // tabs for conditionsTypes + for (let g in _conditions) + { + if (!g_condition_sources[g]) + continue; + + for (let h in _conditions[g]) + { + var target, targets, + + [, , , target] = h.split(':').map((x) => parseInt(x)); + [targets, ] = g_condition_sources[g]; + + let nElseGroups = Object.keys(_conditions[g][h]).length + + // table for elseGroups + for (let i in _conditions[g][h]) + { + let subGroup = [], + group = _conditions[g][h][i], + nEntries = Object.keys(_conditions[g][h][i]).length + buff = ''; + + if (nElseGroups > 1) + { + let rand = $WH.rs(); + buff += '[toggler' + (i > 0 ? '=hidden' : '') + ' id=cell-' + rand + ']' + (i > 0 ? LANG.cnd_or : LANG.cnd_either) + '[/toggler][div' + (i > 0 ? '=hidden' : '') + ' id=cell-' + rand + ']'; + } + + // individual conditions + for (let j in group) + subGroup.push(_parseEntry(group[j], targets, target)); + + for (j in subGroup) + { + if (nEntries > 1 && j > 0 && j == subGroup.length - 1) + buff += LANG.and + '[br]'; + else if (nEntries > 1 && j > 0) + buff += ',[br]'; + + buff += subGroup[j]; + } + + if (nElseGroups > 1) + buff += '[/div]'; + + rows.push(buff); + } + } + } + + return rows.length > 1 ? rows.join('[br]') : rows[0]; + } + +} +/* end custom */ diff --git a/setup/tools/filegen/templates/global.js/contacttool.js b/setup/tools/filegen/templates/global.js/contacttool.js new file mode 100644 index 00000000..516bc733 --- /dev/null +++ b/setup/tools/filegen/templates/global.js/contacttool.js @@ -0,0 +1,550 @@ +var ContactTool = new function() +{ + this.general = 0; + this.comment = 1; + this.post = 2; + this.screenshot = 3; + this.character = 4; + this.video = 5; + this.guide = 6; + + var _dialog; + + var contexts = { + 0: [ // general + [1, true], // General feedback + [2, true], // Bug report + [8, true], // Article misinformation + [3, true], // Typo/mistranslation + [4, true], // Advertise with us + [5, true], // Partnership opportunities + [6, true], // Press inquiry + [7, true] // Other + ], + 1: [ // comment + [15, function(post) { return ((post.roles & U_GROUP_MODERATOR) == 0); }], // Advertising + [16, true], // Inaccurate + [17, true], // Out of date + [18, function(post) { return ((post.roles & U_GROUP_MODERATOR) == 0); }], // Spam + [19, function(post) { return ((post.roles & U_GROUP_MODERATOR) == 0); }], // Vulgar/inappropriate + [20, function(post) { return ((post.roles & U_GROUP_MODERATOR) == 0); }] // Other + ], + 2: [ // forum post + [30, function(post) { return (g_users && g_users[post.user] && (g_users[post.user].roles & U_GROUP_MODERATOR) == 0); }], // Advertising + [37, function(post) { return (g_users && g_users[post.user] && (g_users[post.user].roles & U_GROUP_MODERATOR) == 0 && (post.roles & U_GROUP_MODERATOR) == 0 && g_users[post.user].avatar == 2); }], // Avatar + [31, true], // Inaccurate + [32, true], // Out of date + [33, function(post) { return (g_users && g_users[post.user] && (g_users[post.user].roles & U_GROUP_MODERATOR) == 0); }], // Spam + [34, function(post) { return (g_users && g_users[post.user] && (g_users[post.user].roles & U_GROUP_MODERATOR) == 0 && post.op && !post.sticky); }], // Sticky request + [35, function(post) { return (g_users && g_users[post.user] && (g_users[post.user].roles & U_GROUP_MODERATOR) == 0); }], // Vulgar/inappropriate + [36, function(post) { return (g_users && g_users[post.user] && (g_users[post.user].roles & U_GROUP_MODERATOR) == 0);}] // Other + ], + 3: [ // screenshot + [45, true], // Inaccurate, + [46, true], // Out of date, + [47, function(screen) { return (g_users && g_users[screen.user] && (g_users[screen.user].roles & U_GROUP_MODERATOR) == 0); }], // Vulgar/inappropriate + [48, function(screen) { return (g_users && g_users[screen.user] && (g_users[screen.user].roles & U_GROUP_MODERATOR) == 0); }] // Other + ], + 4: [ // character + [60, true], // Inaccurate completion data + [61, true] // Other + ], + 5: [ // video + [45, true], // Inaccurate, + [46, true], // Out of date, + [47, function(video) { return (g_users && g_users[video.user] && (g_users[video.user].roles & U_GROUP_MODERATOR) == 0); }], // Vulgar/inappropriate + [48, function(video) { return (g_users && g_users[video.user] && (g_users[video.user].roles & U_GROUP_MODERATOR) == 0); }] // Other + ], + 6: [ // Guide + [45, true], // Inaccurate, + [46, true], // Out of date, + [48, true] // Other + ] + }; + + var errors = { + 1: LANG.ct_resp_error1, + 2: LANG.ct_resp_error2, + 3: LANG.ct_resp_error3, + 7: LANG.ct_resp_error7 + }; + + var oldHash = null; + + this.displayError = function(field, message) + { + alert(message); + } + + this.onShow = function() + { + if (location.hash && location.hash != '#contact') + oldHash = location.hash; + if (this.data.mode == 0) + location.replace('#contact'); + } + + this.onHide = function() + { + if (oldHash && (oldHash.indexOf('screenshots:') == -1 || oldHash.indexOf('videos:') == -1)) + location.replace(oldHash); + else + location.replace('#.'); + } + + this.onSubmit = function(data, button, form) + { + if (data.submitting) + return false; + + for (var i = 0; i < form.elements.length; ++i) + form.elements[i].disabled = true; + + var params = [ + 'contact=1', + 'mode=' + $WH.urlencode(data.mode), + 'reason=' + $WH.urlencode(data.reason), + 'desc=' + $WH.urlencode(data.description), + 'ua=' + $WH.urlencode(navigator.userAgent), + 'appname=' + $WH.urlencode(navigator.appName), + 'page=' + $WH.urlencode(data.currenturl) + ]; + + if (data.mode == 0) // contact us + { + if (data.relatedurl) + params.push('relatedurl=' + $WH.urlencode(data.relatedurl)); + if (data.email) + params.push('email=' + $WH.urlencode(data.email)); + } + else if (data.mode == 1) // comment + params.push('id=' + $WH.urlencode(data.comment.id)); + else if (data.mode == 2) // forum post + params.push('id=' + $WH.urlencode(data.post.id)); + else if (data.mode == 3) // screenshot + params.push('id=' + $WH.urlencode(data.screenshot.id)); + else if (data.mode == 4) // character + params.push('id=' + $WH.urlencode(data.profile.source)); + else if (data.mode == 5) // video + params.push('id=' + $WH.urlencode(data.video.id)); + else if (data.mode == 6) // guide + params.push('id=' + $WH.urlencode(data.guide.id)); + + data.submitting = true; + var url = '?contactus'; + new Ajax(url, { + method: 'POST', + params: params.join('&'), + onSuccess: function(xhr, opt) { + var resp = xhr.responseText; + if (resp == 0) + { + if (g_user.name) + alert($WH.sprintf(LANG.ct_dialog_thanks_user, g_user.name)); + else + alert(LANG.ct_dialog_thanks); + + Lightbox.hide(); + } + else + { + if (errors[resp]) + alert(errors[resp]); + else + alert('Error: ' + resp); + } + }, + onFailure: function(xhr, opt) { + alert('Failure submitting contact request: ' + xhr.statusText); + }, + onComplete: function(xhr, opt) { + for (var i = 0; i < form.elements.length; ++i) + form.elements[i].disabled = false; + + data.submitting = false; + } + }); + return false; + } + + this.show = function(opt) + { + if (!opt) + opt = {}; + + var data = { mode: 0 }; + $WH.cO(data, opt); + data.reasons = contexts[data.mode]; + if (location.href.indexOf('#contact') != -1) + data.currenturl = location.href.substr(0, location.href.indexOf('#contact')); + else + data.currenturl = location.href; + + var form = 'contactus'; + if (data.mode != 0) + form = 'reportform'; + + if (!_dialog) + { + this.init(); + } + + _dialog.show(form, { + data: data, + onShow: this.onShow, + onHide: this.onHide, + onSubmit: this.onSubmit + }) + } + + this.checkPound = function() + { + if (location.hash && location.hash == '#contact') + { + ContactTool.show(); + } + } + + var dialog_contacttitle = LANG.ct_dialog_contactwowhead; + + this.init = function() + { + _dialog = new Dialog(); + + Dialog.templates.contactus = { + title: dialog_contacttitle, + width: 550, + buttons: [['okay', LANG.ok], ['cancel', LANG.cancel]], + + fields: [ + { + id: 'reason', + type: 'select', + label: LANG.ct_dialog_reason, + required: 1, + options: [], + compute: function(field, value, form, td) + { + $WH.ee(field); + + for (var i = 0; i < this.data.reasons.length; ++i) + { + var id = this.data.reasons[i][0]; + var check = this.data.reasons[i][1]; + var valid = false; + if (typeof check == 'function') + valid = check(this.extra); + else + valid = check; + + if (!valid) + continue; + + var o = $WH.ce('option'); + o.value = id; + if (value && value == id) + o.selected = true; + + $WH.ae(o, $WH.ct(g_contact_reasons[id])); + $WH.ae(field, o); + } + + field.onchange = function() + { + if (this.value == 1 || this.value == 2 || this.value == 3) + { + form.currenturl.parentNode.parentNode.style.display = ''; + form.relatedurl.parentNode.parentNode.style.display = ''; + } + else + { + form.currenturl.parentNode.parentNode.style.display = 'none'; + form.relatedurl.parentNode.parentNode.style.display = 'none'; + } + }.bind(field); + + td.style.width = '98%'; + }, + validate: function(newValue, data, form) + { + var error = ''; + if (!newValue || newValue.length == 0) + error = LANG.ct_dialog_error_reason; + + if (error == '') + return true; + + ContactTool.displayError(form.reason, error); + form.reason.focus(); + return false; + } + }, + { + id: 'currenturl', + type: 'text', + disabled: true, + label: LANG.ct_dialog_currenturl, + size: 40 + }, + { + id: 'relatedurl', + type: 'text', + label: LANG.ct_dialog_relatedurl, + caption: LANG.ct_dialog_optional, + size: 40, + validate: function(newValue, data, form) + { + var error = ''; + var urlRe = /^(http(s?)\:\/\/|\/)?([\w]+:\w+@)?([a-zA-Z]{1}([\w\-]+\.)+([\w]{2,5}))(:[\d]{1,5})?((\/?\w+\/)+|\/?)(\w+\.[\w]{3,4})?((\?\w+=\w+)?(&\w+=\w+)*)?/; + newValue = newValue.trim(); + if (newValue.length >= 250) + error = LANG.ct_dialog_error_relatedurl; + else if (newValue.length > 0 && !urlRe.test(newValue)) + error = LANG.ct_dialog_error_invalidurl; + + if (error == '') + return true; + + ContactTool.displayError(form.relatedurl, error); + form.relatedurl.focus(); + return false; + } + }, + { + id: 'email', + type: 'text', + label: LANG.ct_dialog_email, + caption: LANG.ct_dialog_email_caption, + compute: function(field, value, form, td, tr) + { + if (g_user.email) + { + this.data.email = g_user.email; + tr.style.display = 'none'; + } + else + { + var func = function() + { + $('#contact-emailwarn').css('display', g_isEmailValid($(form.email).val()) ? 'none' : ''); + Lightbox.reveal(); + }; + + $(field).keyup(func).blur(func); + } + }, + validate: function(newValue, data, form) + { + var error = ''; + newValue = newValue.trim(); + if (newValue.length >= 100) + error = LANG.ct_dialog_error_emaillen; + else if (newValue.length > 0 && !g_isEmailValid(newValue)) + error = LANG.ct_dialog_error_email; + + if (error == '') + return true; + + ContactTool.displayError(form.email, error); + form.email.focus(); + return false; + } + }, + { + id: 'description', + type: 'textarea', + caption: LANG.ct_dialog_desc_caption, + width: '98%', + required: 1, + size: [10, 30], + validate: function(newValue, data, form) + { + var error = ''; + newValue = newValue.trim(); + if (newValue.length == 0 || newValue.length > 10000) + error = LANG.ct_dialog_error_desc; + + if (error == '') + return true; + + ContactTool.displayError(form.description, error); + form.description.focus(); + return false; + } + }, + { + id: 'noemailwarning', + type: 'caption', + compute: function(field, value, form, td) + { + $(td).html('').css('white-space', 'normal').css('padding', '0 4px'); + } + } + ], + + onInit: function(form) + { + + }, + + onShow: function(form) + { + if (this.data.focus && form[this.data.focus]) + setTimeout(g_setCaretPosition.bind(null, form[this.data.focus], form[this.data.focus].value.length), 100); + else if (form['reason'] && !form.reason.value) + setTimeout($WH.bindfunc(form.reason.focus, form.reason), 10); + else if (form['relatedurl'] && !form.relatedurl.value) + setTimeout($WH.bindfunc(form.relatedurl.focus, form.relatedurl), 10); + else if (form['email'] && !form.email.value) + setTimeout($WH.bindfunc(form.email.focus, form.email), 10); + else if (form['description'] && !form.description.value) + setTimeout($WH.bindfunc(form.description.focus, form.description), 10); + + setTimeout(Lightbox.reveal, 250); + } + } + + Dialog.templates.reportform = { + title: LANG.ct_dialog_report, + width: 550, + // height: 360, + buttons: [['okay', LANG.ok], ['cancel', LANG.cancel]], + fields: [ + { + id: 'reason', + type: 'select', + label: LANG.ct_dialog_reason, + options: [], + compute: function(field, value, form, td) + { + switch (this.data.mode) + { + case 1: // comment + form.firstChild.innerHTML = $WH.sprintf(LANG.ct_dialog_reportcomment, '' + this.data.comment.user + ''); + break; + case 2: // forum post + var rep = '' + this.data.post.user + ''; + if (this.data.post.op) + form.firstChild.innerHTML = $WH.sprintf(LANG.ct_dialog_reporttopic, rep); + else + form.firstChild.innerHTML = $WH.sprintf(LANG.ct_dialog_reportpost, rep); + break; + case 3: // screenshot + form.firstChild.innerHTML = $WH.sprintf(LANG.ct_dialog_reportscreen, '' + this.data.screenshot.user + ''); + break; + case 4: // character + $WH.ee(form.firstChild); + $WH.ae(form.firstChild, $WH.ct(LANG.ct_dialog_reportchar)); + break; + case 5: // video + form.firstChild.innerHTML = $WH.sprintf(LANG.ct_dialog_reportvideo, '' + this.data.video.user + ''); + break; + case 6: // guide + form.firstChild.innerHTML = 'Report guide'; + break; + } + form.firstChild.setAttribute('style', ''); + + $WH.ee(field); + + var extra; + if (this.data.mode == 1) + extra = this.data.comment; + else if (this.data.mode == 2) + extra = this.data.post; + else if (this.data.mode == 3) + extra = this.data.screenshot; + else if (this.data.mode == 4) + extra = this.data.profile; + else if (this.data.mode == 5) + extra = this.data.video; + else if (this.data.mode == 6) + extra = this.data.guide; + + $WH.ae(field, $WH.ce('option', { selected: (!value), value: -1 })); + + for (var i = 0; i < this.data.reasons.length; ++i) + { + var id = this.data.reasons[i][0]; + var check = this.data.reasons[i][1]; + var valid = false; + if (typeof check == 'function') + valid = check(extra); + else + valid = check; + + if (!valid) + continue; + + var o = $WH.ce('option'); + o.value = id; + if (value && value == id) + o.selected = true; + + $WH.ae(o, $WH.ct(g_contact_reasons[id])); + $WH.ae(field, o); + } + + td.style.width = '98%'; + }, + validate: function(newValue, data, form) + { + var error = ''; + if (!newValue || newValue == -1 || newValue.length == 0) + error = LANG.ct_dialog_error_reason; + + if (error == '') + return true; + + ContactTool.displayError(form.reason, error); + form.reason.focus(); + return false; + } + }, + { + id: 'description', + type: 'textarea', + caption: LANG.ct_dialog_desc_caption, + width: '98%', + required: 1, + size: [10, 30], + validate: function(newValue, data, form) + { + var error = ''; + newValue = newValue.trim(); + if (newValue.length == 0 || newValue.length > 10000) + error = LANG.ct_dialog_error_desc; + + if (error == '') + return true; + + ContactTool.displayError(form.description, error); + form.description.focus(); + return false; + } + } + ], + + onInit: function(form) + { + + }, + + onShow: function(form) + { + /* Work-around for IE7 */ + var reason = $(form).find("*[name=reason]")[0]; + var description = $(form).find("*[name=description]")[0]; + + if (this.data.focus && form[this.data.focus]) + setTimeout(g_setCaretPosition.bind(null, form[this.data.focus], form[this.data.focus].value.length), 100); + else if (!reason.value) + setTimeout($WH.bindfunc(reason.focus, reason), 10); + else if (!description.value) + setTimeout($WH.bindfunc(description.focus, description), 10); + } + } + } + + $(document).ready(this.checkPound); +}; diff --git a/setup/tools/filegen/templates/global.js/cookies.js b/setup/tools/filegen/templates/global.js/cookies.js new file mode 100644 index 00000000..3cce1d3e --- /dev/null +++ b/setup/tools/filegen/templates/global.js/cookies.js @@ -0,0 +1,37 @@ +// TODO: Create a "Cookies" object + +function g_cookiesEnabled() +{ + document.cookie = 'enabledTest'; + return (document.cookie.indexOf("enabledTest") != -1) ? true : false; +} + +function g_getWowheadCookie(name) +{ + if (g_user.id > 0) + { + return g_user.cookies[name]; // no point checking if it exists, as undefined tests as false anyways + } + else + { + return $WH.gc(name); // plus gc does the same thing.. + } +} + +function g_setWowheadCookie(name, data, browser) +{ + var temp = name.substr(0, 5) == 'temp_'; + if (!browser && g_user.id > 0 && !temp) { + new Ajax('?cookie=' + name + '&' + name + '=' + $WH.urlencode(data), { + method: 'get', + onSuccess: function(xhr) { + if (xhr.responseText == 0) + g_user.cookies[name] = data; + } + }); + } + else if (browser || g_user.id == 0) + { + $WH.sc(name, 14, data, null, location.hostname); + } +} diff --git a/setup/tools/filegen/templates/global.js/dialog.js b/setup/tools/filegen/templates/global.js/dialog.js new file mode 100644 index 00000000..e46fa3a4 --- /dev/null +++ b/setup/tools/filegen/templates/global.js/dialog.js @@ -0,0 +1,568 @@ +var Dialog = function() +{ +var + _self = this, + _template, + _onSubmit = null, + _templateName, + + _funcs = {}, + _data, + + _inited = false, + _form = $WH.ce('form'), + _elements = {}; + + _form.onsubmit = function() { + _processForm(); + return false + }; + + this.show = function(template, opt) + { + if (template) + { + _templateName = template; + _template = Dialog.templates[_templateName]; + + _self.template = _template; + } + else + return; + + if (_template.onInit && !_inited) + (_template.onInit.bind(_self, _form, opt))(); + + if (opt.onBeforeShow) + _funcs.onBeforeShow = opt.onBeforeShow.bind(_self, _form); + + if (_template.onBeforeShow) + _template.onBeforeShow = _template.onBeforeShow.bind(_self, _form); + + if (opt.onShow) + _funcs.onShow = opt.onShow.bind(_self, _form); + + if (_template.onShow) + _template.onShow = _template.onShow.bind(_self, _form); + + if (opt.onHide) + _funcs.onHide = opt.onHide.bind(_self, _form); + + if (_template.onHide) + _template.onHide = _template.onHide.bind(_self, _form); + + if (opt.onSubmit) + _funcs.onSubmit = opt.onSubmit; + + if (_template.onSubmit) + _onSubmit = _template.onSubmit.bind(_self, _form); + + if (opt.data) + { + _inited = false; + _data = {}; + $WH.cO(_data, opt.data); + } + + _self.data = _data; + + Lightbox.show('dialog-' + _templateName, { + onShow: _onShow, + onHide: _onHide + }); + } + + this.getValue = function(id) + { + return _getValue(id); + } + + this.setValue = function(id, value) + { + _setValue(id, value); + } + + this.getSelectedValue = function(id) + { + return _getSelectedValue(id); + } + + this.getCheckedValue = function(id) + { + return _getCheckedValue(id); + } + + function _onShow(dest, first) + { + if (first || !_inited) + _initForm(dest); + + if (_template.onBeforeShow) + _template.onBeforeShow(); + + if (_funcs.onBeforeShow) + _funcs.onBeforeShow(); + + Lightbox.setSize(_template.width, _template.height); + dest.className = 'dialog'; + + _updateForm(); + + if (_template.onShow) + _template.onShow(); + + if (_funcs.onShow) + _funcs.onShow(); + } + + function _initForm(dest) + { + $WH.ee(dest); + $WH.ee(_form); + + var container = $WH.ce('div'); + container.className = 'text'; + $WH.ae(dest, container); + + $WH.ae(container, _form); + + if (_template.title) + { + var h = $WH.ce('h1'); + $WH.ae(h, $WH.ct(_template.title)); + $WH.ae(_form, h); + } + + var t = $WH.ce('table'), + tb = $WH.ce('tbody'), + mergeCell = false; + + $WH.ae(t, tb); + $WH.ae(_form, t); + + for (var i = 0, len = _template.fields.length; i < len; ++i) + { + var + field = _template.fields[i], + element; + + if (!mergeCell) + { + tr = $WH.ce('tr'); + th = $WH.ce('th'); + td = $WH.ce('td'); + } + + field.__tr = tr; + + if (_data[field.id] == null) + _data[field.id] = (field.value ? field.value : ''); + + var options; + if (field.options) + { + options = []; + + if (field.optorder) + $WH.cO(options, field.optorder); + else + { + for (var j in field.options) + options.push(j); + } + + if (field.sort) + options.sort(function(a, b) { return field.sort * $WH.strcmp(field.options[a], field.options[b]); }); + } + + switch (field.type) + { + case 'caption': + + th.colSpan = 2; + th.style.textAlign = 'left'; + th.style.padding = 0; + + if (field.compute) + (field.compute.bind(_self, null, _data[field.id], _form, th, tr))(); + else if (field.label) + $WH.ae(th, $WH.ct(field.label)); + + $WH.ae(tr, th); + $WH.ae(tb, tr); + + continue; + break; + + case 'textarea': + + var f = element = $WH.ce('textarea'); + + f.name = field.id; + + if (field.disabled) + f.disabled = true; + + f.rows = field.size[0]; + f.cols = field.size[1]; + + td.colSpan = 2; + + if (field.label) + { + th.colSpan = 2; + th.style.textAlign = 'left'; + th.style.padding = 0; + td.style.padding = 0; + + $WH.ae(th, $WH.ct(field.label)); + $WH.ae(tr, th); + $WH.ae(tb, tr); + + tr = $WH.ce('tr'); + } + + $WH.ae(td, f); + + break; + + case 'select': + + var f = element = $WH.ce('select'); + + f.name = field.id; + + if (field.size) + f.size = field.size; + + if (field.disabled) + f.disabled = true; + + if (field.multiple) + f.multiple = true; + + for (var j = 0, len2 = options.length; j < len2; ++j) + { + var o = $WH.ce('option'); + + o.value = options[j]; + + $WH.ae(o, $WH.ct(field.options[options[j]])); + $WH.ae(f, o) + } + + $WH.ae(td, f); + + break; + + case 'dynamic': + + td.colSpan = 2; + td.style.textAlign = 'left'; + td.style.padding = 0; + + if (field.compute) + (field.compute.bind(_self, null, _data[field.id], _form, td, tr))(); + + $WH.ae(tr, td); + $WH.ae(tb, tr); + + element = td; + + break; + + case 'checkbox': + case 'radio': + + var k = 0; + element = []; + for (var j = 0, len2 = options.length; j < len2; ++j) + { + var + s = $WH.ce('span'), + f, + l, + uniqueId = 'sdfler46' + field.id + '-' + options[j]; + + if (j > 0 && !field.noInputBr) + $WH.ae(td, $WH.ce('br')); + + l = $WH.ce('label'); + l.setAttribute('for', uniqueId); + l.onmousedown = $WH.rf; + + f = $WH.ce('input', { name: field.id, value: options[j], id: uniqueId }); + f.setAttribute('type', field.type); + + if (field.disabled) + f.disabled = true; + + if (field.submitOnDblClick) + l.ondblclick = f.ondblclick = function(e) { _processForm(); }; + + if (field.compute) + (field.compute.bind(_self, f, _data[field.id], _form, td, tr))(); + + $WH.ae(l, f); + $WH.ae(l, $WH.ct(field.options[options[j]])); + $WH.ae(td, l); + + element.push(f); + } + + break; + + default: // Textbox + + var f = element = $WH.ce('input'); + + f.name = field.id; + + if (field.size) + f.size = field.size; + + if (field.disabled) + f.disabled = true; + + if (field.submitOnEnter) + { + f.onkeypress = function(e) { + e = $WH.$E(e); + if (e.keyCode == 13) + _processForm(); + }; + } + + f.setAttribute('type', field.type); + + $WH.ae(td, f); + + break; + } + + if (field.label) + { + if (field.type == 'textarea') + { + if (field.labelAlign) + td.style.textAlign = field.labelAlign; + + td.colSpan = 2; + } + else + { + if (field.labelAlign) + th.style.textAlign = field.labelAlign; + + $WH.ae(th, $WH.ct(field.label)); + $WH.ae(tr, th); + } + } + + if (field.placeholder) + f.placeholder = field.placeholder; + + if (field.type != 'checkbox' && field.type != 'radio') + { + if (field.width) + f.style.width = field.width; + + if (field.compute && field.type != 'caption' && field.type != 'dynamic') + (field.compute.bind(_self, f, _data[field.id], _form, td, tr))(); + } + + if (field.caption) + { + var s = $WH.ce('small'); + if (field.type != 'textarea') + s.style.paddingLeft = '2px'; + s.className = 'q0'; // commented in 5.0? + $WH.ae(s, $WH.ct(field.caption)); + $WH.ae(td, s); + } + + $WH.ae(tr, td); + $WH.ae(tb, tr); + + mergeCell = field.mergeCell; + + _elements[field.id] = element; + } + + for (var i = _template.buttons.length; i > 0; --i) + { + var + button = _template.buttons[i - 1], + a = $WH.ce('a'); + + a.onclick = _processForm.bind(a, button[0]); + a.className = 'dialog-' + button[0]; + a.href = 'javascript:;'; + $WH.ae(a, $WH.ct(button[1])); + $WH.ae(dest, a); + } + + var _ = $WH.ce('div'); + _.className = 'clear'; + $WH.ae(dest, _); + + _inited = true; + } + + function _updateForm() + { + for (var i = 0, len = _template.fields.length; i < len; ++i) + { + var + field = _template.fields[i], + f = _elements[field.id]; + + switch (field.type) + { + case 'caption': // Do nothing + break; + + case 'select': + for (var j = 0, len2 = f.options.length; j < len2; j++) + f.options[j].selected = (f.options[j].value == _data[field.id] || $WH.in_array(_data[field.id], f.options[j].value) != -1); + break; + + case 'checkbox': + case 'radio': + for (var j = 0, len2 = f.length; j < len2; j++) + f[j].checked = (f[j].value == _data[field.id] || $WH.in_array(_data[field.id], f[j].value) != -1); + break; + + default: + f.value = _data[field.id]; + break; + } + + if (field.update) + (field.update.bind(_self, null, _data[field.id], _form, f))(); + } + } + + function _onHide() + { + if (_template.onHide) + _template.onHide(); + + if (_funcs.onHide) + _funcs.onHide(); + } + + function _processForm(button) + { + // if (button == 'x') // aowow - button naming differs + if (button == 'cancel') // Special case + return Lightbox.hide(); + + for (var i = 0, len = _template.fields.length; i < len; ++i) + { + var + field = _template.fields[i], + newValue; + + switch (field.type) + { + case 'caption': // Do nothing + continue; + + case 'select': + newValue = _getSelectedValue(field.id); + break; + + case 'checkbox': + case 'radio': + newValue = _getCheckedValue(field.id); + break; + + case 'dynamic': + if (field.getValue) + { + newValue = field.getValue(field, _data, _form); + break; + } + default: + newValue = _getValue(field.id); + break; + } + + if (field.validate) + { + if (!field.validate(newValue, _data, _form)) + return; + } + + if (newValue && typeof newValue == 'string') + newValue = $WH.trim(newValue); + + _data[field.id] = newValue; + } + + _submitData(button); + } + + function _submitData(button) + { + var ret; + + if (_onSubmit) + ret = _onSubmit(_data, button, _form); + + if (_funcs.onSubmit) + ret = _funcs.onSubmit(_data, button, _form); + + if (ret === undefined || ret) + Lightbox.hide(); + + return false; + } + + function _getValue(id) + { + return _elements[id].value; + } + + function _setValue(id, value) + { + _elements[id].value = value; + } + + function _getSelectedValue(id) + { + var + result = [], + f = _elements[id]; + + for (var i = 0, len = f.options.length; i < len; i++) + { + if (f.options[i].selected) + result.push(parseInt(f.options[i].value) == f.options[i].value ? parseInt(f.options[i].value) : f.options[i].value); + } + + if (result.length == 1) + result = result[0]; + + return result; + } + + function _getCheckedValue(id) + { + var + result = [], + f = _elements[id]; + + for (var i = 0, len = f.length; i < len; i++) + { + if (f[i].checked) + result.push(parseInt(f[i].value) == f[i].value ? parseInt(f[i].value) : f[i].value); + } + + return result; + } +}; + +Dialog.templates = {}; +Dialog.extraFields = {}; diff --git a/setup/tools/filegen/templates/global.js/dom_manipulation.js b/setup/tools/filegen/templates/global.js/dom_manipulation.js new file mode 100644 index 00000000..787c45e9 --- /dev/null +++ b/setup/tools/filegen/templates/global.js/dom_manipulation.js @@ -0,0 +1,252 @@ +/* +Global functions related to DOM manipulation, events & forms that jQuery doesn't already provide +*/ + +function g_addCss(css) +{ + var style = $WH.ce('style'); + style.type = 'text/css'; + + if (style.styleSheet) // ie + style.styleSheet.cssText = css; + else + $WH.ae(style, $WH.ct(css)); + + var head = $WH.gE(document, 'head')[0]; + $WH.ae(head, style); +} + +function g_setTextNodes(n, text) +{ + if (n.nodeType == 3) + n.nodeValue = text; + else + { + for (var i = 0; i < n.childNodes.length; ++i) + g_setTextNodes(n.childNodes[i], text); + } +} + +function g_setInnerHtml(n, text, nodeType) +{ + if (n.nodeName.toLowerCase() == nodeType) + n.innerHTML = text; + else + { + for (var i = 0; i < n.childNodes.length; ++i) + g_setInnerHtml(n.childNodes[i], text, nodeType); + } +} + +function g_getFirstTextContent(node) +{ + for (var i = 0; i < node.childNodes.length; ++i) + { + if (node.childNodes[i].nodeName == '#text') + return node.childNodes[i].nodeValue; + + var ret = g_getFirstTextContent(node.childNodes[i]); + if (ret) + return ret; + } + + return false; +} + +function g_getTextContent(el) +{ + var txt = ''; + for (var i = 0; i < el.childNodes.length; ++i) + { + if (el.childNodes[i].nodeValue) + txt += el.childNodes[i].nodeValue; + else if (el.childNodes[i].nodeName == 'BR') + txt += '\n'; + + txt += g_getTextContent(el.childNodes[i]); + } + + return txt; +} + +function g_toggleDisplay(el) +{ + el = $(el); + el.toggle(); + if (el.is(':visible')) + return true; + + return false; +} + +function g_enableScroll(enabled) +{ + if (!enabled) + { + $WH.aE(document, 'mousewheel', g_enableScroll.F); + $WH.aE(window, 'DOMMouseScroll', g_enableScroll.F); + } + else + { + $WH.dE(document, 'mousewheel', g_enableScroll.F); + $WH.dE(window, 'DOMMouseScroll', g_enableScroll.F); + } +} + +g_enableScroll.F = function(e) +{ + if (e.stopPropagation) + e.stopPropagation(); + if (e.preventDefault) + e.preventDefault(); + + e.returnValue = false; + e.cancelBubble = true; + + return false; +}; + +// from http://blog.josh420.com/archives/2007/10/setting-cursor-position-in-a-textbox-or-textarea-with-javascript.aspx +function g_setCaretPosition(elem, caretPos) +{ + if (!elem) + return; + + if (elem.createTextRange) + { + var range = elem.createTextRange(); + range.move('character', caretPos); + range.select(); + } + else if (elem.selectionStart != undefined) + { + elem.focus(); + elem.setSelectionRange(caretPos, caretPos); + } + else + elem.focus(); +} + +function g_insertTag(where, tagOpen, tagClose, repFunc) +{ + var n = $WH.ge(where); + + n.focus(); + if (n.selectionStart != null) + { + var s = n.selectionStart, + e = n.selectionEnd, + sL = n.scrollLeft, + sT = n.scrollTop; + + var selectedText = n.value.substring(s, e); + if (typeof repFunc == 'function') + selectedText = repFunc(selectedText); + + n.value = n.value.substr(0, s) + tagOpen + selectedText + tagClose + n.value.substr(e); + n.selectionStart = n.selectionEnd = e + tagOpen.length; + + n.scrollLeft = sL; + n.scrollTop = sT; + } + else if (document.selection && document.selection.createRange) + { + var range = document.selection.createRange(); + + if (range.parentElement() != n) + return; + + var selectedText = range.text; + if (typeof repFunc == 'function') + selectedText = repFunc(selectedText); + + range.text = tagOpen + selectedText + tagClose; +/* + range.moveEnd("character", -tagClose.length); + range.moveStart("character", range.text.length); + + range.select(); +*/ + } + + if (n.onkeyup) + n.onkeyup(); +} + +function g_onAfterTyping(input, func, delay) +{ + var timerId; + var ldsgksdgnlk623 = function() + { + if (timerId) + { + clearTimeout(timerId); + timerId = null; + } + timerId = setTimeout(func, delay); + }; + input.onkeyup = ldsgksdgnlk623; +} + +function g_onClick(el, func) +{ + var firstEvent = 0; + + function rightClk(n) + { + if (firstEvent) + { + if (firstEvent != n) + return; + } + else + firstEvent = n; + + func(true); + } + + el.onclick = function(e) + { + e = $WH.$E(e); + + if (e._button == 2) // middle click + return true; + + return false; + } + + el.oncontextmenu = function() + { + rightClk(1); + + return false; + } + + el.onmouseup = function(e) + { + e = $WH.$E(e); + + if (e._button == 3 || e.shiftKey || e.ctrlKey) // Right/Shift/Ctrl + { + rightClk(2); + } + else if (e._button == 1) // Left + { + func(false); + } + + return false; + } +} + +function g_isLeftClick(e) +{ + e = $WH.$E(e); + return (e && e._button == 1); +} + +function g_preventEmptyFormSubmission() // Used on the homepage and in the top bar +{ + if (!$.trim(this.elements[0].value)) + return false; +} diff --git a/setup/tools/filegen/templates/global.js/favorites.js b/setup/tools/filegen/templates/global.js/favorites.js new file mode 100644 index 00000000..2470302f --- /dev/null +++ b/setup/tools/filegen/templates/global.js/favorites.js @@ -0,0 +1,262 @@ +var Favorites = new function() +{ + var _type = null; + var _typeId = null; + var _favIcon = null; + + this.pageInit = function(h1, type, typeId) + { + if (typeof h1 == 'string') + { + if (!document.querySelector) + return; + + h1 = document.querySelector(h1); + } + + if (!h1 || typeof type != 'number' || typeof typeId != 'number') + return; + + _type = type; + _typeId = typeId; + + createIcon(h1); + } + + function initFavIcon() + { + var h1 = typeof g_pageInfo == 'object' && typeof g_pageInfo.type == 'number' && typeof g_pageInfo.typeId == 'number' ? document.querySelector('#main-contents h1') : null; + if (!h1) { + if (document.readyState !== 'complete') + setTimeout(initFavIcon, 9); + + return; + } + + _type = g_pageInfo.type; + _typeId = g_pageInfo.typeId; + + createIcon(h1); + } + + this.hasFavorites = function() + { + return !!g_favorites.length + } + + this.getMenu = function() + { + var favMenu = []; + var nGroups = 0; + var nEntries = 0; + + for (var i = 0, favGroup; favGroup = g_favorites[i]; i++) + { + if (!favGroup.entities.length) + continue; + + nGroups++; + var subMenu = []; + for (var j = 0, favEntry; favEntry = favGroup.entities[j]; j++) + { + subMenu.push([favEntry[0], favEntry[1], '?' + g_types[favGroup.id] + '=' + favEntry[0]]); + nEntries++ + } + + Menu.sort(subMenu); + favMenu.push([favGroup.id, LANG.types[favGroup.id][2], , subMenu]) + } + + Menu.sort(favMenu); + + // display short favorites as 1-dim list + if ((nGroups == 1 && nEntries <= 45) || (nGroups == 2 && nGroups + nEntries <= 30) || (nGroups > 2 && nGroups + nEntries <= 15)) + { + var list = []; + + for (var i = 0; subMenu = favMenu[i]; i++) + { + list.push([, subMenu[MENU_IDX_NAME]]); + + for (var j = 0, subEntry; subEntry = subMenu[MENU_IDX_SUB][j]; j++) + { + var listEntry = [subEntry[MENU_IDX_ID], subEntry[MENU_IDX_NAME], subEntry[MENU_IDX_URL]]; + + if (subEntry[MENU_IDX_OPT]) + listEntry[MENU_IDX_OPT] = subEntry[MENU_IDX_OPT]; + + list.push(listEntry); + } + } + + favMenu = list; + } + + return favMenu; + } + + this.refreshMenu = function() + { + var menuRoot = $('#toplinks-favorites'); + if (!menuRoot.length) + return; + + var favMenu = Favorites.getMenu(); + if (!favMenu.length) { + menuRoot.hide(); + return; + } + + Menu.add(menuRoot, favMenu); + menuRoot.show(); + } + + function createIcon(heading) + { + _favIcon = $('', { + 'class': 'fav-star', + mouseout: $WH.Tooltip.hide + }).appendTo(heading); + + if (g_user.id) + { + _favIcon.addClass('fav-star' + (isFaved(_type, _typeId) ? '-1' : '-0')).click((function(type, typeId, name) { + toggleEntry(type, typeId, name); + updateIcon(type, typeId); + $WH.Tooltip.hide(); + }).bind(null, _type, _typeId, heading.textContent.trim().replace(/(.+)<.*/, '$1'))); + + _favIcon.mouseover(function(event) { + var tt = this.className.match(/\bfav-star-0\b/) ? LANG.addtofavorites : LANG.removefromfavorites; + $WH.Tooltip.show(this, tt, false, false, 'q2'); + }); + + } + else + { + _favIcon.addClass('fav-star-0').click(function() { + location.href = "?account=signin"; + $WH.Tooltip.hide(); + }).mouseover(function(event) { + $WH.Tooltip.show(this, LANG.favorites_login + '
' + LANG.clicktologin + ''); + }); + } + } + + function updateIcon(type, typeId) + { + if (_favIcon) + { + var rmv = 'fav-star-0'; + var add = 'fav-star-1'; + if (!isFaved(type, typeId)) + { + rmv = 'fav-star-1'; + add = 'fav-star-0'; + } + + _favIcon.removeClass(rmv).addClass(add); + } + } + + function isFaved(type, typeId) + { + var idx = getIndex(type); + if (idx == -1) + return false; + + for (var i = 0, j; j = g_favorites[idx].entities[i]; i++) + if (j[0] == typeId) + return true; + + return false; + } + + function toggleEntry(type, typeId, name) + { + if (isFaved(type, typeId)) + removeEntry(type, typeId); + else + addEntry(type, typeId, name); + } + + function addEntry(type, typeId, name) + { + var idx = getIndex(type, true); + if (idx == -1) + { + /* $WH. */ console.error("Invalid type when adding entity to favorites! Type was:", type); + return; + } + + for (var i = 0, j; j = g_favorites[idx].entities[i]; i++) + { + if (j[0] == typeId) + { + alert(LANG.favorites_duplicate.replace('%s', LANG.types[type][1])); + return; + } + } + + sendUpdate('add', type, typeId); + g_favorites[idx].entities.push([typeId, name]); + Favorites.refreshMenu(); + } + + function removeEntry(type, typeId) + { + var idx = getIndex(type); + if (idx == -1) + return; + + for (var i = 0, j; j = g_favorites[idx].entities[i]; i++) + { + if (j[0] == typeId) + { + sendUpdate('remove', type, typeId); + g_favorites[idx].entities.splice(i, 1); + if (!g_favorites[idx].entities.length) + g_favorites.splice(idx, 1); + + Favorites.refreshMenu(); + return; + } + } + } + + function getIndex(type, createNew) + { + if (!LANG.types[type]) + return -1; + + for (var i = 0, j; j = g_favorites[i]; i++) + if (j.id == type) + return i; + + if (!createNew) + return -1; + + g_favorites.push({ id: type, entities: [] }); + + g_favorites.sort(function(a, b) { return $WH.strcmp(LANG.types[a.id], LANG.types[b.id]) }); + + for (i = 0; j = g_favorites[i]; i++) + if (j.id == type) + return i; + + return -1; + } + + function sendUpdate(method, type, typeId) + { + var data = { + id: typeId, + // sessionKey: g_user.sessionKey + }; + data[method] = type; + $.post('?account=favorites', data); + } + + if (document.querySelector && $WH.localStorage.isSupported()) + initFavIcon(); +}; diff --git a/setup/tools/filegen/templates/global.js/guide.js b/setup/tools/filegen/templates/global.js/guide.js new file mode 100644 index 00000000..20822e62 --- /dev/null +++ b/setup/tools/filegen/templates/global.js/guide.js @@ -0,0 +1,368 @@ +var g_localTime = new Date(); + +/* This function is to get the stars for the vote control for the guides. */ + +function GetStars(stars, ratable, userRating, guideId) +{ + var STARS_MAX = 5; + var averageRating = stars; + + if (userRating) + stars = userRating; + + stars = Math.round(stars*2)/2; + var starsRounded = Math.round(stars); + var ret = $("").addClass('stars').addClass('max-' + STARS_MAX).addClass('stars-' + starsRounded); + + if (!g_user.id) + ratable = false; + + if (ratable) + ret.addClass('ratable'); + + if (userRating) + ret.addClass('rated'); + + /* This is kinda lame but oh well */ + var contents = ''; + + var wbr = '​'; + var tmp = stars; + for (var i = 1; i <= STARS_MAX; ++i) + { + if (tmp < 1 && tmp > 0) + contents += ''; + else + contents += ''; + --tmp; + + contents += '' + wbr + ''; + } + + for (var i = 1; i <= STARS_MAX; ++i) + contents += ''; + + contents += ''; + + ret.append(contents); + + if (ratable) + { + var starNumber = 0; + ret.find('i.clickable').each(function() { var starId = ++starNumber; $(this).click(function() { VoteGuide(guideId, averageRating, starId); }); }) + } + + if (userRating) + { + var clear = $("").addClass('clear').click(function() { VoteGuide(guideId, averageRating, 0); }); + ret.append(clear); + } + + if (stars >= 0) + ret.mouseover(function(event) {$WH.Tooltip.showAtCursor(event, 'Rating: ' + stars + ' / ' + STARS_MAX, 0, 0, 'q');}).mousemove(function(event) {$WH.Tooltip.cursorUpdate(event)}).mouseout(function() {$WH.Tooltip.hide()}); + + return ret; +} + +function VoteGuide(guideId, oldRating, newRating) +{ + // Update stars display + $('#guiderating').html(GetStars(oldRating, true, newRating, guideId)); + + // Vote + $.ajax({cache: false, url: '?guide=vote', type: 'POST', + error: function() { + $('#guiderating').html(GetStars(oldRating, true, 0, guideId)); + alert('Voting failed. Try again later.'); + }, + success: function(json) { + var data = eval('(' + json + ')'); + $('#guiderating-value').text(data.rating); + $('#guiderating-votes').text(GetN5(data.nvotes)); + }, + data: { id: guideId, rating: newRating } + }); +} + +/* g_enhanceTextarea and createOptionsMenuWidget are only ever used by the article/guide editor. Why are they in global.js? */ + +function g_enhanceTextarea (ta, opt) { + if (!(ta instanceof jQuery)) + ta = $(ta); + + if (ta.data("wh-enhanced") || ta.prop("tagName") != "TEXTAREA") + return; + + if (typeof opt != "object") + opt = {}; + + var canResize = (function(el) { + if (!el.dynamicResizeOption) + return true; + + if ($WH.localStorage.get("dynamic-textarea-resizing") === "true") + return true; + + if ($WH.localStorage.get("dynamic-textarea-resizing") === "false") + return false; + + return !el.hasOwnProperty("dynamicSizing") || el.dynamicSizing; + }).bind(null, opt); + + var height = ta.height() || 500; + var wrapper = $("
", { "class": "enhanced-textarea-wrapper" }).insertBefore(ta).append(ta); + + if (!opt.hasOwnProperty("color")) + wrapper.addClass("enhanced-textarea-dark"); + else if (opt.color) + wrapper.addClass("enhanced-textarea-" + opt.color); + + if (!opt.hasOwnProperty("dynamicSizing") || opt.dynamicSizing || opt.dynamicResizeOption) { + var expander = $("
", { "class": "enhanced-textarea-expander" }).prependTo(wrapper); + var dynamicResize = function(textarea, exactHeight, canResizeFn) { + if (!canResizeFn()) + return; + + // E.css("height", E.siblings(".enhanced-textarea-expander").html($WH.htmlentities(E.val()).replace(/\n/g, "
") + "
").height() + (D ? 14 : 34) + "px"); + textarea.css("height", textarea.siblings(".enhanced-textarea-expander").html($WH.htmlentities(textarea.val()) + "
").height() + (exactHeight ? 14 : 34) + "px"); + }; + + ta.bind("keydown keyup change", dynamicResize.bind(this, ta, opt.exactLineHeights, canResize)); + dynamicResize(ta, opt.exactLineHeights, canResize); + + var setWidth = function(el) { el.css("width", el.parent().width() + "px"); }; + + setWidth(expander); + setTimeout(setWidth.bind(null, expander), 1); + + if (!opt.dynamicResizeOption || (opt.dynamicResizeOption && canResize())) + wrapper.addClass("enhanced-textarea-dynamic-sizing"); + } + + if (!opt.hasOwnProperty("focusChanges") || opt.focusChanges) + wrapper.addClass("enhanced-textarea-focus-changes"); + + if (opt.markup) { + var _markupMenu = $("
", { "class": "enhanced-textarea-markup-wrapper" }).prependTo(wrapper); + var _segments = $("
", { "class": "enhanced-textarea-markup" }).appendTo(_markupMenu); + var _toolbar = $("
", { "class": "enhanced-textarea-markup-segment" }).appendTo(_segments); + var _menu = $("
", { "class": "enhanced-textarea-markup-segment" }).appendTo(_segments); + + if (opt.markup == "inline") + ar_AddInlineToolbar(ta.get(0), _toolbar.get(0), _menu.get(0)); + else + ar_AddToolbar(ta.get(0), _toolbar.get(0), _menu.get(0)); + + if (opt.dynamicResizeOption) { + var _dynResize = $("
", { "class": "enhanced-textarea-markup-segment" }).appendTo(_segments); + var _lblDynResize = $("
'; if ($description) - $x .= '
'.Util::jsEscape($description).'
'; + $x .= '
'.$description.'
'; if ($criteria) { @@ -253,16 +242,19 @@ class AchievementList extends BaseType return $x; } - public function getSourceData() + public function getSourceData(int $id = 0) : array { $data = []; foreach ($this->iterate() as $__) { + if ($id && $id != $this->id) + continue; + $data[$this->id] = array( "n" => $this->getField('name', true), "s" => $this->curTpl['faction'], - "t" => TYPE_ACHIEVEMENT, + "t" => Type::ACHIEVEMENT, "ti" => $this->id ); } @@ -274,11 +266,12 @@ class AchievementList extends BaseType class AchievementListFilter extends Filter { - - protected $enums = array( + protected string $type = 'achievements'; + protected static array $enums = array( + 4 => parent::ENUM_ZONE, // location 11 => array( 327 => 160, // Lunar Festival - 335 => 187, // Love is in the Air + 423 => 187, // Love is in the Air 181 => 159, // Noblegarden 201 => 163, // Children's Week 341 => 161, // Midsummer Fire Festival @@ -288,8 +281,8 @@ class AchievementListFilter extends Filter 141 => 156, // Feast of Winter Veil 409 => -3456, // Day of the Dead 398 => -3457, // Pirates' Day - FILTER_ENUM_ANY => true, - FILTER_ENUM_NONE => false, + parent::ENUM_ANY => true, + parent::ENUM_NONE => false, 283 => -1, // valid events without achievements 285 => -1, 353 => -1, 420 => -1, 400 => -1, 284 => -1, 374 => -1, @@ -297,116 +290,100 @@ class AchievementListFilter extends Filter ) ); - protected $genericFilter = array( // misc (bool): _NUMERIC => useFloat; _STRING => localized; _FLAG => match Value; _BOOLEAN => stringSet - 2 => [FILTER_CR_BOOLEAN, 'reward_loc0', true ], // givesreward - 3 => [FILTER_CR_STRING, 'reward', STR_LOCALIZED ], // rewardtext - 4 => [FILTER_CR_NYI_PH, null, 1, ], // location [enum] - 5 => [FILTER_CR_CALLBACK, 'cbSeries', ACHIEVEMENT_CU_FIRST_SERIES, null], // first in series [yn] - 6 => [FILTER_CR_CALLBACK, 'cbSeries', ACHIEVEMENT_CU_LAST_SERIES, null], // last in series [yn] - 7 => [FILTER_CR_BOOLEAN, 'chainId', ], // partseries - 9 => [FILTER_CR_NUMERIC, 'id', NUM_CAST_INT, true], // id - 10 => [FILTER_CR_STRING, 'ic.name', ], // icon - 11 => [FILTER_CR_CALLBACK, 'cbRelEvent', null, null], // related event [enum] - 14 => [FILTER_CR_FLAG, 'cuFlags', CUSTOM_HAS_COMMENT ], // hascomments - 15 => [FILTER_CR_FLAG, 'cuFlags', CUSTOM_HAS_SCREENSHOT ], // hasscreenshots - 16 => [FILTER_CR_FLAG, 'cuFlags', CUSTOM_HAS_VIDEO ], // hasvideos - 18 => [FILTER_CR_STAFFFLAG, 'flags', ] // flags + protected static array $genericFilter = array( + 2 => [parent::CR_BOOLEAN, 'reward_loc0', true ], // givesreward + 3 => [parent::CR_STRING, 'reward', STR_LOCALIZED ], // rewardtext + 4 => [parent::CR_NYI_PH, null, 1, ], // location [enum] + 5 => [parent::CR_CALLBACK, 'cbSeries', ACHIEVEMENT_CU_FIRST_SERIES, null], // first in series [yn] + 6 => [parent::CR_CALLBACK, 'cbSeries', ACHIEVEMENT_CU_LAST_SERIES, null], // last in series [yn] + 7 => [parent::CR_BOOLEAN, 'chainId', ], // partseries + 9 => [parent::CR_NUMERIC, 'id', NUM_CAST_INT, true], // id + 10 => [parent::CR_STRING, 'ic.name', ], // icon + 11 => [parent::CR_CALLBACK, 'cbRelEvent', null, null], // related event [enum] + 14 => [parent::CR_FLAG, 'cuFlags', CUSTOM_HAS_COMMENT ], // hascomments + 15 => [parent::CR_FLAG, 'cuFlags', CUSTOM_HAS_SCREENSHOT ], // hasscreenshots + 16 => [parent::CR_FLAG, 'cuFlags', CUSTOM_HAS_VIDEO ], // hasvideos + 18 => [parent::CR_STAFFFLAG, 'flags', ] // flags ); - // fieldId => [checkType, checkValue[, fieldIsArray]] - protected $inputFields = array( - 'cr' => [FILTER_V_RANGE, [2, 18], true ], // criteria ids - 'crs' => [FILTER_V_LIST, [FILTER_ENUM_NONE, FILTER_ENUM_ANY, [0, 99999]], true ], // criteria operators - 'crv' => [FILTER_V_REGEX, '/[\p{C};:]/ui', true ], // criteria values - only printable chars, no delimiters - 'na' => [FILTER_V_REGEX, '/[\p{C};]/ui', false], // name / description - only printable chars, no delimiter - 'ex' => [FILTER_V_EQUAL, 'on', false], // extended name search - 'ma' => [FILTER_V_EQUAL, 1, false], // match any / all filter - 'si' => [FILTER_V_LIST, [1, 2, 3, -1, -2], false], // side - 'minpt' => [FILTER_V_RANGE, [1, 99], false], // required level min - 'maxpt' => [FILTER_V_RANGE, [1, 99], false] // required level max + protected static array $inputFields = array( + '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_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 + 'minpt' => [parent::V_RANGE, [1, 99], false], // required level min + 'maxpt' => [parent::V_RANGE, [1, 99], false] // required level max ); - protected function createSQLForCriterium(&$cr) - { - if (in_array($cr[0], array_keys($this->genericFilter))) - if ($genCr = $this->genericCriterion($cr)) - return $genCr; - - unset($cr); - $this->error = true; - return [1]; - } - - protected function createSQLForValues() + protected function createSQLForValues() : array { $parts = []; - $_v = &$this->fiData['v']; + $_v = &$this->values; // name ex: +description, +rewards - if (isset($_v['na'])) + if ($_v['na']) { $_ = []; - if (isset($_v['ex']) && $_v['ex'] == 'on') - $_ = $this->modularizeString(['name_loc'.User::$localeId, 'reward_loc'.User::$localeId, 'description_loc'.User::$localeId]); + if ($_v['ex'] == 'on') + $_ = $this->buildLikeLookup([['na', 'name_loc'.Lang::getLocale()->value], ['na', 'reward_loc'.Lang::getLocale()->value], ['na', 'description_loc'.Lang::getLocale()->value]]); else - $_ = $this->modularizeString(['name_loc'.User::$localeId]); + $_ = $this->buildLikeLookup([['na', 'name_loc'.Lang::getLocale()->value]]); if ($_) $parts[] = $_; } // points min - if (isset($_v['minpt'])) + if ($_v['minpt']) $parts[] = ['points', $_v['minpt'], '>=']; // points max - if (isset($_v['maxpt'])) + if ($_v['maxpt']) $parts[] = ['points', $_v['maxpt'], '<=']; // faction (side) - if (isset($_v['si'])) + if ($_v['si']) { - switch ($_v['si']) + $parts[] = match ($_v['si']) { - case -1: // faction, exclusive both - case -2: - $parts[] = ['faction', -$_v['si']]; - break; - case 1: // faction, inclusive both - case 2: - case 3: // both - $parts[] = ['faction', $_v['si'], '&']; - break; - } + -SIDE_ALLIANCE, // equals faction + -SIDE_HORDE => ['faction', -$_v['si']], + SIDE_ALLIANCE, // includes faction + SIDE_HORDE, + SIDE_BOTH => ['faction', $_v['si'], '&'] + }; } return $parts; } - protected function cbRelEvent($cr, $value) + protected function cbRelEvent(int $cr, int $crs, string $crv) : ?array { - if (!isset($this->enums[$cr[0]][$cr[1]])) - return false; + if (!isset(self::$enums[$cr][$crs])) + return null; - $_ = $this->enums[$cr[0]][$cr[1]]; + $_ = self::$enums[$cr][$crs]; if (is_int($_)) return ($_ > 0) ? ['category', $_] : ['id', abs($_)]; else { - $ids = array_filter($this->enums[$cr[0]], function($x) { return is_int($x) && $x > 0; }); + $ids = array_filter(self::$enums[$cr], fn($x) => is_int($x) && $x > 0); return ['category', $ids, $_ ? null : '!']; } - return false; + return null; } - protected function cbSeries($cr, $value) + protected function cbSeries(int $cr, int $crs, string $crv, int $seriesFlag) : ?array { - if ($this->int2Bool($cr[1])) - return $cr[1] ? ['AND', ['chainId', 0, '!'], ['cuFlags', $value, '&']] : ['AND', ['chainId', 0, '!'], [['cuFlags', $value, '&'], 0]]; + if ($this->int2Bool($crs)) + return $crs ? [DB::AND, ['chainId', 0, '!'], ['cuFlags', $seriesFlag, '&']] : [DB::AND, ['chainId', 0, '!'], [['cuFlags', $seriesFlag, '&'], 0]]; - return false; + return null; } } diff --git a/includes/dbtypes/areatrigger.class.php b/includes/dbtypes/areatrigger.class.php new file mode 100644 index 00000000..527afb25 --- /dev/null +++ b/includes/dbtypes/areatrigger.class.php @@ -0,0 +1,99 @@ + [['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`'] + ); + + public function __construct(array $conditions = [], array $miscData = []) + { + parent::__construct($conditions, $miscData); + + foreach ($this->iterate() as $id => &$_curTpl) + if (!$_curTpl['name']) + $_curTpl['name'] = Lang::areatrigger('unnamed', [$id]); + } + + public static function getName(int $id) : ?LocString + { + 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; + } + + public function getListviewData() : array + { + $data = []; + + foreach ($this->iterate() as $__) + { + $data[$this->id] = array( + 'id' => $this->curTpl['id'], + 'type' => $this->curTpl['type'], + 'name' => $this->curTpl['name'], + ); + + if ($_ = $this->curTpl['areaId']) + $data[$this->id]['location'] = explode(',', $_); + } + + return $data; + } + + public function getJSGlobals(int $addMask = GLOBALINFO_ANY) : array { return []; } + + public function renderTooltip() : ?string { return null; } +} + +class AreaTriggerListFilter extends Filter +{ + protected string $type = 'areatrigger'; + protected static array $genericFilter = array( + 2 => [parent::CR_NUMERIC, 'id', NUM_CAST_INT] // id + ); + + // fieldId => [checkType, checkValue[, fieldIsArray]] + protected static array $inputFields = array( + '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_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 + ); + + protected function createSQLForValues() : array + { + $parts = []; + $_v = &$this->values; + + // name [str] + if ($_v['na']) + if ($_ = $this->buildLikeLookup([['na', 'name']])) + $parts[] = $_; + + // type [list] + if ($_v['ty']) + $parts[] = ['type', $_v['ty']]; + + return $parts; + } +} + +?> diff --git a/includes/dbtypes/arenateam.class.php b/includes/dbtypes/arenateam.class.php new file mode 100644 index 00000000..9103da0c --- /dev/null +++ b/includes/dbtypes/arenateam.class.php @@ -0,0 +1,368 @@ +iterate() as $__) + { + $data[$this->id] = array( + 'name' => $this->curTpl['name'], + 'realm' => Profiler::urlize($this->curTpl['realmName'], true), + 'realmname' => $this->curTpl['realmName'], + // 'battlegroup' => Profiler::urlize($this->curTpl['battlegroup']), // was renamed to subregion somewhere around cata release + // 'battlegroupname' => $this->curTpl['battlegroup'], + 'region' => Profiler::urlize($this->curTpl['region']), + 'faction' => $this->curTpl['faction'], + 'size' => $this->curTpl['type'], + 'rank' => $this->curTpl['rank'], + 'wins' => $this->curTpl['seasonWins'], + 'games' => $this->curTpl['seasonGames'], + 'rating' => $this->curTpl['rating'], + 'members' => $this->curTpl['members'] + ); + } + + return $data; + } + + // plz dont.. + public static function getName(int|string $id) : ?LocString { return null; } + + public function renderTooltip() : ?string { return null; } + public function getJSGlobals(int $addMask = 0) : array { return []; } +} + + +class ArenaTeamListFilter extends Filter +{ + use TrProfilerFilter; + + protected string $type = 'arenateams'; + protected static array $genericFilter = []; + protected static array $inputFields = array( + '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 + '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 + 'bg' => [parent::V_EQUAL, null, false], // battlegroup - unsued here, but var expected by template + 'sv' => [parent::V_CALLBACK, 'cbServerCheck', false] // server + ); + + public array $extraOpts = []; + + protected function createSQLForValues() : array + { + $parts = []; + $_v = $this->values; + + // region (rg), battlegroup (bg) and server (sv) are passed to ArenaTeamList as miscData and handled there + + // name [str] + if ($_v['na']) + if ($_ = $this->buildLikeLookup([['na', 'at.name']], $_v['ex'] == 'on')) + $parts[] = $_; + + // side [list] + if ($_v['si'] == SIDE_ALLIANCE) + $parts[] = ['c.race', ChrRace::fromMask(ChrRace::MASK_ALLIANCE)]; + else if ($_v['si'] == SIDE_HORDE) + $parts[] = ['c.race', ChrRace::fromMask(ChrRace::MASK_HORDE)]; + + // size [int] + if ($_v['sz']) + $parts[] = ['at.type', $_v['sz']]; + + return $parts; + } +} + + +class RemoteArenaTeamList extends ArenaTeamList +{ + protected string $queryBase = 'SELECT `at`.*, `at`.`arenaTeamId` AS ARRAY_KEY FROM arena_team at'; + protected array $queryOpts = array( + 'at' => [['atm', 'c'], 'g' => 'ARRAY_KEY', 'o' => 'rating DESC'], + 'atm' => ['j' => 'arena_team_member atm ON atm.`arenaTeamId` = at.`arenaTeamId`'], + 'c' => ['j' => 'characters c ON c.`guid` = atm.`guid` AND c.`deleteInfos_Account` IS NULL AND c.`level` <= 80 AND (c.`extra_flags` & '.Profiler::CHAR_GMFLAGS.') = 0', 's' => ', BIT_OR(IF(c.`race` IN (1, 3, 4, 7, 11), 1, 2)) - 1 AS "faction"'] + ); + + private array $members = []; + private array $rankOrder = []; + + public function __construct(array $conditions = [], array $miscData = []) + { + // select DB by realm + if (!$this->selectRealms($miscData)) + { + trigger_error('RemoteArenaTeamList::__construct - cannot access any realm.', E_USER_WARNING); + return; + } + + parent::__construct($conditions, $miscData); + + if ($this->error) + return; + + // 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` = %i ORDER BY `rating` DESC', $type); + + reset($this->dbNames); // only use when querying single realm + $realms = Profiler::getRealms(); + $distrib = []; + + // post processing + foreach ($this->iterate() as $guid => &$curTpl) + { + // battlegroup + $curTpl['battlegroup'] = Cfg::get('BATTLEGROUP'); + + // realm, rank + $r = explode(':', $guid); + if (!empty($realms[$r[0]])) + { + $curTpl['realm'] = $r[0]; + $curTpl['realmName'] = $realms[$r[0]]['name']; + $curTpl['region'] = $realms[$r[0]]['region']; + $curTpl['rank'] = array_search($curTpl['arenaTeamId'], $this->rankOrder[$r[0]][$curTpl['type']]) + 1; + } + else + { + trigger_error('arena team #'.$guid.' belongs to nonexistent realm #'.$r, E_USER_WARNING); + unset($this->templates[$guid]); + continue; + } + + // empty name + if (!$curTpl['name']) + { + trigger_error('arena team #'.$guid.' on realm #'.$r.' has empty name.', E_USER_WARNING); + unset($this->templates[$guid]); + continue; + } + + // team members + $this->members[$r[0]][$r[1]] = $r[1]; + + // equalize distribution + if (empty($distrib[$curTpl['realm']])) + $distrib[$curTpl['realm']] = 1; + else + $distrib[$curTpl['realm']]++; + } + + // get team members + foreach ($this->members as $realmId => &$teams) + $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 %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_numeric($c)) + $limit = max(0, (int)$c); + + if (!$limit) // int:0 means unlimited, so skip early + return; + + $total = array_sum($distrib); + foreach ($distrib as &$d) + $d = ceil($limit * $d / $total); + + foreach ($this->iterate() as $guid => &$curTpl) + { + if ($limit <= 0 || $distrib[$curTpl['realm']] <= 0) + { + unset($this->templates[$guid]); + continue; + } + + $r = explode(':', $guid); + if (isset($this->members[$r[0]][$r[1]])) + $curTpl['members'] = array_values($this->members[$r[0]][$r[1]]); // [name, classId, isCaptain] + + $distrib[$curTpl['realm']]--; + $limit--; + } + } + + public function initializeLocalEntries() : void + { + if (!$this->templates) + return; + + $profiles = []; + // init members for tooltips + foreach ($this->members as $realmId => $teams) + { + $gladiators = []; + foreach ($teams as $team) + $gladiators = array_merge($gladiators, array_keys($team)); + + $profiles[$realmId] = new RemoteProfileList(array(['c.guid', $gladiators]), ['sv' => $realmId]); + + if (!$profiles[$realmId]->error) + $profiles[$realmId]->initializeLocalEntries(); + } + + $data = []; + foreach ($this->iterate() as $guid => $__) + { + $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 + 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 %in AND `realmGUID` IN %in', + $data['realm'], $data['realmGUID'] + ); + + foreach ($this->iterate() as $guid => &$_curTpl) + if (isset($localIds[$guid])) + $_curTpl['id'] = $localIds[$guid]; + + + // profiler_arena_team_member requires profiles and arena teams to be filled + foreach ($this->members as $realmId => $teams) + { + if (empty($profiles[$realmId])) + continue; + + $memberData = []; + foreach ($teams as $teamId => $team) + { + $clearMembers = []; + foreach ($team as $memberId => $member) + { + $clearMembers[] = $profiles[$realmId]->getEntry($realmId.':'.$memberId)['id']; + + $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()->qry( + 'DELETE atm + 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 + ); + } + + DB::Aowow()->qry('INSERT INTO ::profiler_arena_team_member %m ON DUPLICATE KEY UPDATE `profileId` = `profileId`', $memberData); + } + } +} + + +class LocalArenaTeamList extends ArenaTeamList +{ + 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"'] + ); + + public function __construct(array $conditions = [], array $miscData = []) + { + $realms = Profiler::getRealms(); + + // graft realm selection from miscData onto conditions + if (isset($miscData['sv'])) + $realms = array_filter($realms, fn($x) => Profiler::urlize($x['name']) == Profiler::urlize($miscData['sv'])); + + if (isset($miscData['rg'])) + $realms = array_filter($realms, fn($x) => $x['region'] == $miscData['rg']); + + if (!$realms) + { + trigger_error('LocalArenaTeamList::__construct - cannot access any realm.', E_USER_WARNING); + return; + } + + if ($conditions) + { + array_unshift($conditions, DB::AND); + $conditions = [DB::AND, ['realm', array_keys($realms)], $conditions]; + } + else + $conditions = [['realm', array_keys($realms)]]; + + parent::__construct($conditions, $miscData); + + if ($this->error) + return; + + // post processing + $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 %in', + $this->getFoundIDs() + ); + + foreach ($this->iterate() as $id => &$curTpl) + { + if ($curTpl['realm'] && !isset($realms[$curTpl['realm']])) + continue; + + if (isset($realms[$curTpl['realm']])) + { + $curTpl['realmName'] = $realms[$curTpl['realm']]['name']; + $curTpl['region'] = $realms[$curTpl['realm']]['region']; + } + + // battlegroup + $curTpl['battlegroup'] = Cfg::get('BATTLEGROUP'); + + $curTpl['members'] = array_values($members[$id]); + } + } + + public function getProfileUrl() : string + { + $url = '?arena-team='; + + return $url.implode('.', array( + $this->getField('region'), + Profiler::urlize($this->getField('realmName'), true), + Profiler::urlize($this->getField('name')) + )); + } +} + + +?> diff --git a/includes/types/charclass.class.php b/includes/dbtypes/charclass.class.php similarity index 58% rename from includes/types/charclass.class.php rename to includes/dbtypes/charclass.class.php index a3d574f0..5c6db072 100644 --- a/includes/types/charclass.class.php +++ b/includes/dbtypes/charclass.class.php @@ -1,26 +1,32 @@ [['ic']], + 'ic' => ['j' => ['::icons ic ON ic.`id` = c.`iconId`', true], 's' => ', ic.`name` AS "iconString"'] + ); - public function __construct($conditions = []) + public function __construct($conditions = [], array $miscData = []) { - parent::__construct($conditions); + parent::__construct($conditions, $miscData); foreach ($this->iterate() as $k => &$_curTpl) $_curTpl['skills'] = explode(' ', $_curTpl['skills']); } - public function getListviewData() + public function getListviewData() : array { $data = []; @@ -46,7 +52,7 @@ class CharClassList extends BaseType return $data; } - public function getJSGlobals($addMask = 0) + public function getJSGlobals(int $addMask = 0) : array { $data = []; @@ -56,8 +62,7 @@ class CharClassList extends BaseType return $data; } - public function addRewardsToJScript(&$ref) { } - public function renderTooltip() { } + public function renderTooltip() : ?string { return null; } } ?> diff --git a/includes/dbtypes/charrace.class.php b/includes/dbtypes/charrace.class.php new file mode 100644 index 00000000..c790aafe --- /dev/null +++ b/includes/dbtypes/charrace.class.php @@ -0,0 +1,58 @@ + [['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 + { + $data = []; + + foreach ($this->iterate() as $__) + { + $data[$this->id] = array( + 'id' => $this->id, + 'name' => $this->getField('name', true), + 'classes' => $this->curTpl['classMask'], + 'faction' => $this->curTpl['factionId'], + 'leader' => $this->curTpl['leader'], + 'zone' => $this->curTpl['startAreaId'], + 'side' => $this->curTpl['side'] + ); + + if ($this->curTpl['expansion']) + $data[$this->id]['expansion'] = $this->curTpl['expansion']; + } + + return $data; + } + + public function getJSGlobals(int $addMask = 0) : array + { + $data = []; + + foreach ($this->iterate() as $__) + $data[Type::CHR_RACE][$this->id] = ['name' => $this->getField('name', true)]; + + return $data; + } + + public function renderTooltip() : ?string { return null; } +} + +?> diff --git a/includes/dbtypes/creature.class.php b/includes/dbtypes/creature.class.php new file mode 100644 index 00000000..6c259153 --- /dev/null +++ b/includes/dbtypes/creature.class.php @@ -0,0 +1,578 @@ + [['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 = []) + { + parent::__construct($conditions, $miscData); + + if ($this->error) + return; + + // post processing + foreach ($this->iterate() as $_id => &$curTpl) + { + // check for attackspeeds + if (!$curTpl['atkSpeed']) + $curTpl['atkSpeed'] = 2.0; + else + $curTpl['atkSpeed'] /= 1000; + + if (!$curTpl['rngAtkSpeed']) + $curTpl['rngAtkSpeed'] = 2.0; + else + $curTpl['rngAtkSpeed'] /= 1000; + } + } + + public function renderTooltip() : ?string + { + if (!$this->curTpl) + return null; + + $level = '??'; + $type = $this->curTpl['type']; + $row3 = [Lang::game('level')]; + $fam = $this->curTpl['family']; + + if (!($this->curTpl['typeFlags'] & NPC_TYPEFLAG_BOSS_MOB)) + { + $level = $this->curTpl['minLevel']; + if ($level != $this->curTpl['maxLevel']) + $level .= ' - '.$this->curTpl['maxLevel']; + } + else + $level = '??'; + + $row3[] = $level; + + if ($type) + $row3[] = Lang::game('ct', $type); + + if ($_ = Lang::npc('rank', $this->curTpl['rank'])) + $row3[] = '('.$_.')'; + + $x = ''; + $x .= ''; + + if ($sn = $this->getField('subname', true)) + $x .= ''; + + $x .= ''; + + if ($type == 1 && $fam) // 1: Beast + $x .= ''; + + $fac = new FactionList(array([['cuFlags', CUSTOM_EXCLUDE_FOR_LISTVIEW, '&'], 0], ['id', (int)$this->getField('factionId')])); + if (!$fac->error) + $x .= ''; + + $x .= '
'.Util::htmlEscape($this->getField('name', true)).'
'.Util::htmlEscape($sn).'
'.implode(' ', $row3).'
'.Lang::game('fa', $fam).'
'.$fac->getField('name', true).'
'; + + return $x; + } + + public function getRandomModelId() : int + { + // dwarf?? [null, 30754, 30753, 30755, 30736] + // totems use hardcoded models, tauren model is base + $totems = [null, 4589, 4588, 4587, 4590]; // slot => modelId + $data = []; + + for ($i = 1; $i < 5; $i++) + if ($_ = $this->curTpl['displayId'.$i]) + $data[] = $_; + + if (count($data) == 1 && ($slotId = array_search($data[0], $totems))) + $data = DB::World()->selectCol('SELECT `DisplayId` FROM player_totem_model WHERE `TotemSlot` = %i', $slotId); + + return !$data ? 0 : $data[array_rand($data)]; + } + + public function getBaseStats(string $type) : array + { + // i'm aware of the BaseVariance/RangedVariance fields ... i'm just totaly unsure about the whole damage calculation + switch ($type) + { + case 'health': + $hMin = $this->getField('healthMin'); + $hMax = $this->getField('healthMax'); + return [$hMin, $hMax]; + case 'power': + $mMin = $this->getField('manaMin'); + $mMax = $this->getField('manaMax'); + return [$mMin, $mMax]; + case 'armor': + $aMin = $this->getField('armorMin'); + $aMax = $this->getField('armorMax'); + return [$aMin, $aMax]; + case 'melee': + $mleMin = ($this->getField('dmgMin') + ($this->getField('mleAtkPwrMin') / 14)) * $this->getField('dmgMultiplier') * $this->getField('atkSpeed'); + $mleMax = ($this->getField('dmgMax') * 1.5 + ($this->getField('mleAtkPwrMax') / 14)) * $this->getField('dmgMultiplier') * $this->getField('atkSpeed'); + return [$mleMin, $mleMax]; + case 'ranged': + $rngMin = ($this->getField('dmgMin') + ($this->getField('rngAtkPwrMin') / 14)) * $this->getField('dmgMultiplier') * $this->getField('rngAtkSpeed'); + $rngMax = ($this->getField('dmgMax') * 1.5 + ($this->getField('rngAtkPwrMax') / 14)) * $this->getField('dmgMultiplier') * $this->getField('rngAtkSpeed'); + return [$rngMin, $rngMax]; + case 'resistance': + $r = []; + for ($i = SPELL_SCHOOL_HOLY; $i < SPELL_SCHOOL_ARCANE+1; $i++) + $r[$i] = $this->getField('resistance'.$i); + + return $r; + default: + return []; + } + } + + public function isBoss() : bool + { + 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 + * + * NPCINFO_TAMEABLE (0x1): include texture & react + * NPCINFO_MODEL (0x2): + * NPCINFO_REP (0x4): include repreward + */ + + $data = []; + $rewRep = []; + + 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 %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() + ); + } + + + foreach ($this->iterate() as $__) + { + if ($addInfoMask & NPCINFO_MODEL) + { + $texStr = strtolower($this->curTpl['textureString']); + + if (isset($data[$texStr])) + { + if ($data[$texStr]['minLevel'] > $this->curTpl['minLevel']) + $data[$texStr]['minLevel'] = $this->curTpl['minLevel']; + + if ($data[$texStr]['maxLevel'] < $this->curTpl['maxLevel']) + $data[$texStr]['maxLevel'] = $this->curTpl['maxLevel']; + + $data[$texStr]['count']++; + } + else + $data[$texStr] = array( + 'family' => $this->curTpl['family'], + 'minLevel' => $this->curTpl['minLevel'], + 'maxLevel' => $this->curTpl['maxLevel'], + 'modelId' => $this->curTpl['modelId'], + 'displayId' => $this->curTpl['displayId1'], + 'skin' => $texStr, + 'count' => 1 + ); + } + else + { + $data[$this->id] = array( + 'family' => $this->curTpl['family'], + 'minlevel' => $this->curTpl['minLevel'], + 'maxlevel' => $this->curTpl['maxLevel'], + 'id' => $this->id, + 'boss' => $this->isBoss() ? 1 : 0, + 'classification' => $this->curTpl['rank'], + 'location' => $this->getSpawns(SPAWNINFO_ZONES), + 'name' => $this->getField('name', true), + 'type' => $this->curTpl['type'], + 'react' => [$this->curTpl['A'], $this->curTpl['H']], + ); + + + if ($this->getField('startsQuests')) + $data[$this->id]['hasQuests'] = 1; + + if ($_ = $this->getField('subname', true)) + $data[$this->id]['tag'] = $_; + + if ($addInfoMask & NPCINFO_TAMEABLE) // only first skin of first model ... we're omitting potentially 11 skins here .. but the lv accepts only one .. w/e + $data[$this->id]['skin'] = $this->curTpl['textureString']; + + if ($addInfoMask & NPCINFO_REP) + { + $data[$this->id]['reprewards'] = []; + if ($rewRep[$this->id]) + foreach ($rewRep[$this->id] as $fac => $val) + $data[$this->id]['reprewards'][] = [$fac, $val]; + } + } + } + + ksort($data); + return $data; + } + + public function getJSGlobals(int $addMask = 0) : array + { + $data = []; + + foreach ($this->iterate() as $__) + $data[Type::NPC][$this->id] = ['name' => $this->getField('name', true)]; + + return $data; + } + + public function getSourceData(int $id = 0) : array + { + $data = []; + + foreach ($this->iterate() as $__) + { + if ($id && $id != $this->id) + continue; + + $data[$this->id] = array( + 'n' => $this->getField('parentId') ? $this->getField('parent', true) : $this->getField('name', true), + 't' => Type::NPC, + 'ti' => $this->getField('parentId') ?: $this->id + ); + } + + return $data; + } +} + + +class CreatureListFilter extends Filter +{ + protected string $type = 'npcs'; + protected static array $enums = array( + 3 => parent::ENUM_FACTION, // faction + 6 => parent::ENUM_ZONE, // foundin + 42 => parent::ENUM_FACTION, // increasesrepwith + 43 => parent::ENUM_FACTION, // decreasesrepwith + 38 => parent::ENUM_EVENT // relatedevent + ); + + protected static array $genericFilter = array( + 1 => [parent::CR_CALLBACK, 'cbHealthMana', 'healthMax', 'healthMin'], // health [num] + 2 => [parent::CR_CALLBACK, 'cbHealthMana', 'manaMin', 'manaMax' ], // mana [num] + 3 => [parent::CR_CALLBACK, 'cbFaction', null, null ], // faction [enum] + 5 => [parent::CR_FLAG, 'npcflag', NPC_FLAG_REPAIRER ], // canrepair + 6 => [parent::CR_ENUM, 's.areaId', false, true ], // foundin + 7 => [parent::CR_CALLBACK, 'cbQuestRelation', 'startsQuests', 0x1 ], // startsquest [enum] + 8 => [parent::CR_CALLBACK, 'cbQuestRelation', 'endsQuests', 0x2 ], // endsquest [enum] + 9 => [parent::CR_BOOLEAN, 'lootId', ], // lootable + 10 => [parent::CR_CALLBACK, 'cbRegularSkinLoot', NPC_TYPEFLAG_SPECIALLOOT ], // skinnable [yn] + 11 => [parent::CR_BOOLEAN, 'pickpocketLootId', ], // pickpocketable + 12 => [parent::CR_CALLBACK, 'cbMoneyDrop', null, null ], // averagemoneydropped [op] [int] + 15 => [parent::CR_CALLBACK, 'cbSpecialSkinLoot', NPC_TYPEFLAG_SKIN_WITH_HERBALISM, null ], // gatherable [yn] + 16 => [parent::CR_CALLBACK, 'cbSpecialSkinLoot', NPC_TYPEFLAG_SKIN_WITH_MINING, null ], // minable [yn] + 18 => [parent::CR_FLAG, 'npcflag', NPC_FLAG_AUCTIONEER ], // auctioneer + 19 => [parent::CR_FLAG, 'npcflag', NPC_FLAG_BANKER ], // banker + 20 => [parent::CR_FLAG, 'npcflag', NPC_FLAG_BATTLEMASTER ], // battlemaster + 21 => [parent::CR_FLAG, 'npcflag', NPC_FLAG_FLIGHT_MASTER ], // flightmaster + 22 => [parent::CR_FLAG, 'npcflag', NPC_FLAG_GUILD_MASTER ], // guildmaster + 23 => [parent::CR_FLAG, 'npcflag', NPC_FLAG_INNKEEPER ], // innkeeper + 24 => [parent::CR_FLAG, 'npcflag', NPC_FLAG_CLASS_TRAINER ], // talentunlearner + 25 => [parent::CR_FLAG, 'npcflag', NPC_FLAG_GUILD_MASTER ], // tabardvendor + 27 => [parent::CR_FLAG, 'npcflag', NPC_FLAG_STABLE_MASTER ], // stablemaster + 28 => [parent::CR_FLAG, 'npcflag', NPC_FLAG_TRAINER ], // trainer + 29 => [parent::CR_FLAG, 'npcflag', NPC_FLAG_VENDOR ], // vendor + 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_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_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] + ); + + protected static array $inputFields = array( + '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_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, [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] + ); + + public array $extraOpts = []; + + protected function createSQLForValues() : array + { + $parts = []; + $_v = &$this->values; + + // name [str] + if ($_v['na']) + { + $f = [['na', ['nml.nName', 'nml.nSubname']]]; + if ($_v['ex'] != 'on') + $f = [['na', 'nml.nName']]; + + 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] + if ($_v['fa']) + $parts[] = ['family', $_v['fa']]; + + // creatureLevel min [int] + if ($_v['minle']) + $parts[] = ['minLevel', $_v['minle'], '>=']; + + // creatureLevel max [int] + if ($_v['maxle']) + $parts[] = ['maxLevel', $_v['maxle'], '<=']; + + // classification [list] + if ($_v['cl']) + $parts[] = ['rank', $_v['cl']]; + + // react Alliance [int] + if (!is_null($_v['ra'])) + $parts[] = ['ft.A', $_v['ra']]; + + // react Horde [int] + if (!is_null($_v['rh'])) + $parts[] = ['ft.H', $_v['rh']]; + + return $parts; + } + + protected function cbPetFamily(string &$val) : bool + { + if (!$this->parentCats || $this->parentCats[0] != 1) + return false; + + if (!Util::checkNumeric($val, NUM_CAST_INT)) + return false; + + $type = parent::V_LIST; + $valid = [[1, 9], 11, 12, 20, 21, [24, 27], [30, 35], [37, 39], [41, 46]]; + + return $this->checkInput($type, $valid, $val); + } + + protected function cbRelEvent(int $cr, int $crs, string $crv) : ?array + { + 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 %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 %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` = %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]; + } + + return null; + } + + protected function cbMoneyDrop(int $cr, int $crs, string $crv) : ?array + { + if (!Util::checkNumeric($crv, NUM_CAST_INT) || !$this->int2Op($crs)) + return null; + + return [DB::AND, ['((minGold + maxGold) / 2)', $crv, $crs]]; + } + + protected function cbQuestRelation(int $cr, int $crs, string $crv, $field, $val) : ?array + { + switch ($crs) + { + case 1: // any + return [DB::AND, ['qse.method', $val, '&'], ['qse.questId', null, '!']]; + case 2: // 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 [DB::AND, ['qse.method', $val, '&'], ['qse.questId', null, '!'], [['qt.reqRaceMask', ChrRace::MASK_ALLIANCE, '&'], 0], ['qt.reqRaceMask', ChrRace::MASK_HORDE, '&']]; + case 4: // both + 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]; + } + + return null; + } + + protected function cbHealthMana(int $cr, int $crs, string $crv, $minField, $maxField) : ?array + { + if (!Util::checkNumeric($crv, NUM_CAST_INT) || !$this->int2Op($crs)) + return null; + + // remap OP for this special case + switch ($crs) + { + case '=': // min > max is totally possible + $this->extraOpts['ct']['h'][] = $minField.' = '.$maxField.' AND '.$minField.' = '.$crv; + break; + case '>': + case '>=': + case '<': + case '<=': + $this->extraOpts['ct']['h'][] = 'IF('.$minField.' > '.$maxField.', '.$maxField.', '.$minField.') '.$crs.' '.$crv; + break; + } + + + return [1]; // always true, use post-filter + } + + protected function cbSpecialSkinLoot(int $cr, int $crs, string $crv, $typeFlag) : ?array + { + if (!$this->int2Bool($crs)) + return null; + + + if ($crs) + return [DB::AND, ['skinLootId', 0, '>'], ['typeFlags', $typeFlag, '&']]; + else + return [DB::OR, ['skinLootId', 0], [['typeFlags', $typeFlag, '&'], 0]]; + } + + protected function cbRegularSkinLoot(int $cr, int $crs, string $crv, $typeFlag) : ?array + { + if (!$this->int2Bool($crs)) + return null; + + if ($crs) + return [DB::AND, ['skinLootId', 0, '>'], [['typeFlags', $typeFlag, '&'], 0]]; + else + return [DB::OR, ['skinLootId', 0], ['typeFlags', $typeFlag, '&']]; + } + + protected function cbReputation(int $cr, int $crs, string $crv, $op) : ?array + { + if (!in_array($crs, self::$enums[$cr])) + return null; + + 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` = %i AND `RewOnKillRepValue1` '.$op.' 0) OR (`RewOnKillRepFaction2` = %i AND `RewOnKillRepValue2` '.$op.' 0)', $crs, $crs)) + return ['id', $cIds]; + else + return [0]; + } + + protected function cbFaction(int $cr, int $crs, string $crv) : ?array + { + if (!Util::checkNumeric($crs, NUM_CAST_INT)) + return null; + + if (!in_array($crs, self::$enums[$cr])) + return null; + + $facTpls = []; + $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 new file mode 100644 index 00000000..79744097 --- /dev/null +++ b/includes/dbtypes/currency.class.php @@ -0,0 +1,88 @@ + [['ic']], + 'ic' => ['j' => ['::icons ic ON ic.`id` = c.`iconId`', true], 's' => ', ic.`name` AS "iconString"'] + ); + + public function __construct(array $conditions = [], array $miscData = []) + { + parent::__construct($conditions, $miscData); + + foreach ($this->iterate() as &$_curTpl) + $_curTpl['iconString'] = $_curTpl['iconString'] ?: DEFAULT_ICON; + } + + + public function getListviewData() : array + { + $data = []; + + foreach ($this->iterate() as $__) + { + $data[$this->id] = array( + 'id' => $this->id, + 'category' => $this->curTpl['category'], + 'name' => $this->getField('name', true), + 'icon' => $this->curTpl['iconString'] + ); + } + + return $data; + } + + public function getJSGlobals(int $addMask = 0) : array + { + $data = []; + + foreach ($this->iterate() as $__) + { + // todo (low): un-hardcode icon strings + $icon = match ($this->id) + { + CURRENCY_HONOR_POINTS => ['pvp-currency-alliance', 'pvp-currency-horde' ], + CURRENCY_ARENA_POINTS => ['pvp-arenapoints-icon', 'pvp-arenapoints-icon' ], + default => [$this->curTpl['iconString'], $this->curTpl['iconString']] + }; + + $data[Type::CURRENCY][$this->id] = ['name' => $this->getField('name', true), 'icon' => $icon]; + } + + return $data; + } + + public function renderTooltip() : ?string + { + if (!$this->curTpl) + return null; + + $x = '
'; + $x .= ''.$this->getField('name', true).'
'; + + // cata+ (or go fill it by hand) + if ($_ = $this->getField('description', true)) + $x .= '
'.$_.'
'; + + if ($_ = $this->getField('cap')) + $x .= '
'.Lang::currency('cap').''.Lang::nf($_).'
'; + + $x .= '
'; + + return $x; + } +} + +?> diff --git a/includes/dbtypes/emote.class.php b/includes/dbtypes/emote.class.php new file mode 100644 index 00000000..3510d687 --- /dev/null +++ b/includes/dbtypes/emote.class.php @@ -0,0 +1,65 @@ +iterate() as &$curTpl) + { + // remap for generic access + $curTpl['name'] = $curTpl['cmd']; + } + } + + public static function getName(int $id) : ?LocString + { + if ($n = DB::Aowow()->SelectRow('SELECT `cmd` AS "name_loc0" FROM %n WHERE `id` = %i', self::$dataTable, $id)) + return new LocString($n); + return null; + } + + public function getListviewData() : array + { + $data = []; + + foreach ($this->iterate() as $__) + { + $data[$this->id] = array( + 'id' => $this->curTpl['id'], + 'name' => $this->curTpl['cmd'], + 'preview' => Util::parseHtmlText($this->getField('meToExt', true) ?: $this->getField('meToNone', true) ?: $this->getField('extToMe', true) ?: $this->getField('extToExt', true) ?: $this->getField('extToNone', true), true) + ); + } + + return $data; + } + + public function getJSGlobals(int $addMask = GLOBALINFO_ANY) : array + { + $data = []; + + foreach ($this->iterate() as $__) + $data[Type::EMOTE][$this->id] = ['name' => $this->getField('cmd')]; + + return $data; + } + + public function renderTooltip() : ?string { return null; } +} + +?> diff --git a/includes/dbtypes/enchantment.class.php b/includes/dbtypes/enchantment.class.php new file mode 100644 index 00000000..28c170e4 --- /dev/null +++ b/includes/dbtypes/enchantment.class.php @@ -0,0 +1,263 @@ + Type::ENCHANTMENT + 'ie' => [['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 = []) + { + parent::__construct($conditions, $miscData); + + $relSpells = []; + + // post processing + foreach ($this->iterate() as &$curTpl) + { + $curTpl['spells'] = []; // [spellId, triggerType, charges, chanceOrPpm] + for ($i = 1; $i <=3; $i++) + { + if ($curTpl['object'.$i] <= 0) + continue; + + switch ($curTpl['type'.$i]) // SPELL_TRIGGER_* just reused for wording + { + case ENCHANTMENT_TYPE_COMBAT_SPELL: + $proc = -$this->getField('ppmRate') ?: ($this->getField('procChance') ?: $this->getField('amount'.$i)); + $curTpl['spells'][$i] = [$curTpl['object'.$i], SPELL_TRIGGER_HIT, $curTpl['charges'], $proc]; + $relSpells[] = $curTpl['object'.$i]; + break; + case ENCHANTMENT_TYPE_EQUIP_SPELL: + $curTpl['spells'][$i] = [$curTpl['object'.$i], SPELL_TRIGGER_EQUIP, $curTpl['charges'], 0]; + $relSpells[] = $curTpl['object'.$i]; + break; + case ENCHANTMENT_TYPE_USE_SPELL: + $curTpl['spells'][$i] = [$curTpl['object'.$i], SPELL_TRIGGER_USE, $curTpl['charges'], 0]; + $relSpells[] = $curTpl['object'.$i]; + break; + } + } + } + + 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 + { + $data = []; + + foreach ($this->iterate() as $__) + { + $data[$this->id] = array( + 'id' => $this->id, + 'name' => $this->getField('name', true), + 'spells' => [] + ); + + if ($this->curTpl['skillLine'] > 0) + $data[$this->id]['reqskill'] = $this->curTpl['skillLine']; + + if ($this->curTpl['skillLevel'] > 0) + $data[$this->id]['reqskillrank'] = $this->curTpl['skillLevel']; + + if ($this->curTpl['requiredLevel'] > 0) + $data[$this->id]['reqlevel'] = $this->curTpl['requiredLevel']; + + foreach ($this->curTpl['spells'] as [$spellId, $trigger, $charges, $procChance]) + { + // spell is procing + $trgSpell = 0; + if ($this->relSpells && $this->relSpells->getEntry($spellId) && ($_ = $this->relSpells->canTriggerSpell())) + { + foreach ($_ as $idx) + { + if ($trgSpell = $this->relSpells->getField('effect'.$idx.'TriggerSpell')) + { + $this->triggerIds[] = $trgSpell; + $data[$this->id]['spells'][$trgSpell] = $charges; + } + } + } + + // spell was not proccing + if (!$trgSpell) + $data[$this->id]['spells'][$spellId] = $charges; + } + + if (!$data[$this->id]['spells']) + unset($data[$this->id]['spells']); + + Util::arraySumByKey($data[$this->id], $this->jsonStats[$this->id]->toJson(includeEmpty: false)); + } + + return $data; + } + + public function getStatGainForCurrent() : array + { + return $this->jsonStats[$this->id]->toJson(includeEmpty: true); + } + + public function getRelSpell(int $id) : ?array + { + if ($this->relSpells) + return $this->relSpells->getEntry($id); + + return null; + } + + public function getJSGlobals(int $addMask = GLOBALINFO_ANY) : array + { + $data = []; + + if ($addMask & GLOBALINFO_SELF) + foreach ($this->iterate() as $__) + $data[Type::ENCHANTMENT][$this->id] = ['name' => $this->getField('name', true)]; + + if ($addMask & GLOBALINFO_RELATED) + { + if ($this->relSpells) + $data = $this->relSpells->getJSGlobals(GLOBALINFO_SELF); + + foreach ($this->triggerIds as $tId) + if (empty($data[Type::SPELL][$tId])) + $data[Type::SPELL][$tId] = $tId; + } + + return $data; + } + + public function renderTooltip() : ?string { return null; } +} + + +class EnchantmentListFilter extends Filter +{ + protected string $type = 'enchantments'; + protected static array $enums = array( + 3 => parent::ENUM_PROFESSION // requiresprof + ); + + protected static array $genericFilter = array( + 2 => [parent::CR_NUMERIC, 'id', NUM_CAST_INT, true], // id + 3 => [parent::CR_ENUM, 'skillLine' ], // requiresprof + 4 => [parent::CR_NUMERIC, 'skillLevel', NUM_CAST_INT ], // reqskillrank + 5 => [parent::CR_BOOLEAN, 'conditionId' ], // hascondition + 10 => [parent::CR_FLAG, 'cuFlags', CUSTOM_HAS_COMMENT ], // hascomments + 11 => [parent::CR_FLAG, 'cuFlags', CUSTOM_HAS_SCREENSHOT ], // hasscreenshots + 12 => [parent::CR_FLAG, 'cuFlags', CUSTOM_HAS_VIDEO ], // hasvideos + 20 => [parent::CR_NUMERIC, 'is.str', NUM_CAST_INT, true], // str + 21 => [parent::CR_NUMERIC, 'is.agi', NUM_CAST_INT, true], // agi + 22 => [parent::CR_NUMERIC, 'is.sta', NUM_CAST_INT, true], // sta + 23 => [parent::CR_NUMERIC, 'is.int', NUM_CAST_INT, true], // int + 24 => [parent::CR_NUMERIC, 'is.spi', NUM_CAST_INT, true], // spi + 25 => [parent::CR_NUMERIC, 'is.arcres', NUM_CAST_INT, true], // arcres + 26 => [parent::CR_NUMERIC, 'is.firres', NUM_CAST_INT, true], // firres + 27 => [parent::CR_NUMERIC, 'is.natres', NUM_CAST_INT, true], // natres + 28 => [parent::CR_NUMERIC, 'is.frores', NUM_CAST_INT, true], // frores + 29 => [parent::CR_NUMERIC, 'is.shares', NUM_CAST_INT, true], // shares + 30 => [parent::CR_NUMERIC, 'is.holres', NUM_CAST_INT, true], // holres + 32 => [parent::CR_NUMERIC, 'is.dps', NUM_CAST_FLOAT, true], // dps + 34 => [parent::CR_NUMERIC, 'is.dmg', NUM_CAST_FLOAT, true], // dmg + 37 => [parent::CR_NUMERIC, 'is.mleatkpwr', NUM_CAST_INT, true], // mleatkpwr + 38 => [parent::CR_NUMERIC, 'is.rgdatkpwr', NUM_CAST_INT, true], // rgdatkpwr + 39 => [parent::CR_NUMERIC, 'is.rgdhitrtng', NUM_CAST_INT, true], // rgdhitrtng + 40 => [parent::CR_NUMERIC, 'is.rgdcritstrkrtng', NUM_CAST_INT, true], // rgdcritstrkrtng + 41 => [parent::CR_NUMERIC, 'is.armor', NUM_CAST_INT, true], // armor + 42 => [parent::CR_NUMERIC, 'is.defrtng', NUM_CAST_INT, true], // defrtng + 43 => [parent::CR_NUMERIC, 'is.block', NUM_CAST_INT, true], // block + 44 => [parent::CR_NUMERIC, 'is.blockrtng', NUM_CAST_INT, true], // blockrtng + 45 => [parent::CR_NUMERIC, 'is.dodgertng', NUM_CAST_INT, true], // dodgertng + 46 => [parent::CR_NUMERIC, 'is.parryrtng', NUM_CAST_INT, true], // parryrtng + 48 => [parent::CR_NUMERIC, 'is.splhitrtng', NUM_CAST_INT, true], // splhitrtng + 49 => [parent::CR_NUMERIC, 'is.splcritstrkrtng', NUM_CAST_INT, true], // splcritstrkrtng + 50 => [parent::CR_NUMERIC, 'is.splheal', NUM_CAST_INT, true], // splheal + 51 => [parent::CR_NUMERIC, 'is.spldmg', NUM_CAST_INT, true], // spldmg + 52 => [parent::CR_NUMERIC, 'is.arcsplpwr', NUM_CAST_INT, true], // arcsplpwr + 53 => [parent::CR_NUMERIC, 'is.firsplpwr', NUM_CAST_INT, true], // firsplpwr + 54 => [parent::CR_NUMERIC, 'is.frosplpwr', NUM_CAST_INT, true], // frosplpwr + 55 => [parent::CR_NUMERIC, 'is.holsplpwr', NUM_CAST_INT, true], // holsplpwr + 56 => [parent::CR_NUMERIC, 'is.natsplpwr', NUM_CAST_INT, true], // natsplpwr + 57 => [parent::CR_NUMERIC, 'is.shasplpwr', NUM_CAST_INT, true], // shasplpwr + 60 => [parent::CR_NUMERIC, 'is.healthrgn', NUM_CAST_INT, true], // healthrgn + 61 => [parent::CR_NUMERIC, 'is.manargn', NUM_CAST_INT, true], // manargn + 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 + 84 => [parent::CR_NUMERIC, 'is.mlecritstrkrtng', NUM_CAST_INT, true], // mlecritstrkrtng + 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 + 97 => [parent::CR_NUMERIC, 'is.feratkpwr', NUM_CAST_INT, true], // feratkpwr + 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 + 114 => [parent::CR_NUMERIC, 'is.armorpenrtng', NUM_CAST_INT, true], // armorpenrtng + 115 => [parent::CR_NUMERIC, 'is.health', NUM_CAST_INT, true], // health + 116 => [parent::CR_NUMERIC, 'is.mana', NUM_CAST_INT, true], // mana + 117 => [parent::CR_NUMERIC, 'is.exprtng', NUM_CAST_INT, true], // exprtng + 119 => [parent::CR_NUMERIC, 'is.hitrtng', NUM_CAST_INT, true], // hitrtng + 123 => [parent::CR_NUMERIC, 'is.splpwr', NUM_CAST_INT, true] // splpwr + ); + + protected static array $inputFields = array( + '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_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 + ); + + protected function createSQLForValues() : array + { + $parts = []; + $_v = &$this->values; + + //string + if ($_v['na']) + if ($_ = $this->buildLikeLookup([['na', 'name_loc'.Lang::getLocale()->value]])) + $parts[] = $_; + + // type + if ($_v['ty']) + $parts[] = [DB::OR, ['type1', $_v['ty']], ['type2', $_v['ty']], ['type3', $_v['ty']]]; + + return $parts; + } +} + +?> diff --git a/includes/types/faction.class.php b/includes/dbtypes/faction.class.php similarity index 57% rename from includes/types/faction.class.php rename to includes/dbtypes/faction.class.php index f327e95c..cf76280d 100644 --- a/includes/types/faction.class.php +++ b/includes/dbtypes/faction.class.php @@ -1,25 +1,27 @@ [['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($conditions = []) + public function __construct(array $conditions = [], array $miscData = []) { - parent::__construct($conditions); + parent::__construct($conditions, $miscData); if ($this->error) return; @@ -35,13 +37,7 @@ class FactionList extends BaseType } } - public static function getName($id) - { - $n = DB::Aowow()->SelectRow('SELECT name_loc0, name_loc2, name_loc3, name_loc6, name_loc8 FROM ?_factions WHERE id = ?d', $id); - return Util::localizedString($n, 'name'); - } - - public function getListviewData() + public function getListviewData() : array { $data = []; @@ -70,17 +66,17 @@ class FactionList extends BaseType return $data; } - public function getJSGlobals($addMask = 0) + public function getJSGlobals(int $addMask = 0) : array { $data = []; foreach ($this->iterate() as $__) - $data[TYPE_FACTION][$this->id] = ['name' => $this->getField('name', true)]; + $data[Type::FACTION][$this->id] = ['name' => $this->getField('name', true)]; return $data; } - public function renderTooltip() { } + public function renderTooltip() : ?string { return null; } } diff --git a/includes/dbtypes/gameobject.class.php b/includes/dbtypes/gameobject.class.php new file mode 100644 index 00000000..7dcc92c1 --- /dev/null +++ b/includes/dbtypes/gameobject.class.php @@ -0,0 +1,253 @@ + [['ft', 'qse']], + '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 = []) + { + parent::__construct($conditions, $miscData); + + if ($this->error) + return; + + // post processing + foreach ($this->iterate() as $_id => &$curTpl) + { + if (!$curTpl['name_loc'.Lang::getLocale()->value]) + $curTpl['name_loc'.Lang::getLocale()->value] = Lang::gameObject('unnamed', [$_id]); + + // unpack miscInfo + $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])) + $curTpl['spells'] = array_combine(['onUse', 'onSuccess', 'aura', 'triggered'], [$curTpl['onUseSpell'], $curTpl['onSuccessSpell'], $curTpl['auraSpell'], $curTpl['triggeredSpell']]); + + if (!$curTpl['miscInfo']) + continue; + + switch ($curTpl['type']) + { + case OBJECT_CHEST: + case OBJECT_FISHINGHOLE: + $curTpl['lootStack'] = explode(' ', $curTpl['miscInfo']); + break; + case OBJECT_CAPTURE_POINT: + $curTpl['capture'] = explode(' ', $curTpl['miscInfo']); + break; + case OBJECT_MEETINGSTONE: + $curTpl['mStone'] = explode(' ', $curTpl['miscInfo']); + break; + } + } + } + + public function getListviewData() : array + { + $data = []; + foreach ($this->iterate() as $__) + { + $data[$this->id] = array( + 'id' => $this->id, + 'name' => Lang::unescapeUISequences($this->getField('name', true), Lang::FMT_RAW), + 'type' => $this->getField('typeCat'), + 'location' => $this->getSpawns(SPAWNINFO_ZONES) + ); + + if (!empty($this->curTpl['reqSkill'])) + $data[$this->id]['skill'] = $this->curTpl['reqSkill']; + + if ($this->curTpl['startsQuests']) + $data[$this->id]['hasQuests'] = 1; + + } + + return $data; + } + + public function renderTooltip($interactive = false) : ?string + { + if (!$this->curTpl) + return null; + + $x = ''; + $x .= ''; + if ($this->curTpl['typeCat']) + if ($_ = Lang::gameObject('type', $this->curTpl['typeCat'])) + $x .= ''; + + if (isset($this->curTpl['lockId'])) + if ($locks = Lang::getLocks($this->curTpl['lockId'])) + foreach ($locks as $l) + $x .= ''; + + $x .= '
'.Lang::unescapeUISequences($this->getField('name', true), Lang::FMT_HTML).'
'.$_.'
'.Lang::game('requires', [$l]).'
'; + + return $x; + } + + public function getJSGlobals(int $addMask = 0) : array + { + $data = []; + + foreach ($this->iterate() as $__) + $data[Type::OBJECT][$this->id] = ['name' => Lang::unescapeUISequences($this->getField('name', true), Lang::FMT_RAW)]; + + return $data; + } + + public function getSourceData(int $id = 0) : array + { + $data = []; + + foreach ($this->iterate() as $__) + { + if ($id && $id != $this->id) + continue; + + $data[$this->id] = array( + 'n' => $this->getField('name', true), + 't' => Type::OBJECT, + 'ti' => $this->id + ); + } + + return $data; + } +} + + +class GameObjectListFilter extends Filter +{ + protected string $type = 'objects'; + protected static array $enums = array( + 1 => parent::ENUM_ZONE, + 16 => parent::ENUM_EVENT, + 50 => [1, 2, 3, 4, 663, 883] + ); + + protected static array $genericFilter = array( + 1 => [parent::CR_ENUM, 's.areaId', false, true], // foundin + 2 => [parent::CR_CALLBACK, 'cbQuestRelation', 'startsQuests', 0x1 ], // startsquest [side] + 3 => [parent::CR_CALLBACK, 'cbQuestRelation', 'endsQuests', 0x2 ], // endsquest [side] + 4 => [parent::CR_CALLBACK, 'cbOpenable', null, null], // openable [yn] + 5 => [parent::CR_NYI_PH, null, 0 ], // averagemoneycontained [op] [int] - GOs don't contain money, match against 0 + 7 => [parent::CR_NUMERIC, 'reqSkill', NUM_CAST_INT ], // requiredskilllevel + 11 => [parent::CR_FLAG, 'cuFlags', CUSTOM_HAS_SCREENSHOT ], // hasscreenshots + 13 => [parent::CR_FLAG, 'cuFlags', CUSTOM_HAS_COMMENT ], // hascomments + 15 => [parent::CR_NUMERIC, 'id', NUM_CAST_INT ], // id + 16 => [parent::CR_CALLBACK, 'cbRelEvent', null, null], // relatedevent (ignore removed by event) + 18 => [parent::CR_FLAG, 'cuFlags', CUSTOM_HAS_VIDEO ], // hasvideos + 50 => [parent::CR_ENUM, 'spellFocusId', true, true], // spellfocus + ); + + protected static array $inputFields = array( + '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_NAME, false, false], // name - only printable chars, no delimiter + 'ma' => [parent::V_EQUAL, 1, false] // match any / all filter + ); + + public array $extraOpts = []; + + protected function createSQLForValues() : array + { + $parts = []; + $_v = $this->values; + + // name + if ($_v['na']) + { + if ($_ = $this->buildMatchLookup([['na', 'nml.nName']])) + $parts[] = $_; + else if ($_ = $this->buildLikeLookup([['na', 'name_loc'.Lang::getLocale()->value]])) + $parts[] = $_; + } + + return $parts; + } + + protected function cbOpenable(int $cr, int $crs, string $crv) : ?array + { + if ($this->int2Bool($crs)) + return $crs ? [DB::OR, ['flags', 0x2, '&'], ['type', 3]] : [DB::AND, [['flags', 0x2, '&'], 0], ['type', 3, '!']]; + + return null; + } + + protected function cbQuestRelation(int $cr, int $crs, string $crv, $field, $value) : ?array + { + switch ($crs) + { + case 1: // any + return [DB::AND, ['qse.method', $value, '&'], ['qse.questId', null, '!']]; + case 2: // alliance only + 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 [DB::AND, ['qse.method', $value, '&'], ['qse.questId', null, '!'], [['qt.reqRaceMask', ChrRace::MASK_ALLIANCE, '&'], 0], ['qt.reqRaceMask', ChrRace::MASK_HORDE, '&']]; + case 4: // both + 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]; + } + + return null; + } + + protected function cbRelEvent(int $cr, int $crs, string $crv) : ?array + { + 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 %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 %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` = %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]; + } + + return null; + } +} + +?> diff --git a/includes/dbtypes/guide.class.php b/includes/dbtypes/guide.class.php new file mode 100644 index 00000000..d1cc5f5c --- /dev/null +++ b/includes/dbtypes/guide.class.php @@ -0,0 +1,170 @@ + [['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"'] + ); + + public function __construct(array $conditions = [], array $miscData = []) + { + parent::__construct($conditions, $miscData); + + if ($this->error) + return; + + $ratings = GuideMgr::getRatings($this->getFoundIDs()); + + // post processing + foreach ($this->iterate() as $id => &$_curTpl) + $_curTpl = array_merge($_curTpl, $ratings[$id]); + } + + public static function getName(int $id) : ?LocString + { + if ($n = DB::Aowow()->SelectRow('SELECT `title` AS "name_loc0" FROM %n WHERE `id` = %i', self::$dataTable, $id)) + return new LocString($n); + return null; + } + + public function getArticle(int $rev = -1) : string + { + if ($rev < -1) + $rev = -1; + + if (empty($this->article[$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']]) + { + Markup::parseTags($this->article[$a['rev']], $this->jsGlobals); + return $this->article[$a['rev']]; + } + else + trigger_error('GuideList::getArticle - linked article is missing'); + } + + return $this->article[$rev] ?? ''; + } + + public function getListviewData(bool $addDescription = false) : array + { + $data = []; + + foreach ($this->iterate() as $__) + { + $data[$this->id] = array( + 'id' => $this->id, + 'category' => $this->getField('category'), + 'title' => $this->getField('title'), + 'description' => $this->getField('description'), + 'sticky' => !!($this->getField('cuFlags') & CC_FLAG_STICKY), + 'nvotes' => $this->getField('nvotes'), + 'url' => '?guide=' . ($this->getField('url') ?: $this->id), + 'status' => $this->getField('status'), + 'author' => $this->getField('author'), + 'authorroles' => $this->getField('roles'), + 'rating' => $this->getField('rating'), + 'views' => $this->getField('views'), + 'comments' => $this->getField('comments'), + // 'patch' => $this->getField(''), // 30305 - patch is pointless, use date instead + 'date' => $this->getField('date'), // ok + 'when' => date(Util::$dateFormatInternal, $this->getField('date')) + ); + + if ($this->getField('category') == 1) + { + $data[$this->id]['classs'] = $this->getField('classId'); + $data[$this->id]['spec'] = $this->getField('specId'); + } + } + + return $data; + } + + public function userCanView() : bool + { + // is owner || is staff + return $this->getField('userId') == User::$id || User::isInGroup(U_GROUP_STAFF); + } + + public function canBeViewed() : bool + { + // currently approved || has prev. approved version + return $this->getField('status') == GuideMgr::STATUS_APPROVED || $this->getField('rev') > 0; + } + + public function canBeReported() : bool + { + // not own guide && is not archived + return $this->getField('userId') != User::$id && $this->getField('status') != GuideMgr::STATUS_ARCHIVED; + } + + public function getJSGlobals(int $addMask = GLOBALINFO_ANY) : array + { + return $this->jsGlobals; + } + + public function renderTooltip() : ?string + { + $specStr = ''; + + if ($this->getField('classId') && $this->getField('category') == 1) + { + if ($c = $this->getField('classId')) + { + $n = Lang::game('cl', $c); + $specStr .= '  –  %s'; + + if (($s = $this->getField('specId')) > -1) + { + $i = Game::$specIconStrings[$c][$s]; + $n = ''; + $specStr .= ''.Lang::game('classSpecs', $c, $s).''; + } + + $specStr = sprintf($specStr, $n); + } + } + + $tt = '
'.$this->getField('title').'
'; + $tt .= '
'.Lang::game('guide').''.Lang::guide('byAuthor', [$this->getField('author')]).'
'; + $tt .= '
'.Lang::guide('category', $this->getField('category')).$specStr.''.Lang::guide('patch').' 3.3.5
'; + $tt .= '
'.$this->getField('description').'
'; + $tt .= '
'; + + return $tt; + } +} + +?> diff --git a/includes/types/guild.class.php b/includes/dbtypes/guild.class.php similarity index 50% rename from includes/types/guild.class.php rename to includes/dbtypes/guild.class.php index c6136261..e31ea4cc 100644 --- a/includes/types/guild.class.php +++ b/includes/dbtypes/guild.class.php @@ -1,14 +1,18 @@ getGuildScores(); @@ -16,23 +20,23 @@ class GuildList extends BaseType foreach ($this->iterate() as $__) { $data[$this->id] = array( - 'name' => "$'".$this->curTpl['name']."'", // MUST be a string + 'name' => '$"'.str_replace ('"', '', $this->curTpl['name']).'"', // MUST be a string, omit any quotes in name 'members' => $this->curTpl['members'], 'faction' => $this->curTpl['faction'], 'achievementpoints' => $this->getField('achievementpoints'), 'gearscore' => $this->getField('gearscore'), - 'realm' => Profiler::urlize($this->curTpl['realmName']), + 'realm' => Profiler::urlize($this->curTpl['realmName'], true), 'realmname' => $this->curTpl['realmName'], - // 'battlegroup' => Profiler::urlize($this->curTpl['battlegroup']), // was renamed to subregion somewhere around cata release + // 'battlegroup' => Profiler::urlize($this->curTpl['battlegroup']), // was renamed to subregion somewhere around cata release // 'battlegroupname' => $this->curTpl['battlegroup'], 'region' => Profiler::urlize($this->curTpl['region']) ); } - return array_values($data); + return $data; } - private function getGuildScores() + private function getGuildScores() : void { /* Guild gear scores and achievement points are derived using a weighted average of all of the known characters in that guild. @@ -44,18 +48,16 @@ class GuildList extends BaseType 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) @@ -78,96 +80,69 @@ class GuildList extends BaseType } } - public function renderTooltip() {} - public function getJSGlobals($addMask = 0) {} + public static function getName(int $id) : ?LocString { return null; } + + public function renderTooltip() : ?string { return null; } + public function getJSGlobals(int $addMask = 0) : array { return []; } } class GuildListFilter extends Filter { - public $extraOpts = []; - protected $genericFilter = []; + use TrProfilerFilter; - // fieldId => [checkType, checkValue[, fieldIsArray]] - protected $inputFields = array( - 'na' => [FILTER_V_REGEX, '/[\p{C};]/ui', false], // name - only printable chars, no delimiter - 'ma' => [FILTER_V_EQUAL, 1, false], // match any / all filter - 'ex' => [FILTER_V_EQUAL, 'on', false], // only match exact - 'si' => [FILTER_V_LIST, [1, 2], false], // side - 'rg' => [FILTER_V_CALLBACK, 'cbRegionCheck', false], // region - 'sv' => [FILTER_V_CALLBACK, 'cbServerCheck', false], // server + protected string $type = 'guilds'; + protected static array $genericFilter = []; + protected static array $inputFields = array( + '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 + '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 + 'sv' => [parent::V_CALLBACK, 'cbServerCheck', false] // server ); - protected function createSQLForCriterium(&$cr) { } + public array $extraOpts = []; - protected function createSQLForValues() + protected function createSQLForValues() : array { $parts = []; - $_v = $this->fiData['v']; + $_v = $this->values; // region (rg), battlegroup (bg) and server (sv) are passed to GuildList as miscData and handled there // name [str] - if (!empty($_v['na'])) - if ($_ = $this->modularizeString(['g.name'], $_v['na'], !empty($_v['ex']) && $_v['ex'] == 'on')) + if ($_v['na']) + if ($_ = $this->buildLikeLookup([['na', 'g.name']], $_v['ex'] == 'on')) $parts[] = $_; // side [list] - if (!empty($_v['si'])) - { - if ($_v['si'] == 1) - $parts[] = ['c.race', [1, 3, 4, 7, 11]]; - else if ($_v['si'] == 2) - $parts[] = ['c.race', [2, 5, 6, 8, 10]]; - } + if ($_v['si'] == SIDE_ALLIANCE) + $parts[] = ['c.race', ChrRace::fromMask(ChrRace::MASK_ALLIANCE)]; + else if ($_v['si'] == SIDE_HORDE) + $parts[] = ['c.race', ChrRace::fromMask(ChrRace::MASK_HORDE)]; return $parts; } - - protected function cbRegionCheck(&$v) - { - if ($v == 'eu' || $v == 'us') - { - $this->parentCats[0] = $v; // directly redirect onto this region - $v = ''; // remove from filter - - return true; - } - - return false; - } - - protected function cbServerCheck(&$v) - { - foreach (Profiler::getRealms() as $realm) - if ($realm['name'] == $v) - { - $this->parentCats[1] = Profiler::urlize($v);// directly redirect onto this server - $v = ''; // remove from filter - - return true; - } - - return false; - } } class RemoteGuildList extends GuildList { - protected $queryBase = 'SELECT `g`.*, `g`.`guildid` AS ARRAY_KEY FROM guild g'; - protected $queryOpts = array( + protected string $queryBase = 'SELECT `g`.*, `g`.`guildid` AS ARRAY_KEY FROM guild g'; + protected array $queryOpts = array( 'g' => [['gm', 'c'], 'g' => 'ARRAY_KEY'], - 'gm' => ['j' => 'guild_member gm ON gm.guildid = g.guildid', 's' => ', COUNT(1) AS members'], - 'c' => ['j' => 'characters c ON c.guid = gm.guid', 's' => ', BIT_OR(IF(c.race IN (1, 3, 4, 7, 11), 1, 2)) - 1 AS faction'] + 'gm' => ['j' => 'guild_member gm ON gm.`guildid` = g.`guildid`', 's' => ', COUNT(1) AS "members"'], + 'c' => ['j' => 'characters c ON c.`guid` = gm.`guid`', 's' => ', BIT_OR(IF(c.`race` IN (1, 3, 4, 7, 11), 1, 2)) - 1 AS "faction"'] ); - public function __construct($conditions = [], $miscData = null) + public function __construct(array $conditions = [], array $miscData = []) { // select DB by realm if (!$this->selectRealms($miscData)) { - trigger_error('no access to auth-db or table realmlist is empty', E_USER_WARNING); + trigger_error('RemoteGuildList::__construct - cannot access any realm.', E_USER_WARNING); return; } @@ -177,15 +152,14 @@ class RemoteGuildList extends GuildList return; reset($this->dbNames); // only use when querying single realm - $realmId = key($this->dbNames); - $realms = Profiler::getRealms(); - $distrib = []; + $realms = Profiler::getRealms(); + $distrib = []; // post processing foreach ($this->iterate() as $guid => &$curTpl) { // battlegroup - $curTpl['battlegroup'] = CFG_BATTLEGROUP; + $curTpl['battlegroup'] = Cfg::get('BATTLEGROUP'); $r = explode(':', $guid)[0]; if (!empty($realms[$r])) @@ -196,7 +170,15 @@ class RemoteGuildList extends GuildList } else { - trigger_error('character "'.$curTpl['name'].'" belongs to nonexistant realm #'.$r, E_USER_WARNING); + trigger_error('guild #'.$guid.' belongs to nonexistent realm #'.$r, E_USER_WARNING); + unset($this->templates[$guid]); + continue; + } + + // empty name + if (!$curTpl['name']) + { + trigger_error('guild #'.$guid.' on realm #'.$r.' has empty name.', E_USER_WARNING); unset($this->templates[$guid]); continue; } @@ -208,10 +190,14 @@ class RemoteGuildList extends GuildList $distrib[$curTpl['realm']]++; } - $limit = CFG_SQL_LIMIT_DEFAULT; + // 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); + + if (!$limit) // int:0 means unlimited, so skip early + return; $total = array_sum($distrib); foreach ($distrib as &$d) @@ -230,29 +216,27 @@ class RemoteGuildList extends GuildList } } - public function initializeLocalEntries() + 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 IGNORE INTO ?_profiler_guild (?#) VALUES '.$ins, 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) @@ -264,17 +248,38 @@ class RemoteGuildList extends GuildList class LocalGuildList extends GuildList { - protected $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($conditions = [], $miscData = null) + public function __construct(array $conditions = [], array $miscData = []) { + $realms = Profiler::getRealms(); + + // graft realm selection from miscData onto conditions + if (isset($miscData['sv'])) + $realms = array_filter($realms, fn($x) => Profiler::urlize($x['name']) == Profiler::urlize($miscData['sv'])); + + if (isset($miscData['rg'])) + $realms = array_filter($realms, fn($x) => $x['region'] == $miscData['rg']); + + if (!$realms) + { + trigger_error('LocalGuildList::__construct - cannot access any realm.', E_USER_WARNING); + return; + } + + if ($conditions) + { + array_unshift($conditions, DB::AND); + $conditions = [DB::AND, ['realm', array_keys($realms)], $conditions]; + } + else + $conditions = [['realm', array_keys($realms)]]; + parent::__construct($conditions, $miscData); if ($this->error) return; - $realms = Profiler::getRealms(); - foreach ($this->iterate() as $id => &$curTpl) { if ($curTpl['realm'] && !isset($realms[$curTpl['realm']])) @@ -287,17 +292,17 @@ class LocalGuildList extends GuildList } // battlegroup - $curTpl['battlegroup'] = CFG_BATTLEGROUP; + $curTpl['battlegroup'] = Cfg::get('BATTLEGROUP'); } } - public function getProfileUrl() + public function getProfileUrl() : string { $url = '?guild='; return $url.implode('.', array( - Profiler::urlize($this->getField('region')), - Profiler::urlize($this->getField('realmName')), + $this->getField('region'), + Profiler::urlize($this->getField('realmName'), true), Profiler::urlize($this->getField('name')) )); } diff --git a/includes/dbtypes/icon.class.php b/includes/dbtypes/icon.class.php new file mode 100644 index 00000000..c3971a97 --- /dev/null +++ b/includes/dbtypes/icon.class.php @@ -0,0 +1,205 @@ + '::items', + 'nSpells' => '::spell', + 'nAchievements' => '::achievement', + 'nCurrencies' => '::currencies', + 'nPets' => '::pet' + ); + + 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"'] + ); + */ + + public function __construct(array $conditions = [], array $miscData = []) + { + parent::__construct($conditions, $miscData); + + if (!$this->getFoundIDs()) + return; + + foreach ($this->pseudoJoin as $var => $tbl) + { + $res = DB::Aowow()->selectCol($this->pseudoQry, $tbl, $this->getFoundIDs()); + foreach ($res as $icon => $qty) + $this->templates[$icon][$var] = $qty; + } + } + + public static function getName(int $id) : ?LocString + { + if ($n = DB::Aowow()->selectRow('SELECT `name` AS "name_loc0" FROM %n WHERE `id` = %i', self::$dataTable, $id)) + return new LocString($n); + return null; + } + + public function getListviewData(int $addInfoMask = 0x0) : array + { + $data = []; + + foreach ($this->iterate() as $__) + { + $data[$this->id] = array( + 'id' => $this->id, + 'name' => $this->getField('name_source', true, true), + 'icon' => $this->getField('name', true, true), + 'itemcount' => (int)$this->getField('nItems'), + 'spellcount' => (int)$this->getField('nSpells'), + 'achievementcount' => (int)$this->getField('nAchievements'), + 'npccount' => 0, // UNUSED + 'petabilitycount' => 0, // UNUSED + 'currencycount' => (int)$this->getField('nCurrencies'), + 'missionabilitycount' => 0, // UNUSED + 'buildingcount' => 0, // UNUSED + 'petcount' => (int)$this->getField('nPets'), + 'threatcount' => 0, // UNUSED + 'classcount' => 0 // class icons are hardcoded and not referenced in dbc + ); + } + + return $data; + } + + public function getJSGlobals(int $addMask = GLOBALINFO_ANY) : array + { + $data = []; + + foreach ($this->iterate() as $__) + $data[Type::ICON][$this->id] = ['name' => $this->getField('name', true, true), 'icon' => $this->getField('name', true, true)]; + + return $data; + } + + public function renderTooltip() : ?string { return null; } +} + + +class IconListFilter extends Filter +{ + private array $iconTotals = []; + private array $criterion2field = array( + 1 => '::items', // items [num] + 2 => '::spell', // spells [num] + 3 => '::achievement', // achievements [num] + // 4 => '', // battlepets [num] + // 5 => '', // battlepetabilities [num] + 6 => '::currencies', // currencies [num] + // 7 => '', // garrisonabilities [num] + // 8 => '', // garrisonbuildings [num] + 9 => '::pet', // hunterpets [num] + // 10 => '', // garrisonmissionthreats [num] + 11 => '::classes', // classes [num] + 13 => '' // used [num] + ); + + protected string $type = 'icons'; + protected static array $genericFilter = array( + 1 => [parent::CR_CALLBACK, 'cbUsedBy' ], // items [num] + 2 => [parent::CR_CALLBACK, 'cbUsedBy' ], // spells [num] + 3 => [parent::CR_CALLBACK, 'cbUsedBy' ], // achievements [num] + 6 => [parent::CR_CALLBACK, 'cbUsedBy' ], // currencies [num] + 9 => [parent::CR_CALLBACK, 'cbUsedBy' ], // hunterpets [num] + 11 => [parent::CR_CALLBACK, 'cbUsedBy' ], // classes [num] + 13 => [parent::CR_CALLBACK, 'cbUsedBy', true] // used [num] + ); + + protected static array $inputFields = array( + '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_NAME, false, false], // name - only printable chars, no delimiter + 'ma' => [parent::V_EQUAL, 1, false] // match any / all filter + ); + + public array $extraOpts = []; + + protected function createSQLForValues() : array + { + $parts = []; + $_v = &$this->values; + + //string + if ($_v['na']) + if ($_ = $this->buildLikeLookup([['na', 'name']])) + $parts[] = $_; + + return $parts; + } + + protected function cbUsedBy(int $cr, int $crs, string $crv, ?bool $all = false) : ?array + { + if (!Util::checkNumeric($crv, NUM_CAST_INT) || ![$filter, $negate] = $this->int2Filter($crs, $crv)) + return null; + + $total = $this->prepareIconTotals($all ? 0 : $cr); + + $ids = array_filter($total, $filter); + + if ($negate) + return $ids ? ['id', array_keys($ids), '!'] : [1]; + else + return $ids ? ['id', array_keys($ids)] : ['id', array_keys($total), '!']; + } + + private function int2Filter(mixed $op, int $y) : ?array + { + return match ($op) { + 1 => [fn($x) => $x > $y, false], + 2 => [fn($x) => $x >= $y, false], + 3 => [fn($x) => $x == $y, false], + 4 => [fn($x) => $x > $y, true], + 5 => [fn($x) => $x >= $y, true], + 6 => [fn($x) => $x == $y, true], + default => null + }; + } + + private function prepareIconTotals(int $forCr = 0) : array + { + foreach ($this->criterion2field as $cr => $tbl) + { + 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 %n GROUP BY `iconId`', $tbl); + } + + if ($forCr) + return $this->iconTotals[$forCr]; + + if (!isset($this->iconTotals['all'])) + { + $this->iconTotals['all'] = []; + Util::arraySumByKey($this->iconTotals['all'], ...$this->iconTotals); + } + + return $this->iconTotals['all']; + } +} + +?> diff --git a/includes/dbtypes/item.class.php b/includes/dbtypes/item.class.php new file mode 100644 index 00000000..2932d2a9 --- /dev/null +++ b/includes/dbtypes/item.class.php @@ -0,0 +1,2643 @@ + Type::ITEM + 'i' => [['is', 'src', 'ic'], 'o' => 'i.`quality` DESC, i.`itemLevel` DESC'], + '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 = []) + { + parent::__construct($conditions, $miscData); + + foreach ($this->iterate() as &$_curTpl) + { + // item is scaling; overwrite other values + if ($_curTpl['scalingStatDistribution'] > 0 && $_curTpl['scalingStatValue'] > 0) + $this->initScalingStats(); + + // fix missing icons + $_curTpl['iconString'] = $_curTpl['iconString'] ?: DEFAULT_ICON; + + // from json to json .. the gentle fuckups of legacy code integration + $this->initJsonStats(); + $this->jsonStats[$this->id] = (new StatsContainer())->fromJson($_curTpl, true)->toJson(Stat::FLAG_ITEM /* | Stat::FLAG_SERVERSIDE */, false); + + if ($miscData) + { + // readdress itemset .. is wrong for virtual sets + if (isset($miscData['pcsToSet']) && isset($miscData['pcsToSet'][$this->id])) + $this->json[$this->id]['itemset'] = $miscData['pcsToSet'][$this->id]; + + // additional rel attribute for listview rows + if (isset($miscData['extraOpts']['relEnchant'])) + $this->relEnchant = $miscData['extraOpts']['relEnchant']; + } + + // sources + for ($i = 1; $i < 25; $i++) + { + if ($_ = $_curTpl['src'.$i]) + $this->sources[$this->id][$i][] = $_; + + unset($_curTpl['src'.$i]); + } + } + } + + // todo (med): information will get lost if one vendor sells one item multiple times with different costs (e.g. for item 54637) + // wowhead seems to have had the same issues + public function getExtendedCost(?array $filter = [], ?array &$reqRating = []) : array + { + if ($this->error) + return []; + + $idx = $this->id; + + 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()->selectAssoc( + 'SELECT nv.`item`, nv.`entry`, 0 AS "eventId", nv.`maxcount`, nv.`extendedCost`, nv.`incrtime` + FROM npc_vendor nv + WHERE nv.`item` IN %in + UNION + 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` + WHERE nv2.`item` IN %in + UNION + 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 genv.`item` IN %in', + $itemIds, $itemIds, $itemIds + ); + + foreach ($rawEntries as $costEntry) + { + if ($costEntry['extendedCost']) + $xCostData[] = $costEntry['extendedCost']; + + if (!isset($itemz[$costEntry['item']][$costEntry['entry']])) + $itemz[$costEntry['item']][$costEntry['entry']] = [$costEntry]; + else + $itemz[$costEntry['item']][$costEntry['entry']][] = $costEntry; + } + + if ($xCostData) + $xCostData = DB::Aowow()->selectAssoc('SELECT *, `id` AS ARRAY_KEY FROM ::itemextendedcost WHERE `id` IN %in', $xCostData); + + $cItems = []; + foreach ($itemz as $k => $vendors) + { + foreach ($vendors as $l => $vendor) + { + foreach ($vendor as $m => $vInfo) + { + $costs = []; + if (!empty($xCostData[$vInfo['extendedCost']])) + $costs = $xCostData[$vInfo['extendedCost']]; + + $data = array( + 'stock' => $vInfo['maxcount'] ?: -1, + 'event' => $vInfo['eventId'], + 'restock' => $vInfo['incrtime'], + 'reqRating' => $costs ? $costs['reqPersonalRating'] : 0, + 'reqBracket' => $costs ? $costs['reqArenaSlot'] : 0 + ); + + // hardcode arena) & honor + if (!empty($costs['reqArenaPoints'])) + { + $data[-103] = $costs['reqArenaPoints']; + $this->jsGlobals[Type::CURRENCY][CURRENCY_ARENA_POINTS] = CURRENCY_ARENA_POINTS; + } + + if (!empty($costs['reqHonorPoints'])) + { + $data[-104] = $costs['reqHonorPoints']; + $this->jsGlobals[Type::CURRENCY][CURRENCY_HONOR_POINTS] = CURRENCY_HONOR_POINTS; + } + + for ($i = 1; $i < 6; $i++) + { + if (!empty($costs['reqItemId'.$i]) && $costs['itemCount'.$i] > 0) + { + $data[$costs['reqItemId'.$i]] = $costs['itemCount'.$i]; + $cItems[] = $costs['reqItemId'.$i]; + } + } + + // no extended cost or additional gold required + if (!$costs || $this->getField('flagsExtra') & 0x04) + { + $this->getEntry($k); + if ($_ = $this->getField('buyPrice')) + $data[0] = $_; + } + + $vendor[$m] = $data; + } + $vendors[$l] = $vendor; + } + + $itemz[$k] = $vendors; + } + + // convert items to currency if possible + if ($cItems) + { + $moneyItems = new CurrencyList(array(['itemId', $cItems])); + foreach ($moneyItems->getJSGlobals() as $type => $jsData) + foreach ($jsData as $k => $v) + $this->jsGlobals[$type][$k] = $v; + + foreach ($itemz as $itemId => $vendors) + { + foreach ($vendors as $npcId => $costData) + { + foreach ($costData as $itr => $cost) + { + foreach ($cost as $k => $v) + { + if (in_array($k, $cItems)) + { + $found = false; + foreach ($moneyItems->iterate() as $__) + { + if ($moneyItems->getField('itemId') == $k) + { + unset($cost[$k]); + $cost[-$moneyItems->id] = $v; + $found = true; + break; + } + } + + if (!$found) + $this->jsGlobals[Type::ITEM][$k] = $k; + } + } + $costData[$itr] = $cost; + } + $vendors[$npcId] = $costData; + } + $itemz[$itemId] = $vendors; + } + } + + $this->vendors = $itemz; + } + + $result = $this->vendors; + + // apply filter if given + $tok = !empty($filter[Type::ITEM]) ? $filter[Type::ITEM] : null; + $cur = !empty($filter[Type::CURRENCY]) ? $filter[Type::CURRENCY] : null; + + foreach ($result as $itemId => &$data) + { + $reqRating = []; + foreach ($data as $npcId => $entries) + { + foreach ($entries as $costs) + { + if ($tok || $cur) // bought with specific token or currency + { + $valid = false; + foreach ($costs as $k => $qty) + { + if ((!$tok || $k == $tok) && (!$cur || $k == -$cur)) + { + $valid = true; + break; + } + } + + if (!$valid) + unset($data[$npcId]); + } + + // reqRating ins't really a cost .. so pass it by ref instead of return + // data was invalid and deleted or some source doesn't require arena rating + if (!isset($data[$npcId]) || ($reqRating && !$reqRating[0])) + continue; + + // use lowest total value + if (!$costs['reqRating']) + $reqRating = [0, 2]; + else if ($costs['reqRating'] && (!$reqRating || $reqRating[0] > $costs['reqRating'])) + $reqRating = [$costs['reqRating'], $costs['reqBracket']]; + } + } + + if (empty($data)) + unset($result[$itemId]); + } + + // restore internal index; + $this->getEntry($idx); + + return $result; + } + + public function getListviewData(int $addInfoMask = 0x0, ?array $miscData = null) : array + { + /* + * ITEMINFO_JSON (0x01): jsonStats (including spells) and subitems parsed + * ITEMINFO_SUBITEMS (0x02): searched by comparison + * ITEMINFO_VENDOR (0x04): costs-obj, when displayed as vendor + * ITEMINFO_GEM (0x10): gem infos and score + * ITEMINFO_MODEL (0x20): sameModelAs-Tab + */ + + $data = []; + + // random item is random + if ($addInfoMask & ITEMINFO_SUBITEMS) + $this->initSubItems(); + + if ($addInfoMask & ITEMINFO_JSON) + { + $this->extendJsonStats(); + Util::arraySumByKey($data, $this->jsonStats); + } + + $extCosts = []; + if ($addInfoMask & ITEMINFO_VENDOR) + $extCosts = $this->getExtendedCost($miscData); + + $extCostOther = []; + foreach ($this->iterate() as $__) + { + foreach ($this->json[$this->id] as $k => $v) + $data[$this->id][$k] = $v; + + // json vs listview quirk + $data[$this->id]['name'] = $data[$this->id]['quality'].Lang::unescapeUISequences($this->getField('name', true), Lang::FMT_RAW); + unset($data[$this->id]['quality']); + + if (!empty($this->relEnchant) && $this->curTpl['randomEnchant']) + { + if (($x = array_search($this->curTpl['randomEnchant'], array_column($this->relEnchant, 'entry'))) !== false) + { + $data[$this->id]['rel'] = 'rand='.$this->relEnchant[$x]['ench']; + $data[$this->id]['name'] .= ' '.$this->relEnchant[$x]['name']; + } + } + + if ($addInfoMask & ITEMINFO_JSON) + { + if ($_ = intVal(($this->curTpl['minMoneyLoot'] + $this->curTpl['maxMoneyLoot']) / 2)) + $data[$this->id]['avgmoney'] = $_; + + if ($_ = $this->curTpl['repairPrice']) + $data[$this->id]['repaircost'] = $_; + } + + if ($addInfoMask & (ITEMINFO_JSON | ITEMINFO_GEM)) + if (isset($this->curTpl['score'])) + $data[$this->id]['score'] = $this->curTpl['score']; + + if ($addInfoMask & ITEMINFO_GEM) + { + $data[$this->id]['uniqEquip'] = ($this->curTpl['flags'] & ITEM_FLAG_UNIQUEEQUIPPED) ? 1 : 0; + $data[$this->id]['socketLevel'] = 0; // not used with wotlk + } + + if ($addInfoMask & ITEMINFO_VENDOR) + { + // just use the first results + // todo (med): dont use first vendor; search for the right one + if (!empty($extCosts[$this->id])) + { + $cost = reset($extCosts[$this->id]); + foreach ($cost as $itr => $entries) + { + $currency = []; + $tokens = []; + $costArr = []; + + foreach ($entries as $k => $qty) + { + if (is_string($k)) + continue; + + if ($k > 0) + $tokens[] = [$k, $qty]; + else if ($k < 0) + $currency[] = [-$k, $qty]; + } + + $costArr['stock'] = $entries['stock'];// display as column in lv + $costArr['avail'] = $entries['stock'];// display as number on icon + $costArr['cost'] = [empty($entries[0]) ? 0 : $entries[0]]; + $costArr['restock'] = $entries['restock']; + + if ($entries['event']) + if (Conditions::extendListviewRow($costArr, Conditions::SRC_NONE, $this->id, [Conditions::ACTIVE_EVENT, $entries['event']])) + $this->jsGlobals[Type::WORLDEVENT][$entries['event']] = $entries['event']; + + if ($currency || $tokens) // fill idx:3 if required + $costArr['cost'][] = $currency; + + if ($tokens) + $costArr['cost'][] = $tokens; + + if (!empty($entries['reqRating'])) + $costArr['reqarenartng'] = $entries['reqRating']; + + if ($itr > 0) + $extCostOther[$this->id][] = $costArr; + else + $data[$this->id] = array_merge($data[$this->id], $costArr); + } + } + + if ($x = $this->curTpl['buyPrice']) + $data[$this->id]['buyprice'] = $x; + + if ($x = $this->curTpl['sellPrice']) + $data[$this->id]['sellprice'] = $x; + + if ($x = $this->curTpl['buyCount']) + $data[$this->id]['stack'] = $x; + } + + if ($this->curTpl['class'] == ITEM_CLASS_GLYPH) + $data[$this->id]['glyph'] = $this->curTpl['subSubClass']; + + if ($x = $this->curTpl['requiredSkill']) + $data[$this->id]['reqskill'] = $x; + + if ($x = $this->curTpl['requiredSkillRank']) + $data[$this->id]['reqskillrank'] = $x; + + if ($x = $this->curTpl['requiredSpell']) + $data[$this->id]['reqspell'] = $x; + + if ($x = $this->curTpl['requiredFaction']) + $data[$this->id]['reqfaction'] = $x; + + if ($x = $this->curTpl['requiredFactionRank']) + { + $data[$this->id]['reqrep'] = $x; + $data[$this->id]['standing'] = $x; // used in /faction item-listing + } + + if ($x = $this->curTpl['slots']) + $data[$this->id]['nslots'] = $x; + + $_ = $this->curTpl['requiredRace']; + if (ChrRace::sideFromMask($_) != SIDE_BOTH) + $data[$this->id]['reqrace'] = $_; + + if ($_ = $this->curTpl['requiredClass']) + $data[$this->id]['reqclass'] = $_; // $data[$this->id]['classes'] ?? + + if ($this->curTpl['flags'] & ITEM_FLAG_HEROIC) + $data[$this->id]['heroic'] = true; + + if ($addInfoMask & ITEMINFO_MODEL) + if ($_ = $this->getField('displayId')) + $data[$this->id]['displayid'] = $_; + + if ($this->getSources($s, $sm)) + { + $data[$this->id]['source'] = $s; + if ($sm) + $data[$this->id]['sourcemore'] = $sm; + } + + if (!empty($this->curTpl['cooldown'])) + $data[$this->id]['cooldown'] = $this->curTpl['cooldown'] / 1000; + } + + foreach ($extCostOther as $itemId => $duplicates) + foreach ($duplicates as $d) + $data[] = array_merge($data[$itemId], $d); // we dont really use keys on data, but this may cause errors in future + + /* even more complicated crap + modelviewer {type:X, displayid:Y, slot:z} .. not sure, when to set + */ + + return $data; + } + + public function getJSGlobals(int $addMask = GLOBALINFO_SELF, ?array &$extra = []) : array + { + $data = $addMask & GLOBALINFO_RELATED ? $this->jsGlobals : []; + + foreach ($this->iterate() as $id => $__) + { + if ($addMask & GLOBALINFO_SELF) + { + $data[Type::ITEM][$id] = array( + 'name' => Lang::unescapeUISequences($this->getField('name', true), Lang::FMT_RAW), + '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) + { + $extra[$id] = array( + // 'id' => $id, + 'tooltip' => $this->renderTooltip(true), + 'spells' => new \StdClass // placeholder for knownSpells + ); + } + } + + return $data; + } + + /* + enhance (set by comparison tool or formated external links) + ench: enchantmentId + sock: bool (extraScoket (gloves, belt)) + gems: array (:-separated itemIds) + rand: >0: randomPropId; <0: randomSuffixId + interactive (set to place javascript/anchors to manipulate level and ratings or link to filters (static tooltips vs popup tooltip)) + subOf (tabled layout doesn't work if used as sub-tooltip in other item or spell tooltips; use line-break instead) + */ + public function getField(string $field, bool $localized = false, bool $silent = false, ?array $enhance = []) : mixed + { + $res = parent::getField($field, $localized, $silent); + + if ($field == 'name' && !empty($enhance['r'])) + if ($this->getRandEnchantForItem($enhance['r'])) + $res .= ' '.Util::localizedString($this->enhanceR, 'name'); + + return $res; + } + + public function renderTooltip(bool $interactive = false, int $subOf = 0, ?array $enhance = []) : ?string + { + if ($this->error) + return null; + + $_name = Lang::unescapeUISequences($this->getField('name', true), Lang::FMT_HTML); + $_reqLvl = $this->curTpl['requiredLevel']; + $_quality = $this->curTpl['quality']; + $_flags = $this->curTpl['flags']; + $_class = $this->curTpl['class']; + $_subClass = $this->curTpl['subClass']; + $_slot = $this->curTpl['slot']; + $causesScaling = false; + + if (!empty($enhance['r'])) + { + if ($this->getRandEnchantForItem($enhance['r'])) + { + $_name .= ' '.Util::localizedString($this->enhanceR, 'name'); + $randEnchant = ''; + + for ($i = 1; $i < 6; $i++) + { + if ($this->enhanceR['enchantId'.$i] <= 0) + continue; + + $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()); + $randEnchant .= ''.str_replace('$i', $amount, Util::localizedString($enchant, 'name')).'
'; + } + else + $randEnchant .= ''.Util::localizedString($enchant, 'name').'
'; + } + } + else + unset($enhance['r']); + } + + if (isset($enhance['s']) && !in_array($_slot, [INVTYPE_WRISTS, INVTYPE_WAIST, INVTYPE_HANDS])) + unset($enhance['s']); + + // IMPORTAT: DO NOT REMOVE THE HTML-COMMENTS! THEY ARE REQUIRED TO UPDATE THE TOOLTIP CLIENTSIDE + $x = ''; + + // upper table: stats + if (!$subOf) + $x .= '
'; + + // name; quality + if ($subOf) + $x .= ''.$_name.''; + else + $x .= ''.$_name.''; + + // heroic tag + if (($_flags & ITEM_FLAG_HEROIC) && $_quality == ITEM_QUALITY_EPIC) + $x .= '
'.Lang::item('heroic').''; + + // 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` = %i LIMIT 1', $_); + $x .= '
'.Util::localizedString($map, 'name').''; + } + + // requires area + if ($this->curTpl['area']) + { + $area = DB::Aowow()->selectRow('SELECT * FROM ::zones WHERE `id` = %i LIMIT 1', $this->curTpl['area']); + $x .= '
'.Util::localizedString($area, 'name'); + } + + // conjured + if ($_flags & ITEM_FLAG_CONJURED) + $x .= '
'.Lang::item('conjured'); + + // bonding + if ($_flags & ITEM_FLAG_ACCOUNTBOUND) + $x .= '
'.Lang::item('bonding', 0); + else if ($this->curTpl['bonding']) + $x .= '
'.Lang::item('bonding', $this->curTpl['bonding']); + + // unique || unique-equipped || unique-limited + if ($this->curTpl['maxCount'] == 1) + $x .= '
'.Lang::item('unique', 0); + // not for currency tokens + else if ($this->curTpl['maxCount'] && $this->curTpl['bagFamily'] != 8192) + $x .= '
'.sprintf(Lang::item('unique', 1), $this->curTpl['maxCount']); + else if ($_flags & ITEM_FLAG_UNIQUEEQUIPPED) + $x .= '
'.Lang::item('uniqueEquipped', 0); + else if ($this->curTpl['itemLimitCategory']) + { + $limit = DB::Aowow()->selectRow('SELECT * FROM ::itemlimitcategory WHERE `id` = %i', $this->curTpl['itemLimitCategory']); + $x .= '
'.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` = %i', $eId)) + $x .= '
'.sprintf(Lang::game('requires'), ''.Util::localizedString($hName, 'name').''); + + // item begins a quest + if ($this->curTpl['startQuest']) + $x .= '
'.Lang::item('startQuest').''; + + // containerType (slotCount) + if ($this->curTpl['slots'] > 0) + { + $fam = $this->curTpl['bagFamily'] ? log($this->curTpl['bagFamily'], 2) + 1 : 0; + $x .= '
'.Lang::item('bagSlotString', [$this->curTpl['slots'], Lang::item('bagFamily', $fam)]); + } + + if (in_array($_class, [ITEM_CLASS_ARMOR, ITEM_CLASS_WEAPON, ITEM_CLASS_AMMUNITION])) + { + $x .= ''; + + // Class + if ($_slot) + $x .= ''; + + // Subclass + if ($_class == ITEM_CLASS_ARMOR && $_subClass > 0) + $x .= ''; + else if ($_class == ITEM_CLASS_WEAPON) + $x .= ''; + else if ($_class == ITEM_CLASS_AMMUNITION) + $x .= ''; + + $x .= '
'.Lang::item('inventoryType', $_slot).''.Lang::item('armorSubClass', $_subClass).''.Lang::item('weaponSubClass', $_subClass).''.Lang::item('projectileSubClass', $_subClass).'
'; + } + else if ($_slot && $_class != ITEM_CLASS_CONTAINER) // yes, slot can occur on random items and is then also displayed <_< .. excluding Bags >_> + $x .= '
'.Lang::item('inventoryType', $_slot).'
'; + else + $x .= '
'; + + // Weapon/Ammunition Stats (not limited to weapons (see item:1700)) + $speed = $this->curTpl['delay'] / 1000; + $sc1 = $this->curTpl['dmgType1']; + $sc2 = $this->curTpl['dmgType2']; + $dmgmin = $this->curTpl['tplDmgMin1'] + $this->curTpl['dmgMin2']; + $dmgmax = $this->curTpl['tplDmgMax1'] + $this->curTpl['dmgMax2']; + $dps = $speed ? ($dmgmin + $dmgmax) / (2 * $speed) : 0; + + if ($_class == ITEM_CLASS_AMMUNITION && $dmgmin && $dmgmax) + { + if ($sc1) + $x .= sprintf(Lang::item('damage', 'ammo', 1), ($dmgmin + $dmgmax) / 2, Lang::game('sc', $sc1)).'
'; + else + $x .= sprintf(Lang::item('damage', 'ammo', 0), ($dmgmin + $dmgmax) / 2).'
'; + } + else if ($dps) + { + if ($this->curTpl['tplDmgMin1'] == $this->curTpl['tplDmgMax1']) + $dmg = sprintf(Lang::item('damage', 'single', $sc1 ? 1 : 0), $this->curTpl['tplDmgMin1'], $sc1 ? Lang::game('sc', $sc1) : null); + else + $dmg = sprintf(Lang::item('damage', 'range', $sc1 ? 1 : 0), $this->curTpl['tplDmgMin1'], $this->curTpl['tplDmgMax1'], $sc1 ? Lang::game('sc', $sc1) : null); + + if ($_class == ITEM_CLASS_WEAPON) // do not use localized format here! + $x .= '
'.$dmg.''.Lang::item('speed').' '.number_format($speed, 2).'
'; + else + $x .= ''.$dmg.'
'; + + // secondary damage is set + if (($this->curTpl['dmgMin2'] || $this->curTpl['dmgMax2']) && $this->curTpl['dmgMin2'] != $this->curTpl['dmgMax2']) + $x .= sprintf(Lang::item('damage', 'range', $sc2 ? 3 : 2), $this->curTpl['dmgMin2'], $this->curTpl['dmgMax2'], $sc2 ? Lang::game('sc', $sc2) : null).'
'; + else if ($this->curTpl['dmgMin2']) + $x .= sprintf(Lang::item('damage', 'single', $sc2 ? 3 : 2), $this->curTpl['dmgMin2'], $sc2 ? Lang::game('sc', $sc2) : null).'
'; + + if ($_class == ITEM_CLASS_WEAPON) + $x .= ''.Lang::item('dps', [$dps]).'
'; + + // display FeralAttackPower if set + if ($fap = $this->getFeralAP()) + $x .= '('.$fap.' '.Lang::item('fap').')
'; + } + + // Armor + if ($_class == ITEM_CLASS_ARMOR && $this->curTpl['armorDamageModifier'] > 0) + { + $spanI = 'class="q2"'; + if ($interactive) + $spanI = 'class="q2 tip" onmouseover="$WH.Tooltip.showAtCursor(event, $WH.sprintf(LANG.tooltip_armorbonus, '.$this->curTpl['armorDamageModifier'].'), 0, 0, \'q\')" onmousemove="$WH.Tooltip.cursorUpdate(event)" onmouseout="$WH.Tooltip.hide()"'; + + $x .= ''.Lang::item('armor', [$this->curTpl['tplArmor']]).'
'; + } + else if ($this->curTpl['tplArmor']) + $x .= ''.Lang::item('armor', [$this->curTpl['tplArmor']]).'
'; + + // Block (note: block value from field block and from field stats or parsed from itemSpells are displayed independently) + if ($this->curTpl['tplBlock']) + $x .= ''.sprintf(Lang::item('block'), $this->curTpl['tplBlock']).'
'; + + // Item is a gem (don't mix with sockets) + if ($geId = $this->curTpl['gemEnchantmentId']) + { + $gemEnch = DB::Aowow()->selectRow('SELECT * FROM ::itemenchantment WHERE `id` = %i', $geId); + $x .= ''.Util::localizedString($gemEnch, 'name').'
'; + + // activation conditions for meta gems + if (!empty($gemEnch['conditionId'])) + $x .= Game::getEnchantmentCondition($gemEnch['conditionId'], $interactive); + } + + // Random Enchantment - if random enchantment is set, prepend stats from it + if ($this->curTpl['randomEnchant'] && empty($enhance['r'])) + $x .= ''.Lang::item('randEnchant').'
'; + else if (!empty($enhance['r'])) + $x .= $randEnchant; + + // itemMods (display stats and save ratings for later use) + for ($j = 1; $j <= 10; $j++) + { + $type = $this->curTpl['statType'.$j]; + $qty = $this->curTpl['statValue'.$j]; + + if (!$qty || $type <= 0) + continue; + + $statId = Stat::getIndexFrom(Stat::IDX_ITEM_MOD, $type); + + // base stat + switch ($statId) + { + case Stat::MANA: + case Stat::HEALTH: + case Stat::AGILITY: + case Stat::STRENGTH: + 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 .= ''.Lang::item('statType', $type, [ord($qty > 0 ? '+' : '-'), abs($qty)]).'
'; + break; + default: // rating with % for reqLevel + $green[] = $this->formatRating($statId, $type, $qty, $interactive, $causesScaling); + } + } + + // magic resistances + foreach (Game::$resistanceFields as $j => $rowName) + if ($rowName && $this->curTpl[$rowName] != 0) + $x .= '+'.$this->curTpl[$rowName].' '.Lang::game('resistances', $j).'
'; + + // Enchantment + if (isset($enhance['e'])) + { + if ($enchText = DB::Aowow()->selectRow('SELECT * FROM ::itemenchantment WHERE `id` = %s', $enhance['e'])) + $x .= ''.Util::localizedString($enchText, 'name').'
'; + else + { + unset($enhance['e']); + $x .= ''; + } + } + else // enchantment placeholder + $x .= ''; + + // Sockets w/ Gems + if (!empty($enhance['g'])) + { + $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 %in', + $enhance['g'] + ); + + foreach ($enhance['g'] as $k => $v) + if ($v && !in_array($v, array_keys($gems))) // 0 is valid + unset($enhance['g'][$k]); + } + else + $enhance['g'] = []; + + // zero fill empty sockets + $sockCount = isset($enhance['s']) ? 1 : 0; + if (!empty($this->json[$this->id]['nsockets'])) + $sockCount += $this->json[$this->id]['nsockets']; + + while ($sockCount > count($enhance['g'])) + $enhance['g'][] = 0; + + $enhance['g'] = array_reverse($enhance['g']); + + $hasMatch = 1; + // fill native sockets + for ($j = 1; $j <= 3; $j++) + { + if (!$this->curTpl['socketColor'.$j]) + continue; + + for ($i = 0; $i < 4; $i++) + if (($this->curTpl['socketColor'.$j] & (1 << $i))) + $colorId = $i; + + $pop = array_pop($enhance['g']); + $col = $pop ? 1 : 0; + $hasMatch &= $pop ? (($gems[$pop]['colorMask'] & (1 << $colorId)) ? 1 : 0) : 0; + $icon = $pop ? sprintf('style="background-image: url(%s/images/wow/icons/tiny/%s.gif)"', Cfg::get('STATIC_URL'), strtolower($gems[$pop]['iconString'])) : null; + $text = $pop ? Util::localizedString($gems[$pop], 'name') : Lang::item('socket', $colorId); + + if ($interactive) + $x .= ''.$text.'
'; + else + $x .= ''.$text.'
'; + } + + // fill extra socket + if (isset($enhance['s'])) + { + $pop = array_pop($enhance['g']); + $col = $pop ? 1 : 0; + $icon = $pop ? sprintf('style="background-image: url(%s/images/wow/icons/tiny/%s.gif)"', Cfg::get('STATIC_URL'), strtolower($gems[$pop]['iconString'])) : null; + $text = $pop ? Util::localizedString($gems[$pop], 'name') : Lang::item('socket', -1); + + if ($interactive) + $x .= ''.$text.'
'; + else + $x .= ''.$text.'
'; + } + else // prismatic socket placeholder + $x .= ''; + + if ($_ = $this->curTpl['socketBonus']) + { + $sbonus = DB::Aowow()->selectRow('SELECT * FROM ::itemenchantment WHERE `id` = %i', $_); + $x .= ''.Lang::item('socketBonus', [''.Util::localizedString($sbonus, 'name').'']).'
'; + } + + // durability + if ($dur = $this->curTpl['durability']) + $x .= sprintf(Lang::item('durability'), $dur, $dur).'
'; + + // max duration + if ($dur = $this->curTpl['duration']) + { + $rt = ''; + if ($this->curTpl['flagsCustom'] & 0x1) + $rt = $interactive ? ' ('.sprintf(Util::$dfnString, 'LANG.tooltip_realduration', Lang::item('realTime')).')' : ' ('.Lang::item('realTime').')'; + + $x .= Lang::formatTime(abs($dur) * 1000, 'item', 'duration').$rt."
"; + } + + // required classes + $jsg = []; + if ($classes = Lang::getClassString($this->curTpl['requiredClass'], $jsg)) + { + foreach ($jsg as $js) + $this->jsGlobals[Type::CHR_CLASS][$js] ??= $js; + + $x .= Lang::game('classes').Lang::main('colon').$classes.'
'; + } + + // required races + $jsg = []; + if ($races = Lang::getRaceString($this->curTpl['requiredRace'], $jsg)) + { + foreach ($jsg as $js) + $this->jsGlobals[Type::CHR_RACE][$js] ??= $js; + + $x .= Lang::game('races').Lang::main('colon').$races.'
'; + } + + // required honorRank (not used anymore) + if ($rhr = $this->curTpl['requiredHonorRank']) + $x .= Lang::game('requires', [implode(' / ', Lang::game('pvpRank', $rhr))]).'
'; + + // required CityRank..? + // what the f.. + + // required level + if (($_flags & ITEM_FLAG_ACCOUNTBOUND) && $_quality == ITEM_QUALITY_HEIRLOOM) + $x .= sprintf(Lang::item('reqLevelRange'), 1, MAX_LEVEL, ($interactive ? sprintf(Util::$changeLevelString, MAX_LEVEL) : ''.MAX_LEVEL)).'
'; + else if ($_reqLvl > 1) + $x .= sprintf(Lang::item('reqMinLevel'), $_reqLvl).'
'; + + // required arena team rating / personal rating / todo (low): sort out what kind of rating + if (!empty($this->getExtendedCost([], $reqRating)[$this->id]) && $reqRating && $reqRating[0]) + $x .= sprintf(Lang::item('reqRating', $reqRating[1]), $reqRating[0]).'
'; + + // item level + if (in_array($_class, [ITEM_CLASS_ARMOR, ITEM_CLASS_WEAPON])) + $x .= sprintf(Lang::item('itemLevel'), $this->curTpl['itemLevel']).'
'; + + // required skill + if ($reqSkill = $this->curTpl['requiredSkill']) + { + $_ = ''.SkillList::getName($reqSkill).''; + if ($this->curTpl['requiredSkillRank'] > 0) + $_ .= ' ('.$this->curTpl['requiredSkillRank'].')'; + + $x .= sprintf(Lang::game('requires'), $_).'
'; + } + + // required spell + if ($reqSpell = $this->curTpl['requiredSpell']) + $x .= Lang::game('requires2').' '.SpellList::getName($reqSpell).'
'; + + // required reputation w/ faction + if ($reqFac = $this->curTpl['requiredFaction']) + $x .= sprintf(Lang::game('requires'), ''.FactionList::getName($reqFac).' - '.Lang::game('rep', $this->curTpl['requiredFactionRank'])).'
'; + + // locked or openable + if ($locks = Lang::getLocks($this->curTpl['lockId'], $arr, true)) + $x .= ''.Lang::item('locked').'
'.implode('
', array_map(fn($x) => Lang::game('requires', [$x]), $locks)).'

'; + else if ($this->curTpl['flags'] & ITEM_FLAG_OPENABLE) + $x .= ''.Lang::item('openClick').'
'; + + // upper table: done + if (!$subOf) + $x .= '
'; + + // spells on item + if (!$this->canTeachSpell()) + { + $itemSpellsAndTrigger = []; + for ($j = 1; $j <= 5; $j++) + { + if ($this->curTpl['spellId'.$j] > 0) + { + $cd = $this->curTpl['spellCooldown'.$j]; + if ($cd < $this->curTpl['spellCategoryCooldown'.$j]) + $cd = $this->curTpl['spellCategoryCooldown'.$j]; + + $extra = []; + if ($cd >= 5000 && $this->curTpl['spellTrigger'.$j] != SPELL_TRIGGER_EQUIP) + { + $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 + $extra[] = Lang::item('cooldown', 0, [Lang::formatTime($cd, 'game', 'timeAbbrev', true)]); + } + if ($this->curTpl['spellTrigger'.$j] == SPELL_TRIGGER_HIT) + if ($ppm = $this->curTpl['spellppmRate'.$j]) + $extra[] = Lang::spell('ppm', [$ppm]); + + $itemSpellsAndTrigger[$this->curTpl['spellId'.$j]] = [$this->curTpl['spellTrigger'.$j], $extra ? ' '.implode(', ', $extra) : '']; + } + } + + if ($itemSpellsAndTrigger) + { + $itemSpells = new SpellList(array(['s.id', array_keys($itemSpellsAndTrigger)])); + foreach ($itemSpells->iterate() as $sId => $__) + { + [$parsed, $_, $scaling] = $itemSpells->parseText('description', $_reqLvl > 1 ? $_reqLvl : MAX_LEVEL); + if (!$parsed && User::isInGroup(U_GROUP_EMPLOYEE)) + $parsed = '<'.$itemSpells->getField('name', true, true).'>'; + else if (!$parsed) + continue; + + if ($scaling) + $causesScaling = true; + + if ($interactive) + { + $link = '%s'; + $parsed = preg_replace_callback('/([^;]*)( .*?<\/small>)([^&]*)/i', function($m) use($link) { + $m[1] = $m[1] ? sprintf($link, $m[1]) : ''; + $m[3] = $m[3] ? sprintf($link, $m[3]) : ''; + return $m[1].$m[2].$m[3]; + }, $parsed, -1, $nMatches + ); + + if (!$nMatches) + $parsed = sprintf($link, $parsed); + } + + $green[] = Lang::item('trigger', $itemSpellsAndTrigger[$itemSpells->id][0]).$parsed.$itemSpellsAndTrigger[$itemSpells->id][1]; + } + } + } + + // lower table (ratings, spells, ect) + if (!$subOf) + $x .= '
'; + + if (isset($green)) + foreach ($green as $j => $bonus) + if ($bonus) + $x .= ''.$bonus.'
'; + + // Item Set + $pieces = []; + if ($setId = $this->getField('itemset')) + { + $condition = [ + ['refSetId', $setId], + // ['quality', $this->curTpl['quality']], + ['minLevel', $this->curTpl['itemLevel'], '<='], + ['maxLevel', $this->curTpl['itemLevel'], '>='] + ]; + + $itemset = new ItemsetList($condition); + if (!$itemset->error && $itemset->pieceToSet) + { + // handle special cases where: + // > itemset has items of different qualities (handled by not limiting for this in the initial query) + // > itemset is virtual and multiple instances have the same itemLevel but not quality (filter below) + foreach ($itemset->iterate() as $id => $__) + { + if ($itemset->getField('quality') == $this->curTpl['quality']) + { + $itemset->pieceToSet = array_filter($itemset->pieceToSet, function($x) use ($id) { return $id == $x; }); + break; + } + } + + $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 %in + GROUP BY b.`id`', + array_keys($itemset->pieceToSet) + ); + + foreach ($pieces as $k => &$p) + $p = ''.Util::localizedString($p, 'name').''; + + $xSet = '
'.Lang::item('setName', [''.$itemset->getField('name', true).'', 0, count($pieces)]).''; + + if ($skId = $itemset->getField('skillId')) // bonus requires skill to activate + { + $xSet .= '
'.sprintf(Lang::game('requires'), ''.SkillList::getName($skId).''); + + if ($_ = $itemset->getField('skillLevel')) + $xSet .= ' ('.$_.')'; + + $xSet .= '
'; + } + + // list pieces + $xSet .= '
'.implode('
', $pieces).'

'; + + // get bonuses + $setSpellsAndIdx = []; + for ($j = 1; $j <= 8; $j++) + if ($_ = $itemset->getField('spell'.$j)) + $setSpellsAndIdx[$_] = $j; + + $setSpells = []; + if ($setSpellsAndIdx) + { + $boni = new SpellList(array(['s.id', array_keys($setSpellsAndIdx)])); + foreach ($boni->iterate() as $__) + { + [$parsed, $_, $scaling] = $boni->parseText('description', $_reqLvl > 1 ? $_reqLvl : MAX_LEVEL); + if ($scaling && $interactive) + $causesScaling = true; + + $setSpells[] = array( + 'tooltip' => $parsed, + 'entry' => $itemset->getField('spell'.$setSpellsAndIdx[$boni->id]), + 'bonus' => $itemset->getField('bonus'.$setSpellsAndIdx[$boni->id]) + ); + } + } + + // sort and list bonuses + $xSet .= ''; + for ($i = 0; $i < count($setSpells); $i++) + { + for ($j = $i; $j < count($setSpells); $j++) + { + if ($setSpells[$j]['bonus'] >= $setSpells[$i]['bonus']) + continue; + + $tmp = $setSpells[$i]; + $setSpells[$i] = $setSpells[$j]; + $setSpells[$j] = $tmp; + } + $xSet .= ''.Lang::item('setBonus', [$setSpells[$i]['bonus'], ''.$setSpells[$i]['tooltip'].'']).''; + if ($i < count($setSpells) - 1) + $xSet .= '
'; + } + $xSet .= '
'; + } + } + + // recipes, vanity pets, mounts + if ($this->canTeachSpell()) + { + $craftSpell = new SpellList(array(['s.id', intVal($this->curTpl['spellId2'])])); + if (!$craftSpell->error) + { + $xCraft = ''; + if ($desc = $this->getField('description', true)) + $x .= ''.Lang::item('trigger', SPELL_TRIGGER_USE).' '.$desc.'
'; + + // recipe handling (some stray Techniques have subclass == 0), place at bottom of tooltipp + if ($_class == ITEM_CLASS_RECIPE || $this->curTpl['bagFamily'] == 16) + { + if ($craftSpell->canCreateItem()) + { + $craftItem = new ItemList(array(['i.id', (int)$craftSpell->curTpl['effect1CreateItemId']])); + if (!$craftItem->error) + if ($itemTT = $craftItem->renderTooltip($interactive, $this->id)) + $xCraft .= '

'.$itemTT.'
'; + } + + $reagentItems = []; + for ($i = 1; $i <= 8; $i++) + if ($rId = $craftSpell->getField('reagent'.$i)) + $reagentItems[$rId] = $craftSpell->getField('reagentCount'.$i); + + if ($reagentItems) + { + $reagents = new ItemList(array(['i.id', array_keys($reagentItems)])); + $reqReag = []; + + foreach ($reagents->iterate() as $__) + $reqReag[] = ''.$reagents->getField('name', true).' ('.$reagentItems[$reagents->id].')'; + + $xCraft .= '

'.Lang::game('requires2').' '.implode(', ', $reqReag).'
'; + } + } + } + } + + // misc (no idea, how to organize the
better) + $xMisc = []; + + // itemset: pieces and boni + if (isset($xSet)) + $xMisc[] = $xSet; + + // funny, yellow text at the bottom, omit if we have a recipe + if ($this->curTpl['description_loc0'] && !$this->canTeachSpell()) + $xMisc[] = '"'.Util::parseHtmlText($this->getField('description', true), false).'"'; + + // readable + if ($this->curTpl['pageTextId']) + $xMisc[] = ''.Lang::item('readClick').''; + + // charges + for ($i = 1; $i < 6; $i++) + { + if (in_array($this->curTpl['spellTrigger'.$i], [SPELL_TRIGGER_USE, SPELL_TRIGGER_SOULSTONE, SPELL_TRIGGER_USE_NODELAY, SPELL_TRIGGER_LEARN]) && $this->curTpl['spellCharges'.$i]) + { + $xMisc[] = ''.Lang::item('charges', [abs($this->curTpl['spellCharges'.$i])]).''; + break; + } + } + + // list required reagents + if (isset($xCraft)) + $xMisc[] = $xCraft; + + if ($xMisc) + $x .= implode('
', $xMisc); + + if ($sp = $this->curTpl['sellPrice']) + $x .= '
'.Lang::item('sellPrice').Lang::main('colon').Util::formatMoney($sp).'
'; + + if (!$subOf) + $x .= '
'; + + // tooltip scaling + if (!isset($xCraft)) + { + $itemId = $subOf ?: $this->id; + + $x .= ''; + } + + return $x; + } + + public function getRandEnchantForItem(int $randId) : bool + { + // 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` = %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() : float + { + if (empty($this->randPropPoints[$this->curTpl['itemLevel']])) + $this->randPropPoints[$this->curTpl['itemLevel']] = DB::Aowow()->selectRow('SELECT * FROM ::itemrandomproppoints WHERE `id` = %s', $this->curTpl['itemLevel']); + + $rpp = &$this->randPropPoints[$this->curTpl['itemLevel']]; + + if (!$rpp) + return 0.0; + + $fieldIdx = match((int)$this->curTpl['slot']) + { + 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 + return match((int)$this->curTpl['quality']) + { + 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 + { + $enchantments = []; // buffer Ids for lookup id => src; src>0: socketBonus; src<0: gemEnchant + + foreach ($this->iterate() as $__) + { + // fetch and add socketbonusstats + if (!empty($this->json[$this->id]['socketbonus'])) + $enchantments[$this->json[$this->id]['socketbonus']][] = $this->id; + + // Item is a gem (don't mix with sockets) + if ($geId = $this->curTpl['gemEnchantmentId']) + $enchantments[$geId][] = -$this->id; + } + + if ($enchantments) + { + $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) + { + if (empty($eStats[$eId])) + continue; + + foreach ($items as $item) + { + if ($item > 0) // apply socketBonus + $this->json[$item]['socketbonusstat'] = array_filter($eStats[$eId]); + else /* if ($item < 0) */ // apply gemEnchantment + Util::arraySumByKey($this->json[-$item], array_filter($eStats[$eId])); + } + } + } + + foreach ($this->json as $item => $json) + foreach ($json as $k => $v) + if (!$v && !in_array($k, ['classs', 'subclass', 'quality', 'side', 'gearscore'])) + unset($this->json[$item][$k]); + } + + public function getOnUseStats() : ?StatsContainer + { + if ($this->curTpl['class'] != ITEM_CLASS_CONSUMABLE) + return null; + + $onUseStats = new StatsContainer(); + + // convert Spells + for ($h = 1; $h <= 5; $h++) + { + if ($this->curTpl['spellId'.$h] <= 0) + continue; + + if ($this->curTpl['spellTrigger'.$h] != SPELL_TRIGGER_USE) + continue; + + if ($spell = DB::Aowow()->selectRow( + '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); + } + + return $onUseStats; + } + + public function getSourceData(int $id = 0) : array + { + $data = []; + + foreach ($this->iterate() as $__) + { + if ($id && $id != $this->id) + continue; + + $data[$this->id] = array( + 'n' => $this->getField('name', true), + 't' => Type::ITEM, + 'ti' => $this->id, + 'q' => $this->curTpl['quality'], + // 'p' => PvP [NYI] + 'icon' => $this->curTpl['iconString'] + ); + } + + return $data; + } + + private function canTeachSpell() : bool + { + if (!in_array($this->curTpl['spellId1'], LEARN_SPELLS)) + return false; + + // needs learnable spell + if (!$this->curTpl['spellId2']) + return false; + + return true; + } + + private function getFeralAP() : float + { + // must be weapon + if ($this->curTpl['class'] != ITEM_CLASS_WEAPON) + return 0.0; + + // thats fucked up.. + if (!$this->curTpl['delay']) + return 0.0; + + // must have enough damage + $dps = ($this->curTpl['tplDmgMin1'] + $this->curTpl['dmgMin2'] + $this->curTpl['tplDmgMax1'] + $this->curTpl['dmgMax2']) / (2 * $this->curTpl['delay'] / 1000); + if ($dps <= 54.8) + return 0.0; + + $subClasses = [ITEM_SUBCLASS_MISC_WEAPON]; + $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)) + $subClasses[] = $i; + + // cannot be used by druids + if (!in_array($this->curTpl['subClass'], $subClasses)) + return 0.0; + + return round(($dps - 54.8) * 14); + } + + public function isRangedWeapon() : bool + { + if ($this->curTpl['class'] != ITEM_CLASS_WEAPON) + return false; + + return in_array($this->curTpl['subClassBak'], [ITEM_SUBCLASS_BOW, ITEM_SUBCLASS_GUN, ITEM_SUBCLASS_THROWN, ITEM_SUBCLASS_CROSSBOW, ITEM_SUBCLASS_WAND]); + } + + public function isBodyArmor() : bool + { + if ($this->curTpl['class'] != ITEM_CLASS_ARMOR) + return false; + + return in_array($this->curTpl['subClassBak'], [ITEM_SUBCLASS_CLOTH_ARMOR, ITEM_SUBCLASS_LEATHER_ARMOR, ITEM_SUBCLASS_MAIL_ARMOR, ITEM_SUBCLASS_PLATE_ARMOR]); + } + + public function isDisplayable() : bool + { + if (!$this->curTpl['displayId']) + return false; + + return in_array($this->curTpl['slot'], array( + INVTYPE_HEAD, INVTYPE_SHOULDERS, INVTYPE_BODY, INVTYPE_CHEST, INVTYPE_WAIST, INVTYPE_LEGS, INVTYPE_FEET, INVTYPE_WRISTS, + INVTYPE_HANDS, INVTYPE_WEAPON, INVTYPE_SHIELD, INVTYPE_RANGED, INVTYPE_CLOAK, INVTYPE_2HWEAPON, INVTYPE_TABARD, INVTYPE_ROBE, + INVTYPE_WEAPONMAINHAND, INVTYPE_WEAPONOFFHAND, INVTYPE_HOLDABLE, INVTYPE_THROWN, INVTYPE_RANGEDRIGHT)); + } + + private function formatRating(int $statId, int $itemMod, int $qty, bool $interactive = false, bool &$scaling = false) : string + { + // clamp level range + $ssdLvl = isset($this->ssd[$this->id]) ? $this->ssd[$this->id]['maxLevel'] : 1; + $reqLvl = $this->curTpl['requiredLevel'] > 1 ? $this->curTpl['requiredLevel'] : MAX_LEVEL; + $level = min(max($reqLvl, $ssdLvl), MAX_LEVEL); + + // unknown rating + if (!$statId) + { + if (User::isInGroup(U_GROUP_EMPLOYEE)) + return Lang::item('statType', count(Lang::item('statType')) - 1, [$itemMod, $qty]); + else + return ''; + } + + // level independent Bonus + if (Stat::isLevelIndependent($statId)) + return Lang::item('trigger', SPELL_TRIGGER_EQUIP).str_replace('%d', ''.$qty, Lang::item('statType', $itemMod)); + + // rating-Bonuses + $scaling = true; + + if ($interactive) + $js = ' ('.sprintf(Util::$changeLevelString, Util::setRatingLevel($level, $statId, $qty)).')'; + else + $js = ' ('.Util::setRatingLevel($level, $statId, $qty).')'; + + return Lang::item('trigger', SPELL_TRIGGER_EQUIP).str_replace('%d', ''.$qty.$js, Lang::item('statType', $itemMod)); + } + + private function getSSDMod(string $type) : int + { + $mask = $this->curTpl['scalingStatValue']; + + $mask &= match ($type) + { + 'stats' => 0x04001F, + 'armor' => 0xF001E0, + 'dps' => 0x007E00, + 'spell' => 0x008000, + 'fap' => 0x010000, // unused + default => 0x0 + }; + + $field = null; + for ($i = 0; $i < count(Util::$ssdMaskFields); $i++) + if ($mask & (1 << $i)) + $field = Util::$ssdMaskFields[$i]; + + 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` = %i', $this->curTpl['scalingStatDistribution']); + + if (!$this->ssd[$this->id]) + return; + + // stats and ratings + for ($i = 1; $i <= 10; $i++) + { + if ($this->ssd[$this->id]['statMod'.$i] <= 0) + { + $this->templates[$this->id]['statType'.$i] = 0; + $this->templates[$this->id]['statValue'.$i] = 0; + } + else + { + $this->templates[$this->id]['statType'.$i] = $this->ssd[$this->id]['statMod'.$i]; + $this->templates[$this->id]['statValue'.$i] = intVal(($this->getSSDMod('stats') * $this->ssd[$this->id]['modifier'.$i]) / 10000); + } + } + + // armor: only replace if set + if ($ssvArmor = $this->getSSDMod('armor')) + $this->templates[$this->id]['armor'] = $ssvArmor; + + // if set dpsMod in ScalingStatValue use it for min/max damage + // mle: 20% range / rgd: 30% range + if ($extraDPS = $this->getSSDMod('dps')) // dmg_x2 not used for heirlooms + { + $range = isset($this->json[$this->id]['rgddps']) ? 0.3 : 0.2; + $average = $extraDPS * $this->curTpl['delay'] / 1000; + + $this->templates[$this->id]['tplDmgMin1'] = floor((1 - $range) * $average); + $this->templates[$this->id]['tplDmgMax1'] = floor((1 + $range) * $average); + } + + // apply Spell Power from ScalingStatValue if set + if ($spellBonus = $this->getSSDMod('spell')) + { + $this->templates[$this->id]['statType10'] = ITEM_MOD_SPELL_POWER; + $this->templates[$this->id]['statValue10'] = $spellBonus; + } + } + + public function initSubItems() : void + { + if (!array_keys($this->templates)) + return; + + $subItemIds = []; + foreach ($this->iterate() as $__) + if ($_ = $this->getField('randomEnchant')) + $subItemIds[abs($_)] = $_; + + if (!$subItemIds) + return; + + // remember: id < 0: randomSuffix; id > 0: randomProperty + $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] + ); + + $randIds = []; + foreach ($subItemTpls as $tpl) + $randIds = array_merge($randIds, array_keys($tpl)); + + if (!$randIds) + return; + + $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'), + array_column($randEnchants, 'enchantId3'), + array_column($randEnchants, 'enchantId4'), + array_column($randEnchants, 'enchantId5') + )); + + $enchants = new EnchantmentList(array(['id', $enchIds])); + foreach ($enchants->iterate() as $eId => $_) + { + $this->rndEnchIds[$eId] = array( + 'text' => $enchants->getField('name', true), + 'stats' => $enchants->getStatGainForCurrent() + ); + } + + foreach ($this->iterate() as $mstItem => $__) + { + if (!$this->getField('randomEnchant')) + continue; + + if (empty($subItemTpls[$this->getField('randomEnchant')])) + continue; + + foreach ($subItemTpls[$this->getField('randomEnchant')] as $subId => $data) + { + if (empty($randEnchants[$subId])) + continue; + + $data = array_merge($randEnchants[$subId], $data); + $jsonEquip = []; + $jsonText = []; + + for ($i = 1; $i < 6; $i++) + { + $enchId = $data['enchantId'.$i]; + if ($enchId <= 0 || empty($this->rndEnchIds[$enchId])) + continue; + + if ($data['allocationPct'.$i] > 0) // RandomSuffix: scaling Enchantment; enchId < 0 + { + $qty = intVal($data['allocationPct'.$i] * $this->generateEnchSuffixFactor()); + $stats = array_fill_keys(array_keys($this->rndEnchIds[$enchId]['stats']), $qty); + + $jsonText[$enchId] = str_replace('$i', $qty, $this->rndEnchIds[$enchId]['text']); + Util::arraySumByKey($jsonEquip, $stats); + } + else // RandomProperty: static Enchantment; enchId > 0 + { + $jsonText[$enchId] = $this->rndEnchIds[$enchId]['text']; + Util::arraySumByKey($jsonEquip, $this->rndEnchIds[$enchId]['stats']); + } + } + + $this->subItems[$mstItem][$subId] = array( + 'name' => Util::localizedString($data, 'name'), + 'enchantment' => $jsonText, + 'jsonequip' => $jsonEquip, + 'chance' => $data['chance'] // hmm, only needed for item detail page... + ); + } + + if (!empty($this->subItems[$mstItem])) + $this->json[$mstItem]['subitems'] = $this->subItems[$mstItem]; + } + } + + public function getScoreTotal(int $class = 0, array $spec = [], int $mhItem = 0, int $ohItem = 0) : int + { + if (!$class || !$spec) + return array_sum(array_column($this->json, 'gearscore')); + + $score = 0.0; + $mh = $oh = []; + + foreach ($this->json as $j) + { + if ($j['id'] == $mhItem) + $mh = $j; + else if ($j['id'] == $ohItem) + $oh = $j; + else if (!empty($j['gearscore'])) + { + if ($j['slot'] == INVTYPE_RELIC) + $score += 20; + + $score += round($j['gearscore']); + } + } + + $score += array_sum(Util::fixWeaponScores($class, $spec, $mh, $oh)); + + return $score; + } + + private function initJsonStats() : void + { + $class = $this->curTpl['class']; + $subclass = $this->curTpl['subClass']; + + $json = array( + 'id' => $this->id, + 'quality' => ITEM_QUALITY_HEIRLOOM - $this->curTpl['quality'], + 'classs' => $class, + 'subclass' => $subclass, + 'subsubclass' => $this->curTpl['subSubClass'], + 'heroic' => ($this->curTpl['flags'] & ITEM_FLAG_HEROIC) >> 3, + 'side' => $this->curTpl['flagsExtra'] & 0x3 ? SIDE_BOTH - ($this->curTpl['flagsExtra'] & 0x3) : ChrRace::sideFromMask($this->curTpl['requiredRace']), + 'slot' => $this->curTpl['slot'], + 'slotbak' => $this->curTpl['slotBak'], + 'level' => $this->curTpl['itemLevel'], + 'reqlevel' => $this->curTpl['requiredLevel'], + 'displayid' => $this->curTpl['displayId'], + 'holres' => $this->curTpl['resHoly'], + 'firres' => $this->curTpl['resFire'], + 'natres' => $this->curTpl['resNature'], + 'frores' => $this->curTpl['resFrost'], + 'shares' => $this->curTpl['resShadow'], + 'arcres' => $this->curTpl['resArcane'], + 'armorbonus' => $class != ITEM_CLASS_ARMOR ? 0 : max(0, intVal($this->curTpl['armorDamageModifier'])), + 'armor' => $this->curTpl['tplArmor'], + 'dura' => $this->curTpl['durability'], + 'itemset' => $this->curTpl['itemset'], + 'socket1' => $this->curTpl['socketColor1'], + 'socket2' => $this->curTpl['socketColor2'], + 'socket3' => $this->curTpl['socketColor3'], + 'nsockets' => ($this->curTpl['socketColor1'] > 0 ? 1 : 0) + ($this->curTpl['socketColor2'] > 0 ? 1 : 0) + ($this->curTpl['socketColor3'] > 0 ? 1 : 0), + 'socketbonus' => $this->curTpl['socketBonus'], + 'scadist' => $this->curTpl['scalingStatDistribution'], + 'scaflags' => $this->curTpl['scalingStatValue'] + ); + + $json = array_map('intval', $json); + + $json['name'] = $this->getField('name', true); + $json['icon'] = $this->curTpl['iconString']; + + if ($class == ITEM_CLASS_AMMUNITION) + $json['dps'] = round(($this->curTpl['tplDmgMin1'] + $this->curTpl['dmgMin2'] + $this->curTpl['tplDmgMax1'] + $this->curTpl['dmgMax2']) / 2, 2); + else if ($class == ITEM_CLASS_WEAPON) + { + $json['dmgtype1'] = (int)$this->curTpl['dmgType1']; + $json['dmgmin1'] = (int)($this->curTpl['tplDmgMin1'] + $this->curTpl['dmgMin2']); + $json['dmgmax1'] = (int)($this->curTpl['tplDmgMax1'] + $this->curTpl['dmgMax2']); + $json['speed'] = round($this->curTpl['delay'] / 1000, 2); + $json['dps'] = $json['speed'] ? round(($json['dmgmin1'] + $json['dmgmax1']) / (2 * $json['speed']), 1) : 0.0; + + if ($this->isRangedWeapon()) + { + $json['rgddmgmin'] = $json['dmgmin1']; + $json['rgddmgmax'] = $json['dmgmax1']; + $json['rgdspeed'] = $json['speed']; + $json['rgddps'] = $json['dps']; + } + else + { + $json['mledmgmin'] = $json['dmgmin1']; + $json['mledmgmax'] = $json['dmgmax1']; + $json['mlespeed'] = $json['speed']; + $json['mledps'] = $json['dps']; + } + + if ($fap = $this->getFeralAP()) + $json['feratkpwr'] = $fap; + } + + if ($class == ITEM_CLASS_ARMOR || $class == ITEM_CLASS_WEAPON) + $json['gearscore'] = Util::getEquipmentScore($json['level'], $this->getField('quality'), $json['slot'], $json['nsockets']); + else if ($class == ITEM_CLASS_GEM) + $json['gearscore'] = Util::getGemScore($json['level'], $this->getField('quality'), $this->getField('requiredSkill') == SKILL_JEWELCRAFTING, $this->id); + + // clear zero-values afterwards + foreach ($json as $k => $v) + if (!$v && !in_array($k, ['classs', 'subclass', 'quality', 'side', 'gearscore'])) + unset($json[$k]); + + $this->json[$json['id']] = $json; + } +} + + +class ItemListFilter extends Filter +{ + public const /* int */ GROUP_BY_NONE = 0; + public const /* int */ GROUP_BY_SLOT = 1; + public const /* int */ GROUP_BY_LEVEL = 2; + 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 %in UNION + SELECT `item` FROM game_event_npc_vendor WHERE `extendedCost` IN %in'; + + protected string $type = 'items'; + protected static array $enums = array( + 16 => parent::ENUM_ZONE, // drops in zone + 17 => parent::ENUM_FACTION, // requiresrepwith + 99 => parent::ENUM_PROFESSION, // requiresprof + 86 => parent::ENUM_PROFESSION, // craftedprof + 87 => parent::ENUM_PROFESSION, // reagentforability + 105 => parent::ENUM_HEROICDUNGEON, // drops in nh dungeon + 106 => parent::ENUM_HEROICDUNGEON, // drops in hc dungeon + 126 => parent::ENUM_ZONE, // rewardedbyquestin + 147 => parent::ENUM_MULTIMODERAID, // drops in nh raid 10 + 148 => parent::ENUM_MULTIMODERAID, // drops in nh raid 25 + 149 => parent::ENUM_HEROICRAID, // drops in hc raid 10 + 150 => parent::ENUM_HEROICRAID, // drops in hc raid 25 + 152 => parent::ENUM_CLASSS, // class-specific + 153 => parent::ENUM_RACE, // race-specific + 160 => parent::ENUM_EVENT, // relatedevent + 169 => parent::ENUM_EVENT, // requiresevent + 158 => parent::ENUM_CURRENCY, // purchasablewithcurrency + 118 => array( // itemcurrency + 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, + 16202, 10938, 11134, 11174, 22447, 20725, 14343, 34053, 10978, 11138, 22448, 11177, 11083, 10940, 11137, 22450 + ), + 91 => array( // tool + 3, 14, 162, 168, 141, 2, 4, 169, 161, 15, 167, 81, 21, 165, 12, 62, 10, 101, 189, 6, + 63, 41, 8, 7, 190, 9, 166, 121, 5 + ), + 66 => array( // profession specialization + 1 => -1, + 2 => [ 9788, 9787, 17041, 17040, 17039 ], + 3 => -1, + 4 => -1, + 5 => [20219, 20222 ], + 6 => -1, + 7 => -1, + 8 => [10656, 10658, 10660 ], + 9 => -1, + 10 => [26798, 26801, 26797 ], + 11 => [ 9788, 9787, 17041, 17040, 17039, 20219, 20222, 10656, 10658, 10660, 26798, 26801, 26797], // i know, i know .. lazy as fuck + 12 => false, + 13 => -1, + 14 => -1, + 15 => -1 + ), + 128 => array( // source + 1 => true, // Any + 2 => false, // None + 3 => SRC_CRAFTED, + 4 => SRC_DROP, + 5 => SRC_PVP, + 6 => SRC_QUEST, + 7 => SRC_VENDOR, + 9 => SRC_STARTER, + 10 => SRC_EVENT, + 11 => SRC_ACHIEVEMENT, + 12 => SRC_FISHING + ) + ); + + protected static array $genericFilter = array( + 2 => [parent::CR_CALLBACK, 'cbFieldHasVal', 'bonding', 1 ], // bindonpickup [yn] + 3 => [parent::CR_CALLBACK, 'cbFieldHasVal', 'bonding', 2 ], // bindonequip [yn] + 4 => [parent::CR_CALLBACK, 'cbFieldHasVal', 'bonding', 3 ], // bindonuse [yn] + 5 => [parent::CR_CALLBACK, 'cbFieldHasVal', 'bonding', [4, 5] ], // questitem [yn] + 6 => [parent::CR_CALLBACK, 'cbQuestRelation', null, null ], // startsquest [side] + 7 => [parent::CR_BOOLEAN, 'description_loc0', true ], // hasflavortext + 8 => [parent::CR_BOOLEAN, 'requiredDisenchantSkill' ], // disenchantable + 9 => [parent::CR_FLAG, 'flags', ITEM_FLAG_CONJURED ], // conjureditem + 10 => [parent::CR_BOOLEAN, 'lockId' ], // locked + 11 => [parent::CR_FLAG, 'flags', ITEM_FLAG_OPENABLE ], // openable + 12 => [parent::CR_BOOLEAN, 'itemset' ], // partofset + 13 => [parent::CR_BOOLEAN, 'randomEnchant' ], // randomlyenchanted + 14 => [parent::CR_BOOLEAN, 'pageTextId' ], // readable + 15 => [parent::CR_CALLBACK, 'cbFieldHasVal', 'maxCount', 1 ], // unique [yn] + 16 => [parent::CR_CALLBACK, 'cbDropsInZone', null, null ], // dropsin [zone] + 17 => [parent::CR_ENUM, 'requiredFaction', true, true ], // requiresrepwith + 18 => [parent::CR_CALLBACK, 'cbFactionQuestReward', null, null ], // rewardedbyfactionquest [side] + 20 => [parent::CR_NUMERIC, 'is.str', NUM_CAST_INT, true ], // str + 21 => [parent::CR_NUMERIC, 'is.agi', NUM_CAST_INT, true ], // agi + 22 => [parent::CR_NUMERIC, 'is.sta', NUM_CAST_INT, true ], // sta + 23 => [parent::CR_NUMERIC, 'is.int', NUM_CAST_INT, true ], // int + 24 => [parent::CR_NUMERIC, 'is.spi', NUM_CAST_INT, true ], // spi + 25 => [parent::CR_NUMERIC, 'is.arcres', NUM_CAST_INT, true ], // arcres + 26 => [parent::CR_NUMERIC, 'is.firres', NUM_CAST_INT, true ], // firres + 27 => [parent::CR_NUMERIC, 'is.natres', NUM_CAST_INT, true ], // natres + 28 => [parent::CR_NUMERIC, 'is.frores', NUM_CAST_INT, true ], // frores + 29 => [parent::CR_NUMERIC, 'is.shares', NUM_CAST_INT, true ], // shares + 30 => [parent::CR_NUMERIC, 'is.holres', NUM_CAST_INT, true ], // holres + 32 => [parent::CR_NUMERIC, 'is.dps', NUM_CAST_FLOAT, true ], // dps + 33 => [parent::CR_NUMERIC, 'is.dmgmin1', NUM_CAST_INT, true ], // dmgmin1 + 34 => [parent::CR_NUMERIC, 'is.dmgmax1', NUM_CAST_INT, true ], // dmgmax1 + 35 => [parent::CR_CALLBACK, 'cbDamageType', null, null ], // damagetype [enum] + 36 => [parent::CR_NUMERIC, 'is.speed', NUM_CAST_FLOAT, true ], // speed + 37 => [parent::CR_NUMERIC, 'is.mleatkpwr', NUM_CAST_INT, true ], // mleatkpwr + 38 => [parent::CR_NUMERIC, 'is.rgdatkpwr', NUM_CAST_INT, true ], // rgdatkpwr + 39 => [parent::CR_NUMERIC, 'is.rgdhitrtng', NUM_CAST_INT, true ], // rgdhitrtng + 40 => [parent::CR_NUMERIC, 'is.rgdcritstrkrtng', NUM_CAST_INT, true ], // rgdcritstrkrtng + 41 => [parent::CR_NUMERIC, 'is.armor', NUM_CAST_INT, true ], // armor + 42 => [parent::CR_NUMERIC, 'is.defrtng', NUM_CAST_INT, true ], // defrtng + 43 => [parent::CR_NUMERIC, 'is.block', NUM_CAST_INT, true ], // block + 44 => [parent::CR_NUMERIC, 'is.blockrtng', NUM_CAST_INT, true ], // blockrtng + 45 => [parent::CR_NUMERIC, 'is.dodgertng', NUM_CAST_INT, true ], // dodgertng + 46 => [parent::CR_NUMERIC, 'is.parryrtng', NUM_CAST_INT, true ], // parryrtng + 48 => [parent::CR_NUMERIC, 'is.splhitrtng', NUM_CAST_INT, true ], // splhitrtng + 49 => [parent::CR_NUMERIC, 'is.splcritstrkrtng', NUM_CAST_INT, true ], // splcritstrkrtng + 50 => [parent::CR_NUMERIC, 'is.splheal', NUM_CAST_INT, true ], // splheal + 51 => [parent::CR_NUMERIC, 'is.spldmg', NUM_CAST_INT, true ], // spldmg + 52 => [parent::CR_NUMERIC, 'is.arcsplpwr', NUM_CAST_INT, true ], // arcsplpwr + 53 => [parent::CR_NUMERIC, 'is.firsplpwr', NUM_CAST_INT, true ], // firsplpwr + 54 => [parent::CR_NUMERIC, 'is.frosplpwr', NUM_CAST_INT, true ], // frosplpwr + 55 => [parent::CR_NUMERIC, 'is.holsplpwr', NUM_CAST_INT, true ], // holsplpwr + 56 => [parent::CR_NUMERIC, 'is.natsplpwr', NUM_CAST_INT, true ], // natsplpwr + 57 => [parent::CR_NUMERIC, 'is.shasplpwr', NUM_CAST_INT, true ], // shasplpwr + 59 => [parent::CR_NUMERIC, 'durability', NUM_CAST_INT, true ], // dura + 60 => [parent::CR_NUMERIC, 'is.healthrgn', NUM_CAST_INT, true ], // healthrgn + 61 => [parent::CR_NUMERIC, 'is.manargn', NUM_CAST_INT, true ], // manargn + 62 => [parent::CR_CALLBACK, 'cbCooldown', null, null ], // cooldown [op] [int] + 63 => [parent::CR_NUMERIC, 'buyPrice', NUM_CAST_INT, true ], // buyprice + 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', 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', 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', 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 + 80 => [parent::CR_CALLBACK, 'cbHasSockets', null, null ], // has sockets [enum] + 81 => [parent::CR_CALLBACK, 'cbFitsGemSlot', null, null ], // fits gem slot [enum] + 83 => [parent::CR_FLAG, 'flags', ITEM_FLAG_UNIQUEEQUIPPED ], // uniqueequipped + 84 => [parent::CR_NUMERIC, 'is.mlecritstrkrtng', NUM_CAST_INT, true ], // mlecritstrkrtng + 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', 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', 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 + 97 => [parent::CR_NUMERIC, 'is.feratkpwr', NUM_CAST_INT, true ], // feratkpwr + 98 => [parent::CR_FLAG, 'flags', ITEM_FLAG_PARTYLOOT ], // partyloot + 99 => [parent::CR_ENUM, 'requiredSkill' ], // requiresprof + 100 => [parent::CR_NUMERIC, 'is.nsockets', NUM_CAST_INT ], // nsockets + 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, '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_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 + 114 => [parent::CR_NUMERIC, 'is.armorpenrtng', NUM_CAST_INT, true ], // armorpenrtng + 115 => [parent::CR_NUMERIC, 'is.health', NUM_CAST_INT, true ], // health + 116 => [parent::CR_NUMERIC, 'is.mana', NUM_CAST_INT, true ], // mana + 117 => [parent::CR_NUMERIC, 'is.exprtng', NUM_CAST_INT, true ], // exprtng + 118 => [parent::CR_CALLBACK, 'cbPurchasableWith', null, null ], // purchasablewithitem [enum] + 119 => [parent::CR_NUMERIC, 'is.hitrtng', NUM_CAST_INT, true ], // hitrtng + 123 => [parent::CR_NUMERIC, 'is.splpwr', NUM_CAST_INT, true ], // splpwr + 124 => [parent::CR_CALLBACK, 'cbHasRandEnchant', null, null ], // randomenchants [str] + 125 => [parent::CR_CALLBACK, 'cbReqArenaRating', null, null ], // reqarenartng [op] [int] todo (low): 'find out, why "IN (W, X, Y) AND IN (X, Y, Z)" doesn't result in "(X, Y)" + 126 => [parent::CR_CALLBACK, 'cbQuestRewardIn', null, null ], // rewardedbyquestin [zone-any] + 128 => [parent::CR_CALLBACK, 'cbSource', null, null ], // source [enum] + 129 => [parent::CR_CALLBACK, 'cbSoldByNPC', null, null ], // soldbynpc [str-small] + 130 => [parent::CR_FLAG, 'cuFlags', CUSTOM_HAS_COMMENT ], // hascomments + 132 => [parent::CR_CALLBACK, 'cbGlyphType', null, null ], // glyphtype [enum] + 133 => [parent::CR_FLAG, 'flags', ITEM_FLAG_ACCOUNTBOUND ], // accountbound + 134 => [parent::CR_NUMERIC, 'is.mledps', NUM_CAST_FLOAT, true ], // mledps + 135 => [parent::CR_NUMERIC, 'is.mledmgmin', NUM_CAST_INT, true ], // mledmgmin + 136 => [parent::CR_NUMERIC, 'is.mledmgmax', NUM_CAST_INT, true ], // mledmgmax + 137 => [parent::CR_NUMERIC, 'is.mlespeed', NUM_CAST_FLOAT, true ], // mlespeed + 138 => [parent::CR_NUMERIC, 'is.rgddps', NUM_CAST_FLOAT, true ], // rgddps + 139 => [parent::CR_NUMERIC, 'is.rgddmgmin', NUM_CAST_INT, true ], // rgddmgmin + 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', 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 + 147 => [parent::CR_CALLBACK, 'cbDropsInInstance', SRC_FLAG_RAID_DROP, 1, ], // dropsinnormal10 [multimoderaid-any] + 148 => [parent::CR_CALLBACK, 'cbDropsInInstance', SRC_FLAG_RAID_DROP, 2, ], // dropsinnormal25 [multimoderaid-any] + 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' ], // 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 + 157 => [parent::CR_FLAG, 'flags', ITEM_FLAG_SMARTLOOT ], // smartloot + 158 => [parent::CR_CALLBACK, 'cbPurchasableWith', null, null ], // purchasablewithcurrency [enum] + 159 => [parent::CR_FLAG, 'flags', ITEM_FLAG_MILLABLE ], // millable + 160 => [parent::CR_NYI_PH, null, 1, ], // relatedevent [enum] like 169 .. crawl though npc_vendor and loot_templates of event-related spawns + 161 => [parent::CR_CALLBACK, 'cbAvailable', null, null ], // availabletoplayers [yn] + 162 => [parent::CR_FLAG, 'flags', ITEM_FLAG_DEPRECATED ], // deprecated + 163 => [parent::CR_CALLBACK, 'cbDisenchantsInto', null, null ], // disenchantsinto [disenchanting] + 165 => [parent::CR_NUMERIC, 'repairPrice', NUM_CAST_INT, true ], // repaircost + 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', 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 + ); + + protected static array $inputFields = array( + 'wt' => [parent::V_CALLBACK, 'cbWeightKeyCheck', true ], // weight keys + 'wtv' => [parent::V_RANGE, [1, 999], true ], // weight values + 'jc' => [parent::V_LIST, [1], false], // use jewelcrafter gems for weight calculation + 'gm' => [parent::V_LIST, [2, 3, 4], false], // gem rarity for weight calculation + 'cr' => [parent::V_RANGE, [1, 177], 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 + '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_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, [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 + public array $wtCnd = []; + + public function createConditionsForWeights() : array + { + if (empty($this->values['wt'])) + return []; + + $this->wtCnd = []; + $select = []; + $wtSum = 0; + + foreach ($this->values['wt'] as $k => $v) + { + if ($str = Stat::getWeightJson($v)) + { + $qty = intVal($this->values['wtv'][$k]); + + $select[] = '(IFNULL(`is`.`'.$str.'`, 0) * '.$qty.')'; + $this->wtCnd[] = ['is.'.$str, 0, '>']; + $wtSum += $qty; + } + } + + if (count($this->wtCnd) > 1) + array_unshift($this->wtCnd, DB::OR); + else if (count($this->wtCnd) == 1) + $this->wtCnd = $this->wtCnd[0]; + + if ($select) + { + $this->extraOpts['is']['s'][] = ', IF(`is`.`typeId` IS NULL, 0, ('.implode(' + ', $select).') / '.$wtSum.') AS "score"'; + $this->extraOpts['is']['o'][] = 'score DESC'; + $this->extraOpts['i']['o'][] = null; // remove default ordering + } + else + $this->extraOpts['is']['s'][] = ', 0 AS "score"'; // prevent errors + + return $this->wtCnd; + } + + public function getConditions() : array + { + if (!$this->ubFilter) + { + $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 + $this->ubFilter[$cId] = [ITEM_CLASS_WEAPON => [ITEM_SUBCLASS_MISC_WEAPON], ITEM_CLASS_ARMOR => [ITEM_SUBCLASS_MISC_ARMOR]]; + + for ($i = 0; $i < 21; $i++) + if ($weaponTypeMask & (1 << $i)) + $this->ubFilter[$cId][ITEM_CLASS_WEAPON][] = $i; + + for ($i = 0; $i < 11; $i++) + if ($armorTypeMask & (1 << $i)) + $this->ubFilter[$cId][ITEM_CLASS_ARMOR][] = $i; + } + } + + return parent::getConditions(); + } + + protected function createSQLForValues() : array + { + $parts = []; + $_v = $this->values; + + // weights [list] + if ($_v['wt'] && $_v['wtv']) + { + // gm - gem quality (qualityId) + // jc - jc-gems included (bool) + + if ($_ = $this->createConditionsForWeights()) + $parts[] = $_; + + foreach ($_v['wt'] as $_) + $this->fiExtraCols[] = $_; + } + + // upgrade for [list] + if ($_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; + } + + // name + if ($_v['na']) + { + 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) + if ($_v['ub']) + { + $parts[] = array( + DB::AND, + [DB::OR, ['requiredClass', 0], ['requiredClass', $this->list2Mask((array)$_v['ub']), '&']], + [ + DB::OR, + ['class', [ITEM_CLASS_WEAPON, 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']) + $parts[] = ['quality', $_v['qu']]; + + // type [list] + if ($_v['ty']) + $parts[] = ['subclass', $_v['ty']]; + + // slot [list] + if ($_v['sl']) + $parts[] = ['slot', $_v['sl']]; + + // side + if ($_v['si']) + { + $parts[] = match ($_v['si']) + { + 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, '&']]], + }; + } + + // itemLevel min + if ($_v['minle']) + $parts[] = ['itemLevel', $_v['minle'], '>=']; + + // itemLevel max + if ($_v['maxle']) + $parts[] = ['itemLevel', $_v['maxle'], '<=']; + + // reqLevel min + if ($_v['minrl']) + $parts[] = ['requiredLevel', $_v['minrl'], '>=']; + + // reqLevel max + if ($_v['maxrl']) + $parts[] = ['requiredLevel', $_v['maxrl'], '<=']; + + return $parts; + } + + protected function cbFactionQuestReward(int $cr, int $crs, string $crv) : ?array + { + return match ($crs) + { + 1 => ['src.src4', null, '!'], // Yes + 2 => ['src.src4', SIDE_ALLIANCE], // Alliance + 3 => ['src.src4', SIDE_HORDE], // Horde + 4 => ['src.src4', SIDE_BOTH], // Both + 5 => ['src.src4', null], // No + default => null + }; + } + + protected function cbAvailable(int $cr, int $crs, string $crv) : ?array + { + if ($this->int2Bool($crs)) + return [['cuFlags', CUSTOM_UNAVAILABLE, '&'], 0, $crs ? null : '!']; + + return null; + } + + protected function cbHasSockets(int $cr, int $crs, string $crv) : ?array + { + return match ($crs) + { + // Meta, Red, Yellow, Blue + 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 + }; + } + + protected function cbFitsGemSlot(int $cr, int $crs, string $crv) : ?array + { + return match ($crs) + { + // Meta, Red, Yellow, Blue + 1, 2, 3, 4 => [DB::AND, ['gemEnchantmentId', 0, '!'], ['gemColorMask', 1 << ($crs - 1), '&']], + 5 => ['gemEnchantmentId', 0, '!'], // Yes + 6 => ['gemEnchantmentId', 0], // No + default => null + }; + } + + protected function cbGlyphType(int $cr, int $crs, string $crv) : ?array + { + return match ($crs) + { + // major, minor + 1, 2 => [DB::AND, ['class', ITEM_CLASS_GLYPH], ['subSubClass', $crs]], + default => null + }; + } + + protected function cbHasRandEnchant(int $cr, int $crs, string $crv) : ?array + { + $n = preg_replace(parent::PATTERN_NAME, '', $crv); + if (!$this->tokenizeString($cr, $n)) + return null; + + $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'); + $x = array_search($set['ench'], $z); + if (isset($randIds[-$z[$x]])) + { + $set['entry'] *= -1; + $set['ench'] *= -1; + } + + $set['name'] = Util::localizedString($randIds[$set['ench']], 'name', true); + } + + // only enhance search results if enchantment by name is unique (implies only one enchantment per item is available) + if (count(array_unique(array_column($randIds, 'name_loc0'))) == 1) + $this->extraOpts['relEnchant'] = $tplIds; + + if ($tplIds) + return ['randomEnchant', array_column($tplIds, 'entry')]; + else + return [0]; // no results aren't really input errors + } + + protected function cbReqArenaRating(int $cr, int $crs, string $crv) : ?array + { + if (!Util::checkNumeric($crv, NUM_CAST_INT) || !$this->int2Op($crs)) + return null; + + $this->fiExtraCols[] = $cr; + + $items = [0]; + 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) : ?array + { + if (!isset(self::$enums[$cr][$crs])) + return null; + + $_ = self::$enums[$cr][$crs]; + if (is_bool($_)) + return $_ ? [$field, 0, '>'] : [$field, 0]; + else if (is_int($_)) + return [$field, 1 << ($_ - 1), '&']; + + return null; + } + + protected function cbDamageType(int $cr, int $crs, string $crv) : ?array + { + if (!$this->checkInput(parent::V_RANGE, [SPELL_SCHOOL_NORMAL, SPELL_SCHOOL_ARCANE], $crs)) + return null; + + return [DB::OR, ['dmgType1', $crs], ['dmgType2', $crs]]; + } + + protected function cbArmorBonus(int $cr, int $crs, string $crv) : ?array + { + if (!Util::checkNumeric($crv, NUM_CAST_FLOAT) || !$this->int2Op($crs)) + return null; + + $this->fiExtraCols[] = $cr; + return [DB::AND, ['armordamagemodifier', $crv, $crs], ['class', ITEM_CLASS_ARMOR]]; + } + + protected function cbCraftedByProf(int $cr, int $crs, string $crv) : ?array + { + if (!isset(self::$enums[$cr][$crs])) + return null; + + $_ = self::$enums[$cr][$crs]; + if (is_bool($_)) + return ['src.src1', null, $_ ? '!' : null]; + else if (is_int($_)) + return ['s.skillLine1', $_]; + + return null; + } + + protected function cbQuestRewardIn(int $cr, int $crs, string $crv) : ?array + { + if (in_array($crs, self::$enums[$cr])) + 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.. + + return null; + } + + protected function cbDropsInZone(int $cr, int $crs, string $crv) : ?array + { + if (in_array($crs, self::$enums[$cr])) + 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.. + + return null; + } + + protected function cbDropsInInstance(int $cr, int $crs, string $crv, int $moreFlag, int $modeBit) : ?array + { + if (in_array($crs, self::$enums[$cr])) + return [DB::AND, ['src.src2', $modeBit, '&'], ['src.moreMask', $moreFlag, '&'], ['src.moreZoneId', $crs]]; + else if ($crs == parent::ENUM_ANY) + return [DB::AND, ['src.src2', $modeBit, '&'], ['src.moreMask', $moreFlag, '&']]; + + return null; + } + + protected function cbPurchasableWith(int $cr, int $crs, string $crv) : ?array + { + if (in_array($crs, self::$enums[$cr])) + $_ = (array)$crs; + else if ($crs == parent::ENUM_ANY) + $_ = self::$enums[$cr]; + else + return null; + + $costs = DB::Aowow()->selectCol( + '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)) + return ['id', $items]; + + return null; + } + + protected function cbSoldByNPC(int $cr, int $crs, string $crv) : ?array + { + if (!Util::checkNumeric($crv, NUM_CAST_INT)) + return null; + + 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]; + } + + protected function cbAvgBuyout(int $cr, int $crs, string $crv) : ?array + { + if (!Util::checkNumeric($crv, NUM_CAST_INT) || !$this->int2Op($crs)) + return null; + + foreach (Profiler::getRealms() as $rId => $__) + { + // todo: do something sensible.. + // // todo (med): get the avgbuyout into the listview + // 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]; + return [1]; + } + + return [0]; + } + + protected function cbAvgMoneyContent(int $cr, int $crs, string $crv) : ?array + { + if (!Util::checkNumeric($crv, NUM_CAST_INT) || !$this->int2Op($crs)) + return null; + + $this->fiExtraCols[] = $cr; + return [DB::AND, ['flags', ITEM_FLAG_OPENABLE, '&'], ['((minMoneyLoot + maxMoneyLoot) / 2)', $crv, $crs]]; + } + + protected function cbCooldown(int $cr, int $crs, string $crv) : ?array + { + if (!Util::checkNumeric($crv, NUM_CAST_INT) || !$this->int2Op($crs)) + return null; + + $crv *= 1000; // field supplied in milliseconds + + $this->fiExtraCols[] = $cr; + $this->extraOpts['is']['s'][] = ', GREATEST(`spellCooldown1`, `spellCooldown2`, `spellCooldown3`, `spellCooldown4`, `spellCooldown5`) AS "cooldown"'; + + return [ + 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]], + ]; + } + + protected function cbQuestRelation(int $cr, int $crs, string $crv) : ?array + { + return match ($crs) + { + // any + 1 => ['startQuest', 0, '>'], + // exclude horde only + 2 => [DB::AND, ['startQuest', 0, '>'], [['flagsExtra', 0x3, '&'], SIDE_HORDE]], + // exclude alliance only + 3 => [DB::AND, ['startQuest', 0, '>'], [['flagsExtra', 0x3, '&'], SIDE_ALLIANCE]], + // both + 4 => [DB::AND, ['startQuest', 0, '>'], [['flagsExtra', 0x3, '&'], 0]], + // none + 5 => ['startQuest', 0], + default => null + }; + } + + protected function cbFieldHasVal(int $cr, int $crs, string $crv, string $field, mixed $val) : ?array + { + if ($this->int2Bool($crs)) + return [$field, $val, $crs ? null : '!']; + + return null; + } + + protected function cbObtainedBy(int $cr, int $crs, string $crv, string $field) : ?array + { + if ($this->int2Bool($crs)) + return ['src.src'.$field, null, $crs ? '!' : null]; + + return null; + } + + protected function cbPvpPurchasable(int $cr, int $crs, string $crv, string $field) : ?array + { + if (!$this->int2Bool($crs)) + return null; + + $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 : '!']; + + return null; + } + + protected function cbDisenchantsInto(int $cr, int $crs, string $crv) : ?array + { + if (!Util::checkNumeric($crs, NUM_CAST_INT)) + return null; + + if (!in_array($crs, self::$enums[$cr])) + return null; + + $refResults = []; + $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 %n WHERE `reference` IN %in', Loot::REFERENCE, $newRefs); + } + + $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 + { + $where = match ($crs) + { + // 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 + }; + + 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]; + + return [0]; + } + + protected function cbReagentForAbility(int $cr, int $crs, string $crv) : ?array + { + if (!isset(self::$enums[$cr][$crs])) + return null; + + $_ = self::$enums[$cr][$crs]; + if ($_ === null) + return null; + + $ids = []; + $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 %in', + is_bool($_) ? array_filter(self::$enums[99], "is_numeric") : $_ + ); + foreach ($spells as $spell) + for ($i = 1; $i < 9; $i++) + if ($spell['reagent'.$i] > 0 && $spell['reagentCount'.$i] > 0) + $ids[] = $spell['reagent'.$i]; + + if (empty($ids)) + return [0]; + else if ($_) + return ['id', $ids]; + else + return ['id', $ids, '!']; + } + + protected function cbSource(int $cr, int $crs, string $crv) : ?array + { + if (!isset(self::$enums[$cr][$crs])) + return null; + + $_ = self::$enums[$cr][$crs]; + if (is_int($_)) // specific + return ['src.src'.$_, null, '!']; + else if ($_) // any + { + $foo = [DB::OR]; + foreach (self::$enums[$cr] as $bar) + if (is_int($bar)) + $foo[] = ['src.src'.$bar, null, '!']; + + return $foo; + } + else // none + return ['src.typeId', null]; + } + + protected function cbTypeCheck(string &$v) : bool + { + if (!$this->parentCats) + return false; + + if (!Util::checkNumeric($v, NUM_CAST_INT)) + return false; + + $c = $this->parentCats; + + if (isset($c[2]) && is_array(Lang::item('cat', $c[0], 1, $c[1]))) + $catList = Lang::item('cat', $c[0], 1, $c[1], 1, $c[2]); + else if (isset($c[1]) && is_array(Lang::item('cat', $c[0]))) + $catList = Lang::item('cat', $c[0], 1, $c[1]); + else + $catList = Lang::item('cat', $c[0]); + + // consumables - always + if ($c[0] == ITEM_CLASS_CONSUMABLE) + return in_array($v, array_keys(Lang::item('cat', 0, 1))); + // weapons - only if parent + else if ($c[0] == ITEM_CLASS_WEAPON && !isset($c[1])) + return in_array($v, array_keys(Lang::spell('weaponSubClass'))); + // armor - only if parent + else if ($c[0] == ITEM_CLASS_ARMOR && !isset($c[1])) + return in_array($v, array_keys(Lang::item('cat', ITEM_CLASS_ARMOR, 1))); + // uh ... other stuff... + else if (!isset($c[1]) && in_array($c[0], [ITEM_CLASS_CONTAINER, ITEM_CLASS_GEM, ITEM_CLASS_TRADEGOOD, ITEM_CLASS_RECIPE, ITEM_CLASS_MISC])) + return in_array($v, array_keys($catList[1])); + + return false; + } + + protected function cbSlotCheck(string &$v) : bool + { + if (!Util::checkNumeric($v, NUM_CAST_INT)) + return false; + + // todo (low): limit to concrete slots + $sl = array_keys(Lang::item('inventoryType')); + $c = $this->parentCats; + + // no selection + if (!isset($c[0])) + return in_array($v, $sl); + + // consumables - any; perm / temp item enhancements + else if ($c[0] == ITEM_CLASS_CONSUMABLE && (!isset($c[1]) || in_array($c[1], [-3, 6]))) + return in_array($v, $sl); + + // weapons - always + else if ($c[0] == ITEM_CLASS_WEAPON) + return in_array($v, $sl); + + // armor - any; any armor + else if ($c[0] == ITEM_CLASS_ARMOR && (!isset($c[1]) || in_array($c[1], [ITEM_SUBCLASS_CLOTH_ARMOR, ITEM_SUBCLASS_LEATHER_ARMOR, ITEM_SUBCLASS_MAIL_ARMOR, ITEM_SUBCLASS_PLATE_ARMOR]))) + return in_array($v, $sl); + + return false; + } + + protected function cbWeightKeyCheck(string &$v) : bool + { + if (preg_match('/\W/i', $v)) + return false; + + return Stat::getIndexFrom(Stat::IDX_FILTER_CR_ID, $v) > 0; + } +} + +?> diff --git a/includes/dbtypes/itemset.class.php b/includes/dbtypes/itemset.class.php new file mode 100644 index 00000000..a80e5886 --- /dev/null +++ b/includes/dbtypes/itemset.class.php @@ -0,0 +1,251 @@ + ['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`'] + ); + + public function __construct(array $conditions = [], array $miscData = []) + { + parent::__construct($conditions, $miscData); + + // post processing + foreach ($this->iterate() as &$_curTpl) + { + $_curTpl['classes'] = ChrClass::fromMask($_curTpl['classMask']); + $this->classes = array_merge($this->classes, $_curTpl['classes']); + + $_curTpl['pieces'] = []; + for ($i = 1; $i < 10; $i++) + { + if ($piece = $_curTpl['item'.$i]) + { + $_curTpl['pieces'][] = $piece; + $this->pieceToSet[$piece] = $this->id; + } + } + } + $this->classes = array_unique($this->classes); + } + + public function getListviewData() : array + { + $data = []; + + foreach ($this->iterate() as $__) + { + $data[$this->id] = array( + 'id' => $this->id, + 'idbak' => $this->curTpl['refSetId'], + 'name' => (7 - $this->curTpl['quality']).$this->getField('name', true), + 'minlevel' => $this->curTpl['minLevel'], + 'maxlevel' => $this->curTpl['maxLevel'], + 'note' => $this->curTpl['contentGroup'], + 'type' => $this->curTpl['type'], + 'reqclass' => $this->curTpl['classMask'], + 'classes' => $this->curTpl['classes'], + 'pieces' => $this->curTpl['pieces'], + 'heroic' => $this->curTpl['heroic'] + ); + } + + return $data; + } + + public function getJSGlobals(int $addMask = GLOBALINFO_ANY) : array + { + $data = []; + + if ($this->classes && ($addMask & GLOBALINFO_RELATED)) + $data[Type::CHR_CLASS] = array_combine($this->classes, $this->classes); + + if ($this->pieceToSet && ($addMask & GLOBALINFO_SELF)) + $data[Type::ITEM] = array_combine(array_keys($this->pieceToSet), array_keys($this->pieceToSet)); + + if ($addMask & GLOBALINFO_SELF) + foreach ($this->iterate() as $id => $__) + $data[Type::ITEMSET][$id] = ['name' => $this->getField('name', true)]; + + return $data; + } + + public function renderTooltip() : ?string + { + if (!$this->curTpl) + return null; + + $x = '
'; + $x .= ''.$this->getField('name', true).'
'; + + $nCl = 0; + if ($_ = $this->getField('classMask')) + { + $jsg = []; + $cl = Lang::getClassString($_, $jsg); + $t = count($jsg) == 1 ? Lang::game('class') : Lang::game('classes'); + $x .= Util::ucFirst($t).Lang::main('colon').$cl.'
'; + } + + if ($_ = $this->getField('contentGroup')) + $x .= Lang::itemset('notes', $_).($this->getField('heroic') ? ' ('.Lang::item('heroic').')' : '').'
'; + + if (!$nCl || !$this->getField('type')) + $x.= Lang::itemset('types', $this->getField('type')).'
'; + + if ($bonuses = $this->getBonuses()) + { + $x .= ''; + + foreach ($bonuses as [$nItems, , $text]) + $x .= '
'.Lang::itemset('_pieces', [$nItems]).''.$text; + + $x .= '
'; + } + + $x .= '
'; + + return $x; + } + + public function getBonuses() : array + { + $spells = []; + for ($i = 1; $i < 9; $i++) + { + $spl = $this->getField('spell'.$i); + $qty = $this->getField('bonus'.$i); + + // cant use spell as index, would change order + if ($spl && $qty) + $spells[] = [$qty, $spl]; + } + + // sort by required pieces ASC + usort($spells, fn(array $a, array $b) => $a[0] <=> $b[0]); + + $setSpells = new SpellList(array(['s.id', array_column($spells, 1)])); + foreach ($spells as &$s) + { + 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; + } +} + + +// missing filter: "Available to Players" +class ItemsetListFilter extends Filter +{ + protected string $type = 'itemsets'; + protected static array $enums = array( + 6 => parent::ENUM_EVENT + ); + + protected static array $genericFilter = array( + 2 => [parent::CR_NUMERIC, 'id', NUM_CAST_INT, true], // id + 3 => [parent::CR_NUMERIC, 'npieces', NUM_CAST_INT ], // pieces + 4 => [parent::CR_STRING, 'bonusText', STR_LOCALIZED ], // bonustext + 5 => [parent::CR_BOOLEAN, 'heroic' ], // heroic + 6 => [parent::CR_ENUM, 'e.holidayId', true, true], // relatedevent + 8 => [parent::CR_FLAG, 'cuFlags', CUSTOM_HAS_COMMENT ], // hascomments + 9 => [parent::CR_FLAG, 'cuFlags', CUSTOM_HAS_SCREENSHOT ], // hasscreenshots + 10 => [parent::CR_FLAG, 'cuFlags', CUSTOM_HAS_VIDEO ], // hasvideos + 12 => [parent::CR_CALLBACK, 'cbAvaliable', ] // available to players [yn] + ); + + protected static array $inputFields = array( + '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_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, [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 + ); + + protected function createSQLForValues() : array + { + $parts = []; + $_v = &$this->values; + + // name [str] + if ($_v['na']) + if ($_ = $this->buildLikeLookup([['na', 'name_loc'.Lang::getLocale()->value]])) + $parts[] = $_; + + // quality [enum] + if ($_v['qu']) + $parts[] = ['quality', $_v['qu']]; + + // type [enum] + if ($_v['ty']) + $parts[] = ['type', $_v['ty']]; + + // itemLevel min [int] + if ($_v['minle']) + $parts[] = ['minLevel', $_v['minle'], '>=']; + + // itemLevel max [int] + if ($_v['maxle']) + $parts[] = ['maxLevel', $_v['maxle'], '<=']; + + // reqLevel min [int] + if ($_v['minrl']) + $parts[] = ['reqLevel', $_v['minrl'], '>=']; + + // reqLevel max [int] + if ($_v['maxrl']) + $parts[] = ['reqLevel', $_v['maxrl'], '<=']; + + // class [enum] + if ($_v['cl']) + $parts[] = ['classMask', $this->list2Mask([$_v['cl']]), '&']; + + // tag [enum] + if ($_v['ta']) + $parts[] = ['contentGroup', intVal($_v['ta'])]; + + return $parts; + } + + protected function cbAvaliable(int $cr, int $crs, string $crv) : ?array + { + return match ($crs) + { + 1 => ['src.typeId', null, '!'], // Yes + 2 => ['src.typeId', null], // No + default => null + }; + } +} + +?> diff --git a/includes/dbtypes/mail.class.php b/includes/dbtypes/mail.class.php new file mode 100644 index 00000000..0e142428 --- /dev/null +++ b/includes/dbtypes/mail.class.php @@ -0,0 +1,77 @@ +error) + return; + + // post processing + foreach ($this->iterate() as $_id => &$_curTpl) + { + $_curTpl['name'] = Util::localizedString($_curTpl, 'subject', true); + if (!$_curTpl['name']) + { + $_curTpl['name'] = sprintf(Lang::mail('untitled'), $_id); + $_curTpl['subject_loc0'] = $_curTpl['name']; + } + } + } + + 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 %n WHERE `id` = %i', self::$dataTable, $id)) + return new LocString($n, 'subject'); + return null; + } + + public function getListviewData() : array + { + $data = []; + + foreach ($this->iterate() as $__) + { + $body = str_replace('[br]', ' ', Util::parseHtmlText($this->getField('text', true), true)); + + $data[$this->id] = array( + 'id' => $this->id, + 'subject' => $this->getField('subject', true), + 'body' => Lang::trimTextClean($body), + 'attachments' => [$this->getField('attachment')] + ); + } + + return $data; + } + + public function getJSGlobals(int $addMask = 0) : array + { + $data = []; + + foreach ($this->iterate() as $__) + if ($a = $this->curTpl['attachment']) + $data[Type::ITEM][$a] = $a; + + return $data; + } + + public function renderTooltip() : ?string { return null; } +} + +?> diff --git a/includes/types/pet.class.php b/includes/dbtypes/pet.class.php similarity index 66% rename from includes/types/pet.class.php rename to includes/dbtypes/pet.class.php index 0b27cd2e..86834fa5 100644 --- a/includes/types/pet.class.php +++ b/includes/dbtypes/pet.class.php @@ -1,24 +1,26 @@ [['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() + public function getListviewData() : array { $data = []; @@ -50,7 +52,7 @@ class PetList extends BaseType return $data; } - public function getJSGlobals($addMask = GLOBALINFO_ANY) + public function getJSGlobals(int $addMask = GLOBALINFO_ANY) : array { $data = []; @@ -59,16 +61,16 @@ class PetList extends BaseType if ($addMask & GLOBALINFO_RELATED) for ($i = 1; $i <= 4; $i++) if ($this->curTpl['spellId'.$i] > 0) - $data[TYPE_SPELL][$this->curTpl['spellId'.$i]] = $this->curTpl['spellId'.$i]; + $data[Type::SPELL][$this->curTpl['spellId'.$i]] = $this->curTpl['spellId'.$i]; if ($addMask & GLOBALINFO_SELF) - $data[TYPE_PET][$this->id] = ['icon' => $this->curTpl['iconString']]; + $data[Type::PET][$this->id] = ['icon' => $this->curTpl['iconString']]; } return $data; } - public function renderTooltip() { } + public function renderTooltip() : ?string { return null; } } ?> diff --git a/includes/dbtypes/profile.class.php b/includes/dbtypes/profile.class.php new file mode 100644 index 00000000..541e4b1a --- /dev/null +++ b/includes/dbtypes/profile.class.php @@ -0,0 +1,750 @@ +iterate() as $__) + { + if (!$this->isVisibleToUser()) + continue; + + if (($addInfoMask & PROFILEINFO_PROFILE) && !$this->isCustom()) + continue; + + if (($addInfoMask & PROFILEINFO_CHARACTER) && $this->isCustom()) + continue; + + $data[$this->id] = array( + 'id' => $this->getField('id'), + 'name' => $this->getField('name'), + 'race' => $this->getField('race'), + 'classs' => $this->getField('class'), + 'gender' => $this->getField('gender'), + 'level' => $this->getField('level'), + 'faction' => ChrRace::tryFrom($this->getField('race'))?->isAlliance() ? 0 : 1, + 'talenttree1' => $this->getField('talenttree1'), + 'talenttree2' => $this->getField('talenttree2'), + 'talenttree3' => $this->getField('talenttree3'), + 'talentspec' => $this->getField('activespec') + 1, // 0 => 1; 1 => 2 + 'achievementpoints' => $this->getField('achievementpoints'), + 'guild' => $this->curTpl['guildname'] ? '$"'.str_replace ('"', '', $this->curTpl['guildname']).'"' : '', // force this to be a string + 'guildrank' => $this->getField('guildrank'), + 'realm' => Profiler::urlize($this->getField('realmName'), true), + 'realmname' => $this->getField('realmName'), + // 'battlegroup' => Profiler::urlize($this->getField('battlegroup')), // was renamed to subregion somewhere around cata release + // 'battlegroupname' => $this->getField('battlegroup'), + 'gearscore' => $this->getField('gearscore') + ); + + if ($addInfoMask & PROFILEINFO_USER) + $data[$this->id]['published'] = (int)!!($this->getField('cuFlags') & PROFILER_CU_PUBLISHED); + + // for the lv this determins if the link is profile= or profile=.. + if (!$this->isCustom()) + $data[$this->id]['region'] = Profiler::urlize($this->getField('region')); + + if ($addInfoMask & PROFILEINFO_ARENA) + { + $data[$this->id]['rating'] = $this->getField('rating'); + $data[$this->id]['captain'] = $this->getField('captain'); + $data[$this->id]['games'] = $this->getField('seasonGames'); + $data[$this->id]['wins'] = $this->getField('seasonWins'); + } + + // Filter asked for skills - add them + foreach ($reqCols as $col) + $data[$this->id][$col] = $this->getField($col); + + if ($addInfoMask & PROFILEINFO_PROFILE) + { + if ($_ = $this->getField('description')) + $data[$this->id]['description'] = $_; + + if ($_ = $this->getField('icon')) + $data[$this->id]['icon'] = $_; + } + + if ($addInfoMask & PROFILEINFO_CHARACTER) + if ($_ = $this->getField('renameItr')) + $data[$this->id]['renameItr'] = $_; + + if ($this->getField('cuFlags') & PROFILER_CU_PINNED) + $data[$this->id]['pinned'] = 1; + + if ($this->getField('deleted')) + $data[$this->id]['deleted'] = 1; + } + + return $data; + } + + public function renderTooltip() : ?string + { + if (!$this->curTpl) + return null; + + $title = ''; + $name = $this->getField('name'); + if ($_ = $this->getField('title')) + $title = (new TitleList(array(['id', $_])))->getField($this->getField('gender') ? 'female' : 'male', true); + + if ($this->isCustom()) + $name .= Lang::profiler('customProfile'); + else if ($title) + $name = sprintf($title, $name); + + $x = ''; + $x .= ''; + if ($g = $this->getField('guildname')) + $x .= ''; + else if ($d = $this->getField('description')) + $x .= ''; + $x .= ''; + $x .= '
'.$name.'
<'.$g.'>
'.$d.'
'.Lang::game('level').' '.$this->getField('level').' '.Lang::game('ra', $this->getField('race')).' '.Lang::game('cl', $this->getField('class')).'
'; + + return $x; + } + + public function getJSGlobals(int $addMask = 0) : array + { + $data = []; + $realms = Profiler::getRealms(); + + foreach ($this->iterate() as $id => $__) + { + if (($addMask & PROFILEINFO_PROFILE) && $this->isCustom()) + { + $profile = array( + 'id' => $this->getField('id'), + 'name' => $this->getField('name'), + 'race' => $this->getField('race'), + 'classs' => $this->getField('class'), + 'level' => $this->getField('level'), + 'gender' => $this->getField('gender') + ); + + if ($_ = $this->getField('icon')) + $profile['icon'] = $_; + + $data[] = $profile; + + continue; + } + + if ($addMask & PROFILEINFO_CHARACTER && !$this->isCustom()) + { + if (!isset($realms[$this->getField('realm')])) + continue; + + $data[] = array( + 'id' => $this->getField('id'), + 'name' => $this->getField('name'), + 'realmname' => $realms[$this->getField('realm')]['name'], + 'region' => $realms[$this->getField('realm')]['region'], + 'realm' => Profiler::urlize($realms[$this->getField('realm')]['name']), + 'race' => $this->getField('race'), + 'classs' => $this->getField('class'), + 'level' => $this->getField('level'), + 'gender' => $this->getField('gender'), + 'pinned' => $this->getField('cuFlags') & PROFILER_CU_PINNED ? 1 : 0 + ); + } + } + + return $data; + } + + public function isCustom() : bool + { + return $this->getField('custom'); + } + + public function isVisibleToUser() : bool + { + if (!$this->isCustom() || User::isInGroup(U_GROUP_ADMIN | U_GROUP_BUREAU)) + return true; + + if ($this->getField('deleted')) + return false; + + if (User::$id == $this->getField('user')) + return true; + + return (bool)($this->getField('cuFlags') & PROFILER_CU_PUBLISHED); + } + + public function getIcon() : string + { + if ($_ = $this->getField('icon')) + return $_; + + return sprintf('chr_%s_%s_%s%02d', + ChrRace::from($this->getField('race'))->json(), + $this->getField('gender') ? 'female' : 'male', + ChrClass::from($this->getField('class'))->json(), + max(1, floor(($this->getField('level') - 60) / 10) + 2) + ); + } + + public static function getName(int $id) : ?LocString { return null; } +} + + +class ProfileListFilter extends Filter +{ + use TrProfilerFilter; + + protected string $type = 'profiles'; + protected static array $genericFilter = array( + 2 => [parent::CR_NUMERIC, 'gearscore', NUM_CAST_INT ], // gearscore [num] + 3 => [parent::CR_CALLBACK, 'cbAchievs', null, null], // achievementpoints [num] + 5 => [parent::CR_NUMERIC, 'talenttree1', NUM_CAST_INT ], // talenttree1 [num] + 6 => [parent::CR_NUMERIC, 'talenttree2', NUM_CAST_INT ], // talenttree2 [num] + 7 => [parent::CR_NUMERIC, 'talenttree3', NUM_CAST_INT ], // talenttree3 [num] + 9 => [parent::CR_STRING, 'g.name' ], // guildname + 10 => [parent::CR_CALLBACK, 'cbHasGuildRank', null, null], // guildrank + 12 => [parent::CR_CALLBACK, 'cbTeamName', 2, null], // teamname2v2 + 15 => [parent::CR_CALLBACK, 'cbTeamName', 3, null], // teamname3v3 + 18 => [parent::CR_CALLBACK, 'cbTeamName', 5, null], // teamname5v5 + 13 => [parent::CR_CALLBACK, 'cbTeamRating', 2, null], // teamrtng2v2 + 16 => [parent::CR_CALLBACK, 'cbTeamRating', 3, null], // teamrtng3v3 + 19 => [parent::CR_CALLBACK, 'cbTeamRating', 5, null], // teamrtng5v5 + 14 => [parent::CR_NYI_PH, null, 0 /* 2 */ ], // teamcontrib2v2 [num] + 17 => [parent::CR_NYI_PH, null, 0 /* 3 */ ], // teamcontrib3v3 [num] + 20 => [parent::CR_NYI_PH, null, 0 /* 5 */ ], // teamcontrib5v5 [num] + 21 => [parent::CR_CALLBACK, 'cbWearsItems', null, null], // wearingitem [str] + 23 => [parent::CR_CALLBACK, 'cbCompletedAcv', null, null], // completedachievement + 25 => [parent::CR_CALLBACK, 'cbProfession', SKILL_ALCHEMY, null], // alchemy [num] + 26 => [parent::CR_CALLBACK, 'cbProfession', SKILL_BLACKSMITHING, null], // blacksmithing [num] + 27 => [parent::CR_CALLBACK, 'cbProfession', SKILL_ENCHANTING, null], // enchanting [num] + 28 => [parent::CR_CALLBACK, 'cbProfession', SKILL_ENGINEERING, null], // engineering [num] + 29 => [parent::CR_CALLBACK, 'cbProfession', SKILL_HERBALISM, null], // herbalism [num] + 30 => [parent::CR_CALLBACK, 'cbProfession', SKILL_INSCRIPTION, null], // inscription [num] + 31 => [parent::CR_CALLBACK, 'cbProfession', SKILL_JEWELCRAFTING, null], // jewelcrafting [num] + 32 => [parent::CR_CALLBACK, 'cbProfession', SKILL_LEATHERWORKING, null], // leatherworking [num] + 33 => [parent::CR_CALLBACK, 'cbProfession', SKILL_MINING, null], // mining [num] + 34 => [parent::CR_CALLBACK, 'cbProfession', SKILL_SKINNING, null], // skinning [num] + 35 => [parent::CR_CALLBACK, 'cbProfession', SKILL_TAILORING, null], // tailoring [num] + 36 => [parent::CR_CALLBACK, 'cbHasGuild', null, null] // hasguild [yn] + ); + + protected static array $inputFields = array( + '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 + '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 + '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 + 'minle' => [parent::V_RANGE, [1, MAX_LEVEL], false], // min level + 'maxle' => [parent::V_RANGE, [1, MAX_LEVEL], false], // max level + 'rg' => [parent::V_CALLBACK, 'cbRegionCheck', false], // region + 'bg' => [parent::V_EQUAL, null, false], // battlegroup - unsued here, but var expected by template + 'sv' => [parent::V_CALLBACK, 'cbServerCheck', false] // server + ); + + public bool $useLocalList = false; + public array $extraOpts = []; + + /* heads up! + a couple of filters are too complex to be run against the characters database + if they are selected, force useage of LocalProfileList + */ + + public function __construct(string|array $data, array $opts = []) + { + parent::__construct($data, $opts); + + if (!empty($this->values['cr'])) + if (array_intersect($this->values['cr'], [2, 5, 6, 7, 21])) + $this->useLocalList = true; + } + + protected function createSQLForValues() : array + { + $parts = []; + $_v = $this->values; + + // region (rg), battlegroup (bg) and server (sv) are passed to ProflieList as miscData and handled there + + // table key differs between remote and local :< + $k = $this->useLocalList ? 'p' : 'c'; + + // name [str] + if ($_v['na']) + { + // 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; + + $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] + if ($_v['si'] == SIDE_ALLIANCE) + $parts[] = [$k.'.race', ChrRace::fromMask(ChrRace::MASK_ALLIANCE)]; + else if ($_v['si'] == SIDE_HORDE) + $parts[] = [$k.'.race', ChrRace::fromMask(ChrRace::MASK_HORDE)]; + + // race [list] + if ($_v['ra']) + $parts[] = [$k.'.race', $_v['ra']]; + + // class [list] + if ($_v['cl']) + $parts[] = [$k.'.class', $_v['cl']]; + + // min level [int] + if ($_v['minle']) + $parts[] = [$k.'.level', $_v['minle'], '>=']; + + // max level [int] + if ($_v['maxle']) + $parts[] = [$k.'.level', $_v['maxle'], '<=']; + + return $parts; + } + + protected function cbProfession(int $cr, int $crs, string $crv, $skillId) : ?array + { + if (!Util::checkNumeric($crv, NUM_CAST_INT) || !$this->int2Op($crs)) + return null; + + $k = 'sk_'.Util::createHash(12); + $col = 'skill-'.$skillId; + + $this->fiExtraCols[$skillId] = $col; + + 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], + 's' => [', '.$k.'.`value` AS "'.$col.'"'] + ); + return [$k.'.skillId', null, '!']; + } + else + { + $this->extraOpts[$k] = array( + 'j' => [sprintf('character_skills %1$s ON `%1$s`.`guid` = c.`guid` AND `%1$s`.`skill` = %2$d AND `%1$s`.`value` %3$s %4$d', $k, $skillId, $crs, $crv), true], + 's' => [', '.$k.'.`value` AS "'.$col.'"'] + ); + return [$k.'.skill', null, '!']; + } + } + + protected function cbCompletedAcv(int $cr, int $crs, string $crv) : ?array + { + if (!Util::checkNumeric($crv, NUM_CAST_INT)) + return null; + + if (!Type::validateIds(Type::ACHIEVEMENT, $crv)) + return null; + + $k = 'acv_'.Util::createHash(12); + + 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]]; + return [$k.'.achievementId', null, '!']; + } + else + { + $this->extraOpts[$k] = ['j' => [sprintf('character_achievement %1$s ON `%1$s`.`guid` = c.`guid` AND `%1$s`.`achievement` = %2$d', $k, $crv), true]]; + return [$k.'.achievement', null, '!']; + } + } + + protected function cbWearsItems(int $cr, int $crs, string $crv) : ?array + { + if (!Util::checkNumeric($crv, NUM_CAST_INT)) + return null; + + if (!Type::validateIds(Type::ITEM, $crv)) + return null; + + $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]]; + return [$k.'.item', null, '!']; + } + + protected function cbHasGuild(int $cr, int $crs, string $crv) : ?array + { + if (!$this->int2Bool($crs)) + return null; + + if ($this->useLocalList) + return ['p.guild', null, $crs ? '!' : null]; + else + return ['gm.guildId', null, $crs ? '!' : null]; + } + + protected function cbHasGuildRank(int $cr, int $crs, string $crv) : ?array + { + if (!Util::checkNumeric($crv, NUM_CAST_INT) || !$this->int2Op($crs)) + return null; + + if ($this->useLocalList) + return ['p.guildrank', $crv, $crs]; + else + return ['gm.rank', $crv, $crs]; + } + + protected function cbTeamName(int $cr, int $crs, string $crv, $size) : ?array + { + $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; + } + + protected function cbTeamRating(int $cr, int $crs, string $crv, $size) : ?array + { + if (!Util::checkNumeric($crv, NUM_CAST_INT) || !$this->int2Op($crs)) + return null; + + return [DB::AND, ['at.type', $size], ['at.rating', $crv, $crs]]; + } + + protected function cbAchievs(int $cr, int $crs, string $crv) : ?array + { + if (!Util::checkNumeric($crv, NUM_CAST_INT) || !$this->int2Op($crs)) + return null; + + if ($this->useLocalList) + return ['p.achievementpoints', $crv, $crs]; + else + return ['cap.counter', $crv, $crs]; + } +} + + +class RemoteProfileList extends ProfileList +{ + protected string $queryBase = 'SELECT `c`.*, `c`.`guid` AS ARRAY_KEY FROM characters c'; + protected array $queryOpts = array( + 'c' => [['gm', 'g', 'cap']], // 12698: use criteria of Achievement 4496 as shortcut to get total achievement points + 'cap' => ['j' => ['character_achievement_progress cap ON cap.`guid` = c.`guid` AND cap.`criteria` = 12698', true], 's' => ', IFNULL(cap.`counter`, 0) AS "achievementpoints"'], + 'gm' => ['j' => ['guild_member gm ON gm.`guid` = c.`guid`', true], 's' => ', gm.`rank` AS "guildrank"'], + 'g' => ['j' => ['guild g ON g.`guildid` = gm.`guildid`', true], 's' => ', g.`guildid` AS "guild", g.`name` AS "guildname"'], + 'atm' => ['j' => ['arena_team_member atm ON atm.`guid` = c.`guid`', true], 's' => ', atm.`personalRating` AS "rating"'], + 'at' => [['atm'], 'j' => 'arena_team at ON atm.`arenaTeamId` = at.`arenaTeamId`', 's' => ', at.`name` AS "arenateam", IF(at.`captainGuid` = c.`guid`, 1, 0) AS "captain"'] + ); + + private array $rnItr = []; // rename iterator [name => nCharsWithThisName] + + public function __construct(array $conditions = [], array $miscData = []) + { + // select DB by realm + if (!$this->selectRealms($miscData)) + { + trigger_error('RemoteProfileList::__construct - cannot access any realm.', E_USER_WARNING); + return; + } + + parent::__construct($conditions, $miscData); + + if ($this->error) + return; + + reset($this->dbNames); // only use when querying single realm + $realmId = key($this->dbNames); + $realms = Profiler::getRealms(); + $talentSpells = []; + $talentLookup = []; + $distrib = []; + + // post processing + foreach ($this->iterate() as $guid => &$curTpl) + { + // battlegroup + $curTpl['battlegroup'] = Cfg::get('BATTLEGROUP'); + + // realm + [$r, $g] = explode(':', $guid); + if (!empty($realms[$r])) + { + $curTpl['realm'] = $r; + $curTpl['realmName'] = $realms[$r]['name']; + $curTpl['region'] = $realms[$r]['region']; + } + else + { + trigger_error('char #'.$guid.' belongs to nonexistent realm #'.$r, E_USER_WARNING); + unset($this->templates[$guid]); + continue; + } + + // empty name + if (!$curTpl['name']) + { + trigger_error('char #'.$guid.' on realm #'.$r.' has empty name.', E_USER_WARNING); + unset($this->templates[$guid]); + continue; + } + + // temp id + $curTpl['id'] = 0; + + // talent points pre + $talentLookup[$r][$g] = []; + $talentSpells[] = $curTpl['class']; + $curTpl['activespec'] = $curTpl['activeTalentGroup']; + + // equalize distribution + if (empty($distrib[$curTpl['realm']])) + $distrib[$curTpl['realm']] = 1; + else + $distrib[$curTpl['realm']]++; + + // char is pending rename + 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` = %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` = %i AND `realmGUID` = %i', $r, $g)) + $curTpl['renameItr'] = $rnItr; + // not yet recognized: get max itr + else + $curTpl['renameItr'] = ++$this->rnItr[$curTpl['name']]; + } + else + $curTpl['renameItr'] = 0; + + $curTpl['cuFlags'] = 0; + } + + 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 %in', array_keys($chars)); + + $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_numeric($c)) + $limit = max(0, (int)$c); + + if (!$limit) // int:0 means unlimited, so skip process + $distrib = []; + + $total = array_sum($distrib); + foreach ($distrib as &$d) + $d = ceil($limit * $d / $total); + + foreach ($this->iterate() as $guid => &$curTpl) + { + if ($distrib) + { + if ($limit <= 0 || $distrib[$curTpl['realm']] <= 0) + { + unset($this->templates[$guid]); + continue; + } + + $distrib[$curTpl['realm']]--; + $limit--; + } + + [$r, $g] = explode(':', $guid); + + // talent points post + $curTpl['talenttree1'] = 0; + $curTpl['talenttree2'] = 0; + $curTpl['talenttree3'] = 0; + if (!empty($talentLookup[$r][$g])) + { + $talents = array_filter($talentLookup[$r][$g], function($v) use ($curTpl) { return $curTpl['activespec'] == $v; } ); + foreach (array_intersect_key($talentSpells, $talents) as $spell => $data) + $curTpl['talenttree'.($data['tab'] + 1)] += $data['rank']; + } + } + } + + public function getListviewData(int $addInfoMask = 0, array $reqCols = []) : array + { + $data = parent::getListviewData($addInfoMask, $reqCols); + + // not wanted on server list + foreach ($data as &$d) + unset($d['published']); + + return $data; + } + + public function initializeLocalEntries() : void + { + if (!$this->templates) + return; + + $baseData = $guildData = []; + foreach ($this->iterate() as $guid => $__) + { + $realmId = $this->getField('realm'); + $guildGUID = $this->getField('guild'); + + $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) + { + $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) + { + 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 %in AND `realmGUID` IN %in', + $guildData['realm'], $guildData['realmGUID'] + ); + + foreach ($baseData['guild'] as $i => &$g) + $g = $localGuilds[$baseData['realm'][$i]][$baseData['guild'][$i]] ?? null; + } + + // basic char data (enough for tooltips) + if ($baseData) + { + // 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()->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) + if (isset($localIds[$guid])) + $_curTpl = array_merge($_curTpl, $localIds[$guid]); + } + } +} + + +class LocalProfileList extends ProfileList +{ + 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"'] + ); + + public function __construct(array $conditions = [], array $miscData = []) + { + $realms = Profiler::getRealms(); + + // graft realm selection from miscData onto conditions + $realmIds = []; + if (isset($miscData['sv'])) + $realmIds = array_keys(array_filter($realms, fn($x) => Profiler::urlize($x['name']) == Profiler::urlize($miscData['sv']))); + + if (isset($miscData['rg'])) + $realmIds = array_merge($realmIds, array_keys(array_filter($realms, fn($x) => $x['region'] == $miscData['rg']))); + + if ($conditions && $realmIds) + { + array_unshift($conditions, DB::AND); + $conditions = [DB::AND, ['realm', $realmIds], $conditions]; + } + else if ($realmIds) + $conditions = [['realm', $realmIds]]; + + parent::__construct($conditions, $miscData); + + if ($this->error) + return; + + foreach ($this->iterate() as $id => &$curTpl) + { + if (!$curTpl['realm']) // custom profile w/o realminfo + continue; + + if (!isset($realms[$curTpl['realm']])) + { + unset($this->templates[$id]); + continue; + } + + $curTpl['realmName'] = $realms[$curTpl['realm']]['name']; + $curTpl['region'] = $realms[$curTpl['realm']]['region']; + $curTpl['battlegroup'] = Cfg::get('BATTLEGROUP'); + } + } + + public function getProfileUrl() : string + { + $url = '?profile='; + + if ($this->isCustom()) + return $url.$this->getField('id'); + + return $url.implode('.', array( + $this->getField('region'), + Profiler::urlize($this->getField('realmName'), true), + urlencode($this->getField('name')) + )); + } +} + + +?> diff --git a/includes/dbtypes/quest.class.php b/includes/dbtypes/quest.class.php new file mode 100644 index 00000000..c24c2573 --- /dev/null +++ b/includes/dbtypes/quest.class.php @@ -0,0 +1,727 @@ + [], + '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 = []) + { + parent::__construct($conditions, $miscData); + + // i don't like this very much + $currencies = DB::Aowow()->selectCol('SELECT `id` AS ARRAY_KEY, `itemId` FROM ::currencies'); + + // post processing + foreach ($this->iterate() as $id => &$_curTpl) + { + $_curTpl['cat1'] = $_curTpl['questSortId']; // should probably be in a method... + $_curTpl['cat2'] = 0; + + foreach (Game::QUEST_CLASSES as $k => $arr) + { + if (in_array($_curTpl['cat1'], $arr)) + { + $_curTpl['cat2'] = $k; + break; + } + } + + // store requirements + $requires = []; + for ($i = 1; $i < 7; $i++) + { + if ($_ = $_curTpl['reqItemId'.$i]) + $requires[Type::ITEM][] = $_; + + if ($i > 4) + continue; + + if ($_curTpl['reqNpcOrGo'.$i] > 0) + $requires[Type::NPC][] = $_curTpl['reqNpcOrGo'.$i]; + else if ($_curTpl['reqNpcOrGo'.$i] < 0) + $requires[Type::OBJECT][] = -$_curTpl['reqNpcOrGo'.$i]; + + if ($_ = $_curTpl['reqSourceItemId'.$i]) + $requires[Type::ITEM][] = $_; + } + if ($requires) + $this->requires[$id] = $requires; + + // store rewards + $rewards = []; + $choices = []; + + if ($_ = $_curTpl['rewardTitleId']) + $rewards[Type::TITLE][] = $_; + + if ($_ = $_curTpl['rewardHonorPoints']) + $rewards[Type::CURRENCY][CURRENCY_HONOR_POINTS] = $_; + + if ($_ = $_curTpl['rewardArenaPoints']) + $rewards[Type::CURRENCY][CURRENCY_ARENA_POINTS] = $_; + + for ($i = 1; $i < 7; $i++) + { + if ($_ = $_curTpl['rewardChoiceItemId'.$i]) + $choices[Type::ITEM][$_] = $_curTpl['rewardChoiceItemCount'.$i]; + + if ($i > 5) + continue; + + if ($_ = $_curTpl['rewardFactionId'.$i]) + $rewards[Type::FACTION][$_] = $_curTpl['rewardFactionValue'.$i]; + + if ($i > 4) + continue; + + if ($_ = $_curTpl['rewardItemId'.$i]) + { + $qty = $_curTpl['rewardItemCount'.$i]; + if (in_array($_, $currencies)) + $rewards[Type::CURRENCY][array_search($_, $currencies)] = $qty; + else + $rewards[Type::ITEM][$_] = $qty; + } + } + if ($rewards) + $this->rewards[$id] = $rewards; + + if ($choices) + $this->choices[$id] = $choices; + } + } + + public function isRepeatable() : bool + { + return $this->curTpl['specialFlags'] & QUEST_FLAG_SPECIAL_REPEATABLE; + } + + public function isDaily() : int + { + if ($this->curTpl['flags'] & QUEST_FLAG_DAILY) + return 1; + + if ($this->curTpl['flags'] & QUEST_FLAG_WEEKLY) + return 2; + + if ($this->curTpl['specialFlags'] & QUEST_FLAG_SPECIAL_MONTHLY) + return 3; + + return 0; + } + + public function isAutoAccept() : bool + { + 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('questSortIdBak'), [-22, -284, -366, -369, -370, -376, -374]) && !$this->isRepeatable(); + } + + public function getSourceData(int $id = 0) : array + { + $data = []; + + foreach ($this->iterate() as $__) + { + if ($id && $id != $this->id) + continue; + + $data[$this->id] = array( + "n" => $this->getField('name', true), + "t" => Type::QUEST, + "ti" => $this->id, + "c" => $this->curTpl['cat1'], + "c2" => $this->curTpl['cat2'] + ); + } + + return $data; + } + + public function getSOMData(int $side = SIDE_BOTH) : array + { + $data = []; + + foreach ($this->iterate() as $__) + { + if (!(ChrRace::sideFromMask($this->curTpl['reqRaceMask']) & $side)) + 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` = %i', + $this->id + ); + + $data[$this->id] = array( + 'level' => $this->curTpl['level'] < 0 ? MAX_LEVEL : $this->curTpl['level'], + 'name' => $this->getField('name', true), + 'category' => $this->curTpl['cat1'], + 'category2' => $this->curTpl['cat2'], + 'series' => $series, + 'first' => $first + ); + + if ($this->isDaily()) + $data[$this->id]['daily'] = 1; + } + + return $data; + } + + public function getListviewData(int $extraFactionId = 0) : array + { + $data = []; + + foreach ($this->iterate() as $__) + { + $data[$this->id] = array( + 'category' => $this->curTpl['cat1'], + 'category2' => $this->curTpl['cat2'], + 'id' => $this->id, + 'level' => $this->curTpl['level'], + 'reqlevel' => $this->curTpl['minLevel'], + 'name' => Lang::unescapeUISequences($this->getField('name', true), Lang::FMT_RAW), + 'side' => ChrRace::sideFromMask($this->curTpl['reqRaceMask']), + 'wflags' => 0x0, + 'xp' => $this->curTpl['rewardXP'] + ); + + if (!empty($this->rewards[$this->id][Type::CURRENCY])) + foreach ($this->rewards[$this->id][Type::CURRENCY] as $iId => $qty) + $data[$this->id]['currencyrewards'][] = [$iId, $qty]; + + if (!empty($this->rewards[$this->id][Type::ITEM])) + foreach ($this->rewards[$this->id][Type::ITEM] as $iId => $qty) + $data[$this->id]['itemrewards'][] = [$iId, $qty]; + + if (!empty($this->choices[$this->id][Type::ITEM])) + foreach ($this->choices[$this->id][Type::ITEM] as $iId => $qty) + $data[$this->id]['itemchoices'][] = [$iId, $qty]; + + if ($_ = $this->curTpl['rewardTitleId']) + $data[$this->id]['titlereward'] = $_; + + if ($_ = $this->curTpl['questInfoId']) + $data[$this->id]['type'] = $_; + + if ($_ = $this->curTpl['reqClassMask']) + $data[$this->id]['reqclass'] = $_; + + if ($_ = ($this->curTpl['reqRaceMask'] & ChrRace::MASK_ALL)) + if ((($_ & ChrRace::MASK_ALLIANCE) != ChrRace::MASK_ALLIANCE) && (($_ & ChrRace::MASK_HORDE) != ChrRace::MASK_HORDE)) + $data[$this->id]['reqrace'] = $_; + + if ($_ = $this->curTpl['rewardOrReqMoney']) + if ($_ > 0) + $data[$this->id]['money'] = $_; + + // todo (med): also get disables + if ($this->curTpl['flags'] & QUEST_FLAG_UNAVAILABLE) + $data[$this->id]['historical'] = true; + + // if ($this->isRepeatable()) // dafuque..? says repeatable and is used as 'disabled'..? + // $data[$this->id]['wflags'] |= QUEST_CU_REPEATABLE; + if ($this->curTpl['cuFlags'] & (CUSTOM_UNAVAILABLE | CUSTOM_DISABLED)) + $data[$this->id]['wflags'] |= QUEST_CU_REPEATABLE; + + if ($this->curTpl['flags'] & QUEST_FLAG_DAILY) + { + $data[$this->id]['wflags'] |= QUEST_CU_DAILY; + $data[$this->id]['daily'] = true; + } + + if ($this->curTpl['flags'] & QUEST_FLAG_WEEKLY) + { + $data[$this->id]['wflags'] |= QUEST_CU_WEEKLY; + $data[$this->id]['weekly'] = true; + } + + if ($this->isSeasonal()) + $data[$this->id]['wflags'] |= QUEST_CU_SEASONAL; + + if ($this->curTpl['flags'] & QUEST_FLAG_TRACKING) // not shown in log + $data[$this->id]['wflags'] |= QUEST_CU_SKIP_LOG; + + if ($this->isAutoAccept()) + $data[$this->id]['wflags'] |= QUEST_CU_AUTO_ACCEPT; + + 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++) + { + $foo = $this->curTpl['rewardFactionId'.$i]; + $bar = $this->curTpl['rewardFactionValue'.$i]; + if ($foo && $bar) + { + $data[$this->id]['reprewards'][] = [$foo, $bar]; + + if ($extraFactionId == $foo) + $data[$this->id]['reputation'] = $bar; + } + } + } + + return $data; + } + + public function parseText(string $type = 'objectives', bool $jsEscaped = true) : string + { + $text = $this->getField($type, true); + if (!$text) + return ''; + + $text = Util::parseHtmlText($text); + + if ($jsEscaped) + $text = Util::jsEscape($text); + + return $text; + } + + public function renderTooltip() : ?string + { + if (!$this->curTpl) + return null; + + $title = Lang::unescapeUISequences(Util::htmlEscape($this->getField('name', true)), Lang::FMT_HTML); + $level = $this->curTpl['level']; + if ($level < 0) + $level = 0; + + $x = ''; + if ($level) + { + $level = sprintf(Lang::quest('questLevel'), $level); + + if ($this->curTpl['flags'] & QUEST_FLAG_DAILY) // daily + $level .= ' '.Lang::quest('daily'); + + $x .= '
'.$title.''.$level.'
'; + } + else + $x .= '
'.$title.'
'; + + + $x .= '

'.$this->parseText('objectives', false); + + + $xReq = ''; + for ($i = 1; $i < 5; $i++) + { + $ot = $this->getField('objectiveText'.$i, true); + $rng = $this->curTpl['reqNpcOrGo'.$i]; + $rngQty = $this->curTpl['reqNpcOrGoCount'.$i]; + + if (!$ot && ($rngQty < 1 || !$rng)) + continue; + + if ($ot) + $name = $ot; + 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); + + $xReq .= '
- '.$name.($rngQty > 1 ? ' x '.$rngQty : ''); + } + + for ($i = 1; $i < 7; $i++) + { + $ri = $this->curTpl['reqItemId'.$i]; + $riQty = $this->curTpl['reqItemCount'.$i]; + + if (!$ri || $riQty < 1) + continue; + + $name = Lang::unescapeUISequences(ItemList::getName($ri), Lang::FMT_HTML) ?: Util::ucFirst(Lang::game('item')).' #'.$ri; + + $xReq .= '
- '.$name.($riQty > 1 ? ' x '.$riQty : ''); + } + + if ($et = $this->getField('end', true)) + $xReq .= '
- '.$et; + + if ($_ = $this->getField('rewardOrReqMoney')) + if ($_ < 0) + $xReq .= '
- '.Lang::quest('money').Lang::main('colon').Util::formatMoney(abs($_)); + + if ($xReq) + $x .= '

'.Lang::quest('requirements').Lang::main('colon').''.$xReq; + + $x .= '
'; + + return $x; + } + + public function getJSGlobals(int $addMask = GLOBALINFO_ANY) : array + { + $data = []; + + foreach ($this->iterate() as $__) + { + if ($addMask & GLOBALINFO_REWARDS) + { + // items + for ($i = 1; $i < 5; $i++) + if ($this->curTpl['rewardItemId'.$i] > 0) + $data[Type::ITEM][$this->curTpl['rewardItemId'.$i]] = $this->curTpl['rewardItemId'.$i]; + + for ($i = 1; $i < 7; $i++) + if ($this->curTpl['rewardChoiceItemId'.$i] > 0) + $data[Type::ITEM][$this->curTpl['rewardChoiceItemId'.$i]] = $this->curTpl['rewardChoiceItemId'.$i]; + + // spells + if ($this->curTpl['rewardSpell'] > 0) + $data[Type::SPELL][$this->curTpl['rewardSpell']] = $this->curTpl['rewardSpell']; + + if ($this->curTpl['rewardSpellCast'] > 0) + $data[Type::SPELL][$this->curTpl['rewardSpellCast']] = $this->curTpl['rewardSpellCast']; + + // titles + if ($this->curTpl['rewardTitleId'] > 0) + $data[Type::TITLE][$this->curTpl['rewardTitleId']] = $this->curTpl['rewardTitleId']; + + // currencies + if (!empty($this->rewards[$this->id][Type::CURRENCY])) + foreach ($this->rewards[$this->id][Type::CURRENCY] as $id => $__) + $data[Type::CURRENCY][$id] = $id; + } + + 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; + } +} + + +class QuestListFilter extends Filter +{ + protected string $type = 'quests'; + protected static array $enums = array( + 37 => parent::ENUM_CLASSS, // classspecific + 38 => parent::ENUM_RACE, // racespecific + 9 => parent::ENUM_FACTION, // objectiveearnrepwith + 33 => parent::ENUM_EVENT, // relatedevent + 43 => parent::ENUM_CURRENCY, // currencyrewarded + 1 => parent::ENUM_FACTION, // increasesrepwith + 10 => parent::ENUM_FACTION // decreasesrepwith + ); + + 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_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, 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_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, [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 + ); + + public array $extraOpts = []; + + protected function createSQLForValues() : array + { + $parts = []; + $_v = $this->values; + + // name + if ($_v['na']) + { + $f = [['na', ['nml.nName', 'nml.nObjectives', 'nml.nDetails']]]; + if ($_v['ex'] != 'on') + $f = [['na', 'nml.nName']]; + + 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 + if ($_v['minle']) + $parts[] = ['level', $_v['minle'], '>=']; // not considering quests that are always at player level (-1) + + // level max + if ($_v['maxle']) + $parts[] = ['level', $_v['maxle'], '<=']; + + // reqLevel min + if ($_v['minrl']) + $parts[] = ['minLevel', $_v['minrl'], '>=']; // ignoring maxLevel + + // reqLevel max + if ($_v['maxrl']) + $parts[] = ['minLevel', $_v['maxrl'], '<=']; // ignoring maxLevel + + // side + if ($_v['si']) + { + $excl = [['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 => [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, '&']] + }; + } + + // questInfoId [list] + if ($_v['ty']) + $parts[] = ['questInfoId', $_v['ty']]; + + return $parts; + } + + protected function cbReputation(int $cr, int $crs, string $crv, string $sign) : ?array + { + if (!Util::checkNumeric($crs, NUM_CAST_INT)) + return null; + + if (!in_array($crs, self::$enums[$cr])) + return null; + + if ($_ = DB::Aowow()->selectRow('SELECT * FROM ::factions WHERE `id` = %i', $crs)) + $this->fiReputationCols[] = [$crs, Util::localizedString($_, 'name')]; + + return [ + 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]] + ]; + } + + protected function cbQuestRelation(int $cr, int $crs, string $crv, $flags) : ?array + { + return match ($crs) + { + Type::NPC, + Type::OBJECT, + Type::ITEM => [DB::AND, ['qse.type', $crs], ['qse.method', $flags, '&']], + default => null + }; + } + + protected function cbCurrencyReward(int $cr, int $crs, string $crv) : ?array + { + if (!Util::checkNumeric($crs, NUM_CAST_INT)) + return null; + + if (!in_array($crs, self::$enums[$cr])) + return null; + + return [ + DB::OR, + ['rewardItemId1', $crs], ['rewardItemId2', $crs], ['rewardItemId3', $crs], ['rewardItemId4', $crs], + ['rewardChoiceItemId1', $crs], ['rewardChoiceItemId2', $crs], ['rewardChoiceItemId3', $crs], ['rewardChoiceItemId4', $crs], ['rewardChoiceItemId5', $crs], ['rewardChoiceItemId6', $crs] + ]; + } + + protected function cbAvailable(int $cr, int $crs, string $crv) : ?array + { + if (!$this->int2Bool($crs)) + return null; + + if ($crs) + return [['cuFlags', CUSTOM_UNAVAILABLE | CUSTOM_DISABLED, '&'], 0]; + else + return ['cuFlags', CUSTOM_UNAVAILABLE | CUSTOM_DISABLED, '&']; + } + + protected function cbItemChoices(int $cr, int $crs, string $crv) : ?array + { + if (!Util::checkNumeric($crv, NUM_CAST_INT) || !$this->int2Op($crs)) + return null; + + $this->extraOpts['q']['s'][] = ', (IF(`rewardChoiceItemId1`, 1, 0) + IF(`rewardChoiceItemId2`, 1, 0) + IF(`rewardChoiceItemId3`, 1, 0) + IF(`rewardChoiceItemId4`, 1, 0) + IF(`rewardChoiceItemId5`, 1, 0) + IF(`rewardChoiceItemId6`, 1, 0)) AS "numChoices"'; + $this->extraOpts['q']['h'][] = '`numChoices` '.$crs.' '.$crv; + return [1]; + } + + protected function cbItemRewards(int $cr, int $crs, string $crv) : ?array + { + if (!Util::checkNumeric($crv, NUM_CAST_INT) || !$this->int2Op($crs)) + return null; + + $this->extraOpts['q']['s'][] = ', (IF(`rewardItemId1`, 1, 0) + IF(`rewardItemId2`, 1, 0) + IF(`rewardItemId3`, 1, 0) + IF(`rewardItemId4`, 1, 0)) AS "numRewards"'; + $this->extraOpts['q']['h'][] = '`numRewards` '.$crs.' '.$crv; + return [1]; + } + + protected function cbLoremaster(int $cr, int $crs, string $crv) : ?array + { + if (!$this->int2Bool($crs)) + return null; + + if ($crs) + 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 [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 + { + if (!$this->int2Bool($crs)) + return null; + + if ($crs) + return [DB::OR, ['sourceSpellId', 0, '>'], ['rewardSpell', 0, '>'], ['rsc.effect1Id', SpellList::EFFECTS_TEACH], ['rsc.effect2Id', SpellList::EFFECTS_TEACH], ['rsc.effect3Id', SpellList::EFFECTS_TEACH]]; + else + return [DB::AND, ['sourceSpellId', 0], ['rewardSpell', 0], ['rewardSpellCast', 0]]; + } + + protected function cbEarnReputation(int $cr, int $crs, string $crv) : ?array + { + if (!Util::checkNumeric($crs, NUM_CAST_INT)) + return null; + + if ($crs == parent::ENUM_ANY) + return [DB::OR, ['reqFactionId1', 0, '>'], ['reqFactionId2', 0, '>']]; + else if ($crs == parent::ENUM_NONE) + return [DB::AND, ['reqFactionId1', 0], ['reqFactionId2', 0]]; + else if (in_array($crs, self::$enums[$cr])) + return [DB::OR, ['reqFactionId1', $crs], ['reqFactionId2', $crs]]; + + return null; + } + + protected function cbClassSpec(int $cr, int $crs, string $crv) : ?array + { + if (!isset(self::$enums[$cr][$crs])) + return null; + + $_ = self::$enums[$cr][$crs]; + if ($_ === true) + return [DB::AND, ['reqClassMask', 0, '!'], [['reqClassMask', ChrClass::MASK_ALL, '&'], ChrClass::MASK_ALL, '!']]; + else if ($_ === false) + return [DB::OR, ['reqClassMask', 0], [['reqClassMask', ChrClass::MASK_ALL, '&'], ChrClass::MASK_ALL]]; + else if (is_int($_)) + return [DB::AND, ['reqClassMask', ChrClass::from($_)->toMask(), '&'], [['reqClassMask', ChrClass::MASK_ALL, '&'], ChrClass::MASK_ALL, '!']]; + + return null; + } + + protected function cbRaceSpec(int $cr, int $crs, string $crv) : ?array + { + if (!isset(self::$enums[$cr][$crs])) + return null; + + $_ = self::$enums[$cr][$crs]; + if ($_ === true) + 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 [DB::OR, ['reqRaceMask', 0], ['reqRaceMask', ChrRace::MASK_ALL], ['reqRaceMask', ChrRace::MASK_ALLIANCE], ['reqRaceMask', ChrRace::MASK_HORDE]]; + else if (is_int($_)) + return [DB::AND, ['reqRaceMask', ChrRace::from($_)->toMask(), '&'], [['reqRaceMask', ChrRace::MASK_ALLIANCE, '&'], ChrRace::MASK_ALLIANCE, '!'], [['reqRaceMask', ChrRace::MASK_HORDE, '&'], ChrRace::MASK_HORDE, '!']]; + + return null; + } + + protected function cbLacksStartEnd(int $cr, int $crs, string $crv) : ?array + { + 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'); + if ($crs) + return ['id', $missing]; + else + return ['id', $missing, '!']; + } +} + + +?> diff --git a/includes/dbtypes/skill.class.php b/includes/dbtypes/skill.class.php new file mode 100644 index 00000000..9aafbcb1 --- /dev/null +++ b/includes/dbtypes/skill.class.php @@ -0,0 +1,73 @@ + [['ic']], + 'ic' => ['j' => ['::icons ic ON ic.`id` = sl.`iconId`', true], 's' => ', ic.`name` AS "iconString"'], + ); + + public function __construct(array $conditions = [], array $miscData = []) + { + parent::__construct($conditions, $miscData); + + // post processing + foreach ($this->iterate() as &$_curTpl) + { + $_ = &$_curTpl['specializations']; // shorthand + if (!$_) + $_ = [0, 0, 0, 0, 0]; + else + $_ = array_pad(explode(' ', $_), 5, 0); + + if (!$_curTpl['iconId']) + $_curTpl['iconString'] = DEFAULT_ICON; + } + } + + public function getListviewData() : array + { + $data = []; + + foreach ($this->iterate() as $__) + { + $data[$this->id] = array( + 'category' => $this->curTpl['typeCat'], + 'categorybak' => $this->curTpl['categoryId'], + 'id' => $this->id, + 'name' => $this->getField('name', true), + 'profession' => $this->curTpl['professionMask'], + 'recipeSubclass' => $this->curTpl['recipeSubClass'], + 'specializations' => Util::toJSON($this->curTpl['specializations'], JSON_NUMERIC_CHECK), + 'icon' => $this->curTpl['iconString'] + ); + } + + return $data; + } + + public function getJSGlobals(int $addMask = 0) : array + { + $data = []; + + foreach ($this->iterate() as $__) + $data[self::$type][$this->id] = ['name' => $this->getField('name', true), 'icon' => $this->curTpl['iconString']]; + + return $data; + } + + public function renderTooltip() : ?string { return null; } +} + +?> diff --git a/includes/dbtypes/sound.class.php b/includes/dbtypes/sound.class.php new file mode 100644 index 00000000..f43a8fb3 --- /dev/null +++ b/includes/dbtypes/sound.class.php @@ -0,0 +1,129 @@ + MIME_TYPE_OGG, SOUND_TYPE_MP3 => MIME_TYPE_MP3]; + + public function __construct(array $conditions = [], array $miscData = []) + { + parent::__construct($conditions, $miscData); + + // post processing + foreach ($this->iterate() as $id => &$_curTpl) + { + $_curTpl['files'] = []; + for ($i = 1; $i < 11; $i++) + { + if ($_curTpl['soundFile'.$i]) + { + $this->fileBuffer[$_curTpl['soundFile'.$i]] = null; + $_curTpl['files'][] = &$this->fileBuffer[$_curTpl['soundFile'.$i]]; + } + + unset($_curTpl['soundFile'.$i]); + } + } + + if ($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 + $data['path'] = str_replace('\\', '\\\\', $data['path'] ? $data['path'] . '\\' . $data['title'] : $data['title']); + // skip file extension + $data['title'] = substr($data['title'], 0, -4); + // enum to string + $data['type'] = self::$fileTypes[$data['type']]; + // get real url + $data['url'] = Cfg::get('STATIC_URL') . '/wowsounds/' . $data['id']; + // v push v + $this->fileBuffer[$id] = $data; + } + } + } + + public static function getName(int $id) : ?LocString + { + if ($n = DB::Aowow()->SelectRow('SELECT `name` AS "name_loc0" FROM %n WHERE `id` = %i', self::$dataTable, $id)) + return new LocString($n); + return null; + } + + public function getListviewData() : array + { + $data = []; + + foreach ($this->iterate() as $__) + { + $data[$this->id] = array( + 'id' => $this->id, + 'type' => $this->getField('cat'), + 'name' => $this->getField('name'), + 'files' => array_values(array_filter($this->getField('files'))) + ); + } + + return $data; + } + + public function getJSGlobals(int $addMask = 0) : array + { + $data = []; + + foreach ($this->iterate() as $__) + $data[self::$type][$this->id] = array( + 'name' => $this->getField('name', true), + 'type' => $this->getField('cat'), + 'files' => array_values(array_filter($this->getField('files'))) + ); + + return $data; + } + + public function renderTooltip() : ?string { return null; } +} + +class SoundListFilter extends Filter +{ + protected string $type = 'sounds'; + protected static array $inputFields = array( + '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 + ); + + protected function createSQLForValues() : array + { + $parts = []; + $_v = &$this->values; + + // name [str] + if ($_v['na']) + if ($_ = $this->buildLikeLookup([['na', 'name']])) + $parts[] = $_; + + // type [list] + if ($_v['ty']) + $parts[] = ['cat', $_v['ty']]; + + return $parts; + } +} + +?> diff --git a/includes/dbtypes/spell.class.php b/includes/dbtypes/spell.class.php new file mode 100644 index 00000000..6aefde19 --- /dev/null +++ b/includes/dbtypes/spell.class.php @@ -0,0 +1,2879 @@ + [ 43, 44, 45, 46, 54, 55, 95, 118, 136, 160, 162, 172, 173, 176, 226, 228, 229, 473], // Weapons + 8 => [293, 413, 414, 415, 433], // Armor + 9 => SKILLS_TRADE_SECONDARY, // sec. Professions + 10 => [ 98, 109, 111, 113, 115, 137, 138, 139, 140, 141, 313, 315, 673, 759], // Languages + 11 => SKILLS_TRADE_PRIMARY // prim. Professions + ); + + 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_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 + ); + public const EFFECTS_TRIGGER = array( + SPELL_EFFECT_DUMMY, SPELL_EFFECT_TRIGGER_MISSILE, SPELL_EFFECT_TRIGGER_SPELL, SPELL_EFFECT_FEED_PET, SPELL_EFFECT_FORCE_CAST, + SPELL_EFFECT_FORCE_CAST_WITH_VALUE, SPELL_EFFECT_TRIGGER_SPELL_WITH_VALUE, SPELL_EFFECT_TRIGGER_MISSILE_SPELL_WITH_VALUE, SPELL_EFFECT_TRIGGER_SPELL_2, SPELL_EFFECT_SUMMON_RAF_FRIEND, + SPELL_EFFECT_TITAN_GRIP, SPELL_EFFECT_FORCE_CAST_2, SPELL_EFFECT_REMOVE_AURA + ); + public const EFFECTS_TEACH = array( + SPELL_EFFECT_LEARN_SPELL, SPELL_EFFECT_LEARN_PET_SPELL /*SPELL_EFFECT_UNLEARN_SPECIALIZATION*/ + ); + public const EFFECTS_MODEL_OBJECT = array( + SPELL_EFFECT_TRANS_DOOR, SPELL_EFFECT_SUMMON_OBJECT_WILD, SPELL_EFFECT_SUMMON_OBJECT_SLOT1, SPELL_EFFECT_SUMMON_OBJECT_SLOT2, SPELL_EFFECT_SUMMON_OBJECT_SLOT3, + SPELL_EFFECT_SUMMON_OBJECT_SLOT4 + ); + 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( // 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 + ); + 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_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_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 + ); + public const AURAS_TRIGGER = array( + SPELL_AURA_DUMMY, SPELL_AURA_PERIODIC_TRIGGER_SPELL, SPELL_AURA_PROC_TRIGGER_SPELL, SPELL_AURA_PERIODIC_TRIGGER_SPELL_FROM_CLIENT, SPELL_AURA_ADD_TARGET_TRIGGER, + SPELL_AURA_PERIODIC_DUMMY, SPELL_AURA_PERIODIC_TRIGGER_SPELL_WITH_VALUE, SPELL_AURA_PROC_TRIGGER_SPELL_WITH_VALUE, SPELL_AURA_CONTROL_VEHICLE, SPELL_AURA_LINKED + ); + public const AURAS_MODEL_NPC = array( + 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( // 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 int $interactive = self::INTERACTIVE_EMBEDDED; + private int $charLevel = MAX_LEVEL; + private array $scaling = []; + private array $parsedText = []; + private static array $spellTypes = array( + 6 => 1, + 8 => 2, + 10 => 4 + ); + + 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 + '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 = []) + { + parent::__construct($conditions, $miscData); + + if ($this->error) + return; + + if (isset($miscData['interactive'])) + $this->interactive = $miscData['interactive']; + + if (isset($miscData['charLevel'])) + $this->charLevel = $miscData['charLevel']; + + // post processing + $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 + if ($idx = $this->canCreateItem()) + foreach ($idx as $i) + $foo[] = (int)$_curTpl['effect'.$i.'CreateItemId']; + + for ($i = 1; $i <= 8; $i++) + if ($_curTpl['reagent'.$i] > 0) + $foo[] = (int)$_curTpl['reagent'.$i]; + + for ($i = 1; $i <= 2; $i++) + if ($_curTpl['tool'.$i] > 0) + $foo[] = (int)$_curTpl['tool'.$i]; + + // ranks + $this->ranks[$this->id] = $this->getField('rank', true); + + // sources + for ($i = 1; $i < 25; $i++) + { + if ($_ = $_curTpl['src'.$i]) + $this->sources[$this->id][$i][] = $_; + + unset($_curTpl['src'.$i]); + } + + // set full masks to 0 + $_curTpl['reqClassMask'] &= ChrClass::MASK_ALL; + if ($_curTpl['reqClassMask'] == ChrClass::MASK_ALL) + $_curTpl['reqClassMask'] = 0; + + $_curTpl['reqRaceMask'] &= ChrRace::MASK_ALL; + if ($_curTpl['reqRaceMask'] == ChrRace::MASK_ALL) + $_curTpl['reqRaceMask'] = 0; + + // unpack skillLines + $_curTpl['skillLines'] = []; + if ($_curTpl['skillLine1'] < 0) + { + foreach (Game::$skillLineMask[$_curTpl['skillLine1']] as $idx => [, $skillLineId]) + if ($_curTpl['skillLine2OrMask'] & (1 << $idx)) + $_curTpl['skillLines'][] = $skillLineId; + } + else if ($sec = $_curTpl['skillLine2OrMask']) + { + if ($this->id == 818) // and another hack .. basic Campfire (818) has deprecated skill Survival (142) as first skillLine + $_curTpl['skillLines'] = [$sec, $_curTpl['skillLine1']]; + else + $_curTpl['skillLines'] = [$_curTpl['skillLine1'], $sec]; + } + else if ($prim = $_curTpl['skillLine1']) + $_curTpl['skillLines'] = [$prim]; + + unset($_curTpl['skillLine1']); + unset($_curTpl['skillLine2OrMask']); + + // fix missing icons + $_curTpl['iconString'] = $_curTpl['iconString'] ?: DEFAULT_ICON; + + $this->scaling[$this->id] = false; + } + + if ($foo) + $this->relItems = new ItemList(array(['i.id', array_unique($foo)])); + } + + // required for item-comparison + public function getStatGain() : array + { + $data = []; + + foreach ($this->iterate() as $__) + { + $data[$this->id] = new StatsContainer(); + + foreach ($this->canEnchantmentItem() as $i) + $data[$this->id]->fromDB(Type::ENCHANTMENT, $this->curTpl['effect'.$i.'MiscValue']); + + // todo: should enchantments be included here...? + $data[$this->id]->fromSpell($this->curTpl); + } + + return $data; + } + + public function getProfilerMods() : array + { + // weapon hand check: param: slot, class, subclass, value + $whCheck = '$function() { var j, w = _inventory.getInventory()[%d]; if (!w[0] || !g_items[w[0]]) { return 0; } j = g_items[w[0]].jsonequip; return (j.classs == %d && (%d & (1 << (j.subclass)))) ? %d : 0; }'; + + $data = []; // flat gains + foreach ($this->getStatGain() as $id => $spellData) + { + $data[$id] = $spellData->toJson(STAT::FLAG_ITEM | STAT::FLAG_PROFILER, false); + + // apply weapon restrictions + $this->getEntry($id); + $class = $this->getField('equippedItemClass'); + $subClass = $this->getField('equippedItemSubClassMask'); + $slot = $subClass & 0x5000C ? 18 : 16; + if ($class != ITEM_CLASS_WEAPON || !$subClass) + continue; + + foreach ($data[$id] as $key => $amt) + $data[$id][$key] = [1, 'functionOf', sprintf($whCheck, $slot, $class, $subClass, $amt)]; + } + + // 4 possible modifiers found + // => [0.15, 'functionOf', ] + // => [0.33, 'percentOf', ] + // => [123, 'add'] + // => ... as from getStatGain() + + $modXByStat = function (array &$arr, int $srcStat, ?string $destStat, int $pts) : void + { + 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 (array &$arr, int $srcStat, string $destStat, array|int $val) : void + { + 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 (int $statId) : string + { + 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 => $__) + { + // 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) + { + $data[$id]['parrypct'] = [5, 'add']; + continue; + } + + if (!($this->getField('attributes0') & SPELL_ATTR0_PASSIVE)) + continue; + + for ($i = 1; $i < 4; $i++) + { + $pts = $this->calculateAmountForCurrent($i)[1]; + $mv = $this->getField('effect'.$i.'MiscValue'); + $mvB = $this->getField('effect'.$i.'MiscValueB'); + $au = $this->getField('effect'.$i.'AuraId'); + $class = $this->getField('equippedItemClass'); + $subClass = $this->getField('equippedItemSubClassMask'); + + + /* ISSUE! + mods formated like ['' => [, 'percentOf', '']] are applied as multiplier and not + as a flat value (that is equal to the percentage, like they should be). So the stats-table won't show the actual deficit + */ + + switch ($au) + { + case SPELL_AURA_MOD_RESISTANCE_PCT: + case SPELL_AURA_MOD_BASE_RESISTANCE_PCT: + // Armor only if explicitly specified only affects armor from equippment + if ($mv == (1 << SPELL_SCHOOL_NORMAL)) + $data[$id]['armor'] = [$pts / 100, 'percentOf', ['armor', 0]]; + else if ($mv) + $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], $mv, 'res', [$pts / 100, 'percentOf', $jsonStat($mvB)]); + break; + case SPELL_AURA_MOD_TOTAL_STAT_PERCENTAGE: + if ($mv > -1) // one stat + $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)) + if ($key = Stat::getJsonString($idx)) + $data[$id][$key] = [$pts / 100, 'percentOf', $key]; + break; + case SPELL_AURA_MOD_SPELL_DAMAGE_OF_STAT_PERCENT: + $mv = $mv ?: SPELL_MAGIC_SCHOOLS; + $modXBySchool($data[$id], $mv, 'spldmg', [$pts / 100, 'percentOf', $jsonStat($mvB)]); + break; + case SPELL_AURA_MOD_RANGED_ATTACK_POWER_OF_STAT_PERCENT: + $modXByStat($data[$id], $mv, 'rgdatkpwr', $pts); + break; + case SPELL_AURA_MOD_ATTACK_POWER_OF_STAT_PERCENT: + $modXByStat($data[$id], $mv, 'mleatkpwr', $pts); + break; + case SPELL_AURA_MOD_SPELL_HEALING_OF_STAT_PERCENT: + $modXByStat($data[$id], $mv, 'splheal', $pts); + break; + case SPELL_AURA_MOD_MANA_REGEN_FROM_STAT: + $modXByStat($data[$id], $mv, 'manargn', $pts); + break; + case SPELL_AURA_MOD_MANA_REGEN_INTERRUPT: + $data[$id]['icmanargn'] = [$pts / 100, 'percentOf', 'oocmanargn']; + break; + case SPELL_AURA_MOD_SPELL_CRIT_CHANCE: + case SPELL_AURA_MOD_SPELL_CRIT_CHANCE_SCHOOL: + $mv = $mv ?: SPELL_MAGIC_SCHOOLS; + $modXBySchool($data[$id], $mv, 'splcritstrkpct', [$pts, 'add']); + if (($mv & SPELL_MAGIC_SCHOOLS) == SPELL_MAGIC_SCHOOLS) + $data[$id]['splcritstrkpct'] = [$pts, 'add']; + break; + case SPELL_AURA_MOD_ATTACK_POWER_OF_ARMOR: + $data[$id]['mleatkpwr'] = [1 / $pts, 'percentOf', 'fullarmor']; + $data[$id]['rgdatkpwr'] = [1 / $pts, 'percentOf', 'fullarmor']; + break; + case SPELL_AURA_MOD_WEAPON_CRIT_PERCENT: + if ($class < 1 || ($class == ITEM_CLASS_WEAPON && ($subClass & 0x5000C))) + $data[$id]['rgdcritstrkpct'] = [1, 'functionOf', sprintf($whCheck, 18, $class, $subClass, $pts)]; + // $data[$id]['rgdcritstrkpct'] = [$pts, 'add']; + if ($class < 1 || ($class == ITEM_CLASS_WEAPON && ($subClass & 0xA5F3))) + $data[$id]['mlecritstrkpct'] = [1, 'functionOf', sprintf($whCheck, 16, $class, $subClass, $pts)]; + // $data[$id]['mlecritstrkpct'] = [$pts, 'add']; + break; + case SPELL_AURA_MOD_PARRY_PERCENT: + $data[$id]['parrypct'] = [$pts, 'add']; + break; + case SPELL_AURA_MOD_DODGE_PERCENT: + $data[$id]['dodgepct'] = [$pts, 'add']; + break; + case SPELL_AURA_MOD_BLOCK_PERCENT: + $data[$id]['blockpct'] = [$pts, 'add']; + break; + case SPELL_AURA_MOD_INCREASE_ENERGY_PERCENT: + if ($mv == POWER_HEALTH) + $data[$id]['health'] = [$pts / 100, 'percentOf', 'health']; + else if ($mv == POWER_ENERGY) + $data[$id]['energy'] = [$pts / 100, 'percentOf', 'energy']; + else if ($mv == POWER_MANA) + $data[$id]['mana'] = [$pts / 100, 'percentOf', 'mana']; + else if ($mv == POWER_RAGE) + $data[$id]['rage'] = [$pts / 100, 'percentOf', 'rage']; + else if ($mv == POWER_RUNIC_POWER) + $data[$id]['runic'] = [$pts / 100, 'percentOf', 'runic']; + break; + case SPELL_AURA_MOD_INCREASE_HEALTH_PERCENT: + $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', '$(x) => g_statistics.combo[x.classs][x.level][5]']; + break; + case SPELL_AURA_MOD_SHIELD_BLOCKVALUE_PCT: + $data[$id]['block'] = [$pts / 100, 'percentOf', 'block']; + break; + case SPELL_AURA_MOD_CRIT_PCT: + $data[$id]['mlecritstrkpct'] = [$pts, 'add']; + $data[$id]['rgdcritstrkpct'] = [$pts, 'add']; + $data[$id]['splcritstrkpct'] = [$pts, 'add']; + break; + case SPELL_AURA_MOD_SPELL_DAMAGE_OF_ATTACK_POWER: + $mv = $mv ?: SPELL_MAGIC_SCHOOLS; + $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']; + break; + case SPELL_AURA_MOD_ATTACK_POWER_PCT: // ingame only melee..? + $data[$id]['mleatkpwr'] = [$pts / 100, 'percentOf', 'mleatkpwr']; + break; + case SPELL_AURA_MOD_HEALTH_REGEN_PERCENT: + $data[$id]['healthrgn'] = [$pts / 100, 'percentOf', 'healthrgn']; + break; + } + } + } + + return $data; + } + + // halper + public function getReagentsForCurrent() : array + { + $data = []; + + for ($i = 1; $i <= 8; $i++) + if ($this->curTpl['reagent'.$i] > 0 && $this->curTpl['reagentCount'.$i]) + $data[$this->curTpl['reagent'.$i]] = [$this->curTpl['reagent'.$i], $this->curTpl['reagentCount'.$i]]; + + return $data; + } + + public function getToolsForCurrent() : array + { + if ($this->tools) + return $this->tools; + + $tools = []; + for ($i = 1; $i <= 2; $i++) + { + // TotemCategory + if ($_ = $this->curTpl['toolCategory'.$i]) + { + $tc = DB::Aowow()->selectRow('SELECT * FROM ::totemcategory WHERE `id` = %i', $_); + $tools[$i + 1] = array( + 'id' => $_, + 'name' => Util::localizedString($tc, 'name')); + } + + // Tools + if (!$this->curTpl['tool'.$i]) + continue; + + foreach ($this->relItems->iterate() as $relId => $__) + { + if ($relId != $this->curTpl['tool'.$i]) + continue; + + $tools[$i - 1] = array( + 'itemId' => $relId, + 'name' => $this->relItems->getField('name', true), + 'quality' => $this->relItems->getField('quality') + ); + + break; + } + } + + $this->tools = array_reverse($tools); + + return $this->tools; + } + + public function getModelInfo(int $spellId = 0, int $effIdx = 0) : array + { + $displays = $results = []; + + foreach ($this->iterate() as $id => $__) + { + if ($spellId && $spellId != $id) + continue; + + for ($i = 1; $i < 4; $i++) + { + if ($spellId && $effIdx && $effIdx != $i) + continue; + + $effMV = $this->curTpl['effect'.$i.'MiscValue']; + if (!$effMV) + continue; + + // GO Model from MiscVal + if (in_array($this->curTpl['effect'.$i.'Id'], SpellList::EFFECTS_MODEL_OBJECT)) + { + if (isset($displays[Type::OBJECT][$id])) + $displays[Type::OBJECT][$id][0][] = $i; + else + $displays[Type::OBJECT][$id] = [[$i], $effMV]; + } + // NPC Model from MiscVal + else if (in_array($this->curTpl['effect'.$i.'Id'], SpellList::EFFECTS_MODEL_NPC) || in_array($this->curTpl['effect'.$i.'AuraId'], SpellList::AURAS_MODEL_NPC)) + { + if (isset($displays[Type::NPC][$id])) + $displays[Type::NPC][$id][0][] = $i; + else + $displays[Type::NPC][$id] = [[$i], $effMV]; + } + // Shapeshift + else if ($this->curTpl['effect'.$i.'AuraId'] == SPELL_AURA_MOD_SHAPESHIFT) + { + $subForms = array( + 892 => [892, 29407, 29406, 29408, 29405], // Cat - NE + 8571 => [8571, 29410, 29411, 29412], // Cat - Tauren + 2281 => [2281, 29413, 29414, 29416, 29417], // Bear - NE + 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` = %i', $effMV)) + { + foreach ([1, 2] as $j) + if (isset($subForms[$st['model'.$j]])) + $st['model'.$j] = $subForms[$st['model'.$j]][array_rand($subForms[$st['model'.$j]])]; + + $results[$id][$i] = array( + 'type' => Type::NPC, + 'creatureType' => $st['creatureType'], + 'displayId' => $st['model2'] ? $st['model'.rand(1, 2)] : $st['model1'], + 'displayName' => Lang::game('st', $effMV) + ); + } + } + } + } + + if (!empty($displays[Type::NPC])) + { + $nModels = new CreatureList(array(['id', array_column($displays[Type::NPC], 1)])); + foreach ($nModels->iterate() as $nId => $__) + { + foreach ($displays[Type::NPC] as $srcId => [$indizes, $npcId]) + { + if ($npcId == $nId) + { + foreach ($indizes as $idx) + { + $res = array( + 'type' => Type::NPC, + 'typeId' => $nId, + 'displayId' => $nModels->getRandomModelId(), + 'displayName' => $nModels->getField('name', true) + ); + + if ($nModels->getField('humanoid')) + $res['humanoid'] = 1; + + $results[$srcId][$idx] = $res; + } + } + } + } + } + + if (!empty($displays[Type::OBJECT])) + { + $oModels = new GameObjectList(array(['id', array_column($displays[Type::OBJECT], 1)])); + foreach ($oModels->iterate() as $oId => $__) + { + foreach ($displays[Type::OBJECT] as $srcId => [$indizes, $objId]) + { + if ($objId == $oId) + { + foreach ($indizes as $idx) + { + $results[$srcId][$idx] = array( + 'type' => Type::OBJECT, + 'typeId' => $oId, + 'displayId' => $oModels->getField('displayId'), + 'displayName' => $oModels->getField('name', true) + ); + } + } + } + } + } + + if ($spellId && $effIdx) + return $results[$spellId][$effIdx] ?? []; + + if ($spellId) + return $results[$spellId] ?? []; + + return $results; + } + + private function createRangesForCurrent() : string + { + if (!$this->curTpl['rangeMaxHostile']) + return ''; + + if ($this->curTpl['attributes3'] & SPELL_ATTR3_DONT_DISPLAY_RANGE) + return ''; + + // minRange exists; show as range + if ($this->curTpl['rangeMinHostile']) + return Lang::spell('range', [$this->curTpl['rangeMinHostile'].' - '.$this->curTpl['rangeMaxHostile']]); + // friend and hostile differ; do color + else if ($this->curTpl['rangeMaxHostile'] != $this->curTpl['rangeMaxFriend']) + return Lang::spell('range', [''.$this->curTpl['rangeMaxHostile'].' - '.$this->curTpl['rangeMaxFriend']. '']); + // hardcode: "melee range" + else if ($this->curTpl['rangeMaxHostile'] == 5) + return Lang::spell('meleeRange'); + // hardcode "unlimited range" + else if ($this->curTpl['rangeMaxHostile'] == 50000) + return Lang::spell('unlimRange'); + // regular case + else + return Lang::spell('range', [$this->curTpl['rangeMaxHostile']]); + } + + public function createPowerCostForCurrent() : string + { + $str = ''; + + $pt = $this->curTpl['powerType']; + $pc = $this->curTpl['powerCost']; + $pcp = $this->curTpl['powerCostPercent']; + $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; + + if ($pt == POWER_RUNE && ($rCost = ($this->curTpl['powerCostRunes'] & 0x333))) + { // Blood 2|1 - Unholy 2|1 - Frost 2|1 + $runes = []; + + for ($i = 0; $i < 3; $i++) + { + if ($rCost & 0x3) + $runes[] = Lang::spell('powerCostRunes', $i, [$rCost & 0x3]); + + $rCost >>= 4; + } + + $str .= implode(' ', $runes); + } + else if ($pcp > 0) // power cost: pct over static + $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 .= ''; + + if (Lang::exist('spell', 'powerCost', $pt)) + $str .= Lang::spell('powerCost', $pt, intVal($pps > 0), [$pc, $pps]); + else + $str .= Lang::spell('powerDisplayCost', intVal($pps > 0), [$pc, Lang::spell('powerTypes', $pt), $pps]); + } + + // append level cost (todo (low): work in as scaling cost) + if ($pcpl > 0) + $str .= Lang::spell('costPerLevel', [$pcpl]); + + return $str; + } + + public function createCastTimeForCurrent(bool $short = true, bool $noInstant = true) : string + { + if (!$this->curTpl['castTime'] && $this->isChanneledSpell()) + return Lang::spell('channeled'); + // SPELL_ATTR0_ABILITY instant ability.. yeah, wording thing only (todo (low): rule is imperfect) + else if (!$this->curTpl['castTime'] && ($this->curTpl['damageClass'] != SPELL_DAMAGE_CLASS_MAGIC || $this->curTpl['attributes0'] & SPELL_ATTR0_ABILITY)) + return Lang::spell('instantPhys'); + // show instant only for player/pet/npc abilities (todo (low): unsure when really hidden (like talent-case)) + 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') : DateTime::formatTimeElapsedFloat($this->curTpl['castTime'] * 1000); + } + + private function createCooldownForCurrent() : string + { + if ($this->curTpl['attributes6'] & SPELL_ATTR6_DONT_DISPLAY_COOLDOWN) + return ''; + else if ($this->curTpl['recoveryTime']) + return Lang::formatTime($this->curTpl['recoveryTime'], 'spell', 'cooldown'); + else if ($this->curTpl['recoveryCategory']) + return Lang::formatTime($this->curTpl['recoveryCategory'], 'spell', 'cooldown'); + + return ''; + } + + // 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'); + $spellLvl = $this->getField('spellLevel'); + $LDSEffs = $this->canLevelDamageScale(); + $modMin = + $modMax = null; + + if ($rppl) + { + if ($level > $maxLvl && $maxLvl > 0) + $level = $maxLvl; + else if ($level < $baseLvl) + $level = $baseLvl; + + 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); + + if ($rppl) + { + $modMin = ''; + $modMax = ''; + } + else if ($this->getField('attributes0') & SPELL_ATTR0_LEVEL_DAMAGE_CALCULATION && in_array($effIdx, $LDSEffs) && $spellLvl) + { + $modMin = ''; + $modMax = ''; + } + + return [$min + $maxBase, $max + $maxBase, $modMin, $modMax]; + } + + public function canCreateItem() : array + { + $idx = []; + for ($i = 1; $i < 4; $i++) + if (in_array($this->curTpl['effect'.$i.'Id'], SpellList::EFFECTS_ITEM_CREATE) || in_array($this->curTpl['effect'.$i.'AuraId'], SpellList::AURAS_ITEM_CREATE)) + if ($this->curTpl['effect'.$i.'CreateItemId'] > 0) + $idx[] = $i; + + return $idx; + } + + public function canTriggerSpell() : array + { + $idx = []; + for ($i = 1; $i < 4; $i++) + if (in_array($this->curTpl['effect'.$i.'Id'], SpellList::EFFECTS_TRIGGER) || in_array($this->curTpl['effect'.$i.'AuraId'], SpellList::AURAS_TRIGGER)) + if ($this->curTpl['effect'.$i.'AuraId'] == SPELL_AURA_DUMMY || $this->curTpl['effect'.$i.'TriggerSpell'] > 0 || ($this->curTpl['effect'.$i.'Id'] == SPELL_EFFECT_TITAN_GRIP && $this->curTpl['effect'.$i.'MiscValue'] > 0)) + $idx[] = $i; + + return $idx; + } + + public function canTeachSpell() : array + { + $idx = []; + for ($i = 1; $i < 4; $i++) + if (in_array($this->curTpl['effect'.$i.'Id'], SpellList::EFFECTS_TEACH)) + if ($this->curTpl['effect'.$i.'TriggerSpell'] > 0) + $idx[] = $i; + + return $idx; + } + + public function canEnchantmentItem() : array + { + $idx = []; + for ($i = 1; $i < 4; $i++) + if (in_array($this->curTpl['effect'.$i.'Id'], SpellList::EFFECTS_ENCHANTMENT)) + $idx[] = $i; + + 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 isScalableHealingSpell() : bool + { + for ($i = 1; $i < 4; $i++) + 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 false; + } + + public function isScalableDamagingSpell() : bool + { + for ($i = 1; $i < 4; $i++) + 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 false; + } + + public function periodicEffectsMask() : int + { + $effMask = 0x0; + + for ($i = 1; $i < 4; $i++) + if ($this->curTpl['effect'.$i.'Periode'] > 0) + $effMask |= 1 << ($i - 1); + + 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->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 + $pfi = '<$PctFire>'; // %fire + $pfr = '<$PctFrost>'; // %frost + $ph = '<$PctHoly>'; // %holy + $pn = '<$PctNature>'; // %nature + $ps = '<$PctShadow>'; // %shadow + $pbh = '<$PctHeal>'; // %heal + $pbhd = '<$PctHealDone>'; // %heal done + $bc2 = '<$bc2>'; // bc2 + + $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; + $gt = $GT = fn($a, $b) => $a > $b; + $gte = $GTE = fn($a, $b) => $a >= $b; + $floor = $FLOOR = fn($a) => floor($a); + $max = $MAX = fn($a, $b) => max($a, $b); + $min = $MIN = fn($a, $b) => min($a, $b); + + if (preg_match_all('/\$\w+\b/i', $formula, $vars)) + { + + $evalable = true; + + foreach ($vars[0] as $var) // oh lord, forgive me this sin .. but is_callable seems to bug out and function_exists doesn't find lambda-functions >.< + { + $var = substr($var, 1); + + if (isset($$var)) + { + $eval = eval('return @$'.$var.';'); // attention: error suppression active here (will be logged anyway) + if (getType($eval) == 'object') + continue; + else if (is_numeric($eval)) + continue; + } + else + $$var = ''; + + $evalable = false; + break; + } + + if (!$evalable) + { + // can't eval constructs because of strings present. replace constructs with strings + $cond = $COND = $this->dfnText('COND(a, b, c)
a ? b : c', 'COND'); + $eq = $EQ = $this->dfnText('EQ(a, b)
a == b', 'EQ'); + $gt = $GT = $this->dfnText('GT(a, b)
a > b', 'GT'); + $gte = $GTE = $this->dfnText('GTE(a, b)
a >= b', 'GTE'); + $floor = $FLOOR = $this->dfnText('FLOOR(a)', 'FLOOR'); + $min = $MIN = $this->dfnText('MIN(a, b)', 'MIN'); + $max = $MAX = $this->dfnText('MAX(a, b)', 'MAX'); + $pl = $PL = $this->dfnText('LANG.level', 'PL'); + + // space out operators for better readability + $formula = preg_replace('/(\+|-|\*|\/)/', ' \1 ', $formula); + + // note the " ! + return eval('return "('.$formula.')";'); + } + else + return eval('return '.$formula.';'); + } + + // since this function may be called recursively, there are cases, where the already evaluated string is tried to be evaled again, throwing parse errors + // todo (med): also quit, if we replaced vars with non-interactive text + if (strstr($formula, '') || strstr($formula, '%s (%s)'; + $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*$} 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 == self::INTERACTIVE_FULL && ($modStrMin || $modStrMax)) + { + $this->scaling[$this->id] = true; + $fmtStringMin = $modStrMin.'%s'; + } + */ + $minPoints = ctype_lower($var) ? $min : $max; + break; + case 'n': // ProcCharges + case 'N': + $base = $srcSpell->getField('procCharges'); + + if (in_array($op, $signs) && is_numeric($oparg) && is_numeric($base)) + eval("\$base = $base $op $oparg;"); + + $minPoints = $base; + break; + case 'o': // TotalAmount for periodic auras (with variance) + case 'O': + $periode = $srcSpell->getField('effect'.$effIdx.'Periode'); + $duration = $srcSpell->getField('duration'); + + if (!$periode) + { + // Mod Power Regeneration & Mod Health Regeneration have an implicit periode of 5sec + $aura = $srcSpell->getField('effect'.$effIdx.'AuraId'); + if ($aura == SPELL_AURA_MOD_REGEN || $aura == SPELL_AURA_MOD_POWER_REGEN) + $periode = 5000; + else + $periode = 3000; + } + + [$min, $max, $modStrMin, $modStrMax] = $srcSpell->calculateAmountForCurrent($effIdx, intVal($duration / $periode)); + + if (in_array($op, $signs) && is_numeric($oparg)) + { + eval("\$min = $min $op $oparg;"); + eval("\$max = $max $op $oparg;"); + } + + if ($this->interactive >= self::INTERACTIVE_EMBEDDED && ($modStrMin || $modStrMax)) + { + $this->scaling[$this->id] = true; + + $fmtStringMin = $modStrMin.'%s'; + $fmtStringMax = $modStrMax.'%s'; + } + + $minPoints = $min; + $maxPoints = $max; + break; + case 'q': // EffectMiscValue + case 'Q': + $base = $srcSpell->getField('effect'.$effIdx.'MiscValue'); + + if (in_array($op, $signs) && is_numeric($oparg) && is_numeric($base)) + eval("\$base = $base $op $oparg;"); + + $minPoints = $base; + break; + case 'r': // SpellRange + case 'R': + $base = $srcSpell->getField('rangeMaxHostile'); + + if (in_array($op, $signs) && is_numeric($oparg) && is_numeric($base)) + eval("\$base = $base $op $oparg;"); + + $minPoints = $base; + break; + case 's': // BasePoints (with variance) + case 'S': + [$min, $max, $modStrMin, $modStrMax] = $srcSpell->calculateAmountForCurrent($effIdx); + $mv = $srcSpell->getField('effect'.$effIdx.'MiscValue'); + $aura = $srcSpell->getField('effect'.$effIdx.'AuraId'); + + if (in_array($op, $signs) && is_numeric($oparg)) + { + eval("\$min = $min $op $oparg;"); + eval("\$max = $max $op $oparg;"); + } + // Aura giving combat ratings + $stats = []; + if ($aura == SPELL_AURA_MOD_RATING) + if ($stats = StatsContainer::convertCombatRating($mv)) + $this->scaling[$this->id] = true; + // Aura end + + if ($stats && $this->interactive >= self::INTERACTIVE_EMBEDDED) + { + $fmtStringMin = '%s (%s)'; + $statId = $stats[0]; // could be multiple ratings in theory, but not expected to be + } + else if (($modStrMin || $modStrMax) && $this->interactive == self::INTERACTIVE_FULL) + { + $this->scaling[$this->id] = true; + $fmtStringMin = $modStrMin.'%s'; + $fmtStringMax = $modStrMax.'%s'; + } + + $minPoints = $min; + $maxPoints = $max; + break; + case 't': // Periode + case 'T': + $base = $srcSpell->getField('effect'.$effIdx.'Periode') / 1000; + + if (in_array($op, $signs) && is_numeric($oparg) && is_numeric($base)) + eval("\$base = $base $op $oparg;"); + + $minPoints = $base; + break; + case 'u': // StackCount + case 'U': + $base = $srcSpell->getField('stackAmount'); + + if (in_array($op, $signs) && is_numeric($oparg) && is_numeric($base)) + eval("\$base = $base $op $oparg;"); + + $minPoints = $base; + break; + case 'v': // MaxTargetLevel + case 'V': + $base = $srcSpell->getField('MaxTargetLevel'); + + if (in_array($op, $signs) && is_numeric($oparg) && is_numeric($base)) + eval("\$base = $base $op $oparg;"); + + $minPoints = $base; + break; + case 'x': // ChainTargetCount + case 'X': + $base = $srcSpell->getField('effect'.$effIdx.'ChainTarget'); + + if (in_array($op, $signs) && is_numeric($oparg) && is_numeric($base)) + eval("\$base = $base $op $oparg;"); + + $minPoints = $base; + break; + case 'z': // HomeZone + $fmtStringMin = Lang::spell('home'); + break; + } + + // handle excessively precise floats + if (is_float($minPoints)) + $minPoints = round($minPoints, 2); + if (isset($maxPoints) && is_float($maxPoints)) + $maxPoints = round($maxPoints, 2); + + return [$minPoints, $maxPoints, $fmtStringMin, $fmtStringMax, $statId]; + } + + // description-, buff-parsing component + private function resolveFormulaString(string $formula, int $precision = 0) : array + { + $fSuffix = '%s'; + $fStat = 0; + + // step 1: formula unpacking redux + while (($formStartPos = strpos($formula, '${')) !== false) + { + $formBrktCnt = 0; + $formPrecision = 0; + $formCurPos = $formStartPos; + + $formOutStr = ''; + + while ($formCurPos < strlen($formula)) + { + $char = $formula[$formCurPos]; + + if ($char == '}') + $formBrktCnt--; + + if ($formBrktCnt) + $formOutStr .= $char; + + if ($char == '{') + $formBrktCnt++; + + if (!$formBrktCnt && $formCurPos != $formStartPos) + break; + + $formCurPos++; + } + + if (isset($formula[++$formCurPos]) && $formula[$formCurPos] == '.') + { + $formPrecision = (int)$formula[++$formCurPos]; + ++$formCurPos; // for some odd reason the precision decimal survives if we dont increment further.. + } + + [$formOutStr, $fSuffix, $fStat] = $this->resolveFormulaString($formOutStr, $formPrecision); + + $formula = substr_replace($formula, $formOutStr, $formStartPos, ($formCurPos - $formStartPos)); + } + + // note: broken tooltip on this one + // ${58644m1/-10} gets matched as a formula (ok), 58644m1 has no $ prefixed (not ok) + // the client scraps the m1 and prints -5864 + if ($this->id == 58644) + $formula = '$'.$formula; + + // step 2: resolve variables + $pos = 0; // continue strpos-search from this offset + $str = ''; + while (($npos = strpos($formula, '$', $pos)) !== false) + { + if ($npos != $pos) + $str .= substr($formula, $pos, $npos - $pos); + + $pos = $npos++; + + if ($formula[$pos] == '$') + $pos++; + + $varParts = $this->matchVariableString(substr($formula, $pos), $len); + if (!$varParts) + { + $str .= '#'; // mark as done, reset below + continue; + } + + $pos += $len; + + // we are resolving a formula -> omit ranges + [$minPoints, , $fmtStringMin, , $statId] = $this->resolveVariableString($varParts); + + // time within formula -> rebase to seconds and omit timeUnit + if (strtolower($varParts['var']) == 'd') + { + $minPoints /= 1000; + unset($fmtStringMin); + } + + $str .= $minPoints; + + // overwrite eventually inherited strings + if (isset($fmtStringMin)) + $fSuffix = $fmtStringMin; + + // overwrite eventually inherited stat + if (isset($statId)) + $fStat = $statId; + } + $str .= substr($formula, $pos); + $str = str_replace('#', '$', $str); // reset markers + + // step 3: try to evaluate result + $evaled = $this->resolveEvaluation($str); + + $return = is_numeric($evaled) ? round($evaled, $precision) : $evaled; + + 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! + // 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 + { + // oooo..kaaayy.. parsing text in 6 or 7 easy steps + // we don't use the internal iterator here. This func has to be called for the individual template. + // otherwise it will get a bit messy, when we iterate, while we iterate *yo dawg!* + + /* + 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(%s) + $<> - variables + () - regular use for function-like calls + + variables in use .. caseSensitive + + game variables (optionally replace with textVars) + $PlayerName - Cpt. Obvious + $PL / $pl - PlayerLevel + $STR - Strength Attribute (not seen) + $AGI - Agility Attribute (not seen) + $STA - Stamina Attribute (not seen) + $INT - Intellect Attribute (not seen) + $SPI - Spirit Attribute + $AP - Atkpwr + $RAP - RngAtkPwr + $HND - hands used by weapon (1H, 2H) => (1, 2) + $MWS - MainhandWeaponSpeed + $mw / $MW - MainhandWeaponDamage Min/Max + $rwb / $RWB - RangedWeapon..Bonus? Min/Max + $sp - Spellpower + $spa - Spellpower Arcane + $spfi - Spellpower Fire + $spfr - Spellpower Frost + $sph - Spellpower Holy + $spn - Spellpower Nature + $sps - Spellpower Shadow + $bh - Bonus Healing + $pa - %-ArcaneDmg (as float) // V seems broken + $pfi - %-FireDmg (as float) + $pfr - %-FrostDmg (as float) + $ph - %-HolyDmg (as float) + $pn - %-NatureDmg (as float) + $ps - %-ShadowDmg (as float) + $pbh - %-HealingBonus (as float) + $pbhd - %-Healing Done (as float) // all above seem broken + $bc2 - baseCritChance? always 3.25 (unsure) + + spell variables (the stuff we can actually parse) rounding... >5 up? + $a - SpellRadius; per EffectIdx + $b - PointsPerComboPoint; per EffectIdx + $d / $D - SpellDuration; appended timeShorthand; d/D maybe base/max duration?; interpret "0" as "until canceled" + $e - EffectValueMultiplier; per EffectIdx + $f / $F - EffectDamageMultiplier; per EffectIdx + $g / $G - Gender-Switch $Gmale:female; + $h / $H - ProcChance + $i - MaxAffectedTargets + $l - LastValue-Switch; last value as condition $Ltrue:false; + $m / $M - BasePoints; per EffectIdx; m/M +1/+effectDieSides + $n - ProcCharges + $o - TotalAmount (for periodic auras); per EffectIdx + $q - EffectMiscValue; per EffectIdx + $r - SpellRange (hostile) + $s / $S - BasePoints; per EffectIdx; as Range, if applicable + $t / $T - EffectPeriode; per EffectIdx + $u - StackAmount + $v - MaxTargetLevel + $x - MaxAffectedTargets + $z - no place like + + deviations from standard procedures + division - example: $/10;2687s1 => $2687s1/10 + - also: $61829/5;s1 => $61829s1/5 + + functions in use .. caseInsensitive + $cond(a, b, c) - like SQL, if A is met use B otherwise use C + $eq(a, b) - a == b + $floor(a) - floor() + $gt(a, b) - a > b + $gte(a, b) - a >= b + $min(a, b) - min() + $max(a, b) - max() + */ + + $this->charLevel = $level; + + // step -1: already handled? + 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); + if (empty($data) || $data == "[]") // empty tooltip shouldn't be displayed anyway + return ['', [], false]; + + // step 1: if the text is supplemented with text-variables, get and replace them + if ($this->curTpl['spellDescriptionVariableId'] > 0) + { + if (empty($this->spellVars[$this->id])) + { + $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)) + $this->spellVars[$this->id][$matches[1]] = $matches[2]; + } + + // replace self-references + $reset = true; + while ($reset) + { + $reset = false; + foreach ($this->spellVars[$this->id] as $k => $sv) + { + if (preg_match('/\$<(\w*\d*)>/i', $sv, $matches)) + { + $this->spellVars[$this->id][$k] = str_replace('$<'.$matches[1].'>', '${'.$this->spellVars[$this->id][$matches[1]].'}', $sv); + $reset = true; + } + } + } + + // finally, replace SpellDescVars + foreach ($this->spellVars[$this->id] as $k => $sv) + $data = str_replace('$<'.$k.'>', $sv, $data); + } + + // step 2: resolving conditions + // aura- or spell-conditions cant be resolved for our purposes, so force them to false for now (todo (low): strg+f "know" in aowowPower.js ^.^) + + /* sequences + a) simple - $?cond[A][B] // simple case of b) + b) elseif - $?cond[A]?cond[B]..[C] // can probably be repeated as often as you wanted + c) recursive - $?cond[A][$?cond[B][..]] // can probably be stacked as deep as you wanted + + only case a) can be used for KNOW-parameter + */ + + $relSpells = []; + $data = $this->handleConditions($data, $relSpells, true); + + // step 3: unpack formulas ${ .. }.X + $data = $this->handleFormulas($data, true); + + // step 4: find and eliminate regular variables + $data = $this->handleVariables($data, true); + + // step 5: variable-dependent variable-text + // special case $lONE:ELSE[:ELSE2]; or $|ONE:ELSE[:ELSE2]; + while (preg_match('/([\d\.]+)([^\d]*)(\$[l|]:*)([^:]*):([^;]*);/i', $data, $m)) + { + $plurals = explode(':', $m[5]); + $replace = ''; + + if (count($plurals) == 2) // special case: ruRU + { + switch (substr($m[1], -1)) // check last digit of number + { + case 1: + // but not 11 (teen number) + if (!in_array($m[1], [11])) + { + $replace = $m[4]; + break; + } + case 2: + case 3: + case 4: + // but not 12, 13, 14 (teen number) [11 is passthrough] + if (!in_array($m[1], [11, 12, 13, 14])) + { + $replace = $plurals[0]; + break; + } + break; + default: + $replace = $plurals[1]; + } + + } + else + $replace = ($m[1] == 1 ? $m[4] : $plurals[0]); + + $data = str_ireplace($m[1].$m[2].$m[3].$m[4].':'.$m[5].';', $m[1].$m[2].$replace, $data); + } + + // step 6: HTMLize + // colors + $data = preg_replace('/\|cff([a-f0-9]{6})(.+?)\|r/i', '$2', $data); + + // line endings + $data = strtr($data, ["\r" => '', "\n" => '
']); + + // cache result + $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]]; + } + + private function handleFormulas(string $data, bool $topLevel = false) : string + { + // they are stacked recursively but should be balanced .. hf + while (($formStartPos = strpos($data, '${')) !== false) + { + $formBrktCnt = 0; + $formPrecision = 0; + $formCurPos = $formStartPos; + + $formOutStr = ''; + + while ($formCurPos < strlen($data)) // only hard-exit condition, we'll hit those breaks eventually^^ + { + $char = $data[$formCurPos]; + + if ($char == '}') + $formBrktCnt--; + + if ($formBrktCnt) + $formOutStr .= $char; + + if ($char == '{') + $formBrktCnt++; + + if (!$formBrktCnt && $formCurPos != $formStartPos) + break; + + // advance position + $formCurPos++; + } + + $formCurPos++; + + // check for precision-modifiers + if ($formCurPos + 1 < strlen($data) && $data[$formCurPos] == '.' && is_numeric($data[$formCurPos + 1])) + { + $formPrecision = $data[$formCurPos + 1]; + $formCurPos += 2; + } + [$formOutVal, $formOutStr, $statId] = $this->resolveFormulaString($formOutStr, $formPrecision ?: ($topLevel ? 0 : 10)); + + 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); + + $data = substr_replace($data, $resolved, $formStartPos, ($formCurPos - $formStartPos)); + } + + return $data; + } + + private function handleVariables(string $data, bool $topLevel = false) : string + { + $pos = 0; // continue strpos-search from this offset + $str = ''; + while (($npos = strpos($data, '$', $pos)) !== false) + { + if ($npos != $pos) + $str .= substr($data, $pos, $npos - $pos); + + $pos = $npos++; + + if ($data[$pos] == '$') + $pos++; + + $varParts = $this->matchVariableString(substr($data, $pos), $len); + if (!$varParts) + { + $str .= '#'; // mark as done, reset below + continue; + } + + $pos += $len; + + [$minPoints, $maxPoints, $fmtStringMin, $fmtStringMax, $statId] = $this->resolveVariableString($varParts); + $resolved = is_numeric($minPoints) ? abs($minPoints) : $minPoints; + if (isset($fmtStringMin)) + { + 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); + } + + if (isset($maxPoints) && $minPoints != $maxPoints && !isset($statId)) + { + $_ = is_numeric($maxPoints) ? abs($maxPoints) : $maxPoints; + $resolved .= Lang::game('valueDelim'); + $resolved .= isset($fmtStringMax) ? sprintf($fmtStringMax, $_) : $_; + } + + $str .= $resolved; + } + $str .= substr($data, $pos); + $str = str_replace('#', '$', $str); // reset marker + + return $str; + } + + private function handleConditions(string $data, array &$relSpells, bool $topLevel = false) : string + { + while (($condStartPos = strpos($data, '$?')) !== false) + { + $condBrktCnt = 0; + $condCurPos = $condStartPos + 2; // after the '$?' + $targetPart = 3; // we usually want the second pair of brackets + $curPart = 0; // parts: $? 0 [ 1 ] 2 [ 3 ] 4 ... + $condParts = []; + $isLastElse = false; + + while ($condCurPos < strlen($data)) // only hard-exit condition, we'll hit those breaks eventually^^ + { + $char = $data[$condCurPos]; + + // advance position + $condCurPos++; + + if ($char == '[') + { + $condBrktCnt++; + + if ($condBrktCnt == 1) + $curPart++; + + // previously there was no condition -> last else + if ($condBrktCnt == 1) + if (($curPart && ($curPart % 2)) && (!isset($condParts[$curPart - 1]) || empty(trim($condParts[$curPart - 1])))) + $isLastElse = true; + + if (empty($condParts[$curPart])) + continue; + } + + if (empty($condParts[$curPart])) + $condParts[$curPart] = $char; + else + $condParts[$curPart] .= $char; + + if ($char == ']') + { + $condBrktCnt--; + + if (!$condBrktCnt) + { + $condParts[$curPart] = substr($condParts[$curPart], 0, -1); + $curPart++; + } + + if ($condBrktCnt) + continue; + + if ($isLastElse) + break; + } + } + + // this should be 0 if all went well + if ($condBrktCnt > 0) + { + trigger_error('SpellList::handleConditions() - string contains unbalanced condition', E_USER_WARNING); + $condParts[3] = $condParts[3] ?? ''; + } + + // check if it is know-compatible + $know = 0; + if (preg_match('/\(?(\!?)[as](\d+)\)?$/i', $condParts[0], $m)) + { + if (!strstr($condParts[1], '$?')) + if (!strstr($condParts[3], '$?')) + if (!isset($condParts[5])) + $know = $m[2]; + + // found a negation -> switching condition target + if ($m[1] == '!') + $targetPart = 1; + } + // if not, what part of the condition should be used? + else if (preg_match('/(([\W\D]*[as]\d+)|([^\[]*))/i', $condParts[0], $m) && !empty($m[3])) + { + $cnd = $this->resolveEvaluation($m[3]); + if ((is_numeric($cnd) || is_bool($cnd)) && $cnd) // only case, deviating from normal; positive result -> use [true] + $targetPart = 1; + } + + // recursive conditions + if (strstr($condParts[$targetPart], '$?')) + $condParts[$targetPart] = $this->handleConditions($condParts[$targetPart], $relSpells); + + if ($know && $topLevel) + { + foreach ([1, 3] as $pos) + { + if (strstr($condParts[$pos], '${')) + $condParts[$pos] = $this->handleFormulas($condParts[$pos]); + + if (strstr($condParts[$pos], '$')) + $condParts[$pos] = $this->handleVariables($condParts[$pos]); + } + + // false condition first + if (!isset($relSpells[$know])) + $relSpells[$know] = []; + + $relSpells[$know][] = [$condParts[3], $condParts[1]]; + + $data = substr_replace($data, ''.$condParts[$targetPart].'', $condStartPos, ($condCurPos - $condStartPos)); + } + else + $data = substr_replace($data, $condParts[$targetPart], $condStartPos, ($condCurPos - $condStartPos)); + } + + return $data; + } + + public function renderBuff(int $level = MAX_LEVEL, int $interactive = self::INTERACTIVE_EMBEDDED, ?array &$buffSpells = []) : ?string + { + $buffSpells = []; + + if (!$this->curTpl) + return null; + + // doesn't have a buff + if (!$this->getField('buff', true)) + return null; + + $this->interactive = $interactive; + $this->charLevel = $level; + + $x = ''; + + // spellName + $x .= ''; + + // dispelType (if applicable) + if ($this->curTpl['dispelType']) + if ($dispel = Lang::game('dt', $this->curTpl['dispelType'])) + $x .= ''; + + $x .= '
'.$this->getField('name', true).''.$dispel.'
'; + + $x .= '
'; + + // parse Buff-Text + [$buffTT, $buffSp, ] = $this->parseText('buff'); + + $buffSpells = Util::parseHtmlText($buffSp); + + $x .= $buffTT.'
'; + + // duration + if ($this->curTpl['duration'] > 0 && !($this->curTpl['attributes5'] & SPELL_ATTR5_HIDE_DURATION)) + $x .= ''.Lang::formatTime($this->curTpl['duration'], 'spell', 'timeRemaining').''; + + $x .= '
'; + + $min = $this->scaling[$this->id] ? ($this->getField('baseLevel') ?: 1) : 1; + $max = $this->scaling[$this->id] ? MAX_LEVEL : 1; + // scaling information - spellId:min:max:curr + $x .= ''; + + return $x; + } + + public function renderTooltip(int $level = MAX_LEVEL, int $interactive = self::INTERACTIVE_EMBEDDED, ?array &$ttSpells = []) : ?string + { + $ttSpells = []; + + if (!$this->curTpl) + return null; + + $this->interactive = $interactive; + $this->charLevel = $level; + + // fetch needed texts + $name = $this->getField('name', true); + $rank = $this->getField('rank', true); + $tools = $this->getToolsForCurrent(); + $cool = $this->createCooldownForCurrent(); + $cast = $this->createCastTimeForCurrent(); + $cost = $this->createPowerCostForCurrent(); + $range = $this->createRangesForCurrent(); + + [$desc, $spells, ] = $this->parseText('description'); + + $ttSpells = Util::parseHtmlText($spells); + + // get reagents + $reagents = $this->getReagentsForCurrent(); + 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); + + // get stances + $stances = ''; + if ($this->curTpl['stanceMask'] && !($this->curTpl['attributes2'] & SPELL_ATTR2_NOT_NEED_SHAPESHIFT)) + $stances = Lang::game('requires2').' '.Lang::getStances($this->curTpl['stanceMask']); + + // get item requirement (skip for professions) + $reqItems = ''; + if ($this->curTpl['typeCat'] != 11) + { + $class = $this->getField('equippedItemClass'); + $mask = $this->getField('equippedItemSubClassMask'); + $reqItems = Lang::getRequiredItems($class, $mask); + } + + // get created items (may need improvement) + $createItem = ''; + if (in_array($this->curTpl['typeCat'], [9, 11])) // only Professions + { + foreach ($this->canCreateItem() as $idx) + { + // has createItem Scroll of Enchantment + if ($this->curTpl['effect'.$idx.'Id'] == SPELL_EFFECT_ENCHANT_ITEM) + continue; + + foreach ($this->relItems->iterate() as $cId => $__) + { + if ($cId != $this->curTpl['effect'.$idx.'CreateItemId']) + continue; + + $createItem = $this->relItems->renderTooltip(true, $this->id); + break 2; + } + } + } + + $x = ''; + $x .= '
'; + + // name & rank + if ($rank) + $x .= '
'.$name.''.$rank.'
'; + else + $x .= ''.$name.'
'; + + // powerCost & ranges + if ($range && $cost) + $x .= '
'.$cost.''.$range.'
'; + else if ($cost || $range) + $x .= $range.$cost.'
'; + + // castTime & cooldown + if ($cast && $cool) // tabled layout + { + $x .= ''; + $x .= ''; + if ($stances) + $x.= ''; + + $x .= '
'.$cast.''.$cool.'
'.$stances.'
'; + } + else if ($cast || $cool) // line-break layout + { + $x .= $cast.$cool; + + if ($stances) + $x .= '
'.$stances; + } + + $x .= '
'; + + $xTmp = []; + + if ($tools) + { + $_ = Lang::spell('tools').':
'; + while ($tool = array_pop($tools)) + { + if (isset($tool['itemId'])) + $_ .= ''.$tool['name'].''; + else if (isset($tool['id'])) + $_ .= ''.$tool['name'].''; + else + $_ .= $tool['name']; + + if (!empty($tools)) + $_ .= ', '; + else + $_ .= '
'; + } + + $xTmp[] = $_.'
'; + } + + if ($reagents) + { + $_ = Lang::spell('reagents').':
'; + while ([$iId, $qty, $text, $exists] = array_pop($reagents)) + { + $_ .= $exists ? ''.$text.'' : $text; + if ($qty > 1) + $_ .= ' ('.$qty.')'; + + $_ .= empty($reagents) ? '
' : ', '; + } + + $xTmp[] = $_.'
'; + } + + if ($reqItems) + $xTmp[] = Lang::game('requires2').' '.$reqItems; + + if ($desc) + $xTmp[] = ''.$desc.''; + + if ($createItem) + $xTmp[] = $createItem; + + if ($xTmp) + $x .= '
'.implode('
', $xTmp).'
'; + + // 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 .= ''; + + return $x; + } + + public function getTalentHeadForCurrent() : string + { + // power cost: pct over static + $cost = $this->createPowerCostForCurrent(); + + // ranges + $range = $this->createRangesForCurrent(); + + // cast times + $cast = $this->createCastTimeForCurrent(); + + // cooldown or categorycooldown + $cool = $this->createCooldownForCurrent(); + + // assemble parts + // upper: cost :: range + // lower: time :: cooldown + $x = ''; + + // upper + if ($cost && $range) + $x .= '
'.$cost.''.$range.'
'; + else + $x .= $cost.$range; + + if (($cost xor $range) && ($cast xor $cool)) + $x .= '
'; + + // lower + if ($cast && $cool) + $x .= '
'.$cast.''.$cool.'
'; + else + $x .= $cast.$cool; + + return $x; + } + + public function getColorsForCurrent() : array + { + $gry = $this->curTpl['skillLevelGrey']; + $ylw = $this->curTpl['skillLevelYellow']; + $grn = (int)(($ylw + $gry) / 2); + $org = $this->curTpl['learnedAt']; + + if ($ylw < $org) + $ylw = 0; + + if ($grn < $org || $grn < $ylw) + $grn = 0; + + if ($org >= $ylw || $org >= $grn || $org >= $gry) + $org = 0; + + return $gry > 1 ? [$org, $ylw, $grn, $gry] : []; + } + + public function getListviewData(int $addInfoMask = 0x0) : array + { + $data = []; + + if ($addInfoMask & ITEMINFO_MODEL) + $modelInfo = $this->getModelInfo(); + + foreach ($this->iterate() as $__) + { + $quality = ($this->curTpl['cuFlags'] & SPELL_CU_QUALITY_MASK) >> 8; + $talent = $this->curTpl['cuFlags'] & (SPELL_CU_TALENT | SPELL_CU_TALENTSPELL) && $this->curTpl['spellLevel'] <= 1; + + $data[$this->id] = array( + 'id' => $this->id, + 'name' => ($quality ?: '@').$this->getField('name', true), + 'icon' => $this->curTpl['iconStringAlt'] ?: $this->curTpl['iconString'], + 'level' => $talent ? $this->curTpl['talentLevel'] : $this->curTpl['spellLevel'], + 'school' => $this->curTpl['schoolMask'], + 'cat' => $this->curTpl['typeCat'], + 'trainingcost' => $this->curTpl['trainingCost'], + 'skill' => count($this->curTpl['skillLines']) > 4 ? array_merge(array_splice($this->curTpl['skillLines'], 0, 4), [-1]): $this->curTpl['skillLines'], // display max 4 skillLines (fills max three lines in listview) + 'reagents' => array_values($this->getReagentsForCurrent()), + 'source' => [] + // 'talentspec' => $this->curTpl['skillLines'][0] not used: g_chr_specs has the wrong structure for it; also setting .cat and .type does the same + ); + + // Sources + if ($this->getSources($s, $sm)) + { + $data[$this->id]['source'] = $s; + if ($sm) + $data[$this->id]['sourcemore'] = $sm; + } + + // Proficiencies + if ($this->curTpl['typeCat'] == -11) + foreach (self::$spellTypes as $cat => $type) + if (in_array($this->curTpl['skillLines'][0], self::$skillLines[$cat])) + $data[$this->id]['type'] = $type; + + // creates item + foreach ($this->canCreateItem() as $idx) + { + $max = $this->curTpl['effect'.$idx.'DieSides'] + $this->curTpl['effect'.$idx.'BasePoints']; + $min = $this->curTpl['effect'.$idx.'DieSides'] > 1 ? 1 : $max; + + $data[$this->id]['creates'] = [$this->curTpl['effect'.$idx.'CreateItemId'], $min, $max]; + break; + } + + // Profession + if (in_array($this->curTpl['typeCat'], [9, 11])) + { + if ($la = $this->curTpl['learnedAt']) + $data[$this->id]['learnedat'] = $la; + else if (($la = $this->curTpl['reqSkillLevel']) > 1) + $data[$this->id]['learnedat'] = $la; + + $data[$this->id]['colors'] = $this->getColorsForCurrent(); + } + + // glyph + if ($this->curTpl['typeCat'] == -13) + $data[$this->id]['glyphtype'] = $this->curTpl['cuFlags'] & SPELL_CU_GLYPH_MAJOR ? 1 : 2; + + if ($r = $this->getField('rank', true)) + $data[$this->id]['rank'] = $r; + + if ($mask = $this->curTpl['reqClassMask']) + $data[$this->id]['reqclass'] = $mask; + + if ($mask = $this->curTpl['reqRaceMask']) + $data[$this->id]['reqrace'] = $mask; + + if ($addInfoMask & ITEMINFO_MODEL) + { + // may have multiple models set, in this case i've no idea what should be picked + for ($i = 1; $i < 4; $i++) + { + if (!empty($modelInfo[$this->id][$i])) + { + $data[$this->id]['npcId'] = $modelInfo[$this->id][$i]['typeId']; + $data[$this->id]['displayId'] = $modelInfo[$this->id][$i]['displayId']; + $data[$this->id]['displayName'] = $modelInfo[$this->id][$i]['displayName']; + break; + } + } + } + } + + return $data; + } + + public function getJSGlobals(int $addMask = GLOBALINFO_SELF, ?array &$extra = []) : array + { + $data = []; + + if ($this->relItems && ($addMask & GLOBALINFO_RELATED)) + $data = $this->relItems->getJSGlobals(); + + foreach ($this->iterate() as $id => $__) + { + if ($addMask & GLOBALINFO_RELATED) + { + foreach (ChrClass::fromMask($this->curTpl['reqClassMask']) as $cId) + $data[Type::CHR_CLASS][$cId] = $cId; + + foreach (ChrRace::fromMask($this->curTpl['reqRaceMask']) as $rId) + $data[Type::CHR_RACE][$rId] = $rId; + + // play sound effect + for ($i = 1; $i < 4; $i++) + if ($this->getField('effect'.$i.'Id') == SPELL_EFFECT_PLAY_SOUND || $this->getField('effect'.$i.'Id') == SPELL_EFFECT_PLAY_MUSIC) + $data[Type::SOUND][$this->getField('effect'.$i.'MiscValue')] = $this->getField('effect'.$i.'MiscValue'); + } + + if ($addMask & GLOBALINFO_SELF) + { + $data[Type::SPELL][$id] = array( + 'icon' => $this->curTpl['iconStringAlt'] ?: $this->curTpl['iconString'], + '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) + { + $buff = $this->renderBuff(MAX_LEVEL, true, $buffSpells); + $tTip = $this->renderTooltip(MAX_LEVEL, true, $spells); + + foreach ($spells as $relId => $_) + if (empty($data[Type::SPELL][$relId])) + $data[Type::SPELL][$relId] = $relId; + + foreach ($buffSpells as $relId => $_) + if (empty($data[Type::SPELL][$relId])) + $data[Type::SPELL][$relId] = $relId; + + $extra[$id] = array( + // 'id' => $id, + 'tooltip' => $tTip, + 'buff' => $buff ?: null, + 'spells' => $spells, + 'buffspells' => $buffSpells ?: null + ); + } + } + + return $data; + } + + // mostly similar to TC + public function getCastingTimeForBonus(bool $asDOT = false) : int + { + $areaTargets = [7, 8, 15, 16, 20, 24, 30, 31, 33, 34, 37, 54, 56, 59, 104, 108]; + $castingTime = $this->IsChanneledSpell() ? $this->curTpl['duration'] : ($this->curTpl['castTime'] * 1000); + + if (!$castingTime) + return 3500; + + if ($castingTime > 7000) + $castingTime = 7000; + + if ($castingTime < 1500) + $castingTime = 1500; + + if ($asDOT && !$this->isChanneledSpell()) + $castingTime = 3500; + + $overTime = 0; + $nEffects = 0; + $isDirect = false; + $isArea = false; + + for ($i = 1; $i <= 3; $i++) + { + if (in_array($this->curTpl['effect'.$i.'Id'], SpellLIst::EFFECTS_DIRECT_SCALING)) + $isDirect = true; + else if (in_array($this->curTpl['effect'.$i.'AuraId'], SpellList::AURAS_PERIODIC_SCALING)) + if ($_ = $this->curTpl['duration']) + $overTime = $_; + else if ($this->curTpl['effect'.$i.'AuraId']) + $nEffects++; + + if (in_array($this->curTpl['effect'.$i.'ImplicitTargetA'], $areaTargets) || in_array($this->curTpl['effect'.$i.'ImplicitTargetB'], $areaTargets)) + $isArea = true; + } + + // Combined Spells with Both Over Time and Direct Damage + if ($overTime > 0 && $castingTime > 0 && $isDirect) + { + // mainly for DoTs which are 3500 here otherwise + $originalCastTime = $this->curTpl['castTime'] * 1000; + if ($this->curTpl['attributes0'] & SPELL_ATTR0_REQ_AMMO) + $originalCastTime += 500; + + if ($originalCastTime > 7000) + $originalCastTime = 7000; + + if ($originalCastTime < 1500) + $originalCastTime = 1500; + + // Portion to Over Time + $PtOT = ($overTime / 15000) / (($overTime / 15000) + ($originalCastTime / 3500)); + + if ($asDOT) + $castingTime = $castingTime * $PtOT; + else if ($PtOT < 1) + $castingTime = $castingTime * (1 - $PtOT); + else + $castingTime = 0; + } + + // Area Effect Spells receive only half of bonus + if ($isArea) + $castingTime /= 2; + + // -5% of total per any additional effect + $castingTime -= ($nEffects * 175); + if ($castingTime < 0) + $castingTime = 0; + + return $castingTime; + } + + public function getSourceData(int $id = 0) : array + { + $data = []; + + foreach ($this->iterate() as $__) + { + if ($id && $id != $this->id) + continue; + + $data[$this->id] = array( + 'n' => $this->getField('name', true), + 't' => Type::SPELL, + 'ti' => $this->id, + 's' => empty($this->curTpl['skillLines']) ? 0 : $this->curTpl['skillLines'][0], + 'c' => $this->curTpl['typeCat'], + 'icon' => $this->curTpl['iconStringAlt'] ?: $this->curTpl['iconString'], + ); + } + + return $data; + } +} + + +class SpellListFilter extends Filter +{ + const MAX_SPELL_EFFECT = 167; + const MAX_SPELL_AURA = 316; + + public static array $attributesFilter = array( // attrFieldId => [attrBit => cr, ...]; if cr < 0 ? filter is negated + 0 => array( + SPELL_ATTR0_REQ_AMMO => 48, + SPELL_ATTR0_ON_NEXT_SWING => 49, + SPELL_ATTR0_PASSIVE => 50, + SPELL_ATTR0_HIDDEN_CLIENTSIDE => 51, + SPELL_ATTR0_HIDE_IN_COMBAT_LOG => 84, + SPELL_ATTR0_ON_NEXT_SWING_2 => 52, + SPELL_ATTR0_DAYTIME_ONLY => 53, + SPELL_ATTR0_NIGHT_ONLY => 54, + SPELL_ATTR0_INDOORS_ONLY => 55, + SPELL_ATTR0_OUTDOORS_ONLY => 56, + SPELL_ATTR0_NOT_SHAPESHIFT => -31, + SPELL_ATTR0_ONLY_STEALTHED => 38, + SPELL_ATTR0_LEVEL_DAMAGE_CALCULATION => 58, + SPELL_ATTR0_STOP_ATTACK_TARGET => 59, + SPELL_ATTR0_IMPOSSIBLE_DODGE_PARRY_BLOCK => 60, + SPELL_ATTR0_CASTABLE_WHILE_DEAD => 61, + SPELL_ATTR0_CASTABLE_WHILE_MOUNTED => 62, + SPELL_ATTR0_DISABLED_WHILE_ACTIVE => 63, + SPELL_ATTR0_NEGATIVE_1 => 69, + SPELL_ATTR0_CASTABLE_WHILE_SITTING => 64, + SPELL_ATTR0_CANT_USED_IN_COMBAT => -33, + SPELL_ATTR0_UNAFFECTED_BY_INVULNERABILITY => 46, + SPELL_ATTR0_CANT_CANCEL => 57 + ), + 1 => array( + SPELL_ATTR1_DRAIN_ALL_POWER => 65, + SPELL_ATTR1_CHANNELED_1 => 27, // general filter + SPELL_ATTR1_NOT_BREAK_STEALTH => 68, + SPELL_ATTR1_CHANNELED_2 => 66, // attributes filter + SPELL_ATTR1_CANT_BE_REFLECTED => 67, // WH - 69: all effects are harmful points here + SPELL_ATTR1_CANT_TARGET_IN_COMBAT => 70, + SPELL_ATTR1_NO_THREAT => 71, + SPELL_ATTR1_IS_PICKPOCKET => 72, + SPELL_ATTR1_DISPEL_AURAS_ON_IMMUNITY => 73, + SPELL_ATTR1_UNAFFECTED_BY_SCHOOL_IMMUNE => 47, + SPELL_ATTR1_IS_FISHING => 74 + ), + 2 => array( + SPELL_ATTR2_CANT_TARGET_TAPPED => 75, + SPELL_ATTR2_PRESERVE_ENCHANT_IN_ARENA => 76, + SPELL_ATTR2_NOT_NEED_SHAPESHIFT => 77, + SPELL_ATTR2_CANT_CRIT => -34, + SPELL_ATTR2_FOOD_BUFF => 78 + ), + 3 => array( + SPELL_ATTR3_ONLY_TARGET_PLAYERS => 79, + SPELL_ATTR3_MAIN_HAND => 80, + SPELL_ATTR3_BATTLEGROUND => 43, + SPELL_ATTR3_NO_INITIAL_AGGRO => 81, + SPELL_ATTR3_DEATH_PERSISTENT => 36, + SPELL_ATTR3_IGNORE_HIT_RESULT => -35, + SPELL_ATTR3_REQ_WAND => 82, // unused attribute + SPELL_ATTR3_REQ_OFFHAND => 83 + ), + 4 => array( + SPELL_ATTR4_FADES_WHILE_LOGGED_OUT => 85, + SPELL_ATTR4_NOT_STEALABLE => -39, + SPELL_ATTR4_NOT_USABLE_IN_ARENA => -44, + SPELL_ATTR4_USABLE_IN_ARENA => 44 + ), + 5 => array( + SPELL_ATTR5_USABLE_WHILE_STUNNED => 42, + SPELL_ATTR5_SINGLE_TARGET_SPELL => 86, + SPELL_ATTR5_START_PERIODIC_AT_APPLY => 87, + SPELL_ATTR5_USABLE_WHILE_FEARED => 89, + SPELL_ATTR5_USABLE_WHILE_CONFUSED => 88 + ), + 6 => array( + SPELL_ATTR6_ONLY_IN_ARENA => 90, // unused attribute + SPELL_ATTR6_NOT_IN_RAID_INSTANCE => 91 + ), + 7 => array( + SPELL_ATTR7_DISABLE_AURA_WHILE_DEAD => 92, // aka Paladin Aura + SPELL_ATTR7_SUMMON_PLAYER_TOTEM => 93 + ) + ); + + protected string $type = 'spells'; + protected static array $enums = array( + 9 => array( // sources index + 1 => true, // Any + 2 => false, // None + 3 => SRC_CRAFTED, + 4 => SRC_DROP, + 6 => SRC_QUEST, + 7 => SRC_VENDOR, + 8 => SRC_TRAINER, + 9 => SRC_DISCOVERY, + 10 => SRC_TALENT + ), + 22 => array( + 1 => true, // Weapons + 2 => true, // Armor + 3 => true, // Armor Proficiencies + 4 => true, // Armor Specializations + 5 => true // Languages + ), + 40 => array( // damage class index + 1 => 0, // none + 2 => 1, // magic + 3 => 2, // melee + 4 => 3 // ranged + ), + 45 => array( // power type index + // 1 => ??, // burning embers + // 2 => ??, // chi + // 3 => ??, // demonic fury + 4 => POWER_ENERGY, // energy + 5 => POWER_FOCUS, // focus + 6 => POWER_HEALTH, // health + // 7 => ??, // holy power + 8 => POWER_MANA, // mana + 9 => POWER_RAGE, // rage + 10 => POWER_RUNE, // runes + 11 => POWER_RUNIC_POWER, // runic power + // 12 => ??, // shadow orbs + // 13 => ??, // soul shard + 14 => POWER_HAPPINESS, // happiness v custom v + 15 => -1, // ammo + 16 => -41, // pyrite + 17 => -61, // steam pressure + 18 => -101, // heat + 19 => -121, // ooze + 20 => -141, // blood power + 21 => -142 // wrath + ) + ); + + protected static array $genericFilter = array( + 1 => [parent::CR_CALLBACK, 'cbCost', ], // costAbs [op] [int] + 2 => [parent::CR_NUMERIC, 'powerCostPercent', NUM_CAST_INT ], // prcntbasemanarequired + 3 => [parent::CR_BOOLEAN, 'spellFocusObject' ], // requiresnearbyobject + 4 => [parent::CR_NUMERIC, 'trainingcost', NUM_CAST_INT ], // trainingcost + 5 => [parent::CR_BOOLEAN, 'reqSpellId' ], // requiresprofspec + 8 => [parent::CR_FLAG, 'cuFlags', CUSTOM_HAS_SCREENSHOT ], // hasscreenshots + 9 => [parent::CR_CALLBACK, 'cbSource', ], // source [enum] + 10 => [parent::CR_FLAG, 'cuFlags', SPELL_CU_FIRST_RANK ], // firstrank + 11 => [parent::CR_FLAG, 'cuFlags', CUSTOM_HAS_COMMENT ], // hascomments + 12 => [parent::CR_FLAG, 'cuFlags', SPELL_CU_LAST_RANK ], // lastrank + 13 => [parent::CR_NUMERIC, 'rankNo', NUM_CAST_INT ], // rankno + 14 => [parent::CR_NUMERIC, 'id', NUM_CAST_INT, true ], // id + 15 => [parent::CR_STRING, 'ic.name', ], // icon + 17 => [parent::CR_FLAG, 'cuFlags', CUSTOM_HAS_VIDEO ], // hasvideos + 19 => [parent::CR_FLAG, 'attributes0', SPELL_ATTR0_LEVEL_DAMAGE_CALCULATION ], // scaling + 20 => [parent::CR_CALLBACK, 'cbReagents', ], // has Reagents [yn] + 22 => [parent::CR_CALLBACK, 'cbProficiency', null, null ], // proficiencytype [proficiencytype] + // 26 => [parent::CR_NUMERIC, 'startRecoveryCategory', NUM_CAST_INT, false ], // gcd-cat + 25 => [parent::CR_BOOLEAN, 'skillLevelYellow' ], // rewardsskillups + 27 => [parent::CR_FLAG, 'attributes1', SPELL_ATTR1_CHANNELED_1, true ], // channeled [yn] + 28 => [parent::CR_NUMERIC, 'castTime', NUM_CAST_FLOAT ], // casttime [num] + 29 => [parent::CR_CALLBACK, 'cbAuraNames', ], // appliesaura [effectauranames] + 31 => [parent::CR_FLAG, 'attributes0', SPELL_ATTR0_NOT_SHAPESHIFT ], // usablewhenshapeshifted [yn] + 33 => [parent::CR_CALLBACK, 'cbInverseFlag', 'attributes0', SPELL_ATTR0_CANT_USED_IN_COMBAT], // combatcastable [yn] + 34 => [parent::CR_CALLBACK, 'cbInverseFlag', 'attributes2', SPELL_ATTR2_CANT_CRIT ], // chancetocrit [yn] + 35 => [parent::CR_CALLBACK, 'cbInverseFlag', 'attributes3', SPELL_ATTR3_IGNORE_HIT_RESULT ], // chancetomiss [yn] + 36 => [parent::CR_FLAG, 'attributes3', SPELL_ATTR3_DEATH_PERSISTENT ], // persiststhroughdeath [yn] + 38 => [parent::CR_FLAG, 'attributes0', SPELL_ATTR0_ONLY_STEALTHED ], // requiresstealth [yn] + 39 => [parent::CR_FLAG, 'attributes4', SPELL_ATTR4_NOT_STEALABLE ], // spellstealable [yn] + 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] + 47 => [parent::CR_FLAG, 'attributes1', SPELL_ATTR1_UNAFFECTED_BY_SCHOOL_IMMUNE ], // disregardschoolimmunity [yn] + 48 => [parent::CR_FLAG, 'attributes0', SPELL_ATTR0_REQ_AMMO ], // reqrangedweapon [yn] + 49 => [parent::CR_FLAG, 'attributes0', SPELL_ATTR0_ON_NEXT_SWING ], // onnextswingplayers [yn] + 50 => [parent::CR_FLAG, 'attributes0', SPELL_ATTR0_PASSIVE ], // passivespell [yn] + 51 => [parent::CR_FLAG, 'attributes1', SPELL_ATTR1_DONT_DISPLAY_IN_AURA_BAR ], // hiddenaura [yn] + 52 => [parent::CR_FLAG, 'attributes0', SPELL_ATTR0_ON_NEXT_SWING_2 ], // onnextswingnpcs [yn] + 53 => [parent::CR_FLAG, 'attributes0', SPELL_ATTR0_DAYTIME_ONLY ], // daytimeonly [yn] + 54 => [parent::CR_FLAG, 'attributes0', SPELL_ATTR0_NIGHT_ONLY ], // nighttimeonly [yn] + 55 => [parent::CR_FLAG, 'attributes0', SPELL_ATTR0_INDOORS_ONLY ], // indoorsonly [yn] + 56 => [parent::CR_FLAG, 'attributes0', SPELL_ATTR0_OUTDOORS_ONLY ], // outdoorsonly [yn] + 57 => [parent::CR_FLAG, 'attributes0', SPELL_ATTR0_CANT_CANCEL ], // uncancellableaura [yn] + 58 => [parent::CR_FLAG, 'attributes0', SPELL_ATTR0_LEVEL_DAMAGE_CALCULATION ], // damagedependsonlevel [yn] + 59 => [parent::CR_FLAG, 'attributes0', SPELL_ATTR0_STOP_ATTACK_TARGET ], // stopsautoattack [yn] + 60 => [parent::CR_FLAG, 'attributes0', SPELL_ATTR0_IMPOSSIBLE_DODGE_PARRY_BLOCK ], // cannotavoid [yn] + 61 => [parent::CR_FLAG, 'attributes0', SPELL_ATTR0_CASTABLE_WHILE_DEAD ], // usabledead [yn] + 62 => [parent::CR_FLAG, 'attributes0', SPELL_ATTR0_CASTABLE_WHILE_MOUNTED ], // usablemounted [yn] + 63 => [parent::CR_FLAG, 'attributes0', SPELL_ATTR0_DISABLED_WHILE_ACTIVE ], // delayedrecoverystarttime [yn] + 64 => [parent::CR_FLAG, 'attributes0', SPELL_ATTR0_CASTABLE_WHILE_SITTING ], // usablesitting [yn] + 65 => [parent::CR_FLAG, 'attributes1', SPELL_ATTR1_DRAIN_ALL_POWER ], // usesallpower [yn] + 66 => [parent::CR_FLAG, 'attributes1', SPELL_ATTR1_CHANNELED_2 ], // channeled [yn] + 67 => [parent::CR_FLAG, 'attributes1', SPELL_ATTR1_CANT_BE_REFLECTED ], // cannotreflect [yn] + 68 => [parent::CR_FLAG, 'attributes1', SPELL_ATTR1_NOT_BREAK_STEALTH ], // usablestealthed [yn] + 69 => [parent::CR_FLAG, 'attributes0', SPELL_ATTR0_NEGATIVE_1 ], // harmful [yn] - WH interprets attributes1 0x80 as "all effects are harmful", but it really is CANT_BE_REFLECTED. So here is an approximation. + 70 => [parent::CR_FLAG, 'attributes1', SPELL_ATTR1_CANT_TARGET_IN_COMBAT ], // targetnotincombat [yn] + 71 => [parent::CR_FLAG, 'attributes1', SPELL_ATTR1_NO_THREAT ], // nothreat [yn] + 72 => [parent::CR_FLAG, 'attributes1', SPELL_ATTR1_IS_PICKPOCKET ], // pickpocket [yn] + 73 => [parent::CR_FLAG, 'attributes1', SPELL_ATTR1_DISPEL_AURAS_ON_IMMUNITY ], // dispelauraonimmunity [yn] + 74 => [parent::CR_FLAG, 'attributes1', SPELL_ATTR1_IS_FISHING ], // reqfishingpole [yn] + 75 => [parent::CR_FLAG, 'attributes2', SPELL_ATTR2_CANT_TARGET_TAPPED ], // requntappedtarget [yn] + 76 => [parent::CR_FLAG, 'attributes2', SPELL_ATTR2_PRESERVE_ENCHANT_IN_ARENA ], // targetownitem [yn + 77 => [parent::CR_FLAG, 'attributes2', SPELL_ATTR2_NOT_NEED_SHAPESHIFT ], // doesntreqshapeshift [yn] + 78 => [parent::CR_FLAG, 'attributes2', SPELL_ATTR2_FOOD_BUFF ], // foodbuff [yn] + 79 => [parent::CR_FLAG, 'attributes3', SPELL_ATTR3_ONLY_TARGET_PLAYERS ], // targetonlyplayer [yn] + 80 => [parent::CR_FLAG, 'attributes3', SPELL_ATTR3_MAIN_HAND ], // reqmainhand [yn] + 81 => [parent::CR_FLAG, 'attributes3', SPELL_ATTR3_NO_INITIAL_AGGRO ], // doesntengagetarget [yn] + 82 => [parent::CR_FLAG, 'attributes3', SPELL_ATTR3_REQ_WAND ], // reqwand [yn] + 83 => [parent::CR_FLAG, 'attributes3', SPELL_ATTR3_REQ_OFFHAND ], // reqoffhand [yn] + 84 => [parent::CR_FLAG, 'attributes0', SPELL_ATTR0_HIDE_IN_COMBAT_LOG ], // nolog [yn] + 85 => [parent::CR_FLAG, 'attributes4', SPELL_ATTR4_FADES_WHILE_LOGGED_OUT ], // auratickswhileloggedout [yn] + 86 => [parent::CR_FLAG, 'attributes5', SPELL_ATTR5_SINGLE_TARGET_SPELL ], // onlyaffectsonetarget [yn] + 87 => [parent::CR_FLAG, 'attributes5', SPELL_ATTR5_START_PERIODIC_AT_APPLY ], // startstickingatapplication [yn] + 88 => [parent::CR_FLAG, 'attributes5', SPELL_ATTR5_USABLE_WHILE_CONFUSED ], // usableconfused [yn] + 89 => [parent::CR_FLAG, 'attributes5', SPELL_ATTR5_USABLE_WHILE_FEARED ], // usablefeared [yn] + 90 => [parent::CR_FLAG, 'attributes6', SPELL_ATTR6_ONLY_IN_ARENA ], // onlyarena [yn] + 91 => [parent::CR_FLAG, 'attributes6', SPELL_ATTR6_NOT_IN_RAID_INSTANCE ], // notinraid [yn] + 92 => [parent::CR_FLAG, 'attributes7', SPELL_ATTR7_DISABLE_AURA_WHILE_DEAD ], // paladinaura [yn] + 93 => [parent::CR_FLAG, 'attributes7', SPELL_ATTR7_SUMMON_PLAYER_TOTEM ], // totemspell [yn] + 95 => [parent::CR_CALLBACK, 'cbBandageSpell' ], // bandagespell [yn] - was that an attribute at one point? + 96 => [parent::CR_STAFFFLAG, 'attributes0' ], // flags1 [flags] + 97 => [parent::CR_STAFFFLAG, 'attributes1' ], // flags2 [flags] + 98 => [parent::CR_STAFFFLAG, 'attributes2' ], // flags3 [flags] + 99 => [parent::CR_STAFFFLAG, 'attributes3' ], // flags4 [flags] + 100 => [parent::CR_STAFFFLAG, 'attributes4' ], // flags5 [flags] + 101 => [parent::CR_STAFFFLAG, 'attributes5' ], // flags6 [flags] + 102 => [parent::CR_STAFFFLAG, 'attributes6' ], // flags7 [flags] + 103 => [parent::CR_STAFFFLAG, 'attributes7' ], // flags8 [flags] + 104 => [parent::CR_STAFFFLAG, 'targets' ], // flags9 [flags] + 105 => [parent::CR_STAFFFLAG, 'stanceMaskNot' ], // flags10 [flags] + 106 => [parent::CR_STAFFFLAG, 'spellFamilyFlags1' ], // flags11 [flags] + 107 => [parent::CR_STAFFFLAG, 'spellFamilyFlags2' ], // flags12 [flags] + 108 => [parent::CR_STAFFFLAG, 'spellFamilyFlags3' ], // flags13 [flags] + 109 => [parent::CR_CALLBACK, 'cbEffectNames', ], // effecttype [effecttype] + // 110 => [parent::CR_NYI_PH, null, null, null ], // scalingap [yn] // unreasonably complex for now + // 111 => [parent::CR_NYI_PH, null, null, null ], // scalingsp [yn] // unreasonably complex for now + 114 => [parent::CR_CALLBACK, 'cbReqFaction' ], // requiresfaction [side] + 116 => [parent::CR_BOOLEAN, 'startRecoveryTime' ] // onGlobalCooldown [yn] + ); + + protected static array $inputFields = array( + '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_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, [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 + 'sc' => [parent::V_RANGE, [0, 6], true ], // magic schools + 'dt' => [parent::V_LIST, [[1, 6], 9], false], // dispel types + 'me' => [parent::V_RANGE, [1, 31], false] // mechanics + ); + + protected function createSQLForValues() : array + { + $parts = []; + $_v = &$this->values; + + // string (extended) + if ($_v['na']) + { + $f = [['na', ['nml.nName', 'nml.nBuff', 'nml.nDescription']]]; + if ($_v['ex'] != 'on') + $f = [['na', 'nml.nName']]; + + 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 + if ($_v['minle']) + $parts[] = ['spellLevel', $_v['minle'], '>=']; + + // spellLevel max + if ($_v['maxle']) + $parts[] = ['spellLevel', $_v['maxle'], '<=']; + + // skillLevel min + if ($_v['minrs']) + $parts[] = ['learnedAt', $_v['minrs'], '>=']; + + // skillLevel max + if ($_v['maxrs']) + $parts[] = ['learnedAt', $_v['maxrs'], '<=']; + + // race + if ($_v['ra']) + $parts[] = [DB::AND, [['reqRaceMask', ChrRace::MASK_ALL, '&'], ChrRace::MASK_ALL, '!'], ['reqRaceMask', $this->list2Mask([$_v['ra']]), '&']]; + + // class [list] + if ($_v['cl']) + $parts[] = ['reqClassMask', $this->list2Mask($_v['cl']), '&']; + + // school [list] + if ($_v['sc']) + $parts[] = ['schoolMask', $this->list2Mask($_v['sc'], true), '&']; + + // glyph type [list] wonky, admittedly, but consult SPELL_CU_* in defines and it makes sense + if ($_v['gl']) + $parts[] = ['cuFlags', ($this->list2Mask($_v['gl']) << 6), '&']; + + // dispel type + if ($_v['dt']) + $parts[] = ['dispelType', $_v['dt']]; + + // mechanic + if ($_v['me']) + $parts[] = [DB::OR, ['mechanic', $_v['me']], ['effect1Mechanic', $_v['me']], ['effect2Mechanic', $_v['me']], ['effect3Mechanic', $_v['me']]]; + + return $parts; + } + + protected function cbClasses(string &$val) : bool + { + if (!$this->parentCats || !in_array($this->parentCats[0], [-13, -2, 7])) + return false; + + if (!Util::checkNumeric($val, NUM_CAST_INT)) + return false; + + $type = parent::V_LIST; + $valid = ChrClass::fromMask(ChrClass::MASK_ALL); + + return $this->checkInput($type, $valid, $val); + } + + protected function cbGlyphs(string &$val) : bool + { + if (!$this->parentCats || $this->parentCats[0] != -13) + return false; + + if (!Util::checkNumeric($val, NUM_CAST_INT)) + return false; + + $type = parent::V_LIST; + $valid = [1, 2]; + + return $this->checkInput($type, $valid, $val); + } + + protected function cbCost(int $cr, int $crs, string $crv) : ?array + { + if (!Util::checkNumeric($crv, NUM_CAST_INT) || !$this->int2Op($crs)) + return null; + + 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]] + ]; + } + + protected function cbSource(int $cr, int $crs, string $crv) : ?array + { + if (!isset(self::$enums[$cr][$crs])) + return null; + + $_ = self::$enums[$cr][$crs]; + if (is_int($_)) // specific + return ['src.src'.$_, null, '!']; + else if ($_) // any + { + $foo = [DB::OR]; + foreach (self::$enums[$cr] as $bar) + if (is_int($bar)) + $foo[] = ['src.src'.$bar, null, '!']; + + return $foo; + } + else // none + return ['src.typeId', null]; + + return null; + } + + protected function cbReagents(int $cr, int $crs, string $crv) : ?array + { + if (!$this->int2Bool($crs)) + return null; + + if ($crs) + return [DB::OR, ['reagent1', 0, '>'], ['reagent2', 0, '>'], ['reagent3', 0, '>'], ['reagent4', 0, '>'], ['reagent5', 0, '>'], ['reagent6', 0, '>'], ['reagent7', 0, '>'], ['reagent8', 0, '>']]; + else + 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 + { + if (!$this->checkInput(parent::V_RANGE, [1, self::MAX_SPELL_AURA], $crs)) + return null; + + return [DB::OR, ['effect1AuraId', $crs], ['effect2AuraId', $crs], ['effect3AuraId', $crs]]; + } + + protected function cbEffectNames(int $cr, int $crs, string $crv) : ?array + { + if (!$this->checkInput(parent::V_RANGE, [1, self::MAX_SPELL_EFFECT], $crs)) + return null; + + return [DB::OR, ['effect1Id', $crs], ['effect2Id', $crs], ['effect3Id', $crs]]; + } + + protected function cbInverseFlag(int $cr, int $crs, string $crv, string $field, int $flag) : ?array + { + if (!$this->int2Bool($crs)) + return null; + + if ($crs) + return [[$field, $flag, '&'], 0]; + else + return [$field, $flag, '&']; + } + + protected function cbSpellstealable(int $cr, int $crs, string $crv, string $field, int $flag) : ?array + { + if (!$this->int2Bool($crs)) + return null; + + if ($crs) + return [DB::AND, [[$field, $flag, '&'], 0], ['dispelType', SPELL_DAMAGE_CLASS_MAGIC]]; + else + return [DB::OR, [$field, $flag, '&'], ['dispelType', SPELL_DAMAGE_CLASS_MAGIC, '!']]; + } + + protected function cbReqFaction(int $cr, int $crs, string $crv) : ?array + { + return match ($crs) + { + // yes + 1 => ['reqRaceMask', 0, '!'], + // alliance + 2 => [DB::AND, [['reqRaceMask', ChrRace::MASK_HORDE, '&'], 0], ['reqRaceMask', ChrRace::MASK_ALLIANCE, '&']], + // horde + 3 => [DB::AND, [['reqRaceMask', ChrRace::MASK_ALLIANCE, '&'], 0], ['reqRaceMask', ChrRace::MASK_HORDE, '&']], + // both + 4 => [DB::AND, ['reqRaceMask', ChrRace::MASK_ALLIANCE, '&'], ['reqRaceMask', ChrRace::MASK_HORDE, '&']], + // no + 5 => ['reqRaceMask', 0], + default => null + }; + } + + /* unused - for reference: attribute flag or item class mask */ + protected function cbEquippedWeapon(int $cr, int $crs, string $crv, int $mask, bool $useInvType) : ?array + { + if (!$this->int2Bool($crs)) + return null; + + $field = $useInvType ? 'equippedItemInventoryTypeMask' : 'equippedItemSubClassMask'; + + if ($crs) + return [DB::AND, ['equippedItemClass', ITEM_CLASS_WEAPON], [$field, $mask, '&']]; + else + return [DB::OR, ['equippedItemClass', ITEM_CLASS_WEAPON, '!'], [[$field, $mask, '&'], 0]]; + } + + /* unused - for reference: attribute flag or cooldown time constraint */ + protected function cbUsableInArena(int $cr, int $crs, string $crv) : ?array + { + if (!$this->int2Bool($crs)) + return null; + + if ($crs) + return [DB::AND, + [['attributes4', SPELL_ATTR4_NOT_USABLE_IN_ARENA, '&'], 0], + [DB::OR, ['recoveryTime', 10 * MINUTE * 1000, '<='], ['attributes4', SPELL_ATTR4_USABLE_IN_ARENA, '&']] + ]; + else + return [DB::OR, + ['attributes4', SPELL_ATTR4_NOT_USABLE_IN_ARENA, '&'], + [DB::AND, ['recoveryTime', 10 * MINUTE * 1000, '>'], [['attributes4', SPELL_ATTR4_USABLE_IN_ARENA, '&'], 0]] + ]; + } + + protected function cbBandageSpell(int $cr, int $crs, string $crv) : ?array + { + if (!$this->int2Bool($crs)) + return null; + + if ($crs) // match exact, not as flag + return [DB::AND, ['attributes1', SPELL_ATTR1_CHANNELED_1 | SPELL_ATTR1_CHANNELED_2 | SPELL_ATTR1_CHANNEL_TRACK_TARGET], ['effect1ImplicitTargetA', 21]]; + else + 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 + { + if (!isset(self::$enums[$cr][$crs])) + return null; + + $skill1Ids = []; + $skill2Mask = 0x0; + + switch($crs) + { + case 1: // Weapons + foreach (Game::$skillLineMask[-3] as $bit => $_) + $skill2Mask |= (1 << $bit); + $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'); + 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'); + break; + } + + if (!$skill1Ids) + return [0]; + + $cnd = ['skillLine1', $skill1Ids]; + if ($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 new file mode 100644 index 00000000..b583a290 --- /dev/null +++ b/includes/dbtypes/title.class.php @@ -0,0 +1,180 @@ + [['src']], // 11: Type::TITLE + 'src' => ['j' => ['::source src ON `type` = 11 AND `typeId` = t.`id`', true], 's' => ', `src13`, `moreType`, `moreTypeId`'] + ); + + public function __construct(array $conditions = [], array $miscData = []) + { + parent::__construct($conditions, $miscData); + + // post processing + foreach ($this->iterate() as &$_curTpl) + { + // preparse sources - notice: under this system titles can't have more than one source (or two for achivements), which is enough for standard TC cases but may break custom cases + if ($_curTpl['moreType'] == Type::ACHIEVEMENT) + $this->sources[$this->id][SRC_ACHIEVEMENT][] = $_curTpl['moreTypeId']; + else if ($_curTpl['moreType'] == Type::QUEST) + $this->sources[$this->id][SRC_QUEST][] = $_curTpl['moreTypeId']; + else if ($_curTpl['src13']) + $this->sources[$this->id][SRC_CUSTOM_STRING][] = $_curTpl['src13']; + + // titles display up to two achievements at once + if ($_curTpl['src12Ext']) + $this->sources[$this->id][SRC_ACHIEVEMENT][] = $_curTpl['src12Ext']; + + unset($_curTpl['src12Ext']); + unset($_curTpl['moreType']); + unset($_curTpl['moreTypeId']); + unset($_curTpl['src3']); + + // shorthand for more generic access; required by CommunityContent to determine subject + foreach (Locale::cases() as $loc) + if ($loc->validate()) + $_curTpl['name'] = new LocString($_curTpl, 'male', fn($x) => trim(str_replace('%s', '', $x))); + // $_curTpl['name_loc'.$loc->value] = trim(str_replace('%s', '', $_curTpl['male_loc'.$loc->value])); + } + } + + 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 %n WHERE `id` = %i', self::$dataTable, $id)) + return new LocString($n, 'male', fn($x) => trim(str_replace('%s', '', $x))); + return null; + } + + public function getListviewData() : array + { + $data = []; + $this->createSource(); + + foreach ($this->iterate() as $__) + { + $data[$this->id] = array( + 'id' => $this->id, + 'name' => $this->getField('male', true), + 'namefemale' => $this->getField('female', true), + 'side' => $this->curTpl['side'], + 'gender' => $this->curTpl['gender'], + 'expansion' => $this->curTpl['expansion'], + 'category' => $this->curTpl['category'] + ); + + if (!empty($this->curTpl['source'])) + $data[$this->id]['source'] = $this->curTpl['source']; + } + + return $data; + } + + public function getJSGlobals(int $addMask = 0) : array + { + $data = []; + + foreach ($this->iterate() as $__) + { + $data[Type::TITLE][$this->id]['name'] = $this->getField('male', true); + + if ($_ = $this->getField('female', true)) + $data[Type::TITLE][$this->id]['namefemale'] = $_; + } + + return $data; + } + + private function createSource() : void + { + $sources = array( + SRC_QUEST => [], + SRC_ACHIEVEMENT => [], + SRC_CUSTOM_STRING => [] + ); + + foreach ($this->iterate() as $__) + { + if (empty($this->sources[$this->id])) + continue; + + foreach (array_keys($sources) as $srcKey) + if (isset($this->sources[$this->id][$srcKey])) + $sources[$srcKey] = array_merge($sources[$srcKey], $this->sources[$this->id][$srcKey]); + } + + // fill in the details + if (!empty($sources[SRC_QUEST])) + $sources[SRC_QUEST] = (new QuestList(array(['id', $sources[SRC_QUEST]])))->getSourceData(); + + if (!empty($sources[SRC_ACHIEVEMENT])) + $sources[SRC_ACHIEVEMENT] = (new AchievementList(array(['id', $sources[SRC_ACHIEVEMENT]])))->getSourceData(); + + foreach ($this->sources as $Id => $src) + { + $tmp = []; + + // Quest-source + if (isset($src[SRC_QUEST])) + { + foreach ($src[SRC_QUEST] as $s) + { + if (isset($sources[SRC_QUEST][$s]['s'])) + $this->faction2Side($sources[SRC_QUEST][$s]['s']); + + $tmp[SRC_QUEST][] = $sources[SRC_QUEST][$s]; + } + } + + // Achievement-source + if (isset($src[SRC_ACHIEVEMENT])) + { + foreach ($src[SRC_ACHIEVEMENT] as $s) + { + if (isset($sources[SRC_ACHIEVEMENT][$s]['s'])) + $this->faction2Side($sources[SRC_ACHIEVEMENT][$s]['s']); + + $tmp[SRC_ACHIEVEMENT][] = $sources[SRC_ACHIEVEMENT][$s]; + } + } + + // other source (only one item possible, so no iteration needed) + if (isset($src[SRC_CUSTOM_STRING])) + $tmp[SRC_CUSTOM_STRING] = [Lang::game('pvpSources', $Id)]; + + $this->templates[$Id]['source'] = $tmp; + } + } + + public function getHtmlizedName(int $gender = GENDER_MALE) : string + { + $field = $gender == GENDER_FEMALE ? 'female' : 'male'; + return str_replace('%s', '<'.Util::ucFirst(Lang::main('name')).'>', $this->getField($field, true)); + } + + public function renderTooltip() : ?string { return null; } + + private function faction2Side(int &$faction) : void // thats weird.. and hopefully unique to titles + { + if ($faction == 2) // Horde + $faction = 0; + else if ($faction != 1) // Alliance + $faction = -1; // Both + } +} + +?> diff --git a/includes/dbtypes/user.class.php b/includes/dbtypes/user.class.php new file mode 100644 index 00000000..18122f6b --- /dev/null +++ b/includes/dbtypes/user.class.php @@ -0,0 +1,89 @@ + [['r']], + '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 + { + $data = []; + + foreach ($this->iterate() as $userId => $__) + { + $data[$this->curTpl['username']] = array( + 'border' => $this->getPremiumborder(), + 'roles' => $this->curTpl['userGroups'], + 'joined' => date(Util::$dateFormatInternal, $this->curTpl['joinDate']), + 'posts' => 0, // forum posts + // 'gold' => 0, // achievement system + // 'silver' => 0, // achievement system + // 'copper' => 0, // achievement system + 'reputation' => $this->curTpl['reputation'] + ); + + // custom titles (only seen on user page..?) + if ($_ = $this->curTpl['title']) + $data[$this->curTpl['username']]['title'] = $_; + + switch ($this->curTpl['avatar']) + { + case 1: + $data[$this->curTpl['username']]['avatar'] = $this->curTpl['avatar']; + $data[$this->curTpl['username']]['avatarmore'] = $this->curTpl['wowicon']; + break; + case 2: + if ($this->isPremium()) + { + 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; + } + } + break; + } + + // more optional data + // sig: markdown formated string (only used in forum?) + } + + return [Type::USER => $data]; + } + + // seen as null|1|3 .. changes the border around the avatar (chosen from account > premium tab?) + // changed at the end of MoP. No longer a jsBool but index to Icon.premiumBorderClasses + private function getPremiumBorder() : int + { + if (!$this->isPremium() || !$this->curTpl['avatar']) + return 2; // 2 is "none" + + return $this->curTpl['avatarborder']; + } + + public function isPremium() : bool + { + return $this->curTpl['userGroups'] & U_GROUP_PREMIUM || $this->curTpl['reputation'] >= Cfg::get('REP_REQ_PREMIUM'); + } + + public function getListviewData() : array { return []; } + public function renderTooltip() : ?string { return null; } + + public static function getName($id) : ?LocString { return null; } +} + +?> diff --git a/includes/dbtypes/worldevent.class.php b/includes/dbtypes/worldevent.class.php new file mode 100644 index 00000000..399d6083 --- /dev/null +++ b/includes/dbtypes/worldevent.class.php @@ -0,0 +1,176 @@ + [['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 = []) + { + parent::__construct($conditions, $miscData); + + // unseting elements while we iterate over the array will cause the pointer to reset + $replace = []; + + // post processing + foreach ($this->iterate() as $__) + { + // emulate category + $sT = $this->curTpl['scheduleType']; + if (!$this->curTpl['holidayId']) + $this->curTpl['category'] = 0; + else if ($sT == 2) + $this->curTpl['category'] = 3; + else if (in_array($sT, [0, 1])) + $this->curTpl['category'] = 2; + else if ($sT == -1) + $this->curTpl['category'] = 1; + + // preparse requisites + if ($this->curTpl['requires']) + $this->curTpl['requires'] = explode(' ', $this->curTpl['requires']); + + // change Ids if holiday is set + if ($this->curTpl['holidayId'] > 0) + { + $this->curTpl['name'] = $this->getField('name', true); + $replace[$this->id] = $this->curTpl; + } + else // set a name if holiday is missing + { + // template + $this->curTpl['name_loc0'] = $this->curTpl['nameINT']; + $this->curTpl['iconString'] = 'trade_engineering'; + $this->curTpl['name'] = '(SERVERSIDE) '.$this->getField('nameINT', true); + $replace[$this->id] = $this->curTpl; + } + } + + foreach ($replace as $old => $data) + { + unset($this->templates[$old]); + $this->templates[$data['eventId']] = $data; + } + } + + public static function getName(int $id) : ?LocString + { + $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` = %i', + $id + ); + + return $row ? new LocString($row) : null; + } + + public static function updateDates(?array $date = null, ?int &$start = null, ?int &$end = null, ?int &$rec = null) : bool + { + if (!$date || empty($date['firstDate']) || empty($date['length'])) + return false; + + $start = $date['firstDate']; + $end = $date['firstDate'] + $date['length']; + $rec = $date['rec'] ?: -1; // interval + + if ($rec < 0 || $date['lastDate'] < time()) + return true; + + $nIntervals = (int)ceil((time() - $end) / $rec); + + $start += $nIntervals * $rec; + $end += $nIntervals * $rec; + + return true; + } + + public static function updateListview(Listview &$listview) : void + { + foreach ($listview->iterate() as &$row) + { + WorldEventList::updateDates($row['_date'] ?? null, $start, $end, $rec); + + $row['startDate'] = $start ? date(Util::$dateFormatInternal, $start) : null; + $row['endDate'] = $end ? date(Util::$dateFormatInternal, $end - 1) : null; + $row['rec'] = $rec; + + unset($row['_date']); + } + } + + public function getListviewData() : array + { + $data = []; + + foreach ($this->iterate() as $__) + { + $data[$this->id] = array( + 'category' => $this->curTpl['category'], + 'id' => $this->id, + 'name' => $this->getField('name', true), + '_date' => array( + 'rec' => $this->curTpl['occurence'], + 'length' => $this->curTpl['length'], + 'firstDate' => $this->curTpl['startTime'], + 'lastDate' => $this->curTpl['endTime'] + ) + ); + } + + return $data; + } + + public function getJSGlobals(int $addMask = 0) : array + { + $data = []; + + foreach ($this->iterate() as $__) + $data[Type::WORLDEVENT][$this->id] = ['name' => $this->getField('name', true), 'icon' => $this->curTpl['iconString']]; + + return $data; + } + + public function renderTooltip() : ?string + { + if (!$this->curTpl) + return null; + + $x = '
'; + + // head v that extra % is nesecary because we are using sprintf later on + $x .= '
'.$this->getField('name', true).''.Lang::event('category', $this->getField('category')).'
'; + + // use string-placeholder for dates + // start + $x .= Lang::event('start').'%s
'; + // end + $x .= Lang::event('end').'%s'; + + $x .= '
'; + + // desc + if ($this->getField('holidayId')) + if ($_ = $this->getField('description', true)) + $x .= '
'.$_.'
'; + + return $x; + } +} + +?> diff --git a/includes/types/zone.class.php b/includes/dbtypes/zone.class.php similarity index 76% rename from includes/types/zone.class.php rename to includes/dbtypes/zone.class.php index e1e97d98..668eac6e 100644 --- a/includes/types/zone.class.php +++ b/includes/dbtypes/zone.class.php @@ -1,20 +1,22 @@ selectRow('SELECT name_loc0, name_loc2, name_loc3, name_loc6, name_loc8 FROM ?_zones WHERE id = ?d', $id ); - return Util::localizedString($n, 'name'); - } - - public function getListviewData() + public function getListviewData() : array { $data = []; @@ -95,17 +90,17 @@ class ZoneList extends BaseType return $data; } - public function getJSGlobals($addMask = 0) + public function getJSGlobals(int $addMask = 0) : array { $data = []; foreach ($this->iterate() as $__) - $data[TYPE_ZONE][$this->id] = ['name' => $this->getField('name', true)]; + $data[Type::ZONE][$this->id] = ['name' => $this->getField('name', true)]; return $data; } - public function renderTooltip() { } + public function renderTooltip() : ?string { return null; } } ?> diff --git a/includes/defines.php b/includes/defines.php index 4f92bbdc..4075085a 100644 --- a/includes/defines.php +++ b/includes/defines.php @@ -1,5 +1,7 @@ 10min +define('SPELL_ATTR4_AREA_TARGET_CHAIN', 0x00040000); // Chain area targets DESCRIPTION [NYI] Hits area targets over time instead of all at once +define('SPELL_ATTR4_ALLOW_PROC_WHILE_SITTING', 0x00080000); // [WoWDev Wiki] +define('SPELL_ATTR4_NOT_CHECK_SELFCAST_POWER', 0x00100000); // Allow self-cast to override stronger aura (client only) - NOTE! modern name AURA_NEVER_BOUNCES (similar meaning) +define('SPELL_ATTR4_DONT_REMOVE_IN_ARENA', 0x00200000); // Keep when entering arena +define('SPELL_ATTR4_PROC_SUPPRESS_SWING_ANIM', 0x00400000); // [WoWDev Wiki] Disables client side weapon swing animation. +define('SPELL_ATTR4_CANT_TRIGGER_ITEM_SPELLS', 0x00800000); // Cannot trigger item spells +define('SPELL_ATTR4_AUTO_RANGED_COMBAT', 0x01000000); // [WoWDev Wiki] +define('SPELL_ATTR4_IS_PET_SCALING', 0x02000000); // Pet Scaling aura +define('SPELL_ATTR4_CAST_ONLY_IN_OUTLAND', 0x04000000); // Only in Outland/Northrend - NOTE! modern client name is ONLY_FLYING_AREAS (similar, more correct), WH is "Allow Equip While Casting", (wtf, seriously) +define('SPELL_ATTR4_FORCE_DISPLAY_CASTBAR', 0x08000000); // +define('SPELL_ATTR4_IGNORE_COMBAT_TIMER', 0x10000000); // [WoWDev Wiki] +define('SPELL_ATTR4_AURA_BOUNCE_FAILS_SPELL', 0x20000000); // [WoWDev Wiki] +define('SPELL_ATTR4_OBSOLETE', 0x40000000); // [WoWDev Wiki] Deprecates the spell making it greyed out and gives "You can't use that here" error. Still usable with the triggered flag command though. +define('SPELL_ATTR4_USE_FACING_FROM_SPELL', 0x80000000); // [WoWDev Wiki] Affects orientation. The value used is likely related to FacingCasterFlags in Spell.dbc for 3.3.5. + +define('SPELL_ATTR5_CAN_CHANNEL_WHEN_MOVING', 0x00000001); // Can be channeled while moving +define('SPELL_ATTR5_NO_REAGENT_WHILE_PREP', 0x00000002); // No reagents during arena preparation +define('SPELL_ATTR5_REMOVE_ON_ARENA_ENTER', 0x00000004); // Remove when entering arena DESCRIPTION Force this aura to be removed on entering arena, regardless of other properties +define('SPELL_ATTR5_USABLE_WHILE_STUNNED', 0x00000008); // Usable while stunned +define('SPELL_ATTR5_TRIGGERS_CHANNELING', 0x00000010); // [WoWDev Wiki] Likely more script oriented. +define('SPELL_ATTR5_SINGLE_TARGET_SPELL', 0x00000020); // Single-target aura DESCRIPTION Remove previous application to another unit if applied +define('SPELL_ATTR5_IGNORE_AREA_EFFECT_PVP_CHECK', 0x00000040); // [WoWDev Wiki] Possible world PvP flag for objectives such as Spirit Towers? +define('SPELL_ATTR5_NOT_ON_PLAYER', 0x00000080); // [WoWDev Wiki] Opposite of SPELL_ATTR3_ONLY_TARGET_PLAYERS +define('SPELL_ATTR5_CANT_TARGET_PLAYER_CONTROLLED', 0x00000100); // Cannot target player controlled units but can target players +define('SPELL_ATTR5_START_PERIODIC_AT_APPLY', 0x00000200); // Immediately do periodic tick on apply +define('SPELL_ATTR5_HIDE_DURATION', 0x00000400); // Do not send aura duration to client +define('SPELL_ATTR5_ALLOW_TARGET_OF_TARGET_AS_TARGET', 0x00000800); // Auto-target target of target (client only) +define('SPELL_ATTR5_MELEE_CHAIN_TARGETING', 0x00001000); // [WoWDev Wiki] Cleave related? +define('SPELL_ATTR5_HASTE_AFFECT_DURATION', 0x00002000); // Duration scales with Haste Rating +define('SPELL_ATTR5_NOT_USABLE_WHILE_CHARMED', 0x00004000); // Charmed units cannot cast this spell +define('SPELL_ATTR5_TREAT_AS_AREA_EFFECT', 0x00008000); // [WoWDev Wiki] Related to multi-target spells? +define('SPELL_ATTR5_AURA_AFFECTS_NOT_JUST_REQ_EQUIPPED_ITEM', 0x00010000); // [WoWDev Wiki] +define('SPELL_ATTR5_USABLE_WHILE_FEARED', 0x00020000); // Usable while feared +define('SPELL_ATTR5_USABLE_WHILE_CONFUSED', 0x00040000); // Usable while confused +define('SPELL_ATTR5_DONT_TURN_DURING_CAST', 0x00080000); // Do not auto-turn while casting +define('SPELL_ATTR5_DO_NOT_ATTEMPT_A_PET_RESUMMON_WHEN_DISMOUNTING', 0x00100000); // [WoWDev Wiki] +define('SPELL_ATTR5_IGNORE_TARGET_REQUIREMENTS', 0x00200000); // [WoWDev Wiki] +define('SPELL_ATTR5_NOT_ON_TRIVIAL', 0x00400000); // [WoWDev Wiki] +define('SPELL_ATTR5_NO_PARTIAL_RESISTS', 0x00800000); // [WoWDev Wiki] Spell will either be fully resisted or deal the full amount of damage. +define('SPELL_ATTR5_IGNORE_CASTER_REQUIREMENTS', 0x01000000); // [WoWDev Wiki] +define('SPELL_ATTR5_ALWAYS_LINE_OF_SIGHT', 0x02000000); // [WoWDev Wiki] Constant line of sight required for spell duration. +define('SPELL_ATTR5_SKIP_CHECKCAST_LOS_CHECK', 0x04000000); // Ignore line of sight checks +define('SPELL_ATTR5_DONT_SHOW_AURA_IF_SELF_CAST', 0x08000000); // Don't show aura if self-cast (client only) +define('SPELL_ATTR5_DONT_SHOW_AURA_IF_NOT_SELF_CAST', 0x10000000); // Don't show aura unless self-cast (client only) +define('SPELL_ATTR5_AURA_UNIQUE_PER_CASTER', 0x20000000); // [WoWDev Wiki] Could be used for debuff grouping. +define('SPELL_ATTR5_ALWAYS_SHOW_GROUND_TEXTURE', 0x40000000); // [WoWDev Wiki] Likely refers to the Projected Texture setting and will cause this spell to ignore its value. +define('SPELL_ATTR5_ADD_MELEE_HIT_RATING', 0x80000000); // [WoWDev Wiki] (Forces nearby enemies to attack caster?) + +define('SPELL_ATTR6_DONT_DISPLAY_COOLDOWN', 0x00000001); // Don't display cooldown (client only) +define('SPELL_ATTR6_ONLY_IN_ARENA', 0x00000002); // Only usable in arena +define('SPELL_ATTR6_IGNORE_CASTER_AURAS', 0x00000004); // Ignore all preventing caster auras - NOTE! leak Data and WH name this NOT_AN_ATTACK +define('SPELL_ATTR6_ASSIST_IGNORE_IMMUNE_FLAG', 0x00000008); // Ignore immunity flags when assisting +define('SPELL_ATTR6_IGNORE_FOR_MOD_TIME_RATE', 0x00000010); // [WoWDev Wiki] +define('SPELL_ATTR6_DONT_CONSUME_PROC_CHARGES', 0x00000020); // Don't consume proc charges +define('SPELL_ATTR6_USE_SPELL_CAST_EVENT', 0x00000040); // Generate spell_cast event instead of aura_start (client only) - NOTE! FLOATING_COMBAT_TEXT_ON_CAST in modern client, but visual UI procs are not in 335 +define('SPELL_ATTR6_AURA_IS_WEAPON_PROC', 0x00000080); // [WoWDev Wiki] +define('SPELL_ATTR6_CANT_TARGET_CROWD_CONTROLLED', 0x00000100); // Do not implicitly target in CC DESCRIPTION Implicit targeting (chaining and area targeting) will not impact crowd controlled targets +define('SPELL_ATTR6_ALLOW_ON_CHARMED_TARGETS', 0x00000200); // [WoWDev Wiki] +define('SPELL_ATTR6_CAN_TARGET_POSSESSED_FRIENDS', 0x00000400); // Can target possessed friends DESCRIPTION [NYI] - NOTE! leak data and WH name this NO_AURA_LOG and it really prevents aura apply/remove messages in combat log +define('SPELL_ATTR6_NOT_IN_RAID_INSTANCE', 0x00000800); // Unusable in raid instances +define('SPELL_ATTR6_CASTABLE_WHILE_ON_VEHICLE', 0x00001000); // Castable while caster is on vehicle +define('SPELL_ATTR6_CAN_TARGET_INVISIBLE', 0x00002000); // Can target invisible units +define('SPELL_ATTR6_AI_PRIMARY_RANGED_ATTACK', 0x00004000); // [WoWDev Wiki] Related to Shoot? Needs description. +define('SPELL_ATTR6_NO_PUSHBACK', 0x00008000); // [WoWDev Wiki] +define('SPELL_ATTR6_NO_JUMP_PATHING', 0x00010000); // [WoWDev Wiki] +define('SPELL_ATTR6_ALLOW_EQUIP_WHILE_CASTING', 0x00020000); // [WoWDev Wiki] Mount related? +define('SPELL_ATTR6_CAST_BY_CHARMER', 0x00040000); // Spell is cast by charmer DESCRIPTION Client will prevent casting if not possessed, charmer will be caster for all intents and purposes +define('SPELL_ATTR6_DELAY_COMBAT_TIMER_DURING_CAST', 0x00080000); // [WoWDev Wiki] +define('SPELL_ATTR6_ONLY_VISIBLE_TO_CASTER', 0x00100000); // Only visible to caster (client only) +define('SPELL_ATTR6_CLIENT_UI_TARGET_EFFECTS', 0x00200000); // Client UI target effects (client only) - NOTE! SHOW_MECHANIC_AS_COMBAT_TEXT in modern client .. neither descriptor seems to be true +define('SPELL_ATTR6_ABSORB_CANNOT_BE_IGNORE', 0x00400000); // [WoWDev Wiki] +define('SPELL_ATTR6_TAPS_IMMEDIATELY', 0x00800000); // [WoWDev Wiki] +define('SPELL_ATTR6_CAN_TARGET_UNTARGETABLE', 0x01000000); // Can target untargetable units +define('SPELL_ATTR6_NOT_RESET_SWING_IF_INSTANT', 0x02000000); // Do not reset swing timer if cast time is instant +define('SPELL_ATTR6_VEHICLE_IMMUNITY_CATEGORY', 0x04000000); // [WoWDev Wiki] immunity to some buffs for some vehicles. +define('SPELL_ATTR6_LIMIT_PCT_HEALING_MODS', 0x08000000); // Limit applicable %healing modifiers DESCRIPTION This prevents certain healing modifiers from applying - see implementation if you really care about details +define('SPELL_ATTR6_DO_NOT_AUTO_SELECT_TARGET_WITH_INITIATES_COMBAT', 0x10000000); // [WoWDev Wiki] Death grip? +define('SPELL_ATTR6_LIMIT_PCT_DAMAGE_MODS', 0x20000000); // Limit applicable %damage modifiers DESCRIPTION This prevents certain damage modifiers from applying - see implementation if you really care about details +define('SPELL_ATTR6_DISABLE_TIED_EFFECT_POINTS', 0x40000000); // [WoWDev Wiki] The value used is likely from the SpellEffect column EffectBasePoints +define('SPELL_ATTR6_IGNORE_CATEGORY_COOLDOWN_MODS', 0x80000000); // Ignore cooldown modifiers for category cooldown + +define('SPELL_ATTR7_ALLOW_SPELL_REFLECTION', 0x00000001); // [WoWDev Wiki] Allow spell to be reflected. Will likely interfere if used with SPELL_ATTR1_CANT_BE_REFLECTED. +define('SPELL_ATTR7_IGNORE_DURATION_MODS', 0x00000002); // Ignore duration modifiers +define('SPELL_ATTR7_DISABLE_AURA_WHILE_DEAD', 0x00000004); // Reactivate at resurrect (client only) +define('SPELL_ATTR7_IS_CHEAT_SPELL', 0x00000008); // Is cheat spell DESCRIPTION Cannot cast if caster doesn't have UnitFlag2 & UNIT_FLAG2_ALLOW_CHEAT_SPELLS +define('SPELL_ATTR7_TREAT_AS_RAID_BUFF', 0x00000010); // [WoWDev Wiki] Spell assumes certain properties that would classify it as a "raid buff". (This is only a guess.) +define('SPELL_ATTR7_SUMMON_PLAYER_TOTEM', 0x00000020); // Summons player-owned totem +define('SPELL_ATTR7_NO_PUSHBACK_ON_DAMAGE', 0x00000040); // Damage dealt by this does not cause spell pushback +define('SPELL_ATTR7_PREPARE_FOR_VEHICLE_CONTROL_END', 0x00000080); // [WoWDev Wiki] Attribute is most likely server side only. +define('SPELL_ATTR7_HORDE_ONLY', 0x00000100); // Horde only +define('SPELL_ATTR7_ALLIANCE_ONLY', 0x00000200); // Alliance only +define('SPELL_ATTR7_DISPEL_CHARGES', 0x00000400); // Dispel/Spellsteal remove individual charges +define('SPELL_ATTR7_INTERRUPT_ONLY_NONPLAYER', 0x00000800); // Only interrupt non-player casting +define('SPELL_ATTR7_CAN_CAUSE_SILENCE', 0x00001000); // [WoWDev Wiki] Will only Silence NPCs/creatures. (Not confirmed.) +define('SPELL_ATTR7_NO_UI_NOT_INTERRUPTIBLE', 0x00002000); // [WoWDev Wiki] Can always be interrupted, even if caster is immune. +define('SPELL_ATTR7_RECAST_ON_RESUMMON', 0x00004000); // [WoWDev Wiki] only on 52150 Raise Dead. +define('SPELL_ATTR7_RESET_SWING_TIMER_AT_SPELL_START', 0x00008000); // [WoWDev Wiki] (Exorcism - guaranteed crit vs families?) +define('SPELL_ATTR7_CAN_RESTORE_SECONDARY_POWER', 0x00010000); // Can restore secondary power DESCRIPTION Only spells with this attribute can replenish a non-active power type - NOTE! replaed with ONLY_IN_SPELLBOOK_UNTIL_LEARNED in modern client +define('SPELL_ATTR7_DO_NOT_LOG_PVP_KILL', 0x00020000); // [WoWDev Wiki] +define('SPELL_ATTR7_HAS_CHARGE_EFFECT', 0x00040000); // Has charge effect +define('SPELL_ATTR7_ZONE_TELEPORT', 0x00080000); // Is zone teleport - NOTE! REPORT_SPELL_FAILURE_TO_UNIT_TARGET in modern client, but may still serve the same purpose as teleport spell ofter use custom error messages +define('SPELL_ATTR7_NO_CLIENT_FAIL_WHILE_STUNNED_FLEEING_CONFUSED', 0x00100000); // [WoWDev Wiki] Client will skip or bypass checking for stunned, fleeing, and confused states. +define('SPELL_ATTR7_RETAIN_COOLDOWN_THROUGH_LOAD', 0x00200000); // [WoWDev Wiki] +define('SPELL_ATTR7_IGNORE_COLD_WEATHER_FLYING', 0x00400000); // Ignore cold weather flying restriction DESCRIPTION Set for loaner mounts, allows them to be used despite lacking required flight skill +define('SPELL_ATTR7_CANT_DODGE', 0x00800000); // Spell cannot be dodged +define('SPELL_ATTR7_CANT_PARRY', 0x01000000); // Spell cannot be parried +define('SPELL_ATTR7_CANT_MISS', 0x02000000); // Spell cannot be missed +define('SPELL_ATTR7_TREAT_AS_NPC_AOE', 0x04000000); // [WoWDev Wiki] +define('SPELL_ATTR7_BYPASS_NO_RESURRECT_AURA', 0x08000000); // Bypasses the prevent resurrection aura +define('SPELL_ATTR7_CONSOLIDATED_RAID_BUFF', 0x10000000); // Consolidate in raid buff frame (client only) +define('SPELL_ATTR7_REFLECTION_ONLY_DEFENDS', 0x20000000); // [WoWDev Wiki] This possibly allows for a spell to be reflected but not damage the target and instead act more as a deflect. +define('SPELL_ATTR7_CAN_PROC_FROM_SUPPRESSED_TARGET_PROCS', 0x40000000); // [WoWDev Wiki] +define('SPELL_ATTR7_CLIENT_INDICATOR', 0x80000000); // Client indicator (client only) + + +// (some) Skill ids +define('SKILL_FIRST_AID', 129); +define('SKILL_BLACKSMITHING', 164); +define('SKILL_LEATHERWORKING', 165); +define('SKILL_ALCHEMY', 171); +define('SKILL_HERBALISM', 182); +define('SKILL_COOKING', 185); +define('SKILL_MINING', 186); +define('SKILL_TAILORING', 197); +define('SKILL_ENGINEERING', 202); +define('SKILL_ENCHANTING', 333); +define('SKILL_FISHING', 356); +define('SKILL_SKINNING', 393); +define('SKILL_LOCKPICKING', 633); +define('SKILL_JEWELCRAFTING', 755); +define('SKILL_RIDING', 762); +define('SKILL_INSCRIPTION', 773); +define('SKILL_MOUNTS', 777); +define('SKILL_COMPANIONS', 778); + +define('SKILLS_TRADE_PRIMARY', [SKILL_BLACKSMITHING, SKILL_LEATHERWORKING, SKILL_ALCHEMY, SKILL_HERBALISM, SKILL_MINING, SKILL_TAILORING, SKILL_ENGINEERING, SKILL_ENCHANTING, SKILL_SKINNING, SKILL_JEWELCRAFTING, SKILL_INSCRIPTION]); +define('SKILLS_TRADE_SECONDARY', [SKILL_FIRST_AID, SKILL_COOKING, SKILL_FISHING, SKILL_RIDING]); + +// (some) key currencies +define('CURRENCY_ARENA_POINTS', 103); +define('CURRENCY_HONOR_POINTS', 104); // AchievementCriteriaCondition define('ACHIEVEMENT_CRITERIA_CONDITION_NO_DEATH', 1); // reset progress on death @@ -707,11 +1890,13 @@ define('ACHIEVEMENT_CRITERIA_CONDITION_NOT_IN_GROUP', 10); define('ACHIEVEMENT_FLAG_COUNTER', 0x0001); // Just count statistic (never stop and complete) define('ACHIEVEMENT_FLAG_HIDDEN', 0x0002); // Not sent to client - internal use only define('ACHIEVEMENT_FLAG_STORE_MAX_VALUE', 0x0004); // Store only max value? used only in "Reach level xx" -define('ACHIEVEMENT_FLAG_SUMM', 0x0008); // Use summ criteria value from all reqirements (and calculate max value) +define('ACHIEVEMENT_FLAG_SUM', 0x0008); // Use sum criteria value from all reqirements (and calculate max value) define('ACHIEVEMENT_FLAG_MAX_USED', 0x0010); // Show max criteria (and calculate max value ??) define('ACHIEVEMENT_FLAG_REQ_COUNT', 0x0020); // Use not zero req count (and calculate max value) define('ACHIEVEMENT_FLAG_AVERAGE', 0x0040); // Show as average value (value / time_in_days) depend from other flag (by def use last criteria value) -define('ACHIEVEMENT_FLAG_BAR', 0x0080); // Show as progress bar (value / max vale) depend from other flag (by def use last criteria value) +define('ACHIEVEMENT_FLAG_PROGRESS_BAR', 0x0080); // Show as progress bar (value / max vale) depend from other flag (by def use last criteria value) +define('ACHIEVEMENT_FLAG_REALM_FIRST', 0x0100); // first max race/class/profession +define('ACHIEVEMENT_FLAG_REALM_FIRST_KILL', 0x0200); // first boss kill // AchievementCriteriaFlags define('ACHIEVEMENT_CRITERIA_FLAG_SHOW_PROGRESS_BAR', 0x0001); // Show progress as bar @@ -781,7 +1966,7 @@ define('ACHIEVEMENT_CRITERIA_TYPE_USE_GAMEOBJECT', 68); define('ACHIEVEMENT_CRITERIA_TYPE_BE_SPELL_TARGET2', 69); // define('ACHIEVEMENT_CRITERIA_TYPE_SPECIAL_PVP_KILL', 70); define('ACHIEVEMENT_CRITERIA_TYPE_FISH_IN_GAMEOBJECT', 72); -define('ACHIEVEMENT_CRITERIA_TYPE_EARNED_PVP_TITLE', 74); +define('ACHIEVEMENT_CRITERIA_TYPE_ON_LOGIN', 74); define('ACHIEVEMENT_CRITERIA_TYPE_LEARN_SKILLLINE_SPELLS', 75); // define('ACHIEVEMENT_CRITERIA_TYPE_WIN_DUEL', 76); // define('ACHIEVEMENT_CRITERIA_TYPE_LOSE_DUEL', 77); @@ -821,95 +2006,79 @@ define('ACHIEVEMENT_CRITERIA_TYPE_LEARN_SKILL_LINE', 112); // define('ACHIEVEMENT_CRITERIA_TYPE_DISENCHANT_ROLLS', 117); // define('ACHIEVEMENT_CRITERIA_TYPE_USE_LFD_TO_GROUP_WITH_PLAYERS', 119); -// TrinityCore - Condition System -define('CND_SRC_CREATURE_LOOT_TEMPLATE', 1); -define('CND_SRC_DISENCHANT_LOOT_TEMPLATE', 2); -define('CND_SRC_FISHING_LOOT_TEMPLATE', 3); -define('CND_SRC_GAMEOBJECT_LOOT_TEMPLATE', 4); -define('CND_SRC_ITEM_LOOT_TEMPLATE', 5); -define('CND_SRC_MAIL_LOOT_TEMPLATE', 6); -define('CND_SRC_MILLING_LOOT_TEMPLATE', 7); -define('CND_SRC_PICKPOCKETING_LOOT_TEMPLATE', 8); -define('CND_SRC_PROSPECTING_LOOT_TEMPLATE', 9); -define('CND_SRC_REFERENCE_LOOT_TEMPLATE', 10); -define('CND_SRC_SKINNING_LOOT_TEMPLATE', 11); -define('CND_SRC_SPELL_LOOT_TEMPLATE', 12); -define('CND_SRC_SPELL_IMPLICIT_TARGET', 13); -define('CND_SRC_GOSSIP_MENU', 14); -define('CND_SRC_GOSSIP_MENU_OPTION', 15); -define('CND_SRC_CREATURE_TEMPLATE_VEHICLE', 16); -define('CND_SRC_SPELL', 17); -define('CND_SRC_SPELL_CLICK_EVENT', 18); -define('CND_SRC_QUEST_ACCEPT', 19); -define('CND_SRC_QUEST_SHOW_MARK', 20); -define('CND_SRC_VEHICLE_SPELL', 21); -define('CND_SRC_SMART_EVENT', 22); -define('CND_SRC_NPC_VENDOR', 23); -define('CND_SRC_SPELL_PROC', 24); +// TrinityCore - Achievement Criteria Data +define('ACHIEVEMENT_CRITERIA_DATA_TYPE_NONE', 0); +define('ACHIEVEMENT_CRITERIA_DATA_TYPE_T_CREATURE', 1); +define('ACHIEVEMENT_CRITERIA_DATA_TYPE_T_PLAYER_CLASS_RACE', 2); +define('ACHIEVEMENT_CRITERIA_DATA_TYPE_T_PLAYER_LESS_HEALTH', 3); +define('ACHIEVEMENT_CRITERIA_DATA_TYPE_T_PLAYER_DEAD', 4); +define('ACHIEVEMENT_CRITERIA_DATA_TYPE_S_AURA', 5); +define('ACHIEVEMENT_CRITERIA_DATA_TYPE_S_AREA', 6); +define('ACHIEVEMENT_CRITERIA_DATA_TYPE_T_AURA', 7); +define('ACHIEVEMENT_CRITERIA_DATA_TYPE_VALUE', 8); +define('ACHIEVEMENT_CRITERIA_DATA_TYPE_T_LEVEL', 9); +define('ACHIEVEMENT_CRITERIA_DATA_TYPE_T_GENDER', 10); +define('ACHIEVEMENT_CRITERIA_DATA_TYPE_SCRIPT', 11); +define('ACHIEVEMENT_CRITERIA_DATA_TYPE_MAP_DIFFICULTY', 12); +define('ACHIEVEMENT_CRITERIA_DATA_TYPE_MAP_PLAYER_COUNT', 13); +define('ACHIEVEMENT_CRITERIA_DATA_TYPE_T_TEAM', 14); +define('ACHIEVEMENT_CRITERIA_DATA_TYPE_S_DRUNK', 15); +define('ACHIEVEMENT_CRITERIA_DATA_TYPE_HOLIDAY', 16); +define('ACHIEVEMENT_CRITERIA_DATA_TYPE_BG_LOSS_TEAM_SCORE', 17); +define('ACHIEVEMENT_CRITERIA_DATA_TYPE_INSTANCE_SCRIPT', 18); +define('ACHIEVEMENT_CRITERIA_DATA_TYPE_S_EQUIPED_ITEM', 19); +define('ACHIEVEMENT_CRITERIA_DATA_TYPE_MAP_ID', 20); +define('ACHIEVEMENT_CRITERIA_DATA_TYPE_S_PLAYER_CLASS_RACE', 21); +define('ACHIEVEMENT_CRITERIA_DATA_TYPE_NTH_BIRTHDAY', 22); +define('ACHIEVEMENT_CRITERIA_DATA_TYPE_S_KNOWN_TITLE', 23); +// define('ACHIEVEMENT_CRITERIA_DATA_TYPE_GAME_EVENT', 24); // not in 3.3.5a +define('ACHIEVEMENT_CRITERIA_DATA_TYPE_S_ITEM_QUALITY', 25); -define('CND_AURA', 1); // aura is applied: spellId, UNUSED, NULL -define('CND_ITEM', 2); // owns item: itemId, count, UNUSED -define('CND_ITEM_EQUIPPED', 3); // has item equipped: itemId, NULL, NULL -define('CND_ZONEID', 4); // is in zone: areaId, NULL, NULL -define('CND_REPUTATION_RANK', 5); // reputation status: factionId, rankMask, NULL -define('CND_TEAM', 6); // is on team: teamId, NULL, NULL -define('CND_SKILL', 7); // has skill: skillId, value, NULL -define('CND_QUESTREWARDED', 8); // has finished quest: questId, NULL, NULL -define('CND_QUESTTAKEN', 9); // has accepted quest: questId, NULL, NULL -define('CND_DRUNKENSTATE', 10); // has drunken status: stateId, NULL, NULL -define('CND_WORLD_STATE', 11); -define('CND_ACTIVE_EVENT', 12); // world event is active: eventId, NULL, NULL -define('CND_INSTANCE_INFO', 13); -define('CND_QUEST_NONE', 14); // never seen quest: questId, NULL, NULL -define('CND_CLASS', 15); // belongs to classes: classMask, NULL, NULL -define('CND_RACE', 16); // belongs to races: raceMask, NULL, NULL -define('CND_ACHIEVEMENT', 17); // obtained achievement: achievementId, NULL, NULL -define('CND_TITLE', 18); // obtained title: titleId, NULL, NULL -define('CND_SPAWNMASK', 19); -define('CND_GENDER', 20); // has gender: genderId, NULL, NULL -define('CND_UNIT_STATE', 21); -define('CND_MAPID', 22); // is on map: mapId, NULL, NULL -define('CND_AREAID', 23); // is in area: areaId, NULL, NULL -define('CND_UNUSED_24', 24); -define('CND_SPELL', 25); // knows spell: spellId, NULL, NULL -define('CND_PHASEMASK', 26); // is in phase: phaseMask, NULL, NULL -define('CND_LEVEL', 27); // player level is..: level, operator, NULL -define('CND_QUEST_COMPLETE', 28); // has completed quest: questId, NULL, NULL -define('CND_NEAR_CREATURE', 29); // is near creature: creatureId, dist, NULL -define('CND_NEAR_GAMEOBJECT', 30); // is near gameObject: gameObjectId, dist, NULL -define('CND_OBJECT_ENTRY', 31); // target is ???: objectType, id, NULL -define('CND_TYPE_MASK', 32); // target is type: typeMask, NULL, NULL -define('CND_RELATION_TO', 33); -define('CND_REACTION_TO', 34); -define('CND_DISTANCE_TO', 35); // distance to target targetType, dist, operator -define('CND_ALIVE', 36); // target is alive: NULL, NULL, NULL -define('CND_HP_VAL', 37); // targets absolute health: amount, operator, NULL -define('CND_HP_PCT', 38); // targets relative health: amount, operator, NULL +// TrinityCore - Account Security +define('SEC_PLAYER', 0); +define('SEC_MODERATOR', 1); +define('SEC_GAMEMASTER', 2); +define('SEC_ADMINISTRATOR', 3); +define('SEC_CONSOLE', 4); // console only - should not be encountered -// profiler queue interactions -define('PR_QUEUE_STATUS_ENDED', 0); -define('PR_QUEUE_STATUS_WAITING', 1); -define('PR_QUEUE_STATUS_WORKING', 2); -define('PR_QUEUE_STATUS_READY', 3); -define('PR_QUEUE_STATUS_ERROR', 4); -define('PR_QUEUE_ERROR_UNK', 0); -define('PR_QUEUE_ERROR_CHAR', 1); -define('PR_QUEUE_ERROR_ARMORY', 2); +// Areatrigger types +define('AT_TYPE_NONE', 0); +define('AT_TYPE_TAVERN', 1); +define('AT_TYPE_TELEPORT', 2); +define('AT_TYPE_OBJECTIVE', 3); +define('AT_TYPE_SMART', 4); +define('AT_TYPE_SCRIPT', 5); -// profiler completion manager -define('PR_EXCLUDE_GROUP_UNAVAILABLE', 0x001); -define('PR_EXCLUDE_GROUP_TCG', 0x002); -define('PR_EXCLUDE_GROUP_COLLECTORS_EDITION', 0x004); -define('PR_EXCLUDE_GROUP_PROMOTION', 0x008); -define('PR_EXCLUDE_GROUP_WRONG_REGION', 0x010); -define('PR_EXCLUDE_GROUP_REQ_ALLIANCE', 0x020); -define('PR_EXCLUDE_GROUP_REQ_HORDE', 0x040); -define('PR_EXCLUDE_GROUP_OTHER_FACTION', PR_EXCLUDE_GROUP_REQ_ALLIANCE | PR_EXCLUDE_GROUP_REQ_HORDE); -define('PR_EXCLUDE_GROUP_REQ_FISHING', 0x080); -define('PR_EXCLUDE_GROUP_REQ_ENGINEERING', 0x100); -define('PR_EXCLUDE_GROUP_REQ_TAILORING', 0x200); -define('PR_EXCLUDE_GROUP_WRONG_PROFESSION', PR_EXCLUDE_GROUP_REQ_FISHING | PR_EXCLUDE_GROUP_REQ_ENGINEERING | PR_EXCLUDE_GROUP_REQ_TAILORING); -define('PR_EXCLUDE_GROUP_REQ_CANT_BE_EXALTED', 0x400); -define('PR_EXCLUDE_GROUP_ANY', 0x7FF); +// summon types +define('SUMMONER_TYPE_CREATURE', 0); +define('SUMMONER_TYPE_GAMEOBJECT', 1); + +// Map Types +define('MAP_TYPE_ZONE', 0); +define('MAP_TYPE_TRANSIT', 1); +define('MAP_TYPE_DUNGEON', 2); +define('MAP_TYPE_RAID', 3); +define('MAP_TYPE_BATTLEGROUND', 4); +define('MAP_TYPE_DUNGEON_HC', 5); +define('MAP_TYPE_ARENA', 6); +define('MAP_TYPE_MMODE_RAID', 7); +define('MAP_TYPE_MMODE_RAID_HC', 8); + +define('EMOTE_FLAG_ONLY_STANDING', 0x0001); // Only while standig +define('EMOTE_FLAG_USE_MOUNT', 0x0002); // Emote applies to mount +define('EMOTE_FLAG_NOT_CHANNELING', 0x0004); // Not while channeling +define('EMOTE_FLAG_ANIM_TALK', 0x0008); // Talk anim - talk +define('EMOTE_FLAG_ANIM_QUESTION', 0x0010); // Talk anim - question +define('EMOTE_FLAG_ANIM_EXCLAIM', 0x0020); // Talk anim - exclamation +define('EMOTE_FLAG_ANIM_SHOUT', 0x0040); // Talk anim - shout +define('EMOTE_FLAG_NOT_SWIMMING', 0x0080); // Not while swimming +define('EMOTE_FLAG_ANIM_LAUGH', 0x0100); // Talk anim - laugh +define('EMOTE_FLAG_CAN_LIE_ON_GROUND', 0x0200); // Ok while sleeping or dead +define('EMOTE_FLAG_NOT_FROM_CLIENT', 0x0400); // Disallow from client +define('EMOTE_FLAG_NOT_CASTING', 0x0800); // Not while casting +define('EMOTE_FLAG_END_MOVEMENT', 0x1000); // Movement ends +define('EMOTE_FLAG_INTERRUPT_ON_ATTACK', 0x2000); // Interrupt on attack +define('EMOTE_FLAG_ONLY_STILL', 0x4000); // Only while still +define('EMOTE_FLAG_NOT_FLYING', 0x8000); // Not while flying ?> diff --git a/includes/game.php b/includes/game.php deleted file mode 100644 index 07ef3279..00000000 --- a/includes/game.php +++ /dev/null @@ -1,242 +0,0 @@ - [ 0], - 0 => [ 1, 3, 4, 8, 10, 11, 12, 25, 28, 33, 36, 38, 40, 41, 44, 45, 46, 47, 51, 85, 130, 139, 267, 279, 1497, 1519, 1537, 2257, 3430, 3433, 3487, 4080, 4298], - 1 => [ 14, 15, 16, 17, 141, 148, 215, 331, 357, 361, 400, 405, 406, 440, 490, 493, 618, 1216, 1377, 1637, 1638, 1657, 3524, 3525, 3557], -/*todo*/ 2 => [ 133, 206, 209, 491, 717, 718, 719, 722, 796, 978, 1196, 1337, 1417, 1581, 1583, 1584, 1941, 2017, 2057, 2100, 2366, 2367, 2437, 2557, 3477, 3562, 3713, 3714, 3715, 3716, 3717, 3789, 3790, 3791, 3792, 3845, 3846, 3847, 3849, 3905, 4095, 4100, 4120, 4196, 4228, 4264, 4272, 4375, 4415, 4494, 4723], -/*todo*/ 3 => [ 1977, 2159, 2562, 2677, 2717, 3428, 3429, 3456, 3606, 3805, 3836, 3840, 3842, 4273, 4500, 4722, 4812], - 4 => [ -372, -263, -262, -261, -162, -161, -141, -82, -81, -61], - 5 => [ -373, -371, -324, -304, -264, -201, -182, -181, -121, -101, -24], - 6 => [ -25, 2597, 3277, 3358, 3820, 4384, 4710], - 7 => [-1010, -368, -367, -365, -344, -241, -1], - 8 => [ 3483, 3518, 3519, 3520, 3521, 3522, 3523, 3679, 3703], // Skettis is no parent - 9 => [-1006, -1005, -1003, -1002, -1001, -376, -375, -374, -370, -369, -366, -364, -284, -41, -22], // 22: seasonal, 284: special => not in the actual menu - 10 => [ 65, 66, 67, 210, 394, 495, 3537, 3711, 4024, 4197, 4395, 4742] // Coldara is no parent - ); - - /* 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 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 - */ - public static $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 - [25, 654], [26, 655], [27, 656], [30, 763], [31, 767], [32, 766], [33, 765], // Hyena, Bird of Prey, Wind Serpent, Dragonhawk, Ravager, Warp Stalker, Sporebat - [34, 764], [35, 768], [37, 775], [38, 780], [39, 781], [41, 783], [42, 784], // Nether Ray, Serpent, Moth, Chimaera, Devilsaur, Silithid, Worm - [43, 786], [44, 785], [45, 787], [46, 788] // Rhino, Wasp, Core Hound, Spirit Beast - ), - -2 => array( // Pets (Warlock) - [15, 189], [16, 204], [17, 205], [19, 207], [23, 188], [29, 761] // Felhunter, Voidwalker, Succubus, Doomguard, Imp, Felguard - ), - -3 => array( // Ranged Weapons - [null, 45], [null, 46], [null, 226] // Bow, Gun, Crossbow - ) - ); - - public static $trainerTemplates = array( // TYPE => Id => templateList - TYPE_CLASS => array( - 1 => [-200001, -200002], // Warrior - 2 => [-200003, -200004, -200020, -200021], // Paladin - 3 => [-200013, -200014], // Hunter - 4 => [-200015, -200016], // Rogue - 5 => [-200011, -200012], // Priest - 6 => [-200019], // DK - 7 => [-200017, -200018], // Shaman (HighlevelAlly Id missing..?) - 8 => [-200007, -200008], // Mage - 9 => [-200009, -200010], // Warlock - 11 => [-200005, -200006] // Druid - ), - TYPE_SKILL => array( - 171 => [-201001, -201002, -201003], // Alchemy - 164 => [-201004, -201005, -201006, -201007, -201008],// Blacksmithing - 333 => [-201009, -201010, -201011], // Enchanting - 202 => [-201012, -201013, -201014, -201015, -201016, -201017], // Engineering - 182 => [-201018, -201019, -201020], // Herbalism - 773 => [-201021, -201022, -201023], // Inscription - 755 => [-201024, -201025, -201026], // Jewelcrafting - 165 => [-201027, -201028, -201029, -201030, -201031, -201032], // Leatherworking - 186 => [-201033, -201034, -201035], // Mining - 393 => [-201036, -201037, -201038], // Skinning - 197 => [-201039, -201040, -201041, -201042], // Tailoring - 356 => [-202001, -202002, -202003], // Fishing - 185 => [-202004, -202005, -202006], // Cooking - 129 => [-202007, -202008, -202009], // First Aid - 762 => [-202010, -202011, -202012] // Riding - ) - ); - - public static $sockets = array( // jsStyle Strings - 'meta', 'red', 'yellow', 'blue' - ); - - // 'replicates' $WH.g_statToJson - public static $itemMods = array( // zero-indexed; "mastrtng": unused mastery; _[a-z] => taken mods.. - 'dmg', 'mana', 'health', 'agi', 'str', 'int', 'spi', - 'sta', 'energy', 'rage', 'focus', 'runicpwr', 'defrtng', 'dodgertng', - 'parryrtng', 'blockrtng', 'mlehitrtng', 'rgdhitrtng', 'splhitrtng', 'mlecritstrkrtng', 'rgdcritstrkrtng', - 'splcritstrkrtng', '_mlehitrtng', '_rgdhitrtng', '_splhitrtng', '_mlecritstrkrtng', '_rgdcritstrkrtng', '_splcritstrkrtng', - 'mlehastertng', 'rgdhastertng', 'splhastertng', 'hitrtng', 'critstrkrtng', '_hitrtng', '_critstrkrtng', - 'resirtng', 'hastertng', 'exprtng', 'atkpwr', 'rgdatkpwr', 'feratkpwr', 'splheal', - 'spldmg', 'manargn', 'armorpenrtng', 'splpwr', 'healthrgn', 'splpen', 'block', // ITEM_MOD_BLOCK_VALUE - 'mastrtng', 'armor', 'firres', 'frores', 'holres', 'shares', 'natres', - 'arcres', 'firsplpwr', 'frosplpwr', 'holsplpwr', 'shasplpwr', 'natsplpwr', 'arcsplpwr' - ); - - public static $class2SpellFamily = array( - // null Warrior Paladin Hunter Rogue Priest DK Shaman Mage Warlock null Druid - null, 4, 10, 9, 8, 6, 15, 11, 3, 5, null, 7 - ); - - public static function itemModByRatingMask($mask) - { - if (($mask & 0x1C000) == 0x1C000) // special case resilience - return ITEM_MOD_RESILIENCE_RATING; - - if (($mask & 0x00E0) == 0x00E0) // hit rating - all subcats (mle, rgd, spl) - return ITEM_MOD_HIT_RATING; - - if (($mask & 0x0700) == 0x0700) // crit rating - all subcats (mle, rgd, spl) - return ITEM_MOD_CRIT_RATING; - - for ($j = 0; $j < count(self::$combatRatingToItemMod); $j++) - { - if (!self::$combatRatingToItemMod[$j]) - continue; - - if (!($mask & (1 << $j))) - continue; - - return self::$combatRatingToItemMod[$j]; - } - - return 0; - } - - public static function sideByRaceMask($race) - { - // Any - if (!$race || ($race & RACE_MASK_ALL) == RACE_MASK_ALL) - return SIDE_BOTH; - - // Horde - if ($race & RACE_MASK_HORDE && !($race & RACE_MASK_ALLIANCE)) - return SIDE_HORDE; - - // Alliance - if ($race & RACE_MASK_ALLIANCE && !($race & RACE_MASK_HORDE)) - return SIDE_ALLIANCE; - - return SIDE_BOTH; - } - - public static function getReputationLevelForPoints($pts) - { - 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; - } - - public static function getTaughtSpells(&$spell) - { - $extraIds = [-1]; // init with -1 to prevent empty-array errors - $lookup = [-1]; - switch (gettype($spell)) - { - case 'object': - if (get_class($spell) != 'SpellList') - return []; - - $lookup[] = $spell->id; - foreach ($spell->canTeachSpell() as $idx) - $extraIds[] = $spell->getField('effect'.$idx.'TriggerSpell'); - - break; - case 'integer': - $lookup[] = $spell; - break; - case 'array': - $lookup = $spell; - break; - default: - return []; - } - - // 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), - $extraIds - ); - - // return list of integers, not strings - array_walk($data, function (&$v, $k) { - $v = intVal($v); - }); - - return $data; - } - - public static function getPageText($ptId) - { - $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', User::$localeId, User::$localeString, $ptId)) - { - $ptId = $row['NextPageID']; - $pages[] = Util::parseHtmlText(Util::localizedString($row, 'Text')); - } - else - { - trigger_error('Referenced PageTextId #'.$ptId.' is not in DB', E_USER_WARNING); - break; - } - } - - return $pages; - } - -} - -?> diff --git a/includes/game/chrclass.class.php b/includes/game/chrclass.class.php new file mode 100644 index 00000000..3a94bc4b --- /dev/null +++ b/includes/game/chrclass.class.php @@ -0,0 +1,79 @@ +value & $classMask; + } + + public function toMask() : int + { + return 1 << ($this->value - 1); + } + + public static function fromMask(int $classMask = self::MASK_ALL) : array + { + $x = []; + foreach (self::cases() as $cl) + if ($cl->toMask() & $classMask) + $x[] = $cl->value; + + return $x; + } + + public function json() : string + { + return match ($this) + { + self::WARRIOR => 'warrior', + self::PALADIN => 'paladin', + self::HUNTER => 'hunter', + self::ROGUE => 'rogue', + self::PRIEST => 'priest', + self::DEATHKNIGHT => 'deathknight', + self::SHAMAN => 'shaman', + self::MAGE => 'mage', + self::WARLOCK => 'warlock', + self::DRUID => 'druid' + }; + } + + public function spellFamily() : int + { + return match ($this) + { + self::WARRIOR => SPELLFAMILY_WARRIOR, + self::PALADIN => SPELLFAMILY_PALADIN, + self::HUNTER => SPELLFAMILY_HUNTER, + self::ROGUE => SPELLFAMILY_ROGUE, + self::PRIEST => SPELLFAMILY_PRIEST, + self::DEATHKNIGHT => SPELLFAMILY_DEATHKNIGHT, + self::SHAMAN => SPELLFAMILY_SHAMAN, + self::MAGE => SPELLFAMILY_MAGE, + self::WARLOCK => SPELLFAMILY_WARLOCK, + self::DRUID => SPELLFAMILY_DRUID + }; + } +} + +?> diff --git a/includes/game/chrrace.class.php b/includes/game/chrrace.class.php new file mode 100644 index 00000000..70e481c1 --- /dev/null +++ b/includes/game/chrrace.class.php @@ -0,0 +1,144 @@ +value & $raceMask; + } + + public function toMask() : int + { + return 1 << ($this->value - 1); + } + + public function isAlliance() : bool + { + return $this->toMask() & self::MASK_ALLIANCE; + } + + public function isHorde() : bool + { + return $this->toMask() & self::MASK_HORDE; + } + + public function getSide() : int + { + if ($this->isHorde() && $this->isAlliance()) + return SIDE_BOTH; + else if ($this->isHorde()) + return SIDE_HORDE; + else if ($this->isAlliance()) + return SIDE_ALLIANCE; + else + return SIDE_NONE; + } + + public function getTeam() : int + { + if ($this->isHorde() && $this->isAlliance()) + return TEAM_NEUTRAL; + else if ($this->isHorde()) + return TEAM_HORDE; + else if ($this->isAlliance()) + return TEAM_ALLIANCE; + else + return TEAM_NEUTRAL; + } + + public function json() : string + { + return match ($this) + { + self::HUMAN => 'human', + self::ORC => 'orc', + self::DWARF => 'dwarf', + self::NIGHTELF => 'nightelf', + self::UNDEAD => 'scourge', + self::TAUREN => 'tauren', + self::GNOME => 'gnome', + self::TROLL => 'troll', + self::BLOODELF => 'bloodelf', + self::DRAENEI => 'draenei', + default => '' + }; + } + + public static function fromMask(int $raceMask = self::MASK_ALL) : array + { + $x = []; + foreach (self::cases() as $cl) + if ($cl->toMask() & $raceMask) + $x[] = $cl->value; + + return $x; + } + + public static function sideFromMask(int $raceMask) : int + { + // Any + if (!$raceMask || ($raceMask & self::MASK_ALL) == self::MASK_ALL) + return SIDE_BOTH; + + // Horde + if ($raceMask & self::MASK_HORDE && !($raceMask & self::MASK_ALLIANCE)) + return SIDE_HORDE; + + // Alliance + if ($raceMask & self::MASK_ALLIANCE && !($raceMask & self::MASK_HORDE)) + return SIDE_ALLIANCE; + + return SIDE_BOTH; + } + + public static function teamFromMask(int $raceMask) : int + { + // Any + if (!$raceMask || ($raceMask & self::MASK_ALL) == self::MASK_ALL) + return TEAM_NEUTRAL; + + // Horde + if ($raceMask & self::MASK_HORDE && !($raceMask & self::MASK_ALLIANCE)) + return TEAM_HORDE; + + // Alliance + if ($raceMask & self::MASK_ALLIANCE && !($raceMask & self::MASK_HORDE)) + return TEAM_ALLIANCE; + + return TEAM_NEUTRAL; + } +} + +?> diff --git a/includes/game/chrstatistics.php b/includes/game/chrstatistics.php new file mode 100644 index 00000000..1777e31c --- /dev/null +++ b/includes/game/chrstatistics.php @@ -0,0 +1,724 @@ + ['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], + self::STRENGTH => ['str', ITEM_MOD_STRENGTH, null, 20, self::FLAG_ITEM], + self::INTELLECT => ['int', ITEM_MOD_INTELLECT, null, 23, self::FLAG_ITEM], + self::SPIRIT => ['spi', ITEM_MOD_SPIRIT, null, 24, self::FLAG_ITEM], + self::STAMINA => ['sta', ITEM_MOD_STAMINA, null, 22, self::FLAG_ITEM], + self::ENERGY => ['energy', null, null, null, self::FLAG_ITEM], + self::RAGE => ['rage', null, null, null, self::FLAG_ITEM], + self::FOCUS => ['focus', null, null, null, self::FLAG_ITEM], + self::RUNIC_POWER => ['runic', null, null, null, self::FLAG_ITEM | self::FLAG_SERVERSIDE], + self::DEFENSE_RTG => ['defrtng', ITEM_MOD_DEFENSE_SKILL_RATING, CR_DEFENSE_SKILL, 42, self::FLAG_ITEM | self::FLAG_LVL_SCALING], + self::DODGE_RTG => ['dodgertng', ITEM_MOD_DODGE_RATING, CR_DODGE, 45, self::FLAG_ITEM | self::FLAG_LVL_SCALING], + self::PARRY_RTG => ['parryrtng', ITEM_MOD_PARRY_RATING, CR_PARRY, 46, self::FLAG_ITEM | self::FLAG_LVL_SCALING], + self::BLOCK_RTG => ['blockrtng', ITEM_MOD_BLOCK_RATING, CR_BLOCK, 44, self::FLAG_ITEM | self::FLAG_LVL_SCALING], + self::MELEE_HIT_RTG => ['mlehitrtng', ITEM_MOD_HIT_MELEE_RATING, CR_HIT_MELEE, 95, self::FLAG_ITEM | self::FLAG_LVL_SCALING], + self::RANGED_HIT_RTG => ['rgdhitrtng', ITEM_MOD_HIT_RANGED_RATING, CR_HIT_RANGED, 39, self::FLAG_ITEM | self::FLAG_LVL_SCALING], + self::SPELL_HIT_RTG => ['splhitrtng', ITEM_MOD_HIT_SPELL_RATING, CR_HIT_SPELL, 48, self::FLAG_ITEM | self::FLAG_LVL_SCALING], + self::MELEE_CRIT_RTG => ['mlecritstrkrtng', ITEM_MOD_CRIT_MELEE_RATING, CR_CRIT_MELEE, 84, self::FLAG_ITEM | self::FLAG_LVL_SCALING], + self::RANGED_CRIT_RTG => ['rgdcritstrkrtng', ITEM_MOD_CRIT_RANGED_RATING, CR_CRIT_RANGED, 40, self::FLAG_ITEM | self::FLAG_LVL_SCALING], + self::SPELL_CRIT_RTG => ['splcritstrkrtng', ITEM_MOD_CRIT_SPELL_RATING, CR_CRIT_SPELL, 49, self::FLAG_ITEM | self::FLAG_LVL_SCALING], + self::MELEE_HIT_TAKEN_RTG => ['_mlehitrtng', ITEM_MOD_HIT_TAKEN_MELEE_RATING, CR_HIT_TAKEN_MELEE, null, self::FLAG_ITEM | self::FLAG_LVL_SCALING], + self::RANGED_HIT_TAKEN_RTG => ['_rgdhitrtng', ITEM_MOD_HIT_TAKEN_RANGED_RATING, CR_HIT_TAKEN_RANGED, null, self::FLAG_ITEM | self::FLAG_LVL_SCALING], + self::SPELL_HIT_TAKEN_RTG => ['_splhitrtng', ITEM_MOD_HIT_TAKEN_SPELL_RATING, CR_HIT_TAKEN_SPELL, null, self::FLAG_ITEM | self::FLAG_LVL_SCALING], + self::MELEE_CRIT_TAKEN_RTG => ['_mlecritstrkrtng', ITEM_MOD_CRIT_TAKEN_MELEE_RATING, CR_CRIT_TAKEN_MELEE, null, self::FLAG_ITEM | self::FLAG_LVL_SCALING], + self::RANGED_CRIT_TAKEN_RTG => ['_rgdcritstrkrtng', ITEM_MOD_CRIT_TAKEN_RANGED_RATING, CR_CRIT_TAKEN_RANGED, null, self::FLAG_ITEM | self::FLAG_LVL_SCALING], + self::SPELL_CRIT_TAKEN_RTG => ['_splcritstrkrtng', ITEM_MOD_CRIT_TAKEN_SPELL_RATING, CR_CRIT_TAKEN_SPELL, null, self::FLAG_ITEM | self::FLAG_LVL_SCALING], + self::MELEE_HASTE_RTG => ['mlehastertng', ITEM_MOD_HASTE_MELEE_RATING, CR_HASTE_MELEE, 78, self::FLAG_ITEM | self::FLAG_LVL_SCALING], + self::RANGED_HASTE_RTG => ['rgdhastertng', ITEM_MOD_HASTE_RANGED_RATING, CR_HASTE_RANGED, 101, self::FLAG_ITEM | self::FLAG_LVL_SCALING], + self::SPELL_HASTE_RTG => ['splhastertng', ITEM_MOD_HASTE_SPELL_RATING, CR_HASTE_SPELL, 102, self::FLAG_ITEM | self::FLAG_LVL_SCALING], + self::HIT_RTG => ['hitrtng', ITEM_MOD_HIT_RATING, -CR_HIT_MELEE, 119, self::FLAG_ITEM | self::FLAG_LVL_SCALING], + self::CRIT_RTG => ['critstrkrtng', ITEM_MOD_CRIT_RATING, -CR_CRIT_MELEE, 96, self::FLAG_ITEM | self::FLAG_LVL_SCALING], + self::HIT_TAKEN_RTG => ['_hitrtng', ITEM_MOD_HIT_TAKEN_RATING, -CR_HIT_TAKEN_MELEE, null, self::FLAG_ITEM | self::FLAG_LVL_SCALING], + self::CRIT_TAKEN_RTG => ['_critstrkrtng', ITEM_MOD_CRIT_TAKEN_RATING, -CR_CRIT_TAKEN_MELEE, null, self::FLAG_ITEM | self::FLAG_LVL_SCALING], + self::RESILIENCE_RTG => ['resirtng', ITEM_MOD_RESILIENCE_RATING, -CR_CRIT_TAKEN_MELEE, 79, self::FLAG_ITEM | self::FLAG_LVL_SCALING], + self::HASTE_RTG => ['hastertng', ITEM_MOD_HASTE_RATING, -CR_HASTE_MELEE, 103, self::FLAG_ITEM | self::FLAG_LVL_SCALING], + self::EXPERTISE_RTG => ['exprtng', ITEM_MOD_EXPERTISE_RATING, CR_EXPERTISE, 117, self::FLAG_ITEM | self::FLAG_LVL_SCALING], + self::ATTACK_POWER => ['atkpwr', ITEM_MOD_ATTACK_POWER, null, 77, self::FLAG_ITEM], + self::RANGED_ATTACK_POWER => ['rgdatkpwr', ITEM_MOD_RANGED_ATTACK_POWER, null, 38, self::FLAG_ITEM], + self::FERAL_ATTACK_POWER => ['feratkpwr', ITEM_MOD_FERAL_ATTACK_POWER, null, 97, self::FLAG_ITEM], + self::HEALING_SPELL_POWER => ['splheal', ITEM_MOD_SPELL_HEALING_DONE, null, 50, self::FLAG_ITEM], + self::DAMAGE_SPELL_POWER => ['spldmg', ITEM_MOD_SPELL_DAMAGE_DONE, null, 51, self::FLAG_ITEM], + self::MANA_REGENERATION => ['manargn', ITEM_MOD_MANA_REGENERATION, null, 61, self::FLAG_ITEM], + self::ARMOR_PENETRATION_RTG => ['armorpenrtng', ITEM_MOD_ARMOR_PENETRATION_RATING, CR_ARMOR_PENETRATION, 114, self::FLAG_ITEM | self::FLAG_LVL_SCALING], + self::SPELL_POWER => ['splpwr', ITEM_MOD_SPELL_POWER, null, 123, self::FLAG_ITEM], + 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', 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], + self::SHADOW_SPELL_POWER => ['shasplpwr', null, null, 57, self::FLAG_ITEM], + self::NATURE_SPELL_POWER => ['natsplpwr', null, null, 56, self::FLAG_ITEM], + 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::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], + self::WEAPON_DPS => ['dps', null, null, 32, self::FLAG_SERVERSIDE | self::FLAG_FLOAT_VALUE], + self::MELEE_DAMAGE_MIN => ['mledmgmin', null, null, 135, self::FLAG_SERVERSIDE], + self::MELEE_DAMAGE_MAX => ['mledmgmax', null, null, 136, self::FLAG_SERVERSIDE], + self::MELEE_SPEED => ['mlespeed', null, null, 137, self::FLAG_SERVERSIDE | self::FLAG_FLOAT_VALUE], + self::MELEE_DPS => ['mledps', null, null, 134, self::FLAG_SERVERSIDE | self::FLAG_FLOAT_VALUE | self::FLAG_PROFILER], + self::RANGED_DAMAGE_MIN => ['rgddmgmin', null, null, 139, self::FLAG_SERVERSIDE], + 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::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 + self::EXPERTISE => ['exp', null, null, null, self::FLAG_PROFILER], + self::ARMOR_PENETRATION_PCT => ['armorpenpct', null, null, null, self::FLAG_PROFILER], + self::MELEE_HIT_PCT => ['mlehitpct', null, null, null, self::FLAG_PROFILER], + self::MELEE_CRIT_PCT => ['mlecritstrkpct', null, null, null, self::FLAG_PROFILER], + self::MELEE_HASTE_PCT => ['mlehastepct', null, null, null, self::FLAG_PROFILER], + self::RANGED_HIT_PCT => ['rgdhitpct', null, null, null, self::FLAG_PROFILER], + self::RANGED_CRIT_PCT => ['rgdcritstrkpct', null, null, null, self::FLAG_PROFILER], + self::RANGED_HASTE_PCT => ['rgdhastepct', null, null, null, self::FLAG_PROFILER], + self::SPELL_HIT_PCT => ['splhitpct', null, null, null, self::FLAG_PROFILER], + self::SPELL_CRIT_PCT => ['splcritstrkpct', null, null, null, self::FLAG_PROFILER], + self::SPELL_HASTE_PCT => ['splhastepct', null, null, null, self::FLAG_PROFILER], + self::MANA_REGENERATION_SPI => ['spimanargn', null, null, null, self::FLAG_PROFILER], + self::MANA_REGENERATION_OC => ['oocmanargn', null, null, null, self::FLAG_PROFILER], + self::MANA_REGENERATION_IC => ['icmanargn', null, null, null, self::FLAG_PROFILER], + self::ARMOR_TOTAL => ['fullarmor', null, null, null, self::FLAG_PROFILER], + self::DEFENSE => ['def', null, null, null, self::FLAG_PROFILER], + self::DODGE_PCT => ['dodgepct', null, null, null, self::FLAG_PROFILER], + self::PARRY_PCT => ['parrypct', null, null, null, self::FLAG_PROFILER], + self::BLOCK_PCT => ['blockpct', null, null, null, self::FLAG_PROFILER], + self::RESILIENCE_PCT => ['resipct', null, null, null, self::FLAG_PROFILER] + ); + + /* Combat Rating needed for 1% effect at level 60 (Note: Shaman, Druid, Paladin and Death Knight have a /1.3 modifier on HASTE not set here) + * Data taken from gtcombatratings.dbc for level 60 [idx % 100 = 59] + * Corrections from gtoctclasscombatratingscalar.dbc with Warrior as base [idx = ratingId + 1] + * Maybe create this data during setup, but then again it will never change for 3.3.5a + */ + private static $crPerPctPoint = array( + CR_WEAPON_SKILL => 2.50, CR_DEFENSE_SKILL => 1.50, CR_DODGE => 13.80, CR_PARRY => 13.80, CR_BLOCK => 5.00, + CR_HIT_MELEE => 10.00, CR_HIT_RANGED => 10.00, CR_HIT_SPELL => 8.00, CR_CRIT_MELEE => 14.00, CR_CRIT_RANGED => 14.00, + CR_CRIT_SPELL => 14.00, CR_HIT_TAKEN_MELEE => 10.00, CR_HIT_TAKEN_RANGED => 10.00, CR_HIT_TAKEN_SPELL => 8.00, CR_CRIT_TAKEN_MELEE => 28.75, + CR_CRIT_TAKEN_RANGED => 28.75, CR_CRIT_TAKEN_SPELL => 28.75, CR_HASTE_MELEE => 10.00, CR_HASTE_RANGED => 10.00, CR_HASTE_SPELL => 10.00, + CR_WEAPON_SKILL_MAINHAND => 2.50, CR_WEAPON_SKILL_OFFHAND => 2.50, CR_WEAPON_SKILL_RANGED => 2.50, CR_EXPERTISE => 2.50, CR_ARMOR_PENETRATION => 4.69512 / 1.1, + ); + + public static function isLevelIndependent(int $stat) : bool + { + if (!isset(self::$data[$stat])) + return false; + + 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..? + if (!isset(self::$data[$stat]) || self::$data[$stat][self::IDX_COMBAT_RATING] === null) + return 0.0; + + // note: originally any CRIT_TAKEN_RTG stat was set to 0 in favor of RESILIENCE_RTG + // we keep the dbc value and just link RESILIENCE_RTG to CRIT_TAKEN_RTG + // note2: the js expects some stats to be directly mapped to a combat rating that doesn't exist + // picked the next best one in this case and denoted it with a negative value in the $data dump + return self::$crPerPctPoint[abs(self::$data[$stat][self::IDX_COMBAT_RATING])]; + } + + public static function getJsonString(int $stat) : string + { + if (!isset(self::$data[$stat])) + return ''; + + return self::$data[$stat][self::IDX_JSON_STR]; + } + + public static function getFilterCriteriumId(int $stat) : ?int + { + if (!isset(self::$data[$stat])) + return null; + + return self::$data[$stat][self::IDX_FILTER_CR_ID]; + } + + public static function getFlags(int $stat) : int + { + if (!isset(self::$data[$stat])) + return 0; + + return self::$data[$stat][self::IDX_FLAGS]; + } + + public static function getJsonStringsFor(int $flags = Stat::FLAG_NONE) : array + { + $x = []; + foreach (self::$data as $k => [$s, , , , $f]) + if ($s && (!$flags || $flags & $f)) + $x[$k] = $s; + + return $x; + } + + public static function getCombatRatingsFor(int $flags = Stat::FLAG_NONE) : array + { + $x = []; + foreach (self::$data as $k => [, , $c, , $f]) + if ($c > 0 && (!$flags || $flags & $f)) + $x[$k] = $c; + + return $x; + } + + public static function getFilterCriteriumIdFor(int $flags = Stat::FLAG_NONE) : array + { + $x = []; + foreach (self::$data as $k => [, , , $cr, $f]) + if ($cr && (!$flags || $flags & $f)) + $x[$k] = $cr; + + return $x; + } + + public static function getIndexFrom(int $idx, string $search) : int + { + return array_find_key(self::$data, fn($x) => $x[$idx] == $search) ?: 0; + } +} + +class StatsContainer implements \Countable +{ + private array $store = []; + + 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 = []) + { + if ($relSpells) + $this->relSpells = $relSpells; + + if ($relEnchantments) + $this->relEnchantments = $relEnchantments; + } + + /**********/ + /* Source */ + /**********/ + + public function fromItem(array $item) : self + { + if (!$item) + return $this; + + // convert itemMods to stats + for ($i = 1; $i <= 10; $i++) + { + $mod = $item['statType'.$i]; + $val = $item['statValue'.$i]; + if (!$mod || !$val) + continue; + + if ($idx = Stat::getIndexFrom(Stat::IDX_ITEM_MOD, $mod)) + Util::arraySumByKey($this->store, [$idx => $val]); + } + + // also occurs as seperate field (gets summed in calculation but not in tooltip) + if ($item['tplBlock']) + Util::arraySumByKey($this->store, [Stat::BLOCK => $item['tplBlock']]); + + // convert spells to stats + for ($i = 1; $i <= 5; $i++) + if (in_array($item['spellTrigger'.$i], [SPELL_TRIGGER_EQUIP, SPELL_TRIGGER_USE, SPELL_TRIGGER_USE_NODELAY])) + if ($relS = $this->relS($item['spellId'.$i])) + $this->fromSpell($relS); + + // for ITEM_CLASS_GEM get stats from enchantment + if ($relE = $this->relE($item['gemEnchantmentId'])) + $this->fromEnchantment($relE); + + return $this; + } + + public function fromEnchantment(array $enchantment) : self + { + if (!$enchantment) + return $this; + + for ($i = 1; $i <= 3; $i++) + { + $type = $enchantment['type'.$i]; + $object = $enchantment['object'.$i]; + $amount = $enchantment['amount'.$i]; // !CAUTION! scaling enchantments are initialized with "0" as amount. 0 is a valid amount! + + if ($type == ENCHANTMENT_TYPE_EQUIP_SPELL && ($relS = $this->relS($object))) + $this->fromSpell($relS); + else + foreach ($this->convertEnchantment($type, $object) as $idx) + Util::arraySumByKey($this->store, [$idx => $amount]); + } + + return $this; + } + + public function fromSpell(array $spell, bool $onlyFoodBuff = false) : self + { + if (!$spell) + return $this; + + if ($onlyFoodBuff && !($spell['attributes2'] & SPELL_ATTR2_FOOD_BUFF)) + return $this; + + $tmpStore = []; + + for ($i = 1; $i <= 3; $i++) + { + $eff = $spell['effect'.$i.'Id']; + $aura = $spell['effect'.$i.'AuraId']; + $mVal = $spell['effect'.$i.'MiscValue']; + $amt = $spell['effect'.$i.'BasePoints'] + $spell['effect'.$i.'DieSides']; + + 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]); + } + + foreach (self::$combinedSpellStats as $combined => $stats) + { + 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); + + return $this; + } + + public function fromJson(array &$json, bool $pruneFromSrc = false) : self + { + if (!$json) + return $this; + + foreach (Stat::getJsonStringsFor() as $idx => $key) + { + if (isset($json[$key])) // 0 is a valid amount! + { + if (Stat::getFlags($idx) & Stat::FLAG_FLOAT_VALUE) + Util::arraySumByKey($this->store, [$idx => (float)$json[$key]]); + else + Util::arraySumByKey($this->store, [$idx => (int)$json[$key]]); + } + + if ($pruneFromSrc) + unset($json[$key]); + } + + return $this; + } + + public function fromDB(int $type, int $typeId, int $fieldFlags = Stat::FLAG_NONE) : self + { + 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; + + $idx = Stat::getIndexFrom(Stat::IDX_JSON_STR, $key); + $float = Stat::getFlags($idx) & Stat::FLAG_FLOAT_VALUE; + + if (Util::checkNumeric($amt, $float ? NUM_CAST_FLOAT : NUM_CAST_INT)) + Util::arraySumByKey($this->store, [$idx => $amt]); + } + + return $this; + } + + public function fromContainer(StatsContainer ...$container) : self + { + foreach ($container as $c) + Util::arraySumByKey($this->store, $c->toRaw()); + + return $this; + } + + + /**********/ + /* Output */ + /**********/ + + public function toJson(int $outFlags = Stat::FLAG_NONE, bool $includeEmpty = true) : array + { + $out = []; + foreach ($this->store as $stat => $amt) + if ((!$outFlags || (Stat::getFlags($stat) & $outFlags)) && ($amt || $includeEmpty)) + $out[Stat::getJsonString($stat)] = $amt; + + return $out; + } + + public function toRaw() : array + { + return $this->store; + } + + public function filter(?callable $filterFn = null) : self + { + $this->store = array_filter($this->store, $filterFn, ARRAY_FILTER_USE_BOTH); + return $this; + } + + public function count() : int + { + return count($this->store); + } + + /****************/ + /* internal use */ + /****************/ + + private function relE(int $enchantmentId) : array + { + return $this->relEnchantments[$enchantmentId] ?? []; + } + + private function relS(int $spellId) : array + { + return $this->relSpells[$spellId] ?? []; + } + + private static function convertEnchantment(int $type, int $object) : array + { + switch ($type) + { + case ENCHANTMENT_TYPE_PRISMATIC_SOCKET: + return [Stat::EXTRA_SOCKETS]; + case ENCHANTMENT_TYPE_DAMAGE: + return [Stat::WEAPON_DAMAGE]; + case ENCHANTMENT_TYPE_TOTEM: + return [Stat::WEAPON_DPS]; + case ENCHANTMENT_TYPE_STAT: // ITEM_MOD_* + return [Stat::getIndexFrom(Stat::IDX_ITEM_MOD, $object)]; + case ENCHANTMENT_TYPE_RESISTANCE: + 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: + default: + return []; + } + + return []; + } + + public static function convertCombatRating(int $mask) : array + { + $hitMask = (1 << CR_HIT_MELEE) | (1 << CR_HIT_RANGED) | (1 << CR_HIT_SPELL); + if (($mask & $hitMask) == $hitMask) + return [Stat::HIT_RTG]; // generic hit rating + + $critMask = (1 << CR_CRIT_MELEE) | (1 << CR_CRIT_RANGED) | (1 << CR_CRIT_SPELL); + 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 + + $result = []; // there really shouldn't be multiple ratings in that mask besides the cases above, but who knows.. + foreach (Stat::getCombatRatingsFor() as $stat => $cr) + if ($mask & (1 << $cr)) + $result[] = $stat; + + return $result; + } + + private static function convertSpellEffect(int $auraId, int $miscValue, int &$amount) : array + { + $stats = []; + + switch ($auraId) + { + case SPELL_AURA_MOD_STAT: + 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: + return [Stat::HEALTH]; + case SPELL_AURA_MOD_DAMAGE_DONE: + // + weapon damage + if ($miscValue == (1 << SPELL_SCHOOL_NORMAL)) + return [Stat::WEAPON_DAMAGE]; + + // full magic mask + if ($miscValue == SPELL_MAGIC_SCHOOLS) + return [Stat::DAMAGE_SPELL_POWER]; + + if ($miscValue & (1 << SPELL_SCHOOL_HOLY)) + $stats[] = Stat::HOLY_SPELL_POWER; + if ($miscValue & (1 << SPELL_SCHOOL_FIRE)) + $stats[] = Stat::FIRE_SPELL_POWER; + if ($miscValue & (1 << SPELL_SCHOOL_NATURE)) + $stats[] = Stat::NATURE_SPELL_POWER; + if ($miscValue & (1 << SPELL_SCHOOL_FROST)) + $stats[] = Stat::FROST_SPELL_POWER; + if ($miscValue & (1 << SPELL_SCHOOL_SHADOW)) + $stats[] = Stat::SHADOW_SPELL_POWER; + if ($miscValue & (1 << SPELL_SCHOOL_ARCANE)) + $stats[] = Stat::ARCANE_SPELL_POWER; + + return $stats; + 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 + 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)) + return $stat; + + return []; + case SPELL_AURA_MOD_RESISTANCE_EXCLUSIVE: + case SPELL_AURA_MOD_BASE_RESISTANCE: + case SPELL_AURA_MOD_RESISTANCE: + // Armor only if explicitly specified + if ($miscValue == (1 << SPELL_SCHOOL_NORMAL)) + return [Stat::ARMOR]; + + // Holy resistance only if explicitly specified (should it even exist...?) + if ($miscValue == (1 << SPELL_SCHOOL_HOLY)) + return [Stat::HOLY_RESISTANCE]; + + if ($miscValue & (1 << SPELL_SCHOOL_FIRE)) + $stats[] = Stat::FIRE_RESISTANCE; + if ($miscValue & (1 << SPELL_SCHOOL_NATURE)) + $stats[] = Stat::NATURE_RESISTANCE; + if ($miscValue & (1 << SPELL_SCHOOL_FROST)) + $stats[] = Stat::FROST_RESISTANCE; + if ($miscValue & (1 << SPELL_SCHOOL_SHADOW)) + $stats[] = Stat::SHADOW_RESISTANCE; + if ($miscValue & (1 << SPELL_SCHOOL_ARCANE)) + $stats[] = Stat::ARCANE_RESISTANCE; + + return $stats; + case SPELL_AURA_PERIODIC_HEAL: // hp5 + case SPELL_AURA_MOD_REGEN: + case SPELL_AURA_MOD_HEALTH_REGEN_IN_COMBAT: + return [Stat::HEALTH_REGENERATION]; + case SPELL_AURA_MOD_POWER_REGEN: // mp5 + return [Stat::MANA_REGENERATION]; + case SPELL_AURA_MOD_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: + return [Stat::BLOCK]; + case SPELL_AURA_MOD_EXPERTISE: + return [Stat::EXPERTISE]; + case SPELL_AURA_MOD_TARGET_RESISTANCE: + $amount = abs($amount); // functionally negative, but we work with the absolute amount + if ($miscValue == 0x7C) // SPELL_MAGIC_SCHOOLS & ~SPELL_SCHOOL_HOLY + return [Stat::SPELL_PENETRATION]; + } + + return []; + } +} + +?> 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 @@ +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 @@ +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 [[lootRows], [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 @@ + [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 new file mode 100644 index 00000000..11cc6ab1 --- /dev/null +++ b/includes/game/misc.php @@ -0,0 +1,376 @@ + 'inv_misc_questionmark', + 0 => 'spell_nature_elementalabsorption', + 6 => ['spell_deathknight_bloodpresence', 'spell_deathknight_frostpresence', 'spell_deathknight_unholypresence' ], + 11 => ['spell_nature_starfall', 'ability_racial_bearform', 'spell_nature_healingtouch' ], + 3 => ['ability_hunter_beasttaming', 'ability_marksmanship', 'ability_hunter_swiftstrike' ], + 8 => ['spell_holy_magicalsentry', 'spell_fire_firebolt02', 'spell_frost_frostbolt02' ], + 2 => ['spell_holy_holybolt', 'spell_holy_devotionaura', 'spell_holy_auraoflight' ], + 5 => ['spell_holy_wordfortitude', 'spell_holy_holybolt', 'spell_shadow_shadowwordpain' ], + 4 => ['ability_rogue_eviscerate', 'ability_backstab', 'ability_stealth' ], + 7 => ['spell_nature_lightning', 'spell_nature_lightningshield', 'spell_nature_magicimmunity' ], + 9 => ['spell_shadow_deathcoil', 'spell_shadow_metamorphosis', 'spell_shadow_rainoffire' ], + 1 => ['ability_rogue_eviscerate', 'ability_warrior_innerrage', 'ability_warrior_defensivestance' ] + ); + + public const /* array */ QUEST_CLASSES = array( + -2 => [ 0], + 0 => [ 1, 3, 4, 8, 9, 10, 11, 12, 25, 28, 33, 36, 38, 40, 41, 44, 45, 46, 47, 51, 85, 130, 132, 139, 154, 267, 1497, 1519, 1537, 2257, 3430, 3431, 3433, 3487, 4080, 4298], + 1 => [ 14, 15, 16, 17, 141, 148, 188, 215, 220, 331, 357, 361, 363, 400, 405, 406, 440, 490, 493, 618, 1377, 1637, 1638, 1657, 1769, 3524, 3525, 3526, 3557], + 2 => [ 206, 209, 491, 717, 718, 719, 721, 722, 796, 1176, 1196, 1337, 1477, 1581, 1583, 1584, 1941, 2017, 2057, 2100, 2366, 2367, 2437, 2557, 3535, 3562, 3688, 3713, 3714, 3715, 3716, 3717, 3789, 3790, 3791, 3792, 3842, 3847, 3848, 3849, 3905, 4100, 4131, 4196, 4228, 4264, 4265, 4272, 4277, 4415, 4416, 4494, 4522, 4723, 4809, 4813, 4820], + 3 => [ 1977, 2159, 2677, 2717, 3428, 3429, 3456, 3457, 3606, 3607, 3805, 3836, 3845, 3923, 3959, 4075, 4273, 4493, 4500, 4603, 4722, 4812, 4987], + 4 => [ -372, -263, -262, -261, -162, -161, -141, -82, -81, -61], + 5 => [ -373, -371, -324, -304, -264, -201, -182, -181, -121, -101, -24], + 6 => [ -25, 2597, 3277, 3358, 3820, 4384, 4710], + 7 => [-1010, -368, -367, -365, -344, -241, -1], + 8 => [ 3483, 3518, 3519, 3520, 3521, 3522, 3523, 3679, 3703], + 9 => [-1005, -1003, -1002, -1001, -376, -375, -374, -370, -369, -366, -364, -41, -22], // 22: seasonal + 10 => [ 65, 66, 67, 210, 394, 495, 2817, 3537, 3711, 4024, 4197, 4395, 4742] + ); + + // questSortId for quests need updating + // partially points non-instanced area with identical name for instance quests + 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 + 22 => 0, // Programmer Isle + 35 => 33, // Booty Bay => Stranglethorn Vale + 131 => 132, // Kharanos => Coldridge Valley + 24 => 9, // Northshire Abbey => Northshire Valley + 279 => 36, // Dalaran Crater => Alterac Mountains + 4342 => 4298, // Acherus: The Ebon Hold => The Scarlet Enclave + 2079 => 15, // Alcaz Island => Dustwallow Marsh + 1939 => 440, // Abyssal Sands => Tanaris + 393 => 363, // Darkspeer Strand => Valley of Trials + 702 => 141, // Rut'theran Village => Teldrassil + 221 => 220, // Camp Narache => Red Cloud Mesa + 1116 => 357, // Feathermoon Stronghold => Feralas + 236 => 209, // Shadowfang Keep + 4769 => 4742, // Hrothgar's Landing => Hrothgar's Landing + 4613 => 4395, // Dalaran City => Dalaran + 4522 => 210, // Icecrown Citadell => Icecrown + 3896 => 3703, // Aldor Rise => Shattrath City + 3696 => 3522, // The Barrier Hills => Blade's Edge Mountains + 2839 => 2597, // Alterac Valley + 19 => 1977, // Zul'Gurub + 4445 => 4273, // Ulduar + 2300 => 1941, // Caverns of Time + 3545 => 3535, // Hellfire Citadel + 2562 => 3457, // Karazhan + 3840 => 3959, // Black Temple + 1717 => 491, // Razorfen Kraul + 978 => 1176, // Zul'Farrak + 133 => 721, // Gnomeregan + 3607 => 3905, // Serpentshrine Cavern + 3845 => 3842, // Tempest Keep + 1517 => 1337, // Uldaman + 1417 => 1477 // Sunken Temple + ); + + public static array $questSubCats = array( + 1 => [132], // Dun Morogh: Coldridge Valley + 12 => [9], // Elwynn Forest: Northshire Valley + 141 => [188], // Teldrassil: Shadowglen + 3524 => [3526], // Azuremyst Isle: Ammen Vale + + 14 => [363], // Durotar: Valley of Trials + 85 => [154], // Tirisfal Glades: Deathknell + 215 => [220], // Mulgore: Red Cloud Mesa + 3430 => [3431], // Eversong Woods: Sunstrider Isle + + 46 => [25], // Burning Steppes: Blackrock Mountain + 361 => [1769], // Felwood: Timbermaw Hold + 3519 => [3679], // Terokkar: Skettis + 3535 => [3562, 3713, 3714], // Hellfire Citadel + 3905 => [3715, 3716, 3717], // Coilfang Reservoir + 3688 => [3789, 3790, 3792], // Auchindoun + 1941 => [2366, 2367, 4100], // Caverns of Time + 3842 => [3847, 3848, 3849], // Tempest Keep + 4522 => [4809, 4813, 4820] // Icecrown Citadel + ); + + /* 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 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 + */ + 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 + [25, 654], [26, 655], [27, 656], [30, 763], [31, 767], [32, 766], [33, 765], // Hyena, Bird of Prey, Wind Serpent, Dragonhawk, Ravager, Warp Stalker, Sporebat + [34, 764], [35, 768], [37, 775], [38, 780], [39, 781], [41, 783], [42, 784], // Nether Ray, Serpent, Moth, Chimaera, Devilsaur, Silithid, Worm + [43, 786], [44, 785], [45, 787], [46, 788] // Rhino, Wasp, Core Hound, Spirit Beast + ), + -2 => array( // Pets (Warlock) + [15, 189], [16, 204], [17, 205], [19, 207], [23, 188], [29, 761] // Felhunter, Voidwalker, Succubus, Doomguard, Imp, Felguard + ), + -3 => array( // Ranged Weapons + [null, 45], [null, 46], [null, 226] // Bow, Gun, Crossbow + ) + ); + + public static array $sockets = array( // jsStyle Strings + 'meta', 'red', 'yellow', 'blue' + ); + + public static function getReputationLevelForPoints(int $pts) : int + { + 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(mixed &$spell) : array + { + $extraIds = [-1]; // init with -1 to prevent empty-array errors + $lookup = [-1]; + switch (gettype($spell)) + { + case 'object': + if (get_class($spell) != SpellList::class) + return []; + + $lookup[] = $spell->id; + foreach ($spell->canTeachSpell() as $idx) + $extraIds[] = $spell->getField('effect'.$idx.'TriggerSpell'); + + break; + case 'integer': + $lookup[] = $spell; + break; + case 'array': + $lookup = $spell; + break; + default: + return []; + } + + // 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 %in', $lookup), + DB::World()->selectCol('SELECT spellId FROM skill_discovery_template WHERE reqSpell IN %in', $lookup), + $extraIds + ); + + // return list of integers, not strings + $data = array_map('intVal', $data); + + return $data; + } + + public static function getBook(int $ptId, ?int $startPage = null) : ?Book + { + $pages = []; + while ($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'); + continue; + } + + trigger_error('Referenced PageTextId #'.$ptId.' is not in DB', E_USER_WARNING); + break; + } + + return $pages ? new Book($pages, page: $startPage) : null; + } + + public static function getQuotesForCreature(int $creatureId, bool $asHTML = false, string $talkSource = '') : array + { + $nQuotes = 0; + $quotes = []; + $soundIds = []; + + $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", + %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 broadcast_text bct ON ct.`BroadcastTextId` = bct.`ID` + %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 + ); + + foreach ($quoteSrc as $grp => $text) + { + $group = []; + foreach ($text as $t) + { + if ($t['soundId']) + $soundIds[] = $t['soundId']; + + $msg = Util::localizedString($t, 'text'); + if (!$msg) + continue; + + // fixup .. either set %s for emotes or dont >.< + if (in_array($t['talkType'], [2, 16]) && strpos($msg, '%s') === false) + $msg = '%s '.$msg; + + // fixup: bad case-insensitivity + $msg = Util::parseHtmlText(str_replace('%S', '%s', htmlentities($msg)), !$asHTML); + + if ($talkSource) + $msg = sprintf($msg, $talkSource); + + // convert [old, new] talkType to css compatible + $t['talkType'] = match ((int)$t['talkType']) + { + 0, 12 => 2, // say - yellow-ish + 1, 14 => 1, // yell - dark red + 2, 16, // emote + 3, 41 => 4, // boss emote - orange + 4, 15, // whisper + 5, 42 => 3, // boss whisper - pink-ish + default => 2 + }; + + // prefix + $prefix = ''; + if ($t['talkType'] != 4) + $prefix = ($talkSource ?: '%s').' '.Lang::npc('textTypes', $t['talkType']).Lang::main('colon').($t['lang'] ? '['.Lang::game('languages', $t['lang']).'] ' : ' '); + + if ($asHTML) + $msg = '
'.$prefix.($t['range'] ? sprintf(Util::$dfnString, Lang::npc('textRanges', $t['range']), $msg) : $msg).'
'; + else + $msg = '[div][span class=s'.$t['talkType'].']'.$prefix.html_entity_decode($msg).'[/span][/div]'; + + $line = array( + 'range' => $t['range'], + 'text' => $msg + ); + + $nQuotes++; + $group[] = $line; + } + + if ($group) + $quotes[$grp] = $group; + } + + return [$quotes, $nQuotes, $soundIds]; + } + + public static function getBreakpointsForSkill(int $skillId, int $reqLevel) : array + { + if ($skillId == SKILL_FISHING) + return array( + round(sqrt(.25) * $reqLevel), // 25% valid catches + round(sqrt(.50) * $reqLevel), // 50% valid catches + round(sqrt(.75) * $reqLevel), // 75% valid catches + $reqLevel // 100% valid catches + ); + + switch ($skillId) + { + case SKILL_SKINNING: + $reqLevel /= 5; // we pass creature level * 5 (so, skill value), but formula depends on actual creature level + if ($reqLevel < 10) + $reqLevel = 0; + else if ($reqLevel < 20) + $reqLevel = ($reqLevel - 10) * 10; + else + $reqLevel *= 5; + case SKILL_HERBALISM: + case SKILL_LOCKPICKING: + case SKILL_JEWELCRAFTING: + case SKILL_INSCRIPTION: + case SKILL_MINING: + case SKILL_ENGINEERING: + $points = [$reqLevel]; // red/orange + + if ($reqLevel + 25 <= MAX_SKILL) // orange/yellow + $points[] = $reqLevel + 25; + + if ($reqLevel + 50 <= MAX_SKILL) // yellow/green + $points[] = $reqLevel + 50; + + if ($reqLevel + 100 <= MAX_SKILL) // green/grey + $points[] = $reqLevel + 100; + + return $points; + default: + return [$reqLevel]; + } + } + + public static function getEnchantmentCondition(int $conditionId, bool $interactive = false) : string + { + $gemCnd = DB::Aowow()->selectRow('SELECT * FROM ::itemenchantmentcondition WHERE `id` = %i', $conditionId); + if (!$gemCnd) + return ''; + + $x = ''; + for ($i = 1; $i < 6; $i++) + { + if (!$gemCnd['color'.$i]) + continue; + + $fiColors = function (int $idx) + { + return match ($idx) + { + 2 => '0:3:5', // red + 3 => '2:4:5', // yellow + 4 => '1:3:4', // blue + default => '' // uhhh.... + }; + }; + + $bLink = $gemCnd['color'.$i] ? ($interactive ? ''. Lang::item('gemColors', $gemCnd['color'.$i] - 1).'' : Lang::item('gemColors', $gemCnd['color'.$i] - 1)) : ''; + $cLink = $gemCnd['cmpColor'.$i] ? ($interactive ? ''.Lang::item('gemColors', $gemCnd['cmpColor'.$i] - 1).'' : Lang::item('gemColors', $gemCnd['cmpColor'.$i] - 1)) : ''; + + switch ($gemCnd['comparator'.$i]) + { + case ENCHANT_CONDITION_LESS_VALUE: // requires less than N gems + case ENCHANT_CONDITION_MORE_VALUE: // requires at least N gems + $x .= ''.Lang::item('gemRequires').Lang::item('gemConditions', $gemCnd['comparator'.$i], [$gemCnd['value'.$i], $bLink]).'
'; + break; + case ENCHANT_CONDITION_MORE_COMPARE: // requires more gems than gems + $x .= ''.Lang::item('gemRequires').Lang::item('gemConditions', $gemCnd['comparator'.$i], [$bLink, $cLink]).'
'; + break; + } + } + + return $x; + } +} + +?> diff --git a/includes/game/worldposition.class.php b/includes/game/worldposition.class.php new file mode 100644 index 00000000..df13aaf6 --- /dev/null +++ b/includes/game/worldposition.class.php @@ -0,0 +1,196 @@ += 100 || $set['posY'] >= 100) + { + $set = null; + return true; + } + + if (empty(self::$alphaMapCache[$areaId])) + self::$alphaMapCache[$areaId] = imagecreatefrompng($file); + + // alphaMaps are 1000 x 1000, adapt points [black => valid point] + if (!imagecolorat(self::$alphaMapCache[$areaId], $set['posX'] * 10, $set['posY'] * 10)) + $set = null; + + return true; + } + + public static function checkZonePos(array $points) : array + { + $result = []; + + foreach ($points as $res) + { + if (self::alphaMapCheck($res['areaId'], $res)) + { + if (!$res) + continue; + + // some rough measure how central the spawn is on the map (the lower the number, the better) + // 0: perfect center; 1: touches a border + $q = abs( (($res['posX'] - 50) / 50) * (($res['posY'] - 50) / 50) ); + + if (empty($result) || $result[0] > $q) + $result = [$q, $res]; + } + // capitals (auto-discovered) and no hand-made alphaMap available + else if (in_array($res['areaId'], self::$capitalCities)) + return $res; + // add with lowest quality if alpha map is missing + else if (empty($result)) + $result = [1.0, $res]; + } + + // spawn does not really match on a map, but we need at least one result + if (!$result) + { + usort($points, fn($a, $b) => $a['dist'] <=> $b['dist']); + $result = [1.0, $points[0]]; + } + + return $result[1]; + } + + public static function getForGUID(int $type, int ...$guids) : array + { + $result = []; + + switch ($type) + { + case Type::NPC: + $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()->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()->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()->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()->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()->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; + default: + trigger_error('WorldPosition::getForGUID - unsupported TYPE #'.$type, E_USER_WARNING); + } + + if ($diff = array_diff($guids, array_keys($result))) + trigger_error('WorldPosition::getForGUID - no spawn points for TYPE #'.$type.' GUIDS: '.implode(', ', $diff), E_USER_WARNING); + + return $result; + } + + public static function toZonePos(int $mapId, float $mapX, float $mapY, int $preferedAreaId = 0, int $preferedFloor = -1) : array + { + if (!$mapId < 0) + return []; + + 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(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 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 + 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` = %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`', + $mapId + ) ?: []; + } +} + +?> diff --git a/includes/kernel.php b/includes/kernel.php index d2f0a283..6afe3dbb 100644 --- a/includes/kernel.php +++ b/includes/kernel.php @@ -1,81 +1,237 @@ !extension_loaded($x))) + $error .= 'Required Extension '.implode(', ', $ext)." was not found. Please check if it should exist, using \"php -m\"\n\n"; + +if ($ext = array_filter($badExt, fn($x) => extension_loaded($x))) + $error .= 'Loaded Extension '.implode(', ', $ext)." is incompatible and must be disabled.\n\n"; + +if (version_compare(PHP_VERSION, '8.2.0') < 0) + $error .= 'PHP Version 8.2 or higher required! Your version is '.PHP_VERSION.".\nCore functions are unavailable!\n"; + +if ($error) + die(CLI ? strip_tags($error) : $error); + + +require_once 'includes/defines.php'; +require_once 'includes/locale.class.php'; +require_once 'localization/lang.class.php'; +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 +require_once 'includes/user.class.php'; // Session handling (could be skipped for CLI context except for username and password validation used in account creation) +require_once 'includes/game/misc.php'; // Misc game related data & functions + +// game client data interfaces +spl_autoload_register(function (string $class) : void +{ + if ($i = strrpos($class, '\\')) + $class = substr($class, $i + 1); + + if (preg_match('/[^\w]/i', $class)) + return; + + if ($class == 'Stat' || $class == 'StatsContainer') // entity statistics conversion + 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 +spl_autoload_register(function (string $class) : void +{ + if ($i = strrpos($class, '\\')) + $class = substr($class, $i + 1); + + if (preg_match('/[^\w]/i', $class)) + return; + + if (file_exists('includes/components/'.strtolower($class).'.class.php')) + require_once 'includes/components/'.strtolower($class).'.class.php'; + else if (file_exists('includes/components/frontend/'.strtolower($class).'.class.php')) + require_once 'includes/components/frontend/'.strtolower($class).'.class.php'; + else if (file_exists('includes/components/response/'.strtolower($class).'.class.php')) + require_once 'includes/components/response/'.strtolower($class).'.class.php'; +}); + +// TC systems in components +spl_autoload_register(function (string $class) : void +{ + switch ($class) + { + case __NAMESPACE__.'\SmartAI': + case __NAMESPACE__.'\SmartEvent': + case __NAMESPACE__.'\SmartAction': + case __NAMESPACE__.'\SmartTarget': + require_once 'includes/components/SmartAI/SmartAI.class.php'; + require_once 'includes/components/SmartAI/SmartEvent.class.php'; + require_once 'includes/components/SmartAI/SmartAction.class.php'; + require_once 'includes/components/SmartAI/SmartTarget.class.php'; + break; + case __NAMESPACE__.'\Conditions': + require_once 'includes/components/Conditions/Conditions.class.php'; + break; + } +}); + +// autoload List-classes, associated filters +spl_autoload_register(function (string $class) : void +{ + if ($i = strrpos($class, '\\')) + $class = substr($class, $i + 1); + + if (preg_match('/[^\w]/i', $class)) + return; + + if (!stripos($class, 'list')) + return; + + $class = strtolower(str_replace('ListFilter', 'List', $class)); + + $cl = match ($class) + { + 'localprofilelist', + 'remoteprofilelist' => 'profile', + 'localarenateamlist', + 'remotearenateamlist' => 'arenateam', + 'localguildlist', + 'remoteguildlist' => 'guild', + default => strtr($class, ['list' => '']) + }; + + if (file_exists('includes/dbtypes/'.$cl.'.class.php')) + require_once 'includes/dbtypes/'.$cl.'.class.php'; + else + throw new \Exception('could not register type class: '.$cl); +}); + +set_error_handler(function(int $errNo, string $errStr, string $errFile, int $errLine) : bool +{ + // either from test function or handled separately + 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; + + $logLevel = match($errNo) + { + E_RECOVERABLE_ERROR, E_USER_ERROR => LOG_LEVEL_ERROR, + E_WARNING, E_USER_WARNING => LOG_LEVEL_WARN, + E_NOTICE, E_USER_NOTICE => LOG_LEVEL_INFO, + default => 0 + }; + $errName = match($errNo) + { + E_RECOVERABLE_ERROR => 'RECOVERABLE_ERROR', + E_USER_ERROR => 'USER_ERROR', + E_USER_WARNING, E_WARNING => 'WARNING', + E_USER_NOTICE, E_NOTICE => 'NOTICE', + 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()->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 + ); + + $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($logMsg, U_GROUP_EMPLOYEE, $logLevel); + + return true; +}, E_ALL); + +// 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()->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() + ); + + if (CLI) + fwrite(STDERR, "\nException - ".$e->getMessage()."\n ".$e->getFile(). '('.$e->getLine().")\n".$e->getTraceAsString()."\n\n"); + else + { + Util::addNote('Exception - '.$e->getMessage().' @ '.$e->getFile(). ':'.$e->getLine()."\n".$e->getTraceAsString(), U_GROUP_EMPLOYEE, LOG_LEVEL_ERROR); + (new TemplateResponse())->generateError(); + } +}); + +// handle fatal errors +register_shutdown_function(function() : void +{ + // defer undisplayed error/exception notes + if (!CLI && ($n = Util::getNotes())) + $_SESSION['notes'][] = [$n[0], $n[1], 'Deferred issues from previous request']; + + 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()->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'] + ); + + if (CLI) + fwrite(STDERR, "\nFatal Error - ".$e['message'].' @ '.$e['file']. ':'.$e['line']."\n\n"); + else if (User::isInGroup(U_GROUP_EMPLOYEE)) + echo "\nFatal Error - ".$e['message'].' @ '.$e['file']. ':'.$e['line']."\n\n"; + } +}); + +// Setup DB-Wrapper if (file_exists('config/config.php')) require_once 'config/config.php'; else $AoWoWconf = []; - -mb_internal_encoding('UTF-8'); - - -define('OS_WIN', substr(PHP_OS, 0, 3) == 'WIN'); - - -require_once 'includes/defines.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/utilities.php'; // helper functions -require_once 'includes/game.php'; // game related data & functions -require_once 'includes/profiler.class.php'; -require_once 'includes/user.class.php'; -require_once 'includes/markup.class.php'; // manipulate markup text -require_once 'includes/database.class.php'; // wrap DBSimple -require_once 'includes/community.class.php'; // handle comments, screenshots and videos -require_once 'includes/loot.class.php'; // build lv-tabs containing loot-information -require_once 'localization/lang.class.php'; -require_once 'pages/genericPage.class.php'; - - -// autoload List-classes, associated filters and pages -spl_autoload_register(function ($class) { - $class = strtolower(str_replace('ListFilter', 'List', $class)); - - if (class_exists($class)) // already registered - return; - - if (preg_match('/[^\w]/i', $class)) // name should contain only letters - return; - - if (stripos($class, 'list')) - { - require_once 'includes/basetype.class.php'; - - $cl = strtr($class, ['list' => '']); - if ($cl == 'remoteprofile' || $cl == 'localprofile') - $cl = 'profile'; - if ($cl == 'remotearenateam' || $cl == 'localarenateam') - $cl = 'arenateam'; - if ($cl == 'remoteguild' || $cl == 'localguild') - $cl = 'guild'; - - if (file_exists('includes/types/'.$cl.'.class.php')) - require_once 'includes/types/'.$cl.'.class.php'; - else - throw new Exception('could not register type class: '.$cl); - - return; - } - else if (stripos($class, 'ajax') === 0) - { - require_once 'includes/ajaxHandler.class.php'; // handles ajax and jsonp requests - - if (file_exists('includes/ajaxHandler/'.strtr($class, ['ajax' => '']).'.class.php')) - require_once 'includes/ajaxHandler/'.strtr($class, ['ajax' => '']).'.class.php'; - else - throw new Exception('could not register ajaxHandler class: '.$class); - - return; - } - else if (file_exists('pages/'.strtr($class, ['page' => '']).'.php')) - require_once 'pages/'.strtr($class, ['page' => '']).'.php'; -}); - - -// Setup DB-Wrapper if (!empty($AoWoWconf['aowow']['db'])) DB::load(DB_AOWOW, $AoWoWconf['aowow']); @@ -90,161 +246,43 @@ if (!empty($AoWoWconf['characters'])) if (!empty($charDBInfo)) DB::load(DB_CHARACTERS . $realm, $charDBInfo); - -// load config to constants -$sets = DB::isConnectable(DB_AOWOW) ? DB::Aowow()->select('SELECT `key` AS ARRAY_KEY, `value`, `flags` FROM ?_config') : []; -foreach ($sets as $k => $v) -{ - $php = $v['flags'] & CON_FLAG_PHP; - - // this should not have been possible - if (!strlen($v['value']) && !($v['flags'] & CON_FLAG_TYPE_STRING) && !$php) - { - trigger_error('Aowow config value CFG_'.strtoupper($k).' is empty - config will not be used!', E_USER_ERROR); - continue; - } - - if ($v['flags'] & CON_FLAG_TYPE_INT) - $val = intVal($v['value']); - else if ($v['flags'] & CON_FLAG_TYPE_FLOAT) - $val = floatVal($v['value']); - else if ($v['flags'] & CON_FLAG_TYPE_BOOL) - $val = (bool)$v['value']; - else if ($v['flags'] & CON_FLAG_TYPE_STRING) - $val = preg_replace("/[\p{C}]/ui", '', $v['value']); - else if ($php) - { - trigger_error('PHP config value '.strtolower($k).' has no type set - config will not be used!', E_USER_ERROR); - continue; - } - else // if (!$php) - { - trigger_error('Aowow config value CFG_'.strtoupper($k).' has no type set - value forced to 0!', E_USER_ERROR); - $val = 0; - } - - if ($php) - ini_set(strtolower($k), $val); - else - define('CFG_'.strtoupper($k), $val); -} +$AoWoWconf = null; // empty auths -// handle non-fatal errors and notices -error_reporting(!empty($AoWoWconf['aowow']) && CFG_DEBUG ? E_AOWOW : 0); -set_error_handler(function($errNo, $errStr, $errFile, $errLine) -{ - $errName = 'unknown error'; // errors not in this list can not be handled by set_error_handler (as per documentation) or are ignored - $uGroup = U_GROUP_EMPLOYEE; +// for CLI and early errors in erb context +Lang::load(Locale::EN); - if ($errNo == E_WARNING) // 0x0002 - $errName = 'E_WARNING'; - else if ($errNo == E_PARSE) // 0x0004 - $errName = 'E_PARSE'; - else if ($errNo == E_NOTICE) // 0x0008 - $errName = 'E_NOTICE'; - else if ($errNo == E_USER_ERROR) // 0x0100 - $errName = 'E_USER_ERROR'; - else if ($errNo == E_USER_WARNING) // 0x0200 - $errName = 'E_USER_WARNING'; - else if ($errNo == E_USER_NOTICE) // 0x0400 - { - $errName = 'E_USER_NOTICE'; - $uGroup = U_GROUP_STAFF; - } - else if ($errNo == E_RECOVERABLE_ERROR) // 0x1000 - $errName = 'E_RECOVERABLE_ERROR'; - - Util::addNote($uGroup, $errName.' - '.$errStr.' @ '.$errFile. ':'.$errLine); - - if (DB::isConnectable(DB_AOWOW)) - DB::Aowow()->query('INSERT INTO ?_errors (`date`, `version`, `phpError`, `file`, `line`, `query`, `userGroups`, `message`) VALUES (UNIX_TIMESTAMP(), ?d, ?d, ?, ?d, ?, ?d, ?) ON DUPLICATE KEY UPDATE `date` = UNIX_TIMESTAMP()', - AOWOW_REVISION, $errNo, $errFile, $errLine, CLI ? 'CLI' : $_SERVER['QUERY_STRING'], User::$groups, $errStr - ); - - return true; -}, E_AOWOW); - -// handle exceptions -set_exception_handler(function ($ex) -{ - Util::addNote(U_GROUP_EMPLOYEE, 'Exception - '.$ex->getMessage().' @ '.$ex->getFile(). ':'.$ex->getLine()."\n".$ex->getTraceAsString()); - - if (DB::isConnectable(DB_AOWOW)) - DB::Aowow()->query('INSERT INTO ?_errors (`date`, `version`, `phpError`, `file`, `line`, `query`, `userGroups`, `message`) VALUES (UNIX_TIMESTAMP(), ?d, ?d, ?, ?d, ?, ?d, ?) ON DUPLICATE KEY UPDATE `date` = UNIX_TIMESTAMP()', - AOWOW_REVISION, $ex->getCode(), $ex->getFile(), $ex->getLine(), CLI ? 'CLI' : $_SERVER['QUERY_STRING'], User::$groups, $ex->getMessage() - ); - - if (!CLI) - (new GenericPage(null))->error(); - else - echo 'Exception - '.$ex->getMessage()."\n ".$ex->getFile(). '('.$ex->getLine().")\n".$ex->getTraceAsString()."\n"; -}); - -// handle fatal errors -register_shutdown_function(function() -{ - if (($e = error_get_last()) && $e['type'] & (E_ERROR | E_COMPILE_ERROR | E_CORE_ERROR)) - { - Util::addNote(U_GROUP_EMPLOYEE, 'Fatal Error - '.$e['message'].' @ '.$e['file']. ':'.$e['line']); - - if (DB::isConnectable(DB_AOWOW)) - DB::Aowow()->query('INSERT INTO ?_errors (`date`, `version`, `phpError`, `file`, `line`, `query`, `userGroups`, `message`) VALUES (UNIX_TIMESTAMP(), ?d, ?d, ?, ?d, ?, ?d, ?) ON DUPLICATE KEY UPDATE `date` = UNIX_TIMESTAMP()', - AOWOW_REVISION, $e['type'], $e['file'], $e['line'], CLI ? 'CLI' : $_SERVER['QUERY_STRING'], User::$groups, $e['message'] - ); - - if (CLI) - echo 'Fatal Error - '.$e['message'].' @ '.$e['file']. ':'.$e['line']."\n"; - - // cant generate a page for web view :( - die(); - } -}); - -$secure = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] != 'off') || (!empty($AoWoWconf['aowow']) && CFG_FORCE_SSL); -if (defined('CFG_STATIC_HOST')) // points js to images & scripts - define('STATIC_URL', ($secure ? 'https://' : 'http://').CFG_STATIC_HOST); - -if (defined('CFG_SITE_HOST')) // points js to executable files - define('HOST_URL', ($secure ? 'https://' : 'http://').CFG_SITE_HOST); +// load config from DB +Cfg::load(); if (!CLI) { - if (!defined('CFG_SITE_HOST') || !defined('CFG_STATIC_HOST')) - die('error: SITE_HOST or STATIC_HOST not configured'); + // not displaying the brb gnomes as static_host is missing, but eh... + if (!DB::isConnected(DB_AOWOW) || !DB::isConnected(DB_WORLD) || !Cfg::get('HOST_URL') || !Cfg::get('STATIC_URL')) + (new TemplateResponse())->generateMaintenance(); // Setup Session - if (CFG_SESSION_CACHE_DIR && Util::writeDir(CFG_SESSION_CACHE_DIR)) - session_save_path(getcwd().'/'.CFG_SESSION_CACHE_DIR); + $cacheDir = Cfg::get('SESSION_CACHE_DIR'); + if ($cacheDir && Util::writeDir($cacheDir)) + session_save_path(getcwd().'/'.$cacheDir); - session_set_cookie_params(15 * YEAR, '/', '', $secure, true); + session_set_cookie_params(15 * YEAR, '/', '', (($_SERVER['HTTPS'] ?? 'off') != 'off') || Cfg::get('FORCE_SSL'), true); session_cache_limiter('private'); - session_start(); - if (!empty($AoWoWconf['aowow']) && User::init()) - User::save(); // save user-variables in session - - // hard-override locale for this call (should this be here..?) - // all strings attached.. - if (!empty($AoWoWconf['aowow'])) + if (!session_start()) { - if (isset($_GET['locale']) && (CFG_LOCALES & (1 << (int)$_GET['locale']))) - User::useLocale($_GET['locale']); - - Lang::load(User::$localeString); + trigger_error('failed to start session', E_USER_ERROR); + (new TemplateResponse())->generateError(); } - // parse page-parameters .. sanitize before use! - $str = explode('&', $_SERVER['QUERY_STRING'], 2)[0]; - $_ = explode('=', $str, 2); - $pageCall = $_[0]; - $pageParam = isset($_[1]) ? $_[1] : null; + if (User::init()) + User::save(); // save user-variables in session - Util::$wowheadLink = 'http://'.Util::$subDomains[User::$localeId].'.wowhead.com/'.$str; + // hard override locale for this call (should this be here..?) + if (isset($_GET['locale']) && ($loc = Locale::tryFrom((int)$_GET['locale']))) + Lang::load($loc); + else + Lang::load(User::$preferedLoc); } -else if (!empty($AoWoWconf['aowow'])) - Lang::load('enus'); - -$AoWoWconf = null; // empty auths ?> 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 @@ -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 6489c89a..00000000 --- a/includes/libs/DbSimple/Connect.php +++ /dev/null @@ -1,262 +0,0 @@ -нужен для ленивой инициализации коннекта к базе - * - * @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
";
-		print_r($info);
-		echo "
"; - exit(); - } - - /** - * Выставляет запрос для инициализации - * - * @param string $query запрос - */ - public function addInit($query) - { - $args = func_get_args(); - if ($this->DbSimple !== null) - return call_user_func_array(array(&$this->DbSimple, 'query'), $args); - $this->init[] = $args; - } - - /** - * Устанавливает новый обработчик ошибок - * Обработчик получает 2 аргумента: - * - сообщение об ошибке - * - массив (код, сообщение, запрос, контекст) - * - * @param callback|null|false $handler обработчик ошибок - *
null - по умолчанию - *
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 86719ae1..00000000 --- a/includes/libs/DbSimple/Database.php +++ /dev/null @@ -1,1412 +0,0 @@ -. - * - * 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 -{ - /** - * 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($query) - { - $args = func_get_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, $query) - { - $args = func_get_args(); - array_shift($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 = func_get_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 = func_get_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 = func_get_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 = func_get_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 = func_get_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; - } - - /** - * Задает имя класса строки - * - *
для следующего запроса каждая строка будет - * заменена классом, конструктору которого передается - * массив поле=>значение для этой строки - * - * @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 @@ -. - * - * 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 6b67dd14..00000000 --- a/includes/libs/DbSimple/Mysqli.php +++ /dev/null @@ -1,231 +0,0 @@ -_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); - $result = mysqli_query($this->link, $queryMain[0]); - if ($result === false) - return $this->_setDbError($queryMain[0]); - 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 @@ -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 @@ - 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/libs/qqFileUploader.class.php b/includes/libs/qqFileUploader.class.php new file mode 100644 index 00000000..dae92a58 --- /dev/null +++ b/includes/libs/qqFileUploader.class.php @@ -0,0 +1,187 @@ +handleUpload('uploads/'); + +// to pass data through iframe you will need to encode all html tags +echo htmlspecialchars(json_encode($result), ENT_NOQUOTES); + +/******************************************/ + + + +/** + * Handle file uploads via XMLHttpRequest + */ +class qqUploadedFileXhr +{ + /** + * Save the file to the specified path + * @return boolean TRUE on success + */ + function save(string $path) : bool + { + $input = fopen("php://input", "r"); + $temp = tmpfile(); + $realSize = stream_copy_to_stream($input, $temp); + fclose($input); + + if ($realSize != $this->getSize()) + return false; + + $target = fopen($path, "w"); + fseek($temp, 0, SEEK_SET); + stream_copy_to_stream($temp, $target); + fclose($target); + + return true; + } + + function getName() : string + { + return $_GET['qqfile']; + } + + function getSize(): int + { + if (isset($_SERVER["CONTENT_LENGTH"])) + return (int)$_SERVER["CONTENT_LENGTH"]; + + throw new Exception('Getting content length is not supported.'); + return 0; + } +} + +/** + * Handle file uploads via regular form post (uses the $_FILES array) + */ +class qqUploadedFileForm +{ + /** + * Save the file to the specified path + * @return boolean TRUE on success + */ + function save(string $path) : bool + { + if(!move_uploaded_file($_FILES['qqfile']['tmp_name'], $path)) + return false; + + return true; + } + + function getName() : string + { + return $_FILES['qqfile']['name']; + } + + function getSize() : int + { + return $_FILES['qqfile']['size']; + } +} + +class qqFileUploader +{ + private $allowedExtensions = array(); + private $sizeLimit = 10485760; + private $file; + + public function __construct(array $allowedExtensions = array(), $sizeLimit = 10485760) + { + $this->allowedExtensions = array_map("strtolower", $allowedExtensions); + $this->sizeLimit = $sizeLimit; + + $this->checkServerSettings(); + + if (isset($_GET['qqfile'])) + $this->file = new qqUploadedFileXhr(); + else if (isset($_FILES['qqfile'])) + $this->file = new qqUploadedFileForm(); + else + $this->file = null; + } + + public function getName() : string + { + return $this->file?->getName() ?? ''; + } + + private function checkServerSettings() : void + { + $postSize = $this->toBytes(ini_get('post_max_size')); + $uploadSize = $this->toBytes(ini_get('upload_max_filesize')); + + if ($postSize < $this->sizeLimit || $uploadSize < $this->sizeLimit) + { + $size = max(1, $this->sizeLimit / 1024 / 1024) . 'M'; + die("{'error':'increase post_max_size and upload_max_filesize to $size'}"); + } + } + + private function toBytes(string $str) : int + { + $val = substr(trim($str), 0, -1); + $last = strtolower(substr($str, -1, 1)); + switch ($last) + { + case 'g': $val *= 1024; + case 'm': $val *= 1024; + case 'k': $val *= 1024; + } + + return $val; + } + + /** + * Returns array('success' => true, 'newFilename' => 'myDoc123.doc') or array('error' => 'error message') + */ + function handleUpload(string $uploadDirectory, string $newName = '', bool $replaceOldFile = FALSE) : array + { + if (!is_writable($uploadDirectory)) + return ['error' => "Server error. Upload directory isn't writable."]; + + if (!$this->file) + return ['error' => 'No files were uploaded.']; + + $size = $this->file->getSize(); + + if ($size == 0) + return ['error' => 'File is empty']; + + if ($size > $this->sizeLimit) + return ['error' => 'File is too large']; + + $pathinfo = pathinfo($this->getName()); + $filename = $newName ?: $pathinfo['filename']; + //$filename = md5(uniqid()); + $ext = @$pathinfo['extension']; // hide notices if extension is empty + + if ($this->allowedExtensions && !in_array(strtolower($ext), $this->allowedExtensions)) + { + $these = implode(', ', $this->allowedExtensions); + return ['error' => 'File has an invalid extension, it should be one of '. $these . '.']; + } + + // don't overwrite previous files that were uploaded + if (!$replaceOldFile) + while (file_exists($uploadDirectory . $filename . '.' . $ext)) + $filename .= rand(10, 99); + + if ($this->file->save($uploadDirectory . $filename . '.' . $ext)) + return ['success' => true, 'newFilename' => $filename . '.' . $ext]; + else + return ['error' => 'Could not save uploaded file. The upload was cancelled, or server error encountered']; + } +} diff --git a/includes/locale.class.php b/includes/locale.class.php new file mode 100644 index 00000000..a8d8923e --- /dev/null +++ b/includes/locale.class.php @@ -0,0 +1,179 @@ + 'en', + self::KR => 'ko', + self::FR => 'fr', + self::DE => 'de', + self::CN => 'cn', + self::TW => 'tw', + self::ES => 'es', + self::MX => 'mx', + self::RU => 'ru', + self::JP => 'jp', + self::PT => 'pt', + self::IT => 'it' + }; + } + + public function json() : string // internal usage / json string + { + return match ($this) + { + self::EN => 'enus', + self::KR => 'kokr', + self::FR => 'frfr', + self::DE => 'dede', + self::CN => 'zhcn', + self::TW => 'zhtw', + self::ES => 'eses', + self::MX => 'esmx', + self::RU => 'ruru', + self::JP => 'jajp', + self::PT => 'ptpt', + self::IT => 'itit' + }; + } + + public function title() : string // localized language name + { + return match ($this) + { + self::EN => 'English', + self::KR => '한국어', + self::FR => 'Français', + self::DE => 'Deutsch', + self::CN => '简体中文', + self::TW => '繁體中文', + self::ES => 'Español', + self::MX => 'Mexicano', + self::RU => 'Русский', + self::JP => '日本語', + self::PT => 'Português', + self::IT => 'Italiano' + }; + } + + public function gameDirs() : array // setup data source / wow client locale code + { + return match ($this) + { + self::EN => ['enGB', 'enUS', ''], + self::KR => ['koKR'], + self::FR => ['frFR'], + self::DE => ['deDE'], + self::CN => ['zhCN', 'enCN'], + self::TW => ['zhTW', 'enTW'], + self::ES => ['esES'], + self::MX => ['esMX'], + self::RU => ['ruRU'], + self::JP => ['jaJP'], + self::PT => ['ptPT', 'ptBR'], + self::IT => ['itIT'] + }; + } + + public function httpCode() : array // HTTP_ACCEPT_LANGUAGE + { + return match ($this) + { + self::EN => ['en', 'en-au', 'en-bz', 'en-ca', 'en-ie', 'en-jm', 'en-nz', 'en-ph', 'en-za', 'en-tt', 'en-gb', 'en-us', 'en-zw'], + self::KR => ['ko', 'ko-kp', 'ko-kr'], + self::FR => ['fr', 'fr-be', 'fr-ca', 'fr-fr', 'fr-lu', 'fr-mc', 'fr-ch'], + self::DE => ['de', 'de-at', 'de-de', 'de-li', 'de-lu', 'de-ch'], + self::CN => ['zh', 'zh-hk', 'zh-cn', 'zh-sg'], + self::TW => ['tw', 'zh-tw'], + self::ES => ['es', 'es-ar', 'es-bo', 'es-cl', 'es-co', 'es-cr', 'es-do', 'es-ec', 'es-sv', 'es-gt', 'es-hn', 'es-ni', 'es-pa', 'es-py', 'es-pe', 'es-pr', 'es-es', 'es-uy', 'es-ve'], + self::MX => ['mx', 'es-mx'], + self::RU => ['ru', 'ru-mo'], + self::JP => ['ja'], + self::PT => ['pt', 'pt-br'], + self::IT => ['it', 'it-ch'] + }; + } + + public function isLogographic() : bool + { + return $this == Locale::CN || $this == Locale::TW || $this == Locale::KR; + } + + public function validate() : ?self + { + return ($this->maskBit() & self::MASK_ALL & (Cfg::get('LOCALES') ?: 0xFFFF)) ? $this : null; + } + + public function maskBit() : int + { + return (1 << $this->value); + } + + public static function tryFromDomain(string $str) : ?self + { + foreach (self::cases() as $l) + if ($l->validate() && $str == $l->domain()) + return $l; + + return null; + } + + public static function tryFromHttpAcceptLanguage(string $httpAccept) : ?self + { + if (!$httpAccept) + return null; + + $available = []; + + // e.g.: de,de-DE;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6 + foreach (explode(',', $_SERVER['HTTP_ACCEPT_LANGUAGE']) as $loc) + if (preg_match('/([a-z\-]+)(?:\s*;\s*q\s*=\s*([\.\d]+))?/ui', $loc, $m, PREG_UNMATCHED_AS_NULL)) + $available[Util::lower($m[1])] = floatVal($m[2] ?? 1); // no quality set: assume 100% + + arsort($available, SORT_NUMERIC); // highest quality on top + + foreach ($available as $code => $_) + foreach (self::cases() as $l) + if ($l->validate() && in_array($code, $l->httpCode())) + return $l; + + return null; + } + + public static function getFallback() : self + { + foreach (Locale::cases() as $l) + if ($l->validate()) + return $l; + + // wow, you really fucked up your config mate! + trigger_error('Locale::getFallback - there are no valid locales', E_USER_ERROR); + return self::EN; + } +} + +?> diff --git a/includes/loot.class.php b/includes/loot.class.php deleted file mode 100644 index bfa3be85..00000000 --- a/includes/loot.class.php +++ /dev/null @@ -1,636 +0,0 @@ -results); - - while (list($k, $__) = each($this->results)) - yield $k => $this->results[$k]; - } - - public function getResult() - { - return $this->results; - } - - private function createStack($l) // issue: TC always has an equal distribution between min/max - { - if (empty($l['min']) || empty($l['max']) || $l['max'] <= $l['min']) - return null; - - $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($data) - { - 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 getByContainerRecursive($tableName, $lootId, &$handledRefs, $groupId = 0, $baseChance = 1.0) - { - $loot = []; - $rawItems = []; - - if (!$tableName || !$lootId) - return null; - - $rows = DB::World()->select('SELECT * FROM ?# WHERE entry = ?d{ AND groupid = ?d}', $tableName, $lootId, $groupId ?: DBSIMPLE_SKIP); - if (!$rows) - return null; - - $groupChances = []; - $nGroupEquals = []; - foreach ($rows as $entry) - { - $set = array( - 'quest' => $entry['QuestRequired'], - 'group' => $entry['GroupId'], - 'parentRef' => $tableName == LOOT_REFERENCE ? $lootId : 0, - 'realChanceMod' => $baseChance - ); - - // 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 - ||||' - |||'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) - list($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']) - { - if (empty($groupChances[$entry['GroupId']])) - $groupChances[$entry['GroupId']] = 0; - - $groupChances[$entry['GroupId']] += $entry['Chance']; - $set['groupChance'] = $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; - } - - $cnt = empty($nGroupEquals[$k]) ? 1 : $nGroupEquals[$k]; - - $groupChances[$k] = (100 - $sum) / $cnt; // is applied as backReference to items with 0-chance - } - - return [$loot, array_unique($rawItems)]; - } - - public function getByContainer($table, $entry) - { - $this->entry = intVal($entry); - - if (!in_array($table, $this->lootTemplates) || !$this->entry) - return null; - - /* - todo (high): implement conditions on loot (and conditions in general) - - also - - // 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 = []; - $struct = self::getByContainerRecursive($table, $this->entry, $handledRefs); - if (!$struct) - return false; - - $items = new ItemList(array(['i.id', $struct[1]], CFG_SQL_LIMIT_NONE)); - $this->jsGlobals = $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 ($struct[0] 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 ($_ = self::createStack($loot)) - $base['pctstack'] = $_; - - if (empty($loot['reference'])) // regular drop - { - 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[] 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($entry, $maxResults = CFG_SQL_LIMIT_DEFAULT, $lootTableList = []) - { - $this->entry = intVal($entry); - - if (!$this->entry) - return false; - - // [fileName, tabData, tabName, tabId, extraCols, hiddenCols, visibleCols] - $tabsFinal = array( - ['item', [], '$LANG.tab_containedin', 'contained-in-item', [], [], []], - ['item', [], '$LANG.tab_disenchantedfrom', 'disenchanted-from', [], [], []], - ['item', [], '$LANG.tab_prospectedfrom', 'prospected-from', [], [], []], - ['item', [], '$LANG.tab_milledfrom', 'milled-from', [], [], []], - ['creature', [], '$LANG.tab_droppedby', 'dropped-by', [], [], []], - ['creature', [], '$LANG.tab_pickpocketedfrom', 'pickpocketed-from', [], [], []], - ['creature', [], '$LANG.tab_skinnedfrom', 'skinned-from', [], [], []], - ['creature', [], '$LANG.tab_minedfromnpc', 'mined-from-npc', [], [], []], - ['creature', [], '$LANG.tab_salvagedfrom', 'salvaged-from', [], [], []], - ['creature', [], '$LANG.tab_gatheredfromnpc', 'gathered-from-npc', [], [], []], - ['quest', [], '$LANG.tab_rewardfrom', 'reward-from-quest', [], [], []], - ['zone', [], '$LANG.tab_fishedin', 'fished-in-zone', [], [], []], - ['object', [], '$LANG.tab_containedin', 'contained-in-object', [], [], []], - ['object', [], '$LANG.tab_minedfrom', 'mined-from-object', [], [], []], - ['object', [], '$LANG.tab_gatheredfrom', 'gathered-from-object', [], [], []], - ['object', [], '$LANG.tab_fishedin', 'fished-in-object', [], [], []], - ['spell', [], '$LANG.tab_createdby', 'created-by', [], [], []], - ['achievement', [], '$LANG.tab_rewardfrom', 'reward-from-achievement', [], [], []] - ); - $refResults = []; - $chanceMods = []; - $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(lt2.chance) 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'; - - $calcChance = function ($refs, $parents = []) use (&$chanceMods) - { - $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($chanceMods[$ref['item']])) - { - $chance *= $chanceMods[$ref['item']][0]; - $chance = 1 - pow(1 - $chance, $chanceMods[$ref['item']][1]); - } - - // save chance for parent-ref - $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); - }; - - /* - get references containing the item - */ - $newRefs = DB::World()->select( - sprintf($query, 'lt1.item = ?d AND lt1.reference = 0'), - LOOT_REFERENCE, LOOT_REFERENCE, - $this->entry - ); - - while ($newRefs) - { - $curRefs = $newRefs; - $newRefs = DB::World()->select( - sprintf($query, 'lt1.reference IN (?a)'), - LOOT_REFERENCE, LOOT_REFERENCE, - array_keys($curRefs) - ); - - $refResults += $calcChance($curRefs, array_column($newRefs, 'item')); - } - - /* - search the real loot-templates for the itemId and gathered refds - */ - for ($i = 1; $i < count($this->lootTemplates); $i++) - { - if ($lootTableList && !in_array($this->lootTemplates[$i], $lootTableList)) - continue; - - $result = $calcChance(DB::World()->select( - sprintf($query, '{lt1.reference IN (?a) OR }(lt1.reference = 0 AND lt1.item = ?d)'), - $this->lootTemplates[$i], $this->lootTemplates[$i], - $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 skinnig-loot as these templates are shared for several tabs (fish, herb, ore) (herb, ore, leather) - $ids = array_slice(array_keys($result), 0, $maxResults); - - switch ($this->lootTemplates[$i]) - { - 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 $__id => $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['itemCreate']], ['effect1AuraId', SpellList::$auras['itemCreate']]]], - ['AND', ['effect2CreateItemId', $this->entry], ['OR', ['effect2Id', SpellList::$effects['itemCreate']], ['effect2AuraId', SpellList::$auras['itemCreate']]]], - ['AND', ['effect3CreateItemId', $this->entry], ['OR', ['effect3Id', SpellList::$effects['itemCreate']], ['effect3AuraId', SpellList::$auras['itemCreate']]]], - ); - 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'])) - $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; - - switch ($tabsFinal[abs($tabId)][0]) - { - case 'creature': // new CreatureList - case 'item': // new ItemList - case 'zone': // new ZoneList - $oName = ucFirst($tabsFinal[abs($tabId)][0]).'List'; - $srcObj = new $oName(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_HERBLOOT) - $tabId = 9; - else if ($tabId < 0 && $curTpl['typeFlags'] & NPC_TYPEFLAG_ENGINEERLOOT) - $tabId = 8; - else if ($tabId < 0 && $curTpl['typeFlags'] & NPC_TYPEFLAG_MININGLOOT) - $tabId = 7; - else if ($tabId < 0) - $tabId = abs($tabId); // general case (skinning) - - $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] = [$data[0], $tabData]; - } - - - return true; - } -} - -?> \ No newline at end of file diff --git a/includes/markup.class.php b/includes/markup.class.php deleted file mode 100644 index 8a4f3fc8..00000000 --- a/includes/markup.class.php +++ /dev/null @@ -1,148 +0,0 @@ -text = $text; - } - - public function parseGlobalsFromText(&$jsg = []) - { - if (preg_match_all(self::$dbTagPattern, $this->text, $matches, PREG_SET_ORDER)) - { - foreach ($matches as $match) - { - if ($match[1] == 'statistic') - $match[1] = 'achievement'; - else if ($match[1] == 'icondb') - $match[1] = 'icon'; - else if ($match[1] == 'money') - { - if (stripos($match[0], 'items')) - { - if (preg_match('/items=([0-9,]+)/i', $match[0], $submatch)) - { - $sm = explode(',', $submatch[1]); - for ($i = 0; $i < count($sm); $i+=2) - $this->jsGlobals[TYPE_ITEM][$sm[$i]] = $sm[$i]; - } - } - - if (stripos($match[0], 'currency')) - { - if (preg_match('/currency=([0-9,]+)/i', $match[0], $submatch)) - { - $sm = explode(',', $submatch[1]); - for ($i = 0; $i < count($sm); $i+=2) - $this->jsGlobals[TYPE_CURRENCY][$sm[$i]] = $sm[$i]; - } - } - } - else if ($type = array_search($match[1], Util::$typeStrings)) - $this->jsGlobals[$type][$match[2]] = $match[2]; - } - } - - Util::mergeJsGlobals($jsg, $this->jsGlobals); - - return $this->jsGlobals; - } - - public function stripTags($globals = []) - { - // since this is an article the db-tags should already be parsed - $text = preg_replace_callback(self::$dbTagPattern, function ($match) use ($globals) { - if ($match[1] == 'statistic') - $match[1] = 'achievement'; - else if ($match[1] == 'icondb') - $match[1] = 'icon'; - else if ($match[1] == 'money') - { - $moneys = []; - if (stripos($match[0], 'items')) - { - if (preg_match('/items=([0-9,]+)/i', $match[0], $submatch)) - { - $sm = explode(',', $submatch[1]); - for ($i = 0; $i < count($sm); $i += 2) - { - if (!empty($globals[TYPE_ITEM][1][$sm[$i]])) - $moneys[] = $globals[TYPE_ITEM][1][$sm[$i]]['name']; - else - $moneys[] = Util::ucFirst(Lang::game('item')).' #'.$sm[$i]; - } - } - } - - if (stripos($match[0], 'currency')) - { - if (preg_match('/currency=([0-9,]+)/i', $match[0], $submatch)) - { - $sm = explode(',', $submatch[1]); - for ($i = 0; $i < count($sm); $i += 2) - { - if (!empty($globals[TYPE_CURRENCY][1][$sm[$i]])) - $moneys[] = $globals[TYPE_CURRENCY][1][$sm[$i]]['name']; - else - $moneys[] = Util::ucFirst(Lang::game('curency')).' #'.$sm[$i]; - } - } - } - - return Lang::concat($moneys); - } - - if ($type = array_search($match[1], Util::$typeStrings)) - { - if (!empty($globals[$type][1][$match[2]])) - return $globals[$type][1][$match[2]]['name']; - else - return Util::ucFirst(Lang::game($match[1])).' #'.$match[2]; - } - - trigger_error('Markup::stripTags() - encountered unhandled db-tag: '.var_export($match)); - return ''; - }, $this->text); - - $text = str_replace('[br]', "\n", $text); - $stripped = ''; - - $inTag = false; - for ($i = 0; $i < strlen($text); $i++) - { - if ($text[$i] == '[') - $inTag = true; - if (!$inTag) - $stripped .= $text[$i]; - if ($text[$i] == ']') - $inTag = false; - } - - return $stripped; - } - - public function fromHtml() - { - } - - public function toHtml() - { - } -} - -?> diff --git a/includes/profiler.class.php b/includes/profiler.class.php deleted file mode 100644 index 6be872d5..00000000 --- a/includes/profiler.class.php +++ /dev/null @@ -1,828 +0,0 @@ - [INVTYPE_HEAD], // head - 2 => [INVTYPE_NECK], // neck - 3 => [INVTYPE_SHOULDERS], // shoulder - 4 => [INVTYPE_BODY], // shirt - 5 => [INVTYPE_CHEST, INVTYPE_ROBE], // chest - 6 => [INVTYPE_WAIST], // waist - 7 => [INVTYPE_LEGS], // legs - 8 => [INVTYPE_FEET], // feet - 9 => [INVTYPE_WRISTS], // wrists - 10 => [INVTYPE_HANDS], // hands - 11 => [INVTYPE_FINGER], // finger1 - 12 => [INVTYPE_FINGER], // finger2 - 13 => [INVTYPE_TRINKET], // trinket1 - 14 => [INVTYPE_TRINKET], // trinket2 - 15 => [INVTYPE_CLOAK], // chest - 16 => [INVTYPE_WEAPONMAINHAND, INVTYPE_WEAPON, INVTYPE_2HWEAPON], // mainhand - 17 => [INVTYPE_WEAPONOFFHAND, INVTYPE_WEAPON, INVTYPE_HOLDABLE, INVTYPE_SHIELD], // offhand - 18 => [INVTYPE_RANGED, INVTYPE_THROWN, INVTYPE_RELIC], // ranged + relic - 19 => [INVTYPE_TABARD], // tabard - ); - - public static $raidProgression = array( // statisticAchievement => relevantCriterium - 1098 => 3271, // Onyxia's Lair 10 - 1756 => 13345, // Onyxia's Lair 25 - 4031 => 12230, 4034 => 12234, 4038 => 12238, 4042 => 12242, 4046 => 12246, // Trial of the Crusader 25 nh - 4029 => 12231, 4035 => 12235, 4039 => 12239, 4043 => 12243, 4047 => 12247, // Trial of the Crusader 25 hc - 4030 => 12229, 4033 => 12233, 4037 => 12237, 4041 => 12241, 4045 => 12245, // Trial of the Crusader 10 hc - 4028 => 12228, 4032 => 12232, 4036 => 12236, 4040 => 12240, 4044 => 12244, // Trial of the Crusader 10 nh - 4642 => 13091, 4656 => 13106, 4661 => 13111, 4664 => 13114, 4667 => 13117, 4670 => 13120, 4673 => 13123, 4676 => 13126, 4679 => 13129, 4682 => 13132, 4685 => 13135, 4688 => 13138, // Icecrown Citadel 25 hc - 4641 => 13092, 4655 => 13105, 4660 => 13109, 4663 => 13112, 4666 => 13115, 4669 => 13118, 4672 => 13121, 4675 => 13124, 4678 => 13127, 4681 => 13130, 4683 => 13133, 4687 => 13136, // Icecrown Citadel 25 nh - 4640 => 13090, 4654 => 13104, 4659 => 13110, 4662 => 13113, 4665 => 13116, 4668 => 13119, 4671 => 13122, 4674 => 13125, 4677 => 13128, 4680 => 13131, 4684 => 13134, 4686 => 13137, // Icecrown Citadel 10 hc - 4639 => 13089, 4643 => 13093, 4644 => 13094, 4645 => 13095, 4646 => 13096, 4647 => 13097, 4648 => 13098, 4649 => 13099, 4650 => 13100, 4651 => 13101, 4652 => 13102, 4653 => 13103, // Icecrown Citadel 10 nh - // 4823 => 13467, // Ruby Sanctum 25 hc - // 4820 => 13465, // Ruby Sanctum 25 nh - // 4822 => 13468, // Ruby Sanctum 10 hc - // 4821 => 13466, // Ruby Sanctum 10 nh - ); - - public static function getBuyoutForItem($itemId) - { - 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 0; - } - - public static function queueStart(&$msg = '') - { - $queuePID = self::queueStatus(); - - if ($queuePID) - { - $msg = 'queue already running'; - return true; - } - - if (OS_WIN) // here be gremlins! .. suggested was "start /B php prQueue" as background process. but that closes itself - pclose(popen('start php prQueue --log=cache/profiling.log', 'r')); - else - exec('php prQueue --log=cache/profiling.log > /dev/null 2>/dev/null &'); - - usleep(500000); - if (self::queueStatus()) - return true; - else - { - $msg = 'failed to start queue'; - return false; - } - } - - public static function queueStatus() - { - if (!file_exists(self::PID_FILE)) - return 0; - - $pid = file_get_contents(self::PID_FILE); - $cmd = OS_WIN ? 'tasklist /NH /FO CSV /FI "PID eq %d"' : 'ps --no-headers p %d'; - - exec(sprintf($cmd, $pid), $out); - if ($out && stripos($out[0], $pid) !== false) - return $pid; - - // have pidFile but no process with this pid - self::queueFree(); - return 0; - } - - public static function queueLock($pid) - { - $queuePID = self::queueStatus(); - if ($queuePID && $queuePID != $pid) - { - trigger_error('pSync - another queue with PID #'.$queuePID.' is already running', E_USER_ERROR); - CLI::write('Profiler::queueLock() - another queue with PID #'.$queuePID.' is already runnung', CLI::LOG_ERROR); - return false; - } - - // no queue running; create or overwrite pidFile - $ok = false; - if ($fh = fopen(self::PID_FILE, 'w')) - { - if (fwrite($fh, $pid)) - $ok = true; - - fclose($fh); - } - - return $ok; - } - - public static function queueFree() - { - unlink(self::PID_FILE); - } - - public static function urlize($str, $allowLocales = false, $profile = false) - { - $search = ['<', '>', ' / ', "'"]; - $replace = ['<', '>', '-', '' ]; - $str = str_replace($search, $replace, $str); - - if ($profile) - { - $str = str_replace(['(', ')'], ['', ''], $str); - $accents = array( - "ß" => "ss", - "á" => "a", "ä" => "a", "à" => "a", "â" => "a", - "è" => "e", "ê" => "e", "é" => "e", "ë" => "e", - "í" => "i", "î" => "i", "ì" => "i", "ï" => "i", - "ñ" => "n", - "ò" => "o", "ó" => "o", "ö" => "o", "ô" => "o", - "ú" => "u", "ü" => "u", "û" => "u", "ù" => "u", - "œ" => "oe", - "Á" => "A", "Ä" => "A", "À" => "A", "Â" => "A", - "È" => "E", "Ê" => "E", "É" => "E", "Ë" => "E", - "Í" => "I", "Î" => "I", "Ì" => "I", "Ï" => "I", - "Ñ" => "N", - "Ò" => "O", "Ó" => "O", "Ö" => "O", "Ô" => "O", - "Ú" => "U", "Ü" => "U", "Û" => "U", "Ù" => "U", - "Œ" => "Oe" - ); - $str = strtr($str, $accents); - } - - $str = trim($str); - - if ($allowLocales) - $str = str_replace(' ', '-', $str); - else - $str = preg_replace('/[^a-z0-9]/i', '-', $str); - - $str = str_replace('--', '-', $str); - $str = str_replace('--', '-', $str); - - $str = rtrim($str, '-'); - $str = strtolower($str); - - return $str; - } - - public static function getRealms() - { - if (DB::isConnectable(DB_AUTH) && !self::$realms) - { - self::$realms = DB::Auth()->select('SELECT id AS ARRAY_KEY, name, IF(timezone IN (8, 9, 10, 11, 12), "eu", "us") AS region FROM realmlist WHERE allowedSecurityLevel = 0 AND gamebuild = ?d', WOW_BUILD); - foreach (self::$realms as $rId => $rData) - { - if (DB::isConnectable(DB_CHARACTERS . $rId)) - continue; - - // realm in db but no connection info set - unset(self::$realms[$rId]); - } - } - - return self::$realms; - } - - private static function queueInsert($realmId, $guid, $type, $localId) - { - 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)) - { - // not on already scheduled - recalc time and set status to PR_QUEUE_STATUS_WAITING - if ($rData['status'] != PR_QUEUE_STATUS_WAITING) - { - $newTime = CFG_DEBUG ? time() : max($rData['time'] + CFG_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); - } - } - 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); - } - - public static function scheduleResync($type, $realmId, $guid) - { - $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)) - 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)) - 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)) - self::queueInsert($realmId, $guid, TYPE_ARENA_TEAM, $newId); - - break; - default: - trigger_error('scheduling resync for unknown type #'.$type.' omiting..', E_USER_WARNING); - return 0; - } - - if (!$newId) - trigger_error('Profiler::scheduleResync() - tried to resync type #'.$type.' guid #'.$guid.' from realm #'.$realmId.' without preloaded data', E_USER_ERROR); - else if (!self::queueStart($msg)) - trigger_error('Profiler::scheduleResync() - '.$msg, E_USER_ERROR); - - return $newId; - } - - public static function resyncStatus($type, array $subjectGUIDs) - { - $response = [CFG_PROFILER_QUEUE ? 2 : 0]; // in theory you could have multiple queues; used as divisor for: (15 / x) + 2 - if (!$subjectGUIDs) - $response[] = [PR_QUEUE_STATUS_ENDED, 0, 0, PR_QUEUE_ERROR_CHAR]; - 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); - - $subjectStatus = DB::Aowow()->select('SELECT typeId AS ARRAY_KEY, status, realm 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); - 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]['errCode']]; - else - $response[] = array( - $subjectStatus[$guid]['status'], - $subjectStatus[$guid]['status'] != PR_QUEUE_STATUS_READY ? CFG_PROFILER_RESYNC_PING : 0, - array_search($type.':'.$guid, $queue) + 1, - 0, - 1 // nResycTries - unsure about this one - ); - } - } - - return $response; - } - - public static function getCharFromRealm($realmId, $charGuid) - { - $char = DB::Characters($realmId)->selectRow('SELECT c.* FROM characters c WHERE c.guid = ?d', $charGuid); - if (!$char) - return false; - - // 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 - $profileId = DB::Aowow()->selectCell('SELECT id FROM ?_profiler_profiles WHERE realm = ?d AND realmGUID = ?d', $realmId, $char['guid']); - - CLI::write('fetching char #'.$charGuid.' from realm #'.$realmId); - CLI::write('writing...'); - - - /*************/ - /* equipment */ - /*************/ - - /* enchantment-Indizes - * 0: permEnchant - * 3: tempEnchant - * 6: gem1 - * 9: gem2 - * 12: gem3 - * 15: socketBonus [not used] - * 18: extraSocket [only check existance] - * 21 - 30: randomProp enchantments - */ - - - 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']); - - $gemItems = []; - $permEnch = []; - $mhItem = 0; - $ohItem = 0; - - foreach ($items as $slot => $item) - { - $ench = explode(' ', $item['enchantments']); - $gEnch = []; - foreach ([6, 9, 12] as $idx) - if ($ench[$idx]) - $gEnch[$idx] = $ench[$idx]; - - if ($gEnch) - { - $gi = DB::Aowow()->selectCol('SELECT gemEnchantmentId AS ARRAY_KEY, id FROM ?_items WHERE class = 3 AND gemEnchantmentId IN (?a)', $gEnch); - foreach ($gEnch as $eId) - { - if (isset($gemItems[$eId])) - $gemItems[$eId][1]++; - else - $gemItems[$eId] = [$gi[$eId], 1]; - } - } - - if ($slot + 1 == 16) - $mhItem = $item['itemEntry']; - if ($slot + 1 == 17) - $ohItem = $item['itemEntry']; - - if ($ench[0]) - $permEnch[$slot] = $ench[0]; - - $data = array( - 'id' => $profileId, - 'slot' => $slot + 1, - 'item' => $item['itemEntry'], - 'subItem' => $item['randomPropertyId'], - 'permEnchant' => $ench[0], - 'tempEnchant' => $ench[3], - 'extraSocket' => (int)!!$ench[18], - 'gem1' => isset($gemItems[$ench[6]]) ? $gemItems[$ench[6]][0] : 0, - 'gem2' => isset($gemItems[$ench[9]]) ? $gemItems[$ench[9]][0] : 0, - 'gem3' => isset($gemItems[$ench[12]]) ? $gemItems[$ench[12]][0] : 0, - '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)); - } - - CLI::write(' ..inventory'); - - - /**************/ - /* basic info */ - /**************/ - - $data = array( - 'realm' => $realmId, - 'realmGUID' => $charGuid, - 'name' => $char['name'], - 'race' => $char['race'], - 'class' => $char['class'], - 'level' => $char['level'], - 'gender' => $char['gender'], - 'skincolor' => $char['playerBytes'] & 0xFF, - 'facetype' => ($char['playerBytes'] >> 8) & 0xFF, // maybe features - 'hairstyle' => ($char['playerBytes'] >> 16) & 0xFF, - 'haircolor' => ($char['playerBytes'] >> 24) & 0xFF, - 'features' => $char['playerBytes2'] & 0xFF, // maybe facetype - 'title' => $char['chosenTitle'] ? DB::Aowow()->selectCell('SELECT id FROM ?_titles WHERE bitIdx = ?d', $char['chosenTitle']) : 0, - 'playedtime' => $char['totaltime'], - 'nomodelMask' => ($char['playerFlags'] & 0x400 ? (1 << SLOT_HEAD) : 0) | ($char['playerFlags'] & 0x800 ? (1 << SLOT_BACK) : 0), - 'talenttree1' => 0, - 'talenttree2' => 0, - 'talenttree3' => 0, - 'talentbuild1' => '', - 'talentbuild2' => '', - 'glyphs1' => '', - 'glyphs2' => '', - 'activespec' => $char['activespec'], - 'guild' => null, - 'guildRank' => null, - 'gearscore' => 0, - 'achievementpoints' => 0 - ); - - - /********************/ - /* talents + glyphs */ - /********************/ - - $t = DB::Characters($realmId)->selectCol('SELECT spec AS ARRAY_KEY, spell AS ARRAY_KEY2, spell FROM character_talent WHERE guid = ?d', $char['guid']); - $g = DB::Characters($realmId)->select('SELECT spec 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']); - 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', !empty($t[$i]) ? $t[$i] : [0], $char['class'], $j); - $data['talentbuild'.($i + 1)] .= implode('', $_); - if ($char['activespec'] == $i) - $data['talenttree'.($j + 1)] = array_sum($_); - } - - // glyphs - if (isset($g[$i])) - { - $gProps = []; - for ($j = 1; $j <= 6; $j++) - if ($g[$i]['g'.$j]) - $gProps[$j] = $g[$i]['g'.$j]; - - if ($gProps) - if ($gItems = DB::Aowow()->selectCol('SELECT i.id FROM ?_glyphproperties gp JOIN ?_spell s ON s.effect1MiscValue = gp.id AND s.effect1Id = 74 JOIN ?_items i ON i.class = 16 AND i.spellId1 = s.id WHERE gp.id IN (?a)', $gProps)) - $data['glyphs'.($i + 1)] = implode(':', $gItems); - } - } - - $t = array( - 'spent' => [$data['talenttree1'], $data['talenttree2'], $data['talenttree3']], - 'spec' => 0 - ); - if ($t['spent'][0] > $t['spent'][1] && $t['spent'][0] > $t['spent'][2]) - $t['spec'] = 1; - else if ($t['spent'][1] > $t['spent'][0] && $t['spent'][1] > $t['spent'][2]) - $t['spec'] = 2; - else if ($t['spent'][2] > $t['spent'][1] && $t['spent'][2] > $t['spent'][0]) - $t['spec'] = 3; - - // calc gearscore - if ($items) - $data['gearscore'] += (new ItemList(array(['id', array_column($items, 'itemEntry')])))->getScoreTotal($data['class'], $t, $mhItem, $ohItem); - - if ($gemItems) - { - $gemScores = new ItemList(array(['id', array_column($gemItems, 0)])); - foreach ($gemItems as list($itemId, $mult)) - if (isset($gemScores->json[$itemId]['gearscore'])) - $data['gearscore'] += $gemScores->json[$itemId]['gearscore'] * $mult; - } - - if ($permEnch) // fuck this shit .. we are guestimating this! - { - // 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); - foreach ($permEnch as $eId) - { - if ($x = Util::getEnchantmentScore(0, 0, !!$profSpec[$eId][1], $eId)) - $data['gearscore'] += $x; - else if ($profSpec[$eId][0] != 776) // not runeforging - $data['gearscore'] += 17; // assume high quality enchantment for unknown cases - } - } - - $data['lastupdated'] = time(); - - CLI::write(' ..basic info'); - - - /***************/ - /* hunter pets */ - /***************/ - - if ((1 << ($char['class'] - 1)) == CLASS_HUNTER) - { - DB::Aowow()->query('DELETE FROM ?_profiler_pets WHERE owner = ?d', $profileId); - $pets = DB::Characters($realmId)->select('SELECT id AS ARRAY_KEY, id, entry, modelId, name FROM character_pet WHERE owner = ?d', $charGuid); - foreach ($pets as $petGuid => $petData) - { - $morePet = DB::Aowow()->selectRow('SELECT p.`type`, c.family FROM ?_pet p JOIN ?_creature c ON c.family = p.id WHERE c.id = ?d', $petData['entry']); - $petSpells = DB::Characters($realmId)->selectCol('SELECT spell FROM pet_spell WHERE guid = ?d', $petGuid); - - $_ = 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 id ORDER BY row, col ASC', $petSpells ?: [0], 1 << $morePet['type']); - $pet = array( - 'id' => $petGuid, - 'owner' => $profileId, - 'name' => $petData['name'], - 'family' => $morePet['family'], - 'npc' => $petData['entry'], - 'displayId' => $petData['modelId'], - 'talents' => implode('', $_) - ); - - DB::Aowow()->query('INSERT INTO ?_profiler_pets (?#) VALUES (?a)', array_keys($pet), array_values($pet)); - } - - CLI::write(' ..hunter pets'); - } - - - /*******************/ - /* completion data */ - /*******************/ - - DB::Aowow()->query('DELETE FROM ?_profiler_completion WHERE id = ?d', $profileId); - - // done quests - if ($quests = DB::Characters($realmId)->select('SELECT ?d AS id, ?d AS `type`, quest AS typeId FROM character_queststatus_rewarded WHERE guid = ?d', $profileId, TYPE_QUEST, $char['guid'])) - foreach (Util::createSqlBatchInsert($quests) as $q) - DB::Aowow()->query('INSERT INTO ?_profiler_completion (?#) VALUES '.$q, array_keys($quests[0])); - - CLI::write(' ..quests'); - - - // known skills (professions only) - $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, ?d AS `type`, skill AS typeId, `value` AS cur, max FROM character_skills WHERE guid = ?d AND skill IN (?a)', $profileId, TYPE_SKILL, $char['guid'], $skAllowed); - - // manually apply racial profession bonuses - foreach ($skills as &$sk) - { - // Blood Elves - Arcane Affinity - if ($sk['typeId'] == 333 && $char['race'] == 10) - { - $sk['cur'] += 10; - $sk['max'] += 10; - } - // Draenei - Gemcutting - if ($sk['typeId'] == 755 && $char['race'] == 11) - { - $sk['cur'] += 5; - $sk['max'] += 5; - } - // Tauren - Cultivation - // Gnomes - Engineering Specialization - if (($sk['typeId'] == 182 && $char['race'] == 6) || - ($sk['typeId'] == 202 && $char['race'] == 7)) - { - $sk['cur'] += 15; - $sk['max'] += 15; - } - } - unset($sk); - - if ($skills) - foreach (Util::createSqlBatchInsert($skills) as $sk) - DB::Aowow()->query('INSERT INTO ?_profiler_completion (?#) VALUES '.$sk, array_keys($skills[0])); - - CLI::write(' ..professions'); - - - // reputation - if ($reputation = DB::Characters($realmId)->select('SELECT ?d AS id, ?d AS `type`, faction AS typeId, standing AS cur FROM character_reputation WHERE guid = ?d AND (flags & 0xC) = 0', $profileId, TYPE_FACTION, $char['guid'])) - foreach (Util::createSqlBatchInsert($reputation) as $rep) - DB::Aowow()->query('INSERT INTO ?_profiler_completion (?#) VALUES '.$rep, array_keys($reputation[0])); - - CLI::write(' ..reputation'); - - - // known titles - $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 SELECT ?d, ?d, id, NULL, NULL FROM ?_titles WHERE bitIdx IN (?a)', $profileId, TYPE_TITLE, $indizes); - - CLI::write(' ..titles'); - - - // achievements - if ($achievements = DB::Characters($realmId)->select('SELECT ?d AS id, ?d AS `type`, achievement AS typeId, date AS cur FROM character_achievement WHERE guid = ?d', $profileId, TYPE_ACHIEVEMENT, $char['guid'])) - { - foreach (Util::createSqlBatchInsert($achievements) as $a) - DB::Aowow()->query('INSERT INTO ?_profiler_completion (?#) VALUES '.$a, array_keys($achievements[0])); - - $data['achievementpoints'] = DB::Aowow()->selectCell('SELECT SUM(points) FROM ?_achievement WHERE id IN (?a)', array_column($achievements, 'typeId')); - } - - CLI::write(' ..achievements'); - - - // raid progression - if ($progress = DB::Characters($realmId)->select('SELECT ?d AS id, ?d AS `type`, criteria AS typeId, date AS cur, counter AS `max` FROM character_achievement_progress WHERE guid = ?d AND criteria IN (?a)', $profileId, TYPE_ACHIEVEMENT, $char['guid'], self::$raidProgression)) - { - array_walk($progress, function (&$val) { $val['typeId'] = array_search($val['typeId'], self::$raidProgression); }); - foreach (Util::createSqlBatchInsert($progress) as $p) - DB::Aowow()->query('INSERT INTO ?_profiler_completion (?#) VALUES '.$p, array_keys($progress[0])); - } - - CLI::write(' ..raid progression'); - - - // known spells - if ($spells = DB::Characters($realmId)->select('SELECT ?d AS id, ?d AS `type`, spell AS typeId FROM character_spell WHERE guid = ?d AND disabled = 0', $profileId, TYPE_SPELL, $char['guid'])) - foreach (Util::createSqlBatchInsert($spells) as $s) - DB::Aowow()->query('INSERT INTO ?_profiler_completion (?#) VALUES '.$s, array_keys($spells[0])); - - CLI::write(' ..known spells (vanity pets & mounts)'); - - - /****************/ - /* related data */ - /****************/ - - // 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'])) - { - $guildId = 0; - if (!($guildId = DB::Aowow()->selectCell('SELECT id FROM ?_profiler_guild WHERE realm = ?d AND realmGUID = ?d', $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 - ); - - $guildId = DB::Aowow()->query('INSERT IGNORE INTO ?_profiler_guild (?#) VALUES (?a)', array_keys($gData), array_values($gData)); - } - - $data['guild'] = $guildId; - $data['guildRank'] = $guild['rank']; - } - - - // 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']); - 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))) - { - $team = array( // only most basic data - 'realm' => $realmId, - 'realmGUID' => $rGuid, - 'name' => $t['name'], - 'nameUrl' => self::urlize($t['name']), - 'type' => $t['type'], - 'cuFlags' => PROFILER_CU_NEEDS_RESYNC - ); - - $teamId = DB::Aowow()->query('INSERT IGNORE INTO ?_profiler_arena_team (?#) VALUES (?a)', array_keys($team), array_values($team)); - } - - $member = array( - 'arenaTeamId' => $teamId, - 'profileId' => $profileId, - 'captain' => $t['captain'], - 'weekGames' => $t['weekGames'], - 'weekWins' => $t['weekWins'], - 'seasonGames' => $t['seasonGames'], - 'seasonWins' => $t['seasonWins'], - 'personalRating' => $t['personalRating'] - ); - - 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)); - } - - CLI::write(' ..associated arena teams'); - - /*********************/ - /* 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); - - return true; - } - - public static function getGuildFromRealm($realmId, $guildGuid) - { - $guild = DB::Characters($realmId)->selectRow('SELECT guildId, name, createDate, info, backgroundColor, emblemStyle, emblemColor, borderStyle, borderColor FROM guild WHERE guildId = ?d', $guildGuid); - if (!$guild) - return false; - - // 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']); - - CLI::write('fetching guild #'.$guildGuid.' from realm #'.$realmId); - CLI::write('writing...'); - - - /**************/ - /* Guild Data */ - /**************/ - - 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); - - // 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))); - - CLI::write(' ..guild data'); - - - /***************/ - /* Member Data */ - /***************/ - - $conditions = array( - ['g.guildid', $guildGuid], - ['deleteInfos_Account', null], - ['level', MAX_LEVEL, '<='], // prevents JS errors - [['extra_flags', self::CHAR_GMFLAGS, '&'], 0] // not a staff char - ); - - // this here should all happen within ProfileList - $members = new RemoteProfileList($conditions, ['sv' => $realmId]); - if (!$members->error) - $members->initializeLocalEntries(); - else - return false; - - CLI::write(' ..guild members'); - - - /*********************/ - /* mark guild as done */ - /*********************/ - - DB::Aowow()->query('UPDATE ?_profiler_guild SET cuFlags = cuFlags & ?d WHERE id = ?d', ~PROFILER_CU_NEEDS_RESYNC, $guildId); - - return true; - } - - public static function getArenaTeamFromRealm($realmId, $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 = ?d', $teamGuid); - if (!$team) - return false; - - // 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']); - - CLI::write('fetching arena team #'.$teamGuid.' from realm #'.$realmId); - CLI::write('writing...'); - - - /*************/ - /* Team Data */ - /*************/ - - $captain = $team['captainGuid']; - unset($team['captainGuid']); - unset($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); - - CLI::write(' ..team data'); - - - /***************/ - /* Member Data */ - /***************/ - - $members = DB::Characters($realmId)->select(' - 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', - MAX_LEVEL, - self::CHAR_GMFLAGS, - $teamGuid - ); - - $conditions = array( - ['c.guid', array_keys($members)], - ['deleteInfos_Account', null], - ['level', MAX_LEVEL, '<='], // prevents JS errors - [['extra_flags', self::CHAR_GMFLAGS, '&'], 0] // not a staff char - ); - - $mProfiles = new RemoteProfileList($conditions, ['sv' => $realmId]); - if (!$mProfiles->error) - { - $mProfiles->initializeLocalEntries(); - foreach ($mProfiles->iterate() as $__) - { - - $mGuid = $mProfiles->getField('guid'); - - $members[$mGuid]['arenaTeamId'] = $teamId; - $members[$mGuid]['captain'] = (int)($mGuid == $captain); - $members[$mGuid]['profileId'] = $mProfiles->getField('id'); - } - - DB::Aowow()->query('DELETE FROM ?_profiler_arena_team_member WHERE arenaTeamId = ?d', $teamId); - - foreach (Util::createSqlBatchInsert($members) as $m) - DB::Aowow()->query('INSERT INTO ?_profiler_arena_team_member (?#) VALUES '.$m, array_keys(reset($members))); - - } - else - return false; - - CLI::write(' ..team members'); - - /*********************/ - /* mark team as done */ - /*********************/ - - DB::Aowow()->query('UPDATE ?_profiler_arena_team SET cuFlags = cuFlags & ?d WHERE id = ?d', ~PROFILER_CU_NEEDS_RESYNC, $teamId); - - return true; - } -} - -?> diff --git a/includes/setup/cli.class.php b/includes/setup/cli.class.php new file mode 100644 index 00000000..ef16296f --- /dev/null +++ b/includes/setup/cli.class.php @@ -0,0 +1,350 @@ + $row) + { + if (!is_array($out[0])) + { + unset($out[$i]); + continue; + } + + $nCols = max($nCols, count($row)); + + for ($j = 0; $j < $nCols; $j++) + $pads[$j] = max($pads[$j] ?? 0, mb_strlen(self::purgeEscapes($row[$j] ?? ''))); + } + + foreach ($out as $i => $row) + { + for ($j = 0; $j < $nCols; $j++) + { + if (!isset($row[$j])) + break; + + $len = ($pads[$j] - mb_strlen(self::purgeEscapes($row[$j]))); + for ($k = 0; $k < $len; $k++) // can't use str_pad(). it counts invisible chars. + $row[$j] .= ' '; + } + + if ($i || $headless) + self::write(' '.implode(' ' . self::tblDelim(' ') . ' ', $row), self::LOG_NONE, $timestamp); + else + self::write(self::tblHead(' '.implode(' ', $row)), self::LOG_NONE, $timestamp); + } + + if (!$headless) + self::write(self::tblHead(str_pad('', array_sum($pads) + count($pads) * 3 - 2)), self::LOG_NONE, $timestamp); + + self::write(); + } + + + /***********/ + /* logging */ + /***********/ + + public static function initLogFile(string $file = '') : void + { + if (!$file) + return; + + $file = self::nicePath($file); + if (!file_exists($file)) + self::$logHandle = fopen($file, 'w'); + else + { + $logFileParts = pathinfo($file); + + $i = 1; + while (file_exists($logFileParts['dirname'].'/'.$logFileParts['filename'].$i.(isset($logFileParts['extension']) ? '.'.$logFileParts['extension'] : ''))) + $i++; + + $file = $logFileParts['dirname'].'/'.$logFileParts['filename'].$i.(isset($logFileParts['extension']) ? '.'.$logFileParts['extension'] : ''); + self::$logHandle = fopen($file, 'w'); + } + } + + private static function tblHead(string $str) : string + { + return CLI_HAS_E ? "\e[1;48;5;236m".$str."\e[0m" : $str; + } + + private static function tblDelim(string $str) : string + { + return CLI_HAS_E ? "\e[48;5;236m".$str."\e[0m" : $str; + } + + public static function grey(string $str) : string + { + return CLI_HAS_E ? "\e[90m".$str."\e[0m" : $str; + } + + public static function red(string $str) : string + { + return CLI_HAS_E ? "\e[31m".$str."\e[0m" : $str; + } + + public static function green(string $str) : string + { + return CLI_HAS_E ? "\e[32m".$str."\e[0m" : $str; + } + + public static function yellow(string $str) : string + { + return CLI_HAS_E ? "\e[33m".$str."\e[0m" : $str; + } + + public static function blue(string $str) : string + { + return CLI_HAS_E ? "\e[36m".$str."\e[0m" : $str; + } + + public static function bold(string $str) : string + { + return CLI_HAS_E ? "\e[1m".$str."\e[0m" : $str; + } + + public static function write(string $txt = '', int $lvl = self::LOG_BLANK, bool $timestamp = true, bool $tmpRow = false) : void + { + $msg = ''; + if ($txt) + { + if ($timestamp) + $msg = str_pad(date('H:i:s'), 10); + + $msg .= match ($lvl) + { + 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; + } + + // https://shiroyasha.svbtle.com/escape-sequences-a-quick-guide-1#movement_1 + $msg = (self::$overwriteLast && CLI_HAS_E ? "\e[1G\e[0K" : "\n") . $msg; + self::$overwriteLast = $tmpRow; + + fwrite($lvl == self::LOG_ERROR ? STDERR : STDOUT, $msg); + + if (self::$logHandle) // remove control sequences from log + fwrite(self::$logHandle, self::purgeEscapes($msg)); + + flush(); + } + + private static function purgeEscapes(string $msg) : string + { + return preg_replace(["/\e\[[\d;]+[mK]/", "/\e\[\d+G/"], ['', "\n"], $msg); + } + + public static function nicePath(string $fileOrPath, string ...$pathParts) : string + { + $path = ''; + + if ($pathParts) + { + foreach ($pathParts as &$pp) + $pp = trim($pp); + + $path .= implode(DIRECTORY_SEPARATOR, $pathParts); + } + + $path .= ($path ? DIRECTORY_SEPARATOR : '').trim($fileOrPath); + + // remove double quotes (from erroneous user input), single quotes are + // valid chars for filenames and removing those mutilates several wow icons + $path = str_replace('"', '', $path); + + if (!$path) // empty strings given. (faulty dbc data?) + return ''; + + if (DIRECTORY_SEPARATOR == '/') // *nix + { + $path = str_replace('\\', '/', $path); + $path = preg_replace('/\/+/i', '/', $path); + } + else if (DIRECTORY_SEPARATOR == '\\') // win + { + $path = str_replace('/', '\\', $path); + $path = preg_replace('/\\\\+/i', '\\', $path); + } + else + self::write('Dafuq! Your directory separator is "'.DIRECTORY_SEPARATOR.'". Please report this!', self::LOG_ERROR); + + // resolve *nix home shorthand + if (!OS_WIN) + { + if (preg_match('/^~(\w+)\/.*/i', $path, $m)) + $path = '/home/'.substr($path, 1); + else if (substr($path, 0, 2) == '~/') + $path = getenv('HOME').substr($path, 1); + else if ($path[0] == DIRECTORY_SEPARATOR && substr($path, 0, 6) != '/home/') + $path = substr($path, 1); + } + + return $path; + } + + + /**************/ + /* read input */ + /**************/ + + /* + since the CLI on WIN ist not interactive, the following things have to be considered + you do not receive keystrokes but whole strings upon pressing (wich also appends a \r) + as such and probably other control chars can not be registered + this also means, you can't hide input at all, least process it + */ + + public static function read(array $fields, ?array &$userInput = []) : bool + { + // first time set + if (self::$hasReadline === null) + self::$hasReadline = function_exists('readline_callback_handler_install'); + + // prevent default output if able + if (self::$hasReadline) + readline_callback_handler_install('', function() { }); + + if (!STDIN) + return false; + + stream_set_blocking(STDIN, false); + + // pad default values onto $fields + array_walk($fields, function(&$val, $_, $pad) { $val += $pad; }, ['', false, false, '']); + + foreach ($fields as $name => [$desc, $isHidden, $singleChar, $validPattern]) + { + $charBuff = ''; + + if ($desc) + fwrite(STDOUT, "\n".$desc.": "); + + while (true) { + if (feof(STDIN)) + return false; + + $r = [STDIN]; + $w = $e = null; + $n = stream_select($r, $w, $e, 200000); + + if (!$n || !in_array(STDIN, $r)) + 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 can be empty if used non-interactive + if (!$chars) + return false; + + $ordinals = array_map('ord', $chars); + + if ($ordinals[0] == self::CHR_ESC) + { + if (count($ordinals) == 1) + { + fwrite(STDOUT, chr(self::CHR_BELL)); + return false; + } + else + continue; + } + + foreach ($chars as $idx => $char) + { + $keyId = $ordinals[$idx]; + + // skip char if horizontal tab or \r if followed by \n + if ($keyId == self::CHR_TAB || ($keyId == self::CHR_CR && ($ordinals[$idx + 1] ?? '') == self::CHR_LF)) + continue; + + if ($keyId == self::CHR_BACKSPACE) + { + if (!$charBuff) + continue 2; + + $charBuff = mb_substr($charBuff, 0, -1); + if (!$isHidden && self::$hasReadline) + fwrite(STDOUT, chr(self::CHR_BACK)." ".chr(self::CHR_BACK)); + } + // standalone \n or \r + else if ($keyId == self::CHR_LF || $keyId == self::CHR_CR) + { + $userInput[$name] = $charBuff; + break 2; + } + else if (!$validPattern || preg_match($validPattern, $char)) + { + $charBuff .= $char; + if (!$isHidden && self::$hasReadline) + fwrite(STDOUT, $char); + + if ($singleChar && self::$hasReadline) + { + $userInput[$name] = $charBuff; + break 2; + } + } + } + } + } + + fwrite(STDOUT, chr(self::CHR_BELL)); + + foreach ($userInput as $ui) + if (strlen($ui)) + return true; + + $userInput = null; + return true; + } +} + +?> 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 @@ +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 @@ +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 @@ +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 new file mode 100644 index 00000000..7c36620d --- /dev/null +++ b/includes/setup/timer.class.php @@ -0,0 +1,39 @@ +intv = $intervall / 1000; // in msec + $this->t_cur = microtime(true); + } + + public function update() : bool + { + $this->t_new = microtime(true); + if ($this->t_new > $this->t_cur + $this->intv) + { + $this->t_cur = $this->t_cur + $this->intv; + return true; + } + + return false; + } + + public function reset() : void + { + $this->t_cur = microtime(true) - $this->intv; + } +} + +?> diff --git a/includes/shared.php b/includes/shared.php deleted file mode 100644 index 41f768d6..00000000 --- a/includes/shared.php +++ /dev/null @@ -1,26 +0,0 @@ -'.$r." was not found. Please check if it should exist, using \"php -m\"\n\n"; - -if (version_compare(PHP_VERSION, '5.5.0') < 0) - $error .= 'PHP Version 5.5.0 or higher required! Your version is '.PHP_VERSION.".\nCore functions are unavailable!\n"; - -if ($error) -{ - echo CLI ? strip_tags($error) : $error; - die(); -} - - -// include all necessities, set up basics -require_once 'includes/kernel.php'; - -?> diff --git a/includes/type.class.php b/includes/type.class.php new file mode 100644 index 00000000..6676009b --- /dev/null +++ b/includes/type.class.php @@ -0,0 +1,260 @@ + [CreatureList::class, 'npc', 'g_npcs', self::FLAG_RANDOM_SEARCHABLE | self::FLAG_FILTRABLE | self::FLAG_DB_TYPE], + self::OBJECT => [GameObjectList::class, 'object', 'g_objects', self::FLAG_RANDOM_SEARCHABLE | self::FLAG_FILTRABLE | self::FLAG_DB_TYPE], + self::ITEM => [ItemList::class, 'item', 'g_items', self::FLAG_RANDOM_SEARCHABLE | self::FLAG_FILTRABLE | self::FLAG_DB_TYPE | self::FLAG_HAS_ICON], + self::ITEMSET => [ItemsetList::class, 'itemset', 'g_itemsets', self::FLAG_RANDOM_SEARCHABLE | self::FLAG_FILTRABLE | self::FLAG_DB_TYPE], + self::QUEST => [QuestList::class, 'quest', 'g_quests', self::FLAG_RANDOM_SEARCHABLE | self::FLAG_FILTRABLE | self::FLAG_DB_TYPE], + self::SPELL => [SpellList::class, 'spell', 'g_spells', self::FLAG_RANDOM_SEARCHABLE | self::FLAG_FILTRABLE | self::FLAG_DB_TYPE | self::FLAG_HAS_ICON], + self::ZONE => [ZoneList::class, 'zone', 'g_gatheredzones', self::FLAG_RANDOM_SEARCHABLE | self::FLAG_DB_TYPE], + self::FACTION => [FactionList::class, 'faction', 'g_factions', self::FLAG_RANDOM_SEARCHABLE | self::FLAG_DB_TYPE], + self::PET => [PetList::class, 'pet', 'g_pets', self::FLAG_RANDOM_SEARCHABLE | self::FLAG_DB_TYPE | self::FLAG_HAS_ICON], + self::ACHIEVEMENT => [AchievementList::class, 'achievement', 'g_achievements', self::FLAG_RANDOM_SEARCHABLE | self::FLAG_FILTRABLE | self::FLAG_DB_TYPE | self::FLAG_HAS_ICON], + self::TITLE => [TitleList::class, 'title', 'g_titles', self::FLAG_RANDOM_SEARCHABLE | self::FLAG_DB_TYPE], + self::WORLDEVENT => [WorldEventList::class, 'event', 'g_holidays', self::FLAG_RANDOM_SEARCHABLE | self::FLAG_DB_TYPE | self::FLAG_HAS_ICON], + self::CHR_CLASS => [CharClassList::class, 'class', 'g_classes', self::FLAG_RANDOM_SEARCHABLE | self::FLAG_DB_TYPE], + self::CHR_RACE => [CharRaceList::class, 'race', 'g_races', self::FLAG_RANDOM_SEARCHABLE | self::FLAG_DB_TYPE], + self::SKILL => [SkillList::class, 'skill', 'g_skills', self::FLAG_RANDOM_SEARCHABLE | self::FLAG_DB_TYPE | self::FLAG_HAS_ICON], + self::STATISTIC => [AchievementList::class, 'achievement', 'g_achievements', self::FLAG_NONE], // alias for achievements; exists only for Markup + self::CURRENCY => [CurrencyList::class, 'currency', 'g_gatheredcurrencies', self::FLAG_RANDOM_SEARCHABLE | self::FLAG_DB_TYPE | self::FLAG_HAS_ICON], + self::SOUND => [SoundList::class, 'sound', 'g_sounds', self::FLAG_RANDOM_SEARCHABLE | self::FLAG_FILTRABLE | self::FLAG_DB_TYPE], + self::ICON => [IconList::class, 'icon', 'g_icons', self::FLAG_RANDOM_SEARCHABLE | self::FLAG_FILTRABLE | self::FLAG_DB_TYPE | self::FLAG_HAS_ICON], + self::GUIDE => [GuideList::class, 'guide', '', self::FLAG_DB_TYPE], + self::PROFILE => [ProfileList::class, 'profile', '', self::FLAG_FILTRABLE], // x - not known in javascript + self::GUILD => [GuildList::class, 'guild', '', self::FLAG_FILTRABLE], // x + self::ARENA_TEAM => [ArenaTeamList::class, 'arena-team', '', self::FLAG_FILTRABLE], // x + self::USER => [UserList::class, 'user', 'g_users', self::FLAG_NONE], // x + self::EMOTE => [EmoteList::class, 'emote', 'g_emotes', self::FLAG_RANDOM_SEARCHABLE | self::FLAG_DB_TYPE], + self::ENCHANTMENT => [EnchantmentList::class, 'enchantment', 'g_enchantments', self::FLAG_RANDOM_SEARCHABLE | self::FLAG_FILTRABLE | self::FLAG_DB_TYPE], + self::AREATRIGGER => [AreatriggerList::class, 'areatrigger', '', self::FLAG_FILTRABLE | self::FLAG_DB_TYPE], + self::MAIL => [MailList::class, 'mail', '', self::FLAG_RANDOM_SEARCHABLE | self::FLAG_DB_TYPE] + ); + + + /********************/ + /* Field Operations */ + /********************/ + + public static function newList(int $type, array $conditions = []) : ?DBTypeList + { + if (!self::exists($type)) + return null; + + return new (self::$data[$type][self::IDX_LIST_OBJ])($conditions); + } + + public static function newFilter(string $fileStr, array|string $data, array $opts = []) : ?Filter + { + $x = self::getFileStringsFor(self::FLAG_FILTRABLE); + if ($type = array_search($fileStr, $x)) + return new (self::$data[$type][self::IDX_LIST_OBJ].'Filter')($data, $opts); + + return null; + } + + public static function validateIds(int $type, int|array $ids) : array + { + if (!self::exists($type)) + return []; + + if (!(self::$data[$type][self::IDX_FLAGS] & self::FLAG_DB_TYPE)) + return []; + + 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 + { + return self::exists($type) && self::$data[$type][self::IDX_FLAGS] & self::FLAG_HAS_ICON; + } + + public static function isRandomSearchable(int $type) : bool + { + return self::exists($type) && self::$data[$type][self::IDX_FLAGS] & self::FLAG_RANDOM_SEARCHABLE; + } + + public static function getFileString(int $type) : string + { + if (!self::exists($type)) + return ''; + + return self::$data[$type][self::IDX_FILE_STR]; + } + + public static function getJSGlobalString(int $type) : string + { + if (!self::exists($type)) + return ''; + + return self::$data[$type][self::IDX_JSG_TPL]; + } + + public static function getJSGlobalTemplate(int $type) : array + { + if (!self::exists($type) || !self::$data[$type][self::IDX_JSG_TPL]) + return []; + + // [key, [data], [extraData]] + return [self::$data[$type][self::IDX_JSG_TPL], [], []]; + } + + public static function checkClassAttrib(int $type, string $attr, ?int $attrVal = null) : bool + { + if (!self::exists($type)) + return false; + + return isset((self::$data[$type][self::IDX_LIST_OBJ])::$$attr) && ($attrVal === null || ((self::$data[$type][self::IDX_LIST_OBJ])::$$attr & $attrVal)); + } + + public static function getClassAttrib(int $type, string $attr) : mixed + { + if (!self::exists($type)) + return null; + + return (self::$data[$type][self::IDX_LIST_OBJ])::$$attr ?? null; + } + + public static function exists(int $type) : ?int + { + return !empty(self::$data[$type]) ? $type : null; + } + + public static function getIndexFrom(int $idx, string $match) : int + { + $i = array_search($match, array_column(self::$data, $idx)); + if ($i === false) + return 0; + + return array_keys(self::$data)[$i]; + } + + + /*********************/ + /* Column Operations */ + /*********************/ + + public static function getClassesFor(int $flags = 0x0, string $attr = '', ?int $attrVal = null) : array + { + $x = []; + foreach (self::$data as $k => [$o, , , $f]) + if ($o && (!$flags || $flags & $f)) + if (!$attr || self::checkClassAttrib($k, $attr, $attrVal)) + $x[$k] = $o; + + return $x; + } + + public static function getFileStringsFor(int $flags = 0x0) : array + { + $x = []; + foreach (self::$data as $k => [, $s, , $f]) + if ($s && (!$flags || $flags & $f)) + $x[$k] = $s; + + return $x; + } + + public static function getJSGTemplatesFor(int $flags = 0x0) : array + { + $x = []; + foreach (self::$data as $k => [, , $a, $f]) + if ($a && (!$flags || $flags & $f)) + $x[$k] = $a; + + return $x; + } +} + +?> diff --git a/includes/types/arenateam.class.php b/includes/types/arenateam.class.php deleted file mode 100644 index 7bb5ebc8..00000000 --- a/includes/types/arenateam.class.php +++ /dev/null @@ -1,346 +0,0 @@ -iterate() as $__) - { - $data[$this->id] = array( - 'name' => $this->curTpl['name'], - 'realm' => Profiler::urlize($this->curTpl['realmName']), - 'realmname' => $this->curTpl['realmName'], - // 'battlegroup' => Profiler::urlize($this->curTpl['battlegroup']), // was renamed to subregion somewhere around cata release - // 'battlegroupname' => $this->curTpl['battlegroup'], - 'region' => Profiler::urlize($this->curTpl['region']), - 'faction' => $this->curTpl['faction'], - 'size' => $this->curTpl['type'], - 'rank' => $this->curTpl['rank'], - 'wins' => $this->curTpl['seasonWins'], - 'games' => $this->curTpl['seasonGames'], - 'rating' => $this->curTpl['rating'], - 'members' => $this->curTpl['members'] - ); - } - - return array_values($data); - } - - public function renderTooltip() {} - public function getJSGlobals($addMask = 0) {} -} - - -class ArenaTeamListFilter extends Filter -{ - public $extraOpts = []; - protected $genericFilter = []; - - // fieldId => [checkType, checkValue[, fieldIsArray]] - protected $inputFields = array( - 'na' => [FILTER_V_REGEX, '/[\p{C};]/ui', false], // name - only printable chars, no delimiter - 'ma' => [FILTER_V_EQUAL, 1, false], // match any / all filter - 'ex' => [FILTER_V_EQUAL, 'on', false], // only match exact - 'si' => [FILTER_V_LIST, [1, 2], false], // side - 'sz' => [FILTER_V_LIST, [2, 3, 5], false], // tema size - 'rg' => [FILTER_V_CALLBACK, 'cbRegionCheck', false], // region - 'sv' => [FILTER_V_CALLBACK, 'cbServerCheck', false], // server - ); - - protected function createSQLForCriterium(&$cr) { } - - protected function createSQLForValues() - { - $parts = []; - $_v = $this->fiData['v']; - - // region (rg), battlegroup (bg) and server (sv) are passed to ArenaTeamList as miscData and handled there - - // name [str] - if (!empty($_v['na'])) - if ($_ = $this->modularizeString(['at.name'], $_v['na'], !empty($_v['ex']) && $_v['ex'] == 'on')) - $parts[] = $_; - - // side [list] - if (!empty($_v['si'])) - { - if ($_v['si'] == 1) - $parts[] = ['c.race', [1, 3, 4, 7, 11]]; - else if ($_v['si'] == 2) - $parts[] = ['c.race', [2, 5, 6, 8, 10]]; - } - - // size [int] - if (!empty($_v['sz'])) - $parts[] = ['at.type', $_v['sz']]; - - return $parts; - } - - protected function cbRegionCheck(&$v) - { - if ($v == 'eu' || $v == 'us') - { - $this->parentCats[0] = $v; // directly redirect onto this region - $v = ''; // remove from filter - - return true; - } - - return false; - } - - protected function cbServerCheck(&$v) - { - foreach (Profiler::getRealms() as $realm) - if ($realm['name'] == $v) - { - $this->parentCats[1] = Profiler::urlize($v);// directly redirect onto this server - $v = ''; // remove from filter - - return true; - } - - return false; - } -} - - -class RemoteArenaTeamList extends ArenaTeamList -{ - protected $queryBase = 'SELECT `at`.*, `at`.`arenaTeamId` AS ARRAY_KEY FROM arena_team at'; - protected $queryOpts = array( - 'at' => [['atm', 'c'], 'g' => 'ARRAY_KEY', 'o' => 'rating DESC'], - 'atm' => ['j' => 'arena_team_member atm ON atm.arenaTeamId = at.arenaTeamId'], - 'c' => ['j' => 'characters c ON c.guid = atm.guid AND c.deleteInfos_Account IS NULL AND c.level <= 80 AND (c.extra_flags & '.Profiler::CHAR_GMFLAGS.') = 0', 's' => ', BIT_OR(IF(c.race IN (1, 3, 4, 7, 11), 1, 2)) - 1 AS faction'] - ); - - private $members = []; - - public function __construct($conditions = [], $miscData = null) - { - // select DB by realm - if (!$this->selectRealms($miscData)) - { - trigger_error('no access to auth-db or table realmlist is empty', E_USER_WARNING); - return; - } - - parent::__construct($conditions, $miscData); - - if ($this->error) - return; - - // 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); - - reset($this->dbNames); // only use when querying single realm - $realmId = key($this->dbNames); - $realms = Profiler::getRealms(); - $distrib = []; - - // post processing - foreach ($this->iterate() as $guid => &$curTpl) - { - // battlegroup - $curTpl['battlegroup'] = CFG_BATTLEGROUP; - - // realm, rank - $r = explode(':', $guid); - if (!empty($realms[$r[0]])) - { - $curTpl['realm'] = $r[0]; - $curTpl['realmName'] = $realms[$r[0]]['name']; - $curTpl['region'] = $realms[$r[0]]['region']; - $curTpl['rank'] = array_search($curTpl['arenaTeamId'], $this->rankOrder[$r[0]][$curTpl['type']]) + 1; - } - else - { - trigger_error('arena team "'.$curTpl['name'].'" belongs to nonexistant realm #'.$r, E_USER_WARNING); - unset($this->templates[$guid]); - continue; - } - - // team members - $this->members[$r[0]][$r[1]] = $r[1]; - - // equalize distribution - if (empty($distrib[$curTpl['realm']])) - $distrib[$curTpl['realm']] = 1; - else - $distrib[$curTpl['realm']]++; - } - - // get team members - foreach ($this->members as $realmId => &$teams) - $teams = DB::Characters($realmId)->select(' - 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', - $teams, - MAX_LEVEL, - Profiler::CHAR_GMFLAGS - ); - - // equalize subject distribution across realms - $limit = CFG_SQL_LIMIT_DEFAULT; - foreach ($conditions as $c) - if (is_int($c)) - $limit = $c; - - $total = array_sum($distrib); - foreach ($distrib as &$d) - $d = ceil($limit * $d / $total); - - foreach ($this->iterate() as $guid => &$curTpl) - { - if ($limit <= 0 || $distrib[$curTpl['realm']] <= 0) - { - unset($this->templates[$guid]); - continue; - } - - $r = explode(':', $guid); - if (isset($this->members[$r[0]][$r[1]])) - $curTpl['members'] = array_values($this->members[$r[0]][$r[1]]); // [name, classId, isCaptain] - - $distrib[$curTpl['realm']]--; - $limit--; - } - } - - public function initializeLocalEntries() - { - $profiles = []; - // init members for tooltips - foreach ($this->members as $realmId => $teams) - { - $gladiators = []; - foreach ($teams as $team) - $gladiators = array_merge($gladiators, array_keys($team)); - - $profiles[$realmId] = new RemoteProfileList(array(['c.guid', $gladiators], CFG_SQL_LIMIT_NONE), ['sv' => $realmId]); - - if (!$profiles[$realmId]->error) - $profiles[$realmId]->initializeLocalEntries(); - } - - $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 - ); - } - - // basic arena team data - foreach (Util::createSqlBatchInsert($data) as $ins) - DB::Aowow()->query('INSERT IGNORE INTO ?_profiler_arena_team (?#) VALUES '.$ins, array_keys(reset($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') - ); - - foreach ($this->iterate() as $guid => &$_curTpl) - if (isset($localIds[$guid])) - $_curTpl['id'] = $localIds[$guid]; - - - // profiler_arena_team_member requires profiles and arena teams to be filled - foreach ($this->members as $realmId => $teams) - { - if (empty($profiles[$realmId])) - continue; - - $memberData = []; - foreach ($teams as $teamId => $team) - foreach ($team as $memberId => $member) - $memberData[] = array( - 'arenaTeamId' => $localIds[$realmId.':'.$teamId], - 'profileId' => $profiles[$realmId]->getEntry($realmId.':'.$memberId)['id'], - 'captain' => $member[2] - ); - - foreach (Util::createSqlBatchInsert($memberData) as $ins) - DB::Aowow()->query('INSERT IGNORE INTO ?_profiler_arena_team_member (?#) VALUES '.$ins, array_keys(reset($memberData))); - } - } -} - - -class LocalArenaTeamList extends ArenaTeamList -{ - protected $queryBase = 'SELECT at.*, at.id AS ARRAY_KEY FROM ?_profiler_arena_team at'; - - public function __construct($conditions = [], $miscData = null) - { - parent::__construct($conditions, $miscData); - - if ($this->error) - return; - - $realms = Profiler::getRealms(); - - // post processing - $members = DB::Aowow()->selectCol('SELECT *, arenaTeamId AS ARRAY_KEY, profileId AS ARRAY_KEY2 FROM ?_profiler_arena_team_member WHERE arenaTeamId IN (?a)', $this->getFoundIDs()); - - foreach ($this->iterate() as $id => &$curTpl) - { - if ($curTpl['realm'] && !isset($realms[$curTpl['realm']])) - continue; - - if (isset($realms[$curTpl['realm']])) - { - $curTpl['realmName'] = $realms[$curTpl['realm']]['name']; - $curTpl['region'] = $realms[$curTpl['realm']]['region']; - } - - // battlegroup - $curTpl['battlegroup'] = CFG_BATTLEGROUP; - - $curTpl['members'] = $members[$id]; - } - } - - public function getProfileUrl() - { - $url = '?arena-team='; - - return $url.implode('.', array( - Profiler::urlize($this->getField('region')), - Profiler::urlize($this->getField('realmName')), - Profiler::urlize($this->getField('name')) - )); - } -} - - -?> diff --git a/includes/types/charrace.class.php b/includes/types/charrace.class.php deleted file mode 100644 index 9fcfd4c5..00000000 --- a/includes/types/charrace.class.php +++ /dev/null @@ -1,52 +0,0 @@ -iterate() as $__) - { - $data[$this->id] = array( - 'id' => $this->id, - 'name' => $this->getField('name', true), - 'classes' => $this->curTpl['classMask'], - 'faction' => $this->curTpl['factionId'], - 'leader' => $this->curTpl['leader'], - 'zone' => $this->curTpl['startAreaId'], - 'side' => $this->curTpl['side'] - ); - - if ($this->curTpl['expansion']) - $data[$this->id]['expansion'] = $this->curTpl['expansion']; - } - - return $data; - } - - public function getJSGlobals($addMask = 0) - { - $data = []; - - foreach ($this->iterate() as $__) - $data[TYPE_RACE][$this->id] = ['name' => $this->getField('name', true)]; - - return $data; - } - - public function addRewardsToJScript(&$ref) { } - public function renderTooltip() { } -} - -?> diff --git a/includes/types/creature.class.php b/includes/types/creature.class.php deleted file mode 100644 index 191f14ec..00000000 --- a/includes/types/creature.class.php +++ /dev/null @@ -1,553 +0,0 @@ - [['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_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.A, ft.H, ft.factionId'], - '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($conditions = [], $miscData = null) - { - parent::__construct($conditions, $miscData); - - if ($this->error) - return; - - // post processing - foreach ($this->iterate() as $_id => &$curTpl) - { - // check for attackspeeds - if (!$curTpl['atkSpeed']) - $curTpl['atkSpeed'] = 2.0; - else - $curTpl['atkSpeed'] /= 1000; - - if (!$curTpl['rngAtkSpeed']) - $curTpl['rngAtkSpeed'] = 2.0; - else - $curTpl['rngAtkSpeed'] /= 1000; - } - } - - public static function getName($id) - { - $n = DB::Aowow()->SelectRow('SELECT name_loc0, name_loc2, name_loc3, name_loc6, name_loc8 FROM ?_creature WHERE id = ?d', $id); - return Util::localizedString($n, 'name'); - } - - public function renderTooltip() - { - if (!$this->curTpl) - return null; - - $level = '??'; - $type = $this->curTpl['type']; - $row3 = [Lang::game('level')]; - $fam = $this->curTpl['family']; - - if (!($this->curTpl['typeFlags'] & 0x4)) - { - $level = $this->curTpl['minLevel']; - if ($level != $this->curTpl['maxLevel']) - $level .= ' - '.$this->curTpl['maxLevel']; - } - else - $level = '??'; - - $row3[] = $level; - - if ($type) - $row3[] = Lang::game('ct', $type); - - if ($_ = Lang::npc('rank', $this->curTpl['rank'])) - $row3[] = '('.$_.')'; - - $x = ''; - $x .= ''; - - if ($sn = $this->getField('subname', true)) - $x .= ''; - - $x .= ''; - - if ($type == 1 && $fam) // 1: Beast - $x .= ''; - - $fac = new FactionList(array([['cuFlags', CUSTOM_EXCLUDE_FOR_LISTVIEW, '&'], 0], ['id', (int)$this->getField('factionId')])); - if (!$fac->error) - $x .= ''; - - $x .= '
'.$this->getField('name', true).'
'.$sn.'
'.implode(' ', $row3).'
'.Lang::game('fa', $fam).'
'.$fac->getField('name', true).'
'; - - return $x; - } - - public function getRandomModelId() - { - // totems use hardcoded models, tauren model is base - $totems = array( // tauren => [orc, dwarf(?!), troll, tauren, draenei] - 4589 => [30758, 30754, 30762, 4589, 19074], // fire - 4588 => [30757, 30753, 30761, 4588, 19073], // earth - 4587 => [30759, 30755, 30763, 4587, 19075], // water - 4590 => [30756, 30736, 30760, 4590, 19071], // air - ); - - $data = []; - - for ($i = 1; $i < 5; $i++) - if ($_ = $this->curTpl['displayId'.$i]) - $data[] = $_; - - if (count($data) == 1 && in_array($data[0], array_keys($totems))) - $data = $totems[$data[0]]; - - return !$data ? 0 : $data[array_rand($data)]; - } - - public function getBaseStats($type) - { - // i'm aware of the BaseVariance/RangedVariance fields ... i'm just totaly unsure about the whole damage calculation - switch ($type) - { - case 'health': - $hMin = $this->getField('healthMin'); - $hMax = $this->getField('healthMax'); - return [$hMin, $hMax]; - case 'power': - $mMin = $this->getField('manaMin'); - $mMax = $this->getField('manaMax'); - return [$mMin, $mMax]; - case 'armor': - $aMin = $this->getField('armorMin'); - $aMax = $this->getField('armorMax'); - return [$aMin, $aMax]; - case 'melee': - $mleMin = ($this->getField('dmgMin') + ($this->getField('mleAtkPwrMin') / 14)) * $this->getField('dmgMultiplier') * $this->getField('atkSpeed'); - $mleMax = ($this->getField('dmgMax') * 1.5 + ($this->getField('mleAtkPwrMax') / 14)) * $this->getField('dmgMultiplier') * $this->getField('atkSpeed'); - return [$mleMin, $mleMax]; - case 'ranged': - $rngMin = ($this->getField('dmgMin') + ($this->getField('rngAtkPwrMin') / 14)) * $this->getField('dmgMultiplier') * $this->getField('rngAtkSpeed'); - $rngMax = ($this->getField('dmgMax') * 1.5 + ($this->getField('rngAtkPwrMax') / 14)) * $this->getField('dmgMultiplier') * $this->getField('rngAtkSpeed'); - return [$rngMin, $rngMax]; - default: - return [0, 0]; - } - } - - public function isBoss() - { - return ($this->curTpl['cuFlags'] & NPC_CU_INSTANCE_BOSS) || ($this->curTpl['typeFlags'] & 0x4 && $this->curTpl['rank']); - } - - public function getListviewData($addInfoMask = 0x0) - { - /* looks like this data differs per occasion - * - * NPCINFO_TAMEABLE (0x1): include texture & react - * NPCINFO_MODEL (0x2): - * NPCINFO_REP (0x4): include repreward - */ - - $data = []; - $rewRep = []; - - 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', - $this->getFoundIDs(), - $this->getFoundIDs() - ); - } - - - foreach ($this->iterate() as $__) - { - if ($addInfoMask & NPCINFO_MODEL) - { - $texStr = strtolower($this->curTpl['textureString']); - - if (isset($data[$texStr])) - { - if ($data[$texStr]['minLevel'] > $this->curTpl['minLevel']) - $data[$texStr]['minLevel'] = $this->curTpl['minLevel']; - - if ($data[$texStr]['maxLevel'] < $this->curTpl['maxLevel']) - $data[$texStr]['maxLevel'] = $this->curTpl['maxLevel']; - - $data[$texStr]['count']++; - } - else - $data[$texStr] = array( - 'family' => $this->curTpl['family'], - 'minLevel' => $this->curTpl['minLevel'], - 'maxLevel' => $this->curTpl['maxLevel'], - 'modelId' => $this->curTpl['modelId'], - 'displayId' => $this->curTpl['displayId1'], - 'skin' => $texStr, - 'count' => 1 - ); - } - else - { - $data[$this->id] = array( - 'family' => $this->curTpl['family'], - 'minlevel' => $this->curTpl['minLevel'], - 'maxlevel' => $this->curTpl['maxLevel'], - 'id' => $this->id, - 'boss' => $this->isBoss() ? 1 : 0, - 'classification' => $this->curTpl['rank'], - 'location' => $this->getSpawns(SPAWNINFO_ZONES), - 'name' => $this->getField('name', true), - 'type' => $this->curTpl['type'], - 'react' => [$this->curTpl['A'], $this->curTpl['H']], - ); - - - if ($this->getField('startsQuests')) - $data[$this->id]['hasQuests'] = 1; - - if ($_ = $this->getField('subname', true)) - $data[$this->id]['tag'] = $_; - - if ($addInfoMask & NPCINFO_TAMEABLE) // only first skin of first model ... we're omitting potentially 11 skins here .. but the lv accepts only one .. w/e - $data[$this->id]['skin'] = $this->curTpl['textureString']; - - if ($addInfoMask & NPCINFO_REP) - { - $data[$this->id]['reprewards'] = []; - if ($rewRep[$this->id]) - foreach ($rewRep[$this->id] as $fac => $val) - $data[$this->id]['reprewards'][] = [$fac, $val]; - } - } - } - - ksort($data); - return $data; - } - - public function getJSGlobals($addMask = 0) - { - $data = []; - - foreach ($this->iterate() as $__) - $data[TYPE_NPC][$this->id] = ['name' => $this->getField('name', true)]; - - return $data; - } - - public function getSourceData() - { - $data = []; - - foreach ($this->iterate() as $__) - { - $data[$this->id] = array( - 'n' => $this->getField('parentId') ? $this->getField('parent', true) : $this->getField('name', true), - 't' => TYPE_NPC, - 'ti' => $this->getField('parentId') ?: $this->id, - // 'bd' => (int)($this->curTpl['cuFlags'] & NPC_CU_INSTANCE_BOSS || ($this->curTpl['typeFlags'] & 0x4 && $this->curTpl['rank'])) - // 'z' where am i spawned - // 'dd' DungeonDifficulty requires 'z' - ); - } - - return $data; - } - - public function addRewardsToJScript(&$refs) { } - - -} - - -class CreatureListFilter extends Filter -{ - public $extraOpts = null; - protected $enums = array( - 3 => 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) - ); - - // cr => [type, field, misc, extraCol] - protected $genericFilter = array( // misc (bool): _NUMERIC => useFloat; _STRING => localized; _FLAG => match Value; _BOOLEAN => stringSet - 1 => [FILTER_CR_CALLBACK, 'cbHealthMana', 'healthMax', 'healthMin'], // health [num] - 2 => [FILTER_CR_CALLBACK, 'cbHealthMana', 'manaMin', 'manaMax' ], // mana [num] - 3 => [FILTER_CR_CALLBACK, 'cbFaction', null, null ], // faction [enum] - 5 => [FILTER_CR_FLAG, 'npcflag', NPC_FLAG_REPAIRER ], // canrepair - 6 => [FILTER_CR_ENUM, 's.areaId', null ], // foundin - 7 => [FILTER_CR_CALLBACK, 'cbQuestRelation', 'startsQuests', 0x1 ], // startsquest [enum] - 8 => [FILTER_CR_CALLBACK, 'cbQuestRelation', 'endsQuests', 0x2 ], // endsquest [enum] - 9 => [FILTER_CR_BOOLEAN, 'lootId', ], // lootable - 10 => [FILTER_CR_BOOLEAN, 'cbRegularSkinLoot', NPC_TYPEFLAG_SPECIALLOOT ], // skinnable [yn] - 11 => [FILTER_CR_BOOLEAN, 'pickpocketLootId', ], // pickpocketable - 12 => [FILTER_CR_CALLBACK, 'cbMoneyDrop', null, null ], // averagemoneydropped [op] [int] - 15 => [FILTER_CR_CALLBACK, 'cbSpecialSkinLoot', NPC_TYPEFLAG_HERBLOOT, null ], // gatherable [yn] - 16 => [FILTER_CR_CALLBACK, 'cbSpecialSkinLoot', NPC_TYPEFLAG_ENGINEERLOOT, null ], // minable [yn] - 18 => [FILTER_CR_FLAG, 'npcflag', NPC_FLAG_AUCTIONEER ], // auctioneer - 19 => [FILTER_CR_FLAG, 'npcflag', NPC_FLAG_BANKER ], // banker - 20 => [FILTER_CR_FLAG, 'npcflag', NPC_FLAG_BATTLEMASTER ], // battlemaster - 21 => [FILTER_CR_FLAG, 'npcflag', NPC_FLAG_FLIGHT_MASTER ], // flightmaster - 22 => [FILTER_CR_FLAG, 'npcflag', NPC_FLAG_GUILD_MASTER ], // guildmaster - 23 => [FILTER_CR_FLAG, 'npcflag', NPC_FLAG_INNKEEPER ], // innkeeper - 24 => [FILTER_CR_FLAG, 'npcflag', NPC_FLAG_CLASS_TRAINER ], // talentunlearner - 25 => [FILTER_CR_FLAG, 'npcflag', NPC_FLAG_GUILD_MASTER ], // tabardvendor - 27 => [FILTER_CR_FLAG, 'npcflag', NPC_FLAG_STABLE_MASTER ], // stablemaster - 28 => [FILTER_CR_FLAG, 'npcflag', NPC_FLAG_TRAINER ], // trainer - 29 => [FILTER_CR_FLAG, 'npcflag', NPC_FLAG_VENDOR ], // vendor - 31 => [FILTER_CR_FLAG, 'cuFlags', CUSTOM_HAS_SCREENSHOT ], // hasscreenshots - 32 => [FILTER_CR_FLAG, 'cuFlags', NPC_CU_INSTANCE_BOSS ], // instanceboss - 33 => [FILTER_CR_FLAG, 'cuFlags', CUSTOM_HAS_COMMENT ], // hascomments - 34 => [FILTER_CR_NYI_PH, 1, null ], // usemodel [str] - displayId -> id:creatureDisplayInfo.dbc/model -> id:cratureModelData.dbc/modelPath - 35 => [FILTER_CR_STRING, 'textureString' ], // useskin - 37 => [FILTER_CR_NUMERIC, 'id', NUM_CAST_INT, true ], // id - 38 => [FILTER_CR_CALLBACK, 'cbRelEvent', null, null ], // relatedevent [enum] - 40 => [FILTER_CR_FLAG, 'cuFlags', CUSTOM_HAS_VIDEO ], // hasvideos - 41 => [FILTER_CR_NYI_PH, 1, null ], // haslocation [yn] [staff] - 42 => [FILTER_CR_CALLBACK, 'cbReputation', '>', null ], // increasesrepwith [enum] - 43 => [FILTER_CR_CALLBACK, 'cbReputation', '<', null ], // decreasesrepwith [enum] - 44 => [FILTER_CR_CALLBACK, 'cbSpecialSkinLoot', NPC_TYPEFLAG_MININGLOOT, null ] // salvageable [yn] - ); - - // fieldId => [checkType, checkValue[, fieldIsArray]] - protected $inputFields = array( - 'cr' => [FILTER_V_LIST, [[1, 3],[5, 12], 15, 16, [18, 25], [27, 29], [31, 35], 37, 38, [40, 44]], true ], // criteria ids - 'crs' => [FILTER_V_LIST, [FILTER_ENUM_NONE, FILTER_ENUM_ANY, [0, 9999]], true ], // criteria operators - 'crv' => [FILTER_V_REGEX, '/[\p{C}:;]/ui', true ], // criteria values - only printable chars, no delimiter - 'na' => [FILTER_V_REGEX, '/[\p{C};]/ui', false], // name / subname - only printable chars, no delimiter - 'ex' => [FILTER_V_EQUAL, 'on', false], // also match subname - 'ma' => [FILTER_V_EQUAL, 1, false], // match any / all filter - 'fa' => [FILTER_V_CALLBACK, 'cbPetFamily', true ], // pet family [list] - cat[0] == 1 - 'minle' => [FILTER_V_RANGE, [1, 99], false], // min level [int] - 'maxle' => [FILTER_V_RANGE, [1, 99], false], // max level [int] - 'cl' => [FILTER_V_RANGE, [0, 4], true ], // classification [list] - 'ra' => [FILTER_V_LIST, [-1, 0, 1], false], // react alliance [int] - 'rh' => [FILTER_V_LIST, [-1, 0, 1], false] // react horde [int] - ); - - protected function createSQLForCriterium(&$cr) - { - if (in_array($cr[0], array_keys($this->genericFilter))) - if ($genCr = $this->genericCriterion($cr)) - return $genCr; - - unset($cr); - $this->error = true; - return [1]; - } - - protected function createSQLForValues() - { - $parts = []; - $_v = &$this->fiData['v']; - - // name [str] - if (isset($_v['na'])) - { - $_ = []; - if (isset($_v['ex']) && $_v['ex'] == 'on') - $_ = $this->modularizeString(['name_loc'.User::$localeId, 'subname_loc'.User::$localeId]); - else - $_ = $this->modularizeString(['name_loc'.User::$localeId]); - - if ($_) - $parts[] = $_; - } - - // pet family [list] - if (isset($_v['fa'])) - $parts[] = ['family', $_v['fa']]; - - // creatureLevel min [int] - if (isset($_v['minle'])) - $parts[] = ['minLevel', $_v['minle'], '>=']; - - // creatureLevel max [int] - if (isset($_v['maxle'])) - $parts[] = ['maxLevel', $_v['maxle'], '<=']; - - // classification [list] - if (isset($_v['cl'])) - $parts[] = ['rank', $_v['cl']]; - - // react Alliance [int] - if (isset($_v['ra'])) - $parts[] = ['ft.A', $_v['ra']]; - - // react Horde [int] - if (isset($_v['rh'])) - $parts[] = ['ft.H', $_v['rh']]; - - return $parts; - } - - protected function cbPetFamily(&$val) - { - if (!$this->parentCats || $this->parentCats[0] != 1) - return false; - - if (!Util::checkNumeric($val, NUM_REQ_INT)) - return false; - - $type = FILTER_V_LIST; - $valid = [[1, 9], 11, 12, 20, 21, [24, 27], [30, 35], [37, 39], [41, 46]]; - - return $this->checkInput($type, $valid, $val); - } - - protected function cbRelEvent($cr) - { - if (!Util::checkNumeric($cr[1], NUM_REQ_INT)) - return false; - - if ($cr[1] == FILTER_ENUM_ANY) - { - $eventIds = DB::Aowow()->selectCol('SELECT id FROM ?_events WHERE holidayId <> 0'); - $cGuids = DB::World()->selectCol('SELECT DISTINCT guid FROM game_event_creature WHERE eventEntry IN (?a)', $eventIds); - return ['s.guid', $cGuids]; - } - else if ($cr[1] == FILTER_ENUM_NONE) - { - $eventIds = DB::Aowow()->selectCol('SELECT id FROM ?_events WHERE holidayId <> 0'); - $cGuids = DB::World()->selectCol('SELECT DISTINCT guid FROM game_event_creature WHERE eventEntry IN (?a)', $eventIds); - return ['s.guid', $cGuids, '!']; - } - else if ($cr[1]) - { - $eventIds = DB::Aowow()->selectCol('SELECT id FROM ?_events WHERE holidayId = ?d', $cr[1]); - $cGuids = DB::World()->selectCol('SELECT DISTINCT guid FROM game_event_creature WHERE eventEntry IN (?a)', $eventIds); - return ['s.guid', $cGuids]; - } - - return false; - } - - protected function cbMoneyDrop($cr) - { - if (!Util::checkNumeric($cr[2], NUM_CAST_INT) || !$this->int2Op($cr[1])) - return false; - - return ['AND', ['((minGold + maxGold) / 2)', $cr[2], $cr[1]]]; - } - - protected function cbQuestRelation($cr, $field, $val) - { - switch ($cr[1]) - { - case 1: // any - return ['AND', ['qse.method', $val, '&'], ['qse.questId', null, '!']]; - case 2: // alliance - return ['AND', ['qse.method', $val, '&'], ['qse.questId', null, '!'], [['qt.reqRaceMask', RACE_MASK_HORDE, '&'], 0], ['qt.reqRaceMask', RACE_MASK_ALLIANCE, '&']]; - case 3: // horde - return ['AND', ['qse.method', $val, '&'], ['qse.questId', null, '!'], [['qt.reqRaceMask', RACE_MASK_ALLIANCE, '&'], 0], ['qt.reqRaceMask', RACE_MASK_HORDE, '&']]; - case 4: // both - return ['AND', ['qse.method', $val, '&'], ['qse.questId', null, '!'], ['OR', ['AND', ['qt.reqRaceMask', RACE_MASK_ALLIANCE, '&'], ['qt.reqRaceMask', RACE_MASK_HORDE, '&']], ['qt.reqRaceMask', 0]]]; - case 5: // none - $this->extraOpts['ct']['h'][] = $field.' = 0'; - return [1]; - } - - return false; - } - - protected function cbHealthMana($cr, $minField, $maxField) - { - if (!Util::checkNumeric($cr[2], NUM_CAST_INT) || !$this->int2Op($cr[1])) - return false; - - // remap OP for this special case - switch ($cr[1]) - { - case '=': // min > max is totally possible - $this->extraOpts['ct']['h'][] = $minField.' = '.$maxField.' AND '.$minField.' = '.$cr[2]; - break; - case '>': - case '>=': - case '<': - case '<=': - $this->extraOpts['ct']['h'][] = 'IF('.$minField.' > '.$maxField.', '.$maxField.', '.$minField.') '.$cr[1].' '.$cr[2]; - break; - } - - - return [1]; // always true, use post-filter - } - - protected function cbSpecialSkinLoot($cr, $typeFlag) - { - if (!$this->int2Bool($cr[1])) - return false; - - - if ($cr[1]) - return ['AND', ['skinLootId', 0, '>'], ['typeFlags', $typeFlag, '&']]; - else - return ['OR', ['skinLootId', 0], [['typeFlags', $typeFlag, '&'], 0]]; - } - - protected function cbRegularSkinLoot($cr, $typeFlag) - { - if (!$this->int2Bool($cr[1])) - return false; - - if ($cr[1]) - return ['AND', ['skinLootId', 0, '>'], [['typeFlags', $typeFlag, '&'], 0]]; - else - return ['OR', ['skinLootId', 0], ['typeFlags', $typeFlag, '&']]; - } - - protected function cbReputation($cr, $op) - { - if (!in_array($cr[1], $this->enums[3])) // reuse - return false; - - if ($_ = DB::Aowow()->selectRow('SELECT * FROM ?_factions WHERE id = ?d', $cr[1])) - $this->formData['reputationCols'][] = [$cr[1], 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)', $cr[1], $cr[1])) - return ['id', $cIds]; - else - return [0]; - } - - protected function cbFaction($cr) - { - if (!Util::checkNumeric($cr[1], NUM_REQ_INT)) - return false; - - if (!in_array($cr[1], $this->enums[$cr[0]])) - return false; - - - $facTpls = []; - $facs = new FactionList(array('OR', ['parentFactionId', $cr[1]], ['id', $cr[1]])); - foreach ($facs->iterate() as $__) - $facTpls = array_merge($facTpls, $facs->getField('templateIds')); - - return $facTpls ? ['faction', $facTpls] : [0]; - } -} - -?> diff --git a/includes/types/currency.class.php b/includes/types/currency.class.php deleted file mode 100644 index f0437552..00000000 --- a/includes/types/currency.class.php +++ /dev/null @@ -1,87 +0,0 @@ - [['ic']], - 'ic' => ['j' => ['?_icons ic ON ic.id = c.iconId', true], 's' => ', ic.name AS iconString'] - ); - - public function __construct($conditions = []) - { - parent::__construct($conditions); - - foreach ($this->iterate() as &$_curTpl) - if (!$_curTpl['iconString']) - $_curTpl['iconString'] = 'inv_misc_questionmark'; - } - - - public function getListviewData() - { - $data = []; - - foreach ($this->iterate() as $__) - { - $data[$this->id] = array( - 'id' => $this->id, - 'category' => $this->curTpl['category'], - 'name' => $this->getField('name', true), - 'icon' => $this->curTpl['iconString'] - ); - } - - return $data; - } - - public function getJSGlobals($addMask = 0) - { - $data = []; - - foreach ($this->iterate() as $__) - { - // todo (low): find out, why i did this in the first place - if ($this->id == 104) // in case of honor commit sebbuku - $icon = ['inv_bannerpvp_02', 'inv_bannerpvp_01']; // ['alliance', 'horde']; - else if ($this->id == 103) // also arena-icon diffs from item-icon - $icon = ['money_arena', 'money_arena']; - else - $icon = [$this->curTpl['iconString'], $this->curTpl['iconString']]; - - $data[TYPE_CURRENCY][$this->id] = ['name' => $this->getField('name', true), 'icon' => $icon]; - } - - return $data; - } - - public function renderTooltip() - { - if (!$this->curTpl) - return array(); - - $x = '
'; - $x .= ''.Util::jsEscape($this->getField('name', true)).'
'; - - // cata+ (or go fill it by hand) - if ($_ = $this->getField('description', true)) - $x .= '
'.Util::jsEscape($_).'
'; - - if ($_ = $this->getField('cap')) - $x .= '
'.Lang::currency('cap').Lang::main('colon').''.Lang::nf($_).'
'; - - $x .= '
'; - - return $x; - } -} - -?> diff --git a/includes/types/emote.class.php b/includes/types/emote.class.php deleted file mode 100644 index 523777d9..00000000 --- a/includes/types/emote.class.php +++ /dev/null @@ -1,58 +0,0 @@ -iterate() as &$curTpl) - { - // remap for generic access - $curTpl['name'] = $curTpl['cmd']; - } - } - - public function getListviewData() - { - $data = []; - - foreach ($this->iterate() as $__) - { - $data[$this->id] = array( - 'id' => $this->curTpl['id'], - 'name' => $this->curTpl['cmd'], - 'preview' => $this->getField('self', true) ?: ($this->getField('noTarget', true) ?: $this->getField('target', true)) - ); - - // [nyi] sounds - } - - return $data; - } - - public function getJSGlobals($addMask = GLOBALINFO_ANY) - { - $data = []; - - foreach ($this->iterate() as $__) - $data[TYPE_EMOTE][$this->id] = ['name' => $this->getField('cmd')]; - - return $data; - } - - public function renderTooltip() { } -} - -?> diff --git a/includes/types/enchantment.class.php b/includes/types/enchantment.class.php deleted file mode 100644 index cb245818..00000000 --- a/includes/types/enchantment.class.php +++ /dev/null @@ -1,353 +0,0 @@ - TYPE_ENCHANTMENT - 'ie' => [['is']], - 'is' => ['j' => ['?_item_stats `is` ON `is`.`type` = 502 AND `is`.`typeId` = `ie`.`id`', true], 's' => ', `is`.*'], - ); - - public function __construct($conditions = []) - { - parent::__construct($conditions); - - // post processing - foreach ($this->iterate() as &$curTpl) - { - $curTpl['spells'] = []; // [spellId, triggerType, charges, chanceOrPpm] - for ($i = 1; $i <=3; $i++) - { - if ($curTpl['object'.$i] <= 0) - continue; - - switch ($curTpl['type'.$i]) - { - case 1: - $proc = -$this->getField('ppmRate') ?: ($this->getField('procChance') ?: $this->getField('amount'.$i)); - $curTpl['spells'][$i] = [$curTpl['object'.$i], 2, $curTpl['charges'], $proc]; - $this->relSpells[] = $curTpl['object'.$i]; - break; - case 3: - $curTpl['spells'][$i] = [$curTpl['object'.$i], 1, $curTpl['charges'], 0]; - $this->relSpells[] = $curTpl['object'.$i]; - break; - case 7: - $curTpl['spells'][$i] = [$curTpl['object'.$i], 0, $curTpl['charges'], 0]; - $this->relSpells[] = $curTpl['object'.$i]; - break; - } - } - - // floats are fetched as string from db :< - $curTpl['dmg'] = floatVal($curTpl['dmg']); - $curTpl['dps'] = floatVal($curTpl['dps']); - - // remove zero-stats - foreach (Game::$itemMods as $str) - if ($curTpl[$str] == 0) // empty(0.0f) => true .. yeah, sure - unset($curTpl[$str]); - - if ($curTpl['dps'] == 0) - unset($curTpl['dps']); - } - - if ($this->relSpells) - $this->relSpells = new SpellList(array(['id', $this->relSpells])); - } - - // use if you JUST need the name - public static function getName($id) - { - $n = DB::Aowow()->SelectRow('SELECT name_loc0, name_loc2, name_loc3, name_loc6, name_loc8 FROM ?_itemenchantment WHERE id = ?d', $id ); - return Util::localizedString($n, 'name'); - } - // end static use - - public function getListviewData($addInfoMask = 0x0) - { - $data = []; - - foreach ($this->iterate() as $__) - { - $data[$this->id] = array( - 'id' => $this->id, - 'name' => $this->getField('name', true), - 'spells' => [] - ); - - if ($this->curTpl['skillLine'] > 0) - $data[$this->id]['reqskill'] = $this->curTpl['skillLine']; - - if ($this->curTpl['skillLevel'] > 0) - $data[$this->id]['reqskillrank'] = $this->curTpl['skillLevel']; - - if ($this->curTpl['requiredLevel'] > 0) - $data[$this->id]['reqlevel'] = $this->curTpl['requiredLevel']; - - foreach ($this->curTpl['spells'] as $s) - { - // enchant is procing or onUse - if ($s[1] == 2 || $s[1] == 0) - $data[$this->id]['spells'][$s[0]] = $s[2]; - // spell is procing - else if ($this->relSpells && $this->relSpells->getEntry($s[0]) && ($_ = $this->relSpells->canTriggerSpell())) - { - foreach ($_ as $idx) - { - $this->triggerIds[] = $this->relSpells->getField('effect'.$idx.'TriggerSpell'); - $data[$this->id]['spells'][$this->relSpells->getField('effect'.$idx.'TriggerSpell')] = $s[2]; - } - } - } - - if (!$data[$this->id]['spells']) - unset($data[$this->id]['spells']); - - Util::arraySumByKey($data[$this->id], $this->getStatGain()); - } - - return $data; - } - - public function getStatGain($addScalingKeys = false) - { - $data = []; - - foreach (Game::$itemMods as $str) - if (isset($this->curTpl[$str])) - $data[$str] = $this->curTpl[$str]; - - if (isset($this->curTpl['dps'])) - $data['dps'] = $this->curTpl['dps']; - - // scaling enchantments are saved as 0 to item_stats, thus return empty - if ($addScalingKeys) - { - $spellStats = []; - if ($this->relSpells) - $spellStats = $this->relSpells->getStatGain(); - - for ($h = 1; $h <= 3; $h++) - { - $obj = (int)$this->curTpl['object'.$h]; - - switch ($this->curTpl['type'.$h]) - { - case 3: // TYPE_EQUIP_SPELL Spells from ObjectX (use of amountX?) - if (!empty($spellStats[$obj])) - foreach ($spellStats[$obj] as $mod => $_) - if ($str = Game::$itemMods[$mod]) - Util::arraySumByKey($data, [$str => 0]); - - $obj = null; - break; - case 4: // TYPE_RESISTANCE +AmountX resistance for ObjectX School - switch ($obj) - { - case 0: // Physical - $obj = ITEM_MOD_ARMOR; - break; - case 1: // Holy - $obj = ITEM_MOD_HOLY_RESISTANCE; - break; - case 2: // Fire - $obj = ITEM_MOD_FIRE_RESISTANCE; - break; - case 3: // Nature - $obj = ITEM_MOD_NATURE_RESISTANCE; - break; - case 4: // Frost - $obj = ITEM_MOD_FROST_RESISTANCE; - break; - case 5: // Shadow - $obj = ITEM_MOD_SHADOW_RESISTANCE; - break; - case 6: // Arcane - $obj = ITEM_MOD_ARCANE_RESISTANCE; - break; - default: - $obj = null; - } - break; - case 5: // TYPE_STAT +AmountX for Statistic by type of ObjectX - if ($obj < 2) // [mana, health] are on [0, 1] respectively and are expected on [1, 2] .. - $obj++; // 0 is weaponDmg .. ehh .. i messed up somewhere - - break; // stats are directly assigned below - default: // TYPE_NONE dnd stuff; skip assignment below - $obj = null; - } - - if ($obj !== null) - if ($str = Game::$itemMods[$obj]) // check if we use these mods - Util::arraySumByKey($data, [$str => 0]); - } - } - - return $data; - } - - public function getRelSpell($id) - { - if ($this->relSpells) - return $this->relSpells->getEntry($id); - - return null; - } - - public function getJSGlobals($addMask = GLOBALINFO_ANY) - { - $data = []; - - if ($addMask & GLOBALINFO_SELF) - foreach ($this->iterate() as $__) - $data[TYPE_ENCHANTMENT][$this->id] = ['name' => $this->getField('name', true)]; - - if ($addMask & GLOBALINFO_RELATED) - { - if ($this->relSpells) - $data = $this->relSpells->getJSGlobals(GLOBALINFO_SELF); - - foreach ($this->triggerIds as $tId) - if (empty($data[TYPE_SPELL][$tId])) - $data[TYPE_SPELL][$tId] = $tId; - } - - return $data; - } - - public function renderTooltip() { } -} - - -class EnchantmentListFilter extends Filter -{ - protected $enums = array( - 3 => array( // requiresprof - null, 171, 164, 185, 333, 202, 129, 755, 165, 186, 197, true, false, 356, 182, 773 - ) - ); - - protected $genericFilter = array( // misc (bool): _NUMERIC => useFloat; _STRING => localized; _FLAG => match Value; _BOOLEAN => stringSet - 2 => [FILTER_CR_NUMERIC, 'id', NUM_CAST_INT, true], // id - 3 => [FILTER_CR_ENUM, 'skillLine' ], // requiresprof - 4 => [FILTER_CR_NUMERIC, 'skillLevel', NUM_CAST_INT ], // reqskillrank - 5 => [FILTER_CR_BOOLEAN, 'conditionId' ], // hascondition - 10 => [FILTER_CR_FLAG, 'cuFlags', CUSTOM_HAS_COMMENT ], // hascomments - 11 => [FILTER_CR_FLAG, 'cuFlags', CUSTOM_HAS_SCREENSHOT ], // hasscreenshots - 12 => [FILTER_CR_FLAG, 'cuFlags', CUSTOM_HAS_VIDEO ], // hasvideos - 20 => [FILTER_CR_NUMERIC, 'is.str', NUM_CAST_INT, true], // str - 21 => [FILTER_CR_NUMERIC, 'is.agi', NUM_CAST_INT, true], // agi - 22 => [FILTER_CR_NUMERIC, 'is.sta', NUM_CAST_INT, true], // sta - 23 => [FILTER_CR_NUMERIC, 'is.int', NUM_CAST_INT, true], // int - 24 => [FILTER_CR_NUMERIC, 'is.spi', NUM_CAST_INT, true], // spi - 25 => [FILTER_CR_NUMERIC, 'is.arcres', NUM_CAST_INT, true], // arcres - 26 => [FILTER_CR_NUMERIC, 'is.firres', NUM_CAST_INT, true], // firres - 27 => [FILTER_CR_NUMERIC, 'is.natres', NUM_CAST_INT, true], // natres - 28 => [FILTER_CR_NUMERIC, 'is.frores', NUM_CAST_INT, true], // frores - 29 => [FILTER_CR_NUMERIC, 'is.shares', NUM_CAST_INT, true], // shares - 30 => [FILTER_CR_NUMERIC, 'is.holres', NUM_CAST_INT, true], // holres - 32 => [FILTER_CR_NUMERIC, 'is.dps', NUM_CAST_FLOAT, true], // dps - 34 => [FILTER_CR_NUMERIC, 'is.dmg', NUM_CAST_FLOAT, true], // dmg - 37 => [FILTER_CR_NUMERIC, 'is.mleatkpwr', NUM_CAST_INT, true], // mleatkpwr - 38 => [FILTER_CR_NUMERIC, 'is.rgdatkpwr', NUM_CAST_INT, true], // rgdatkpwr - 39 => [FILTER_CR_NUMERIC, 'is.rgdhitrtng', NUM_CAST_INT, true], // rgdhitrtng - 40 => [FILTER_CR_NUMERIC, 'is.rgdcritstrkrtng', NUM_CAST_INT, true], // rgdcritstrkrtng - 41 => [FILTER_CR_NUMERIC, 'is.armor' , NUM_CAST_INT, true], // armor - 42 => [FILTER_CR_NUMERIC, 'is.defrtng', NUM_CAST_INT, true], // defrtng - 43 => [FILTER_CR_NUMERIC, 'is.block', NUM_CAST_INT, true], // block - 44 => [FILTER_CR_NUMERIC, 'is.blockrtng', NUM_CAST_INT, true], // blockrtng - 45 => [FILTER_CR_NUMERIC, 'is.dodgertng', NUM_CAST_INT, true], // dodgertng - 46 => [FILTER_CR_NUMERIC, 'is.parryrtng', NUM_CAST_INT, true], // parryrtng - 48 => [FILTER_CR_NUMERIC, 'is.splhitrtng', NUM_CAST_INT, true], // splhitrtng - 49 => [FILTER_CR_NUMERIC, 'is.splcritstrkrtng', NUM_CAST_INT, true], // splcritstrkrtng - 50 => [FILTER_CR_NUMERIC, 'is.splheal', NUM_CAST_INT, true], // splheal - 51 => [FILTER_CR_NUMERIC, 'is.spldmg', NUM_CAST_INT, true], // spldmg - 52 => [FILTER_CR_NUMERIC, 'is.arcsplpwr', NUM_CAST_INT, true], // arcsplpwr - 53 => [FILTER_CR_NUMERIC, 'is.firsplpwr', NUM_CAST_INT, true], // firsplpwr - 54 => [FILTER_CR_NUMERIC, 'is.frosplpwr', NUM_CAST_INT, true], // frosplpwr - 55 => [FILTER_CR_NUMERIC, 'is.holsplpwr', NUM_CAST_INT, true], // holsplpwr - 56 => [FILTER_CR_NUMERIC, 'is.natsplpwr', NUM_CAST_INT, true], // natsplpwr - 57 => [FILTER_CR_NUMERIC, 'is.shasplpwr', NUM_CAST_INT, true], // shasplpwr - 60 => [FILTER_CR_NUMERIC, 'is.healthrgn', NUM_CAST_INT, true], // healthrgn - 61 => [FILTER_CR_NUMERIC, 'is.manargn', NUM_CAST_INT, true], // manargn - 77 => [FILTER_CR_NUMERIC, 'is.atkpwr', NUM_CAST_INT, true], // atkpwr - 78 => [FILTER_CR_NUMERIC, 'is.mlehastertng', NUM_CAST_INT, true], // mlehastertng - 79 => [FILTER_CR_NUMERIC, 'is.resirtng', NUM_CAST_INT, true], // resirtng - 84 => [FILTER_CR_NUMERIC, 'is.mlecritstrkrtng', NUM_CAST_INT, true], // mlecritstrkrtng - 94 => [FILTER_CR_NUMERIC, 'is.splpen', NUM_CAST_INT, true], // splpen - 95 => [FILTER_CR_NUMERIC, 'is.mlehitrtng', NUM_CAST_INT, true], // mlehitrtng - 96 => [FILTER_CR_NUMERIC, 'is.critstrkrtng', NUM_CAST_INT, true], // critstrkrtng - 97 => [FILTER_CR_NUMERIC, 'is.feratkpwr', NUM_CAST_INT, true], // feratkpwr - 101 => [FILTER_CR_NUMERIC, 'is.rgdhastertng', NUM_CAST_INT, true], // rgdhastertng - 102 => [FILTER_CR_NUMERIC, 'is.splhastertng', NUM_CAST_INT, true], // splhastertng - 103 => [FILTER_CR_NUMERIC, 'is.hastertng', NUM_CAST_INT, true], // hastertng - 114 => [FILTER_CR_NUMERIC, 'is.armorpenrtng', NUM_CAST_INT, true], // armorpenrtng - 115 => [FILTER_CR_NUMERIC, 'is.health', NUM_CAST_INT, true], // health - 116 => [FILTER_CR_NUMERIC, 'is.mana', NUM_CAST_INT, true], // mana - 117 => [FILTER_CR_NUMERIC, 'is.exprtng', NUM_CAST_INT, true], // exprtng - 119 => [FILTER_CR_NUMERIC, 'is.hitrtng', NUM_CAST_INT, true], // hitrtng - 123 => [FILTER_CR_NUMERIC, 'is.splpwr', NUM_CAST_INT, true] // splpwr - ); - - // fieldId => [checkType, checkValue[, fieldIsArray]] - protected $inputFields = array( - 'cr' => [FILTER_V_RANGE, [2, 123], true ], // criteria ids - 'crs' => [FILTER_V_RANGE, [1, 15], true ], // criteria operators - 'crv' => [FILTER_V_RANGE, [0, 99999], true ], // criteria values - only numerals - 'na' => [FILTER_V_REGEX, '/[\p{C};]/ui', false], // name - only printable chars, no delimiter - 'ma' => [FILTER_V_EQUAL, 1, false], // match any / all filter - 'ty' => [FILTER_V_RANGE, [1, 8], true ] // types - ); - - protected function createSQLForCriterium(&$cr) - { - if (in_array($cr[0], array_keys($this->genericFilter))) - if ($genCr = $this->genericCriterion($cr)) - return $genCr; - - unset($cr); - $this->error = true; - return [1]; - } - - protected function createSQLForValues() - { - $parts = []; - $_v = &$this->fiData['v']; - - //string - if (isset($_v['na'])) - if ($_ = $this->modularizeString(['name_loc'.User::$localeId])) - $parts[] = $_; - - // type - if (isset($_v['ty'])) - { - $_ = (array)$_v['ty']; - if (!array_diff($_, [1, 2, 3, 4, 5, 6, 7, 8])) - $parts[] = ['OR', ['type1', $_], ['type2', $_], ['type3', $_]]; - else - unset($_v['ty']); - } - - return $parts; - } -} - -?> diff --git a/includes/types/gameobject.class.php b/includes/types/gameobject.class.php deleted file mode 100644 index 7838ac21..00000000 --- a/includes/types/gameobject.class.php +++ /dev/null @@ -1,247 +0,0 @@ - [['ft', 'qse']], - 'ft' => ['j' => ['?_factiontemplate ft ON ft.id = o.faction', true], 's' => ', ft.factionId, ft.A, ft.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($conditions = [], $miscData = null) - { - parent::__construct($conditions, $miscData); - - if ($this->error) - return; - - // post processing - foreach ($this->iterate() as $_id => &$curTpl) - { - // unpack miscInfo - $curTpl['lootStack'] = []; - $curTpl['spells'] = []; - - if (in_array($curTpl['type'], [OBJECT_GOOBER, OBJECT_RITUAL, OBJECT_SPELLCASTER, OBJECT_FLAGSTAND, OBJECT_FLAGDROP, OBJECT_AURA_GENERATOR, OBJECT_TRAP])) - $curTpl['spells'] = array_combine(['onUse', 'onSuccess', 'aura', 'triggered'], [$curTpl['onUseSpell'], $curTpl['onSuccessSpell'], $curTpl['auraSpell'], $curTpl['triggeredSpell']]); - - if (!$curTpl['miscInfo']) - continue; - - switch ($curTpl['type']) - { - case OBJECT_CHEST: - case OBJECT_FISHINGHOLE: - $curTpl['lootStack'] = explode(' ', $curTpl['miscInfo']); - break; - case OBJECT_CAPTURE_POINT: - $curTpl['capture'] = explode(' ', $curTpl['miscInfo']); - break; - case OBJECT_MEETINGSTONE: - $curTpl['mStone'] = explode(' ', $curTpl['miscInfo']); - break; - } - } - } - - public static function getName($id) - { - $n = DB::Aowow()->SelectRow('SELECT name_loc0, name_loc2, name_loc3, name_loc6, name_loc8 FROM ?_objects WHERE id = ?d', $id); - return Util::localizedString($n, 'name'); - } - - public function getListviewData() - { - $data = []; - foreach ($this->iterate() as $__) - { - $data[$this->id] = array( - 'id' => $this->id, - 'name' => $this->getField('name', true), - 'type' => $this->curTpl['typeCat'], - 'location' => $this->getSpawns(SPAWNINFO_ZONES) - ); - - if (!empty($this->curTpl['reqSkill'])) - $data[$this->id]['skill'] = $this->curTpl['reqSkill']; - - if ($this->curTpl['startsQuests']) - $data[$this->id]['hasQuests'] = 1; - - } - - return $data; - } - - public function renderTooltip($interactive = false) - { - if (!$this->curTpl) - return array(); - - $x = ''; - $x .= ''; - if ($this->curTpl['typeCat']) - if ($_ = Lang::gameObject('type', $this->curTpl['typeCat'])) - $x .= ''; - - if (isset($this->curTpl['lockId'])) - if ($locks = Lang::getLocks($this->curTpl['lockId'])) - foreach ($locks as $l) - $x .= ''; - - $x .= '
'.$this->getField('name', true).'
'.$_.'
'.$l.'
'; - - return $x; - } - - public function getJSGlobals($addMask = 0) - { - $data = []; - - foreach ($this->iterate() as $__) - $data[TYPE_OBJECT][$this->id] = ['name' => $this->getField('name', true)]; - - return $data; - } - - public function getSourceData() - { - $data = []; - - foreach ($this->iterate() as $__) - { - $data[$this->id] = array( - 'n' => $this->getField('name', true), - 't' => TYPE_OBJECT, - 'ti' => $this->id - // 'bd' => bossdrop - // 'dd' => dungeondifficulty - ); - } - - return $data; - } -} - - -class GameObjectListFilter extends Filter -{ - public $extraOpts = []; - - protected $genericFilter = array( - 1 => [FILTER_CR_ENUM, 's.areaId', null ], // foundin - 2 => [FILTER_CR_CALLBACK, 'cbQuestRelation', 'startsQuests', 0x1 ], // startsquest [side] - 3 => [FILTER_CR_CALLBACK, 'cbQuestRelation', 'endsQuests', 0x2 ], // endsquest [side] - 4 => [FILTER_CR_CALLBACK, 'cbOpenable', null, null], // openable [yn] - 5 => [FILTER_CR_NYI_PH, null, null ], // averagemoneycontained [op] [int] - GOs don't contain money, match against 0 - 7 => [FILTER_CR_NUMERIC, 'reqSkill', NUM_CAST_INT ], // requiredskilllevel - 11 => [FILTER_CR_FLAG, 'cuFlags', CUSTOM_HAS_SCREENSHOT ], // hasscreenshots - 13 => [FILTER_CR_FLAG, 'cuFlags', CUSTOM_HAS_COMMENT ], // hascomments - 15 => [FILTER_CR_NUMERIC, 'id', NUM_CAST_INT ], // id - 16 => [FILTER_CR_CALLBACK, 'cbRelEvent', null, null], // relatedevent (ignore removed by event) - 18 => [FILTER_CR_FLAG, 'cuFlags', CUSTOM_HAS_VIDEO ] // hasvideos - ); - - // fieldId => [checkType, checkValue[, fieldIsArray]] - protected $inputFields = array( - 'cr' => [FILTER_V_LIST, [[1, 5], 7, 11, 13, 15, 16, 18], true ], // criteria ids - 'crs' => [FILTER_V_LIST, [FILTER_ENUM_NONE, FILTER_ENUM_ANY, [0, 5000]], true ], // criteria operators - 'crv' => [FILTER_V_RANGE, [0, 99999], true ], // criteria values - only numeric input values expected - 'na' => [FILTER_V_REGEX, '/[\p{C};]/ui', false], // name - only printable chars, no delimiter - 'ma' => [FILTER_V_EQUAL, 1, false] // match any / all filter - ); - - protected function createSQLForCriterium(&$cr) - { - if (in_array($cr[0], array_keys($this->genericFilter))) - if ($genCR = $this->genericCriterion($cr)) - return $genCR; - - unset($cr); - $this->error = true; - return [1]; - } - - protected function createSQLForValues() - { - $parts = []; - $_v = $this->fiData['v']; - - // name - if (isset($_v['na'])) - if ($_ = $this->modularizeString(['name_loc'.User::$localeId])) - $parts[] = $_; - - return $parts; - } - - protected function cbOpenable($cr) - { - if ($this->int2Bool($cr[1])) - return $cr[1] ? ['OR', ['flags', 0x2, '&'], ['type', 3]] : ['AND', [['flags', 0x2, '&'], 0], ['type', 3, '!']]; - - return false; - } - - protected function cbQuestRelation($cr, $field, $value) - { - switch ($cr[1]) - { - case 1: // any - return ['AND', ['qse.method', $value, '&'], ['qse.questId', null, '!']]; - case 2: // alliance only - return ['AND', ['qse.method', $value, '&'], ['qse.questId', null, '!'], [['qt.reqRaceMask', RACE_MASK_HORDE, '&'], 0], ['qt.reqRaceMask', RACE_MASK_ALLIANCE, '&']]; - case 3: // horde only - return ['AND', ['qse.method', $value, '&'], ['qse.questId', null, '!'], [['qt.reqRaceMask', RACE_MASK_ALLIANCE, '&'], 0], ['qt.reqRaceMask', RACE_MASK_HORDE, '&']]; - case 4: // both - return ['AND', ['qse.method', $value, '&'], ['qse.questId', null, '!'], ['OR', ['AND', ['qt.reqRaceMask', RACE_MASK_ALLIANCE, '&'], ['qt.reqRaceMask', RACE_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]; - } - - return false; - } - - protected function cbRelEvent($cr) - { - if (!Util::checkNumeric($cr[1], NUM_REQ_INT)) - return false;; - - if ($cr[1] == FILTER_ENUM_ANY) - { - $eventIds = DB::Aowow()->selectCol('SELECT id FROM ?_events WHERE holidayId <> 0'); - $goGuids = DB::World()->selectCol('SELECT DISTINCT guid FROM game_event_gameobject WHERE eventEntry IN (?a)', $eventIds); - return ['s.guid', $goGuids]; - } - else if ($cr[1] == FILTER_ENUM_NONE) - { - $eventIds = DB::Aowow()->selectCol('SELECT id FROM ?_events WHERE holidayId <> 0'); - $goGuids = DB::World()->selectCol('SELECT DISTINCT guid FROM game_event_gameobject WHERE eventEntry IN (?a)', $eventIds); - return ['s.guid', $goGuids, '!']; - } - else if ($cr[1]) - { - $eventIds = DB::Aowow()->selectCol('SELECT id FROM ?_events WHERE holidayId = ?d', $cr[1]); - $goGuids = DB::World()->selectCol('SELECT DISTINCT guid FROM game_event_gameobject WHERE eventEntry IN (?a)', $eventIds); - return ['s.guid', $goGuids]; - } - - return false; - } -} - -?> diff --git a/includes/types/icon.class.php b/includes/types/icon.class.php deleted file mode 100644 index 94ce25c0..00000000 --- a/includes/types/icon.class.php +++ /dev/null @@ -1,237 +0,0 @@ - '?_items', - 'nSpells' => '?_spell', - 'nAchievements' => '?_achievement', - 'nCurrencies' => '?_currencies', - 'nPets' => '?_pet' - ); - - protected $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 $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'] - ); - */ - - public function __construct($conditions) - { - parent::__construct($conditions); - - if (!$this->getFoundIDs()) - return; - - foreach ($this->pseudoJoin as $var => $tbl) - { - $res = DB::Aowow()->selectCol($this->pseudoQry, $tbl, $this->getFoundIDs()); - foreach ($res as $icon => $qty) - $this->templates[$icon][$var] = $qty; - } - } - - - // use if you JUST need the name - public static function getName($id) - { - $n = DB::Aowow()->SelectRow('SELECT name FROM ?_icons WHERE id = ?d', $id ); - return Util::localizedString($n, 'name'); - } - // end static use - - public function getListviewData($addInfoMask = 0x0) - { - $data = []; - - foreach ($this->iterate() as $__) - { - $data[$this->id] = array( - 'id' => $this->id, - 'name' => $this->getField('name', true, true), - 'icon' => $this->getField('name', true, true), - 'itemcount' => (int)$this->getField('nItems'), - 'spellcount' => (int)$this->getField('nSpells'), - 'achievementcount' => (int)$this->getField('nAchievements'), - 'npccount' => 0, // UNUSED - 'petabilitycount' => 0, // UNUSED - 'currencycount' => (int)$this->getField('nCurrencies'), - 'missionabilitycount' => 0, // UNUSED - 'buildingcount' => 0, // UNUSED - 'petcount' => (int)$this->getField('nPets'), - 'threatcount' => 0, // UNUSED - 'classcount' => 0 // class icons are hardcoded and not referenced in dbc - ); - } - - return $data; - } - - public function getJSGlobals($addMask = GLOBALINFO_ANY) - { - $data = []; - - foreach ($this->iterate() as $__) - $data[TYPE_ICON][$this->id] = ['name' => $this->getField('name', true, true), 'icon' => $this->getField('name', true, true)]; - - return $data; - } - - public function renderTooltip() { } -} - - -class IconListFilter extends Filter -{ - public $extraOpts = null; - - // cr => [type, field, misc, extraCol] - private $criterion2field = array( - 1 => '?_items', // items [num] - 2 => '?_spell', // spells [num] - 3 => '?_achievement', // achievements [num] - // 4 => '', // battlepets [num] - // 5 => '', // battlepetabilities [num] - 6 => '?_currencies', // currencies [num] - // 7 => '', // garrisonabilities [num] - // 8 => '', // garrisonbuildings [num] - 9 => '?_pet', // hunterpets [num] - // 10 => '', // garrisonmissionthreats [num] - 11 => '', // classes [num] - 13 => '' // used [num] - ); - private $totalUses = []; - - protected $genericFilter = array( - 1 => [FILTER_CR_CALLBACK, 'cbUseAny' ], // items [num] - 2 => [FILTER_CR_CALLBACK, 'cbUseAny' ], // spells [num] - 3 => [FILTER_CR_CALLBACK, 'cbUseAny' ], // achievements [num] - 6 => [FILTER_CR_CALLBACK, 'cbUseAny' ], // currencies [num] - 9 => [FILTER_CR_CALLBACK, 'cbUseAny' ], // hunterpets [num] - 11 => [FILTER_CR_NYI_PH, null, null], // classes [num] - 13 => [FILTER_CR_CALLBACK, 'cbUseAll' ] // used [num] - ); - - // fieldId => [checkType, checkValue[, fieldIsArray]] - protected $inputFields = array( - 'cr' => [FILTER_V_LIST, [1, 2, 3, 6, 9, 11, 13], true ], // criteria ids - 'crs' => [FILTER_V_RANGE, [1, 6], true ], // criteria operators - 'crv' => [FILTER_V_RANGE, [0, 99999], true ], // criteria values - all criteria are numeric here - 'na' => [FILTER_V_REGEX, '/[\p{C};]/ui', false], // name - only printable chars, no delimiter - 'ma' => [FILTER_V_EQUAL, 1, false] // match any / all filter - ); - - private function _getCnd($op, $val, $tbl) - { - switch ($op) - { - case '>': - case '>=': - case '=': - $ids = DB::Aowow()->selectCol('SELECT iconId AS ARRAY_KEY, COUNT(*) AS n FROM ?# GROUP BY iconId HAVING n '.$op.' '.$val, $tbl); - return $ids ? ['id', array_keys($ids)] : [1]; - case '<=': - if ($val) - $op = '>'; - break; - case '<': - if ($val) - $op = '>='; - break; - case '!=': - if ($val) - $op = '='; - break; - } - - $ids = DB::Aowow()->selectCol('SELECT iconId AS ARRAY_KEY, COUNT(*) AS n FROM ?# GROUP BY iconId HAVING n '.$op.' '.$val, $tbl); - return $ids ? ['id', array_keys($ids), '!'] : [1]; - } - - protected function createSQLForCriterium(&$cr) - { - if (in_array($cr[0], array_keys($this->genericFilter))) - if ($genCr = $this->genericCriterion($cr)) - return $genCr; - - unset($cr); - $this->error = true; - return [1]; - } - - protected function createSQLForValues() - { - $parts = []; - $_v = &$this->fiData['v']; - - //string - if (isset($_v['na'])) - if ($_ = $this->modularizeString(['name'])) - $parts[] = $_; - - return $parts; - } - - protected function cbUseAny($cr, $value) - { - if (Util::checkNumeric($cr[2], NUM_CAST_INT) && $this->int2Op($cr[1])) - return $this->_getCnd($cr[1], $cr[2], $this->criterion2field[$cr[0]]); - - return false; - } - - protected function cbUseAll($cr) - { - if (!Util::checkNumeric($cr[2], NUM_CAST_INT) || !$this->int2Op($cr[1])) - return false; - - if (!$this->totalUses) - { - foreach ($this->criterion2field as $tbl) - { - if (!$tbl) - continue; - - $res = DB::Aowow()->selectCol('SELECT iconId AS ARRAY_KEY, COUNT(*) AS n FROM ?# GROUP BY iconId', $tbl); - Util::arraySumByKey($this->totalUses, $res); - } - } - - if ($cr[1] == '=') - $cr[1] = '=='; - - $op = $cr[1]; - if ($cr[1] == '<=' && $cr[2]) - $op = '>'; - else if ($cr[1] == '<' && $cr[2]) - $op = '>='; - else if ($cr[1] == '!=' && $cr[2]) - $op = '=='; - $ids = array_filter($this->totalUses, function ($x) use ($op, $cr) { return eval('return '.$x.' '.$op.' '.$cr[2].';'); }); - - if ($cr[1] != $op) - return $ids ? ['id', array_keys($ids), '!'] : [1]; - else - return $ids ? ['id', array_keys($ids)] : ['id', array_keys($this->totalUses), '!']; - } -} - -?> diff --git a/includes/types/item.class.php b/includes/types/item.class.php deleted file mode 100644 index c1d21903..00000000 --- a/includes/types/item.class.php +++ /dev/null @@ -1,2588 +0,0 @@ - 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, 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($conditions = [], $miscData = null) - { - parent::__construct($conditions, $miscData); - - foreach ($this->iterate() as &$_curTpl) - { - // item is scaling; overwrite other values - if ($_curTpl['scalingStatDistribution'] > 0 && $_curTpl['scalingStatValue'] > 0) - $this->initScalingStats(); - - $this->initJsonStats(); - - // readdress itemset .. is wrong for virtual sets - if ($miscData && isset($miscData['pcsToSet']) && isset($miscData['pcsToSet'][$this->id])) - $this->json[$this->id]['itemset'] = $miscData['pcsToSet'][$this->id]; - - // unify those pesky masks - $_ = &$_curTpl['requiredClass']; - $_ &= CLASS_MASK_ALL; - if ($_ < 0 || $_ == CLASS_MASK_ALL) - $_ = 0; - unset($_); - - $_ = &$_curTpl['requiredRace']; - $_ &= RACE_MASK_ALL; - if ($_ < 0 || $_ == RACE_MASK_ALL) - $_ = 0; - unset($_); - - // sources - for ($i = 1; $i < 25; $i++) - { - if ($_ = $_curTpl['src'.$i]) - $this->sources[$this->id][$i][] = $_; - - unset($_curTpl['src'.$i]); - } - } - } - - // use if you JUST need the name - public static function getName($id) - { - $n = DB::Aowow()->selectRow('SELECT name_loc0, name_loc2, name_loc3, name_loc6, name_loc8 FROM ?_items WHERE id = ?d', $id); - return Util::localizedString($n, 'name'); - } - - // todo (med): information will get lost if one vendor sells one item multiple times with different costs (e.g. for item 54637) - // wowhead seems to have had the same issues - public function getExtendedCost($filter = [], &$reqRating = []) - { - if ($this->error) - return []; - - if (empty($this->vendors)) - { - $itemz = DB::World()->select(' - SELECT nv.item AS ARRAY_KEY1, nv.entry AS ARRAY_KEY2, 0 AS eventId, nv.maxcount, nv.extendedCost FROM npc_vendor nv WHERE {nv.entry IN (?a) AND} nv.item IN (?a) - UNION - SELECT genv.item AS ARRAY_KEY1, c.id AS ARRAY_KEY2, ge.eventEntry AS eventId, genv.maxcount, genv.extendedCost 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) - ); - - $xCosts = []; - foreach ($itemz as $i => $vendors) - $xCosts = array_merge($xCosts, array_column($vendors, 'extendedCost')); - - if ($xCosts) - $xCosts = DB::Aowow()->select('SELECT *, id AS ARRAY_KEY FROM ?_itemextendedcost WHERE id IN (?a)', $xCosts); - - $cItems = []; - foreach ($itemz as $k => $vendors) - { - foreach ($vendors as $l => $vInfo) - { - $costs = []; - if (!empty($xCosts[$vInfo['extendedCost']])) - $costs = $xCosts[$vInfo['extendedCost']]; - - $data = array( - 'stock' => $vInfo['maxcount'] ?: -1, - 'event' => $vInfo['eventId'], - 'reqRating' => $costs ? $costs['reqPersonalRating'] : 0, - 'reqBracket' => $costs ? $costs['reqArenaSlot'] : 0 - ); - - // hardcode arena(103) & honor(104) - if (!empty($costs['reqArenaPoints'])) - { - $data[-103] = $costs['reqArenaPoints']; - $this->jsGlobals[TYPE_CURRENCY][103] = 103; - } - - if (!empty($costs['reqHonorPoints'])) - { - $data[-104] = $costs['reqHonorPoints']; - $this->jsGlobals[TYPE_CURRENCY][104] = 104; - } - - for ($i = 1; $i < 6; $i++) - { - if (!empty($costs['reqItemId'.$i]) && $costs['itemCount'.$i] > 0) - { - $data[$costs['reqItemId'.$i]] = $costs['itemCount'.$i]; - $cItems[] = $costs['reqItemId'.$i]; - } - } - - // no extended cost or additional gold required - if (!$costs || $this->getField('flagsExtra') & 0x04) - if ($_ = $this->getField('buyPrice')) - $data[0] = $_; - - $vendors[$l] = $data; - } - - $itemz[$k] = $vendors; - } - - // convert items to currency if possible - if ($cItems) - { - $moneyItems = new CurrencyList(array(['itemId', $cItems])); - foreach ($moneyItems->getJSGlobals() as $type => $jsData) - foreach ($jsData as $k => $v) - $this->jsGlobals[$type][$k] = $v; - - foreach ($itemz as $id => $vendors) - { - foreach ($vendors as $l => $costs) - { - foreach ($costs as $k => $v) - { - if (in_array($k, $cItems)) - { - $found = false; - foreach ($moneyItems->iterate() as $__) - { - if ($moneyItems->getField('itemId') == $k) - { - unset($costs[$k]); - $costs[-$moneyItems->id] = $v; - $found = true; - break; - } - } - - if (!$found) - $this->jsGlobals[TYPE_ITEM][$k] = $k; - } - } - $vendors[$l] = $costs; - } - $itemz[$id] = $vendors; - } - } - - $this->vendors = $itemz; - } - - $result = $this->vendors; - - // apply filter if given - $tok = !empty($filter[TYPE_ITEM]) ? $filter[TYPE_ITEM] : null; - $cur = !empty($filter[TYPE_CURRENCY]) ? $filter[TYPE_CURRENCY] : null; - - foreach ($result as $itemId => &$data) - { - $reqRating = []; - foreach ($data as $npcId => $costs) - { - if ($tok || $cur) // bought with specific token or currency - { - $valid = false; - foreach ($costs as $k => $qty) - { - if ((!$tok || $k == $tok) && (!$cur || $k == -$cur)) - { - $valid = true; - break; - } - } - - if (!$valid) - unset($data[$npcId]); - } - - // reqRating ins't really a cost .. so pass it by ref instead of return - // use highest total value - if (isset($data[$npcId]) && $costs['reqRating'] && (!$reqRating || $reqRating[0] < $costs['reqRating'])) - $reqRating = [$costs['reqRating'], $costs['reqBracket']]; - } - - if ($reqRating) - $data['reqRating'] = $reqRating[0]; - - if (empty($data)) - unset($result[$itemId]); - } - - return $result; - } - - public function getListviewData($addInfoMask = 0x0, $miscData = null) - { - /* - * ITEMINFO_JSON (0x01): itemMods (including spells) and subitems parsed - * ITEMINFO_SUBITEMS (0x02): searched by comparison - * ITEMINFO_VENDOR (0x04): costs-obj, when displayed as vendor - * ITEMINFO_GEM (0x10): gem infos and score - * ITEMINFO_MODEL (0x20): sameModelAs-Tab - */ - - $data = []; - - // random item is random - if ($addInfoMask & ITEMINFO_SUBITEMS) - $this->initSubItems(); - - if ($addInfoMask & ITEMINFO_JSON) - $this->extendJsonStats(); - - foreach ($this->iterate() as $__) - { - foreach ($this->json[$this->id] as $k => $v) - $data[$this->id][$k] = $v; - - // json vs listview quirk - $data[$this->id]['name'] = $data[$this->id]['quality'].$data[$this->id]['name']; - unset($data[$this->id]['quality']); - - if ($addInfoMask & ITEMINFO_JSON) - { - foreach ($this->itemMods[$this->id] as $k => $v) - $data[$this->id][$k] = $v; - - if ($_ = intVal(($this->curTpl['minMoneyLoot'] + $this->curTpl['maxMoneyLoot']) / 2)) - $data[$this->id]['avgmoney'] = $_; - - if ($_ = $this->curTpl['repairPrice']) - $data[$this->id]['repaircost'] = $_; - } - - if ($addInfoMask & (ITEMINFO_JSON | ITEMINFO_GEM)) - if (isset($this->curTpl['score'])) - $data[$this->id]['score'] = $this->curTpl['score']; - - if ($addInfoMask & ITEMINFO_GEM) - { - $data[$this->id]['uniqEquip'] = ($this->curTpl['flags'] & ITEM_FLAG_UNIQUEEQUIPPED) ? 1 : 0; - $data[$this->id]['socketLevel'] = 0; // not used with wotlk - } - - if ($addInfoMask & ITEMINFO_VENDOR) - { - // just use the first results - // todo (med): dont use first result; search for the right one - if (!empty($this->getExtendedCost($miscData)[$this->id])) - { - $cost = reset($this->getExtendedCost($miscData)[$this->id]); - $currency = []; - $tokens = []; - - foreach ($cost as $k => $qty) - { - if (is_string($k)) - continue; - - if ($k > 0) - $tokens[] = [$k, $qty]; - else if ($k < 0) - $currency[] = [-$k, $qty]; - } - - $data[$this->id]['stock'] = $cost['stock']; // display as column in lv - $data[$this->id]['avail'] = $cost['stock']; // display as number on icon - $data[$this->id]['cost'] = [empty($cost[0]) ? 0 : $cost[0]]; - - if ($cost['event']) - { - $this->jsGlobals[TYPE_WORLDEVENT][$cost['event']] = $cost['event']; - $row['condition'][0][$this->id][] = [[CND_ACTIVE_EVENT, $cost['event']]]; - } - - if ($currency || $tokens) // fill idx:3 if required - $data[$this->id]['cost'][] = $currency; - - if ($tokens) - $data[$this->id]['cost'][] = $tokens; - - if (!empty($cost['reqRating'])) - $data[$this->id]['reqarenartng'] = $cost['reqRating']; - } - - if ($x = $this->curTpl['buyPrice']) - $data[$this->id]['buyprice'] = $x; - - if ($x = $this->curTpl['sellPrice']) - $data[$this->id]['sellprice'] = $x; - - if ($x = $this->curTpl['buyCount']) - $data[$this->id]['stack'] = $x; - } - - if ($this->curTpl['class'] == ITEM_CLASS_GLYPH) - $data[$this->id]['glyph'] = $this->curTpl['subSubClass']; - - if ($x = $this->curTpl['requiredSkill']) - $data[$this->id]['reqskill'] = $x; - - if ($x = $this->curTpl['requiredSkillRank']) - $data[$this->id]['reqskillrank'] = $x; - - if ($x = $this->curTpl['requiredSpell']) - $data[$this->id]['reqspell'] = $x; - - if ($x = $this->curTpl['requiredFaction']) - $data[$this->id]['reqfaction'] = $x; - - if ($x = $this->curTpl['requiredFactionRank']) - { - $data[$this->id]['reqrep'] = $x; - $data[$this->id]['standing'] = $x; // used in /faction item-listing - } - - if ($x = $this->curTpl['slots']) - $data[$this->id]['nslots'] = $x; - - $_ = $this->curTpl['requiredRace']; - if ($_ && $_ & RACE_MASK_ALLIANCE != RACE_MASK_ALLIANCE && $_ & RACE_MASK_HORDE != RACE_MASK_HORDE) - $data[$this->id]['reqrace'] = $_; - - if ($_ = $this->curTpl['requiredClass']) - $data[$this->id]['reqclass'] = $_; // $data[$this->id]['classes'] ?? - - if ($this->curTpl['flags'] & ITEM_FLAG_HEROIC) - $data[$this->id]['heroic'] = true; - - if ($addInfoMask & ITEMINFO_MODEL) - if ($_ = $this->getField('displayId')) - $data[$this->id]['displayid'] = $_; - - if ($this->getSources($s, $sm) && !($addInfoMask & ITEMINFO_MODEL)) - { - $data[$this->id]['source'] = $s; - if ($sm) - $data[$this->id]['sourcemore'] = $sm; - } - - if (!empty($this->curTpl['cooldown'])) - $data[$this->id]['cooldown'] = $this->curTpl['cooldown'] / 1000; - } - - /* even more complicated crap - modelviewer {type:X, displayid:Y, slot:z} .. not sure, when to set - */ - - return $data; - } - - public function getJSGlobals($addMask = GLOBALINFO_SELF, &$extra = []) - { - $data = $addMask & GLOBALINFO_RELATED ? $this->jsGlobals : []; - - foreach ($this->iterate() as $id => $__) - { - if ($addMask & GLOBALINFO_SELF) - { - $data[TYPE_ITEM][$id] = array( - 'name' => $this->getField('name', true), - 'quality' => $this->curTpl['quality'], - 'icon' => $this->curTpl['iconString'] - ); - } - - if ($addMask & GLOBALINFO_EXTRA) - { - $extra[$id] = array( - 'id' => $id, - 'tooltip' => $this->renderTooltip(true), - 'spells' => new StdClass // placeholder for knownSpells - ); - } - } - - return $data; - } - - /* - enhance (set by comparison tool or formated external links) - ench: enchantmentId - sock: bool (extraScoket (gloves, belt)) - gems: array (:-separated itemIds) - rand: >0: randomPropId; <0: randomSuffixId - interactive (set to place javascript/anchors to manipulate level and ratings or link to filters (static tooltips vs popup tooltip)) - subOf (tabled layout doesn't work if used as sub-tooltip in other item or spell tooltips; use line-break instead) - */ - public function getField($field, $localized = false, $silent = false, $enhance = []) - { - $res = parent::getField($field, $localized, $silent); - - if ($field == 'name' && !empty($enhance['r'])) - if ($this->getRandEnchantForItem($enhance['r'])) - $res .= ' '.Util::localizedString($this->enhanceR, 'name'); - - return $res; - } - - public function renderTooltip($interactive = false, $subOf = 0, $enhance = []) - { - if ($this->error) - return; - - $_name = $this->getField('name', true); - $_reqLvl = $this->curTpl['requiredLevel']; - $_quality = $this->curTpl['quality']; - $_flags = $this->curTpl['flags']; - $_class = $this->curTpl['class']; - $_subClass = $this->curTpl['subClass']; - $_slot = $this->curTpl['slot']; - $causesScaling = false; - - if (!empty($enhance['r'])) - { - if ($this->getRandEnchantForItem($enhance['r'])) - { - $_name .= ' '.Util::localizedString($this->enhanceR, 'name'); - $randEnchant = ''; - - for ($i = 1; $i < 6; $i++) - { - if ($this->enhanceR['enchantId'.$i] <= 0) - continue; - - $enchant = DB::Aowow()->selectRow('SELECT * FROM ?_itemenchantment WHERE Id = ?d', $this->enhanceR['enchantId'.$i]); - if ($this->enhanceR['allocationPct'.$i] > 0) - { - $amount = intVal($this->enhanceR['allocationPct'.$i] * $this->generateEnchSuffixFactor()); - $randEnchant .= ''.str_replace('$i', $amount, Util::localizedString($enchant, 'name')).'
'; - } - else - $randEnchant .= ''.Util::localizedString($enchant, 'name').'
'; - } - } - else - unset($enhance['r']); - } - - if (isset($enhance['s']) && !in_array($_slot, [INVTYPE_WRISTS, INVTYPE_WAIST, INVTYPE_HANDS])) - unset($enhance['s']); - - // IMPORTAT: DO NOT REMOVE THE HTML-COMMENTS! THEY ARE REQUIRED TO UPDATE THE TOOLTIP CLIENTSIDE - $x = ''; - - // upper table: stats - if (!$subOf) - $x .= '
'; - - // name; quality - if ($subOf) - $x .= ''.$_name.''; - else - $x .= ''.$_name.''; - - // heroic tag - if (($_flags & ITEM_FLAG_HEROIC) && $_quality == ITEM_QUALITY_EPIC) - $x .= '
'.Lang::item('heroic').''; - - // 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', $_); - $x .= '
'.Util::localizedString($map, 'name').''; - } - - // requires area - if ($this->curTpl['area']) - { - $area = DB::Aowow()->selectRow('SELECT * FROM ?_zones WHERE Id=?d LIMIT 1', $this->curTpl['area']); - $x .= '
'.Util::localizedString($area, 'name'); - } - - // conjured - if ($_flags & ITEM_FLAG_CONJURED) - $x .= '
'.Lang::item('conjured'); - - // bonding - if ($_flags & ITEM_FLAG_ACCOUNTBOUND) - $x .= '
'.Lang::item('bonding', 0); - else if ($this->curTpl['bonding']) - $x .= '
'.Lang::item('bonding', $this->curTpl['bonding']); - - // unique || unique-equipped || unique-limited - if ($this->curTpl['maxCount'] == 1) - $x .= '
'.Lang::item('unique', 0); - // not for currency tokens - else if ($this->curTpl['maxCount'] && $this->curTpl['bagFamily'] != 8192) - $x .= '
'.sprintf(Lang::item('unique', 1), $this->curTpl['maxCount']); - else if ($_flags & ITEM_FLAG_UNIQUEEQUIPPED) - $x .= '
'.Lang::item('uniqueEquipped', 0); - else if ($this->curTpl['itemLimitCategory']) - { - $limit = DB::Aowow()->selectRow("SELECT * FROM ?_itemlimitcategory WHERE id = ?", $this->curTpl['itemLimitCategory']); - $x .= '
'.sprintf(Lang::item($limit['isGem'] ? 'uniqueEquipped' : 'unique', 2), Util::localizedString($limit, 'name'), $limit['count']); - } - - // max duration - if ($dur = $this->curTpl['duration']) - { - $rt = ''; - if ($this->curTpl['flagsCustom'] & 0x1) - $rt = $interactive ? ' ('.sprintf(Util::$dfnString, 'LANG.tooltip_realduration', Lang::item('realTime')).')' : ' ('.Lang::item('realTime').')'; - - $x .= "
".Lang::game('duration').Lang::main('colon').Util::formatTime(abs($dur) * 1000).$rt; - } - - // 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)) - $x .= '
'.sprintf(Lang::game('requires'), ''.Util::localizedString($hName, 'name').''); - - // item begins a quest - if ($this->curTpl['startQuest']) - $x .= '
'.Lang::item('startQuest').''; - - // containerType (slotCount) - if ($this->curTpl['slots'] > 0) - { - $fam = $this->curTpl['bagFamily'] ? log($this->curTpl['bagFamily'], 2) + 1 : 0; - $x .= '
'.Lang::item('bagSlotString', [$this->curTpl['slots'], Lang::item('bagFamily', $fam)]); - } - - if (in_array($_class, [ITEM_CLASS_ARMOR, ITEM_CLASS_WEAPON, ITEM_CLASS_AMMUNITION])) - { - $x .= ''; - - // Class - if ($_slot) - $x .= ''; - - // Subclass - if ($_class == ITEM_CLASS_ARMOR && $_subClass > 0) - $x .= ''; - else if ($_class == ITEM_CLASS_WEAPON) - $x .= ''; - else if ($_class == ITEM_CLASS_AMMUNITION) - $x .= ''; - - $x .= '
'.Lang::item('inventoryType', $_slot).''.Lang::item('armorSubClass', $_subClass).''.Lang::item('weaponSubClass', $_subClass).''.Lang::item('projectileSubClass', $_subClass).'
'; - } - else if ($_slot && $_class != ITEM_CLASS_CONTAINER) // yes, slot can occur on random items and is then also displayed <_< .. excluding Bags >_> - $x .= '
'.Lang::item('inventoryType', $_slot).'
'; - else - $x .= '
'; - - // Weapon/Ammunition Stats (not limited to weapons (see item:1700)) - $speed = $this->curTpl['delay'] / 1000; - $sc1 = $this->curTpl['dmgType1']; - $sc2 = $this->curTpl['dmgType2']; - $dmgmin = $this->curTpl['dmgMin1'] + $this->curTpl['dmgMin2']; - $dmgmax = $this->curTpl['dmgMax1'] + $this->curTpl['dmgMax2']; - $dps = $speed ? ($dmgmin + $dmgmax) / (2 * $speed) : 0; - - if ($_class == ITEM_CLASS_AMMUNITION && $dmgmin && $dmgmax) - { - if ($sc1) - $x .= sprintf(Lang::item('damage', 'ammo', 1), ($dmgmin + $dmgmax) / 2, Lang::game('sc', $sc1)).'
'; - else - $x .= sprintf(Lang::item('damage', 'ammo', 0), ($dmgmin + $dmgmax) / 2).'
'; - } - else if ($dps) - { - if ($this->curTpl['dmgMin1'] == $this->curTpl['dmgMax1']) - $dmg = sprintf(Lang::item('damage', 'single', $sc1 ? 1 : 0), $this->curTpl['dmgMin1'], $sc1 ? Lang::game('sc', $sc1) : null); - else - $dmg = sprintf(Lang::item('damage', 'range', $sc1 ? 1 : 0), $this->curTpl['dmgMin1'], $this->curTpl['dmgMax1'], $sc1 ? Lang::game('sc', $sc1) : null); - - if ($_class == ITEM_CLASS_WEAPON) // do not use localized format here! - $x .= '
'.$dmg.''.Lang::item('speed').' '.number_format($speed, 2).'
'; - else - $x .= ''.$dmg.'
'; - - // secondary damage is set - if (($this->curTpl['dmgMin2'] || $this->curTpl['dmgMax2']) && $this->curTpl['dmgMin2'] != $this->curTpl['dmgMax2']) - $x .= sprintf(Lang::item('damage', 'range', $sc2 ? 3 : 2), $this->curTpl['dmgMin2'], $this->curTpl['dmgMax2'], $sc2 ? Lang::game('sc', $sc2) : null).'
'; - else if ($this->curTpl['dmgMin2']) - $x .= sprintf(Lang::item('damage', 'single', $sc2 ? 3 : 2), $this->curTpl['dmgMin2'], $sc2 ? Lang::game('sc', $sc2) : null).'
'; - - if ($_class == ITEM_CLASS_WEAPON) - $x .= ''.sprintf(Lang::item('dps'), $dps).'
'; // do not use localized format here! - - // display FeralAttackPower if set - if ($fap = $this->getFeralAP()) - $x .= '('.$fap.' '.Lang::item('fap').')
'; - } - - // Armor - if ($_class == ITEM_CLASS_ARMOR && $this->curTpl['armorDamageModifier'] > 0) - { - $spanI = 'class="q2"'; - if ($interactive) - $spanI = 'class="q2 tip" onmouseover="$WH.Tooltip.showAtCursor(event, $WH.sprintf(LANG.tooltip_armorbonus, '.$this->curTpl['armorDamageModifier'].'), 0, 0, \'q\')" onmousemove="$WH.Tooltip.cursorUpdate(event)" onmouseout="$WH.Tooltip.hide()"'; - - $x .= ''.Lang::item('armor', [intVal($this->curTpl['armor'] - $this->curTpl['armorDamageModifier'])]).'
'; - } - else if (($this->curTpl['armor'] - $this->curTpl['armorDamageModifier']) > 0) - $x .= ''.Lang::item('armor', [intVal($this->curTpl['armor'] - $this->curTpl['armorDamageModifier'])]).'
'; - - // Block (note: block value from field block and from field stats or parsed from itemSpells are displayed independently) - if ($this->curTpl['tplBlock']) - $x .= ''.sprintf(Lang::item('block'), $this->curTpl['tplBlock']).'
'; - - // 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); - $x .= ''.Util::localizedString($gemEnch, 'name').'
'; - - // activation conditions for meta gems - if (!empty($gemEnch['conditionId'])) - { - if ($gemCnd = DB::Aowow()->selectRow('SELECT * FROM ?_itemenchantmentcondition WHERE id = ?d', $gemEnch['conditionId'])) - { - for ($i = 1; $i < 6; $i++) - { - if (!$gemCnd['color'.$i]) - continue; - - $vspfArgs = []; - switch ($gemCnd['comparator'.$i]) - { - case 2: // requires less than ( || ) gems - case 5: // requires at least than ( || ) gems - $vspfArgs = [$gemCnd['value'.$i], Lang::item('gemColors', $gemCnd['color'.$i] - 1)]; - break; - case 3: // requires more than ( || ) gems - $vspfArgs = [Lang::item('gemColors', $gemCnd['color'.$i] - 1), Lang::item('gemColors', $gemCnd['cmpColor'.$i] - 1)]; - break; - default: - continue; - } - - $x .= ''.Lang::achievement('reqNumCrt').' '.Lang::item('gemConditions', $gemCnd['comparator'.$i], $vspfArgs).'
'; - } - } - } - } - - // Random Enchantment - if random enchantment is set, prepend stats from it - if ($this->curTpl['randomEnchant'] && empty($enhance['r'])) - $x .= ''.Lang::item('randEnchant').'
'; - else if (!empty($enhance['r'])) - $x .= $randEnchant; - - // itemMods (display stats and save ratings for later use) - for ($j = 1; $j <= 10; $j++) - { - $type = $this->curTpl['statType'.$j]; - $qty = $this->curTpl['statValue'.$j]; - - if (!$qty || $type <= 0) - continue; - - // base stat - switch ($type) - { - case ITEM_MOD_MANA: - case ITEM_MOD_HEALTH: - // $type += 1; // i think i fucked up somewhere mapping item_mods: offsets may be required somewhere - case ITEM_MOD_AGILITY: - case ITEM_MOD_STRENGTH: - case ITEM_MOD_INTELLECT: - case ITEM_MOD_SPIRIT: - case ITEM_MOD_STAMINA: - $x .= ''.($qty > 0 ? '+' : '-').abs($qty).' '.Lang::item('statType', $type).'
'; - break; - default: // rating with % for reqLevel - $green[] = $this->parseRating($type, $qty, $interactive, $causesScaling); - - } - } - - // magic resistances - foreach (Game::$resistanceFields as $j => $rowName) - if ($rowName && $this->curTpl[$rowName] != 0) - $x .= '+'.$this->curTpl[$rowName].' '.Lang::game('resistances', $j).'
'; - - // Enchantment - if (isset($enhance['e'])) - { - if ($enchText = DB::Aowow()->selectRow('SELECT * FROM ?_itemenchantment WHERE Id = ?', $enhance['e'])) - $x .= ''.Util::localizedString($enchText, 'name').'
'; - else - { - unset($enhance['e']); - $x .= ''; - } - } - else // enchantment placeholder - $x .= ''; - - // Sockets w/ Gems - if (!empty($enhance['g'])) - { - $gems = DB::Aowow()->select(' - 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)', - $enhance['g']); - foreach ($enhance['g'] as $k => $v) - if ($v && !in_array($v, array_keys($gems))) // 0 is valid - unset($enhance['g'][$k]); - } - else - $enhance['g'] = []; - - // zero fill empty sockets - $sockCount = isset($enhance['s']) ? 1 : 0; - if (!empty($this->json[$this->id]['nsockets'])) - $sockCount += $this->json[$this->id]['nsockets']; - - while ($sockCount > count($enhance['g'])) - $enhance['g'][] = 0; - - $enhance['g'] = array_reverse($enhance['g']); - - $hasMatch = 1; - // fill native sockets - for ($j = 1; $j <= 3; $j++) - { - if (!$this->curTpl['socketColor'.$j]) - continue; - - for ($i = 0; $i < 4; $i++) - if (($this->curTpl['socketColor'.$j] & (1 << $i))) - $colorId = $i; - - $pop = array_pop($enhance['g']); - $col = $pop ? 1 : 0; - $hasMatch &= $pop ? (($gems[$pop]['colorMask'] & (1 << $colorId)) ? 1 : 0) : 0; - $icon = $pop ? sprintf(Util::$bgImagePath['tiny'], STATIC_URL, strtolower($gems[$pop]['iconString'])) : null; - $text = $pop ? Util::localizedString($gems[$pop], 'name') : Lang::item('socket', $colorId); - - if ($interactive) - $x .= ''.$text.'
'; - else - $x .= ''.$text.'
'; - } - - // fill extra socket - if (isset($enhance['s'])) - { - $pop = array_pop($enhance['g']); - $col = $pop ? 1 : 0; - $icon = $pop ? sprintf(Util::$bgImagePath['tiny'], STATIC_URL, strtolower($gems[$pop]['iconString'])) : null; - $text = $pop ? Util::localizedString($gems[$pop], 'name') : Lang::item('socket', -1); - - if ($interactive) - $x .= ''.$text.'
'; - else - $x .= ''.$text.'
'; - } - else // prismatic socket placeholder - $x .= ''; - - if ($_ = $this->curTpl['socketBonus']) - { - $sbonus = DB::Aowow()->selectRow('SELECT * FROM ?_itemenchantment WHERE Id = ?d', $_); - $x .= ''.Lang::item('socketBonus').Lang::main('colon').''.Util::localizedString($sbonus, 'name').'
'; - } - - // durability - if ($dur = $this->curTpl['durability']) - $x .= sprintf(Lang::item('durability'), $dur, $dur).'
'; - - // required classes - if ($classes = Lang::getClassString($this->curTpl['requiredClass'], $jsg, $__)) - { - foreach ($jsg as $js) - if (empty($this->jsGlobals[TYPE_CLASS][$js])) - $this->jsGlobals[TYPE_CLASS][$js] = $js; - - $x .= Lang::game('classes').Lang::main('colon').$classes.'
'; - } - - // required races - if ($races = Lang::getRaceString($this->curTpl['requiredRace'], $__, $jsg, $__)) - { - foreach ($jsg as $js) - if (empty($this->jsGlobals[TYPE_RACE][$js])) - $this->jsGlobals[TYPE_RACE][$js] = $js; - - if ($races != Lang::game('ra', 0)) // not "both", but display combinations like: troll, dwarf - $x .= Lang::game('races').Lang::main('colon').$races.'
'; - } - - // required honorRank (not used anymore) - if ($rhr = $this->curTpl['requiredHonorRank']) - $x .= sprintf(Lang::game('requires'), Lang::game('pvpRank', $rhr)).'
'; - - // required CityRank..? - // what the f.. - - // required level - if (($_flags & ITEM_FLAG_ACCOUNTBOUND) && $_quality == ITEM_QUALITY_HEIRLOOM) - $x .= sprintf(Lang::item('reqLevelRange'), 1, MAX_LEVEL, ($interactive ? sprintf(Util::$changeLevelString, MAX_LEVEL) : ''.MAX_LEVEL)).'
'; - else if ($_reqLvl > 1) - $x .= sprintf(Lang::item('reqMinLevel'), $_reqLvl).'
'; - - // required arena team rating / personal rating / todo (low): sort out what kind of rating - if (!empty($this->getExtendedCost([], $reqRating)[$this->id]) && $reqRating) - $x .= sprintf(Lang::item('reqRating', $reqRating[1]), $reqRating[0]).'
'; - - // item level - if (in_array($_class, [ITEM_CLASS_ARMOR, ITEM_CLASS_WEAPON])) - $x .= sprintf(Lang::item('itemLevel'), $this->curTpl['itemLevel']).'
'; - - // required skill - if ($reqSkill = $this->curTpl['requiredSkill']) - { - $_ = ''.SkillList::getName($reqSkill).''; - if ($this->curTpl['requiredSkillRank'] > 0) - $_ .= ' ('.$this->curTpl['requiredSkillRank'].')'; - - $x .= sprintf(Lang::game('requires'), $_).'
'; - } - - // required spell - if ($reqSpell = $this->curTpl['requiredSpell']) - $x .= Lang::game('requires2').' '.SpellList::getName($reqSpell).'
'; - - // required reputation w/ faction - if ($reqFac = $this->curTpl['requiredFaction']) - $x .= sprintf(Lang::game('requires'), ''.FactionList::getName($reqFac).' - '.Lang::game('rep', $this->curTpl['requiredFactionRank'])).'
'; - - // locked or openable - if ($locks = Lang::getLocks($this->curTpl['lockId'], true)) - $x .= ''.Lang::item('locked').'
'.implode('
', $locks).'

'; - else if ($this->curTpl['flags'] & ITEM_FLAG_OPENABLE) - $x .= ''.Lang::item('openClick').'
'; - - // upper table: done - if (!$subOf) - $x .= '
'; - - // spells on item - if (!$this->canTeachSpell()) - { - $itemSpellsAndTrigger = []; - for ($j = 1; $j <= 5; $j++) - { - if ($this->curTpl['spellId'.$j] > 0) - { - $cd = $this->curTpl['spellCooldown'.$j]; - if ($cd < $this->curTpl['spellCategoryCooldown'.$j]) - $cd = $this->curTpl['spellCategoryCooldown'.$j]; - - $cd = $cd < 5000 ? null : ' ('.sprintf(Lang::game('cooldown'), Util::formatTime($cd)).')'; - - $itemSpellsAndTrigger[$this->curTpl['spellId'.$j]] = [$this->curTpl['spellTrigger'.$j], $cd]; - } - } - - if ($itemSpellsAndTrigger) - { - $cooldown = ''; - - $itemSpells = new SpellList(array(['s.id', array_keys($itemSpellsAndTrigger)])); - foreach ($itemSpells->iterate() as $__) - if ($parsed = $itemSpells->parseText('description', $_reqLvl > 1 ? $_reqLvl : MAX_LEVEL, false, $causesScaling)[0]) - { - if ($interactive) - { - $link = '%s'; - $parsed = preg_replace_callback('/([^;]*)( .*?<\/small>)([^&]*)/i', function($m) use($link) { - $m[1] = $m[1] ? sprintf($link, $m[1]) : ''; - $m[3] = $m[3] ? sprintf($link, $m[3]) : ''; - return $m[1].$m[2].$m[3]; - }, $parsed, -1, $nMatches - ); - - if (!$nMatches) - $parsed = sprintf($link, $parsed); - } - - $green[] = Lang::item('trigger', $itemSpellsAndTrigger[$itemSpells->id][0]).$parsed.$itemSpellsAndTrigger[$itemSpells->id][1]; - } - } - } - - // lower table (ratings, spells, ect) - if (!$subOf) - $x .= '
'; - - if (isset($green)) - foreach ($green as $j => $bonus) - if ($bonus) - $x .= ''.$bonus.'
'; - - // Item Set - $pieces = []; - if ($setId = $this->getField('itemset')) - { - // while Ids can technically be used multiple times the only difference in data are the items used. So it doesn't matter what we get - $itemset = new ItemsetList(array(['id', $setId])); - if (!$itemset->error && $itemset->pieceToSet) - { - $pieces = DB::Aowow()->select(' - SELECT b.id AS ARRAY_KEY, b.name_loc0, b.name_loc2, b.name_loc3, 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) - GROUP BY b.id;', - array_keys($itemset->pieceToSet) - ); - - foreach ($pieces as $k => &$p) - $p = ''.Util::localizedString($p, 'name').''; - - $xSet = '
'.Lang::item('setName', [''.$itemset->getField('name', true).'', 0, count($pieces)]).''; - - if ($skId = $itemset->getField('skillId')) // bonus requires skill to activate - { - $xSet .= '
'.sprintf(Lang::game('requires'), ''.SkillList::getName($skId).''); - - if ($_ = $itemset->getField('skillLevel')) - $xSet .= ' ('.$_.')'; - - $xSet .= '
'; - } - - // list pieces - $xSet .= '
'.implode('
', $pieces).'

'; - - // get bonuses - $setSpellsAndIdx = []; - for ($j = 1; $j <= 8; $j++) - if ($_ = $itemset->getField('spell'.$j)) - $setSpellsAndIdx[$_] = $j; - - $setSpells = []; - if ($setSpellsAndIdx) - { - $boni = new SpellList(array(['s.id', array_keys($setSpellsAndIdx)])); - foreach ($boni->iterate() as $__) - { - $setSpells[] = array( - 'tooltip' => $boni->parseText('description', $_reqLvl > 1 ? $_reqLvl : MAX_LEVEL, false, $causesScaling)[0], - 'entry' => $itemset->getField('spell'.$setSpellsAndIdx[$boni->id]), - 'bonus' => $itemset->getField('bonus'.$setSpellsAndIdx[$boni->id]) - ); - } - } - - // sort and list bonuses - $xSet .= ''; - for ($i = 0; $i < count($setSpells); $i++) - { - for ($j = $i; $j < count($setSpells); $j++) - { - if ($setSpells[$j]['bonus'] >= $setSpells[$i]['bonus']) - continue; - - $tmp = $setSpells[$i]; - $setSpells[$i] = $setSpells[$j]; - $setSpells[$j] = $tmp; - } - $xSet .= ''.Lang::item('setBonus', [$setSpells[$i]['bonus'], ''.$setSpells[$i]['tooltip'].'']).''; - if ($i < count($setSpells) - 1) - $xSet .= '
'; - } - $xSet .= '
'; - } - } - - // recipes, vanity pets, mounts - if ($this->canTeachSpell()) - { - $craftSpell = new SpellList(array(['s.id', intVal($this->curTpl['spellId2'])])); - if (!$craftSpell->error) - { - $xCraft = ''; - if ($desc = $this->getField('description', true)) - $x .= ''.Lang::item('trigger', 0).' '.$desc.'
'; - - // recipe handling (some stray Techniques have subclass == 0), place at bottom of tooltipp - if ($_class == ITEM_CLASS_RECIPE || $this->curTpl['bagFamily'] == 16) - { - $craftItem = new ItemList(array(['i.id', (int)$craftSpell->curTpl['effect1CreateItemId']])); - if (!$craftItem->error) - { - if ($itemTT = $craftItem->renderTooltip($interactive, $this->id)) - $xCraft .= '

'.$itemTT.'
'; - - $reagentItems = []; - for ($i = 1; $i <= 8; $i++) - if ($rId = $craftSpell->getField('reagent'.$i)) - $reagentItems[$rId] = $craftSpell->getField('reagentCount'.$i); - - if (isset($xCraft) && $reagentItems) - { - $reagents = new ItemList(array(['i.id', array_keys($reagentItems)])); - $reqReag = []; - - foreach ($reagents->iterate() as $__) - $reqReag[] = ''.$reagents->getField('name', true).' ('.$reagentItems[$reagents->id].')'; - - $xCraft .= '

'.Lang::game('requires2').' '.implode(', ', $reqReag).'
'; - } - } - } - } - } - - // misc (no idea, how to organize the
better) - $xMisc = []; - - // itemset: pieces and boni - if (isset($xSet)) - $xMisc[] = $xSet; - - // funny, yellow text at the bottom, omit if we have a recipe - if ($this->curTpl['description_loc0'] && !$this->canTeachSpell()) - $xMisc[] = '"'.$this->getField('description', true).'"'; - - // readable - if ($this->curTpl['pageTextId']) - $xMisc[] = ''.Lang::item('readClick').''; - - // charges (i guess checking first spell is enough) - if ($this->curTpl['spellCharges1']) - $xMisc[] = ''.Lang::item('charges', [abs($this->curTpl['spellCharges1'])]).''; - - // list required reagents - if (isset($xCraft)) - $xMisc[] = $xCraft; - - if ($xMisc) - $x .= implode('
', $xMisc); - - if ($sp = $this->curTpl['sellPrice']) - $x .= '
'.Lang::item('sellPrice').Lang::main('colon').Util::formatMoney($sp).'
'; - - if (!$subOf) - $x .= '
'; - - // tooltip scaling - if (!isset($xCraft)) - { - $link = [$subOf ? $subOf : $this->id, 1]; // itemId, scaleMinLevel - if (isset($this->ssd[$this->id])) // is heirloom - { - array_push($link, - $this->ssd[$this->id]['maxLevel'], // scaleMaxLevel - $this->ssd[$this->id]['maxLevel'], // scaleCurLevel - $this->curTpl['scalingStatDistribution'], // scaleDist - $this->curTpl['scalingStatValue'] // scaleFlags - ); - } - else // may still use level dependant ratings - { - array_push($link, - $causesScaling ? MAX_LEVEL : 1, // scaleMaxLevel - $_reqLvl > 1 ? $_reqLvl : MAX_LEVEL // scaleCurLevel - ); - } - $x .= ''; - } - - return $x; - } - - public function getRandEnchantForItem($randId) - { - // 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)) - $this->enhanceR = $_; - - return !empty($this->enhanceR); - } - - // from Trinity - public function generateEnchSuffixFactor() - { - $rpp = DB::Aowow()->selectRow('SELECT * FROM ?_itemrandomproppoints WHERE Id = ?', $this->curTpl['itemLevel']); - if (!$rpp) - return 0; - - switch ($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; - } - - // Select rare/epic modifier - switch ($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; - } - - public function extendJsonStats() - { - $enchantments = []; // buffer Ids for lookup id => src; src>0: socketBonus; src<0: gemEnchant - - foreach ($this->iterate() as $__) - { - $this->itemMods[$this->id] = []; - - foreach (Game::$itemMods as $mod) - { - if (isset($this->curTpl[$mod]) && ($_ = floatVal($this->curTpl[$mod]))) - { - if (!isset($this->itemMods[$this->id][$mod])) - $this->itemMods[$this->id][$mod] = 0; - - $this->itemMods[$this->id][$mod] += $_; - } - } - - // fetch and add socketbonusstats - if (!empty($this->json[$this->id]['socketbonus'])) - $enchantments[$this->json[$this->id]['socketbonus']][] = $this->id; - - // Item is a gem (don't mix with sockets) - if ($geId = $this->curTpl['gemEnchantmentId']) - $enchantments[$geId][] = -$this->id; - } - - 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); - - // and merge enchantments back - foreach ($enchantments as $eId => $items) - { - if (empty($eStats[$eId])) - continue; - - foreach ($items as $item) - { - if ($item > 0) // apply socketBonus - $this->json[$item]['socketbonusstat'] = array_filter($eStats[$eId]); - else /* if ($item < 0) */ // apply gemEnchantment - Util::arraySumByKey($this->json[-$item][$mod], array_filter($eStats[$eId])); - } - } - } - - foreach ($this->json as $item => $json) - foreach ($json as $k => $v) - if (!$v && !in_array($k, ['classs', 'subclass', 'quality', 'side', 'gearscore'])) - unset($this->json[$item][$k]); - } - - public function getOnUseStats() - { - $onUseStats = []; - - // convert Spells - $useSpells = []; - for ($h = 1; $h <= 5; $h++) - { - if ($this->curTpl['spellId'.$h] <= 0) - continue; - - if ($this->curTpl['class'] != ITEM_CLASS_CONSUMABLE || $this->curTpl['spellTrigger'.$h]) - continue; - - $useSpells[] = $this->curTpl['spellId'.$h]; - } - - if ($useSpells) - { - $eqpSplList = new SpellList(array(['s.id', $useSpells])); - foreach ($eqpSplList->getStatGain() as $stat) - Util::arraySumByKey($onUseStats, $stat); - } - - return $onUseStats; - } - - public function getSourceData() - { - $data = []; - - foreach ($this->iterate() as $__) - { - $data[$this->id] = array( - 'n' => $this->getField('name', true), - 't' => TYPE_ITEM, - 'ti' => $this->id, - 'q' => $this->curTpl['quality'], - // 'p' => PvP [NYI] - 'icon' => $this->curTpl['iconString'] - ); - } - - return $data; - } - - private function canTeachSpell() - { - // 483: learn recipe; 55884: learn mount/pet - if (!in_array($this->curTpl['spellId1'], [483, 55884])) - return false; - - // needs learnable spell - if (!$this->curTpl['spellId2']) - return false; - - return true; - } - - private function getFeralAP() - { - // must be weapon - if ($this->curTpl['class'] != ITEM_CLASS_WEAPON) - return 0; - - // must be 2H weapon (2H-Mace, Polearm, Staff, ..Fishing Pole) - if (!in_array($this->curTpl['subClass'], [5, 6, 10, 20])) - return 0; - - // thats fucked up.. - if (!$this->curTpl['delay']) - return 0; - - // must have enough damage - $dps = ($this->curTpl['dmgMin1'] + $this->curTpl['dmgMin2'] + $this->curTpl['dmgMax1'] + $this->curTpl['dmgMax2']) / (2 * $this->curTpl['delay'] / 1000); - if ($dps < 54.8) - return 0; - - return round(($dps - 54.8) * 14, 0); - } - - public function getSources(&$s, &$sm) - { - $s = $sm = null; - if (empty($this->sources[$this->id])) - return false; - - if ($this->sourceMore === null) - { - $buff = []; - $this->sourceMore = []; - - foreach ($this->iterate() as $_curTpl) - if ($_curTpl['moreType'] && $_curTpl['moreTypeId']) - $buff[$_curTpl['moreType']][] = $_curTpl['moreTypeId']; - - foreach ($buff as $type => $ids) - $this->sourceMore[$type] = (new Util::$typeClasses[$type](array(['id', $ids])))->getSourceData(); - } - - $s = array_keys($this->sources[$this->id]); - if ($this->curTpl['moreType'] && $this->curTpl['moreTypeId'] && !empty($this->sourceMore[$this->curTpl['moreType']][$this->curTpl['moreTypeId']])) - $sm = [$this->sourceMore[$this->curTpl['moreType']][$this->curTpl['moreTypeId']]]; - else if (!empty($this->sources[$this->id][3])) - $sm = [['p' => $this->sources[$this->id][3][0]]]; - - return true; - } - - private function parseRating($type, $value, $interactive = false, &$scaling = false) - { - // clamp level range - $ssdLvl = isset($this->ssd[$this->id]) ? $this->ssd[$this->id]['maxLevel'] : 1; - $reqLvl = $this->curTpl['requiredLevel'] > 1 ? $this->curTpl['requiredLevel'] : MAX_LEVEL; - $level = min(max($reqLvl, $ssdLvl), MAX_LEVEL); - - // unknown rating - if (in_array($type, [2, 8, 9, 10, 11]) || $type > ITEM_MOD_BLOCK_VALUE || $type < 0) - { - if (User::isInGroup(U_GROUP_EMPLOYEE)) - return sprintf(Lang::item('statType', count(Lang::item('statType')) - 1), $type, $value); - else - return null; - } - // level independant Bonus - else if (in_array($type, Game::$lvlIndepRating)) - return Lang::item('trigger', 1).str_replace('%d', ''.$value, Lang::item('statType', $type)); - // rating-Bonuses - else - { - $scaling = true; - - if ($interactive) - $js = ' ('.sprintf(Util::$changeLevelString, Util::setRatingLevel($level, $type, $value)).')'; - else - $js = ' ('.Util::setRatingLevel($level, $type, $value).')'; - - return Lang::item('trigger', 1).str_replace('%d', ''.$value.$js, Lang::item('statType', $type)); - } - } - - private function getSSDMod($type) - { - $mask = $this->curTpl['scalingStatValue']; - - switch ($type) - { - case 'stats': $mask &= 0x04001F; break; - case 'armor': $mask &= 0xF001E0; break; - case 'dps' : $mask &= 0x007E00; break; - case 'spell': $mask &= 0x008000; break; - case 'fap' : $mask &= 0x010000; break; // unused - default: $mask &= 0x0; - } - - $field = null; - for ($i = 0; $i < count(Util::$ssdMaskFields); $i++) - 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; - } - - private function initScalingStats() - { - $this->ssd[$this->id] = DB::Aowow()->selectRow('SELECT * FROM ?_scalingstatdistribution WHERE id = ?d', $this->curTpl['scalingStatDistribution']); - - if (!$this->ssd[$this->id]) - return; - - // stats and ratings - for ($i = 1; $i <= 10; $i++) - { - if ($this->ssd[$this->id]['statMod'.$i] <= 0) - { - $this->templates[$this->id]['statType'.$i] = 0; - $this->templates[$this->id]['statValue'.$i] = 0; - } - else - { - $this->templates[$this->id]['statType'.$i] = $this->ssd[$this->id]['statMod'.$i]; - $this->templates[$this->id]['statValue'.$i] = intVal(($this->getSSDMod('stats') * $this->ssd[$this->id]['modifier'.$i]) / 10000); - } - } - - // armor: only replace if set - if ($ssvArmor = $this->getSSDMod('armor')) - $this->templates[$this->id]['armor'] = $ssvArmor; - - // if set dpsMod in ScalingStatValue use it for min/max damage - // mle: 20% range / rgd: 30% range - if ($extraDPS = $this->getSSDMod('dps')) // dmg_x2 not used for heirlooms - { - $range = isset($this->json[$this->id]['rgddps']) ? 0.3 : 0.2; - $average = $extraDPS * $this->curTpl['delay'] / 1000; - - $this->templates[$this->id]['dmgMin1'] = floor((1 - $range) * $average); - $this->templates[$this->id]['dmgMax1'] = floor((1 + $range) * $average); - } - - // apply Spell Power from ScalingStatValue if set - if ($spellBonus = $this->getSSDMod('spell')) - { - $this->templates[$this->id]['statType10'] = ITEM_MOD_SPELL_POWER; - $this->templates[$this->id]['statValue10'] = $spellBonus; - } - } - - public function initSubItems() - { - if (!array_keys($this->templates)) - return; - - $subItemIds = []; - foreach ($this->iterate() as $__) - if ($_ = $this->getField('randomEnchant')) - $subItemIds[abs($_)] = $_; - - if (!$subItemIds) - 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)', - array_keys(array_filter($subItemIds, function ($v) { return $v > 0; })) ?: [0], - array_keys(array_filter($subItemIds, function ($v) { return $v < 0; })) ?: [0] - ); - - $randIds = []; - foreach ($subItemTpls as $tpl) - $randIds = array_merge($randIds, array_keys($tpl)); - - if (!$randIds) - return; - - $randEnchants = DB::Aowow()->select('SELECT *, id AS ARRAY_KEY FROM ?_itemrandomenchant WHERE id IN (?a)', $randIds); - $enchIds = array_unique(array_merge( - array_column($randEnchants, 'enchantId1'), - array_column($randEnchants, 'enchantId2'), - array_column($randEnchants, 'enchantId3'), - array_column($randEnchants, 'enchantId4'), - array_column($randEnchants, 'enchantId5') - )); - - $enchants = new EnchantmentList(array(['id', $enchIds], CFG_SQL_LIMIT_NONE)); - foreach ($enchants->iterate() as $eId => $_) - { - $this->rndEnchIds[$eId] = array( - 'text' => $enchants->getField('name', true), - 'stats' => $enchants->getStatGain(true) - ); - } - - foreach ($this->iterate() as $mstItem => $__) - { - if (!$this->getField('randomEnchant')) - continue; - - if (empty($subItemTpls[$this->getField('randomEnchant')])) - continue; - - foreach ($subItemTpls[$this->getField('randomEnchant')] as $subId => $data) - { - if (empty($randEnchants[$subId])) - continue; - - $data = array_merge($randEnchants[$subId], $data); - $jsonEquip = []; - $jsonText = []; - - for ($i = 1; $i < 6; $i++) - { - $enchId = $data['enchantId'.$i]; - if ($enchId <= 0 || empty($this->rndEnchIds[$enchId])) - continue; - - if ($data['allocationPct'.$i] > 0) // RandomSuffix: scaling Enchantment; enchId < 0 - { - $qty = intVal($data['allocationPct'.$i] * $this->generateEnchSuffixFactor()); - $stats = array_fill_keys(array_keys($this->rndEnchIds[$enchId]['stats']), $qty); - - $jsonText[$enchId] = str_replace('$i', $qty, $this->rndEnchIds[$enchId]['text']); - Util::arraySumByKey($jsonEquip, $stats); - } - else // RandomProperty: static Enchantment; enchId > 0 - { - $jsonText[$enchId] = $this->rndEnchIds[$enchId]['text']; - Util::arraySumByKey($jsonEquip, $this->rndEnchIds[$enchId]['stats']); - } - } - - $this->subItems[$mstItem][$subId] = array( - 'name' => Util::localizedString($data, 'name'), - 'enchantment' => $jsonText, - 'jsonequip' => $jsonEquip, - 'chance' => $data['chance'] // hmm, only needed for item detail page... - ); - } - - if (!empty($this->subItems[$mstItem])) - $this->json[$mstItem]['subitems'] = $this->subItems[$mstItem]; - } - } - - public function getScoreTotal($class = 0, $spec = [], $mhItem = 0, $ohItem = 0) - { - if (!$class || !$spec) - return array_sum(array_column($this->json, 'gearscore')); - - $score = 0.0; - $mh = $oh = []; - - foreach ($this->json as $j) - { - if ($j['id'] == $mhItem) - $mh = $j; - else if ($j['id'] == $ohItem) - $oh = $j; - else if ($j['gearscore']) - { - if ($j['slot'] == INVTYPE_RELIC) - $score += 20; - - $score += round($j['gearscore']); - } - } - - $score += array_sum(Util::fixWeaponScores($class, $spec, $mh, $oh)); - - return $score; - } - - private function initJsonStats() - { - $json = array( - 'id' => $this->id, - 'name' => $this->getField('name', true), - 'quality' => ITEM_QUALITY_HEIRLOOM - $this->curTpl['quality'], - 'icon' => $this->curTpl['iconString'], - 'classs' => $this->curTpl['class'], - 'subclass' => $this->curTpl['subClass'], - 'subsubclass' => $this->curTpl['subSubClass'], - 'heroic' => ($this->curTpl['flags'] & 0x8) >> 3, - 'side' => $this->curTpl['flagsExtra'] & 0x3 ? 3 - ($this->curTpl['flagsExtra'] & 0x3) : Game::sideByRaceMask($this->curTpl['requiredRace']), - 'slot' => $this->curTpl['slot'], - 'slotbak' => $this->curTpl['slotBak'], - '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'], - 'frores' => $this->curTpl['resFrost'], - 'shares' => $this->curTpl['resShadow'], - 'arcres' => $this->curTpl['resArcane'], - 'armorbonus' => $this->curTpl['armorDamageModifier'], - 'armor' => $this->curTpl['armor'], - 'dura' => $this->curTpl['durability'], - 'itemset' => $this->curTpl['itemset'], - 'socket1' => $this->curTpl['socketColor1'], - 'socket2' => $this->curTpl['socketColor2'], - 'socket3' => $this->curTpl['socketColor3'], - 'nsockets' => ($this->curTpl['socketColor1'] > 0 ? 1 : 0) + ($this->curTpl['socketColor2'] > 0 ? 1 : 0) + ($this->curTpl['socketColor3'] > 0 ? 1 : 0), - 'socketbonus' => $this->curTpl['socketBonus'], - 'scadist' => $this->curTpl['scalingStatDistribution'], - 'scaflags' => $this->curTpl['scalingStatValue'] - ); - - if ($this->curTpl['class'] == ITEM_CLASS_WEAPON || $this->curTpl['class'] == ITEM_CLASS_AMMUNITION) - { - - $json['dmgtype1'] = $this->curTpl['dmgType1']; - $json['dmgmin1'] = $this->curTpl['dmgMin1'] + $this->curTpl['dmgMin2']; - $json['dmgmax1'] = $this->curTpl['dmgMax1'] + $this->curTpl['dmgMax2']; - $json['speed'] = number_format($this->curTpl['delay'] / 1000, 2); - $json['dps'] = !floatVal($json['speed']) ? 0 : number_format(($json['dmgmin1'] + $json['dmgmax1']) / (2 * $json['speed']), 1); - - if (in_array($json['subclass'], [2, 3, 18, 19])) - { - $json['rgddmgmin'] = $json['dmgmin1']; - $json['rgddmgmax'] = $json['dmgmax1']; - $json['rgdspeed'] = $json['speed']; - $json['rgddps'] = $json['dps']; - } - else if ($json['classs'] != ITEM_CLASS_AMMUNITION) - { - $json['mledmgmin'] = $json['dmgmin1']; - $json['mledmgmax'] = $json['dmgmax1']; - $json['mlespeed'] = $json['speed']; - $json['mledps'] = $json['dps']; - } - - if ($fap = $this->getFeralAP()) - $json['feratkpwr'] = $fap; - } - - if ($this->curTpl['armorDamageModifier'] > 0) - $json['armor'] -= $this->curTpl['armorDamageModifier']; - - if ($this->curTpl['class'] == ITEM_CLASS_ARMOR || $this->curTpl['class'] == ITEM_CLASS_WEAPON) - $json['gearscore'] = Util::getEquipmentScore($json['level'], $this->getField('quality'), $json['slot'], $json['nsockets']); - else if ($this->curTpl['class'] == ITEM_CLASS_GEM) - $json['gearscore'] = Util::getGemScore($json['level'], $this->getField('quality'), $this->getField('requiredSkill') == 755, $this->id); - - // clear zero-values afterwards - foreach ($json as $k => $v) - if (!$v && !in_array($k, ['classs', 'subclass', 'quality', 'side', 'gearscore'])) - unset($json[$k]); - - Util::checkNumeric($json); - - $this->json[$json['id']] = $json; - } - - public function addRewardsToJScript(&$ref) { } -} - - -class ItemListFilter extends Filter -{ - private $ubFilter = []; // usable-by - limit weapon/armor selection per CharClass - itemClass => available itemsubclasses - private $extCostQuery = 'SELECT item FROM npc_vendor WHERE extendedCost IN (?a) UNION - SELECT item FROM game_event_npc_vendor WHERE extendedCost IN (?a)'; - private $otFields = [18 => 4, 68 => 15, 69 => 16, 70 => 17, 72 => 2, 73 => 19, 75 => 21, 76 => 23, 88 => 20, 92 => 5, 93 => 3, 143 => 18, 171 => 8, 172 => 12]; - - public $extraOpts = []; // score for statWeights - public $wtCnd = []; - protected $enums = array( - 99 => array( // profession | recycled for 86, 87 - null, 171, 164, 185, 333, 202, 129, 755, 165, 186, 197, true, false, 356, 182, 773 - ), - 66 => array( // profession specialization - 1 => -1, - 2 => [ 9788, 9787, 17041, 17040, 17039 ], - 3 => -1, - 4 => -1, - 5 => [20219, 20222 ], - 6 => -1, - 7 => -1, - 8 => [10656, 10658, 10660 ], - 9 => -1, - 10 => [26798, 26801, 26797 ], - 11 => [ 9788, 9787, 17041, 17040, 17039, 20219, 20222, 10656, 10658, 10660, 26798, 26801, 26797], // i know, i know .. lazy as fuck - 12 => false, - 13 => -1, - 14 => -1, - 15 => -1 - ), - 152 => array( // class-specific - null, 1, 2, 3, 4, 5, 6, 7, 8, 9, null, 11, true, false - ), - 153 => array( // race-specific - null, 1, 2, 3, 4, 5, 6, 7, 8, null, 10, 11, true, false - ), - 158 => array( // currency - 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 - ), - 118 => array( // tokens - 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 - ), - 128 => array( // source - 1 => true, // Any - 2 => false, // None - 3 => 1, // Crafted - 4 => 2, // Drop - 5 => 3, // PvP - 6 => 4, // Quest - 7 => 5, // Vendor - 9 => 10, // Starter - 10 => 11, // Event - 11 => 12 // Achievement - ), - 126 => array( // Zones - 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 - ), - 163 => array( // enchantment mats - 34057, 22445, 11176, 34052, 11082, 34055, 16203, 10939, 11135, 11175, 22446, 16204, 34054, 14344, 11084, 11139, 22449, 11178, - 10998, 34056, 16202, 10938, 11134, 11174, 22447, 20725, 14343, 34053, 10978, 11138, 22448, 11177, 11083, 10940, 11137, 22450 - ) - ); - - // cr => [type, field, misc, extraCol] - protected $genericFilter = array( // misc (bool): _NUMERIC => useFloat; _STRING => localized; _FLAG => match Value; _BOOLEAN => stringSet - 2 => [FILTER_CR_CALLBACK, 'cbFieldHasVal', 'bonding', 1 ], // bindonpickup [yn] - 3 => [FILTER_CR_CALLBACK, 'cbFieldHasVal', 'bonding', 2 ], // bindonequip [yn] - 4 => [FILTER_CR_CALLBACK, 'cbFieldHasVal', 'bonding', 3 ], // bindonuse [yn] - 5 => [FILTER_CR_CALLBACK, 'cbFieldHasVal', 'bonding', [4, 5] ], // questitem [yn] - 6 => [FILTER_CR_CALLBACK, 'cbQuestRelation', null, null ], // startsquest [side] - 7 => [FILTER_CR_BOOLEAN, 'description_loc0', true ], // hasflavortext - 8 => [FILTER_CR_BOOLEAN, 'requiredDisenchantSkill' ], // disenchantable - 9 => [FILTER_CR_FLAG, 'flags', ITEM_FLAG_CONJURED ], // conjureditem - 10 => [FILTER_CR_BOOLEAN, 'lockId' ], // locked - 11 => [FILTER_CR_FLAG, 'flags', ITEM_FLAG_OPENABLE ], // openable - 12 => [FILTER_CR_BOOLEAN, 'itemset' ], // partofset - 13 => [FILTER_CR_BOOLEAN, 'randomEnchant' ], // randomlyenchanted - 14 => [FILTER_CR_BOOLEAN, 'pageTextId' ], // readable - 15 => [FILTER_CR_CALLBACK, 'cbFieldHasVal', 'maxCount', 1 ], // unique [yn] - 16 => [FILTER_CR_NYI_PH, null, 1, ], // dropsin [zone] - 17 => [FILTER_CR_ENUM, 'requiredFaction' ], // requiresrepwith - 18 => [FILTER_CR_CALLBACK, 'cbFactionQuestReward', null, null ], // rewardedbyfactionquest [side] - 20 => [FILTER_CR_NUMERIC, 'is.str', NUM_CAST_INT, true ], // str - 21 => [FILTER_CR_NUMERIC, 'is.agi', NUM_CAST_INT, true ], // agi - 22 => [FILTER_CR_NUMERIC, 'is.sta', NUM_CAST_INT, true ], // sta - 23 => [FILTER_CR_NUMERIC, 'is.int', NUM_CAST_INT, true ], // int - 24 => [FILTER_CR_NUMERIC, 'is.spi', NUM_CAST_INT, true ], // spi - 25 => [FILTER_CR_NUMERIC, 'is.arcres', NUM_CAST_INT, true ], // arcres - 26 => [FILTER_CR_NUMERIC, 'is.firres', NUM_CAST_INT, true ], // firres - 27 => [FILTER_CR_NUMERIC, 'is.natres', NUM_CAST_INT, true ], // natres - 28 => [FILTER_CR_NUMERIC, 'is.frores', NUM_CAST_INT, true ], // frores - 29 => [FILTER_CR_NUMERIC, 'is.shares', NUM_CAST_INT, true ], // shares - 30 => [FILTER_CR_NUMERIC, 'is.holres', NUM_CAST_INT, true ], // holres - 32 => [FILTER_CR_NUMERIC, 'is.dps', NUM_CAST_FLOAT, true ], // dps - 33 => [FILTER_CR_NUMERIC, 'is.dmgmin1', NUM_CAST_INT, true ], // dmgmin1 - 34 => [FILTER_CR_NUMERIC, 'is.dmgmax1', NUM_CAST_INT, true ], // dmgmax1 - 35 => [FILTER_CR_CALLBACK, 'cbDamageType', null, null ], // damagetype [enum] - 36 => [FILTER_CR_NUMERIC, 'is.speed', NUM_CAST_FLOAT, true ], // speed - 37 => [FILTER_CR_NUMERIC, 'is.mleatkpwr', NUM_CAST_INT, true ], // mleatkpwr - 38 => [FILTER_CR_NUMERIC, 'is.rgdatkpwr', NUM_CAST_INT, true ], // rgdatkpwr - 39 => [FILTER_CR_NUMERIC, 'is.rgdhitrtng', NUM_CAST_INT, true ], // rgdhitrtng - 40 => [FILTER_CR_NUMERIC, 'is.rgdcritstrkrtng', NUM_CAST_INT, true ], // rgdcritstrkrtng - 41 => [FILTER_CR_NUMERIC, 'is.armor' , NUM_CAST_INT, true ], // armor - 42 => [FILTER_CR_NUMERIC, 'is.defrtng', NUM_CAST_INT, true ], // defrtng - 43 => [FILTER_CR_NUMERIC, 'is.block', NUM_CAST_INT, true ], // block - 44 => [FILTER_CR_NUMERIC, 'is.blockrtng', NUM_CAST_INT, true ], // blockrtng - 45 => [FILTER_CR_NUMERIC, 'is.dodgertng', NUM_CAST_INT, true ], // dodgertng - 46 => [FILTER_CR_NUMERIC, 'is.parryrtng', NUM_CAST_INT, true ], // parryrtng - 48 => [FILTER_CR_NUMERIC, 'is.splhitrtng', NUM_CAST_INT, true ], // splhitrtng - 49 => [FILTER_CR_NUMERIC, 'is.splcritstrkrtng', NUM_CAST_INT, true ], // splcritstrkrtng - 50 => [FILTER_CR_NUMERIC, 'is.splheal', NUM_CAST_INT, true ], // splheal - 51 => [FILTER_CR_NUMERIC, 'is.spldmg', NUM_CAST_INT, true ], // spldmg - 52 => [FILTER_CR_NUMERIC, 'is.arcsplpwr', NUM_CAST_INT, true ], // arcsplpwr - 53 => [FILTER_CR_NUMERIC, 'is.firsplpwr', NUM_CAST_INT, true ], // firsplpwr - 54 => [FILTER_CR_NUMERIC, 'is.frosplpwr', NUM_CAST_INT, true ], // frosplpwr - 55 => [FILTER_CR_NUMERIC, 'is.holsplpwr', NUM_CAST_INT, true ], // holsplpwr - 56 => [FILTER_CR_NUMERIC, 'is.natsplpwr', NUM_CAST_INT, true ], // natsplpwr - 57 => [FILTER_CR_NUMERIC, 'is.shasplpwr', NUM_CAST_INT, true ], // shasplpwr - 59 => [FILTER_CR_NUMERIC, 'durability', NUM_CAST_INT, true ], // dura - 60 => [FILTER_CR_NUMERIC, 'is.healthrgn', NUM_CAST_INT, true ], // healthrgn - 61 => [FILTER_CR_NUMERIC, 'is.manargn', NUM_CAST_INT, true ], // manargn - 62 => [FILTER_CR_CALLBACK, 'cbCooldown', null, null ], // cooldown [op] [int] - 63 => [FILTER_CR_NUMERIC, 'buyPrice', NUM_CAST_INT, true ], // buyprice - 64 => [FILTER_CR_NUMERIC, 'sellPrice', NUM_CAST_INT, true ], // sellprice - 65 => [FILTER_CR_CALLBACK, 'cbAvgMoneyContent', null, null ], // avgmoney [op] [int] - 66 => [FILTER_CR_ENUM, 'requiredSpell' ], // requiresprofspec - 68 => [FILTER_CR_CALLBACK, 'cbObtainedBy', null, null ], // otdisenchanting [yn] - 69 => [FILTER_CR_CALLBACK, 'cbObtainedBy', null, null ], // otfishing [yn] - 70 => [FILTER_CR_CALLBACK, 'cbObtainedBy', null, null ], // otherbgathering [yn] - 71 => [FILTER_CR_FLAG, 'cuFlags', ITEM_CU_OT_ITEMLOOT ], // otitemopening [yn] - 72 => [FILTER_CR_CALLBACK, 'cbObtainedBy', null, null ], // otlooting [yn] - 73 => [FILTER_CR_CALLBACK, 'cbObtainedBy', null, null ], // otmining [yn] - 74 => [FILTER_CR_FLAG, 'cuFlags', ITEM_CU_OT_OBJECTLOOT ], // otobjectopening [yn] - 75 => [FILTER_CR_CALLBACK, 'cbObtainedBy', null, null ], // otpickpocketing [yn] - 76 => [FILTER_CR_CALLBACK, 'cbObtainedBy', null, null ], // otskinning [yn] - 77 => [FILTER_CR_NUMERIC, 'is.atkpwr', NUM_CAST_INT, true ], // atkpwr - 78 => [FILTER_CR_NUMERIC, 'is.mlehastertng', NUM_CAST_INT, true ], // mlehastertng - 79 => [FILTER_CR_NUMERIC, 'is.resirtng', NUM_CAST_INT, true ], // resirtng - 80 => [FILTER_CR_CALLBACK, 'cbHasSockets', null, null ], // has sockets [enum] - 81 => [FILTER_CR_CALLBACK, 'cbFitsGemSlot', null, null ], // fits gem slot [enum] - 83 => [FILTER_CR_FLAG, 'flags', ITEM_FLAG_UNIQUEEQUIPPED ], // uniqueequipped - 84 => [FILTER_CR_NUMERIC, 'is.mlecritstrkrtng', NUM_CAST_INT, true ], // mlecritstrkrtng - 85 => [FILTER_CR_CALLBACK, 'cbObjectiveOfQuest', null, null ], // objectivequest [side] - 86 => [FILTER_CR_CALLBACK, 'cbCraftedByProf', null, null ], // craftedprof [enum] - 87 => [FILTER_CR_CALLBACK, 'cbReagentForAbility', null, null ], // reagentforability [enum] - 88 => [FILTER_CR_CALLBACK, 'cbObtainedBy', null, null ], // otprospecting [yn] - 89 => [FILTER_CR_FLAG, 'flags', ITEM_FLAG_PROSPECTABLE ], // prospectable - 90 => [FILTER_CR_CALLBACK, 'cbAvgBuyout', null, null ], // avgbuyout [op] [int] - 91 => [FILTER_CR_ENUM, 'totemCategory' ], // tool - 92 => [FILTER_CR_CALLBACK, 'cbObtainedBy', null, null ], // soldbyvendor [yn] - 93 => [FILTER_CR_CALLBACK, 'cbObtainedBy', null, null ], // otpvp [pvp] - 94 => [FILTER_CR_NUMERIC, 'is.splpen', NUM_CAST_INT, true ], // splpen - 95 => [FILTER_CR_NUMERIC, 'is.mlehitrtng', NUM_CAST_INT, true ], // mlehitrtng - 96 => [FILTER_CR_NUMERIC, 'is.critstrkrtng', NUM_CAST_INT, true ], // critstrkrtng - 97 => [FILTER_CR_NUMERIC, 'is.feratkpwr', NUM_CAST_INT, true ], // feratkpwr - 98 => [FILTER_CR_FLAG, 'flags', ITEM_FLAG_PARTYLOOT ], // partyloot - 99 => [FILTER_CR_ENUM, 'requiredSkill' ], // requiresprof - 100 => [FILTER_CR_NUMERIC, 'is.nsockets', NUM_CAST_INT ], // nsockets - 101 => [FILTER_CR_NUMERIC, 'is.rgdhastertng', NUM_CAST_INT, true ], // rgdhastertng - 102 => [FILTER_CR_NUMERIC, 'is.splhastertng', NUM_CAST_INT, true ], // splhastertng - 103 => [FILTER_CR_NUMERIC, 'is.hastertng', NUM_CAST_INT, true ], // hastertng - 104 => [FILTER_CR_STRING, 'description', STR_LOCALIZED ], // flavortext - 105 => [FILTER_CR_NYI_PH, null, 1, ], // dropsinnormal [heroicdungeon-any] - 106 => [FILTER_CR_NYI_PH, null, 1, ], // dropsinheroic [heroicdungeon-any] - 107 => [FILTER_CR_NYI_PH, null, 1, ], // effecttext [str] not yet parsed ['effectsParsed_loc'.User::$localeId, $cr[2]] - 109 => [FILTER_CR_CALLBACK, 'cbArmorBonus', null, null ], // armorbonus [op] [int] - 111 => [FILTER_CR_NUMERIC, 'requiredSkillRank', NUM_CAST_INT, true ], // reqskillrank - 113 => [FILTER_CR_FLAG, 'cuFlags', CUSTOM_HAS_SCREENSHOT ], // hasscreenshots - 114 => [FILTER_CR_NUMERIC, 'is.armorpenrtng', NUM_CAST_INT, true ], // armorpenrtng - 115 => [FILTER_CR_NUMERIC, 'is.health', NUM_CAST_INT, true ], // health - 116 => [FILTER_CR_NUMERIC, 'is.mana', NUM_CAST_INT, true ], // mana - 117 => [FILTER_CR_NUMERIC, 'is.exprtng', NUM_CAST_INT, true ], // exprtng - 118 => [FILTER_CR_CALLBACK, 'cbPurchasableWith', null, null ], // purchasablewithitem [enum] - 119 => [FILTER_CR_NUMERIC, 'is.hitrtng', NUM_CAST_INT, true ], // hitrtng - 123 => [FILTER_CR_NUMERIC, 'is.splpwr', NUM_CAST_INT, true ], // splpwr - 124 => [FILTER_CR_CALLBACK, 'cbHasRandEnchant', null, null ], // randomenchants [str] - 125 => [FILTER_CR_CALLBACK, 'cbReqArenaRating', null, null ], // reqarenartng [op] [int] todo (low): 'find out, why "IN (W, X, Y) AND IN (X, Y, Z)" doesn't result in "(X, Y)" - 126 => [FILTER_CR_CALLBACK, 'cbQuestRewardIn', null, null ], // rewardedbyquestin [zone-any] - 128 => [FILTER_CR_CALLBACK, 'cbSource', null, null ], // source [enum] - 129 => [FILTER_CR_CALLBACK, 'cbSoldByNPC', null, null ], // soldbynpc [str-small] - 130 => [FILTER_CR_FLAG, 'cuFlags', CUSTOM_HAS_COMMENT ], // hascomments - 132 => [FILTER_CR_CALLBACK, 'cbGlyphType', null, null ], // glyphtype [enum] - 133 => [FILTER_CR_FLAG, 'flags', ITEM_FLAG_ACCOUNTBOUND ], // accountbound - 134 => [FILTER_CR_NUMERIC, 'is.mledps', NUM_CAST_FLOAT, true ], // mledps - 135 => [FILTER_CR_NUMERIC, 'is.mledmgmin', NUM_CAST_INT, true ], // mledmgmin - 136 => [FILTER_CR_NUMERIC, 'is.mledmgmax', NUM_CAST_INT, true ], // mledmgmax - 137 => [FILTER_CR_NUMERIC, 'is.mlespeed', NUM_CAST_FLOAT, true ], // mlespeed - 138 => [FILTER_CR_NUMERIC, 'is.rgddps', NUM_CAST_FLOAT, true ], // rgddps - 139 => [FILTER_CR_NUMERIC, 'is.rgddmgmin', NUM_CAST_INT, true ], // rgddmgmin - 140 => [FILTER_CR_NUMERIC, 'is.rgddmgmax', NUM_CAST_INT, true ], // rgddmgmax - 141 => [FILTER_CR_NUMERIC, 'is.rgdspeed', NUM_CAST_FLOAT, true ], // rgdspeed - 142 => [FILTER_CR_STRING, 'ic.name' ], // icon - 143 => [FILTER_CR_CALLBACK, 'cbObtainedBy', null, null ], // otmilling [yn] - 144 => [FILTER_CR_CALLBACK, 'cbPvpPurchasable', 'reqHonorPoints', null ], // purchasablewithhonor [yn] - 145 => [FILTER_CR_CALLBACK, 'cbPvpPurchasable', 'reqHonorPoints', null ], // purchasablewitharena [yn] - 146 => [FILTER_CR_FLAG, 'flags', ITEM_FLAG_HEROIC ], // heroic - 147 => [FILTER_CR_NYI_PH, null, 1, ], // dropsinnormal10 [multimoderaid-any] - 148 => [FILTER_CR_NYI_PH, null, 1, ], // dropsinnormal25 [multimoderaid-any] - 149 => [FILTER_CR_NYI_PH, null, 1, ], // dropsinheroic10 [heroicraid-any] - 150 => [FILTER_CR_NYI_PH, null, 1, ], // dropsinheroic25 [heroicraid-any] - 151 => [FILTER_CR_NUMERIC, 'id', NUM_CAST_INT, true ], // id - 152 => [FILTER_CR_CALLBACK, 'cbClassRaceSpec', 'requiredClass', CLASS_MASK_ALL], // classspecific [enum] - 153 => [FILTER_CR_CALLBACK, 'cbClassRaceSpec', 'requiredRace', RACE_MASK_ALL ], // racespecific [enum] - 154 => [FILTER_CR_FLAG, 'flags', ITEM_FLAG_REFUNDABLE ], // refundable - 155 => [FILTER_CR_FLAG, 'flags', ITEM_FLAG_USABLE_ARENA ], // usableinarenas - 156 => [FILTER_CR_FLAG, 'flags', ITEM_FLAG_USABLE_SHAPED ], // usablewhenshapeshifted - 157 => [FILTER_CR_FLAG, 'flags', ITEM_FLAG_SMARTLOOT ], // smartloot - 158 => [FILTER_CR_CALLBACK, 'cbPurchasableWith', null, null ], // purchasablewithcurrency [enum] - 159 => [FILTER_CR_FLAG, 'flags', ITEM_FLAG_MILLABLE ], // millable - 160 => [FILTER_CR_NYI_PH, null, 1, ], // relatedevent [enum] like 169 .. crawl though npc_vendor and loot_templates of event-related spawns - 161 => [FILTER_CR_CALLBACK, 'cbAvailable', null, null ], // availabletoplayers [yn] - 162 => [FILTER_CR_FLAG, 'flags', ITEM_FLAG_DEPRECATED ], // deprecated - 163 => [FILTER_CR_CALLBACK, 'cbDisenchantsInto', null, null ], // disenchantsinto [disenchanting] - 165 => [FILTER_CR_NUMERIC, 'repairPrice', NUM_CAST_INT, true ], // repaircost - 167 => [FILTER_CR_FLAG, 'cuFlags', CUSTOM_HAS_VIDEO ], // hasvideos - 168 => [FILTER_CR_CALLBACK, 'cbFieldHasVal', 'spellId1', [483, 55884] ], // teachesspell [yn] - 483: learn recipe; 55884: learn mount/pet - 169 => [FILTER_CR_ENUM, 'e.holidayId' ], // requiresevent - 171 => [FILTER_CR_CALLBACK, 'cbObtainedBy', null, null ], // otredemption [yn] - 172 => [FILTER_CR_CALLBACK, 'cbObtainedBy', null, null ], // rewardedbyachievement [yn] - 176 => [FILTER_CR_STAFFFLAG, 'flags' ], // flags - 177 => [FILTER_CR_STAFFFLAG, 'flagsExtra' ], // flags2 - ); - - // fieldId => [checkType, checkValue[, fieldIsArray]] - protected $inputFields = array( - 'wt' => [FILTER_V_CALLBACK, 'cbWeightKeyCheck', true ], // weight keys - 'wtv' => [FILTER_V_RANGE, [1, 999], true ], // weight values - 'jc' => [FILTER_V_LIST, [1], false], // use jewelcrafter gems for weight calculation - 'gm' => [FILTER_V_LIST, [2, 3, 4], false], // gem rarity for weight calculation - 'cr' => [FILTER_V_RANGE, [1, 177], true ], // criteria ids - 'crs' => [FILTER_V_LIST, [FILTER_ENUM_NONE, FILTER_ENUM_ANY, [0, 99999]], true ], // criteria operators - 'crv' => [FILTER_V_REGEX, '/[\p{C};:]/ui', true ], // criteria values - only printable chars, no delimiters - 'upg' => [FILTER_V_RANGE, [1, 999999], true ], // upgrade item ids - 'gb' => [FILTER_V_LIST, [0, 1, 2, 3], false], // search result grouping - 'na' => [FILTER_V_REGEX, '/[\p{C};]/ui', false], // name - only printable chars, no delimiter - 'ma' => [FILTER_V_EQUAL, 1, false], // match any / all filter - 'ub' => [FILTER_V_LIST, [[1, 9], 11], false], // usable by classId - 'qu' => [FILTER_V_RANGE, [0, 7], true ], // quality ids - 'ty' => [FILTER_V_CALLBACK, 'cbTypeCheck', true ], // item type - dynamic by current group - 'sl' => [FILTER_V_CALLBACK, 'cbSlotCheck', true ], // item slot - dynamic by current group - 'si' => [FILTER_V_LIST, [1, 2, 3, -1, -2], false], // side - 'minle' => [FILTER_V_RANGE, [1, 999], false], // item level min - 'maxle' => [FILTER_V_RANGE, [1, 999], false], // item level max - 'minrl' => [FILTER_V_RANGE, [1, MAX_LEVEL], false], // required level min - 'maxrl' => [FILTER_V_RANGE, [1, MAX_LEVEL], false] // required level max - ); - - public function __construct($fromPOST = false, $opts = []) - { - $classes = new CharClassList(); - foreach ($classes->iterate() as $cId => $_tpl) - { - // preselect misc subclasses - $this->ubFilter[$cId] = [ITEM_CLASS_WEAPON => [14], ITEM_CLASS_ARMOR => [0]]; - - for ($i = 0; $i < 21; $i++) - if ($_tpl['weaponTypeMask'] & (1 << $i)) - $this->ubFilter[$cId][ITEM_CLASS_WEAPON][] = $i; - - for ($i = 0; $i < 11; $i++) - if ($_tpl['armorTypeMask'] & (1 << $i)) - $this->ubFilter[$cId][ITEM_CLASS_ARMOR][] = $i; - } - - parent::__construct($fromPOST, $opts); - } - - public function createConditionsForWeights() - { - if (empty($this->fiData['v']['wt'])) - return null; - - $this->wtCnd = []; - $select = []; - $wtSum = 0; - - foreach ($this->fiData['v']['wt'] as $k => $v) - { - $str = Util::$itemFilter[$v]; - $qty = intVal($this->fiData['v']['wtv'][$k]); - - if ($str == 'rgdspeed') // dont need no duplicate column - $str = 'speed'; - - if ($str == 'mledps') // todo (med): unify rngdps and mledps to dps - $str = 'dps'; - - $select[] = '(`is`.`'.$str.'` * '.$qty.')'; - $this->wtCnd[] = ['is.'.$str, 0, '>']; - $wtSum += $qty; - } - - if (count($this->wtCnd) > 1) - array_unshift($this->wtCnd, 'OR'); - else if (count($this->wtCnd) == 1) - $this->wtCnd = $this->wtCnd[0]; - - if ($select) - { - $this->extraOpts['is']['s'][] = ', IF(is.typeId IS NULL, 0, ('.implode(' + ', $select).') / '.$wtSum.') AS score'; - $this->extraOpts['is']['o'][] = 'score DESC'; - $this->extraOpts['i']['o'][] = null; // remove default ordering - } - else - $this->extraOpts['is']['s'][] = ', 0 AS score'; // prevent errors - - return $this->wtCnd; - } - - protected function createSQLForCriterium(&$cr) - { - if (in_array($cr[0], array_keys($this->genericFilter))) - if ($genCr = $this->genericCriterion($cr)) - return $genCr; - - unset($cr); - $this->error = true; - return [1]; - } - - protected function createSQLForValues() - { - $parts = []; - $_v = $this->fiData['v']; - - // weights - if (!empty($_v['wt']) && !empty($_v['wtv'])) - { - // gm - gem quality (qualityId) - // jc - jc-gems included (bool) - - $parts[] = $this->createConditionsForWeights(); - - foreach ($_v['wt'] as $_) - $this->formData['extraCols'][] = $_; - } - - // upgrade for [form only] - if (isset($_v['upg'])) - { - $_ = DB::Aowow()->selectCol('SELECT id as ARRAY_KEY, slot FROM ?_items WHERE class IN (2, 3, 4) AND id IN (?a)', (array)$_v['upg']); - if ($_ === null) - { - unset($_v['upg']); - unset($this->formData['form']['upg']); - } - else - { - $this->formData['form']['upg'] = $_; - if ($_) - $parts[] = ['slot', $_]; - } - } - - // group by [form only] - if (isset($_v['gb'])) - $this->formData['form']['gb'] = $_v['gb']; - - // name - if (isset($_v['na'])) - if ($_ = $this->modularizeString(['name_loc'.User::$localeId])) - $parts[] = $_; - - // usable-by (not excluded by requiredClass && armor or weapons match mask from ?_classes) - if (isset($_v['ub'])) - { - $parts[] = array( - 'AND', - ['OR', ['requiredClass', 0], ['requiredClass', $this->list2Mask((array)$_v['ub']), '&']], - [ - 'OR', - ['class', [2, 4], '!'], - ['AND', ['class', 2], ['subclassbak', $this->ubFilter[$_v['ub']][ITEM_CLASS_WEAPON]]], - ['AND', ['class', 4], ['subclassbak', $this->ubFilter[$_v['ub']][ITEM_CLASS_ARMOR]]] - ] - ); - } - - // quality [list] - if (isset($_v['qu'])) - $parts[] = ['quality', $_v['qu']]; - - // type - if (isset($_v['ty'])) - $parts[] = ['subclass', $_v['ty']]; - - // slot - if (isset($_v['sl'])) - $parts[] = ['slot', $_v['sl']]; - - // side - if (isset($_v['si'])) - { - $ex = [['requiredRace', RACE_MASK_ALL, '&'], RACE_MASK_ALL, '!']; - $notEx = ['OR', ['requiredRace', 0], [['requiredRace', RACE_MASK_ALL, '&'], RACE_MASK_ALL]]; - - switch ($_v['si']) - { - case 3: - $parts[] = $notEx; - break; - case 2: - $parts[] = ['AND', [['flagsExtra', 0x3, '&'], [0, 1]], ['OR', $notEx, ['requiredRace', RACE_MASK_HORDE, '&']]]; - break; - case -2: - $parts[] = ['OR', [['flagsExtra', 0x3, '&'], 1], ['AND', $ex, ['requiredRace', RACE_MASK_HORDE, '&']]]; - break; - case 1: - $parts[] = ['AND', [['flagsExtra', 0x3, '&'], [0, 2]], ['OR', $notEx, ['requiredRace', RACE_MASK_ALLIANCE, '&']]]; - break; - case -1: - $parts[] = ['OR', [['flagsExtra', 0x3, '&'], 2], ['AND', $ex, ['requiredRace', RACE_MASK_ALLIANCE, '&']]]; - break; - } - } - - // itemLevel min - if (isset($_v['minle'])) - $parts[] = ['itemLevel', $_v['minle'], '>=']; - - // itemLevel max - if (isset($_v['maxle'])) - $parts[] = ['itemLevel', $_v['maxle'], '<=']; - - // reqLevel min - if (isset($_v['minrl'])) - $parts[] = ['requiredLevel', $_v['minrl'], '>=']; - - // reqLevel max - if (isset($_v['maxrl'])) - $parts[] = ['requiredLevel', $_v['maxrl'], '<=']; - - return $parts; - } - - protected function cbFactionQuestReward($cr) - { - if (!isset($this->otFields[$cr[0]])) - return false; - - $field = 'src.src'.$this->otFields[$cr[0]]; - switch ($cr[1]) - { - case 1: // Yes - return [$field, null, '!']; - case 2: // Alliance - return [$field, 1]; - case 3: // Horde - return [$field, 2]; - case 4: // Both - return [$field, 3]; - case 5: // No - return [$field, null]; - } - - return false; - } - - protected function cbAvailable($cr) - { - if ($this->int2Bool($cr[1])) - return [['cuFlags', CUSTOM_UNAVAILABLE, '&'], 0, $cr[1] ? null : '!']; - - return false; - } - - protected function cbHasSockets($cr) - { - switch ($cr[1]) - { - case 5: // Yes - return ['is.nsockets', 0, '!']; - case 6: // No - return ['is.nsockets', 0]; - case 1: // Meta - case 2: // Red - case 3: // Yellow - case 4: // Blue - $mask = 1 << ($cr[1] - 1); - return ['OR', ['socketColor1', $mask], ['socketColor2', $mask], ['socketColor3', $mask]]; - } - - return false; - } - - protected function cbFitsGemSlot($cr) - { - switch ($cr[1]) - { - case 5: // Yes - return ['gemEnchantmentId', 0, '!']; - case 6: // No - return ['gemEnchantmentId', 0]; - case 1: // Meta - case 2: // Red - case 3: // Yellow - case 4: // Blue - $mask = 1 << ($cr[1] - 1); - return ['AND', ['gemEnchantmentId', 0, '!'], ['gemColorMask', $mask, '&']]; - } - } - - protected function cbGlyphType($cr) - { - switch ($cr[1]) - { - case 1: // Major - case 2: // Minor - return ['AND', ['class', 16], ['subSubClass', $cr[1]]]; - } - - return false; - } - - protected function cbHasRandEnchant($cr) - { - $randIds = DB::Aowow()->selectCol('SELECT id AS ARRAY_KEY, ABS(id) FROM ?_itemrandomenchant WHERE name_loc?d LIKE ?', User::$localeId, '%'.$cr[2].'%'); - $tplIds = $randIds ? DB::World()->select('SELECT entry, ench FROM item_enchantment_template WHERE ench IN (?a)', $randIds) : []; - foreach ($tplIds as $k => &$set) - if (array_search($set['ench'], $randIds) < 0) - $set['entry'] *= -1; - - if ($tplIds) - return ['randomEnchant', array_column($tplIds, 'entry')]; - else - return [0]; // no results aren't really input errors - } - - protected function cbReqArenaRating($cr) - { - if (!Util::checkNumeric($cr[2], NUM_CAST_INT) || !$this->int2Op($cr[1])) - return false; - - $this->formData['extraCols'][] = $cr[0]; - - $costs = DB::Aowow()->selectCol('SELECT id FROM ?_itemextendedcost WHERE reqPersonalrating '.$cr[1].' '.$cr[2]); - $items = DB::World()->selectCol($this->extCostQuery, $costs, $costs); - return ['id', $items]; - } - - protected function cbClassRaceSpec($cr, $field, $mask) - { - if (!isset($this->enums[$cr[0]][$cr[1]])) - return false; - - $_ = $this->enums[$cr[0]][$cr[1]]; - if (is_bool($_)) - return $_ ? ['AND', [[$field, $mask, '&'], $mask, '!'], [$field, 0, '>']] : ['OR', [[$field, $mask, '&'], $mask], [$field, 0]]; - else if (is_int($_)) - return ['AND', [[$field, $mask, '&'], $mask, '!'], [$field, 1 << ($_ - 1), '&']]; - - return false; - } - - protected function cbDamageType($cr) - { - if (!$this->checkInput(FILTER_V_RANGE, [0, 6], $cr[1])) - return false; - - return ['OR', ['dmgType1', $cr[1]], ['dmgType2', $cr[1]]]; - } - - protected function cbArmorBonus($cr) - { - if (!Util::checkNumeric($cr[2], NUM_CAST_FLOAT) || !$this->int2Op($cr[1])) - return false; - - $this->formData['extraCols'][] = $cr[0]; - return ['AND', ['armordamagemodifier', $cr[2], $cr[1]], ['class', ITEM_CLASS_ARMOR]]; - } - - protected function cbCraftedByProf($cr) - { - if (!isset($this->enums[99][$cr[1]])) - return false; - - $_ = $this->enums[99][$cr[1]]; - if (is_bool($_)) - return ['src.src1', null, $_ ? '!' : null]; - else if (is_int($_)) - return ['s.skillLine1', $_]; - - return false; - } - - protected function cbQuestRewardIn($cr) - { - if (in_array($cr[1], $this->enums[$cr[0]])) - return ['AND', ['src.src4', null, '!'], ['src.moreZoneId', $cr[1]]]; - else if ($cr[1] == FILTER_ENUM_ANY) - return ['src.src4', null, '!']; // well, this seems a bit redundant.. - - return false; - } - - protected function cbPurchasableWith($cr) - { - if (in_array($cr[1], $this->enums[$cr[0]])) - $_ = (array)$cr[1]; - else if ($cr[1] == FILTER_ENUM_ANY) - $_ = $this->enums[$cr[0]]; - else - return false; - - $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)', - $_, $_, $_, $_, $_ - ); - if ($items = DB::World()->selectCol($this->extCostQuery, $costs, $costs)) - return ['id', $items]; - } - - protected function cbSoldByNPC($cr) - { - if (!Util::checkNumeric($cr[2], NUM_CAST_INT)) - return false; - - 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', $cr[2], $cr[2])) - return ['i.id', $iIds]; - else - return [0]; - } - - protected function cbAvgBuyout($cr) - { - if (!Util::checkNumeric($cr[2], NUM_CAST_INT) || !$this->int2Op($cr[1])) - return false; - - foreach (Profiler::getRealms() as $rId => $__) - { - // 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 avgbuyout '.$cr[1].' ?f', $c[1])) - // return ['i.id', array_keys($_)]; - // else - // return [0]; - return [1]; - } - - return [0]; - } - - protected function cbAvgMoneyContent($cr) - { - if (!Util::checkNumeric($cr[2], NUM_CAST_INT) || !$this->int2Op($cr[1])) - return false; - - $this->formData['extraCols'][] = $cr[0]; - return ['AND', ['flags', ITEM_FLAG_OPENABLE, '&'], ['((minMoneyLoot + maxMoneyLoot) / 2)', $cr[2], $cr[1]]]; - } - - protected function cbCooldown($cr) - { - if (!Util::checkNumeric($cr[2], NUM_CAST_INT) || !$this->int2Op($cr[1])) - return false; - - $cr[2] *= 1000; // field supplied in milliseconds - - $this->formData['extraCols'][] = $cr[0]; - $this->extraOpts['is']['s'][] = ', IF(spellCooldown1 > 1, spellCooldown1, IF(spellCooldown2 > 1, spellCooldown2, IF(spellCooldown3 > 1, spellCooldown3, IF(spellCooldown4 > 1, spellCooldown4, IF(spellCooldown5 > 1, spellCooldown5,))))) AS cooldown'; - - return [ - 'OR', - ['AND', ['spellTrigger1', 0], ['spellId1', 0, '!'], ['spellCooldown1', 0, '>'], ['spellCooldown1', $cr[2], $cr[1]]], - ['AND', ['spellTrigger2', 0], ['spellId2', 0, '!'], ['spellCooldown2', 0, '>'], ['spellCooldown2', $cr[2], $cr[1]]], - ['AND', ['spellTrigger3', 0], ['spellId3', 0, '!'], ['spellCooldown3', 0, '>'], ['spellCooldown3', $cr[2], $cr[1]]], - ['AND', ['spellTrigger4', 0], ['spellId4', 0, '!'], ['spellCooldown4', 0, '>'], ['spellCooldown4', $cr[2], $cr[1]]], - ['AND', ['spellTrigger5', 0], ['spellId5', 0, '!'], ['spellCooldown5', 0, '>'], ['spellCooldown5', $cr[2], $cr[1]]], - ]; - } - - protected function cbQuestRelation($cr) - { - switch ($cr[1]) - { - case 1: // any - return ['startQuest', 0, '>']; - case 2: // exclude horde only - return ['AND', ['startQuest', 0, '>'], [['flagsExtra', 0x3, '&'], 2]]; - case 3: // exclude alliance only - return ['AND', ['startQuest', 0, '>'], [['flagsExtra', 0x3, '&'], 1]]; - case 4: // both - return ['AND', ['startQuest', 0, '>'], [['flagsExtra', 0x3, '&'], 0]]; - case 5: // none - return ['startQuest', 0]; - } - - return false; - } - - protected function cbFieldHasVal($cr, $field, $val) - { - if ($this->int2Bool($cr[1])) - return [$field, $val, $cr[1] ? null : '!']; - - return false; - } - - protected function cbObtainedBy($cr, $field) - { - if ($this->int2Bool($cr[1])) - return ['src.src'.$this->otFields[$cr[0]], null, $cr[1] ? '!' : null]; - - return false; - } - - protected function cbPvpPurchasable($cr, $field) - { - if (!$this->int2Bool($cr[1])) - return false; - - $costs = DB::Aowow()->selectCol('SELECT id FROM ?_itemextendedcost WHERE ?# > 0', $field); - if ($items = DB::World()->selectCol($this->extCostQuery, $costs, $costs)) - return ['id', $items, $cr[1] ? null : '!']; - - return false; - } - - protected function cbDisenchantsInto($cr) - { - if (!Util::checkNumeric($cr[1], NUM_REQ_INT)) - return false; - - if (!in_array($cr[1], $this->enums[$cr[0]])) - return false; - - $refResults = []; - $newRefs = DB::World()->selectCol('SELECT entry FROM ?# WHERE item = ?d AND reference = 0', LOOT_REFERENCE, $cr[1]); - while ($newRefs) - { - $refResults += $newRefs; - $newRefs = DB::World()->selectCol('SELECT entry FROM ?# WHERE reference IN (?a)', 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, $cr[1]); - - return $lootIds ? ['disenchantId', $lootIds] : [0]; - } - - protected function cbObjectiveOfQuest($cr) - { - $w = ''; - switch ($cr[1]) - { - case 1: // Yes - case 5: // No - $w = 1; - return; - case 2: // Alliance - $w = 'reqRaceMask & '.RACE_MASK_ALLIANCE.' AND (reqRaceMask & '.RACE_MASK_HORDE.') = 0'; - break; - case 3: // Horde - $w = 'reqRaceMask & '.RACE_MASK_HORDE.' AND (reqRaceMask & '.RACE_MASK_ALLIANCE.') = 0'; - break; - case 4: // Both - $w = '(reqRaceMask & '.RACE_MASK_ALLIANCE.' AND reqRaceMask & '.RACE_MASK_HORDE.') OR reqRaceMask = 0'; - break; - default: - return false; - } - - $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 ($itemIds) - return ['id', $itemIds, $cr[1] == 5 ? '!' : null]; - - return [0]; - } - - protected function cbReagentForAbility($cr) - { - if (!isset($this->enums[99][$cr[1]])) - return false; - - $_ = $this->enums[99][$cr[1]]; - if ($_ === null) - return false; - - $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 - 'SELECT reagent1, reagent2, reagent3, reagent4, reagent5, reagent6, reagent7, reagent8, - reagentCount1, reagentCount2, reagentCount3, reagentCount4, reagentCount5, reagentCount6, reagentCount7, reagentCount8 - FROM ?_spell - WHERE skillLine1 IN (?a)', - is_bool($_) ? array_filter($this->enums[99], "is_numeric") : $_ - ); - foreach ($spells as $spell) - for ($i = 1; $i < 9; $i++) - if ($spell['reagent'.$i] > 0 && $spell['reagentCount'.$i] > 0) - $ids[] = $spell['reagent'.$i]; - - if (empty($ids)) - return [0]; - else if ($_) - return ['id', $ids]; - else - return ['id', $ids, '!']; - } - - protected function cbSource($cr) - { - if (!isset($this->enums[$cr[0]][$cr[1]])) - return false; - - $_ = $this->enums[$cr[0]][$cr[1]]; - if (is_int($_)) // specific - return ['src.src'.$_, null, '!']; - else if ($_) // any - { - $foo = ['OR']; - foreach ($this->enums[$cr[0]] as $bar) - if (is_int($bar)) - $foo[] = ['src.src'.$bar, null, '!']; - - return $foo; - } - else // none - { - $foo = ['AND']; - foreach ($this->enums[$cr[0]] as $bar) - if (is_int($bar)) - $foo[] = ['src.src'.$bar, null]; - - return $foo; - } - } - - protected function cbTypeCheck(&$v) - { - if (!$this->parentCats) - return false; - - if (!Util::checkNumeric($v, NUM_REQ_INT)) - return false; - - $c = $this->parentCats; - - if (isset($c[2]) && is_array(Lang::item('cat', $c[0], 1, $c[1]))) - $catList = Lang::item('cat', $c[0], 1, $c[1], 1, $c[2]); - else if (isset($c[1]) && is_array(Lang::item('cat', $c[0]))) - $catList = Lang::item('cat', $c[0], 1, $c[1]); - else - $catList = Lang::item('cat', $c[0]); - - // consumables - always - if ($c[0] == 0) - return in_array($v, array_keys(Lang::item('cat', 0, 1))); - // weapons - only if parent - else if ($c[0] == 2 && !isset($c[1])) - return in_array($v, array_keys(Lang::spell('weaponSubClass'))); - // armor - only if parent - else if ($c[0] == 4 && !isset($c[1])) - return in_array($v, array_keys(Lang::item('cat', 4, 1))); - // uh ... other stuff... - else if (in_array($c[0], [1, 3, 7, 9, 15]) && !isset($c[1])) - return in_array($v, array_keys($catList[1])); - - return false; - } - - protected function cbSlotCheck(&$v) - { - if (!Util::checkNumeric($v, NUM_REQ_INT)) - return false; - - // todo (low): limit to concrete slots - $sl = array_keys(Lang::item('inventoryType')); - $c = $this->parentCats; - - // no selection - if (!isset($c[0])) - return in_array($v, $sl); - - // consumables - any; perm / temp item enhancements - else if ($c[0] == 0 && (!isset($c[1]) || in_array($c[1], [-3, 6]))) - return in_array($v, $sl); - - // weapons - always - else if ($c[0] == 2) - return in_array($v, $sl); - - // armor - any; any armor - else if ($c[0] == 4 && (!isset($c[1]) || in_array($c[1], [1, 2, 3, 4]))) - return in_array($v, $sl); - - return false; - } - - protected function cbWeightKeyCheck(&$v) - { - if (preg_match('/\W/i', $v)) - return false; - - return isset(Util::$itemFilter[$v]); - } -} - -?> diff --git a/includes/types/itemset.class.php b/includes/types/itemset.class.php deleted file mode 100644 index 87fd3acb..00000000 --- a/includes/types/itemset.class.php +++ /dev/null @@ -1,259 +0,0 @@ - ['o' => 'maxlevel DESC'], - 'e' => ['j' => ['?_events e ON e.id = `set`.eventId', true], 's' => ', e.holidayId'] - ); - - public function __construct($conditions = []) - { - parent::__construct($conditions); - - // post processing - foreach ($this->iterate() as &$_curTpl) - { - $_curTpl['classes'] = []; - $_curTpl['pieces'] = []; - for ($i = 1; $i < 12; $i++) - { - if ($_curTpl['classMask'] & (1 << ($i - 1))) - { - $this->classes[] = $i; - $_curTpl['classes'][] = $i; - } - } - - for ($i = 1; $i < 10; $i++) - { - if ($piece = $_curTpl['item'.$i]) - { - $_curTpl['pieces'][] = $piece; - $this->pieceToSet[$piece] = $this->id; - } - } - } - $this->classes = array_unique($this->classes); - } - - public function getListviewData() - { - $data = []; - - foreach ($this->iterate() as $__) - { - $data[$this->id] = array( - 'id' => $this->id, - 'idbak' => $this->curTpl['refSetId'], - 'name' => (7 - $this->curTpl['quality']).$this->getField('name', true), - 'minlevel' => $this->curTpl['minLevel'], - 'maxlevel' => $this->curTpl['maxLevel'], - 'note' => $this->curTpl['contentGroup'], - 'type' => $this->curTpl['type'], - 'reqclass' => $this->curTpl['classMask'], - 'classes' => $this->curTpl['classes'], - 'pieces' => $this->curTpl['pieces'], - 'heroic' => $this->curTpl['heroic'] - ); - } - - return $data; - } - - public function getJSGlobals($addMask = GLOBALINFO_ANY) - { - $data = []; - - if ($this->classes && ($addMask & GLOBALINFO_RELATED)) - $data[TYPE_CLASS] = array_combine($this->classes, $this->classes); - - if ($this->pieceToSet && ($addMask & GLOBALINFO_SELF)) - $data[TYPE_ITEM] = array_combine(array_keys($this->pieceToSet), array_keys($this->pieceToSet)); - - if ($addMask & GLOBALINFO_SELF) - foreach ($this->iterate() as $id => $__) - $data[TYPE_ITEMSET][$id] = ['name' => $this->getField('name', true)]; - - return $data; - } - - public function renderTooltip() - { - if (!$this->curTpl) - return array(); - - $x = '
'; - $x .= ''.Util::jsEscape($this->getField('name', true)).'
'; - - $nClasses = 0; - if ($_ = $this->getField('classMask')) - { - $cl = Lang::getClassString($_, $__, $nClasses); - $x .= Util::ucFirst($nClasses > 1 ? Lang::game('classes') : Lang::game('class')).Lang::main('colon').$cl.'
'; - } - - if ($_ = $this->getField('contentGroup')) - $x .= Util::jsEscape(Lang::itemset('notes', $_)).($this->getField('heroic') ? ' ('.Lang::item('heroic').')' : '').'
'; - - if (!$nClasses || !$this->getField('contentGroup')) - $x.= Lang::itemset('types', $this->getField('type')).'
'; - - if ($bonuses = $this->getBonuses()) - { - $x .= ''; - - foreach ($bonuses as $b) - $x .= '
'.$b['bonus'].' '.Lang::itemset('_pieces').Lang::main('colon').''.Util::jsEscape($b['desc']); - - $x .= '
'; - } - - $x .= '
'; - - return $x; - } - - public function getBonuses() - { - $spells = []; - for ($i = 1; $i < 9; $i++) - { - $spl = $this->getField('spell'.$i); - $qty = $this->getField('bonus'.$i); - - // cant use spell as index, would change order - if ($spl && $qty) - $spells[] = ['id' => $spl, 'bonus' => $qty]; - } - - // sort by required pieces ASC - usort($spells, function($a, $b) { - if ($a['bonus'] == $b['bonus']) - return 0; - - return ($a['bonus'] > $b['bonus']) ? 1 : -1; - }); - - $setSpells = new SpellList(array(['s.id', array_column($spells, 'id')])); - foreach ($setSpells->iterate() as $spellId => $__) - { - foreach ($spells as &$s) - { - if ($spellId != $s['id']) - continue; - - $s['desc'] = $setSpells->parseText('description', $this->getField('reqLevel') ?: MAX_LEVEL)[0]; - } - } - - return $spells; - } -} - - -// missing filter: "Available to Players" -class ItemsetListFilter extends Filter -{ - // cr => [type, field, misc, extraCol] - protected $genericFilter = array( // misc (bool): _NUMERIC => useFloat; _STRING => localized; _FLAG => match Value; _BOOLEAN => stringSet - 2 => [FILTER_CR_NUMERIC, 'id', NUM_CAST_INT, true], // id - 3 => [FILTER_CR_NUMERIC, 'npieces', NUM_CAST_INT ], // pieces - 4 => [FILTER_CR_STRING, 'bonusText', STR_LOCALIZED ], // bonustext - 5 => [FILTER_CR_BOOLEAN, 'heroic', ], // heroic - 6 => [FILTER_CR_ENUM, 'e.holidayId', ], // relatedevent - 8 => [FILTER_CR_FLAG, 'cuFlags', CUSTOM_HAS_COMMENT ], // hascomments - 9 => [FILTER_CR_FLAG, 'cuFlags', CUSTOM_HAS_SCREENSHOT ], // hasscreenshots - 10 => [FILTER_CR_FLAG, 'cuFlags', CUSTOM_HAS_VIDEO ], // hasvideos - 12 => [FILTER_CR_NYI_PH, null, 1 ] // available to players [yn] - ugh .. scan loot, quest and vendor templates and write to ?_itemset - ); - - // fieldId => [checkType, checkValue[, fieldIsArray]] - protected $inputFields = array( - 'cr' => [FILTER_V_RANGE, [2, 12], true ], // criteria ids - 'crs' => [FILTER_V_LIST, [FILTER_ENUM_NONE, FILTER_ENUM_ANY, [0, 424]], true ], // criteria operators - 'crv' => [FILTER_V_REGEX, '/[\p{C};:]/ui', true ], // criteria values - only printable chars, no delimiters - 'na' => [FILTER_V_REGEX, '/[\p{C};]/ui', false], // name / description - only printable chars, no delimiter - 'ma' => [FILTER_V_EQUAL, 1, false], // match any / all filter - 'qu' => [FILTER_V_RANGE, [0, 7], true ], // quality - 'ty' => [FILTER_V_RANGE, [1, 12], true ], // set type - 'minle' => [FILTER_V_RANGE, [1, 999], false], // min item level - 'maxle' => [FILTER_V_RANGE, [1, 999], false], // max itemlevel - 'minrl' => [FILTER_V_RANGE, [1, MAX_LEVEL], false], // min required level - 'maxrl' => [FILTER_V_RANGE, [1, MAX_LEVEL], false], // max required level - 'cl' => [FILTER_V_LIST, [[1, 9], 11], false], // class - 'ta' => [FILTER_V_RANGE, [1, 30], false] // tag / content group - ); - - protected function createSQLForCriterium(&$cr) - { - if (in_array($cr[0], array_keys($this->genericFilter))) - if ($genCR = $this->genericCriterion($cr)) - return $genCR; - - unset($cr); - $this->error = true; - return [1]; - } - - protected function createSQLForValues() - { - $parts = []; - $_v = &$this->fiData['v']; - - // name [str] - if (isset($_v['na'])) - if ($_ = $this->modularizeString(['name_loc'.User::$localeId])) - $parts[] = $_; - - // quality [enum] - if (isset($_v['qu'])) - $parts[] = ['quality', $_v['qu']]; - - // type [enum] - if (isset($_v['ty'])) - $parts[] = ['type', $_v['ty']]; - - // itemLevel min [int] - if (isset($_v['minle'])) - $parts[] = ['minLevel', $_v['minle'], '>=']; - - // itemLevel max [int] - if (isset($_v['maxle'])) - $parts[] = ['maxLevel', $_v['maxle'], '<=']; - - // reqLevel min [int] - if (isset($_v['minrl'])) - $parts[] = ['reqLevel', $_v['minrl'], '>=']; - - // reqLevel max [int] - if (isset($_v['maxrl'])) - $parts[] = ['reqLevel', $_v['maxrl'], '<=']; - - // class [enum] - if (isset($_v['cl'])) - $parts[] = ['classMask', $this->list2Mask([$_v['cl']]), '&']; - - // tag [enum] - if (isset($_v['ta'])) - $parts[] = ['contentGroup', intVal($_v['ta'])]; - - return $parts; - } -} - -?> diff --git a/includes/types/profile.class.php b/includes/types/profile.class.php deleted file mode 100644 index 02561231..00000000 --- a/includes/types/profile.class.php +++ /dev/null @@ -1,711 +0,0 @@ -iterate() as $__) - { - if ($this->getField('user') && User::$id != $this->getField('user') && !($this->getField('cuFlags') & PROFILER_CU_PUBLISHED)) - continue; - - if (($addInfo & PROFILEINFO_PROFILE) && !$this->isCustom()) - continue; - - if (($addInfo & PROFILEINFO_CHARACTER) && $this->isCustom()) - continue; - - $data[$this->id] = array( - 'id' => $this->getField('id'), - 'name' => $this->getField('name'), - 'race' => $this->getField('race'), - 'classs' => $this->getField('class'), - 'gender' => $this->getField('gender'), - 'level' => $this->getField('level'), - 'faction' => (1 << ($this->getField('race') - 1)) & RACE_MASK_ALLIANCE ? 0 : 1, - 'talenttree1' => $this->getField('talenttree1'), - 'talenttree2' => $this->getField('talenttree2'), - 'talenttree3' => $this->getField('talenttree3'), - 'talentspec' => $this->getField('activespec') + 1, // 0 => 1; 1 => 2 - 'achievementpoints' => $this->getField('achievementpoints'), - 'guild' => '$"'.$this->getField('guildname').'"', // force this to be a string - 'guildrank' => $this->getField('guildrank'), - 'realm' => Profiler::urlize($this->getField('realmName')), - 'realmname' => $this->getField('realmName'), - // 'battlegroup' => Profiler::urlize($this->getField('battlegroup')), // was renamed to subregion somewhere around cata release - // 'battlegroupname' => $this->getField('battlegroup'), - 'gearscore' => $this->getField('gearscore') - ); - - - // for the lv this determins if the link is profile= or profile=.. - if ($this->isCustom()) - $data[$this->id]['published'] = (int)!!($this->getField('cuFlags') & PROFILER_CU_PUBLISHED); - else - $data[$this->id]['region'] = Profiler::urlize($this->getField('region')); - - if ($addInfo & PROFILEINFO_ARENA) - { - $data[$this->id]['rating'] = $this->getField('rating'); - $data[$this->id]['captain'] = $this->getField('captain'); - $data[$this->id]['games'] = $this->getField('seasonGames'); - $data[$this->id]['wins'] = $this->getField('seasonWins'); - } - - // Filter asked for skills - add them - foreach ($reqCols as $col) - $data[$this->id][$col] = $this->getField($col); - - if ($addInfo & PROFILEINFO_PROFILE) - if ($_ = $this->getField('description')) - $data[$this->id]['description'] = $_; - - if ($addInfo & PROFILEINFO_PROFILE) - if ($_ = $this->getField('icon')) - $data[$this->id]['icon'] = $_; - - if ($this->getField('cuFlags') & PROFILER_CU_PINNED) - $data[$this->id]['pinned'] = 1; - - if ($this->getField('cuFlags') & PROFILER_CU_DELETED) - $data[$this->id]['deleted'] = 1; - } - - return array_values($data); - } - - public function renderTooltip($interactive = false) - { - if (!$this->curTpl) - return []; - - $title = ''; - $name = $this->getField('name'); - if ($_ = $this->getField('chosenTitle')) - $title = (new TitleList(array(['bitIdx', $_])))->getField($this->getField('gender') ? 'female' : 'male', true); - - if ($this->isCustom()) - $name .= ' (Custom Profile)'; - else if ($title) - $name = sprintf($title, $name); - - $x = ''; - $x .= ''; - if ($g = $this->getField('guildname')) - $x .= ''; - else if ($d = $this->getField('description')) - $x .= ''; - $x .= ''; - $x .= '
'.$name.'
<'.$g.'>
'.$d.'
'.Lang::game('level').' '.$this->getField('level').' '.Lang::game('ra', $this->getField('race')).' '.Lang::game('cl', $this->getField('class')).'
'; - - return $x; - } - - public function getJSGlobals($addMask = 0) - { - $data = []; - $realms = Profiler::getRealms(); - - foreach ($this->iterate() as $id => $__) - { - if (($addMask & PROFILEINFO_PROFILE) && ($this->getField('cuFlags') & PROFILER_CU_PROFILE)) - { - $profile = array( - 'id' => $this->getField('id'), - 'name' => $this->getField('name'), - 'race' => $this->getField('race'), - 'classs' => $this->getField('class'), - 'level' => $this->getField('level'), - 'gender' => $this->getField('gender') - ); - - if ($_ = $this->getField('icon')) - $profile['icon'] = $_; - - $data[] = $profile; - - continue; - } - - if ($addMask & PROFILEINFO_CHARACTER && !($this->getField('cuFlags') & PROFILER_CU_PROFILE)) - { - if (!isset($realms[$this->getField('realm')])) - continue; - - $data[] = array( - 'id' => $this->getField('id'), - 'name' => $this->getField('name'), - 'realmname' => $realms[$this->getField('realm')]['name'], - 'region' => $realms[$this->getField('realm')]['region'], - 'realm' => Profiler::urlize($realms[$this->getField('realm')]['name']), - 'race' => $this->getField('race'), - 'classs' => $this->getField('class'), - 'level' => $this->getField('level'), - 'gender' => $this->getField('gender'), - 'pinned' => $this->getField('cuFlags') & PROFILER_CU_PINNED ? 1 : 0 - ); - } - } - - return $data; - } - - public function isCustom() - { - return $this->getField('cuFlags') & PROFILER_CU_PROFILE; - } -} - - -class ProfileListFilter extends Filter -{ - public $useLocalList = false; - public $extraOpts = []; - - private $realms = []; - - protected $enums = array( - -1 => array( // arena team sizes - // by name by rating by contrib - 12 => 2, 13 => 2, 14 => 2, - 15 => 3, 16 => 3, 17 => 3, - 18 => 5, 19 => 5, 20 => 5 - ) - ); - - protected $genericFilter = array( // misc (bool): _NUMERIC => useFloat; _STRING => localized; _FLAG => match Value; _BOOLEAN => stringSet - 2 => [FILTER_CR_NUMERIC, 'gearscore', NUM_CAST_INT ], // gearscore [num] - 3 => [FILTER_CR_NUMERIC, 'achievementpoints', NUM_CAST_INT ], // achievementpoints [num] - 5 => [FILTER_CR_NUMERIC, 'talenttree1', NUM_CAST_INT ], // talenttree1 [num] - 6 => [FILTER_CR_NUMERIC, 'talenttree2', NUM_CAST_INT ], // talenttree2 [num] - 7 => [FILTER_CR_NUMERIC, 'talenttree3', NUM_CAST_INT ], // talenttree3 [num] - 9 => [FILTER_CR_STRING, 'g.name', ], // guildname - 10 => [FILTER_CR_CALLBACK, 'cbHasGuildRank', null, null], // guildrank - 12 => [FILTER_CR_CALLBACK, 'cbTeamName', null, null], // teamname2v2 - 15 => [FILTER_CR_CALLBACK, 'cbTeamName', null, null], // teamname3v3 - 18 => [FILTER_CR_CALLBACK, 'cbTeamName', null, null], // teamname5v5 - 13 => [FILTER_CR_CALLBACK, 'cbTeamRating', null, null], // teamrtng2v2 - 16 => [FILTER_CR_CALLBACK, 'cbTeamRating', null, null], // teamrtng3v3 - 19 => [FILTER_CR_CALLBACK, 'cbTeamRating', null, null], // teamrtng5v5 - 14 => [FILTER_CR_NYI_PH, 0 ], // teamcontrib2v2 [num] - 17 => [FILTER_CR_NYI_PH, 0 ], // teamcontrib3v3 [num] - 20 => [FILTER_CR_NYI_PH, 0 ], // teamcontrib5v5 [num] - 21 => [FILTER_CR_CALLBACK, 'cbWearsItems', null, null], // wearingitem [str] - 23 => [FILTER_CR_CALLBACK, 'cbCompletedAcv', null, null], // completedachievement - 25 => [FILTER_CR_CALLBACK, 'cbProfession', 171, null], // alchemy [num] - 26 => [FILTER_CR_CALLBACK, 'cbProfession', 164, null], // blacksmithing [num] - 27 => [FILTER_CR_CALLBACK, 'cbProfession', 333, null], // enchanting [num] - 28 => [FILTER_CR_CALLBACK, 'cbProfession', 202, null], // engineering [num] - 29 => [FILTER_CR_CALLBACK, 'cbProfession', 182, null], // herbalism [num] - 30 => [FILTER_CR_CALLBACK, 'cbProfession', 773, null], // inscription [num] - 31 => [FILTER_CR_CALLBACK, 'cbProfession', 755, null], // jewelcrafting [num] - 32 => [FILTER_CR_CALLBACK, 'cbProfession', 165, null], // leatherworking [num] - 33 => [FILTER_CR_CALLBACK, 'cbProfession', 186, null], // mining [num] - 34 => [FILTER_CR_CALLBACK, 'cbProfession', 393, null], // skinning [num] - 35 => [FILTER_CR_CALLBACK, 'cbProfession', 197, null], // tailoring [num] - 36 => [FILTER_CR_CALLBACK, 'cbHasGuild', null, null] // hasguild [yn] - ); - - - // fieldId => [checkType, checkValue[, fieldIsArray]] - protected $inputFields = array( - 'cr' => [FILTER_V_RANGE, [1, 36], true ], // criteria ids - 'crs' => [FILTER_V_LIST, [FILTER_ENUM_NONE, FILTER_ENUM_ANY, [0, 5000]], true ], // criteria operators - 'crv' => [FILTER_V_REGEX, '/[\p{C};]/ui', true ], // criteria values - 'na' => [FILTER_V_REGEX, '/[\p{C};]/ui', false], // name - only printable chars, no delimiter - 'ma' => [FILTER_V_EQUAL, 1, false], // match any / all filter - 'ex' => [FILTER_V_EQUAL, 'on', false], // only match exact - 'si' => [FILTER_V_LIST, [1, 2], false], // side - 'ra' => [FILTER_V_LIST, [[1, 8], 10, 11], true ], // race - 'cl' => [FILTER_V_LIST, [[1, 9], 11], true ], // class - 'minle' => [FILTER_V_RANGE, [1, MAX_LEVEL], false], // min level - 'maxle' => [FILTER_V_RANGE, [1, MAX_LEVEL], false], // max level - 'rg' => [FILTER_V_CALLBACK, 'cbRegionCheck', false], // region - 'sv' => [FILTER_V_CALLBACK, 'cbServerCheck', false], // server - ); - - /* heads up! - a couple of filters are too complex to be run against the characters database - if they are selected, force useage of LocalProfileList - */ - - public function __construct($fromPOST = false, $opts = []) - { - if (!empty($opts['realms'])) - $this->realms = $opts['realms']; - else - $this->realms = array_keys(Profiler::getRealms()); - - parent::__construct($fromPOST, $opts); - - if (!empty($this->fiData['c']['cr'])) - if (array_intersect($this->fiData['c']['cr'], [2, 3, 5, 6, 7, 21])) - $this->useLocalList = true; - } - - protected function createSQLForCriterium(&$cr) - { - if (in_array($cr[0], array_keys($this->genericFilter))) - if ($genCR = $this->genericCriterion($cr)) - return $genCR; - - unset($cr); - $this->error = true; - return [1]; - } - - protected function createSQLForValues() - { - $parts = []; - $_v = $this->fiData['v']; - - // region (rg), battlegroup (bg) and server (sv) are passed to ProflieList as miscData and handled there - - // table key differs between remote and local :< - $k = $this->useLocalList ? 'p' : 'c'; - - // name [str] - the table is case sensitive. Since i down't want to destroy indizes, lets alter the search terms - if (!empty($_v['na'])) - { - $lower = $this->modularizeString([$k.'.name'], Util::lower($_v['na']), !empty($_v['ex']) && $_v['ex'] == 'on'); - $proper = $this->modularizeString([$k.'.name'], Util::ucWords($_v['na']), !empty($_v['ex']) && $_v['ex'] == 'on'); - - $parts[] = ['OR', $lower, $proper]; - } - - // side [list] - if (!empty($_v['si'])) - { - if ($_v['si'] == 1) - $parts[] = [$k.'.race', [1, 3, 4, 7, 11]]; - else if ($_v['si'] == 2) - $parts[] = [$k.'.race', [2, 5, 6, 8, 10]]; - } - - // race [list] - if (!empty($_v['ra'])) - $parts[] = [$k.'.race', $_v['ra']]; - - // class [list] - if (!empty($_v['cl'])) - $parts[] = [$k.'.class', $_v['cl']]; - - // min level [int] - if (isset($_v['minle'])) - $parts[] = [$k.'.level', $_v['minle'], '>=']; - - // max level [int] - if (isset($_v['maxle'])) - $parts[] = [$k.'.level', $_v['maxle'], '<=']; - - return $parts; - } - - protected function cbRegionCheck(&$v) - { - if ($v == 'eu' || $v == 'us') - { - $this->parentCats[0] = $v; // directly redirect onto this region - $v = ''; // remove from filter - - return true; - } - - return false; - } - - protected function cbServerCheck(&$v) - { - foreach (Profiler::getRealms() as $realm) - if ($realm['name'] == $v) - { - $this->parentCats[1] = Profiler::urlize($v);// directly redirect onto this server - $v = ''; // remove from filter - - return true; - } - - return false; - } - - protected function cbProfession($cr, $skillId) - { - if (!Util::checkNumeric($cr[2], NUM_CAST_INT) || !$this->int2Op($cr[1])) - return; - - $k = 'sk_'.Util::createHash(12); - $col = 'skill'.$skillId; - - $this->formData['extraCols'][$skillId] = $col; - - if ($this->useLocalList) - { - $this->extraOpts[$k] = array( - 'j' => ['?_profiler_completion '.$k.' ON '.$k.'.id = p.id AND '.$k.'.`type` = '.TYPE_SKILL.' AND '.$k.'.typeId = '.$skillId.' AND '.$k.'.cur '.$cr[1].' '.$cr[2], true], - 's' => [', '.$k.'.cur AS '.$col] - ); - return [$k.'.typeId', null, '!']; - } - else - { - $this->extraOpts[$k] = array( - 'j' => ['character_skills '.$k.' ON '.$k.'.guid = c.guid AND '.$k.'.skill = '.$skillId.' AND '.$k.'.value '.$cr[1].' '.$cr[2], true], - 's' => [', '.$k.'.value AS '.$col] - ); - return [$k.'.skill', null, '!']; - } - } - - protected function cbCompletedAcv($cr) - { - if (!Util::checkNumeric($cr[2], NUM_CAST_INT)) - return false; - - if (!DB::Aowow()->selectCell('SELECT 1 FROM ?_achievement WHERE id = ?d', $cr[2])) - return false; - - $k = 'acv_'.Util::createHash(12); - - if ($this->useLocalList) - { - $this->extraOpts[$k] = ['j' => ['?_profiler_completion '.$k.' ON '.$k.'.id = p.id AND '.$k.'.`type` = '.TYPE_ACHIEVEMENT.' AND '.$k.'.typeId = '.$cr[2], true]]; - return [$k.'.typeId', null, '!']; - } - else - { - $this->extraOpts[$k] = ['j' => ['character_achievement '.$k.' ON '.$k.'.guid = c.guid AND '.$k.'.achievement = '.$cr[2], true]]; - return [$k.'.achievement', null, '!']; - } - } - - protected function cbWearsItems($cr) - { - if (!Util::checkNumeric($cr[2], NUM_CAST_INT)) - return false; - - if (!DB::Aowow()->selectCell('SELECT 1 FROM ?_items WHERE id = ?d', $cr[2])) - return false; - - $k = 'i_'.Util::createHash(12); - - $this->extraOpts[$k] = ['j' => ['?_profiler_items '.$k.' ON '.$k.'.id = p.id AND '.$k.'.item = '.$cr[2], true]]; - return [$k.'.item', null, '!']; - } - - protected function cbHasGuild($cr) - { - if (!$this->int2Bool($cr[1])) - return false; - - if ($this->useLocalList) - return ['p.guild', null, $cr[1] ? '!' : null]; - else - return ['gm.guildId', null, $cr[1] ? '!' : null]; - } - - protected function cbHasGuildRank($cr) - { - if (!Util::checkNumeric($cr[2], NUM_CAST_INT) || !$this->int2Op($cr[1])) - return false; - - if ($this->useLocalList) - return ['p.guildrank', $cr[2], $cr[1]]; - else - return ['gm.rank', $cr[2], $cr[1]]; - } - - protected function cbTeamName($cr) - { - if ($_ = $this->modularizeString(['at.name'], $cr[2])) - return ['AND', ['at.type', $this->enums[-1][$cr[0]]], $_]; - - return false; - } - - protected function cbTeamRating($cr) - { - if (!Util::checkNumeric($cr[2], NUM_CAST_INT) || !$this->int2Op($cr[1])) - return false; - - return ['AND', ['at.type', $this->enums[-1][$cr[0]]], ['at.rating', $cr[2], $cr[1]]]; - } -} - - -class RemoteProfileList extends ProfileList -{ - protected $queryBase = 'SELECT `c`.*, `c`.`guid` AS ARRAY_KEY FROM characters c'; - protected $queryOpts = array( - 'c' => [['gm', 'g', 'ca', 'ct'], 'g' => 'ARRAY_KEY', 'o' => 'level DESC, name ASC'], - 'ca' => ['j' => ['character_achievement ca ON ca.guid = c.guid', true], 's' => ', GROUP_CONCAT(DISTINCT ca.achievement SEPARATOR " ") AS _acvs'], - 'ct' => ['j' => ['character_talent ct ON ct.guid = c.guid AND ct.spec = c.activespec', true], 's' => ', GROUP_CONCAT(DISTINCT ct.spell SEPARATOR " ") AS _talents'], - 'gm' => ['j' => ['guild_member gm ON gm.guid = c.guid', true], 's' => ', gm.rank AS guildrank'], - 'g' => ['j' => ['guild g ON g.guildid = gm.guildid', true], 's' => ', g.guildid AS guild, g.name AS guildname'], - 'atm' => ['j' => ['arena_team_member atm ON atm.guid = c.guid', true], 's' => ', atm.personalRating AS rating'], - 'at' => [['atm'], 'j' => 'arena_team at ON atm.arenaTeamId = at.arenaTeamId', 's' => ', at.name AS arenateam, IF(at.captainGuid = c.guid, 1, 0) AS captain'] - ); - - public function __construct($conditions = [], $miscData = null) - { - // select DB by realm - if (!$this->selectRealms($miscData)) - { - trigger_error('no access to auth-db or table realmlist is empty', E_USER_WARNING); - return; - } - - parent::__construct($conditions, $miscData); - - if ($this->error) - return; - - reset($this->dbNames); // only use when querying single realm - $realmId = key($this->dbNames); - $realms = Profiler::getRealms(); - $acvCache = []; - $talentCache = []; - $atCache = []; - $distrib = null; - $talentData = []; - $limit = CFG_SQL_LIMIT_DEFAULT; - - foreach ($conditions as $c) - if (is_int($c)) - $limit = $c; - - // post processing - foreach ($this->iterate() as $guid => &$curTpl) - { - // battlegroup - $curTpl['battlegroup'] = CFG_BATTLEGROUP; - - // realm - $r = explode(':', $guid)[0]; - if (!empty($realms[$r])) - { - $curTpl['realm'] = $r; - $curTpl['realmName'] = $realms[$r]['name']; - $curTpl['region'] = $realms[$r]['region']; - } - else - { - trigger_error('character "'.$curTpl['name'].'" belongs to nonexistant realm #'.$r, E_USER_WARNING); - unset($this->templates[$guid]); - continue; - } - - // temp id - $curTpl['id'] = 0; - - // achievement points pre - if ($acvs = explode(' ', $curTpl['_acvs'])) - foreach ($acvs as $a) - if ($a && !isset($acvCache[$a])) - $acvCache[$a] = $a; - - // talent points pre - if ($talents = explode(' ', $curTpl['_talents'])) - foreach ($talents as $t) - if ($t && !isset($talentCache[$t])) - $talentCache[$t] = $t; - - // equalize distribution - if ($limit != CFG_SQL_LIMIT_NONE) - { - if (empty($distrib[$curTpl['realm']])) - $distrib[$curTpl['realm']] = 1; - else - $distrib[$curTpl['realm']]++; - } - - $curTpl['cuFlags'] = 0; - } - - if ($talentCache) - $talentData = DB::Aowow()->select('SELECT spell AS ARRAY_KEY, tab, rank FROM ?_talents WHERE spell IN (?a)', $talentCache); - - if ($distrib !== null) - { - $total = array_sum($distrib); - foreach ($distrib as &$d) - $d = ceil($limit * $d / $total); - } - - if ($acvCache) - $acvCache = DB::Aowow()->selectCol('SELECT id AS ARRAY_KEY, points FROM ?_achievement WHERE id IN (?a)', $acvCache); - - foreach ($this->iterate() as $guid => &$curTpl) - { - if ($distrib !== null) - { - if ($limit <= 0 || $distrib[$curTpl['realm']] <= 0) - { - unset($this->templates[$guid]); - continue; - } - - $distrib[$curTpl['realm']]--; - $limit--; - } - - - $a = explode(' ', $curTpl['_acvs']); - $t = explode(' ', $curTpl['_talents']); - unset($curTpl['_acvs']); - unset($curTpl['_talents']); - - // achievement points post - $curTpl['achievementpoints'] = array_sum(array_intersect_key($acvCache, array_combine($a, $a))); - - // talent points post - $curTpl['talenttree1'] = 0; - $curTpl['talenttree2'] = 0; - $curTpl['talenttree3'] = 0; - foreach ($talentData as $spell => $data) - if (in_array($spell, $t)) - $curTpl['talenttree'.($data['tab'] + 1)] += $data['rank']; - } - } - - public function getListviewData($addInfoMask = 0, array $reqCols = []) - { - $data = parent::getListviewData($addInfoMask, $reqCols); - - // not wanted on server list - foreach ($data as &$d) - unset($d['published']); - - return $data; - } - - public function initializeLocalEntries() - { - $baseData = $guildData = []; - foreach ($this->iterate() as $guid => $__) - { - $baseData[$guid] = array( - 'realm' => $this->getField('realm'), - 'realmGUID' => $this->getField('guid'), - 'name' => $this->getField('name'), - 'race' => $this->getField('race'), - 'class' => $this->getField('class'), - 'level' => $this->getField('level'), - 'gender' => $this->getField('gender'), - 'guild' => $this->getField('guild') ?: null, - 'guildrank' => $this->getField('guild') ? $this->getField('guildrank') : null, - 'cuFlags' => PROFILER_CU_NEEDS_RESYNC - ); - - if ($this->getField('guild')) - $guildData[] = array( - 'realm' => $this->getField('realm'), - 'realmGUID' => $this->getField('guild'), - 'name' => $this->getField('guildname'), - 'nameUrl' => Profiler::urlize($this->getField('guildname')), - 'cuFlags' => PROFILER_CU_NEEDS_RESYNC - ); - } - - // basic guild data (satisfying table constraints) - if ($guildData) - { - foreach (Util::createSqlBatchInsert($guildData) as $ins) - DB::Aowow()->query('INSERT IGNORE INTO ?_profiler_guild (?#) VALUES '.$ins, array_keys(reset($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') - ); - - foreach ($baseData as &$bd) - if ($bd['guild']) - $bd['guild'] = $localGuilds[$bd['realm']][$bd['guild']]; - } - - // basic char data (enough for tooltips) - if ($baseData) - { - foreach (Util::createSqlBatchInsert($baseData) as $ins) - DB::Aowow()->query('INSERT IGNORE INTO ?_profiler_profiles (?#) VALUES '.$ins, array_keys(reset($baseData))); - - // 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') - ); - - foreach ($this->iterate() as $guid => &$_curTpl) - if (isset($localIds[$guid])) - $_curTpl = array_merge($_curTpl, $localIds[$guid]); - } - } -} - - -class LocalProfileList extends ProfileList -{ - protected $queryBase = 'SELECT p.*, p.id AS ARRAY_KEY FROM ?_profiler_profiles p'; - protected $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'] - ); - - public function __construct($conditions = [], $miscData = null) - { - parent::__construct($conditions, $miscData); - - if ($this->error) - return; - - $realms = Profiler::getRealms(); - - // post processing - $acvPoints = DB::Aowow()->selectCol('SELECT pc.id AS ARRAY_KEY, SUM(a.points) FROM ?_profiler_completion pc LEFT JOIN ?_achievement a ON a.id = pc.typeId WHERE pc.`type` = ?d AND pc.id IN (?a) GROUP BY pc.id', TYPE_ACHIEVEMENT, $this->getFoundIDs()); - - foreach ($this->iterate() as $id => &$curTpl) - { - if ($curTpl['realm'] && !isset($realms[$curTpl['realm']])) - continue; - - if (isset($realms[$curTpl['realm']])) - { - $curTpl['realmName'] = $realms[$curTpl['realm']]['name']; - $curTpl['region'] = $realms[$curTpl['realm']]['region']; - } - - // battlegroup - $curTpl['battlegroup'] = CFG_BATTLEGROUP; - - $curTpl['achievementpoints'] = isset($acvPoints[$id]) ? $acvPoints[$id] : 0; - } - } - - public function getProfileUrl() - { - $url = '?profile='; - - if ($this->isCustom()) - return $url.$this->getField('id'); - - return $url.implode('.', array( - Profiler::urlize($this->getField('region')), - Profiler::urlize($this->getField('realmName')), - urlencode($this->getField('name')) - )); - } -} - - -?> diff --git a/includes/types/quest.class.php b/includes/types/quest.class.php deleted file mode 100644 index 417d8797..00000000 --- a/includes/types/quest.class.php +++ /dev/null @@ -1,720 +0,0 @@ - [], - '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($conditions = [], $miscData = null) - { - parent::__construct($conditions, $miscData); - - // i don't like this very much - $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['cat2'] = 0; - - foreach (Game::$questClasses as $k => $arr) - { - if (in_array($_curTpl['cat1'], $arr)) - { - $_curTpl['cat2'] = $k; - break; - } - } - - // store requirements - $requires = []; - for ($i = 1; $i < 7; $i++) - { - if ($_ = $_curTpl['reqItemId'.$i]) - $requires[TYPE_ITEM][] = $_; - - if ($i > 4) - continue; - - if ($_curTpl['reqNpcOrGo'.$i] > 0) - $requires[TYPE_NPC][] = $_curTpl['reqNpcOrGo'.$i]; - else if ($_curTpl['reqNpcOrGo'.$i] < 0) - $requires[TYPE_OBJECT][] = -$_curTpl['reqNpcOrGo'.$i]; - - if ($_ = $_curTpl['reqSourceItemId'.$i]) - $requires[TYPE_ITEM][] = $_; - } - if ($requires) - $this->requires[$id] = $requires; - - // store rewards - $rewards = []; - $choices = []; - - if ($_ = $_curTpl['rewardTitleId']) - $rewards[TYPE_TITLE][] = $_; - - if ($_ = $_curTpl['rewardHonorPoints']) - $rewards[TYPE_CURRENCY][104] = $_; - - if ($_ = $_curTpl['rewardArenaPoints']) - $rewards[TYPE_CURRENCY][103] = $_; - - for ($i = 1; $i < 7; $i++) - { - if ($_ = $_curTpl['rewardChoiceItemId'.$i]) - $choices[TYPE_ITEM][$_] = $_curTpl['rewardChoiceItemCount'.$i]; - - if ($i > 5) - continue; - - if ($_ = $_curTpl['rewardFactionId'.$i]) - $rewards[TYPE_FACTION][$_] = $_curTpl['rewardFactionValue'.$i]; - - if ($i > 4) - continue; - - if ($_ = $_curTpl['rewardItemId'.$i]) - { - $qty = $_curTpl['rewardItemCount'.$i]; - if (in_array($_, $currencies)) - $rewards[TYPE_CURRENCY][array_search($_, $currencies)] = $qty; - else - $rewards[TYPE_ITEM][$_] = $qty; - } - } - if ($rewards) - $this->rewards[$id] = $rewards; - - if ($choices) - $this->choices[$id] = $choices; - } - } - - // static use START - public static function getName($id) - { - $n = DB::Aowow()->SelectRow('SELECT name_loc0, name_loc2, name_loc3, name_loc6, name_loc8 FROM ?_quests WHERE id = ?d', $id); - return Util::localizedString($n, 'name'); - } - // static use END - - public function isRepeatable() - { - return $this->curTpl['flags'] & QUEST_FLAG_REPEATABLE || $this->curTpl['specialFlags'] & QUEST_FLAG_SPECIAL_REPEATABLE; - } - - public function isDaily() - { - if ($this->curTpl['flags'] & QUEST_FLAG_DAILY) - return 1; - - if ($this->curTpl['flags'] & QUEST_FLAG_WEEKLY) - return 2; - - if ($this->curTpl['specialFlags'] & QUEST_FLAG_SPECIAL_MONTHLY) - return 3; - - return 0; - } - - // using reqPlayerKills and rewardHonor as a crutch .. has TC this even implemented..? - public function isPvPEnabled() - { - return $this->curTpl['reqPlayerKills'] || $this->curTpl['rewardHonorPoints'] || $this->curTpl['rewardArenaPoints']; - } - - // by TC definition - public function isSeasonal() - { - return in_array($this->getField('zoneOrSortBak'), [-22, -284, -366, -369, -370, -376, -374]) && !$this->isRepeatable(); - } - - public function getSourceData() - { - $data = []; - - foreach ($this->iterate() as $__) - { - $data[$this->id] = array( - "n" => $this->getField('name', true), - "t" => TYPE_QUEST, - "ti" => $this->id, - "c" => $this->curTpl['cat1'], - "c2" => $this->curTpl['cat2'] - ); - } - - return $data; - } - - public function getSOMData($side = SIDE_BOTH) - { - $data = []; - - foreach ($this->iterate() as $__) - { - if (!(Game::sideByRaceMask($this->curTpl['reqRaceMask']) & $side)) - continue; - - list($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', - $this->id - ); - - $data[$this->id] = array( - 'level' => $this->curTpl['level'] < 0 ? MAX_LEVEL : $this->curTpl['level'], - 'name' => $this->getField('name', true), - 'category' => $this->curTpl['cat1'], - 'category2' => $this->curTpl['cat2'], - 'series' => $series, - 'first' => $first - ); - - if ($this->isDaily()) - $data[$this->id]['daily'] = 1; - } - - return $data; - } - - public function getListviewData($extraFactionId = 0) // i should formulate a propper parameter.. - { - $data = []; - - foreach ($this->iterate() as $__) - { - $data[$this->id] = array( - 'category' => $this->curTpl['cat1'], - 'category2' => $this->curTpl['cat2'], - 'id' => $this->id, - 'level' => $this->curTpl['level'], - 'reqlevel' => $this->curTpl['minLevel'], - 'name' => $this->getField('name', true), - 'side' => Game::sideByRaceMask($this->curTpl['reqRaceMask']), - 'wflags' => 0x0, - 'xp' => $this->curTpl['rewardXP'] - ); - - if (!empty($this->rewards[$this->id][TYPE_CURRENCY])) - foreach ($this->rewards[$this->id][TYPE_CURRENCY] as $iId => $qty) - $data[$this->id]['currencyrewards'][] = [$iId, $qty]; - - if (!empty($this->rewards[$this->id][TYPE_ITEM])) - foreach ($this->rewards[$this->id][TYPE_ITEM] as $iId => $qty) - $data[$this->id]['itemrewards'][] = [$iId, $qty]; - - if (!empty($this->choices[$this->id][TYPE_ITEM])) - foreach ($this->choices[$this->id][TYPE_ITEM] as $iId => $qty) - $data[$this->id]['itemchoices'][] = [$iId, $qty]; - - if ($_ = $this->curTpl['rewardTitleId']) - $data[$this->id]['titlereward'] = $_; - - if ($_ = $this->curTpl['type']) - $data[$this->id]['type'] = $_; - - if ($_ = $this->curTpl['reqClassMask']) - $data[$this->id]['reqclass'] = $_; - - if ($_ = ($this->curTpl['reqRaceMask'] & RACE_MASK_ALL)) - if ((($_ & RACE_MASK_ALLIANCE) != RACE_MASK_ALLIANCE) && (($_ & RACE_MASK_HORDE) != RACE_MASK_HORDE)) - $data[$this->id]['reqrace'] = $_; - - if ($_ = $this->curTpl['rewardOrReqMoney']) - if ($_ > 0) - $data[$this->id]['money'] = $_; - - // todo (med): also get disables - if ($this->curTpl['flags'] & QUEST_FLAG_UNAVAILABLE) - $data[$this->id]['historical'] = true; - - // if ($this->isRepeatable()) // dafuque..? says repeatable and is used as 'disabled'..? - // $data[$this->id]['wflags'] |= QUEST_CU_REPEATABLE; - if ($this->curTpl['cuFlags'] & (CUSTOM_UNAVAILABLE | CUSTOM_DISABLED)) - $data[$this->id]['wflags'] |= QUEST_CU_REPEATABLE; - - if ($this->curTpl['flags'] & QUEST_FLAG_DAILY) - { - $data[$this->id]['wflags'] |= QUEST_CU_DAILY; - $data[$this->id]['daily'] = true; - } - - if ($this->curTpl['flags'] & QUEST_FLAG_WEEKLY) - { - $data[$this->id]['wflags'] |= QUEST_CU_WEEKLY; - $data[$this->id]['weekly'] = true; - } - - if ($this->isSeasonal()) - $data[$this->id]['wflags'] |= QUEST_CU_SEASONAL; - - if ($this->curTpl['flags'] & QUEST_FLAG_AUTO_REWARDED) // not shown in log - $data[$this->id]['wflags'] |= QUEST_CU_SKIP_LOG; - - if ($this->curTpl['flags'] & QUEST_FLAG_AUTO_ACCEPT) // self-explanatory - $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); - - $data[$this->id]['reprewards'] = []; - for ($i = 1; $i < 6; $i++) - { - $foo = $this->curTpl['rewardFactionId'.$i]; - $bar = $this->curTpl['rewardFactionValue'.$i]; - if ($foo && $bar) - { - $data[$this->id]['reprewards'][] = [$foo, $bar]; - - if ($extraFactionId == $foo) - $data[$this->id]['reputation'] = $bar; - } - } - } - - return $data; - } - - public function parseText($type = 'objectives', $jsEscaped = true) - { - $text = $this->getField($type, true); - if (!$text) - return ''; - - $text = Util::parseHtmlText($text); - - if ($jsEscaped) - $text = Util::jsEscape($text); - - return $text; - } - - public function renderTooltip() - { - if (!$this->curTpl) - return null; - - $title = Util::jsEscape($this->getField('name', true)); - $level = $this->curTpl['level']; - if ($level < 0) - $level = 0; - - $x = ''; - if ($level) - { - $level = sprintf(Lang::quest('questLevel'), $level); - - if ($this->curTpl['flags'] & QUEST_FLAG_DAILY) // daily - $level .= ' '.Lang::quest('daily'); - - $x .= '
'.$title.''.$level.'
'; - } - else - $x .= '
'.$title.'
'; - - - $x .= '

'.$this->parseText('objectives'); - - - $xReq = ''; - for ($i = 1; $i < 5; $i++) - { - $ot = $this->getField('objectiveText'.$i, true); - $rng = $this->curTpl['reqNpcOrGo'.$i]; - $rngQty = $this->curTpl['reqNpcOrGoCount'.$i]; - - if ($rngQty < 1 && (!$rng || $ot)) - continue; - - if ($ot) - $name = $ot; - else - $name = $rng > 0 ? CreatureList::getName($rng) : GameObjectList::getName(-$rng); - - $xReq .= '
- '.Util::jsEscape($name).($rngQty > 1 ? ' x '.$rngQty : null); - } - - for ($i = 1; $i < 7; $i++) - { - $ri = $this->curTpl['reqItemId'.$i]; - $riQty = $this->curTpl['reqItemCount'.$i]; - - if (!$ri || $riQty < 1) - continue; - - $xReq .= '
- '.Util::jsEscape(ItemList::getName($ri)).($riQty > 1 ? ' x '.$riQty : null); - } - - if ($et = $this->getField('end', true)) - $xReq .= '
- '.Util::jsEscape($et); - - if ($_ = $this->getField('rewardOrReqMoney')) - if ($_ < 0) - $xReq .= '
- '.Lang::quest('money').Lang::main('colon').Util::formatMoney(abs($_)); - - if ($xReq) - $x .= '

'.Lang::quest('requirements').Lang::main('colon').''.$xReq; - - $x .= '
'; - - return $x; - } - - public function getJSGlobals($addMask = GLOBALINFO_ANY) - { - $data = []; - - foreach ($this->iterate() as $__) - { - if ($addMask & GLOBALINFO_REWARDS) - { - // items - for ($i = 1; $i < 5; $i++) - if ($this->curTpl['rewardItemId'.$i] > 0) - $data[TYPE_ITEM][$this->curTpl['rewardItemId'.$i]] = $this->curTpl['rewardItemId'.$i]; - - for ($i = 1; $i < 7; $i++) - if ($this->curTpl['rewardChoiceItemId'.$i] > 0) - $data[TYPE_ITEM][$this->curTpl['rewardChoiceItemId'.$i]] = $this->curTpl['rewardChoiceItemId'.$i]; - - // spells - if ($this->curTpl['rewardSpell'] > 0) - $data[TYPE_SPELL][$this->curTpl['rewardSpell']] = $this->curTpl['rewardSpell']; - - if ($this->curTpl['rewardSpellCast'] > 0) - $data[TYPE_SPELL][$this->curTpl['rewardSpellCast']] = $this->curTpl['rewardSpellCast']; - - // titles - if ($this->curTpl['rewardTitleId'] > 0) - $data[TYPE_TITLE][$this->curTpl['rewardTitleId']] = $this->curTpl['rewardTitleId']; - - // currencies - if (!empty($this->rewards[$this->id][TYPE_CURRENCY])) - foreach ($this->rewards[$this->id][TYPE_CURRENCY] as $id => $__) - $data[TYPE_CURRENCY][$id] = $id; - } - - if ($addMask & GLOBALINFO_SELF) - $data[TYPE_QUEST][$this->id] = ['name' => $this->getField('name', true)]; - } - - return $data; - } -} - - -class QuestListFilter extends Filter -{ - public $extraOpts = []; - protected $enums = array( // massive enums could be put here, if you want to restrict inputs further to be valid IDs instead of just integers - 37 => [null, 1, 2, 3, 4, 5, 6, 7, 8, 9, null, 11, true, false], - 38 => [null, 1, 2, 3, 4, 5, 6, 7, 8, null, 10, 11, true, false], - ); - protected $genericFilter = array( - 1 => [FILTER_CR_CALLBACK, 'cbReputation', '>', null], // increasesrepwith - 2 => [FILTER_CR_NUMERIC, 'rewardXP', NUM_CAST_INT ], // experiencegained - 3 => [FILTER_CR_NUMERIC, 'rewardOrReqMoney', NUM_CAST_INT ], // moneyrewarded - 4 => [FILTER_CR_CALLBACK, 'cbSpellRewards', null, null], // spellrewarded [yn] - 5 => [FILTER_CR_FLAG, 'flags', QUEST_FLAG_SHARABLE ], // sharable - 6 => [FILTER_CR_NUMERIC, 'timeLimit', NUM_CAST_INT ], // timer - 7 => [FILTER_CR_NYI_PH, null, 1 ], // firstquestseries - 9 => [FILTER_CR_CALLBACK, 'cbEarnReputation', null, null], // objectiveearnrepwith [enum] - 10 => [FILTER_CR_CALLBACK, 'cbReputation', '<', null], // decreasesrepwith - 11 => [FILTER_CR_NUMERIC, 'suggestedPlayers', NUM_CAST_INT ], // suggestedplayers - 15 => [FILTER_CR_NYI_PH, null, 1 ], // lastquestseries - 16 => [FILTER_CR_NYI_PH, null, 1 ], // partseries - 18 => [FILTER_CR_FLAG, 'cuFlags', CUSTOM_HAS_SCREENSHOT ], // hasscreenshots - 19 => [FILTER_CR_CALLBACK, 'cbQuestRelation', 0x1, null], // startsfrom [enum] - 21 => [FILTER_CR_CALLBACK, 'cbQuestRelation', 0x2, null], // endsat [enum] - 22 => [FILTER_CR_CALLBACK, 'cbItemRewards', null, null], // itemrewards [op] [int] - 23 => [FILTER_CR_CALLBACK, 'cbItemChoices', null, null], // itemchoices [op] [int] - 24 => [FILTER_CR_CALLBACK, 'cbLacksStartEnd', null, null], // lacksstartend [yn] - 25 => [FILTER_CR_FLAG, 'cuFlags', CUSTOM_HAS_COMMENT ], // hascomments - 27 => [FILTER_CR_FLAG, 'flags', QUEST_FLAG_DAILY ], // daily - 28 => [FILTER_CR_FLAG, 'flags', QUEST_FLAG_WEEKLY ], // weekly - 29 => [FILTER_CR_FLAG, 'flags', QUEST_FLAG_REPEATABLE ], // repeatable - 30 => [FILTER_CR_NUMERIC, 'id', NUM_CAST_INT, true], // id - 33 => [FILTER_CR_ENUM, 'e.holidayId' ], // relatedevent - 34 => [FILTER_CR_CALLBACK, 'cbAvailable', null, null], // availabletoplayers [yn] - 36 => [FILTER_CR_FLAG, 'cuFlags', CUSTOM_HAS_VIDEO ], // hasvideos - 37 => [FILTER_CR_CALLBACK, 'cbClassSpec', null, null], // classspecific [enum] - 37 => [FILTER_CR_CALLBACK, 'cbRaceSpec', null, null], // racespecific [enum] - 42 => [FILTER_CR_STAFFFLAG, 'flags' ], // flags - 43 => [FILTER_CR_CALLBACK, 'cbCurrencyReward', null, null], // currencyrewarded [enum] - 44 => [FILTER_CR_CALLBACK, 'cbLoremaster', null, null], // countsforloremaster_stc [yn] - 45 => [FILTER_CR_BOOLEAN, 'rewardTitleId' ] // titlerewarded - ); - - // fieldId => [checkType, checkValue[, fieldIsArray]] - protected $inputFields = array( - 'cr' => [FILTER_V_RANGE, [1, 45], true ], // criteria ids - 'crs' => [FILTER_V_LIST, [FILTER_ENUM_NONE, FILTER_ENUM_ANY, [0, 99999]], true ], // criteria operators - 'crv' => [FILTER_V_REGEX, '/\D/', true ], // criteria values - only numerals - 'na' => [FILTER_V_REGEX, '/[\p{C};]/ui', false], // name / text - only printable chars, no delimiter - 'ex' => [FILTER_V_EQUAL, 'on', false], // also match subname - 'ma' => [FILTER_V_EQUAL, 1, false], // match any / all filter - 'minle' => [FILTER_V_RANGE, [1, 99], false], // min quest level - 'maxle' => [FILTER_V_RANGE, [1, 99], false], // max quest level - 'minrl' => [FILTER_V_RANGE, [1, 99], false], // min required level - 'maxrl' => [FILTER_V_RANGE, [1, 99], false], // max required level - 'si' => [FILTER_V_LIST, [-2, -1, 1, 2, 3], false], // siede - 'ty' => [FILTER_V_LIST, [0, 1, 21, 41, 62, [81, 85], 88, 89], true ] // type - ); - - protected function createSQLForCriterium(&$cr) - { - if (in_array($cr[0], array_keys($this->genericFilter))) - if ($genCr = $this->genericCriterion($cr)) - return $genCr; - - unset($cr); - $this->error = true; - return [1]; - } - - protected function createSQLForValues() - { - $parts = []; - $_v = $this->fiData['v']; - - // name - if (isset($_v['na'])) - { - $_ = []; - if (isset($_v['ex']) && $_v['ex'] == 'on') - $_ = $this->modularizeString(['name_loc'.User::$localeId, 'objectives_loc'.User::$localeId, 'details_loc'.User::$localeId]); - else - $_ = $this->modularizeString(['name_loc'.User::$localeId]); - - if ($_) - $parts[] = $_; - } - - // level min - if (isset($_v['minle'])) - $parts[] = ['level', $_v['minle'], '>=']; // not considering quests that are always at player level (-1) - - // level max - if (isset($_v['maxle'])) - $parts[] = ['level', $_v['maxle'], '<=']; - - // reqLevel min - if (isset($_v['minrl'])) - $parts[] = ['minLevel', $_v['minrl'], '>=']; // ignoring maxLevel - - // reqLevel max - if (isset($_v['maxrl'])) - $parts[] = ['minLevel', $_v['maxrl'], '<=']; // ignoring maxLevel - - // side - if (isset($_v['si'])) - { - $ex = [['reqRaceMask', RACE_MASK_ALL, '&'], RACE_MASK_ALL, '!']; - $notEx = ['OR', ['reqRaceMask', 0], [['reqRaceMask', RACE_MASK_ALL, '&'], RACE_MASK_ALL]]; - - switch ($_v['si']) - { - case 3: - $parts[] = $notEx; - break; - case 2: - $parts[] = ['OR', $notEx, ['reqRaceMask', RACE_MASK_HORDE, '&']]; - break; - case -2: - $parts[] = ['AND', $ex, ['reqRaceMask', RACE_MASK_HORDE, '&']]; - break; - case 1: - $parts[] = ['OR', $notEx, ['reqRaceMask', RACE_MASK_ALLIANCE, '&']]; - break; - case -1: - $parts[] = ['AND', $ex, ['reqRaceMask', RACE_MASK_ALLIANCE, '&']]; - break; - } - } - - // type [list] - if (isset($_v['ty'])) - $parts[] = ['type', $_v['ty']]; - - return $parts; - } - - protected function cbReputation($cr, $sign) - { - if (!Util::checkNumeric($cr[1], NUM_REQ_INT) || $cr[1] <= 0) - return false; - - if ($_ = DB::Aowow()->selectRow('SELECT * FROM ?_factions WHERE id = ?d', $cr[1])) - $this->formData['reputationCols'][] = [$cr[1], Util::localizedString($_, 'name')]; - - return [ - 'OR', - ['AND', ['rewardFactionId1', $cr[1]], ['rewardFactionValue1', 0, $sign]], - ['AND', ['rewardFactionId2', $cr[1]], ['rewardFactionValue2', 0, $sign]], - ['AND', ['rewardFactionId3', $cr[1]], ['rewardFactionValue3', 0, $sign]], - ['AND', ['rewardFactionId4', $cr[1]], ['rewardFactionValue4', 0, $sign]], - ['AND', ['rewardFactionId5', $cr[1]], ['rewardFactionValue5', 0, $sign]] - ]; - } - - protected function cbQuestRelation($cr, $flags) - { - switch ($cr[1]) - { - case 1: // npc - return ['AND', ['qse.type', TYPE_NPC], ['qse.method', $flags, '&']]; - case 2: // object - return ['AND', ['qse.type', TYPE_OBJECT], ['qse.method', $flags, '&']]; - case 3: // item - return ['AND', ['qse.type', TYPE_ITEM], ['qse.method', $flags, '&']]; - } - - return false; - } - - protected function cbCurrencyReward($cr) - { - if (!Util::checkNumeric($cr[1], NUM_REQ_INT) || $cr[1] <= 0) - return false; - - return [ - 'OR', - ['rewardItemId1', $cr[1]], ['rewardItemId2', $cr[1]], ['rewardItemId3', $cr[1]], ['rewardItemId4', $cr[1]], - ['rewardChoiceItemId1', $cr[1]], ['rewardChoiceItemId2', $cr[1]], ['rewardChoiceItemId3', $cr[1]], ['rewardChoiceItemId4', $cr[1]], ['rewardChoiceItemId5', $cr[1]], ['rewardChoiceItemId6', $cr[1]] - ]; - } - - protected function cbAvailable($cr) - { - if (!$this->int2Bool($cr[1])) - return false; - - if ($cr[1]) - return [['cuFlags', CUSTOM_UNAVAILABLE | CUSTOM_DISABLED, '&'], 0]; - else - return ['cuFlags', CUSTOM_UNAVAILABLE | CUSTOM_DISABLED, '&']; - } - - protected function cbItemChoices($cr) - { - if (!Util::checkNumeric($cr[2], NUM_CAST_INT) || !$this->int2Op($cr[1])) - return false; - - $this->extraOpts['q']['s'][] = ', (IF(rewardChoiceItemId1, 1, 0) + IF(rewardChoiceItemId2, 1, 0) + IF(rewardChoiceItemId3, 1, 0) + IF(rewardChoiceItemId4, 1, 0) + IF(rewardChoiceItemId5, 1, 0) + IF(rewardChoiceItemId6, 1, 0)) as numChoices'; - $this->extraOpts['q']['h'][] = 'numChoices '.$cr[1].' '.$cr[2]; - return [1]; - } - - protected function cbItemRewards($cr) - { - if (!Util::checkNumeric($cr[2], NUM_CAST_INT) || !$this->int2Op($cr[1])) - return false; - - $this->extraOpts['q']['s'][] = ', (IF(rewardItemId1, 1, 0) + IF(rewardItemId2, 1, 0) + IF(rewardItemId3, 1, 0) + IF(rewardItemId4, 1, 0)) as numRewards'; - $this->extraOpts['q']['h'][] = 'numRewards '.$cr[1].' '.$cr[2]; - return [1]; - } - - protected function cbLoremaster($cr) - { - if (!$this->int2Bool($cr[1])) - return false; - - if ($cr[1]) - 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]]; - else - return ['OR', ['zoneOrSort', 0, '<'], ['flags', QUEST_FLAG_DAILY | QUEST_FLAG_WEEKLY | QUEST_FLAG_REPEATABLE , '&'], ['specialFlags', QUEST_FLAG_SPECIAL_REPEATABLE | QUEST_FLAG_SPECIAL_MONTHLY , '&']];; - } - - protected function cbSpellRewards($cr) - { - if (!$this->int2Bool($cr[1])) - return false; - - if ($cr[1]) - return ['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]]; - } - - protected function cbEarnReputation($cr) - { - if (!Util::checkNumeric($cr[1], NUM_REQ_INT)) - return false; - - if ($cr[1] > 0) - return ['OR', ['reqFactionId1', $cr[1]], ['reqFactionId2', $cr[1]]]; - else if ($cr[1] == FILTER_ENUM_ANY) // any - return ['OR', ['reqFactionId1', 0, '>'], ['reqFactionId2', 0, '>']]; - else if ($cr[1] == FILTER_ENUM_NONE) // none - return ['AND', ['reqFactionId1', 0], ['reqFactionId2', 0]]; - - return false; - } - - protected function cbClassSpec($cr) - { - if (!isset($this->enums[$cr[0]][$cr[1]])) - return false; - - $_ = $this->enums[$cr[0]][$cr[1]]; - if ($_ === true) - return ['AND', ['reqClassMask', 0, '!'], [['reqClassMask', CLASS_MASK_ALL, '&'], CLASS_MASK_ALL, '!']]; - else if ($_ === false) - return ['OR', ['reqClassMask', 0], [['reqClassMask', CLASS_MASK_ALL, '&'], CLASS_MASK_ALL]]; - else if (is_int($_)) - return ['AND', ['reqClassMask', (1 << ($_ - 1)), '&'], [['reqClassMask', CLASS_MASK_ALL, '&'], CLASS_MASK_ALL, '!']]; - - return false; - } - - protected function cbRaceSpec($cr) - { - if (!isset($this->enums[$cr[0]][$cr[1]])) - return false; - - $_ = $this->enums[$cr[0]][$cr[1]]; - if ($_ === true) - return ['AND', ['reqRaceMask', 0, '!'], [['reqRaceMask', RACE_MASK_ALL, '&'], RACE_MASK_ALL, '!'], [['reqRaceMask', RACE_MASK_ALLIANCE, '&'], RACE_MASK_ALLIANCE, '!'], [['reqRaceMask', RACE_MASK_HORDE, '&'], RACE_MASK_HORDE, '!']]; - else if ($_ === false) - return ['OR', ['reqRaceMask', 0], ['reqRaceMask', RACE_MASK_ALL], ['reqRaceMask', RACE_MASK_ALLIANCE], ['reqRaceMask', RACE_MASK_HORDE]]; - else if (is_int($_)) - return ['AND', ['reqRaceMask', (1 << ($_ - 1)), '&'], [['reqRaceMask', RACE_MASK_ALLIANCE, '&'], RACE_MASK_ALLIANCE, '!'], [['reqRaceMask', RACE_MASK_HORDE, '&'], RACE_MASK_HORDE, '!']]; - - return false; - } - - protected function cbLacksStartEnd($cr) - { - if (!$this->int2Bool($cr[1])) - return false; - - $missing = DB::Aowow()->selectCol('SELECT questId, max(method) a, min(method) b FROM ?_quests_startend GROUP BY questId HAVING (a | b) <> 3'); - if ($cr[1]) - return ['id', $missing]; - else - return ['id', $missing, '!']; - } -} - - -?> diff --git a/includes/types/skill.class.php b/includes/types/skill.class.php deleted file mode 100644 index 2aa2dd13..00000000 --- a/includes/types/skill.class.php +++ /dev/null @@ -1,81 +0,0 @@ - [['ic']], - 'ic' => ['j' => ['?_icons ic ON ic.id = sl.iconId', true], 's' => ', ic.name AS iconString'], - ); - - public function __construct($conditions = []) - { - parent::__construct($conditions); - - // post processing - foreach ($this->iterate() as &$_curTpl) - { - $_ = &$_curTpl['specializations']; // shorthand - if (!$_) - $_ = [0, 0, 0, 0, 0]; - else - { - $_ = explode(' ', $_); - while (count($_) < 5) - $_[] = 0; - } - - if (!$_curTpl['iconId']) - $_curTpl['iconString'] = 'inv_misc_questionmark'; - } - } - - public static function getName($id) - { - $n = DB::Aowow()->SelectRow('SELECT name_loc0, name_loc2, name_loc3, name_loc6, name_loc8 FROM ?_skillline WHERE id = ?d', $id); - return Util::localizedString($n, 'name'); - } - - public function getListviewData() - { - $data = []; - - foreach ($this->iterate() as $__) - { - $data[$this->id] = array( - 'category' => $this->curTpl['typeCat'], - 'categorybak' => $this->curTpl['categoryId'], - 'id' => $this->id, - 'name' => Util::jsEscape($this->getField('name', true)), - 'profession' => $this->curTpl['professionMask'], - 'recipeSubclass' => $this->curTpl['recipeSubClass'], - 'specializations' => Util::toJSON($this->curTpl['specializations'], JSON_NUMERIC_CHECK), - 'icon' => Util::jsEscape($this->curTpl['iconString']) - ); - } - - return $data; - } - - public function getJSGlobals($addMask = 0) - { - $data = []; - - foreach ($this->iterate() as $__) - $data[self::$type][$this->id] = ['name' => Util::jsEscape($this->getField('name', true)), 'icon' => Util::jsEscape($this->curTpl['iconString'])]; - - return $data; - } - - public function renderTooltip() { } -} - -?> diff --git a/includes/types/sound.class.php b/includes/types/sound.class.php deleted file mode 100644 index 86001e76..00000000 --- a/includes/types/sound.class.php +++ /dev/null @@ -1,136 +0,0 @@ - 'audio/ogg; codecs="vorbis"', - SOUND_TYPE_MP3 => 'audio/mpeg' - ); - - public function __construct($conditions = []) - { - parent::__construct($conditions); - - // post processing - foreach ($this->iterate() as $id => &$_curTpl) - { - $_curTpl['files'] = []; - for ($i = 1; $i < 11; $i++) - { - if ($_curTpl['soundFile'.$i]) - { - $this->fileBuffer[$_curTpl['soundFile'.$i]] = null; - $_curTpl['files'][] = &$this->fileBuffer[$_curTpl['soundFile'.$i]]; - } - - unset($_curTpl['soundFile'.$i]); - } - } - - if ($this->fileBuffer) - { - $files = DB::Aowow()->select('SELECT id AS ARRAY_KEY, `id`, `file` AS title, `type` FROM ?_sounds_files sf WHERE id IN (?a)', array_keys($this->fileBuffer)); - foreach ($files as $id => $data) - { - // skipp file extension - $data['title'] = substr($data['title'], 0, -4); - // enum to string - $data['type'] = self::$fileTypes[$data['type']]; - // get real url - $data['url'] = STATIC_URL . '/wowsounds/' . $data['id']; - // v push v - $this->fileBuffer[$id] = $data; - } - } - } - - public static function getName($id) - { - $this->getEntry($id); - - return $this->getField('name'); - } - - public function getListviewData() - { - $data = []; - - foreach ($this->iterate() as $__) - { - $data[$this->id] = array( - 'id' => $this->id, - 'type' => $this->getField('cat'), - 'name' => $this->getField('name'), - 'files' => array_values(array_filter($this->getField('files'))) - ); - } - - return $data; - } - - public function getJSGlobals($addMask = 0) - { - $data = []; - - foreach ($this->iterate() as $__) - $data[self::$type][$this->id] = array( - 'name' => Util::jsEscape($this->getField('name', true)), - 'type' => $this->getField('cat'), - 'files' => array_values(array_filter($this->getField('files'))) - ); - - return $data; - } - - public function renderTooltip() { } -} - -class SoundListFilter extends Filter -{ - // we have no criteria for this one... - protected function createSQLForCriterium(&$cr) - { - unset($cr); - $this->error = true; - return [1]; - } - - // fieldId => [checkType, checkValue[, fieldIsArray]] - protected $inputFields = array( - 'na' => [FILTER_V_REGEX, '/[\p{C};]/ui', false], // name - only printable chars, no delimiter - 'ty' => [FILTER_V_LIST, [[1, 4], 6, 9, 10, 12, 13, 14, 16, 17, [19, 23], [25, 31], 50, 52, 53], true ] // type - ); - - protected function createSQLForValues() - { - $parts = []; - $_v = &$this->fiData['v']; - - // name [str] - if (isset($_v['na'])) - if ($_ = $this->modularizeString(['name'])) - $parts[] = $_; - - // type [list] - if (isset($_v['ty'])) - $parts[] = ['cat', $_v['ty']]; - - return $parts; - } -} - -?> diff --git a/includes/types/spell.class.php b/includes/types/spell.class.php deleted file mode 100644 index 9bc9cb4a..00000000 --- a/includes/types/spell.class.php +++ /dev/null @@ -1,2542 +0,0 @@ - [ 43, 44, 45, 46, 54, 55, 95, 118, 136, 160, 162, 172, 173, 176, 226, 228, 229, 473], // Weapons - 8 => [293, 413, 414, 415, 433], // Armor - 9 => [129, 185, 356, 762], // sec. Professions - 10 => [ 98, 109, 111, 113, 115, 137, 138, 139, 140, 141, 313, 315, 673, 759], // Languages - 11 => [164, 165, 171, 182, 186, 197, 202, 333, 393, 755, 773] // prim. Professions - ); - - public static $spellTypes = array( - 6 => 1, - 8 => 2, - 10 => 4 - ); - - public static $effects = array( - 'heal' => [ 0,/*3,*/10, 67, 75, 136 ], // , Dummy, Heal, Heal Max Health, Heal Mechanical, Heal Percent - 'damage' => [ 0, 2, 3, 9, 62 ], // , Dummy, School Damage, Health Leech, Power Burn - 'itemCreate' => [24, 34, 59, 66, 157 ], // createItem, changeItem, randomItem, createManaGem, createItem2 - 'trigger' => [ 3, 32, 64, 101, 142, 148, 151, 152, 155, 160, 164], // dummy, trigger missile, trigger spell, feed pet, force cast, force cast with value, unk, trigger spell 2, unk, dualwield 2H, unk, remove aura - 'teach' => [36, 57, /*133*/ ] // learn spell, learn pet spell, /*unlearn specialization*/ - ); - - public static $auras = array( - 'heal' => [ 4, 8, 62, 69, 97, 226 ], // Dummy, Periodic Heal, Periodic Health Funnel, School Absorb, Mana Shield, Periodic Dummy - 'damage' => [ 3, 4, 15, 53, 89, 162, 226 ], // Periodic Damage, Dummy, Damage Shield, Periodic Health Leech, Periodic Damage Percent, Power Burn Mana, Periodic Dummy - 'itemCreate' => [86 ], // Channel Death Item - 'trigger' => [ 4, 23, 42, 48, 109, 226, 227, 231, 236, 284 ], // dummy; 23/227: periodic trigger spell (with value); 42/231: proc trigger spell (with value); 48: unk; 109: add target trigger; 226: periodic dummy; 236: control vehicle; 284: linked - 'teach' => [ ] - ); - - private $spellVars = []; - private $refSpells = []; - private $tools = []; - private $interactive = false; - private $charLevel = MAX_LEVEL; - - protected $queryBase = 'SELECT s.*, s.id AS ARRAY_KEY FROM ?_spell s'; - protected $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_loc6 AS rangeText_loc6, sr.name_loc8 AS rangeText_loc8'], - 'src' => ['j' => ['?_source src ON type = 6 AND typeId = s.id', 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($conditions = []) - { - parent::__construct($conditions); - - if ($this->error) - return; - - // post processing - $foo = DB::World()->selectCol('SELECT perfectItemType FROM skill_perfect_item_template WHERE spellId IN (?a)', $this->getFoundIDs()); - foreach ($this->iterate() as &$_curTpl) - { - // required for globals - if ($idx = $this->canCreateItem()) - foreach ($idx as $i) - $foo[] = (int)$_curTpl['effect'.$i.'CreateItemId']; - - for ($i = 1; $i <= 8; $i++) - if ($_curTpl['reagent'.$i] > 0) - $foo[] = (int)$_curTpl['reagent'.$i]; - - for ($i = 1; $i <= 2; $i++) - if ($_curTpl['tool'.$i] > 0) - $foo[] = (int)$_curTpl['tool'.$i]; - - // ranks - $this->ranks[$this->id] = $this->getField('rank', true); - - // sources - for ($i = 1; $i < 25; $i++) - { - if ($_ = $_curTpl['src'.$i]) - $this->sources[$this->id][$i][] = $_; - - unset($_curTpl['src'.$i]); - } - - // set full masks to 0 - $_curTpl['reqClassMask'] &= CLASS_MASK_ALL; - if ($_curTpl['reqClassMask'] == CLASS_MASK_ALL) - $_curTpl['reqClassMask'] = 0; - - $_curTpl['reqRaceMask'] &= RACE_MASK_ALL; - if ($_curTpl['reqRaceMask'] == RACE_MASK_ALL) - $_curTpl['reqRaceMask'] = 0; - - // unpack skillLines - $_curTpl['skillLines'] = []; - if ($_curTpl['skillLine1'] < 0) - { - foreach (Game::$skillLineMask[$_curTpl['skillLine1']] as $idx => $pair) - if ($_curTpl['skillLine2OrMask'] & (1 << $idx)) - $_curTpl['skillLines'][] = $pair[1]; - } - else if ($sec = $_curTpl['skillLine2OrMask']) - { - if ($this->id == 818) // and another hack .. basic Campfire (818) has deprecated skill Survival (142) as first skillLine - $_curTpl['skillLines'] = [$sec, $_curTpl['skillLine1']]; - else - $_curTpl['skillLines'] = [$_curTpl['skillLine1'], $sec]; - } - else if ($prim = $_curTpl['skillLine1']) - $_curTpl['skillLines'] = [$prim]; - - unset($_curTpl['skillLine1']); - unset($_curTpl['skillLine2OrMask']); - - if (!$_curTpl['iconString']) - $_curTpl['iconString'] = 'inv_misc_questionmark'; - } - - if ($foo) - $this->relItems = new ItemList(array(['i.id', array_unique($foo)], CFG_SQL_LIMIT_NONE)); - } - - // use if you JUST need the name - public static function getName($id) - { - $n = DB::Aowow()->SelectRow('SELECT name_loc0, name_loc2, name_loc3, name_loc6, name_loc8 FROM ?_spell WHERE id = ?d', $id ); - return Util::localizedString($n, 'name'); - } - // end static use - - // required for item-comparison - public function getStatGain() - { - $data = []; - - foreach ($this->iterate() as $__) - { - $stats = []; - - for ($i = 1; $i <= 3; $i++) - { - $pts = $this->calculateAmountForCurrent($i)[1]; - $mv = $this->curTpl['effect'.$i.'MiscValue']; - $au = $this->curTpl['effect'.$i.'AuraId']; - - // Enchant Item Permanent (53) / Temporary (54) - if (in_array($this->curTpl['effect'.$i.'Id'], [53, 54])) - { - if ($mv && ($json = DB::Aowow()->selectRow('SELECT * FROM ?_item_stats WHERE `type` = ?d AND `typeId` = ?d', TYPE_ENCHANTMENT, $mv))) - { - $mods = []; - foreach ($json as $str => $val) - if ($val && ($idx = array_search($str, Game::$itemMods))) - $mods[$idx] = $val; - - if ($mods) - Util::arraySumByKey($stats, $mods); - } - - continue; - } - - switch ($au) - { - case 29: // ModStat MiscVal:type - if ($mv < 0) // all stats - { - for ($iMod = ITEM_MOD_AGILITY; $iMod <= ITEM_MOD_STAMINA; $iMod++) - Util::arraySumByKey($stats, [$iMod => $pts]); - } - else if ($mv == STAT_STRENGTH) // one stat - Util::arraySumByKey($stats, [ITEM_MOD_STRENGTH => $pts]); - else if ($mv == STAT_AGILITY) - Util::arraySumByKey($stats, [ITEM_MOD_AGILITY => $pts]); - else if ($mv == STAT_STAMINA) - Util::arraySumByKey($stats, [ITEM_MOD_STAMINA => $pts]); - else if ($mv == STAT_INTELLECT) - Util::arraySumByKey($stats, [ITEM_MOD_INTELLECT => $pts]); - else if ($mv == STAT_SPIRIT) - Util::arraySumByKey($stats, [ITEM_MOD_SPIRIT => $pts]); - else // one bullshit - trigger_error('AuraId 29 of spell #'.$this->id.' has wrong statId #'.$mv, E_USER_WARNING); - - break; - case 34: // Increase Health - case 230: - case 250: - Util::arraySumByKey($stats, [ITEM_MOD_HEALTH => $pts]); - break; - case 13: // damage splpwr + physical (dmg & any) - // + weapon damage - if ($mv == (1 << SPELL_SCHOOL_NORMAL)) - { - Util::arraySumByKey($stats, [ITEM_MOD_WEAPON_DMG => $pts]); - break; - } - - // full magic mask, also counts towards healing - if ($mv == SPELL_MAGIC_SCHOOLS) - { - Util::arraySumByKey($stats, [ITEM_MOD_SPELL_POWER => $pts]); - Util::arraySumByKey($stats, [ITEM_MOD_SPELL_DAMAGE_DONE => $pts]); - } - else - { - // HolySpellpower (deprecated; still used in randomproperties) - if ($mv & (1 << SPELL_SCHOOL_HOLY)) - Util::arraySumByKey($stats, [ITEM_MOD_HOLY_POWER => $pts]); - - // FireSpellpower (deprecated; still used in randomproperties) - if ($mv & (1 << SPELL_SCHOOL_FIRE)) - Util::arraySumByKey($stats, [ITEM_MOD_FIRE_POWER => $pts]); - - // NatureSpellpower (deprecated; still used in randomproperties) - if ($mv & (1 << SPELL_SCHOOL_NATURE)) - Util::arraySumByKey($stats, [ITEM_MOD_NATURE_POWER => $pts]); - - // FrostSpellpower (deprecated; still used in randomproperties) - if ($mv & (1 << SPELL_SCHOOL_FROST)) - Util::arraySumByKey($stats, [ITEM_MOD_FROST_POWER => $pts]); - - // ShadowSpellpower (deprecated; still used in randomproperties) - if ($mv & (1 << SPELL_SCHOOL_SHADOW)) - Util::arraySumByKey($stats, [ITEM_MOD_SHADOW_POWER => $pts]); - - // ArcaneSpellpower (deprecated; still used in randomproperties) - if ($mv & (1 << SPELL_SCHOOL_ARCANE)) - Util::arraySumByKey($stats, [ITEM_MOD_ARCANE_POWER => $pts]); - } - - break; - case 135: // healing splpwr (healing & any) .. not as a mask.. - Util::arraySumByKey($stats, [ITEM_MOD_SPELL_HEALING_DONE => $pts]); - break; - case 35: // ModPower - MiscVal:type see defined Powers only energy/mana in use - if ($mv == POWER_HEALTH) - Util::arraySumByKey($stats, [ITEM_MOD_HEALTH => $pts]); - else if ($mv == POWER_ENERGY) - Util::arraySumByKey($stats, [ITEM_MOD_ENERGY => $pts]); - else if ($mv == POWER_RAGE) - Util::arraySumByKey($stats, [ITEM_MOD_RAGE => $pts]); - else if ($mv == POWER_MANA) - Util::arraySumByKey($stats, [ITEM_MOD_MANA => $pts]); - else if ($mv == POWER_RUNIC_POWER) - Util::arraySumByKey($stats, [ITEM_MOD_RUNIC_POWER => $pts]); - - break; - case 189: // CombatRating MiscVal:ratingMask - case 220: - if ($mod = Game::itemModByRatingMask($mv)) - Util::arraySumByKey($stats, [$mod => $pts]); - break; - case 143: // Resistance MiscVal:school - case 83: - case 22: - if ($mv == 1) // Armor only if explicitly specified - { - Util::arraySumByKey($stats, [ITEM_MOD_ARMOR => $pts]); - break; - } - - if ($mv == 2) // holy-resistance ONLY if explicitly specified (shouldn't even exist...) - { - Util::arraySumByKey($stats, [ITEM_MOD_HOLY_RESISTANCE => $pts]); - break; - } - - for ($j = 0; $j < 7; $j++) - { - if (($mv & (1 << $j)) == 0) - continue; - - switch ($j) - { - case 2: - Util::arraySumByKey($stats, [ITEM_MOD_FIRE_RESISTANCE => $pts]); - break; - case 3: - Util::arraySumByKey($stats, [ITEM_MOD_NATURE_RESISTANCE => $pts]); - break; - case 4: - Util::arraySumByKey($stats, [ITEM_MOD_FROST_RESISTANCE => $pts]); - break; - case 5: - Util::arraySumByKey($stats, [ITEM_MOD_SHADOW_RESISTANCE => $pts]); - break; - case 6: - Util::arraySumByKey($stats, [ITEM_MOD_ARCANE_RESISTANCE => $pts]); - break; - } - } - break; - case 8: // hp5 - case 84: - case 161: - Util::arraySumByKey($stats, [ITEM_MOD_HEALTH_REGEN => $pts]); - break; - case 85: // mp5 - Util::arraySumByKey($stats, [ITEM_MOD_MANA_REGENERATION => $pts]); - break; - case 99: // atkpwr - Util::arraySumByKey($stats, [ITEM_MOD_ATTACK_POWER => $pts]); - break; // ?carries over to rngatkpwr? - case 124: // rngatkpwr - Util::arraySumByKey($stats, [ITEM_MOD_RANGED_ATTACK_POWER => $pts]); - break; - case 158: // blockvalue - Util::arraySumByKey($stats, [ITEM_MOD_BLOCK_VALUE => $pts]); - break; - case 240: // ModExpertise - Util::arraySumByKey($stats, [ITEM_MOD_EXPERTISE_RATING => $pts]); - break; - case 123: // Mod Target Resistance - if ($mv == 0x7C && $pts < 0) - Util::arraySumByKey($stats, [ITEM_MOD_SPELL_PENETRATION => -$pts]); - break; - } - } - - $data[$this->id] = $stats; - } - - return $data; - } - - public function getProfilerMods() - { - // weapon hand check: param: slot, class, subclass, value - $whCheck = '$function() { var j, w = _inventory.getInventory()[%d]; if (!w[0] || !g_items[w[0]]) { return 0; } j = g_items[w[0]].jsonequip; return (j.classs == %d && (%d & (1 << (j.subclass)))) ? %d : 0; }'; - - $data = $this->getStatGain(); // flat gains - foreach ($data as $id => &$spellData) - { - foreach ($spellData as $modId => $val) - { - if (!isset(Game::$itemMods[$modId])) - continue; - - if ($modId == ITEM_MOD_EXPERTISE_RATING) // not a rating .. pure expertise - $spellData['exp'] = $val; - else - $spellData[Game::$itemMods[$modId]] = $val; - - unset($spellData[$modId]); - } - - // apply weapon restrictions - $this->getEntry($id); - $class = $this->getField('equippedItemClass'); - $subClass = $this->getField('equippedItemSubClassMask'); - $slot = $subClass & 0x5000C ? 18 : 16; - if ($class != ITEM_CLASS_WEAPON || !$subClass) - continue; - - foreach ($spellData as $json => $pts) - $spellData[$json] = [1, 'functionOf', sprintf($whCheck, $slot, $class, $subClass, $pts)]; - } - - // 4 possible modifiers found - // => [0.15, 'functionOf', ] - // => [0.33, 'percentOf', ] - // => [123, 'add'] - // => ... as from getStatGain() - - $modXByStat = function (&$arr, $stat, $pts) use (&$mv) - { - 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']; - }; - - $modXBySchool = function (&$arr, $stat, $val, $mask = null) use (&$mv) - { - 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]; - }; - - $jsonStat = function ($stat) - { - 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'; - }; - - foreach ($this->iterate() as $id => $__) - { - // Priest: Spirit of Redemption is a spell but also a passive. *yaaayyyy* - if (($this->getField('cuFlags') & SPELL_CU_TALENTSPELL) && $id != 20711) - continue; - - // curious cases of OH MY FUCKING GOD WHY?! - if ($id == 16268) // Shaman - Spirit Weapons (parry is normaly stored in g_statistics) - { - $data[$id]['parrypct'] = [5, 'add']; - continue; - } - - if ($id == 20550) // Tauren - Endurance (dependant on base health) ... if you are looking for something elegant, look away! - { - $data[$id]['health'] = [0.05, 'functionOf', '$function(p) { return g_statistics.combo[p.classs][p.level][5]; }']; - continue; - } - - for ($i = 1; $i < 4; $i++) - { - $pts = $this->calculateAmountForCurrent($i)[1]; - $mv = $this->getField('effect'.$i.'MiscValue'); - $mvB = $this->getField('effect'.$i.'MiscValueB'); - $au = $this->getField('effect'.$i.'AuraId'); - $class = $this->getField('equippedItemClass'); - $subClass = $this->getField('equippedItemSubClassMask'); - - - /* ISSUE! - mods formated like ['' => [, 'percentOf', '']] are applied as multiplier and not - as a flat value (that is equal to the percentage, like they should be). So the stats-table won't show the actual deficit - */ - - switch ($au) - { - case 101: // Mod Resistance Percent - case 142: // Mod Base Resistance Percent - if ($mv == 1) // Armor only if explicitly specified only affects armor from equippment - $data[$id]['armor'] = [$pts / 100, 'percentOf', ['armor', 0]]; - else if ($mv) - $modXBySchool($data[$id], 'res', $pts); - break; - case 182: // Mod Resistance Of Stat Percent - if ($mv == 1) // Armor only if explicitly specified - $data[$id]['armor'] = [$pts / 100, 'percentOf', $jsonStat($mvB)]; - else if ($mv) - $modXBySchool($data[$id], 'res', [$pts / 100, 'percentOf', $jsonStat($mvB)]); - break; - case 137: // mod stat percent - if ($mv > -1) // one stat - $modXByStat($data[$id], null, $pts); - else if ($mv < 0) // all stats - for ($iMod = ITEM_MOD_AGILITY; $iMod <= ITEM_MOD_STAMINA; $iMod++) - $data[$id][Game::$itemMods[$iMod]] = [$pts / 100, 'percentOf', Game::$itemMods[$iMod]]; - break; - case 174: // Mod Spell Damage Of Stat Percent - $mv = $mv ?: SPELL_MAGIC_SCHOOLS; - $modXBySchool($data[$id], 'spldmg', [$pts / 100, 'percentOf', $jsonStat($mvB)]); - break; - case 212: // Mod Ranged Attack Power Of Stat Percent - $modXByStat($data[$id], 'rgdatkpwr', $pts); - break; - case 268: // Mod Attack Power Of Stat Percent - $modXByStat($data[$id], 'mleatkpwr', $pts); - break; - case 175: // Mod Spell Healing Of Stat Percent - $modXByStat($data[$id], 'splheal', $pts); - break; - case 219: // Mod Mana Regeneration from Stat - $modXByStat($data[$id], 'manargn', $pts); - break; - case 134: // Mod Mana Regeneration Interrupt - $data[$id]['icmanargn'] = [$pts / 100, 'percentOf', 'oocmanargn']; - break; - case 57: // Mod Spell Crit Chance - case 71: // Mod Spell Crit Chance School - $mv = $mv ?: SPELL_MAGIC_SCHOOLS; - $modXBySchool($data[$id], 'splcritstrkpct', [$pts, 'add']); - if (($mv & SPELL_MAGIC_SCHOOLS) == SPELL_MAGIC_SCHOOLS) - $data[$id]['splcritstrkpct'] = [$pts, 'add']; - break; - case 285: // Mod Attack Power Of Armor - $data[$id]['mleatkpwr'] = [1 / $pts, 'percentOf', 'fullarmor']; - $data[$id]['rgdatkpwr'] = [1 / $pts, 'percentOf', 'fullarmor']; - break; - case 52: // Mod Physical Crit Percent - if ($class < 1 || ($class == ITEM_CLASS_WEAPON && ($subClass & 0x5000C))) - $data[$id]['rgdcritstrkpct'] = [1, 'functionOf', sprintf($whCheck, 18, $class, $subClass, $pts)]; - // $data[$id]['rgdcritstrkpct'] = [$pts, 'add']; - if ($class < 1 || ($class == ITEM_CLASS_WEAPON && ($subClass & 0xA5F3))) - $data[$id]['mlecritstrkpct'] = [1, 'functionOf', sprintf($whCheck, 16, $class, $subClass, $pts)]; - // $data[$id]['mlecritstrkpct'] = [$pts, 'add']; - break; - case 47: // Mod Parry Percent - $data[$id]['parrypct'] = [$pts, 'add']; - break; - case 49: // Mod Dodge Percent - $data[$id]['dodgepct'] = [$pts, 'add']; - break; - case 51: // Mod Block Percent - $data[$id]['blockpct'] = [$pts, 'add']; - break; - case 132: // Mod Increase Energy Percent - if ($mv == POWER_HEALTH) - $data[$id]['health'] = [$pts / 100, 'percentOf', 'health']; - else if ($mv == POWER_ENERGY) - $data[$id]['energy'] = [$pts / 100, 'percentOf', 'energy']; - else if ($mv == POWER_MANA) - $data[$id]['mana'] = [$pts / 100, 'percentOf', 'mana']; - else if ($mv == POWER_RAGE) - $data[$id]['rage'] = [$pts / 100, 'percentOf', 'rage']; - else if ($mv == POWER_RUNIC_POWER) - $data[$id]['runic'] = [$pts / 100, 'percentOf', 'runic']; - break; - case 133: // Mod Increase Health Percent - $data[$id]['health'] = [$pts / 100, 'percentOf', 'health']; - break; - case 150: // Mod Shield Blockvalue Percent - $data[$id]['block'] = [$pts / 100, 'percentOf', 'block']; - break; - case 290: // Mod Crit Percent - $data[$id]['mlecritstrkpct'] = [$pts, 'add']; - $data[$id]['rgdcritstrkpct'] = [$pts, 'add']; - $data[$id]['splcritstrkpct'] = [$pts, 'add']; - break; - case 237: // Mod Spell Damage Of Attack Power - $mv = $mv ?: SPELL_MAGIC_SCHOOLS; - $modXBySchool($data[$id], 'spldmg', [$pts / 100, 'percentOf', 'mleatkpwr']); - break; - case 238: // Mod Spell Healing Of Attack Power - $data[$id]['splheal'] = [$pts / 100, 'percentOf', 'mleatkpwr']; - break; - case 166: // Mod Attack Power Percent [ingmae only melee..?] - $data[$id]['mleatkpwr'] = [$pts / 100, 'percentOf', 'mleatkpwr']; - break; - case 88: // Mod Health Regeneration Percent - $data[$id]['healthrgn'] = [$pts / 100, 'percentOf', 'healthrgn']; - break; - } - } - } - - return $data; - } - - // halper - public function getReagentsForCurrent() - { - $data = []; - - for ($i = 1; $i <= 8; $i++) - if ($this->curTpl['reagent'.$i] > 0 && $this->curTpl['reagentCount'.$i]) - $data[$this->curTpl['reagent'.$i]] = [$this->curTpl['reagent'.$i], $this->curTpl['reagentCount'.$i]]; - - return $data; - } - - public function getToolsForCurrent() - { - if ($this->tools) - return $this->tools; - - $tools = []; - for ($i = 1; $i <= 2; $i++) - { - // TotemCategory - if ($_ = $this->curTpl['toolCategory'.$i]) - { - $tc = DB::Aowow()->selectRow('SELECT * FROM ?_totemcategory WHERE id = ?d', $_); - $tools[$i + 1] = array( - 'id' => $_, - 'name' => Util::localizedString($tc, 'name')); - } - - // Tools - if (!$this->curTpl['tool'.$i]) - continue; - - foreach ($this->relItems->iterate() as $relId => $__) - { - if ($relId != $this->curTpl['tool'.$i]) - continue; - - $tools[$i - 1] = array( - 'itemId' => $relId, - 'name' => $this->relItems->getField('name', true), - 'quality' => $this->relItems->getField('quality') - ); - - break; - } - } - - $this->tools = array_reverse($tools); - - return $this->tools; - } - - public function getModelInfo($spellId = 0, $effIdx = 0) - { - $displays = [0 => []]; - foreach ($this->iterate() as $id => $__) - { - if ($spellId && $spellId != $id) - continue; - - for ($i = 1; $i < 4; $i++) - { - $effMV = $this->curTpl['effect'.$i.'MiscValue']; - if (!$effMV) - continue; - - // GO Model from MiscVal - if (in_array($this->curTpl['effect'.$i.'Id'], [50, 76, 104, 105, 106, 107])) - { - if (isset($displays[TYPE_OBJECT][$id])) - $displays[TYPE_OBJECT][$id][0][] = $i; - else - $displays[TYPE_OBJECT][$id] = [[$i], $effMV]; - } - // NPC Model from MiscVal - else if (in_array($this->curTpl['effect'.$i.'Id'], [28, 90, 134]) || in_array($this->curTpl['effect'.$i.'AuraId'], [56, 78])) - { - if (isset($displays[TYPE_NPC][$id])) - $displays[TYPE_NPC][$id][0][] = $i; - else - $displays[TYPE_NPC][$id] = [[$i], $effMV]; - } - // Shapeshift - else if ($this->curTpl['effect'.$i.'AuraId'] == 36) - { - $subForms = array( - 892 => [892, 29407, 29406, 29408, 29405], // Cat - NE - 8571 => [8571, 29410, 29411, 29412], // Cat - Tauren - 2281 => [2281, 29413, 29414, 29416, 29417], // Bear - NE - 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)) - { - foreach ([1, 2] as $j) - if (isset($subForms[$st['model'.$j]])) - $st['model'.$j] = $subForms[$st['model'.$j]][array_rand($subForms[$st['model'.$j]])]; - - $displays[0][$id][$i] = array( - 'typeId' => 0, - 'displayId' => $st['model2'] ? $st['model'.rand(1, 2)] : $st['model1'], - 'creatureType' => $st['creatureType'], - 'displayName' => Lang::game('st', $effMV) - ); - } - } - } - } - - $results = $displays[0]; - - if (!empty($displays[TYPE_NPC])) - { - $nModels = new CreatureList(array(['id', array_column($displays[TYPE_NPC], 1)])); - foreach ($nModels->iterate() as $nId => $__) - { - $srcId = 0; - foreach ($displays[TYPE_NPC] as $srcId => $set) - if ($set[1] == $nId) - break; - - foreach ($set[0] as $idx) - { - $results[$srcId][$idx] = array( - 'typeId' => $nId, - 'displayId' => $nModels->getRandomModelId(), - 'displayName' => $nModels->getField('name', true) - ); - } - } - } - - if (!empty($displays[TYPE_OBJECT])) - { - $oModels = new GameObjectList(array(['id', array_column($displays[TYPE_OBJECT], 1)])); - foreach ($oModels->iterate() as $oId => $__) - { - $srcId = 0; - foreach ($displays[TYPE_OBJECT] as $srcId => $set) - if ($set[1] == $oId) - break; - - foreach ($set[0] as $idx) - { - $results[$srcId][$idx] = array( - 'typeId' => $oId, - 'displayId' => $oModels->getField('displayId'), - 'displayName' => $oModels->getField('name', true) - ); - } - } - } - - if ($spellId && $effIdx) - return !empty($results[$spellId][$effIdx]) ? $results[$spellId][$effIdx] : 0; - - return $results; - } - - private function createRangesForCurrent() - { - if (!$this->curTpl['rangeMaxHostile']) - return ''; - - // minRange exists; show as range - if ($this->curTpl['rangeMinHostile']) - return sprintf(Lang::spell('range'), $this->curTpl['rangeMinHostile'].' - '.$this->curTpl['rangeMaxHostile']); - // friend and hostile differ; do color - else if ($this->curTpl['rangeMaxHostile'] != $this->curTpl['rangeMaxFriend']) - return sprintf(Lang::spell('range'), ''.$this->curTpl['rangeMaxHostile'].' - '.$this->curTpl['rangeMaxFriend']. ''); - // hardcode: "melee range" - else if ($this->curTpl['rangeMaxHostile'] == 5) - return Lang::spell('meleeRange'); - // hardcode "unlimited range" - else if ($this->curTpl['rangeMaxHostile'] == 50000) - return Lang::spell('unlimRange'); - // regular case - else - return sprintf(Lang::spell('range'), $this->curTpl['rangeMaxHostile']); - } - - public function createPowerCostForCurrent() - { - $str = ''; - - // check for custom PowerDisplay - $pt = $this->curTpl['powerType']; - - if ($pt == POWER_RUNE && ($rCost = ($this->curTpl['powerCostRunes'] & 0x333))) - { // Blood 2|1 - Unholy 2|1 - Frost 2|1 - $runes = []; - if ($_ = (($rCost & 0x300) >> 8)) - $runes[] = $_.' '.Lang::spell('powerRunes', 2); - if ($_ = (($rCost & 0x030) >> 4)) - $runes[] = $_.' '.Lang::spell('powerRunes', 1); - if ($_ = ($rCost & 0x003)) - $runes[] = $_.' '.Lang::spell('powerRunes', 0); - - $str .= implode(', ', $runes); - } - else if ($this->curTpl['powerCostPercent'] > 0) // power cost: pct over static - $str .= $this->curTpl['powerCostPercent']."% ".sprintf(Lang::spell('pctCostOf'), strtolower(Lang::spell('powerTypes', $pt))); - else if ($this->curTpl['powerCost'] > 0 || $this->curTpl['powerPerSecond'] > 0 || $this->curTpl['powerCostPerLevel'] > 0) - $str .= ($pt == POWER_RAGE || $pt == POWER_RUNIC_POWER ? $this->curTpl['powerCost'] / 10 : $this->curTpl['powerCost']).' '.Util::ucFirst(Lang::spell('powerTypes', $pt)); - - // append periodic cost - if ($this->curTpl['powerPerSecond'] > 0) - $str .= sprintf(Lang::spell('costPerSec'), $this->curTpl['powerPerSecond']); - - // append level cost (todo (low): work in as scaling cost) - if ($this->curTpl['powerCostPerLevel'] > 0) - $str .= sprintf(Lang::spell('costPerLevel'), $this->curTpl['powerCostPerLevel']); - - return $str; - } - - public function createCastTimeForCurrent($short = true, $noInstant = true) - { - if ($this->isChanneledSpell()) - return Lang::spell('channeled'); - else if ($this->curTpl['castTime'] > 0) - return $short ? sprintf(Lang::spell('castIn'), $this->curTpl['castTime'] / 1000) : Util::formatTime($this->curTpl['castTime']); - // show instant only for player/pet/npc abilities (todo (low): unsure when really hidden (like talent-case)) - else if ($noInstant && !in_array($this->curTpl['typeCat'], [11, 7, -3, -6, -8, 0]) && !($this->curTpl['cuFlags'] & SPELL_CU_TALENTSPELL)) - return ''; - // SPELL_ATTR0_ABILITY instant ability.. yeah, wording thing only (todo (low): rule is imperfect) - else if ($this->curTpl['damageClass'] != 1 || $this->curTpl['attributes0'] & 0x10) - return Lang::spell('instantPhys'); - else // instant cast - return Lang::spell('instantMagic'); - } - - private function createCooldownForCurrent() - { - if ($this->curTpl['recoveryTime']) - return sprintf(Lang::game('cooldown'), Util::formatTime($this->curTpl['recoveryTime'], true)); - else if ($this->curTpl['recoveryCategory']) - return sprintf(Lang::game('cooldown'), Util::formatTime($this->curTpl['recoveryCategory'], true)); - else - return ''; - } - - // formulae base from TC - private function calculateAmountForCurrent($effIdx, $altTpl = null) - { - $ref = $altTpl ? $altTpl : $this; - $level = $this->charLevel; - $rppl = $ref->getField('effect'.$effIdx.'RealPointsPerLevel'); - $base = $ref->getField('effect'.$effIdx.'BasePoints'); - $add = $ref->getField('effect'.$effIdx.'DieSides'); - $maxLvl = $ref->getField('maxLevel'); - $baseLvl = $ref->getField('baseLevel'); - - if ($rppl) - { - if ($level > $maxLvl && $maxLvl > 0) - $level = $maxLvl; - else if ($level < $baseLvl) - $level = $baseLvl; - - if (!$ref->getField('atributes0') & 0x40) // SPELL_ATTR0_PASSIVE - $level -= $ref->getField('spellLevel'); - - $base += (int)($level * $rppl); - } - - return [ - $add ? $base + 1 : $base, - $base + $add, - $rppl ? '' : null, - $rppl ? '' : null - ]; - } - - public function canCreateItem() - { - $idx = []; - for ($i = 1; $i < 4; $i++) - if (in_array($this->curTpl['effect'.$i.'Id'], SpellList::$effects['itemCreate']) || in_array($this->curTpl['effect'.$i.'AuraId'], SpellList::$auras['itemCreate'])) - if ($this->curTpl['effect'.$i.'CreateItemId'] > 0) - $idx[] = $i; - - return $idx; - } - - public function canTriggerSpell() - { - $idx = []; - for ($i = 1; $i < 4; $i++) - if (in_array($this->curTpl['effect'.$i.'Id'], SpellList::$effects['trigger']) || in_array($this->curTpl['effect'.$i.'AuraId'], SpellList::$auras['trigger'])) - if ($this->curTpl['effect'.$i.'TriggerSpell'] > 0 || ($this->curTpl['effect'.$i.'Id'] == 155 && $this->curTpl['effect'.$i.'MiscValue'] > 0)) - $idx[] = $i; - - return $idx; - } - - public function canTeachSpell() - { - $idx = []; - for ($i = 1; $i < 4; $i++) - if (in_array($this->curTpl['effect'.$i.'Id'], SpellList::$effects['teach']) || in_array($this->curTpl['effect'.$i.'AuraId'], SpellList::$auras['teach'])) - if ($this->curTpl['effect'.$i.'TriggerSpell'] > 0) - $idx[] = $i; - - return $idx; - } - - public function isChanneledSpell() - { - return $this->curTpl['attributes1'] & 0x44; - } - - public function isHealingSpell() - { - 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; - - return true; - } - - public function isDamagingSpell() - { - 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; - - return true; - } - - public function periodicEffectsMask() - { - $effMask = 0x0; - - for ($i = 1; $i < 4; $i++) - if ($this->curTpl['effect'.$i.'Periode'] > 0) - $effMask |= 1 << ($i - 1); - - return $effMask; - } - - // description-, buff-parsing component - private function resolveEvaluation($formula) - { - // 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'); - - // only 'ron test spell', guess its %-dmg mod; no idea what bc2 might be - $pa = '<$PctArcane>'; // %arcane - $pfi = '<$PctFire>'; // %fire - $pfr = '<$PctFrost>'; // %frost - $ph = '<$PctHoly>'; // %holy - $pn = '<$PctNature>'; // %nature - $ps = '<$PctShadow>'; // %shadow - $pbh = '<$PctHeal>'; // %heal - $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'; - - $cond = $COND = function($a, $b, $c) { return $a ? $b : $c; }; - $eq = $EQ = function($a, $b) { return $a == $b; }; - $gt = $GT = function($a, $b) { return $a > $b; }; - $gte = $GTE = function($a, $b) { return $a <= $b; }; - $floor = $FLOOR = function($a) { return floor($a); }; - $max = $MAX = function($a, $b) { return max($a, $b); }; - $min = $MIN = function($a, $b) { return min($a, $b); }; - - if (preg_match_all('/\$\w+\b/i', $formula, $vars)) - { - - $evalable = true; - - foreach ($vars[0] as $var) // oh lord, forgive me this sin .. but is_callable seems to bug out and function_exists doesn't find lambda-functions >.< - { - $var = substr($var, 1); - - if (isset($$var)) - { - $eval = eval('return @$'.$var.';'); // attention: error suppression active here (will be logged anyway) - if (getType($eval) == 'object') - continue; - else if (is_numeric($eval)) - continue; - } - else - $$var = ''; - - $evalable = false; - break; - } - - if (!$evalable) - { - // can't eval constructs because of strings present. replace constructs with strings - $cond = $COND = !$this->interactive ? 'COND' : sprintf(Util::$dfnString, 'COND(a, b, c)
a ? b : c', 'COND'); - $eq = $EQ = !$this->interactive ? 'EQ' : sprintf(Util::$dfnString, 'EQ(a, b)
a == b', 'EQ'); - $gt = $GT = !$this->interactive ? 'GT' : sprintf(Util::$dfnString, 'GT(a, b)
a > b', 'GT'); - $gte = $GTE = !$this->interactive ? 'GTE' : sprintf(Util::$dfnString, 'GTE(a, b)
a <= b', 'GT'); - $floor = $FLOOR = !$this->interactive ? 'FLOOR' : sprintf(Util::$dfnString, 'FLOOR(a)', 'FLOOR'); - $min = $MIN = !$this->interactive ? 'MIN' : sprintf(Util::$dfnString, 'MIN(a, b)', 'MIN'); - $max = $MAX = !$this->interactive ? 'MAX' : sprintf(Util::$dfnString, 'MAX(a, b)', 'MAX'); - $pl = $PL = !$this->interactive ? 'PL' : sprintf(Util::$dfnString, 'LANG.level', 'PL'); - - // note the " ! - return eval('return "'.$formula.'";'); - } - else - return eval('return '.$formula.';'); - } - - // since this function may be called recursively, there are cases, where the already evaluated string is tried to be evaled again, throwing parse errors - // todo (med): also quit, if we replaced vars with non-interactive text - if (strstr($formula, '') || strstr($formula, '%s (%s)'; - $result[4] = $rType; - } - - $result[0] = $base; - break; - case 'n': // ProcCharges - case 'N': - $base = $srcSpell->getField('procCharges'); - - if (in_array($op, $signs) && is_numeric($oparg) && is_numeric($base)) - eval("\$base = $base $op $oparg;"); - - $result[0] = $base; - break; - case 'o': // TotalAmount for periodic auras (with variance) - case 'O': - list($min, $max, $modStrMin, $modStrMax) = $this->calculateAmountForCurrent($effIdx, $srcSpell); - $periode = $srcSpell->getField('effect'.$effIdx.'Periode'); - $duration = $srcSpell->getField('duration'); - - if (!$periode) - { - // Mod Power Regeneration & Mod Health Regeneration have an implicit periode of 5sec - $aura = $srcSpell->getField('effect'.$effIdx.'AuraId'); - if ($aura == 84 || $aura == 85) - $periode = 5000; - else - $periode = 3000; - } - - $min *= $duration / $periode; - $max *= $duration / $periode; - - if (in_array($op, $signs) && is_numeric($oparg)) - { - eval("\$min = $min $op $oparg;"); - eval("\$max = $max $op $oparg;"); - } - - if ($this->interactive) - { - $result[2] = $modStrMin.'%s'; - $result[3] = $modStrMax.'%s'; - } - - $result[0] = $min; - $result[1] = $max; - break; - case 'q': // EffectMiscValue - case 'Q': - $base = $srcSpell->getField('effect'.$effIdx.'MiscValue'); - - if (in_array($op, $signs) && is_numeric($oparg) && is_numeric($base)) - eval("\$base = $base $op $oparg;"); - - $result[0] = $base; - break; - case 'r': // SpellRange - case 'R': - $base = $srcSpell->getField('rangeMaxHostile'); - - if (in_array($op, $signs) && is_numeric($oparg) && is_numeric($base)) - eval("\$base = $base $op $oparg;"); - - $result[0] = $base; - break; - case 's': // BasePoints (with variance) - case 'S': - list($min, $max, $modStrMin, $modStrMax) = $this->calculateAmountForCurrent($effIdx, $srcSpell); - $mv = $srcSpell->getField('effect'.$effIdx.'MiscValue'); - $aura = $srcSpell->getField('effect'.$effIdx.'AuraId'); - - if (in_array($op, $signs) && is_numeric($oparg)) - { - eval("\$min = $min $op $oparg;"); - eval("\$max = $max $op $oparg;"); - } - // Aura giving combat ratings - $rType = 0; - if ($aura == 189) - if ($rType = Game::itemModByRatingMask($mv)) - $usesScalingRating = true; - // Aura end - - if ($rType) - { - $result[2] = '%s (%s)'; - $result[4] = $rType; - } - else if ($aura == 189 && $this->interactive) - { - $result[2] = $modStrMin.'%s'; - $result[3] = $modStrMax.'%s'; - } - - $result[0] = $min; - $result[1] = $max; - break; - case 't': // Periode - case 'T': - $base = $srcSpell->getField('effect'.$effIdx.'Periode') / 1000; - - if (in_array($op, $signs) && is_numeric($oparg) && is_numeric($base)) - eval("\$base = $base $op $oparg;"); - - $result[0] = $base; - break; - case 'u': // StackCount - case 'U': - $base = $srcSpell->getField('stackAmount'); - - if (in_array($op, $signs) && is_numeric($oparg) && is_numeric($base)) - eval("\$base = $base $op $oparg;"); - - $result[0] = $base; - break; - case 'v': // MaxTargetLevel - case 'V': - $base = $srcSpell->getField('MaxTargetLevel'); - - if (in_array($op, $signs) && is_numeric($oparg) && is_numeric($base)) - eval("\$base = $base $op $oparg;"); - - $result[0] = $base; - break; - case 'x': // ChainTargetCount - case 'X': - $base = $srcSpell->getField('effect'.$effIdx.'ChainTarget'); - - if (in_array($op, $signs) && is_numeric($oparg) && is_numeric($base)) - eval("\$base = $base $op $oparg;"); - - $result[0] = $base; - break; - case 'z': // HomeZone - $result[2] = Lang::spell('home'); - break; - } - - // handle excessively precise floats - if (is_float($result[0])) - $result[0] = round($result[0], 2); - if (isset($result[1]) && is_float($result[1])) - $result[1] = round($result[1], 2); - - return $result; - } - - // description-, buff-parsing component - private function resolveFormulaString($formula, $precision = 0, &$scaling) - { - $fSuffix = '%s'; - $fRating = 0; - - // step 1: formula unpacking redux - while (($formStartPos = strpos($formula, '${')) !== false) - { - $formBrktCnt = 0; - $formPrecision = 0; - $formCurPos = $formStartPos; - - $formOutStr = ''; - - while ($formCurPos <= strlen($formula)) - { - $char = $formula[$formCurPos]; - - if ($char == '}') - $formBrktCnt--; - - if ($formBrktCnt) - $formOutStr .= $char; - - if ($char == '{') - $formBrktCnt++; - - if (!$formBrktCnt && $formCurPos != $formStartPos) - break; - - $formCurPos++; - } - - if (isset($formula[++$formCurPos]) && $formula[$formCurPos] == '.') - { - $formPrecision = (int)$formula[++$formCurPos]; - ++$formCurPos; // for some odd reason the precision decimal survives if we dont increment further.. - } - - list($formOutStr, $fSuffix, $fRating) = $this->resolveFormulaString($formOutStr, $formPrecision, $scaling); - - $formula = substr_replace($formula, $formOutStr, $formStartPos, ($formCurPos - $formStartPos)); - } - - // note: broken tooltip on this one - // ${58644m1/-10} gets matched as a formula (ok), 58644m1 has no $ prefixed (not ok) - // the client scraps the m1 and prints -5864 - if ($this->id == 58644) - $formula = '$'.$formula; - - // step 2: resolve variables - $pos = 0; // continue strpos-search from this offset - $str = ''; - while (($npos = strpos($formula, '$', $pos)) !== false) - { - if ($npos != $pos) - $str .= substr($formula, $pos, $npos - $pos); - - $pos = $npos++; - - if ($formula[$pos] == '$') - $pos++; - - $varParts = $this->matchVariableString(substr($formula, $pos), $len); - if (!$varParts) - { - $str .= '#'; // mark as done, reset below - continue; - } - - $pos += $len; - - // we are resolving a formula -> omit ranges - $var = $this->resolveVariableString($varParts, $scaling); - - // time within formula -> rebase to seconds and omit timeUnit - if (strtolower($varParts['var']) == 'd') - { - $var[0] /= 1000; - unset($var[2]); - } - - $str .= $var[0]; - - // overwrite eventually inherited strings - if (isset($var[2])) - $fSuffix = $var[2]; - - // overwrite eventually inherited ratings - if (isset($var[4])) - $fRating = $var[4]; - } - $str .= substr($formula, $pos); - $str = str_replace('#', '$', $str); // reset marks - - // step 3: try to evaluate result - $evaled = $this->resolveEvaluation($str); - - $return = is_numeric($evaled) ? Lang::nf($evaled, $precision, true) : $evaled; - - return [$return, $fSuffix, $fRating]; - } - - // 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($type = 'description', $level = MAX_LEVEL, $interactive = false, &$scaling = false) - { - // oooo..kaaayy.. parsing text in 6 or 7 easy steps - // we don't use the internal iterator here. This func has to be called for the individual template. - // otherwise it will get a bit messy, when we iterate, while we iterate *yo dawg!* - - /* 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(?) - $<> - variables - () - regular use for function-like calls - - variables in use .. caseSensitive - - game variables (optionally replace with textVars) - $PlayerName - Cpt. Obvious - $PL / $pl - PlayerLevel - $STR - Strength Attribute (not seen) - $AGI - Agility Attribute (not seen) - $STA - Stamina Attribute (not seen) - $INT - Intellect Attribute (not seen) - $SPI - Spirit Attribute - $AP - Atkpwr - $RAP - RngAtkPwr - $HND - hands used by weapon (1H, 2H) => (1, 2) - $MWS - MainhandWeaponSpeed - $mw / $MW - MainhandWeaponDamage Min/Max - $rwb / $RWB - RangedWeapon..Bonus? Min/Max - $sp - Spellpower - $spa - Spellpower Arcane - $spfi - Spellpower Fire - $spfr - Spellpower Frost - $sph - Spellpower Holy - $spn - Spellpower Nature - $sps - Spellpower Shadow - $bh - Bonus Healing - $pa - %-ArcaneDmg (as float) // V seems broken - $pfi - %-FireDmg (as float) - $pfr - %-FrostDmg (as float) - $ph - %-HolyDmg (as float) - $pn - %-NatureDmg (as float) - $ps - %-ShadowDmg (as float) - $pbh - %-HealingBonus (as float) - $pbhd - %-Healing Done (as float) // all above seem broken - $bc2 - baseCritChance? always 3.25 (unsure) - - spell variables (the stuff we can actually parse) rounding... >5 up? - $a - SpellRadius; per EffectIdx - $b - PointsPerComboPoint; per EffectIdx - $d / $D - SpellDuration; appended timeShorthand; d/D maybe base/max duration?; interpret "0" as "until canceled" - $e - EffectValueMultiplier; per EffectIdx - $f / $F - EffectDamageMultiplier; per EffectIdx - $g / $G - Gender-Switch $Gmale:female; - $h / $H - ProcChance - $i - MaxAffectedTargets - $l - LastValue-Switch; last value as condition $Ltrue:false; - $m / $M - BasePoints; per EffectIdx; m/M +1/+effectDieSides - $n - ProcCharges - $o - TotalAmount (for periodic auras); per EffectIdx - $q - EffectMiscValue; per EffectIdx - $r - SpellRange (hostile) - $s / $S - BasePoints; per EffectIdx; as Range, if applicable - $t / $T - EffectPeriode; per EffectIdx - $u - StackAmount - $v - MaxTargetLevel - $x - MaxAffectedTargets - $z - no place like - - deviations from standard procedures - division - example: $/10;2687s1 => $2687s1/10 - - also: $61829/5;s1 => $61829s1/5 - - functions in use .. caseInsensitive - $cond(a, b, c) - like SQL, if A is met use B otherwise use C - $eq(a, b) - a == b - $floor(a) - floor() - $gt(a, b) - a > b - $gte(a, b) - a >= b - $min(a, b) - min() - $max(a, b) - max() - */ - - $this->interactive = $interactive; - $this->charLevel = $level; - - // step 0: get text - $data = $this->getField($type, true); - if (empty($data) || $data == "[]") // empty tooltip shouldn't be displayed anyway - return ['', []]; - - // step 1: if the text is supplemented with text-variables, get and replace them - if ($this->curTpl['spellDescriptionVariableId'] > 0) - { - if (empty($this->spellVars[$this->id])) - { - $spellVars = DB::Aowow()->SelectCell('SELECT vars FROM ?_spellvariables WHERE id = ?d', $this->curTpl['spellDescriptionVariableId']); - $spellVars = explode("\n", $spellVars); - foreach ($spellVars as $sv) - if (preg_match('/\$(\w*\d*)=(.*)/i', trim($sv), $matches)) - $this->spellVars[$this->id][$matches[1]] = $matches[2]; - } - - // replace self-references - $reset = true; - while ($reset) - { - $reset = false; - foreach ($this->spellVars[$this->id] as $k => $sv) - { - if (preg_match('/\$<(\w*\d*)>/i', $sv, $matches)) - { - $this->spellVars[$this->id][$k] = str_replace('$<'.$matches[1].'>', '${'.$this->spellVars[$this->id][$matches[1]].'}', $sv); - $reset = true; - } - } - } - - // finally, replace SpellDescVars - foreach ($this->spellVars[$this->id] as $k => $sv) - $data = str_replace('$<'.$k.'>', $sv, $data); - } - - // step 2: resolving conditions - // aura- or spell-conditions cant be resolved for our purposes, so force them to false for now (todo (low): strg+f "know" in aowowPower.js ^.^) - - /* sequences - a) simple - $?cond[A][B] // simple case of b) - b) elseif - $?cond[A]?cond[B]..[C] // can probably be repeated as often as you wanted - c) recursive - $?cond[A][$?cond[B][..]] // can probably be stacked as deep as you wanted - - only case a) can be used for KNOW-parameter - */ - - $relSpells = []; - $data = $this->handleConditions($data, $scaling, $relSpells, true); - - // step 3: unpack formulas ${ .. }.X - $data = $this->handleFormulas($data, $scaling, true); - - // step 4: find and eliminate regular variables - $data = $this->handleVariables($data, $scaling, true); - - // step 5: variable-dependant variable-text - // special case $lONE:ELSE[:ELSE2]; or $|ONE:ELSE[:ELSE2]; - while (preg_match('/([\d\.]+)([^\d]*)(\$[l|]:*)([^:]*):([^;]*);/i', $data, $m)) - { - $plurals = explode(':', $m[5]); - $replace = ''; - - if (count($plurals) == 2) // special case: ruRU - { - switch (substr($m[1], -1)) // check last digit of number - { - case 1: - // but not 11 (teen number) - if (!in_array($m[1], [11])) - { - $replace = $m[4]; - break; - } - case 2: - case 3: - case 4: - // but not 12, 13, 14 (teen number) [11 is passthrough] - if (!in_array($m[1], [11, 12, 13, 14])) - { - $replace = $plurals[0]; - break; - } - break; - default: - $replace = $plurals[1]; - } - - } - else - $replace = ($m[1] == 1 ? $m[4] : $plurals[0]); - - $data = str_ireplace($m[1].$m[2].$m[3].$m[4].':'.$m[5].';', $m[1].$m[2].$replace, $data); - } - - // step 6: HTMLize - // colors - $data = preg_replace('/\|cff([a-f0-9]{6})(.+?)\|r/i', '$2', $data); - - // line endings - $data = strtr($data, ["\r" => '', "\n" => '
']); - - return [$data, $relSpells]; - } - - private function handleFormulas($data, &$scaling, $topLevel = false) - { - // they are stacked recursively but should be balanced .. hf - while (($formStartPos = strpos($data, '${')) !== false) - { - $formBrktCnt = 0; - $formPrecision = 0; - $formCurPos = $formStartPos; - - $formOutStr = ''; - - while ($formCurPos <= strlen($data)) // only hard-exit condition, we'll hit those breaks eventually^^ - { - $char = $data[$formCurPos]; - - if ($char == '}') - $formBrktCnt--; - - if ($formBrktCnt) - $formOutStr .= $char; - - if ($char == '{') - $formBrktCnt++; - - if (!$formBrktCnt && $formCurPos != $formStartPos) - break; - - // advance position - $formCurPos++; - } - - $formCurPos++; - - // check for precision-modifiers - if ($formCurPos + 1 < strlen($data) && $data[$formCurPos] == '.' && is_numeric($data[$formCurPos + 1])) - { - $formPrecision = $data[$formCurPos + 1]; - $formCurPos += 2; - } - list($formOutVal, $formOutStr, $ratingId) = $this->resolveFormulaString($formOutStr, $formPrecision ?: ($topLevel ? 0 : 10), $scaling); - - if ($ratingId && Util::checkNumeric($formOutVal) && $this->interactive) - $resolved = sprintf($formOutStr, $ratingId, abs($formOutVal), sprintf(Util::$setRatingLevelString, $this->charLevel, $ratingId, abs($formOutVal), Util::setRatingLevel($this->charLevel, $ratingId, abs($formOutVal)))); - else if ($ratingId && Util::checkNumeric($formOutVal)) - $resolved = sprintf($formOutStr, $ratingId, abs($formOutVal), Util::setRatingLevel($this->charLevel, $ratingId, abs($formOutVal))); - else - $resolved = sprintf($formOutStr, Util::checkNumeric($formOutVal) ? abs($formOutVal) : $formOutVal); - - $data = substr_replace($data, $resolved, $formStartPos, ($formCurPos - $formStartPos)); - } - - return $data; - } - - private function handleVariables($data, &$scaling, $topLevel = false) - { - $pos = 0; // continue strpos-search from this offset - $str = ''; - while (($npos = strpos($data, '$', $pos)) !== false) - { - if ($npos != $pos) - $str .= substr($data, $pos, $npos - $pos); - - $pos = $npos++; - - if ($data[$pos] == '$') - $pos++; - - $varParts = $this->matchVariableString(substr($data, $pos), $len); - if (!$varParts) - { - $str .= '#'; // mark as done, reset below - continue; - } - - $pos += $len; - - $var = $this->resolveVariableString($varParts, $scaling); - $resolved = is_numeric($var[0]) ? abs($var[0]) : $var[0]; - if (isset($var[2])) - { - if (isset($var[4]) && $this->interactive) - $resolved = sprintf($var[2], $var[4], abs($var[0]), sprintf(Util::$setRatingLevelString, $this->charLevel, $var[4], abs($var[0]), Util::setRatingLevel($this->charLevel, $var[4], abs($var[0])))); - else if (isset($var[4])) - $resolved = sprintf($var[2], $var[4], abs($var[0]), Util::setRatingLevel($this->charLevel, $var[4], abs($var[0]))); - else - $resolved = sprintf($var[2], $resolved); - } - - if (isset($var[1]) && $var[0] != $var[1] && !isset($var[4])) - { - $_ = is_numeric($var[1]) ? abs($var[1]) : $var[1]; - $resolved .= Lang::game('valueDelim'); - $resolved .= isset($var[3]) ? sprintf($var[3], $_) : $_; - } - - $str .= $resolved; - } - $str .= substr($data, $pos); - $str = str_replace('#', '$', $str); // reset marker - - return $str; - } - - private function handleConditions($data, &$scaling, &$relSpells, $topLevel = false) - { - while (($condStartPos = strpos($data, '$?')) !== false) - { - $condBrktCnt = 0; - $condCurPos = $condStartPos + 2; // after the '$?' - $targetPart = 3; // we usually want the second pair of brackets - $curPart = 0; // parts: $? 0 [ 1 ] 2 [ 3 ] 4 ... - $condParts = []; - $isLastElse = false; - - while ($condCurPos <= strlen($data)) // only hard-exit condition, we'll hit those breaks eventually^^ - { - $char = $data[$condCurPos]; - - // advance position - $condCurPos++; - - if ($char == '[') - { - $condBrktCnt++; - - if ($condBrktCnt == 1) - $curPart++; - - // previously there was no condition -> last else - if ($condBrktCnt == 1) - if (($curPart && ($curPart % 2)) && (!isset($condParts[$curPart - 1]) || empty(trim($condParts[$curPart - 1])))) - $isLastElse = true; - - if (empty($condParts[$curPart])) - continue; - } - - if (empty($condParts[$curPart])) - $condParts[$curPart] = $char; - else - $condParts[$curPart] .= $char; - - if ($char == ']') - { - $condBrktCnt--; - - if (!$condBrktCnt) - { - $condParts[$curPart] = substr($condParts[$curPart], 0, -1); - $curPart++; - } - - if ($condBrktCnt) - continue; - - if ($isLastElse) - break; - } - } - - // check if it is know-compatible - $know = 0; - if (preg_match('/\(?(\!?)[as](\d+)\)?$/i', $condParts[0], $m)) - { - if (!strstr($condParts[1], '$?')) - if (!strstr($condParts[3], '$?')) - if (!isset($condParts[5])) - $know = $m[2]; - - // found a negation -> switching condition target - if ($m[1] == '!') - $targetPart = 1; - } - // if not, what part of the condition should be used? - else if (preg_match('/(([\W\D]*[as]\d+)|([^\[]*))/i', $condParts[0], $m) && !empty($m[3])) - { - $cnd = $this->resolveEvaluation($m[3]); - if ((is_numeric($cnd) || is_bool($cnd)) && $cnd) // only case, deviating from normal; positive result -> use [true] - $targetPart = 1; - } - - // recursive conditions - if (strstr($condParts[$targetPart], '$?')) - $condParts[$targetPart] = $this->handleConditions($condParts[$targetPart], $scaling, $relSpells); - - if ($know && $topLevel) - { - foreach ([1, 3] as $pos) - { - if (strstr($condParts[$pos], '${')) - $condParts[$pos] = $this->handleFormulas($condParts[$pos], $scaling); - - if (strstr($condParts[$pos], '$')) - $condParts[$pos] = $this->handleVariables($condParts[$pos], $scaling); - } - - // false condition first - if (!isset($relSpells[$know])) - $relSpells[$know] = []; - - $relSpells[$know][] = [$condParts[3], $condParts[1]]; - - $data = substr_replace($data, ''.$condParts[$targetPart].'', $condStartPos, ($condCurPos - $condStartPos)); - } - else - $data = substr_replace($data, $condParts[$targetPart], $condStartPos, ($condCurPos - $condStartPos)); - } - - return $data; - } - - public function renderBuff($level = MAX_LEVEL, $interactive = false) - { - if (!$this->curTpl) - return ['', []]; - - // doesn't have a buff - if (!$this->getField('buff', true)) - return ['', []]; - - $this->interactive = $interactive; - - $x = ''; - - // spellName - $x .= ''; - - // dispelType (if applicable) - if ($this->curTpl['dispelType']) - if ($dispel = Lang::game('dt', $this->curTpl['dispelType'])) - $x .= ''; - - $x .= '
'.$this->getField('name', true).''.$dispel.'
'; - - $x .= '
'; - - // parse Buff-Text - $btt = $this->parseText('buff', $level, $this->interactive, $scaling); - $x .= $btt[0].'
'; - - // duration - if ($this->curTpl['duration'] > 0) - $x .= ''.sprintf(Lang::spell('remaining'), Util::formatTime($this->curTpl['duration'])).''; - - $x .= '
'; - - // scaling information - spellId:min:max:curr - $x .= ''; - - return [$x, $btt[1]]; - } - - public function renderTooltip($level = MAX_LEVEL, $interactive = false) - { - if (!$this->curTpl) - return ['', []]; - - $this->interactive = $interactive; - - // fetch needed texts - $name = $this->getField('name', true); - $rank = $this->getField('rank', true); - $desc = $this->parseText('description', $level, $this->interactive, $scaling); - $tools = $this->getToolsForCurrent(); - $cool = $this->createCooldownForCurrent(); - $cast = $this->createCastTimeForCurrent(); - $cost = $this->createPowerCostForCurrent(); - $range = $this->createRangesForCurrent(); - - // get reagents - $reagents = $this->getReagentsForCurrent(); - foreach ($reagents as &$r) - $r[2] = ItemList::getName($r[0]); - - $reagents = array_reverse($reagents); - - // get stances (check: SPELL_ATTR2_NOT_NEED_SHAPESHIFT) - $stances = ''; - if ($this->curTpl['stanceMask'] && !($this->curTpl['attributes2'] & 0x80000)) - $stances = Lang::game('requires2').' '.Lang::getStances($this->curTpl['stanceMask']); - - // get item requirement (skip for professions) - $reqItems = ''; - if ($this->curTpl['typeCat'] != 11) - { - $class = $this->getField('equippedItemClass'); - $mask = $this->getField('equippedItemSubClassMask'); - $reqItems = Lang::getRequiredItems($class, $mask); - } - - // get created items (may need improvement) - $createItem = ''; - if (in_array($this->curTpl['typeCat'], [9, 11])) // only Professions - { - foreach ($this->canCreateItem() as $idx) - { - if ($this->curTpl['effect'.$idx.'Id'] == 53)// Enchantment (has createItem Scroll of Enchantment) - continue; - - foreach ($this->relItems->iterate() as $cId => $__) - { - if ($cId != $this->curTpl['effect'.$idx.'CreateItemId']) - continue; - - $createItem = $this->relItems->renderTooltip(true, $this->id); - break 2; - } - } - } - - $x = ''; - $x .= '
'; - - // name & rank - if ($rank) - $x .= '
'.$name.''.$rank.'
'; - else - $x .= ''.$name.'
'; - - // powerCost & ranges - if ($range && $cost) - $x .= '
'.$cost.''.$range.'
'; - else if ($cost || $range) - $x .= $range.$cost.'
'; - - // castTime & cooldown - if ($cast && $cool) // tabled layout - { - $x .= ''; - $x .= ''; - if ($stances) - $x.= ''; - - $x .= '
'.$cast.''.$cool.'
'.$stances.'
'; - } - else if ($cast || $cool) // line-break layout - { - $x .= $cast.$cool; - - if ($stances) - $x .= '
'.$stances; - } - - $x .= '
'; - - $xTmp = []; - - if ($tools) - { - $_ = Lang::spell('tools').':
'; - while ($tool = array_pop($tools)) - { - if (isset($tool['itemId'])) - $_ .= ''.$tool['name'].''; - else if (isset($tool['id'])) - $_ .= ''.$tool['name'].''; - else - $_ .= $tool['name']; - - if (!empty($tools)) - $_ .= ', '; - else - $_ .= '
'; - } - - $xTmp[] = $_.'
'; - } - - if ($reagents) - { - $_ = Lang::spell('reagents').':
'; - while ($reagent = array_pop($reagents)) - { - $_ .= ''.$reagent[2].''; - if ($reagent[1] > 1) - $_ .= ' ('.$reagent[1].')'; - - $_ .= empty($reagents) ? '
' : ', '; - } - - $xTmp[] = $_.'
'; - } - - if ($reqItems) - $xTmp[] = Lang::game('requires2').' '.$reqItems; - - if ($desc[0]) - $xTmp[] = ''.$desc[0].''; - - if ($createItem) - $xTmp[] = $createItem; - - if ($xTmp) - $x .= '
'.implode('
', $xTmp).'
'; - - // scaling information - spellId:min:max:curr - $x .= ''; - - return [$x, $desc[1]]; - } - - public function getTalentHeadForCurrent() - { - // power cost: pct over static - $cost = $this->createPowerCostForCurrent(); - - // ranges - $range = $this->createRangesForCurrent(); - - // cast times - $cast = $this->createCastTimeForCurrent(); - - // cooldown or categorycooldown - $cool = $this->createCooldownForCurrent(); - - // assemble parts - // upper: cost :: range - // lower: time :: cooldown - $x = ''; - - // upper - if ($cost && $range) - $x .= '
'.$cost.''.$range.'
'; - else - $x .= $cost.$range; - - if (($cost xor $range) && ($cast xor $cool)) - $x .= '
'; - - // lower - if ($cast && $cool) - $x .= '
'.$cast.''.$cool.'
'; - else - $x .= $cast.$cool; - - return $x; - } - - public function getColorsForCurrent() - { - $gry = $this->curTpl['skillLevelGrey']; - $ylw = $this->curTpl['skillLevelYellow']; - $grn = (int)(($ylw + $gry) / 2); - $org = $this->curTpl['learnedAt']; - - if (($org && $ylw < $org) || $ylw >= $gry) - $ylw = 0; - - if (($org && $grn < $org) || $grn >= $gry) - $grn = 0; - - if (($grn && $org >= $grn) || $org >= $gry) - $org = 0; - - return $gry > 1 ? [$org, $ylw, $grn, $gry] : null; - } - - public function getListviewData($addInfoMask = 0x0) - { - $data = []; - - if ($addInfoMask & ITEMINFO_MODEL) - $modelInfo = $this->getModelInfo(); - - foreach ($this->iterate() as $__) - { - $quality = ($this->curTpl['cuFlags'] & SPELL_CU_QUALITY_MASK) >> 8; - $talent = $this->curTpl['cuFlags'] & (SPELL_CU_TALENT | SPELL_CU_TALENTSPELL) && $this->curTpl['spellLevel'] <= 1; - - $data[$this->id] = array( - 'id' => $this->id, - 'name' => ($quality ?: '@').$this->getField('name', true), - 'icon' => $this->curTpl['iconStringAlt'] ?: $this->curTpl['iconString'], - 'level' => $talent ? $this->curTpl['talentLevel'] : $this->curTpl['spellLevel'], - 'school' => $this->curTpl['schoolMask'], - 'cat' => $this->curTpl['typeCat'], - 'trainingcost' => $this->curTpl['trainingCost'], - 'skill' => count($this->curTpl['skillLines']) > 4 ? array_merge(array_splice($this->curTpl['skillLines'], 0, 4), [-1]): $this->curTpl['skillLines'], // display max 4 skillLines (fills max three lines in listview) - 'reagents' => array_values($this->getReagentsForCurrent()), - 'source' => [] - // 'talentspec' => $this->curTpl['skillLines'][0] not used: g_chr_specs has the wrong structure for it; also setting .cat and .type does the same - ); - - // Sources - if (!empty($this->sources[$this->id])) - { - $data[$this->id]['source'] = array_keys($this->sources[$this->id]); - if (!empty($this->sources[$this->id][3])) - $data[$this->id]['sourcemore'] = [['p' => $this->sources[$this->id][3][0]]]; - } - - // Proficiencies - if ($this->curTpl['typeCat'] == -11) - foreach (self::$spellTypes as $cat => $type) - if (in_array($this->curTpl['skillLines'][0], self::$skillLines[$cat])) - $data[$this->id]['type'] = $type; - - // creates item - foreach ($this->canCreateItem() as $idx) - { - $max = $this->curTpl['effect'.$idx.'DieSides'] + $this->curTpl['effect'.$idx.'BasePoints']; - $min = $this->curTpl['effect'.$idx.'DieSides'] > 1 ? 1 : $max; - - $data[$this->id]['creates'] = [$this->curTpl['effect'.$idx.'CreateItemId'], $min, $max]; - break; - } - - // Profession - if (in_array($this->curTpl['typeCat'], [9, 11])) - { - if ($la = $this->curTpl['learnedAt']) - $data[$this->id]['learnedat'] = $la; - else if (($la = $this->curTpl['reqSkillLevel']) > 1) - $data[$this->id]['learnedat'] = $la; - - $data[$this->id]['colors'] = $this->getColorsForCurrent(); - } - - // glyph - if ($this->curTpl['typeCat'] == -13) - $data[$this->id]['glyphtype'] = $this->curTpl['cuFlags'] & SPELL_CU_GLYPH_MAJOR ? 1 : 2; - - if ($r = $this->getField('rank', true)) - $data[$this->id]['rank'] = $r; - - if ($mask = $this->curTpl['reqClassMask']) - $data[$this->id]['reqclass'] = $mask; - - if ($mask = $this->curTpl['reqRaceMask']) - $data[$this->id]['reqrace'] = $mask; - - - if ($addInfoMask & ITEMINFO_MODEL) - { - // may have multiple models set, in this case i've no idea what should be picked - for ($i = 1; $i < 4; $i++) - { - if (!empty($modelInfo[$this->id][$i])) - { - $data[$this->id]['npcId'] = $modelInfo[$this->id][$i]['typeId']; - $data[$this->id]['displayId'] = $modelInfo[$this->id][$i]['displayId']; - $data[$this->id]['displayName'] = $modelInfo[$this->id][$i]['displayName']; - break; - } - } - } - } - - return $data; - } - - public function getJSGlobals($addMask = GLOBALINFO_SELF, &$extra = []) - { - $data = []; - - if ($this->relItems && ($addMask & GLOBALINFO_RELATED)) - $data = $this->relItems->getJSGlobals(); - - foreach ($this->iterate() as $id => $__) - { - if ($addMask & GLOBALINFO_RELATED) - { - if ($mask = $this->curTpl['reqClassMask']) - for ($i = 0; $i < 11; $i++) - if ($mask & (1 << $i)) - $data[TYPE_CLASS][$i + 1] = $i + 1; - - if ($mask = $this->curTpl['reqRaceMask']) - for ($i = 0; $i < 11; $i++) - if ($mask & (1 << $i)) - $data[TYPE_RACE][$i + 1] = $i + 1; - - // play sound effect - for ($i = 1; $i < 4; $i++) - if ($this->getField('effect'.$i.'Id') == 131 || $this->getField('effect'.$i.'Id') == 132) - $data[TYPE_SOUND][$this->getField('effect'.$i.'MiscValue')] = $this->getField('effect'.$i.'MiscValue'); - } - - if ($addMask & GLOBALINFO_SELF) - { - $iconString = $this->curTpl['iconStringAlt'] ? 'iconStringAlt' : 'iconString'; - - $data[TYPE_SPELL][$id] = array( - 'icon' => $this->curTpl[$iconString], - 'name' => $this->getField('name', true), - ); - } - - if ($addMask & GLOBALINFO_EXTRA) - { - $buff = $this->renderBuff(MAX_LEVEL, true); - $tTip = $this->renderTooltip(MAX_LEVEL, true); - - foreach ($tTip[1] as $relId => $_) - if (empty($data[TYPE_SPELL][$relId])) - $data[TYPE_SPELL][$relId] = $relId; - - foreach ($buff[1] as $relId => $_) - if (empty($data[TYPE_SPELL][$relId])) - $data[TYPE_SPELL][$relId] = $relId; - - $extra[$id] = array( - 'id' => $id, - 'tooltip' => $tTip[0], - 'buff' => !empty($buff[0]) ? $buff[0] : null, - 'spells' => $tTip[1], - 'buffspells' => !empty($buff[1]) ? $buff[1] : null - ); - } - } - - return $data; - } - - // mostly similar to TC - public function getCastingTimeForBonus($asDOT = false) - { - $areaTargets = [7, 8, 15, 16, 20, 24, 30, 31, 33, 34, 37, 54, 56, 59, 104, 108]; - $castingTime = $this->IsChanneledSpell() ? $this->curTpl['duration'] : $this->curTpl['castTime']; - - if (!$castingTime) - return 3500; - - if ($castingTime > 7000) - $castingTime = 7000; - - if ($castingTime < 1500) - $castingTime = 1500; - - if ($asDOT && !$this->isChanneledSpell()) - $castingTime = 3500; - - $overTime = 0; - $nEffects = 0; - $isDirect = false; - $isArea = false; - - for ($i = 1; $i <= 3; $i++) - { - if (in_array($this->curTpl['effect'.$i.'Id'], [2, 7, 8, 9, 62, 67])) - $isDirect = true; - else if (in_array($this->curTpl['effect'.$i.'AuraId'], [3, 8, 53])) - if ($_ = $this->curTpl['duration']) - $overTime = $_; - else if ($this->curTpl['effect'.$i.'AuraId']) - $nEffects++; - - if (in_array($this->curTpl['effect'.$i.'ImplicitTargetA'], $areaTargets) || in_array($this->curTpl['effect'.$i.'ImplicitTargetB'], $areaTargets)) - $isArea = true; - } - - // Combined Spells with Both Over Time and Direct Damage - if ($overTime > 0 && $castingTime > 0 && $isDirect) - { - // mainly for DoTs which are 3500 here otherwise - $originalCastTime = $this->curTpl['castTime']; - if ($this->curTpl['attributes0'] & 0x2) // requires Ammo - $originalCastTime += 500; - - if ($originalCastTime > 7000) - $originalCastTime = 7000; - - if ($originalCastTime < 1500) - $originalCastTime = 1500; - - // Portion to Over Time - $PtOT = ($overTime / 15000) / (($overTime / 15000) + (OriginalCastTime / 3500)); - - if ($asDOT) - $castingTime = $castingTime * $PtOT; - else if ($PtOT < 1) - $castingTime = $castingTime * (1 - $PtOT); - else - $castingTime = 0; - } - - // Area Effect Spells receive only half of bonus - if ($isArea) - $castingTime /= 2; - - // -5% of total per any additional effect - $castingTime -= ($nEffects * 175); - if ($castingTime < 0) - $castingTime = 0; - - return $castingTime; - } - - public function getSourceData() - { - $data = []; - - foreach ($this->iterate() as $__) - { - $data[$this->id] = array( - 'n' => $this->getField('name', true), - 't' => TYPE_SPELL, - 'ti' => $this->id, - 's' => empty($this->curTpl['skillLines']) ? 0 : $this->curTpl['skillLines'][0], - 'c' => $this->curTpl['typeCat'], - 'icon' => $this->curTpl['iconStringAlt'] ? $this->curTpl['iconStringAlt'] : $this->curTpl['iconString'], - ); - } - - return $data; - } -} - - -class SpellListFilter extends Filter -{ - // sources in filter and general use different indizes - private $enums = array( - 9 => array( - 1 => true, // Any - 2 => false, // None - 3 => 1, // Crafted - 4 => 2, // Drop - 6 => 4, // Quest - 7 => 5, // Vendor - 8 => 6, // Trainer - 9 => 7, // Discovery - 10 => 9 // Talent - ) - ); - - // cr => [type, field, misc, extraCol] - protected $genericFilter = array( // misc (bool): _NUMERIC => useFloat; _STRING => localized; _FLAG => match Value; _BOOLEAN => stringSet - 1 => [FILTER_CR_CALLBACK, 'cbCost', null, null], // costAbs [op] [int] - 2 => [FILTER_CR_NUMERIC, 'powerCostPercent', NUM_CAST_INT ], // prcntbasemanarequired - 3 => [FILTER_CR_BOOLEAN, 'spellFocusObject' ], // requiresnearbyobject - 4 => [FILTER_CR_NUMERIC, 'trainingcost', NUM_CAST_INT ], // trainingcost - 5 => [FILTER_CR_BOOLEAN, 'reqSpellId' ], // requiresprofspec - 8 => [FILTER_CR_FLAG, 'cuFlags', CUSTOM_HAS_SCREENSHOT ], // hasscreenshots - 9 => [FILTER_CR_CALLBACK, 'cbSource', null, null], // source [enum] - 10 => [FILTER_CR_FLAG, 'cuFlags', SPELL_CU_FIRST_RANK ], // firstrank - 11 => [FILTER_CR_FLAG, 'cuFlags', CUSTOM_HAS_COMMENT ], // hascomments - 12 => [FILTER_CR_FLAG, 'cuFlags', SPELL_CU_LAST_RANK ], // lastrank - 13 => [FILTER_CR_NUMERIC, 'rankNo', NUM_CAST_INT ], // rankno - 14 => [FILTER_CR_NUMERIC, 'id', NUM_CAST_INT, true], // id - 15 => [FILTER_CR_STRING, 'ic.name', ], // icon - 17 => [FILTER_CR_FLAG, 'cuFlags', CUSTOM_HAS_VIDEO ], // hasvideos - 19 => [FILTER_CR_FLAG, 'attributes0', 0x80000 ], // scaling - 20 => [FILTER_CR_CALLBACK, 'cbReagents', null, null], // has Reagents [yn] - 25 => [FILTER_CR_BOOLEAN, 'skillLevelYellow' ] // rewardsskillups - ); - - // fieldId => [checkType, checkValue[, fieldIsArray]] - protected $inputFields = array( - 'cr' => [FILTER_V_RANGE, [1, 25], true ], // criteria ids - 'crs' => [FILTER_V_LIST, [FILTER_ENUM_NONE, FILTER_ENUM_ANY, [0, 99999]], true ], // criteria operators - 'crv' => [FILTER_V_REGEX, '/[\p{C};:]/ui', true ], // criteria values - only printable chars, no delimiters - 'na' => [FILTER_V_REGEX, '/[\p{C};]/ui', false], // name / text - only printable chars, no delimiter - 'ex' => [FILTER_V_EQUAL, 'on', false], // extended name search - 'ma' => [FILTER_V_EQUAL, 1, false], // match any / all filter - 'minle' => [FILTER_V_RANGE, [1, 99], false], // spell level min - 'maxle' => [FILTER_V_RANGE, [1, 99], false], // spell level max - 'minrs' => [FILTER_V_RANGE, [1, 999], false], // required skill level min - 'maxrs' => [FILTER_V_RANGE, [1, 999], false], // required skill level max - 'ra' => [FILTER_V_LIST, [[1, 8], 10, 11], false], // races - 'cl' => [FILTER_V_CALLBACK, 'cbClasses', true ], // classes - 'gl' => [FILTER_V_CALLBACK, 'cbGlyphs', true ], // glyph type - 'sc' => [FILTER_V_RANGE, [0, 6], true ], // magic schools - 'dt' => [FILTER_V_LIST, [[1, 6], 9], false], // dispel types - 'me' => [FILTER_V_RANGE, [1, 31], false] // mechanics - ); - - protected function createSQLForCriterium(&$cr) - { - if (in_array($cr[0], array_keys($this->genericFilter))) - if ($genCr = $this->genericCriterion($cr)) - return $genCr; - - unset($cr); - $this->error = true; - return [1]; - } - - protected function createSQLForValues() - { - $parts = []; - $_v = &$this->fiData['v']; - - //string (extended) - if (isset($_v['na'])) - { - $_ = []; - if (isset($_v['ex']) && $_v['ex'] == 'on') - $_ = $this->modularizeString(['name_loc'.User::$localeId, 'buff_loc'.User::$localeId, 'description_loc'.User::$localeId]); - else - $_ = $this->modularizeString(['name_loc'.User::$localeId]); - - if ($_) - $parts[] = $_; - } - - // spellLevel min todo (low): talentSpells (typeCat -2) commonly have spellLevel 1 (and talentLevel >1) -> query is inaccurate - if (isset($_v['minle'])) - $parts[] = ['spellLevel', $_v['minle'], '>=']; - - // spellLevel max - if (isset($_v['maxle'])) - $parts[] = ['spellLevel', $_v['maxle'], '<=']; - - // skillLevel min - if (isset($_v['minrs'])) - $parts[] = ['learnedAt', $_v['minrs'], '>=']; - - // skillLevel max - if (isset($_v['maxrs'])) - $parts[] = ['learnedAt', $_v['maxrs'], '<=']; - - // race - if (isset($_v['ra'])) - $parts[] = ['AND', [['reqRaceMask', RACE_MASK_ALL, '&'], RACE_MASK_ALL, '!'], ['reqRaceMask', $this->list2Mask([$_v['ra']]), '&']]; - - // class [list] - if (isset($_v['cl'])) - $parts[] = ['reqClassMask', $this->list2Mask($_v['cl']), '&']; - - // school [list] - if (isset($_v['sc'])) - $parts[] = ['schoolMask', $this->list2Mask($_v['sc'], true), '&']; - - // glyph type [list] wonky, admittedly, but consult SPELL_CU_* in defines and it makes sense - if (isset($_v['gl'])) - $parts[] = ['cuFlags', ($this->list2Mask($_v['gl']) << 6), '&']; - - // dispel type - if (isset($_v['dt'])) - $parts[] = ['dispelType', $_v['dt']]; - - // mechanic - if (isset($_v['me'])) - $parts[] = ['OR', ['mechanic', $_v['me']], ['effect1Mechanic', $_v['me']], ['effect2Mechanic', $_v['me']], ['effect3Mechanic', $_v['me']]]; - - return $parts; - } - - protected function cbClasses(&$val) - { - if (!$this->parentCats || !in_array($this->parentCats[0], [-13, -2, 7])) - return false; - - if (!Util::checkNumeric($val, NUM_REQ_INT)) - return false; - - $type = FILTER_V_LIST; - $valid = [[1, 9], 11]; - - return $this->checkInput($type, $valid, $val); - } - - protected function cbGlyphs(&$val) - { - if (!$this->parentCats || $this->parentCats[0] != -13) - return false; - - if (!Util::checkNumeric($val, NUM_REQ_INT)) - return false; - - $type = FILTER_V_LIST; - $valid = [1, 2]; - - return $this->checkInput($type, $valid, $val); - } - - protected function cbCost($cr) - { - if (!Util::checkNumeric($cr[2], NUM_CAST_INT) || !$this->int2Op($cr[1])) - return false; - - return ['OR', ['AND', ['powerType', [1, 6]], ['powerCost', (10 * $cr[2]), $cr[1]]], ['AND', ['powerType', [1, 6], '!'], ['powerCost', $cr[2], $cr[1]]]]; - } - - protected function cbSource($cr) - { - if (!isset($this->enums[$cr[0]][$cr[1]])) - return false; - - $_ = $this->enums[$cr[0]][$cr[1]]; - if (is_int($_)) // specific - return ['src.src'.$_, null, '!']; - else if ($_) // any - return ['OR', ['src.src1', null, '!'], ['src.src2', null, '!'], ['src.src4', null, '!'], ['src.src5', null, '!'], ['src.src6', null, '!'], ['src.src7', null, '!'], ['src.src9', null, '!']]; - else if (!$_) // none - return ['AND', ['src.src1', null], ['src.src2', null], ['src.src4', null], ['src.src5', null], ['src.src6', null], ['src.src7', null], ['src.src9', null]]; - - return false; - } - - protected function cbReagents($cr) - { - if (!$this->int2Bool($cr[1])) - return false; - - if ($cr[1]) - return ['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]]; - } -} - -?> diff --git a/includes/types/title.class.php b/includes/types/title.class.php deleted file mode 100644 index 00ac972a..00000000 --- a/includes/types/title.class.php +++ /dev/null @@ -1,174 +0,0 @@ - [['src']], // 11: TYPE_TITLE - 'src' => ['j' => ['?_source src ON type = 11 AND typeId = t.id', true], 's' => ', src13, moreType, moreTypeId'] - ); - - public function __construct($conditions = []) - { - parent::__construct($conditions); - - // post processing - foreach ($this->iterate() as $id => &$_curTpl) - { - // preparse sources - notice: under this system titles can't have more than one source (or two for achivements), which is enough for standard TC cases but may break custom cases - if ($_curTpl['moreType'] == TYPE_ACHIEVEMENT) - $this->sources[$this->id][12][] = $_curTpl['moreTypeId']; - else if ($_curTpl['moreType'] == TYPE_QUEST) - $this->sources[$this->id][4][] = $_curTpl['moreTypeId']; - else if ($_curTpl['src13']) - $this->sources[$this->id][13][] = $_curTpl['src13']; - - // titles display up to two achievements at once - if ($_curTpl['src12Ext']) - $this->sources[$this->id][12][] = $_curTpl['src12Ext']; - - unset($_curTpl['src12Ext']); - unset($_curTpl['moreType']); - unset($_curTpl['moreTypeId']); - unset($_curTpl['src3']); - - // shorthand for more generic access - foreach (Util::$localeStrings as $i => $str) - if ($str) - $_curTpl['name_loc'.$i] = trim(str_replace('%s', '', $_curTpl['male_loc'.$i])); - } - } - - public function getListviewData() - { - $data = []; - $this->createSource(); - - foreach ($this->iterate() as $__) - { - $data[$this->id] = array( - 'id' => $this->id, - 'name' => $this->getField('male', true), - 'namefemale' => $this->getField('female', true), - 'side' => $this->curTpl['side'], - 'gender' => $this->curTpl['gender'], - 'expansion' => $this->curTpl['expansion'], - 'category' => $this->curTpl['category'] - ); - - if (!empty($this->curTpl['source'])) - $data[$this->id]['source'] = $this->curTpl['source']; - } - - return $data; - } - - public function getJSGlobals($addMask = 0) - { - $data = []; - - foreach ($this->iterate() as $__) - { - $data[TYPE_TITLE][$this->id]['name'] = $this->getField('male', true); - - if ($_ = $this->getField('female', true)) - $data[TYPE_TITLE][$this->id]['namefemale'] = $_; - } - - return $data; - } - - private function createSource() - { - $sources = array( - 4 => [], // Quest - 12 => [], // Achievements - 13 => [] // simple text - ); - - foreach ($this->iterate() as $__) - { - if (empty($this->sources[$this->id])) - continue; - - foreach (array_keys($sources) as $srcKey) - if (isset($this->sources[$this->id][$srcKey])) - $sources[$srcKey] = array_merge($sources[$srcKey], $this->sources[$this->id][$srcKey]); - } - - // fill in the details - if (!empty($sources[4])) - $sources[4] = (new QuestList(array(['id', $sources[4]])))->getSourceData(); - - if (!empty($sources[12])) - $sources[12] = (new AchievementList(array(['id', $sources[12]])))->getSourceData(); - - if (!empty($sources[13])) - $sources[13] = DB::Aowow()->SELECT('SELECT *, Id AS ARRAY_KEY FROM ?_sourcestrings WHERE Id IN (?a)', $sources[13]); - - foreach ($this->sources as $Id => $src) - { - $tmp = []; - - // Quest-source - if (isset($src[4])) - { - foreach ($src[4] as $s) - { - if (isset($sources[4][$s]['s'])) - $this->faction2Side($sources[4][$s]['s']); - - $tmp[4][] = $sources[4][$s]; - } - } - - // Achievement-source - if (isset($src[12])) - { - foreach ($src[12] as $s) - { - if (isset($sources[12][$s]['s'])) - $this->faction2Side($sources[12][$s]['s']); - - $tmp[12][] = $sources[12][$s]; - } - } - - // other source (only one item possible, so no iteration needed) - if (isset($src[13])) - $tmp[13] = [Util::localizedString($sources[13][$this->sources[$Id][13][0]], 'source')]; - - $this->templates[$Id]['source'] = $tmp; - } - } - - public function getHtmlizedName($gender = GENDER_MALE) - { - $field = $gender == GENDER_FEMALE ? 'female' : 'male'; - return str_replace('%s', '<'.Util::ucFirst(Lang::main('name')).'>', $this->getField($field, true)); - } - - public function renderTooltip() { } - - private function faction2Side(&$faction) // thats weird.. and hopefully unique to titles - { - if ($faction == 2) // Horde - $faction = 0; - else if ($faction != 1) // Alliance - $faction = -1; // Both - } -} - -?> diff --git a/includes/types/user.class.php b/includes/types/user.class.php deleted file mode 100644 index 2a23413c..00000000 --- a/includes/types/user.class.php +++ /dev/null @@ -1,61 +0,0 @@ - [['r']], - 'r' => ['j' => ['?_account_reputation r ON r.userId = a.id', true], 's' => ', IFNULL(SUM(r.amount), 0) AS reputation', 'g' => 'a.id'] - ); - - public function getListviewData() { } - - public function getJSGlobals($addMask = 0) - { - $data = []; - - foreach ($this->iterate() as $__) - { - $data[$this->curTpl['displayName']] = array( - 'border' => 0, // border around avatar (rarityColors) - 'roles' => $this->curTpl['userGroups'], - 'joined' => date(Util::$dateFormatInternal, $this->curTpl['joinDate']), - 'posts' => 0, // forum posts - // 'gold' => 0, // achievement system - // 'silver' => 0, // achievement system - // 'copper' => 0, // achievement system - 'reputation' => $this->curTpl['reputation'] - ); - - // custom titles (only ssen on user page..?) - if ($_ = $this->curTpl['title']) - $data[$this->curTpl['displayName']]['title'] = $_; - - if ($_ = $this->curTpl['avatar']) - { - $data[$this->curTpl['displayName']]['avatar'] = is_numeric($_) ? 2 : 1; - $data[$this->curTpl['displayName']]['avatarmore'] = $_; - } - - // more optional data - // sig: markdown formated string (only used in forum?) - // border: seen as null|1|3 .. changes the border around the avatar (i suspect its meaning changed and got decupled from premium-status with the introduction of patron-status) - } - - return [TYPE_USER => $data]; - } - - public function renderTooltip() { } -} - -?> diff --git a/includes/types/worldevent.class.php b/includes/types/worldevent.class.php deleted file mode 100644 index b124771c..00000000 --- a/includes/types/worldevent.class.php +++ /dev/null @@ -1,196 +0,0 @@ - [['h']], - 'h' => ['j' => ['?_holidays h ON e.holidayId = h.id', true], 'o' => '-e.id ASC'] - ); - - public function __construct($conditions = []) - { - parent::__construct($conditions); - - // unseting elements while we iterate over the array will cause the pointer to reset - $replace = []; - - // post processing - foreach ($this->iterate() as $__) - { - // emulate category - $sT = $this->curTpl['scheduleType']; - if (!$this->curTpl['holidayId']) - $this->curTpl['category'] = 0; - else if ($sT == 2) - $this->curTpl['category'] = 3; - else if (in_array($sT, [0, 1])) - $this->curTpl['category'] = 2; - else if ($sT == -1) - $this->curTpl['category'] = 1; - - // preparse requisites - if ($this->curTpl['requires']) - $this->curTpl['requires'] = explode(' ', $this->curTpl['requires']); - - // change Ids if holiday is set - if ($this->curTpl['holidayId'] > 0) - { - $this->curTpl['name'] = $this->getField('name', true); - $replace[$this->id] = $this->curTpl; - } - else // set a name if holiday is missing - { - // template - $this->curTpl['name_loc0'] = $this->curTpl['nameINT']; - $this->curTpl['iconString'] = 'trade_engineering'; - $this->curTpl['name'] = '(SERVERSIDE) '.$this->getField('nameINT', true); - $replace[$this->id] = $this->curTpl; - } - } - - foreach ($replace as $old => $data) - { - unset($this->templates[$old]); - $this->templates[$data['id']] = $data; - } - } - - public static function getName($id) - { - $row = DB::Aowow()->SelectRow(' - SELECT - IFNULL(h.name_loc0, e.description) AS name_loc0, - h.name_loc2, - h.name_loc3, - h.name_loc6, - h.name_loc8 - FROM - ?_events e - LEFT JOIN - ?_holidays h ON e.holidayId = h.id - WHERE - e.id = ?d', - $id - ); - - return Util::localizedString($row, 'name'); - } - - public static function updateDates($date = null) - { - if (!$date || empty($date['firstDate']) || empty($date['length'])) - { - return array( - 'start' => 0, - 'end' => 0, - 'rec' => 0 - ); - } - - // Convert everything to seconds - $firstDate = intVal($date['firstDate']); - $lastDate = !empty($date['lastDate']) ? intVal($date['lastDate']) : 5000000000; // in the far far FAR future..; - $interval = !empty($date['rec']) ? intVal($date['rec']) : -1; - $length = intVal($date['length']); - - $curStart = $firstDate; - $curEnd = $firstDate + $length; - $nextStart = $curStart + $interval; - $nextEnd = $curEnd + $interval; - - while ($interval > 0 && $nextEnd <= $lastDate && $curEnd < time()) - { - $curStart = $nextStart; - $curEnd = $nextEnd; - $nextStart = $curStart + $interval; - $nextEnd = $curEnd + $interval; - } - - return array( - 'start' => $curStart, - 'end' => $curEnd, - 'rec' => $interval - ); - } - - public function getListviewData($forNow = false) - { - $data = []; - - foreach ($this->iterate() as $__) - { - $data[$this->id] = array( - 'category' => $this->curTpl['category'], - 'id' => $this->id, - 'name' => $this->getField('name', true), - '_date' => array( - 'rec' => $this->curTpl['occurence'], - 'length' => $this->curTpl['length'], - 'firstDate' => $this->curTpl['startTime'], - 'lastDate' => $this->curTpl['endTime'] - ) - ); - } - - if ($forNow) - { - foreach ($data as &$d) - { - $u = self::updateDates($d['_date']); - unset($d['_date']); - $d['startDate'] = $u['start']; - $d['endDate'] = $u['end']; - $d['rec'] = $u['rec']; - } - } - - return $data; - } - - public function getJSGlobals($addMask = 0) - { - $data = []; - - foreach ($this->iterate() as $__) - $data[TYPE_WORLDEVENT][$this->id] = ['name' => $this->getField('name', true), 'icon' => $this->curTpl['iconString']]; - - return $data; - } - - public function renderTooltip() - { - if (!$this->curTpl) - return null; - - $x = '
'; - - // head v that extra % is nesecary because we are using sprintf later on - $x .= '
'.Util::jsEscape($this->getField('name', true)).''.Lang::event('category', $this->getField('category')).'
'; - - // use string-placeholder for dates - // start - $x .= Lang::event('start').Lang::main('colon').'%s
'; - // end - $x .= Lang::event('end').Lang::main('colon').'%s'; - - $x .= '
'; - - // desc - if ($this->getField('holidayId')) - if ($_ = $this->getField('description', true)) - $x .= '
'.Util::jsEscape($_).'
'; - - return $x; - } -} - -?> diff --git a/includes/user.class.php b/includes/user.class.php index 9144eaed..9fa1bd59 100644 --- a/includes/user.class.php +++ b/includes/user.class.php @@ -1,448 +1,213 @@ selectRow('SELECT count, unbanDate FROM ?_account_bannedips WHERE ip = ? AND type = 0', self::$ip)) - { - if ($ipBan['count'] > CFG_ACC_FAILED_AUTH_COUNT && $ipBan['unbanDate'] > time()) - return false; - else if ($ipBan['unbanDate'] <= time()) - DB::Aowow()->query('DELETE FROM ?_account_bannedips WHERE ip = ?', self::$ip); - } - - // try to restore session - if (empty($_SESSION['user'])) - return false; - - // timed out... - if (!empty($_SESSION['timeout']) && $_SESSION['timeout'] <= time()) - return false; - - $query = DB::Aowow()->SelectRow(' - SELECT a.id, a.passHash, a.displayName, a.locale, a.userGroups, a.userPerms, a.allowExpire, BIT_OR(ab.typeMask) AS bans, IFNULL(SUM(r.amount), 0) as reputation, a.avatar, a.dailyVotes, a.excludeGroups - 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 - GROUP BY a.id', - $_SESSION['user'] - ); - - if (!$query) - return false; - - // password changed, terminate session - if (AUTH_MODE_SELF && $query['passHash'] != $_SESSION['hash']) - { - self::destroy(); - return false; - } - - self::$id = intval($query['id']); - self::$displayName = $query['displayName']; - self::$passHash = $query['passHash']; - self::$expires = (bool)$query['allowExpire']; - self::$reputation = $query['reputation']; - self::$banStatus = $query['bans']; - self::$groups = $query['bans'] & (ACC_BAN_TEMP | ACC_BAN_PERM) ? 0 : intval($query['userGroups']); - self::$perms = $query['bans'] & (ACC_BAN_TEMP | ACC_BAN_PERM) ? 0 : intval($query['userPerms']); - self::$dailyVotes = $query['dailyVotes']; - self::$excludeGroups = $query['excludeGroups']; - - $conditions = array( - [['cuFlags', PROFILER_CU_DELETED, '&'], 0], - ['OR', ['user', self::$id], ['ap.accountId', self::$id]] - ); - - if (self::isInGroup(U_GROUP_ADMIN | U_GROUP_BUREAU)) - array_shift($conditions); - - self::$profiles = (new LocalProfileList($conditions)); - - if ($query['avatar']) - self::$avatar = $query['avatar']; - - if (self::$localeId != $query['locale']) // reset, if changed - self::setLocale(intVal($query['locale'])); - - // stuff, that updates on a daily basis goes here (if you keep you session alive indefinitly, the signin-handler doesn't do very much) - // - conscutive visits - // - votes per day - // - reputation for daily visit - if (self::$id) - { - $lastLogin = DB::Aowow()->selectCell('SELECT curLogin FROM ?_account WHERE id = ?d', 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) - self::$dailyVotes = self::getMaxDailyVotes(); - - DB::Aowow()->query(' - UPDATE ?_account - SET dailyVotes = ?d, prevLogin = curLogin, curLogin = UNIX_TIMESTAMP(), prevIP = curIP, curIP = ? - WHERE id = ?d', - self::$dailyVotes, - self::$ip, - self::$id - ); - - // gain rep for daily visit - if (!(self::$banStatus & (ACC_BAN_TEMP | ACC_BAN_PERM)) && !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 forgott a corner case - 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); - else - DB::Aowow()->query('UPDATE ?_account SET consecutiveVisits = 0 WHERE id = ?d', self::$id); - } - } - - return true; - } - - private static function setIP() - { $ipAddr = ''; - $method = ['HTTP_CLIENT_IP', 'HTTP_X_FORWARDED_FOR', 'HTTP_X_FORWARDED', 'HTTP_FORWARDED_FOR', 'HTTP_FORWARDED', 'REMOTE_ADDR']; - - foreach ($method as $m) + foreach (['HTTP_CLIENT_IP', 'HTTP_X_FORWARDED_FOR', 'HTTP_X_FORWARDED', 'HTTP_FORWARDED_FOR', 'HTTP_FORWARDED', 'REMOTE_ADDR'] as $env) { - if ($rawIp = getenv($m)) + if ($rawIp = getenv($env)) { - if ($m == 'HTTP_X_FORWARDED') + if ($env == '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; - } - /****************/ - /* set language */ - /****************/ - // set and save - public static function setLocale($set = -1) - { - $loc = LOCALE_EN; + # set locale # - // get - if ($set != -1 && isset(Util::$localeStrings[$set])) - $loc = $set; - else if (isset($_SESSION['locale']) && isset(Util::$localeStrings[$_SESSION['locale']])) - $loc = $_SESSION['locale']; - else if (!empty($_SERVER["HTTP_ACCEPT_LANGUAGE"])) + if (isset($_SESSION['locale']) && $_SESSION['locale'] instanceof Locale) + self::$preferedLoc = $_SESSION['locale']->validate() ?? Locale::getFallback(); + else if (!empty($_SERVER["HTTP_ACCEPT_LANGUAGE"]) && ($loc = Locale::tryFromHttpAcceptLanguage($_SERVER["HTTP_ACCEPT_LANGUAGE"]))) + self::$preferedLoc = $loc; + else + self::$preferedLoc = Locale::getFallback(); + + + # 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'] ?? ''; + + 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` = %s AND `type` = %i', self::$ip, IP_BAN_TYPE_LOGIN_ATTEMPT)) { - $loc = strtolower(substr($_SERVER["HTTP_ACCEPT_LANGUAGE"], 0, 2)); - switch ($loc) { - case 'ru': $loc = LOCALE_RU; break; - case 'es': $loc = LOCALE_ES; break; - case 'de': $loc = LOCALE_DE; break; - case 'fr': $loc = LOCALE_FR; break; - default: $loc = LOCALE_EN; - } + if ($ipBan['count'] > Cfg::get('ACC_FAILED_AUTH_COUNT') && $ipBan['active']) + return false; + else if (!$ipBan['active']) + DB::Aowow()->qry('DELETE FROM ::account_bannedips WHERE `ip` = %s', self::$ip); } - // check; pick first viable if failed - if (CFG_LOCALES && !(CFG_LOCALES & (1 << $loc))) - { - foreach (Util::$localeStrings as $idx => $__) - { - if (CFG_LOCALES & (1 << $idx)) - { - $loc = $idx; - break; - } - } - } - // set - if (self::$id) - DB::Aowow()->query('UPDATE ?_account SET locale = ? WHERE id = ?', $loc, self::$id); + # try to restore session # - self::useLocale($loc); - } + if (empty($_SESSION['user'])) + return false; - // only use once - public static function useLocale($use) - { - self::$localeId = isset(Util::$localeStrings[$use]) ? $use : LOCALE_EN; - self::$localeString = self::localeString(self::$localeId); - } - - private static function localeString($loc = -1) - { - if (!isset(Util::$localeStrings[$loc])) - $loc = 0; - - return Util::$localeStrings[$loc]; - } - - /*******************/ - /* auth mechanisms */ - /*******************/ - - public static function Auth($name, $pass) - { - $user = 0; - $hash = ''; - - switch (CFG_ACC_AUTH_MODE) - { - case AUTH_MODE_SELF: - { - if (!self::$ip) - return AUTH_INTERNAL_ERR; - - // handle login try limitation - $ip = DB::Aowow()->selectRow('SELECT ip, count, unbanDate FROM ?_account_bannedips WHERE type = 0 AND ip = ?', self::$ip); - if (!$ip || $ip['unbanDate'] < time()) // no entry exists or time expired; set count to 1 - DB::Aowow()->query('REPLACE INTO ?_account_bannedips (ip, type, count, unbanDate) VALUES (?, 0, 1, UNIX_TIMESTAMP() + ?d)', self::$ip, CFG_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_ACC_FAILED_AUTH_BLOCK, self::$ip); - - if ($ip && $ip['count'] >= CFG_ACC_FAILED_AUTH_COUNT && $ip['unbanDate'] >= time()) - return AUTH_IPBANNED; - - $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.user = ? - GROUP BY a.id', - $name - ); - if (!$query) - return AUTH_WRONGUSER; - - self::$passHash = $query['passHash']; - if (!self::verifyCrypt($pass)) - return AUTH_WRONGPASS; - - // successfull auth; clear bans for this IP - DB::Aowow()->query('DELETE FROM ?_account_bannedips WHERE type = 0 AND ip = ?', self::$ip); - - if ($query['bans'] & (ACC_BAN_PERM | ACC_BAN_TEMP)) - return AUTH_BANNED; - - $user = $query['id']; - $hash = $query['passHash']; - break; - } - case AUTH_MODE_REALM: - { - if (!DB::isConnectable(DB_AUTH)) - return AUTH_INTERNAL_ERR; - - $wow = DB::Auth()->selectRow('SELECT a.id, a.sha_pass_hash, 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); - if (!$wow) - return AUTH_WRONGUSER; - - self::$passHash = $wow['sha_pass_hash']; - if (!self::verifySHA1($name, $pass)) - return AUTH_WRONGPASS; - - if ($wow['hasBan']) - return AUTH_BANNED; - - if ($_ = self::checkOrCreateInDB($wow['id'], $name)) - $user = $_; - else - return AUTH_INTERNAL_ERR; - - break; - } - case AUTH_MODE_EXTERNAL: - { - if (!file_exists('config/extAuth.php')) - return AUTH_INTERNAL_ERR; - - require 'config/extAuth.php'; - - $extGroup = -1; - $result = extAuth($name, $pass, $extId, $extGroup); - - if ($result == AUTH_OK && $extId) - { - if ($_ = self::checkOrCreateInDB($extId, $name, $extGroup)) - $user = $_; - else - return AUTH_INTERNAL_ERR; - - break; - } - - return $result; - } - default: - return AUTH_INTERNAL_ERR; - } - - // kickstart session - session_unset(); - $_SESSION['user'] = $user; - $_SESSION['hash'] = $hash; - - return AUTH_OK; - } - - // create a linked account for our settings if nessecary - private static function checkOrCreateInDB($extId, $name, $userGroup = -1) - { - if (!intVal($extId)) - return 0; - - $userGroup = intVal($userGroup); - - if ($_ = DB::Aowow()->selectCell('SELECT id FROM ?_account WHERE extId = ?d', $extId)) - { - if ($userGroup >= U_GROUP_NONE) - DB::Aowow()->query('UPDATE ?_account SET userGroups = ?d WHERE extId = ?d', $userGroup, $extId); - return $_; - } - - $newId = DB::Aowow()->query('INSERT IGNORE INTO ?_account (extId, user, displayName, joinDate, prevIP, prevLogin, locale, status, userGroups) VALUES (?d, ?, ?, UNIX_TIMESTAMP(), ?, UNIX_TIMESTAMP(), ?d, ?d, ?d)', - $extId, - $name, - Util::ucFirst($name), - isset($_SERVER["REMOTE_ADDR"]) ? $_SERVER["REMOTE_ADDR"] : '', - User::$localeId, - ACC_STATUS_OK, - $userGroup >= U_GROUP_NONE ? $userGroup : U_GROUP_NONE + $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` = %i + GROUP BY a.`id`', + $_SESSION['user'] ); - if ($newId) - Util::gainSiteReputation($newId, SITEREP_ACTION_REGISTER); - - return $newId; - } - - private static function createSalt() - { - $algo = '$2a'; - $strength = '$09'; - $salt = '$'.Util::createHash(22); - - return $algo.$strength.$salt; - } - - // crypt used by aowow - public static function hashCrypt($pass) - { - return crypt($pass, self::createSalt()); - } - - public static function verifyCrypt($pass, $hash = '') - { - $_ = $hash ?: self::$passHash; - return $_ === crypt($pass, $_); - } - - // sha1 used by TC / MaNGOS - private static function hashSHA1($name, $pass) - { - return sha1(strtoupper($name).':'.strtoupper($pass)); - } - - private static function verifySHA1($name, $pass) - { - return strtoupper(self::$passHash) === strtoupper(self::hashSHA1($name, $pass)); - } - - public static function isValidName($name, &$errCode = 0) - { - $errCode = 0; - - // different auth modes require different usernames - $min = 0; // external case - $max = 0; - if (CFG_ACC_AUTH_MODE == AUTH_MODE_SELF) + if (!$session || !$userData) { - $min = 4; - $max = 16; + self::destroy(); + return false; } - else if (CFG_ACC_AUTH_MODE == AUTH_MODE_REALM) + else if ($session['expires'] && $session['expires'] < time()) { - $min = 3; - $max = 32; + 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()->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; } - if (($min && mb_strlen($name) < $min) || ($max && mb_strlen($name) > $max)) - $errCode = 1; - else if (preg_match('/[^\w\d\-]/i', $name)) - $errCode = 2; + 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()); - return $errCode == 0; + if ($loc = Locale::tryFrom($userData['locale'])) + self::$preferedLoc = $loc; + + // reset expired account statuses + if ($userData['statusTimer'] && $userData['statusTimer'] < time() && $userData['status'] != ACC_STATUS_NEW) + { + 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; + } + + + /*******************************/ + /* past here we are logged in */ + /*******************************/ + + self::$id = intVal($userData['id']); + self::$username = $userData['username']; + self::$reputation = $userData['reputation']; + self::$banStatus = $userData['bans']; + self::$groups = self::isBanned() ? 0 : intval($userData['userGroups']); + self::$perms = self::isBanned() ? 0 : intval($userData['userPerms']); + self::$dailyVotes = $userData['dailyVotes']; + self::$excludeGroups = $userData['excludeGroups']; + self::$status = $userData['status']; + self::$debug = $userData['debug']; + self::$email = $userData['email']; + self::$avatarborder = $userData['avatarborder']; + + + # reset premium options # + + if (!self::isPremium()) + { + if ($userData['avatar'] == 2) + { + 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 + } + + + # update daily limits # + + if (!self::isBanned()) + { + $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) + self::$dailyVotes = self::getMaxDailyVotes(); + + 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 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) + if ((date('j', $lastLogin) + 1 == date('j') || (date('j') == 1 && date('n', $lastLogin) != date('n'))) && (time() - $lastLogin) < 2 * DAY) + DB::Aowow()->qry('UPDATE ::account SET `consecutiveVisits` = `consecutiveVisits` + 1 WHERE `id` = %i', self::$id); + else + DB::Aowow()->qry('UPDATE ::account SET `consecutiveVisits` = 0 WHERE `id` = %i', self::$id); + } + } + + return true; } - public static function isValidPass($pass, &$errCode = 0) - { - $errCode = 0; - - // only enforce for own passwords - if (mb_strlen($pass) < 6 && CFG_ACC_AUTH_MODE == AUTH_MODE_SELF) - $errCode = 1; - // else if (preg_match('/[^\w\d!"#\$%]/', $pass)) // such things exist..? :o - // $errCode = 2; - - return $errCode == 0; - } - - public static function save() + public static function save(bool $toDB = false) { $_SESSION['user'] = self::$id; - $_SESSION['hash'] = self::$passHash; - $_SESSION['locale'] = self::$localeId; - $_SESSION['timeout'] = self::$expires ? time() + CFG_SESSION_TIMEOUT_DELAY : 0; + $_SESSION['locale'] = self::$preferedLoc; // $_SESSION['dataKey'] does not depend on user login status and is set in User::init() + + if (self::isLoggedIn() && $toDB) + DB::Aowow()->qry('UPDATE ::account SET `locale` = %s WHERE `id` = %s', self::$preferedLoc->value, self::$id); } public static function destroy() @@ -450,124 +215,342 @@ class User session_regenerate_id(true); // session itself is not destroyed; status changed => regenerate id session_unset(); - $_SESSION['locale'] = self::$localeId; // keep locale + $_SESSION['locale'] = self::$preferedLoc; // keep locale $_SESSION['dataKey'] = self::$dataKey; // keep dataKey - self::$id = 0; - self::$displayName = ''; - self::$perms = 0; - self::$groups = U_GROUP_NONE; + self::$id = 0; + self::$username = ''; + self::$perms = 0; + self::$groups = U_GROUP_NONE; } + + /*******************/ + /* auth mechanisms */ + /*******************/ + + public static function authenticate(string $login, #[\SensitiveParameter] string $password) : int + { + $userId = 0; + + $result = match (Cfg::get('ACC_AUTH_MODE')) + { + AUTH_MODE_SELF => self::authSelf($login, $password, $userId), + AUTH_MODE_REALM => self::authRealm($login, $password, $userId), + AUTH_MODE_EXTERNAL => self::authExtern($login, $password, $userId), + default => AUTH_INTERNAL_ERR + }; + + // also banned? its a feature block, not login block.. + if ($result == AUTH_OK || $result == AUTH_BANNED) + { + session_unset(); + $_SESSION['user'] = $userId; + self::$id = $userId; + } + + return $result; + } + + 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` = %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()->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()->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; + + $email = filter_var($nameOrEmail, FILTER_VALIDATE_EMAIL); + + $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 %if', $email, 'a.`email` %else a.`login` %end = %s AND `status` <> %i + GROUP BY a.`id`', + $nameOrEmail, + ACC_STATUS_DELETED + ); + + if (!$query) + return AUTH_WRONGUSER; + + if (!self::verifyCrypt($password, $query['passHash'])) + return AUTH_WRONGPASS; + + // successfull auth; clear bans for this 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; + + $userId = $query['id']; + + return AUTH_OK; + } + + 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 = %s LIMIT 1', $name); + if (!$wow) + return AUTH_WRONGUSER; + + if (!self::verifySRP6($name, $password, $wow['salt'], $wow['verifier'])) + return AUTH_WRONGPASS; + + if ($wow['hasBan']) + return AUTH_BANNED; + + if ($_ = self::checkOrCreateInDB($wow['id'], $name)) + $userId = $_; + else + return AUTH_INTERNAL_ERR; + + return AUTH_OK; + } + + private static function authExtern(string $nameOrEmail, #[\SensitiveParameter] string $password, int &$userId) : int + { + if (!file_exists('config/extAuth.php')) + { + trigger_error('User::authExtern - AUTH_MODE_EXTERNAL is selected but config/extAuth.php does not exist!', E_USER_ERROR); + return AUTH_INTERNAL_ERR; + } + + require 'config/extAuth.php'; + + if (!function_exists('\extAuth')) + { + trigger_error('User::authExtern - AUTH_MODE_EXTERNAL is selected but function extAuth() is not defined!', E_USER_ERROR); + return AUTH_INTERNAL_ERR; + } + + $extGroup = -1; + $extId = 0; + $result = \extAuth($nameOrEmail, $password, $extId, $extGroup); + + // assert we don't have an email passed back from extAuth + if (filter_var($nameOrEmail, FILTER_VALIDATE_EMAIL)) + return AUTH_WRONGUSER; + + if ($result == AUTH_OK && $extId) + { + if ($_ = self::checkOrCreateInDB($extId, $nameOrEmail, $extGroup)) + $userId = $_; + else + return AUTH_INTERNAL_ERR; + } + + return $result; + } + + // 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` = %i', $extId)) + { + if ($userGroup >= U_GROUP_NONE) + DB::Aowow()->qry('UPDATE ::account SET `userGroups` = %i WHERE `extId` = %i', $userGroup, $extId); + return $_; + } + + $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"] ?? '', + self::$preferedLoc->value, + ACC_STATUS_NONE, + $userGroup >= U_GROUP_NONE ? $userGroup : U_GROUP_NONE + ); + + if ($newId) + Util::gainSiteReputation($newId, SITEREP_ACTION_REGISTER); + + return $newId ?: 0; + } + + // crypt used by us + public static function hashCrypt(#[\SensitiveParameter] string $pass) : string + { + return password_hash($pass, PASSWORD_BCRYPT, ['cost' => 15]); + } + + public static function verifyCrypt(#[\SensitiveParameter] string $pass, string $hash) : bool + { + return password_verify($pass, $hash); + } + + // SRP6 used by TC + private static function verifySRP6(string $user, string $pass, string $salt, string $verifier) : bool + { + $g = gmp_init(7); + $N = gmp_init('894B645E89E1535BBDAD5B8B290650530801B18EBFBF5E8FAB3C82872A3E9BB7', 16); + $x = gmp_import( + sha1($salt . sha1(strtoupper($user . ':' . $pass), TRUE), TRUE), + 1, + GMP_LSW_FIRST + ); + $v = gmp_powm($g, $x, $N); + return ($verifier === str_pad(gmp_export($v, 1, GMP_LSW_FIRST), 32, chr(0), STR_PAD_RIGHT)); + } + + /*********************/ /* access management */ /*********************/ - public static function isInGroup($group) + public static function isInGroup(int $group) : bool { - return (self::$groups & $group) != 0; + return $group == U_GROUP_NONE || (self::$groups & $group) != U_GROUP_NONE; } - public static function canComment() + public static function canComment() : bool { - if (!self::$id || self::$banStatus & (ACC_BAN_COMMENT | ACC_BAN_PERM | ACC_BAN_TEMP)) + if (!self::isLoggedIn() || self::isBanned(ACC_BAN_COMMENT)) return false; - return self::$perms || self::$reputation >= CFG_REP_REQ_COMMENT; + return self::$perms || self::$reputation >= Cfg::get('REP_REQ_COMMENT'); } - public static function canReply() + public static function canReply() : bool { - if (!self::$id || self::$banStatus & (ACC_BAN_COMMENT | ACC_BAN_PERM | ACC_BAN_TEMP)) + if (!self::isLoggedIn() || self::isBanned(ACC_BAN_COMMENT)) return false; - return self::$perms || self::$reputation >= CFG_REP_REQ_REPLY; + return self::$perms || self::$reputation >= Cfg::get('REP_REQ_REPLY'); } - public static function canUpvote() + public static function canUpvote() : bool { - if (!self::$id || self::$banStatus & (ACC_BAN_COMMENT | ACC_BAN_PERM | ACC_BAN_TEMP)) + if (!self::isLoggedIn() || self::isBanned(ACC_BAN_COMMENT)) return false; - return self::$perms || (self::$reputation >= CFG_REP_REQ_UPVOTE && self::$dailyVotes > 0); + return self::$perms || (self::$reputation >= Cfg::get('REP_REQ_UPVOTE') && self::$dailyVotes > 0); } - public static function canDownvote() + public static function canDownvote() : bool { - if (!self::$id || self::$banStatus & (ACC_BAN_RATE | ACC_BAN_PERM | ACC_BAN_TEMP)) + if (!self::isLoggedIn() || self::isBanned(ACC_BAN_RATE)) return false; - return self::$perms || (self::$reputation >= CFG_REP_REQ_DOWNVOTE && self::$dailyVotes > 0); + return self::$perms || (self::$reputation >= Cfg::get('REP_REQ_DOWNVOTE') && self::$dailyVotes > 0); } - public static function canSupervote() + public static function canSupervote() : bool { - if (!self::$id || self::$banStatus & (ACC_BAN_RATE | ACC_BAN_PERM | ACC_BAN_TEMP)) + if (!self::isLoggedIn() || self::isBanned(ACC_BAN_RATE) || self::isInGroup(U_GROUP_PENDING)) return false; - return self::$reputation >= CFG_REP_REQ_SUPERVOTE; + return self::$reputation >= Cfg::get('REP_REQ_SUPERVOTE'); } - public static function canUploadScreenshot() + public static function canUploadScreenshot() : bool { - if (!self::$id || self::$banStatus & (ACC_BAN_SCREENSHOT | ACC_BAN_PERM | ACC_BAN_TEMP)) + if (!self::isLoggedIn() || self::isBanned(ACC_BAN_SCREENSHOT) || self::isInGroup(U_GROUP_PENDING)) return false; return true; } - public static function canSuggestVideo() + public static function canWriteGuide() : bool { - if (!self::$id || self::$banStatus & (ACC_BAN_VIDEO | ACC_BAN_PERM | ACC_BAN_TEMP)) + if (!self::isLoggedIn() || self::isBanned(ACC_BAN_GUIDE) || self::isInGroup(U_GROUP_PENDING)) return false; return true; } - public static function isPremium() + public static function canSuggestVideo() : bool { - return self::isInGroup(U_GROUP_PREMIUM) || self::$reputation >= CFG_REP_REQ_PREMIUM; + if (!self::isLoggedIn() || self::isBanned(ACC_BAN_VIDEO) || self::isInGroup(U_GROUP_PENDING)) + return false; + + return true; } + public static function isPremium() : bool + { + return !self::isBanned() && (self::isInGroup(U_GROUP_PREMIUM) || self::$reputation >= Cfg::get('REP_REQ_PREMIUM')); + } + + public static function isLoggedIn() : bool + { + return self::$id > 0; // more checks? maybe check pending email verification here? (self::isInGroup(U_GROUP_PENDING)) + } + + public static function isBanned(int $addBanMask = 0x0) : bool + { + return self::$banStatus & (ACC_BAN_TEMP | ACC_BAN_PERM | $addBanMask); + } + + public static function isRecovering() : bool + { + return self::$status != ACC_STATUS_NONE && self::$status != ACC_STATUS_NEW; + } + + /**************/ /* js-related */ /**************/ - public static function decrementDailyVotes() + public static function decrementDailyVotes() : void { + if (!self::isLoggedIn() || self::isBanned(ACC_BAN_RATE)) + 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 getCurDailyVotes() + public static function getCurrentDailyVotes() : int { + if (!self::isLoggedIn() || self::isBanned(ACC_BAN_RATE) || self::$dailyVotes < 0) + return 0; + return self::$dailyVotes; } - public static function getMaxDailyVotes() + public static function getMaxDailyVotes() : int { - if (!self::$id || self::$banStatus & (ACC_BAN_PERM | ACC_BAN_TEMP)) + if (!self::isLoggedIn() || self::isBanned(ACC_BAN_RATE)) return 0; - return CFG_USER_MAX_VOTES + (self::$reputation >= CFG_REP_REQ_VOTEMORE_BASE ? 1 + intVal((self::$reputation - CFG_REP_REQ_VOTEMORE_BASE) / CFG_REP_REQ_VOTEMORE_ADD) : 0); + $threshold = Cfg::get('REP_REQ_VOTEMORE_BASE'); + $extra = Cfg::get('REP_REQ_VOTEMORE_ADD'); + $base = Cfg::get('USER_MAX_VOTES'); + + return $base + max(0, intVal((self::$reputation - $threshold + $extra) / $extra)); } - public static function getReputation() + public static function getReputation() : int { + if (!self::isLoggedIn() || self::$reputation < 0) + return 0; + return self::$reputation; } - public static function getUserGlobals() + public static function getUserGlobal() : array { $gUser = array( 'id' => self::$id, - 'name' => self::$displayName, + 'name' => self::$username, 'roles' => self::$groups, 'permissions' => self::$perms, 'cookies' => [] ); - if (!self::$id || self::$banStatus & (ACC_BAN_TEMP | ACC_BAN_PERM)) + if (!self::isLoggedIn() || self::isBanned()) return $gUser; $gUser['commentban'] = !self::canComment(); @@ -575,11 +558,22 @@ class User $gUser['canDownvote'] = self::canDownvote(); $gUser['canPostReplies'] = self::canReply(); $gUser['superCommentVotes'] = self::canSupervote(); - $gUser['downvoteRep'] = CFG_REP_REQ_DOWNVOTE; - $gUser['upvoteRep'] = CFG_REP_REQ_UPVOTE; + $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; - $gUser['settings'] = (new StdClass); // profiler requires this to be set; has property premiumborder (NYI) + + if (self::$debug) + $gUser['debug'] = true; // csv id-list output option on listviews + + if (self::isPremium()) + { + $gUser['premium'] = 1; + $gUser['settings'] = ['premiumborder' => self::$avatarborder]; + } + else + $gUser['settings'] = (new \StdClass); // existence is checked in Profiler.js before g_user.excludegroups is applied; should this contain - "defaultModel":{"gender":2,"race":6} ? if ($_ = self::getProfilerExclusions()) $gUser = array_merge($gUser, $_); @@ -587,6 +581,9 @@ class User if ($_ = self::getProfiles()) $gUser['profiles'] = $_; + if ($_ = self::getGuides()) + $gUser['guides'] = $_; + if ($_ = self::getWeightScales()) $gUser['weightscales'] = $_; @@ -596,58 +593,203 @@ class User return $gUser; } - public static function getWeightScales() + public static function getWeightScales() : array { $result = []; - $res = DB::Aowow()->selectCol('SELECT id AS ARRAY_KEY, name FROM ?_account_weightscales WHERE userId = ?d', self::$id); + if (!self::isLoggedIn() || self::isBanned()) + return $result; + + $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); return $result; } - public static function getProfilerExclusions() + public static function getProfilerExclusions() : array { $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)) + + if (!self::isLoggedIn() || self::isBanned()) + return $result; + + if (!Cfg::get('PROFILER_ENABLE')) + return $result; + + 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); return $result; } - public static function getCharacters() + public static function getCharacters() : array { - if (!self::$profiles) + if (!self::loadProfiles()) return []; return self::$profiles->getJSGlobals(PROFILEINFO_CHARACTER); } - public static function getProfiles() + public static function getProfiles() : array { - if (!self::$profiles) + if (!self::loadProfiles()) return []; return self::$profiles->getJSGlobals(PROFILEINFO_PROFILE); } - public static function getCookies() + public static function getPinnedCharacter() : array { - $data = []; + if (!self::loadProfiles()) + return []; - if (self::$id) - $data = DB::Aowow()->selectCol('SELECT name AS ARRAY_KEY, data FROM ?_account_cookies WHERE userId = ?d', self::$id); + $realms = Profiler::getRealms(); + + foreach (self::$profiles->iterate() as $id => $_) + if (self::$profiles->getField('cuFlags') & PROFILER_CU_PINNED) + if (isset($realms[self::$profiles->getField('realm')])) + return [ + $id, + self::$profiles->getField('name'), + self::$profiles->getField('region') . '.' . Profiler::urlize($realms[self::$profiles->getField('realm')]['name'], true) . '.' . Profiler::urlize(self::$profiles->getField('name'), true, true) + ]; + + return []; + } + + public static function getGuides() : array + { + $result = []; + + if (!self::isLoggedIn() || self::isBanned(ACC_BAN_GUIDE)) + return $result; + + 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'])); + $result = $guides; + } + + return $result; + } + + public static function getCookies() : array + { + if (!self::isLoggedIn()) + return []; + + return DB::Aowow()->selectPairs('SELECT `name`, `data` FROM ::account_cookies WHERE `userId` = %i', self::$id); + } + + public static function getFavorites() : array + { + 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` = %i', self::$id); + if (!$res) + return []; + + $data = []; + foreach ($res as $type => $ids) + { + $tc = Type::newList($type, [['id', $ids]]); + if (!$tc || $tc->error) + continue; + + $entities = []; + foreach ($tc->iterate() as $id => $__) + $entities[] = [$id, $tc->getField('name', true, true)]; + + if ($entities) + $data[] = ['id' => $type, 'entities' => $entities]; + } 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 35591265..39e9f297 100644 --- a/includes/utilities.php +++ b/includes/utilities.php @@ -1,325 +1,63 @@ $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 { $node = dom_import_simplexml($this); $no = $node->ownerDocument; - $node->appendChild($no->createCDATASection($str)); + $node->appendChild($no->createCDATASection($cData)); return $this; } } -class CLI +abstract class Util { - const CHR_BELL = 7; - const CHR_BACK = 8; - const CHR_TAB = 9; - const CHR_LF = 10; - const CHR_CR = 13; - const CHR_ESC = 27; - const CHR_BACKSPACE = 127; + /* NOTE! + * FILE_ACCESS should be 0755 or less, but CLI and web interface both access the same files. While in CLI php is executed with the current users perms, + * while the web interface is always executed by www-data (or whoever runs the web server) who does not own the files previously created via CLI. + * And thus web interface actions fail with permission denied, unless the files are flagged +wx for everyone. + * This probably has to be solved on the system level by having www-data and the CLI user share a group or something. + */ + public const FILE_ACCESS = 0777; + public const DIR_ACCESS = 0777; - const LOG_OK = 0; - const LOG_WARN = 1; - const LOG_ERROR = 2; - const LOG_INFO = 3; - - private static $logHandle = null; - private static $hasReadline = null; - - - /***********/ - /* logging */ - /***********/ - - public static function initLogFile($file = '') - { - if (!$file) - return; - - $file = self::nicePath($file); - if (!file_exists($file)) - self::$logHandle = fopen($file, 'w'); - else - { - $logFileParts = pathinfo($file); - - $i = 1; - while (file_exists($logFileParts['dirname'].'/'.$logFileParts['filename'].$i.(isset($logFileParts['extension']) ? '.'.$logFileParts['extension'] : ''))) - $i++; - - $file = $logFileParts['dirname'].'/'.$logFileParts['filename'].$i.(isset($logFileParts['extension']) ? '.'.$logFileParts['extension'] : ''); - self::$logHandle = fopen($file, 'w'); - } - } - - public static function red($str) - { - return OS_WIN ? $str : "\e[31m".$str."\e[0m"; - } - - public static function green($str) - { - return OS_WIN ? $str : "\e[32m".$str."\e[0m"; - } - - public static function yellow($str) - { - return OS_WIN ? $str : "\e[33m".$str."\e[0m"; - } - - public static function blue($str) - { - return OS_WIN ? $str : "\e[36m".$str."\e[0m"; - } - - public static function bold($str) - { - return OS_WIN ? $str : "\e[1m".$str."\e[0m"; - } - - public static function write($txt = '', $lvl = -1) - { - $msg = "\n"; - if ($txt) - { - $msg = str_pad(date('H:i:s'), 10); - switch ($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; - default: - $msg .= ' '; - } - - $msg .= $txt."\n"; - } - - echo $msg; - - if (self::$logHandle) // remove highlights for logging - fwrite(self::$logHandle, preg_replace(["/\e\[\d+m/", "/\e\[0m/"], '', $msg)); - - flush(); - } - - public static function nicePath(/* $file = '', ...$pathParts */) - { - $path = ''; - - switch (func_num_args()) - { - case 0: - return ''; - case 1: - $path = func_get_arg(0); - break; - default: - $args = func_get_args(); - $file = array_shift($args); - $path = implode(DIRECTORY_SEPARATOR, $args).DIRECTORY_SEPARATOR.$file; - } - - if (DIRECTORY_SEPARATOR == '/') // *nix - { - $path = str_replace('\\', '/', $path); - $path = preg_replace('/\/+/i', '/', $path); - } - else if (DIRECTORY_SEPARATOR == '\\') // win - { - $path = str_replace('/', '\\', $path); - $path = preg_replace('/\\\\+/i', '\\', $path); - } - else - CLI::write('Dafuq! Your directory separator is "'.DIRECTORY_SEPARATOR.'". Please report this!', CLI::LOG_ERROR); - - $path = trim($path); - - // resolve *nix home shorthand - if (!OS_WIN) - { - if (preg_match('/^~(\w+)\/.*/i', $path, $m)) - $path = '/home/'.substr($path, 1); - else if (substr($path, 0, 2) == '~/') - $path = getenv('HOME').substr($path, 1); - else if ($path[0] == DIRECTORY_SEPARATOR && substr($path, 0, 6) != '/home/') - $path = substr($path, 1); - } - - // remove quotes (from erronous user input) - $path = str_replace(['"', "'"], ['', ''], $path); - - return $path; - } - - - /**************/ - /* read input */ - /**************/ - - /* - since the CLI on WIN ist not interactive, the following things have to be considered - you do not receive keystrokes but whole strings upon pressing (wich also appends a \r) - as such and probably other control chars can not be registered - this also means, you can't hide input at all, least process it - */ - - public static function readInput(&$fields, $singleChar = false) - { - // first time set - if (self::$hasReadline === null) - self::$hasReadline = function_exists('readline_callback_handler_install'); - - // prevent default output if able - if (self::$hasReadline) - readline_callback_handler_install('', function() { }); - - foreach ($fields as $name => $data) - { - $vars = ['desc', 'isHidden', 'validPattern']; - foreach ($vars as $idx => $v) - $$v = isset($data[$idx]) ? $data[$idx] : false; - - $charBuff = ''; - - if ($desc) - echo "\n".$desc.": "; - - while (true) { - $r = [STDIN]; - $w = $e = null; - $n = stream_select($r, $w, $e, 200000); - - if ($n && in_array(STDIN, $r)) { - $char = stream_get_contents(STDIN, 1); - $keyId = ord($char); - - // ignore this one - if ($keyId == self::CHR_TAB) - continue; - - // WIN sends \r\n as sequence, ignore one - if ($keyId == self::CHR_CR && OS_WIN) - continue; - - // will not be send on WIN .. other ways of returning from setup? (besides ctrl + c) - if ($keyId == self::CHR_ESC) - { - echo chr(self::CHR_BELL); - return false; - } - else if ($keyId == self::CHR_BACKSPACE) - { - if (!$charBuff) - continue; - - $charBuff = mb_substr($charBuff, 0, -1); - if (!$isHidden && self::$hasReadline) - echo chr(self::CHR_BACK)." ".chr(self::CHR_BACK); - } - else if ($keyId == self::CHR_LF) - { - $fields[$name] = $charBuff; - break; - } - else if (!$validPattern || preg_match($validPattern, $char)) - { - $charBuff .= $char; - if (!$isHidden && self::$hasReadline) - echo $char; - - if ($singleChar && self::$hasReadline) - { - $fields[$name] = $charBuff; - break; - } - } - } - } - } - - echo chr(self::CHR_BELL); - - foreach ($fields as $f) - if (strlen($f)) - return true; - - $fields = null; - return true; - } -} - - -class Util -{ - const FILE_ACCESS = 0777; - - const GEM_SCORE_BASE_WOTLK = 16; // rare quality wotlk gem score - const GEM_SCORE_BASE_BC = 8; // rare quality bc gem score + private const GEM_SCORE_BASE_WOTLK = 16; // rare quality wotlk gem score + private const GEM_SCORE_BASE_BC = 8; // rare quality bc gem score private static $perfectGems = null; - public static $localeStrings = array( // zero-indexed - 'enus', null, 'frfr', 'dede', null, null, 'eses', null, 'ruru' - ); - - public static $subDomains = array( - 'www', null, 'fr', 'de', null, null, 'es', null, 'ru' - ); - - public static $typeClasses = array( - null, 'CreatureList', 'GameObjectList', 'ItemList', 'ItemsetList', 'QuestList', 'SpellList', - 'ZoneList', 'FactionList', 'PetList', 'AchievementList', 'TitleList', 'WorldEventList', 'CharClassList', - 'CharRaceList', 'SkillList', null, 'CurrencyList', null, 'SoundList', - TYPE_ICON => 'IconList', - TYPE_EMOTE => 'EmoteList', - TYPE_ENCHANTMENT => 'EnchantmentList' - ); - - public static $typeStrings = array( // zero-indexed - null, 'npc', 'object', 'item', 'itemset', 'quest', 'spell', 'zone', 'faction', - 'pet', 'achievement', 'title', 'event', 'class', 'race', 'skill', null, 'currency', - null, 'sound', - TYPE_ICON => 'icon', - TYPE_USER => 'user', - TYPE_EMOTE => 'emote', - TYPE_ENCHANTMENT => 'enchantment' - ); - - # todo (high): find a sensible way to write data here on setup - private static $gtCombatRatings = array( - 12 => 1.5, 13 => 13.8, 14 => 13.8, 15 => 5, 16 => 10, 17 => 10, 18 => 8, 19 => 14, 20 => 14, - 21 => 14, 22 => 10, 23 => 10, 24 => 8, 25 => 0, 26 => 0, 27 => 0, 28 => 10, 29 => 10, - 30 => 10, 31 => 10, 32 => 14, 33 => 0, 34 => 0, 35 => 28.75, 36 => 10, 37 => 2.5, 44 => 4.268292513760655 - ); - - public static $itemFilter = array( - 20 => 'str', 21 => 'agi', 23 => 'int', 22 => 'sta', 24 => 'spi', 25 => 'arcres', 26 => 'firres', 27 => 'natres', - 28 => 'frores', 29 => 'shares', 30 => 'holres', 37 => 'mleatkpwr', 32 => 'dps', 35 => 'damagetype', 33 => 'dmgmin1', 34 => 'dmgmax1', - 36 => 'speed', 38 => 'rgdatkpwr', 39 => 'rgdhitrtng', 40 => 'rgdcritstrkrtng', 41 => 'armor', 44 => 'blockrtng', 43 => 'block', 42 => 'defrtng', - 45 => 'dodgertng', 46 => 'parryrtng', 48 => 'splhitrtng', 49 => 'splcritstrkrtng', 50 => 'splheal', 51 => 'spldmg', 52 => 'arcsplpwr', 53 => 'firsplpwr', - 54 => 'frosplpwr', 55 => 'holsplpwr', 56 => 'natsplpwr', 60 => 'healthrgn', 61 => 'manargn', 57 => 'shasplpwr', 77 => 'atkpwr', 78 => 'mlehastertng', - 79 => 'resirtng', 84 => 'mlecritstrkrtng', 94 => 'splpen', 95 => 'mlehitrtng', 96 => 'critstrkrtng', 97 => 'feratkpwr', 100 => 'nsockets', 101 => 'rgdhastertng', - 102 => 'splhastertng', 103 => 'hastertng', 114 => 'armorpenrtng', 115 => 'health', 116 => 'mana', 117 => 'exprtng', 119 => 'hitrtng', 123 => 'splpwr', - 134 => 'mledps', 135 => 'mledmgmin', 136 => 'mledmgmax', 137 => 'mlespeed', 138 => 'rgddps', 139 => 'rgddmgmin', 140 => 'rgddmgmax', 141 => 'rgdspeed' + public static $regions = array( + 'us', 'eu', 'kr', 'tw', 'cn', 'dev' ); public static $ssdMaskFields = array( @@ -331,23 +69,11 @@ 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 = '%s'; - public static $setRatingLevelString = '%s'; + public static $lvTabNoteString = '%s'; public static $filterResultString = '$$WH.sprintf(LANG.lvnote_filterresults, \'%s\')'; public static $tryFilteringString = '$$WH.sprintf(%s, %s, %s) + LANG.dash + LANG.lvnote_tryfiltering.replace(\'\', \'\')'; @@ -358,172 +84,65 @@ class Util public static $mapSelectorString = '%s (%d)'; - public static $expansionString = array( // 3 & 4 unused .. obviously - null, 'bc', 'wotlk', 'cata', 'mop' - ); - - public static $bgImagePath = array ( - 'tiny' => 'style="background-image: url(%s/images/wow/icons/tiny/%s.gif)"', - 'small' => 'style="background-image: url(%s/images/wow/icons/small/%s.jpg)"', - 'medium' => 'style="background-image: url(%s/images/wow/icons/medium/%s.jpg)"', - 'large' => 'style="background-image: url(%s/images/wow/icons/large/%s.jpg)"', - ); - - public static $configCats = array( - 'Other', 'Site', 'Caching', 'Account', 'Session', 'Site Reputation', 'Google Analytics', 'Profiler' - ); + public static $expansionString = [null, 'bc', 'wotlk']; public static $tcEncoding = '0zMcmVokRsaqbdrfwihuGINALpTjnyxtgevElBCDFHJKOPQSUWXYZ123456789'; - public static $wowheadLink = ''; private static $notes = []; - public static function addNote($uGroupMask, $str) + public static function addNote(string $note, int $uGroupMask = U_GROUP_EMPLOYEE, int $level = LOG_LEVEL_ERROR) : void { - self::$notes[] = [$uGroupMask, $str]; + self::$notes[] = [$note, $uGroupMask, $level]; } - public static function getNotes() + public static function getNotes() : array { $notes = []; + $severity = LOG_LEVEL_INFO; + foreach (self::$notes as $k => [$note, $uGroup, $level]) + { + if ($uGroup && !User::isInGroup($uGroup)) + continue; - foreach (self::$notes as $data) - if (!$data[0] || User::isInGroup($data[0])) - $notes[] = $data[1]; + if ($level < $severity) + $severity = $level; - return $notes; + $notes[] = $note; + unset(self::$notes[$k]); + } + + return [$notes, $severity]; } - private static $execTime = 0.0; - - public static function execTime($set = false) + public static function formatMoney(int $qty) : string { - if ($set) - { - self::$execTime = microTime(true); - return; - } + if ($qty <= 0) + return ''; - if (!self::$execTime) - return; + $parts = []; - $newTime = microTime(true); - $tDiff = $newTime - self::$execTime; - self::$execTime = $newTime; + if ($g = intdiv($qty, 10000)) + $parts[] = ''.$g.''; - return self::formatTime($tDiff * 1000, true); + if ($s = intdiv($qty % 10000, 100)) + $parts[] = ''.$s.''; + + if ($c = ($qty % 100)) + $parts[] = ''.$c.''; + + return implode(' ', $parts); } - public static function formatMoney($qty) + // pageTexts, questTexts and mails + public static function parseHtmlText(string|array $text, bool $markdown = false) : string|array { - $money = ''; - - if ($qty >= 10000) + if (is_array($text)) { - $g = floor($qty / 10000); - $money .= ''.$g.' '; - $qty -= $g * 10000; + foreach ($text as &$t) + $t = self::parseHtmlText($t, $markdown); + + return $text; } - if ($qty >= 100) - { - $s = floor($qty / 100); - $money .= ''.$s.' '; - $qty -= $s * 100; - } - - if ($qty > 0) - $money .= ''.$qty.''; - - return $money; - } - - public static function parseTime($sec) - { - $time = ['d' => 0, 'h' => 0, 'm' => 0, 's' => 0, 'ms' => 0]; - - if ($sec >= 3600 * 24) - { - $time['d'] = floor($sec / 3600 / 24); - $sec -= $time['d'] * 3600 * 24; - } - - if ($sec >= 3600) - { - $time['h'] = floor($sec / 3600); - $sec -= $time['h'] * 3600; - } - - if ($sec >= 60) - { - $time['m'] = floor($sec / 60); - $sec -= $time['m'] * 60; - } - - if ($sec > 0) - { - $time['s'] = (int)$sec; - $sec -= $time['s']; - } - - if (($sec * 1000) % 1000) - $time['ms'] = (int)($sec * 1000); - - return $time; - } - - public static function formatTime($base, $short = false) - { - $s = self::parseTime($base / 1000); - $fmt = []; - - if ($short) - { - if ($_ = round($s['d'] / 364)) - return $_." ".Lang::timeUnits('ab', 0); - if ($_ = round($s['d'] / 30)) - return $_." ".Lang::timeUnits('ab', 1); - if ($_ = round($s['d'] / 7)) - return $_." ".Lang::timeUnits('ab', 2); - if ($_ = round($s['d'])) - return $_." ".Lang::timeUnits('ab', 3); - if ($_ = round($s['h'])) - return $_." ".Lang::timeUnits('ab', 4); - if ($_ = round($s['m'])) - return $_." ".Lang::timeUnits('ab', 5); - if ($_ = round($s['s'] + $s['ms'] / 1000, 2)) - return $_." ".Lang::timeUnits('ab', 6); - if ($s['ms']) - return $s['ms']." ".Lang::timeUnits('ab', 7); - - return '0 '.Lang::timeUnits('ab', 6); - } - else - { - $_ = $s['d'] + $s['h'] / 24; - if ($_ > 1 && !($_ % 364)) // whole years - return round(($s['d'] + $s['h'] / 24) / 364, 2)." ".Lang::timeUnits($s['d'] / 364 == 1 && !$s['h'] ? 'sg' : 'pl', 0); - if ($_ > 1 && !($_ % 30)) // whole month - return round(($s['d'] + $s['h'] / 24) / 30, 2)." ".Lang::timeUnits($s['d'] / 30 == 1 && !$s['h'] ? 'sg' : 'pl', 1); - if ($_ > 1 && !($_ % 7)) // whole weeks - return round(($s['d'] + $s['h'] / 24) / 7, 2)." ".Lang::timeUnits($s['d'] / 7 == 1 && !$s['h'] ? 'sg' : 'pl', 2); - if ($s['d']) - return round($s['d'] + $s['h'] / 24, 2)." ".Lang::timeUnits($s['d'] == 1 && !$s['h'] ? 'sg' : 'pl', 3); - if ($s['h']) - return round($s['h'] + $s['m'] / 60, 2)." ".Lang::timeUnits($s['h'] == 1 && !$s['m'] ? 'sg' : 'pl', 4); - if ($s['m']) - return round($s['m'] + $s['s'] / 60, 2)." ".Lang::timeUnits($s['m'] == 1 && !$s['s'] ? 'sg' : 'pl', 5); - if ($s['s']) - return round($s['s'] + $s['ms'] / 1000, 2)." ".Lang::timeUnits($s['s'] == 1 && !$s['ms'] ? 'sg' : 'pl', 6); - if ($s['ms']) - return $s['ms']." ".Lang::timeUnits($s['ms'] == 1 ? 'sg' : 'pl', 7); - - return '0 '.Lang::timeUnits('pl', 6); - } - } - - // pageText for Books (Item or GO) and questText - public static function parseHtmlText($text) - { if (stristr($text, '')) // text is basically a html-document with weird linebreak-syntax { $pairs = array( @@ -531,57 +150,61 @@ class Util '' => '', '' => '', '' => '', - '

' => '
' + '

' => $markdown ? '[br]' : '
' ); // html may contain 'Pictures' and FlavorImages and "stuff" $text = preg_replace_callback( '/src="([^"]+)"/i', - function ($m) { return 'src="'.STATIC_URL.'/images/wow/'.strtr($m[1], ['\\' => '/']).'.png"'; }, + function ($m) { return sprintf('src="%s/images/wow/%s.png"', Cfg::get('STATIC_URL'), strtr($m[1], ['\\' => '/'])); }, strtr($text, $pairs) ); } else - $text = strtr($text, ["\n" => '
', "\r" => '']); + $text = strtr($text, ["\n" => $markdown ? '[br]' : '
', "\r" => '']); + + // escape fake html-ish tags the browser skipsh dishplaying ...! + $text = preg_replace('/<([^\s\/]+)>/iu', '<\1>', $text); $from = array( - '/\|T([\w]+\\\)*([^\.]+)\.blp:\d+\|t/ui', // images (force size to tiny) |T:|t - '/\|c(\w{6})\w{2}([^\|]+)\|r/ui', // color |c|r - '/\$g\s*([^:;]+)\s*:\s*([^:;]+)\s*(:?[^:;]*);/ui',// directed gender-reference $g::: - '/\$t([^;]+);/ui', // nonsense, that the client apparently ignores - '/\|\d\-?\d?\((\$\w)\)/ui', // and another modifier for something russian |3-6($r) + '/\$g\s*([^:;]*)\s*:\s*([^:;]*)\s*(:?[^:;]*);/ui',// directed gender-reference $g:: + '/\$t([^;]+);/ui', // HK rank. $t:; (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 + '/\$b/i' // line break ); - $to = array( - '', - '\2', + $toMD = array( + '<\1/\2>', + '<'.implode('/', Lang::game('pvpRank', 1)).'>', + '<\1>', + '[span class=q0>WorldState #\1[/span]', + '<'.Lang::game('class').'>', + '<'.Lang::game('race').'>', + '<'.Lang::main('name').'>', + '[br]' + ); + + $toHTML = array( '<\1/\2>', - '', - '\1', + '<'.implode('/', Lang::game('pvpRank', 1)).'>', '<\1>', - 'WorldState #\1' + 'WorldState #\1', + '<'.Lang::game('class').'>', + '<'.Lang::game('race').'>', + '<'.Lang::main('name').'>', + '
' ); - $text = preg_replace($from, $to, $text); + $text = preg_replace($from, $markdown ? $toMD : $toHTML, $text); - $pairs = array( - '$c' => '<'.Lang::game('class').'>', - '$C' => '<'.Lang::game('class').'>', - '$r' => '<'.Lang::game('race').'>', - '$R' => '<'.Lang::game('race').'>', - '$n' => '<'.Lang::main('name').'>', - '$N' => '<'.Lang::main('name').'>', - '$b' => '
', - '$B' => '
', - '|n' => '' // what .. the fuck .. another type of line terminator? (only in spanish though) - ); - - return strtr($text, $pairs); + return Lang::unescapeUISequences($text, $markdown ? Lang::FMT_MARKUP : Lang::FMT_HTML); } - public static function asHex($val) + public static function asHex(int $val) : string { $_ = decHex($val); while (fMod(strLen($_), 4)) // in 4-blocks @@ -590,17 +213,20 @@ class Util return '0x'.strToUpper($_); } - public static function asBin($val) + public static function asBin(int $val) : string { $_ = decBin($val); while (fMod(strLen($_), 4)) // in 4-blocks $_ = '0'.$_; - return 'b'.strToUpper($_); + 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) @@ -609,11 +235,14 @@ class Util return $data; } - return htmlspecialchars(trim($data), ENT_QUOTES, 'utf-8'); + 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) @@ -622,7 +251,8 @@ class Util return $data; } - return strtr(trim($data), array( + return strtr($data, array( + '/' => '\/', '\\' => '\\\\', "'" => "\\'", '"' => '\\"', @@ -631,41 +261,47 @@ class Util )); } - public static function defStatic($data) + public static function defStatic(array|string $data) : array|string { if (is_array($data)) { foreach ($data as &$v) - $v = self::defStatic($v); + if ($v) + $v = self::defStatic($v); return $data; } return strtr($data, array( - ' '' => 'scr"+"ipt>', - 'HOST_URL' => HOST_URL, - 'STATIC_URL' => STATIC_URL + 'HOST_URL' => Cfg::get('HOST_URL'), + 'STATIC_URL' => Cfg::get('STATIC_URL'), + 'NAME' => Cfg::get('NAME'), + 'NAME_SHORT' => Cfg::get('NAME_SHORT'), + 'CONTACT_EMAIL' => Cfg::get('CONTACT_EMAIL') )); } // default back to enUS if localization unavailable - public static function localizedString($data, $field, $silent = false) + public static function localizedString(array $data, string $field, bool $silent = false) : string { + // only display placeholder markers for staff + if (!User::isInGroup(U_GROUP_EMPLOYEE | U_GROUP_TESTER | U_GROUP_LOCALIZER)) + $silent = true; + // default case: selected locale available - if (!empty($data[$field.'_loc'.User::$localeId])) - return $data[$field.'_loc'.User::$localeId]; + if (!empty($data[$field.'_loc'.Lang::getLocale()->value])) + return $data[$field.'_loc'.Lang::getLocale()->value]; // locale not enUS; aowow-type localization available; add brackets if not silent - else if (User::$localeId != LOCALE_EN && !empty($data[$field.'_loc0'])) + else if (Lang::getLocale() != Locale::EN && !empty($data[$field.'_loc0'])) return $silent ? $data[$field.'_loc0'] : '['.$data[$field.'_loc0'].']'; // locale not enUS; TC localization; add brackets if not silent - else if (User::$localeId != LOCALE_EN && !empty($data[$field])) + else if (Lang::getLocale() != Locale::EN && !empty($data[$field])) return $silent ? $data[$field] : '['.$data[$field].']'; // locale enUS; TC localization; return normal - else if (User::$localeId == LOCALE_EN && !empty($data[$field])) + else if (Lang::getLocale() == Locale::EN && !empty($data[$field])) return $data[$field]; // nothing to find; be empty @@ -674,12 +310,13 @@ class Util } // for item and spells - public static function setRatingLevel($level, $type, $val) + public static function setRatingLevel(int $level, int $statId, int $val, bool $interactive = false) : string { - if (in_array($type, [ITEM_MOD_DEFENSE_SKILL_RATING, ITEM_MOD_DODGE_RATING, ITEM_MOD_PARRY_RATING, ITEM_MOD_BLOCK_RATING, ITEM_MOD_RESILIENCE_RATING]) && $level < 34) + if (in_array($statId, [Stat::DEFENSE_RTG, Stat::DODGE_RTG, Stat::PARRY_RTG, Stat::BLOCK_RTG, Stat::RESILIENCE_RTG]) && $level < 34) $level = 34; - if (!isset(self::$gtCombatRatings[$type])) + $factor = Stat::getRatingPctFactor($statId); + if (!$factor) $result = 0; else { @@ -693,115 +330,202 @@ class Util $c = 2 / 52; // do not use localized number format here! - $result = number_format($val / self::$gtCombatRatings[$type] / $c, 2); + $result = number_format($val / $factor / $c, 2); } - if (!in_array($type, array(ITEM_MOD_DEFENSE_SKILL_RATING, ITEM_MOD_EXPERTISE_RATING))) + if (!in_array($statId, [Stat::DEFENSE_RTG, Stat::EXPERTISE_RTG])) $result .= '%'; - return sprintf(Lang::item('ratingString'), ''.$result, ''.$level); + $result = Lang::item('ratingString', [$statId, $result, $level]); + + return $interactive ? sprintf(self::$setRatingLevelString, $level, $statId, $val, $result) : $result; } - public static function powerUseLocale($domain = 'www') + // default ucFirst doesn't convert UTF-8 chars (php 8.4 finally implemented this .. see ya in 2027) + public static function ucFirst(string $str) : string { - foreach (Util::$localeStrings as $k => $v) - { - if (strstr($v, $domain)) - { - User::useLocale($k); - Lang::load(User::$localeString); - return; - } - } - - if ($domain == 'www') - { - User::useLocale(LOCALE_EN); - Lang::load(User::$localeString); - } - } - - // default ucFirst doesn't convert UTF-8 chars - public static function ucFirst($str) - { - $len = mb_strlen($str) - 1; $first = mb_substr($str, 0, 1); - $rest = mb_substr($str, 1, $len); + $rest = mb_substr($str, 1); return mb_strtoupper($first).$rest; } - public static function ucWords($str) + public static function ucWords(string $str) : string { return mb_convert_case($str, MB_CASE_TITLE); } - public static function lower($str) + public static function lower(string $str) : string { return mb_strtolower($str); } - // note: valid integer > 32bit are returned as float - public static function checkNumeric(&$data, $typeCast = NUM_ANY) + 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 { if ($data === null) return false; - else if (!is_array($data)) + + if (is_array($data)) { - $data = trim($data); - if (preg_match('/^-?\d*,\d+$/', $data)) - $data = strtr($data, ',', '.'); - - if (is_numeric($data)) - { - $data += 0; // becomes float or int - - if ((is_float($data) && $typeCast == NUM_REQ_INT) || - (is_int($data) && $typeCast == NUM_REQ_FLOAT)) - return false; - - if (is_float($data) && $typeCast == NUM_CAST_INT) - $data = intval($data); - - if (is_int($data) && $typeCast == NUM_CAST_FLOAT) - $data = floatval($data); - - return true; - } - - return false; + array_walk($data, function(&$x) use($typeCast) { self::checkNumeric($x, $typeCast); }); + return false; // always false for passed arrays } - array_walk($data, function(&$x) use($typeCast) { self::checkNumeric($x, $typeCast); }); + // already in required state + if ((is_float($data) && $typeCast == NUM_REQ_FLOAT) || + (is_int($data) && $typeCast == NUM_REQ_INT)) + return true; - return false; // always false for passed arrays + // irreconcilable state + if ((!is_int($data) && $typeCast == NUM_REQ_INT) || + (!is_float($data) && $typeCast == NUM_REQ_FLOAT)) + return false; + + $number = $data; // do not transform strings, store state + $nMatches = 0; + + $number = trim($number); + $number = preg_replace('/^(-?\d*)[.,](\d+)$/', '$1.$2', $number, -1, $nMatches); + + // is float string + if ($nMatches) + { + if ($typeCast == NUM_CAST_INT) + $data = intVal($number); + else // NUM_CAST_FLOAT || NUM_ANY + $data = floatVal($number); + + return true; + } + + // is int string (is_numeric can only handle strings in base 10) + if (is_numeric($number) || preg_match('/^0[xb]?\d+/', $number)) + { + $number = intVal($number, 0); // 'base 0' auto-detects base + if ($typeCast == NUM_CAST_FLOAT) + $data = floatVal($number); + else // NUM_CAST_INT || NUM_ANY + $data = $number; + + return true; + } + + // is string string + return false; } - public static function arraySumByKey(&$ref) + public static function arraySumByKey(array &$ref, array ...$adds) : void { - $nArgs = func_num_args(); - if (!is_array($ref) || $nArgs < 2) + if (!$adds) return; - for ($i = 1; $i < $nArgs; $i++) + foreach ($adds as $arr) { - $arr = func_get_arg($i); - if (!is_array($arr)) - continue; - foreach ($arr as $k => $v) { if (!isset($ref[$k])) - $ref[$k] = 0; + { + if (is_array($v)) + $ref[$k] = []; + else if (is_numeric($v)) + $ref[$k] = 0; + else + continue; + } - $ref[$k] += $v; + if (is_array($ref[$k]) && is_array($v)) + Util::arraySumByKey($ref[$k], $v); + else if (is_numeric($ref[$k]) && is_numeric($v)) + $ref[$k] += $v; } } } - public static function isValidEmail($email) + public static function createNumRange(int $min, int $max, string $delim = '', ?callable $fn = null) : string { - return preg_match('/^([a-z0-9._-]+)(\+[a-z0-9._-]+)?(@[a-z0-9.-]+\.[a-z]{2,4})$/i', $email); + 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)) + return $_; + if ($_ = self::validateUsername($val)) + return $_; + + return ''; + } + + public static function validateUsername(?string $name, ?int &$errCode = 0) : string + { + if (is_null($name) || $name === '') + return ''; + + $errCode = 0; + $nameMatch = []; + [$min, $max, $pattern] = match(Cfg::get('ACC_AUTH_MODE')) + { + AUTH_MODE_SELF => [4, 16, '/^[a-z0-9]{4,16}$/i'], + AUTH_MODE_REALM => [3, 32, '/^[^[:cntrl:]]+$/'],// i don't think TC has character requirements on the login..? + default => [0, 0, '/^[^[:cntrl:]]+$/'] // external case with unknown requirements + }; + + if (($min && mb_strlen($name) < $min) || ($max && mb_strlen($name) > $max)) + $errCode = 1; + else if ($pattern && !preg_match($pattern, trim(urldecode($name)), $nameMatch)) + $errCode = 2; + + return $errCode ? '' : ($nameMatch[0] ?: $name); + } + + public static function validatePassword(?string $pass, ?int &$errCode = 0) : string + { + if (is_null($pass) || $pass === '') + return ''; + + $errCode = 0; + $passMatch = ''; + [$min, $max, $pattern] = match(Cfg::get('ACC_AUTH_MODE')) + { + AUTH_MODE_SELF => [6, 0, '/^[^[:cntrl:]]+$/'], + AUTH_MODE_REALM => [0, 0, '/^[^[:cntrl:]]+$/'], + default => [0, 0, '/^[^[:cntrl:]]+$/'] + }; + + if (($min && mb_strlen($pass) < $min) || ($max && mb_strlen($pass) > $max)) + $errCode = 1; + else if ($pattern && !preg_match($pattern, $pass, $passMatch)) + $errCode = 2; + + return $errCode ? '' : ($passMatch[0] ?: $pass); + } + + public static function validateEmail(?string $email) : string + { + if (is_null($email) || $email === '') + return ''; + + if (preg_match('/^([a-z0-9._-]+)(\+[a-z0-9._-]+)?(@[a-z0-9.-]+\.[a-z]{2,4})$/i', urldecode(trim($email)), $m)) + return $m[0]; + + return ''; } public static function loadStaticFile($file, &$result, $localized = false) @@ -809,8 +533,8 @@ class Util $success = true; if ($localized) { - if (file_exists('datasets/'.User::$localeString.'/'.$file)) - $result .= file_get_contents('datasets/'.User::$localeString.'/'.$file); + if (file_exists('datasets/'.Lang::getLocale()->json().'/'.$file)) + $result .= file_get_contents('datasets/'.Lang::getLocale()->json().'/'.$file); else if (file_exists('datasets/enus/'.$file)) $result .= file_get_contents('datasets/enus/'.$file); else @@ -827,7 +551,8 @@ class Util return $success; } - public static function createHash($length = 40) // just some random numbers for unsafe identifictaion purpose + // just some random numbers for unsafe identification purpose + public static function createHash(int $length = 40) : string { static $seed = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; $hash = ''; @@ -838,21 +563,17 @@ class Util return $hash; } - public static function mergeJsGlobals(&$master) + public static function mergeJsGlobals(array &$master, array ...$adds) : bool { - $args = func_get_args(); - if (count($args) < 2) // insufficient args + if (!$adds) // insufficient args return false; - if (!is_array($master)) - $master = []; - - for ($i = 1; $i < count($args); $i++) // skip first (master) entry + foreach ($adds as $arr) { - foreach ($args[$i] as $type => $data) + foreach ($arr as $type => $data) { // bad data or empty - if (empty(Util::$typeStrings[$type]) || !is_array($data) || !$data) + if (!Type::exists($type) || !is_array($data) || !$data) continue; if (!isset($master[$type])) @@ -882,26 +603,26 @@ class Util switch ($action) { case SITEREP_ACTION_REGISTER: - $x['amount'] = CFG_REP_REWARD_REGISTER; + $x['amount'] = Cfg::get('REP_REWARD_REGISTER'); break; case SITEREP_ACTION_DAILYVISIT: $x['sourceA'] = time(); - $x['amount'] = CFG_REP_REWARD_DAILYVISIT; + $x['amount'] = Cfg::get('REP_REWARD_DAILYVISIT'); break; case SITEREP_ACTION_COMMENT: if (empty($miscData['id'])) return false; $x['sourceA'] = $miscData['id']; // commentId - $x['amount'] = CFG_REP_REWARD_COMMENT; + $x['amount'] = Cfg::get('REP_REWARD_COMMENT'); break; case SITEREP_ACTION_UPVOTED: case SITEREP_ACTION_DOWNVOTED: 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, @@ -910,15 +631,15 @@ class Util $x['sourceA'] = $miscData['id']; // commentId $x['sourceB'] = $miscData['voterId']; - $x['amount'] = $action == SITEREP_ACTION_UPVOTED ? CFG_REP_REWARD_UPVOTED : CFG_REP_REWARD_DOWNVOTED; + $x['amount'] = $action == SITEREP_ACTION_UPVOTED ? Cfg::get('REP_REWARD_UPVOTED') : Cfg::get('REP_REWARD_DOWNVOTED'); break; - case SITEREP_ACTION_UPLOAD: + case SITEREP_ACTION_SUBMIT_SCREENSHOT: + case SITEREP_ACTION_SUGGEST_VIDEO: if (empty($miscData['id']) || empty($miscData['what'])) return false; $x['sourceA'] = $miscData['id']; // screenshotId or videoId - $x['sourceB'] = $miscData['what']; // screenshot:1 or video:NYD - $x['amount'] = CFG_REP_REWARD_UPLOAD; + $x['amount'] = $action == SITEREP_ACTION_SUBMIT_SCREENSHOT ? Cfg::get('REP_REWARD_SUBMIT_SCREENSHOT') : Cfg::get('REP_REWARD_SUGGEST_VIDEO'); break; case SITEREP_ACTION_GOOD_REPORT: // NYI case SITEREP_ACTION_BAD_REPORT: @@ -926,14 +647,14 @@ class Util return false; $x['sourceA'] = $miscData['id']; - $x['amount'] = $action == SITEREP_ACTION_GOOD_REPORT ? CFG_REP_REWARD_GOOD_REPORT : CFG_REP_REWARD_BAD_REPORT; + $x['amount'] = $action == SITEREP_ACTION_GOOD_REPORT ? Cfg::get('REP_REWARD_GOOD_REPORT') : Cfg::get('REP_REWARD_BAD_REPORT'); break; - case SITEREP_ACTION_ARTICLE: // NYI - if (empty($miscData['id'])) // reportId + case SITEREP_ACTION_ARTICLE: + if (empty($miscData['id'])) // guideId return false; $x['sourceA'] = $miscData['id']; - $x['amount'] = CFG_REP_REWARD_ARTICLE; + $x['amount'] = Cfg::get('REP_REWARD_ARTICLE'); break; case SITEREP_ACTION_USER_WARNED: // NYI case SITEREP_ACTION_USER_SUSPENDED: @@ -941,231 +662,48 @@ class Util return false; $x['sourceA'] = $miscData['id']; - $x['amount'] = $action == SITEREP_ACTION_USER_WARNED ? CFG_REP_REWARD_USER_WARNED : CFG_REP_REWARD_USER_SUSPENDED; + $x['amount'] = $action == SITEREP_ACTION_USER_WARNED ? Cfg::get('REP_REWARD_USER_WARNED') : Cfg::get('REP_REWARD_USER_SUSPENDED'); break; } - $x = array_merge($x, array( + $x += array( 'userId' => $user, 'action' => $action, - 'date' => !empty($miscData['date']) ? $miscData['date'] : time() - )); - - return DB::Aowow()->query('INSERT IGNORE INTO ?_account_reputation (?#) VALUES (?a)', array_keys($x), array_values($x)); - } - - public static function getServerConditions($srcType, $srcGroup = null, $srcEntry = null) - { - if (!$srcGroup && !$srcEntry) - return []; - - $result = []; - $jsGlobals = []; - - $conditions = DB::World()->select( - 'SELECT SourceTypeOrReferenceId, SourceEntry, SourceGroup, ElseGroup, - ConditionTypeOrReference, ConditionTarget, ConditionValue1, ConditionValue2, ConditionValue3, NegativeCondition - FROM conditions - WHERE SourceTypeOrReferenceId IN (?a) AND ?# = ?d - ORDER BY SourceTypeOrReferenceId, SourceEntry, SourceGroup, ElseGroup ASC', - is_array($srcType) ? $srcType : [$srcType], - $srcGroup ? 'SourceGroup' : 'SourceEntry', - $srcGroup ?: $srcEntry + 'date' => $miscData['date'] ?? time() ); - foreach ($conditions as $c) - { - switch ($c['SourceTypeOrReferenceId']) - { - case CND_SRC_SPELL_CLICK_EVENT: // 18 - case CND_SRC_VEHICLE_SPELL: // 21 - case CND_SRC_NPC_VENDOR: // 23 - $jsGlobals[TYPE_NPC][] = $c['SourceGroup']; - break; - } - - switch ($c['ConditionTypeOrReference']) - { - case CND_AURA: // 1 - $c['ConditionValue2'] = null; // do not use his param - case CND_SPELL: // 25 - $jsGlobals[TYPE_SPELL][] = $c['ConditionValue1']; - break; - case CND_ITEM: // 2 - $c['ConditionValue3'] = null; // do not use his param - case CND_ITEM_EQUIPPED: // 3 - $jsGlobals[TYPE_ITEM][] = $c['ConditionValue1']; - break; - case CND_MAPID: // 22 - break down to area or remap for use with g_zone_categories - switch ($c['ConditionValue1']) - { - case 530: // outland - $c['ConditionValue1'] = 8; - break; - case 571: // northrend - $c['ConditionValue1'] = 10; - break; - case 0: // old world is fine - case 1: - break; - default: // remap for area - $cnd = array( - ['mapId', (int)$c['ConditionValue1']], - ['parentArea', 0], // not child zones - [['cuFlags', CUSTOM_EXCLUDE_FOR_LISTVIEW, '&'], 0], - 1 // only one result - ); - $zone = new ZoneList($cnd); - if (!$zone->error) - { - $jsGlobals[TYPE_ZONE][] = $zone->getField('id'); - $c['ConditionTypeOrReference'] = CND_ZONEID; - $c['ConditionValue1'] = $zone->getField('id'); - break; - } - else - continue; - } - case CND_ZONEID: // 4 - case CND_AREAID: // 23 - $jsGlobals[TYPE_ZONE][] = $c['ConditionValue1']; - break; - case CND_REPUTATION_RANK: // 5 - $jsGlobals[TYPE_FACTION][] = $c['ConditionValue1']; - break; - case CND_SKILL: // 7 - $jsGlobals[TYPE_SKILL][] = $c['ConditionValue1']; - break; - case CND_QUESTREWARDED: // 8 - case CND_QUESTTAKEN: // 9 - case CND_QUEST_NONE: // 14 - case CND_QUEST_COMPLETE: // 28 - $jsGlobals[TYPE_QUEST][] = $c['ConditionValue1']; - break; - case CND_ACTIVE_EVENT: // 12 - $jsGlobals[TYPE_WORLDEVENT][] = $c['ConditionValue1']; - break; - case CND_ACHIEVEMENT: // 17 - $jsGlobals[TYPE_ACHIEVEMENT][] = $c['ConditionValue1']; - break; - case CND_TITLE: // 18 - $jsGlobals[TYPE_TITLE][] = $c['ConditionValue1']; - break; - case CND_NEAR_CREATURE: // 29 - $jsGlobals[TYPE_NPC][] = $c['ConditionValue1']; - break; - case CND_NEAR_GAMEOBJECT: // 30 - $jsGlobals[TYPE_OBJECT][] = $c['ConditionValue1']; - break; - case CND_CLASS: // 15 - for ($i = 0; $i < 11; $i++) - if ($c['ConditionValue1'] & (1 << $i)) - $jsGlobals[TYPE_CLASS][] = $i + 1; - break; - case CND_RACE: // 16 - for ($i = 0; $i < 11; $i++) - if ($c['ConditionValue1'] & (1 << $i)) - $jsGlobals[TYPE_RACE][] = $i + 1; - break; - case CND_OBJECT_ENTRY: // 31 - if ($c['ConditionValue1'] == 3) - $jsGlobals[TYPE_NPC][] = $c['ConditionValue2']; - else if ($c['ConditionValue1'] == 5) - $jsGlobals[TYPE_OBJECT][] = $c['ConditionValue2']; - break; - case CND_TEAM: // 6 - if ($c['ConditionValue1'] == 469) // Alliance - $c['ConditionValue1'] = 1; - else if ($c['ConditionValue1'] == 67) // Horde - $c['ConditionValue1'] = 2; - else - continue; - } - - $res = [$c['NegativeCondition'] ? -$c['ConditionTypeOrReference'] : $c['ConditionTypeOrReference']]; - foreach ([1, 2, 3] as $i) - if (($_ = $c['ConditionValue'.$i]) || $c['ConditionTypeOrReference'] = CND_DISTANCE_TO) - $res[] = $_; - - $group = $c['SourceEntry']; - if (!in_array($c['SourceTypeOrReferenceId'], [CND_SRC_CREATURE_TEMPLATE_VEHICLE, CND_SRC_SPELL, CND_SRC_QUEST_ACCEPT, CND_SRC_QUEST_SHOW_MARK, CND_SRC_SPELL_PROC])) - $group = $c['SourceEntry'] . ':' . $c['SourceGroup']; - - $result[$c['SourceTypeOrReferenceId']] [$group] [$c['ElseGroup']] [] = $res; - } - - return [$result, $jsGlobals]; - } - - public static function sendNoCacheHeader() - { - header('Expires: Sat, 01 Jan 2000 01:00:00 GMT'); - header('Last-Modified: ' . gmdate('D, d M Y H:i:s') . ' GMT'); - header('Cache-Control: no-store, no-cache, must-revalidate'); - header('Cache-Control: post-check=0, pre-check=0', false); - header('Pragma: no-cache'); + return DB::Aowow()->qry('INSERT IGNORE INTO ::account_reputation %v', $x); } public static function toJSON($data, $forceFlags = 0) { $flags = $forceFlags ?: (JSON_NUMERIC_CHECK | JSON_UNESCAPED_UNICODE); - if (CFG_DEBUG && !$forceFlags) + if (Cfg::get('DEBUG') && !$forceFlags) $flags |= JSON_PRETTY_PRINT; $json = json_encode($data, $flags); // handle strings prefixed with $ as js-variables // literal: match everything (lazy) between first pair of unescaped double quotes. First character must be $. - $json = preg_replace_callback('/(? str_replace('\"', '"', $m[1]), $json); return $json; } - public static function createSqlBatchInsert(array $data) - { - $nRows = 100; - $nItems = count(reset($data)); - $result = []; - $buff = []; - - if (!count($data)) - return []; - - foreach ($data as $d) - { - if (count($d) != $nItems) - return []; - - $d = array_map(function ($x) { - if ($x === null) - return 'NULL'; - - return 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; + + $parentDir = mb_substr($file, 0, mb_strrpos($file, '/')); + if (!self::writeDir($parentDir)) + return false; + if ($handle = @fOpen($file, "w")) { if (fWrite($handle, $content)) @@ -1179,25 +717,32 @@ class Util trigger_error('could not create file', E_USER_ERROR); if ($success) - @chmod($file, Util::FILE_ACCESS); + @chmod($file, self::FILE_ACCESS); return $success; } - public static function writeDir($dir) + public static function writeDir(string $dir, bool &$exist = true) : bool { - // remove multiple slashes - $dir = preg_replace('|/+|', '/', $dir); + // remove multiple slashes; trailing slashes + $dir = preg_replace(['/\/+/', '/\/$/'], ['/', ''], $dir) ?: '.'; + $exist = is_dir($dir); - if (is_dir($dir)) + if ($exist) { - if (!is_writable($dir) && !@chmod($dir, Util::FILE_ACCESS)) - trigger_error('cannot write into directory', E_USER_ERROR); + if (fileperms($dir) != self::DIR_ACCESS && !@chmod($dir, self::DIR_ACCESS)) + trigger_error(CLI::bold($dir) . ' may be inaccessible to the web service.', E_USER_WARNING); return is_writable($dir); } - if (@mkdir($dir, Util::FILE_ACCESS, true)) + // apparently chmod can't edit a whole path at once + $path = ''; + foreach(explode('/', $dir) as $segment) + if (is_dir($path .= $segment.'/') && fileperms($path) != self::DIR_ACCESS) + @chmod($path, self::DIR_ACCESS); + + if (@mkdir($dir, self::DIR_ACCESS, true)) return true; trigger_error('could not create directory', E_USER_ERROR); @@ -1209,8 +754,11 @@ class Util /* Good Skill */ /**************/ - public static function getEquipmentScore($itemLevel, $quality, $slot, $nSockets = 0) + public static function getEquipmentScore(int $itemLevel, int $quality, int $slot, int $nSockets = 0) : float { + if ($itemLevel < 0) // can this even happen? + $itemLevel = 0; + $score = $itemLevel; // quality mod @@ -1282,21 +830,21 @@ class Util // subtract sockets if ($nSockets) { - // items by expantion overlap in this range. luckily highlevel raid items are exclusivly epic or better + // items by expansion overlap in this range. luckily highlevel raid items are exclusivly epic or better if ($itemLevel > 164 || ($itemLevel > 134 && $quality < ITEM_QUALITY_EPIC)) $score -= $nSockets * self::GEM_SCORE_BASE_WOTLK; else $score -= $nSockets * self::GEM_SCORE_BASE_BC; } - return round(max(0.0, $score), 4); + return round($score, 4); } - public static function getGemScore($itemLevel, $quality, $profSpec = false, $itemId = 0) + public static function getGemScore(int $itemLevel, int $quality, bool $profSpec = false, int $itemId = 0) : float { // 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) @@ -1336,8 +884,11 @@ class Util return 0.0; } - public static function getEnchantmentScore($itemLevel, $quality, $profSpec = false, $idOverride = 0) + public static function getEnchantmentScore(int $itemLevel, int $quality, bool $profSpec = false, int $idOverride = 0) : float { + if ($itemLevel < 0) // can this even happen? + $itemLevel = 0; + // some hardcoded values, that defy lookups (cheaper but not skillbound profession versions of spell threads, leg armor) if (in_array($idOverride, [3327, 3328, 3872, 3873])) return 20.0; @@ -1346,28 +897,30 @@ class Util return 40.0; // other than the constraints (0 - 20 points; 40 for profession perks), everything in here is guesswork - $score = max(min($itemLevel, 80), 0); + $score = min($itemLevel, 80); switch ($quality) { case ITEM_QUALITY_HEIRLOOM: // because i say so! - $score = 80.0; + $score = 20.0; break; case ITEM_QUALITY_RARE: - $score /= 1.2; + $score /= 4.8; break; case ITEM_QUALITY_UNCOMMON: - $score /= 1.6; + $score /= 6.4; break; case ITEM_QUALITY_NORMAL: - $score /= 2.5; + $score /= 10.0; break; + default: + $score /= 4.0; } - return round(max(0.0, $score / 4), 4); + return round($score, 4); } - public static function fixWeaponScores($class, $talents, $mainHand, $offHand) + public static function fixWeaponScores(int $class, array $talents, array $mainHand, array $offHand) : array { $mh = 1; $oh = 1; @@ -1411,10 +964,203 @@ class Util } return array( - round($mainHand['gearscore'] * $mh), - round($offHand['gearscore'] * $oh) + round(($mainHand['gearscore'] ?? 0) * $mh), + round(($offHand['gearscore'] ?? 0) * $oh) ); } + + // orientation is 2*M_PI for a full circle, increasing counterclockwise + public static function O2Deg($o) + { + // orientation values can exceed boundaries (for whatever reason) + while ($o < 0) + $o += 2*M_PI; + + while ($o >= 2*M_PI) + $o -= 2*M_PI; + + $deg = 360 * (1 - ($o / (2*M_PI) ) ); + if ($deg == 360) + $deg = 0; + + $dir = Lang::game('orientation'); + $desc = ''; + foreach ($dir as $f => $d) + { + if (!$f) + continue; + + if ( ($deg >= (45 * $f) - 22.5) && ($deg <= (45 * $f) + 22.5) ) + { + $desc = $d; + break; + } + } + + if (!$desc) + $desc = $dir[0]; + + return [(int)$deg, $desc]; + } + + public static function mask2bits(int $bitmask, int $offset = 0) : array + { + $bits = []; + $i = 0; + while ($bitmask) + { + if ($bitmask & (1 << $i)) + { + $bitmask &= ~(1 << $i); + $bits[] = ($i + $offset); + } + $i++; + } + + 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); + if (!$points || count($points) < 2) + return []; + + $floors = []; + $menu = [[null, "Move Location to..."]]; + foreach ($points as $p) + { + if ($p['multifloor']) + $floors[$p['areaId']][] = $p['floor']; + + if (isset($menu[$p['areaId']])) + continue; + else if ($p['areaId'] == $parentArea) + $menu[$p['areaId']] = [$p['areaId'], '$g_zones['.$p['areaId'].']', '', null, ['class' => 'checked q0']]; + else + $menu[$p['areaId']] = [$p['areaId'], '$g_zones['.$p['areaId'].']', '$spawnposfix.bind(null, '.$type.', '.$guid.', '.$p['areaId'].', 0)', null, null]; + } + + foreach ($floors as $area => $f) + { + $menu[$area][MENU_IDX_URL] = null; + $menu[$area][MENU_IDX_SUB] = []; + if ($menu[$area][MENU_IDX_OPT]) + $menu[$area][MENU_IDX_OPT]['class'] = 'checked'; + + foreach ($f as $n) + { + if ($n == $parentFloor) + $menu[$area][MENU_IDX_SUB][] = [$n, '$g_zone_areas['.$area.']['.($n - 1).']', '', null, ['class' => 'checked q0']]; + else + $menu[$area][MENU_IDX_SUB][] = [$n, '$g_zone_areas['.$area.']['.($n - 1).']', '$spawnposfix.bind(null, '.$type.', '.$guid.', '.$area.', '.$n.')']; + } + } + + return array_values($menu); + } + + public static function sendMail(string $email, string $tplFile, array $vars = [], int $expiration = 0) : bool + { + if (!self::validateEmail($email)) + return false; + + $template = ''; + if (file_exists('template/mails/'.$tplFile.'_'.User::$preferedLoc->value.'.tpl')) + $template = file_get_contents('template/mails/'.$tplFile.'_'.User::$preferedLoc->value.'.tpl'); + else + { + foreach (Locale::cases() as $l) + { + if (!$l->validate() || !file_exists('template/mails/'.$tplFile.'_'.$l->value.'.tpl')) + continue; + + $template = file_get_contents('template/mails/'.$tplFile.'_'.$l->value.'.tpl'); + break; + } + } + + if (!$template) + { + trigger_error('Util::SendMail() - mail template not found: '.$tplFile, E_USER_ERROR); + return false; + } + + [, $subject, $body] = explode("\n", $template, 3); + + $body = Util::defStatic($body); + + if ($expiration) + { + $vars += array_fill(0, 9, null); // vsprintf requires all unused indizes to also be set... + $vars[9] = DateTime::formatTimeElapsed($expiration * 1000, 0); + } + + if ($vars) + $body = vsprintf($body, $vars); + + $subject = Cfg::get('NAME_SHORT').Lang::main('colon') . $subject; + $header = 'From: ' . Cfg::get('CONTACT_EMAIL') . "\n" . + 'Reply-To: ' . Cfg::get('CONTACT_EMAIL') . "\n" . + 'X-Mailer: PHP/' . phpversion(); + + if (Cfg::get('DEBUG') >= LOG_LEVEL_INFO) + { + Util::addNote("Redirected from Util::sendMail:\n\nTo: " . $email . "\n\nSubject: " . $subject . "\n\n" . $body, U_GROUP_NONE, LOG_LEVEL_INFO); + return true; + } + + return mail($email, $subject, $body, $header); + } } ?> diff --git a/index.php b/index.php index 66bc986c..6be334f8 100644 --- a/index.php +++ b/index.php @@ -1,148 +1,96 @@ maintenance(); - - -$altClass = ''; -switch ($pageCall) +$pageCall = 'home'; // default to Homepage unless specified otherwise +$pageParam = ''; +parse_str(parse_url($_SERVER['REQUEST_URI'], PHP_URL_QUERY), $query); +foreach ($query as $page => $param) { - /* called by user */ - case '': // no parameter given -> MainPage - $altClass = 'home'; - case 'home': - case 'admin': - case 'account': // account management [nyi] - case 'achievement': - case 'achievements': - case 'arena-team': - case 'arena-teams': - case 'class': - case 'classes': - case 'currency': - case 'currencies': - case 'compare': // tool: item comparison - case 'emote': - case 'emotes': - case 'enchantment': - case 'enchantments': - case 'event': - case 'events': - case 'faction': - case 'factions': - case 'guild': - case 'guilds': - case 'icon': - case 'icons': - case 'item': - case 'items': - case 'itemset': - case 'itemsets': - case 'maps': // tool: map listing - case 'npc': - case 'npcs': - case 'object': - case 'objects': - case 'pet': - case 'pets': - case 'petcalc': // tool: pet talent calculator - if ($pageCall == 'petcalc') - $altClass = 'talent'; - case 'profile': // character profiler [nyi] - case 'profiles': // character profile listing [nyi] - case 'profiler': // character profiler main page - case 'quest': - case 'quests': - case 'race': - case 'races': - case 'screenshot': // prepare uploaded screenshots - case 'search': // tool: searches - case 'skill': - case 'skills': - case 'sound': - case 'sounds': - case 'spell': - case 'spells': - case 'talent': // tool: talent calculator - case 'title': - case 'titles': - case 'user': - case 'video': - case 'zone': - case 'zones': - /* called by script */ - case 'data': // tool: dataset-loader - case 'cookie': // lossless cookies and user settings - case 'contactus': - case 'comment': - case 'filter': // pre-evaluate filter POST-data; sanitize and forward as GET-data - case 'go-to-comment': // find page the comment is on and forward - case 'locale': // subdomain-workaround, change the language - $cleanName = str_replace(['-', '_'], '', ucFirst($altClass ?: $pageCall)); - try // can it be handled as ajax? - { - $class = 'Ajax'.$cleanName; - $ajax = new $class(explode('.', $pageParam)); - if ($ajax->handle($out)) - { - Util::sendNoCacheHeader(); + // could be an array + if (!is_string($param)) + { + $pageCall = ''; // just .. fail + break; + } - if ($ajax->doRedirect) - header('Location: '.$out, true, 302); - else - { - header('Content-type: '.$ajax->getContentType()); - die($out); - } - } - else - throw new Exception('not handled as ajax'); - } - catch (Exception $e) // no, apparently not.. - { - $class = $cleanName.'Page'; - (new $class($pageCall, $pageParam))->display(); - } + // 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; + } - break; - /* other pages */ - case 'whats-new': - case 'searchplugins': - case 'searchbox': - case 'tooltips': - case 'help': - case 'faq': - case 'aboutus': - case 'reputation': - case 'privilege': - case 'privileges': - case 'top-users': - (new MorePage($pageCall, $pageParam))->display(); - break; - case 'latest-additions': - case 'latest-articles': - case 'latest-comments': - case 'latest-screenshots': - case 'latest-videos': - case 'unrated-comments': - case 'missing-screenshots': - case 'most-comments': - case 'random': - (new UtilityPage($pageCall, $pageParam))->display(); - break; - default: // unk parameter given -> ErrorPage - if (isset($_GET['power'])) - die('$WowheadPower.register(0, '.User::$localeId.', {})'); - else // in conjunction with a proper rewriteRule in .htaccess... - (new GenericPage($pageCall))->error(); - break; + $pageCall = preg_replace('/[^\w\-]/i', '', $page); + $pageParam = $param ?? ''; + break; // only use first k/v-pair to determine page +} + +[$classMod, $file] = match (true) +{ + // is search ajax + isset($_GET['json']) => ['Json', $pageCall . '_json' ], + isset($_GET['opensearch']) => ['Open', $pageCall . '_open' ], + // is powered tooltip + isset($_GET['power']) => ['Power', $pageCall . '_power' ], + // is item data xml dump + isset($_GET['xml']) => ['Xml', $pageCall . '_xml' ], + // is community content feed + isset($_GET['rss']) => ['Rss', $pageCall . '_rss' ], + // is sounds playlist + isset($_GET['playlist']) => ['Playlist', $pageCall . '_playlist'], + // pageParam can be sub page + (bool)preg_match('/^[a-z\-]+$/i', $pageParam) => [Util::ucFirst(strtr($pageParam, ['-' => ''])), Util::lower($pageParam)], + // no pageParam or PageParam is param for BasePage + default => ['Base', $pageCall ] +}; + +// admin=X pages are mixed html and ajax on the same endpoint .. meh +if ($pageCall == 'admin' && isset($_GET['action']) && preg_match('/^[a-z]+$/', $_GET['action'])) +{ + $classMod .= 'Action' . Util::ucFirst($_GET['action']); + $file .= '_' . Util::lower($_GET['action']); +} + +try { + $responder = new \StdClass; + + // 1. try specialized response + if (file_exists('endpoints/'.$pageCall.'/'.$file.'.php') && $pageCall != $file) + { + require_once 'endpoints/'.$pageCall.'/'.$file.'.php'; + + $class = __NAMESPACE__.'\\' . Util::ucFirst(strtr($pageCall, ['-' => ''])).$classMod.'Response'; + $responder = new $class($pageParam); + } + // 2. try generalized response + else if (file_exists('endpoints/'.$pageCall.'/'.$pageCall.'.php')) + { + require_once 'endpoints/'.$pageCall.'/'.$pageCall.'.php'; + + $class = __NAMESPACE__.'\\' . Util::ucFirst(strtr($pageCall, ['-' => ''])).'BaseResponse'; + $responder = new $class($pageParam); + } + // 3. throw .. your hands in the air and give up + if (!is_callable([$responder, 'process'])) + throw new \Exception('request handler '.$pageCall.'::'.$classMod.'('.$pageParam.') not found'); + + $responder->process(); +} +catch (\Exception $e) +{ + if (isset($_GET['json']) || isset($_GET['opensearch']) || isset($_GET['power']) || isset($_GET['xml']) || isset($_GET['rss'])) + (new TextResponse($pageParam))->generate404(); + else + (new TemplateResponse($pageParam))->generateError($pageCall); } ?> 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 @@ +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 c699079b..7579c1de 100644 --- a/localization/lang.class.php +++ b/localization/lang.class.php @@ -1,20 +1,29 @@ json().'.php')) + die('File for locale '.$loc->name.' not found.'); else - require 'localization/locale_'.$loc.'.php'; + require 'localization/locale_'.$loc->json().'.php'; foreach ($lang as $k => $v) self::$$k = $v; // *cough* .. reuse-hacks (because copy-pastaing text for 5 locales sucks) - self::$item['cat'][2] = [self::$item['cat'][2], self::$spell['weaponSubClass']]; + self::$item['cat'][2][1] = self::$spell['weaponSubClass']; self::$item['cat'][2][1][14] .= ' ('.self::$item['cat'][2][0].')'; - - // not localized .. for whatever reason - self::$profiler['regions'] = array( - 'eu' => "Europe", - 'us' => "US & Oceanic" - ); - self::$main['moreTitles']['privilege'] = self::$privileges['_privileges']; + + self::$locale = $loc; } - public static function __callStatic($prop, $args) + public static function getLocale() : Locale + { + return self::$locale; + } + + public static function __callStatic(string $prop, ?array $args = []) : string|array|null + { + $vspfArgs = []; + foreach ($args as $i => $arg) + { + if (!is_array($arg)) + continue; + + $vspfArgs = $arg; + unset($args[$i]); + } + + if (($x = self::exist($prop, ...$args)) !== null) + return self::vspf($x, $vspfArgs); + + $dbt = debug_backtrace()[0]; + $file = explode(DIRECTORY_SEPARATOR, $dbt['file']); + trigger_error('Lang - undefined property Lang::$'.$prop.'[\''.implode('\'][\'', $args).'\'], called in '.array_pop($file).':'.$dbt['line'], E_USER_WARNING); + + return null; + } + + public static function exist(string $prop, string ...$args) : string|array|null { if (!isset(self::$$prop)) - { - trigger_error('Lang - tried to use undefined property Lang::$'.$prop, E_USER_WARNING); return null; - } - $vspfArgs = []; - - $var = self::$$prop; - foreach ($args as $arg) + $ref = self::$$prop; + foreach ($args as $a) { - if (is_array($arg)) - { - $vspfArgs = $arg; - continue; - } - else if (!isset($var[$arg])) - { - trigger_error('Lang - undefined key "'.$arg.'" in property Lang::$'.$prop.'[\''.implode('\'][\'', $args).'\']', E_USER_WARNING); + if (!isset($ref[$a])) return null; - } - $var = $var[$arg]; + $ref = $ref[$a]; } - // meh :x - if ($var === null && $prop == 'spell' && count($args) == 1) - { - if ($args[0] == 'effects') - $var = self::$$prop['unkEffect']; - else if ($args[0] == 'auras') - $var = self::$$prop['unkAura']; - } - - return self::vspf($var, $vspfArgs); + return $ref; } - public static function concat($args, $useAnd = true, $callback = null) + public static function concat(array $args, int $concat = self::CONCAT_AND, ?callable $callback = null) : string { - $b = ''; - $i = 0; - $n = count($args); - foreach ($args as $k => $arg) + $buff = ''; + $callback ??= fn($x) => $x; + + reset($args); + + if (count($args) < 2) + return $callback(current($args), key($args)); + + do { - if (is_callable($callback)) - $b .= $callback($arg, $k); + $item = $callback(current($args), key($args)); + $arg = next($args); + + if ($arg !== false || $concat == self::CONCAT_NONE) + $buff .= ', '.$item; + else if ($concat == self::CONCAT_AND) + $buff .= self::main('and').$item; else - $b .= $arg; + $buff .= self::main('or').$item; + } + while ($arg !== false); - if ($n > 1 && $i < ($n - 2)) - $b .= ', '; - else if ($n > 1 && $i == $n - 2) - $b .= Lang::main($useAnd ? 'and' : 'or'); + return substr($buff, 2); + } + // truncate string after X chars. If X is inside a word truncate behind it. + public static function trimTextClean(string $text, int $len = 100) : string + { + // remove line breaks + $text = strtr($text, ["\n" => ' ', "\r" => ' ']); + + // limit whitespaces to one at a time + $text = preg_replace('/\s+/', ' ', trim($text)); + + if ($len <= 0 || mb_strlen($text) <= $len) + return $text; + + $n = 0; + $b = []; + $parts = explode(' ', $text); + while ($n < $len && $parts) + { + $_ = array_shift($parts); + $n += mb_strlen($_); + $b[] = $_; + } + + return implode(' ', $b).'…'; + } + + // add line breaks to string after X chars. If X is inside a word break behind it. + public static function breakTextClean(string $text, int $len = 30, int $fmt = self::FMT_HTML) : string + { + // remove line breaks + $text = strtr($text, ["\n" => ' ', "\r" => ' ']); + + // limit whitespaces to one at a time + $text = preg_replace('/\s+/', ' ', trim($text)); + + if ($len <= 0 || mb_strlen($text) <= $len) + return $text; + + $row = []; + $i = 0; + $n = 0; + foreach (explode(' ', $text) as $p) + { + $row[$i][] = $p; + $n += (mb_strlen($p) + 1); + + if ($n < $len) + continue; + + $n = 0; $i++; } + foreach ($row as &$r) + $r = implode(' ', $r); - return $b; + $separator = match ($fmt) + { + self::FMT_HTML => '
', + self::FMT_MARKUP => '[br]', + self::FMT_RAW => "\n", + default => "\n" + }; + + return implode($separator, $row); } - public static function sort($prop, $group, $method = SORT_NATURAL) + public static function sort(string $prop, string $group, int $method = SORT_NATURAL) : void { if (!isset(self::$$prop)) { trigger_error('Lang::sort - tried to use undefined property Lang::$'.$prop, E_USER_WARNING); - return null; + return; } $var = &self::$$prop; if (!isset($var[$group])) { trigger_error('Lang::sort - tried to use undefined property Lang::$'.$prop.'[\''.$group.'\']', E_USER_WARNING); - return null; + return; } asort($var[$group], $method); } // todo: expand - public static function getInfoBoxForFlags($flags) + public static function getInfoBoxForFlags(int $cuFlags) : array { $tmp = []; - if ($flags & CUSTOM_DISABLED) - $tmp[] = '[tooltip name=disabledHint]'.Util::jsEscape(self::main('disabledHint')).'[/tooltip][span class=tip tooltip=disabledHint]'.Util::jsEscape(self::main('disabled')).'[/span]'; + if ($cuFlags & CUSTOM_DISABLED) + $tmp[] = '[tooltip name=disabledHint]'.self::main('disabledHint').'[/tooltip][span class=tip tooltip=disabledHint]'.self::main('disabled').'[/span]'; - if ($flags & CUSTOM_SERVERSIDE) - $tmp[] = '[tooltip name=serversideHint]'.Util::jsEscape(self::main('serversideHint')).'[/tooltip][span class=tip tooltip=serversideHint]'.Util::jsEscape(self::main('serverside')).'[/span]'; + if ($cuFlags & CUSTOM_SERVERSIDE) + $tmp[] = '[tooltip name=serversideHint]'.self::main('serversideHint').'[/tooltip][span class=tip tooltip=serversideHint]'.self::main('serverside').'[/span]'; - if ($flags & CUSTOM_UNAVAILABLE) + if ($cuFlags & CUSTOM_UNAVAILABLE) $tmp[] = self::main('unavailable'); - if ($flags & CUSTOM_EXCLUDE_FOR_LISTVIEW && User::isInGroup(U_GROUP_STAFF)) + if ($cuFlags & CUSTOM_EXCLUDE_FOR_LISTVIEW && User::isInGroup(U_GROUP_STAFF)) $tmp[] = '[tooltip name=excludedHint]This entry is excluded from lists and is not searchable.[/tooltip][span tooltip=excludedHint class="tip q10"]Hidden[/span]'; return $tmp; } - public static function getLocks($lockId, $interactive = false) + public static function getLocks(int $lockId, ?array &$ids = [], bool $interactive = false, int $fmt = self::FMT_HTML) : array { $locks = []; - $lock = DB::Aowow()->selectRow('SELECT * FROM ?_lock WHERE id = ?d', $lockId); + $ids = []; + $lock = DB::Aowow()->selectRow('SELECT * FROM ::lock WHERE `id` = %i', $lockId); if (!$lock) return $locks; @@ -173,60 +267,100 @@ class Lang $rank = $lock['reqSkill'.$i]; $name = ''; - if ($lock['type'.$i] == 1) // opened by item + switch ($lock['type'.$i]) { - $name = ItemList::getName($prop); - if (!$name) - continue; + case LOCK_TYPE_ITEM: + $name = ItemList::getName($prop); + if (!$name) + continue 2; - if ($interactive) - $name = ''.$name.''; - } - else if ($lock['type'.$i] == 2) // opened by skill - { - // exclude unusual stuff - if (!in_array($prop, [1, 2, 3, 4, 9, 16, 20])) - continue; - - $name = self::spell('lockType', $prop); - if (!$name) - continue; - - if ($interactive) - { - $skill = 0; - switch ($prop) + if ($fmt == self::FMT_HTML) + $name = $interactive ? ''.$name.'' : ''.$name.''; + else if ($interactive && $fmt == self::FMT_MARKUP) { - case 1: $skill = 633; break; // Lockpicking - case 2: $skill = 182; break; // Herbing - case 3: $skill = 186; break; // Mining - case 20: $skill = 773; break; // Scribing + $name = '[item='.$prop.']'; + $ids[Type::ITEM][] = $prop; } - if ($skill) - $name = ''.$name.''; - } + break; + case LOCK_TYPE_SKILL: + $name = self::spell('lockType', $prop); + if (!$name) + continue 2; - if ($rank > 0) - $name .= ' ('.$rank.')'; + // 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 ? ''.$name.'' : ''.$name.''; + 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 ? ''.$name.'' : ''.$name.''; + 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 + continue 2; + break; + case LOCK_TYPE_SPELL: + $name = SpellList::getName($prop); + if (!$name) + continue 2; + + if ($fmt == self::FMT_HTML) + $name = $interactive ? ''.$name.'' : ''.$name.''; + else if ($interactive && $fmt == self::FMT_MARKUP) + { + $name = '[spell='.$prop.']'; + $ids[Type::SPELL][] = $prop; + } + + break; + default: + continue 2; } - else - continue; - $locks[$lock['type'.$i] == 1 ? $prop : -$prop] = sprintf(self::game('requires'), $name); + $locks[$lock['type'.$i] == LOCK_TYPE_ITEM ? $prop : -$prop] = $name; } return $locks; } - public static function getReputationLevelForPoints($pts) + public static function getReputationLevelForPoints(int $pts) : string { - $_ = Game::getReputationLevelForPoints($pts); - - return self::game('rep', $_); + return self::game('rep', Game::getReputationLevelForPoints($pts)); } - public static function getRequiredItems($class, $mask, $short = true) + public static function getRequiredItems(int $class, int $mask, bool $short = true) : string { if (!in_array($class, [ITEM_CLASS_MISC, ITEM_CLASS_ARMOR, ITEM_CLASS_WEAPON])) return ''; @@ -252,7 +386,7 @@ class Lang } if ($class == ITEM_CLASS_MISC) // yeah hardcoded.. sue me! - return self::spell('cat', -5); + return self::spell('cat', -5, 0); $tmp = []; $strs = self::spell($class == ITEM_CLASS_ARMOR ? 'armorSubClass' : 'weaponSubClass'); @@ -268,7 +402,7 @@ class Lang return implode(', ', $tmp); } - public static function getStances($stanceMask) + public static function getStances(int $stanceMask) : string { $stanceMask &= 0xFF37F6FF; // clamp to available stances/forms.. @@ -288,12 +422,18 @@ class Lang return implode(', ', $tmp); } - public static function getMagicSchools($schoolMask) + public static function getMagicSchools(int $schoolMask, bool $short = false) : string { $schoolMask &= SPELL_ALL_SCHOOLS; // clamp to available schools.. $tmp = []; $i = 0; + if ($short && $schoolMask == SPELL_ALL_SCHOOLS) + return self::main('all'); + + if ($short && $schoolMask == SPELL_MAGIC_SCHOOLS) + return self::main('all').' ('.self::game('dt', 1).')'; + while ($schoolMask) { if ($schoolMask & (1 << $i)) @@ -307,186 +447,362 @@ class Lang return implode(', ', $tmp); } - public static function getClassString($classMask, &$ids = [], &$n = 0, $asHTML = true) + public static function getClassString(int $classMask, array &$ids = [], int $fmt = self::FMT_HTML) : string { - $classMask &= CLASS_MASK_ALL; // clamp to available classes.. + $classMask &= ChrClass::MASK_ALL; // clamp to available classes.. - if ($classMask == CLASS_MASK_ALL) // available to all classes - return false; + if (!$classMask || $classMask == ChrClass::MASK_ALL)// available to all classes + return ''; - $tmp = []; - $i = 1; - $base = $asHTML ? '%2$s' : '[class=%d]'; - $br = $asHTML ? '' : '[br]'; - - while ($classMask) + [$base, $br] = match ($fmt) { - if ($classMask & (1 << ($i - 1))) - { - $tmp[$i] = (!fMod(count($tmp) + 1, 3) ? $br : null).sprintf($base, $i, self::game('cl', $i)); - $classMask &= ~(1 << ($i - 1)); - } - $i++; - } + self::FMT_HTML => ['%2$s', ''], + self::FMT_MARKUP => ['[class=%1$d]', '[br]'], + self::FMT_RAW => ['%2$s', ''], + default => ['%2$s', ''] + }; + + $tmp = []; + foreach (ChrClass::fromMask($classMask) as $c) + $tmp[$c] = (!fMod(count($tmp) + 1, 3) ? $br : '').sprintf($base, $c, self::game('cl', $c)); - $n = count($tmp); $ids = array_keys($tmp); return implode(', ', $tmp); } - public static function getRaceString($raceMask, &$side = 0, &$ids = [], &$n = 0, $asHTML = true) + public static function getRaceString(int $raceMask, array &$ids = [], int $fmt = self::FMT_HTML) : string { - $raceMask &= RACE_MASK_ALL; // clamp to available races.. + $raceMask &= ChrRace::MASK_ALL; // clamp to available races.. - if ($raceMask == RACE_MASK_ALL) // available to all races (we don't display 'both factions') - return false; + if (!$raceMask || $raceMask == ChrRace::MASK_ALL) // available to all races (we don't display 'both factions') + return ''; - $tmp = []; - $i = 1; - $base = $asHTML ? '%s' : '[race=%d]'; - $br = $asHTML ? '' : '[br]'; - - if (!$raceMask) - { - $side |= SIDE_BOTH; - return self::game('ra', 0); - } - - if ($raceMask & RACE_MASK_HORDE) - $side |= SIDE_HORDE; - - if ($raceMask & RACE_MASK_ALLIANCE) - $side |= SIDE_ALLIANCE; - - if ($raceMask == RACE_MASK_HORDE) + if ($raceMask == ChrRace::MASK_HORDE) return self::game('ra', -2); - if ($raceMask == RACE_MASK_ALLIANCE) + if ($raceMask == ChrRace::MASK_ALLIANCE) return self::game('ra', -1); - while ($raceMask) + [$base, $br] = match ($fmt) { - if ($raceMask & (1 << ($i - 1))) - { - $tmp[$i] = (!fMod(count($tmp) + 1, 3) ? $br : null).sprintf($base, $i, self::game('ra', $i)); - $raceMask &= ~(1 << ($i - 1)); - } - $i++; - } + self::FMT_HTML => ['%2$s', ''], + self::FMT_MARKUP => ['[race=%1$d]', '[br]'], + self::FMT_RAW => ['%2$s', ''], + default => ['%2$s', ''] + }; + + $tmp = []; + foreach (ChrRace::fromMask($raceMask) as $r) + $tmp[$r] = (!fMod(count($tmp) + 1, 3) ? $br : '').sprintf($base, $r, self::game('ra', $r)); - $n = count($tmp); $ids = array_keys($tmp); return implode(', ', $tmp); } - public static function nf($number, $decimals = 0, $no1k = false) + public static function formatSkillBreakpoints(array $bp, int $fmt = self::FMT_MARKUP) : string { - // [decimal, thousand] - $seps = array( - LOCALE_EN => [',', '.'], - LOCALE_FR => [' ', ','], - LOCALE_DE => ['.', ','], - LOCALE_ES => ['.', ','], - LOCALE_RU => [' ', ','] - ); + $tmp = self::game('difficulty'); - return number_format($number, $decimals, $seps[User::$localeId][1], $no1k ? '' : $seps[User::$localeId][0]); + $base = match ($fmt) + { + self::FMT_HTML => '%2$s ', + self::FMT_MARKUP => '[color=r%1$d]%2$s[/color] ', + self::FMT_RAW => '%2$s ', + default => '%2$s ' + }; + + for ($i = 0; $i < 4; $i++) + if (!empty($bp[$i])) + $tmp .= sprintf($base, $i + 1, $bp[$i]); + + return trim($tmp); } - private static function vspf($var, $args) + public static function nf(float $number, int $decimals = 0, bool $no1k = false) : string + { + return number_format($number, $decimals, self::main('nfSeparators', 1), $no1k ? '' : self::main('nfSeparators', 0)); + } + + public static function typeName(int $type) : string + { + return Util::ucFirst(self::game(Type::getFileString($type))); + } + + public static function formatTime(int $msec, string $prop = 'game', string $src = 'timeAbbrev', bool $concat = false) : string + { + if ($msec < 0) + $msec = 0; + + $time = DateTime::parse($msec); // [$ms, $s, $m, $h, $d] + $mult = [0, 1000, 60, 60, 24]; + $total = 0; + $ref = []; + $result = []; + + if (is_array(self::$$prop[$src])) + $ref = &self::$$prop[$src]; + else + { + trigger_error('Lang::formatTime - tried to access undefined property Lang::$'.$prop, E_USER_WARNING); + return ''; + } + + if (!$msec) + return self::vspf($ref[0], [0]); + + for ($i = 4; $i > 0; $i--) + { + $total += $time[$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 implode(', ', $result); + } + + private static function vspf(null|array|string $var, array $args = []) : null|array|string { if (is_array($var)) { foreach ($var as &$v) - $v == self::vspf($v, $args); + $v = self::vspf($v, $args); return $var; } + if (!$var) // may be null or empty. Handled differently depending on context + return $var; + + $var = Cfg::applyToString($var); + if ($args) $var = vsprintf($var, $args); - // line break - // |n - $var = str_replace('|n', '
', $var); + return self::unescapeUISequences($var); + } - // color - // |c|r - $var = preg_replace('/\|cff([a-f0-9]{6})(.+?)\|r/i', '$2', $var); + /* Quoted from WoWWiki - UI Escape Sequences (https://wowwiki-archive.fandom.com/wiki/UI_escape_sequences) + * number |1singular;plural; + Will choose a word depending on whether the digit preceding it is 0/1 or not (i.e. 1,11,21 return the first string, as will 0,10,40). Note that unlike |4 singular and plural forms are separated by semi-colon. - // icon - // |T:0:0:0:-1|t - not used, skip if found - $var = preg_replace('/\|T[^\|]+\|t/', '', $var); + * |2text + Before vowels outputs d' (with apostrophe) and removes any leading spaces from text, otherwise outputs de (with trailing space) - // hyperlink - // |H|h|h - not used, truncate structure if found - $var = preg_replace('/\|H[^\|]+\|h([^\|]+)\|h/', '$1', $var); + * |3-formid(text) + Displays text declined to the specified form (index ranges from 1 to GetNumDeclensionSets()). - // french preposition : de - // |2 - $var = preg_replace_callback('/\|2\s(\w)/i', function ($m) { - if (in_array(strtolower($m[1]), ['a', 'e', 'h', 'i', 'o', 'u'])) - return "d'".$m[1]; - else - return 'de '.$m[1]; - }, $var); + * number |4singular:plural; -or- number |4singular:plural1:plural2; + Will choose a form based on the number preceding it. More than two forms (separated by colons) may be required by locale 8 (ruRU). + **/ - // russian word cunjugation thingy - // |3-() - $var = preg_replace_callback('/\|3-(\d)\(([^\)]+)\)/i', function ($m) { - switch ($m[0]) + public static function unescapeUISequences(?string $var, int $fmt = -1) : string + { + if (!$var) + return ''; + + if (strpos($var, '|') === false) + return $var; + + // line break |n + $var = preg_replace_callback('/\|n/i', function ($m) use ($fmt) { - case 1: // seen cases - case 2: - case 3: - case 4: - case 5: - case 6: - case 7: - default: // passthrough .. unk case - return $m[1]; - } - - }, $var); - - // numeric switch - // |4:[:]; - $var = preg_replace_callback('/([\d\.\,]+)([^\d]*)\|4([^:]*):([^;]*);/i', function ($m) { - $plurals = explode(':', $m[4]); - $result = ''; - - if (count($plurals) == 2) // special case: ruRU - { - switch (substr($m[1], -1)) // check last digit of number + switch ($fmt) { - case 1: - // but not 11 (teen number) - if (!in_array($m[1], [11])) - { - $result = $m[3]; - break; - } - case 2: - case 3: - case 4: - // but not 12, 13, 14 (teen number) [11 is passthrough] - if (!in_array($m[1], [11, 12, 13, 14])) - { - $result = $plurals[0]; - break; - } - break; + case -1: // default Lang::vspf case + case self::FMT_HTML: + return '
'; + case self::FMT_MARKUP: + return '[br]'; + case self::FMT_RAW: default: - $result = $plurals[1]; + return ''; } - } - else - $result = ($m[1] == 1 ? $m[3] : $plurals[0]); + }, $var); - return $m[1].$m[2].$result; - }, $var); + // color |c|r + $var = preg_replace_callback('/\|c([[:xdigit:]]{2})([[:xdigit:]]{6})(.+?)\|r/is', function ($m) use ($fmt) + { + [$_, $a, $rgb, $text] = $m; + + switch ($fmt) + { + case -1: // default Lang::vspf case + case self::FMT_HTML: + return sprintf('%3$s', $rgb, $a, $text); + case self::FMT_MARKUP: + return sprintf('[span color=#%1$s]%3$s[/span]', $rgb, $a, $text); // doesn't support alpha + case self::FMT_RAW: + default: + return $text; + } + }, $var); + + // icon |T:0:0:0:-1|t + $var = preg_replace_callback('/\|T([\w]+\\\)*([^\.:]+)(?:\.[bB][lL][pP])?:([^\|]+)\|t/', function ($m) use ($fmt) + { + /* iconParam - size1, size2, xoffset, yoffset + size1 == 0; size2 omitted: Width = Height = TextHeight (always square!) + size1 > 0; size2 omitted: Width = Height = size1 (always square!) + size1 == 0; size2 == 0 : Width = Height = TextHeight (always square!) + size1 > 0; size2 == 0 : Width = TextHeight; Height = size1 (size1 is height!!!) + size1 == 0; size2 > 0 : Width = size2 * TextHeight; Height = TextHeight (size2 is an aspect ratio and defines width!!!) + size1 > 0; size2 > 0 : Width = size1; Height = size2 + */ + + [$_, $iconPath, $iconName, $iconParam] = $m; + + switch ($fmt) + { + case self::FMT_HTML: + return ''; + case self::FMT_MARKUP: + return '[icon name='.Util::lower($iconName).']'; + case self::FMT_RAW: + default: + return ''; + } + }, $var); + + // hyperlink |H|h|h + $var = preg_replace_callback('/\|H([^:]+):([^\|]+)\|h([^\|]+)\|h/i', function ($m) use ($fmt) + { + /* type Params + |Hchannel channelName, channelname == CHANNEL ? channelNr : null + |Hachievement AchievementID, PlayerGUID, isComplete, Month, Day, Year, criteriaMask1, criteriaMask2, criteriaMask3, criteriaMask4 - 32bit masks of Achievement_criteria.dbc/UIOrder only for achievements that display a todo list + |Hquest QuestID, QuestLevel + |Hitem itemId enchantId gemId1 gemId2 gemId3 gemId4 suffixId uniqueId linkLevel + |Henchant SpellID (from craftwindow) + |Htalent TalentID, TalentRank + |Hspell SpellID, PlayerLevel? + |Htrade SpellID, curSkill, maxSkill, PlayerGUID, base64_encode(known recipes bitmask) + |Hplayer Name + |Hunit GUID ? - combatlog + |Hicon ? "source"|"dest" - combatlog + |Haction ? - combatlog + */ + + [$_, $linkType, $linkVars, $text] = $m; + + $linkVars = explode(':', $linkVars); + + $spfVars = ['', $linkVars[0], $text]; + + switch ($linkType) + { + case 'trade': + case 'enchant': + $linkType = 'spell'; + case 'achievement': // markdown COULD implement completed status + case 'quest': + case 'item': // markdown COULD implement enchantments/gems + case 'spell': + $spfVars[0] = $linkType; + break; + case 'talent': + 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; + break; + } + default: + return ''; + } + + switch ($fmt) + { + case self::FMT_HTML: + return sprintf('%s', $spfVars); + case self::FMT_MARKUP: + return sprintf('[%s=%d]', $spfVars); + case self::FMT_RAW: + default: + return sprintf('(%s #%d) %s', $spfVars); + } + }, $var); + + // |1 - digit singular/plural |1; + $var = preg_replace_callback('/(\d+)\s*\|1([^;]+);([^;]+);/is', function ($m) + { + [$_, $num, $singular, $plural] = $m; + + switch ($num[-1]) + { + case 0: + case 1: + return $num . ' ' . $singular; + default: + return $num . ' ' . $plural; + } + }, $var); + + // |2 - frFR preposition: de |2 + $var = preg_replace_callback('/\|2\s?(.)/i', function ($m) + { + [$_, $char] = $m; + + switch (strtolower($char)) + { + case 'h': + if (self::$locale != Locale::FR) + return 'de ' . $char; + case 'a': + case 'e': + case 'i': + case 'o': + case 'u': + return "d'" . $char; + default: + return 'de ' . $char; + } + }, $var); + + // |3 - ruRU declinations |3-() + $var = preg_replace_callback('/\|3-(\d+)\(([^\)]+)\)/iu', function ($m) + { + [$_, $caseIdx, $word] = $m; + + if ($caseIdx > 11 || $caseIdx < 1) // max caseIdx seen in DeclinedWordCases.dbc + return $word; + + 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` = %i AND dc.`word` = %s', $caseIdx, $word)) + return $declWord; + + return $word; + }, $var); + + // |4 - numeric switch |4:[:]; + $var = preg_replace_callback('/([\d\.\,]+)([^\d]*)\|4([^:]*):([^:;]+)(?::([^;]+))?;/is', function ($m) + { + [$_, $num, $pad, $singular, $plural1, $plural2] = array_pad($m, 6, null); + + if (self::$locale != Locale::RU || !$plural2) + return $num . $pad . ($num == 1 ? $singular : $plural1); + + // singular - ends in 1, but not teen number + if ($num[-1] == 1 && $num != 11) + return $num . $pad . $singular; + + // genitive singular - ends in 2, 3, 4, but not teen number + if (($num[-1] == 2 && $num != 12) || ($num[-1] == 3 && $num != 13) || ($num[-1] == 4 && $num != 14)) + return $num . $pad . $plural1; + + // genitive plural - everything else + return $num . $pad . $plural2; + }, $var); return $var; } diff --git a/localization/locale_dede.php b/localization/locale_dede.php index 9c4ab46f..1ca688b4 100644 --- a/localization/locale_dede.php +++ b/localization/locale_dede.php @@ -1,12 +1,13 @@ \World of Warcraft\Data\deDE\patch-deDE-3.MPQ\Interface\FrameXML\GlobalStrings.lua - like: ITEM_MOD_*, POWER_TYPE_*, ITEM_BIND_*, PVP_RANK_* */ $lang = array( @@ -16,6 +17,7 @@ $lang = array( 'pl' => ["Jahre", "Monate", "Wochen", "Tage", "Stunden", "Minuten", "Sekunden", "Millisekunden"], 'ab' => ["J.", "M.", "W.", "Tag", "Std.", "Min.", "Sek.", "Ms."] ), + 'lang' => ['Englisch', null, 'Französisch', 'Deutsch', 'Chinesisch', null, 'Spanisch', null, 'Russisch'], 'main' => array( 'name' => "Name", 'link' => "Link", @@ -23,42 +25,48 @@ $lang = array( 'jsError' => "Stelle bitte sicher, dass JavaScript aktiviert ist.", 'language' => "Sprache", 'feedback' => "Rückmeldung", - 'numSQL' => "Anzahl an MySQL-Queries", - 'timeSQL' => "Zeit für MySQL-Queries", + 'numSQL' => "Anzahl an SQL-Queries", + 'timeSQL' => "Zeit für SQL-Queries", 'noJScript' => 'Diese Seite macht ausgiebigen Gebrauch von JavaScript.
Bitte aktiviert JavaScript in Eurem Browser.', - 'userProfiles' => "Deine Charaktere", + // 'userProfiles' => "Deine Charaktere", 'pageNotFound' => "Dies %s existiert nicht.", 'gender' => "Geschlecht", 'sex' => [null, "Mann", "Frau"], 'players' => "Spieler", + 'thePlayer' => "Der Spieler", 'quickFacts' => "Kurzübersicht", 'screenshots' => "Screenshots", 'videos' => "Videos", - 'side' => "Seite", + 'side' => "Seite: ", 'related' => "Weiterführende Informationen", 'contribute' => "Beitragen", - // 'replyingTo' => "Antwort zu einem Kommentar von", + // 'replyingTo' => "Antwort zu einem Kommentar von", 'submit' => "Absenden", + 'save' => 'Speichern', 'cancel' => "Abbrechen", 'rewards' => "Belohnungen", 'gains' => "Belohnungen", - 'login' => "Login", + // 'login' => "Login", 'forum' => "Forum", - 'n_a' => "n. v.", - 'siteRep' => "Ruf", + 'siteRep' => "Ruf: ", 'yourRepHistory'=> "Dein Ruf-Verlauf", 'aboutUs' => "Über Aowow", 'and' => " und ", 'or' => " oder ", 'back' => "Zurück", 'reputationTip' => "Rufpunkte", - 'byUserTimeAgo' => 'Von %1$s vor %s', + 'byUser' => 'Von %1$s ', 'help' => "Hilfe", + 'status' => "Status", + 'yes' => "Ja", + 'no' => "Nein", + 'any' => "Beliebig", + 'all' => "Alle", // filter 'extSearch' => "Erweiterte Suche", 'addFilter' => "Weiteren Filter hinzufügen", - 'match' => "Verwendete Filter", + 'match' => "Verwendete Filter: ", 'allFilter' => "Alle Filter", 'oneFilter' => "Mindestens einer", 'applyFilter' => "Filter anwenden", @@ -66,7 +74,7 @@ $lang = array( 'refineSearch' => 'Tipp: Präzisiere deine Suche mit Durchsuchen einer Unterkategorie.', 'clear' => "leeren", 'exactMatch' => "Exakt passend", - '_reqLevel' => "Mindeststufe", + '_reqLevel' => "Mindeststufe: ", // infobox 'unavailable' => "Nicht für Spieler verfügbar", @@ -79,7 +87,11 @@ $lang = array( 'links' => "Links", 'compare' => "Vergleichen", 'view3D' => "3D-Ansicht", - 'findUpgrades' => "Bessere Gegenstände finden...", + 'findUpgrades' => "Bessere Gegenstände finden…", + 'report' => "Melden", + 'writeGuide' => "Neuen Leitfaden erstellen", + 'edit' => "Bearbeiten", + 'changelog' => 'Änderungsprotokoll', // miscTools 'errPageTitle' => "Seite nicht gefunden", @@ -92,27 +104,26 @@ $lang = array( ), // article & infobox - 'englishOnly' => "Diese Seite ist nur in Englisch verfügbar.", + 'langOnly' => "Diese Seite ist nur in %s verfügbar.", // calculators - 'preset' => "Vorlage", + 'preset' => "Vorlage: ", 'addWeight' => "Weitere Gewichtung hinzufügen", 'createWS' => "Gewichtungsverteilung erstellen", 'jcGemsOnly' => "JS-exklusive
Edelsteine einschließen", 'cappedHint' => 'Tipp: Entfernt Gewichtungen für gedeckte Werte wie Trefferwertung.', - 'groupBy' => "Ordnen nach", + 'groupBy' => "Ordnen nach: ", 'gb' => array( ["Nichts", "none"], ["Platz", "slot"], ["Stufe", "level"], ["Quelle", "source"] ), 'compareTool' => "Gegenstandsvergleichswerkzeug", 'talentCalc' => "Talentrechner", 'petCalc' => "Begleiterrechner", - 'chooseClass' => "Wählt eine Klasse", - 'chooseFamily' => "Wählt eine Tierart", + 'chooseClass' => "Wählt eine Klasse:", + 'chooseFamily' => "Wählt eine Tierart:", // search 'search' => "Suche", - 'searchButton' => "Suche", 'foundResult' => "Suchergebnisse für", 'noResult' => "Keine Ergebnisse für", 'tryAgain' => "Bitte versucht es mit anderen Suchbegriffen oder überprüft deren Schreibweise.", @@ -122,6 +133,26 @@ $lang = array( 'colon' => ': ', 'dateFmtShort' => "d.m.Y", 'dateFmtLong' => "d.m.Y \u\m H:i", + 'dateFmtIntl' => "d. MMMM y", + '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.", @@ -129,6 +160,10 @@ $lang = array( 'genericError' => "Ein Fehler trat auf; aktualisiert die Seite und versucht es nochmal. Wenn der Fehler bestehen bleibt, bitte meldet es bei feedback", # LANG.genericerror 'bannedRating' => "Ihr wurdet davon gesperrt, Kommentare zu bewerten.", # LANG.tooltip_banned_rating 'tooManyVotes' => "Ihr habt die tägliche Grenze für erlaubte Bewertungen erreicht. Kommt morgen mal wieder!", # LANG.tooltip_too_many_votes + 'alreadyReport' => "Ihr habt dies bereits gemeldet.", # LANG.ct_resp_error7 + 'textTooShort' => "Eure Nachricht ist zu kurz.", + 'cannotComment' => "Ihr wurdet davon gesperrt, Kommentare zu verfassen.", + 'textLength' => "Euer Kommentar ist %d Zeichen lang und muss mindestens %d Zeichen und höchstens %d Zeichen lang sein.", 'moreTitles' => array( 'reputation' => "Benutzerruf", @@ -147,6 +182,61 @@ $lang = array( ) ) ), + 'guide' => array( + 'myGuides' => "Meine Leitfäden", + 'editTitle' => "Eigenen Leitfaden bearbeiten", + 'newTitle' => "Leitfaden erstellen", + 'author' => "Autor: ", + 'spec' => "Spezialisierung: ", + 'sticky' => "Angeheftet", + 'views' => "Ansichten: ", + 'patch' => "Patch", + 'added' => "Hinzugefügt: ", + 'rating' => "Wertung: ", + 'votes' => "[span id=guiderating-value]%.2g[/span]/5 ([span id=guiderating-votes][n5=%d][/span] Bewertungen) [span id=guiderating][/span]", + 'noVotes' => "nicht genug Bewertungen [span id=guiderating][/span]", + 'byAuthor' => "Von %s", + 'notFound' => "Dieser Leitfaden existiert nicht.", + 'clTitle' => 'Änderungsprotokoll für "%2$s"', + 'clStatusSet' => 'Status gesetzt auf %s: ', + 'clCreated' => 'Erstellt: ', + 'clMinorEdit' => 'Kleinere Bearbeitung', + 'editor' => array( + 'fullTitle' => 'Ganze Überschrift', + 'fullTitleTip' => 'Der vollständige Titel des Leitfadens wird auf der Leitfadenseite verwendet und kann eine SEO-orientierte Formulierung enthalten.', + 'name' => 'Name', + 'nameTip' => 'Dies sollte ein einfacher und klarer Name für den Leitfaden sein, der an Orten wie Menüs und Leitfadenlisten verwendet werden kann.', + 'description' => 'Beschreibung', + 'descriptionTip' => 'Beschreibung, die für Suchmaschinen verwendet wird.

Wenn leer, wird es automatisch generiert.', + // 'commentEmail' => 'Emailbenachrichtigung', + // 'commentEmailTip' => 'Soll der Autor darüber benachrichtigt werden, dass Nutzer diesen Guide kommentieren?', + 'changelog' => 'Änderungsprotokoll für diese Änderung', + 'changelogTip' => 'Änderungsprotokoll für diese Änderung', + 'save' => 'Speichern', + 'submit' => 'Zur Ansicht einsenden', + 'autoupdate' => 'Autom. Update', + 'showAdjPrev' => 'Zeige angrenzende Vorschau', + 'preview' => 'Vorschau', + 'class-spec' => 'Klasse / Spez.', + 'category' => 'Kategorie', + 'testGuide' => 'Sehen Sie, wie Ihr Leitfaden aussehen wird', + 'images' => 'Bilder', + 'statusTip' => array( + GuideMgr::STATUS_DRAFT => 'Ihr Leitfaden ist im "Entwurfs"-Status und Sie sind der einzige der ihn sehen kann. Bearbeiten Sie ihn so lange Sie wollen und wenn Sie fertig sind reichen Sie ihn zur Überprüfung ein.', + GuideMgr::STATUS_REVIEW => 'Ihr Leitfaden wird überprüft.', + GuideMgr::STATUS_APPROVED => 'Ihr Leitfaden wurde veröffentlicht.', + GuideMgr::STATUS_REJECTED => 'Ihr Leitfaden wurde abgewiesen. Nachdem die Mängel behoben wurde kann er erneut zur Überprüfung eingereicht werden.', + GuideMgr::STATUS_ARCHIVED => 'Ihr Leitfaden ist veraltet und wurde archiviert. Er wird nicht mehr in der Übersicht gelistet und ist kann nicht mehr bearbeitet werden.]', + ) + ), + 'category' => array( + null, "Klassen", "Berufe", "Weltereignisse", "Neue Spieler & Stufenfortschritt", + "Schlachtzüge & Bosskämpfe", "Wirtschaft & Währung", "Erfolge", "Gegenstände, Haus- & Reittiere", "Anderes" + ), + 'status' => array( + null, "Entwurf", "Zulassung ausstehend", "Zugelassen", "Abgelehnt", "Archiviert" + ) + ), 'profiler' => array( 'realm' => "Realm", 'region' => "Region", @@ -156,23 +246,35 @@ $lang = array( '_cpFooter' => "Falls Ihr eine genauere Suche möchtet, probiert unsere erweiterten Suchoptionen. Ihr könnt außerdem ein neues individuelles Profil erstellen.", 'firstUseTitle' => "%s von %s", 'complexFilter' => "Komplexer Filter ausgewählt! Suchergebnisse sind auf gecachte Charaktere beschränkt.", - + 'customProfile' => " (Benutzerprofil)", 'resync' => "Resynchronisieren", 'guildRoster' => "Gildenliste für <%s>", 'arenaRoster' => "Arena-Teamliste für <%s>", 'atCaptain' => "Teamkapitän", - + 'atSize' => "Größe: ", 'profiler' => "Charakter-Profiler", - 'arenaTeams' => "Arena Teams", - 'guilds' => "Gilden", - + '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.", 'profile' => "Dieser Charakter existiert nicht oder wurde noch nicht in die Datenbank übernommen." ), - 'dummyNPCs' => array( - 100001 => "Luftschiffkampf", 200001 => "Bestien von Nordend", 200002 => "Fraktionschampions", 200003 => "Zwillingsval'kyr" + 'regions' => array( + 'us' => "Americas", + 'eu' => "Europa", + 'kr' => "Korea", + 'tw' => "Taiwan", + 'cn' => "China", + 'dev' => "Entwicklung" + ), + 'encounterNames'=> array( + 243 => "Die Sieben", + 334 => "Großchampions", + 629 => "Bestien von Nordened", 637 => "Fraktionschampions", 641 => "Zwillingsval'kyr", + 692 => "Die vier Reiter", + 748 => "Der Eiserne Rat", + 847 => "Kanonenschiffsschlacht von Eiskrone" ), ), 'screenshot' => array( @@ -189,88 +291,127 @@ $lang = array( ), 'error' => array( 'unkFormat' => "Unbekanntes Bildformat.", - 'tooSmall' => "Euer Screenshot ist viel zu klein. (< ".CFG_SCREENSHOT_MIN_SIZE."x".CFG_SCREENSHOT_MIN_SIZE.").", + 'tooSmall' => "Euer Screenshot ist viel zu klein. (< CFG_SCREENSHOT_MIN_SIZE x CFG_SCREENSHOT_MIN_SIZE).", 'selectSS' => "Wählt bitte den Screenshot aus, den Ihr hochladen möchtet.", 'notAllowed' => "Es ist euch nicht erlaubt einen Screenshot hochzuladen!", ) ), + 'video' => array( + 'submission' => "Video-Einsendung", + 'thanks' => array( + 'contrib' => "Vielen Dank für Euren Beitrag!", + 'goBack' => 'Klickt hier, um zu der vorherigen Seite zurückzukehren.', + 'note' => "Hinweis: Euer Video muss zunächst zugelassen werden, bevor es auf der Seite erscheint. Dies kann bis zu 72 Stunden dauern." + ), + 'error' => array( + 'isPrivate' => "Das vorgeschlagene Video ist privat.", + 'noExist' => "An der eingereichten Url existiert kein Video.", + 'selectVI' => "Bitte gebt gültige Videoinformationen ein.", + 'notAllowed' => "Es ist euch nicht erlaubt Videos vorzuschlagen!" + ) + ), 'game' => array( - 'achievement' => "Erfolg", + // type strings + 'npc' => "NPC", // 1 + 'npcs' => "NPCs", + 'object' => "Objekt", // 2 + 'objects' => "Objekte", + 'item' => "Gegenstand", // 3 + 'items' => "Gegenstände", + 'itemset' => "Ausrüstungsset", // 4 + 'itemsets' => "Ausrüstungssets", + 'quest' => "Quest", // 5 + 'quests' => "Quests", + 'spell' => "Zauber", // 6 + 'spells' => "Zauber", + 'zone' => "Zone", // 7 + 'zones' => "Gebiete", + 'faction' => "Fraktion", // 8 + 'factions' => "Fraktionen", + 'pet' => "Begleiter", // 9 + 'pets' => "Begleiter", + 'achievement' => "Erfolg", // 10 'achievements' => "Erfolge", - 'class' => "Klasse", + 'title' => "Titel", // 11 + 'titles' => "Titel", + 'event' => "Weltereignis", // 12 + 'events' => "Weltereignisse", + 'class' => "Klasse", // 13 'classes' => "Klassen", - 'currency' => "Währung", + 'race' => "Volk", // 14 + 'races' => "Völker", + 'skill' => "Fertigkeit", // 15 + 'skills' => "Fertigkeiten", + 'currency' => "Währung", // 17 'currencies' => "Währungen", - 'difficulty' => "Modus", + 'sound' => "Klang", // 19 + 'sounds' => "Klänge", + 'icon' => "Icon", // 29 + 'icons' => "Icons", + 'profile' => "Profil", // 100 + 'profiles' => "Profile", + 'guild' => "Gilde", // 101 + 'guilds' => "Gilden", + 'arenateam' => "Arena Team", // 102 + 'arenateams' => "Arena Teams", + 'guide' => "Leitfaden", // 300 + 'guides' => "Leitfäden", + 'emote' => "Emote", // 501 + 'emotes' => "Emotes", + 'enchantment' => "Verzauberung", // 502 + 'enchantments' => "Verzauberungen", + 'areatrigger' => "Areatrigger", // 503 + 'areatriggers' => "Areatrigger", + 'mail' => "Brief", // 504 + 'mails' => "Briefe", + + 'cooldown' => "%s Abklingzeit", + 'difficulty' => "Modus: ", 'dispelType' => "Bannart", 'duration' => "Dauer", - 'emote' => "Emote", - 'emotes' => "Emotes", - 'enchantment' => "Verzauberung", - 'enchantments' => "Verzauberungen", - 'object' => "Objekt", - 'objects' => "Objekte", - 'glyphType' => "Glyphenart", - 'race' => "Volk", - 'races' => "Völker", - 'title' => "Titel", - 'titles' => "Titel", - 'eventShort' => "Ereignis", - 'event' => "Weltereigniss", - 'events' => "Weltereignisse", - 'faction' => "Fraktion", - 'factions' => "Fraktionen", - 'cooldown' => "%s Abklingzeit", - 'icon' => "Icon", - 'icons' => "Icons", - 'item' => "Gegenstand", - 'items' => "Gegenstände", - 'itemset' => "Ausrüstungsset", - 'itemsets' => "Ausrüstungssets", + 'eventShort' => "Ereignis: %s", + 'flags' => "Flags", + 'glyphType' => "Glyphenart: ", + 'level' => "Stufe", 'mechanic' => "Auswirkung", - 'mechAbbr' => "Ausw.", - 'meetingStone' => "Versammlungsstein", - 'npc' => "NPC", - 'npcs' => "NPCs", - 'pet' => "Begleiter", - 'pets' => "Begleiter", - 'profile' => "", - 'profiles' => "Profile", - 'quest' => "Quest", - 'quests' => "Quests", + 'mechAbbr' => "Ausw.: ", + 'meetingStone' => "Versammlungsstein: ", 'requires' => "Benötigt %s", 'requires2' => "Benötigt", 'reqLevel' => "Benötigt Stufe %s", - 'reqSkillLevel' => "Benötigte Fertigkeitsstufe", - 'level' => "Stufe", + 'reqSkillLevel' => "Benötigte Fertigkeitsstufe: ", 'school' => "Magieart", - 'skill' => "Fertigkeit", - 'skills' => "Fertigkeiten", - 'sound' => "Klang", - 'sounds' => "Klänge", - 'spell' => "Zauber", - 'spells' => "Zauber", - 'type' => "Art", + 'type' => "Art: ", 'valueDelim' => " - ", // " bis " - 'zone' => "Zone", - 'zones' => "Gebiete", + 'target' => "", 'pvp' => "PvP", 'honorPoints' => "Ehrenpunkte", 'arenaPoints' => "Arenapunkte", 'heroClass' => "Heldenklasse", - 'resource' => "Ressource", - 'resources' => "Ressourcen", - 'role' => "Rolle", - 'roles' => "Rollen", - 'specs' => "Spezialisierungen", + 'resource' => "Ressource: ", + 'resources' => "Ressourcen: ", + 'role' => "Rolle: ", + 'roles' => "Rollen: ", + 'specs' => "Spezialisierungen: ", '_roles' => ["Heiler", "Nahkampf-DPS", "Distanz-DPS", "Tank"], 'phases' => "Phasen", - 'mode' => "Modus", - 'modes' => [-1 => "Beliebig", "Normal / Normal 10", "Heroisch / Normal 25", "Heroisch 10", "Heroisch 25"], + 'mode' => "Modus: ", + '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( + '', + "%d |4Sek.:Sek.;", + "%d |4Min.:Min.;", + "%d |4Std.:Std.;", + "%d |4Tag:Tage;" + ), 'sources' => array( "Unbekannt", "Hergestellt", "Drop", "PvP", "Quest", "Händler", "Lehrer", "Entdeckung", "Einlösung", "Talent", "Startausrüstung", "Ereignis", @@ -278,6 +419,10 @@ $lang = array( "Gemahlen", "Abgebaut", "Sondiert", "Aus Taschendiebstahl", "Geborgen", "Gehäutet", "In-Game-Store" ), + 'pvpSources' => array( + 42 => "Arenasaison 1", 52 => "Arenasaison 2", 71 => "Arenasaison 3", 80 => "Arenasaison 4", 157 => "Arenasaison 5", + 163 => "Arenasaison 6", 167 => "Arenasaison 7", 169 => "Arenasaison 8", 177 => "2009 Arena-Turnier" + ), 'languages' => array( 1 => "Orcisch", 2 => "Darnassisch", 3 => "Taurisch", 6 => "Zwergisch", 7 => "Gemeinsprache", 8 => "Dämonisch", 9 => "Titanisch", 10 => "Thalassisch", 11 => "Drachisch", 12 => "Kalimagisch", 13 => "Gnomisch", 14 => "Trollisch", @@ -287,9 +432,9 @@ $lang = array( 'si' => [1 => "Allianz", -1 => "Nur für Allianz", 2 => "Horde", -2 => "Nur für Horde", 3 => "Beide"], 'resistances' => [null, 'Heiligwiderstand', 'Feuerwiderstand', 'Naturwiderstand', 'Frostwiderstand', 'Schattenwiderstand', 'Arkanwiderstand'], 'sc' => ["Körperlich", "Heilig", "Feuer", "Natur", "Frost", "Schatten", "Arkan"], - 'dt' => [null, "Magie", "Fluch", "Krankheit", "Gift", "Verstohlenheit", "Unsichtbarkeit", null, null, "Wut"], + 'dt' => [null, "Magie", "Fluch", "Krankheit", "Gift", "Verstohlenheit", "Unsichtbarkeit", "Magie, Fluch, Krankheit, Gift", "Zauber (NSC)", "Wut"], 'cl' => [null, "Krieger", "Paladin", "Jäger", "Schurke", "Priester", "Todesritter", "Schamane", "Magier", "Hexenmeister", null, "Druide"], - 'ra' => [-2 => "Horde", -1 => "Allianz", "Beide", "Mensch", "Orc", "Zwerg", "Nachtelf", "Untoter", "Tauren", "Gnom", "Troll", null, "Blutelf", "Draenei"], + 'ra' => [-2 => "Horde", -1 => "Allianz", null, "Mensch", "Orc", "Zwerg", "Nachtelf", "Untoter", "Tauren", "Gnom", "Troll", null, "Blutelf", "Draenei"], 'rep' => ["Hasserfüllt", "Feindselig", "Unfreundlich", "Neutral", "Freundlich", "Wohlwollend", "Respektvoll", "Ehrfürchtig"], 'st' => array( "Vorgabe", "Katzengestalt", "Baum des Lebens", "Reisegestalt", "Wassergestalt", "Bärengestalt", @@ -320,13 +465,460 @@ $lang = array( 38 => "Schimäre", 39 => "Teufelssaurier", 41 => "Silithid", 42 => "Wurm", 43 => "Rhinozeros", 44 => "Wespe", 45 => "Kernhund", 46 => "Geisterbestie" ), - 'pvpRank' => array( - null, "Gefreiter / Späher", "Fußknecht / Grunzer", - "Landsknecht / Waffenträger", "Feldwebel / Schlachtrufer", "Fähnrich / Rottenmeister", - "Leutnant / Steingardist", "Hauptmann / Blutgardist", "Kürassier / Zornbringer", - "Ritter der Allianz / Klinge der Horde", "Feldkomandant / Feldherr", "Rittmeister / Sturmreiter", - "Marschall / Kriegsherr", "Feldmarschall / Kriegsfürst", "Großmarschall / Oberster Kriegsfürst" + 'classSpecs' => array( + -1 => 'Untalentiert', + 0 => 'Hybride', + 6 => ['Blut', 'Frost', 'Unheilig' ], + 11 => ['Gleichgewicht', 'Wilder Kampf', 'Wiederherstellung'], + 3 => ['Tierherrschaft', 'Treffsicherheit', 'Überleben' ], + 8 => ['Arkan', 'Feuer', 'Frost' ], + 2 => ['Heilig', 'Schutz', 'Vergeltung' ], + 5 => ['Disziplin', 'Heilig', 'Schattenmagie' ], + 4 => ['Meucheln', 'Kampf', 'Täuschung' ], + 7 => ['Elementarkampf', 'Verstärkung', 'Wiederherstellung'], + 9 => ['Gebrechen', 'Dämonologie', 'Zerstörung' ], + 1 => ['Waffen', 'Furor', 'Schutz' ] ), + 'pvpRank' => array( // PVP_RANK_* + null, ["Späher", "Gefreiter"], ["Grunzer", "Fußknecht"], + ["Waffenträger", "Landsknecht"], ["Schlachtrufer", "Feldwebel"], ["Rottenmeister", "Fähnrich"], + ["Steingardist", "Leutnant"], ["Blutgardist", "Hauptmann"], ["Zornbringer", "Kürassier"], + ["Klinge der Horde", "Ritter der Allianz"], ["Feldherr", "Feldkommandant"], ["Sturmreiter", "Rittmeister"], + ["Kriegsherr", "Marschall"], ["Kriegsfürst", "Feldmarschall"], ["Oberster Kriegsfürst", "Großmarschall"] + ), + 'orientation' => ['Nord', 'Nordost', 'Ost', 'Südost', 'Süd', 'Südwest', 'West', 'Nordwest'] + ), + 'unit' => array( + 'flags' => array( + UNIT_FLAG_SERVER_CONTROLLED => 'Servergesteuert', + UNIT_FLAG_NON_ATTACKABLE => 'Nicht angreifbar', + UNIT_FLAG_REMOVE_CLIENT_CONTROL => 'Steuerung durch Client gesperrt', + UNIT_FLAG_PVP_ATTACKABLE => 'PvP-angreifbar', + UNIT_FLAG_RENAME => 'Umbenannt', + UNIT_FLAG_PREPARATION => 'Arenavorbereitung', + UNIT_FLAG_UNK_6 => 'UNK-6', + UNIT_FLAG_NOT_ATTACKABLE_1 => 'Nicht angreifbar', + UNIT_FLAG_IMMUNE_TO_PC => 'Immun gegen Spieler', + UNIT_FLAG_IMMUNE_TO_NPC => 'Immun gegen Kreaturen', + UNIT_FLAG_LOOTING => 'Lootanimation', + UNIT_FLAG_PET_IN_COMBAT => 'Pet im Kampf', + 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)', + UNIT_FLAG_UNK_16 => 'UNK-16 (kann nicht angreifen)', + UNIT_FLAG_PACIFIED => 'Befriedet', + UNIT_FLAG_STUNNED => 'Betäubt', + UNIT_FLAG_IN_COMBAT => 'Im Kampf', + UNIT_FLAG_TAXI_FLIGHT => 'Taxiflug', + UNIT_FLAG_DISARMED => 'Enwaffnet', + UNIT_FLAG_CONFUSED => 'Verwirrt', + UNIT_FLAG_FLEEING => 'Fiehend', + UNIT_FLAG_PLAYER_CONTROLLED => 'Spielergesteuert', + UNIT_FLAG_NOT_SELECTABLE => 'Nicht anwählbar', + UNIT_FLAG_SKINNABLE => 'Kürschnerbar', + UNIT_FLAG_MOUNT => 'Beritten', + UNIT_FLAG_UNK_28 => 'UNK-28', + UNIT_FLAG_UNK_29 => 'UNK-29 (Unterbinde Emotes)', + UNIT_FLAG_SHEATHE => 'Waffe eingesteckt', + UNIT_FLAG_UNK_31 => 'UNK-31' + ), + 'flags2' => array( + UNIT_FLAG2_FEIGN_DEATH => 'Totstellen', + UNIT_FLAG2_UNK1 => 'UNK-1 (unit model versteckt)', + UNIT_FLAG2_IGNORE_REPUTATION => 'Ignoriere Reputation', + UNIT_FLAG2_COMPREHEND_LANG => 'Verstehe Sprache', + UNIT_FLAG2_MIRROR_IMAGE => 'Spiegelbild', + UNIT_FLAG2_INSTANTLY_APPEAR_MODEL => 'Instant spawn', + UNIT_FLAG2_FORCE_MOVEMENT => 'Erzwinge Bewegung', + UNIT_FLAG2_DISARM_OFFHAND => 'Entwaffne Nebenhand', + UNIT_FLAG2_DISABLE_PRED_STATS => 'Vorausgesagte Statistiken deaktiviert', + UNIT_FLAG2_DISARM_RANGED => 'Entwaffne Fernkampf', + UNIT_FLAG2_REGENERATE_POWER => 'Regeneriere Energie/Mana/Wut', + UNIT_FLAG2_RESTRICT_PARTY_INTERACTION => 'Beschränke Gruppeninteraktion', + UNIT_FLAG2_PREVENT_SPELL_CLICK => 'Verhindere Zauber-Klick', + UNIT_FLAG2_ALLOW_ENEMY_INTERACT => 'Interaktion mit Gegner zulassen', + UNIT_FLAG2_DISABLE_TURN => 'Rotation deaktiviert', + UNIT_FLAG2_UNK2 => 'UNK-2', + UNIT_FLAG2_PLAY_DEATH_ANIM => 'Spezielle Todesanimation abspielen', + UNIT_FLAG2_ALLOW_CHEAT_SPELLS => 'Cheat-Zauber zulassen' + ), + 'dynFlags' => array( + UNIT_DYNFLAG_LOOTABLE => 'Plünderbar', + UNIT_DYNFLAG_TRACK_UNIT => 'Verfolgt', + UNIT_DYNFLAG_TAPPED => 'Getappt', + UNIT_DYNFLAG_TAPPED_BY_PLAYER => 'Getappt durch Spieler', + UNIT_DYNFLAG_SPECIALINFO => 'Besondere Info', + UNIT_DYNFLAG_DEAD => 'Tot', + UNIT_DYNFLAG_REFER_A_FRIEND => 'Refer-a-friend', + UNIT_DYNFLAG_TAPPED_BY_ALL_THREAT_LIST => 'Getappt durch ganze Aggro-Liste' + ), + 'bytes1' => array( +/*idx:0*/ array( + UNIT_STAND_STATE_STAND => 'Stehend', + UNIT_STAND_STATE_SIT => 'Am Boden sitzend', + UNIT_STAND_STATE_SIT_CHAIR => 'Auf Stuhl sitzend', + UNIT_STAND_STATE_SLEEP => 'Schlafend', + UNIT_STAND_STATE_SIT_LOW_CHAIR => 'Auf niedrigem Stuhl sitzend', + UNIT_STAND_STATE_SIT_MEDIUM_CHAIR => 'Auf mittlerem Stuhl sitzend', + UNIT_STAND_STATE_SIT_HIGH_CHAIR => 'Auf hohem Stuhl sitzend', + UNIT_STAND_STATE_DEAD => 'Tot', + UNIT_STAND_STATE_KNEEL => 'Kniehend', + UNIT_STAND_STATE_SUBMERGED => 'Untergetaucht' + ), + null, +/*idx:2*/ array( + UNIT_VIS_FLAGS_UNK1 => 'UNK-1', + UNIT_VIS_FLAGS_CREEP => 'Creep', + UNIT_VIS_FLAGS_UNTRACKABLE => 'Unaufspürbar', + UNIT_VIS_FLAGS_UNK4 => 'UNK-4', + UNIT_VIS_FLAGS_UNK5 => 'UNK-5' + ), +/*idx:3*/ array( + 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]', + 'idxUNK' => '[span class=q10]unbenutzter Offset [b class=q1]%d[/b] übergeben an UnitFieldBytes1[/span]' + ) + ), + 'smartAI' => array( + 'eventUNK' => '[span class=q10]Unbenkanntes Event #[b class=q1]%d[/b] in Benutzung.[/span]', + 'eventTT' => '[b class=q1]EventType %d[/b][br][table][tr][td]PhaseMask[/td][td=header]0x%04X[/td][/tr][tr][td]Chance[/td][td=header]%d%%[/td][/tr][tr][td]Flags[/td][td=header]0x%04X[/td][/tr][tr][td]Param1[/td][td=header]%d[/td][/tr][tr][td]Param2[/td][td=header]%d[/td][/tr][tr][td]Param3[/td][td=header]%d[/td][/tr][tr][td]Param4[/td][td=header]%d[/td][/tr][tr][td]Param5[/td][td=header]%d[/td][/tr][/table]', + 'events' => array( + SmartEvent::EVENT_UPDATE_IC => ['(%12$d)?:Im Kampf, ;(%11$s)?Nach %11$s:Sofort;', 'Wiederhole alle %s'], + SmartEvent::EVENT_UPDATE_OOC => ['(%12$d)?:Nicht im Kampf, ;(%11$s)?Nach %11$s:Sofort;', 'Wiederhole alle %s'], + SmartEvent::EVENT_HEALTH_PCT => ['Ab %11$s%% Gesundheit', 'Wiederhole alle %s'], + SmartEvent::EVENT_MANA_PCT => ['Ab %11$s%% Mana', 'Wiederhole alle %s'], + SmartEvent::EVENT_AGGRO => ['Bei Aggro', ''], + SmartEvent::EVENT_KILL => ['Beim Töten (%3$d)?eines Spielers:(%4$d)?von [npc=%4$d]:einer Kreatur;;', 'Abklingzeit: %s'], + SmartEvent::EVENT_DEATH => ['Im Tod', ''], + SmartEvent::EVENT_EVADE => ['Beim Entkommen', ''], + SmartEvent::EVENT_SPELLHIT => ['Von (%11$s)?%11$s-:;(%1$d)?[spell=%1$d]:Zauber; getroffen', 'Abklingzeit: %s'], + SmartEvent::EVENT_RANGE => ['#target# innerhalb von %11$sm', 'Wiederhole alle %s'], +/* 10*/ SmartEvent::EVENT_OOC_LOS => ['(%5$d)?Spieler:Einheit;(%11$s)? (%11$s):; kommt ausserhalb des Kampfes innerhalb von %2$dm ins Sichtfeld', 'Abklingzeit: %s'], + SmartEvent::EVENT_RESPAWN => ['Beim Erscheinen(%11$s)? in %11$s:;(%12$d)? in [zone=%12$d]:;', ''], + SmartEvent::EVENT_TARGET_HEALTH_PCT => ['#target# hat %11$s%% Gesundheit', 'Wiederhole alle %s'], + SmartEvent::EVENT_VICTIM_CASTING => ['#target# wirkt (%3$d)?[spell=%3$d]:beliebigen Zauber;', 'Wiederhole alle %s'], + SmartEvent::EVENT_FRIENDLY_HEALTH => ['Freundlicher NPC innerhalb von %2$dm hat %1$d Gesundheit', 'Wiederhole alle %s'], + SmartEvent::EVENT_FRIENDLY_IS_CC => ['Freundlicher NPC innerhalb von %1$dm ist beeinflusst von \'Crowd Control\'', 'Wiederhole alle %s'], + SmartEvent::EVENT_FRIENDLY_MISSING_BUFF => ['Freundlichem NPC innerhalb von %2$dm fehlt [spell=%1$d]', 'Wiederhole alle %s'], + SmartEvent::EVENT_SUMMONED_UNIT => ['(%1$d)?[npc=%1$d]:Beliebige Kreatur; wurde gerade beschworen', 'Abklingzeit: %s'], + SmartEvent::EVENT_TARGET_MANA_PCT => ['#target# hat %11$s%% Mana', 'Wiederhole alle %s'], + SmartEvent::EVENT_ACCEPTED_QUEST => ['Gebe (%1$d)?[quest=%1$d]:beliebiges Quest;', 'Abklingzeit: %s'], +/* 20*/ SmartEvent::EVENT_REWARD_QUEST => ['Belohne (%1$d)?[quest=%1$d]:beliebiges Quest;', 'Abklingzeit: %s'], + SmartEvent::EVENT_REACHED_HOME => ['Erreiche \'Heimat\'-Koordinaten', ''], + SmartEvent::EVENT_RECEIVE_EMOTE => ['Das Ziel von [emote=%1$d] seiend', 'Abklingzeit: %s'], + SmartEvent::EVENT_HAS_AURA => ['(%2$d)?Habe %2$d Aufladungen von [spell=%1$d]:Aura von [spell=%1$d] fehlt; ', 'Wiederhole alle %s'], + SmartEvent::EVENT_TARGET_BUFFED => ['#target# hat (%2$d)?%2$d Aufladungen:Aura; von [spell=%1$d]', 'Wiederhole alle %s'], + SmartEvent::EVENT_RESET => ['Beim Reset', ''], + SmartEvent::EVENT_IC_LOS => ['(%5$d)?Spieler:Einheit;(%11$s)? (%11$s):; kommt im Kampf innerhalb von %2$dm ins Sichtfeld', 'Abklingzeit: %s'], + SmartEvent::EVENT_PASSENGER_BOARDED => ['Ein Passagier steigt zu', 'Abklingzeit: %s'], + SmartEvent::EVENT_PASSENGER_REMOVED => ['Ein Passagier steigt ab', 'Abklingzeit: %s'], + SmartEvent::EVENT_CHARMED => ['(%1$d)?Bezaubert werden:Bezauberung läuft ab;', ''], +/* 30*/ SmartEvent::EVENT_CHARMED_TARGET => ['Beim Bezaubern von #target#', ''], + SmartEvent::EVENT_SPELLHIT_TARGET => ['#target# wird von (%11$s)?%11$s :;(%1$d)?[spell=%1$d]:Zauber; getroffen', 'Abklingzeit: %s'], + SmartEvent::EVENT_DAMAGED => ['Nehme %11$s Punkte Schaden', 'Wiederhole alle %s'], + SmartEvent::EVENT_DAMAGED_TARGET => ['#target# nahm %11$s Punkte Schaden', 'Wiederhole alle %s'], + SmartEvent::EVENT_MOVEMENTINFORM => ['Beende (%1$d)?%11$s:Bewegung; an Punkt #[b]%2$d[/b]', ''], + SmartEvent::EVENT_SUMMON_DESPAWNED => ['Beschworener NPC(%1$d)? [npc=%1$d]:; verschwindet', 'Abklingzeit: %s'], + SmartEvent::EVENT_CORPSE_REMOVED => ['Leiche verschwindet', ''], + SmartEvent::EVENT_AI_INIT => ['KI initialisiert', ''], + SmartEvent::EVENT_DATA_SET => ['Datenfeld #[b]%1$d[/b] auf [b]%2$d[/b] gesetzt', 'Abklingzeit: %s'], + SmartEvent::EVENT_WAYPOINT_START => ['Beginne Pfad(%2$d)? #[b]%2$d[/b]:; an (%1$d)?Wegpunkt #[b]%1$d[/b]:beliebigem Wegpunkt;', ''], +/* 40*/ SmartEvent::EVENT_WAYPOINT_REACHED => ['Erreiche (%1$d)?Wegpunkt #[b]%1$d[/b]:beliebigen Wegpunkt;(%2$d)? auf Pfad #[b]%2$d[/b]:;', ''], + SmartEvent::EVENT_TRANSPORT_ADDPLAYER => null, + SmartEvent::EVENT_TRANSPORT_ADDCREATURE => null, + SmartEvent::EVENT_TRANSPORT_REMOVE_PLAYER => null, + SmartEvent::EVENT_TRANSPORT_RELOCATE => null, + SmartEvent::EVENT_INSTANCE_PLAYER_ENTER => null, + SmartEvent::EVENT_AREATRIGGER_ONTRIGGER => ['Bei Aktivierung', ''], + SmartEvent::EVENT_QUEST_ACCEPTED => null, + SmartEvent::EVENT_QUEST_OBJ_COMPLETION => null, + SmartEvent::EVENT_QUEST_COMPLETION => null, +/* 50*/ SmartEvent::EVENT_QUEST_REWARDED => null, + SmartEvent::EVENT_QUEST_FAIL => null, + SmartEvent::EVENT_TEXT_OVER => ['(%2$d)?[npc=%2$d]:Beliebige Kreatur; ist fertig mit der Wiedergabe von Textgruppe #[b]%1$d[/b]', ''], + SmartEvent::EVENT_RECEIVE_HEAL => ['Erhalte %11$s Punkte Heilung', 'Abklingzeit: %s'], + SmartEvent::EVENT_JUST_SUMMONED => ['Wurde gerade beschworen', ''], + SmartEvent::EVENT_WAYPOINT_PAUSED => ['Pausiere Pfad(%2$d)? #[b]%2$d[/b]:; an (%1$d)?Wegpunkt #[b]%1$d[/b]:beliebigem Wegpunkt;', ''], + SmartEvent::EVENT_WAYPOINT_RESUMED => ['Setze Pfad(%2$d)? #[b]%2$d[/b]:; an (%1$d)?Wegpunkt #[b]%1$d[/b]:beliebigem Wegpunkt; fort', ''], + SmartEvent::EVENT_WAYPOINT_STOPPED => ['Halte Pfad(%2$d)? #[b]%2$d[/b]:; an (%1$d)?Waypoint #[b]%1$d[/b]:beliebigem Wegpunkt; an', ''], + SmartEvent::EVENT_WAYPOINT_ENDED => ['Beende aktuellen Pfad(%2$d)? #[b]%2$d[/b]:; an (%1$d)?Waypoint #[b]%1$d[/b]:beliebigem Wegpunkt;', ''], + SmartEvent::EVENT_TIMED_EVENT_TRIGGERED => ['Geplanted Ereignis #[b]%1$d[/b] löst aus', ''], +/* 60*/ SmartEvent::EVENT_UPDATE => ['(%11$s)?Nach %11$s:Sofort;', 'Wiederhole alle %s'], + SmartEvent::EVENT_LINK => ['Nach Ereignis %11$s', ''], + SmartEvent::EVENT_GOSSIP_SELECT => ['Wähle Gossip:[br](%11$s)?[span class=q1]%11$s[/span]:Menü #[b]%1$d[/b] - Option #[b]%2$d[/b];', ''], + SmartEvent::EVENT_JUST_CREATED => ['Beim ersten Erscheinen in der Welt', ''], + SmartEvent::EVENT_GOSSIP_HELLO => ['Öffne Gossip', '(%1$d)?onGossipHello:;(%2$d)?onReportUse:;'], + SmartEvent::EVENT_FOLLOW_COMPLETED => ['Ist fertig mit folgen', ''], + SmartEvent::EVENT_EVENT_PHASE_CHANGE => ['Ereignisphase wurde geändert und passt auf %11$s', ''], + SmartEvent::EVENT_IS_BEHIND_TARGET => ['Stehe hinter #target#', 'Abklingzeit: %s'], + SmartEvent::EVENT_GAME_EVENT_START => ['[event=%1$d] beginnt', ''], + SmartEvent::EVENT_GAME_EVENT_END => ['[event=%1$d] endet', ''], +/* 70*/ SmartEvent::EVENT_GO_LOOT_STATE_CHANGED => ['Zustand geändert auf: %11$s', ''], + SmartEvent::EVENT_GO_EVENT_INFORM => ['Im Template definiertes Ereignis #[b]%1$d[/b] wurde ausgelöst', ''], + 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 %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_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', + SmartEvent::FLAG_DIFFICULTY_0 => '5N Dungeon / 10N Schlachtzug', + SmartEvent::FLAG_DIFFICULTY_1 => '5H Dungeon / 25N Schlachtzug', + SmartEvent::FLAG_DIFFICULTY_2 => '10H Schlachtzug', + SmartEvent::FLAG_DIFFICULTY_3 => '25H Schlachtzug', + SmartEvent::FLAG_DEBUG_ONLY => null, // only occurs in debug build; do not output + SmartEvent::FLAG_NO_RESET => 'Nicht resetbar', + SmartEvent::FLAG_WHILE_CHARMED => 'Auch wenn bezaubert' + ), + 'actionUNK' => '[span class=q10]Unbekannte Action #[b class=q1]%d[/b] in Benutzung.[/span]', + 'actionTT' => '[b class=q1]ActionType %d[/b][br][table][tr][td]Param1[/td][td=header]%d[/td][/tr][tr][td]Param2[/td][td=header]%d[/td][/tr][tr][td]Param3[/td][td=header]%d[/td][/tr][tr][td]Param4[/td][td=header]%d[/td][/tr][tr][td]Param5[/td][td=header]%d[/td][/tr][tr][td]Param6[/td][td=header]%d[/td][/tr][/table]', + 'actions' => array( // [body, footer] + null, + SmartAction::ACTION_TALK => ['(%3$d)?Gib:#target# gibt; (%11$s)?TextGroup:[span class=q10]unbekannten Text[/span]; #[b]%1$d[/b] (3$d)?für #target#:für den Auslöser; wieder%11$s', 'Dauer: %s'], + SmartAction::ACTION_SET_FACTION => ['Setze Fraktion von #target# (%1$d)?auf [faction=%11$d]:zurück;.', ''], + SmartAction::ACTION_MORPH_TO_ENTRY_OR_MODEL => ['(%11$d)?Setze Aussehen zurück.:Nimm Erscheinungsbild an:;(%1$d)? [npc=%1$d]:;(%2$d)?[model npc=%2$d border=1 float=right][/model]:;', ''], + SmartAction::ACTION_SOUND => ['Spiele Audio für (%2$d)?auslösenden Spieler:alle Spieler im Sichtfeld;:[div][sound=%1$d][/div]', 'Wiedergabe durch Umwelt'], + SmartAction::ACTION_PLAY_EMOTE => ['(%1$d)?Emote [emote=%1$d] zu #target#.:Beende fortlaufenden Emote;', ''], + SmartAction::ACTION_FAIL_QUEST => ['[quest=%1$d] von #target# schlägt fehl.', ''], + SmartAction::ACTION_OFFER_QUEST => ['(%2$d)?Füge [quest=%1$d] dem Log von #target# hinzu:Biete [quest=%1$d] #target# an;.', ''], + SmartAction::ACTION_SET_REACT_STATE => ['#target# wird %11$s.', ''], + SmartAction::ACTION_ACTIVATE_GOBJECT => ['#target# wird aktiviert.', ''], +/* 10*/ SmartAction::ACTION_RANDOM_EMOTE => ['Emote %11$s zu #target#.', ''], + SmartAction::ACTION_CAST => ['Wirke [spell=%1$d] auf #target#.', '%1$s'], + SmartAction::ACTION_SUMMON_CREATURE => ['Beschwöre [npc=%1$d](%4$d)?, den Auslöser angreifend:;(%3$d)? für %11$s:.;', '%1$s'], + SmartAction::ACTION_THREAT_SINGLE_PCT => ['Ändere Bedrohung von #target# um %11$+d%%.', ''], + SmartAction::ACTION_THREAT_ALL_PCT => ['Ändere Bedrohung aller Gegner um %11$+d%%.', ''], + SmartAction::ACTION_CALL_AREAEXPLOREDOREVENTHAPPENS => ['Erfülle Entdeckungsereignis von [quest=%1$d] für #target#.', ''], + SmartAction::ACTION_SET_INGAME_PHASE_ID => null, + SmartAction::ACTION_SET_EMOTE_STATE => ['(%1$d)?Emote [emote=%1$d] kontinuierlich zu #target#:Beende fortlaufenden Emote.;', ''], + SmartAction::ACTION_SET_UNIT_FLAG => ['Setze (%2$d)?UnitFlags2:UnitFlags; %11$s.', ''], + SmartAction::ACTION_REMOVE_UNIT_FLAG => ['Setze (%2$d)?UnitFlags2:UnitFlags; %11$s zurück.', ''], +/* 20*/ SmartAction::ACTION_AUTO_ATTACK => ['(%1$d)?Beginne:Beende; automatische Angriffe gegen #target#.', ''], + SmartAction::ACTION_ALLOW_COMBAT_MOVEMENT => ['(%1$d)?Erlaube:Verbiete; Bewegung im Kampf.', ''], + SmartAction::ACTION_SET_EVENT_PHASE => ['Setze Ereignisphase von #target# auf [b]%1$d[/b].', ''], + SmartAction::ACTION_INC_EVENT_PHASE => ['(%1$d)?Inkrementiere:Dekrementiere; Ereignisphase von #target#.', ''], + SmartAction::ACTION_EVADE => ['#target# entkommt (%1$d)?zur letzten gespeicherten Position:zum Spawnpunkt;.', ''], + SmartAction::ACTION_FLEE_FOR_ASSIST => ['Fliehe nach Hilfe.', 'Benutze Standard Flucht-Emote'], + 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 %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.', ''], + SmartAction::ACTION_CALL_KILLEDMONSTER => ['Ein Tod von [npc=%1$d] wird (%11$s)?%11$s:#target#; gutgeschrieben.', ''], + SmartAction::ACTION_SET_INST_DATA => ['Setze Instanz (%3$d)?Boss State:Datenfeld; #[b]%1$d[/b] auf [b]%2$d[/b].', ''], + SmartAction::ACTION_SET_INST_DATA64 => ['Speichere GUID von #target# in Datenfeld #[b]%1$d[/b] der Instanz', ''], + SmartAction::ACTION_UPDATE_TEMPLATE => ['Transformiere zu [npc=%1$d].', 'Übernehme Stufe von [npc=%1$d]'], + SmartAction::ACTION_DIE => ['Stirb!', ''], + SmartAction::ACTION_SET_IN_COMBAT_WITH_ZONE => ['Beginne Kampf mit allen Einheiten in der Zone.', ''], + SmartAction::ACTION_CALL_FOR_HELP => ['Rufe im Umkreis von %1$dm nach Hilfe.', 'Benutze Standard Hilfe-Emote'], +/* 40*/ SmartAction::ACTION_SET_SHEATH => ['Stecke %11$s -waffen ein.', ''], + SmartAction::ACTION_FORCE_DESPAWN => ['Entferne #target#(%1$d)? nach %11$s:;(%2$d)? und setze es nach %12$s wieder ein.:;', ''], + SmartAction::ACTION_SET_INVINCIBILITY_HP_LEVEL => ['Werde unverwundbar mit weniger als (%2$d)?%2$d%%:%1$d; Gesundheit.', ''], + SmartAction::ACTION_MOUNT_TO_ENTRY_OR_MODEL => ['Sitze (%11$d)?ab:auf; (%1$d)?[npc=%1$d].:;(%2$d)?[model npc=%2$d border=1 float=right][/model]:;', ''], + SmartAction::ACTION_SET_INGAME_PHASE_MASK => ['Setze Sichtbarkeit von #target# auf Phase %11$s.', ''], + SmartAction::ACTION_SET_DATA => ['[b]%2$d[/b] wird in Datenfeld #[b]%1$d[/b] von #target# abgelegt.', ''], + SmartAction::ACTION_ATTACK_STOP => ['Beende Angriff.', ''], + SmartAction::ACTION_SET_VISIBILITY => ['#target# wird (%1$d)?sichtbar:unsichtbar;.', ''], + SmartAction::ACTION_SET_ACTIVE => ['#target# kann(%1$d)?: keine; Grids aktivieren.', ''], + SmartAction::ACTION_ATTACK_START => ['Greife #target# an.', ''], +/* 50*/ SmartAction::ACTION_SUMMON_GO => ['Beschwöre [object=%1$d] bei #target#(%2$d)? für %11$s:.;', 'Verschwinden an Beschwörer geknüpft'], + SmartAction::ACTION_KILL_UNIT => ['#target# stirbt!', ''], + SmartAction::ACTION_ACTIVATE_TAXI => ['Fliege von [span class=q1]%11$s[/span] nach [span class=q1]%12$s[/span]', ''], + SmartAction::ACTION_WP_START => ['(%1$d)?Renne:Gehe; auf Pfad #[b]%2$d[/b].(%4$d)? Verknüpft mit [quest=%4$d].:;(%5$d)? Verschwinde nach %11$s:;', 'Wiederholbar(%12$s)? [DEPRECATED] Reagiere auf dem Pfad %12$s:;'], + SmartAction::ACTION_WP_PAUSE => ['Pausiere Pfad für %11$s.', ''], + SmartAction::ACTION_WP_STOP => ['Beende Pfad(%1$d)? und verschwinde nach %11$s:;. (%2$d)?[quest=%2$d]:Quest vom Pfadanfang; (%3$d)?schlägt fehl:wird abgeschlossen;.', ''], + SmartAction::ACTION_ADD_ITEM => ['Gib #target# %2$d [item=%1$d].', ''], + SmartAction::ACTION_REMOVE_ITEM => ['Nimm %2$d [item=%1$d] von #target#.', ''], + SmartAction::ACTION_INSTALL_AI_TEMPLATE => ['Verhalten als %11$s.', ''], + SmartAction::ACTION_SET_RUN => ['Wähle Bewegung (%1$d)?Rennen:Gehen;.', ''], +/* 60*/ SmartAction::ACTION_SET_DISABLE_GRAVITY => ['(%1$d)?Ignoriere:Berücksichtige; Scherkraft!', ''], + SmartAction::ACTION_SET_SWIM => ['Kann(%1$d)?: nicht; schwimmen.', ''], + SmartAction::ACTION_TELEPORT => ['#target# wird zu [lightbox=map zone=%11$d(%12$s)? pins=%12$s:;]Werltkoordinaten[/lightbox] teleportiert.', ''], + SmartAction::ACTION_SET_COUNTER => ['(%3$d)?Setze:Erhöhe; Zähler #[b]%1$d[/b] von #target# (%3$d)?auf:um; [b]%2$d[/b].', ''], + SmartAction::ACTION_STORE_TARGET_LIST => ['Speichere #target# als Ziel in #[b]%1$d[/b].', ''], + SmartAction::ACTION_WP_RESUME => ['Setze Pfad fort.', ''], + SmartAction::ACTION_SET_ORIENTATION => ['Richte nach (%11$s)?%11$s:\'Heimat\'-Position; aus.', ''], + SmartAction::ACTION_CREATE_TIMED_EVENT => ['(%6$d)?%6$d%% Chance:Löse; Verzögertes Ereignis #[b]%1$d[/b](%11$s)? nach %11$s:; (%6$d)?auszulösen:aus;.', 'Wiederhole alle %s'], + SmartAction::ACTION_PLAYMOVIE => ['Spiele Video #[b]%1$d[/b] für #target# ab.', ''], + SmartAction::ACTION_MOVE_TO_POS => ['Bewege (%4$d)?innerhalb von %4$dm von:nach; Punkt #[b]%1$d[/b] bei #target#(%2$d)? auf einerm Transporter:;.', 'ohne Wegfindung'], +/* 70*/ SmartAction::ACTION_ENABLE_TEMP_GOBJ => ['#target# erscheint für %11$s.', ''], + SmartAction::ACTION_EQUIP => ['(%11$s)?Rüste %11$s:Lege nicht-standard Ausrüstung ab;(%1$d)? vom Ausrüstungs-Template #[b]%1$d[/b]:; an #target#(%12$d)?: aus;.', 'Hinweis: Gegenstände für Kreaturen haben nicht zwingend ein Gegenstands-Template'], + SmartAction::ACTION_CLOSE_GOSSIP => ['Schließe Gossip-Fenster.', ''], + SmartAction::ACTION_TRIGGER_TIMED_EVENT => ['Löse Verzögertes Ereignis #[b]%1$d[/b] aus.', ''], + SmartAction::ACTION_REMOVE_TIMED_EVENT => ['Lösche Verzögertes Ereignis #[b]%1$d[/b].', ''], + SmartAction::ACTION_ADD_AURA => ['Wende Aura von [spell=%1$d] auf #target# an.', ''], + SmartAction::ACTION_OVERRIDE_SCRIPT_BASE_OBJECT => ['Benutze #target# als Basis für alle weiteren SmartAction-Ereignisse.', ''], + SmartAction::ACTION_RESET_SCRIPT_BASE_OBJECT => ['Setze Basis für SmartAction-Ereignisse zurück.', ''], + SmartAction::ACTION_CALL_SCRIPT_RESET => ['Setze aktuelle SmartAI zurück.', ''], + SmartAction::ACTION_SET_RANGED_MOVEMENT => ['Setze Abstand für Fernkampf auf [b]%1$d[/b]m(%2$d)?, %2$d°:;.', ''], +/* 80*/ SmartAction::ACTION_CALL_TIMED_ACTIONLIST => ['Rufe Timed Actionlist [url=#sai-actionlist-%1$d onclick=TalTabClick(%1$d)]#%1$d[/url] auf. Läuft %11$s ab.', ''], + SmartAction::ACTION_SET_NPC_FLAG => ['Setze NpcFlags von #target# auf %11$s.', ''], + SmartAction::ACTION_ADD_NPC_FLAG => ['Füge NpcFlags %11$s #target# hinzu.', ''], + SmartAction::ACTION_REMOVE_NPC_FLAG => ['Entferne NpcFlags %11$s von #target#.', ''], + SmartAction::ACTION_SIMPLE_TALK => ['#target# gibt (%11$s)?TextGroup:[span class=q10]unbekannten Text[/span]; #[b]%1$d[/b] für #target# wieder%11$s', ''], + SmartAction::ACTION_SELF_CAST => ['#target# wirkt [spell=%1$d] auf #target#.(%4$d)? (max. %4$d |4Ziel:Ziele;):;', '%1$s'], + SmartAction::ACTION_CROSS_CAST => ['%11$s wirkt [spell=%1$d] auf #target#.', '%1$s'], + SmartAction::ACTION_CALL_RANDOM_TIMED_ACTIONLIST => ['Rufe zufällige Timed Actionlist auf: %11$s', ''], + SmartAction::ACTION_CALL_RANDOM_RANGE_TIMED_ACTIONLIST => ['Rufe zufällige Timed Actionlist aus Zahlenbreich auf: %11$s', ''], + SmartAction::ACTION_RANDOM_MOVE => ['(%1$d)?Bewege #target# zu zufälligem Punkt innerhalb von %1$dm:#target# beendet zufälllige Bewegung.;', ''], +/* 90*/ SmartAction::ACTION_SET_UNIT_FIELD_BYTES_1 => ['Setze UnitFieldBytes1 von #target# auf %11$s.', ''], + SmartAction::ACTION_REMOVE_UNIT_FIELD_BYTES_1 => ['Entferne UnitFieldBytes1 %11$s von #target#.', ''], + SmartAction::ACTION_INTERRUPT_SPELL => ['Unterbreche Wirken (%2$d)?von [spell=%2$d]:vom aktuellen Zauber;.', '(%1$d)?Inklusive Spontanzauber:; (%3$d)? Inklusive verzögerter Zauber:;'], + SmartAction::ACTION_SEND_GO_CUSTOM_ANIM => ['Setze Animationsfortschritt auf [b]%1$d[/b].', ''], + SmartAction::ACTION_SET_DYNAMIC_FLAG => ['Setze Dynamic Flag von #target# auf %11$s.', ''], + SmartAction::ACTION_ADD_DYNAMIC_FLAG => ['Füge Dynamic Flag %11$s #target# hinzu.', ''], + SmartAction::ACTION_REMOVE_DYNAMIC_FLAG => ['Entferne Dynamic Flag %11$s von #target#.', ''], + SmartAction::ACTION_JUMP_TO_POS => ['Springe auf feste Position — [b]X: %11$.2f, Y: %12$.2f, Z: %13$.2f, [i]v[/i][sub]xy[/sub]: %1$d [i]v[/i][sub]z[/sub]: %2$d[/b]', ''], + SmartAction::ACTION_SEND_GOSSIP_MENU => ['Zeige Gossip #[b]%1$d[/b] / TextID #[b]%2$d[/b].', ''], + SmartAction::ACTION_GO_SET_LOOT_STATE => ['Setze Plündern-Status von #target# auf %11$s.', ''], +/*100*/ SmartAction::ACTION_SEND_TARGET_TO_TARGET => ['Sende gespeicherte Ziele aus #[b]%1$d[/b] an #target#.', ''], + SmartAction::ACTION_SET_HOME_POS => ['Setze Heimat-Position auf (%11$d)?aktuelle Position.:feste Position — [b]X: %11$.2f, Y: %12$.2f, Z: %13$.2f[/b];', ''], + SmartAction::ACTION_SET_HEALTH_REGEN => ['(%1$d)?Lasse:Verhindere; Gesundheitsregeneration von #target#(%1$d)? zu:;.', ''], + SmartAction::ACTION_SET_ROOT => ['(%1$d)?Lasse:Verhindere; Bewegung im Kampf von #target#(%1$d)? zu:;.', ''], + SmartAction::ACTION_SET_GO_FLAG => ['Setze Gameobject Flags von #target# auf %11$s.', ''], + SmartAction::ACTION_ADD_GO_FLAG => ['Füge Gameobject Flag %11$s #target# hinzu.', ''], + SmartAction::ACTION_REMOVE_GO_FLAG => ['Entferne Gameobject Flag %11$s von #target#.', ''], + SmartAction::ACTION_SUMMON_CREATURE_GROUP => ['Beschwöre Kreaturengruppe #[b]%1$d[/b](%2$d)?, den Auslöser angreifend:;.[br](%11$s)?[span class=breadcrumb-arrow] [/span]%11$s:[span class=q0][/span];', ''], + SmartAction::ACTION_SET_POWER => ['Setze %11$s von #target# auf [b]%2$d[/b].', ''], + SmartAction::ACTION_ADD_POWER => ['Gib #target# [b]%2$d[/b] %11$s.', ''], +/*110*/ SmartAction::ACTION_REMOVE_POWER => ['Entferne [b]%2$d[/b] %11$s von #target#.', ''], + SmartAction::ACTION_GAME_EVENT_STOP => ['Beende [event=%1$d].', ''], + SmartAction::ACTION_GAME_EVENT_START => ['Starte [event=%1$d].', ''], + SmartAction::ACTION_START_CLOSEST_WAYPOINT => ['#target# beginnt Pfad. Betritt den Pfad am nächstliegenden dieser Punkte: %11$s.', ''], + SmartAction::ACTION_MOVE_OFFSET => ['Bewege zu relativer Position — [b]X: %12$.2f, Y: %13$.2f, Z: %14$.2f[/b]', ''], + SmartAction::ACTION_RANDOM_SOUND => ['Spiele zufälliges Audio für (%5$d)?auslösenden Spieler:alle Spieler im Sichtfeld;:%11$s', 'Wiedergabe durch Umwelt'], + SmartAction::ACTION_SET_CORPSE_DELAY => ['Setze Verzögerung für das Verschwindne der Leiche von #target# auf %11$s.', 'Zeitfaktor für geplünderte Leichen wird angewendet'], + SmartAction::ACTION_DISABLE_EVADE => ['(%1$d)?Verhindere:Lasse; Entkommen(%1$d)?: zu;.', ''], + SmartAction::ACTION_GO_SET_GO_STATE => ['Setze Gameobject Status auf %11$s.', ''], + SmartAction::ACTION_SET_CAN_FLY => ['(%1$d)?Lasse:Verhindere; Fliegen(%1$d)? zu:;.', ''], +/*120*/ SmartAction::ACTION_REMOVE_AURAS_BY_TYPE => ['Entferne alle Auren mit [b]%11$s[/b] von #target#.', ''], + SmartAction::ACTION_SET_SIGHT_DIST => ['Setze Sichtweite von #target# auf %1$dm.', ''], + SmartAction::ACTION_FLEE => ['#target# flieht für %11$s und sucht Hilfe.', ''], + SmartAction::ACTION_ADD_THREAT => ['Ändere Bedrohung von #target# um %11$+d Punkte.', ''], + SmartAction::ACTION_LOAD_EQUIPMENT => ['(%2$d)?Lege nicht-standard Ausrüstung ab:Rüste %11$s; von Ausrüstungs-Template #[b]%1$d[/b] an #target#(%12$d)?: aus;.', 'Hinweis: Gegenstände für Kreaturen haben nicht zwingend ein Gegenstands-Template'], + SmartAction::ACTION_TRIGGER_RANDOM_TIMED_EVENT => ['Löse definiertes verzögertes Ereignis innerhalb von %11$s aus.', ''], + SmartAction::ACTION_REMOVE_ALL_GAMEOBJECTS => ['Entferne alle Gameobjects von #target#.', ''], + SmartAction::ACTION_PAUSE_MOVEMENT => ['Pausiere Bewegung aus Slot #[b]%1$d[/b] für %11$s.', 'Erzwungen'], + 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 %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.', ''], + SmartAction::ACTION_SET_MOVEMENT_SPEED => ['Setze Geschwindigkeit von MotionType #[b]%1$d[/b] auf [b]%11$.2f[/b]', ''], + SmartAction::ACTION_PLAY_SPELL_VISUAL_KIT => null, + SmartAction::ACTION_OVERRIDE_LIGHT => ['(%3$d)?Ändere Skybox in [zone=%1$d] auf #[b]%3$d[/b]:Setze Skybox in [zone=%1$d] zurück;.', 'Übergang: %s'], + SmartAction::ACTION_OVERRIDE_WEATHER => ['Ändere Wetter in [zone=%1$d] zu %11$s mit %3$d%% Stärke.', ''], +/*140*/ SmartAction::ACTION_SET_AI_ANIM_KIT => null, + SmartAction::ACTION_SET_HOVER => ['(%1$d)?Aktiviere:Deaktiviere; Schweben.', ''], + SmartAction::ACTION_SET_HEALTH_PCT => ['Set health percentage of #target# to %1$d%%.', ''], + SmartAction::ACTION_CREATE_CONVERSATION => null, + SmartAction::ACTION_SET_IMMUNE_PC => ['(%1$d)?Aktiviere:Deaktiviere; #target# Immunität gegen Spieler.', ''], + SmartAction::ACTION_SET_IMMUNE_NPC => ['(%1$d)?Aktiviere:Deaktiviere; #target# Immunität gegen NPCs.', ''], + SmartAction::ACTION_SET_UNINTERACTIBLE => ['(%1$d)?Verhindere:Erlaube; Interaktion mit #target#.', ''], + SmartAction::ACTION_ACTIVATE_GAMEOBJECT => ['Aktiviere Gameobject (Methode: %1$d)', ''], + SmartAction::ACTION_ADD_TO_STORED_TARGET_LIST => ['Füge #target# zur Zieleliste #%1$d hinzu.', ''], + SmartAction::ACTION_BECOME_PERSONAL_CLONE_FOR_PLAYER => null, +/*150*/ SmartAction::ACTION_TRIGGER_GAME_EVENT => null, + SmartAction::ACTION_DO_ACTION => null + ), + 'targetUNK' => '[span class=q10]unbekanntes Ziel#[b class=q1]%d[/b][/span]', + 'targetTT' => '[b class=q1]TargetType %d[/b][br][table][tr][td]Param1[/td][td=header]%d[/td][/tr][tr][td]Param2[/td][td=header]%d[/td][/tr][tr][td]Param3[/td][td=header]%d[/td][/tr][tr][td]Param4[/td][td=header]%d[/td][/tr][tr][td]X[/td][td=header]%17$.2f[/td][/tr][tr][td]Y[/td][td=header]%18$.2f[/td][/tr][tr][td]Z[/td][td=header]%19$.2f[/td][/tr][tr][td]O[/td][td=header]%20$.2f[/td][/tr][/table]', + 'targets' => array( + SmartTarget::TARGET_NONE => '[span class=q0][/span]', + SmartTarget::TARGET_SELF => 'selbst', + SmartTarget::TARGET_VICTIM => 'Gegner', + SmartTarget::TARGET_HOSTILE_SECOND_AGGRO => '(%2$d)?Spieler:Einheit;(%11$s)? mit %11$s:;(%1$d)? innerhalb von %1$dm:; an 2. Stelle in Aggro', + SmartTarget::TARGET_HOSTILE_LAST_AGGRO => '(%2$d)?Spieler:Einheit;(%11$s)? mit %11$s:;(%1$d)? innerhalb von %1$dm:; an letzter Stelle in Aggro', + SmartTarget::TARGET_HOSTILE_RANDOM => '(%2$d)?zufälliger Spieler:zufällige Einheit;(%11$s)? mit %11$s:;(%1$d)? innerhalb von %1$dm:;', + SmartTarget::TARGET_HOSTILE_RANDOM_NOT_TOP => '(%2$d)?zufälliger nicht-Tank Spieler:zufällige nicht-Tank Einheit;(%11$s)? mit %11$s:;(%1$d)? innerhalb von %1$dm:;', + SmartTarget::TARGET_ACTION_INVOKER => 'Auslöser', + SmartTarget::TARGET_POSITION => 'Weltkoordinaten', + SmartTarget::TARGET_CREATURE_RANGE => '(%1$d)?Instanz von [npc=%1$d]:beliebige Kreatur; innerhalb von %11$sm(%4$d)? (max. %4$d |4Ziel:Ziele;):;', +/*10*/ SmartTarget::TARGET_CREATURE_GUID => '(%11$d)?[npc=%11$d]:NPC; [small class=q0](GUID: %1$d)[/small]', + SmartTarget::TARGET_CREATURE_DISTANCE => '(%1$d)?Instanz von [npc=%1$d]:beliebige Kreatur;(%2$d)? innerhalb von %2$dm:;(%3$d)? (max. %3$d |4Ziel:Ziele;):;', + SmartTarget::TARGET_STORED => 'vorher gespeichertes Ziel', + SmartTarget::TARGET_GAMEOBJECT_RANGE => '(%1$d)?Instanz von [object=%1$d]:beliebiges Objekt; innerhalb von %11$sm(%4$d)? (max. %4$d |4Ziel:Ziele;):;', + SmartTarget::TARGET_GAMEOBJECT_GUID => '(%11$d)?[object=%11$d]:Gameobject; [small class=q0](GUID: %1$d)[/small]', + SmartTarget::TARGET_GAMEOBJECT_DISTANCE => '(%1$d)?Instanz von [object=%1$d]:beliebiges Objekt;(%2$d)? innerhalb von %2$dm:;(%3$d)? (max. %3$d |4Ziel:Ziele;):;', + SmartTarget::TARGET_INVOKER_PARTY => 'Gruppe des Auslösenden', + SmartTarget::TARGET_PLAYER_RANGE => 'alle Spieler innerhalb von %11$sm', + SmartTarget::TARGET_PLAYER_DISTANCE => 'alle Spieler innerhalb von %1$dm', + SmartTarget::TARGET_CLOSEST_CREATURE => 'näheste (%3$d)?tote:lebendige; (%1$d)?[npc=%1$d]:beliebige Kreatur; innerhalb von (%2$d)?%2$d:100;m', +/*20*/ SmartTarget::TARGET_CLOSEST_GAMEOBJECT => 'nähestes (%1$d)?[object=%1$d]:beliebiges Gameobject; innerhalb von (%2$d)?%2$d:100;m', + SmartTarget::TARGET_CLOSEST_PLAYER => 'nähester Spieler innerhalb von %1$dm', + SmartTarget::TARGET_ACTION_INVOKER_VEHICLE => 'Fahrzeug des Auslösenden', + SmartTarget::TARGET_OWNER_OR_SUMMONER => 'Besitzer oder Beschwörer', + SmartTarget::TARGET_THREAT_LIST => 'alle Einheiten(%1$d)? innerhalb von %1$dm:;, die mit mir im Kampf sind', + SmartTarget::TARGET_CLOSEST_ENEMY => '(%2$d)?nähester angreifbarer Spieler:näheste angreifbare Einheit; innerhalb von %1$dm', + SmartTarget::TARGET_CLOSEST_FRIENDLY => '(%2$d)?nähester freundlicher Spieler:näheste freundliche Einheit; innerhalb von %1$dm', + SmartTarget::TARGET_LOOT_RECIPIENTS => 'alle Spieler mit Lootberechtigung', + SmartTarget::TARGET_FARTHEST => 'am weitesten (%2$d)?entferter, kämpfender Spieler:entferte, kämpfende Einheit; innerhalb von %1$dm(%3$d)? und im Sichtfeld:;', + SmartTarget::TARGET_VEHICLE_PASSENGER => 'Fahrzeugzusatz in (%1$d)?Sitz %11$s:allen Sitzen;', +/*30*/ SmartTarget::TARGET_CLOSEST_UNSPAWNED_GO => '(%1$d)?näheste ungespawnte Instanz von [object=%1$d]:nähestes ungespawntes Objekt; innerhalb von %11$sm(%3$d)' + ), + 'castFlags' => array( + SmartAI::CAST_FLAG_INTERRUPT_PREV => 'Unterbreche aktives wirken', + SmartAI::CAST_FLAG_TRIGGERED => 'Ausgelöst', + SmartAI::CAST_FLAG_AURA_MISSING => 'Aura fehlt', + SmartAI::CAST_FLAG_COMBAT_MOVE => 'Bewegung im Kampf' + ), + 'spawnFlags' => array( + SmartAI::SPAWN_FLAG_IGNORE_RESPAWN => 'Überschreibe und resette Respawnzeit', + SmartAI::SPAWN_FLAG_FORCE_SPAWN => 'Erzwinge spawn, wenn bereits gespawnt', + SmartAI::SPAWN_FLAG_NOSAVE_RESPAWN => 'Lösche Respawnzeit bei Despawn' + ), + 'GOStates' => ['aktiv', 'bereit', 'zerstört'], + 'summonTypes' => [null, 'Despawn nach Zeit oder wenn Leiche verschwindet', 'Despawn nach Zeit oder beim Sterben', 'Despawn nach Zeit', 'Despawn nach Zeit ausserhalb des Kampfes', 'Despawn beim Sterben', 'Despawn nach Zeit nach dem Tod', 'Despawn wenn Leiche verschwindet', 'Manueller despawn'], + 'aiTpl' => ['einfache KI', 'Zauberer', 'Geschütz', 'passive Kreatur', 'Käfig für Kreatur', 'Kreatur im Käfig'], + 'reactStates' => ['passiv', 'defensiv', 'aggressiv', 'helfend'], + 'sheaths' => ['alle', 'Nahkampf', 'Fernkampf'], + 'saiUpdate' => ['ausserhalb des Kampfes', 'im Kampf', 'immer'], + 'lootStates' => ['Nicht bereit', 'Bereit', 'Aktiviert', 'Gerade deaktiviert'], + 'weatherStates' => ['Schön', 'Nebel', 'Niesel', 'leichter Regen', 'Regen', 'starker Rain', 'leichter Schneefall', 'Schneefall', 'starker Schneefall', 22 => 'leichter Sandsturm', 41=> 'Sandsturm', 42 => 'starker Sandsturm', 86 => 'Gewitter', 90 => 'schwarzer Regen', 106 => 'schwarzer Schneefall'], + 'hostilityModes' => ['feindlich', 'nicht-feindlich', ''/*any*/], + 'motionTypes' => ['IdleMotion', 'RandomMotion', 'WaypointMotion', null, 'ConfusedMotion', 'ChaseMotion', 'HomeMotion', 'FlightMotion', 'PointMotion', 'FleeingMotion', 'DistractMotion', 'AssistanceMotion', 'AssistanceDistractMotion', 'TimedFleeingMotion', 'FollowMotion', 'RotateMotion', 'EffectMotion', 'SplineChainMotion', 'FormationMotion'], + + 'GOStateUNK' => '[span class=q10]unbekannter Gameobject-Status #[b class=q1]%d[/b][/span]', + 'summonTypeUNK' => '[span class=q10]unbekannter SummonType #[b class=q1]%d[/b][/span]', + 'aiTplUNK' => '[span class=q10]unbekanntes AI-Template #[b class=q1]%d[/b][/span]', + 'reactStateUNK' => '[span class=q10]unbekannter ReactState #[b class=q1]%d[/b][/span]', + 'sheathUNK' => '[span class=q10]unbekannter sheath #[b class=q1]%d[/b][/span]', + 'saiUpdateUNK' => '[span class=q10]unbekannte Updatebedingung #[b class=q1]%d[/b][/span]', + 'lootStateUNK' => '[span class=q10]unbekannter Plündern-Status #[b class=q1]%d[/b][/span]', + 'weatherStateUNK' => '[span class=q10]unbekannter Wetter-Zustand #[b class=q1]%d[/b][/span]', + 'powerTypeUNK' => '[span class=q10]unbekannte Ressource #[b class=q1]%d[/b][/span]', + 'hostilityModeUNK' => '[span class=q10]unbekannte Gegner-Beziehung #[b class=q1]%d[/b][/span]', + 'motionTypeUNK' => '[span class=q10]unbekannter Bewegungstyp #[b class=q1]%d[/b][/span]', + 'entityUNK' => '[b class=q10]unbekannte Entität[/b]', + + 'empty' => '[span class=q0][/span]' ), 'account' => array( 'title' => "Aowow-Konto", @@ -338,7 +930,6 @@ $lang = array( "Screenshot-Verwalter", "Video-Verwalter", "API-Partner", "Ausstehend" ), // signIn - 'doSignIn' => "Mit Eurem AoWoW-Konto anmelden", 'signIn' => "Anmelden", 'user' => "Benutzername", 'pass' => "Kennwort", @@ -349,95 +940,218 @@ $lang = array( 'accCreate' => 'Noch kein Konto? Jetzt eins erstellen!', // recovery - 'recoverUser' => "Benutzernamenanfrage", - 'recoverPass' => "Kennwort zurücksetzen: Schritt %s von 2", - 'newPass' => "Neues Kennwort", + 'newPass' => "Neues Kennwort:", + 'confNewPass' => "Neues Kennwort bestätigen:", + 'passResetHint' => 'Wenn ihr euer Kennwort nicht mehr wisst, könnt ihr es auf dieser Seite zurücksetzen.', + // 'tokenExpires' => "Das Token wird in %s verfallen.", // creation - 'register' => "Registrierung: Schritt %s von 2", - 'passConfirm' => "Kennwort bestätigen", + 'passConfirm' => "Kennwort bestätigen:", // dashboard - 'ipAddress' => "IP-Adresse", - 'lastIP' => "Letzte bekannte IP", - 'myAccount' => "Mein Account", - 'editAccount' => "Benutze die folgenden Formulare um deine Account-Informationen zu aktualisieren", - 'viewPubDesc' => 'Die Beschreibung in deinem öffentlichen Profil ansehen', + 'ipAddress' => "IP-Adresse: ", + 'lastIP' => "Letzte bekannte IP: ", + // 'myAccount' => "Mein Account", + // 'editAccount' => "Benutze die folgenden Formulare um deine Account-Informationen zu aktualisieren", + // 'viewPubDesc' => 'Die Beschreibung in deinem öffentlichen Profil ansehen', // bans 'accBanned' => "Dieses Konto wurde geschlossen", - 'bannedBy' => "Gebannt durch", - 'ends' => "Endet am", + 'bannedBy' => "Gebannt durch: ", + 'reason' => "Grund: ", + 'ends' => "Endet am: ", 'permanent' => "Der Bann ist permanent", - 'reason' => "Grund", 'noReason' => "Es wurde kein Grund angegeben.", // form-text 'emailInvalid' => "Diese E-Mail-Adresse ist ungültig.", // message_emailnotvalid - 'emailNotFound' => "Die E-Mail-Adresse, die Ihr eingegeben habt, ist mit keinem Konto verbunden.

Falls Ihr die E-Mail-Adresse vergessen habt, mit der Ihr Euer Konto erstellt habt, kontaktiert Ihr bitte ".CFG_CONTACT_EMAIL." für Hilfestellung.", - 'createAccSent' => "Eine Nachricht wurde soeben an %s versandt. Folgt den Anweisungen um euer Konto zu erstellen.", - 'recovUserSent' => "Eine Nachricht wurde soeben an %s versandt. Folgt den Anweisungen um euren Benutzernamen zu erhalten.", - 'recovPassSent' => "Eine Nachricht wurde soeben an %s versandt. Folgt den Anweisungen um euer Kennwort zurückzusetzen.", - 'accActivated' => 'Euer Konto wurde soeben aktiviert.
Ihr könnt euch nun anmelden', 'userNotFound' => "Ein Konto mit diesem Namen existiert nicht.", 'wrongPass' => "Dieses Kennwort ist ungültig.", - // 'accInactive' => "Dieses Konto wurde bisher nicht aktiviert.", - 'loginExceeded' => "Die maximale Anzahl an Anmelde-Versuchen von dieser IP wurde überschritten. Bitte versucht es in %s erneut.", - 'signupExceeded'=> "Die maximale Anzahl an Regustrierungen von dieser IP wurde überschritten. Bitte versucht es in %s erneut.", + // 'accInactive' => "Dieses Konto wurde bisher nicht aktiviert.", 'errNameLength' => "Euer Benutzername muss mindestens 4 Zeichen lang sein.", // message_usernamemin 'errNameChars' => "Euer Benutzername kann nur aus Buchstaben und Zahlen bestehen.", // message_usernamenotvalid 'errPassLength' => "Euer Kennwort muss mindestens 6 Zeichen lang sein.", // message_passwordmin 'passMismatch' => "Die eingegebenen Kennworte stimmen nicht überein.", 'nameInUse' => "Es existiert bereits ein Konto mit diesem Namen.", 'mailInUse' => "Diese E-Mail-Adresse ist bereits mit einem Konto verbunden.", - 'isRecovering' => "Dieses Konto wird bereits wiederhergestellt. Folgt den Anweisungen in der Nachricht oder wartet %s bis das Token verfällt.", 'passCheckFail' => "Die Kennwörter stimmen nicht überein.", // message_passwordsdonotmatch - 'newPassDiff' => "Euer neues Kennwort muss sich von eurem alten Kennwort unterscheiden." // message_newpassdifferent + 'newPassDiff' => "Euer neues Kennwort muss sich von eurem alten Kennwort unterscheiden.", // message_newpassdifferent + 'newMailDiff' => "Eure neue E-Mail-Adresse muss sich von eurer alten E-Mail-Adresse unterscheiden.", // message_newemaildifferent + + // premium avatar manager + 'uploadAvatar' => "Neuen Avatar hochladen", + 'goToManager' => "Zur Avatarverwaltung gehen", + 'manageAvatars' => "Avatare verwalten", + 'avatarSlots' => '%1$d / %2$d Avatarplätze belegt', + 'manageBorders' => "Premium Rahmen verwalten", + 'selectAvatar' => "Wählt einen Avatar zum hochladen.", + 'errTooSmall' => "Euer Avatar muss wenigstens %dpx groß sein.", + 'cropAvatar' => "Ihr könnt Euren Avatar zuschneiden.", + 'avatarSubmit' => "Avatar-Einsendung", + 'reminder' => "Erinnerung", + 'avatarCoC' => "Dass Benutzen von Bildern, die gegen die Regeln verstoßen kann zum Verlust Eures Premium-Status führen.", + + // settings + 'settings' => "Kontoeinstellungen", + 'settingsNote' => "Du kannst einfach die unten stehenden Formulare ausfüllen, um deine Kontodaten zu aktualisieren.", + 'tabGeneral' => "Allgemein", + 'tabPersonal' => "Persönliches", + 'tabCommunity' => "Community", + 'tabPremium' => "Premium", + 'preferences' => "Voreinstellungen", + 'modelviewer' => "Modellviewer", + 'mvNote' => "Vorgegebenes Charaktermodell:", + 'lists' => "Listen", + 'listsNote' => "Zeigt IDs in unterstützten Listen", + 'announcements' => "Bekanntmachungen", + 'annNote' => "Entfernt die Daten von Bekanntmachungen, die du geschlossen hast, damit sie wieder sichtbar werden.", + 'purge' => "Löschen", + 'curPass' => "Derzeitiges Kennwort:", + 'globalLogout' => "Von allen Browsern/Geräten abmelden", + 'curEmail' => "Momentane E-Mail-Adresse:", + 'newEmail' => "Neue E-Mail-Adresse:", + 'userPage' => "Benutzerseite", + 'publicDesc' => "Öffentliche Beschreibung", + 'publicDescNote'=> 'Erzähl uns etwas über dich und deine WoW-Charaktere. Alles, was du hier eingibst, erscheint auf deiner Benutzerseite.', + 'forums' => "Foren", + 'signature' => "Signatur", + 'signatureNote' => "Deine Signatur erscheint unter all deinen Forenbeiträgen.", + 'usernameNote' => "Nutzernamen können nur einmal alle %s geändert werden und müssen 4-16 Zeichen lang sein. Sonderzeichen sind nicht erlaubt.", + 'curName' => "Aktueller Nutzername:", + 'newName' => "Neuer Nutzername:", + 'accDelete' => "Konto löschen", + 'accDeleteNote' => 'Wenn du dein Konto und alle persönlichen Daten vollständig löschen möchtest, dann geh zu unserer Kontolöschung.', + 'avatar' => "Avatar", + 'avatarNote' => "Dein Avatar wird neben all deinen Forenbeiträgen angezeigt.", + 'avWowIcon' => "World of Warcraft-Icon", + 'avWowIconNote' => 'z.B. INV_Axe_54
Tipp: Um den Namen eines Symbols herauszufinden, doppelklickt einfach auf das große Symbol, während ihr auf einer Gegenstands- oder Zauberseite seid. Kopiert den Text anschließend und fügt ihn oben ein.', + 'avIconName' => "Symbolname:", + 'none' => "Keins", + 'preview' => "Vorschau", + 'custom' => "Benutzerdefiniert", + 'premiumStatus' => "Premium Status", + 'status' => "Status", + 'active' => "Activ", + 'inactive' => "Inaktiv", + 'activeCD' => "Ihr müsst bis zum %s warten um euren Nutzernamen erneut zu ändern.", + 'updateMessage' => array( + 'general' => "Deine Einstellungen wurden aktualisiert.", + 'community' => "Eure öffentliche Beschreibung und Forensignatur wurden erfolgreich aktualisiert.", + 'personal' => "Eine Bestätigungsnachricht wurde an %s versandt.", + 'username' => 'Nutzername von %1$s zu %2$s geändert.', + 'avNotFound' => "Symbol nicht gefunden.", + 'avSuccess' => "Euer Avatar wurde erfolgreich aktualisiert.", + 'avNoChange' => "Es wurden keine Änderungen durchgeführt.", + 'av1stUser' => "Glückwunsch! Ihr habt eine einzigartige Auswahl getroffen! /jubeln", + 'avNthUser' => "Zur Eurer Information, Euer Symbol wird bereits von %d anderen Benutzer(n) benutzt." + ), + 'inputbox' => array( + 'head' => array( + 'success' => "Erfolg", + 'error' => "Hoppla!", + 'register' => "Registrierung: Schritt %s von 2", + 'recoverUser' => "Benutzernamenanfrage", + 'recoverPass' => "Kennwort zurücksetzen: Schritt %s von 2", + 'resendMail' => "Bestätigungsmail erneut senden", + 'signin' => "Mit Eurem Konto anmelden" + ), + 'message' => array( + 'accActivated' => 'Euer Konto wurde soeben aktiviert.
Ihr könnt euch nun anmelden', + 'resendMail' => "Wenn Sie sich registriert haben, aber keine Bestätigungs-E-Mail erhalten haben, geben Sie Ihre E-Mail-Adresse unten ein und senden Sie das Formular ab. (Bitte überprüfen Sie Ihre Spam- oder Papierkorb-Ordner, um sicherzustellen, dass die E-Mail nicht versehentlich an der falschen Stelle abgelegt wurde!)", + 'mailChangeOk' => "Ihre E-Mail-Adresse wurde erfolgreich geändert.", + 'mailRevertOk' => "Ihre Anfrage zur Änderung der E-Mail-Adresse wurde storniert/zurückgesetzt.", + 'passChangeOk' => "Ihr Kennwort wurde erfolgreich geändert.", + 'deleteAccSent' => "Eine E-Mail mit einem Bestätigungslink wurde an %s gesendet.", + 'deleteOk' => "Ihr Konto wurde erfolgreich entfernt. Wir hoffen, Sie bald wiederzusehen!

Sie können dieses Fenster jetzt schließen.", + 'deleteCancel' => "Die Kontolöschung wurde abgebrochen.", + 'createAccSent' => 'Eine Nachricht wurde soeben an %s versandt. Folgt einfach den darin enthaltenen Anweisungen, um Euer Konto zu erstellen.

Falls du keine Bestätigungsnachricht erhalten hast klicke hier um eine neue zu senden.', + 'recovUserSent' => "Eine Nachricht wurde soeben an %s versandt. Folgt einfach den darin enthaltenen Anweisungen, um euren Benutzernamen zu erhalten.", + 'recovPassSent' => "Eine Nachricht wurde soeben an %s versandt. Folgt einfach den darin enthaltenen Anweisungen, um euer Kennwort zurückzusetzen.", + ), + 'error' => array( + 'mailTokenUsed' => 'Dieser Schlüssel zur Änderung der E-Mail-Adresse wurde entweder bereits verwendet oder ist ungültig. Besuchen Sie Ihre Kontoeinstellungen, um es erneut zu versuchen.', + 'passTokenUsed' => 'Dieser Schlüssel zur Änderung des Kennworts wurde entweder bereits verwendet oder ist ungültig. Besuchen Sie Ihre Kontoeinstellungen, um es erneut zu versuchen.', + 'purgeTokenUsed' => 'Dieser Schlüssel zum Löschen des Kontos wurde entweder bereits verwendet oder ist ungültig. Besuchen Sie Ihre Kontoeinstellungen, um es erneut zu versuchen.', + 'passTokenLost' => "Kein Token wurde bereitgestellt. Wenn Sie in einer E-Mail einen Link zum Zurücksetzen des Kennworts erhalten haben, kopieren Sie die gesamte URL (einschließlich des Tokens am Ende) in die Adressleiste Ihres Browsers.", + 'isRecovering' => "Dieses Konto wird bereits wiederhergestellt. Folgt den Anweisungen in der Nachricht oder wartet %s bis das Token verfällt.", + 'loginExceeded' => "Die maximale Anzahl an Anmelde-Versuchen von dieser IP wurde überschritten. Bitte versucht es in %s erneut.", + 'signupExceeded' => "Die maximale Anzahl an Registrierungen von dieser IP wurde überschritten. Bitte versucht es in %s erneut.", + // 'emailNotFound' => "Die E-Mail-Adresse, die Ihr eingegeben habt, ist mit keinem Konto verbunden.

Falls Ihr die E-Mail-Adresse vergessen habt, mit der Ihr Euer Konto erstellt habt, kontaktiert Ihr bitte CFG_CONTACT_EMAIL für Hilfestellung.", + 'emailNotFound' => "Diese E-Mail-Adresse wurde in unserem System nicht gefunden.", + ) + ) ), 'user' => array( 'notFound' => "Der Benutzer \"%s\" wurde nicht gefunden!", 'removed' => "(Entfernt)", - 'joinDate' => "Mitglied seit", - 'lastLogin' => "Letzter Besuch", - 'userGroups' => "Rolle", - 'consecVisits' => "Aufeinanderfolgende Besuche", + 'joinDate' => "Mitglied seit: ", + 'lastLogin' => "Letzter Besuch: ", + 'userGroups' => "Rolle: ", + 'consecVisits' => "Aufeinanderfolgende Besuche: ", 'publicDesc' => "Öffentliche Beschreibung", 'profileTitle' => "Profil von %s", 'contributions' => "Beiträge", - 'uploads' => "Hochladevorgänge", - 'comments' => "Kommentare", - 'screenshots' => "Screenshots", - 'videos' => "Videos", - 'posts' => "Forenbeiträge" - ), - 'mail' => array( - 'tokenExpires' => "Das Token wird in %s verfallen.", - 'accConfirm' => ["Kontobestätigung", "Willkommen bei ".CFG_NAME_SHORT."!\r\n\r\nKlicke auf den Link um euren Account zu aktivieren.\r\n\r\n".HOST_URL."?account=signup&token=%s\r\n\r\nFalls Ihr diese Mail nicht angefordert habt kann sie einfach ignoriert werden."], - 'recoverUser' => ["Benutzernamenanfrage", "Folgt diesem Link um euch anzumelden.\r\n\r\n".HOST_URL."?account=signin&token=%s\r\n\r\nFalls Ihr diese Mail nicht angefordert habt kann sie einfach ignoriert werden."], - 'resetPass' => ["Kennwortreset", "Folgt diesem Link um euer Kennwort zurückzusetzen.\r\n\r\n".HOST_URL."?account=forgotpassword&token=%s\r\n\r\nFalls Ihr diese Mail nicht angefordert habt kann sie einfach ignoriert werden."] + 'uploads' => "Hochladevorgänge: ", + 'comments' => "Kommentare: ", + 'screenshots' => "Screenshots: ", + 'videos' => "Videos: ", + 'posts' => "Forenbeiträge: " ), 'emote' => array( + 'id' => "Emote-ID: ", 'notFound' => "Dieses Emote existiert nicht.", - 'self' => "An Euch selbst", - 'target' => "An Andere mit Ziel", - 'noTarget' => "An Andere ohne Ziel", +// 'self' => "An Euch selbst", +// 'target' => "An Andere mit Ziel", +// 'noTarget' => "An Andere ohne Ziel", + 'targeted' => "Benutzt mit Ziel", + 'untargeted' => "Benutzt ohne Ziel", 'isAnimated' => "Besitzt eine Animation", + 'eventSound' => "Ereignis-Klang", 'aliases' => "Aliasse", 'noText' => "Dieses Emote besitzt keinen Text.", + 'noCommand' => "Dieses Emote besitzt keinen /-Befehl. Es kann nicht benutzt werden.", + 'flags' => array( + EMOTE_FLAG_ONLY_STANDING => "Nur im stehen", + EMOTE_FLAG_USE_MOUNT => "Emote benutzt das Reittier", + EMOTE_FLAG_NOT_CHANNELING => "Nicht beim kanalisieren", + EMOTE_FLAG_ANIM_TALK => "Talk anim - Reden", + EMOTE_FLAG_ANIM_QUESTION => "Talk anim - Fragen", + EMOTE_FLAG_ANIM_EXCLAIM => "Talk anim - Ausrufen", + EMOTE_FLAG_ANIM_SHOUT => "Talk anim - Schreien", + EMOTE_FLAG_NOT_SWIMMING => "Nicht beim schwimmen", + EMOTE_FLAG_ANIM_LAUGH => "Talk anim - Lachen", + EMOTE_FLAG_CAN_LIE_ON_GROUND => "Nutzbar im Schlaf/Tot", + EMOTE_FLAG_NOT_FROM_CLIENT => "Nur für Kreaturen", + EMOTE_FLAG_NOT_CASTING => "Nicht beim Zaubern", + EMOTE_FLAG_END_MOVEMENT => "Emote endet Bewegung", + EMOTE_FLAG_INTERRUPT_ON_ATTACK => "Unerbrochen durch Kampf", + EMOTE_FLAG_ONLY_STILL => "Nur in Ruhe", + EMOTE_FLAG_NOT_FLYING => "Nicht im Flug" + ), + '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" ) ), + '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.", - 'cat' => [0 => "Anderes", 9 => "Bücher", 3 => "Behälter", -5 => "Truhen", 25 => "Fischschwärme", -3 => "Kräuter", -4 => "Erzadern", -2 => "Quest", -6 => "Werkzeuge"], - 'type' => [ 9 => "Buch", 3 => "Behälter", -5 => "Truhe", 25 => "", -3 => "Kraut", -4 => "Erzvorkommen", -2 => "Quest", -6 => ""], + '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.", 'npcLootPH' => 'Der Behälter %s beinhaltet die Beute vom Kampf gegen %s. Er erscheint nach seinem Tod.', 'key' => "Schlüssel", @@ -447,73 +1161,194 @@ $lang = array( 'triggeredBy' => "Ausgelöst durch", 'capturePoint' => "Eroberungspunkt", 'foundIn' => "Dieses Objekt befindet sich in", - 'restock' => "Wird alle %s wieder aufgefüllt." + '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_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", + "Disturb / Trigger Trap", "Unlock", "Lock", "Open", "Unlock & Open", + "Close", "Toggle Open", "Destroy", "Rebuild", "Creation", + "Despawn", "Make Inert", "Make Active", "Close & Lock", "Use ArtKit 0", + "Use ArtKit 1", "Use ArtKit 2", "Use ArtKit 3", "Set Tap List" + ) ), 'npc' => array( + 'id' => "NPC-ID: ", 'notFound' => "Dieser NPC existiert nicht.", - 'classification'=> "Einstufung", - 'petFamily' => "Tierart", - 'react' => "Reaktion", - 'worth' => "Wert", + 'classification'=> "Einstufung: %s", + 'petFamily' => "Tierart: ", + 'react' => "Reaktion: %s", + 'worth' => "Wert: %s", 'unkPosition' => "Der Aufenthaltsort dieses NPCs ist nicht bekannt.", - 'difficultyPH' => "Dieser NPC ist ein Platzhalter für einen anderen Modus von", + 'difficultyPH' => 'Dieser NPC ist ein Platzhalter für einen anderen Modus von %2$s.', 'seat' => "Sitz", 'accessory' => "Zusätze", 'accessoryFor' => "Dieser NPC ist Zusatz für Fahrzeug", - 'quotes' => "Zitate", - 'gainsDesc' => "Nach dem Töten dieses NPCs erhaltet Ihr", + 'quotes' => "Zitate (%d)", + 'gainsDesc' => "Nach dem Töten dieses NPCs erhaltet Ihr: ", 'repWith' => "Ruf mit der Fraktion", 'stopsAt' => "Endet bei %s", 'vehicle' => "Fahrzeug", 'stats' => "Werte", - 'melee' => "Nahkampf", - 'ranged' => "Fernkampf", - 'armor' => "Rüstung", + 'melee' => "Nahkampf: ", + 'ranged' => "Fernkampf: ", + 'armor' => "Rüstung: ", + 'resistances' => "Widerstände: ", 'foundIn' => "Dieser NPC befindet sich in", 'tameable' => "Zähmbar (%s)", 'waypoint' => "Wegpunkt", 'wait' => "Wartezeit", - 'respawnIn' => "Wiedereinstieg in", + 'respawnIn' => "Respawn in: %s", + 'despawnAfter' => "Gespawnt durch Script
Despawn nach: %s", 'rank' => [0 => "Normal", 1 => "Elite", 4 => "Rar", 2 => "Rar Elite", 3 => "Boss"], 'textRanges' => [null, "an das Gebiet gesendet", "an die Zone gesendet", "an die Map gesendet", "an die Welt gesendet"], 'textTypes' => [null, "schreit", "sagt", "flüstert"], - 'modes' => array( - 1 => ["Normal", "Heroisch"], - 2 => ["10-Spieler Normal", "25-Spieler Normal", "10-Spieler Heroisch", "25-Spieler Heroisch"] - ), + 'mechanicimmune'=> 'Nicht anfällig für Mechanik: %s', + '_extraFlags' => 'Extra Flags: ', + 'versions' => 'Schwierigkeitsgrade: ', 'cat' => array( "Nicht kategorisiert", "Wildtiere", "Drachkin", "Dämonen", "Elementare", "Riesen", "Untote", "Humanoide", "Tiere", "Mechanisch", "Nicht spezifiziert", "Totems", "Haustiere", "Gaswolken" + ), + 'npcFlags' => array( + NPC_FLAG_GOSSIP => 'Gossip', + NPC_FLAG_QUEST_GIVER => 'Questgeber', + NPC_FLAG_TRAINER => 'Lehrer', + NPC_FLAG_CLASS_TRAINER => 'Klassen-Lehrer', + NPC_PROFESSION_TRAINER => 'Berufe-Lehrer', + NPC_FLAG_VENDOR => 'Händler', + NPC_FLAG_VENDOR_AMMO => 'Munitionshändler', + NPC_FLAG_VENDOR_FOOD => 'Lebensmittelhänler', + NPC_FLAG_VENDOR_POISON => 'Gifthändler', + NPC_FLAG_VENDOR_REAGENT => 'Reagenzienhändler', + NPC_FLAG_REPAIRER => 'Reparatur', + NPC_FLAG_FLIGHT_MASTER => 'Flugmeister', + NPC_FLAG_SPIRIT_HEALER => 'Geistheiler', + NPC_FLAG_SPIRIT_GUIDE => 'Geistführer', + NPC_FLAG_INNKEEPER => 'Gastwirt', + NPC_FLAG_BANKER => 'Bankier', + NPC_FLAG_PETITIONER => 'Petition', + NPC_FLAG_GUILD_MASTER => 'Gildenmeister', + NPC_FLAG_BATTLEMASTER => 'Kampfmeister', + NPC_FLAG_AUCTIONEER => 'Auktionator', + NPC_FLAG_STABLE_MASTER => 'Stallmeister', + NPC_FLAG_GUILD_BANK => 'Gildenbank', + NPC_FLAG_SPELLCLICK => 'Zauber-Klick', + NPC_FLAG_MAILBOX => 'Briefkasten' + ), + 'extraFlags' => array( + CREATURE_FLAG_EXTRA_INSTANCE_BIND => 'Bindet Angreifer im Tod an die Instanz', + CREATURE_FLAG_EXTRA_CIVILIAN => "[tooltip name=civilian]- hat keine Aggro\n- Tod kostet Ehre[/tooltip][span class=tip tooltip=civilian]Zivilist[/span]", + CREATURE_FLAG_EXTRA_NO_PARRY => 'Kann nicht [spell=3127]', + CREATURE_FLAG_EXTRA_NO_PARRY_HASTEN => 'Erhält keine Eile nach [spell=3127]', + CREATURE_FLAG_EXTRA_NO_BLOCK => 'Kann nicht [spell=107]', + CREATURE_FLAG_EXTRA_NO_CRUSHING_BLOWS => 'Kann keine schmetternden Schläge verursachen', + CREATURE_FLAG_EXTRA_NO_XP => 'Belohnt keine Erfahrung', + CREATURE_FLAG_EXTRA_TRIGGER => 'Auslöser NPC', + CREATURE_FLAG_EXTRA_NO_TAUNT => 'Immun gegen Spott', + // CREATURE_FLAG_EXTRA_NO_MOVE_FLAGS_UPDATE => '', // ?? + CREATURE_FLAG_EXTRA_GHOST_VISIBILITY => '[tooltip name=spirit]Nur für tote Spieler sichtbar[/tooltip][span class=tip tooltip=spirit]Geist[/span]', + CREATURE_FLAG_EXTRA_USE_OFFHAND_ATTACK => 'Benutzt [spell=674]', + CREATURE_FLAG_EXTRA_NO_SELL_VENDOR => 'Händler kauft nicht vom Spieler', + CREATURE_FLAG_EXTRA_IGNORE_COMBAT => 'Kann nicht in einen Kampf verwickelt werden', + CREATURE_FLAG_EXTRA_WORLDEVENT => 'Gehört zu Weltereignis', + CREATURE_FLAG_EXTRA_GUARD => "[tooltip name=guard]- greift PvP-Angreifer an\n- ignoriert Unsichtbarkeit, Verstohlenheit und [spell=5384][/tooltip][span class=tip tooltip=guard]Wache[/span]", + CREATURE_FLAG_EXTRA_IGNORE_FEIGN_DEATH => 'Ignoriert [spell=5384]', + CREATURE_FLAG_EXTRA_NO_CRIT => 'Kann keine kritischen Treffer verursachen', + CREATURE_FLAG_EXTRA_NO_SKILL_GAINS => 'Angreifer erhält keine Waffenfertigkeit', + CREATURE_FLAG_EXTRA_OBEYS_TAUNT_DIMINISHING_RETURNS => 'Spott hat abnehmende Wirkung', + CREATURE_FLAG_EXTRA_ALL_DIMINISH => 'Alle Mechaniken haben abnehmende Wirkung', + CREATURE_FLAG_EXTRA_NO_PLAYER_DAMAGE_REQ => 'Angreifender Spieler ist immer lootberechtigt', + // CREATURE_FLAG_EXTRA_DUNGEON_BOSS => '', // set during runtime + CREATURE_FLAG_EXTRA_IGNORE_PATHFINDING => 'Ignoriert Wegfindung', + CREATURE_FLAG_EXTRA_IMMUNITY_KNOCKBACK => 'Immung gegen Rückstoß' ) ), 'event' => array( + 'id' => "Weltereignis-ID: ", 'notFound' => "Dieses Weltereignis existiert nicht.", - 'start' => "Anfang", - 'end' => "Ende", - 'interval' => "Intervall", + 'start' => "Anfang: ", + 'end' => "Ende: ", + 'interval' => "Intervall: ", 'inProgress' => "Ereignis findet gerade statt", 'category' => ["Nicht kategorisiert", "Feiertage", "Wiederkehrend", "Spieler vs. Spieler"] ), 'achievement' => array( + 'id' => "Erfolgs-ID: ", 'notFound' => "Dieser Erfolg existiert nicht.", 'criteria' => "Kriterien", 'points' => "Punkte", 'series' => "Reihe", - 'outOf' => "von", 'criteriaType' => "Criterium Typ-Id:", 'itemReward' => "Ihr bekommt", 'titleReward' => 'Euch wird der Titel "%s" verliehen', 'slain' => "getötet", - 'reqNumCrt' => "Benötigt", + 'reqNumCrt' => 'Benötigt %1$d von %2$d', 'rfAvailable' => "Verfügbar auf Realm: ", - '_transfer' => 'Dieser Erfolg wird mit %s vertauscht, wenn Ihr zur %s wechselt.', + '_transfer' => 'Dieser Erfolg wird mit %s vertauscht, wenn Ihr zur %s wechselt.', + 'cat' => array( + 1 => "Statistiken", 21 => "Spieler gegen Spieler", + 81 => "Heldentaten", 92 => "Allgemein", + 95 => "Spieler gegen Spieler", 96 => "Quests", + 97 => "Erkundung", 122 => "Tode", + 123 => "Arenen", 124 => "Schlachtfelder", + 125 => "Dungeons", 126 => "Welt", + 127 => "Wiederbelebung", 128 => "Siege", + 130 => "Charakter", 131 => "Geselligkeit", + 132 => "Fertigkeiten", 133 => "Quests", + 134 => "Reise", 135 => "Kreaturen", + 136 => "Ehrenhafte Siege", 137 => "Todesstöße", + 140 => "Vermögen", 141 => "Kampf", + 145 => "Verbrauchsgüter", 147 => "Ruf", + 152 => "Gewertete Arenen", 153 => "Schlachtfelder", + 154 => "Welt", 155 => "Weltereignisse", + 156 => "Winterhauch", 158 => "Schlotternächte", + 159 => "Nobelgarten", 160 => "Mondfest", + 161 => "Sonnenwende", 162 => "Braufest", + 163 => "Kinderwoche", 165 => "Arena", + 168 => "Dungeon & Schlachtzug", 169 => "Berufe", + 170 => "Kochkunst", 171 => "Angeln", + 172 => "Erste Hilfe", 173 => "Berufe", + 178 => "Sekundäre Fertigkeiten", 187 => "Liebe liegt in der Luft", + 191 => "Ausrüstung", 201 => "Ruf", + 14777 => "Östliche Königreiche", 14778 => "Kalimdor", + 14779 => "Scherbenwelt", 14780 => "Nordend", + 14801 => "Alteractal", 14802 => "Arathibecken", + 14803 => "Auge des Sturms", 14804 => "Kriegshymnenschlucht", + 14805 => "The Burning Crusade", 14806 => "Wrath of the Lich King - Dungeons", + 14807 => "Dungeon & Schlachtzug", 14808 => "Classic", + 14821 => "Classic", 14822 => "The Burning Crusade", + 14823 => "Wrath of the Lich King", 14861 => "Classic", + 14862 => "The Burning Crusade", 14863 => "Wrath of the Lich King", + 14864 => "Classic", 14865 => "The Burning Crusade", + 14866 => "Wrath of the Lich King", 14881 => "Strand der Uralten", + 14901 => "Tausendwinter", 14921 => "Wrath of the Lich King - Heroische Dungeons", + 14922 => "Wrath of the Lich King - Schlachtzüge für 10 Spieler", 14923 => "Wrath of the Lich King - Schlachtzüge für 25 Spieler", + 14941 => "Argentumturnier", 14961 => "Geheimnisse von Ulduar - Schlachtzug für 10 Spieler", + 14962 => "Geheimnisse von Ulduar - Schlachtzug für 25 Spieler", 14963 => "Geheimnisse v. Ulduar", + 14981 => "Die Pilgerfreuden", 15001 => "Der Ruf des Kreuzzugs - Schlachtzug für 10 Spieler", + 15002 => "Der Ruf des Kreuzzugs - Schlachtzug für 25 Spieler", 15003 => "Insel der Eroberung", + 15021 => "Der Ruf des Kreuzzugs", 15041 => "Der Untergang des Lichkönigs - Schlachtzug für 10 Spieler", + 15042 => "Der Untergang des Lichkönigs - Schlachtzug für 25 Spieler", 15062 => "Der Untergang des Lichkönigs" + ) ), 'chrClass' => array( + 'id' => "Klassen-ID: ", 'notFound' => "Diese Klasse existiert nicht." ), 'race' => array( + 'id' => "Volks-ID: ", 'notFound' => "Dieses Volk existiert nicht.", - 'racialLeader' => "Volksanführer", + 'racialLeader' => "Volksanführer: ", 'startZone' => "Startgebiet", ), 'maps' => array( @@ -532,9 +1367,10 @@ $lang = array( 'Miscellaneous' => "Diverse", 'Azeroth' => "Azeroth", 'CosmicMap' => "Kosmische Karte", + 'floorN' => "%d. Stockwerk" ), 'privileges' => array( - 'main' => "Auf unserer Seite könnt Ihr Ruf erringen. Hauptsächlich erringt man Ruf dadurch, dass Eure Kommentare positiv bewertet werden.

Das heißt, Euer Ruf hängt in gewissem Maße davon ab, wie sehr Ihr der Community beiträgt.

Mit dem Sammeln von Ruf verdient Ihr Euch auch das Vertrauen der Gemeinschaft ein, und Ihr erhält Privilegien. Unten könnt Ihr eine vollständige Liste einsehen.", + 'main' => "Auf unserer Seite könnt Ihr Ruf erringen. Hauptsächlich erringt man Ruf dadurch, dass Eure Kommentare positiv bewertet werden.

Das heißt, Euer Ruf hängt in gewissem Maße davon ab, wie sehr Ihr der Community beiträgt.

Mit dem Sammeln von Ruf verdient Ihr Euch auch das Vertrauen der Gemeinschaft ein, und Ihr erhält Privilegien. Unten könnt Ihr eine vollständige Liste einsehen.", 'privilege' => "Privileg", 'privileges' => "Privilegien", 'requiredRep' => "Benötigter Ruf", @@ -548,21 +1384,27 @@ $lang = array( ) ), 'zone' => array( + 'id' => "Gebiets-ID: ", 'notFound' => "Dieses Gebiet existiert nicht.", - 'attunement' => ["Einstimmung", "Heroische Einstimmung"], - 'key' => ["Schlüssel", "Heroischer Schlüssel"], - 'location' => "Ort", - 'raidFaction' => "Schlachtzugsfraktion", - 'boss' => "Endboss", + 'attunement' => ["Einstimmung: ", "Heroische Einstimmung: "], + 'key' => ["Schlüssel: ", "Heroischer Schlüssel: "], + 'location' => "Ort: ", + 'faction' => "Fraktion: ", + 'factions' => "Fraktionen: ", + 'raidFaction' => "Schlachtzugsfraktion: ", + 'reputationHub' => "Reputation Hub: ", + 'boss' => "Endboss: ", 'reqLevels' => "Mindeststufe: [tooltip=instancereqlevel_tip]%d[/tooltip], [tooltip=lfgreqlevel_tip]%d[/tooltip]", 'zonePartOf' => "Diese Zone ist Teil von [zone=%d].", 'autoRez' => "Automatische Wiederbelebung", 'city' => "Stadt", - 'territory' => "Territorium", - 'instanceType' => "Instanzart", + 'territory' => "Territorium: ", + 'instanceType' => "Instanzart: ", 'hcAvailable' => "Heroischer Modus verfügbar (%d)", - 'numPlayers' => "Anzahl an Spielern", + 'numPlayers' => 'Anzahl an Spielern: %1$s', + 'numPlayersVs' => 'Anzahl an Spielern: %1$dv%1$d', 'noMap' => "Für dieses Gebiet steht keine Karte zur Verfügung.", + 'fishingSkill' => "25 – 100% Chance einen gelisteten Fisch zu fangen.", 'instanceTypes' => ["Zone", "Durchgang", "Dungeon", "Schlachtzug", "Battleground", "Dungeon", "Arena", "Schlachtzug", "Schlachtzug"], 'territories' => ["Allianz", "Horde", "Umkämpft", "Sicheres Gebiet", "PvP", "Welt-PvP"], 'cat' => array( @@ -571,26 +1413,27 @@ $lang = array( ) ), 'quest' => array( + 'id' => "Quest-ID: ", 'notFound' => "Diese Quest existiert nicht.", '_transfer' => 'Dieses Quest wird mit %s vertauscht, wenn Ihr zur %s wechselt.', 'questLevel' => "Stufe %s", 'requirements' => "Anforderungen", - 'reqMoney' => "Benötigtes Geld", + 'reqMoney' => "Benötigtes Geld: %s", 'money' => "Geld", 'additionalReq' => "Zusätzliche Anforderungen um das Quest zu erhalten", 'reqRepWith' => 'Eure Reputation mit %s %s %s sein', 'reqRepMin' => "muss mindestens", 'reqRepMax' => "darf höchstens", 'progress' => "Fortschritt", - 'provided' => "Bereitgestellt", + 'provided' => "(Bereitgestellt)", 'providedItem' => "Bereitgestellter Gegenstand", 'completion' => "Abschluss", 'description' => "Beschreibung", - 'playerSlain' => "Spieler getötet", - 'profession' => "Beruf", - 'timer' => "Zeitbegrenzung", - 'loremaster' => "Meister der Lehren", - 'suggestedPl' => "Empfohlene Spielerzahl", + 'playerSlain' => "Spieler getötet (%d)", + 'profession' => "Beruf: ", + 'timer' => "Zeitbegrenzung: ", + 'loremaster' => "Meister der Lehren: ", + 'suggestedPl' => "Empfohlene Spielerzahl: %d", 'keepsPvpFlag' => "Hält Euch im PvP", 'daily' => 'Täglich', 'weekly' => "Wöchentlich", @@ -611,101 +1454,104 @@ $lang = array( 'enabledByQ' => "Aktiviert durch", 'enabledByQDesc'=> "Ihr könnt diese Quest nur annehmen, wenn eins der nachfolgenden Quests aktiv ist", 'gainsDesc' => "Bei Abschluss dieser Quest erhaltet Ihr", - 'theTitle' => 'den Titel "%s"', - 'mailDelivery' => "Ihr werdet diesen Brief%s%s erhalten", - 'mailBy' => ' von %s', - 'mailIn' => " nach %s", 'unavailable' => "Diese Quest wurde als nicht genutzt markiert und kann weder erhalten noch vollendet werden.", 'experience' => "Erfahrung", 'expConvert' => "(oder %s, wenn auf Stufe %d vollendet)", 'expConvert2' => "%s, wenn auf Stufe %d vollendet", - 'chooseItems' => "Auf Euch wartet eine dieser Belohnungen", - 'receiveItems' => "Ihr bekommt", - 'receiveAlso' => "Ihr bekommt außerdem", - 'spellCast' => "Der folgende Zauber wird auf Euch gewirkt", - 'spellLearn' => "Ihr erlernt", + 'rewardChoices' => "Auf Euch wartet eine dieser Belohnungen:", // REWARD_CHOICES + 'rewardItems' => "Ihr bekommt:", // REWARD_ITEMS_ONLY + 'rewardAlso' => "Ihr bekommt außerdem:", // REWARD_ITEMS + 'rewardSpell' => "Ihr erlernt:", // REWARD_SPELL + 'rewardAura' => "Der folgende Zauber wird auf Euch gewirkt:", // REWARD_AURA + 'rewardTradeSkill'=>"Ihr erlernt die Herstellung von:", // REWARD_TRADESKILL_SPELL + 'rewardTitle' => 'Euch wird folgender Titel verliehen: "%s"', // REWARD_TITLE 'bonusTalents' => "%d |4Talentpunkt:Talentpunkte;", 'spellDisplayed'=> ' (%s wird angezeigt)', - 'attachment' => "Anlage", + 'questPoolDesc' => 'Nur %d |4Quest kann:Quests können; aus diesem Tab gleichzeitig aktiv sein', + 'autoaccept' => 'Automatisches Annehmen', 'questInfo' => array( 0 => "Normal", 1 => "Gruppe", 21 => "Leben", 41 => "PvP", 62 => "Schlachtzug", 81 => "Dungeon", 82 => "Weltereignis", 83 => "Legendär", 84 => "Eskorte", 85 => "Heroisch", 88 => "Schlachtzug (10)", 89 => "Schlachtzug (25)" ), 'cat' => array( 0 => array( "Östliche Königreiche", - 36 => "Alteracgebirge", 45 => "Arathihochland", 46 => "Brennende Steppe", 279 => "Dalarankrater", 25 => "Der Schwarzfels", - 2257 => "Die Tiefenbahn", 1 => "Dun Morogh", 10 => "Dämmerwald", 1537 => "Eisenschmiede", 41 => "Gebirgspass der Totenwinde", - 3433 => "Geisterlande", 47 => "Hinterland", 3430 => "Immersangwald", 4080 => "Insel von Quel'Danas", 38 => "Loch Modan", - 4298 => "Pestländer: Die Scharlachrote Enklave", 44 => "Rotkammgebirge", 33 => "Schlingendorntal", 51 => "Sengende Schlucht", 3487 => "Silbermond", - 130 => "Silberwald", 1519 => "Sturmwind", 11 => "Sumpfland", 8 => "Sümpfe des Elends", 85 => "Tirisfal", - 1497 => "Unterstadt", 4 => "Verwüstete Lande", 267 => "Vorgebirge des Hügellands", 12 => "Wald von Elwynn", 40 => "Westfall", - 28 => "Westliche Pestländer", 3 => "Ödland", 139 => "Östliche Pestländer" + 1 => "Dun Morogh", 3 => "Ödland", 4 => "Verwüstete Lande", 8 => "Sümpfe des Elends", 9 => "Nordhaintal", + 10 => "Dämmerwald", 11 => "Sumpfland", 12 => "Wald von Elwynn", 25 => "Der Schwarzfels", 28 => "Westliche Pestländer", + 33 => "Schlingendorntal", 36 => "Alteracgebirge", 38 => "Loch Modan", 40 => "Westfall", 41 => "Gebirgspass der Totenwinde", + 44 => "Rotkammgebirge", 45 => "Arathihochland", 46 => "Brennende Steppe", 47 => "Hinterland", 51 => "Sengende Schlucht", + 85 => "Tirisfal", 130 => "Silberwald", 132 => "Eisklammtal", 139 => "Östliche Pestländer", 154 => "Todesend", + 267 => "Vorgebirge des Hügellands", 1497 => "Unterstadt", 1519 => "Sturmwind", 1537 => "Eisenschmiede", 2257 => "Die Tiefenbahn", + 3430 => "Immersangwald", 3431 => "Insel der Sonnenwanderer", 3433 => "Geisterlande", 3487 => "Silbermond", 4080 => "Insel von Quel'Danas", + 4298 => "Die Scharlachrote Enklave" ), 1 => array( "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", 1216 => "Holzschlundfeste", 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" - ), - 8 => array( "Scherbenwelt", - 3483 => "Höllenfeuerhalbinsel", 3518 => "Nagrand", 3523 => "Nethersturm", 3520 => "Schattenmondtal", 3522 => "Schergrat", - 3703 => "Shattrath", 3679 => "Skettis", 3519 => "Wälder von Terokkar", 3521 => "Zangarmarschen" - ), - 10 => array( "Nordend", - 3537 => "Boreanische Tundra", 4395 => "Dalaran", 495 => "Der heulende Fjord", 4742 => "Hrothgar's Landeplatz", 67 => "Die Sturmgipfel", - 65 => "Drachenöde", 210 => "Eiskrone", 394 => "Grizzlyhügel", 4024 => "Kaltarra", 3711 => "Sholazarbecken", - 4197 => "Tausendwintersee", 66 => "Zul'Drak" + 14 => "Durotar", 15 => "Düstermarschen", 16 => "Azshara", 17 => "Brachland", 141 => "Teldrassil", + 148 => "Dunkelküste", 188 => "Laubschattental", 215 => "Mulgore", 220 => "Hochwolkenebene", 331 => "Eschental", + 357 => "Feralas", 361 => "Teufelswald", 363 => "Das Tal der Prüfungen", 400 => "Tausend Nadeln", 405 => "Desolace", + 406 => "Steinkrallengebirge", 440 => "Tanaris", 490 => "Krater von Un'Goro", 493 => "Mondlichtung", 618 => "Winterquell", + 1377 => "Silithus", 1637 => "Orgrimmar", 1638 => "Donnerfels", 1657 => "Darnassus", 1769 => "Holzschlundfeste", + 3524 => "Azurmythosinsel", 3525 => "Blutmythosinsel", 3526 => "Am'mental", 3557 => "Die Exodar", ), 2 => array( "Dungeons", - 4494 => "Ahn'kahet: Das Alte Königreich", 3790 => "Auchenaikrypta", 4277 => "Azjol-Nerub", 209 => "Burg Schattenfang", 206 => "Burg Utgarde", - 4100 => "Das Ausmerzen von Stratholme", 4228 => "Das Oculus", 796 => "Das Scharlachrote Kloster", 717 => "Das Verlies", 3713 => "Der Blutkessel", - 3905 => "Der Echsenkessel", 2437 => "Der Flammenschlund", 4120 => "Der Nexus", 3716 => "Der Tiefensumpf", 2366 => "Der schwarze Morast", - 3848 => "Die Arkatraz", 3847 => "Die Botanika", 3715 => "Die Dampfkammer", 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", 1581 => "Die Todesminen", - 4415 => "Die Violette Festung", 3714 => "Die zerschmetterten Hallen", 2557 => "Düsterbruch", 4196 => "Feste Drak'Tharon", 3845 => "Festung der Stürme", - 721 => "Gnomeregan", 4813 => "Grube von Saron", 4416 => "Gundrak", 4820 => "Hallen der Reflexion", 1941 => "Höhlen der Zeit", - 3562 => "Höllenfeuerbollwerk", 3535 => "Höllenfeuerzitadelle", 722 => "Hügel der Klingenhauer", 491 => "Kral der Klingenhauer", 3792 => "Managruft", - 2100 => "Maraudon", 4723 => "Prüfung des Champions", 3789 => "Schattenlabyrinth", 2057 => "Scholomance", 1583 => "Schwarzfelsspitze", - 1584 => "Schwarzfelstiefen", 3791 => "Sethekkhallen", 2017 => "Stratholme", 4131 => "Terrasse der Magister", 719 => "Tiefschwarze Grotte", - 1196 => "Turm Utgarde", 1337 => "Uldaman", 1477 => "Versunkener Tempel", 2367 => "Vorgebirge des Alten Hügellands", 1176 => "Zul'Farrak" + 206 => "Burg Utgarde", 209 => "Burg Schattenfang", 491 => "Kral der Klingenhauer", 717 => "Das Verlies", 718 => "Die Höhlen des Wehklagens", + 719 => "Tiefschwarze Grotte", 721 => "Gnomeregan", 722 => "Hügel der Klingenhauer", 796 => "Das Scharlachrote Kloster", 1176 => "Zul'Farrak", + 1196 => "Turm Utgarde", 1337 => "Uldaman", 1477 => "Versunkener Tempel", 1581 => "Die Todesminen", 1583 => "Schwarzfelsspitze", + 1584 => "Schwarzfelstiefen", 1941 => "Höhlen der Zeit", 2017 => "Stratholme", 2057 => "Scholomance", 2100 => "Maraudon", + 2366 => "Der schwarze Morast", 2367 => "Vorgebirge des Alten Hügellands",2437 => "Der Flammenschlund", 2557 => "Düsterbruch", 3535 => "Höllenfeuerzitadelle", + 3562 => "Höllenfeuerbollwerk", 3688 => "Auchindoun", 3713 => "Der Blutkessel", 3714 => "Die zerschmetterten Hallen", 3715 => "Die Dampfkammer", + 3716 => "Der Tiefensumpf", 3717 => "Die Sklavenunterkünfte", 3789 => "Schattenlabyrinth", 3790 => "Auchenaikrypta", 3791 => "Sethekkhallen", + 3792 => "Managruft", 3842 => "Festung der Stürme", 3847 => "Die Botanika", 3848 => "Die Arkatraz", 3849 => "Die Mechanar", + 3905 => "Der Echsenkessel", 4100 => "Das Ausmerzen von Stratholme", 4131 => "Terrasse der Magister", 4196 => "Feste Drak'Tharon", 4228 => "Das Oculus", + 4264 => "Die Hallen des Steins", 4265 => "Der Nexus", 4272 => "Die Hallen der Blitze", 4277 => "Azjol-Nerub", 4415 => "Die Violette Festung", + 4416 => "Gundrak", 4494 => "Ahn'kahet: Das Alte Königreich",4522 => "Eiskronenzitadelle", 4723 => "Prüfung des Champions", 4809 => "Die Seelenschmiede", + 4813 => "Grube von Saron", 4820 => "Hallen der Reflexion" ), 3 => array( "Schlachtzüge", - 4603 => "Archavon's Kammer", 3842 => "Das Auge", 4500 => "Das Auge der Ewigkeit", 4493 => "Das Obsidiansanktum", 3959 => "Der Schwarze Tempel", - 4812 => "Eiskronenzitadelle", 2717 => "Geschmolzener Kern", 3923 => "Gruul's Unterschlupf", 3607 => "Höhle des Schlangenschreins", 3606 => "Hyjalgipfel", - 3457 => "Karazhan", 3836 => "Magtheridons Kammer", 3456 => "Naxxramas", 2159 => "Onyxias Hort", 2677 => "Pechschwingenhort", - 4722 => "Prüfung des Kreuzfahrers", 3429 => "Ruinen von Ahn'Qiraj", 4075 => "Sonnenbrunnenplateau", 3428 => "Tempel von Ahn'Qiraj", 4273 => "Ulduar", - 3805 => "Zul'Aman", 1977 => "Zul'Gurub" + 1977 => "Zul'Gurub", 2159 => "Onyxias Hort", 2677 => "Pechschwingenhort", 2717 => "Geschmolzener Kern", 3428 => "Tempel von Ahn'Qiraj", + 3429 => "Ruinen von Ahn'Qiraj", 3456 => "Naxxramas", 3457 => "Karazhan", 3606 => "Hyjalgipfel", 3607 => "Höhle des Schlangenschreins", + 3805 => "Zul'Aman", 3836 => "Magtheridons Kammer", 3845 => "Festung der Stürme", 3923 => "Gruuls Unterschlupf", 3959 => "Der Schwarze Tempel", + 4075 => "Sonnenbrunnenplateau", 4273 => "Ulduar", 4493 => "Das Obsidiansanktum", 4500 => "Das Auge der Ewigkeit", 4603 => "Archavons Kammer", + 4722 => "Prüfung des Kreuzfahrers", 4812 => "Eiskronenzitadelle", 4987 => "Das Rubinsanktum" ), 4 => array( "Klassen", - -263 => "Druide", -61 => "Hexenmeister", -261 => "Jäger", -81 => "Krieger", -161 => "Magier", - -141 => "Paladin", -262 => "Priester", -82 => "Schamane", -162 => "Schurke", -372 => "Todesritter" + -61 => "Hexenmeister", -81 => "Krieger", -82 => "Schamane", -141 => "Paladin", -161 => "Magier", + -162 => "Schurke", -261 => "Jäger", -262 => "Priester", -263 => "Druide", -372 => "Todesritter" ), 5 => array( "Berufe", - -181 => "Alchemie", -101 => "Angeln", -324 => "Erste Hilfe", -201 => "Ingenieurskunst", -371 => "Inschriftenkunde", - -373 => "Juwelenschleifen", -304 => "Kochkunst", -24 => "Kräuterkunde", -182 => "Lederverarbeitung", -121 => "Schmiedekunst", - -264 => "Schneiderei" + -24 => "Kräuterkunde", -101 => "Angeln", -121 => "Schmiedekunst", -181 => "Alchemie", -182 => "Lederverarbeitung", + -201 => "Ingenieurskunst", -264 => "Schneiderei", -304 => "Kochkunst", -324 => "Erste Hilfe", -371 => "Inschriftenkunde", + -373 => "Juwelenschleifen" ), 6 => array( "Schlachtfelder", - 2597 => "Alteractal", 3358 => "Arathibecken", 3820 => "Auge des Sturms", 4710 => "Insel der Eroberung", 3277 => "Kriegshymnenschlucht", - -25 => "Schlachtfelder", 4384 => "Strand der Uralten" - ), - 9 => array( "Weltereignisse", - -370 => "Braufest", -1002 => "Kinderwoche", -364 => "Dunkelmond-Jahrmarkt", -41 => "Tag der Toten", -1003 => "Schlotternächte", - -1005 => "Erntedankfest", -376 => "Liebe liegt in der Luft", -366 => "Mondfest", -369 => "Sonnenwende", -1006 => "Neujahr", - -375 => "Die Pilgerfreuden", -374 => "Nobelgarten", -1001 => "Winterhauch" + 2597 => "Alteractal", 3277 => "Kriegshymnenschlucht", 3358 => "Arathibecken", 3820 => "Auge des Sturms", 4384 => "Strand der Uralten", + 4710 => "Insel der Eroberung", -25 => "Schlachtfelder" ), 7 => array( "Verschiedenes", - -365 => "Krieg von Ahn'Qiraj", -1010 => "Dungeonfinder", -1 => "Episch", -344 => "Legendär", -367 => "Ruf", - -368 => "Invasion der Geißel", -241 => "Turnier" + -1 => "Episch", -241 => "Turnier", -344 => "Legendär", -365 => "Krieg von Ahn'Qiraj", -367 => "Ruf", + -368 => "Invasion der Geißel", -1010 => "Dungeonfinder" + ), + 8 => array( "Scherbenwelt", + 3483 => "Höllenfeuerhalbinsel", 3518 => "Nagrand", 3519 => "Wälder von Terokkar", 3520 => "Schattenmondtal", 3521 => "Zangarmarschen", + 3522 => "Schergrat", 3523 => "Nethersturm", 3679 => "Skettis", 3703 => "Shattrath" + ), + 9 => array( "Weltereignisse", + -22 => "Saisonbedingt", -41 => "Tag der Toten", -364 => "Dunkelmond-Jahrmarkt", -366 => "Mondfest", -369 => "Sonnenwendfest", + -370 => "Braufest", -374 => "Nobelgarten", -375 => "Pilgerfreuden", -376 => "Liebe liegt in der Luft", -1001 => "Winterhauch", + -1002 => "Kinderwoche", -1003 => "Schlotternächte", -1005 => "Erntedankfest" + ), + 10 => array( "Nordend", + 65 => "Drachenöde", 66 => "Zul'Drak", 67 => "Die Sturmgipfel", 210 => "Eiskrone", 394 => "Grizzlyhügel", + 495 => "Der heulende Fjord", 3537 => "Boreanische Tundra", 3711 => "Sholazarbecken", 4024 => "Kaltarra", 4197 => "Tausendwintersee", + 4395 => "Dalaran", 4742 => "Hrothgars Landestelle" ), -2 => "Nicht kategorisiert" - ) + ) ), 'icon' => array( 'notFound' => "Dieses Icon existiert nicht." ), 'title' => array( + 'id' => "Titel-ID: ", 'notFound' => "Dieser Titel existiert nicht.", '_transfer' => 'Dieser Titel wird mit %s vertauscht, wenn Ihr zur %s wechselt.', 'cat' => array( @@ -713,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", @@ -720,8 +1567,9 @@ $lang = array( ) ), 'currency' => array( + 'id' => "Währungs-ID: ", 'notFound' => "Diese Währung existiert nicht.", - 'cap' => "Obergrenze", + 'cap' => "Obergrenze: ", 'cat' => array( 1 => "Verschiedenes", 2 => "Spieler gegen Spieler", 4 => "Classic", 21 => "Wrath of the Lich King", 22 => "Dungeon und Schlachtzug", 23 => "Burning Crusade", 41 => "Test", 3 => "Unbenutzt" ) @@ -740,18 +1588,31 @@ $lang = array( "Narration Music", "Narration", 50 => "Zone Ambience", 52 => "Emitters", 53 => "Vehicles", 1000 => "Meine Playlist" ) ), + 'mail' => array( + 'id' => "Brief-ID: ", + 'notFound' => "Dieser Brief existiert nicht.", + 'attachment' => "Anlage", + 'mailDelivery' => 'Ihr werdet diesen Brief%s%s erhalten', + 'mailBy' => ' von %s', + 'mailIn' => " nach %s", + 'delay' => "Verzögerung: %s", + 'sender' => "Absender: %s", + '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.", 'maxStanding' => "Max. Ruf", - 'quartermaster' => "Rüstmeister", + 'quartermaster' => "Rüstmeister: ", 'customRewRate' => "Abweichende Belohnungsraten", '_transfer' => 'Die Reputation mit dieser Fraktion wird mit dem für %s vertauscht, wenn Ihr zur %s wechselt.', 'cat' => array( @@ -762,14 +1623,15 @@ $lang = array( ) ), 'itemset' => array( + 'id' => "Ausrüstungsset-ID: ", 'notFound' => "Dieses Ausrüstungsset existiert nicht.", '_desc' => "%s ist das %s. Es enthält %s Teile.", '_descTagless' => "%s 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", + '_tag' => "Tag: ", 'summary' => "Zusammenfassung", 'notes' => array( null, "Dungeon-Set 1", "Dungeon-Set 2", "Tier 1 Raid-Set", @@ -787,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", @@ -802,22 +1665,26 @@ $lang = array( '_globCD' => "Globale Abklingzeit", '_gcdCategory' => "GCD-Kategorie", '_value' => "Wert", - '_radius' => "Radius", - '_interval' => "Interval", - '_inSlot' => "im Platz", + '_radius' => "Radius: ", + '_interval' => "Interval: ", + '_inSlot' => "im Platz: ", '_collapseAll' => "Alle einklappen", '_expandAll' => "Alle ausklappen", - '_transfer' => 'Dieser Zauber wird mit %s vertauscht, wenn Ihr zur %s wechselt.', + '_transfer' => 'Dieser Zauber wird mit %s vertauscht, wenn Ihr zur %s wechselt.', + '_affected' => "Betroffene Zauber: ", + '_seeMore' => "Mehr anzeigen", + '_rankRange' => "Rang: %d - %d", + '_showXmore' => "Zeige %d weitere", + + '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", - 'remaining' => "Noch %s", - 'untilCanceled' => "bis Abbruch", - 'castIn' => "Wirken in %s Sek.", - 'instantPhys' => "Sofort", - 'instantMagic' => "Spontanzauber", + 'trainingCost' => "Trainingskosten: ", 'channeled' => "Kanalisiert", 'range' => "%s Meter Reichweite", 'meleeRange' => "Nahkampfreichweite", @@ -828,15 +1695,50 @@ $lang = array( 'pctCostOf' => "vom Grund%s", 'costPerSec' => ", plus %s pro Sekunde", 'costPerLevel' => ", plus %s pro Stufe", + 'pointsPerCP' => ", plus %s pro Combopunkt", 'stackGroup' => "Stack Gruppierung", 'linkedWith' => "Verknüpft mit", - '_scaling' => "Skalierung", - '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" + 'apMod' => " (AP mod: %.3g)", + 'spMod' => " (ZM mod: %.3g)", + 'instantPhys' => "Sofort", + 'castTime' => array( + "Spontanzauber", + "Wirken in %.3g Sek.", + "Wirken in %.3g Min." ), - 'powerRunes' => ["Frost", "Unheilig", "Blut", "Tod"], - 'powerTypes' => array( + 'cooldown' => array( + "Keine Abklingzeit", + "%.3g Sek. Abklingzeit", + "%.3g Min. Abklingzeit", + "%.3g |4Stunde:Stunden; Abklingzeit", + "%.3g |4Tag:Tage; Abklingzeit" + ), + 'duration' => array( + "bis Abbruch", + "%.2G Sek.", + "%.2G Min.", + "%.2G |4Stunde:Stunden;", + "%.2G |4Tag:Tage;" + ), + 'timeRemaining' => array( + "", + "Noch %d |4Sekunde:Sekunden;", + "Noch %d |4Minute:Minuten;", + "Noch %d |4Stunde:Stunden;", + "Noch %d |4Tag:Tage;" + ), + 'powerCost' => array( + -2 => ["%d Gesundheit", '%1$d Gesundheit und %2$d pro Sek.'], + 0 => ["%d Mana", '%1$d Mana und %2$d pro Sek.' ], + 1 => ["%d Wut", '%1$d Wut und %2$d pro Sek.' ], + 2 => ["%d Fokus", "%d Fokus und %d pro Sek." ], + 3 => ["%d Energie", "%d Energie und %d pro Sek." ], + 6 => ["%d Runenmacht", "%d Runenmacht, plus %d pro Sek." ], + ), + 'powerDisplayCost' => ["%d %s", "%d %s, plus %d pro Sek"], + 'powerCostRunes'=> ["%d Blut", "%d Unheilig", "%d Frost"], + 'powerRunes' => ["Blut", "Unheilig", "Frost", "Tod"], + 'powerTypes' => array( // POWER_TYPE_* // conventional -2 => "Gesundheit", 0 => "Mana", 1 => "Wut", 2 => "Fokus", 3 => "Energie", 4 => "Zufriedenheit", 5 => "Runen", 6 => "Runenmacht", @@ -857,7 +1759,7 @@ $lang = array( -4 => "Völkerfertigkeiten", -2 => "Talente", -6 => "Haustiere", - -5 => "Reittiere", + -5 => ["Reittiere", 1 => "Reittiere", 2 => "Flugreittiere", 3 => "Verschiedene"], -3 => array( "Begleiterfertigkeiten", 782 => "Ghul", 270 => "Allgemein", 213 => "Aasvogel", 210 => "Bär", 763 => "Drachenfalke", 211 => "Eber", 767 => "Felshetzer", 653 => "Fledermaus", 788 => "Geisterbestie", 215 => "Gorilla", 654 => "Hyäne", 209 => "Katze", 787 => "Kernhund", @@ -907,9 +1809,9 @@ $lang = array( ), 'spellModOp' => array( "Schaden", "Dauer", "Bedrohung", "Effekt 1", "Aufladungen", - "Reichweite", "Radius", "kritische Trefferchance", "Alle Effekte", "Zauberzeitverlust", + "Reichweite", "Radius", "Kritische Trefferchance", "Alle Effekte", "Zauberzeitverlust", "Zauberzeit", "Abklingzeit", "Effekt 2", "Ignoriere Rüstung", "Kosten", - "Kritischer Bonusschaden", "Chance auf Fehlschlag", "Sprung-Ziele", "Chance auf Auslösung", "Intervall", + "Kritischer Bonusschaden", "Trefferchance", "Sprung-Ziele", "Chance auf Auslösung", "Intervall", "Multiplikator (Schaden)", "Globale Abklingzeit", "Schaden über Zeit", "Effekt 3", "Multiplikator (Bonus)", null, "Auslösungen pro Minute", "Multiplikator (Betrag)", "Widerstand gegen Bannung", "kritischer Bonusschaden2", "Kostenrückerstattung bei Fehlschlag" @@ -921,6 +1823,9 @@ $lang = array( "erhaltene kritische Fernkampftreffer", "erhaltene kritische Zaubertreffer", "Nahkampftempo", "Fernkampftempo", "Zaubertempo", "Waffenfertigkeit Haupthand", "Waffenfertigkeit Nebenhand", "Waffenfertigkeit Fernkampf", "Waffenkunde", "Rüstungsdurchschlag" ), + 'combatRatingMask' => array( + 0xE0 => "Trefferchance", 0x700 => "Kritische Trefferchance", 0x1C000 => "Abhärtung" + ), 'lockType' => array( null, "Schlossknacken", "Kräuterkunde", "Bergbau", "Falle entschärfen", "Öffnen", "Schatz (DND)", "Verkalkte Elfenedelsteine (DND)", "Schließen", "Falle scharf machen", @@ -929,113 +1834,388 @@ $lang = array( "Inschriftenkunde", "Vom Fahrzeug öffnen" ), 'stealthType' => ["Allgemein", "Falle"], - 'invisibilityType' => ["Allgemein", 3 => "Falle", 6 => "Trunkenheit"], - 'unkEffect' => 'Unknown Effect', + 'invisibilityType' => ["Allgemein", "UNK-1", "UNK-2", "Falle", "UNK-4", "UNK-5", "Trunkenheit", "UNK-7", "UNK-8", "UNK-9", "UNK-10", "UNK-11"], + 'summonControl' => ["Ungesteuert", "Wächter", "Begleiter", "Bezaubert", "Gesteuertes Fahrzeug", "Ungesteuertes Fahrzeug"], + 'summonSlot' => ["Begleiter", "Feuertotem", "Erdtotem", "Wassertotem", "Lufttotem", "Haustier", "Quest"], + 'unkEffect' => 'Unknown Effect (%1$d)', 'effects' => array( /*0-5 */ 'None', 'Instakill', 'School Damage', 'Dummy', 'Portal Teleport', 'Teleport Units', -/*6+ */ 'Apply Aura', 'Environmental Damage', 'Power Drain', 'Health Leech', 'Heal', 'Bind', -/*12+ */ 'Portal', 'Ritual Base', 'Ritual Specialize', 'Ritual Activate Portal', 'Quest Complete', 'Weapon Damage NoSchool', -/*18+ */ 'Resurrect', 'Add Extra Attacks', 'Dodge', 'Evade', 'Parry', 'Block', -/*24+ */ 'Create Item', 'Can Use Weapon', 'Defense', 'Persistent Area Aura', 'Summon', 'Leap', -/*30+ */ 'Energize', 'Weapon Damage Percent', 'Trigger Missile', 'Open Lock', 'Summon Change Item', 'Apply Area Aura Party', -/*36+ */ 'Learn Spell', 'Spell Defense', 'Dispel', 'Language', 'Dual Wield', 'Jump', -/*42+ */ 'Jump Dest', 'Teleport Units Face Caster','Skill Step', 'Add Honor', 'Spawn', 'Trade Skill', -/*48+ */ 'Stealth', 'Detect', 'Trans Door', 'Force Critical Hit', 'Guarantee Hit', 'Enchant Item Permanent', -/*54+ */ 'Enchant Item Temporary', 'Tame Creature', 'Summon Pet', 'Learn Pet Spell', 'Weapon Damage Flat', 'Create Random Item', -/*60+ */ 'Proficiency', 'Send Event', 'Power Burn', 'Threat', 'Trigger Spell', 'Apply Area Aura Raid', -/*66+ */ 'Create Mana Gem', 'Heal Max Health', 'Interrupt Cast', 'Distract', 'Pull', 'Pickpocket', -/*72+ */ 'Add Farsight', 'Untrain Talents', 'Apply Glyph', 'Heal Mechanical', 'Summon Object Wild', 'Script Effect', -/*78+ */ 'Attack', 'Sanctuary', 'Add Combo Points', 'Create House', 'Bind Sight', 'Duel', -/*84+ */ 'Stuck', 'Summon Player', 'Activate Object', 'WMO Damage', 'WMO Repair', 'WMO Change', +/*6+ */ 'Apply Aura', 'Environmental Damage', 'Drain Power', 'Drain Health', 'Heal', 'Bind', +/*12+ */ 'Portal', 'Ritual Base', 'Ritual Specialize', 'Ritual Activate Portal', 'Complete Quest', 'Weapon Damage - No School', +/*18+ */ 'Resurrect with % Health', 'Add Extra Attacks', 'Can Dodge', 'Can Evade', 'Can Parry', 'Can Block', +/*24+ */ 'Create Item', 'Can Use Weapon', 'Know Defense Skill', 'Persistent Area Aura', 'Summon', 'Leap', +/*30+ */ 'Give Power', 'Weapon Damage - %', 'Trigger Missile', 'Open Lock', 'Transform Item', 'Apply Area Aura - Party', +/*36+ */ 'Learn Spell', 'Know Spell Defense', 'Dispel', 'Learn Language', 'Dual Wield', 'Jump to Target', +/*42+ */ 'Jump Behind Target', 'Teleport Target to Caster','Learn Skill Step', 'Give Honor', 'Spawn', 'Trade Skill', +/*48+ */ 'Stealth', 'Detect Stealthed', 'Summon Object', 'Force Critical Hit', 'Guarantee Hit', 'Enchant Item Permanent', +/*54+ */ 'Enchant Item Temporary', 'Tame Creature', 'Summon Pet', 'Learn Spell - Pet', 'Weapon Damage - Flat', 'Open Item & Fast Loot', +/*60+ */ 'Proficiency', 'Send Script Event', 'Burn Power', 'Modify Threat - Flat', 'Trigger Spell', 'Apply Area Aura - Raid', +/*66+ */ 'Create Mana Gem', 'Heal to Full', 'Interrupt Cast', 'Distract', 'Distract Move', 'Pickpocket', +/*72+ */ 'Far Sight', 'Forget Talents', 'Apply Glyph', 'Heal Mechanical', 'Summon Object - Temporary','Script Effect', +/*78+ */ 'Attack', 'Abort All Pending Attacks','Add Combo Points', 'Create House', 'Bind Sight', 'Duel', +/*84+ */ 'Stuck', 'Summon Player', 'Activate Object', 'Siege Damage', 'Repair Building', 'Siege Building Action', /*90+ */ 'Kill Credit', 'Threat All', 'Enchant Held Item', 'Force Deselect', 'Self Resurrect', 'Skinning', /*96+ */ 'Charge', 'Cast Button', 'Knock Back', 'Disenchant', 'Inebriate', 'Feed Pet', -/*102+ */ 'Dismiss Pet', 'Reputation', 'Summon Object Slot1', 'Summon Object Slot2', 'Summon Object Slot3', 'Summon Object Slot4', -/*108+ */ 'Dispel Mechanic', 'Summon Dead Pet', 'Destroy All Totems', 'Durability Damage', 'Summon Demon', 'Resurrect Flat', -/*114+ */ 'Attack Me', 'Durability Damage Percent','Skin Player Corpse', 'Spirit Heal', 'Skill', 'Apply Area Aura Pet', -/*120+ */ 'Teleport Graveyard', 'Weapon Damage Normalized', null, 'Send Taxi', 'Pull Towards', 'Modify Threat Percent', -/*126+ */ 'Steal Beneficial Buff', 'Prospecting', 'Apply Area Aura Friend', 'Apply Area Aura Enemy', 'Redirect Threat', 'Play Sound', -/*132+ */ 'Play Music', 'Unlearn Specialization', 'Kill Credit2', 'Call Pet', 'Heal Percent', 'Energize Percent', -/*138+ */ 'Leap Back', 'Clear Quest', 'Force Cast', 'Force Cast With Value', 'Trigger Spell With Value', 'Apply Area Aura Owner', -/*144+ */ 'Knock Back Dest', 'Pull Towards Dest', 'Activate Rune', 'Quest Fail', null, 'Charge Dest', -/*150+ */ 'Quest Start', 'Trigger Spell 2', null, 'Create Tamed Pet', 'Discover Taxi', 'Dual Wield 2H Weapons', -/*156+ */ 'Enchant Item Prismatic', 'Create Item 2', 'Milling', 'Allow Rename Pet', null, 'Talent Spec Count', -/*162-164*/ 'Talent Spec Select', null, 'Remove Aura' +/*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', '', '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', '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.', '', 'Remove Aura' ), - 'unkAura' => 'Unknown Aura', + 'unkAura' => 'Unknown Aura (%1$d)', 'auras' => array( -/*0- */ 'None', 'Bind Sight', 'Mod Possess', 'Periodic Damage', 'Dummy', -/*5+ */ 'Mod Confuse', 'Mod Charm', 'Mod Fear', 'Periodic Heal', 'Mod Attack Speed', - 'Mod Threat', 'Taunt', 'Stun', 'Mod Damage Done Flat', 'Mod Damage Taken Flat', - 'Damage Shield', 'Mod Stealth', 'Mod Stealth Detection', 'Mod Invisibility', 'Mod Invisibility Detection', - 'Mod Health Percent', 'Mod Power Percent', 'Mod Resistance Flat', 'Periodic Trigger Spell', 'Periodic Energize', -/*25+ */ 'Pacify', 'Root', 'Silence', 'Reflect Spells', 'Mod Stat Flat', - 'Mod Skill', 'Mod Increase Speed', 'Mod Increase Mounted Speed', 'Mod Decrease Speed', 'Mod Increase Health', - 'Mod Increase Power', 'Shapeshift', 'Spell Effect Immunity', 'Spell Aura Immunity', 'School Immunity', - 'Damage Immunity', 'Dispel Immunity', 'Proc Trigger Spell', 'Proc Trigger Damage', 'Track Creatures', - 'Track Resources', 'Mod Parry Skill', 'Mod Parry Percent', null, 'Mod Dodge Percent', -/*50+ */ 'Mod Critical Healing Amount', 'Mod Block Percent', 'Mod Physical Crit Percent', 'Periodic Health Leech', 'Mod Hit Chance', - 'Mod Spell Hit Chance', 'Transform', 'Mod Spell Crit Chance', 'Mod Increase Swim Speed', 'Mod Damage Done Versus Creature', - 'Pacify Silence', 'Mod Scale', 'Periodic Health Funnel', 'Periodic Mana Funnel', 'Periodic Mana Leech', - 'Mod Casting Speed (not stacking)', 'Feign Death', 'Disarm', 'Stalked', 'School Absorb', - 'Extra Attacks', 'Mod Spell Crit Chance School', 'Mod Power Cost School Percent', 'Mod Power Cost School Flat', 'Reflect Spells School', -/*75+ */ 'Language', 'Far Sight', 'Mechanic Immunity', 'Mounted', 'Mod Damage Done Percent', - 'Mod Stat Percent', 'Split Damage Percent', 'Water Breathing', 'Mod Base Resistance Flat', 'Mod Health Regeneration', - 'Mod Power Regeneration', 'Channel Death Item', 'Mod Damage Taken Percent', 'Mod Health Regeneration Percent', 'Periodic Damage Percent', - 'Mod Resist Chance', 'Mod Detect Range', 'Prevent Fleeing', 'Unattackable', 'Interrupt Regeneration', - 'Ghost', 'Spell Magnet', 'Mana Shield', 'Mod Skill Value', 'Mod Attack Power', -/*100+ */ 'Auras Visible', 'Mod Resistance Percent', 'Mod Melee Attack Power Versus', 'Mod Total Threat', 'Water Walk', - 'Feather Fall', 'Hover', 'Add Flat Modifier', 'Add Percent Modifier', 'Add Target Trigger', - 'Mod Power Regeneration Percent', 'Add Caster Hit Trigger', 'Override Class Scripts', 'Mod Ranged Damage Taken Flat', 'Mod Ranged Damage Taken Percent', - 'Mod Healing', 'Mod Regeneration During Combat', 'Mod Mechanic Resistance', 'Mod Healing Taken Percent', 'Share Pet Tracking', - 'Untrackable', 'Empathy', 'Mod Offhand Damage Percent', 'Mod Target Resistance', 'Mod Ranged Attack Power', -/*125+ */ 'Mod Melee Damage Taken Flat', 'Mod Melee Damage Taken Percent', 'Ranged Attack Power Attacker Bonus', 'Possess Pet', 'Mod Speed Always', - 'Mod Mounted Speed Always', 'Mod Ranged Attack Power Versus', 'Mod Increase Energy Percent', 'Mod Increase Health Percent', 'Mod Mana Regeneration Interrupt', - 'Mod Healing Done Flat', 'Mod Healing Done Percent', 'Mod Total Stat Percentage', 'Mod Melee Haste', 'Force Reaction', - 'Mod Ranged Haste', 'Mod Ranged Ammo Haste', 'Mod Base Resistance Percent', 'Mod Resistance Exclusive', 'Safe Fall', - 'Mod Pet Talent Points', 'Allow Tame Pet Type', 'Mechanic Immunity Mask', 'Retain Combo Points', 'Reduce Pushback', -/*150+ */ 'Mod Shield Blockvalue Percent', 'Track Stealthed', 'Mod Detected Range', 'Split Damage Flat', 'Mod Stealth Level', - 'Mod Water Breathing', 'Mod Reputation Gain', 'Pet Damage Multi', 'Mod Shield Blockvalue', 'No PvP Credit', - 'Mod AoE Avoidance', 'Mod Health Regeneration In Combat', 'Power Burn Mana', 'Mod Crit Damage Bonus', null, - 'Melee Attack Power Attacker Bonus', 'Mod Attack Power Percent', 'Mod Ranged Attack Power Percent', 'Mod Damage Done Versus', 'Mod Crit Percent Versus', - 'Change Model', 'Mod Speed (not stacking)', 'Mod Mounted Speed (not stacking)', null, 'Mod Spell Damage Of Stat Percent', -/*175+ */ 'Mod Spell Healing Of Stat Percent', 'Spirit Of Redemption', 'AoE Charm', 'Mod Debuff Resistance', 'Mod Attacker Spell Crit Chance', - 'Mod Spell Damage Versus', null, 'Mod Resistance Of Stat Percent', 'Mod Critical Threat', 'Mod Attacker Melee Hit Chance', +/*0- */ 'None', 'Bind Sight', 'Possess', 'Periodic Damage - Flat', 'Dummy', +/*5+ */ 'Confuse', 'Charm', 'Fear', 'Periodic Heal', 'Mod Attack Speed', + 'Mod Threat', 'Taunt', 'Stun', 'Mod Damage Done - Flat', 'Mod Damage Taken - Flat', + 'Damage Shield', 'Stealth', 'Mod Stealth Detection Level', 'Invisibility', 'Mod Invisibility Detection Level', + 'Regenerate Health - %', 'Regenerate Power - %', 'Mod Resistance - Flat', 'Periodically Trigger Spell', 'Periodically Give Power', +/*25+ */ 'Pacify', 'Root', 'Silence', 'Reflect Spells', 'Mod Stat - Flat', + '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 %', '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', + 'Mod Spell Haste % (not stacking)', 'Feign Death', 'Disarm', 'Stalked', 'Mod Absorb School Damage', + 'Extra Attacks', 'Mod Spell School Crit Chance', 'Mod Spell School Power Cost - %', 'Mod Spell School Power Cost - Flat', 'Reflect Spells School From School', +/*75+ */ 'Force Language', 'Far Sight', 'Mechanic Immunity', 'Mounted', 'Mod Damage Done - %', + 'Mod Stat - %', 'Split Damage - %', 'Underwater Breathing', 'Mod Base Resistance - Flat', 'Mod Health Regeneration - Flat', + 'Mod Power Regeneration - Flat', 'Create Item on Death', 'Mod Damage Taken - %', 'Mod Health Regeneration - %', 'Periodic Damage - %', + 'Mod Resist Chance', 'Mod Aggro Range', 'Prevent Fleeing', 'Unattackable', 'Interrupt Power Decay', + 'Ghost', 'Spell Magnet', 'Absorb Damage - Mana Shield', 'Mod Skill Value', 'Mod Attack Power - Flat', +/*100+ */ 'Always Show Debuffs', 'Mod Resistance - %', 'Mod Melee Attack Power vs Creature', 'Mod Total Threat - Temporary', 'Water Walking', + 'Feather Fall', 'Levitate / Hover', 'Add Modifier - Flat', 'Add Modifier - %', 'Proc Spell on Target', + 'Mod Power Regeneration - %', 'Intercept % of Attacks Against Target','Override Class Script', 'Mod Ranged Damage Taken - Flat', 'Mod Ranged Damage Taken - %', + 'Mod Healing Taken - Flat', 'Allow % of Health Regen During Combat','Mod Mechanic Resistance', 'Mod Healing Taken - %', 'Share Pet Tracking', + 'Untrackable', 'Beast Lore', 'Mod Offhand Damage Done %', 'Mod Target Resistance - Flat', 'Mod Ranged Attack Power - Flat', +/*125+ */ 'Mod Melee Damage Taken - Flat', 'Mod Melee Damage Taken - %', 'Mod Attacker Ranged Attack Power', 'Possess Pet', 'Increase Run Speed % - Stacking', + 'Incerase Mounted Speed % - Stacking', 'Mod Ranged Attack Power vs Creature', 'Mod Maximum Power - %', 'Mod Maximum Health - %', 'Allow % of Mana Regen During Combat', + 'Mod Healing Done - Flat', 'Mod Healing Done - %', 'Mod Stat - %', 'Mod Melee Haste %', 'Force Reputation', + 'Mod Ranged Haste %', 'Mod Ranged Ammo Haste %', 'Mod Base Resistance - %', 'Mod Resistance - Flat (not stacking)', 'Safe Fall', + '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 %', '', + '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)', '', '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', '', '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 Faction Reputation Gain', 'Use Normal Movement Speed', 'Mod Melee Ranged Haste', 'Mod Haste', 'Mod Target Absorb School', - 'Mod Target Ability Absorb School', 'Mod Cooldown', 'Mod Attacker Spell And Weapon Crit Chance', null, 'Mod Increases Spell Percent to Hit', -/*200+ */ 'Mod XP Percent', 'Fly', 'Ignore Combat Result', 'Mod Attacker Melee Crit Damage', 'Mod Attacker Ranged Crit Damage', - 'Mod School Crit Damage Taken', 'Mod Increase Vehicle Flight Speed', 'Mod Increase Mounted Flight Speed', 'Mod Increase Flight Speed', 'Mod Mounted Flight Speed Always', - 'Mod Vehicle Speed Always', 'Mod Flight Speed (not stacking)', 'Mod Ranged Attack Power Of Stat Percent', 'Mod Rage from Damage Dealt', 'Tamed Pet Passive', - 'Arena Preparation', 'Haste Spells', 'Killing Spree', 'Haste Ranged', 'Mod Mana Regeneration from Stat', - 'Mod Rating from Stat', 'Ignore Threat', null, 'Raid Proc from Charge', null, -/*225+ */ 'Raid Proc from Charge With Value', 'Periodic Dummy', 'Periodic Trigger Spell With Value', 'Detect Stealth', 'Mod AoE Damage Avoidance', - 'Mod Increase Health', 'Proc Trigger Spell With Value', 'Mod Mechanic Duration', 'Mod Display Model', 'Mod Mechanic Duration (not stacking)', - 'Mod Dispel Resist', 'Control Vehicle', 'Mod Spell Damage Of Attack Power', 'Mod Spell Healing Of Attack Power', 'Mod Scale 2', - 'Mod Expertise', 'Force Move Forward', 'Mod Spell Damage from Healing', 'Mod Faction', 'Comprehend Language', - 'Mod Aura Duration By Dispel', 'Mod Aura Duration By Dispel (not stacking)', 'Clone Caster', 'Mod Combat Result Chance', 'Convert Rune', -/*250+ */ 'Mod Increase Health 2', 'Mod Enemy Dodge', 'Mod Speed Slow All', 'Mod Block Crit Chance', 'Mod Disarm Offhand', - 'Mod Mechanic Damage Taken Percent', 'No Reagent Use', 'Mod Target Resist By Spell Class', 'Mod Spell Visual', 'Mod HoT Percent', - 'Screen Effect', 'Phase', 'Ability Ignore Aurastate', 'Allow Only Ability', null, - null, null, 'Mod Immune Aura Apply School', 'Mod Attack Power Of Stat Percent', 'Mod Ignore Target Resist', - 'Mod Ability Ignore Target Resist', 'Mod Damage Taken Percent From Caster', 'Ignore Melee Reset', 'X Ray', 'Ability Consume No Ammo', -/*275+ */ 'Mod Ignore Shapeshift', 'Mod Mechanic Damage Done Percent', 'Mod Max Affected Targets', 'Mod Disarm Ranged', 'Initialize Images', - 'Mod Armor Penetration Percent', 'Mod Honor Gain Percent', 'Mod Base Health Percent', 'Mod Healing Received', 'Linked', - 'Mod Attack Power Of Armor', 'Ability Periodic Crit', 'Deflect Spells', 'Ignore Hit Direction', null, - 'Mod Crit Percent', 'Mod XP Quest Percent', 'Open Stable', 'Override Spells', 'Prevent Power Regeneration', - null, 'Set Vehicle Id', 'Block Spell Family', 'Strangulate', null, -/*300+ */ 'Share Damage Percent', 'School Heal Absorb', null, 'Mod Damage Done Versus Aurastate', 'Mod Fake Inebriate', - 'Mod Minimum Speed', null, 'Heal Absorb Test', 'Hunter Trap', null, - 'Mod Creature AoE Damage Avoidance', null, null, null, 'Prevent Ressurection', + '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', '', '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', '', '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)', + 'Mod Expertise', 'Force Move Forward', 'Mod Spell & Healing Power by % of Int','Faction Override', 'Comprehend Language', + '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', '', + '', '', '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', '', + 'Mod Crit Chance', 'Mod Quest Experience Gained %', 'Open Stable', 'Override Spells', 'Prevent Power Regeneration', + '', '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( + SPELL_ATTR0_PROC_FAILURE_BURNS_CHARGE => 'Proc-Fehlschlag verbraucht Aufladung', + SPELL_ATTR0_REQ_AMMO => 'Benötigt eine Fernkampfwaffe', + SPELL_ATTR0_ON_NEXT_SWING => 'Mit dem nächsten Schwung (Spieler)', + SPELL_ATTR0_IS_REPLENISHMENT => 'Verfehlen durch Immunität nicht loggen', + SPELL_ATTR0_ABILITY => 'Ist Fähigkeit', + SPELL_ATTR0_TRADESPELL => 'Handwerksrezept', + SPELL_ATTR0_PASSIVE => 'Passiver Zauber', + SPELL_ATTR0_HIDDEN_CLIENTSIDE => 'Aura ist versteckt', + SPELL_ATTR0_HIDE_IN_COMBAT_LOG => 'Erscheint nicht im Log', + SPELL_ATTR0_TARGET_MAINHAND_ITEM => 'Nur angelegte Gegenstände', + SPELL_ATTR0_ON_NEXT_SWING_2 => 'Mit dem nächsten Schwung (NSCs)', + SPELL_ATTR0_WEARER_CASTS_PROC_TRIGGER => 'Träger wirkt Proc-Auslöser', + SPELL_ATTR0_DAYTIME_ONLY => 'Kann nur tagsüber benutzt werden', + SPELL_ATTR0_NIGHT_ONLY => 'Kann nur nachts verwendet werden', + SPELL_ATTR0_INDOORS_ONLY => 'Kann nur drinnen verwendet werden', + SPELL_ATTR0_OUTDOORS_ONLY => 'Kann nur draußen verwendet werden', + SPELL_ATTR0_NOT_SHAPESHIFT => 'Kann nicht verwendet werden, während Ihr gestaltverwandelt seid', + SPELL_ATTR0_ONLY_STEALTHED => 'Muss in Verstohlenheit sein', + SPELL_ATTR0_DONT_AFFECT_SHEATH_STATE => 'Waffe nicht wegstecken', + SPELL_ATTR0_LEVEL_DAMAGE_CALCULATION => 'Zauberschaden ist abhängig von der Stufe des Zauberers', + SPELL_ATTR0_STOP_ATTACK_TARGET => 'Stoppt Autoangriff', + SPELL_ATTR0_IMPOSSIBLE_DODGE_PARRY_BLOCK => 'Kann nicht ausgewichen, pariert oder geblockt werden', + SPELL_ATTR0_CAST_TRACK_TARGET => 'Ziel bei Wirken verfolgen (nur Spieler)', + SPELL_ATTR0_CASTABLE_WHILE_DEAD => 'Kann tot verwendet werden', + SPELL_ATTR0_CASTABLE_WHILE_MOUNTED => 'Kann verwendet werden, während Ihr auf einem Reittier sitzt', + SPELL_ATTR0_DISABLED_WHILE_ACTIVE => 'Abklingzeit beginnt, nachdem die Aura schwindet', + SPELL_ATTR0_NEGATIVE_1 => 'Aura ist Schwächungszauber', + SPELL_ATTR0_CASTABLE_WHILE_SITTING => 'Kann im Sitzen benutzt werden', + SPELL_ATTR0_CANT_USED_IN_COMBAT => 'Kann nicht im Kampf verwendet werden', + SPELL_ATTR0_UNAFFECTED_BY_INVULNERABILITY => 'Nicht betroffen von Unverwundbarkeit', + SPELL_ATTR0_HEARTBEAT_RESIST_CHECK => 'Herzschlagresistenz', + SPELL_ATTR0_CANT_CANCEL => 'Aura kann nicht entfernt werden' + ), + 'attributes1' => array( + SPELL_ATTR1_DISMISS_PET => 'Zuerst Begleiter freigeben', + SPELL_ATTR1_DRAIN_ALL_POWER => 'Braucht alle Ressourcen auf', + SPELL_ATTR1_CHANNELED_1 => 'Kanalisiert 1', + SPELL_ATTR1_CANT_BE_REDIRECTED => 'Kann nicht umgelenkt werden', + SPELL_ATTR1_NO_SKILL_INCREASE => 'Keine Fähigkeitenerhöhung', + SPELL_ATTR1_NOT_BREAK_STEALTH => 'Beendet Verstohlenheitsmodus nicht', + SPELL_ATTR1_CHANNELED_2 => 'Kanalisiert 2', + SPELL_ATTR1_CANT_BE_REFLECTED => 'Kann nicht reflektiert werden', + SPELL_ATTR1_CANT_TARGET_IN_COMBAT => 'Das Ziel darf sich nicht im Kampf befinden', + SPELL_ATTR1_MELEE_COMBAT_START => 'Initiiere Kampf (aktiviert Autoangriff)', + SPELL_ATTR1_NO_THREAT => 'Generiert keine Bedrohung', + SPELL_ATTR1_DONT_REFRESH_DURATION_ON_RECAST => 'Einzigartige Aura', + SPELL_ATTR1_IS_PICKPOCKET => 'Taschendiebstahl-Zauber', + SPELL_ATTR1_FARSIGHT => 'Fernsicht umschalten', + SPELL_ATTR1_CHANNEL_TRACK_TARGET => 'Ziel beim Kanalisieren verfolgen', + SPELL_ATTR1_DISPEL_AURAS_ON_IMMUNITY => 'Entfernt Auren bei Immunität', + SPELL_ATTR1_UNAFFECTED_BY_SCHOOL_IMMUNE => 'Nicht betroffen von Immunität gegen diese Magiesart', + SPELL_ATTR1_UNAUTOCASTABLE_BY_PET => 'Kein Auto-Zaubern (KI)', + SPELL_ATTR1_PREVENTS_ANIM => 'Verhindert Animation', + SPELL_ATTR1_CANT_TARGET_SELF => 'Zauberer kann nicht Ziel sein', + SPELL_ATTR1_FINISHING_MOVE_DAMAGE => 'Erfordert Combo-Punkte auf dem Ziel (Effektstärke)', + SPELL_ATTR1_THREAT_ONLY_ON_MISS => 'Bedrohung nur bei verfehlen', + SPELL_ATTR1_FINISHING_MOVE_DURATION => 'Erfordert Combo-Punkte auf dem Ziel (Effektdauer)', + SPELL_ATTR1_IGNORE_OWNERS_DEATH => 'Ignoriere Tod des Besitzers', + SPELL_ATTR1_IS_FISHING => 'Erfordert Angelrute', + SPELL_ATTR1_AURA_STAYS_AFTER_COMBAT => 'Aura bleibt nach Kampf bestehen', + SPELL_ATTR1_REQUIRE_ALL_TARGETS => 'Benötige alle Ziele', + SPELL_ATTR1_DISCOUNT_POWER_ON_MISS => 'Reduzierte Kosten bei Verfehlen', + SPELL_ATTR1_DONT_DISPLAY_IN_AURA_BAR => 'Kein Aura-Symbol', + SPELL_ATTR1_CHANNEL_DISPLAY_SPELL_NAME => 'Name im Zauberbalken', + SPELL_ATTR1_ENABLE_AT_DODGE => 'Kombo beim Ausweichen', + SPELL_ATTR1_CAST_WHEN_LEARNED => 'Beim erlernen Zaubern', + ), + 'attributes2' => array( + SPELL_ATTR2_CAN_TARGET_DEAD => 'Totes Ziel zulässig', + SPELL_ATTR2_NO_SHAPESHIFT_UI => 'Kein Gestaltwandel-UI', + SPELL_ATTR2_CAN_TARGET_NOT_IN_LOS => 'Ignoriere Sichtlinie', + SPELL_ATTR2_ALLOW_LOW_LEVEL_BUFF => 'Erlaube Verstärkungszauber auf niederstufigem Ziel', + SPELL_ATTR2_DISPLAY_IN_STANCE_BAR => 'Spezialaktionsleiste benutzen', + SPELL_ATTR2_AUTOREPEAT_FLAG => 'Automatische Wiederholung', + SPELL_ATTR2_CANT_TARGET_TAPPED => 'Setzt ein unmarkiertes Ziel voraus', + SPELL_ATTR2_DO_NOT_REPORT_SPELL_FAILURE => 'Fehlgeschlagene Zauber nicht melden', + SPELL_ATTR2_INCLUDE_IN_ADVANCED_COMBAT_LOG => '', + SPELL_ATTR2_ALWAYS_CAST_AS_UNIT => 'Immer als Einheit zaubern', + SPELL_ATTR2_SPECIAL_TAMING_FLAG => 'Markierung für besonderes Zähmen', + SPELL_ATTR2_HEALTH_FUNNEL => 'Lebenslinie', + SPELL_ATTR2_CHAIN_FROM_CASTER => 'Verkettung vom Zauberer ausgehend', + SPELL_ATTR2_PRESERVE_ENCHANT_IN_ARENA => 'Das Ziel muss ein eigener Gegenstand sein', + SPELL_ATTR2_ALLOW_WHILE_INVISIBLE => 'Nutzbar während unsichtbar', + SPELL_ATTR2_DO_NOT_CONSUME_IF_GAINED_DURING_CAST => 'Nicht verbrauchen, wenn beim Zaubern erlangt', + SPELL_ATTR2_TAME_BEAST => 'Kein aktiver Begleiter', + SPELL_ATTR2_NOT_RESET_AUTO_ACTIONS => 'Kampftimer nicht zurücksetzen', + SPELL_ATTR2_REQ_DEAD_PET => 'Erfordert toten Begleiter', + SPELL_ATTR2_NOT_NEED_SHAPESHIFT => 'Gestaltwandlung nicht erforderlich', + SPELL_ATTR2_INITIATE_COMBAT_POST_CAST_ENABLES_AUTO_ATTACK => 'Initiiere Kampf nach Wirken (aktiviert Autoangriff)', + SPELL_ATTR2_FAIL_ON_ALL_TARGETS_IMMUNE => 'Scheitern, wenn alle Ziele immun', + SPELL_ATTR2_NO_INITIAL_THREAT => 'Keine Initialbedrohung', + SPELL_ATTR2_IS_ARCANE_CONCENTRATION => 'Abklingzeit bei Fehlschlag proccen', + SPELL_ATTR2_ITEM_CAST_WITH_OWNER_SKILL => 'Gegenstand mit Besitzerfertigkeit gewirkt', + SPELL_ATTR2_DONT_BLOCK_MANA_REGEN => 'Manaregenaration nicht blockieren', + SPELL_ATTR2_UNAFFECTED_BY_AURA_SCHOOL_IMMUNE => 'Ignoriert Immunität gegen Magieart', + SPELL_ATTR2_IGNORE_WEAPONSKILL => 'Ignoriere Waffenfertigkeit', + SPELL_ATTR2_NOT_AN_ACTION => 'Keine Aktion', + SPELL_ATTR2_CANT_CRIT => 'Kann nicht kritisch treffen', + SPELL_ATTR2_ACTIVE_THREAT => 'Aktive Bedrohung', + SPELL_ATTR2_FOOD_BUFF => 'Essens-/Getränk-Stärkungszauber', + ), + 'attributes3' => array( + SPELL_ATTR3_PVP_ENABLING => 'Aktiviert PvP', + SPELL_ATTR3_IGNORE_PROC_SUBCLASS_MASK => 'Keine Ausrüstungs-Voraussetzung für Proc', + SPELL_ATTR3_NO_CASTING_BAR_TEXT => 'Kein Text im Zauberbalken', + SPELL_ATTR3_COMPLETELY_BLOCKED => 'Vollständig geblockt', + SPELL_ATTR3_IGNORE_RESURRECTION_TIMER => 'Kein Wiederbelebungs-Verzögerung', + SPELL_ATTR3_NO_DURABILTIY_LOSS => 'Kein Haltbarkeitsverlust', + SPELL_ATTR3_NO_AVOIDANCE => 'Kann nicht vermieden werden', + SPELL_ATTR3_STACK_FOR_DIFF_CASTERS => 'Nutzt Regeln fürs DoT-Stapeln', + SPELL_ATTR3_ONLY_TARGET_PLAYERS => 'Kann nur einen Spieler zum Ziel haben', + SPELL_ATTR3_NOT_A_PROC => 'Kein Proc', + SPELL_ATTR3_MAIN_HAND => 'Benötigt eine Haupthandwaffe', + SPELL_ATTR3_BATTLEGROUND => 'Nur in Schlachtfeldern benutzbar', + SPELL_ATTR3_ONLY_TARGET_GHOSTS => 'Nur auf Geister', + SPELL_ATTR3_DONT_DISPLAY_CHANNEL_BAR => 'Verstecke Kanalisierungsbalken', + SPELL_ATTR3_IS_HONORLESS_TARGET => 'Ist Ehrenloses Ziel', + SPELL_ATTR3_NORMAL_RANGED_ATTACK => 'Normaler Fernkampfangriff', + SPELL_ATTR3_CANT_TRIGGER_PROC => 'Wirker Procs unterdrücken', + SPELL_ATTR3_NO_INITIAL_AGGRO => 'Verwickelt das Ziel nicht in einen Kampf', + SPELL_ATTR3_IGNORE_HIT_RESULT => 'Kann nicht verfehlen', + SPELL_ATTR3_DISABLE_PROC => 'Deaktiviert Procs', + SPELL_ATTR3_DEATH_PERSISTENT => 'Wirkt über den Tod hinaus', + SPELL_ATTR3_ONLY_PROC_OUTDOORS => 'Procct nur draußen', + SPELL_ATTR3_REQ_WAND => 'Benötigt einen Zauberstab', + SPELL_ATTR3_NO_DAMAGE_HISTORY => 'Keine Schadenshistorie', + SPELL_ATTR3_REQ_OFFHAND => 'Benötigt eine Nebenhandwaffe', + SPELL_ATTR3_TREAT_AS_PERIODIC => 'Als periodischen Zauber behandeln', + SPELL_ATTR3_CAN_PROC_FROM_PROCS => 'Kann durch Procs proccen', + SPELL_ATTR3_DRAIN_SOUL => 'Proc nur bei Wirker', + SPELL_ATTR3_IGNORE_CASTER_AND_TARGET_RESTRICTIONS => 'Ignoriere Beschränkungen an Wirker und Ziel', + SPELL_ATTR3_NO_DONE_BONUS => 'Ignoriere Zauberer-Modifikatoren', + SPELL_ATTR3_DONT_DISPLAY_RANGE => 'Reichweite nicht anzeigen', + SPELL_ATTR3_NOT_ON_AOE_IMMUNE => 'Nicht bei AoE-Immunität' + ), + 'attributes4' => array( + SPELL_ATTR4_IGNORE_RESISTANCES => 'Wirken nicht im Log', + SPELL_ATTR4_PROC_ONLY_ON_CASTER => 'Klassenauslöser nur am Ziel', + SPELL_ATTR4_FADES_WHILE_LOGGED_OUT => 'Dauert an, während Ihr ausgeloggt seid', + SPELL_ATTR4_NO_HELPFUL_THREAT => 'Verursacht keine hilfreiche Bedrohung', + SPELL_ATTR4_NO_HARMFUL_THREAT => 'Verursacht keine offensive Bedrohung', + SPELL_ATTR4_ALLOW_CLIENT_TARGETING => 'Erlaube Client-Zielsetzung', + SPELL_ATTR4_NOT_STEALABLE => 'Zauber kann nicht geraubt werden', + SPELL_ATTR4_CAN_CAST_WHILE_CASTING => 'Zaubern während des zauberns zulässig', + SPELL_ATTR4_FIXED_DAMAGE => 'Ignoriere Modifikatoren für erlittenen Schaden', + SPELL_ATTR4_TRIGGER_ACTIVATE => 'Combat Feedback When Usable', + SPELL_ATTR4_SPELL_VS_EXTEND_COST => 'Kostenskalierung mit Waffengeschwindigkeit', + SPELL_ATTR4_NO_PARTIAL_IMMUNITY => 'Keine teilweise Immunität', + SPELL_ATTR4_AURA_IS_BUFF => 'Aura ist Stärkungszauber', + SPELL_ATTR4_DO_NOT_LOG_CASTER => 'Zauberer nicht loggen', + SPELL_ATTR4_DAMAGE_DOESNT_BREAK_AURAS => 'Reaktiver Schadens-Proc', + SPELL_ATTR4_NOT_IN_SPELLBOOK => 'Nicht im Zauberbuch', + SPELL_ATTR4_NOT_USABLE_IN_ARENA => 'Kann nicht in der Arena verwendet werden', + SPELL_ATTR4_USABLE_IN_ARENA => 'Benutzbar in Arenen', + SPELL_ATTR4_AREA_TARGET_CHAIN => 'Überspringende Kettengeschosse', + SPELL_ATTR4_ALLOW_PROC_WHILE_SITTING => 'Erlaube Proc im Sitzen', + SPELL_ATTR4_NOT_CHECK_SELFCAST_POWER => 'Anwendung der Aura kann nicht fehlschlagen', + SPELL_ATTR4_DONT_REMOVE_IN_ARENA => 'Zulässig beim Betreten der Arena', + SPELL_ATTR4_PROC_SUPPRESS_SWING_ANIM => 'Proc unterdrückt Schwung-Animation', + SPELL_ATTR4_CANT_TRIGGER_ITEM_SPELLS => 'Unterdrücke Waffen-Procs', + SPELL_ATTR4_AUTO_RANGED_COMBAT => 'Automatischer Fernkampf', + SPELL_ATTR4_IS_PET_SCALING => 'Skalliert mit Statistiken des Besitzers', + SPELL_ATTR4_CAST_ONLY_IN_OUTLAND => 'Nur in Flugzonen', + SPELL_ATTR4_FORCE_DISPLAY_CASTBAR => 'Zauberbalkenanzeige erzwingen', + SPELL_ATTR4_IGNORE_COMBAT_TIMER => 'Ignoriere Kampftimer', + SPELL_ATTR4_AURA_BOUNCE_FAILS_SPELL => 'Abweisung der Aura unterbricht Zauber', + SPELL_ATTR4_OBSOLETE => '', + SPELL_ATTR4_USE_FACING_FROM_SPELL => 'Blickrichtung von Zauber benutzen' + ), + 'attributes5' => array( + SPELL_ATTR5_CAN_CHANNEL_WHEN_MOVING => 'Erlaube Aktionen beim kanalisieren', + SPELL_ATTR5_NO_REAGENT_WHILE_PREP => 'Keine Reagenzkosten bei Aura', + SPELL_ATTR5_REMOVE_ON_ARENA_ENTER => 'Beim Betreten der Arena entfernt', + SPELL_ATTR5_USABLE_WHILE_STUNNED => 'Verwendbar, während Ihr betäubt seid', + SPELL_ATTR5_TRIGGERS_CHANNELING => 'Löst Kanalisieren aus', + SPELL_ATTR5_SINGLE_TARGET_SPELL => 'Die Aura wirkt auf nur ein Ziel', + SPELL_ATTR5_IGNORE_AREA_EFFECT_PVP_CHECK => 'Ignoriere PvP-Check für Gebietseffekt', + SPELL_ATTR5_NOT_ON_PLAYER => 'Nicht auf Spielern', + SPELL_ATTR5_CANT_TARGET_PLAYER_CONTROLLED => 'Nicht auf von Spielern gesteuertem NSC', + SPELL_ATTR5_START_PERIODIC_AT_APPLY => 'Beginnt zu ticken, sobald die Aura angewendet wird', + SPELL_ATTR5_HIDE_DURATION => 'Dauer nicht anzeigen', + SPELL_ATTR5_ALLOW_TARGET_OF_TARGET_AS_TARGET => 'Implizierte Zielfindung', + SPELL_ATTR5_MELEE_CHAIN_TARGETING => 'Kettenzielsetzung für Nahkampf', + SPELL_ATTR5_HASTE_AFFECT_DURATION => 'Zaubertempo beeinflusst Intervall', + SPELL_ATTR5_NOT_USABLE_WHILE_CHARMED => 'Nicht benutzbar wenn bezaubert', + SPELL_ATTR5_TREAT_AS_AREA_EFFECT => 'Als Gebietseffekt behandeln', + SPELL_ATTR5_AURA_AFFECTS_NOT_JUST_REQ_EQUIPPED_ITEM => 'Aura betrifft nicht nur benötigten angelegten Gegenstand', + SPELL_ATTR5_USABLE_WHILE_FEARED => 'Verwendbar, während Ihr verängstigt seid', + SPELL_ATTR5_USABLE_WHILE_CONFUSED => 'Verwendbar, während Ihr verwirrt seid', + SPELL_ATTR5_DONT_TURN_DURING_CAST => 'KI ist Ziel nicht zugewandt', + SPELL_ATTR5_DO_NOT_ATTEMPT_A_PET_RESUMMON_WHEN_DISMOUNTING => 'Herbeirufen eines Begleiters beim Absteigen nicht versuchen', + SPELL_ATTR5_IGNORE_TARGET_REQUIREMENTS => 'Ignoriere Zielvoraussetzungen', + SPELL_ATTR5_NOT_ON_TRIVIAL => 'Nicht auf trivialen Zielen', + SPELL_ATTR5_NO_PARTIAL_RESISTS => 'Kein teilweises Widerstehen', + SPELL_ATTR5_IGNORE_CASTER_REQUIREMENTS => 'Ignoriere Zauberer-Voraussetzungen', + SPELL_ATTR5_ALWAYS_LINE_OF_SIGHT => 'Immer in Sichtlinie', + SPELL_ATTR5_SKIP_CHECKCAST_LOS_CHECK => 'AoE immer in Sichtlinie', + SPELL_ATTR5_DONT_SHOW_AURA_IF_SELF_CAST => 'Kein Aura-Symbol beim Zauberer', + SPELL_ATTR5_DONT_SHOW_AURA_IF_NOT_SELF_CAST => 'Kein Aura-Symbol beim Ziel', + SPELL_ATTR5_AURA_UNIQUE_PER_CASTER => 'Aura je Wirker einzigartig', + SPELL_ATTR5_ALWAYS_SHOW_GROUND_TEXTURE => 'Immer Bodentexturen zeigen', + SPELL_ATTR5_ADD_MELEE_HIT_RATING => 'Nahkampftrefferwertung hinzufügen' + ), + 'attributes6' => array( + SPELL_ATTR6_DONT_DISPLAY_COOLDOWN => 'Keine Abklingzeit im Tooltip', + SPELL_ATTR6_ONLY_IN_ARENA => 'Nur in der Arena benutzbar', + SPELL_ATTR6_IGNORE_CASTER_AURAS => 'Ignoriere Auren auf Zauberer', + SPELL_ATTR6_ASSIST_IGNORE_IMMUNE_FLAG => 'Kann immunem Spieler assistieren', + SPELL_ATTR6_IGNORE_FOR_MOD_TIME_RATE => 'Für Mod Zeitrate ignorieren', + SPELL_ATTR6_DONT_CONSUME_PROC_CHARGES => 'Keine Ressourcen aufbrauchen', + SPELL_ATTR6_USE_SPELL_CAST_EVENT => 'Sende \'spell cast\' Ereignis', + SPELL_ATTR6_AURA_IS_WEAPON_PROC => 'Aura ist Waffen-Proc', + SPELL_ATTR6_CANT_TARGET_CROWD_CONTROLLED => 'Springt nicht auf Ziele unter Gruppenkontrolle über', + SPELL_ATTR6_ALLOW_ON_CHARMED_TARGETS => 'Zulässig für bezauberte Ziele', + SPELL_ATTR6_CAN_TARGET_POSSESSED_FRIENDS => 'Aura nicht im Log', + SPELL_ATTR6_NOT_IN_RAID_INSTANCE => 'Kann nicht im Schlachtzug verwendet werden', + SPELL_ATTR6_CASTABLE_WHILE_ON_VEHICLE => 'Zulässig beim führen eines Fahrzeugs', + SPELL_ATTR6_CAN_TARGET_INVISIBLE => 'Ignoriere Phasenwechsel', + SPELL_ATTR6_AI_PRIMARY_RANGED_ATTACK => 'Primärer Fernkampfangriff für KI', + SPELL_ATTR6_NO_PUSHBACK => 'Keine Zauberzeiterhöhung durch Schaden', + SPELL_ATTR6_NO_JUMP_PATHING => 'Keine Wegfindung für Sprung', + SPELL_ATTR6_ALLOW_EQUIP_WHILE_CASTING => 'Erlaube Anlegen beim Zaubern', + SPELL_ATTR6_CAST_BY_CHARMER => 'Vom Steuernden ausgehend', + SPELL_ATTR6_DELAY_COMBAT_TIMER_DURING_CAST => 'Verzögere Kampftimer während Zauber', + SPELL_ATTR6_ONLY_VISIBLE_TO_CASTER => 'Aura-Symbol nur für Zauberer sichtbar (Max 10)', + SPELL_ATTR6_CLIENT_UI_TARGET_EFFECTS => '', + SPELL_ATTR6_ABSORB_CANNOT_BE_IGNORE => 'Absorbtion kann nicht ignoriert werden', + SPELL_ATTR6_TAPS_IMMEDIATELY => 'Tappt sofort', + SPELL_ATTR6_CAN_TARGET_UNTARGETABLE => 'Kann nicht-Anvisierbares anvisieren', + SPELL_ATTR6_NOT_RESET_SWING_IF_INSTANT => 'Schlagtimer bei Spontanzauber nicht zurücksetzen', + SPELL_ATTR6_VEHICLE_IMMUNITY_CATEGORY => 'Fahrzeugimmunitätenkategorie', + SPELL_ATTR6_LIMIT_PCT_HEALING_MODS => 'Ignoriere Heilungsmodifikatoren', + SPELL_ATTR6_DO_NOT_AUTO_SELECT_TARGET_WITH_INITIATES_COMBAT => 'Wählt nicht automatisch Ziele, wenn es Kampf initiieren würde', + SPELL_ATTR6_LIMIT_PCT_DAMAGE_MODS => 'Ignoriere Schadensmodifikator für Zauberer', + SPELL_ATTR6_DISABLE_TIED_EFFECT_POINTS => 'Gebundene Effektpunkte deaktivieren', // Tie: "Gebunden"? "Gleichstand"? + SPELL_ATTR6_IGNORE_CATEGORY_COOLDOWN_MODS => 'Ignoriere Modifikatoren für Kategorie-Abklingzeit' + ), + 'attributes7' => array( + SPELL_ATTR7_ALLOW_SPELL_REFLECTION => '', + SPELL_ATTR7_IGNORE_DURATION_MODS => 'Kein Zieldauer-Modifikator', + SPELL_ATTR7_DISABLE_AURA_WHILE_DEAD => 'Paladin Aura', + SPELL_ATTR7_IS_CHEAT_SPELL => 'Debug Zauber', + SPELL_ATTR7_TREAT_AS_RAID_BUFF => 'Als Schlachtzugs-Stärkungszauber behandeln', + SPELL_ATTR7_SUMMON_PLAYER_TOTEM => 'Totem', + SPELL_ATTR7_NO_PUSHBACK_ON_DAMAGE => 'Verursacht keine Zauberzeitverlängerung', + SPELL_ATTR7_PREPARE_FOR_VEHICLE_CONTROL_END => 'Für Ende der Fahrzeugsteuerung vorbereiten', + SPELL_ATTR7_HORDE_ONLY => 'Horde-spezifischer Zauber', + SPELL_ATTR7_ALLIANCE_ONLY => 'Allianz-spezifischer Zauber', + SPELL_ATTR7_DISPEL_CHARGES => 'Magiebannung entfernt Stapel', + SPELL_ATTR7_INTERRUPT_ONLY_NONPLAYER => 'Kann Unterbrechen verursachen', + SPELL_ATTR7_CAN_CAUSE_SILENCE => 'Kann Stille verursachen', + SPELL_ATTR7_NO_UI_NOT_INTERRUPTIBLE => 'Nicht unterbrechbar bei fehlendem UI', + SPELL_ATTR7_RECAST_ON_RESUMMON => 'Neuwirken bei Wiederbeschwörung', + SPELL_ATTR7_RESET_SWING_TIMER_AT_SPELL_START => 'Schwungtimer bei Zauberbeginn zurücksetzen', + SPELL_ATTR7_CAN_RESTORE_SECONDARY_POWER => 'Kann inaktive Ressourcen wiederherstellen', + SPELL_ATTR7_DO_NOT_LOG_PVP_KILL => 'PvP-Todesstoß nicht loggen', + SPELL_ATTR7_HAS_CHARGE_EFFECT => 'Attacke bei Sturmangriff auf Einheit', + SPELL_ATTR7_ZONE_TELEPORT => 'Zauberfehlschlag an Einheitsziel melden', + SPELL_ATTR7_NO_CLIENT_FAIL_WHILE_STUNNED_FLEEING_CONFUSED => 'Kein Abbruch durch Client während Betäubung, Flucht, Verwirrung', + SPELL_ATTR7_RETAIN_COOLDOWN_THROUGH_LOAD => 'Abklingzeit beim Laden beibehalten', + SPELL_ATTR7_IGNORE_COLD_WEATHER_FLYING => 'Ignoriere Voraussetzung für Kaltwetterflug', + SPELL_ATTR7_CANT_DODGE => 'Angriff nicht ausweichbar', + SPELL_ATTR7_CANT_PARRY => 'Angriff nicht parrierbar', + SPELL_ATTR7_CANT_MISS => 'Angriff nicht verfehlbar', + SPELL_ATTR7_TREAT_AS_NPC_AOE => 'Als NSC-Gebietseffekt behandeln', + SPELL_ATTR7_BYPASS_NO_RESURRECT_AURA => 'Umgehe Auren mit \'Verhindere Wiederbelebung\'', + SPELL_ATTR7_CONSOLIDATED_RAID_BUFF => 'Wird mit anderen Stärkungszaubern zusammengefasst', + SPELL_ATTR7_REFLECTION_ONLY_DEFENDS => 'Reflektion beschützt nur', + SPELL_ATTR7_CAN_PROC_FROM_SUPPRESSED_TARGET_PROCS => 'Kann von unterdrückten Ziel-Procs proccen', + SPELL_ATTR7_CLIENT_INDICATOR => 'Zauber immer loggen', ) ), 'item' => array( + 'id' => "Gegenstands-ID: ", 'notFound' => "Dieser Gegenstand existiert nicht .", 'armor' => "%s Rüstung", 'block' => "%s Blocken", 'charges' => "%d |4Aufladung:Aufladungen;", 'locked' => "Verschlossen", - 'ratingString' => "%s @ L%s", + 'ratingString' => '%2$s @ Lvl%3$d', 'heroic' => "Heroisch", 'startQuest' => "Dieser Gegenstand startet eine Quest", 'bagSlotString' => '%1$d Platz %2$s', @@ -1059,7 +2239,7 @@ $lang = array( 'refundable' => "Rückzahlbar", 'noNeedRoll' => "Kann nicht für Bedarf werfen", 'atKeyring' => "Passt in den Schlüsselbund", - 'worth' => "Wert", + 'worth' => "Wert: ", 'consumable' => "Verbrauchbar", 'nonConsumable' => "Nicht verbrauchbar", 'accountWide' => "Accountweit", @@ -1068,17 +2248,17 @@ $lang = array( 'prospectable' => "Sondierbar", 'disenchantable'=> "Kann entzaubert werden", 'cantDisenchant'=> "Kann nicht entzaubert werden", - 'repairCost' => "Reparaturkosten", - 'tool' => "Werkzeug", + 'repairCost' => "Reparaturkosten: ", + 'tool' => "Werkzeug: ", 'cost' => "Preis", 'content' => "Inhalt", - '_transfer' => 'Dieser Gegenstand wird mit %s vertauscht, wenn Ihr zur %s wechselt.', + '_transfer' => 'Dieser Gegenstand wird mit %s vertauscht, wenn Ihr zur %s wechselt.', '_unavailable' => "Dieser Gegenstand ist nicht für Spieler verfügbar.", '_rndEnchants' => "Zufällige Verzauberungen", '_chance' => "(Chance von %s%%)", - 'slot' => "Platz", - '_quality' => "Qualität", - 'usableBy' => "Benutzbar von", + 'slot' => "Platz: ", + '_quality' => "Qualität: ", + 'usableBy' => "Benutzbar von: ", 'buyout' => "Sofortkaufpreis", 'each' => "Stück", 'tabOther' => "Anderes", @@ -1088,24 +2268,43 @@ $lang = array( 'uniqueEquipped'=> ["Einzigartig anlegbar", null, "Einzigartig angelegt: %s (%d)"], 'speed' => "Tempo", 'dps' => "(%.1f Schaden pro Sekunde)", + '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.", + "Dauer: %d Min.", + "Dauer: %d |4Stunde:Stunden;", + "Dauer: %d |4Tag:Tage;" + ), + 'cooldown' => array( // ITEM_COOLDOWN_TOTAL* + '(%s Abklingzeit)', + "(%d Sek. Abklingzeit)", + "(%d Min. Abklingzeit)", + "(%d |4Stunde:Stunden; Abklingzeit)", + "(%d |4Tag:Tage; Abklingzeit)" + ), 'damage' => array( // *DAMAGE_TEMPLATE* // basic, basic /w school, add basic, add basic /w school 'single' => ['%d Schaden', '%1$d %2$sschaden', '+ %1$d Schaden', '+ %1$d %2$sschaden' ], 'range' => ['%1$d - %2$d Schaden', '%1$d - %2$d %3$sschaden', '+ %1$d - %2$d Schaden', '+ %1$d - %2$d %3$sschaden' ], 'ammo' => ["Verursacht %g zusätzlichen Schaden pro Sekunde.", "Verursacht %g zusätzlichen %sschaden pro Sekunde", "+ %g Schaden pro Sekunde", "+ %g %sschaden pro Sekunde"] ), - 'gems' => "Edelsteine", - 'socketBonus' => "Sockelbonus", + 'gems' => "Edelsteine: ", + 'socketBonus' => "Sockelbonus: %s", 'socket' => array( "Metasockel", "Roter Sockel", "Gelber Sockel", "Blauer Sockel", -1 => "Prismatischer Sockel" ), 'gemColors' => array( // *_GEM "Meta", "Rot", "Gelb", "Blau" ), + 'gemRequires' => "Benötigt ", // ENCHANT_CONDITION_REQUIRES 'gemConditions' => array( // ENCHANT_CONDITION_* in GlobalStrings.lua; 2 not in use (use as PH) - 2 => "weniger als %d |4Edelstein:Edelsteine; der Kategorie %s", - 3 => "mehr Edelsteine der Kategorie %s als Edelsteine der Kategorie %s", - 5 => "mindestens %d |4Edelstein:Edelsteine; der Kategorie %s" + ENCHANT_CONDITION_LESS_VALUE => "weniger als %d |4Edelstein:Edelsteine; der Kategorie %s", + ENCHANT_CONDITION_MORE_COMPARE => "mehr Edelsteine der Kategorie %s als Edelsteine der Kategorie %s", + ENCHANT_CONDITION_MORE_VALUE => "mindestens %d |4Edelstein:Edelsteine; der Kategorie %s" ), 'reqRating' => array( // ITEM_REQ_ARENA_RATING* "Benötigt eine persönliche Arenawertung und Teamwertung von %d.", @@ -1120,7 +2319,7 @@ $lang = array( "Benutzen: ", "Anlegen: ", "Chance bei Treffer: ", "", "", "", "" ), - 'bonding' => array( + 'bonding' => array( // ITEM_BIND_* "Accountgebunden", "Wird beim Aufheben gebunden", "Wird beim Anlegen gebunden", "Wird bei Benutzung gebunden", "Questgegenstand", "Questgegenstand" ), @@ -1154,7 +2353,7 @@ $lang = array( ), 'elixirType' => [null, "Kampf", "Wächter"], 'cat' => array( - 2 => "Waffen", // self::$spell['weaponSubClass'] + 2 => array("Waffen", []), // filled with self::$spell['weaponSubClass'] on load 4 => array("Rüstung", array( 1 => "Stoffrüstung", 2 => "Lederrüstung", 3 => "Schwere Rüstung", 4 => "Plattenrüstung", 6 => "Schilde", 7 => "Buchbände", 8 => "Götzen", 9 => "Totems", 10 => "Siegel", -6 => "Umhänge", -5 => "Nebenhandgegenstände", -8 => "Hemden", @@ -1197,54 +2396,54 @@ $lang = array( 12 => "Quest", 13 => "Schlüssel", ), - 'statType' => array( - "Mana", - "Gesundheit", + 'statType' => array( // ITEM_MOD_* + '%1$c%2$d Mana', + '%1$c%2$d Gesundheit', null, - "Beweglichkeit", - "Stärke", - "Intelligenz", - "Willenskraft", - "Ausdauer", + '%1$c%2$d Beweglichkeit', + '%1$c%2$d Stärke', + '%1$c%2$d Intelligenz', + '%1$c%2$d Willenskraft', + '%1$c%2$d Ausdauer', null, null, null, null, "Erhöht die Verteidigungswertung um %d.", "Erhöht Eure Ausweichwertung um %d.", "Erhöht Eure Parierwertung um %d.", "Erhöht Eure Blockwertung um %d.", "Erhöht Nahkampftrefferwertung um %d.", - "Erhöht Fernkampftrefferwertung um %d.", + "Erhöht Distanztrefferwertung um %d.", "Erhöht Zaubertrefferwertung um %d.", "Erhöht kritische Nahkampftrefferwertung um %d.", - "Erhöht kritische Fernkampftrefferwertung um %d.", + "Erhöht kritische Distanztrefferwertung um %d.", "Erhöht kritische Zaubertrefferwertung um %d.", - "Erhöht Vermeidungswertung für Nahkampftreffer um +3.", + "Erhöht Vermeidungswertung für Nahkampftreffer um %d.", "Erhöht Vermeidungswertung für Distanztreffer um %d.", "Erhöht Vermeidungswertung für Zaubertreffer um %d.", "Erhöht Vermeidungswertung für kritische Nahkampftreffer um %d.", "Erhöht Vermeidungswertung für kritische Distanztreffer um %d.", "Erhöht Vermeidungswertung für kritische Zaubertreffer um %d.", "Erhöht Nahkampftempowertung um %d.", - "Erhöht Fernkampftempowertung um %d.", + "Erhöht Distanztempowertung um %d.", "Erhöht Zaubertempowertung um %d.", - "Erhöht Eure Trefferwertung um %d.", - "Erhöht Eure kritische Trefferwertung um %d.", + "Erhöht Trefferwertung um %d.", + "Erhöht kritische Trefferwertung um %d.", "Erhöht Vermeidungswertung um %d.", "Erhöht Vermeidungswertung für kritische Treffer um %d.", "Erhöht Eure Abhärtungswertung um %d.", - "Erhöht Eure Tempowertung um %d.", - "Erhöht Waffenkundewertung um %d.", + "Erhöht Tempowertung um %d.", + "Erhöht Eure Waffenkundewertung um %d.", "Erhöht Angriffskraft um %d.", "Erhöht Distanzangriffskraft um %d.", "Erhöht die Angriffskraft in Katzen-, Bären-, Terrorbären- und Mondkingestalt um %d.", - "Erhöht den von Zaubern und Effekten verursachten Schaden um bis zu %d.", "Erhöht die von Zaubern und Effekten verursachte Heilung um bis zu %d.", + "Erhöht den von Zaubern und Effekten verursachten Schaden um bis zu %d.", "Stellt alle 5 Sek. %d Mana wieder her.", - "Erhöht Euren Rüstungsdurchschlagwert um %d.", + "Erhöht Eure Rüstungsdurchschlagwertung um %d.", "Erhöht die Zaubermacht um %d.", "Stellt alle 5 Sek. %d Gesundheit wieder her.", "Erhöht den Zauberdurchschlag um %d.", - "Erhöht Blockwert um %d.", - "Unbekannter Bonus #%d (%d)", + "Erhöht den Blockwert Eures Schilds um %d.", + "Unbekannter Bonus #%d (%d)" ) ) ); diff --git a/localization/locale_enus.php b/localization/locale_enus.php index e12e9978..6735239a 100644 --- a/localization/locale_enus.php +++ b/localization/locale_enus.php @@ -1,10 +1,11 @@ ["years", "months", "weeks", "days", "hours", "minutes", "seconds", "milliseconds"], 'ab' => ["yr", "mo", "wk", "day", "hr", "min", "sec", "ms"] ), + 'lang' => ['English', null, 'French', 'German', 'Chinese', null, 'Spanish', null, 'Russian'], 'main' => array( 'name' => "name", 'link' => "Link", @@ -23,42 +25,48 @@ $lang = array( 'jsError' => "Please make sure you have javascript enabled.", 'language' => "Language", 'feedback' => "Feedback", - 'numSQL' => "Number of MySQL queries", - 'timeSQL' => "Time of MySQL queries", + 'numSQL' => "Number of SQL queries", + 'timeSQL' => "Time of SQL queries", 'noJScript' => 'This site makes extensive use of JavaScript.
Please enable JavaScript in your browser.', - 'userProfiles' => "My Profiles", + // 'userProfiles' => "My Profiles", 'pageNotFound' => "This %s doesn't exist.", 'gender' => "Gender", 'sex' => [null, "Male", "Female"], 'players' => "Players", + 'thePlayer' => "The Player", 'quickFacts' => "Quick Facts", 'screenshots' => "Screenshots", 'videos' => "Videos", - 'side' => "Side", + 'side' => "Side: ", 'related' => "Related", 'contribute' => "Contribute", - // 'replyingTo' => "The answer to a comment from", + // 'replyingTo' => "The answer to a comment from", 'submit' => "Submit", + 'save' => 'Save', 'cancel' => "Cancel", 'rewards' => "Rewards", 'gains' => "Gains", - 'login' => "Login", + // 'login' => "Login", 'forum' => "Forum", - 'n_a' => "n/a", - 'siteRep' => "Reputation", + 'siteRep' => "Reputation: ", 'yourRepHistory'=> "Your Reputation History", 'aboutUs' => "About us & contact", 'and' => " and ", 'or' => " or ", 'back' => "Back", 'reputationTip' => "Reputation points", - 'byUserTimeAgo' => 'By %1$s %s ago', + 'byUser' => 'By %1$s ', // mind the \s 'help' => "Help", + 'status' => "Status", + 'yes' => "Yes", + 'no' => "No", + 'any' => "Any", + 'all' => "All", // filter 'extSearch' => "Extended search", 'addFilter' => "Add another Filter", - 'match' => "Match", + 'match' => "Match: ", 'allFilter' => "All filters", 'oneFilter' => "At least one", 'applyFilter' => "Apply filter", @@ -66,7 +74,7 @@ $lang = array( 'refineSearch' => 'Tip: Refine your search by browsing a subcategory.', 'clear' => "clear", 'exactMatch' => "Exact match", - '_reqLevel' => "Required level", + '_reqLevel' => "Required level: ", // infobox 'unavailable' => "Not available to players", // alternative wording found: "No longer available to players" ... aw screw it <_< @@ -79,7 +87,11 @@ $lang = array( 'links' => "Links", 'compare' => "Compare", 'view3D' => "View in 3D", - 'findUpgrades' => "Find upgrades...", + 'findUpgrades' => "Find upgrades…", + 'report' => "Report", + 'writeGuide' => "Write New Guide", + 'edit' => "Edit", + 'changelog' => 'Changelog', // misc Tools 'errPageTitle' => "Page not found", @@ -92,27 +104,26 @@ $lang = array( ), // article & infobox - 'englishOnly' => "This page is only available in English.", + 'langOnly' => "This page is only available in %s.", // calculators - 'preset' => "Preset", + 'preset' => "Preset: ", 'addWeight' => "Add another weight", 'createWS' => "Create a weight scale", 'jcGemsOnly' => "Include JC-only
gems", 'cappedHint' => 'Tip: Remove weights for capped statistics such as Hit rating.', - 'groupBy' => "Group By", + 'groupBy' => "Group By: ", 'gb' => array( ["None", "none"], ["Slot", "slot"], ["Level", "level"], ["Source", "source"] ), 'compareTool' => "Item Comparison Tool", 'talentCalc' => "Talent Calculator", 'petCalc' => "Hunter Pet Calculator", - 'chooseClass' => "Choose a class", - 'chooseFamily' => "Choose a pet family", + 'chooseClass' => "Choose a class:", + 'chooseFamily' => "Choose a pet family:", // search 'search' => "Search", - 'searchButton' => "Search", 'foundResult' => "Search Results for", 'noResult' => "No Results for", 'tryAgain' => "Please try some different keywords or check your spelling.", @@ -121,7 +132,27 @@ $lang = array( // formating 'colon' => ': ', 'dateFmtShort' => "Y/m/d", - 'dateFmtLong' => "Y/m/d \a\\t H:i", + 'dateFmtLong' => "Y/m/d \a\\t g:i A", + 'dateFmtIntl' => "MMMM d, y", + '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.", @@ -129,6 +160,10 @@ $lang = array( 'genericError' => "An error has occurred; refresh the page and try again. If the error persists email feedback", # LANG.genericerror 'bannedRating' => "You have been banned from rating comments.", # LANG.tooltip_banned_rating 'tooManyVotes' => "You have reached the daily voting cap. Come back tomorrow!", # LANG.tooltip_too_many_votes + 'alreadyReport' => "You've already reported this.", # LANG.ct_resp_error7 + 'textTooShort' => "Your message is too short.", + 'cannotComment' => "You have been banned from writing comments.", + 'textLength' => "Your comment has %d characters and must have at least %d and at most %d characters.", 'moreTitles' => array( 'reputation' => "Website Reputation", @@ -147,6 +182,61 @@ $lang = array( ) ) ), + 'guide' => array( + 'myGuides' => "My Guides", + 'editTitle' => "Edit your Guide", + 'newTitle' => "Create New Guide", + 'author' => "Author: ", + 'spec' => "Specialization: ", + 'sticky' => "Sticky Status", + 'views' => "Views: ", + 'patch' => "Patch", + 'added' => "Added: ", + 'rating' => "Rating: ", + 'votes' => "[span id=guiderating-value]%.2g[/span]/5 ([span id=guiderating-votes][n5=%d][/span] votes) [span id=guiderating][/span]", + 'noVotes' => "not enough votes [span id=guiderating][/span]", + 'byAuthor' => "By %s", + 'notFound' => "This guide doesn't exist.", + 'clTitle' => 'Changelog For "%2$s"', + 'clStatusSet' => 'Status set to %s: ', + 'clCreated' => 'Created: ', + 'clMinorEdit' => 'Minor Edit', + 'editor' => array( + 'fullTitle' => 'Full Title', + 'fullTitleTip' => 'The full guide title will be used on the guide page and may include SEO-oriented phrasing.', + 'name' => 'Name', + 'nameTip' => 'This should be a simple and clear name of what the guide is, for use in places like menus and guide lists.', + 'description' => 'Description', + 'descriptionTip' => "Description that will be used for search engines.

If left empty, it will be generated automatically.", + // 'commentEmail' => 'Comment Emails', + // 'commentEmailTip' => 'Should the author get emailed whenever a user comments on this guide?', + 'changelog' => 'Changelog For This Edit', + 'changelogTip' => 'Enter your changelog for this update here.', + 'save' => 'Save', + 'submit' => 'Submit for Review', + 'autoupdate' => 'Autoupdate', + 'showAdjPrev' => 'Show adjacent preview', + 'preview' => 'Preview', + 'class-spec' => 'Class / Spec', + 'category' => 'Category', + 'testGuide' => 'See how your guide will look', + 'images' => 'Images', + 'statusTip' => array( + GuideMgr::STATUS_DRAFT => 'Your guide is in "Draft" status and you are the only one able to see it. Keep editing it as long as you like, and when you feel it's ready submit it for review.', + GuideMgr::STATUS_REVIEW => 'Your guide is being reviewed.', + GuideMgr::STATUS_APPROVED => 'Your guide has been published.', + GuideMgr::STATUS_REJECTED => 'Your guide has been rejected. After it\'s shortcomings have been remedied you may resubmit it for review.', + GuideMgr::STATUS_ARCHIVED => 'Your guide is outdated and has been archived. Is will no longer be listed and can\'t be edited.', + ) + ), + 'category' => array( + null, "Classes", "Professions", "World Events", "New Players & Leveling", + "Raid & Boss Fights", "Economy & Money", "Achievements", "Vanity Items, Pets & Mounts", "Other" + ), + 'status' => array( + null, "Draft", "Waiting for Approval", "Approved", "Rejected", "Archived" + ), + ), 'profiler' => array( 'realm' => "Realm", 'region' => "Region", @@ -156,23 +246,35 @@ $lang = array( '_cpFooter' => "If you want a more refined search try out our advanced search options. You can also create a new custom profile.", 'firstUseTitle' => "%s of %s", 'complexFilter' => "Complex filter selected! Search results are limited to cached Characters.", - + 'customProfile' => " (Custom Profile)", 'resync' => "Resync", 'guildRoster' => "Guild Roster for <%s>", 'arenaRoster' => "Arena Team Roster for <%s>", 'atCaptain' => "Arena Team Captain", - + 'atSize' => "Size: ", 'profiler' => "Character Profiler", - 'arenaTeams' => "Arena Teams", - 'guilds' => "Guilds", - + '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.", 'profile' => "This character doesn't exist or is not yet in the database." ), - 'dummyNPCs' => array( - 100001 => "Gunship Battle", 200001 => "Northrend Beasts", 200002 => "Faction Champions", 200003 => "Val'kyr Twins" + 'regions' => array( + 'us' => "Americas", + 'eu' => "Europe", + 'kr' => "Korea", + 'tw' => "Taiwan", + 'cn' => "China", + 'dev' => "Development" + ), + 'encounterNames'=> array( // from dungeonencounter.dbc + 243 => "The Seven", + 334 => "Grand Champions", + 629 => "Northrend Beasts", 637 => "Faction Champions", 641 => "Val'kyr Twins", + 692 => "The Four Horsemen", + 748 => "The Iron Council", + 847 => "Icecrown Gunship Battle" ), ), 'screenshot' => array( @@ -189,88 +291,127 @@ $lang = array( ), 'error' => array( 'unkFormat' => "Unknown image format.", - 'tooSmall' => "Your screenshot is way too small. (< ".CFG_SCREENSHOT_MIN_SIZE."x".CFG_SCREENSHOT_MIN_SIZE.").", + 'tooSmall' => "Your screenshot is way too small. (< CFG_SCREENSHOT_MIN_SIZE x CFG_SCREENSHOT_MIN_SIZE).", 'selectSS' => "Please select the screenshot to upload.", 'notAllowed' => "You are not allowed to upload screenshots!", ) ), + 'video' => array( + 'submission' => "Video Suggestion", + 'thanks' => array( + 'contrib' => "Thanks a lot for your contribution!", + 'goBack' => 'Click here to go back to the page you came from.', + 'note' => "Note: Your video will need to be approved before appearing on the site. This can take up to 72 hours." + ), + 'error' => array( + 'isPrivate' => "The suggested video is private.", + 'noExist' => "No video found at the provided Url.", + 'selectVI' => "Please enter valid video information.", // message_novideo + 'notAllowed' => "You are not allowed to suggest videos!", + ) + ), 'game' => array( - 'achievement' => "achievement", - 'achievements' => "Achievements", - 'class' => "class", - 'classes' => "Classes", - 'currency' => "currency", - 'currencies' => "Currencies", - 'difficulty' => "Difficulty", - 'dispelType' => "Dispel type", - 'duration' => "Duration", - 'emote' => "emote", - 'emotes' => "Emotes", - 'enchantment' => "enchantment", - 'enchantments' => "Enchantments", + // type strings + 'npc' => "NPC", + 'npcs' => "NPCs", 'object' => "object", 'objects' => "Objects", - 'glyphType' => "Glyph type", - 'race' => "race", - 'races' => "Races", - 'title' => "title", - 'titles' => "Titles", - 'eventShort' => "Event", - 'event' => "World Event", - 'events' => "World Events", - 'faction' => "faction", - 'factions' => "Factions", - 'cooldown' => "%s cooldown", - 'icon' => "icon", - 'icons' => "icons", 'item' => "item", 'items' => "Items", 'itemset' => "item Set", 'itemsets' => "Item Sets", - 'mechanic' => "Mechanic", - 'mechAbbr' => "Mech.", - 'meetingStone' => "Meeting Stone", - 'npc' => "NPC", - 'npcs' => "NPCs", - 'pet' => "Pet", - 'pets' => "Hunter Pets", - 'profile' => "profile", - 'profiles' => "Profiles", 'quest' => "quest", 'quests' => "Quests", + 'spell' => "spell", + 'spells' => "Spells", + 'zone' => "zone", + 'zones' => "Zones", + 'faction' => "faction", + 'factions' => "Factions", + 'pet' => "Pet", + 'pets' => "Hunter Pets", + 'achievement' => "achievement", + 'achievements' => "Achievements", + 'title' => "title", + 'titles' => "Titles", + 'event' => "World Event", + 'events' => "World Events", + 'class' => "class", + 'classes' => "Classes", + 'race' => "race", + 'races' => "Races", + 'skill' => "skill", + 'skills' => "Skills", + 'currency' => "currency", + 'currencies' => "Currencies", + 'sound' => "sound", + 'sounds' => "Sounds", + 'icon' => "icon", + 'icons' => "icons", + 'profile' => "profile", + 'profiles' => "Profiles", + 'guild' => "Guild", + 'guilds' => "Guilds", + 'arenateam' => "Arena Team", + 'arenateams' => "Arena Teams", + 'guide' => "Guide", + 'guides' => "Guides", + 'emote' => "emote", + 'emotes' => "Emotes", + 'enchantment' => "enchantment", + 'enchantments' => "Enchantments", + 'areatrigger' => "areatrigger", + 'areatriggers' => "Areatrigger", + 'mail' => "mail", + 'mails' => "Mails", + + 'cooldown' => "%s cooldown", + 'difficulty' => "Difficulty: ", + 'dispelType' => "Dispel type", + 'duration' => "Duration", + 'eventShort' => "Event: %s", + 'flags' => "Flags", + 'glyphType' => "Glyph type: ", + 'level' => "Level", + 'mechanic' => "Mechanic", + 'mechAbbr' => "Mech.: ", + 'meetingStone' => "Meeting Stone: ", 'requires' => "Requires %s", 'requires2' => "Requires", 'reqLevel' => "Requires Level %s", - 'reqSkillLevel' => "Required skill level", - 'level' => "Level", + 'reqSkillLevel' => "Required skill level: ", 'school' => "School", - 'skill' => "skill", - 'skills' => "Skills", - 'sound' => "sound", - 'sounds' => "Sounds", - 'spell' => "spell", - 'spells' => "Spells", - 'type' => "Type", + 'type' => "Type: ", 'valueDelim' => " to ", - 'zone' => "zone", - 'zones' => "Zones", + 'target' => "", 'pvp' => "PvP", // PVP 'honorPoints' => "Honor Points", // HONOR_POINTS 'arenaPoints' => "Arena Points", // ARENA_POINTS 'heroClass' => "Hero class", - 'resource' => "Resource", - 'resources' => "Resources", - 'role' => "Role", // ROLE - 'roles' => "Roles", // LFG_TOOLTIP_ROLES - 'specs' => "Specs", + 'resource' => "Resource: ", + 'resources' => "Resources: ", + 'role' => "Role: ", // ROLE + 'roles' => "Roles: ", // LFG_TOOLTIP_ROLES + 'specs' => "Specs: ", '_roles' => ["Healer", "Melee DPS", "Ranged DPS", "Tank"], 'phases' => "Phases", - 'mode' => "Mode", - 'modes' => [-1 => "Any", "Normal / Normal 10", "Heroic / Normal 25", "Heroic 10", "Heroic 25"], + 'mode' => "Mode: ", + '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( //
'.sprintf(Util::$dfnString, $info[1], $key).''.$key.'
'; - foreach (explode(', ', $info[2]) as $option) - { - $opt = explode(':', $option); - $buff .= ''; - } - $buff .= '
'; - - $buff .= ''; - - if (strstr($info[0], 'default:')) - $buff .= '|'; - else - $buff .= '|'; - - if (!($r['flags'] & CON_FLAG_PERSISTENT)) - $buff .= '|'; - - $buff .= '
' + + '' + + '
' + + '' + + '' + + '' + + '
' + + '' + + '' + + '' + + '' + + '
' + + 'Text counter placeholder' + + '
' + + '
' + + '
infobox)): + namespace Aowow\Template; + + use \Aowow\Lang; + + /** @var PageTemplate $this */ ?> - + +infobox || $this->contributions || $this->series || $this->contribute & (CONTRIBUTE_SS | CONTRIBUTE_VI)): +echo '
'.PHP_EOL; + + if ($this->infobox): +?> + + -contributions)): +contributions): ?> - + + + series)): - foreach ($this->series as $s): - $this->brick('series', ['list' => $s[0], 'listTitle' => $s[1]]); - endforeach; -endif; + if ($this->series): + foreach ($this->series as [$list, $title]): + $this->brick('series', ['list' => $list, 'listTitle' => $title]); + endforeach; + endif; -if (!empty($this->type) && !empty($this->typeId)): + if ($this->contribute & CONTRIBUTE_SS): ?> - + + -community['vi'])): -?> - - - -
- -community['vi'])): -?> - -\n"; -endif; -?> \ No newline at end of file +contribute & CONTRIBUTE_VI && ($this->user::isInGroup(U_GROUP_ADMIN | U_GROUP_BUREAU | U_GROUP_VIDEO) || !empty($this->community['vi']))): +?> + + +
+ + +contribute & CONTRIBUTE_SS): +?> + + + +contribute & CONTRIBUTE_VI && ($this->user::isInGroup(U_GROUP_ADMIN | U_GROUP_BUREAU | U_GROUP_VIDEO) || !empty($this->community['vi']))): +?> + + + +'.PHP_EOL; +endif; +?> diff --git a/template/bricks/inputbox-form-email.tpl.php b/template/bricks/inputbox-form-email.tpl.php new file mode 100644 index 00000000..c12bff9b --- /dev/null +++ b/template/bricks/inputbox-form-email.tpl.php @@ -0,0 +1,50 @@ + + +
+ + + +
+
+

+
+ + + +
+ + + +
+ +
+ + +
+ +
+
+ diff --git a/template/bricks/inputbox-form-password.tpl.php b/template/bricks/inputbox-form-password.tpl.php new file mode 100644 index 00000000..f7629e42 --- /dev/null +++ b/template/bricks/inputbox-form-password.tpl.php @@ -0,0 +1,79 @@ + + +
+ + + +
+
+

+
+ + + + + + + + + + + + + + + + + + +
+ + +
+
+ + diff --git a/template/pages/acc-signIn.tpl.php b/template/bricks/inputbox-form-signin.tpl.php similarity index 52% rename from template/pages/acc-signIn.tpl.php rename to template/bricks/inputbox-form-signin.tpl.php index 5503d52f..06944c46 100644 --- a/template/pages/acc-signIn.tpl.php +++ b/template/bricks/inputbox-form-signin.tpl.php @@ -1,14 +1,13 @@ -brick('header'); ?> - -
-
-
brick('announcement'); + namespace Aowow\Template; - $this->brick('pageTemplate'); + use \Aowow\Lang; + + /** @var PageTemplate $this */ ?> +
+ -
+
-

-
error; ?>
+

+
- - + + - + - +
/> - +
- +
-
-
|
+ +
+
| |
+ + +
-'.Lang::account('accCreate')."
\n"; -endif; -?> + +cfg('ACC_ALLOW_REGISTER')): ?> +
+ + +
-
-
- -brick('footer'); ?> diff --git a/template/pages/acc-signUp.tpl.php b/template/bricks/inputbox-form-signup.tpl.php similarity index 68% rename from template/pages/acc-signUp.tpl.php rename to template/bricks/inputbox-form-signup.tpl.php index b029652a..876ff2e5 100644 --- a/template/pages/acc-signUp.tpl.php +++ b/template/bricks/inputbox-form-signup.tpl.php @@ -1,21 +1,11 @@ -brick('header'); ?> - -
-
-
brick('announcement'); + namespace Aowow\Template; - $this->brick('pageTemplate'); + use \Aowow\Lang; ?> +
-text)): ?> -
-

head; ?>

-
-
text; ?>
-
- + -
+
-

head; ?>

-
error; ?>
+

+
- - + + - + - + - - + + - +
/> - +
- +
@@ -116,9 +104,3 @@ - -
-
-
- -brick('footer'); ?> diff --git a/template/bricks/inputbox-status.tpl.php b/template/bricks/inputbox-status.tpl.php new file mode 100644 index 00000000..dbd52050 --- /dev/null +++ b/template/bricks/inputbox-status.tpl.php @@ -0,0 +1,17 @@ + + +
+ +
+

+
+ + +
+ +
+ + +
diff --git a/template/bricks/lvTabs.tpl.php b/template/bricks/lvTabs.tpl.php index c1049698..8bdb7093 100644 --- a/template/bricks/lvTabs.tpl.php +++ b/template/bricks/lvTabs.tpl.php @@ -1,82 +1,64 @@ user) ? 'tabsRelated' : 'myTabs'; -$isTabbed = !empty($this->forceTabs) || $relTabs || count($this->lvTabs) > 1; + namespace Aowow\Template; -// lvTab: [file, data, extraInclude] - -if ($isTabbed): + /** @var PageTemplate $this */ ?> + +lvTabs && count($this->lvTabs)) || $this->charactersLvData || $this->profilesLvData || $this->contribute): + if ($this->lvTabs?->isTabbed()): +?> +
+ -
lvTabs as $lv): - if ($lv[0]): - continue; - endif; - echo ''; -endforeach; - ?>
- + diff --git a/template/bricks/mail.tpl.php b/template/bricks/mail.tpl.php index f55074c7..384955f2 100644 --- a/template/bricks/mail.tpl.php +++ b/template/bricks/mail.tpl.php @@ -1,13 +1,47 @@ mail): - echo '

'.sprintf(Lang::quest('mailDelivery'), $m['sender'], $m['delay'])."

\n"; + namespace Aowow\Template; - if ($m['subject']): - echo '
'.$m['subject']."
\n"; + use \Aowow\Lang; + + /** @var PageTemplate $this */ + +if (['header' => $header, 'subject' => $subject, 'text' => $text, 'attachments' => $attachments] = $this->mail): + $offset ??= 0; // in case we have multiple icons on the page (prominently quest-rewards) + + echo '

'.Lang::mail('mailDelivery', $header).'

'.PHP_EOL; + + if ($subject): + echo '
'.$subject.'
'.PHP_EOL; endif; - if ($m['text']): - echo '
'.$m['text']."
\n"; + if ($text): + echo '
'.$text.'
'.PHP_EOL; + endif; + + if ($attachments): +?> + + + +renderContainer(20, $offset, true); + endforeach; +?> + +
+ + + + diff --git a/template/bricks/mapper.tpl.php b/template/bricks/mapper.tpl.php index 2b0a8def..0bad6e70 100644 --- a/template/bricks/mapper.tpl.php +++ b/template/bricks/mapper.tpl.php @@ -1,68 +1,109 @@ map) && empty($this->map)): - echo Lang::zone('noMap'); -elseif (!empty($this->map['data'])): - if ($this->type == TYPE_QUEST) : - echo "
\n"; - elseif ($this->type != TYPE_ZONE): - echo '
'.($this->type == TYPE_OBJECT ? Lang::gameObject('foundIn') : ($this->type == TYPE_SOUND ? Lang::sound('foundIn') : Lang::npc('foundIn'))).' '; + namespace Aowow\Template; - $extra = $this->map['extra']; - echo Lang::concat($this->map['mapperData'], true, function ($areaData, $areaId) use ($extra) { - return ''.$extra[$areaId].' ('.reset($areaData)['count'].')'; + use \Aowow\Lang; + + /** @var PageTemplate $this */ + +if ([$mapper, $mapperData, $som, $foundIn] = $this->map): + if ($foundIn): + echo '
'.$foundIn[0].' '; + echo Lang::concat($mapperData, true, function ($areaData, $areaId) use ($foundIn) { + return ''.$foundIn[$areaId].' ('.array_sum(array_column($areaData, 'count')).')'; }); - - echo ".
\n"; + echo '.
'.PHP_EOL; + else: + echo '
'.PHP_EOL; endif; - if (!empty($this->map['data']['zone']) && $this->map['data']['zone'] < 0): + if (isset($mapper['zone']) && $mapper['zone'] < 0): ?> +
+ map['som'])): + if ($som): ?> +
+ +
+ -map['som'])): + elseif ($mapper): + if ($som): ?> +
+ +
+ + + + +
+ - if ($this->type != TYPE_ZONE && $this->type != TYPE_QUEST): - echo " \$WH.gE(\$WH.ge('mapper-zone-generic'), 'a')[0].onclick();\n"; - endif; -?> - //]]> diff --git a/template/bricks/markup.tpl.php b/template/bricks/markup.tpl.php new file mode 100644 index 00000000..95d7038c --- /dev/null +++ b/template/bricks/markup.tpl.php @@ -0,0 +1,9 @@ + + +
+ +
+ + diff --git a/template/bricks/pageTemplate.tpl.php b/template/bricks/pageTemplate.tpl.php index 308edfe5..17c37a7a 100644 --- a/template/bricks/pageTemplate.tpl.php +++ b/template/bricks/pageTemplate.tpl.php @@ -1,33 +1,44 @@ - + //]]> diff --git a/template/bricks/reagentList.tpl.php b/template/bricks/reagentList.tpl.php index 641376de..273f77f0 100644 --- a/template/bricks/reagentList.tpl.php +++ b/template/bricks/reagentList.tpl.php @@ -1,21 +1,30 @@ -

+ + +

+ + + + + $itr): - echo '' . - ''; - - if (!empty($itr['final']) && $enhanced): - echo '
 
'; - elseif ($enhanced): - echo '
 
'; +foreach ($reagents as $k => ['path' => $path, 'level' => $level, 'final' => $final, 'typeStr' => $typeStr, 'icon' => $icon]): + $icon->renderContainer(0, $k); // just to set offset + if ($icon->noIcon): + echo '
' . + ''; endif; - echo ''.$itr['name'].''.($itr['qty'] > 1 ? ' ('.$itr['qty'].')' : null)."\n"; + if ($final && $enhanced): + echo '
 
'; + elseif ($enhanced): + echo '
 
'; + endif; + + echo ''.($icon->href ? ''.$icon->text.'' : $icon->text).''.($icon->num > 1 ? ' ('.$icon->num.')' : '').''.PHP_EOL; endforeach; ?> +
- - + +
  •  
'; + else: + echo '
diff --git a/template/bricks/redButtons.tpl.php b/template/bricks/redButtons.tpl.php index 4cab787b..8c9c4625 100644 --- a/template/bricks/redButtons.tpl.php +++ b/template/bricks/redButtons.tpl.php @@ -1,8 +1,16 @@ + + redButtons[BUTTON_WOWHEAD])): if ($this->redButtons[BUTTON_WOWHEAD]): - echo 'WowheadWowhead'; + echo 'WowheadWowhead'; else: echo 'WowheadWowhead'; endif; @@ -16,7 +24,7 @@ endif; // ingame-links/markdown/ect if (isset($this->redButtons[BUTTON_LINKS])): if ($b = $this->redButtons[BUTTON_LINKS]): - echo ' "'"]).');">'.Lang::main('links').''.Lang::main('links').''; + echo ' "'"]).');">'.Lang::main('links').''.Lang::main('links').''; else: echo ''.Lang::main('links').''.Lang::main('links').''; endif; @@ -25,7 +33,7 @@ endif; // view in 3D if (isset($this->redButtons[BUTTON_VIEW3D])): if ($b = $this->redButtons[BUTTON_VIEW3D]): // json_encode puts property names in brackets wich is not cool with inline javascript - echo ' "'"]).')">'.Lang::main('view3D').''.Lang::main('view3D').''; + echo ' "'"]).')">'.Lang::main('view3D').''.Lang::main('view3D').''; else: echo ''.Lang::main('view3D').''.Lang::main('view3D').''; endif; @@ -34,7 +42,7 @@ endif; // item comparison tool if (isset($this->redButtons[BUTTON_COMPARE])): if ($b = $this->redButtons[BUTTON_COMPARE]): - echo ''.Lang::main('compare').''.Lang::main('compare').''; + echo ''.Lang::main('compare').''.Lang::main('compare').''; else: echo ''.Lang::main('compare').''.Lang::main('compare').''; endif; @@ -80,3 +88,39 @@ if (isset($this->redButtons[BUTTON_RESYNC])): echo ''.Lang::profiler('resync').''.Lang::profiler('resync').''; endif; endif; + +// report guide +if (isset($this->redButtons[BUTTON_GUIDE_REPORT])): + if ($this->redButtons[BUTTON_GUIDE_REPORT]): + echo ''.Lang::main('report').''.Lang::main('report').''; + else: + echo ''.Lang::main('report').''.Lang::main('report').''; + endif; +endif; + +// show guide changelog +if (isset($this->redButtons[BUTTON_GUIDE_LOG])): + if ($this->redButtons[BUTTON_GUIDE_LOG]): + echo ''.Lang::main('changelog').''.Lang::main('changelog').''; + else: + echo ''.Lang::main('changelog').''.Lang::main('changelog').''; + endif; +endif; + +// edit existing guide +if (isset($this->redButtons[BUTTON_GUIDE_EDIT])): + if ($this->redButtons[BUTTON_GUIDE_EDIT]): + echo ''.Lang::main('edit').''.Lang::main('edit').''; + else: + echo ''.Lang::main('edit').''.Lang::main('edit').''; + endif; +endif; + +// create new guide +if (isset($this->redButtons[BUTTON_GUIDE_NEW])): + if ($this->redButtons[BUTTON_GUIDE_NEW]): + echo ''.Lang::main('writeGuide').''.Lang::main('writeGuide').''; + else: + echo ''.Lang::main('writeGuide').''.Lang::main('writeGuide').''; + endif; +endif; diff --git a/template/bricks/rewards.tpl.php b/template/bricks/rewards.tpl.php index 230c5949..c8e336b8 100644 --- a/template/bricks/rewards.tpl.php +++ b/template/bricks/rewards.tpl.php @@ -1,37 +1,44 @@ +
+ $i): - echo '\n"; - echo $k % 2 ? '' : null; + $last = array_key_last($rewards); + foreach ($rewards as $k => $icon): + echo $icon->renderContainer(24, $offset); + echo $k % 2 && $k != $last ? str_repeat(' ', 24) . '' : ''; endforeach; if (count($rewards) % 2): echo ''; endif; ?> +
'.$i['name']."
+ diff --git a/template/bricks/series.tpl.php b/template/bricks/series.tpl.php index fc626bfa..a803f4da 100644 --- a/template/bricks/series.tpl.php +++ b/template/bricks/series.tpl.php @@ -1,30 +1,21 @@ - + + +
+ $itr): - echo ' \n"; + echo $this->renderSeriesItem($idx, $itr, 12); endforeach; ?> +
'.($idx + 1).'
'; - - $_ = array_keys($itr); - $end = array_pop($_); - foreach ($itr as $k => $i): // itemItr - switch ($i['side']): - case 1: $wrap = '%s'; break; - case 2: $wrap = '%s'; break; - default: $wrap = '%s'; break; - endswitch; - - if ($i['typeId'] == $this->typeId): - echo sprintf($wrap, ''.$i['name'].''); - else: - echo sprintf($wrap, ''.$i['name'].''); - endif; - - echo $end == $k ? null : '
'; - endforeach; - echo "
diff --git a/template/bricks/tooltip.tpl.php b/template/bricks/tooltip.tpl.php index a58fe520..e80326b9 100644 --- a/template/bricks/tooltip.tpl.php +++ b/template/bricks/tooltip.tpl.php @@ -1,16 +1,26 @@ -
-
+tooltip; +?> + +
+
-
-
+
+
jsGlobals[6][2][$this->typeId]['buff']); // not set with items - if ($hasBuff): ?> -

-
+ +

+
+ diff --git a/template/listviews/areatrigger.tpl b/template/listviews/areatrigger.tpl new file mode 100644 index 00000000..0a428c0b --- /dev/null +++ b/template/listviews/areatrigger.tpl @@ -0,0 +1,88 @@ +Listview.templates.areatrigger = { + sort: [1], + searchable: 1, + filtrable: 1, + + columns: [ + { + id: 'id', + name: 'ID', + width: '5%', + value: 'id', + compute: function(data, td) { + if (data.id) { + let pre = $WH.ce('pre', { style: { display: 'inline', margin: '0' }}, $WH.ct(data.id)); + $WH.clickToCopy(pre); + $WH.ae(td, pre); + } + } + }, + { + id: 'name', + name: LANG.name, + type: 'text', + align: 'left', + value: 'name', + compute: function(areatrigger, td, tr) { + var wrapper = $WH.ce('div'); + + var a = $WH.ce('a'); + a.style.fontFamily = 'Verdana, sans-serif'; + a.href = this.getItemLink(areatrigger); + + $WH.ae(a, $WH.ct(areatrigger.name)); + $WH.ae(wrapper, a); + $WH.ae(td, wrapper); + }, + sortFunc: function(a, b, col) { + return $WH.strcmp(a.name, b.name); + }, + getVisibleText: function(areatrigger) { + return areatrigger.name; + } + }, + { + id: 'location', + name: LANG.location, + type: 'text', + compute: function(areatrigger, td) { + return Listview.funcBox.location(areatrigger, td); + }, + getVisibleText: function(areatrigger) { + return Listview.funcBox.arrayText(areatrigger.location, g_zones); + }, + sortFunc: function(a, b, col) { + return Listview.funcBox.assocArrCmp(a.location, b.location, g_zones); + } + }, + { + id: 'type', + name: LANG.type, + type: 'text', + value: 'type', + width: '12%', + compute: function(areatrigger, td, tr) { + if (g_trigger_types[areatrigger.type]) + $WH.ae(td, $WH.ct(g_trigger_types[areatrigger.type])) + else + $WH.ae(td, $WH.ct(g_trigger_types[0])); + }, + sortFunc: function(a, b, col) { + return $WH.strcmp(this.getVisibleText(a), this.getVisibleText(b)); + }, + getVisibleText: function(areatrigger) { + return g_trigger_types[areatrigger.type]; + } + } + ], + getItemLink: function(areatrigger) { + return '?areatrigger=' + areatrigger.id; + }, + onBeforeCreate : function() { + // hide duplicate id col + if (this.debug || g_user?.debug) { + let colId = this.columns.findIndex(x => x.id == 'id'); + this.visibility = this.visibility.filter(x => x != colId); + } + } +} diff --git a/template/listviews/commentAdminCol.tpl b/template/listviews/commentAdminCol.tpl new file mode 100644 index 00000000..40360178 --- /dev/null +++ b/template/listviews/commentAdminCol.tpl @@ -0,0 +1,52 @@ +var _ = [ + { + id: 'manage', + name: 'Manage', + type: 'text', + align: 'center', + value: 'subject', + sortable: false, + compute: function(comment, td, tr) { + let wrapper = $WH.ce('div'); + + let send = function (el, id, status) + { + $.ajax({cache: false, url: '?admin=comment', type: 'POST', + error: function() { + alert('Operation failed.'); + }, + success: function(json) { + if (json != 1) + alert('Operation failed.'); + else + $WH.de(el.parentNode); + }, + data: { id: id, status: status } + }) + + return true; + }; + + td.onclick = $WH.sp; + + let a = $WH.ce('span'); + a.style.fontFamily = 'Verdana, sans-serif'; + a.style.marginLeft = '10px'; + a.href = '#'; + + _ = a.cloneNode(); + _.className = 'icon-tick'; + _.onclick = send.bind(this, td, comment.id, 0); + g_addTooltip(_, LANG.lvcomment_uptodate); + $WH.ae(wrapper, _); + + _ = a.cloneNode(); + _.className = 'icon-delete'; + _.onclick = send.bind(this, td, comment.id, 1); + g_addTooltip(_, LANG.delete); + $WH.ae(wrapper, _); + + $WH.ae(td, wrapper); + } + } +]; diff --git a/template/listviews/emote.tpl.php b/template/listviews/emote.tpl similarity index 100% rename from template/listviews/emote.tpl.php rename to template/listviews/emote.tpl diff --git a/template/listviews/enchantment.tpl.php b/template/listviews/enchantment.tpl similarity index 98% rename from template/listviews/enchantment.tpl.php rename to template/listviews/enchantment.tpl index 5b0a9b91..5d679b47 100644 --- a/template/listviews/enchantment.tpl.php +++ b/template/listviews/enchantment.tpl @@ -66,7 +66,7 @@ Listview.templates.enchantment = { if (!enchantment.spells) return null; // no spell - var spellId = $(enchantment.spells).first(); + var spellId = $(enchantment.spells).first()[0]; if (g_spells[spellId]) return g_spells[spellId]['name_' + Locale.getName()]; diff --git a/template/listviews/guideAdminCol.tpl b/template/listviews/guideAdminCol.tpl new file mode 100644 index 00000000..12407e0f --- /dev/null +++ b/template/listviews/guideAdminCol.tpl @@ -0,0 +1,84 @@ +var _ = [ + { + id: 'description', + name: LANG.ct_dialog_description, + type: 'text', + align: 'left', + value: 'description', + after: 'title', + width: '50%', + compute: function(guide, td, tr) { + td.innerText = guide.description; + }, + sortFunc: function(a, b, col) { + return $WH.strcmp(a.description, b.description); + }, + getVisibleText: function(guide) { + return guide.description; + } + }, + { + id: 'manage', + name: 'Manage', + type: 'text', + align: 'center', + value: 'subject', + sortable: false, + compute: function(guide, td, tr) { + let wrapper = $WH.ce('div'); + + let send = function (el, id, status) + { + let message = ''; + if (status == 4) // rejected + { + while (message === '') + message = prompt('Please provide your reasoning.'); + + if (message === null) + return false; + } + + $.ajax({cache: false, url: '?admin=guide', type: 'POST', + error: function() { + alert('Operation failed.'); + }, + success: function(json) { + if (json) + alert('Operation failed: ' + json); + else + $WH.de(el.parentNode); + }, + data: { id: id, status: status, msg: message } + }) + + return true; + }; + + let a = $WH.ce('a'); + a.style.fontFamily = 'Verdana, sans-serif'; + a.style.marginLeft = '10px'; + a.href = '#'; + + _ = a.cloneNode(); + _.className = 'icon-edit'; + _.href = '?guide=edit&id=' + guide.id; + g_addTooltip(_, 'Edit'); + $WH.ae(wrapper, _); + + _ = a.cloneNode(); + _.className = 'icon-tick'; + _.onclick = send.bind(this, td, guide.id, 3); + g_addTooltip(_, 'Approve'); + $WH.ae(wrapper, _); + + _ = a.cloneNode(); + _.className = 'icon-delete'; + _.onclick = send.bind(this, td, guide.id, 4); + g_addTooltip(_, 'Reject'); + $WH.ae(wrapper, _); + + $WH.ae(td, wrapper); + } + } +]; diff --git a/template/listviews/itemStandingCol.tpl b/template/listviews/itemStandingCol.tpl new file mode 100644 index 00000000..86114660 --- /dev/null +++ b/template/listviews/itemStandingCol.tpl @@ -0,0 +1,18 @@ +var _ = [ + { + id: 'standing', + after: 'reqlevel', + name: LANG.standing, + width: '12%', + value: 'standing', + type: 'text', + getValue: function(item) + { + return g_reputation_standings[item.standing]; + }, + compute: function(item, td) + { + return g_reputation_standings[item.standing]; + } + } +]; diff --git a/template/listviews/itemStandingCol.tpl.php b/template/listviews/itemStandingCol.tpl.php deleted file mode 100644 index 2b6440f6..00000000 --- a/template/listviews/itemStandingCol.tpl.php +++ /dev/null @@ -1,54 +0,0 @@ -var _ = [ - { - id: 'standing', - after: 'reqlevel', - name: LANG.standing, - width: '12%', - value: 'standing', - type: 'text', - getValue: function(item) - { - return g_reputation_standings[item.standing]; - }, - compute: function(item, td) - { - return g_reputation_standings[item.standing]; - } - } -]; - diff --git a/template/listviews/mail.tpl b/template/listviews/mail.tpl new file mode 100644 index 00000000..72e05fda --- /dev/null +++ b/template/listviews/mail.tpl @@ -0,0 +1,102 @@ +Listview.templates.mail = { + sort: [1], + searchable: 1, + filtrable: 1, + + columns: [ + { + id: 'id', + name: 'ID', + width: '5%', + value: 'id', + compute: function(data, td) { + if (data.id) { + let pre = $WH.ce('pre', { style: { display: 'inline', margin: '0' }}, $WH.ct(data.id)); + $WH.clickToCopy(pre); + $WH.ae(td, pre); + } + } + }, + { + id: 'subject', + name: LANG.subject, + type: 'text', + align: 'left', + value: 'subject', + compute: function(mail, td, tr) { + var wrapper = $WH.ce('div'); + + var a = $WH.ce('a'); + a.style.fontFamily = 'Verdana, sans-serif'; + a.href = this.getItemLink(mail); + + $WH.ae(a, $WH.ct(mail.subject)); + $WH.ae(wrapper, a); + $WH.ae(td, wrapper); + }, + sortFunc: function(a, b, col) { + return $WH.strcmp(a.subject, b.subject); + }, + getVisibleText: function(mail) { + return mail.subject; + } + }, + { + id: 'body', + name: LANG.text, + type: 'text', + align: 'left', + value: 'body', + compute: function(mail, td, tr) { + td.innerText = mail.body; + }, + sortFunc: function(a, b, col) { + return $WH.strcmp(a.body, b.body); + }, + getVisibleText: function(mail) { + return mail.body; + } + }, + { + id: 'attachments', + name: 'Attachments', + type: 'text', + compute: function(mail, td) { + if (!mail.attachments.length) + return; + + mail.attachments.forEach(function(item, idx, arr) { + if (g_items && g_items[item]) { + i = Icon.create(g_items[item].icon, 0, false, '?item=' + item, 0, 0, false, false, true); + if (idx !== arr.length - 1) + i.style.paddingLeft = '5px'; + $WH.ae(td, i); + } + }); + }, + getVisibleText: function(mail) { + if (!mail.attachments.length) + return null; // no attachments + + var itemId = $(mail.attachments).first()[0]; + if (g_items && g_items[itemId]) + return g_items[itemId]['name_' + Locale.getName()]; + + return ''; // unk item + }, + sortFunc: function(a, b, col) { + return $WH.strcmp(this.getVisibleText(a), this.getVisibleText(b)); + } + } + ], + getItemLink: function(mail) { + return '?mail=' + mail.id; + }, + onBeforeCreate : function() { + // hide duplicate id col + if (this.debug || g_user?.debug) { + let colId = this.columns.findIndex(x => x.id == 'id'); + this.visibility = this.visibility.filter(x => x != colId); + } + } +} diff --git a/template/listviews/membersCol.tpl.php b/template/listviews/membersCol.tpl similarity index 100% rename from template/listviews/membersCol.tpl.php rename to template/listviews/membersCol.tpl diff --git a/template/listviews/npcRepCol.tpl.php b/template/listviews/npcRepCol.tpl similarity index 100% rename from template/listviews/npcRepCol.tpl.php rename to template/listviews/npcRepCol.tpl diff --git a/template/listviews/petFoodCol.tpl.php b/template/listviews/petFoodCol.tpl similarity index 100% rename from template/listviews/petFoodCol.tpl.php rename to template/listviews/petFoodCol.tpl diff --git a/template/listviews/questRepCol.tpl.php b/template/listviews/questRepCol.tpl similarity index 100% rename from template/listviews/questRepCol.tpl.php rename to template/listviews/questRepCol.tpl diff --git a/template/listviews/vendorRestockCol.tpl b/template/listviews/vendorRestockCol.tpl new file mode 100644 index 00000000..11e17540 --- /dev/null +++ b/template/listviews/vendorRestockCol.tpl @@ -0,0 +1,14 @@ +var _ = { + id: 'restock', + name: LANG.restock, + width: '10%', + value: 'restock', + after: 'stack', + compute: function(data, td) { + if (data.restock) { + let t = g_formatTimeElapsed(data.restock); + + $WH.ae(td, $WH.ct(t)); + } + } +}; diff --git a/template/localized/confirm-delete-account_0.tpl.php b/template/localized/confirm-delete-account_0.tpl.php new file mode 100644 index 00000000..15a0ea92 --- /dev/null +++ b/template/localized/confirm-delete-account_0.tpl.php @@ -0,0 +1,32 @@ + + + diff --git a/template/localized/confirm-delete-account_2.tpl.php b/template/localized/confirm-delete-account_2.tpl.php new file mode 100644 index 00000000..71227e07 --- /dev/null +++ b/template/localized/confirm-delete-account_2.tpl.php @@ -0,0 +1,32 @@ + + + diff --git a/template/localized/confirm-delete-account_3.tpl.php b/template/localized/confirm-delete-account_3.tpl.php new file mode 100644 index 00000000..651b728a --- /dev/null +++ b/template/localized/confirm-delete-account_3.tpl.php @@ -0,0 +1,32 @@ + + + diff --git a/template/localized/confirm-delete-account_4.tpl.php b/template/localized/confirm-delete-account_4.tpl.php new file mode 100644 index 00000000..6eae62da --- /dev/null +++ b/template/localized/confirm-delete-account_4.tpl.php @@ -0,0 +1,32 @@ + + + diff --git a/template/localized/confirm-delete-account_6.tpl.php b/template/localized/confirm-delete-account_6.tpl.php new file mode 100644 index 00000000..0587085a --- /dev/null +++ b/template/localized/confirm-delete-account_6.tpl.php @@ -0,0 +1,32 @@ + + + diff --git a/template/localized/confirm-delete-account_8.tpl.php b/template/localized/confirm-delete-account_8.tpl.php new file mode 100644 index 00000000..d9b9a0b1 --- /dev/null +++ b/template/localized/confirm-delete-account_8.tpl.php @@ -0,0 +1,32 @@ + + + diff --git a/template/localized/consent_0.tpl.php b/template/localized/consent_0.tpl.php new file mode 100644 index 00000000..c5e2b3ed --- /dev/null +++ b/template/localized/consent_0.tpl.php @@ -0,0 +1,25 @@ + diff --git a/template/localized/consent_2.tpl.php b/template/localized/consent_2.tpl.php new file mode 100644 index 00000000..6d02f5bf --- /dev/null +++ b/template/localized/consent_2.tpl.php @@ -0,0 +1,25 @@ + diff --git a/template/localized/consent_3.tpl.php b/template/localized/consent_3.tpl.php new file mode 100644 index 00000000..88d31de2 --- /dev/null +++ b/template/localized/consent_3.tpl.php @@ -0,0 +1,25 @@ + diff --git a/template/localized/consent_4.tpl.php b/template/localized/consent_4.tpl.php new file mode 100644 index 00000000..aa455978 --- /dev/null +++ b/template/localized/consent_4.tpl.php @@ -0,0 +1,25 @@ + diff --git a/template/localized/consent_6.tpl.php b/template/localized/consent_6.tpl.php new file mode 100644 index 00000000..0c0e4674 --- /dev/null +++ b/template/localized/consent_6.tpl.php @@ -0,0 +1,25 @@ + diff --git a/template/localized/consent_8.tpl.php b/template/localized/consent_8.tpl.php new file mode 100644 index 00000000..b99ba519 --- /dev/null +++ b/template/localized/consent_8.tpl.php @@ -0,0 +1,25 @@ + diff --git a/template/localized/contrib_0.tpl.php b/template/localized/contrib_0.tpl.php index 45e05530..ede60c11 100644 --- a/template/localized/contrib_0.tpl.php +++ b/template/localized/contrib_0.tpl.php @@ -1,3 +1,9 @@ + + '.PHP_EOL; + echo '
'.$coError.'
'.PHP_EOL; + endif; + + if ($this->user::canComment()): ?> -
+ +
+ +
+ user::isLoggedIn()): ?> + You are not logged in. Please log in or register an account to add your comment. + +
'.PHP_EOL; + echo '
'.$ssError.'
'.PHP_EOL; + endif; + + if ($this->user::canUploadScreenshot()): ?> -
+ +
Note: Your Screenshot will need to be approved before appearing on the site. + +
+ user::isLoggedIn()): ?> + You are not signed in. Please sign in to submit a screenshot. + +
'.PHP_EOL; + echo '
'.$viError.'
'.PHP_EOL; + endif; + + if ($this->user::canSuggestVideo()): ?> +
-
+ Supported: YouTube only
Note: Your video will need to be approved before appearing on the site. + +
+ user::isLoggedIn()): ?> + You are not signed in. Please sign in to submit a video. + +
diff --git a/template/localized/contrib_2.tpl.php b/template/localized/contrib_2.tpl.php index 51655aaa..80eebf38 100644 --- a/template/localized/contrib_2.tpl.php +++ b/template/localized/contrib_2.tpl.php @@ -1,3 +1,9 @@ + + '.PHP_EOL; + echo '
'.$coError.'
'.PHP_EOL; + endif; + + if ($this->user::canComment()): ?> -
+ +
+ +
+ user::isLoggedIn()): ?> + Vous n'êtes pas connecté(e). Veuillez vous connecter ou vous inscrire pour ajouter votre commentaire. + +
'.PHP_EOL; + echo '
'.$ssError.'
'.PHP_EOL; + endif; + + if ($this->user::canUploadScreenshot()): ?> -
+ +
Note: Votre capture d'écran devra être approuvé avant d'apparaitre sur le site. + +
+ user::isLoggedIn()): ?> + Vous n'êtes pas connecté(e). Veuillez vous connecter pour envoyer une capture d'écran. + +
'.PHP_EOL; + echo '
'.$viError.'
'.PHP_EOL; + endif; + + if ($this->user::canSuggestVideo()): ?> +
-
+ Supporté : Youtube seulement
Note: Votre vidéo devra être approuvé avant d'apparaitre sur le site. + +
+ user::isLoggedIn()): ?> + Vous n'êtes pas connecté(e). Veuillez vous connecter pour envoyer une vidéo. + +
diff --git a/template/localized/contrib_3.tpl.php b/template/localized/contrib_3.tpl.php index fcff50d8..f9d4933a 100644 --- a/template/localized/contrib_3.tpl.php +++ b/template/localized/contrib_3.tpl.php @@ -1,3 +1,9 @@ + + '.PHP_EOL; + echo '
'.$coError.'
'.PHP_EOL; + endif; + + if ($this->user::canComment()): ?> -
+ +
+ +
+ user::isLoggedIn()): ?> + Ihr seid nicht angemeldet. Bitte meldet Euch an, oder registriert Euch, um einen Kommentar einzusenden. + +
'.PHP_EOL; + echo '
'.$ssError.'
'.PHP_EOL; + endif; + + if ($this->user::canUploadScreenshot()): ?> -
+ +
Hinweis: Euer Screenshot muss zunächst zugelassen werden, bevor er auf der Seite erscheint. + +
+ user::isLoggedIn()): ?> + Ihr seid nicht angemeldet. Bitte meldet Euch an, um einen Screenshot einzusenden. + +
'.PHP_EOL; + echo '
'.$viError.'
'.PHP_EOL; + endif; + + if ($this->user::canSuggestVideo()): ?> +
-
+ Unterstützt: nur YouTube
Hinweis: Euer Video muss zunächst zugelassen werden, bevor es auf der Seite erscheint. + +
+ user::isLoggedIn()): ?> + Ihr seid nicht angemeldet. Bitte meldet Euch an, um ein Video vorzuschlagen. + +
diff --git a/template/localized/contrib_4.tpl.php b/template/localized/contrib_4.tpl.php new file mode 100644 index 00000000..e96ee20e --- /dev/null +++ b/template/localized/contrib_4.tpl.php @@ -0,0 +1,133 @@ + + + '.PHP_EOL; + echo '
'.$coError.'
'.PHP_EOL; + endif; + + if ($this->user::canComment()): +?> + +
+
+ +
+ + + + + +
+ +user::isLoggedIn()): +?> + + 你尚未登录,请先登录注册一个账号 以发表你的评论。 + + + +
+ + '.PHP_EOL; + echo '
'.$ssError.'
'.PHP_EOL; + endif; + + if ($this->user::canUploadScreenshot()): +?> + +
+
+
+ +
+ 注意:你的截图将在审查后才会出现在站点上。 + + + + +
+ +user::isLoggedIn()): +?> + + 你尚未登录,请先登录以提交截图。 + + + +
+ + '.PHP_EOL; + echo '
'.$viError.'
'.PHP_EOL; + endif; + + if ($this->user::canSuggestVideo()): +?> + +
+
+ 支持:仅限 YouTube +
+ +
+ 说明:您的视频需通过审核才能在站点上显示。 + + + + +
+ +user::isLoggedIn()): +?> + + 你尚未登录,请先登录以提交视频。 + + + +
+ diff --git a/template/localized/contrib_6.tpl.php b/template/localized/contrib_6.tpl.php index 313bba38..91a579f0 100644 --- a/template/localized/contrib_6.tpl.php +++ b/template/localized/contrib_6.tpl.php @@ -1,3 +1,9 @@ + + '.PHP_EOL; + echo '
'.$coError.'
'.PHP_EOL; + endif; + + if ($this->user::canComment()): ?> -
+ +
+ +
+ user::isLoggedIn()): ?> + No has iniciado sesión. Por favor entra a tu cuenta o registra una cuenta para añadir tu comentario. + +
'.PHP_EOL; + echo '
'.$ssError.'
'.PHP_EOL; + endif; + + if ($this->user::canUploadScreenshot()): ?> -
+ +
Nota: Su captura de imagen deberá ser aprobado antes de aparecer en el sitio. + +
+ user::isLoggedIn()): ?> + No has iniciado sesión. Inicia sesión para enviar una captura de pantalla. + +
'.PHP_EOL; + echo '
'.$viError.'
'.PHP_EOL; + endif; + + if ($this->user::canSuggestVideo()): ?> +
-
+ Soportado: Sólo YouTube
Nota: Tu vídeo deberá ser aprobado antes de aparecer en el sitio. + +
+ user::isLoggedIn()): ?> + No has iniciado sesión. Inicia sesión para enviar un video. + +
diff --git a/template/localized/contrib_8.tpl.php b/template/localized/contrib_8.tpl.php index ae9e0ef4..6a30596c 100644 --- a/template/localized/contrib_8.tpl.php +++ b/template/localized/contrib_8.tpl.php @@ -1,3 +1,9 @@ + + '.PHP_EOL; + echo '
'.$coError.'
'.PHP_EOL; + endif; + + if ($this->user::canComment()): ?> -
+ +
+ +
+ user::isLoggedIn()): ?> - YВы не вошли на сайт. Пожалуйста войдите или зарегистрируйтесь, чтобы добавлять комментарии. + + Вы не вошли на сайт. Пожалуйста войдите или зарегистрируйтесь, чтобы добавлять комментарии. + +
'.PHP_EOL; + echo '
'.$ssError.'
'.PHP_EOL; + endif; + + if ($this->user::canUploadScreenshot()): ?> -
+ +
Примечание: перед тем как появиться на сайте, ваше Скриншот должны быть утверждены. + +
+ user::isLoggedIn()): ?> + Вы не вошли на сайт. Пожалуйста войдите, чтобы отправить скриншот. + +
'.PHP_EOL; + echo '
'.$viError.'
'.PHP_EOL; + endif; + + if ($this->user::canSuggestVideo()): ?> +
-
+ Поддерживается: только YouTube
Примечание: перед тем как появиться на сайте, ваше видео должно быть одобрено. + +
+ user::isLoggedIn()): ?> + Вы не вошли на сайт. Пожалуйста войдите, чтобы отправить видео. + +
diff --git a/template/localized/delete-account_0.tpl.php b/template/localized/delete-account_0.tpl.php new file mode 100644 index 00000000..bfef2a6f --- /dev/null +++ b/template/localized/delete-account_0.tpl.php @@ -0,0 +1,21 @@ + + + diff --git a/template/localized/delete-account_2.tpl.php b/template/localized/delete-account_2.tpl.php new file mode 100644 index 00000000..db677dc0 --- /dev/null +++ b/template/localized/delete-account_2.tpl.php @@ -0,0 +1,21 @@ + + + diff --git a/template/localized/delete-account_3.tpl.php b/template/localized/delete-account_3.tpl.php new file mode 100644 index 00000000..a7585340 --- /dev/null +++ b/template/localized/delete-account_3.tpl.php @@ -0,0 +1,21 @@ + + + diff --git a/template/localized/delete-account_4.tpl.php b/template/localized/delete-account_4.tpl.php new file mode 100644 index 00000000..d1c39332 --- /dev/null +++ b/template/localized/delete-account_4.tpl.php @@ -0,0 +1,21 @@ + + + diff --git a/template/localized/delete-account_6.tpl.php b/template/localized/delete-account_6.tpl.php new file mode 100644 index 00000000..8a1fd5c0 --- /dev/null +++ b/template/localized/delete-account_6.tpl.php @@ -0,0 +1,21 @@ + + + diff --git a/template/localized/delete-account_8.tpl.php b/template/localized/delete-account_8.tpl.php new file mode 100644 index 00000000..5864fdbd --- /dev/null +++ b/template/localized/delete-account_8.tpl.php @@ -0,0 +1,21 @@ + + + diff --git a/template/localized/ssReminder_0.tpl.php b/template/localized/ssReminder_0.tpl.php index 52bf538a..c94c311a 100644 --- a/template/localized/ssReminder_0.tpl.php +++ b/template/localized/ssReminder_0.tpl.php @@ -1,4 +1,10 @@ -

Reminder

+ + +

Reminder

Your screenshot will not be approved if it doesn't correspond to the following guidelines.
    diff --git a/template/localized/ssReminder_2.tpl.php b/template/localized/ssReminder_2.tpl.php new file mode 100644 index 00000000..be667edf --- /dev/null +++ b/template/localized/ssReminder_2.tpl.php @@ -0,0 +1,16 @@ + + +

    Rappel

    + Votre capture d'écran ne sera pas approuvée si elle ne respecte pas les directives suivantes. + +
      +
    • Assurez-vous d'augmenter vos paramètres graphiques pour que la capture soit de bonne qualité !
    • +
    • Les captures d'écran du visualiseur de modèles sont supprimées immédiatement (cela inclut généralement la sélection de personnage).
    • +
    • N'incluez pas le texte à l'écran ni le cercle de sélection d'un PNJ.
    • +
    • N'incluez aucune interface utilisateur dans la capture si possible.
    • +
    • Utilisez l'outil de recadrage pour vous concentrer autant que possible sur l'objet et réduire l'environnement inutile, afin de mieux mettre en valeur l'objet sur la vignette qui apparaîtra sur la page de l'objet.
    • +
    diff --git a/template/localized/ssReminder_3.tpl.php b/template/localized/ssReminder_3.tpl.php index 8c1342a5..953d2eea 100644 --- a/template/localized/ssReminder_3.tpl.php +++ b/template/localized/ssReminder_3.tpl.php @@ -1,4 +1,10 @@ -

    Hinweis

    + + +

    Hinweis

    Euer Screenshot wird nicht zugelassen werden, wenn er nicht unseren Richtlinien entspricht.
      diff --git a/template/localized/ssReminder_4.tpl.php b/template/localized/ssReminder_4.tpl.php new file mode 100644 index 00000000..9f7c5bd7 --- /dev/null +++ b/template/localized/ssReminder_4.tpl.php @@ -0,0 +1,16 @@ + + +

      提醒

      + 如果您的截图不符合以下指南,将不会被通过审核。 + +
        +
      • 请确保将您的图形设置调高,以保证截图质量!
      • +
      • 使用模型查看器的截图会被直接删除(包括角色选择界面)。
      • +
      • 请勿包含NPC的屏幕文字选择圈
      • +
      • 如有可能,请勿在截图中包含任何用户界面元素。
      • +
      • 请使用截图裁剪工具尽量聚焦于物品本身,减少不必要的背景,以便在物品页面的缩略图中更好地展示该物品。
      • +
      diff --git a/template/localized/ssReminder_6.tpl.php b/template/localized/ssReminder_6.tpl.php new file mode 100644 index 00000000..e7b3b2b9 --- /dev/null +++ b/template/localized/ssReminder_6.tpl.php @@ -0,0 +1,16 @@ + + +

      Recordatorio

      + Su captura de pantalla no será aprobada si no cumple con las siguientes directrices. + +
        +
      • ¡Asegúrese de subir la configuración gráfica para que la imagen se vea bien!
      • +
      • Las capturas de model viewer se eliminan automáticamente (esto también incluye la selección de personaje, normalmente).
      • +
      • No incluya el texto en pantalla ni el círculo de selección de un PNJ.
      • +
      • No incluya ninguna interfaz de usuario en la imagen si puede evitarlo.
      • +
      • Utilice la herramienta de recorte de capturas para centrarse en el objeto tanto como sea posible y reducir el entorno innecesario, para mostrar mejor el objeto en la miniatura que aparecerá en la página del objeto.
      • +
      diff --git a/template/localized/ssReminder_8.tpl.php b/template/localized/ssReminder_8.tpl.php new file mode 100644 index 00000000..81b43f3c --- /dev/null +++ b/template/localized/ssReminder_8.tpl.php @@ -0,0 +1,16 @@ + + +

      Напоминание

      + Ваш скриншот не будет одобрен, если он не соответствует следующим рекомендациям. + +
        +
      • Обязательно увеличьте свои настройки графики, чтобы скриншот выглядел хорошо!
      • +
      • Скриншоты из просмотрщика моделей удаляются сразу (это также касается выбора персонажа).
      • +
      • Не включайте текст на экране и круг выделения NPC.
      • +
      • Не включайте никакой интерфейс пользователя в скриншот, если это возможно.
      • +
      • Используйте инструмент обрезки скриншотов, чтобы максимально сфокусироваться на предмете и уменьшить ненужное окружение, чтобы лучше показать предмет на миниатюре, которая будет на странице предмета.
      • +
      diff --git a/template/mails/activate-account_0.tpl b/template/mails/activate-account_0.tpl new file mode 100644 index 00000000..ad662a74 --- /dev/null +++ b/template/mails/activate-account_0.tpl @@ -0,0 +1,10 @@ +# Created: May 2018 +Account Creation +Hey! + +Thanks a lot for your interest in contributing to the site. + +Just click this link to activate your account: +HOST_URL?account=activate&key=%s + +The NAME_SHORT team diff --git a/template/mails/activate-account_2.tpl b/template/mails/activate-account_2.tpl new file mode 100644 index 00000000..7394b874 --- /dev/null +++ b/template/mails/activate-account_2.tpl @@ -0,0 +1,10 @@ +# Created: May 2018 +Account Creation +Bonjour ! + +Nous vous remercions chaleureusement pour l'intérêt que vous portez à contribuer à notre site. + +Vous n'avez qu'à cliquer sur le lien ci-dessous pour activer votre compte. +HOST_URL?account=activate&key=%s + +L'équipe NAME_SHORT diff --git a/template/mails/activate-account_3.tpl b/template/mails/activate-account_3.tpl new file mode 100644 index 00000000..43be6bcf --- /dev/null +++ b/template/mails/activate-account_3.tpl @@ -0,0 +1,10 @@ +# Created: May 2018 +Kontoerstellung +Hey! + +Vielen Dank für Euer Interesse an der Mitwirkung bei unserer Webseite. + +Klickt einfach auf den folgenden Link, um Euer Konto zu aktivieren: +HOST_URL?account=activate&key=%s + +Das Team von NAME_SHORT diff --git a/template/mails/activate-account_4.tpl b/template/mails/activate-account_4.tpl new file mode 100644 index 00000000..c0086607 --- /dev/null +++ b/template/mails/activate-account_4.tpl @@ -0,0 +1,10 @@ +# Created by ChatGPT from May 2018 base; locale 0 +账户创建 +你好! + +非常感谢你有兴趣为本站做出贡献。 + +只需点击此链接激活你的账户: +HOST_URL?account=activate&key=%s + +NAME_SHORT 团队敬上 diff --git a/template/mails/activate-account_6.tpl b/template/mails/activate-account_6.tpl new file mode 100644 index 00000000..0c669570 --- /dev/null +++ b/template/mails/activate-account_6.tpl @@ -0,0 +1,10 @@ +# Created by ChatGPT from May 2018 base; locale 0 +Creación de cuenta +¡Hola! + +Muchas gracias por tu interés en contribuir al sitio. + +Simplemente haz clic en este enlace para activar tu cuenta: +HOST_URL?account=activate&key=%s + +El equipo de NAME_SHORT diff --git a/template/mails/activate-account_8.tpl b/template/mails/activate-account_8.tpl new file mode 100644 index 00000000..1a39e951 --- /dev/null +++ b/template/mails/activate-account_8.tpl @@ -0,0 +1,10 @@ +# Created: May 2018 +Account Creation +Привет! + +Благодарим за ваш интерес по наполнению сайта. + +Для активации вашей учетной записи просто кликните по этой ссылке: +HOST_URL?account=activate&key=%s + +Команда NAME_SHORT. diff --git a/template/mails/change-email_0.tpl b/template/mails/change-email_0.tpl new file mode 100644 index 00000000..58a92f10 --- /dev/null +++ b/template/mails/change-email_0.tpl @@ -0,0 +1,11 @@ +# Created: 2025 +Email Change Confirm +Greetings, + +We received a request to change your account's email address. If you made this request, please follow the link below to confirm the change. + +HOST_URL?account=confirm-email-address&key=%1$s + +If you didn't request this change please feel free to disregard this email. If the link did not work or you have any further concerns about this, please contact CONTACT_EMAIL. The link will become invalid %10$s after this email was sent. + +The NAME_SHORT team diff --git a/template/mails/change-email_2.tpl b/template/mails/change-email_2.tpl new file mode 100644 index 00000000..c9743068 --- /dev/null +++ b/template/mails/change-email_2.tpl @@ -0,0 +1,11 @@ +# GPTed from 2025 source +Demande de confirmation de changement d'adresse e-mail +Bonjour, + +Nous avons reçu une demande de modification de l'adresse e-mail associée à votre compte. Si vous êtes à l'origine de cette demande, veuillez suivre le lien ci-dessous pour confirmer le changement. + +HOST_URL?account=confirm-email-address&key=%1$s + +Si vous n'avez pas demandé ce changement, vous pouvez ignorer cet e-mail. Si le lien ne fonctionne pas ou si vous avez d'autres préoccupations à ce sujet, veuillez contacter CONTACT_EMAIL. Ce lien deviendra invalide %10$s après l'envoi de cet e-mail. + +L'équipe NAME_SHORT diff --git a/template/mails/change-email_3.tpl b/template/mails/change-email_3.tpl new file mode 100644 index 00000000..8abae027 --- /dev/null +++ b/template/mails/change-email_3.tpl @@ -0,0 +1,11 @@ +# GPTed from 2025 source +Bestätigung der E-Mail-Änderung angefordert +Hallo, + +Wir haben eine Anfrage zur Änderung Ihrer E-Mail-Adresse erhalten. Wenn Sie diese Anfrage gestellt haben, folgen Sie bitte dem untenstehenden Link, um die Änderung zu bestätigen. + +HOST_URL?account=confirm-email-address&key=%1$s + +Falls Sie diese Änderung nicht angefordert haben, können Sie diese E-Mail ignorieren. Falls der Link nicht funktioniert oder Sie weitere Fragen haben, wenden Sie sich bitte an CONTACT_EMAIL. Der Link wird %10$s nach Versand dieser E-Mail ungültig. + +Das Team von NAME_SHORT diff --git a/template/mails/change-email_4.tpl b/template/mails/change-email_4.tpl new file mode 100644 index 00000000..9fbd9efa --- /dev/null +++ b/template/mails/change-email_4.tpl @@ -0,0 +1,11 @@ +# GPTed from 2025 source +确认更改电子邮件地址 +您好, + +我们收到了一项更改您账户电子邮件地址的请求。如果是您本人操作,请点击下方链接以确认更改。 + +HOST_URL?account=confirm-email-address&key=%1$s + +如果您未曾发起此更改,请忽略此邮件。如果链接无法使用或您对此有任何疑问,请联系 CONTACT_EMAIL。此链接将在本邮件发送后 %10$s 失效。 + +NAME_SHORT 团队敬上 diff --git a/template/mails/change-email_6.tpl b/template/mails/change-email_6.tpl new file mode 100644 index 00000000..16aabc7b --- /dev/null +++ b/template/mails/change-email_6.tpl @@ -0,0 +1,11 @@ +# GPTed from 2025 source +Confirmación de cambio de correo electrónico +Saludos, + +Hemos recibido una solicitud para cambiar la dirección de correo electrónico de su cuenta. Si usted realizó esta solicitud, siga el enlace de abajo para confirmar el cambio. + +HOST_URL?account=confirm-email-address&key=%1$s + +Si usted no solicitó este cambio, puede ignorar este correo. Si el enlace no funciona o tiene alguna inquietud, por favor contacte a CONTACT_EMAIL. El enlace se invalidará %10$s después de que este correo haya sido enviado. + +El equipo de NAME_SHORT diff --git a/template/mails/change-email_8.tpl b/template/mails/change-email_8.tpl new file mode 100644 index 00000000..aaeeed88 --- /dev/null +++ b/template/mails/change-email_8.tpl @@ -0,0 +1,12 @@ +# GPTed from 2025 source +Подтверждение изменения адреса электронной почты +Здравствуйте, + +Мы получили запрос на изменение адреса электронной почты, связанного с вашим аккаунтом. Если вы отправили этот запрос, пожалуйста, перейдите по ссылке ниже для подтверждения изменения. + +HOST_URL?account=confirm-email-address&key=%1$s + +Если вы не запрашивали это изменение, просто проигнорируйте это письмо. Если ссылка не работает или у вас есть дополнительные вопросы, пожалуйста, свяжитесь с CONTACT_EMAIL. Ссылка станет недействительной через %10$s после отправки этого письма. + +Команда NAME_SHORT +Пожалуйста, перейдите по ссылке ниже, чтобы подтвердить ваш новый адрес электронной почты. diff --git a/template/mails/delete-account_0.tpl b/template/mails/delete-account_0.tpl new file mode 100644 index 00000000..7bbef9db --- /dev/null +++ b/template/mails/delete-account_0.tpl @@ -0,0 +1,21 @@ +# 2025 +Please verify your request to be forgotten +Greetings, + +We’ve just received a request to exercise the “right to be forgotten” from the following email address %2$s in accordance with our Privacy Policy. + +Please click on following link HOST_URL?account=confirm-delete&key=%1$s to confirm your selection. You will get one last chance to review your choices once you are back on the site. + +Should you choose to proceed with this process, we will permanently delete or anonymize any Personal Data linked to your account. + +This information will include, but is not limited to: + + * Your Identity %3$s, and the email address associated with this login. + * Your current Premium status and data, should you be a Premium member. + * Your profile information and preferences. + * In some cases, content that you've authored, including comments, guides and forum posts. + * Note that game data connected to your gaming identities will re-appear when other users request data updates, unless you delete that data at the source. + +Once we receive your final confirmation, we will be removing your Personal Data. + +If you have any questions or need further assistance, please contact CONTACT_EMAIL. diff --git a/template/mails/delete-account_2.tpl b/template/mails/delete-account_2.tpl new file mode 100644 index 00000000..7ccda5d2 --- /dev/null +++ b/template/mails/delete-account_2.tpl @@ -0,0 +1,21 @@ +# GPTed from 2025 source +Veuillez vérifier votre demande de droit à l'oubli +Bonjour, + +Nous venons de recevoir une demande d'exercice du « droit à l'oubli » de l'adresse e-mail suivante %2$s conformément à notre politique de confidentialité. + +Veuillez cliquer sur le lien suivant HOST_URL?account=confirm-delete&key=%1$s pour confirmer votre choix. Vous aurez une dernière chance de revoir vos choix une fois de retour sur le site. + +Si vous choisissez de poursuivre ce processus, nous supprimerons ou anonymiserons définitivement toutes les données personnelles liées à votre compte. + +Ces informations incluront, sans s'y limiter : + + * Votre identité %3$s, et l'adresse e-mail associée à cette connexion. + * Votre statut Premium actuel et les données, si vous êtes membre Premium. + * Vos informations de profil et préférences. + * Dans certains cas, le contenu que vous avez créé, y compris les commentaires, guides et messages sur le forum. + * Notez que les données de jeu liées à vos identités de jeu réapparaîtront lorsque d'autres utilisateurs demanderont des mises à jour de données, sauf si vous supprimez ces données à la source. + +Une fois que nous aurons reçu votre confirmation finale, nous supprimerons vos données personnelles. + +Si vous avez des questions ou besoin d'aide supplémentaire, veuillez contacter CONTACT_EMAIL. diff --git a/template/mails/delete-account_3.tpl b/template/mails/delete-account_3.tpl new file mode 100644 index 00000000..b8ce03a4 --- /dev/null +++ b/template/mails/delete-account_3.tpl @@ -0,0 +1,21 @@ +# GPTed from 2025 source +Bitte bestätigen Sie Ihre Anfrage auf Vergessenwerden +Hallo, + +Wir haben gerade eine Anfrage zum "Recht auf Vergessenwerden" von der folgenden E-Mail-Adresse %2$s gemäß unserer Datenschutzrichtlinie erhalten. + +Bitte klicken Sie auf den folgenden Link HOST_URL?account=confirm-delete&key=%1$s, um Ihre Auswahl zu bestätigen. Sie erhalten eine letzte Gelegenheit, Ihre Auswahl zu überprüfen, sobald Sie wieder auf der Website sind. + +Wenn Sie sich entscheiden, diesen Prozess fortzusetzen, werden wir alle mit Ihrem Konto verknüpften personenbezogenen Daten dauerhaft löschen oder anonymisieren. + +Diese Informationen umfassen unter anderem: + + * Ihre Identität %3$s und die mit diesem Login verknüpfte E-Mail-Adresse. + * Ihren aktuellen Premium-Status und Daten, falls Sie ein Premium-Mitglied sind. + * Ihre Profilinformationen und Präferenzen. + * In einigen Fällen von Ihnen erstellte Inhalte, einschließlich Kommentare, Guides und Forenbeiträge. + * Beachten Sie, dass Spieldaten, die mit Ihren Spielidentitäten verbunden sind, wieder erscheinen, wenn andere Nutzer Datenaktualisierungen anfordern, es sei denn, Sie löschen diese Daten an der Quelle. + +Sobald wir Ihre endgültige Bestätigung erhalten haben, werden wir Ihre personenbezogenen Daten entfernen. + +Wenn Sie Fragen haben oder weitere Unterstützung benötigen, kontaktieren Sie bitte CONTACT_EMAIL. diff --git a/template/mails/delete-account_4.tpl b/template/mails/delete-account_4.tpl new file mode 100644 index 00000000..a1738dfe --- /dev/null +++ b/template/mails/delete-account_4.tpl @@ -0,0 +1,21 @@ +# GPTed from 2025 source +请验证您的被遗忘权请求 +您好, + +我们刚刚收到来自以下电子邮件地址 %2$s 的“被遗忘权”请求,依据我们的隐私政策。 + +请点击以下链接 HOST_URL?account=confirm-delete&key=%1$s 以确认您的选择。返回网站后,您将有最后一次机会审查您的选择。 + +如果您选择继续此流程,我们将永久删除或匿名化与您的账户相关的所有个人数据。 + +这些信息包括但不限于: + + * 您的身份 %3$s,以及与此登录关联的电子邮件地址。 + * 您当前的高级会员状态和数据(如适用)。 + * 您的个人资料信息和偏好设置。 + * 在某些情况下,您创作的内容,包括评论、指南和论坛帖子。 + * 请注意,与您的游戏身份相关的游戏数据在其他用户请求数据更新时会重新出现,除非您在源头删除这些数据。 + +一旦我们收到您的最终确认,我们将删除您的个人数据。 + +如有任何疑问或需要进一步帮助,请联系 CONTACT_EMAIL。 diff --git a/template/mails/delete-account_6.tpl b/template/mails/delete-account_6.tpl new file mode 100644 index 00000000..421351f6 --- /dev/null +++ b/template/mails/delete-account_6.tpl @@ -0,0 +1,21 @@ +# GPTed from 2025 source +Por favor, verifique su solicitud de derecho al olvido +Saludos, + +Acabamos de recibir una solicitud para ejercer el "derecho al olvido" desde la siguiente dirección de correo electrónico %2$s de acuerdo con nuestra Política de Privacidad. + +Por favor, haga clic en el siguiente enlace HOST_URL?account=confirm-delete&key=%1$s para confirmar su selección. Tendrá una última oportunidad de revisar sus opciones una vez que regrese al sitio. + +Si decide continuar con este proceso, eliminaremos o anonimizaremos permanentemente cualquier dato personal vinculado a su cuenta. + +Esta información incluirá, pero no se limitará a: + + * Su identidad %3$s y la dirección de correo electrónico asociada a este inicio de sesión. + * Su estado Premium actual y datos, si es miembro Premium. + * Su información de perfil y preferencias. + * En algunos casos, contenido que haya creado, incluyendo comentarios, guías y publicaciones en foros. + * Tenga en cuenta que los datos de juego conectados a sus identidades de juego volverán a aparecer cuando otros usuarios soliciten actualizaciones de datos, a menos que elimine esos datos en la fuente. + +Una vez que recibamos su confirmación final, eliminaremos sus datos personales. + +Si tiene alguna pregunta o necesita más ayuda, por favor contacte a CONTACT_EMAIL. diff --git a/template/mails/delete-account_8.tpl b/template/mails/delete-account_8.tpl new file mode 100644 index 00000000..f9f916c4 --- /dev/null +++ b/template/mails/delete-account_8.tpl @@ -0,0 +1,21 @@ +# GPTed from 2025 source +Пожалуйста, подтвердите ваш запрос на удаление данных +Здравствуйте, + +Мы только что получили запрос на реализацию "права быть забытым" с адреса электронной почты %2$s в соответствии с нашей Политикой конфиденциальности. + +Пожалуйста, перейдите по следующей ссылке HOST_URL?account=confirm-delete&key=%1$s, чтобы подтвердить свой выбор. После возвращения на сайт у вас будет последний шанс пересмотреть свое решение. + +Если вы решите продолжить процесс, мы навсегда удалим или анонимизируем все персональные данные, связанные с вашей учетной записью. + +Эта информация будет включать, но не ограничиваться: + + * Вашу личность %3$s и адрес электронной почты, связанный с этим входом. + * Ваш текущий статус и данные Premium, если вы являетесь Premium-участником. + * Вашу информацию профиля и предпочтения. + * В некоторых случаях созданный вами контент, включая комментарии, руководства и сообщения на форуме. + * Обратите внимание, что игровые данные, связанные с вашими игровыми идентификаторами, появятся снова, когда другие пользователи запросят обновление данных, если только вы не удалите эти данные у источника. + +После получения вашего окончательного подтверждения мы удалим ваши персональные данные. + +Если у вас есть вопросы или вам нужна дополнительная помощь, пожалуйста, свяжитесь с CONTACT_EMAIL. diff --git a/template/mails/recover-user_0.tpl b/template/mails/recover-user_0.tpl new file mode 100644 index 00000000..9ab6d562 --- /dev/null +++ b/template/mails/recover-user_0.tpl @@ -0,0 +1,7 @@ +# Legacy import +User Recovery +Follow this link to log in. + +HOST_URL?account=signin&key=%s + +If you did not request this mail simply ignore it. diff --git a/template/mails/recover-user_2.tpl b/template/mails/recover-user_2.tpl new file mode 100644 index 00000000..6fd398ad --- /dev/null +++ b/template/mails/recover-user_2.tpl @@ -0,0 +1,7 @@ +# Legacy import +Récupération d'utilisateur +Suivez ce lien pour vous connecter. + +HOST_URL?account=signin&key=%s + +Si vous n'avez pas demandé cet e-mail, ignorez le. diff --git a/template/mails/recover-user_3.tpl b/template/mails/recover-user_3.tpl new file mode 100644 index 00000000..83046bb5 --- /dev/null +++ b/template/mails/recover-user_3.tpl @@ -0,0 +1,7 @@ +# Legacy import +Benutzernamenanfrage +Folgt diesem Link um euch anzumelden. + +HOST_URL?account=signin&key=%s + +Falls Ihr diese Mail nicht angefordert habt kann sie einfach ignoriert werden. diff --git a/template/mails/recover-user_4.tpl b/template/mails/recover-user_4.tpl new file mode 100644 index 00000000..f571deae --- /dev/null +++ b/template/mails/recover-user_4.tpl @@ -0,0 +1,7 @@ +# Legacy import +用户恢复 +请点击此链接登录。 + +HOST_URL?account=signin&key=%s + +如果您没有请求此邮件,请忽略它。 diff --git a/template/mails/recover-user_6.tpl b/template/mails/recover-user_6.tpl new file mode 100644 index 00000000..bf5e173c --- /dev/null +++ b/template/mails/recover-user_6.tpl @@ -0,0 +1,7 @@ +# Legacy import +Recuperacion de Usuario +Siga a este enlace para ingresar. + +HOST_URL?account=signin&key=%s + +Si usted no solicitó este correo, por favor ignorelo. diff --git a/template/mails/recover-user_8.tpl b/template/mails/recover-user_8.tpl new file mode 100644 index 00000000..5847f9ac --- /dev/null +++ b/template/mails/recover-user_8.tpl @@ -0,0 +1,7 @@ +# Created by ChatGPT from legacy import; locale 0 +Восстановление пользователя +Перейдите по этой ссылке, чтобы войти в систему. + +HOST_URL?account=signin&key=%s + +Если вы не запрашивали это письмо, просто проигнорируйте его. diff --git a/template/mails/reset-password_0.tpl b/template/mails/reset-password_0.tpl new file mode 100644 index 00000000..7fe9762b --- /dev/null +++ b/template/mails/reset-password_0.tpl @@ -0,0 +1,7 @@ +# Legacy import +Password Reset +Follow this link to reset your password. + +HOST_URL?account=reset-password&key=%s + +If you did not request this mail simply ignore it. diff --git a/template/mails/reset-password_2.tpl b/template/mails/reset-password_2.tpl new file mode 100644 index 00000000..97a3cd75 --- /dev/null +++ b/template/mails/reset-password_2.tpl @@ -0,0 +1,7 @@ +# Legacy import +Réinitialisation du mot de passe +Suivez ce lien pour réinitialiser votre mot de passe. + +HOST_URL?account=reset-password&key=%s + +Si vous n'avez pas fait de demande de réinitialisation, ignorez cet e-mail. diff --git a/template/mails/reset-password_3.tpl b/template/mails/reset-password_3.tpl new file mode 100644 index 00000000..0cf87890 --- /dev/null +++ b/template/mails/reset-password_3.tpl @@ -0,0 +1,7 @@ +# Legacy import +Kennwortreset +Folgt diesem Link um euer Kennwort zurückzusetzen. + +HOST_URL?account=reset-password&key=%s + +Falls Ihr diese Mail nicht angefordert habt kann sie einfach ignoriert werden. diff --git a/template/mails/reset-password_4.tpl b/template/mails/reset-password_4.tpl new file mode 100644 index 00000000..615da1fb --- /dev/null +++ b/template/mails/reset-password_4.tpl @@ -0,0 +1,7 @@ +# Legacy import +重置密码 +点击此链接以重置您的密码。 + +HOST_URL?account=reset-password&key=%s + +如果您没有请求此邮件,请忽略它。 diff --git a/template/mails/reset-password_6.tpl b/template/mails/reset-password_6.tpl new file mode 100644 index 00000000..83ad31fc --- /dev/null +++ b/template/mails/reset-password_6.tpl @@ -0,0 +1,7 @@ +# Legacy import +Reinicio de Contraseña +Siga este enlace para reiniciar su contraseña. + +HOST_URL?account=reset-password&key=%s + +Si usted no solicitó este correo, por favor ignorelo. diff --git a/template/mails/reset-password_8.tpl b/template/mails/reset-password_8.tpl new file mode 100644 index 00000000..ca1317d8 --- /dev/null +++ b/template/mails/reset-password_8.tpl @@ -0,0 +1,7 @@ +# Created by ChatGPT from legacy import; locale 0 +Сброс пароля +Перейдите по этой ссылке, чтобы сбросить свой пароль. + +HOST_URL?account=reset-password&key=%s + +Если вы не запрашивали это письмо, просто проигнорируйте его. diff --git a/template/mails/revert-email_0.tpl b/template/mails/revert-email_0.tpl new file mode 100644 index 00000000..881c02f9 --- /dev/null +++ b/template/mails/revert-email_0.tpl @@ -0,0 +1,11 @@ +# Created: 2025 +Email Change Requested +Greetings, + +We received a request to change your account's email address. If you made this request, please follow the instructions in the confirmation email sent to the address indicated. If you didn't make such a request, please click the link below to prevent the email from being changed. + +HOST_URL?account=revert-email-address&key=%1$s + +If the link did not work or you have any further concerns about this, please contact CONTACT_EMAIL. This link will automatically become invalid %10$s from now. + +The NAME_SHORT team diff --git a/template/mails/revert-email_2.tpl b/template/mails/revert-email_2.tpl new file mode 100644 index 00000000..b9b37958 --- /dev/null +++ b/template/mails/revert-email_2.tpl @@ -0,0 +1,11 @@ +# GPTed from 2025 source +Demande de modification d'adresse e-mail +Bonjour, + +Nous avons reçu une demande de modification de l'adresse e-mail associée à votre compte. Si vous êtes à l'origine de cette demande, veuillez suivre les instructions contenues dans l'e-mail de confirmation envoyé à l'adresse indiquée. Si vous n'êtes pas à l'origine de cette demande, veuillez cliquer sur le lien ci-dessous pour empêcher la modification de l'adresse e-mail. + +HOST_URL?account=revert-email-address&key=%1$s + +Si le lien ne fonctionne pas ou si vous avez d'autres préoccupations à ce sujet, veuillez contacter CONTACT_EMAIL. Ce lien deviendra automatiquement invalide dans %10$s. + +L'équipe NAME_SHORT diff --git a/template/mails/revert-email_3.tpl b/template/mails/revert-email_3.tpl new file mode 100644 index 00000000..4ccc7af9 --- /dev/null +++ b/template/mails/revert-email_3.tpl @@ -0,0 +1,11 @@ +# GPTed from 2025 source +E-Mail-Änderung angefordert +Hallo, + +Wir haben eine Anfrage zur Änderung Ihrer E-Mail-Adresse erhalten. Wenn Sie diese Anfrage gestellt haben, folgen Sie bitte den Anweisungen in der Bestätigungs-E-Mail, die an die angegebene Adresse gesendet wurde. Falls Sie diese Anfrage nicht gestellt haben, klicken Sie bitte auf den untenstehenden Link, um die Änderung der E-Mail-Adresse zu verhindern. + +HOST_URL?account=revert-email-address&key=%1$s + +Falls der Link nicht funktioniert oder Sie weitere Fragen haben, wenden Sie sich bitte an CONTACT_EMAIL. Dieser Link wird automatisch nach %%10$s ungültig. + +Ihr NAME_SHORT-Team diff --git a/template/mails/revert-email_4.tpl b/template/mails/revert-email_4.tpl new file mode 100644 index 00000000..ecc86e88 --- /dev/null +++ b/template/mails/revert-email_4.tpl @@ -0,0 +1,11 @@ +# GPTed from 2025 source +请求更改电子邮件地址 +您好, + +我们收到了一项更改您账户电子邮件地址的请求。如果是您本人操作,请按照发送到指定地址的确认邮件中的说明进行操作。如果不是您本人操作,请点击下方链接以阻止电子邮件地址的更改。 + +HOST_URL?account=revert-email-address&key=%1$s + +如果链接无法使用或您对此有任何疑问,请联系 CONTACT_EMAIL。此链接将在 %10$s 后自动失效。 + +NAME_SHORT 团队敬上 diff --git a/template/mails/revert-email_6.tpl b/template/mails/revert-email_6.tpl new file mode 100644 index 00000000..2fbf927b --- /dev/null +++ b/template/mails/revert-email_6.tpl @@ -0,0 +1,11 @@ +# GPTed from 2025 source +Solicitud de cambio de correo electrónico +Saludos, + +Hemos recibido una solicitud para cambiar la dirección de correo electrónico de su cuenta. Si usted realizó esta solicitud, siga las instrucciones en el correo de confirmación enviado a la dirección indicada. Si no realizó esta solicitud, haga clic en el enlace de abajo para evitar el cambio de correo electrónico. + +HOST_URL?account=revert-email-address&key=%1$s + +Si el enlace no funciona o tiene alguna inquietud, por favor contacte a CONTACT_EMAIL. Este enlace se invalidará automáticamente en %10$s. + +El equipo de NAME_SHORT diff --git a/template/mails/revert-email_8.tpl b/template/mails/revert-email_8.tpl new file mode 100644 index 00000000..b257ad2a --- /dev/null +++ b/template/mails/revert-email_8.tpl @@ -0,0 +1,11 @@ +# GPTed from 2025 source +Запрос на изменение адреса электронной почты +Здравствуйте, + +Мы получили запрос на изменение адреса электронной почты, связанного с вашим аккаунтом. Если вы отправили этот запрос, пожалуйста, следуйте инструкциям в письме с подтверждением, отправленном на указанный адрес. Если вы не отправляли такой запрос, пожалуйста, перейдите по ссылке ниже, чтобы предотвратить изменение адреса электронной почты. + +HOST_URL?account=revert-email-address&key=%1$s + +Если ссылка не работает или у вас есть дополнительные вопросы, пожалуйста, свяжитесь с CONTACT_EMAIL. Эта ссылка автоматически станет недействительной через %10$s. + +Команда NAME_SHORT diff --git a/template/mails/update-password_0.tpl b/template/mails/update-password_0.tpl new file mode 100644 index 00000000..d55404cb --- /dev/null +++ b/template/mails/update-password_0.tpl @@ -0,0 +1,10 @@ +# Created: May 2025 +Password Confirmation +Hey! + +Please click the link below to confirm your new password. +HOST_URL?account=confirm-password&key=%1$s + +Let us know if you have any problems! + +The NAME_SHORT team diff --git a/template/mails/update-password_2.tpl b/template/mails/update-password_2.tpl new file mode 100644 index 00000000..e971a03a --- /dev/null +++ b/template/mails/update-password_2.tpl @@ -0,0 +1,10 @@ +# Created: May 2025 +Confirmation du mot de passe +Bonjour ! + +Veuillez cliquer sur le lien ci-dessous pour confirmer votre nouveau mot de passe. +HOST_URL?account=confirm-password&key=%1$s + +Faites-nous savoir si vous rencontrez des problèmes ! + +L'équipe NAME_SHORT diff --git a/template/mails/update-password_3.tpl b/template/mails/update-password_3.tpl new file mode 100644 index 00000000..348a7abb --- /dev/null +++ b/template/mails/update-password_3.tpl @@ -0,0 +1,10 @@ +# Created: May 2025 +Passwortbestätigung +Hallo! + +Bitte klicke auf den untenstehenden Link, um dein neues Passwort zu bestätigen. +HOST_URL?account=confirm-password&key=%1$s + +Lass uns wissen, falls du Probleme hast! + +Das NAME_SHORT Team diff --git a/template/mails/update-password_4.tpl b/template/mails/update-password_4.tpl new file mode 100644 index 00000000..affa20f4 --- /dev/null +++ b/template/mails/update-password_4.tpl @@ -0,0 +1,10 @@ +# Created by ChatGPT from May 2025 base; locale 0 +密码确认 +你好! + +请点击下面的链接以确认你的新密码。 +HOST_URL?account=confirm-password&key=%1$s + +如果你有任何问题,请告诉我们! + +NAME_SHORT 团队敬上 diff --git a/template/mails/update-password_6.tpl b/template/mails/update-password_6.tpl new file mode 100644 index 00000000..b46dd849 --- /dev/null +++ b/template/mails/update-password_6.tpl @@ -0,0 +1,10 @@ +# Created: May 2025 +Confirmación de contraseña +¡Hola! + +Por favor, haz clic en el siguiente enlace para confirmar tu nueva contraseña. +HOST_URL?account=confirm-password&key=%1$s + +¡Avísanos si tienes algún problema! + +El equipo de NAME_SHORT diff --git a/template/mails/update-password_8.tpl b/template/mails/update-password_8.tpl new file mode 100644 index 00000000..9266fa1d --- /dev/null +++ b/template/mails/update-password_8.tpl @@ -0,0 +1,10 @@ +# Created: May 2025 +Подтверждение пароля +Здравствуйте! + +Пожалуйста, перейдите по ссылке ниже, чтобы подтвердить ваш новый пароль. +HOST_URL?account=confirm-password&key=%1$s + +Сообщите нам, если у вас возникнут какие-либо проблемы! + +Команда NAME_SHORT diff --git a/template/pages/acc-dashboard.tpl.php b/template/pages/acc-dashboard.tpl.php deleted file mode 100644 index 84606e30..00000000 --- a/template/pages/acc-dashboard.tpl.php +++ /dev/null @@ -1,143 +0,0 @@ -brick('header'); ?> - -
      -
      -
      - -brick('announcement'); - - $this->brick('pageTemplate'); - - $this->brick('infobox'); -?> - - - -
      -
      - -

      -banned): -?> -
      -

      -
        -
      • '.Lang::account('bannedBy').''.Lang::main('colon').''.$b['by'][1].''; ?>
      • -
      • '.Lang::account('ends').''.Lang::main('colon').($b['end'] ? date(Lang::main('dateFmtLong'), $b['end']) : Lang::account('permanent')); ?>
      • -
      • '.Lang::account('reason').''.Lang::main('colon').''.($b['reason'] ?: Lang::account('noReason')).''; ?>
      • -
      -
      - - - - - -

      {$lang.publicDesc}

      -
      {$lang.Your_description_has_been_updated_successfully}.
      - -
      - {$lang.viewPublicDesc|sprintf:$user.name}. -
      -
      - -
      - -
      -{* CLAIM CHARACTERS *} -

      [Select Character]

      -{strip} - -{if $user.chars} - - {foreach from=$user.chars item=c} - - - - - {/foreach} -
      - {if $c.this} - {$c.name} - {else} - {$c.name} - {/if} -   - {if $c.guild} - <{$c.guild|escape:"html"}> - {/if} -  — {$c.text} -
      -{else} - [no characters on ths account] -{/if} -
      - -{/strip} -{* CHANGE PASSWORD / EMAIL / DISPLAYNAME / AVATAR * } -

      {$lang.Change_password}

      -
      - -{if isset($cpmsg)} -
      {$cpmsg.msg}
      -{/if} - - - - - -
      {$lang.Current_password}{$lang.colon}
      {$lang.New_password}{$lang.colon}
      {$lang.Confirm_new_password}{$lang.colon}
      -
      - - -
      -
      - -
      - -*/ -?> - -brick('lvTabs'); ?> - -
      -
      -
      - -brick('footer'); ?> diff --git a/template/pages/acc-recover.tpl.php b/template/pages/acc-recover.tpl.php deleted file mode 100644 index 3a26c370..00000000 --- a/template/pages/acc-recover.tpl.php +++ /dev/null @@ -1,134 +0,0 @@ -brick('header'); ?> - -
      -
      -
      -brick('announcement'); - - $this->brick('pageTemplate'); -?> -
      -text)): ?> -
      -

      head; ?>

      -
      -
      text; ?>
      -
      -resetPass): ?> - - -
      -
      -

      head; ?>

      -
      error; ?>
      - - - - - - - - - - - - - - - - - - - -
      - -
      -
      - - - - - -
      -
      -

      head; ?>

      -
      error; ?>
      - -
      - -
      - - -
      - -
      -
      - - -
      -
      -
      - -brick('footer'); ?> diff --git a/template/pages/account.tpl.php b/template/pages/account.tpl.php new file mode 100644 index 00000000..e5565ca9 --- /dev/null +++ b/template/pages/account.tpl.php @@ -0,0 +1,348 @@ +brick('header'); +?> + +
      +
      +
      + +brick('announcement'); + + $this->brick('pageTemplate'); +?> + +
      +

      + +bans): + foreach ($this->bans as $b): + [$end, $reason, $name] = $b; +?> + +
      +

      +
        +
      • '.Lang::account('bannedBy').''.($name ? ''.$name.'' : '<System>');?>
      • +
      • '.Lang::account('ends').''.($end ? date(Lang::main('dateFmtLong'), $end) : Lang::account('permanent'));?>
      • +
      • '.Lang::account('reason').''.''.($reason ?: Lang::account('noReason')).'';?>
      • +
      +
      + + + +
      + + + + +
      +
      + +
      + +
      +
      + + + +cfg('ACC_AUTH_MODE') == AUTH_MODE_SELF): +?> + + + + + + + + + + + +
      +
      + + + + + +
      +
      + + +brick('footer'); ?> diff --git a/template/pages/achievement.tpl.php b/template/pages/achievement.tpl.php index 5d8bfe1d..86a31978 100644 --- a/template/pages/achievement.tpl.php +++ b/template/pages/achievement.tpl.php @@ -1,4 +1,12 @@ -brick('header'); ?> +brick('header'); +?>
      @@ -13,110 +21,84 @@ ?>
      -brick('headIcons'); - -$this->brick('redButtons'); -?> -

      name; ?>

      description; + $this->brick('headIcons'); - echo '

      '.Lang::achievement('criteria').($this->criteria['reqQty'] ? ' – '.Lang::achievement('reqNumCrt').' '.$this->criteria['reqQty'].' '.Lang::achievement('outOf').' '.count($this->criteria['data']).'' : null)."

      \n"; + $this->brick('redButtons'); ?> +

      h1; ?>

      + description.PHP_EOL; ?> +

      reqCrtQty ? ' – '.Lang::achievement('reqNumCrt', [$this->reqCrtQty, count($this->criteria)]).'' : ''); ?>

      -
      - criteria['data'] as $i => $cr): - echo ''; - - if (!isset($cr['icon'])): - echo '
      •  
      '; - endif; - - echo '
      '; - +$rows0 = $rows1 = ''; +foreach ($this->criteria as $i => $icon): // every odd number of elements - if ($i + 1 == round(count($this->criteria['data']) / 2)): - echo '
      '; - - if (!empty($cr['link'])): - echo ''.Util::htmlEscape($cr['link']['text']).''; - endif; - - if (!empty($cr['link']['count']) && $cr['link']['count'] > 1): - echo ' ('.$cr['link']['count'].')'; - endif; - - if (isset($cr['extraText'])): - echo ' '.$cr['extraText']; - endif; - - echo ''; - - if (!empty($cr['extraData'])): - $buff = []; - foreach ($cr['extraData'] as $xd): - $buff[] = $xd[0] ? ''.$xd[1].'' : ''.$xd[1].''; - endforeach; - - echo '
      ('.implode(', ', $buff).')'; - endif; - - echo '
      '; - endif; + ${'rows' . ($i % 2)} .= $icon->renderContainer(20, $i, true); endforeach; + +if ($rows0): + echo '
      '.PHP_EOL; + echo $rows0; + echo '
      '.PHP_EOL; +endif; +if ($rows1): + echo '
      '.PHP_EOL; + echo $rows1; + echo '
      '.PHP_EOL; +endif; ?> - -
      rewards): - if (!empty($r['item'])): - echo '

      '.Lang::main('rewards')."

      \n"; - $this->brick('rewards', ['rewards' => $r['item'], 'rewTitle' => null]); +if ([$rewItems, $rewTitle, $rewText] = $this->rewards): + if ($rewItems): + echo '

      '.Lang::main('rewards').'

      '.PHP_EOL; + $this->brick('rewards', ['rewards' => $rewItems, 'rewTitle' => null]); endif; - if (!empty($r['title'])): + if ($rewTitle): echo '

      '.Lang::main('gains')."

      \n
        "; - foreach ($r['title'] as $i): - echo '
      • '.$i."
      • \n"; + foreach ($rewTitle as $i): + echo '
      • '.$i.'
      • '.PHP_EOL; endforeach; - echo "
      \n"; + echo '
    '.PHP_EOL; endif; - if (empty($r['title']) && empty($r['item']) && $r['text']): - echo '

    '.Lang::main('rewards')."

    \n" . - '
    • '.$r['text']."
    \n"; + if (!$rewTitle && !$rewItems && $rewText): + echo '

    '.Lang::main('rewards').'

    '.PHP_EOL; + echo '
    • '.$rewText.'
    '.PHP_EOL; endif; endif; -$this->brick('mail'); +$this->brickIf($this->mail, 'mail'); -if (!empty($this->transfer)): - echo "
    \n ".$this->transfer."\n"; +if ($this->transfer): + echo '
    '.PHP_EOL; + echo '
    '.PHP_EOL; + echo ' '.$this->transfer.PHP_EOL; endif; ?> -

    +

    brick('lvTabs', ['relTabs' => true]); +$this->brick('lvTabs'); $this->brick('contribute'); ?> diff --git a/template/pages/achievements.tpl.php b/template/pages/achievements.tpl.php index f6011b16..05bd6e51 100644 --- a/template/pages/achievements.tpl.php +++ b/template/pages/achievements.tpl.php @@ -1,6 +1,12 @@ brick('header'); -$f = $this->filter; // shorthand + namespace Aowow\Template; + + use \Aowow\Lang; + + /** @var PageTemplate $this */ + + $this->brick('header'); + $f = $this->filter->values; // shorthand ?>
    @@ -8,62 +14,66 @@ $f = $this->filter; // shorthand
    brick('announcement'); + $this->brick('announcement'); -$this->brick('pageTemplate', ['fi' => empty($f['query']) ? null : ['query' => $f['query'], 'menuItem' => 9]]); + $this->brick('pageTemplate', ['fiQuery' => $this->filter->query, 'fiMenuItem' => [9]]); ?> -
    -
    +
    + +
    + +brick('headIcons'); + + $this->brick('redButtons'); +?> + +

    h1; ?>

    +
    - + - +
    ucFirst(Lang::main('name')).Lang::main('colon'); ?> - - - + + +
     />  /> />  />
      - - + +
        /> - />    /> - />
    -
    +
    -
    - /> /> +
    + /> />
    - - + +
    -brick('filter', ['fi' => $f['initData']]); ?> +renderFilter(12); ?> brick('lvTabs'); ?> diff --git a/template/pages/admin/reports.tpl.php b/template/pages/admin/reports.tpl.php new file mode 100644 index 00000000..ac203920 --- /dev/null +++ b/template/pages/admin/reports.tpl.php @@ -0,0 +1,37 @@ +brick('header'); +?> + + + +
    +
    +
    + +brick('announcement'); + + $this->brick('pageTemplate'); +?> + +
    +

    h1;?>

    + +brick('markup', ['markup' => $this->article]); + + $this->brick('markup', ['markup' => $this->extraText]); + + echo $this->extraHTML ?? ''; +?> + +
    +
    +
    + +brick('footer'); ?> diff --git a/template/pages/admin/screenshots.tpl.php b/template/pages/admin/screenshots.tpl.php index 54aa19a6..e0276d9f 100644 --- a/template/pages/admin/screenshots.tpl.php +++ b/template/pages/admin/screenshots.tpl.php @@ -1,16 +1,23 @@ -brick('header'); ?> +brick('header'); +?>
    brick('announcement'); + $this->brick('announcement'); -$this->brick('pageTemplate'); + $this->brick('pageTemplate'); ?> +
    -

    name; ?>

    +

    h1; ?>

    @@ -22,17 +29,11 @@ $this->brick('pageTemplate'); - - + +
    Page: #» Search by Page#» Search by Page

    @@ -40,20 +41,20 @@ endforeach;
    Menu
    PagesScreenshots: -
    ssNFound ? ' – Show All ('.$this->ssNFound.')' : null); ?>
    +
    ssNFound ? ' – Show All ('.$this->ssNFound.')' : ''); ?>

    Mass Select

    - – Select All
    - – Deselect All
    - – Toggle Selection
    - – Select All Pending
    - – Select All Unique
    - – Select All Approved
    - – Select All Sticky
    + – Select All
    + – Deselect All
    + – Toggle Selection
    + – Select All Pending
    + – Select All Unique
    + – Select All Approved
    + – Select All Sticky
    @@ -108,27 +109,29 @@ endforeach; $WH.ge('pagetypeid').onkeydown = function(e) { e = $WH.$E(e); - var validKeys = [8, 9, 13, 35, 36, 37, 39, 46, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57]; + var validKeys = [8, 9, 13, 35, 36, 37, 38, 39, 40, 46, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 173]; if (!e.ctrlKey && $WH.in_array(validKeys, e.keyCode) == -1) return false; if (e.keyCode == 13 && this.value != '') - ss_Manage(); + ss_Manage(null, $('#pagetype').val(), parseInt($('#pagetypeid').val()) || 0); return true; } + getAll): - echo " var ss_getAll = true;\n"; + echo ' var ss_getAll = true;'.PHP_EOL; endif; if ($this->ssPages): - echo " var ssm_screenshotPages = ".Util::toJSON($this->ssPages).";\n"; - echo " ssm_UpdatePages();\n"; + echo ' var ssm_screenshotPages = ".$this->json($this->ssPages).";'.PHP_EOL; + echo ' ssm_UpdatePages();'.PHP_EOL; elseif ($this->ssData): - echo " var ssm_screenshotData = ".Util::toJSON($this->ssData).";\n"; - echo " ssm_UpdateList();\n"; + echo ' var ssm_screenshotData = ".$this->json($this->ssData).";'.PHP_EOL; + echo ' ssm_UpdateList();'.PHP_EOL; endif; ?> + ss_OnResize();
    diff --git a/template/pages/admin/siteconfig.tpl.php b/template/pages/admin/siteconfig.tpl.php index cdfb067d..6c604cc4 100644 --- a/template/pages/admin/siteconfig.tpl.php +++ b/template/pages/admin/siteconfig.tpl.php @@ -1,4 +1,10 @@ -brick('header'); ?> +brick('header'); +?> +
    +
    +
    + +brick('footer'); ?> diff --git a/template/pages/admin/weight-presets.tpl.php b/template/pages/admin/weight-presets.tpl.php index 77ad54e7..c21baf6a 100644 --- a/template/pages/admin/weight-presets.tpl.php +++ b/template/pages/admin/weight-presets.tpl.php @@ -1,4 +1,10 @@ -brick('header'); ?> +brick('header'); +?> -
    -extraHTML)): - echo $this->extraHTML; - endif; -?>

    Edit

    Icon
    diff --git a/template/pages/areatriggers.tpl.php b/template/pages/areatriggers.tpl.php new file mode 100644 index 00000000..a663845e --- /dev/null +++ b/template/pages/areatriggers.tpl.php @@ -0,0 +1,78 @@ +brick('header'); + $f = $this->filter->values; // shorthand +?> + +
    +
    +
    + +brick('announcement'); + + $this->brick('pageTemplate', ['fiQuery' => $this->filter->query, 'fiMenuItem' => [102]]); +?> + +
    +
    +
    + +brick('headIcons'); + + $this->brick('redButtons'); +?> + +

    h1; ?>

    +
    +
    +
    + +
    + +
    + + + + + +
    ucFirst(Lang::main('name')).Lang::main('colon'); ?> + + +
     />
    +
    + +
    + +
    + /> /> +
    + +
    + +
    + + +
    + +
    +
    +
    + +renderFilter(12); ?> + +brick('lvTabs'); ?> + +
    +
    +
    + +brick('footer'); ?> diff --git a/template/pages/arena-teams.tpl.php b/template/pages/arena-teams.tpl.php index 3168756a..c7636499 100644 --- a/template/pages/arena-teams.tpl.php +++ b/template/pages/arena-teams.tpl.php @@ -1,6 +1,12 @@ brick('header'); -$f = $this->filter; // shorthand + namespace Aowow\Template; + + use \Aowow\Lang; + + /** @var PageTemplate $this */ + + $this->brick('header'); + $f = $this->filter->values; // shorthand ?>
    @@ -8,49 +14,53 @@ $f = $this->filter; // shorthand
    brick('announcement'); + $this->brick('announcement'); -$this->brick('pageTemplate', ['fi' => empty($f['query']) ? null : ['query' => $f['query'], 'menuItem' => 2]]); + $this->brick('pageTemplate', ['fiQuery' => $this->filter->query, 'fiMenuItem' => array_slice($this->pageTemplate['breadcrumb'], 0, 3)]); -# for some arcane reason a newline (\n) means, the first childNode is a text instead of the form for the following div +# pr_setRegionRealm($WH.ge('fi').firstChild, realm, region) - never have \n\s before
    , it will become firstChild (a text node) ?> -
    +
    + +brick('headIcons'); + + $this->brick('redButtons'); +?> + +

    h1; ?>

    +
    - + - + - + - +
    ucFirst(Lang::main('name')).Lang::main('colon'); ?> - - + +
     />  /> />  />
            
        Size     
    @@ -66,7 +76,7 @@ $this->brick('pageTemplate', ['fi' => empty($f['query']) ? null : ['query' => $f
    -brick('filter', ['fi' => $f['initData']]); ?> +renderFilter(12); ?> brick('lvTabs'); ?> diff --git a/template/pages/compare.tpl.php b/template/pages/compare.tpl.php index e7f41813..5f06d246 100644 --- a/template/pages/compare.tpl.php +++ b/template/pages/compare.tpl.php @@ -1,4 +1,10 @@ -brick('header'); ?> +brick('header'); +?>
    @@ -13,12 +19,14 @@
    diff --git a/template/pages/delete.tpl.php b/template/pages/delete.tpl.php new file mode 100644 index 00000000..ddcc3c18 --- /dev/null +++ b/template/pages/delete.tpl.php @@ -0,0 +1,31 @@ +brick('header'); +?> + +
    +
    +
    + +brick('announcement'); + + $this->brick('pageTemplate'); + +if ($this->inputbox): + $this->brick(...$this->inputbox); // $templateName, [$templateVars] +elseif ($this->confirm): + $this->localizedBrick('confirm-delete-account'); +else: + $this->localizedBrick('delete-account'); +endif; +?> + +
    +
    +
    + +brick('footer'); ?> diff --git a/template/pages/detail-page-generic.tpl.php b/template/pages/detail-page-generic.tpl.php index 26406f1f..7757c8ff 100644 --- a/template/pages/detail-page-generic.tpl.php +++ b/template/pages/detail-page-generic.tpl.php @@ -1,4 +1,12 @@ -brick('header'); ?> +brick('header'); +?>
    @@ -13,82 +21,62 @@ ?>
    + brick('headIcons'); $this->brick('redButtons'); -?> - expansion) ? ' class="h1-icon">'.$this->name.'' : '>'.$this->name; ?> + if ($this->expansion && $this->h1): + echo '

    '.$this->h1.'

    '.PHP_EOL; + elseif ($this->h1): + echo '

    '.$this->h1.'

    '.PHP_EOL; + endif; -brick('article'); + $this->brick('markup', ['markup' => $this->article]); + + $this->brick('markup', ['markup' => $this->extraText]); $this->brick('mapper'); -if (isset($this->extraText)): + if ($this->transfer): + echo '
    '.PHP_EOL; + echo ' '.$this->transfer.PHP_EOL; + endif; + + $this->brick('markup', ['markup' => $this->smartAI]); + +if ($this->zoneMusic): ?> -
    - -
    -transfer)): - echo "
    \n ".$this->transfer."\n"; -endif; - -if (!empty($this->zoneMusic)): -?>
    -zoneMusic['music'])): -?> -
    -

    -
    - -zoneMusic['intro'])): -?> -
    -

    -
    - zoneMusic['ambience'])): + foreach ($this->zoneMusic as [$h3, $data, $divId, $opts]): ?> -
    -

    +
    +

    + +
    + -

    + +

    + brick('lvTabs', ['relTabs' => true]); + $this->brick('lvTabs'); $this->brick('contribute'); ?> diff --git a/template/pages/enchantment.tpl.php b/template/pages/enchantment.tpl.php index 04bc7506..f737f555 100644 --- a/template/pages/enchantment.tpl.php +++ b/template/pages/enchantment.tpl.php @@ -1,4 +1,12 @@ -brick('header'); ?> +brick('header'); +?>
    @@ -16,14 +24,14 @@ brick('redButtons'); ?> -

    name; ?>

    +

    h1; ?>

    -brick('article'); ?> +brick('markup', ['markup' => $this->article]); ?> -

    +

    @@ -31,86 +39,98 @@ + activateCondition)): +if ($this->activation): ?> + - - + + + effects as $i => $e): ?> - - - + + + +
    activateCondition; ?>activation; ?>
    -)' : '').''; - if (isset($e['value'])): - echo '
    '.Lang::spell('_value').Lang::main('colon').$e['value']; +
    + +)' : '').''; + + if ($e['value']): + echo '
    '.Lang::spell('_value').Lang::main('colon').$e['value']; endif; - if (!empty($e['proc'])): - echo '
    '; + if ($e['proc']): + echo '
    '; if ($e['proc'] < 0): - echo sprintf(Lang::spell('ppm'), Lang::nf(-$e['proc'], 1)); + echo Lang::spell('ppm', [-$e['proc']]); elseif ($e['proc'] < 100.0): - echo Lang::spell('procChance').Lang::main('colon').$e['proc'].'%'; + echo Lang::spell('procChance', [$e['proc']]); endif; endif; - echo "
    \n"; + echo ''.PHP_EOL; - if (!empty($e['tip'])): + if ($e['tip']): ?> + + + - -'.(strpos($e['icon']['name'], '#') ? $e['icon']['name'] : sprintf('%s', $e['icon']['id'], $e['icon']['name']))."\n"; -?> + renderContainer(0, $i); ?>
    + +
    -

    +

    brick('lvTabs', ['relTabs' => true]); +$this->brick('lvTabs'); $this->brick('contribute'); ?> +
    diff --git a/template/pages/enchantments.tpl.php b/template/pages/enchantments.tpl.php index a52af41d..b818df45 100644 --- a/template/pages/enchantments.tpl.php +++ b/template/pages/enchantments.tpl.php @@ -1,6 +1,12 @@ brick('header'); -$f = $this->filter; // shorthand + namespace Aowow\Template; + + use \Aowow\Lang; + + /** @var PageTemplate $this */ + + $this->brick('header'); + $f = $this->filter->values; // shorthand ?>
    @@ -8,33 +14,37 @@ $f = $this->filter; // shorthand
    brick('announcement'); + $this->brick('announcement'); -$this->brick('pageTemplate', ['fi' => empty($f['query']) ? null : ['query' => $f['query'], 'menuItem' => 101]]); + $this->brick('pageTemplate', ['fiQuery' => $this->filter->query, 'fiMenuItem' => [101]]); ?> -
    +
    +
    + +brick('headIcons'); + + $this->brick('redButtons'); +?> + +

    h1; ?>

    +
    -
    +
    - + @@ -43,7 +53,7 @@ endforeach;
    - /> /> + /> />
    @@ -57,7 +67,7 @@ endforeach;
    -brick('filter', ['fi' => $f['initData']]); ?> +renderFilter(12); ?> brick('lvTabs'); ?> diff --git a/template/pages/guide-edit.tpl.php b/template/pages/guide-edit.tpl.php new file mode 100644 index 00000000..3d83fb77 --- /dev/null +++ b/template/pages/guide-edit.tpl.php @@ -0,0 +1,318 @@ +brick('header'); +?> + +
    +
    +
    + +brick('announcement'); + + $this->brick('pageTemplate'); +?> + +
    +

    h1; ?>

    + +brick('markup', ['markup' => $this->article]); +?> + +
    +
    + +
    +
    + +
    ucFirst(Lang::main('name')).Lang::main('colon'); ?> - +
     /> />
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +*/ +?> + + + + + + + + + + + + +
    + + +
    + + +
    + + +
    + + + +
    + + +
    + + +
    editStatus);?> + +isDraft && $this->typeId): + echo ' ('.Lang::guide('editor', 'testGuide').')'.PHP_EOL; +endif; +?> + +
    + +
    + +
    + +
    + + + +
    + +error): ?> +
    + error . PHP_EOL;?> +
    + + + +
    +
    + + +
    +
    +

    + + + + +
    +
    + + + + +
    + +
    +

     

    +
    + +
    +
    + + +
    +
    + +brick('footer'); ?> diff --git a/template/pages/guilds.tpl.php b/template/pages/guilds.tpl.php index e0ac583d..312606cf 100644 --- a/template/pages/guilds.tpl.php +++ b/template/pages/guilds.tpl.php @@ -1,6 +1,12 @@ brick('header'); -$f = $this->filter; // shorthand + namespace Aowow\Template; + + use \Aowow\Lang; + + /** @var PageTemplate $this */ + + $this->brick('header'); + $f = $this->filter->values; // shorthand ?>
    @@ -8,42 +14,48 @@ $f = $this->filter; // shorthand
    brick('announcement'); + $this->brick('announcement'); -$this->brick('pageTemplate', ['fi' => empty($f['query']) ? null : ['query' => $f['query'], 'menuItem' => 2]]); + $this->brick('pageTemplate', ['fiQuery' => $this->filter->query, 'fiMenuItem' => array_slice($this->pageTemplate['breadcrumb'], 0, 3)]); -# for some arcane reason a newline (\n) means, the first childNode is a text instead of the form for the following div + # pr_setRegionRealm($WH.ge('fi').firstChild, realm, region) - never have \n\s before
    , it will become firstChild (a text node) ?> -
    +
    + +brick('headIcons'); + + $this->brick('redButtons'); +?> + +

    h1; ?>

    +
    - + - + - + @@ -61,7 +73,7 @@ $this->brick('pageTemplate', ['fi' => empty($f['query']) ? null : ['query' => $f
    -brick('filter', ['fi' => $f['initData']]); ?> +renderFilter(12); ?> brick('lvTabs'); ?> diff --git a/template/pages/home.tpl.php b/template/pages/home.tpl.php index b76a8abf..3334e0b2 100644 --- a/template/pages/home.tpl.php +++ b/template/pages/home.tpl.php @@ -1,21 +1,40 @@ + + + brick('head'); ?> - +
    -featuredBox['altHomeLogo'])): ?> - + +homeTitle): + echo " ".PHP_EOL; +endif; + +if ($this->altHomeLogo): +?> + + + +
    -

    Aowow

    +

    concat('title'); ?>

    brick('announcement'); ?> @@ -29,46 +48,56 @@
    oneliner): ?> -

    +

    + - featuredBox): ?>
    + featuredBox): ?> -
    + +
    + featuredBox['overlays']): ?> + +
    +
    @@ -79,12 +108,14 @@ endif; |Github|
    brick('pageTemplate'); ?> +localizedBrickIf($this->consentFooter, 'consent'); ?> + diff --git a/template/pages/icon.tpl.php b/template/pages/icon.tpl.php index 27620c8a..59a3a906 100644 --- a/template/pages/icon.tpl.php +++ b/template/pages/icon.tpl.php @@ -1,4 +1,12 @@ -brick('header'); ?> +brick('header'); +?>
    @@ -13,24 +21,27 @@ ?>
    + brick('redButtons'); ?> -

    name; ?>

    +

    h1; ?>

    + brick('article'); + $this->brick('markup', ['markup' => $this->article]); ?> +
    -

    +

    brick('lvTabs', ['relTabs' => true]); + $this->brick('lvTabs'); $this->brick('contribute'); ?> diff --git a/template/pages/icons.tpl.php b/template/pages/icons.tpl.php index 3d077a7d..35f59368 100644 --- a/template/pages/icons.tpl.php +++ b/template/pages/icons.tpl.php @@ -1,6 +1,12 @@ brick('header'); -$f = $this->filter; // shorthand + namespace Aowow\Template; + + use \Aowow\Lang; + + /** @var PageTemplate $this */ + + $this->brick('header'); + $f = $this->filter->values; // shorthand ?>
    @@ -8,19 +14,29 @@ $f = $this->filter; // shorthand
    brick('announcement'); + $this->brick('announcement'); -$this->brick('pageTemplate', ['fi' => empty($f['query']) ? null : ['query' => $f['query'], 'menuItem' => 101]]); + $this->brick('pageTemplate', ['fiQuery' => $this->filter->query, 'fiMenuItem' => [31]]); ?> -
    +
    +
    + +brick('headIcons'); + + $this->brick('redButtons'); +?> + +

    h1; ?>

    +
    ucFirst(Lang::main('name')).Lang::main('colon'); ?> - - + +
     />  /> />  />
            
         
    - + @@ -29,7 +45,7 @@ $this->brick('pageTemplate', ['fi' => empty($f['query']) ? null : ['query' => $f
    - /> /> + /> />
    @@ -43,7 +59,7 @@ $this->brick('pageTemplate', ['fi' => empty($f['query']) ? null : ['query' => $f
    -brick('filter', ['fi' => $f['initData']]); ?> +renderFilter(12); ?> brick('lvTabs'); ?> diff --git a/template/pages/image-crop.tpl.php b/template/pages/image-crop.tpl.php new file mode 100644 index 00000000..6084bf49 --- /dev/null +++ b/template/pages/image-crop.tpl.php @@ -0,0 +1,49 @@ +brick('header'); +?> + +
    +
    +
    + +brick('announcement'); + + $this->brick('pageTemplate'); +?> + +
    +

    h1; ?>

    + + +
    +
    + +
    + +
    + +
    + + +
    + +

    +
    + + + + +
    +
    +
    + +brick('footer'); ?> diff --git a/template/pages/item.tpl.php b/template/pages/item.tpl.php index 9ea64d2b..070fb0b1 100644 --- a/template/pages/item.tpl.php +++ b/template/pages/item.tpl.php @@ -1,4 +1,12 @@ -brick('header'); ?> +brick('header'); +?>
    @@ -13,80 +21,71 @@ ?>
    + brick('redButtons'); ?> -

    name; ?>

    +

    h1; ?>

    + unavailable): ?> +
    - +
    + brick('tooltip'); - $this->brick('article'); + $this->brick('markup', ['markup' => $this->article]); -if (!empty($this->transfer)): - echo "
    \n ".$this->transfer."\n"; +if ($this->map): + echo '

    '.$this->map[4].'

    '.PHP_EOL; + $this->brick('mapper'); endif; -if (!empty($this->subItems)): +if ($this->transfer): + echo '
    '.PHP_EOL; + echo ' '.$this->transfer.PHP_EOL; +endif; + +if ($this->subItems): ?> +
    -

    +

    + +subItems['data'], ceil(count($this->subItems['data']) / 2)) as $columns): +?>
      -subItems['data'] as $k => $i): - if ($k < (count($this->subItems['data']) / 2)): - $eText = []; - foreach ($i['enchantment'] as $eId => $txt): - $eText[] = ''.$txt.''; - endforeach; - echo '
    • ...'.$i['name'].''; - echo ' '.sprintf(Lang::item('_chance'), $i['chance']).'
      '.implode(', ', $eText).'
    • '; - endif; + ['name' => $name, 'enchantment' => $enchantment, 'chance' => $chance]): + echo '
    • ...'.$name.' '.Lang::item('_chance', [$chance]).'
      '; + echo Lang::concat($enchantment, Lang::CONCAT_NONE, fn($txt, $eId) => ''.$txt.'').'
    • '.PHP_EOL; endforeach; ?> -
    -
    -subItems) > 1): -?> -
    -
      -subItems['data'] as $k => $i): - if ($k >= (count($this->subItems['data']) / 2)): - $eText = []; - foreach ($i['enchantment'] as $eId => $txt): - $eText[] = ''.$txt.''; - endforeach; - echo '
    • ...'.$i['name'].''; - echo ' '.sprintf(Lang::item('_chance'), $i['chance']).'
      '.implode(', ', $eText).'
    • '; - endif; - endforeach; -?>
    + brick('book'); ?> -

    +

    brick('lvTabs', ['relTabs' => true]); + $this->brick('lvTabs'); $this->brick('contribute'); ?> diff --git a/template/pages/items.tpl.php b/template/pages/items.tpl.php index 77e16270..b722653a 100644 --- a/template/pages/items.tpl.php +++ b/template/pages/items.tpl.php @@ -1,6 +1,12 @@ brick('header'); -$f = $this->filter; // shorthand + namespace Aowow\Template; + + use \Aowow\Lang; + + /** @var PageTemplate $this */ + + $this->brick('header'); + $f = $this->filter->values; // shorthand ?>
    @@ -8,104 +14,93 @@ $f = $this->filter; // shorthand
    brick('announcement'); + $this->brick('announcement'); -$this->brick('pageTemplate', ['fi' => empty($f['query']) ? null : ['query' => $f['query'], 'menuItem' => 0]]); + $this->brick('pageTemplate', ['fiQuery' => $this->filter->query, 'fiMenuItem' => [0]]); ?> -
    +
    +
    + +brick('headIcons'); + + $this->brick('redButtons'); +?> + +

    h1; ?>

    +
    -
    +
    slotList): ?> +
    -
    +
    - +makeOptionsList($this->slotList, $f['sl'], 28); ?>
    + typeList): ?> +
    -
    +
    - +makeOptionsList($this->typeList, $f['ty'], 28, function($v, $k, &$e) { + if (($this->pageTemplate['breadcrumb'][2] ?? null) === 0 && ($this->pageTemplate['breadcrumb'][3] ?? null) === $k) + $e = ['selected' => 'selected']; // preselect type for consumables .. blegh >:( + return true; +}); ?>
    +
    ucFirst(Lang::main('name')).Lang::main('colon'); ?> - +
     /> />
    - - + + - + - + @@ -115,7 +110,8 @@ endforeach;
    - />/> + + />/>
    @@ -131,19 +127,17 @@ endforeach;
     />ucFirst(Lang::main('name')).Lang::main('colon'); ?> />
     /> - /> /> - /> - - + +
        /> - />    /> - />
       
    - + - +
    -   /> +   />
    @@ -157,16 +151,8 @@ endforeach;
    - $str): - if ($k): - echo ' \n"; - else: - echo ' \n"; - endif; -endforeach; -?> + +makeRadiosList('gb', Lang::main('gb'), $f['gb'] ?? '', 24, fn($v, &$k) => ($k = $k ?: '') || 1); ?>
    @@ -176,7 +162,7 @@ endforeach;
    - /> +
    @@ -184,7 +170,7 @@ endforeach;
    -brick('filter', ['fi' => $f['initData']]); ?> +renderFilter(12); ?> brick('lvTabs'); ?> diff --git a/template/pages/itemset.tpl.php b/template/pages/itemset.tpl.php index 92944936..42a3b36f 100644 --- a/template/pages/itemset.tpl.php +++ b/template/pages/itemset.tpl.php @@ -1,4 +1,12 @@ -brick('header'); ?> +brick('header'); +?>
    @@ -13,74 +21,93 @@ ?>
    + brick('redButtons'); + $this->brick('redButtons'); if ($this->expansion): - echo '

    '.$this->name."

    \n"; + echo '

    '.$this->h1.'

    '.PHP_EOL; else: - echo '

    '.$this->name."

    \n"; + echo '

    '.$this->h1.'

    '.PHP_EOL; endif; if ($this->unavailable): ?> +
    + brick('article'); +$this->brick('markup', ['markup' => $this->article]); echo $this->description; ?> + + pieces as $iId => $piece): - echo ' \n"; +$iconIdx = 0; +foreach ($this->pieces as [, $icon]): + echo $icon->renderContainer(20, $iconIdx, true); endforeach; ?> +
    '.$piece['name_'.User::$localeString]."

    bonusExt; ?>

    - +
      + spells as $i => $s): - echo '
    • '.$s['bonus'].' '.Lang::itemset('_pieces').Lang::main('colon').''.$s['desc']."
    • \n"; +foreach ($this->spells as [$nItems, $spellId, $text]): + echo '
    • '.Lang::itemset('_pieces', [$nItems]).''.$text.'
    • '.PHP_EOL; endforeach; ?> +
    +summary): +?> +

    + +

    brick('lvTabs', ['relTabs' => true]); +$this->brick('lvTabs'); $this->brick('contribute'); ?> diff --git a/template/pages/itemsets.tpl.php b/template/pages/itemsets.tpl.php index 67cb2242..3f2d09c9 100644 --- a/template/pages/itemsets.tpl.php +++ b/template/pages/itemsets.tpl.php @@ -1,6 +1,12 @@ brick('header'); -$f = $this->filter; // shorthand + namespace Aowow\Template; + + use \Aowow\Lang; + + /** @var PageTemplate $this */ + + $this->brick('header'); + $f = $this->filter->values; // shorthand ?>
    @@ -8,78 +14,66 @@ $f = $this->filter; // shorthand
    brick('announcement'); + $this->brick('announcement'); -$this->brick('pageTemplate', ['fi' => empty($f['query']) ? null : ['query' => $f['query'], 'menuItem' => 2]]); + $this->brick('pageTemplate', ['fiQuery' => $this->filter->query, 'fiMenuItem' => [2]]); ?> -
    - +
    + +
    + +brick('headIcons'); + + $this->brick('redButtons'); +?> + +

    h1; ?>

    +
    -
    +
    -
    +
    - - + + - + - + @@ -90,7 +84,7 @@ endforeach;
    - /> /> + /> />
    @@ -104,7 +98,7 @@ endforeach;
    -brick('filter', ['fi' => $f['initData']]); ?> +renderFilter(12); ?> brick('lvTabs'); ?> diff --git a/template/pages/list-page-generic.tpl.php b/template/pages/list-page-generic.tpl.php index 86f01611..c4e1d1cb 100644 --- a/template/pages/list-page-generic.tpl.php +++ b/template/pages/list-page-generic.tpl.php @@ -1,4 +1,12 @@ -brick('header'); ?> +brick('header'); +?>
    @@ -8,64 +16,47 @@ $this->brick('announcement'); $this->brick('pageTemplate'); - - if (isset($this->notFound)): ?> -
    -
    -

    notFound['title'];?>

    -
    notFound['msg'];?>
    -
    + h1Links)): - echo ' '; - endif; - - if (!empty($this->name)): - echo '

    '.$this->name.'

    '; - endif; - - $this->brick('mapper'); - - $this->brick('article'); - - if (isset($this->extraText)): -?> -
    - - -
    -extraHTML)): - echo $this->extraHTML; - endif; + $this->brick('redButtons'); + if ($this->h1Link): + echo ' '; endif; - if (!empty($this->tabsTitle)): + if ($this->h1): + echo '

    '.$this->h1.'

    '; + endif; + + $this->brick('mapper'); + + $this->brick('markup', ['markup' => $this->article]); + + $this->brick('markup', ['markup' => $this->extraText]); + + echo $this->extraHTML ?? ''; + + if ($this->tabsTitle): echo '

    '.$this->tabsTitle.'

    '; endif; ?> +
    + lvTabs)): + if ($this->lvTabs): $this->brick('lvTabs'); ?> +
    + +
    diff --git a/template/pages/maintenance.tpl.php b/template/pages/maintenance.tpl.php index f59efd51..b65cbd3d 100644 --- a/template/pages/maintenance.tpl.php +++ b/template/pages/maintenance.tpl.php @@ -1,3 +1,9 @@ + + @@ -7,9 +13,9 @@ diff --git a/template/pages/maps.tpl.php b/template/pages/maps.tpl.php index 875f6148..daf37bbe 100644 --- a/template/pages/maps.tpl.php +++ b/template/pages/maps.tpl.php @@ -1,4 +1,12 @@ -brick('header'); ?> +brick('header'); +?>
    @@ -15,33 +23,33 @@
    @@ -49,8 +57,8 @@
    - - + +
    diff --git a/template/pages/npc.tpl.php b/template/pages/npc.tpl.php index 895bf94f..6f2de964 100644 --- a/template/pages/npc.tpl.php +++ b/template/pages/npc.tpl.php @@ -1,4 +1,12 @@ -brick('header'); ?> +brick('header'); +?>
    @@ -13,83 +21,79 @@ ?>
    + brick('redButtons'); ?> -

    name.($this->subname ? ' <'.$this->subname.'>' : null); ?>

    +

    h1.($this->subname ? ' <'.$this->subname.'>' : ''); ?>

    brick('article'); + $this->brick('markup', ['markup' => $this->article]); if ($this->accessory): echo '
    '.Lang::npc('accessoryFor').' '; - echo Lang::concat($this->accessory, true, function ($v, $k) { return ''.$v[1].''; }); - echo ".
    \n"; + echo Lang::concat($this->accessory, true, fn ($v) => ''.$v[1].''); + echo '.
    '.PHP_EOL; endif; -if (is_array($this->placeholder)): - echo '
    '.Lang::npc('difficultyPH').' '.$this->placeholder[1].".
    \n"; +if ($this->placeholder): ?> + +
    placeholder);?>
    + map)): +elseif ($this->map): $this->brick('mapper'); else: - echo ' '.Lang::npc('unkPosition')."\n"; + echo ' '.Lang::npc('unkPosition').''.PHP_EOL; endif; -if ($this->quotes[0]): +if ([$quoteGroups, $count] = $this->quotes): ?> -

    quotes[1]; ?>)

    + +

    + reputation): ?> -

    -reputation as $set): +

    + +reputation as [$mode, $data]): if (count($this->reputation) > 1): - echo '
    • '.$set[0].'
    • '; + echo '
      • '.$mode.'
      • '; endif; echo '
          '; - foreach ($set[1] as $itr): - if ($itr['qty'][1] && User::isInGroup(U_GROUP_EMPLOYEE)) - $qty = intVal($itr['qty'][0]) . sprintf(Util::$dfnString, Lang::faction('customRewRate'), ($itr['qty'][1] > 0 ? '+' : '').intVal($itr['qty'][1])); - else - $qty = intVal(array_sum($itr['qty'])); - - echo '
        • '.$qty.' '.Lang::npc('repWith') . - ' '.$itr['name'].''.($itr['cap'] && $itr['qty'][0] > 0 ? ' ('.sprintf(Lang::npc('stopsAt'), $itr['cap']).')' : null).'
    '; + foreach ($data as [$id, $qty, $name, $cap]): + echo '
  • '.($qty[1] ?: $qty[0]).' '.Lang::npc('repWith') . + ' '.$name.''.($cap && $qty[0] > 0 ? ' ('.Lang::npc('stopsAt', [$cap]).')' : '').'
  • '; endforeach; echo ''; @@ -99,12 +103,16 @@ if ($this->reputation): endif; endforeach; endif; + +$this->brick('markup', ['markup' => $this->smartAI]); + ?> -

    + +

    brick('lvTabs', ['relTabs' => true]); +$this->brick('lvTabs'); $this->brick('contribute'); ?> diff --git a/template/pages/npcs.tpl.php b/template/pages/npcs.tpl.php index 7f153b5e..46a6189a 100644 --- a/template/pages/npcs.tpl.php +++ b/template/pages/npcs.tpl.php @@ -1,6 +1,12 @@ brick('header'); -$f = $this->filter; // shorthand +$f = $this->filter->values; // shorthand ?>
    @@ -8,69 +14,70 @@ $f = $this->filter; // shorthand
    brick('announcement'); + $this->brick('announcement'); -$this->brick('pageTemplate', ['fi' => empty($f['query']) ? null : ['query' => $f['query'], 'menuItem' => 4]]); + $this->brick('pageTemplate', ['fiQuery' => $this->filter->query, 'fiMenuItem' => [4]]); ?> -
    +
    +
    + +brick('headIcons'); + + $this->brick('redButtons'); +?> + +

    h1; ?>

    +
    -
    +
    + petFamPanel): ?>
    -
    +
    + +
     />ucFirst(Lang::main('name')).Lang::main('colon'); ?> />
     /> - /> /> - /> - - + +
        /> - />    /> - />
    ucFirst(Lang::game('class')).Lang::main('colon'); ?>   - +
            
    - + - + \n"; - elseif (isset($ol['typeStr'])): - if (in_array($ol['typeStr'], ['item', 'spell'])): - echo ' '; - else /* if (in_array($ol['typeStr'], ['npc', 'object', 'faction'])) */: - echo ' '; - endif; - - echo '\n"; - endif; - endforeach; + if ($this->end): + echo ' '.PHP_EOL; endif; if ($this->suggestedPl): - echo ' \n"; + echo ' '.PHP_EOL; endif; ?> +
    ucFirst(Lang::main('name')).Lang::main('colon'); ?> - - + +
     />  /> />  />
     /> - /> /> - /> - - +
             - > - - - + + +
    @@ -82,7 +89,7 @@ endforeach;
    - /> /> + /> />
    @@ -96,7 +103,7 @@ endforeach;
    -brick('filter', ['fi' => $f['initData']]); ?> +renderFilter(12); ?> brick('lvTabs'); ?> diff --git a/template/pages/object.tpl.php b/template/pages/object.tpl.php index bb88ee5a..ad44552b 100644 --- a/template/pages/object.tpl.php +++ b/template/pages/object.tpl.php @@ -1,4 +1,12 @@ -brick('header'); ?> +brick('header'); +?>
    @@ -13,32 +21,36 @@ ?>
    + brick('redButtons'); ?> -

    name; ?>

    +

    h1; ?>

    brick('article'); + $this->brick('markup', ['markup' => $this->article]); if ($this->relBoss): - echo "
    ".sprintf(Lang::gameObject('npcLootPH'), $this->name, $this->relBoss[0], $this->relBoss[1])."
    \n"; + echo '
    '.sprintf(Lang::gameObject('npcLootPH'), $this->h1, $this->relBoss[0], $this->relBoss[1]).'
    '.PHP_EOL; echo '
    '; endif; -if (!empty($this->map)): +if ($this->map): $this->brick('mapper'); else: echo Lang::gameObject('unkPosition'); endif; $this->brick('book'); + +$this->brick('markup', ['markup' => $this->smartAI]); + ?> -

    +

    brick('lvTabs', ['relTabs' => true]); +$this->brick('lvTabs'); $this->brick('contribute'); ?> diff --git a/template/pages/objects.tpl.php b/template/pages/objects.tpl.php index 9e042468..aeacf714 100644 --- a/template/pages/objects.tpl.php +++ b/template/pages/objects.tpl.php @@ -1,6 +1,12 @@ brick('header'); -$f = $this->filter; // shorthand + namespace Aowow\Template; + + use \Aowow\Lang; + + /** @var PageTemplate $this */ + + $this->brick('header'); + $f = $this->filter->values; // shorthand ?>
    @@ -8,22 +14,32 @@ $f = $this->filter; // shorthand
    brick('announcement'); + $this->brick('announcement'); -$this->brick('pageTemplate', ['fi' => empty($f['query']) ? null : ['query' => $f['query'], 'menuItem' => 5]]); + $this->brick('pageTemplate', ['fiQuery' => $this->filter->query, 'fiMenuItem' => [5]]); ?> -
    +
    +
    + +brick('headIcons'); + + $this->brick('redButtons'); +?> + +

    h1; ?>

    +
    - +
     />
    ucFirst(Lang::main('name')).Lang::main('colon'); ?> />
    - /> /> + /> />
    @@ -37,7 +53,7 @@ $this->brick('pageTemplate', ['fi' => empty($f['query']) ? null : ['query' => $f
    -brick('filter', ['fi' => $f['initData']]); ?> +renderFilter(12); ?> brick('lvTabs'); ?> diff --git a/template/pages/privilege.tpl.php b/template/pages/privilege.tpl.php index c954dd87..f86c7cdb 100644 --- a/template/pages/privilege.tpl.php +++ b/template/pages/privilege.tpl.php @@ -1,4 +1,10 @@ -brick('header'); ?> +brick('header'); +?>
    @@ -11,11 +17,13 @@ ?>
    -

    name;?>

    -

    privReqPoints;?>


    +

    h1;?>

    +

    privReqPoints;?>


    + brick('article'); + $this->brick('markup', ['markup' => $this->article]); ?> +
    diff --git a/template/pages/privileges.tpl.php b/template/pages/privileges.tpl.php index bf49e25b..5f4005eb 100644 --- a/template/pages/privileges.tpl.php +++ b/template/pages/privileges.tpl.php @@ -1,4 +1,12 @@ -brick('header'); ?> +brick('header'); +?>
    @@ -12,17 +20,19 @@

    -
    +

    -

    +

    + privileges as $id => list($earned, $name, $value)): - echo ' \n"; + foreach ($this->privileges as $id => [$earned, $name, $value]): + echo ' '.PHP_EOL; endforeach; ?> +
     
    '.$name.'
    '.Lang::nf($value)."
     
    '.$name.'
    '.Lang::nf($value).'
    diff --git a/template/pages/profile.tpl.php b/template/pages/profile.tpl.php index c7ff53e1..fed434d7 100644 --- a/template/pages/profile.tpl.php +++ b/template/pages/profile.tpl.php @@ -1,4 +1,10 @@ -brick('header'); ?> +brick('header'); +?>
    @@ -13,7 +19,7 @@
    diff --git a/template/pages/profiler.tpl.php b/template/pages/profiler.tpl.php index 64dcdeaa..8880f490 100644 --- a/template/pages/profiler.tpl.php +++ b/template/pages/profiler.tpl.php @@ -1,4 +1,12 @@ -brick('header'); ?> +brick('header'); +?>
    @@ -21,14 +29,19 @@
    -

    +

    ucFirst(Lang::main('name')).Lang::main('colon'); ?>

    - - +makeRadiosList('rg', $this->regions, $this->rg, 24, function (&$v, $k, &$attribs) { + $attribs = ['class' => 'profiler-button profiler-option-left']; + $v = ''.$v.''; + if ($k == $this->rg) + $attribs['class'] .= ' selected'; + return true; +}); ?>
    @@ -39,7 +52,7 @@
    - +
    diff --git a/template/pages/profiles.tpl.php b/template/pages/profiles.tpl.php index 9fc791b2..7879c37c 100644 --- a/template/pages/profiles.tpl.php +++ b/template/pages/profiles.tpl.php @@ -1,6 +1,12 @@ brick('header'); -$f = $this->filter; // shorthand + namespace Aowow\Template; + + use \Aowow\Lang; + + /** @var PageTemplate $this */ + + $this->brick('header'); + $f = $this->filter->values; // shorthand ?>
    @@ -8,76 +14,69 @@ $f = $this->filter; // shorthand
    brick('announcement'); + $this->brick('announcement'); -$this->brick('pageTemplate', ['fi' => empty($f['query']) ? null : ['query' => $f['query'], 'menuItem' => 2]]); + $this->brick('pageTemplate', ['fiQuery' => $this->filter->query, 'fiMenuItem' => array_slice($this->pageTemplate['breadcrumb'], 0, 3)]); -# for some arcane reason a newline (\n) means, the first childNode is a text instead of the form for the following div + # pr_setRegionRealm($WH.ge('fi').firstChild, realm, region) - never have \n\s before , it will become firstChild (a text node) ?> -
    +
    + +brick('headIcons'); + + $this->brick('redButtons'); +?> + +

    h1; ?>

    +
    -
    +
    ucFirst(Lang::game('class')).Lang::main('colon'); ?>
    -
    +
    ucFirst(Lang::game('race')).Lang::main('colon'); ?>
    - + - + - + - +
    ucFirst(Lang::main('name')).Lang::main('colon'); ?> - - + +
     />  /> />  />
            
          /> - /> /> - />
    @@ -85,7 +84,7 @@ endforeach;
    - />/> + />/>
    @@ -100,17 +99,22 @@ endforeach; roster): ?> +

    roster;?>

    + +
    + +
    -brick('filter', ['fi' => $f['initData']]); ?> +renderFilter(12); ?> brick('lvTabs'); ?> diff --git a/template/pages/quest.tpl.php b/template/pages/quest.tpl.php index 75de1a98..104827e9 100644 --- a/template/pages/quest.tpl.php +++ b/template/pages/quest.tpl.php @@ -1,4 +1,12 @@ -brick('header'); ?> +brick('header'); +?>
    @@ -13,101 +21,90 @@ ?>
    + brick('redButtons'); ?> -

    name; ?>

    +

    h1; ?>

    + unavailable): ?>
    - +
    + objectives): - echo $this->objectives."\n"; + echo $this->objectives.PHP_EOL; elseif ($this->requestItems): - echo '

    '.Lang::quest('progress')."

    \n"; - echo $this->requestItems."\n"; + echo '

    '.Lang::quest('progress').'

    '.PHP_EOL; + echo $this->requestItems.PHP_EOL; elseif ($this->offerReward): - echo '

    '.Lang::quest('completion')."

    \n"; - echo $this->offerReward."\n"; + echo '

    '.Lang::quest('completion').'

    '.PHP_EOL; + echo $this->offerReward.PHP_EOL; endif; +$iconOffset = 0; if ($this->end || $this->objectiveList): ?> + + end): - echo " \n"; - endif; - - if ($o = $this->objectiveList): - foreach ($o as $i => $ol): - if (isset($ol['text'])): - echo ' \n"; - elseif (!empty($ol['proxy'])): // this implies creatures - echo ' '.PHP_EOL; + elseif (is_object($objective)): // has icon set (spell / item / ...) or unordered linked list + echo $objective?->renderContainer(20, $iconOffset, true); + endif; + endforeach; - if ($block2): // may be empty - echo "

     

    ".$this->end."

     

    '.$ol['text']."

     

    '.$ol['name'].$ol['extraText'].''.($ol['qty'] > 1 ? ' ('.$ol['qty'].')' : null).'
    \n"; - - $block1 = array_slice($ol['proxy'], 0, ceil(count($ol['proxy']) / 2), true); - $block2 = array_slice($ol['proxy'], ceil(count($ol['proxy']) / 2), null, true); - - echo "
    \n"; - foreach ($block1 as $pId => $name): - echo ' \n"; + foreach ($this->objectiveList as $objective): + if (is_string($objective)): // just text line + echo ' '.PHP_EOL; + elseif (is_array($objective)): // proxy npc data + ['id' => $id, 'text' => $text, 'qty' => $qty, 'proxy' => $proxies] = $objective; + echo '
    •  
    '.$name."

     

    '.$objective.'

     

    '.$text.''.($qty ? ' ('.$qty.')' : '').'
    '.PHP_EOL; + endforeach; + echo '
    \n"; - foreach ($block2 as $pId => $name): - echo ' \n"; - endforeach; - echo "
    •  
    '.$name."
    \n"; - endif; - - echo "
    •  
    '.$ol['name'].''.($ol['extraText']).(!empty($ol['qty']) ? ' ('.$ol['qty'].')' : null)."

     

    '.$this->end.'

     

    '.Lang::quest('suggestedPl').Lang::main('colon').$this->suggestedPl."

     

    '.Lang::quest('suggestedPl', [$this->suggestedPl]).'
    + providedItem): - echo "
    \n"; - echo ' '.Lang::quest('providedItem').Lang::main('colon')."\n"; - echo " \n"; - echo ' '; - echo '\n"; + if ($this->providedItem): ?> + +
    + +
    '.$p['name'].''.($p['qty'] ? ' ('.$ol['qty'].')' : null)."
    + providedItem->renderContainer(20, $iconOffset, true); ?>
    + brick('mapper'); if ($this->details): - echo '

    '.Lang::quest('description')."

    \n" . $this->details."\n"; + echo '

    '.Lang::quest('description').'

    '.PHP_EOL; + echo ' '.$this->details.PHP_EOL; endif; if ($this->requestItems && $this->objectives): ?> -

    - + +

    + + offerReward && ($this->requestItems || $this->objectives)): ?> -

    - + +

    + + rewards): - $offset = 0; +if ([$spells, $items, $choice, $money] = $this->rewards): + echo '

    '.Lang::main('rewards').'

    '.PHP_EOL; - echo '

    '.Lang::main('rewards')."

    \n"; - - if (!empty($r['choice'])): - $this->brick('rewards', ['rewTitle' => Lang::quest('chooseItems'), 'rewards' => $r['choice'], 'offset' => $offset]); - $offset += count($r['choice']); + if ($choice): + $this->brick('rewards', ['rewTitle' => Lang::quest('rewardChoices'), 'rewards' => $choice, 'offset' => $iconOffset]); + $iconOffset += count($choice); endif; - if (!empty($r['spells'])): - if (!empty($r['choice'])): - echo "
    \n"; + if ($spells): + if ($choice): + echo '
    '.PHP_EOL; endif; - if (!empty($r['spells']['learn'])): - $this->brick('rewards', ['rewTitle' => Lang::quest('spellLearn'), 'rewards' => $r['spells']['learn'], 'offset' => $offset, 'extra' => $r['spells']['extra']]); - $offset += count($r['spells']['learn']); - elseif (!empty($r['spells']['cast'])): - $this->brick('rewards', ['rewTitle' => Lang::quest('spellCast'), 'rewards' => $r['spells']['cast'], 'offset' => $offset, 'extra' => $r['spells']['extra']]); - $offset += count($r['spells']['cast']); - endif; + $this->brick('rewards', ['rewTitle' => $spells['title'], 'rewards' => $spells['cast'], 'offset' => $iconOffset, 'extra' => $spells['extra']]); + $iconOffset += count($spells['cast']); endif; - if (!empty($r['items']) || !empty($r['money'])): - if (!empty($r['choice']) || !empty($r['spells'])): - echo "
    \n"; + if ($items || $money): + if ($choice || $spells): + echo '
    '.PHP_EOL; endif; - $addData = ['rewards' => !empty($r['items']) ? $r['items'] : null, 'offset' => $offset, 'extra' => !empty($r['money']) ? $r['money'] : null]; - $addData['rewTitle'] = empty($r['choice']) ? Lang::quest('receiveItems') : Lang::quest('receiveAlso'); - - $this->brick('rewards', $addData); + $this->brick('rewards', array( + 'rewTitle' => $choice ? Lang::quest('rewardAlso') : Lang::quest('rewardItems'), + 'rewards' => $items ?: null, + 'offset' => $iconOffset, + 'extra' => $money ?: null + )); endif; endif; -if ($g = $this->gains): - echo '

    '.Lang::main('gains')."

    \n"; - echo ' '.Lang::quest('gainsDesc').Lang::main('colon')."\n"; - echo "
      \n"; +if ([$xp, $rep, $title, $tp, $honor, $arena] = $this->gains): +?> - if (!empty($g['xp'])): - echo '
    • '.Lang::nf($g['xp']).' '.Lang::quest('experience')."
    • \n"; +

      + +
        + +
        '.Lang::nf($xp).' '.Lang::quest('experience').'
        '.PHP_EOL; endif; - if (!empty($g['rep'])): - foreach ($g['rep'] as $r): - if ($r['qty'][1] && User::isInGroup(U_GROUP_EMPLOYEE)) - $qty = $r['qty'][0] . sprintf(Util::$dfnString, Lang::faction('customRewRate'), ($r['qty'][1] > 0 ? '+' : '').$r['qty'][1]); - else - $qty = array_sum($r['qty']); - - echo '
      • '.($r['qty'][0] < 0 ? ''.$qty.'' : $qty).' '.Lang::npc('repWith').' '.$r['name']."
      • \n"; + if ($rep): + foreach ($rep as $r): + echo '
      • '.sprintf($r['qty'][0] < 0 ? '%s' : '%s', $r['qty'][1]).' '.Lang::npc('repWith').' '.$r['name'].'
      • '.PHP_EOL; endforeach; endif; - if (!empty($g['title'])): - echo '
      • '.Lang::quest('theTitle', [$g['title']])."
      • \n"; + if ($title): + echo '
      • '.Lang::quest('rewardTitle', $title).'
      • '.PHP_EOL; endif; - if (!empty($g['tp'])): - echo '
      • '.Lang::quest('bonusTalents', [$g['tp']])."
      • \n"; + if ($tp): + echo '
      • '.Lang::quest('bonusTalents', [$tp]).'
      • '.PHP_EOL; endif; - echo "
      \n"; + if ($arena || $honor): + echo '
    • '; + if ($honor[0]): + $a = ''.$honor[0].''; + $a = $honor[1] == SIDE_BOTH ? ''.$a.'' : $a; + echo ''.$a.''; + endif; + if ($arena): + echo ' '.$arena.''; + endif; + echo '
    • '.PHP_EOL; + endif; + + echo '
    '.PHP_EOL; endif; -$this->brick('mail'); +$this->brickIf($this->mail, 'mail', ['offset' => ++$iconOffset]); -if (!empty($this->transfer)): - echo "
    \n ".$this->transfer."\n"; +if ($this->transfer): + echo '
    '.PHP_EOL; + echo '
    '.PHP_EOL; + echo ' '.$this->transfer.PHP_EOL; endif; - ?> -

    + +

    brick('lvTabs', ['relTabs' => true]); +$this->brick('lvTabs'); $this->brick('contribute'); ?> diff --git a/template/pages/quests.tpl.php b/template/pages/quests.tpl.php index 803ed842..1dbeb100 100644 --- a/template/pages/quests.tpl.php +++ b/template/pages/quests.tpl.php @@ -1,6 +1,12 @@ brick('header'); -$f = $this->filter; // shorthand + namespace Aowow\Template; + + use \Aowow\Lang; + + /** @var PageTemplate $this */ + + $this->brick('header'); + $f = $this->filter->values; // shorthand ?>
    @@ -8,55 +14,57 @@ $f = $this->filter; // shorthand
    brick('announcement'); + $this->brick('announcement'); -$this->brick('pageTemplate', ['fi' => empty($f['query']) ? null : ['query' => $f['query'], 'menuItem' => 3]]); + $this->brick('pageTemplate', ['fiQuery' => $this->filter->query, 'fiMenuItem' => [3]]); ?> -
    +
    +
    + +brick('headIcons'); + + $this->brick('redButtons'); +?> + +

    h1; ?>

    +
    -
    +
    - + - + - +
    ucFirst(Lang::main('name')).Lang::main('colon'); ?> - - + +
     />  /> />  />
     /> - /> /> - /> - - + +
        /> - />    /> - />
     
    @@ -64,7 +72,7 @@ endforeach;
    - /> /> + /> />
    @@ -78,7 +86,7 @@ endforeach;
    -brick('filter', ['fi' => $f['initData']]); ?> +renderFilter(12); ?> brick('lvTabs'); ?> diff --git a/template/pages/roster.tpl.php b/template/pages/roster.tpl.php index 23760453..7159bf1b 100644 --- a/template/pages/roster.tpl.php +++ b/template/pages/roster.tpl.php @@ -1,32 +1,39 @@ -brick('header'); ?> +brick('header'); +?>
    brick('announcement'); - -$this->brick('pageTemplate'); + $this->brick('announcement'); + $this->brick('pageTemplate'); ?> +
    + brick('redButtons'); ?> -

    name; ?>

    +

    h1; ?>

    extraHTML)): - echo $this->extraHTML; - endif; + echo $this->extraHTML ?? ''; ?>
    + brick('lvTabs'); ?> +
    diff --git a/template/pages/screenshot.tpl.php b/template/pages/screenshot.tpl.php index f8d95650..60559162 100644 --- a/template/pages/screenshot.tpl.php +++ b/template/pages/screenshot.tpl.php @@ -1,24 +1,32 @@ -brick('header'); ?> +brick('header'); +?>
    brick('announcement'); + $this->brick('announcement'); -$this->brick('pageTemplate'); - -$this->brick('infobox'); + $this->brick('pageTemplate'); + $this->brick('infobox'); ?> +
    -

    name; ?>

    +

    h1; ?>

    @@ -31,7 +39,7 @@ $this->brick('infobox');
    -localizedBrick('ssReminder', User::$localeId); ?> +localizedBrick('ssReminder'); ?> diff --git a/template/pages/search.tpl.php b/template/pages/search.tpl.php index 3c4522a1..a66adbed 100644 --- a/template/pages/search.tpl.php +++ b/template/pages/search.tpl.php @@ -1,42 +1,55 @@ -brick('header'); ?> +brick('header'); +?>
    brick('announcement'); + $this->brick('announcement'); -$this->brick('pageTemplate'); + $this->brick('pageTemplate'); ?>
    - WowheadWowhead + lvTabs): - echo '

    '.Lang::main('foundResult').' '.Util::htmlEscape($this->search).''; - if ($this->invalid): - echo ''.sprintf(Lang::main('ignoredTerms'), implode(', ', $this->invalid)).''; + $this->brick('redButtons'); +if (count($this->lvTabs)): + echo '

    '.Lang::main('foundResult').' '.$this->search.''; + if ($this->invalidTerms): + echo ''.Lang::main('ignoredTerms', [$this->invalidTerms]).''; endif; - echo "

    \n"; + echo ''.PHP_EOL; ?> +
    + brick('lvTabs'); else: - echo '

    '.Lang::main('noResult').' '.Util::htmlEscape($this->search).''; - if ($this->invalid): - echo ''.sprintf(Lang::main('ignoredTerms'), implode(', ', $this->invalid)).''; + echo '

    '.Lang::main('noResult').' '.$this->search.''; + if ($this->invalidTerms): + echo ''.Lang::main('ignoredTerms', [$this->invalidTerms]).''; endif; - echo "

    \n"; + echo ''.PHP_EOL; ?> +
    +
    diff --git a/template/pages/sound-playlist.tpl.php b/template/pages/sound-playlist.tpl.php new file mode 100644 index 00000000..59ea456a --- /dev/null +++ b/template/pages/sound-playlist.tpl.php @@ -0,0 +1,69 @@ +brick('header'); +?> + +
    +
    +
    + +brick('announcement'); + + $this->brick('pageTemplate'); +?> + +
    +

    h1; ?>

    + +brick('markup', ['markup' => $this->article]); ?> + +
    +
    + +
    +
    +
    + +brick('footer'); ?> diff --git a/template/pages/sound.tpl.php b/template/pages/sound.tpl.php index f5141ade..619a7f28 100644 --- a/template/pages/sound.tpl.php +++ b/template/pages/sound.tpl.php @@ -1,4 +1,12 @@ -brick('header'); ?> +brick('header'); +?>
    @@ -11,72 +19,25 @@ ?>
    + brick('redButtons'); ?> -

    name; ?>

    +

    h1; ?>

    brick('article'); + $this->brick('markup', ['markup' => $this->article]); - if ($this->special): + $this->brickIf($this->map, 'mapper'); ?> -
    -
    - -
    - -map)): - $this->brick('mapper'); - endif; -?>
      -

      +

      brick('lvTabs', ['relTabs' => true]); + $this->brick('lvTabs'); $this->brick('contribute'); - endif; ?>
      diff --git a/template/pages/sounds.tpl.php b/template/pages/sounds.tpl.php index cbbf5376..1d9729a2 100644 --- a/template/pages/sounds.tpl.php +++ b/template/pages/sounds.tpl.php @@ -1,6 +1,12 @@ brick('header'); -$f = $this->filter; // shorthand + namespace Aowow\Template; + + use \Aowow\Lang; + + /** @var PageTemplate $this */ + + $this->brick('header'); + $f = $this->filter->values; // shorthand ?>
      @@ -8,33 +14,37 @@ $f = $this->filter; // shorthand
      brick('announcement'); + $this->brick('announcement'); -$this->brick('pageTemplate', ['fi' => empty($f['query']) ? null : ['query' => $f['query'], 'menuItem' => 101]]); + $this->brick('pageTemplate', ['fiQuery' => $this->filter->query, 'fiMenuItem' => [19]]); ?> -

      name; ?> brick('redButtons'); ?>

      -
      + +
      +
      + +brick('headIcons'); + + $this->brick('redButtons'); +?> + +

      h1; ?>

      +
      -
      +
      - + diff --git a/template/pages/spell.tpl.php b/template/pages/spell.tpl.php index de273886..1487db0d 100644 --- a/template/pages/spell.tpl.php +++ b/template/pages/spell.tpl.php @@ -1,4 +1,14 @@ -brick('header'); ?> +brick('header'); + + $iconOffset = 0; +?>
      @@ -16,56 +26,63 @@ brick('redButtons'); ?> -

      name; ?>

      +

      h1; ?>

      brick('tooltip'); + $this->brick('tooltip'); + +if ($this->tools): + echo '
      '.PHP_EOL; +endif; if ($this->reagents[1]): - if ($this->tools): - echo "
      \n"; - endif; - + $iconOffset += count($this->reagents[1]); $this->brick('reagentList', ['reagents' => $this->reagents[1], 'enhanced' => $this->reagents[0]]); +endif; - if ($this->tools): - echo "
      \n"; +if ($this->tools): + echo '
      '.PHP_EOL; - if ($this->reagents[0]): - echo "
      \n"; - endif; + if ($this->reagents[0]): + echo '
      '.PHP_EOL; + endif; ?> +

      ucFirst(Lang::main('name')).Lang::main('colon'); ?> - +
       /> />
      + tools as $i => $t): - echo ' \n"; - endforeach; + foreach ($this->tools as $icon): + echo $icon->renderContainer(20, $iconOffset, true); + endforeach; ?> +
      '.$t['name']."
      + reagents[0]): - echo "
      \n"; - endif; + if ($this->reagents[0]): + echo '
      '.PHP_EOL; endif; endif; ?> +
      -brick('article'); ?> - transfer)): - echo "
      \n ".$this->transfer."\n"; + $this->brick('markup', ['markup' => $this->article]); + +if ($this->transfer): + echo '
      '.PHP_EOL; + echo ' '.$this->transfer.PHP_EOL; endif; ?> @@ -86,34 +103,34 @@ endif; - duration) ? $this->duration : ''.Lang::main('n_a').'');?> + duration ?: ''.Lang::main('n_a').'');?> - school[1]) ? (User::isInGroup(U_GROUP_STAFF) ? sprintf(Util::$dfnString, $this->school[0], $this->school[1]) : $this->school[1]) : ''.Lang::main('n_a').'');?> + school ?: ''.Lang::main('n_a').'');?> - mechanic) ? $this->mechanic : ''.Lang::main('n_a').'');?> + mechanic ?:''.Lang::main('n_a').'');?> - dispel) ? $this->dispel : ''.Lang::main('n_a').'');?> + dispel ?: ''.Lang::main('n_a').'');?> - gcdCat) ? $this->gcdCat : ''.Lang::main('n_a').'');?> + gcdCat ?: ''.Lang::main('n_a').'');?> - powerCost) ? $this->powerCost : Lang::spell('_none'));?> + powerCost ?: Lang::spell('_none'));?> - range.' '.Lang::spell('_distUnit').' ('.$this->rangeName;?>) + range.Lang::spell('_distUnit').' ('.$this->rangeName.')';?> @@ -121,152 +138,185 @@ endif; - cooldown) ? $this->cooldown : ''.Lang::main('n_a').'');?> + cooldown ?: ''.Lang::main('n_a').'');?> '.Lang::spell('_gcd');?> gcd;?> -scaling), [[-1, -1, 0, 0], [0, 0, 0, 0]])): -?> - - - scaling as $k => $s): - if ($s > 0): - echo ' '.sprintf(Lang::spell('scaling', $k), $s * 100)."
      \n"; - endif; - endforeach; +if ($this->stances): ?> - - -stances)): -?> stances;?> + items)): +if ($this->items): ?> + - ', $this->items[0]), $this->items[1]) : $this->items[1]);?> + items;?> + effects as $i => $e): ?> + - + + '.Lang::spell('_value').Lang::main('colon').$e['value']; + if ($e['footer']): + echo '
      '.implode('
      ', $e['footer']).'
      '.PHP_EOL; endif; - if (isset($e['radius'])): - $smallBuf .= '
      '.Lang::spell('_radius').Lang::main('colon').$e['radius'].' '.Lang::spell('_distUnit'); - endif; - - if (isset($e['interval'])): - $smallBuf .= '
      '.Lang::spell('_interval').Lang::main('colon').$e['interval']; - endif; - - if (isset($e['mechanic'])): - $smallBuf .= '
      '.Lang::game('mechanic') .Lang::main('colon').$e['mechanic']; - endif; - - if (isset($e['procData'])): - $smallBuf .= '
      '; - - if ($e['procData'][0] < 0): - $smallBuf .= sprintf(Lang::spell('ppm'), Lang::nf(-$e['procData'][0], 1)); - elseif ($e['procData'][0] < 100.0): - $smallBuf .= Lang::spell('procChance').Lang::main('colon').$e['procData'][0].'%'; - endif; - - if ($e['procData'][1]): - if ($e['procData'][0] < 100.0): - $smallBuf .= '
      '; - endif; - $smallBuf .= sprintf(Lang::game('cooldown'), $e['procData'][1]); - endif; - endif; - - if ($smallBuf): - echo "".$smallBuf."\n"; - endif; - - if (isset($e['markup'])): + if ($e['markup']): echo '
      '; endif; - if (isset($e['icon'])): + if ($e['icon']): ?> + - -'.$e['icon']['name']."\n"; - else: - echo ' \n"; - endif; -?> + renderContainer(iconIdxOffset: $iconTabIdx); ?>
      '.(strpos($e['icon']['name'], '#') ? $e['icon']['name'] : sprintf('%s', $e['icon']['id'], $e['icon']['name']))."
      + $si, 'spellName' => $sn, 'item' => $it, 'icon' => $ic, 'chance' => $ch] = $e['perfectItem']; ?> - - -
      + + + + renderContainer(0, $iconTabIdx, true); ?>
      + +
      + +'.Lang::spell('_seeMore').' brick('lvTabs', ['relTabs' => true]); +$this->brick('lvTabs'); $this->brick('contribute'); ?> +
      diff --git a/template/pages/spells.tpl.php b/template/pages/spells.tpl.php index 21a990dc..e3452808 100644 --- a/template/pages/spells.tpl.php +++ b/template/pages/spells.tpl.php @@ -1,6 +1,12 @@ brick('header'); -$f = $this->filter; // shorthand + namespace Aowow\Template; + + use \Aowow\Lang; + + /** @var PageTemplate $this */ + + $this->brick('header'); + $f = $this->filter->values; // shorthand ?>
      @@ -8,118 +14,97 @@ $f = $this->filter; // shorthand
      brick('announcement'); + $this->brick('announcement'); -$this->brick('pageTemplate', ['fi' => empty($f['query']) ? null : ['query' => $f['query'], 'menuItem' => 1]]); + $this->brick('pageTemplate', ['fiQuery' => $this->filter->query, 'fiMenuItem' => [1]]); ?> -
      +
      +
      + +brick('headIcons'); + + $this->brick('redButtons'); +?> + +

      h1; ?>

      +
      + classPanel): ?>
      -
      +
      ucFirst(Lang::game('class')).Lang::main('colon'); ?>
      + glyphPanel): ?> +
      -
      +
      + + - + - + - + - + @@ -130,7 +115,7 @@ endforeach;
      - /> /> + /> />
      @@ -144,7 +129,7 @@ endforeach;
      -brick('filter', ['fi' => $f['initData']]); ?> +renderFilter(12); ?> brick('lvTabs'); ?> diff --git a/template/pages/talent.tpl.php b/template/pages/talent.tpl.php index 61802b87..2bb03f46 100644 --- a/template/pages/talent.tpl.php +++ b/template/pages/talent.tpl.php @@ -1,4 +1,10 @@ -brick('header'); ?> +brick('header'); +?>
      @@ -10,14 +16,14 @@ $this->brick('pageTemplate'); ?> -
      -
      -

      tcType == 'tc' ? Lang::main('chooseClass') : Lang::main('chooseFamily')) . Lang::main('colon'); ?>

      +
      +
      +

      chooseType; ?>

      -
      +
      diff --git a/template/pages/text-page-generic.tpl.php b/template/pages/text-page-generic.tpl.php index f68388d4..4c05960c 100644 --- a/template/pages/text-page-generic.tpl.php +++ b/template/pages/text-page-generic.tpl.php @@ -1,71 +1,54 @@ -brick('header'); ?> +brick('header'); +?>
      brick('announcement'); + $this->brick('announcement'); -$this->brick('pageTemplate'); + $this->brick('pageTemplate'); -if (isset($this->notFound)): -?> -doResync)): +if ([$typeStr, $id] = $this->doResync): ?> +
      - + -
      + -

      notFound['title']; ?>

      -
      notFound['msg']; ?>
      -doResync)): -?> - - -
      -
      -inputbox): + $this->brick(...$this->inputbox); // $templateName, [$templateVars] else: ?> +
      -

      name; ?>

      +h1 ? '

      '.$this->h1.'

      ' : '');?> brick('article'); + $this->brick('markup', ['markup' => $this->article]); - if (isset($this->extraText)): + $this->brick('markup', ['markup' => $this->extraText]); + + echo $this->extraHTML ?? ''; ?> -
      - -
      + extraHTML)): - echo $this->extraHTML; - endif; - endif; ?> +
      diff --git a/template/pages/user.tpl.php b/template/pages/user.tpl.php index a5c3182d..81bf0372 100644 --- a/template/pages/user.tpl.php +++ b/template/pages/user.tpl.php @@ -1,4 +1,12 @@ -brick('header'); ?> +brick('header'); +?>
      @@ -8,36 +16,56 @@ $this->brick('announcement'); $this->brick('pageTemplate'); +?> + + +brick('infobox'); ?> - -
      + +userIcon): +?> +
      -

      name; ?>

      -
      +

      h1; ?>

      -

      -
      user['description'])): + +

      h1; ?>

      + + + +

      +
      description): ?> -
      - + +
      + +
      - + ?>
      + + +lvTabs)): ?> + + + + +
      -brick('lvTabs', ['relTabs' => true]); ?> +brick('lvTabs'); ?>
      diff --git a/template/pages/video.tpl.php b/template/pages/video.tpl.php new file mode 100644 index 00000000..a069c3ab --- /dev/null +++ b/template/pages/video.tpl.php @@ -0,0 +1,85 @@ +brick('header'); +?> + +
      +
      +
      + +brick('announcement'); + + $this->brick('pageTemplate'); + + $this->brick('infobox'); +?> + +
      +

      h1; ?>

      + +

      viTitle;?>

      +
      +
      + + +
      + + +
      +
      + + + + + +
      +
      +
      + +brick('footer'); ?>
      ucFirst(Lang::main('name')).Lang::main('colon'); ?> - - + +
       />  /> />  />
       /> - /> /> - /> - - + +
          /> - />    /> - />
      ucFirst(Lang::game('race')).Lang::main('colon'); ?>