diff --git a/.gitattributes b/.gitattributes deleted file mode 100644 index c751ac98..00000000 --- a/.gitattributes +++ /dev/null @@ -1,14 +0,0 @@ -* 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 deleted file mode 100644 index f4bb6b76..00000000 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -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 8fbefabd..f72b08f2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,36 +1,30 @@ # cache -/cache/* - -# extract from MPQ -/setup/mpqdata/* +/cache/template/* +/setup/generated/alphaMaps/*.png # generated files /static/js/profile_all.js -/static/js/global.js +/static/js/locale.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/* -/datasets/* - -# extracted sounds -/static/wowsounds/* +/config/config.php +# /datasets/item-scaling # extracted images /static/images/wow/icons/large/* /static/images/wow/icons/medium/* /static/images/wow/icons/small/* /static/images/wow/icons/tiny/* -!/static/images/wow/icons/tiny/quest_[end|start] -/static/images/wow/hunterpettalents/* -/static/images/wow/Interface/* +!/static/images/wow/icons/tiny/quest_* +/static/images/wow/hunterpettalents/icons* +/static/images/wow/interface/* /static/images/wow/loadingscreens/* /static/images/wow/maps/* !/static/images/wow/maps/overlay* /static/images/wow/talents/icons/* -/static/images/wow/talents/backgrounds/* # modelviewer (~7GB data) /static/modelviewer/models/* @@ -42,8 +36,4 @@ /static/uploads/screenshots/* /static/uploads/signatures/* /static/uploads/temp/* -/static/uploads/guide/images/* -# composer -/includes/libs/* -composer.phar diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 00000000..3af5268b --- /dev/null +++ b/.gitmodules @@ -0,0 +1,6 @@ +[submodule "includes/tools/MPQExtractor"] + path = includes/tools/MPQExtractor + url = https://github.com/iamcal/MPQExtractor.git +[submodule "includes/tools/BLPConverter"] + path = includes/tools/BLPConverter + url = https://github.com/Kanma/BLPConverter.git diff --git a/.htaccess b/.htaccess index 0e0530c5..e0ea4395 100644 --- a/.htaccess +++ b/.htaccess @@ -1,19 +1,11 @@ Order Deny,Allow - - Deny from all + + Deny from all - Allow from all + Allow from all - - ForceType application/x-httpd-php - - - - ForceType application/x-httpd-php - - # Block view of some folders Options -Indexes DirectoryIndex index.php @@ -21,15 +13,13 @@ DirectoryIndex index.php # Support for UTF8 AddDefaultCharset utf8 - CharsetDisable on - CharsetRecodeMultipartForms Off + CharsetDisable on + CharsetRecodeMultipartForms Off -# UHD screenshots can get pretty large (cannot be set in config) - - php_value upload_max_filesize 20M - php_value post_max_size 25M - +# 5MB should be enough for the largest screenshots in the land + php_value default_charset UTF-8 + php_value upload_max_filesize 5M RewriteEngine on # RewriteBase /~user/localPath/ # enable if the rules do not work, when they should diff --git a/README b/README new file mode 100644 index 00000000..99f0bc72 Binary files /dev/null and b/README differ diff --git a/README.md b/README.md deleted file mode 100644 index e182aced..00000000 --- a/README.md +++ /dev/null @@ -1,144 +0,0 @@ -![logo](static/images/logos/home.png) - - -## Build Status -![fuck it ship it](https://forthebadge.com/badges/fuck-it-ship-it.svg) - - -## Introduction - -AoWoW is a Database tool for World of Warcraft v3.3.5 (build 12340) -It is based upon the other famous Database tool for WoW, featuring the red smiling rocket. -While the first releases can be found as early as 2008, today it is impossible to say who created this project. -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 purposes of any kind! - - -## Requirements - -+ 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) / (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/) / (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 TrinityCore server (and running it once) will greatly increase the accuracy of spawn points - > Calculate.Creature.Zone.Area.Data = 1 - > Calculate.Gameobject.Zone.Area.Data = 1 - - -## Install - -#### 1. Acquire the required repositories -`git clone git@github.com:Sarjuuk/aowow.git aowow` -`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 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 - * `cache/` - * `config/` - * `static/download/` - * `static/widgets/` - * `static/js/` - * `static/uploads/` - * `static/images/wow/` - * `datasets/` - -#### 4. Extract the client archives (MPQs) -Extract the following directories from the client archives into `setup/mpqdata/`, while maintaining patch order (base mpq -> patch-mpq: 1 -> 9 -> A -> Z). The required paths are scattered across the archives. Overwrite older files if asked to. - .. for every locale you are going to use: - > \/DBFilesClient/ - > \/Interface/WorldMap/ - > \/Interface/FrameXML/GlobalStrings.lua - - .. once is enough (still apply the localeCode though): - > \/Interface/TalentFrame/ - > \/Interface/Icons/ - > \/Interface/Spellbook/ - > \/Interface/PaperDoll/ - > \/Interface/Glues/CharacterCreate/ - > \/Interface/Pictures - > \/Interface/PvPRankBadges - > \/Interface/FlavorImages - > \/Interface/Calendar/Holidays/ - > \/Sound/ - -#### 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. 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. - - -## Troubleshooting - -Q: The Page appears white, without any styles. -A: The static content is not being displayed. You are either using SSL and AoWoW is unable to detect it or STATIC_HOST is not defined properly. Either way this can be fixed via config `php aowow --configure` - -Q: Fatal error: Can't inherit abstract function \ (previously declared abstract in \) in \ -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 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 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 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 -@Sarjuuk: maintainer of the project - - -## Special Thanks -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](https://forthebadge.com/badges/uses-badges.svg) diff --git a/aowow b/aowow old mode 100755 new mode 100644 index 3e1adb62..e3a0fee5 --- a/aowow +++ b/aowow @@ -1,18 +1,12 @@ -#!/usr/bin/env php - + diff --git a/composer.json b/composer.json deleted file mode 100644 index fc43b9aa..00000000 --- a/composer.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "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 deleted file mode 100644 index 8d9a0b87..00000000 --- a/composer.lock +++ /dev/null @@ -1,454 +0,0 @@ -{ - "_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 new file mode 100644 index 00000000..b4036d97 --- /dev/null +++ b/config/config.php.in @@ -0,0 +1,49 @@ + '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 6158987f..5be443b9 100644 --- a/config/extAuth.php.in +++ b/config/extAuth.php.in @@ -4,16 +4,13 @@ if (!defined('AOWOW_REVISION')) die('illegal access'); - function extAuth(string &$usernameOrEmail, #[\SensitiveParameter] string $password, int &$userId = 0, int &$userGroup = -1) : int + function extAuth($user, $pass, &$userId = 0) { /* insert some auth mechanism here - 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) + see defines for usable return values + set userId for identification */ return AUTH_INTERNAL_ERR; diff --git a/datasets/weight-presets b/datasets/weight-presets new file mode 100644 index 00000000..9faeb6d1 --- /dev/null +++ b/datasets/weight-presets @@ -0,0 +1,74 @@ +var wt_presets = { + 1: { + pve: { + arms: {__icon:'ability_rogue_eviscerate'}, + fury: {__icon:'ability_warrior_innerrage',exprtng:100,str:82,critstrkrtng:66,agi:53,armorpenrtng:52,hitrtng:48,hastertng:36,atkpwr:31,armor:5}, + prot: {__icon:'ability_warrior_defensivestance',sta:100,dodgertng:90,defrtng:86,block:81,agi:67,parryrtng:67,blockrtng:48,str:48,exprtng:19,hitrtng:10,armorpenrtng:10,critstrkrtng:7,armor:6,hastertng:1,atkpwr:1} + } + }, + 2: { + pve: { + holy: {__icon:'spell_holy_holybolt',int:100,manargn:88,splpwr:58,critstrkrtng:46,hastertng:35}, + prot: {__icon:'ability_paladin_shieldofthetemplar',sta:100,dodgertng:94,block:86,defrtng:86,exprtng:79,agi:76,parryrtng:76,hitrtng:58,blockrtng:52,str:50,armor:6,atkpwr:6,splpwr:4,critstrkrtng:3}, + retrib: {__icon:'spell_holy_auraoflight',mledps:470,hitrtng:100,str:80,exprtng:66,critstrkrtng:40,atkpwr:34,agi:32,hastertng:30,armorpenrtng:22,splpwr:9} + } + }, + 3: { + pve: { + beast: {__icon:'ability_hunter_beasttaming',rgddps:213,hitrtng:100,agi:58,critstrkrtng:40,int:37,atkpwr:30,armorpenrtng:28,hastertng:21}, + marks: {__icon:'ability_marksmanship',rgddps:379,hitrtng:100,agi:74,critstrkrtng:57,armorpenrtng:40,int:39,atkpwr:32,hastertng:24}, + surv: {__icon:'ability_hunter_swiftstrike',rgddps:181,hitrtng:100,agi:76,critstrkrtng:42,int:35,hastertng:31,atkpwr:29,armorpenrtng:26} + } + }, + 4: { + pve: { + assas: {__icon:'ability_rogue_eviscerate',mledps:170,agi:100,exprtng:87,hitrtng:83,critstrkrtng:81,atkpwr:65,armorpenrtng:65,hastertng:64,str:55}, + combat: {__icon:'ability_backstab',mledps:220,armorpenrtng:100,agi:100,exprtng:82,hitrtng:80,critstrkrtng:75,hastertng:73,str:55,atkpwr:50}, + subtle: {__icon:'ability_stealth',mledps:228,exprtng:100,agi:100,hitrtng:80,armorpenrtng:75,critstrkrtng:75,hastertng:75,str:55,atkpwr:50} + } + }, + 5: { + pve: { + disc: {__icon:'spell_holy_wordfortitude',splpwr:100,manargn:67,int:65,hastertng:59,critstrkrtng:48,spi:22}, + holy: {__icon:'spell_holy_guardianspirit',manargn:100,int:69,splpwr:60,spi:52,critstrkrtng:38,hastertng:31}, + shadow: {__icon:'spell_shadow_shadowwordpain',hitrtng:100,shasplpwr:76,splpwr:76,critstrkrtng:54,hastertng:50,spi:16,int:16} + } + }, + 6: { + pve: { + blooddps: {__icon:'spell_deathknight_bloodpresence',mledps:360,armorpenrtng:100,str:99,hitrtng:91,exprtng:90,critstrkrtng:57,hastertng:55,atkpwr:36,armor:1}, + frostdps: {__icon:'spell_deathknight_frostpresence',mledps:337,hitrtng:100,str:97,exprtng:81,armorpenrtng:61,critstrkrtng:45,atkpwr:35,hastertng:28,armor:1}, + frosttank: {__icon:'spell_deathknight_frostpresence',mledps:419,parryrtng:100,hitrtng:97,str:96,defrtng:85,exprtng:69,dodgertng:61,agi:61,sta:61,critstrkrtng:49,atkpwr:41,armorpenrtng:31,armor:5}, + unholydps: {__icon:'spell_deathknight_unholypresence',mledps:209,str:100,hitrtng:66,exprtng:51,hastertng:48,critstrkrtng:45,atkpwr:34,armorpenrtng:32,armor:1} + } + }, + 7: { + pve: { + elem: {__icon:'spell_nature_lightning',hitrtng:100,splpwr:60,hastertng:56,critstrkrtng:40,int:11}, + enhance: {__icon:'spell_nature_lightningshield',mledps:135,hitrtng:100,exprtng:84,agi:55,int:55,critstrkrtng:55,hastertng:42,str:35,atkpwr:32,splpwr:29,armorpenrtng:26}, + resto: {__icon:'spell_nature_magicimmunity',manargn:100,int:85,splpwr:77,critstrkrtng:62,hastertng:35} + } + }, + 8: { + pve: { + arcane: {__icon:'spell_holy_magicalsentry',hitrtng:100,hastertng:54,arcsplpwr:49,splpwr:49,critstrkrtng:37,int:34,frosplpwr:24,firsplpwr:24,spi:14}, + fire: {__icon:'spell_fire_firebolt02',hitrtng:100,hastertng:53,firsplpwr:46,splpwr:46,critstrkrtng:43,frosplpwr:23,arcsplpwr:23,int:13}, + frost: {__icon:'spell_frost_frostbolt02',hitrtng:100,hastertng:42,frosplpwr:39,splpwr:39,arcsplpwr:19,firsplpwr:19,critstrkrtng:19,int:6} + } + }, + 9: { + pve: { + afflic: {__icon:'spell_shadow_deathcoil',hitrtng:100,shasplpwr:72,splpwr:72,hastertng:61,critstrkrtng:38,firsplpwr:36,spi:34,int:15}, + demo: {__icon:'spell_shadow_metamorphosis',hitrtng:100,hastertng:50,firsplpwr:45,shasplpwr:45,splpwr:45,critstrkrtng:31,spi:29,int:13}, + destro: {__icon:'spell_shadow_rainoffire',hitrtng:100,firsplpwr:47,splpwr:47,hastertng:46,spi:26,shasplpwr:23,critstrkrtng:16,int:13} + } + }, + 11: { + pve: { + balance: {__icon:'spell_nature_starfall',hitrtng:100,splpwr:66,hastertng:54,critstrkrtng:43,spi:22,int:22}, + feraltank: {__icon:'ability_racial_bearform',agi:100,sta:75,dodgertng:65,defrtng:60,exprtng:16,str:10,armor:10,hitrtng:8,hastertng:5,atkpwr:4,feratkpwr:4,critstrkrtng:3}, + resto: {__icon:'spell_nature_healingtouch',splpwr:100,manargn:73,hastertng:57,int:51,spi:32,critstrkrtng:11}, + feraldps: {__icon:'ability_druid_catform',agi:100,armorpenrtng:90,str:80,critstrkrtng:55,exprtng:50,hitrtng:50,feratkpwr:40,atkpwr:40,hastertng:35} + } + } +}; diff --git a/datasets/zones b/datasets/zones new file mode 100644 index 00000000..e6766d53 --- /dev/null +++ b/datasets/zones @@ -0,0 +1,46 @@ +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 deleted file mode 100644 index 524dfc0c..00000000 --- a/endpoints/aboutus/aboutus.php +++ /dev/null @@ -1,34 +0,0 @@ -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 deleted file mode 100644 index 5c4ed8dd..00000000 --- a/endpoints/account/account.php +++ /dev/null @@ -1,174 +0,0 @@ -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 deleted file mode 100644 index e56fad9e..00000000 --- a/endpoints/account/activate.php +++ /dev/null @@ -1,73 +0,0 @@ - ['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 deleted file mode 100644 index c0cdcbdc..00000000 --- a/endpoints/account/confirm-delete.php +++ /dev/null @@ -1,128 +0,0 @@ - [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 deleted file mode 100644 index 05a4f217..00000000 --- a/endpoints/account/confirm-email-address.php +++ /dev/null @@ -1,62 +0,0 @@ - ['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 deleted file mode 100644 index bc972f70..00000000 --- a/endpoints/account/confirm-password.php +++ /dev/null @@ -1,60 +0,0 @@ - ['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 deleted file mode 100644 index 4e71147b..00000000 --- a/endpoints/account/delete-icon.php +++ /dev/null @@ -1,47 +0,0 @@ - ['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 deleted file mode 100644 index 04a46e2b..00000000 --- a/endpoints/account/delete.php +++ /dev/null @@ -1,71 +0,0 @@ - ['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 deleted file mode 100644 index 17a3a032..00000000 --- a/endpoints/account/exclude.php +++ /dev/null @@ -1,81 +0,0 @@ - ['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 deleted file mode 100644 index e459d253..00000000 --- a/endpoints/account/favorites.php +++ /dev/null @@ -1,52 +0,0 @@ - ['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 deleted file mode 100644 index c0ab9581..00000000 --- a/endpoints/account/forgot-password.php +++ /dev/null @@ -1,101 +0,0 @@ - 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 deleted file mode 100644 index c1cff916..00000000 --- a/endpoints/account/forgot-username.php +++ /dev/null @@ -1,100 +0,0 @@ - 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 deleted file mode 100644 index e7bd9840..00000000 --- a/endpoints/account/forum-avatar.php +++ /dev/null @@ -1,108 +0,0 @@ - ['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 deleted file mode 100644 index 7374a210..00000000 --- a/endpoints/account/premium-border.php +++ /dev/null @@ -1,41 +0,0 @@ - ['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 deleted file mode 100644 index c88ff2bb..00000000 --- a/endpoints/account/rename-icon.php +++ /dev/null @@ -1,36 +0,0 @@ - ['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 deleted file mode 100644 index 28fe9c51..00000000 --- a/endpoints/account/resend-submit.php +++ /dev/null @@ -1,52 +0,0 @@ - ['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 deleted file mode 100644 index 7c1fecbf..00000000 --- a/endpoints/account/resend.php +++ /dev/null @@ -1,98 +0,0 @@ - ['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 deleted file mode 100644 index 4d71153f..00000000 --- a/endpoints/account/reset-password.php +++ /dev/null @@ -1,121 +0,0 @@ - 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 deleted file mode 100644 index 106fb6f0..00000000 --- a/endpoints/account/revert-email-address.php +++ /dev/null @@ -1,62 +0,0 @@ - ['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 deleted file mode 100644 index ed8fbf07..00000000 --- a/endpoints/account/signin.php +++ /dev/null @@ -1,148 +0,0 @@ - ['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 deleted file mode 100644 index 73374d1b..00000000 --- a/endpoints/account/signout.php +++ /dev/null @@ -1,40 +0,0 @@ - ['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 deleted file mode 100644 index 2fa29278..00000000 --- a/endpoints/account/signup.php +++ /dev/null @@ -1,163 +0,0 @@ - ['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 deleted file mode 100644 index 2bc1ed2d..00000000 --- a/endpoints/account/update-community-settings.php +++ /dev/null @@ -1,48 +0,0 @@ - ['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 deleted file mode 100644 index 0cf750be..00000000 --- a/endpoints/account/update-email.php +++ /dev/null @@ -1,80 +0,0 @@ - ['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 deleted file mode 100644 index 73a8cda0..00000000 --- a/endpoints/account/update-general-settings.php +++ /dev/null @@ -1,60 +0,0 @@ - ['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 deleted file mode 100644 index 8038f8be..00000000 --- a/endpoints/account/update-password.php +++ /dev/null @@ -1,86 +0,0 @@ - ['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 deleted file mode 100644 index 7876d605..00000000 --- a/endpoints/account/update-username.php +++ /dev/null @@ -1,61 +0,0 @@ - ['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 deleted file mode 100644 index 8bae7835..00000000 --- a/endpoints/account/weightscales.php +++ /dev/null @@ -1,121 +0,0 @@ - ['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 deleted file mode 100644 index 6c63fd11..00000000 --- a/endpoints/achievement/achievement.php +++ /dev/null @@ -1,520 +0,0 @@ - 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 deleted file mode 100644 index 05e78fc6..00000000 --- a/endpoints/achievement/achievement_power.php +++ /dev/null @@ -1,50 +0,0 @@ - ['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 deleted file mode 100644 index 6597306f..00000000 --- a/endpoints/achievements/achievements.php +++ /dev/null @@ -1,170 +0,0 @@ - ['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 deleted file mode 100644 index b1fdc774..00000000 --- a/endpoints/admin/announcements.php +++ /dev/null @@ -1,68 +0,0 @@ - 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 deleted file mode 100644 index 26b78e29..00000000 --- a/endpoints/admin/comment.php +++ /dev/null @@ -1,51 +0,0 @@ - ['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 deleted file mode 100644 index 90ee553e..00000000 --- a/endpoints/admin/guide.php +++ /dev/null @@ -1,81 +0,0 @@ - ['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 deleted file mode 100644 index 26d6f844..00000000 --- a/endpoints/admin/guides.php +++ /dev/null @@ -1,46 +0,0 @@ - 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 deleted file mode 100644 index 4f0f1b00..00000000 --- a/endpoints/admin/out-of-date.php +++ /dev/null @@ -1,34 +0,0 @@ - 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 deleted file mode 100644 index 1fd04b8c..00000000 --- a/endpoints/admin/phpinfo.php +++ /dev/null @@ -1,80 +0,0 @@ - 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 deleted file mode 100644 index a64c8df8..00000000 --- a/endpoints/admin/reports.php +++ /dev/null @@ -1,29 +0,0 @@ - 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 deleted file mode 100644 index c8b87ba8..00000000 --- a/endpoints/admin/screenshots.php +++ /dev/null @@ -1,68 +0,0 @@ - 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 deleted file mode 100644 index 5bb528c2..00000000 --- a/endpoints/admin/screenshots_approve.php +++ /dev/null @@ -1,60 +0,0 @@ - ['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 deleted file mode 100644 index bd61a07c..00000000 --- a/endpoints/admin/screenshots_delete.php +++ /dev/null @@ -1,62 +0,0 @@ - ['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 deleted file mode 100644 index 2dca89cc..00000000 --- a/endpoints/admin/screenshots_editalt.php +++ /dev/null @@ -1,32 +0,0 @@ - ['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 deleted file mode 100644 index bd884d42..00000000 --- a/endpoints/admin/screenshots_list.php +++ /dev/null @@ -1,23 +0,0 @@ - ['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 deleted file mode 100644 index 752469d1..00000000 --- a/endpoints/admin/screenshots_manage.php +++ /dev/null @@ -1,31 +0,0 @@ - ['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 deleted file mode 100644 index 03fd0d5d..00000000 --- a/endpoints/admin/screenshots_relocate.php +++ /dev/null @@ -1,48 +0,0 @@ - ['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 deleted file mode 100644 index d95ac874..00000000 --- a/endpoints/admin/screenshots_sticky.php +++ /dev/null @@ -1,72 +0,0 @@ - ['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 deleted file mode 100644 index 16db9400..00000000 --- a/endpoints/admin/siteconfig.php +++ /dev/null @@ -1,113 +0,0 @@ - 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 deleted file mode 100644 index a99e77a2..00000000 --- a/endpoints/admin/siteconfig_add.php +++ /dev/null @@ -1,34 +0,0 @@ - ['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 deleted file mode 100644 index cef906d0..00000000 --- a/endpoints/admin/siteconfig_remove.php +++ /dev/null @@ -1,30 +0,0 @@ - ['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 deleted file mode 100644 index 5afe0bec..00000000 --- a/endpoints/admin/siteconfig_update.php +++ /dev/null @@ -1,34 +0,0 @@ - ['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 deleted file mode 100644 index 8b6931ab..00000000 --- a/endpoints/admin/spawn-override.php +++ /dev/null @@ -1,105 +0,0 @@ - ['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 deleted file mode 100644 index 04956306..00000000 --- a/endpoints/admin/videos.php +++ /dev/null @@ -1,68 +0,0 @@ - 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 deleted file mode 100644 index efa12210..00000000 --- a/endpoints/admin/videos_approve.php +++ /dev/null @@ -1,44 +0,0 @@ - ['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 deleted file mode 100644 index 7a404124..00000000 --- a/endpoints/admin/videos_delete.php +++ /dev/null @@ -1,43 +0,0 @@ - ['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 deleted file mode 100644 index d6523f6d..00000000 --- a/endpoints/admin/videos_edittitle.php +++ /dev/null @@ -1,31 +0,0 @@ - ['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 deleted file mode 100644 index 7b02d12b..00000000 --- a/endpoints/admin/videos_list.php +++ /dev/null @@ -1,23 +0,0 @@ - ['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 deleted file mode 100644 index fb5180f7..00000000 --- a/endpoints/admin/videos_manage.php +++ /dev/null @@ -1,31 +0,0 @@ - ['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 deleted file mode 100644 index 05fd749e..00000000 --- a/endpoints/admin/videos_order.php +++ /dev/null @@ -1,57 +0,0 @@ - ['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 deleted file mode 100644 index 12230906..00000000 --- a/endpoints/admin/videos_relocate.php +++ /dev/null @@ -1,49 +0,0 @@ - ['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 deleted file mode 100644 index 40c537af..00000000 --- a/endpoints/admin/videos_sticky.php +++ /dev/null @@ -1,56 +0,0 @@ - ['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 deleted file mode 100644 index 3fa4a0ab..00000000 --- a/endpoints/admin/weight-presets.php +++ /dev/null @@ -1,54 +0,0 @@ - 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 deleted file mode 100644 index c73c17ff..00000000 --- a/endpoints/admin/weight-presets_save.php +++ /dev/null @@ -1,74 +0,0 @@ - ['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 deleted file mode 100644 index c8a02e54..00000000 --- a/endpoints/areatrigger/areatrigger.php +++ /dev/null @@ -1,141 +0,0 @@ -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 deleted file mode 100644 index 0d681c95..00000000 --- a/endpoints/areatriggers/areatriggers.php +++ /dev/null @@ -1,102 +0,0 @@ - ['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 deleted file mode 100644 index fc1e1a52..00000000 --- a/endpoints/arena-team/arena-team.php +++ /dev/null @@ -1,148 +0,0 @@ - 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 deleted file mode 100644 index ac57929d..00000000 --- a/endpoints/arena-team/resync.php +++ /dev/null @@ -1,48 +0,0 @@ - ['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 deleted file mode 100644 index ad89f016..00000000 --- a/endpoints/arena-team/status.php +++ /dev/null @@ -1,29 +0,0 @@ - ['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 deleted file mode 100644 index 9983422d..00000000 --- a/endpoints/arena-teams/arena-teams.php +++ /dev/null @@ -1,160 +0,0 @@ - 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 deleted file mode 100644 index 47be4379..00000000 --- a/endpoints/class/class.php +++ /dev/null @@ -1,304 +0,0 @@ -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 deleted file mode 100644 index 07e1f6f9..00000000 --- a/endpoints/classes/classes.php +++ /dev/null @@ -1,48 +0,0 @@ -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 deleted file mode 100644 index 6b9d7931..00000000 --- a/endpoints/comment/add-reply.php +++ /dev/null @@ -1,51 +0,0 @@ - ['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 deleted file mode 100644 index 78dffad6..00000000 --- a/endpoints/comment/add.php +++ /dev/null @@ -1,79 +0,0 @@ - ['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 deleted file mode 100644 index 096a62dd..00000000 --- a/endpoints/comment/delete-reply.php +++ /dev/null @@ -1,41 +0,0 @@ - ['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 deleted file mode 100644 index 582a0b01..00000000 --- a/endpoints/comment/delete.php +++ /dev/null @@ -1,50 +0,0 @@ - ['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 deleted file mode 100644 index 7c79f29a..00000000 --- a/endpoints/comment/detach-reply.php +++ /dev/null @@ -1,30 +0,0 @@ - ['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 deleted file mode 100644 index cd035abc..00000000 --- a/endpoints/comment/downvote-reply.php +++ /dev/null @@ -1,55 +0,0 @@ - ['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 deleted file mode 100644 index 7a6aed50..00000000 --- a/endpoints/comment/edit-reply.php +++ /dev/null @@ -1,65 +0,0 @@ - ['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 deleted file mode 100644 index a3a2e3cb..00000000 --- a/endpoints/comment/edit.php +++ /dev/null @@ -1,63 +0,0 @@ - ['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 deleted file mode 100644 index 55589b45..00000000 --- a/endpoints/comment/flag-reply.php +++ /dev/null @@ -1,45 +0,0 @@ - ['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 deleted file mode 100644 index 70cb3dfd..00000000 --- a/endpoints/comment/out-of-date.php +++ /dev/null @@ -1,58 +0,0 @@ - ['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 deleted file mode 100644 index d080e71e..00000000 --- a/endpoints/comment/rating.php +++ /dev/null @@ -1,31 +0,0 @@ - ['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 deleted file mode 100644 index 2de0ed02..00000000 --- a/endpoints/comment/show-replies.php +++ /dev/null @@ -1,24 +0,0 @@ - ['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 deleted file mode 100644 index 16fcf3a7..00000000 --- a/endpoints/comment/sticky.php +++ /dev/null @@ -1,34 +0,0 @@ - ['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 deleted file mode 100644 index 1d21a6a1..00000000 --- a/endpoints/comment/undelete.php +++ /dev/null @@ -1,49 +0,0 @@ - ['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 deleted file mode 100644 index f6e84a34..00000000 --- a/endpoints/comment/upvote-reply.php +++ /dev/null @@ -1,55 +0,0 @@ - ['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 deleted file mode 100644 index 1bf3b1ff..00000000 --- a/endpoints/comment/vote.php +++ /dev/null @@ -1,84 +0,0 @@ - ['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 deleted file mode 100644 index 67604b4f..00000000 --- a/endpoints/compare/compare.php +++ /dev/null @@ -1,116 +0,0 @@ - ['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 deleted file mode 100644 index ace3fa02..00000000 --- a/endpoints/contactus/contactus.php +++ /dev/null @@ -1,49 +0,0 @@ - ['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 deleted file mode 100644 index 5b5f9af0..00000000 --- a/endpoints/cookie/cookie.php +++ /dev/null @@ -1,54 +0,0 @@ - ['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 deleted file mode 100644 index bc07dbca..00000000 --- a/endpoints/currencies/currencies.php +++ /dev/null @@ -1,76 +0,0 @@ -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 deleted file mode 100644 index 92a761a7..00000000 --- a/endpoints/currency/currency.php +++ /dev/null @@ -1,267 +0,0 @@ -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 deleted file mode 100644 index 57b9c076..00000000 --- a/endpoints/currency/currency_power.php +++ /dev/null @@ -1,50 +0,0 @@ - ['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 deleted file mode 100644 index 8b38397b..00000000 --- a/endpoints/data/data.php +++ /dev/null @@ -1,150 +0,0 @@ - ['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 deleted file mode 100644 index 587d5a5e..00000000 --- a/endpoints/edit/image.php +++ /dev/null @@ -1,48 +0,0 @@ - ['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 deleted file mode 100644 index 8a73cfdd..00000000 --- a/endpoints/emote/emote.php +++ /dev/null @@ -1,226 +0,0 @@ - 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 deleted file mode 100644 index de77397a..00000000 --- a/endpoints/emotes/emotes.php +++ /dev/null @@ -1,61 +0,0 @@ -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 deleted file mode 100644 index 98ae613f..00000000 --- a/endpoints/enchantment/enchantment.php +++ /dev/null @@ -1,320 +0,0 @@ -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 deleted file mode 100644 index 3dfbb79a..00000000 --- a/endpoints/enchantments/enchantments.php +++ /dev/null @@ -1,131 +0,0 @@ - ['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 deleted file mode 100644 index 7796afc1..00000000 --- a/endpoints/event/event.php +++ /dev/null @@ -1,373 +0,0 @@ -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 deleted file mode 100644 index 48c75683..00000000 --- a/endpoints/event/event_power.php +++ /dev/null @@ -1,79 +0,0 @@ - ['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 deleted file mode 100644 index 885a7821..00000000 --- a/endpoints/events/events.php +++ /dev/null @@ -1,96 +0,0 @@ -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 deleted file mode 100644 index e85f5a79..00000000 --- a/endpoints/faction/faction.php +++ /dev/null @@ -1,362 +0,0 @@ -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 deleted file mode 100644 index 5e8dd308..00000000 --- a/endpoints/factions/factions.php +++ /dev/null @@ -1,104 +0,0 @@ - [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 deleted file mode 100644 index 7946f354..00000000 --- a/endpoints/faq/faq.php +++ /dev/null @@ -1,34 +0,0 @@ -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 deleted file mode 100644 index 9830385c..00000000 --- a/endpoints/filter/filter.php +++ /dev/null @@ -1,86 +0,0 @@ -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 deleted file mode 100644 index 139c6949..00000000 --- a/endpoints/get-description/get-description.php +++ /dev/null @@ -1,35 +0,0 @@ - ['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 deleted file mode 100644 index c99d55f4..00000000 --- a/endpoints/go-to-comment/go-to-comment.php +++ /dev/null @@ -1,45 +0,0 @@ - ['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 deleted file mode 100644 index e3cbbcc4..00000000 --- a/endpoints/go-to-reply/go-to-reply.php +++ /dev/null @@ -1,42 +0,0 @@ - ['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 deleted file mode 100644 index eee823e7..00000000 --- a/endpoints/guide/changelog.php +++ /dev/null @@ -1,105 +0,0 @@ - ['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 deleted file mode 100644 index be86d440..00000000 --- a/endpoints/guide/edit.php +++ /dev/null @@ -1,214 +0,0 @@ - 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 deleted file mode 100644 index 45e30c07..00000000 --- a/endpoints/guide/guide.php +++ /dev/null @@ -1,250 +0,0 @@ - ['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 deleted file mode 100644 index 970bc302..00000000 --- a/endpoints/guide/guide_power.php +++ /dev/null @@ -1,59 +0,0 @@ - ['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 deleted file mode 100644 index 95de5c4d..00000000 --- a/endpoints/guide/new.php +++ /dev/null @@ -1,66 +0,0 @@ - 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 deleted file mode 100644 index ff82c837..00000000 --- a/endpoints/guide/vote.php +++ /dev/null @@ -1,50 +0,0 @@ - ['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 deleted file mode 100644 index 23116f41..00000000 --- a/endpoints/guides/guides.php +++ /dev/null @@ -1,73 +0,0 @@ -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 deleted file mode 100644 index 9db0d252..00000000 --- a/endpoints/guild/guild.php +++ /dev/null @@ -1,153 +0,0 @@ - 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 deleted file mode 100644 index 82df7f8f..00000000 --- a/endpoints/guild/resync.php +++ /dev/null @@ -1,48 +0,0 @@ - ['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 deleted file mode 100644 index df311ee0..00000000 --- a/endpoints/guild/status.php +++ /dev/null @@ -1,29 +0,0 @@ - ['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 deleted file mode 100644 index 38f76557..00000000 --- a/endpoints/guilds/guilds.php +++ /dev/null @@ -1,158 +0,0 @@ - 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 deleted file mode 100644 index 92e5f7f8..00000000 --- a/endpoints/help/help.php +++ /dev/null @@ -1,45 +0,0 @@ -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 deleted file mode 100644 index 8ca0caba..00000000 --- a/endpoints/home/home.php +++ /dev/null @@ -1,75 +0,0 @@ - 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 deleted file mode 100644 index a74d9f62..00000000 --- a/endpoints/icon/get-id-from-name.php +++ /dev/null @@ -1,29 +0,0 @@ - ['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 deleted file mode 100644 index b0620539..00000000 --- a/endpoints/icon/icon.php +++ /dev/null @@ -1,158 +0,0 @@ -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 deleted file mode 100644 index 75464fae..00000000 --- a/endpoints/icons/icons.php +++ /dev/null @@ -1,113 +0,0 @@ - ['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 deleted file mode 100644 index 87778919..00000000 --- a/endpoints/item/item.php +++ /dev/null @@ -1,1096 +0,0 @@ -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 deleted file mode 100644 index 25617b47..00000000 --- a/endpoints/item/item_power.php +++ /dev/null @@ -1,70 +0,0 @@ - ['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 deleted file mode 100644 index 19997124..00000000 --- a/endpoints/item/item_xml.php +++ /dev/null @@ -1,230 +0,0 @@ - ['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/endpoints/itemset/itemset.php b/endpoints/itemset/itemset.php deleted file mode 100644 index c86ae527..00000000 --- a/endpoints/itemset/itemset.php +++ /dev/null @@ -1,260 +0,0 @@ -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 deleted file mode 100644 index 042354ae..00000000 --- a/endpoints/itemset/itemset_power.php +++ /dev/null @@ -1,49 +0,0 @@ - ['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 deleted file mode 100644 index 945e5100..00000000 --- a/endpoints/itemsets/itemsets.php +++ /dev/null @@ -1,116 +0,0 @@ - ['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 deleted file mode 100644 index fbb809db..00000000 --- a/endpoints/latest-comments/latest-comments.php +++ /dev/null @@ -1,46 +0,0 @@ - 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 deleted file mode 100644 index 20b61fa5..00000000 --- a/endpoints/latest-comments/latest-comments_rss.php +++ /dev/null @@ -1,41 +0,0 @@ - 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 deleted file mode 100644 index 6e8ba355..00000000 --- a/endpoints/latest-screenshots/latest-screenshots.php +++ /dev/null @@ -1,43 +0,0 @@ - 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 deleted file mode 100644 index 50e6f215..00000000 --- a/endpoints/latest-screenshots/latest-screenshots_rss.php +++ /dev/null @@ -1,42 +0,0 @@ -'; - 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 deleted file mode 100644 index 610552e3..00000000 --- a/endpoints/latest-videos/latest-videos.php +++ /dev/null @@ -1,43 +0,0 @@ - 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 deleted file mode 100644 index 5e3980ec..00000000 --- a/endpoints/latest-videos/latest-videos_rss.php +++ /dev/null @@ -1,42 +0,0 @@ -'; - 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 deleted file mode 100644 index 260ae500..00000000 --- a/endpoints/locale/locale.php +++ /dev/null @@ -1,27 +0,0 @@ - ['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 deleted file mode 100644 index 0b6fe31b..00000000 --- a/endpoints/mail/mail.php +++ /dev/null @@ -1,183 +0,0 @@ -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 deleted file mode 100644 index e1319aca..00000000 --- a/endpoints/mails/mails.php +++ /dev/null @@ -1,59 +0,0 @@ -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 deleted file mode 100644 index 0f99f470..00000000 --- a/endpoints/maps/maps.php +++ /dev/null @@ -1,29 +0,0 @@ -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 deleted file mode 100644 index 8341008c..00000000 --- a/endpoints/missing-screenshots/missing-screenshots.php +++ /dev/null @@ -1,58 +0,0 @@ - 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 deleted file mode 100644 index 20985809..00000000 --- a/endpoints/most-comments/most-comments.php +++ /dev/null @@ -1,110 +0,0 @@ - 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 deleted file mode 100644 index 1f99587d..00000000 --- a/endpoints/most-comments/most-comments_rss.php +++ /dev/null @@ -1,63 +0,0 @@ -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 deleted file mode 100644 index f335369f..00000000 --- a/endpoints/my-guides/my-guides.php +++ /dev/null @@ -1,52 +0,0 @@ -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 deleted file mode 100644 index 1ddf45e5..00000000 --- a/endpoints/npc/npc.php +++ /dev/null @@ -1,1125 +0,0 @@ -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 deleted file mode 100644 index 56fb95af..00000000 --- a/endpoints/npc/npc_power.php +++ /dev/null @@ -1,50 +0,0 @@ - ['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 deleted file mode 100644 index de03c9a4..00000000 --- a/endpoints/npcs/npcs.php +++ /dev/null @@ -1,139 +0,0 @@ - ['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 deleted file mode 100644 index 60d9281e..00000000 --- a/endpoints/object/object.php +++ /dev/null @@ -1,650 +0,0 @@ -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 deleted file mode 100644 index bfd73a60..00000000 --- a/endpoints/object/object_power.php +++ /dev/null @@ -1,50 +0,0 @@ - ['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 deleted file mode 100644 index cf4d8976..00000000 --- a/endpoints/objects/objects.php +++ /dev/null @@ -1,113 +0,0 @@ - ['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 deleted file mode 100644 index 70064a1d..00000000 --- a/endpoints/pet/pet.php +++ /dev/null @@ -1,222 +0,0 @@ -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 deleted file mode 100644 index cff9539b..00000000 --- a/endpoints/petcalc/petcalc.php +++ /dev/null @@ -1,40 +0,0 @@ -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 deleted file mode 100644 index c95fcc5c..00000000 --- a/endpoints/pets/pets.php +++ /dev/null @@ -1,88 +0,0 @@ -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 deleted file mode 100644 index 9065193e..00000000 --- a/endpoints/privilege/privilege.php +++ /dev/null @@ -1,71 +0,0 @@ - '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 deleted file mode 100644 index 83857f04..00000000 --- a/endpoints/privileges/privileges.php +++ /dev/null @@ -1,65 +0,0 @@ - '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 deleted file mode 100644 index aa8a9ed2..00000000 --- a/endpoints/profile/avatar.php +++ /dev/null @@ -1,68 +0,0 @@ - ['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 deleted file mode 100644 index ed7572fd..00000000 --- a/endpoints/profile/delete.php +++ /dev/null @@ -1,47 +0,0 @@ - ['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 deleted file mode 100644 index ad15f385..00000000 --- a/endpoints/profile/link.php +++ /dev/null @@ -1,50 +0,0 @@ - ['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 deleted file mode 100644 index 17277037..00000000 --- a/endpoints/profile/load.php +++ /dev/null @@ -1,302 +0,0 @@ - ['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 deleted file mode 100644 index 9b871a44..00000000 --- a/endpoints/profile/pin.php +++ /dev/null @@ -1,58 +0,0 @@ - ['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 deleted file mode 100644 index c42a50c2..00000000 --- a/endpoints/profile/private.php +++ /dev/null @@ -1,64 +0,0 @@ - ['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 deleted file mode 100644 index 53a7f8be..00000000 --- a/endpoints/profile/profile.php +++ /dev/null @@ -1,187 +0,0 @@ - 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 deleted file mode 100644 index cbc0d0e6..00000000 --- a/endpoints/profile/profile_power.php +++ /dev/null @@ -1,88 +0,0 @@ - ['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 deleted file mode 100644 index 98f70eae..00000000 --- a/endpoints/profile/public.php +++ /dev/null @@ -1,64 +0,0 @@ - ['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 deleted file mode 100644 index 05d63a64..00000000 --- a/endpoints/profile/purge.php +++ /dev/null @@ -1,22 +0,0 @@ - - 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 deleted file mode 100644 index 0b93276a..00000000 --- a/endpoints/profile/resync.php +++ /dev/null @@ -1,42 +0,0 @@ - ['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 deleted file mode 100644 index 70a4e9f7..00000000 --- a/endpoints/profile/save.php +++ /dev/null @@ -1,204 +0,0 @@ - ['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 deleted file mode 100644 index ac086f84..00000000 --- a/endpoints/profile/status.php +++ /dev/null @@ -1,50 +0,0 @@ - ['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 deleted file mode 100644 index 26eddd1d..00000000 --- a/endpoints/profile/summary.php +++ /dev/null @@ -1,17 +0,0 @@ - diff --git a/endpoints/profile/unlink.php b/endpoints/profile/unlink.php deleted file mode 100644 index 322521e3..00000000 --- a/endpoints/profile/unlink.php +++ /dev/null @@ -1,62 +0,0 @@ - ['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 deleted file mode 100644 index dceb00f8..00000000 --- a/endpoints/profile/unpin.php +++ /dev/null @@ -1,55 +0,0 @@ - ['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 deleted file mode 100644 index eb6f79e0..00000000 --- a/endpoints/profiler/profiler.php +++ /dev/null @@ -1,52 +0,0 @@ -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 deleted file mode 100644 index 8821cb0c..00000000 --- a/endpoints/profiles/profiles.php +++ /dev/null @@ -1,224 +0,0 @@ - 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 deleted file mode 100644 index b3d1d74e..00000000 --- a/endpoints/quest/quest.php +++ /dev/null @@ -1,1359 +0,0 @@ -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 deleted file mode 100644 index d0eccf52..00000000 --- a/endpoints/quest/quest_power.php +++ /dev/null @@ -1,50 +0,0 @@ - ['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 deleted file mode 100644 index 86dc7e9b..00000000 --- a/endpoints/quests/quests.php +++ /dev/null @@ -1,153 +0,0 @@ - 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 deleted file mode 100644 index e331f1b0..00000000 --- a/endpoints/race/race.php +++ /dev/null @@ -1,296 +0,0 @@ - [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 deleted file mode 100644 index 43d370b6..00000000 --- a/endpoints/races/races.php +++ /dev/null @@ -1,52 +0,0 @@ -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 deleted file mode 100644 index 21250899..00000000 --- a/endpoints/random/random.php +++ /dev/null @@ -1,48 +0,0 @@ -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 deleted file mode 100644 index 0e160637..00000000 --- a/endpoints/reputation/reputation.php +++ /dev/null @@ -1,58 +0,0 @@ -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 deleted file mode 100644 index 294a4285..00000000 --- a/endpoints/screenshot/add.php +++ /dev/null @@ -1,98 +0,0 @@ - 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 deleted file mode 100644 index 5bf80622..00000000 --- a/endpoints/screenshot/complete.php +++ /dev/null @@ -1,106 +0,0 @@ - 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 deleted file mode 100644 index 9f446899..00000000 --- a/endpoints/screenshot/crop.php +++ /dev/null @@ -1,92 +0,0 @@ - 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 deleted file mode 100644 index d2517671..00000000 --- a/endpoints/screenshot/thankyou.php +++ /dev/null @@ -1,66 +0,0 @@ - 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 deleted file mode 100644 index 11bce6f4..00000000 --- a/endpoints/search/search.php +++ /dev/null @@ -1,113 +0,0 @@ - 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 deleted file mode 100644 index fa8d4b5c..00000000 --- a/endpoints/search/search_json.php +++ /dev/null @@ -1,91 +0,0 @@ - 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 deleted file mode 100644 index 699fc3fa..00000000 --- a/endpoints/search/search_open.php +++ /dev/null @@ -1,128 +0,0 @@ - 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 deleted file mode 100644 index 22c289d9..00000000 --- a/endpoints/searchbox/searchbox.php +++ /dev/null @@ -1,34 +0,0 @@ -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 deleted file mode 100644 index 6dd00b1b..00000000 --- a/endpoints/searchplugins/searchplugins.php +++ /dev/null @@ -1,34 +0,0 @@ -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 deleted file mode 100644 index 2b9a9506..00000000 --- a/endpoints/signature/delete.php +++ /dev/null @@ -1,30 +0,0 @@ - ['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 deleted file mode 100644 index a04ef5ac..00000000 --- a/endpoints/signature/generate.php +++ /dev/null @@ -1,60 +0,0 @@ - ['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 deleted file mode 100644 index e0a5b76b..00000000 --- a/endpoints/signature/signature.php +++ /dev/null @@ -1,55 +0,0 @@ - ['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 deleted file mode 100644 index 09e5ca53..00000000 --- a/endpoints/sitemap/sitemap.php +++ /dev/null @@ -1,52 +0,0 @@ - ['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 deleted file mode 100644 index a95221bf..00000000 --- a/endpoints/skill/skill.php +++ /dev/null @@ -1,374 +0,0 @@ -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 deleted file mode 100644 index 1d1f1a36..00000000 --- a/endpoints/skills/skills.php +++ /dev/null @@ -1,66 +0,0 @@ -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 deleted file mode 100644 index 8db7cf8f..00000000 --- a/endpoints/sound/sound.php +++ /dev/null @@ -1,320 +0,0 @@ -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 deleted file mode 100644 index 334117ae..00000000 --- a/endpoints/sound/sound_playlist.php +++ /dev/null @@ -1,26 +0,0 @@ -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 deleted file mode 100644 index eef7a1ea..00000000 --- a/endpoints/sounds/sounds.php +++ /dev/null @@ -1,120 +0,0 @@ - ['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 deleted file mode 100644 index 3819ddc6..00000000 --- a/endpoints/spell/spell.php +++ /dev/null @@ -1,2536 +0,0 @@ -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 deleted file mode 100644 index 957232f0..00000000 --- a/endpoints/spell/spell_power.php +++ /dev/null @@ -1,58 +0,0 @@ - ['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/endpoints/talent/talent.php b/endpoints/talent/talent.php deleted file mode 100644 index 0ea35fd7..00000000 --- a/endpoints/talent/talent.php +++ /dev/null @@ -1,39 +0,0 @@ -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 deleted file mode 100644 index 21959316..00000000 --- a/endpoints/title/title.php +++ /dev/null @@ -1,203 +0,0 @@ -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 deleted file mode 100644 index a6c3d4d5..00000000 --- a/endpoints/titles/titles.php +++ /dev/null @@ -1,75 +0,0 @@ -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 deleted file mode 100644 index 7ead16a0..00000000 --- a/endpoints/tooltips/tooltips.php +++ /dev/null @@ -1,34 +0,0 @@ -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 deleted file mode 100644 index 464fea0a..00000000 --- a/endpoints/top-users/top-users.php +++ /dev/null @@ -1,96 +0,0 @@ -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 deleted file mode 100644 index 2b7fefc5..00000000 --- a/endpoints/unrated-comments/unrated-comments.php +++ /dev/null @@ -1,41 +0,0 @@ - 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 deleted file mode 100644 index ab9c111d..00000000 --- a/endpoints/upload/image-complete.php +++ /dev/null @@ -1,88 +0,0 @@ - ['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 deleted file mode 100644 index cf7f8279..00000000 --- a/endpoints/upload/image-crop.php +++ /dev/null @@ -1,84 +0,0 @@ -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 deleted file mode 100644 index cff2c560..00000000 --- a/endpoints/user/user.php +++ /dev/null @@ -1,390 +0,0 @@ -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 deleted file mode 100644 index 9459e9a9..00000000 --- a/endpoints/video/add.php +++ /dev/null @@ -1,124 +0,0 @@ - 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 deleted file mode 100644 index 3bb554aa..00000000 --- a/endpoints/video/complete.php +++ /dev/null @@ -1,92 +0,0 @@ - 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 deleted file mode 100644 index 46ed385a..00000000 --- a/endpoints/video/confirm.php +++ /dev/null @@ -1,81 +0,0 @@ - 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 deleted file mode 100644 index f07822ee..00000000 --- a/endpoints/video/thankyou.php +++ /dev/null @@ -1,60 +0,0 @@ - 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 deleted file mode 100644 index 63124dfe..00000000 --- a/endpoints/whats-new/whats-new.php +++ /dev/null @@ -1,34 +0,0 @@ -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 deleted file mode 100644 index 3e54eb4f..00000000 --- a/endpoints/zone/zone.php +++ /dev/null @@ -1,952 +0,0 @@ -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 deleted file mode 100644 index 0f58fd4a..00000000 --- a/endpoints/zones/zones.php +++ /dev/null @@ -1,188 +0,0 @@ -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 new file mode 100644 index 00000000..656d00a5 --- /dev/null +++ b/includes/ajaxHandler.class.php @@ -0,0 +1,1304 @@ +params = explode('.', $params); + + foreach ($_POST as $k => $v) + { + Util::checkNumeric($v); + $this->post[$k] = $v; + } + + foreach ($_GET as $k => $v) + $this->get[$k] = Util::checkNumeric($v) ? $v : is_string($v) ? trim(urldecode($v)) : $v; + } + + private function get($var) + { + if (isset($this->get[$var])) + return $this->get[$var]; + + return null; + } + + private function post($var) + { + if (isset($this->post[$var])) + return $this->post[$var]; + + return null; + } + + public function handle($what) + { + $f = 'handle'.ucFirst(str_replace(['-', '_'], '', $what)); + if (!$what || !method_exists($this, $f)) + return null; + + Util::sendNoCacheHeader(); + + return $this->$f(); + } + + /* responses + header() + */ + private function handleGotocomment() + { + if (!$this->get('id')) + return; + + 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'))) + header('Location: ?'.Util::$typeStrings[$_['type']].'='.$_['typeId'].'#comments:id='.$_['id'].($_['id'] != $this->get('id') ? ':reply='.$this->get('id') : null), true, 302); + } + + /* responses + + */ + private function handleData() + { + if (is_numeric($this->get('locale'))) + User::useLocale($this->get('locale')); + + $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->data_loadProfilerData($set); + break; + case 'companions': + $result .= $this->data_loadProfilerData($set, '778'); + break; + case 'mounts': + $result .= $this->data_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 = $this->get('catg') ?: 'null'; + if ($catg == 'null') + $result .= $this->data_loadProfilerData($set); + else if ($this->data_isLoadOnDemand()) + $result .= "\n\$WowheadProfiler.loadOnDemand('quests', ".$catg.");\n"; + + break; + case 'recipes': + if (!$this->data_isLoadOnDemand() || !$this->get('skill')) + break; + + $skills = array_intersect(explode(',', $this->get('skill')), [171, 164, 333, 202, 182, 773, 755, 165, 186, 393, 197, 185, 129, 356]); + if (!$skills) + break; + + foreach ($skills 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': // generated per character in profiler + 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 .= "-".intVal($_); + 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; + } + + private function handleProfile() + { + if (!$this->params) + return null; + + switch ($this->params[0]) + { + case 'link': + case 'unlink': + $this->profile_handleLink(); // always returns null + return ''; + case 'pin': + case 'unpin': + $this->profile_handlePin(); // always returns null + return ''; + case 'public': + case 'private': + $this->profile_handlePrivacy(); // always returns null + return ''; + case 'avatar': + if ($this->profile_handleAvatar()) // sets an image header + die(); // so it has to die here or another header will be set + case 'resync': + case 'status': + return $this->profile_handleResync($this->params[0] == 'resync'); + case 'save': + return $this->profile_handleSave(); + case 'delete': + return $this->profile_handleDelete(); + case 'purge': + return $this->profile_handlePurge(); + case 'summary': // page is generated by jScript + return ''; // just be empty + case 'load': + return $this->profile_handleLoad(); + default: + return null; + } + } + + /* responses + 0: success + $: silent error + */ + private function handleCookie() + { + if (User::$id && $this->params && !empty($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; + } + + /* responses + 0: success + 1: captcha invalid + 2: description too long + 3: reason missing + 7: already reported + $: prints response + */ + private 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'); + + $subj = intVal($this->post('id')); + + $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, $subj, $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 ($subj) + $update['subject'] = $subj; + + 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'; + } + + private function handleComment() + { + // post sizes + $_minCmt = 10; + $_maxCmt = 7500 * (User::isPremium() ? 3 : 1); + + $_minRpl = 15; + $_maxRpl = 600; + + $result = null; + /* + note: return values must be formated as STRICT json! + */ + switch ($this->params[0]) + { + case 'add': // i .. have problems believing, that everything uses nifty ajax while adding comments requires a brutal header(Loacation: ), yet, thats how it is + if (!$this->get('typeid') || !$this->get('type') || !isset(Util::$typeStrings[$this->get('type')])) + return; // whatever, we cant even send him back + + // trim to max length + if (!User::isInGroup(U_GROUP_MODERATOR) && mb_strlen($this->post('commentbody')) > $_maxCmt) + $this->post['body'] = substr($this->post('body'), 0, $_maxCmt); + + if (User::canComment() && !empty($this->post('commentbody')) && mb_strlen($this->post('commentbody')) >= $_minCmt) + { + 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 filtrable) + if ($tbl = Util::getCCTableParent($this->get('type'))) + DB::Aowow()->query('UPDATE '.$tbl.' SET cuFlags = cuFlags | ?d WHERE id = ?d', CUSTOM_HAS_COMMENT, $this->get('typeid')); + } + } + + header('Location: ?'.Util::$typeStrings[$this->get('type')].'='.$this->get('typeid').'#comments', true, 302); + break; + case 'edit': + if ((!User::canComment() && !User::isInGroup(U_GROUP_MODERATOR)) || !$this->get('id') || !$this->post('body')) + break; + + if (mb_strlen($this->post('body')) < $_minCmt) + break; + + // trim to max length + if (!User::isInGroup(U_GROUP_MODERATOR) && mb_strlen($this->post('body')) > $_maxCmt) + $this->post['body'] = substr($this->post('body'), 0, $_maxCmt); + + $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')); + break; + case 'delete': + if (!$this->post('id')) + break; + + $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, + (array)$this->post('id'), + User::isInGroup(U_GROUP_MODERATOR) ? DBSIMPLE_SKIP : User::$id + ); + + // deflag hasComment (if filtrable) + 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') ?: $this->get('id') + ); + + if (!$coInfo['hasMore'] && ($tbl = Util::getCCTableParent($coInfo['type']))) + DB::Aowow()->query('UPDATE '.$tbl.' SET cuFlags = cuFlags & ~?d WHERE id = ?d', CUSTOM_HAS_COMMENT, $coInfo['typeId']); + } + + break; + case 'undelete': + if (!$this->post('id')) + break; + + $ok = DB::Aowow()->query('UPDATE ?_comments SET flags = flags & ~?d WHERE id IN (?a){ AND userId = deleteUserId AND deleteUserId = ?d}', + CC_FLAG_DELETED, + (array)$this->post('id'), + User::isInGroup(U_GROUP_MODERATOR) ? DBSIMPLE_SKIP : User::$id + ); + + // reflag hasComment (if filtrable) + if ($ok) + { + $coInfo = DB::Aowow()->selectRow('SELECT type, typeId FROM ?_comments WHERE id = ?d', $this->post('id') ?: $this->get('id')); + if ($tbl = Util::getCCTableParent($coInfo['type'])) + DB::Aowow()->query('UPDATE '.$tbl.' SET cuFlags = cuFlags | ?d WHERE id = ?d', CUSTOM_HAS_COMMENT, $coInfo['typeId']); + } + + break; + case 'rating': // up/down - distribution + if (!$this->get('id')) + { + $result = ['success' => 0]; + break; + } + + 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 GROUP BY commentId', $this->get('id'))) + return json_encode($votes, JSON_NUMERIC_CHECK); + + $result = ['success' => 1, 'up' => 0, 'down' => 0]; + break; + case 'vote': // up, down and remove + if (!User::$id || !$this->get('id') || $this->get('rating')) + { + $result = ['error' => 1, 'message' => Lang::main('genericError')]; + break; + } + + $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) + $result = ['error' => 1, 'message' => Lang::main('tooManyVotes')]; + + else if (!$target || $val != $this->get('rating')) + $result = ['error' => 1, 'message' => Lang::main('genericError')]; + + else if (($val > 0 && !User::canUpvote()) || ($val < 0 && !User::canDownvote())) + $result = ['error' => 1, 'message' => Lang::main('bannedRating')]; + + if ($result) + break; + + $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) + { + $result = ['error' => 1, 'message' => Lang::main('genericError')]; + break; + } + + 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]); + + $result = ['error' => 0]; + break; + case 'sticky': // toggle flag + if (!$this->post('id') || !User::isInGroup(U_GROUP_MODERATOR)) + break; + + if ($this->post('sticky')) + DB::Aowow()->query('UPDATE ?_comments SET flags = flags | ?d WHERE id = ?d', CC_FLAG_STICKY, $this->post('id')); + else + DB::Aowow()->query('UPDATE ?_comments SET flags = flags & ~?d WHERE id = ?d', CC_FLAG_STICKY, $this->post('id')); + + break; + case 'out-of-date': // toggle flag + if (!$this->post('id')) + { + $result = 'The comment does not exist.'; + break; + } + + $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')); + else + $ok = DB::Aowow()->query('UPDATE ?_comments SET flags = flags & ~0x4 WHERE id = ?d', $this->post('id')); + } + else if (User::$id && !$this->post('reason') || mb_strlen($this->post('reason')) < 15) + { + $result = 'Your message is too short.'; + break; + } + 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'), + 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" + + $result = Lang::main('genericError'); + break; + case 'show-replies': + $result = !$this->get('id') ? [] : CommunityContent::getCommentReplies($this->get('id')); + break; + case 'add-reply': // also returns all replies on success + if (!User::canComment()) + $result = 'You are not allowed to reply.'; + + else if (!$this->post('body') || mb_strlen($this->post('body')) < $_minRpl || mb_strlen($this->post('body')) > $_maxRpl) + $result = 'Your reply has '.mb_strlen($this->post('body')).' characters and must have at least '.$_minRpl.' and at most '.$_maxRpl.'.'; + + else if (!$this->post('commentId') || !DB::Aowow()->selectCell('SELECT 1 FROM ?_comments WHERE id = ?d', $this->post('commentId'))) + $result = Lang::main('genericError'); + + 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'))) + $result = CommunityContent::getCommentReplies($this->post('commentId')); + + else + $result = Lang::main('genericError'); + + break; + case 'edit-reply': // also returns all replies on success + if (!User::canComment()) + $result = 'You are not allowed to reply.'; + + else if (!$this->post('replyId') || $this->post('commentId')) + $result = Lang::main('genericError'); + + else if (!$this->post('body') || mb_strlen($this->post('body')) < $_minRpl || mb_strlen($this->post('body')) > $_maxRpl) + $result = 'Your reply has '.mb_strlen($this->post('body')).' characters and must have at least '.$_minRpl.' and at most '.$_maxRpl.'.'; + + if ($result) + break; + + $ok = 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 + ); + + $result = $ok ? CommunityContent::getCommentReplies($this->post('commentId')) : Lang::main('genericError'); + break; + case 'detach-reply': + if (!User::isInGroup(U_GROUP_MODERATOR) || !$this->post('id')) + break; + + 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')); + break; + case 'delete-reply': + if (!User::$id || !$this->post('id')) + break; + + if (DB::Aowow()->query('DELETE FROM ?_comments WHERE id = ?d{ AND userId = ?d}', $this->post('id'), User::isInGroup(U_GROUP_MODERATOR) ? DBSIMPLE_SKIP : User::$id)) + DB::Aowow()->query('DELETE FROM ?_comments_rates WHERE commentId = ?d', $this->post('id')); + + break; + case 'flag-reply': + if (!User::$id || $this->post('id')) + break; + + DB::Aowow()->query( + 'INSERT INTO ?_reports (userId, mode, reason, subject, ip, description, userAgent, appName) VALUES (?d, 1, 19, ?d, ?, "", ?, ?)', + User::$id, + $this->post('id'), + User::$ip, + $_SERVER['HTTP_USER_AGENT'], + get_browser(null, true)['browser'] + ); + + break; + case 'upvote-reply': + if (!$this->post('id') || !User::canUpvote()) + break; + + $ok = DB::Aowow()->query( + 'INSERT INTO ?_comments_rates (commentId, userId, value) VALUES (?d, ?d, ?d)', + $this->post('id'), + User::$id, + User::canSupervote() ? 2 : 1 + ); + + if ($ok) + User::decrementDailyVotes(); + + break; + case 'downvote-reply': + if (!$this->post('id') || !User::canUpvote()) + break; + + $ok = DB::Aowow()->query( + 'INSERT INTO ?_comments_rates (commentId, userId, value) VALUES (?d, ?d, ?d)', + $this->post('id'), + User::$id, + User::canSupervote() ? -2 : -1 + ); + + if ($ok) + User::decrementDailyVotes(); + } + + return json_encode($result, JSON_NUMERIC_CHECK); + } + + private function handleLocale() // not sure if this should be here.. + { + User::setLocale($this->params[0]); + User::save(); + + header('Location: '.(isset($_SERVER['HTTP_REFERER']) ? $_SERVER['HTTP_REFERER'] : '.'), true, 302); + } + + private function handleAccount() + { + if (!$this->params || !User::$id) + return null; + + switch ($this->params[0]) + { + case 'exclude': + // profiler completion exclude handler + // $this->post('groups') = bitMask of excludeGroupIds when using .. excludeGroups .. duh + // should probably occur in g_user.excludegroups (dont forget to also set g_users.settings = {}) + return ''; + case 'weightscales': + if (!$this->post('save')) + { + if (!isset($this->post['id'])) + { + $res = DB::Aowow()->selectRow('SELECT max(id) as max, count(id) as num FROM ?_account_weightscales WHERE userId = ?d', User::$id); + if ($res['num'] < 5) // more or less hard-defined in LANG.message_weightscalesaveerror + $this->post['id'] = ++$res['max']; + else + return 0; + } + + if (DB::Aowow()->query('REPLACE INTO ?_account_weightscales VALUES (?d, ?d, ?, ?)', intVal($this->post('id')), User::$id, $this->post('name'), $this->post('scale'))) + return $this->post('id'); + else + return 0; + } + else if ($this->post('delete') && $this->post('id')) + DB::Aowow()->query('DELETE FROM ?_account_weightscales WHERE id = ?d AND userId = ?d', intVal($this->post('id')), User::$id); + else + return 0; + } + + + return null; + } + + private function handleAdmin() + { + if (!$this->get('action') || !$this->params) + return null; + + if ($this->params[0] == 'screenshots') + { + if (!User::isInGroup(U_GROUP_STAFF | U_GROUP_SCREENSHOT)) // comment_mod, handleSSmod, vi_mod ? + return null; + + switch ($this->get('action')) + { + case 'list': // get all => null (optional) + case 'manage': // get: [type => type, typeId => typeId] || [user => username] + case 'editalt': // get: id => ssId; post: alt => caption + case 'approve': // get: id => ssId || ,-separated id-list + case 'sticky': // get: id => ssId || ,-separated id-list + case 'delete': // get: id => ssId || ,-separated id-list + case 'relocate': // get: id => ssId, typeid => typeId (but not type..?) + $fn = 'admin_handleSS'.ucfirst($this->get('action')); + return $this->$fn(); + break; + default: + return null; + } + } + else if ($this->params[0] == 'siteconfig') + { + if (!User::isInGroup(U_GROUP_DEV | U_GROUP_ADMIN)) + return null; + + switch ($this->get('action')) + { + case 'remove': + if (!$this->get('id')) + return 'invalid configuration option given'; + + if (DB::Aowow()->query('DELETE FROM ?_config WHERE `key` = ? AND (`flags` & ?d) = 0', $this->get('id'), CON_FLAG_PERSISTENT)) + return ''; + else + return 'option name is either protected or was not found'; + case 'add': + $key = strtolower(trim($this->get('id'))); + $val = trim($this->get('val')); + + if (!strlen($key)) + return 'empty option name given'; + if (!strlen($val)) + return 'empty value given'; + + if (preg_match('/[^a-z0-9_\.\-]/i', $key, $m)) + return 'invalid chars in option name: "'.$m[0].'"'; + + 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`, `flags`) VALUES (?, ?, ?d)', $key, $val, CON_FLAG_TYPE_STRING | CON_FLAG_PHP); + return ''; + case 'update': + $key = trim($this->get('id')); + $val = trim($this->get('val')); + + if (!strlen($key)) + return 'empty option name given'; + if (!strlen($val)) + return 'empty value given'; + + if (substr($key, 0, 4) == 'CFG_') + $key = substr($key, 4); + + $flags = DB::Aowow()->selectCell('SELECT `flags` FROM ?_config WHERE `key` = ?', $key); + if (!$flags) + return 'configuration option not found'; + + if (preg_match('/[^a-z0-9_\-]/i', $key, $m)) + return 'invalid chars in option name: "'.$m[0].'"'; + + if ($flags & CON_FLAG_TYPE_INT && !preg_match('/^-?\d+$/i', $val)) + return "value must be integer"; + else if ($flags & CON_FLAG_TYPE_FLOAT && !preg_match('/^-?\d*(,|.)?\d+$/i', $val)) + return "value must be float"; + else if ($flags & CON_FLAG_TYPE_BOOL) + $val = (int)!!$val; // *snort* bwahahaa + + DB::Aowow()->query('UPDATE ?_config SET `value` = ? WHERE `key` = ?', $val, $key); + return ''; + default: + return null; + } + } + + return null; + } + + + /**********/ + /* Helper */ + /**********/ + + private function data_isLoadOnDemand() + { + return substr($this->get('callback'), 0, 29) == '$WowheadProfiler.loadOnDemand'; + } + + private function data_loadProfilerData($file, $catg = 'null') + { + $result = ''; + if ($this->data_isLoadOnDemand()) + if (Util::loadStaticFile('p-'.$file, $result, true)) + $result .= "\n\$WowheadProfiler.loadOnDemand('".$file."', ".$catg.");\n"; + + return $result; + } + + private function profile_handleAvatar() // image + { + // something happened in the last years: those textures do not include tiny icons + $s = [/* 'tiny' => 15, */'small' => 18, 'medium' => 36, 'large' => 56]; + $size = $this->get('size') ?: 'medium'; + + if (!$this->get('id') || !preg_match('/^([0-9]+)\.(jpg|gif)$/', $this->get('id'), $matches) || !in_array($size, array_keys($s))) + return false; + + header('Content-Type: image/'.$matches[2]); + + $id = $matches[1]; + $dest = imageCreateTruecolor($s[$size], $s[$size]); + + if (file_exists('uploads/avatars/'.$id.'.jpg')) + { + $offsetX = $offsetY = 0; + + switch ($size) + { + case 'tiny': + $offsetX += $s['small']; + case 'small': + $offsetY += $s['medium']; + case 'medium': + $offsetX += $s['large']; + } + + $src = imageCreateFromJpeg('uploads/avatars/'.$id.'.jpg'); + imagecopymerge($dest, $src, 0, 0, $offsetX, $offsetY, $s[$size], $s[$size], 100); + } + + if ($matches[2] == 'gif') + imageGif($dest); + else + imageJpeg($dest); + + return true; + } + + private function profile_handlePin($id, $mode) // (un)favorite + { + /* params + id: + user: [optional] + return: null + */ + } + + private function profile_handleLink($id, $mode) // links char with account + { + /* params + id: + user: [optional] + return: null + */ + } + + private function profile_handlePrivacy($id, $mode) // public visibility + { + /* params + id: + user: [optional] + return: null + */ + } + + private function profile_handleResync($initNew = true) // resync init and status requests + { + /* params + id: + user: [optional] + return + null [onOK] + int or str [onError] + */ + + if ($initNew) + return '1'; + else + { + /* + 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 + + [ + processId, + [StatusCode, timeToRefresh, iCount, errorCode, iNResyncs], + []... + ] + */ + return '[0, [4, 10000, 1, 2]]'; + } + } + + private function profile_handleSave() // unKill a profile + { + /* params GET + id: + params POST + name, level, class, race, gender, nomodel, talenttree1, talenttree2, talenttree3, activespec, talentbuild1, glyphs1, talentbuild2, glyphs2, gearscore, icon, public [always] + description, source, copy, inv { inventory: array containing itemLinks } [optional] + } + return + int > 0 [profileId, if we came from an armoryProfile create a new one] + int < 0 [onError] + str [onError] + */ + + return 'NYI'; + } + + private function profile_handleDelete() // kill a profile + { + /* params + id: + return + null + */ + + return 'NYI'; + } + + private function profile_handlePurge() // removes certain saved information but not the entire character + { + /* params + id: + data: [string, tabName?] + return + null + */ + + return 'NYI'; + } + + private function profile_handleLoad() + { + /* params + id: profileId + items: string [itemIds.join(':')] + unnamed: unixtime [only to force the browser to reload instead of cache] + return + lots... + */ + + // titles, achievements, characterData, talents (, pets) + // and some onLoad-hook to .. load it registerProfile($data) + // everything else goes through data.php .. strangely enough + + $char = new ProfileList(array(['id', $this->get('id')])); // or string or whatever + + // modify model from auras with profile_getModelForForm + + $buff = ''; + + if ($it = array_column($char->getField('inventory'), 0)) + { + $itemz = new ItemList(array(['id', $it, CFG_SQL_LIMIT_NONE])); + $data = $itemz->getListviewData(ITEMINFO_JSON | ITEMINFO_SUBITEMS); + + // get and apply inventory + foreach ($itemz->iterate() as $iId => $__) + $buff .= 'g_items.add('.$iId.', {name_'.User::$localeString.":'".Util::jsEscape($itemz->getField('name', true))."', quality:".$itemz->getField('quality').", icon:'".$itemz->getField('iconString')."', jsonequip:".json_encode($data[$iId], JSON_NUMERIC_CHECK)."});\n"; + + $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 = @Util::$itemMods[$k]) + $mods[$str] = $v; + } + } + + $buff .= 'g_spells.add('.$id.", {id:".$id.", name:'".Util::jsEscape(substr($data['name'], 1))."', icon:'".$data['icon']."', modifier:".json_encode($mods, JSON_NUMERIC_CHECK)."});\n"; + } + $buff .= "\n"; + } + + /* depending on progress-achievements + // required by progress in JScript move to handleLoad()? + Util::$pageTemplate->extendGlobalIds(TYPE_NPC, [29120, 31134, 29306, 29311, 23980, 27656, 26861, 26723, 28923, 15991]); + */ + + // load available titles + Util::loadStaticFile('p-titles-'.$char->getField('gender'), $buff, true); + + // load available achievements + if (!Util::loadStaticFile('p-achievements', $buff, true)) + { + $buff .= "\n\ng_achievement_catorder = [];"; + $buff .= "\n\ng_achievement_points = [0];"; + } + + // excludes; structure UNK type => [maskBit => [typeIds]] ? + /* + g_user.excludes = [type:[typeIds]] + g_user.includes = [type:[typeIds]] + g_user.excludegroups = groupMask // requires g_user.settings != null + + maskBit are matched against fieldId from excludeGroups + id: 1, label: LANG.dialog_notavail + id: 2, label: LANG.dialog_tcg + id: 4, label: LANG.dialog_collector + id: 8, label: LANG.dialog_promo + id: 16, label: LANG.dialog_nonus + id: 96, label: LANG.dialog_faction + id: 896, label: LANG.dialog_profession + id: 1024, label: LANG.dialog_noexalted + */ + // $buff .= "\n\ng_excludes = {};"; + + // add profile to buffer + $buff .= "\n\n\$WowheadProfiler.registerProfile(".json_encode($char->getEntry(2)).");"; // can't use JSON_NUMERIC_CHECK or the talent-string becomes a float + + return $buff."\n"; + } + + private function profile_getModelForForm($form, $char) + { + switch ($form) + { + case 1: // FORM_CAT + if ($char['race'] == 4) // RACE_NIGHTELF + { + if ($char['hairColor'] >= 0 && $char['hairColor'] <= 2) + return 29407; + else if ($char['hairColor'] == 3) + return 29406; + else if ($char['hairColor'] == 4) + return 29408; + else if ($char['hairColor'] == 7 || $char['hairColor'] == 8) + return 29405; + else + return 892; + } + + if ($char['race'] == 6) // RACE_TAUREN + { + if ($char['gender'] == GENDER_MALE) + { + if ($char['skinColor'] >= 0 && $char['skinColor'] <= 5) + return 29412; + else if ($char['skinColor'] >= 0 && $char['skinColor'] <= 8) + return 29411; + else if ($char['skinColor'] >= 0 && $char['skinColor'] <= 11) + return 29410; + else if (in_array($char['skinColor'], [12, 13, 14, 18])) + return 29410; + else + return 8571; + } + else // if gender == GENDER_FEMALE + { + if ($char['skinColor'] >= 0 && $char['skinColor'] <= 3) + return 29412; + else if ($char['skinColor'] >= 0 && $char['skinColor'] <= 5) + return 29411; + else if ($char['skinColor'] >= 0 && $char['skinColor'] <= 7) + return 29410; + else if ($char['skinColor'] == 10) + return 29410; + else + return 8571; + } + } + case 5: // FORM_DIREBEAR + case 8: // FORM_BEAR + if ($char['race'] == 4) // RACE_NIGHTELF + { + if ($char['hairColor'] >= 0 && $char['hairColor'] <= 2) + return 29413; + else if ($char['hairColor'] == 3) + return 29417; + else if ($char['hairColor'] == 4) + return 29416; + else if ($char['hairColor'] == 6) + return 29414; + else + return 2281; + } + + if ($char['race'] == 6) // RACE_TAUREN + { + if ($char['gender'] == GENDER_MALE) + { + if ($char['skinColor'] >= 0 && $char['skinColor'] <= 2) + return 29415; + else if (in_array($char['skinColor'], [3, 4, 5, 12, 13, 14])) + return 29419; + else if (in_array($char['skinColor'], [9, 10, 11, 15, 16, 17])) + return 29420; + else if ($char['skinColor'] == 18) + return 29421; + else + return 2289; + } + else // if gender == GENDER_FEMALE + { + if ($char['skinColor'] == 0 && $char['skinColor'] == 1) + return 29418; + else if ($char['skinColor'] == 2 && $char['skinColor'] == 3) + return 29419; + else if ($char['skinColor'] >= 6 && $char['skinColor'] <= 9) + return 29420; + else if ($char['skinColor'] == 10) + return 29421; + else + return 2289; + } + } + } + + // hey, still here? you're not a Tauren/Nelf as bear or cat, are you? + return DB::Aowow()->selectCell('SELECT IF(?d == 1, IFNULL(displayIdA, displayIdH), IFNULL(displayIdH, displayIdA)) FROM ?_shapeshiftform WHERE id = ?d', Util::sideByRaceMask(1 << ($char['race'] - 1)), $form); + } + + // get all => null (optional) + // evaled response .. UNK + private function admin_handleSSList() + { + // ssm_screenshotPages + // ssm_numPagesFound + + $pages = CommunityContent::getScreenshotPagesForManager(isset($this->get['all']), $nPages); + $buff = 'ssm_screenshotPages = '.json_encode($pages, JSON_NUMERIC_CHECK).";\n"; + $buff .= 'ssm_numPagesFound = '.$nPages.';'; + + return $buff; + } + + // get: [type => type, typeId => typeId] || [user => username] + // evaled response .. UNK + private function admin_handleSSManage() + { + $res = []; + + if ($this->get('type') && intVal($this->get('type')) && $this->get('typeid') && intVal($this->get('typeid'))) + $res = CommunityContent::getScreenshotsForManager($this->get('type'), $this->get('typeid')); + else if ($this->get('user') && strlen(urldecode($this->get('user'))) > 2) + if ($uId = DB::Aowow()->selectCell('SELECT id FROM ?_account WHERE displayName = ?', strtolower(urldecode($this->get('user'))))) + $res = CommunityContent::getScreenshotsForManager(0, 0, $uId); + + return 'ssm_screenshotData = '.json_encode($res, JSON_NUMERIC_CHECK); + } + + // get: id => SSid + // resp: '' + private function admin_handleSSEditalt() + { + if (!$this->get('id') || !$this->post('alt')) + return ''; + + // doesn't need to be htmlEscaped, ths javascript does that + DB::Aowow()->query('UPDATE ?_screenshots SET caption = ? WHERE id = ?d', $this->post('alt'), $this->get('id')); + + return ''; + } + + // get: id => comma-separated SSids + // resp: '' + private function admin_handleSSApprove($override = []) + { + if (!$this->get('id')) + return ''; + + $ids = $override ?: array_map('intval', explode(',', $this->get('id'))); + + // create resized and thumb version of screenshot + $resized = [772, 618]; + $thumb = [150, 150]; + $path = 'static/uploads/screenshots/%s/%d.jpg'; + + foreach ($ids as $id) + { + // must not be already approved + if ($_ = DB::Aowow()->selectCell('SELECT userIdOwner 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($_, SITEREP_ACTION_UPLOAD, ['id' => $id, 'what' => 1]); + } + } + + return ''; + } + + // get: id => comma-separated SSids + // resp: '' + private function admin_handleSSSticky() + { + if (!$this->get('id')) + 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 + $ids = array_map('intval', explode(',', $this->get('id'))); + + foreach ($ids 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); + + // approve this one (if not already) + $this->admin_handleSSApprove([$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 + private function admin_handleSSDelete() + { + if (!$this->get('id')) + return ''; + + $path = 'static/uploads/screenshots/%s/%d.jpg'; + $ids = array_map('intval', explode(',', $this->get('id'))); + + foreach ($ids as $id) + { + // irrevocably remove already deleted files + if (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 + DB::Aowow()->query('UPDATE ?_screenshots SET status = ?d, userIdDelete = ?d WHERE id IN (?a)', CC_FLAG_DELETED, User::$id, $ids); + + return ''; + } + + // get: id => ssId, typeid => typeId (but not type..?) + // resp: '' + private function admin_handleSSRelocate() + { + if (!$this->get('id') || !$this->get('typeid')) + return ''; + + $type = DB::Aowow()->selectCell('SELECT type FROM ?_screenshots WHERE id = ?d', $this->get('id')); + $typeId = (int)$this->get('typeid'); + + if (!(new Util::$typeClasses[$type]([['id', $typeId]]))->error) + DB::Aowow()->query('UPDATE ?_screenshots SET typeId = ?d WHERE id = ?d', $typeId, $this->get('id')); + + return ''; + } +} + +?> diff --git a/includes/cfg.class.php b/includes/cfg.class.php deleted file mode 100644 index 56b287c8..00000000 --- a/includes/cfg.class.php +++ /dev/null @@ -1,499 +0,0 @@ - '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 new file mode 100644 index 00000000..33457b14 --- /dev/null +++ b/includes/community.class.php @@ -0,0 +1,526 @@ + 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 + 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; + 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 (strlen($c['preview']) > 100) + { + $n = 0; + $b = []; + $parts = explode(' ', $c['preview']); + while ($n < 100 && $parts) + { + $_ = array_shift($parts); + $n += strlen($_); + $b[] = $_; + } + + $c['preview'] = implode(' ', $b).'…'; + } + } + else + { + Util::addNote(U_GROUP_STAFF, 'CommunityClass::getCommentPreviews - comment '.$c['id'].' belongs to nonexistant subject'); + 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 : unapproved + $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 + ); + + 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; + + $cnd = [['id', $ids]]; + if ($t == TYPE_WORLDEVENT) // FKIN HOLIDAYS + array_push($cnd, ['holidayId', $ids], 'OR'); + + $tClass = new Util::$typeClasses[$t]($cnd); + 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'])) + { + Util::addNote(U_GROUP_STAFF | U_GROUP_SCREENSHOT, 'AdminPage::handleScreenshots() - Screenshot linked to nonexistant type/typeId combination '.$p['type'].'/'.$p['typeId']); + 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, $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 }{v.type = ? }{AND v.typeId = ? }AND v.status & ?d AND (v.status & ?d) = 0", + CC_FLAG_STICKY, + $typeOrUser < 0 ? -$typeOrUser : DBSIMPLE_SKIP, + $typeOrUser > 0 ? $typeOrUser : DBSIMPLE_SKIP, + $typeOrUser > 0 ? $typeId : DBSIMPLE_SKIP, + CC_FLAG_APPROVED, + CC_FLAG_DELETED + ); + + if ($typeOrUser < 0) // only for user page + { + 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) // only for user page + { + 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, $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 }{s.type = ? }{AND s.typeId = ? }AND s.status & ?d AND (s.status & ?d) = 0", + CC_FLAG_STICKY, + $typeOrUser < 0 ? -$typeOrUser : DBSIMPLE_SKIP, + $typeOrUser > 0 ? $typeOrUser : DBSIMPLE_SKIP, + $typeOrUser > 0 ? $typeId : DBSIMPLE_SKIP, + CC_FLAG_APPROVED, + CC_FLAG_DELETED + ); + + if ($typeOrUser < 0) // only for user page + { + 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) // only for user page + { + 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 deleted file mode 100644 index 9512d766..00000000 --- a/includes/components/Conditions/Conditions.class.php +++ /dev/null @@ -1,770 +0,0 @@ - - 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 deleted file mode 100644 index 3bf9b55e..00000000 --- a/includes/components/SmartAI/SmartAI.class.php +++ /dev/null @@ -1,837 +0,0 @@ -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 deleted file mode 100644 index 05a354eb..00000000 --- a/includes/components/SmartAI/SmartAction.class.php +++ /dev/null @@ -1,760 +0,0 @@ - [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 deleted file mode 100644 index 5a1f0316..00000000 --- a/includes/components/SmartAI/SmartEvent.class.php +++ /dev/null @@ -1,395 +0,0 @@ - 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 deleted file mode 100644 index 76aacd7c..00000000 --- a/includes/components/SmartAI/SmartTarget.class.php +++ /dev/null @@ -1,185 +0,0 @@ - [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 deleted file mode 100644 index 4df080c8..00000000 --- a/includes/components/avatarmgr.class.php +++ /dev/null @@ -1,122 +0,0 @@ - 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 deleted file mode 100644 index ce7999af..00000000 --- a/includes/components/communitycontent.class.php +++ /dev/null @@ -1,413 +0,0 @@ - 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 deleted file mode 100644 index c0d98f82..00000000 --- a/includes/components/dbtypelist.class.php +++ /dev/null @@ -1,965 +0,0 @@ -: - 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 deleted file mode 100644 index de0339f2..00000000 --- a/includes/components/filter.class.php +++ /dev/null @@ -1,882 +0,0 @@ -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 deleted file mode 100644 index 16382009..00000000 --- a/includes/components/frontend/announcement.class.php +++ /dev/null @@ -1,69 +0,0 @@ -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 deleted file mode 100644 index b1ef62e7..00000000 --- a/includes/components/frontend/book.class.php +++ /dev/null @@ -1,50 +0,0 @@ -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 deleted file mode 100644 index 6014bd31..00000000 --- a/includes/components/frontend/iconelement.class.php +++ /dev/null @@ -1,161 +0,0 @@ -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 deleted file mode 100644 index dff22075..00000000 --- a/includes/components/frontend/infoboxmarkup.class.php +++ /dev/null @@ -1,70 +0,0 @@ -= 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 deleted file mode 100644 index 0c6da5c8..00000000 --- a/includes/components/frontend/listview.class.php +++ /dev/null @@ -1,191 +0,0 @@ - ['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 deleted file mode 100644 index 23cfc5de..00000000 --- a/includes/components/frontend/markup.class.php +++ /dev/null @@ -1,294 +0,0 @@ - $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 deleted file mode 100644 index 62ad87e5..00000000 --- a/includes/components/frontend/summary.class.php +++ /dev/null @@ -1,70 +0,0 @@ - $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 deleted file mode 100644 index 1fa14cd1..00000000 --- a/includes/components/frontend/tabs.class.php +++ /dev/null @@ -1,145 +0,0 @@ - $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 deleted file mode 100644 index 0042c417..00000000 --- a/includes/components/frontend/tooltip.class.php +++ /dev/null @@ -1,64 +0,0 @@ -__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 deleted file mode 100644 index 61a41b5b..00000000 --- a/includes/components/guidemgr.class.php +++ /dev/null @@ -1,104 +0,0 @@ - '#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 deleted file mode 100644 index e8fa79a5..00000000 --- a/includes/components/imageupload.class.php +++ /dev/null @@ -1,306 +0,0 @@ - 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 deleted file mode 100644 index 2bfec006..00000000 --- a/includes/components/locstring.class.php +++ /dev/null @@ -1,67 +0,0 @@ -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 deleted file mode 100644 index 59c3d50b..00000000 --- a/includes/components/pagetemplate.class.php +++ /dev/null @@ -1,579 +0,0 @@ -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 deleted file mode 100644 index e3c4d452..00000000 --- a/includes/components/profiler.class.php +++ /dev/null @@ -1,1078 +0,0 @@ - [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 deleted file mode 100644 index 26b4ca63..00000000 --- a/includes/components/report.class.php +++ /dev/null @@ -1,305 +0,0 @@ - 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 deleted file mode 100644 index 29c0bbc2..00000000 --- a/includes/components/response/baseresponse.class.php +++ /dev/null @@ -1,677 +0,0 @@ - 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 deleted file mode 100644 index d2eb3c94..00000000 --- a/includes/components/response/templateresponse.class.php +++ /dev/null @@ -1,703 +0,0 @@ -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 deleted file mode 100644 index 1541d239..00000000 --- a/includes/components/response/textresponse.class.php +++ /dev/null @@ -1,170 +0,0 @@ -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 deleted file mode 100644 index be782877..00000000 --- a/includes/components/screenshotmgr.class.php +++ /dev/null @@ -1,216 +0,0 @@ - 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 deleted file mode 100644 index 5587071c..00000000 --- a/includes/components/search.class.php +++ /dev/null @@ -1,1628 +0,0 @@ - '_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 deleted file mode 100644 index f7d5b4e2..00000000 --- a/includes/components/sitemap.class.php +++ /dev/null @@ -1,181 +0,0 @@ - [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 deleted file mode 100644 index 1ce894bd..00000000 --- a/includes/components/videomgr.class.php +++ /dev/null @@ -1,220 +0,0 @@ -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 new file mode 100644 index 00000000..23cc9db3 --- /dev/null +++ b/includes/database.class.php @@ -0,0 +1,122 @@ +error) + die('Failed to connect to database.'); + + $interface->setErrorHandler(['DB', 'errorHandler']); + $interface->query('SET NAMES ?', 'utf8'); + if ($options['prefix']) + $interface->setIdentPrefix($options['prefix']); + + 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 deleted file mode 100644 index f0df4f51..00000000 --- a/includes/database.php +++ /dev/null @@ -1,332 +0,0 @@ -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/dbtypes/achievement.class.php b/includes/dbtypes/achievement.class.php deleted file mode 100644 index 5a665136..00000000 --- a/includes/dbtypes/achievement.class.php +++ /dev/null @@ -1,390 +0,0 @@ - [['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`'] - ); - - public function __construct(array $conditions = [], array $miscData = []) - { - parent::__construct($conditions, $miscData); - - if ($this->error) - return; - - // post processing - $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() - ); - - foreach ($this->iterate() as $_id => &$_curTpl) - { - $_curTpl['rewards'] = []; - - if (!empty($rewards[$_id])) - { - $_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 LootByContainer(); - // $mailSrc->getByContainer(Loot::MAIL, $rewards[$_id]['MailTemplateID']); - // foreach ($mailSrc->iterate() as $loot) - // $_curTpl['rewards'][] = [Type::ITEM, $loot['id']]; - - // lets just assume for now, that mailRewards for achievements do not contain references - $mailRew = DB::World()->selectCol('SELECT `Item` FROM mail_loot_template WHERE `Reference` <= 0 AND `entry` = %i', $rewards[$_id]['MailTemplateID']); - foreach ($mailRew AS $mr) - $_curTpl['rewards'][] = [Type::ITEM, $mr]; - } - } - - //"rewards":[[11,137],[3,138]] [type, typeId] - if (!empty($_curTpl['ItemID'])) - $_curTpl['rewards'][] = [Type::ITEM, $_curTpl['ItemID']]; - if (!empty($_curTpl['itemExtra'])) - $_curTpl['rewards'][] = [Type::ITEM, $_curTpl['itemExtra']]; - if (!empty($_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']]; - - // icon - $_curTpl['iconString'] = $_curTpl['iconString'] ?: 'trade_engineering'; - } - } - - 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)]; - - if ($addMask & GLOBALINFO_REWARDS) - foreach ($this->curTpl['rewards'] as $_) - $data[$_[0]][$_[1]] = $_[1]; - } - - return $data; - } - - public function getListviewData(int $addInfoMask = 0x0) : array - { - $data = []; - - foreach ($this->iterate() as $__) - { - $data[$this->id] = array( - 'id' => $this->id, - 'name' => $this->getField('name', true), - 'description' => $this->getField('description', true), - 'points' => $this->curTpl['points'], - 'side' => $this->curTpl['faction'], - 'category' => $this->curTpl['category'], - 'parentcat' => $this->curTpl['parentCat'], - ); - - if ($addInfoMask & ACHIEVEMENTINFO_PROFILE) - $data[$this->id]['icon'] = $this->curTpl['iconString']; - - // going out on a limb here: type = 1 if in level 3 of statistics tree, so, IF (statistic AND parentCat NOT statistic (1)) i guess - if ($this->curTpl['flags'] & ACHIEVEMENT_FLAG_COUNTER && $this->curTpl['parentCat'] != 1) - $data[$this->id]['type'] = 1; - - if ($_ = $this->curTpl['rewards']) - $data[$this->id]['rewards'] = $_; - else if ($_ = $this->getField('reward', true)) - $data[$this->id]['reward'] = $_; - } - - return $data; - } - - // only for current template - public function getCriteria() : array - { - if (isset($this->criteria[$this->id])) - return $this->criteria[$this->id]; - - $result = DB::Aowow()->selectAssoc('SELECT * FROM ::achievementcriteria WHERE `refAchievementId` = %i ORDER BY `order` ASC', $this->curTpl['refAchievement'] ?: $this->id); - if (!$result) - return []; - - $this->criteria[$this->id] = $result; - - return $this->criteria[$this->id]; - } - - public function renderTooltip() : ?string - { - $criteria = $this->getCriteria(); - $tmp = []; - $rows = []; - $i = 0; - foreach ($criteria as $_row) - { - if ($i++ % 2) - $tmp[] = $_row; - else - $rows[] = $_row; - } - if ($tmp) - $rows = array_merge($rows, $tmp); - - $description = $this->getField('description', true); - $name = $this->getField('name', true); - $criteria = ''; - - $i = 0; - foreach ($rows as $crt) - { - $obj = (int)$crt['value1']; - $qty = (int)$crt['value2']; - - // we could show them, but the tooltips are cluttered - if (($crt['completionFlags'] & ACHIEVEMENT_CRITERIA_FLAG_HIDDEN) && User::isInGroup(U_GROUP_STAFF)) - continue; - - $crtName = Util::localizedString($crt, 'name'); - switch ($crt['type']) - { - // link to quest - case ACHIEVEMENT_CRITERIA_TYPE_COMPLETE_QUEST: - if (!$crtName) - $crtName = QuestList::getName($obj); - break; - // link to spell (/w icon) - 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: - if (!$crtName) - $crtName = SpellList::getName($obj); - break; - // link to item (/w icon) - case ACHIEVEMENT_CRITERIA_TYPE_OWN_ITEM: - case ACHIEVEMENT_CRITERIA_TYPE_USE_ITEM: - case ACHIEVEMENT_CRITERIA_TYPE_LOOT_ITEM: - case ACHIEVEMENT_CRITERIA_TYPE_EQUIP_ITEM: - if (!$crtName) - $crtName = ItemList::getName($obj); - break; - // link to faction (/w target reputation) - case ACHIEVEMENT_CRITERIA_TYPE_GAIN_REPUTATION: - if (!$crtName) - $crtName = FactionList::getName($obj); - break; - } - - $criteria .= '- '.$crtName; - - if ($crt['completionFlags'] & ACHIEVEMENT_CRITERIA_FLAG_MONEY_COUNTER) - $criteria .= ' '.Lang::nf($crt['value2' ] / 10000).''; - - $criteria .= '
'; - - if (++$i == round(count($rows)/2)) - $criteria .= ''; - } - - $x = '
'; - $x .= $name; - $x .= '
'; - if ($description || $criteria) - $x .= '
'; - - if ($description) - $x .= '
'.$description.'
'; - - if ($criteria) - { - $x .= '
'.Lang::achievement('criteria').':'; - $x .= '
'.$criteria.'
'; - } - if ($description || $criteria) - $x .= '
'; - - return $x; - } - - 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, - "ti" => $this->id - ); - } - - return $data; - } -} - - -class AchievementListFilter extends Filter -{ - protected string $type = 'achievements'; - protected static array $enums = array( - 4 => parent::ENUM_ZONE, // location - 11 => array( - 327 => 160, // Lunar Festival - 423 => 187, // Love is in the Air - 181 => 159, // Noblegarden - 201 => 163, // Children's Week - 341 => 161, // Midsummer Fire Festival - 372 => 162, // Brewfest - 324 => 158, // Hallow's End - 404 => 14981, // Pilgrim's Bounty - 141 => 156, // Feast of Winter Veil - 409 => -3456, // Day of the Dead - 398 => -3457, // Pirates' Day - 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, - 321 => -1, 424 => -1, 301 => -1 - ) - ); - - 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 - ); - - 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 createSQLForValues() : array - { - $parts = []; - $_v = &$this->values; - - // name ex: +description, +rewards - if ($_v['na']) - { - $_ = []; - 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->buildLikeLookup([['na', 'name_loc'.Lang::getLocale()->value]]); - - if ($_) - $parts[] = $_; - } - - // points min - if ($_v['minpt']) - $parts[] = ['points', $_v['minpt'], '>=']; - - // points max - if ($_v['maxpt']) - $parts[] = ['points', $_v['maxpt'], '<=']; - - // faction (side) - if ($_v['si']) - { - $parts[] = match ($_v['si']) - { - -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(int $cr, int $crs, string $crv) : ?array - { - if (!isset(self::$enums[$cr][$crs])) - return null; - - $_ = self::$enums[$cr][$crs]; - if (is_int($_)) - return ($_ > 0) ? ['category', $_] : ['id', abs($_)]; - else - { - $ids = array_filter(self::$enums[$cr], fn($x) => is_int($x) && $x > 0); - - return ['category', $ids, $_ ? null : '!']; - } - - return null; - } - - protected function cbSeries(int $cr, int $crs, string $crv, int $seriesFlag) : ?array - { - if ($this->int2Bool($crs)) - return $crs ? [DB::AND, ['chainId', 0, '!'], ['cuFlags', $seriesFlag, '&']] : [DB::AND, ['chainId', 0, '!'], [['cuFlags', $seriesFlag, '&'], 0]]; - - return null; - } -} - -?> diff --git a/includes/dbtypes/areatrigger.class.php b/includes/dbtypes/areatrigger.class.php deleted file mode 100644 index 527afb25..00000000 --- a/includes/dbtypes/areatrigger.class.php +++ /dev/null @@ -1,99 +0,0 @@ - [['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 deleted file mode 100644 index 9103da0c..00000000 --- a/includes/dbtypes/arenateam.class.php +++ /dev/null @@ -1,368 +0,0 @@ -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/dbtypes/charrace.class.php b/includes/dbtypes/charrace.class.php deleted file mode 100644 index c790aafe..00000000 --- a/includes/dbtypes/charrace.class.php +++ /dev/null @@ -1,58 +0,0 @@ - [['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 deleted file mode 100644 index 6c259153..00000000 --- a/includes/dbtypes/creature.class.php +++ /dev/null @@ -1,578 +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_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 deleted file mode 100644 index 79744097..00000000 --- a/includes/dbtypes/currency.class.php +++ /dev/null @@ -1,88 +0,0 @@ - [['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 deleted file mode 100644 index 3510d687..00000000 --- a/includes/dbtypes/emote.class.php +++ /dev/null @@ -1,65 +0,0 @@ -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 deleted file mode 100644 index 28c170e4..00000000 --- a/includes/dbtypes/enchantment.class.php +++ /dev/null @@ -1,263 +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(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/dbtypes/gameobject.class.php b/includes/dbtypes/gameobject.class.php deleted file mode 100644 index 7dcc92c1..00000000 --- a/includes/dbtypes/gameobject.class.php +++ /dev/null @@ -1,253 +0,0 @@ - [['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 deleted file mode 100644 index d1cc5f5c..00000000 --- a/includes/dbtypes/guide.class.php +++ /dev/null @@ -1,170 +0,0 @@ - [['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/dbtypes/guild.class.php b/includes/dbtypes/guild.class.php deleted file mode 100644 index e31ea4cc..00000000 --- a/includes/dbtypes/guild.class.php +++ /dev/null @@ -1,312 +0,0 @@ -getGuildScores(); - - $data = []; - foreach ($this->iterate() as $__) - { - $data[$this->id] = array( - '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'], 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']) - ); - } - - return $data; - } - - 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. - 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. [...] - Instead of being based on level, achievement point averages are based around 1,500 points, but the same penalties apply. - */ - $guilds = array_column($this->templates, 'id'); - if (!$guilds) - return; - - $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 = $stats[$id]; - - $nMaxLevel = count(array_filter($stats[$id], fn($x) => $x['level'] >= MAX_LEVEL)); - $levelMod = 1.0; - - if ($nMaxLevel < 25) - $levelMod = 0.85; - if ($nMaxLevel < 10) - $levelMod = 0.66; - if ($nMaxLevel < 1) - $levelMod = 0.20; - - $totalGS = $totalAP = $nMembers = 0; - foreach ($guildStats as $gs) - { - $totalGS += $gs['gearscore'] * $levelMod * min($gs['level'], MAX_LEVEL) / MAX_LEVEL; - $totalAP += $gs['achievementpoints'] * $levelMod * min($gs['achievementpoints'], 1500) / 1500; - $nMembers += min($gs['level'], MAX_LEVEL) / MAX_LEVEL; - } - - $_curTpl['gearscore'] = intval($totalGS / $nMembers); - $_curTpl['achievementpoints'] = intval($totalAP / $nMembers); - } - } - - 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 -{ - use TrProfilerFilter; - - 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 - ); - - public array $extraOpts = []; - - protected function createSQLForValues() : array - { - $parts = []; - $_v = $this->values; - - // region (rg), battlegroup (bg) and server (sv) are passed to GuildList as miscData and handled there - - // name [str] - if ($_v['na']) - if ($_ = $this->buildLikeLookup([['na', 'g.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)]; - - return $parts; - } -} - - -class RemoteGuildList extends GuildList -{ - 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"'] - ); - - public function __construct(array $conditions = [], array $miscData = []) - { - // select DB by realm - if (!$this->selectRealms($miscData)) - { - trigger_error('RemoteGuildList::__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 - $realms = Profiler::getRealms(); - $distrib = []; - - // post processing - foreach ($this->iterate() as $guid => &$curTpl) - { - // battlegroup - $curTpl['battlegroup'] = Cfg::get('BATTLEGROUP'); - - $r = explode(':', $guid)[0]; - if (!empty($realms[$r])) - { - $curTpl['realm'] = $r; - $curTpl['realmName'] = $realms[$r]['name']; - $curTpl['region'] = $realms[$r]['region']; - } - else - { - 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; - } - - // equalize distribution - if (empty($distrib[$curTpl['realm']])) - $distrib[$curTpl['realm']] = 1; - else - $distrib[$curTpl['realm']]++; - } - - // 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; - } - - $distrib[$curTpl['realm']]--; - $limit--; - } - } - - public function initializeLocalEntries() : void - { - if (!$this->templates) - return; - - $data = []; - foreach ($this->iterate() as $guid => $__) - { - $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 - 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 %in AND `realmGUID` IN %in', - $data['realm'], $data['realmGUID'] - ); - - foreach ($this->iterate() as $guid => &$_curTpl) - if (isset($localIds[$guid])) - $_curTpl['id'] = $localIds[$guid]; - } -} - - -class LocalGuildList extends GuildList -{ - protected string $queryBase = 'SELECT g.*, g.`id` AS ARRAY_KEY FROM ::profiler_guild g'; - - 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; - - 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'); - } - } - - public function getProfileUrl() : string - { - $url = '?guild='; - - return $url.implode('.', array( - $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 deleted file mode 100644 index c3971a97..00000000 --- a/includes/dbtypes/icon.class.php +++ /dev/null @@ -1,205 +0,0 @@ - '::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 deleted file mode 100644 index 2932d2a9..00000000 --- a/includes/dbtypes/item.class.php +++ /dev/null @@ -1,2643 +0,0 @@ - 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 deleted file mode 100644 index a80e5886..00000000 --- a/includes/dbtypes/itemset.class.php +++ /dev/null @@ -1,251 +0,0 @@ - ['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 deleted file mode 100644 index 0e142428..00000000 --- a/includes/dbtypes/mail.class.php +++ /dev/null @@ -1,77 +0,0 @@ -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/dbtypes/profile.class.php b/includes/dbtypes/profile.class.php deleted file mode 100644 index 541e4b1a..00000000 --- a/includes/dbtypes/profile.class.php +++ /dev/null @@ -1,750 +0,0 @@ -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 deleted file mode 100644 index c24c2573..00000000 --- a/includes/dbtypes/quest.class.php +++ /dev/null @@ -1,727 +0,0 @@ - [], - '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 deleted file mode 100644 index 9aafbcb1..00000000 --- a/includes/dbtypes/skill.class.php +++ /dev/null @@ -1,73 +0,0 @@ - [['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 deleted file mode 100644 index f43a8fb3..00000000 --- a/includes/dbtypes/sound.class.php +++ /dev/null @@ -1,129 +0,0 @@ - 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 deleted file mode 100644 index 6aefde19..00000000 --- a/includes/dbtypes/spell.class.php +++ /dev/null @@ -1,2879 +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 => 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 deleted file mode 100644 index b583a290..00000000 --- a/includes/dbtypes/title.class.php +++ /dev/null @@ -1,180 +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(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 deleted file mode 100644 index 18122f6b..00000000 --- a/includes/dbtypes/user.class.php +++ /dev/null @@ -1,89 +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 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 deleted file mode 100644 index 399d6083..00000000 --- a/includes/dbtypes/worldevent.class.php +++ /dev/null @@ -1,176 +0,0 @@ - [['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/defines.php b/includes/defines.php index 4075085a..f1edd788 100644 --- a/includes/defines.php +++ b/includes/defines.php @@ -1,66 +1,45 @@ 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); +// ItemMod (differ slightly from client, see g_statToJson) +define('ITEM_MOD_WEAPON_DMG', 0); // < custom +define('ITEM_MOD_MANA', 1); +define('ITEM_MOD_HEALTH', 2); +define('ITEM_MOD_AGILITY', 3); // stats v +define('ITEM_MOD_STRENGTH', 4); +define('ITEM_MOD_INTELLECT', 5); +define('ITEM_MOD_SPIRIT', 6); +define('ITEM_MOD_STAMINA', 7); +define('ITEM_MOD_ENERGY', 8); // powers v +define('ITEM_MOD_RAGE', 9); +define('ITEM_MOD_FOCUS', 10); +define('ITEM_MOD_RUNIC_POWER', 11); +define('ITEM_MOD_DEFENSE_SKILL_RATING', 12); // ratings v +define('ITEM_MOD_DODGE_RATING', 13); +define('ITEM_MOD_PARRY_RATING', 14); +define('ITEM_MOD_BLOCK_RATING', 15); +define('ITEM_MOD_HIT_MELEE_RATING', 16); +define('ITEM_MOD_HIT_RANGED_RATING', 17); +define('ITEM_MOD_HIT_SPELL_RATING', 18); +define('ITEM_MOD_CRIT_MELEE_RATING', 19); +define('ITEM_MOD_CRIT_RANGED_RATING', 20); +define('ITEM_MOD_CRIT_SPELL_RATING', 21); +define('ITEM_MOD_HIT_TAKEN_MELEE_RATING', 22); +define('ITEM_MOD_HIT_TAKEN_RANGED_RATING', 23); +define('ITEM_MOD_HIT_TAKEN_SPELL_RATING', 24); +define('ITEM_MOD_CRIT_TAKEN_MELEE_RATING', 25); +define('ITEM_MOD_CRIT_TAKEN_RANGED_RATING', 26); +define('ITEM_MOD_CRIT_TAKEN_SPELL_RATING', 27); +define('ITEM_MOD_HASTE_MELEE_RATING', 28); +define('ITEM_MOD_HASTE_RANGED_RATING', 29); +define('ITEM_MOD_HASTE_SPELL_RATING', 30); +define('ITEM_MOD_HIT_RATING', 31); +define('ITEM_MOD_CRIT_RATING', 32); +define('ITEM_MOD_HIT_TAKEN_RATING', 33); +define('ITEM_MOD_CRIT_TAKEN_RATING', 34); +define('ITEM_MOD_RESILIENCE_RATING', 35); +define('ITEM_MOD_HASTE_RATING', 36); +define('ITEM_MOD_EXPERTISE_RATING', 37); +define('ITEM_MOD_ATTACK_POWER', 38); +define('ITEM_MOD_RANGED_ATTACK_POWER', 39); +define('ITEM_MOD_FERAL_ATTACK_POWER', 40); +define('ITEM_MOD_SPELL_HEALING_DONE', 41); // deprecated +define('ITEM_MOD_SPELL_DAMAGE_DONE', 42); // deprecated +define('ITEM_MOD_MANA_REGENERATION', 43); +define('ITEM_MOD_ARMOR_PENETRATION_RATING', 44); +define('ITEM_MOD_SPELL_POWER', 45); +define('ITEM_MOD_HEALTH_REGEN', 46); +define('ITEM_MOD_SPELL_PENETRATION', 47); +define('ITEM_MOD_BLOCK_VALUE', 48); +// ITEM_MOD_MASTERY_RATING, 49 +define('ITEM_MOD_ARMOR', 50); // resistances v +define('ITEM_MOD_FIRE_RESISTANCE', 51); +define('ITEM_MOD_FROST_RESISTANCE', 52); +define('ITEM_MOD_HOLY_RESISTANCE', 53); +define('ITEM_MOD_SHADOW_RESISTANCE', 54); +define('ITEM_MOD_NATURE_RESISTANCE', 55); +define('ITEM_MOD_ARCANE_RESISTANCE', 56); // custom v +define('ITEM_MOD_FIRE_POWER', 57); +define('ITEM_MOD_FROST_POWER', 58); +define('ITEM_MOD_HOLY_POWER', 59); +define('ITEM_MOD_SHADOW_POWER', 60); +define('ITEM_MOD_NATURE_POWER', 61); +define('ITEM_MOD_ARCANE_POWER', 62); // AchievementCriteriaCondition define('ACHIEVEMENT_CRITERIA_CONDITION_NO_DEATH', 1); // reset progress on death @@ -1890,13 +652,11 @@ 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_SUM', 0x0008); // Use sum criteria value from all reqirements (and calculate max value) +define('ACHIEVEMENT_FLAG_SUMM', 0x0008); // Use summ 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_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 +define('ACHIEVEMENT_FLAG_BAR', 0x0080); // Show as progress bar (value / max vale) depend from other flag (by def use last criteria value) // AchievementCriteriaFlags define('ACHIEVEMENT_CRITERIA_FLAG_SHOW_PROGRESS_BAR', 0x0001); // Show progress as bar @@ -1950,7 +710,7 @@ define('ACHIEVEMENT_CRITERIA_TYPE_GAIN_REPUTATION', 46); // define('ACHIEVEMENT_CRITERIA_TYPE_ROLL_GREED_ON_LOOT', 51); define('ACHIEVEMENT_CRITERIA_TYPE_HK_CLASS', 52); define('ACHIEVEMENT_CRITERIA_TYPE_HK_RACE', 53); -define('ACHIEVEMENT_CRITERIA_TYPE_DO_EMOTE', 54); +// define('ACHIEVEMENT_CRITERIA_TYPE_DO_EMOTE', 54); // define('ACHIEVEMENT_CRITERIA_TYPE_HEALING_DONE', 55); // define('ACHIEVEMENT_CRITERIA_TYPE_GET_KILLING_BLOWS', 56); define('ACHIEVEMENT_CRITERIA_TYPE_EQUIP_ITEM', 57); @@ -1966,7 +726,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_ON_LOGIN', 74); +define('ACHIEVEMENT_CRITERIA_TYPE_EARNED_PVP_TITLE', 74); define('ACHIEVEMENT_CRITERIA_TYPE_LEARN_SKILLLINE_SPELLS', 75); // define('ACHIEVEMENT_CRITERIA_TYPE_WIN_DUEL', 76); // define('ACHIEVEMENT_CRITERIA_TYPE_LOSE_DUEL', 77); @@ -2006,79 +766,68 @@ 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 - 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); - -// 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 - -// 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); - -// 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 +// 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); +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 ?> diff --git a/includes/game/chrclass.class.php b/includes/game/chrclass.class.php deleted file mode 100644 index 3a94bc4b..00000000 --- a/includes/game/chrclass.class.php +++ /dev/null @@ -1,79 +0,0 @@ -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 deleted file mode 100644 index 70e481c1..00000000 --- a/includes/game/chrrace.class.php +++ /dev/null @@ -1,144 +0,0 @@ -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 deleted file mode 100644 index 1777e31c..00000000 --- a/includes/game/chrstatistics.php +++ /dev/null @@ -1,724 +0,0 @@ - ['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 deleted file mode 100644 index 55c6f513..00000000 --- a/includes/game/loot/loot.class.php +++ /dev/null @@ -1,71 +0,0 @@ -jsGlobals, $data); - } -} - -?> diff --git a/includes/game/loot/lootbycontainer.class.php b/includes/game/loot/lootbycontainer.class.php deleted file mode 100644 index 64d287d4..00000000 --- a/includes/game/loot/lootbycontainer.class.php +++ /dev/null @@ -1,333 +0,0 @@ -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 deleted file mode 100644 index ba7a29ce..00000000 --- a/includes/game/loot/lootbyitem.class.php +++ /dev/null @@ -1,441 +0,0 @@ - [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 deleted file mode 100644 index 11cc6ab1..00000000 --- a/includes/game/misc.php +++ /dev/null @@ -1,376 +0,0 @@ - '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 deleted file mode 100644 index df13aaf6..00000000 --- a/includes/game/worldposition.class.php +++ /dev/null @@ -1,196 +0,0 @@ -= 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 6afe3dbb..e7d8f73d 100644 --- a/includes/kernel.php +++ b/includes/kernel.php @@ -1,237 +1,54 @@ !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 = []; +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'; // misc™ data 'n func +require_once 'includes/ajaxHandler.class.php'; // handles ajax and jsonp requests +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('Filter', '', $class)); + + if (class_exists($class)) // already registered + return; + + if (preg_match('/[^\w]/i', $class)) // name should contain only letters + return; + + if (strpos($class, 'list')) + { + if (!class_exists('BaseType')) + require_once 'includes/types/basetype.class.php'; + + if (file_exists('includes/types/'.strtr($class, ['list' => '']).'.class.php')) + require_once 'includes/types/'.strtr($class, ['list' => '']).'.class.php'; + + return; + } + + 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']); @@ -246,43 +63,133 @@ if (!empty($AoWoWconf['characters'])) if (!empty($charDBInfo)) DB::load(DB_CHARACTERS . $realm, $charDBInfo); -$AoWoWconf = null; // empty auths + +// 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) +{ + // this should not have been possible + if (!strlen($v['value'])) + continue; + + $php = $v['flags'] & CON_FLAG_PHP; + + 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{L}0-9~\s_\-\'\/\.:,]/ui', '', $v['value']); + else + { + Util::addNote(U_GROUP_ADMIN | U_GROUP_DEV, 'Kernel: '.($php ? 'PHP' : 'Aowow').' config value '.($php ? strtolower($k) : 'CFG_'.strtoupper($k)).' has no type set. Value forced to 0!'); + $val = 0; + } + + if ($php) + ini_set(strtolower($k), $val); + else + define('CFG_'.strtoupper($k), $val); +} -// for CLI and early errors in erb context -Lang::load(Locale::EN); +// handle occuring errors +error_reporting(!empty($AoWoWconf['aowow']) && CFG_DEBUG ? (E_ALL & ~(E_DEPRECATED | E_USER_DEPRECATED | E_STRICT)) : 0); +$errHandled = false; +set_error_handler(function($errNo, $errStr, $errFile, $errLine) use (&$errHandled) { + $errName = 'unknown error'; // errors not in this list can not be handled by set_error_handler (as per documentation) or are ignored + 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'; + else if ($errNo == E_RECOVERABLE_ERROR) // 0x1000 + $errName = 'E_RECOVERABLE_ERROR'; -// load config from DB -Cfg::load(); + if (User::isInGroup(U_GROUP_STAFF)) + { + if (!$errHandled) + { + Util::addNote(U_GROUP_STAFF, 'one or more php related error occured, while generating this page.'); + $errHandled = true; + } + + Util::addNote(U_GROUP_STAFF, $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 !((User::isInGroup(U_GROUP_STAFF) && defined('CFG_DEBUG') && CFG_DEBUG) || CLI); +}, E_ALL & ~(E_DEPRECATED | E_USER_DEPRECATED | E_STRICT)); + + +$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); if (!CLI) { - // 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 - $cacheDir = Cfg::get('SESSION_CACHE_DIR'); - if ($cacheDir && Util::writeDir($cacheDir)) - session_save_path(getcwd().'/'.$cacheDir); - - session_set_cookie_params(15 * YEAR, '/', '', (($_SERVER['HTTPS'] ?? 'off') != 'off') || Cfg::get('FORCE_SSL'), true); + session_set_cookie_params(15 * YEAR, '/', '', $secure, true); session_cache_limiter('private'); - if (!session_start()) - { - trigger_error('failed to start session', E_USER_ERROR); - (new TemplateResponse())->generateError(); - } - - if (User::init()) + 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..?) - if (isset($_GET['locale']) && ($loc = Locale::tryFrom((int)$_GET['locale']))) - Lang::load($loc); - else - Lang::load(User::$preferedLoc); + // todo: (low) - move to setup web-interface (when it begins its existance) + if (!defined('CFG_SITE_HOST') || !defined('CFG_STATIC_HOST')) + { + $host = substr($_SERVER['SERVER_NAME'].strtr($_SERVER['SCRIPT_NAME'], ['index.php' => '']), 0, -1); + + define('HOST_URL', ($secure ? 'https://' : 'http://').$host); + define('STATIC_URL', ($secure ? 'https://' : 'http://').$host.'/static'); + + if (User::isInGroup(U_GROUP_ADMIN)) // initial set + { + DB::Aowow()->query('REPLACE INTO ?_config VALUES (?a)', + [['site_host', $host, CON_FLAG_TYPE_STRING | CON_FLAG_PERSISTENT, 'default: '.$host.' - points js to executable files (automaticly set on first run)'], + ['static_host', $host.'/static', CON_FLAG_TYPE_STRING | CON_FLAG_PERSISTENT, 'default: '.$host.'/static - points js to images & scripts (automaticly set on first run)']] + ); + } + } + + // hard-override locale for this call (should this be here..?) + // all strings attached.. + if (!empty($AoWoWconf['aowow'])) + { + if (isset($_GET['locale']) && (CFG_LOCALES & (1 << (int)$_GET['locale']))) + User::useLocale($_GET['locale']); + + Lang::load(User::$localeString); + } + + // parse page-parameters .. sanitize before use! + $str = explode('&', $_SERVER['QUERY_STRING'], 2)[0]; + $_ = explode('=', $str, 2); + $pageCall = $_[0]; + $pageParam = isset($_[1]) ? $_[1] : null; + + Util::$wowheadLink = 'http://'.Util::$subDomains[User::$localeId].'.wowhead.com/'.$str; } +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 new file mode 100644 index 00000000..986cb130 --- /dev/null +++ b/includes/libs/DbSimple/CacherImpl.php @@ -0,0 +1,45 @@ +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 new file mode 100644 index 00000000..6489c89a --- /dev/null +++ b/includes/libs/DbSimple/Connect.php @@ -0,0 +1,262 @@ +нужен для ленивой инициализации коннекта к базе + * + * @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 new file mode 100644 index 00000000..86719ae1 --- /dev/null +++ b/includes/libs/DbSimple/Database.php @@ -0,0 +1,1412 @@ +. + * + * 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 new file mode 100644 index 00000000..5dc2f144 --- /dev/null +++ b/includes/libs/DbSimple/Generic.php @@ -0,0 +1,193 @@ +. + * + * 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 new file mode 100644 index 00000000..2329e607 --- /dev/null +++ b/includes/libs/DbSimple/Mysqli.php @@ -0,0 +1,231 @@ +_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 (mysql_error()) 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 new file mode 100644 index 00000000..aff2e653 --- /dev/null +++ b/includes/libs/DbSimple/Zend/Cache.php @@ -0,0 +1,250 @@ +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 new file mode 100644 index 00000000..3f44e2e1 --- /dev/null +++ b/includes/libs/DbSimple/Zend/Cache/Backend/Interface.php @@ -0,0 +1,99 @@ + 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 deleted file mode 100644 index dae92a58..00000000 --- a/includes/libs/qqFileUploader.class.php +++ /dev/null @@ -1,187 +0,0 @@ -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 deleted file mode 100644 index a8d8923e..00000000 --- a/includes/locale.class.php +++ /dev/null @@ -1,179 +0,0 @@ - '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 new file mode 100644 index 00000000..c9cdfe26 --- /dev/null +++ b/includes/loot.class.php @@ -0,0 +1,617 @@ +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); + } + + 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 + { + Util::addNote(U_GROUP_EMPLOYEE, 'Loot::getByContainerRecursive: unhandled case in calculating chance for item '.$entry['Item'].'!'); + continue; + } + + $loot[] = $set; + } + + foreach (array_keys($nGroupEquals) as $k) + { + $sum = $groupChances[$k]; + if (!$sum) + $sum = 0; + else if ($sum >= 100.01) + { + Util::addNote(U_GROUP_EMPLOYEE, 'Loot::getByContainerRecursive: entry '.$lootId.' / group '.$k.' has a total chance of '.number_format($sum, 2).'%. Some items cannot drop!'); + $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']) + Util::addNote(U_GROUP_EMPLOYEE, 'Loot by Item: ungrouped Item/Ref '.$ref['item'].' has 0% chance assigned!'); + + if ($ref['isGrouped'] && $ref['sumChance'] > 100) + Util::addNote(U_GROUP_EMPLOYEE, 'Loot by Item: group with Item/Ref '.$ref['item'].' has '.number_format($ref['sumChance'], 2).'% total chance! Some items cannot drop!'); + + if ($ref['isGrouped'] && $ref['sumChance'] >= 100 && !$ref['chance']) + Util::addNote(U_GROUP_EMPLOYEE, 'Loot by Item: Item/Ref '.$ref['item'].' with adaptive chance cannot drop. Group already at 100%!'); + + $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 entry FROM achievement_reward WHERE item = ?d{ OR mailTemplate 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; + } + } + + $this->results = $tabsFinal; + + return true; + } +} + +?> \ No newline at end of file diff --git a/includes/markup.class.php b/includes/markup.class.php new file mode 100644 index 00000000..7d4df093 --- /dev/null +++ b/includes/markup.class.php @@ -0,0 +1,50 @@ +text = $text; + } + + public function parseGlobalsFromText(&$jsg = []) + { + if (preg_match_all('/(?text, $matches, PREG_SET_ORDER)) + { + foreach ($matches as $match) + { + if ($match[1] == 'statistic') + $match[1] = 'achievement'; + + 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 fromHtml() + { + } + + public function toHtml() + { + } +} + +?> diff --git a/includes/setup/cli.class.php b/includes/setup/cli.class.php deleted file mode 100644 index ef16296f..00000000 --- a/includes/setup/cli.class.php +++ /dev/null @@ -1,350 +0,0 @@ - $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 deleted file mode 100644 index 2bf355bc..00000000 --- a/includes/setup/datatypes/primitives.php +++ /dev/null @@ -1,104 +0,0 @@ -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 deleted file mode 100644 index c34dba3d..00000000 --- a/includes/setup/files/binaryfile.class.php +++ /dev/null @@ -1,202 +0,0 @@ -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 deleted file mode 100644 index 25d9d170..00000000 --- a/includes/setup/files/dbcfile.class.php +++ /dev/null @@ -1,83 +0,0 @@ -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 deleted file mode 100644 index 7c36620d..00000000 --- a/includes/setup/timer.class.php +++ /dev/null @@ -1,39 +0,0 @@ -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 new file mode 100644 index 00000000..9b5f5248 --- /dev/null +++ b/includes/shared.php @@ -0,0 +1,26 @@ +'.$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/tools/BLPConverter b/includes/tools/BLPConverter new file mode 160000 index 00000000..7217e135 --- /dev/null +++ b/includes/tools/BLPConverter @@ -0,0 +1 @@ +Subproject commit 7217e13598994f19830887f6f9c520217464a27e diff --git a/includes/tools/MPQExtractor b/includes/tools/MPQExtractor new file mode 160000 index 00000000..32c72bb5 --- /dev/null +++ b/includes/tools/MPQExtractor @@ -0,0 +1 @@ +Subproject commit 32c72bb5f075f6f2ce93fa37a677de4cf507ed70 diff --git a/includes/type.class.php b/includes/type.class.php deleted file mode 100644 index 6676009b..00000000 --- a/includes/type.class.php +++ /dev/null @@ -1,260 +0,0 @@ - [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/achievement.class.php b/includes/types/achievement.class.php new file mode 100644 index 00000000..3a2c1224 --- /dev/null +++ b/includes/types/achievement.class.php @@ -0,0 +1,403 @@ + [['si'], 'o' => 'orderInGroup ASC'], + 'si' => ['j' => ['?_icons si ON si.id = a.iconId', true], 's' => ', si.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) + { + parent::__construct($conditions, $miscData); + + if ($this->error) + return; + + // post processing + $rewards = DB::World()->select(' + SELECT ar.entry AS ARRAY_KEY, ar.*, lar.* FROM achievement_reward ar LEFT JOIN locales_achievement_reward lar ON lar.entry = ar.entry WHERE ar.entry IN (?a)', + $this->getFoundIDs() + ); + + foreach ($this->iterate() as $_id => &$_curTpl) + { + $_curTpl['rewards'] = []; + + if (!empty($rewards[$_id])) + { + $_curTpl = array_merge($rewards[$_id], $_curTpl); + + if ($rewards[$_id]['mailTemplate']) + { + // 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']); + // foreach ($mailSrc->iterate() as $loot) + // $_curTpl['rewards'][] = [TYPE_ITEM, $loot['id']]; + + // lets just assume for now, that mailRewards for achievements do not contain references + $mailRew = DB::World()->selectCol('SELECT Item FROM mail_loot_template WHERE Reference <= 0 AND entry = ?d', $rewards[$_id]['mailTemplate']); + foreach ($mailRew AS $mr) + $_curTpl['rewards'][] = [TYPE_ITEM, $mr]; + } + } + + //"rewards":[[11,137],[3,138]] [type, typeId] + if (!empty($_curTpl['item'])) + $_curTpl['rewards'][] = [TYPE_ITEM, $_curTpl['item']]; + if (!empty($_curTpl['itemExtra'])) + $_curTpl['rewards'][] = [TYPE_ITEM, $_curTpl['itemExtra']]; + if (!empty($_curTpl['title_A'])) + $_curTpl['rewards'][] = [TYPE_TITLE, $_curTpl['title_A']]; + if (!empty($_curTpl['title_H'])) + if (empty($_curTpl['title_A']) || $_curTpl['title_A'] != $_curTpl['title_H']) + $_curTpl['rewards'][] = [TYPE_TITLE, $_curTpl['title_H']]; + + // icon + $_curTpl['iconString'] = $_curTpl['iconString'] ?: 'trade_engineering'; + } + } + + public function getJSGlobals($addMask = GLOBALINFO_ANY) + { + $data = []; + + foreach ($this->iterate() as $__) + { + if ($addMask & GLOBALINFO_SELF) + $data[TYPE_ACHIEVEMENT][$this->id] = ['icon' => $this->curTpl['iconString'], 'name' => $this->getField('name', true)]; + + if ($addMask & GLOBALINFO_REWARDS) + foreach ($this->curTpl['rewards'] as $_) + $data[$_[0]][$_[1]] = $_[1]; + } + + return $data; + } + + public function getListviewData($addInfoMask = 0x0) + { + $data = []; + + foreach ($this->iterate() as $__) + { + $data[$this->id] = array( + 'id' => $this->id, + 'name' => $this->getField('name', true), + 'description' => $this->getField('description', true), + 'points' => $this->curTpl['points'], + 'side' => $this->curTpl['faction'], + 'category' => $this->curTpl['category'], + 'parentcat' => $this->curTpl['parentCat'], + ); + + if ($addInfoMask & ACHIEVEMENTINFO_PROFILE) + $data[$this->id]['icon'] = $this->curTpl['iconString']; + + // going out on a limb here: type = 1 if in level 3 of statistics tree, so, IF (statistic AND parentCat NOT statistic (1)) i guess + if ($this->curTpl['flags'] & ACHIEVEMENT_FLAG_COUNTER && $this->curTpl['parentCat'] != 1) + $data[$this->id]['type'] = 1; + + if ($_ = $this->curTpl['rewards']) + $data[$this->id]['rewards'] = $_; + else if ($_ = $this->getField('reward', true)) + $data[$this->id]['reward'] = $_; + } + + return $data; + } + + // only for current template + public function getCriteria() + { + 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); + if (!$result) + return []; + + $this->criteria[$this->id] = $result; + + return $this->criteria[$this->id]; + } + + public function renderTooltip() + { + $criteria = $this->getCriteria(); + $tmp = []; + $rows = []; + $i = 0; + foreach ($criteria as $_row) + { + if ($i++ % 2) + $tmp[] = $_row; + else + $rows[] = $_row; + } + if ($tmp) + $rows = array_merge($rows, $tmp); + + $description = $this->getField('description', true); + $name = $this->getField('name', true); + $criteria = ''; + + $i = 0; + foreach ($rows as $crt) + { + $obj = (int)$crt['value1']; + $qty = (int)$crt['value2']; + + // we could show them, but the tooltips are cluttered + if (($crt['completionFlags'] & ACHIEVEMENT_CRITERIA_FLAG_HIDDEN) && User::$perms <= 0) + 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) + $crtName = QuestList::getName($obj); + break; + // link to spell (/w icon) + 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: + if (!$crtName) + $crtName = SpellList::getName($obj); + break; + // link to item (/w icon) + case ACHIEVEMENT_CRITERIA_TYPE_OWN_ITEM: + case ACHIEVEMENT_CRITERIA_TYPE_USE_ITEM: + case ACHIEVEMENT_CRITERIA_TYPE_LOOT_ITEM: + case ACHIEVEMENT_CRITERIA_TYPE_EQUIP_ITEM: + if (!$crtName) + $crtName = ItemList::getName($obj); + break; + // link to faction (/w target reputation) + case ACHIEVEMENT_CRITERIA_TYPE_GAIN_REPUTATION: + if (!$crtName) + $crtName = FactionList::getName($obj); + break; + } + + if ($crt['completionFlags'] & ACHIEVEMENT_CRITERIA_FLAG_MONEY_COUNTER) + $criteria .= '- '.Util::jsEscape($crtName).' '.number_format($crt['value2' ] / 10000).'
'; + else + $criteria .= '- '.Util::jsEscape($crtName).'
'; + + if (++$i == round(count($rows)/2)) + $criteria .= '
'; + } + + $x = '
'; + $x .= Util::jsEscape($name); + $x .= '
'; + if ($description || $criteria) + $x .= '
'; + + if ($description) + $x .= '
'.Util::jsEscape($description).'
'; + + if ($criteria) + { + $x .= '
'.Lang::achievement('criteria').':'; + $x .= '
'.$criteria.'
'; + } + if ($description || $criteria) + $x .= '
'; + + return $x; + } + + public function getSourceData() + { + $data = []; + + foreach ($this->iterate() as $__) + { + $data[$this->id] = array( + "n" => $this->getField('name', true), + "s" => $this->curTpl['faction'], + "t" => TYPE_ACHIEVEMENT, + "ti" => $this->id + ); + } + + return $data; + } +} + + +class AchievementListFilter extends Filter +{ + + protected $enums = array( + 11 => array( + 327 => 160, // Lunar Festival + 335 => 187, // Love is in the Air + 181 => 159, // Noblegarden + 201 => 163, // Children's Week + 341 => 161, // Midsummer Fire Festival + 372 => 162, // Brewfest + 324 => 158, // Hallow's End + 404 => 14981, // Pilgrim's Bounty + 141 => 156, // Feast of Winter Veil + 409 => -3456, // Day of the Dead + 398 => -3457, // Pirates' Day + FILTER_ENUM_ANY => true, + FILTER_ENUM_NONE => false, + 283 => -1, // valid events without achievements + 285 => -1, 353 => -1, 420 => -1, + 400 => -1, 284 => -1, 374 => -1, + 321 => -1, 424 => -1, 301 => -1 + ) + ); + 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', true ], // rewardtext + 7 => [FILTER_CR_BOOLEAN, 'chainId', ], // partseries + 9 => [FILTER_CR_NUMERIC, 'id', null, true], // id + 10 => [FILTER_CR_STRING, 'si.iconString', ], // icon + 18 => [FILTER_CR_STAFFFLAG, 'flags', ], // flags + 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 + ); + + 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]; + } + + switch ($cr[0]) + { + case 4: // location [enum] +/* todo */ return [1]; // no plausible locations parsed yet + case 5: // first in series [yn] + if ($this->int2Bool($cr[1])) + return $cr[1] ? ['AND', ['chainId', 0, '!'], ['cuFlags', ACHIEVEMENT_CU_FIRST_SERIES, '&']] : ['AND', ['chainId', 0, '!'], [['cuFlags', ACHIEVEMENT_CU_FIRST_SERIES, '&'], 0]]; + + break; + case 6: // last in series [yn] + if ($this->int2Bool($cr[1])) + return $cr[1] ? ['AND', ['chainId', 0, '!'], ['cuFlags', ACHIEVEMENT_CU_LAST_SERIES, '&']] : ['AND', ['chainId', 0, '!'], [['cuFlags', ACHIEVEMENT_CU_LAST_SERIES, '&'], 0]]; + + break; + case 11: // Related Event [enum] + $_ = isset($this->enums[$cr[0]][$cr[1]]) ? $this->enums[$cr[0]][$cr[1]] : null; + if ($_ !== null) + { + if (is_int($_)) + return ($_ > 0) ? ['category', $_] : ['id', abs($_)]; + else + { + $ids = array_filter($this->enums[$cr[0]], function($x) { + return is_int($x) && $x > 0; + }); + + return ['category', $ids, $_ ? null : '!']; + } + } + break; + } + + unset($cr); + $this->error = true; + return [1]; + } + + protected function createSQLForValues() + { + $parts = []; + $_v = &$this->fiData['v']; + + // name ex: +description, +rewards + if (isset($_v['na'])) + { + $_ = []; + if (isset($_v['ex']) && $_v['ex'] == 'on') + $_ = $this->modularizeString(['name_loc'.User::$localeId, 'reward_loc'.User::$localeId, 'description_loc'.User::$localeId]); + else + $_ = $this->modularizeString(['name_loc'.User::$localeId]); + + if ($_) + $parts[] = $_; + } + + // points min + if (isset($_v['minpt'])) + { + if ($this->isSaneNumeric($_v['minpt'])) + $parts[] = ['points', $_v['minpt'], '>=']; + else + unset($_v['minpt']); + } + + // points max + if (isset($_v['maxpt'])) + { + if ($this->isSaneNumeric($_v['maxpt'])) + $parts[] = ['points', $_v['maxpt'], '<=']; + else + unset($_v['maxpt']); + } + + // faction (side) + if (isset($_v['si'])) + { + switch ($_v['si']) + { + case 3: // both + $parts[] = ['faction', 0]; + break; + case -1: // faction, exclusive both + case -2: + $parts[] = ['faction', -$_v['si']]; + break; + case 1: // faction, inclusive both + case 2: + $parts[] = ['OR', ['faction', 0], ['faction', $_v['si']]]; + break; + default: + unset($_v['si']); + } + } + + return $parts; + } +} + +?> diff --git a/includes/types/basetype.class.php b/includes/types/basetype.class.php new file mode 100644 index 00000000..fb17643b --- /dev/null +++ b/includes/types/basetype.class.php @@ -0,0 +1,1083 @@ + + * 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) + { + $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 '('.implode($subLink, $sql).')'; + } + 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]); + + // 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; + + // execure query (finally) + $rows = DB::Aowow()->SelectPage($this->matches, $this->queryBase); + if (!$rows) + return; + + // assign query results to template + foreach ($rows as $k => $tpl) + $this->templates[$k] = $tpl; + + // push first element for instant use + $this->reset(); + + // all clear + $this->error = false; + } + + public function &iterate() + { + // 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 + } + + // reset on __destruct .. Generator, Y U NO HAVE __destruct ?! + $this->reset(); + } + + 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]; + if (Util::checkNumeric($value)) + { + $intVal = intVal($value); + $floatVal = floatVal($value); + return $intVal == $floatVal ? $intVal : $floatVal; + } + else + return $value; + } + + 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) + { + if (!isset($this->queryOpts[$tbl])) // allow adding only to known tables + continue; + + 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 (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 + ); + + 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); + + $set = ['label' => '$
'.implode('
', $label).'
', 'type' => $wpIdx]; + + // connective line + if ($i > 0) + $set['lines'] = [[$wPoints[$i - 1]['posX'], $wPoints[$i - 1]['posY']]]; + + $data[$s['areaId']][$s['floor']]['coords'][] = [$p['posX'], $p['posY'], $set]; + if (empty($wpSum[$s['areaId']][$s['floor']])) + $wpSum[$s['areaId']][$s['floor']] = 1; + else + $wpSum[$s['areaId']][$s['floor']]++; + } + $wpIdx++; + } + } + + $label = []; + + if (User::isInGroup(U_GROUP_STAFF)) + $label[] = $s['guid'] < 0 ? 'Vehicle Accessory' : 'GUID'.Lang::main('colon').$s['guid']; + + if ($s['respawn']) + $label[] = Lang::npc('respawnIn').Lang::main('colon').Util::formatTime($s['respawn'] * 1000, false); + + if (User::isInGroup(U_GROUP_STAFF)) + { + if ($s['phaseMask'] > 1) + $label[] = Lang::game('phases').Lang::main('colon').$s['phaseMask']; + + if ($s['spawnMask'] == 15) + $label[] = 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); + + $label[] = Lang::game('mode').Lang::main('colon').implode(', ', $_); + } + } + + $data[$s['areaId']] [$s['floor']] ['coords'] [] = [$s['posX'], $s['posY'], ['label' => '$
'.implode('
', $label).'
']]; + } + 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; + } + + public function getSpawns($mode) + { + // ony Creatures and GOs can be spawned + if (!self::$type || (self::$type != TYPE_NPC && self::$type != TYPE_OBJECT)) + 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] : []; + } + + return []; + } +} + +/* + 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 $pattern = "/[^\p{L}0-9\s_\-\'\.\?\*]/ui";// delete any char not in unicode, number, space, underscore, hyphen, single quote, dot or common wildcard + private static $wCards = ['*' => '%', '?' => '_']; + private static $criteria = ['cr', 'crs', 'crv']; // [cr]iterium, [cr].[s]ign, [cr].[v]alue + + public $error = false; // erronous search fields + + private $cndSet = []; + + 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 + ); + + // parse the provided request into a usable format; recall self with GET-params if nessecary + public function __construct() + { + // prefer POST over GET, translate to url + if (!empty($_POST)) + { + foreach ($_POST as $k => $v) + { + if (is_array($v)) // array -> max depths:1 + { + if (in_array($k, ['cr', 'wt']) && empty($v[0])) + continue; + + $sub = []; + foreach ($v as $sk => $sv) + $sub[$sk] = Util::checkNumeric($sv) ? $sv : urlencode($sv); + + if (!empty($sub) && in_array($k, self::$criteria)) + $this->fiData['c'][$k] = $sub; + else if (!empty($sub)) + $this->fiData['v'][$k] = $sub; + } + else // stings and integer + { + if (in_array($k, self::$criteria)) + $this->fiData['c'][$k] = Util::checkNumeric($v) ? $v : urlencode($v); + else + $this->fiData['v'][$k] = Util::checkNumeric($v) ? $v : urlencode($v); + } + } + + // do get request + header('Location: '.HOST_URL.'?'.$_SERVER['QUERY_STRING'].'='.$this->urlize(), true, 302); + } + // sanitize input and build sql + else if (!empty($_GET['filter'])) + { + $tmp = explode(';', $_GET['filter']); + $cr = $crs = $crv = []; + + foreach (self::$criteria as $c) + { + foreach ($tmp as $i => $term) + { + if (strpos($term, $c.'=') === 0) + { + $$c = explode(':', explode('=', $term)[1]); + $this->formData['setCriteria'][$c] = $$c; // todo (high): move to checks + unset($tmp[$i]); + } + } + } + + for ($i = 0; $i < max(count($cr), count($crv), count($crs)); $i++) + { + if (!isset($cr[$i]) || !isset($crs[$i]) || !isset($crv[$i]) || + !intVal($cr[$i]) || $crs[$i] === '' || $crv[$i] === '') + { + $this->error = true; + continue; + } + + $this->sanitize($crv[$i]); + + if ($crv[$i] !== '') + { + $this->fiData['c']['cr'][] = intVal($cr[$i]); + $this->fiData['c']['crs'][] = intVal($crs[$i]); + $this->fiData['c']['crv'][] = $crv[$i]; + } + else + $this->error = true; + + } + + foreach ($tmp as $v) + { + if (!strstr($v, '=')) + continue; + + $w = explode('=', $v); + + if (strstr($w[1], ':')) + { + $tmp2 = explode(':', $w[1]); + + $this->formData['form'][$w[0]] = $tmp2; + + array_walk($tmp2, function(&$v) { + $v = intVal($v); + }); + $this->fiData['v'][$w[0]] = $tmp2; + } + else + { + $this->formData['form'][$w[0]] = $w[1]; + + $this->sanitize($w[1]); + + if ($w[1] !== '') + $this->fiData['v'][$w[0]] = $w[1]; + else + $this->error = true; + } + } + } + } + + // use to generate cacheKey for filterable pages + public function __sleep() + { + return ['formData']; + } + + public function urlize(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(';', $_); + } + + // todo: kill data, that is unexpected or points to wrong indizes + private function evaluateFilter() + { + // 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'); + } + + public function getForm($key = null, $raw = false) + { + $form = []; + + if (!$this->formData) + return $form; + + foreach ($this->formData as $name => $data) + { + if (!$data || ($key && $name != $key)) + continue; + + switch ($name) + { + case 'setCriteria': + if ($data || $raw) + $form[$name] = $raw ? $data : 'fi_setCriteria('.Util::toJSON($data['cr']).', '.Util::toJSON($data['crs']).', '.Util::toJSON($data['crv']).');'; + else + $form[$name] = 'fi_setCriteria([], [], []);'; + break; + case 'extraCols': + $form[$name] = $raw ? $data : 'fi_extraCols = '.Util::toJSON(array_unique($data)).';'; + break; + case 'setWeights': + $form[$name] = $raw ? $data : 'fi_setWeights('.Util::toJSON($data).', 0, 1, 1);'; + break; + case 'form': + if ($key == $name) // only if explicitely specified + $form[$name] = $data; + break; + default: + break; + } + } + + return $key ? (empty($form[$key]) ? [] : $form[$key]) : $form; + } + + public function getConditions() + { + if (!$this->cndSet) + $this->evaluateFilter(); + + return $this->cndSet; + } + + // santas little helper.. + 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; + } + } + + protected function modularizeString(array $fields, $string = '') + { + if (!$string) + $string = $this->fiData['v']['na']; + + $qry = []; + foreach ($fields as $n => $f) + { + $sub = []; + $parts = array_filter(explode(' ', $string)); + + foreach ($parts as $p) + { + if ($p[0] == '-' && strlen($p) > 3) + $sub[] = [$f, '%'.substr($p, 1).'%', '!']; + else if ($p[0] != '-' && strlen($p) > 2) + $sub[] = [$f, '%'.$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($list, $noOffset = false) + { + $mask = 0x0; + $o = $noOffset ? 0 : 1; // schoolMask requires this..? + + if (!is_array($list)) + $mask = (1 << (intVal($list) - $o)); + else + foreach ($list as $itm) + $mask += (1 << (intVal($itm) - $o)); + + return $mask; + } + + protected function isSaneNumeric(&$val, $castInt = true) + { + if ($castInt && is_float($val)) + $val = intVal($val); + + if (is_int($val) || (is_float($val) && $val >= 0.0)) + return true; + + return false; + } + + private function sanitize(&$str) + { + $str = preg_replace(Filter::$pattern, '', trim($str)); + $str = Util::checkNumeric($str) ? $str : strtr($str, Filter::$wCards); + } + + 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, $localized) + { + if (!$localized) + return [$field, (string)$value]; + + return $this->modularizeString([$field.'_loc'.User::$localeId], $value); + } + + private function genericNumeric($field, &$value, $op, $castInt) + { + if (!$this->isSaneNumeric($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; + + switch ($gen[0]) + { + case FILTER_CR_NUMERIC: + $result = $this->genericNumeric($gen[1], $cr[2], $cr[1], empty($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], !empty($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; + } + + if ($result && !empty($gen[3])) + $this->formData['extraCols'][] = $cr[0]; + + return $result; + } + + abstract protected function createSQLForCriterium(&$cr); + abstract protected function createSQLForValues(); +} + +?> diff --git a/includes/dbtypes/charclass.class.php b/includes/types/charclass.class.php similarity index 58% rename from includes/dbtypes/charclass.class.php rename to includes/types/charclass.class.php index 5c6db072..f4be8771 100644 --- a/includes/dbtypes/charclass.class.php +++ b/includes/types/charclass.class.php @@ -1,32 +1,25 @@ [['ic']], - 'ic' => ['j' => ['::icons ic ON ic.`id` = c.`iconId`', true], 's' => ', ic.`name` AS "iconString"'] - ); + protected $queryBase = 'SELECT *, id AS ARRAY_KEY FROM ?_classes c'; - public function __construct($conditions = [], array $miscData = []) + public function __construct($conditions = []) { - parent::__construct($conditions, $miscData); + parent::__construct($conditions); foreach ($this->iterate() as $k => &$_curTpl) $_curTpl['skills'] = explode(' ', $_curTpl['skills']); } - public function getListviewData() : array + public function getListviewData() { $data = []; @@ -52,7 +45,7 @@ class CharClassList extends DBTypeList return $data; } - public function getJSGlobals(int $addMask = 0) : array + public function getJSGlobals($addMask = 0) { $data = []; @@ -62,7 +55,8 @@ class CharClassList extends DBTypeList return $data; } - public function renderTooltip() : ?string { return null; } + public function addRewardsToJScript(&$ref) { } + public function renderTooltip() { } } ?> diff --git a/includes/types/charrace.class.php b/includes/types/charrace.class.php new file mode 100644 index 00000000..57d2ab62 --- /dev/null +++ b/includes/types/charrace.class.php @@ -0,0 +1,51 @@ +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 new file mode 100644 index 00000000..0356fb6d --- /dev/null +++ b/includes/types/creature.class.php @@ -0,0 +1,579 @@ + [['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); + + $row3[] = '('.Lang::npc('rank', $this->curTpl['rank']).')'; + + $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): + */ + + $data = []; + + 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']; + } + } + + 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 + 5 => [FILTER_CR_FLAG, 'npcflag', NPC_FLAG_REPAIRER ], // canrepair + 6 => [FILTER_CR_ENUM, 's.areaId', null ], // foundin + 9 => [FILTER_CR_BOOLEAN, 'lootId', ], // lootable + 11 => [FILTER_CR_BOOLEAN, 'pickpocketLootId', ], // pickpocketable + 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 + 19 => [FILTER_CR_FLAG, 'npcflag', NPC_FLAG_BANKER ], // banker + 37 => [FILTER_CR_NUMERIC, 'id', null, true], // id + 35 => [FILTER_CR_STRING, 'textureString' ], // useskin + 32 => [FILTER_CR_FLAG, 'cuFlags', NPC_CU_INSTANCE_BOSS ], // instanceboss + 33 => [FILTER_CR_FLAG, 'cuFlags', CUSTOM_HAS_COMMENT ], // hascomments + 31 => [FILTER_CR_FLAG, 'cuFlags', CUSTOM_HAS_SCREENSHOT ], // hasscreenshots + 40 => [FILTER_CR_FLAG, 'cuFlags', CUSTOM_HAS_VIDEO ], // hasvideos + ); + + 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]; + } + + switch ($cr[0]) + { + case 1: // health [num] + if (!$this->isSaneNumeric($cr[2]) || !$this->int2Op($cr[1])) + break; + + // remap OP for this special case + switch ($cr[1]) + { + case '=': // min > max is totally possible + $this->extraOpts['ct']['h'][] = 'healthMin = healthMax AND healthMin = '.$cr[2]; + break; + case '>': + $this->extraOpts['ct']['h'][] = 'IF(healthMin > healthMax, healthMax, healthMin) > '.$cr[2]; + break; + case '>=': + $this->extraOpts['ct']['h'][] = 'IF(healthMin > healthMax, healthMax, healthMin) >= '.$cr[2]; + break; + case '<': + $this->extraOpts['ct']['h'][] = 'IF(healthMin > healthMax, healthMin, healthMax) < '.$cr[2]; + break; + case '<=': + $this->extraOpts['ct']['h'][] = 'IF(healthMin > healthMax, healthMin, healthMax) <= '.$cr[2]; + break; + } + return [1]; // always true, use post-filter + case 2: // mana [num] + if (!$this->isSaneNumeric($cr[2]) || !$this->int2Op($cr[1])) + break; + + // remap OP for this special case + switch ($cr[1]) + { + case '=': + $this->extraOpts['ct']['h'][] = 'manaMin = manaMax AND manaMin = '.$cr[2]; + break; + case '>': + $this->extraOpts['ct']['h'][] = 'IF(manaMin > manaMax, manaMin, manaMax) > '.$cr[2]; + break; + case '>=': + $this->extraOpts['ct']['h'][] = 'IF(manaMin > manaMax, manaMin, manaMax) >= '.$cr[2]; + break; + case '<': + $this->extraOpts['ct']['h'][] = 'IF(manaMin > manaMax, manaMax, manaMin) < '.$cr[2]; + break; + case '<=': + $this->extraOpts['ct']['h'][] = 'IF(manaMin > manaMax, manaMax, manaMin) <= '.$cr[2]; + break; + } + return [1]; // always true, use post-filter + case 7: // startsquest [enum] + switch ($cr[1]) + { + case 1: // any + return ['AND', ['qse.method', 0x1, '&'], ['qse.questId', null, '!']]; + case 2: // alliance + return ['AND', ['qse.method', 0x1, '&'], ['qse.questId', null, '!'], [['qt.reqRaceMask', RACE_MASK_HORDE, '&'], 0], ['qt.reqRaceMask', RACE_MASK_ALLIANCE, '&']]; + case 3: // horde + return ['AND', ['qse.method', 0x1, '&'], ['qse.questId', null, '!'], [['qt.reqRaceMask', RACE_MASK_ALLIANCE, '&'], 0], ['qt.reqRaceMask', RACE_MASK_HORDE, '&']]; + case 4: // both + return ['AND', ['qse.method', 0x1, '&'], ['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'][] = 'startsQuests = 0'; + return [1]; + } + break; + case 8: // endsquest [enum] + switch ($cr[1]) + { + case 1: // any + return ['AND', ['qse.method', 0x2, '&'], ['qse.questId', null, '!']]; + case 2: // alliance + return ['AND', ['qse.method', 0x2, '&'], ['qse.questId', null, '!'], [['qt.reqRaceMask', RACE_MASK_HORDE, '&'], 0], ['qt.reqRaceMask', RACE_MASK_ALLIANCE, '&']]; + case 3: // horde + return ['AND', ['qse.method', 0x2, '&'], ['qse.questId', null, '!'], [['qt.reqRaceMask', RACE_MASK_ALLIANCE, '&'], 0], ['qt.reqRaceMask', RACE_MASK_HORDE, '&']]; + case 4: // both + return ['AND', ['qse.method', 0x2, '&'], ['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'][] = 'endsQuests = 0'; + return [1]; + } + break; + case 3: // faction [enum] + if (in_array($cr[1], $this->enums[$cr[0]])) + { + $facTpls = []; + $facs = new FactionList(array('OR', ['parentFactionId', $cr[1]], ['id', $cr[1]])); + foreach ($facs->iterate() as $__) + $facTpls = array_merge($facTpls, $facs->getField('templateIds')); + + if (!$facTpls) + return [0]; + + return ['faction', $facTpls]; + } + break; + case 38; // relatedevent + if (!$this->isSaneNumeric($cr[1])) + break; + + 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]; + } + + break; + case 42: // increasesrepwith [enum] + if (in_array($cr[1], $this->enums[3])) // reuse + { + if ($cIds = DB::World()->selectCol('SELECT creature_id FROM creature_onkill_reputation WHERE (RewOnKillRepFaction1 = ?d AND RewOnKillRepValue1 > 0) OR (RewOnKillRepFaction2 = ?d AND RewOnKillRepValue2 > 0)', $cr[1], $cr[1])) + return ['id', $cIds]; + else + return [0]; + } + + break; + case 43: // decreasesrepwith [enum] + if (in_array($cr[1], $this->enums[3])) // reuse + { + if ($cIds = DB::World()->selectCol('SELECT creature_id FROM creature_onkill_reputation WHERE (RewOnKillRepFaction1 = ?d AND RewOnKillRepValue1 < 0) OR (RewOnKillRepFaction2 = ?d AND RewOnKillRepValue2 < 0)', $cr[1], $cr[1])) + return ['id', $cIds]; + else + return [0]; + } + + break; + case 12: // averagemoneydropped [op] [int] + if (!$this->isSaneNumeric($cr[2]) || !$this->int2Op($cr[1])) + break; + + return ['AND', ['((minGold + maxGold) / 2)', $cr[2], $cr[1]]]; + case 15: // gatherable [yn] + if ($this->int2Bool($cr[1])) + { + if ($cr[1]) + return ['AND', ['skinLootId', 0, '>'], ['typeFlags', NPC_TYPEFLAG_HERBLOOT, '&']]; + else + return ['OR', ['skinLootId', 0], [['typeFlags', NPC_TYPEFLAG_HERBLOOT, '&'], 0]]; + } + break; + case 44: // salvageable [yn] + if ($this->int2Bool($cr[1])) + { + if ($cr[1]) + return ['AND', ['skinLootId', 0, '>'], ['typeFlags', NPC_TYPEFLAG_ENGINEERLOOT, '&']]; + else + return ['OR', ['skinLootId', 0], [['typeFlags', NPC_TYPEFLAG_ENGINEERLOOT, '&'], 0]]; + } + break; + case 16: // minable [yn] + if ($this->int2Bool($cr[1])) + { + if ($cr[1]) + return ['AND', ['skinLootId', 0, '>'], ['typeFlags', NPC_TYPEFLAG_MININGLOOT, '&']]; + else + return ['OR', ['skinLootId', 0], [['typeFlags', NPC_TYPEFLAG_MININGLOOT, '&'], 0]]; + } + break; + case 10: // skinnable [yn] + if ($this->int2Bool($cr[1])) + { + if ($cr[1]) + return ['AND', ['skinLootId', 0, '>'], [['typeFlags', NPC_TYPEFLAG_SPECIALLOOT, '&'], 0]]; + else + return ['OR', ['skinLootId', 0], [['typeFlags', NPC_TYPEFLAG_SPECIALLOOT, '&'], 0, '!']]; + } + break; + case 34: // usemodel [str] // displayId -> id:creatureDisplayInfo.dbc/model -> id:cratureModelData.dbc/modelPath + case 41: // haslocation [yn] [staff] +/* todo */ return [1]; + } + + 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'])) + { + $_ = (array)$_v['fa']; + if (!array_diff($_, [1, 2, 3, 4, 5, 6, 7, 8, 9, 11, 12, 20, 21, 24, 25, 26, 27, 30, 31, 32, 33, 34, 35, 37, 38, 39, 41, 42, 43, 44, 45, 46])) + $parts[] = ['family', $_]; + else + unset($_v['cl']); + } + + // creatureLevel min [int] + if (isset($_v['minle'])) + { + if (is_int($_v['minle']) && $_v['minle'] > 0) + $parts[] = ['minLevel', $_v['minle'], '>=']; + else + unset($_v['minle']); + } + + // creatureLevel max [int] + if (isset($_v['maxle'])) + { + if (is_int($_v['maxle']) && $_v['maxle'] > 0) + $parts[] = ['maxLevel', $_v['maxle'], '<=']; + else + unset($_v['maxle']); + } + + // classification [list] + if (isset($_v['cl'])) + { + $_ = (array)$_v['cl']; + if (!array_diff($_, [0, 1, 2, 3, 4])) + $parts[] = ['rank', $_]; + else + unset($_v['cl']); + } + + // react Alliance [int] + if (isset($_v['ra'])) + { + $_ = (int)$_v['ra']; + if (in_array($_, [-1, 0, 1])) + $parts[] = ['ft.A', $_]; + else + unset($_v['ra']); + } + + // react Horde [int] + if (isset($_v['rh'])) + { + $_ = (int)$_v['rh']; + if (in_array($_, [-1, 0, 1])) + $parts[] = ['ft.H', $_]; + else + unset($_v['rh']); + } + + return $parts; + } + +} + +?> diff --git a/includes/types/currency.class.php b/includes/types/currency.class.php new file mode 100644 index 00000000..8b219883 --- /dev/null +++ b/includes/types/currency.class.php @@ -0,0 +1,58 @@ + [['ic']], + 'ic' => ['j' => ['?_icons ic ON ic.id = c.iconId', true], 's' => ', ic.iconString'] + ); + + 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() { } +} + +?> diff --git a/includes/dbtypes/faction.class.php b/includes/types/faction.class.php similarity index 56% rename from includes/dbtypes/faction.class.php rename to includes/types/faction.class.php index cf76280d..9c0ef6ba 100644 --- a/includes/dbtypes/faction.class.php +++ b/includes/types/faction.class.php @@ -1,27 +1,24 @@ [['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`'] - ); + protected $queryBase = 'SELECT f.*, f.parentFactionId AS cat, f.id AS ARRAY_KEY FROM ?_factions f'; + protected $queryOpts = array( + 'f' => [['f2']], + 'f2' => ['j' => ['?_factions f2 ON f.parentFactionId = f2.id', true], 's' => ', IFNULL(f2.parentFactionId, 0) AS cat2'], + 'ft' => ['j' => '?_factiontemplate ft ON ft.factionId = f.id'] + ); - public function __construct(array $conditions = [], array $miscData = []) + public function __construct($conditions = []) { - parent::__construct($conditions, $miscData); + parent::__construct($conditions); if ($this->error) return; @@ -37,7 +34,13 @@ class FactionList extends DBTypeList } } - public function getListviewData() : array + 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() { $data = []; @@ -66,17 +69,17 @@ class FactionList extends DBTypeList return $data; } - public function getJSGlobals(int $addMask = 0) : array + public function getJSGlobals($addMask = 0) { $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() : ?string { return null; } + public function renderTooltip() { } } diff --git a/includes/types/gameobject.class.php b/includes/types/gameobject.class.php new file mode 100644 index 00000000..f93681f5 --- /dev/null +++ b/includes/types/gameobject.class.php @@ -0,0 +1,252 @@ + [['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 ($_ = 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 + 7 => [FILTER_CR_NUMERIC, 'reqSkill', null ], // requiredskilllevel + 11 => [FILTER_CR_FLAG, 'cuFlags', CUSTOM_HAS_SCREENSHOT], // hasscreenshots + 13 => [FILTER_CR_FLAG, 'cuFlags', CUSTOM_HAS_COMMENT ], // hascomments + 15 => [FILTER_CR_NUMERIC, 'id', null ], // id + 18 => [FILTER_CR_FLAG, 'cuFlags', CUSTOM_HAS_VIDEO ], // hasvideos + ); + + 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]; + } + + switch ($cr[0]) + { + case 4: + if (!$this->int2Bool($cr[1])) + break; + + return $cr[1] ? ['OR', ['flags', 0x2, '&'], ['type', 3]] : ['AND', [['flags', 0x2, '&'], 0], ['type', 3, '!']]; + case 5: // averagemoneycontained [op] [int] GOs don't contain money .. eval to 0 == true + if (!$this->isSaneNumeric($cr[2], false) || !$this->int2Op($cr[1])) + break; + + return eval('return ('.$cr[2].' '.$cr[1].' 0)') ? [1] : [0]; + case 2: // startsquest [side] + switch ($cr[1]) + { + case 1: // any + return ['AND', ['qse.method', 0x1, '&'], ['qse.questId', null, '!']]; + case 2: // alliance only + return ['AND', ['qse.method', 0x1, '&'], ['qse.questId', null, '!'], [['qt.reqRaceMask', RACE_MASK_HORDE, '&'], 0], ['qt.reqRaceMask', RACE_MASK_ALLIANCE, '&']]; + case 3: // horde only + return ['AND', ['qse.method', 0x1, '&'], ['qse.questId', null, '!'], [['qt.reqRaceMask', RACE_MASK_ALLIANCE, '&'], 0], ['qt.reqRaceMask', RACE_MASK_HORDE, '&']]; + case 4: // both + return ['AND', ['qse.method', 0x1, '&'], ['qse.questId', null, '!'], ['OR', ['AND', ['qt.reqRaceMask', RACE_MASK_ALLIANCE, '&'], ['qt.reqRaceMask', RACE_MASK_HORDE, '&']], ['qt.reqRaceMask', 0]]]; + case 5: // none + $this->extraOpts['o']['h'][] = 'startsQuests = 0'; + return [1]; + } + break; + case 3: // endsquest [side] + switch ($cr[1]) + { + case 1: // any + return ['AND', ['qse.method', 0x2, '&'], ['qse.questId', null, '!']]; + case 2: // alliance only + return ['AND', ['qse.method', 0x2, '&'], ['qse.questId', null, '!'], [['qt.reqRaceMask', RACE_MASK_HORDE, '&'], 0], ['qt.reqRaceMask', RACE_MASK_ALLIANCE, '&']]; + case 3: // horde only + return ['AND', ['qse.method', 0x2, '&'], ['qse.questId', null, '!'], [['qt.reqRaceMask', RACE_MASK_ALLIANCE, '&'], 0], ['qt.reqRaceMask', RACE_MASK_HORDE, '&']]; + case 4: // both + return ['AND', ['qse.method', 0x2, '&'], ['qse.questId', null, '!'], ['OR', ['AND', ['qt.reqRaceMask', RACE_MASK_ALLIANCE, '&'], ['qt.reqRaceMask', RACE_MASK_HORDE, '&']], ['qt.reqRaceMask', 0]]]; + case 5: // none todo: broken, if entry starts and ends quests... + $this->extraOpts['o']['h'][] = 'endsQuests = 0'; + return [1]; + } + break; + case 16; // relatedevent (ignore removed by event) + if (!$this->isSaneNumeric($cr[1])) + break; + + 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]; + } + + break; + } + + unset($cr); + $this->error = 1; + 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; + } +} + +?> diff --git a/includes/types/item.class.php b/includes/types/item.class.php new file mode 100644 index 00000000..e15559d3 --- /dev/null +++ b/includes/types/item.class.php @@ -0,0 +1,2393 @@ + [['is', 'src', 'ic'], 'o' => 'i.quality DESC, i.itemLevel DESC'], + 'ic' => ['j' => ['?_icons ic ON ic.id = -i.displayId', true], 's' => ', ic.iconString'], + 'is' => ['j' => ['?_item_stats `is` ON `is`.`id` = `i`.`id`', true], 's' => ', `is`.*'], + 's' => ['j' => ['?_spell `s` ON s.effect1CreateItemId = i.id', true], 'g' => 'i.id'], + 'src' => ['j' => ['?_source src ON type = 3 AND 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']; + if ($_ < 0 || ($_ & CLASS_MASK_ALL) == CLASS_MASK_ALL) + $_ = 0; + + $_ = &$_curTpl['requiredRace']; + if ($_ < 0 || ($_ & RACE_MASK_ALL) == RACE_MASK_ALL) + $_ = 0; + + // sources + if ($_curTpl['moreType'] && $_curTpl['moreTypeId']) + $this->sourceMore[$_curTpl['moreType']][] = $_curTpl['moreTypeId']; + + 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, IFNULL(IF(ge.holiday, ge.holiday, -ge.eventEntry), 0) 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(); + + // gather sourceMore data + if (!($addInfoMask & ITEMINFO_MODEL)) // probably others too + foreach ($this->sourceMore as $type => $ids) + $this->sourceMore[$type] = (new Util::$typeClasses[$type](array(['id', $ids], CFG_SQL_LIMIT_NONE)))->getSourceData(); + + 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 (!empty($this->sources[$this->id])) + { + $data[$this->id]['source'] = array_keys($this->sources[$this->id]); + if ($this->curTpl['moreType'] && $this->curTpl['moreTypeId'] && !empty($this->sourceMore[$this->curTpl['moreType']][$this->curTpl['moreTypeId']])) + $data[$this->id]['sourcemore'] = [$this->sourceMore[$this->curTpl['moreType']][$this->curTpl['moreTypeId']]]; + else if (!empty($this->sources[$this->id][3])) + $data[$this->id]['sourcemore'] = [['p' => $this->sources[$this->id][3][0]]]; + } + + 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 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 ($rndEnch = DB::Aowow()->selectRow('SELECT * FROM ?_itemrandomenchant WHERE Id = ?d', $enhance['r'])) + { + $_name .= ' '.Util::localizedString($rndEnch, 'name'); + $randEnchant = ''; + + for ($i = 1; $i < 6; $i++) + { + if ($rndEnch['enchantId'.$i] <= 0) + continue; + + $enchant = DB::Aowow()->selectRow('SELECT * FROM ?_itemenchantment WHERE Id = ?d', $rndEnch['enchantId'.$i]); + if ($rndEnch['allocationPct'.$i] > 0) + { + $amount = intVal($rndEnch['allocationPct'.$i] * $this->generateEnchSuffixFactor()); + $randEnchant .= ''.str_replace('$i', $amount, Util::localizedString($enchant, 'text')).'
'; + } + else + $randEnchant .= ''.Util::localizedString($enchant, 'text').'
'; + } + } + 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'] > 0) + { + $x .= '
'.Lang::item('unique'); + + if ($this->curTpl['maxCount'] > 1) + $x .= ' ('.$this->curTpl['maxCount'].')'; + } + else if ($_flags & ITEM_FLAG_UNIQUEEQUIPPED) + $x .= '
'.Lang::item('uniqueEquipped'); + else if ($this->curTpl['itemLimitCategory']) + { + $limit = DB::Aowow()->selectRow("SELECT * FROM ?_itemlimitcategory WHERE id = ?", $this->curTpl['itemLimitCategory']); + $x .= '
'.($limit['isGem'] ? Lang::item('uniqueEquipped') : Lang::item('unique')).Lang::main('colon').Util::localizedString($limit, 'name').' ('.$limit['count'].')'; + } + + // max duration + if ($dur = $this->curTpl['duration']) + $x .= "
".Lang::game('duration').Lang::main('colon').Util::formatTime(abs($dur) * 1000).($this->curTpl['flagsCustom'] & 0x1 ? ' ('.Lang::item('realTime').')' : null); + + // required holiday + if ($hId = $this->curTpl['holidayId']) + { + $hDay = DB::Aowow()->selectRow("SELECT * FROM ?_holidays WHERE id = ?", $hId); + $x .= '
'.sprintf(Lang::game('requires'), ''.Util::localizedString($hDay, '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; + + // word order differs <_< + if (in_array(User::$localeId, [LOCALE_FR, LOCALE_ES, LOCALE_RU])) + $x .= '
'.sprintf(Lang::item('bagSlotString'), Lang::item('bagFamily', $fam), $this->curTpl['slots']); + else + $x .= '
'.sprintf(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 + $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; + $dmgmin1 = $this->curTpl['dmgMin1'] + $this->curTpl['dmgMin2']; + $dmgmax1 = $this->curTpl['dmgMax1'] + $this->curTpl['dmgMax2']; + $dps = $speed ? ($dmgmin1 + $dmgmax1) / (2 * $speed) : 0; + + if ($_class == ITEM_CLASS_AMMUNITION && $dmgmin1 && $dmgmax1) + $x .= Lang::item('addsDps').' '.number_format(($dmgmin1 + $dmgmax1) / 2, 1).' '.Lang::item('dps2').'
'; + else if ($dps) + { + if ($_class == ITEM_CLASS_WEAPON) + { + $x .= ''; + $x .= ''; + $x .= ''; + $x .= '
'.sprintf($this->curTpl['dmgType1'] ? Lang::item('damageMagic') : Lang::item('damagePhys'), $this->curTpl['dmgMin1'].' - '.$this->curTpl['dmgMax1'], Lang::game('sc', $this->curTpl['dmgType1'])).''.Lang::item('speed').' '.number_format($speed, 2).'
'; + } + else + $x .= ''.sprintf($this->curTpl['dmgType1'] ? Lang::item('damageMagic') : Lang::item('damagePhys'), $this->curTpl['dmgMin1'].' - '.$this->curTpl['dmgMax1'], Lang::game('sc', $this->curTpl['dmgType1'])).'
'; + + // secondary damage is set + if ($this->curTpl['dmgMin2']) + $x .= '+'.sprintf($this->curTpl['dmgType2'] ? Lang::item('damageMagic') : Lang::item('damagePhys'), $this->curTpl['dmgMin2'].' - '.$this->curTpl['dmgMax2'], Lang::game('sc', $this->curTpl['dmgType2'])).'
'; + + if ($_class == ITEM_CLASS_WEAPON) + $x .= '('.number_format($dps, 1).' '.Lang::item('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 .= ''.sprintf(Lang::item('armor'), intVal($this->curTpl['armor'] + $this->curTpl['armorDamageModifier'])).'
'; + } + else if (($this->curTpl['armor'] + $this->curTpl['armorDamageModifier']) > 0) + $x .= ''.sprintf(Lang::item('armor'), intVal($this->curTpl['armor'] + $this->curTpl['armorDamageModifier'])).'
'; + + // Block + if ($this->curTpl['block']) + $x .= ''.sprintf(Lang::item('block'), $this->curTpl['block']).'
'; + + // 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, 'text').'
'; + + // activation conditions for meta gems + if ($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; + + 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').' '.sprintf(Lang::item('gemConditions', $gemCnd['comparator'.$i], $sp), $gemCnd['value'.$i], Lang::item('gemColors', $gemCnd['color'.$i] - 1)).'
'; + break; + case 3: // requires more than ( || ) gems + $x .= ''.Lang::achievement('reqNumCrt').' '.sprintf(Lang::item('gemConditions', 3), Lang::item('gemColors', $gemCnd['color'.$i] - 1), Lang::item('gemColors', $gemCnd['cmpColor'.$i] - 1)).'
'; + break; + } + } + } + } + } + + // Random Enchantment - if random enchantment is set, prepend stats from it + if ($this->curTpl['randomEnchant'] && !isset($enhance['r'])) + $x .= ''.Lang::item('randEnchant').'
'; + else if (isset($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 + if ($type >= ITEM_MOD_AGILITY && $type <= ITEM_MOD_STAMINA) + $x .= ''.($qty > 0 ? '+' : '-').abs($qty).' '.Lang::item('statType', $type).'
'; + else // rating with % for reqLevel + $green[] = $this->parseRating($type, $qty, $interactive, $causesScaling); + } + + // magic resistances + foreach (Util::$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, 'text').'
'; + 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.iconString, ae.*, it.gemColorMask AS colorMask + FROM ?_items it + JOIN ?_itemenchantment ae ON ae.id = it.gemEnchantmentId + JOIN ?_icons ic ON ic.id = -it.displayId + 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], 'text') : 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], 'text') : 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', $this->curTpl['socketBonus']); + $x .= ''.Lang::item('socketBonus').Lang::main('colon').Util::localizedString($sbonus, 'text').'
'; + } + + // durability + if ($dur = $this->curTpl['durability']) + $x .= 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::game('reqLevelHlm'), ' 1'.Lang::game('valueDelim').MAX_LEVEL.' ('.($interactive ? sprintf(Util::$changeLevelString, MAX_LEVEL) : ''.MAX_LEVEL).')').'
'; + else if ($_reqLvl > 1) + $x .= sprintf(Lang::game('reqLevel'), $_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 .= 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] = sprintf($link, $m[1]); + $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 = '
'.$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 .= '('.$setSpells[$i]['bonus'].') '.Lang::item('set').': '.$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 (single charges not shown)) + if ($this->curTpl['spellCharges1'] > 1 || $this->curTpl['spellCharges1'] < -1) + $xMisc[] = ''.abs($this->curTpl['spellCharges1']).' '.Lang::item('charges').''; + + // 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; + } + + // 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 (Util::$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) + { + $parsed = Util::parseItemEnchantment(array_keys($enchantments)); + + // and merge enchantments back + foreach ($parsed as $eId => $stats) + { + foreach ($enchantments[$eId] as $item) + { + if ($item > 0) // apply socketBonus + $this->json[$item]['socketbonusstat'] = $stats; + else /* if ($item < 0) */ // apply gemEnchantment + Util::arraySumByKey($this->json[-$item][$mod], $stats); + } + } + } + + foreach ($this->json as $item => $json) + foreach ($json as $k => $v) + if (!$v && !in_array($k, ['classs', 'subclass', 'quality', 'side'])) + unset($this->json[$item][$k]); + + Util::checkNumeric($this->json); + } + + 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); + } + + 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); + + if (!Lang::item('statType', $type)) // unknown rating + return sprintf(Lang::item('statType', count(Lang::item('statType')) - 1), $type, $value); + else if (in_array($type, Util::$lvlIndepRating)) // level independant Bonus + return Lang::item('trigger', 1).str_replace('%d', ''.$value, Lang::item('statType', $type)); + else // rating-Bonuses + { + $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 = ?", $field, $this->ssd[$this->id]['maxLevel']) : 0; + } + + private function initScalingStats() + { + $this->ssd[$this->id] = DB::Aowow()->selectRow("SELECT * FROM ?_scalingstatdistribution WHERE id = ?", $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 (70% from average), max (130% from average) damage + if ($extraDPS = $this->getSSDMod('dps')) // dmg_x2 not used for heirlooms + { + $average = $extraDPS * $this->curTpl['delay'] / 1000; + $this->templates[$this->id]['dmgMin1'] = number_format(0.7 * $average); + $this->templates[$this->id]['dmgMax1'] = number_format(1.3 * $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); + + 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 = []; + $enchIds = []; + + for ($i = 1; $i < 6; $i++) + { + $enchId = $data['enchantId'.$i]; + if ($enchId <= 0) + continue; + + if (isset($this->rndEnchIds[$enchId])) + continue; + + $enchIds[] = $enchId; + } + + foreach (Util::parseItemEnchantment($enchIds, false, $misc) as $eId => $stats) + { + $this->rndEnchIds[$eId] = array( + 'text' => $misc[$eId]['name'], + 'stats' => $stats + ); + } + + for ($i = 1; $i < 6; $i++) + { + $enchId = $data['enchantId'.$i]; + if ($enchId <= 0) + 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[] = str_replace('$i', $qty, $this->rndEnchIds[$enchId]['text']); + Util::arraySumByKey($jsonEquip, $stats); + } + else // RandomProperty: static Enchantment; enchId > 0 + { + $jsonText[] = $this->rndEnchIds[$enchId]['text']; + Util::arraySumByKey($jsonEquip, $this->rndEnchIds[$enchId]['stats']); + } + } + + $this->subItems[$mstItem][$subId] = array( + 'name' => Util::localizedString($data, 'name'), + 'enchantment' => implode(', ', $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]; + } + } + + 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) : Util::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']; + + // clear zero-values afterwards + foreach ($json as $k => $v) + if (!$v && !in_array($k, ['classs', 'subclass', 'quality', 'side'])) + 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 + 9 => [FILTER_CR_FLAG, 'flags', ITEM_FLAG_CONJURED ], // conjureditem + 11 => [FILTER_CR_FLAG, 'flags', ITEM_FLAG_OPENABLE ], // openable + 83 => [FILTER_CR_FLAG, 'flags', ITEM_FLAG_UNIQUEEQUIPPED ], // uniqueequipped + 89 => [FILTER_CR_FLAG, 'flags', ITEM_FLAG_PROSPECTABLE ], // prospectable + 98 => [FILTER_CR_FLAG, 'flags', ITEM_FLAG_PARTYLOOT ], // partyloot + 133 => [FILTER_CR_FLAG, 'flags', ITEM_FLAG_ACCOUNTBOUND ], // accountbound + 146 => [FILTER_CR_FLAG, 'flags', ITEM_FLAG_HEROIC ], // heroic + 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 + 159 => [FILTER_CR_FLAG, 'flags', ITEM_FLAG_MILLABLE ], // millable + 162 => [FILTER_CR_FLAG, 'flags', ITEM_FLAG_DEPRECATED ], // deprecated + 151 => [FILTER_CR_NUMERIC, 'id', null, true], // id + 100 => [FILTER_CR_NUMERIC, 'is.nsockets' ], // nsockets + 111 => [FILTER_CR_NUMERIC, 'requiredSkillRank', null, true], // reqskillrank + 99 => [FILTER_CR_ENUM, 'requiredSkill' ], // requiresprof + 66 => [FILTER_CR_ENUM, 'requiredSpell' ], // requiresprofspec + 17 => [FILTER_CR_ENUM, 'requiredFaction' ], // requiresrepwith + 169 => [FILTER_CR_ENUM, 'holidayId' ], // requiresevent + 21 => [FILTER_CR_NUMERIC, 'is.agi', null, true], // agi + 23 => [FILTER_CR_NUMERIC, 'is.int', null, true], // int + 22 => [FILTER_CR_NUMERIC, 'is.sta', null, true], // sta + 24 => [FILTER_CR_NUMERIC, 'is.spi', null, true], // spi + 20 => [FILTER_CR_NUMERIC, 'is.str', null, true], // str + 115 => [FILTER_CR_NUMERIC, 'is.health', null, true], // health + 116 => [FILTER_CR_NUMERIC, 'is.mana', null, true], // mana + 60 => [FILTER_CR_NUMERIC, 'is.healthrgn', null, true], // healthrgn + 61 => [FILTER_CR_NUMERIC, 'is.manargn', null, true], // manargn + 41 => [FILTER_CR_NUMERIC, 'is.armor' , null, true], // armor + 44 => [FILTER_CR_NUMERIC, 'is.blockrtng', null, true], // blockrtng + 43 => [FILTER_CR_NUMERIC, 'is.block', null, true], // block + 42 => [FILTER_CR_NUMERIC, 'is.defrtng', null, true], // defrtng + 45 => [FILTER_CR_NUMERIC, 'is.dodgertng', null, true], // dodgertng + 46 => [FILTER_CR_NUMERIC, 'is.parryrtng', null, true], // parryrtng + 79 => [FILTER_CR_NUMERIC, 'is.resirtng', null, true], // resirtng + 77 => [FILTER_CR_NUMERIC, 'is.atkpwr', null, true], // atkpwr + 97 => [FILTER_CR_NUMERIC, 'is.feratkpwr', null, true], // feratkpwr + 114 => [FILTER_CR_NUMERIC, 'is.armorpenrtng', null, true], // armorpenrtng + 96 => [FILTER_CR_NUMERIC, 'is.critstrkrtng', null, true], // critstrkrtng + 117 => [FILTER_CR_NUMERIC, 'is.exprtng', null, true], // exprtng + 103 => [FILTER_CR_NUMERIC, 'is.hastertng', null, true], // hastertng + 119 => [FILTER_CR_NUMERIC, 'is.hitrtng', null, true], // hitrtng + 94 => [FILTER_CR_NUMERIC, 'is.splpen', null, true], // splpen + 123 => [FILTER_CR_NUMERIC, 'is.splpwr', null, true], // splpwr + 52 => [FILTER_CR_NUMERIC, 'is.arcsplpwr', null, true], // arcsplpwr + 53 => [FILTER_CR_NUMERIC, 'is.firsplpwr', null, true], // firsplpwr + 54 => [FILTER_CR_NUMERIC, 'is.frosplpwr', null, true], // frosplpwr + 55 => [FILTER_CR_NUMERIC, 'is.holsplpwr', null, true], // holsplpwr + 56 => [FILTER_CR_NUMERIC, 'is.natsplpwr', null, true], // natsplpwr + 57 => [FILTER_CR_NUMERIC, 'is.shasplpwr', null, true], // shasplpwr + 32 => [FILTER_CR_NUMERIC, 'is.dps', true, true], // dps + 33 => [FILTER_CR_NUMERIC, 'is.dmgmin1', null, true], // dmgmin1 + 34 => [FILTER_CR_NUMERIC, 'is.dmgmax1', null, true], // dmgmax1 + 36 => [FILTER_CR_NUMERIC, 'is.speed', true, true], // speed + 134 => [FILTER_CR_NUMERIC, 'is.mledps', true, true], // mledps + 135 => [FILTER_CR_NUMERIC, 'is.mledmgmin', null, true], // mledmgmin + 136 => [FILTER_CR_NUMERIC, 'is.mledmgmax', null, true], // mledmgmax + 137 => [FILTER_CR_NUMERIC, 'is.mlespeed', true, true], // mlespeed + 138 => [FILTER_CR_NUMERIC, 'is.rgddps', true, true], // rgddps + 139 => [FILTER_CR_NUMERIC, 'is.rgddmgmin', null, true], // rgddmgmin + 140 => [FILTER_CR_NUMERIC, 'is.rgddmgmax', null, true], // rgddmgmax + 141 => [FILTER_CR_NUMERIC, 'is.rgdspeed', true, true], // rgdspeed + 25 => [FILTER_CR_NUMERIC, 'is.arcres', null, true], // arcres + 26 => [FILTER_CR_NUMERIC, 'is.firres', null, true], // firres + 28 => [FILTER_CR_NUMERIC, 'is.frores', null, true], // frores + 30 => [FILTER_CR_NUMERIC, 'is.holres', null, true], // holres + 27 => [FILTER_CR_NUMERIC, 'is.natres', null, true], // natres + 29 => [FILTER_CR_NUMERIC, 'is.shares', null, true], // shares + 37 => [FILTER_CR_NUMERIC, 'is.mleatkpwr', null, true], // mleatkpwr + 84 => [FILTER_CR_NUMERIC, 'is.mlecritstrkrtng', null, true], // mlecritstrkrtng + 78 => [FILTER_CR_NUMERIC, 'is.mlehastertng', null, true], // mlehastertng + 95 => [FILTER_CR_NUMERIC, 'is.mlehitrtng', null, true], // mlehitrtng + 38 => [FILTER_CR_NUMERIC, 'is.rgdatkpwr', null, true], // rgdatkpwr + 40 => [FILTER_CR_NUMERIC, 'is.rgdcritstrkrtng', null, true], // rgdcritstrkrtng + 101 => [FILTER_CR_NUMERIC, 'is.rgdhastertng', null, true], // rgdhastertng + 39 => [FILTER_CR_NUMERIC, 'is.rgdhitrtng', null, true], // rgdhitrtng + 49 => [FILTER_CR_NUMERIC, 'is.splcritstrkrtng', null, true], // splcritstrkrtng + 102 => [FILTER_CR_NUMERIC, 'is.splhastertng', null, true], // splhastertng + 48 => [FILTER_CR_NUMERIC, 'is.splhitrtng', null, true], // splhitrtng + 51 => [FILTER_CR_NUMERIC, 'is.spldmg', null, true], // spldmg + 50 => [FILTER_CR_NUMERIC, 'is.splheal', null, true], // splheal + 8 => [FILTER_CR_BOOLEAN, 'requiredDisenchantSkill' ], // disenchantable + 10 => [FILTER_CR_BOOLEAN, 'lockId' ], // locked + 59 => [FILTER_CR_NUMERIC, 'durability', null, true], // dura + 104 => [FILTER_CR_STRING, 'description', true ], // flavortext + 7 => [FILTER_CR_BOOLEAN, 'description_loc0', true ], // hasflavortext + 142 => [FILTER_CR_STRING, 'iconString', ], // icon + 12 => [FILTER_CR_BOOLEAN, 'itemset', ], // partofset + 13 => [FILTER_CR_BOOLEAN, 'randomEnchant', ], // randomlyenchanted + 14 => [FILTER_CR_BOOLEAN, 'pageTextId', ], // readable + 63 => [FILTER_CR_NUMERIC, 'buyPrice', null, true], // buyprice + 64 => [FILTER_CR_NUMERIC, 'sellPrice', null, true], // sellprice + 165 => [FILTER_CR_NUMERIC, 'repairPrice', null, true], // repaircost + 91 => [FILTER_CR_ENUM, 'totemCategory' ], // tool + 176 => [FILTER_CR_STAFFFLAG, 'flags' ], // flags + 177 => [FILTER_CR_STAFFFLAG, 'flagsExtra' ], // flags2 + 71 => [FILTER_CR_FLAG, 'cuFlags', ITEM_CU_OT_ITEMLOOT ], // otitemopening + 74 => [FILTER_CR_FLAG, 'cuFlags', ITEM_CU_OT_OBJECTLOOT ], // otobjectopening + 130 => [FILTER_CR_FLAG, 'cuFlags', CUSTOM_HAS_COMMENT ], // hascomments + 113 => [FILTER_CR_FLAG, 'cuFlags', CUSTOM_HAS_SCREENSHOT ], // hasscreenshots + 167 => [FILTER_CR_FLAG, 'cuFlags', CUSTOM_HAS_VIDEO ], // hasvideos + ); + + public function __construct() + { + $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(); + } + + public function createConditionsForWeights(&$data) + { + if (!$data['wt'] || !$data['wtv'] || count($data['wt']) != count($data['wtv'])) + return null; + + $this->wtCnd = []; + $select = []; + $wtSum = 0; + + foreach ($data['wt'] as $k => $v) + { + $str = isset(Util::$itemFilter[$v]) ? Util::$itemFilter[$v] : null; + $qty = intVal($data['wtv'][$k]); + + if ($str && $qty) + { + 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; + } + else // well look at that.. erronous indizes or zero-weights + { + unset($data['wt'][$k]); + unset($data['wtv'][$k]); + } + } + + 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.id 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]; + } + + switch ($cr[0]) + { + case 2: // bindonpickup [yn] + if ($this->int2Bool($cr[1])) + return ['bonding', 1, $cr[1] ? null : '!']; + break; + case 3: // bindonequip [yn] + if ($this->int2Bool($cr[1])) + return ['bonding', 2, $cr[1] ? null : '!']; + break; + case 4: // bindonuse [yn] + if ($this->int2Bool($cr[1])) + return ['bonding', 3, $cr[1] ? null : '!']; + break; + case 5: // questitem [yn] + if ($this->int2Bool($cr[1])) + return ['bonding', [4, 5], $cr[1] ? null : '!']; + break; + case 168: // teachesspell [yn] 483: learn recipe; 55884: learn mount/pet + if ($this->int2Bool($cr[1])) + return ['spellId1', [483, 55884], $cr[1] ? null : '!']; + break; + case 15: // unique [yn] + if ($this->int2Bool($cr[1])) + return ['maxCount', 1, $cr[1] ? null : '!']; + break; + case 161: // availabletoplayers [yn] + if ($this->int2Bool($cr[1])) + return [['cuFlags', CUSTOM_UNAVAILABLE, '&'], 0, $cr[1] ? null : '!']; + break; + case 80: // has sockets [enum] + 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]]; + } + break; + case 81: // fits gem slot [enum] + 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, '&']]; + } + break; + case 107: // effecttext [str] not yet parsed ['effectsParsed_loc'.User::$localeId, $cr[2]] +/* todo */ return [1]; + case 132: // glyphtype [enum] + switch ($cr[1]) + { + case 1: // Major + case 2: // Minor + return ['AND', ['class', 16], ['subSubClass', $cr[1]]]; + } + break; + case 124: // randomenchants [str] + $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 + case 125: // reqarenartng [op] [int] todo (low): find out, why "IN (W, X, Y) AND IN (X, Y, Z)" doesn't result in "(X, Y)" + if (!$this->isSaneNumeric($cr[2]) || !$this->int2Op($cr[1])) + break; + + $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]; + case 160: // relatedevent [enum] like 169 .. crawl though npc_vendor and loot_templates of event-related spawns +/* todo */ return [1]; + case 152: // classspecific [enum] + $_ = isset($this->enums[$cr[0]][$cr[1]]) ? $this->enums[$cr[0]][$cr[1]] : null; + if ($_ !== null) + { + if (is_bool($_)) + return $_ ? ['AND', [['requiredClass', CLASS_MASK_ALL, '&'], CLASS_MASK_ALL, '!'], ['requiredClass', 0, '>']] : ['OR', [['requiredClass', CLASS_MASK_ALL, '&'], CLASS_MASK_ALL], ['requiredClass', 0]]; + else if (is_int($_)) + return ['AND', [['requiredClass', CLASS_MASK_ALL, '&'], CLASS_MASK_ALL, '!'], ['requiredClass', 1 << ($_ - 1), '&']]; + } + break; + case 153: // racespecific [enum] + $_ = isset($this->enums[$cr[0]][$cr[1]]) ? $this->enums[$cr[0]][$cr[1]] : null; + if ($_ !== null) + { + if (is_bool($_)) + return $_ ? ['AND', [['requiredRace', RACE_MASK_ALL, '&'], RACE_MASK_ALL, '!'], ['requiredRace', 0, '>']] : ['OR', [['requiredRace', RACE_MASK_ALL, '&'], RACE_MASK_ALL], ['requiredRace', 0]]; + else if (is_int($_)) + return ['AND', [['requiredRace', RACE_MASK_ALL, '&'], RACE_MASK_ALL, '!'], ['requiredRace', 1 << ($_ - 1), '&']]; + } + break; + case 35: // damagetype [enum] + if (!$this->isSaneNumeric($cr[1]) || $cr[1] > 6 || $cr[1] < 0) + break; + + return ['OR', ['dmgType1', $cr[1]], ['dmgType2', $cr[1]]]; + case 109: // armorbonus [op] [int] + if (!$this->isSaneNumeric($cr[2], false) || !$this->int2Op($cr[1])) + break; + + $this->formData['extraCols'][] = $cr[0]; + return ['AND', ['armordamagemodifier', $cr[2], $cr[1]], ['class', ITEM_CLASS_ARMOR]]; + case 86: // craftedprof [enum] + $_ = isset($this->enums[99][$cr[1]]) ? $this->enums[99][$cr[1]] : null; + if (is_bool($_)) + return ['src.src1', null, $_ ? '!' : null]; + else if (is_int($_)) + return ['s.skillLine1', $_]; + + break; + case 16: // dropsin [zone] +/* todo */ return [1]; + case 105: // dropsinnormal [heroicdungeon-any] +/* todo */ return [1]; + case 106: // dropsinheroic [heroicdungeon-any] +/* todo */ return [1]; + case 147: // dropsinnormal10 [multimoderaid-any] +/* todo */ return [1]; + case 148: // dropsinnormal25 [multimoderaid-any] +/* todo */ return [1]; + case 149: // dropsinheroic10 [heroicraid-any] +/* todo */ return [1]; + case 150: // dropsinheroic25 [heroicraid-any] +/* todo */ return [1]; + case 68: // otdisenchanting [yn] + case 69: // otfishing [yn] + case 70: // otherbgathering [yn] + // case 71: // otitemopening [yn] [implemented via cuFlags] + case 72: // otlooting [yn] + case 73: // otmining [yn] + // case 74: // otobjectopening [yn] [implemented via cuFlags] + case 75: // otpickpocketing [yn] + case 76: // otskinning [yn] + case 88: // otprospecting [yn] + case 93: // otpvp [pvp] + case 143: // otmilling [yn] + case 171: // otredemption [yn] + case 172: // rewardedbyachievement [yn] + case 92: // soldbyvendor [yn] + if ($this->int2Bool($cr[1])) + return ['src.src'.$this->otFields[$cr[0]], null, $cr[1] ? '!' : null]; + + break; + case 18: // rewardedbyfactionquest [side] + $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]; + } + + break; + case 126: // rewardedbyquestin [zone-any] + 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.. + + break; + case 158: // purchasablewithcurrency [enum] + case 118: // purchasablewithitem [enum] + if (in_array($cr[1], $this->enums[$cr[0]])) + $_ = (array)$cr[1]; + else if ($cr[1] == FILTER_ENUM_ANY) + $_ = $this->enums[$cr[0]]; + else + break; + + $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]; + + break; + case 144: // purchasablewithhonor [yn] + if ($this->int2Bool($cr[1])) + { + $costs = DB::Aowow()->selectCol('SELECT id FROM ?_itemextendedcost WHERE reqHonorPoints > 0'); + if ($items = DB::World()->selectCol($this->extCostQuery, $costs, $costs)) + return ['id', $items, $cr[1] ? null : '!']; + } + break; + case 145: // purchasablewitharena [yn] + if ($this->int2Bool($cr[1])) + { + $costs = DB::Aowow()->selectCol('SELECT id FROM ?_itemextendedcost WHERE reqArenaPoints > 0'); + if ($items = DB::World()->selectCol($this->extCostQuery, $costs, $costs)) + return ['id', $items, $cr[1] ? null : '!']; + } + break; + case 129: // soldbynpc [str-small] + if (!$this->isSaneNumeric($cr[2], true)) + break; + + 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]; + case 90: // avgbuyout [op] [int] + if (!DB::isConnectable(DB_CHARACTERS)) + return [1]; + + if (!$this->isSaneNumeric($cr[2]) || !$this->int2Op($cr[1])) + break; + + /* no no no .. for each realm! + // 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 [0]; + case 65: // avgmoney [op] [int] + if (!$this->isSaneNumeric($cr[2]) || !$this->int2Op($cr[1])) + break; + + $this->formData['extraCols'][] = $cr[0]; + return ['AND', ['flags', ITEM_FLAG_OPENABLE, '&'], ['((minMoneyLoot + maxMoneyLoot) / 2)', $cr[2], $cr[1]]]; + case 62: // cooldown [op] [int] fuck it .. too complex atm + if (!$this->isSaneNumeric($cr[2]) || !$this->int2Op($cr[1])) + break; + + $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]]], + ]; + case 163: // disenchantsinto [disenchanting] + if (!$this->isSaneNumeric($cr[1])) + break; + + if (!in_array($cr[1], $this->enums[$cr[0]])) + break; + + $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]; + case 85: // objectivequest [side] + $w = ''; + switch ($cr[1]) + { + case 1: // Yes + case 5: // No + $w = 1; + break; + 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: + break 2; + } + + $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]; + case 87: // reagentforability [enum] + $_ = isset($this->enums[99][$cr[1]]) ? $this->enums[99][$cr[1]] : null; + if ($_ !== null) + { + $ids = []; + $spells = DB::Aowow()->select( // todo (med): hmm, selecting all using SpellList would exhaust 128MB of memory :x .. see, that we only select the fields that are really needed + '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, '!']; + } + break; + case 6: // startsquest [side] + 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]; + } + break; + case 128: // source [enum] + $_ = isset($this->enums[$cr[0]][$cr[1]]) ? $this->enums[$cr[0]][$cr[1]] : null; + if ($_ !== null) + { + 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 if (!$_) // none + { + $foo = ['AND']; + foreach ($this->enums[$cr[0]] as $bar) + if (is_int($bar)) + $foo[] = ['src.src'.$bar, null]; + + return $foo; + } + } + break; + } + + unset($cr); + $this->error = 1; + 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) + + // they MAY be strings if only one weight is set + $_v['wt'] = (array)$_v['wt']; + $_v['wtv'] = (array)$_v['wtv']; + + $parts[] = $this->createConditionsForWeights($_v); + + foreach ($_v['wt'] as $_) + $this->formData['extraCols'][] = $_; + + $this->formData['setWeights'] = [$_v['wt'], $_v['wtv']]; + } + + // upgrade for [form only] + if (isset($_v['upg'])) + { + // valid item? + if (!is_int($_v['upg']) && !is_array($_v['upg'])) + { + unset($this->formData['form']['upg']); + unset($_v['upg']); + } + else + { + $_ = 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'])) + { + // valid item? + if (is_int($_v['gb']) && $_v['gb'] >= 0 && $_v['gb'] < 4) + $this->formData['form']['gb'] = $_v['gb']; + else + unset($_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'])) + { + if (in_array($_v['ub'], [1, 2, 3, 4, 5, 6, 7, 8, 9, 11])) + { + $parts[] = array( + 'AND', + ['OR', ['requiredClass', 0], ['requiredClass', $this->list2Mask($_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]]] + ] + ); + } + else + unset($_v['ub']); + } + + // quality [list] + if (isset($_v['qu'])) + { + $_ = (array)$_v['qu']; + if (!array_diff($_, array_keys(Util::$rarityColorStings))) + $parts[] = ['quality', $_]; + else + unset($_v['qu']); + } + + // type + if (isset($_v['ty'])) + { + // should be contextual to 'class' + $_ = (array)$_v['ty']; + $parts[] = ['subclass', $_]; + } + + // slot + if (isset($_v['sl'])) + { + // should be contextual + $_ = (array)$_v['sl']; + $parts[] = ['slot', $_]; + } + + // 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; + default: + unset($_v['si']); + } + } + + // itemLevel min + if (isset($_v['minle'])) + { + if (is_int($_v['minle']) && $_v['minle'] > 0) + $parts[] = ['itemLevel', $_v['minle'], '>=']; + else + unset($_v['minle']); + } + + // itemLevel max + if (isset($_v['maxle'])) + { + if (is_int($_v['maxle']) && $_v['maxle'] > 0) + $parts[] = ['itemLevel', $_v['maxle'], '<=']; + else + unset($_v['maxle']); + } + + // reqLevel min + if (isset($_v['minrl'])) + { + if (is_int($_v['minrl']) && $_v['minrl'] > 0) + $parts[] = ['requiredLevel', $_v['minrl'], '>=']; + else + unset($_v['minrl']); + } + + // reqLevel max + if (isset($_v['maxrl'])) + { + if (is_int($_v['maxrl']) && $_v['maxrl'] > 0) + $parts[] = ['requiredLevel', $_v['maxrl'], '<=']; + else + unset($_v['maxrl']); + } + + return $parts; + } +} + +?> diff --git a/includes/types/itemset.class.php b/includes/types/itemset.class.php new file mode 100644 index 00000000..3c9cf520 --- /dev/null +++ b/includes/types/itemset.class.php @@ -0,0 +1,209 @@ + ['o' => 'maxlevel DESC']]; + + 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() { } +} + + +// 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', null, true], // id + 3 => [FILTER_CR_NUMERIC, 'npieces', ], // pieces + 4 => [FILTER_CR_STRING, 'bonusText', true ], // bonustext + 5 => [FILTER_CR_BOOLEAN, 'heroic', ], // heroic + 6 => [FILTER_CR_ENUM, '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 + ); + + 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]; + } + + switch ($cr[0]) + { + case 12: // available to players [yn] ugh .. scan loot, quest and vendor templates and write to ?_itemset +/* todo */ return [1]; + } + + 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', (array)$_v['qu']]; + + // type [enum] + if (isset($_v['ty'])) + $parts[] = ['type', (array)$_v['ty']]; + + // itemLevel min [int] + if (isset($_v['minle'])) + { + if (is_int($_v['minle']) && $_v['minle'] > 0) + $parts[] = ['minLevel', $_v['minle'], '>=']; + else + unset($_v['minle']); + } + + // itemLevel max [int] + if (isset($_v['maxle'])) + { + if (is_int($_v['maxle']) && $_v['maxle'] > 0) + $parts[] = ['maxLevel', $_v['maxle'], '<=']; + else + unset($_v['maxle']); + } + + // reqLevel min [int] + if (isset($_v['minrl'])) + { + if (is_int($_v['minrl']) && $_v['minrl'] > 0) + $parts[] = ['reqLevel', $_v['minrl'], '>=']; + else + unset($_v['minrl']); + } + + // reqLevel max [int] + if (isset($_v['maxrl'])) + { + if (is_int($_v['maxrl']) && $_v['maxrl'] > 0) + $parts[] = ['reqLevel', $_v['maxrl'], '<=']; + else + unset($_v['maxrl']); + } + + // class [enum] + if (isset($_v['cl'])) + { + if (in_array($_v['cl'], [1, 2, 3, 4, 5, 6, 7, 8, 9, 11])) + $parts[] = ['classMask', $this->list2Mask($_v['cl']), '&']; + else + unset($_v['cl']); + } + + // tag [enum] + if (isset($_v['ta'])) + { + if ($_v['ta'] > 0 && $_v['ta'] < 31) + $parts[] = ['contentGroup', intVal($_v['ta'])]; + else + unset($_v['ta']); + } + + return $parts; + } +} + +?> diff --git a/includes/dbtypes/pet.class.php b/includes/types/pet.class.php similarity index 63% rename from includes/dbtypes/pet.class.php rename to includes/types/pet.class.php index 86834fa5..47ab9f92 100644 --- a/includes/dbtypes/pet.class.php +++ b/includes/types/pet.class.php @@ -1,26 +1,19 @@ [['ic']], - 'ic' => ['j' => ['::icons ic ON p.`iconId` = ic.`id`', true], 's' => ', ic.`name` AS "iconString"'], - ); + protected $queryBase = 'SELECT *, id AS ARRAY_KEY FROM ?_pet p'; - public function getListviewData() : array + public function getListviewData() { $data = []; @@ -52,7 +45,7 @@ class PetList extends DBTypeList return $data; } - public function getJSGlobals(int $addMask = GLOBALINFO_ANY) : array + public function getJSGlobals($addMask = GLOBALINFO_ANY) { $data = []; @@ -61,16 +54,16 @@ class PetList extends DBTypeList 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() : ?string { return null; } + public function renderTooltip() { } } ?> diff --git a/includes/types/profile.class.php b/includes/types/profile.class.php new file mode 100644 index 00000000..a6a6da11 --- /dev/null +++ b/includes/types/profile.class.php @@ -0,0 +1,228 @@ + use ProfileHelper; +// class GuildList extends BaseType +// class ArenaTeamList extends BaseType +class ProfileList extends BaseType +{ + public static $type = 0; // profiles dont actually have one + public static $brickFile = 'profile'; + + protected $queryBase = ''; // SELECT p.*, p.id AS ARRAY_KEY FROM ?_profiles p'; + protected $queryOpts = array( + 'p' => [['pa', 'pg']], + 'pam' => [['?_profiles_arenateam_member pam ON pam.memberId = p.id', true], 's' => ', pam.status'], + 'pa' => ['?_profiles_arenateam pa ON pa.id = pam.teamId', 's' => ', pa.mode, pa.name'], + 'pgm' => [['?_profiles_guid_member pgm ON pgm.memberId = p.Id', true], 's' => ', pgm.rankId'], + 'pg' => ['?_profiles_guild pg ON pg.if = pgm.guildId', 's' => ', pg.name'] + ); + + public function __construct($conditions = [], $miscData = null) + { + $character = array( + 'id' => 2, + 'name' => 'CharName', + 'region' => ['eu', 'Europe'], + 'battlegroup' => ['pure-pwnage', 'Pure Pwnage'], + 'realm' => ['dafuque', 'da\'Fuqúe'], + 'level' => 80, + 'classs' => 11, + 'race' => 6, + 'faction' => 1, // 0:alliance; 1:horde + 'gender' => 1, // 0:male, 1:female + 'skincolor' => 0, // playerbytes % 256 + 'hairstyle' => 0, // (playerbytes >> 16) % 256 + 'haircolor' => 0, // (playerbytes >> 24) % 256 + 'facetype' => 0, // (playerbytes >> 8) % 256 [maybe features] + 'features' => 0, // playerBytes2 % 256 [maybe facetype] + 'source' => 2, // source: used if you create a profile from a genuine character. It inherites region, realm and bGroup + 'sourcename' => 'SourceCharName', // > if these three are false we get a 'genuine' profile [0 for genuine characters..?] + 'user' => 1, // > 'genuine' is the parameter for _isArmoryProfile(allowCustoms) ['' for genuine characters..?] + 'username' => 'TestUser', // > also, if 'source' <> 0, the char-icon is requestet via profile.php?avatar + 'published' => 1, // public / private + 'pinned' => 1, // usable for some utility funcs on site + 'nomodel' => 0x0, // unchecks DisplayOnCharacter by (1 << slotId - 1) + 'title' => 0, // titleId currently in use or null + 'guild' => 'GuildName', // only on chars; id or null + 'description' => 'this is a profile', // only on custom profiles + 'arenateams' => [], // [size(2|3|5) => DisplayName]; DisplayName gets urlized to use as link + 'playedtime' => 0, // exact to the day + 'lastupdated' => 0, // timestamp in ms + 'achievementpoints' => 0, // max you have + 'talents' => array( + 'builds' => array( + ['talents' => '', 'glyphs' => ''], // talents:string of 0-5 points; glyphs: itemIds.join(':') + ), + 'active' => 1 // 1|2 + ), + 'customs' => [], // custom profiles created from this char; profileId => [name, ownerId, iconString(optional)] + 'skills' => [], // skillId => [curVal, maxVal]; can contain anything, should be limited to prim/sec professions + 'inventory' => [], // slotId => [itemId, subItemId, permEnchantId, tempEnchantId, gemItemId1, gemItemId2, gemItemId3, gemItemId4] + 'auras' => [], // custom list of buffs, debuffs [spellId] + + // completion lists: [subjectId => amount/timestamp/1] + 'reputation' => [], // factionId => amount + 'titles' => [], // titleId => 1 + 'spells' => [], // spellId => 1; recipes, pets, mounts + 'achievements' => [], // achievementId => timestamp + 'quests' => [], // questId => 1 + + // UNKNOWN + 'bookmarks' => [2], // UNK pinned or claimed userId => profileIds..? + 'statistics' => [], // UNK all statistics? [achievementId => killCount] + 'activity' => [], // UNK recent achievements? [achievementId => killCount] + 'glyphs' => [], // not really used .. i guess..? + 'pets' => array( // UNK + [], // one array per pet, structure UNK + ), + ); + + // parent::__construct($conditions, $miscData); + @include('datasets/ProfilerExampleChar'); // tmp char data + + $this->templates[2] = $character; + $this->curTpl = $character; + + if ($this->error) + return; + + // post processing + // foreach ($this->iterate() as $_id => &$curTpl) + // { + // } + } + + public function getListviewData() + { + $data = []; + foreach ($this->iterate() as $__) + { + $tDistrib = $this->getTalentDistribution(); + + $data[$this->id] = array( + 'id' => 0, + 'name' => $this->curTpl['name'], + 'achievementpoints' => $this->curTpl['achievementpoints'], + 'guild' => $this->curTpl['guild'], // 0 if none + 'guildRank' => -1, + 'realm' => $this->curTpl['realm'][0], + 'realmname' => $this->curTpl['realm'][1], + 'battlegroup' => $this->curTpl['battlegroup'][0], + 'battlegroupname' => $this->curTpl['battlegroup'][0], + 'region' => $this->curTpl['region'][0], + 'level' => $this->curTpl['level'], + 'race' => $this->curTpl['race'], + 'gender' => $this->curTpl['gender'], + 'classs' => $this->curTpl['classs'], + 'faction' => $this->curTpl['faction'], + 'talenttree1' => $tDistrib[0], + 'talenttree2' => $tDistrib[1], + 'talenttree3' => $tDistrib[2], + 'talentspec' => $this->curTpl['talents']['active'] + ); + + if (!empty($this->curTpl['description'])) + $data[$this->id]['description'] = $this->curTpl['description']; + + if (!empty($this->curTpl['icon'])) + $data[$this->id]['icon'] = $this->curTpl['icon']; + + if ($this->curTpl['cuFlags'] & PROFILE_CU_PUBLISHED) + $data[$this->id]['published'] = 1; + + if ($this->curTpl['cuFlags'] & PROFILE_CU_PINNED) + $data[$this->id]['pinned'] = 1; + + if ($this->curTpl['cuFlags'] & PROFILE_CU_DELETED) + $data[$this->id]['deleted'] = 1; + } + + return $data; + } + + public function renderTooltip($interactive = false) + { + if (!$this->curTpl) + return []; + + $x = ''; + $x .= ''; + if ($g = $this->getField('name')) + $x .= ''; + else if ($d = $this->getField('description')) + $x .= ''; + $x .= ''; + $x .= '
'.$this->getField('name').'
<'.$g.'> ('.$this->getField('guildrank').')
'.$d.'
'.Lang::game('level').' '.$this->getField('level').' '.Lang::game('ra', $this->curTpl['race']).' '.Lang::game('cl', $this->curTpl['classs']).'
'; + + return $x; + } + + public function getJSGlobals($addMask = 0) {} + + private function getTalentDistribution() + { + if (!empty($this->tDistribution)) + $this->tDistribution[$this->curTpl['classId']] = DB::Aowow()->selectCol('SELECT COUNT(t.id) FROM dbc_talent t JOIN dbc_talenttab tt ON t.tabId = tt.id WHERE tt.classMask & ?d GROUP BY tt.id ORDER BY tt.tabNumber ASC', 1 << ($this->curTpl['classId'] - 1)); + + $result = []; + $start = 0; + foreach ($this->tDistribution[$this->curTpl['classId']] as $len) + { + $result[] = array_sum(str_split(substr($this->curTpl['talentString'], $start, $len))); + $start += $len; + } + + return $result; + } +} + + +class ProfileListFilter extends Filter +{ + public $extraOpts = []; + + protected $genericFilter = array( + ); + + 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]; + } + + switch ($cr[0]) + { + default: + break; + } + + unset($cr); + $this->error = 1; + 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; + } +} + +?> diff --git a/includes/types/quest.class.php b/includes/types/quest.class.php new file mode 100644 index 00000000..ebbc81e1 --- /dev/null +++ b/includes/types/quest.class.php @@ -0,0 +1,708 @@ + [], + '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..? + ); + + 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 (Util::$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($strict = false) + { + if ($strict) + return $this->curTpl['flags'] & QUEST_FLAG_DAILY; + else + return $this->curTpl['flags'] & (QUEST_FLAG_DAILY | QUEST_FLAG_WEEKLY) || $this->curTpl['specialFlags'] & QUEST_FLAG_SPECIAL_MONTHLY; + } + + // 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 (!(Util::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' => Util::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['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])) + { + $_ = $this->rewards[$this->id][TYPE_CURRENCY]; + $data[TYPE_CURRENCY] = array_combine(array_keys($_), array_keys($_)); + } + } + + 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( + 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', null, true], // id + 5 => [FILTER_CR_FLAG, 'flags', QUEST_FLAG_SHARABLE ], // sharable + 11 => [FILTER_CR_NUMERIC, 'suggestedPlayers', ], // suggestedplayers + 6 => [FILTER_CR_NUMERIC, 'timeLimit', ], // timer + 42 => [FILTER_CR_STAFFFLAG, 'flags', ], // flags + 45 => [FILTER_CR_BOOLEAN, 'rewardTitleId', ], // titlerewarded + 2 => [FILTER_CR_NUMERIC, 'rewardXP', ], // experiencegained + 3 => [FILTER_CR_NUMERIC, 'rewardOrReqMoney', ], // moneyrewarded + 33 => [FILTER_CR_ENUM, 'holidayId', ], // relatedevent + 25 => [FILTER_CR_FLAG, 'cuFlags', CUSTOM_HAS_COMMENT ], // hascomments + 18 => [FILTER_CR_FLAG, 'cuFlags', CUSTOM_HAS_SCREENSHOT ], // hasscreenshots + 36 => [FILTER_CR_FLAG, 'cuFlags', CUSTOM_HAS_VIDEO ], // hasvideos + ); + + 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]; + } + + switch ($cr[0]) + { + case 1: // increasesrepwith + if ($this->isSaneNumeric($cr[1]) && $cr[1] > 0) + { + return [ + 'OR', + ['AND', ['rewardFactionId1', $cr[1]], ['rewardFactionValue1', 0, '>']], + ['AND', ['rewardFactionId2', $cr[1]], ['rewardFactionValue2', 0, '>']], + ['AND', ['rewardFactionId3', $cr[1]], ['rewardFactionValue3', 0, '>']], + ['AND', ['rewardFactionId4', $cr[1]], ['rewardFactionValue4', 0, '>']], + ['AND', ['rewardFactionId5', $cr[1]], ['rewardFactionValue5', 0, '>']] + ]; + } + break; + case 10: // decreasesrepwith + if ($this->isSaneNumeric($cr[1]) && $cr[1] > 0) + { + return [ + 'OR', + ['AND', ['rewardFactionId1', $cr[1]], ['rewardFactionValue1', 0, '<']], + ['AND', ['rewardFactionId2', $cr[1]], ['rewardFactionValue2', 0, '<']], + ['AND', ['rewardFactionId3', $cr[1]], ['rewardFactionValue3', 0, '<']], + ['AND', ['rewardFactionId4', $cr[1]], ['rewardFactionValue4', 0, '<']], + ['AND', ['rewardFactionId5', $cr[1]], ['rewardFactionValue5', 0, '<']] + ]; + } + break; + case 43: // currencyrewarded + if ($this->isSaneNumeric($cr[1]) && $cr[1] > 0) + { + 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]] + ]; + } + break; + case 34: // availabletoplayers + if ($this->int2Bool($cr[1])) + { + if ($cr[1]) + return ['AND', [['cuFlags', CUSTOM_EXCLUDE_FOR_LISTVIEW, '&'], 0], [['flags', QUEST_FLAG_UNAVAILABLE, '&'], 0]]; + else + return ['OR', ['cuFlags', CUSTOM_EXCLUDE_FOR_LISTVIEW, '&'], ['flags', QUEST_FLAG_UNAVAILABLE, '&']]; + } + break; + case 23: // itemchoices [op] [int] + if (!$this->isSaneNumeric($cr[2], false) || !$this->int2Op($cr[1])) + break; + + $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]; + case 22: // itemrewards [op] [int] + if (!$this->isSaneNumeric($cr[2], false) || !$this->int2Op($cr[1])) + break; + + $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]; + case 44: // countsforloremaster_stc [bool] + if ($this->int2Bool($cr[1])) + { + 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 , '&']];; + } + + break; + case 4: // spellrewarded [bool] + if ($this->int2Bool($cr[1])) + { + 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]]; + } + break; + case 9: // objectiveearnrepwith [enum] + $_ = intVal($cr[1]); + if ($_ > 0) + return ['OR', ['reqFactionId1', $_], ['reqFactionId2', $_]]; + 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]]; + + break; + case 37: // classspecific [enum] + $_ = isset($this->enums[$cr[0]][$cr[1]]) ? $this->enums[$cr[0]][$cr[1]] : null; + if ($_ !== null) + { + 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, '!']]; + } + break; + case 38: // racespecific [enum] + $_ = isset($this->enums[$cr[0]][$cr[1]]) ? $this->enums[$cr[0]][$cr[1]] : null; + if ($_ !== null) + { + 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, '!']]; + } + break; + case 19: // startsfrom [enum] + switch ($cr[1]) + { + case 1: // npc + return ['AND', ['qse.type', TYPE_NPC], ['qse.method', 0x1, '&']]; + case 2: // object + return ['AND', ['qse.type', TYPE_OBJECT], ['qse.method', 0x1, '&']]; + case 3: // item + return ['AND', ['qse.type', TYPE_ITEM], ['qse.method', 0x1, '&']]; + } + break; + case 21: // endsat [enum] + switch ($cr[1]) + { + case 1: // npc + return ['AND', ['qse.type', TYPE_NPC], ['qse.method', 0x2, '&']]; + case 2: // object + return ['AND', ['qse.type', TYPE_OBJECT], ['qse.method', 0x2, '&']]; + } + break; + case 24: // lacksstartend [bool] + $missing = DB::Aowow()->selectCol('SELECT questId, max(method) a, min(method) b FROM ?_quests_startend GROUP BY questId HAVING (a | b) <> 3'); + if ($this->int2Bool($cr[1])) + { + if ($cr[1]) + return ['id', $missing]; + else + return ['id', $missing, '!']; + } + break; + case 7: // firstquestseries + case 15: // lastquestseries + case 16: // partseries +/* todo */ return [1]; // self-joining eats substential amounts of time: should restructure that and also incorporate reqQ and openQ cases from infobox + default: + break; + } + + unset($cr); + $this->error = 1; + 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'])) + { + if (is_int($_v['minle']) && $_v['minle'] > 0) + $parts[] = ['level', $_v['minle'], '>=']; // not considering quests that are always at player level (-1) + else + unset($_v['minle']); + } + + // level max + if (isset($_v['maxle'])) + { + if (is_int($_v['maxle']) && $_v['maxle'] > 0) + $parts[] = ['level', $_v['maxle'], '<=']; + else + unset($_v['maxle']); + } + + // reqLevel min + if (isset($_v['minrl'])) + { + if (is_int($_v['minrl']) && $_v['minrl'] > 0) + $parts[] = ['minLevel', $_v['minrl'], '>='];// ignoring maxLevel + else + unset($_v['minrl']); + } + + // reqLevel max + if (isset($_v['maxrl'])) + { + if (is_int($_v['maxrl']) && $_v['maxrl'] > 0) + $parts[] = ['minLevel', $_v['maxrl'], '<='];// ignoring maxLevel + else + unset($_v['maxrl']); + } + + // 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; + default: + unset($_v['si']); + } + } + + // type [list] + if (isset($_v['ty'])) + { + $_ = (array)$_v['ty']; + if (!array_diff($_, [0, 1, 21, 41, 62, 81, 82, 83, 84, 85, 88, 89])) + $parts[] = ['type', $_]; + else + unset($_v['ty']); + } + + return $parts; + } +} + + +?> diff --git a/includes/types/skill.class.php b/includes/types/skill.class.php new file mode 100644 index 00000000..2b2d0865 --- /dev/null +++ b/includes/types/skill.class.php @@ -0,0 +1,77 @@ + [['si']], + 'si' => ['j' => '?_icons si ON si.id = sl.iconId', 's' => ', si.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; + } + } + } + + 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']), + '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/spell.class.php b/includes/types/spell.class.php new file mode 100644 index 00000000..4ede269f --- /dev/null +++ b/includes/types/spell.class.php @@ -0,0 +1,2176 @@ + [ 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', 'si', 'si', 'sia']], // 6: TYPE_SPELL + 'si' => ['j' => ['?_icons si ON si.id = s.iconId', true], 's' => ', IFNULL (si.iconString, "inv_misc_questionmark") AS iconString'], + 'sia' => ['j' => ['?_icons sia ON sia.id = s.iconIdAlt', true], 's' => ', sia.iconString 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 = []; + 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 (Util::$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 ($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 && ($_ = Util::parseItemEnchantment($mv, true))) + Util::arraySumByKey($stats, $_[$mv]); + + continue; + } + + switch ($au) + { + case 29: // ModStat MiscVal:type + { + if ($mv < 0) // all stats + { + for ($j = 0; $j < 5; $j++) + Util::arraySumByKey($stats, [(ITEM_MOD_AGILITY + $j) => $pts]); + } + else // one stat + Util::arraySumByKey($stats, [(ITEM_MOD_AGILITY + $mv) => $pts]); + + 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 == 0x7E) + { + 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]); + if ($mv == POWER_ENERGY) + Util::arraySumByKey($stats, [ITEM_MOD_ENERGY => $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 = Util::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; + } + } + + $data[$this->id] = $stats; + } + + return $data; + } + + public function getProfilerMods() + { + $data = $this->getStatGain(); // flat gains + + foreach ($this->iterate() as $id => $__) + { + for ($i = 1; $i < 4; $i++) + { + $pts = $this->calculateAmountForCurrent($i)[1]; + $mv = $this->curTpl['effect'.$i.'MiscValue']; + $au = $this->curTpl['effect'.$i.'AuraId']; + + /* 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 ($this->curTpl['effect'.$i.'AuraId']) + { + case 101: + $data[$id][] = ['armor' => [$pts / 100, 'percentOf', 'armor']]; + break; + case 13: // damage done flat + // per magic school, omit physical + break; + case 30: // mod skill + // diff between character skills and trade skills + break; + case 36: // shapeshift + } + } + } + + 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' => Util::localizedString($st, 'name') + ); + } + } + } + } + + $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['rangeMaxHostile']. ''); + // 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'); + $scaling = $this->curTpl['attributes1'] & 0x200; // never a referenced spell, ALWAYS $this; SPELL_ATTR1_MELEE_COMBAT_SPELL: 0x200 + + if ($scaling) + { + if ($level > $maxLvl && $maxLvl > 0) + $level = $maxLvl; + else if ($level < $baseLvl) + $level = $baseLvl; + + $level -= $ref->getField('spellLevel'); + $base += (int)($level * $rppl); + } + + return [ + $add ? $base + 1 : $base, + $base + $add, + $scaling ? '' : null, + $scaling ? '' : 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.'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'); + + // 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, '')) + return $formula; + + // hm, minor eval-issue. eval doesnt understand two operators without a space between them (eg. spelll: 18126) + $formula = preg_replace('/(\+|-|\*|\/)(\+|-|\*|\/)/i', '\1 \2', $formula); + + // there should not be any letters without a leading $ + return eval('return '.$formula.';'); + } + + // description-, buff-parsing component + private function resolveVariableString($variable, &$usesScalingRating) + { + $signs = ['+', '-', '/', '*', '%', '^']; + + $op = $variable[2]; + $oparg = $variable[3]; + $lookup = (int)$variable[4]; + $var = $variable[6] ? $variable[6] : $variable[8]; + $effIdx = $variable[6] ? null : $variable[9]; + $switch = $variable[7] ? explode(':', $variable[7]) : null; + + if (!$var) + return; + + if (!$effIdx) // if EffectIdx is omitted, assume EffectIdx: 1 + $effIdx = 1; + + // cache at least some lookups.. should be moved to single spellList :/ + if ($lookup && !isset($this->refSpells[$lookup])) + $this->refSpells[$lookup] = new SpellList(array(['s.id', $lookup])); + + switch ($var) + { + case 'a': // EffectRadiusMin + case 'A': // EffectRadiusMax + if ($lookup) + $base = $this->refSpells[$lookup]->getField('effect'.$effIdx.'RadiusMax'); + else + $base = $this->getField('effect'.$effIdx.'RadiusMax'); + + if (in_array($op, $signs) && is_numeric($oparg) && is_numeric($base)) + eval("\$base = $base $op $oparg;"); + + return $base; + case 'b': // PointsPerComboPoint + case 'B': + if ($lookup) + $base = $this->refSpells[$lookup]->getField('effect'.$effIdx.'PointsPerComboPoint'); + else + $base = $this->getField('effect'.$effIdx.'PointsPerComboPoint'); + + if (in_array($op, $signs) && is_numeric($oparg) && is_numeric($base)) + eval("\$base = $base $op $oparg;"); + + return $base; + case 'd': // SpellDuration + case 'D': // todo (med): min/max?; /w unit? + if ($lookup) + $base = $this->refSpells[$lookup]->getField('duration'); + else + $base = $this->getField('duration'); + + if ($base <= 0) + return Lang::spell('untilCanceled'); + + if ($op && is_numeric($oparg) && is_numeric($base)) + eval("\$base = $base $op $oparg;"); + + return explode(' ', Util::formatTime(abs($base), true)); + case 'e': // EffectValueMultiplier + case 'E': + if ($lookup) + $base = $this->refSpells[$lookup]->getField('effect'.$effIdx.'ValueMultiplier'); + else + $base = $this->getField('effect'.$effIdx.'ValueMultiplier'); + + if (in_array($op, $signs) && is_numeric($oparg) && is_numeric($base)) + eval("\$base = $base $op $oparg;"); + + return $base; + case 'f': // EffectDamageMultiplier + case 'F': + if ($lookup) + $base = $this->refSpells[$lookup]->getField('effect'.$effIdx.'DamageMultiplier'); + else + $base = $this->getField('effect'.$effIdx.'DamageMultiplier'); + + if (in_array($op, $signs) && is_numeric($oparg) && is_numeric($base)) + eval("\$base = $base $op $oparg;"); + + return $base; + case 'g': // boolean choice with casters gender as condition $gX:Y; + case 'G': + return '<'.$switch[0].'/'.$switch[1].'>'; + case 'h': // ProcChance + case 'H': + if ($lookup) + $base = $this->refSpells[$lookup]->getField('procChance'); + else + $base = $this->getField('procChance'); + + if (in_array($op, $signs) && is_numeric($oparg) && is_numeric($base)) + eval("\$base = $base $op $oparg;"); + + return $base; + case 'i': // MaxAffectedTargets + case 'I': + if ($lookup) + $base = $this->refSpells[$lookup]->getField('maxAffectedTargets'); + else + $base = $this->getField('maxAffectedTargets'); + + if (in_array($op, $signs) && is_numeric($oparg) && is_numeric($base)) + eval("\$base = $base $op $oparg;"); + + return $base; + case 'l': // boolean choice with last value as condition $lX:Y; + case 'L': + return '$l'.$switch[0].':'.$switch[1]; // resolve later by backtracking + case 'm': // BasePoints (minValue) + case 'M': // BasePoints (maxValue) + if ($lookup) + { + $base = $this->refSpells[$lookup]->getField('effect'.$effIdx.'BasePoints'); + $add = $this->refSpells[$lookup]->getField('effect'.$effIdx.'DieSides'); + $mv = $this->refSpells[$lookup]->getField('effect'.$effIdx.'MiscValue'); + $aura = $this->refSpells[$lookup]->getField('effect'.$effIdx.'AuraId'); + + } + else + { + $base = $this->getField('effect'.$effIdx.'BasePoints'); + $add = $this->getField('effect'.$effIdx.'DieSides'); + $mv = $this->getField('effect'.$effIdx.'MiscValue'); + $aura = $this->getField('effect'.$effIdx.'AuraId'); + } + + if (ctype_lower($var)) + $add = 1; + + $base += $add; + + if (in_array($op, $signs) && is_numeric($oparg) && is_numeric($base)) + eval("\$base = $base $op $oparg;"); + + // Aura giving combat ratings + $rType = 0; + if ($aura == 189) + if ($rType = Util::itemModByRatingMask($mv)) + $usesScalingRating = true; + // Aura end + + if ($rType && $this->interactive && $aura == 189) + return ''.abs($base).' ('.sprintf(Util::$setRatingLevelString, $this->charLevel, $rType, abs($base), Util::setRatingLevel($this->charLevel, $rType, abs($base))).')'; + else if ($rType && $aura == 189) + return ''.abs($base).' ('.Util::setRatingLevel($this->charLevel, $rType, abs($base)).')'; + else + return $base; + case 'n': // ProcCharges + case 'N': + if ($lookup) + $base = $this->refSpells[$lookup]->getField('procCharges'); + else + $base = $this->getField('procCharges'); + + if (in_array($op, $signs) && is_numeric($oparg) && is_numeric($base)) + eval("\$base = $base $op $oparg;"); + + return $base; + case 'o': // TotalAmount for periodic auras (with variance) + case 'O': + if ($lookup) + { + list($min, $max, $modStrMin, $modStrMax) = $this->calculateAmountForCurrent($effIdx, $this->refSpells[$lookup]); + $periode = $this->refSpells[$lookup]->getField('effect'.$effIdx.'Periode'); + $duration = $this->refSpells[$lookup]->getField('duration'); + } + else + { + list($min, $max, $modStrMin, $modStrMax) = $this->calculateAmountForCurrent($effIdx); + $periode = $this->getField('effect'.$effIdx.'Periode'); + $duration = $this->getField('duration'); + } + + if (!$periode) + $periode = 3000; + + $min *= $duration / $periode; + $max *= $duration / $periode; + $equal = $min == $max; + + if (in_array($op, $signs) && is_numeric($oparg)) + if ($equal) + eval("\$min = $min $op $oparg;"); + + if ($this->interactive) + return $modStrMin.$min . (!$equal ? Lang::game('valueDelim') . $modStrMax.$max : null); + else + return $min . (!$equal ? Lang::game('valueDelim') . $max : null); + case 'q': // EffectMiscValue + case 'Q': + if ($lookup) + $base = $this->refSpells[$lookup]->getField('effect'.$effIdx.'MiscValue'); + else + $base = $this->getField('effect'.$effIdx.'MiscValue'); + + if (in_array($op, $signs) && is_numeric($oparg) && is_numeric($base)) + eval("\$base = $base $op $oparg;"); + + return $base; + case 'r': // SpellRange + case 'R': + if ($lookup) + $base = $this->refSpells[$lookup]->getField('rangeMaxHostile'); + else + $base = $this->getField('rangeMaxHostile'); + + if (in_array($op, $signs) && is_numeric($oparg) && is_numeric($base)) + eval("\$base = $base $op $oparg;"); + + return $base; + case 's': // BasePoints (with variance) + case 'S': + if ($lookup) + { + list($min, $max, $modStrMin, $modStrMax) = $this->calculateAmountForCurrent($effIdx, $this->refSpells[$lookup]); + $mv = $this->refSpells[$lookup]->getField('effect'.$effIdx.'MiscValue'); + $aura = $this->refSpells[$lookup]->getField('effect'.$effIdx.'AuraId'); + } + else + { + list($min, $max, $modStrMin, $modStrMax) = $this->calculateAmountForCurrent($effIdx); + $mv = $this->getField('effect'.$effIdx.'MiscValue'); + $aura = $this->getField('effect'.$effIdx.'AuraId'); + } + $equal = $min == $max; + + if (in_array($op, $signs) && is_numeric($oparg)) + { + eval("\$min = $min $op $oparg;"); + if (!$equal) + eval("\$max = $max $op $oparg;"); + } + + // Aura giving combat ratings + $rType = 0; + if ($aura == 189) + if ($rType = Util::itemModByRatingMask($mv)) + $usesScalingRating = true; + // Aura end + + if ($rType && $equal && $this->interactive && $aura == 189) + return ''.$min.' ('.sprintf(Util::$setRatingLevelString, $this->charLevel, $rType, $min, Util::setRatingLevel($this->charLevel, $rType, $min)).')'; + else if ($rType && $equal && $aura == 189) + return ''.$min.' ('.Util::setRatingLevel($this->charLevel, $rType, $min).')'; + else if ($this->interactive && $aura == 189) + return $modStrMin.$min . (!$equal ? Lang::game('valueDelim') . $modStrMax.$max : null); + else + return $min . (!$equal ? Lang::game('valueDelim') . $max : null); + case 't': // Periode + case 'T': + if ($lookup) + $base = $this->refSpells[$lookup]->getField('effect'.$effIdx.'Periode') / 1000; + else + $base = $this->getField('effect'.$effIdx.'Periode') / 1000; + + if (in_array($op, $signs) && is_numeric($oparg) && is_numeric($base)) + eval("\$base = $base $op $oparg;"); + + return $base; + case 'u': // StackCount + case 'U': + if ($lookup) + $base = $this->refSpells[$lookup]->getField('stackAmount'); + else + $base = $this->getField('stackAmount'); + + if (in_array($op, $signs) && is_numeric($oparg) && is_numeric($base)) + eval("\$base = $base $op $oparg;"); + + return $base; + case 'v': // MaxTargetLevel + case 'V': + if ($lookup) + $base = $this->refSpells[$lookup]->getField('MaxTargetLevel'); + else + $base = $this->getField('MaxTargetLevel'); + + if (in_array($op, $signs) && is_numeric($oparg) && is_numeric($base)) + eval("\$base = $base $op $oparg;"); + + return $base; + case 'x': // ChainTargetCount + case 'X': + if ($lookup) + $base = $this->refSpells[$lookup]->getField('effect'.$effIdx.'ChainTarget'); + else + $base = $this->getField('effect'.$effIdx.'ChainTarget'); + + if (in_array($op, $signs) && is_numeric($oparg) && is_numeric($base)) + eval("\$base = $base $op $oparg;"); + + return $base; + case 'z': // HomeZone + return Lang::spell('home'); + } + } + + // description-, buff-parsing component + private function resolveFormulaString($formula, $precision = 0, &$scaling) + { + // 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 = $this->resolveFormulaString($formOutStr, $formPrecision, $scaling); + + $formula = substr_replace($formula, $formOutStr, $formStartPos, ($formCurPos - $formStartPos)); + } + + // step 2: resolve variables + $pos = 0; // continue strpos-search from this offset + $str = ''; + $suffix = ''; + while (($npos = strpos($formula, '$', $pos)) !== false) + { + if ($npos != $pos) + $str .= substr($formula, $pos, $npos - $pos); + + $pos = $npos++; + + if ($formula[$pos] == '$') + $pos++; + + if (!preg_match('/^(([\+\-\*\/])(\d+);)?(\d*)(([g])([\w\s]*:[\w\s]*);|([a-z])([123]?)\b)/i', substr($formula, $pos), $result)) + { + $str .= '#'; // mark as done, reset below + continue; + } + $pos += strlen($result[0]); + + $var = $this->resolveVariableString($result, $scaling); + if (is_array($var)) + { + $str .= $var[0]; + $suffix = ' '.$var[1]; + } + else + $str .= $var; + } + $str .= substr($formula, $pos); + $str = str_replace('#', '$', $str); // reset marks + + // step 3: try to evaluate result + $evaled = $this->resolveEvaluation($str); + + $return = is_numeric($evaled) ? number_format($evaled, $precision, '.', '') : $evaled; + return $return.$suffix; + } + + // 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 + $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 + + 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 array("", []); + + // 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 IF, AND ONLY IF it is not containing further variables ($) AND it has a simple spell-condition ( \(?!?[as]\d+\)? ) + + _[100].tooltip_enus = '
Charge
8 - 25 yd range
Instant20 sec cooldown
Requires Warrior
Requires level 3
Charge to an enemy, stunning it for 1 sec. Generates 20 Rage.
'; + _[100].buff_enus = ''; + _[100].spells_enus = {"58377": [["", "and 2 additional nearby targets "]], "103828": [["1 sec", "3 sec and reducing movement speed by 50% for 15 sec"]]}; + _[100].buffspells_enus = {}; + + Turns the Shaman into a Ghost Wolf, increasing speed by $s2%$?s59289[ and regenerating $59289s1% of your maximum health every 5 sec][]. + Lasts 5 min. $?$gte($pl,68)[][Cannot be used on items level 138 and higher.] + */ + + // \1: full pattern match; \2: any sequence, that may include an aura/spell-ref; \3: any other sequence, between "?$" and "[" + while (preg_match('/\$\?(([\W\D]*[as]\d+)|([^\[]*))/i', $data, $matches)) + { + $condBrktCnt = 0; + $targetPart = 3; // we usually want the second pair of brackets + $curPart = 0; // parts: $? 0 [ 1 ] 2 [ 3 ] 4 + $relSpells = []; // see spells_enus + + $condOutStr = ''; + + if (!empty($matches[3])) // we can do this! -> eval + { + $cnd = $this->resolveEvaluation($matches[3]); + if ((is_numeric($cnd) || is_bool($cnd)) && $cnd) // only case, deviating from normal; positive result -> use [true] + $targetPart = 1; + + $condStartPos = strpos($data, $matches[3]) - 2; + $condCurPos = $condStartPos; + + } + + else if (!empty($matches[2])) + { + $condStartPos = strpos($data, $matches[2]) - 2; + $condCurPos = $condStartPos; + } + else // empty too? WTF?! GTFO! + die('what a terrible failure'); + + while ($condCurPos <= strlen($data)) // only hard-exit condition, we'll hit those breaks eventually^^ + { + // we're through with this condition. replace with found result and continue + if ($curPart == 4 || $condCurPos == strlen($data)) + { + $data = substr_replace($data, $condOutStr, $condStartPos, ($condCurPos - $condStartPos)); + break; + } + + $char = $data[$condCurPos]; + + // advance position + $condCurPos++; + + if ($char == '[') + { + if (!$condBrktCnt) + $curPart++; + + $condBrktCnt++; + + if ($condBrktCnt == 1) + continue; + } + else if ($char == ']') + { + if ($condBrktCnt == 1) + $curPart++; + + $condBrktCnt--; + + if (!$condBrktCnt) + continue; + } + + // we got an elseif .. since they are self-containing we can just remove everything we've got up to here and restart the iteration + if ($curPart == 2 && $char == '?') + { + $replace = $targetPart == 1 ? $condOutStr.' $' : '$'; + $data = substr_replace($data, $replace, $condStartPos, ($condCurPos - $condStartPos) - 1); + break; + } + + if ($curPart == $targetPart) + $condOutStr .= $char; + + } + } + + // step 3: unpack formulas ${ .. }.X + // 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; + } + $formOutStr = $this->resolveFormulaString($formOutStr, $formPrecision, $scaling); + + $data = substr_replace($data, $formOutStr, $formStartPos, ($formCurPos - $formStartPos)); + } + + // step 4: find and eliminate regular variables + $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++; + + // ( (op) (oparg); )? (refSpell) ( ([g]ifText:elseText; | (var) (effIdx) ) + if (!preg_match('/^(([\+\-\*\/])(\d+);)?(\d*)(([g])([\w\s]*:[\w\s]*);|([a-z])([123]?)\b)/i', substr($data, $pos), $result)) + { + $str .= '#'; // mark as done, reset below + continue; + } + + $pos += strlen($result[0]); + + $var = $this->resolveVariableString($result, $scaling); + $resolved = is_array($var) ? $var[0] : $var; + $str .= is_numeric($resolved) ? abs($resolved) : $resolved; + if (is_array($var)) + $str .= ' '.$var[1]; + } + $str .= substr($data, $pos); + $str = str_replace('#', '$', $str); // reset marker + + // step 5: variable-dependant variable-text + // special case $lONE:ELSE; + // todo (low): russian uses THREE (wtf?! oO) cases ($l[singular]:[plural1]:[plural2]) .. explode() chooses always the first plural option :/ + while (preg_match('/([\d\.]+)([^\d]*)(\$l:*)([^:]*):([^;]*);/i', $str, $m)) + $str = str_ireplace($m[1].$m[2].$m[3].$m[4].':'.$m[5].';', $m[1].$m[2].($m[1] == 1 ? $m[4] : explode(':', $m[5])[0]), $str); + + // step 6: HTMLize + // colors + $str = preg_replace('/\|cff([a-f0-9]{6})(.+?)\|r/i', '$2', $str); + + // line endings + $str = strtr($str, ["\r" => '', "\n" => '
']); + + return array($str, []/*$relSpells*/); + } + + public function renderBuff($level = MAX_LEVEL, $interactive = false) + { + if (!$this->curTpl) + return array(); + + // doesn't have a buff + if (!$this->getField('buff', true)) + return array(); + + $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 array($x, $btt[1]); + } + + public function renderTooltip($level = MAX_LEVEL, $interactive = false) + { + if (!$this->curTpl) + return array(); + + $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 array($x, $desc ? $desc[1] : null); + } + + 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; + } + + 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) + { +/* +spells / buffspells = { + "58377": [["", "and 2 additional nearby targets "]], + "103828": [["stunning", "rooting"], ["1 sec", "4 sec and reducing movement speed by 50% for 15 sec"]] +}; +*/ + $buff = $this->renderBuff(MAX_LEVEL, true); + $tTip = $this->renderTooltip(MAX_LEVEL, true); + + $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 + 2 => [FILTER_CR_NUMERIC, 'powerCostPercent', ], // prcntbasemanarequired + 3 => [FILTER_CR_BOOLEAN, 'spellFocusObject' ], // requiresnearbyobject + 4 => [FILTER_CR_NUMERIC, 'trainingcost' ], // trainingcost + 5 => [FILTER_CR_BOOLEAN, 'reqSpellId' ], // requiresprofspec + 10 => [FILTER_CR_FLAG, 'cuFlags', SPELL_CU_FIRST_RANK ], // firstrank + 12 => [FILTER_CR_FLAG, 'cuFlags', SPELL_CU_LAST_RANK ], // lastrank + 13 => [FILTER_CR_NUMERIC, 'rankNo', ], // rankno + 14 => [FILTER_CR_NUMERIC, 'id', null, true], // id + 15 => [FILTER_CR_STRING, 'si.iconString', ], // icon + 19 => [FILTER_CR_FLAG, 'attributes0', 0x80000 ], // scaling + 25 => [FILTER_CR_BOOLEAN, 'skillLevelYellow' ], // rewardsskillups + 11 => [FILTER_CR_FLAG, 'cuFlags', CUSTOM_HAS_COMMENT ], // hascomments + 8 => [FILTER_CR_FLAG, 'cuFlags', CUSTOM_HAS_SCREENSHOT ], // hasscreenshots + 17 => [FILTER_CR_FLAG, 'cuFlags', CUSTOM_HAS_VIDEO ], // hasvideos + ); + + 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]; + } + + switch ($cr[0]) + { + case 1: // costAbs [op] [int] + if (!$this->isSaneNumeric($cr[2])) + break; + + if (!$this->int2Op($cr[1])) + break; + + return ['OR', ['AND', ['powerType', [1, 6]], ['powerCost', (10 * $cr[2]), $cr[1]]], ['AND', ['powerType', [1, 6], '!'], ['powerCost', $cr[2], $cr[1]]]]; + case 9: // source [enum] + $_ = isset($this->enums[$cr[0]][$cr[1]]) ? $this->enums[$cr[0]][$cr[1]] : null; + if ($_ !== null) + { + 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]]; + } + break; + case 20: // has Reagents [yn] + if ($this->int2Bool($cr[1])) + { + 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]]; + } + } + + 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'])) + { + if (is_int($_v['minle']) && $_v['minle'] > 0) + $parts[] = ['spellLevel', $_v['minle'], '>=']; + else + unset($_v['minle']); + } + + // spellLevel max + if (isset($_v['maxle'])) + { + if (is_int($_v['maxle']) && $_v['maxle'] > 0) + $parts[] = ['spellLevel', $_v['maxle'], '<=']; + else + unset($_v['maxle']); + } + + // skillLevel min + if (isset($_v['minrs'])) + { + if (is_int($_v['minrs']) && $_v['minrs'] > 0) + $parts[] = ['learnedAt', $_v['minrs'], '>=']; + else + unset($_v['minrs']); + } + + // skillLevel max + if (isset($_v['maxrs'])) + { + if (is_int($_v['maxrs']) && $_v['maxrs'] > 0) + $parts[] = ['learnedAt', $_v['maxrs'], '<=']; + else + unset($_v['maxrs']); + } + + // race + if (isset($_v['ra'])) + { + if (in_array($_v['ra'], [1, 2, 3, 4, 5, 6, 7, 8, 10, 11])) + $parts[] = ['AND', [['reqRaceMask', RACE_MASK_ALL, '&'], RACE_MASK_ALL, '!'], ['reqRaceMask', $this->list2Mask($_v['ra']), '&']]; + else + unset($_v['ra']); + } + + // class [list] + if (isset($_v['cl'])) + { + $_ = (array)$_v['cl']; + if (!array_diff($_, [1, 2, 3, 4, 5, 6, 7, 8, 9, 11])) + $parts[] = ['reqClassMask', $this->list2Mask($_), '&']; + else + unset($_v['cl']); + } + + // school [list] + if (isset($_v['sc'])) + { + $_ = (array)$_v['sc']; + if (!array_diff($_, [0, 1, 2, 3, 4, 5, 6])) + $parts[] = ['schoolMask', $this->list2Mask($_, true), '&']; + else + unset($_v['sc']); + } + + // glyph type [list] wonky, admittedly, but consult SPELL_CU_* in defines and it makes sense + if (isset($_v['gl'])) + { + if (in_array($_v['gl'], [1, 2])) + $parts[] = ['cuFlags', ($this->list2Mask($_v['gl']) << 6), '&']; + else + unset($_v['gl']); + } + + // dispel type + if (isset($_v['dt'])) + { + if (in_array($_v['dt'], [1, 2, 3, 4, 5, 6, 9])) + $parts[] = ['dispelType', $_v['dt']]; + else + unset($_v['dt']); + } + + // mechanic + if (isset($_v['me'])) + { + if ($_v['me'] > 0 && $_v['me'] < 32) + $parts[] = ['OR', ['mechanic', $_v['me']], ['effect1Mechanic', $_v['me']], ['effect2Mechanic', $_v['me']], ['effect3Mechanic', $_v['me']]]; + else + unset($_v['me']); + } + + return $parts; + } +} + +?> diff --git a/includes/types/title.class.php b/includes/types/title.class.php new file mode 100644 index 00000000..ef979af3 --- /dev/null +++ b/includes/types/title.class.php @@ -0,0 +1,173 @@ + [['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 new file mode 100644 index 00000000..e32442a7 --- /dev/null +++ b/includes/types/user.class.php @@ -0,0 +1,60 @@ + [['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 new file mode 100644 index 00000000..d5ec19ff --- /dev/null +++ b/includes/types/worldevent.class.php @@ -0,0 +1,164 @@ + [['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']); + + $this->curTpl['eventBak'] = -$this->curTpl['id']; + + // change Ids if holiday is set + if ($this->curTpl['holidayId'] > 0) + { + $this->curTpl['id'] = $this->curTpl['holidayId']; + $this->curTpl['name'] = $this->getField('name', true); + $replace[$this->id] = $this->curTpl; + unset($this->curTpl['description']); + } + else // set a name if holiday is missing + { + // template + $this->curTpl['name_loc0'] = $this->curTpl['description']; + $this->curTpl['iconString'] = 'trade_engineering'; + $this->curTpl['name'] = '(SERVERSIDE) '.$this->getField('description', 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) + { + if ($id > 0) + $row = DB::Aowow()->SelectRow('SELECT * FROM ?_holidays WHERE Id = ?d', intVal($id)); + else + $row = DB::Aowow()->SelectRow('SELECT description as name FROM ?_events WHERE Id = ?d', intVal(-$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() { } +} + +?> diff --git a/includes/dbtypes/zone.class.php b/includes/types/zone.class.php similarity index 77% rename from includes/dbtypes/zone.class.php rename to includes/types/zone.class.php index 668eac6e..52dea10f 100644 --- a/includes/dbtypes/zone.class.php +++ b/includes/types/zone.class.php @@ -1,22 +1,19 @@ 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() { $data = []; @@ -90,17 +94,17 @@ class ZoneList extends DBTypeList return $data; } - public function getJSGlobals(int $addMask = 0) : array + public function getJSGlobals($addMask = 0) { $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() : ?string { return null; } + public function renderTooltip() { } } ?> diff --git a/includes/user.class.php b/includes/user.class.php index 9fa1bd59..e4e67df8 100644 --- a/includes/user.class.php +++ b/includes/user.class.php @@ -1,213 +1,402 @@ 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 + // session have a dataKey to access the JScripts (yes, also the anons) + if (empty($_SESSION['dataKey'])) + $_SESSION['dataKey'] = Util::createHash(); // just some random numbers for identifictaion 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)) + // check IP bans + if ($ipBan = DB::Aowow()->selectRow('SELECT count, unbanDate FROM ?_account_bannedips WHERE ip = ? AND type = 0', self::$ip)) { - if ($ipBan['count'] > Cfg::get('ACC_FAILED_AUTH_COUNT') && $ipBan['active']) + if ($ipBan['count'] > CFG_FAILED_AUTH_COUNT && $ipBan['unbanDate'] > time()) return false; - else if (!$ipBan['active']) - DB::Aowow()->qry('DELETE FROM ::account_bannedips WHERE `ip` = %s', self::$ip); + else if ($ipBan['unbanDate'] <= time()) + DB::Aowow()->query('DELETE FROM ?_account_bannedips WHERE ip = ?', self::$ip); } - - # try to restore session # - + // try to restore session if (empty($_SESSION['user'])) return false; - $session = DB::Aowow()->selectRow('SELECT `userId`, `expires` FROM ::account_sessions WHERE `status` = %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`', + // 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 + 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 (!$session || !$userData) + if (!$query) + return false; + + // password changed, terminate session + if (AUTH_MODE_SELF && $query['passHash'] != $_SESSION['hash']) { self::destroy(); return false; } - else if ($session['expires'] && $session['expires'] < time()) + + 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']; + + 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) { - 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; - } - - DB::Aowow()->qry('UPDATE ::account_sessions SET `touched` = %i, `expires` = IF(`expires`, %i, 0) WHERE `sessionId` = %s', time(), time() + Cfg::get('SESSION_TIMEOUT_DELAY'), session_id()); - - if ($loc = Locale::tryFrom($userData['locale'])) - self::$preferedLoc = $loc; - - // 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); + $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) + // 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', + 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 reputation for daily visit - if (!(self::isBanned()) && !self::isInGroup(U_GROUP_PENDING)) + // gain rep for daily visit + if (!(self::$banStatus & (ACC_BAN_TEMP | ACC_BAN_PERM))) Util::gainSiteReputation(self::$id, SITEREP_ACTION_DAILYVISIT); - // - increment consecutive visits (next day or first of new month and not more than 48h) + // 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()->qry('UPDATE ::account SET `consecutiveVisits` = `consecutiveVisits` + 1 WHERE `id` = %i', self::$id); + DB::Aowow()->query('UPDATE ?_account SET consecutiveVisits = consecutiveVisits + 1 WHERE id = ?d', self::$id); else - DB::Aowow()->qry('UPDATE ::account SET `consecutiveVisits` = 0 WHERE `id` = %i', self::$id); + DB::Aowow()->query('UPDATE ?_account SET consecutiveVisits = 0 WHERE id = ?d', self::$id); } } return true; } - public static function save(bool $toDB = false) + 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) + { + if ($rawIp = getenv($m)) + { + if ($m == 'HTTP_X_FORWARDED') + $rawIp = explode(',', $rawIp)[0]; // [ip, proxy1, proxy2] + + // check IPv4 + if ($ipAddr = filter_var($rawIp, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 | FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) + break; + + // check IPv6 + if ($ipAddr = filter_var($rawIp, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6 | FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) + break; + } + } + + self::$ip = $ipAddr ?: null; + } + + /****************/ + /* set language */ + /****************/ + + // set and save + public static function setLocale($set = -1) + { + $loc = LOCALE_EN; + + // 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"])) + { + $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; + } + } + + // check + if ($loc != LOCALE_EN && !(CFG_LOCALES & (1 << $loc))) + $loc = LOCALE_EN; + + // set + if (self::$id) + DB::Aowow()->query('UPDATE ?_account SET locale = ? WHERE id = ?', $loc, self::$id); + + self::useLocale($loc); + } + + // 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_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_FAILED_AUTH_EXCLUSION); + else // entry already exists; increment count + DB::Aowow()->query('UPDATE ?_account_bannedips SET count = count + 1, unbanDate = UNIX_TIMESTAMP() + ?d WHERE ip = ?', CFG_FAILED_AUTH_EXCLUSION, self::$ip); + + if ($ip && $ip['count'] >= CFG_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; + + if ($query['status'] & ACC_STATUS_NEW) + return AUTH_ACC_INACTIVE; + + // 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'; + $result = extAuth($name, $pass, $extId); + + if ($result == AUTH_OK && $extId) + { + if ($_ = self::checkOrCreateInDB($extId, $name)) + $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) + { + if ($_ = DB::Aowow()->selectCell('SELECT id FROM ?_account WHERE extId = ?d', $extId)) + return $_; + + $newId = DB::Aowow()->query('INSERT IGNORE INTO ?_account (extId, user, displayName, joinDate, prevIP, prevLogin, locale, status) VALUES (?d, ?, ?, UNIX_TIMESTAMP(), ?, UNIX_TIMESTAMP(), ?d, ?d)', + $extId, + $name, + Util::ucFirst($name), + isset($_SERVER["REMOTE_ADDR"]) ? $_SERVER["REMOTE_ADDR"] : '', + User::$localeId, + ACC_STATUS_OK + ); + + 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 self::$passHash == self::hashSHA1($name, $pass); + } + + public static function isValidName($name, &$errCode = 0) + { + $errCode = 0; + + if (strlen($name) < 4 || strlen($name) > 16) + $errCode = 1; + else if (preg_match('/[^\w\d]/i', $name)) + $errCode = 2; + + return $errCode == 0; + } + + public static function isValidPass($pass, &$errCode = 0) + { + $errCode = 0; + + if (strlen($pass) < 6 || strlen($pass) > 16) + $errCode = 1; + // else if (preg_match('/[^\w\d!"#\$%]/', $pass)) // such things exist..? :o + // $errCode = 2; + + return $errCode == 0; + } + + public static function save() { $_SESSION['user'] = self::$id; - $_SESSION['locale'] = self::$preferedLoc; + $_SESSION['hash'] = self::$passHash; + $_SESSION['locale'] = self::$localeId; + $_SESSION['timeout'] = self::$expires ? time() + CFG_SESSION_TIMEOUT_DELAY : 0; // $_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() @@ -215,375 +404,114 @@ class User session_regenerate_id(true); // session itself is not destroyed; status changed => regenerate id session_unset(); - $_SESSION['locale'] = self::$preferedLoc; // keep locale + $_SESSION['locale'] = self::$localeId; // keep locale $_SESSION['dataKey'] = self::$dataKey; // keep dataKey - self::$id = 0; - self::$username = ''; - self::$perms = 0; - self::$groups = U_GROUP_NONE; + self::$id = 0; + self::$displayName = ''; + 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(int $group) : bool + public static function isInGroup($group) { - return $group == U_GROUP_NONE || (self::$groups & $group) != U_GROUP_NONE; + return (self::$groups & $group) != 0; } - public static function canComment() : bool + public static function canComment() { - if (!self::isLoggedIn() || self::isBanned(ACC_BAN_COMMENT)) + if (!self::$id || self::$banStatus & (ACC_BAN_COMMENT | ACC_BAN_PERM | ACC_BAN_TEMP)) return false; - return self::$perms || self::$reputation >= Cfg::get('REP_REQ_COMMENT'); + return self::$perms || self::$reputation >= CFG_REP_REQ_COMMENT; } - public static function canReply() : bool + public static function canUpvote() { - if (!self::isLoggedIn() || self::isBanned(ACC_BAN_COMMENT)) + if (!self::$id || self::$banStatus & (ACC_BAN_COMMENT | ACC_BAN_PERM | ACC_BAN_TEMP)) return false; - return self::$perms || self::$reputation >= Cfg::get('REP_REQ_REPLY'); + return self::$perms || (self::$reputation >= CFG_REP_REQ_UPVOTE && self::$dailyVotes > 0); } - public static function canUpvote() : bool + public static function canDownvote() { - if (!self::isLoggedIn() || self::isBanned(ACC_BAN_COMMENT)) + if (!self::$id || self::$banStatus & (ACC_BAN_RATE | ACC_BAN_PERM | ACC_BAN_TEMP)) return false; - return self::$perms || (self::$reputation >= Cfg::get('REP_REQ_UPVOTE') && self::$dailyVotes > 0); + return self::$perms || (self::$reputation >= CFG_REP_REQ_DOWNVOTE && self::$dailyVotes > 0); } - public static function canDownvote() : bool + public static function canSupervote() { - if (!self::isLoggedIn() || self::isBanned(ACC_BAN_RATE)) + if (!self::$id || self::$banStatus & (ACC_BAN_RATE | ACC_BAN_PERM | ACC_BAN_TEMP)) return false; - return self::$perms || (self::$reputation >= Cfg::get('REP_REQ_DOWNVOTE') && self::$dailyVotes > 0); + return self::$reputation >= CFG_REP_REQ_SUPERVOTE; } - public static function canSupervote() : bool + public static function isPremium() { - if (!self::isLoggedIn() || self::isBanned(ACC_BAN_RATE) || self::isInGroup(U_GROUP_PENDING)) - return false; - - return self::$reputation >= Cfg::get('REP_REQ_SUPERVOTE'); + return self::isInGroup(U_GROUP_PREMIUM) || self::$reputation >= CFG_REP_REQ_PREMIUM; } - public static function canUploadScreenshot() : bool - { - if (!self::isLoggedIn() || self::isBanned(ACC_BAN_SCREENSHOT) || self::isInGroup(U_GROUP_PENDING)) - return false; - - return true; - } - - public static function canWriteGuide() : bool - { - if (!self::isLoggedIn() || self::isBanned(ACC_BAN_GUIDE) || self::isInGroup(U_GROUP_PENDING)) - return false; - - return true; - } - - public static function canSuggestVideo() : bool - { - 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() : void + public static function decrementDailyVotes() { - if (!self::isLoggedIn() || self::isBanned(ACC_BAN_RATE)) - return; - self::$dailyVotes--; - DB::Aowow()->qry('UPDATE ::account SET `dailyVotes` = %i WHERE `id` = %i', self::$dailyVotes, self::$id); + DB::Aowow()->query('UPDATE ?_account SET dailyVotes = ?d WHERE id = ?d', self::$dailyVotes, self::$id); } - public static function getCurrentDailyVotes() : int + public static function getCurDailyVotes() { - if (!self::isLoggedIn() || self::isBanned(ACC_BAN_RATE) || self::$dailyVotes < 0) - return 0; - return self::$dailyVotes; } - public static function getMaxDailyVotes() : int + public static function getMaxDailyVotes() { - if (!self::isLoggedIn() || self::isBanned(ACC_BAN_RATE)) + if (!self::$id || self::$banStatus & (ACC_BAN_PERM | ACC_BAN_TEMP)) return 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)); + 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); } - public static function getReputation() : int + public static function getReputation() { - if (!self::isLoggedIn() || self::$reputation < 0) - return 0; - return self::$reputation; } - public static function getUserGlobal() : array + public static function getUserGlobals() { $gUser = array( 'id' => self::$id, - 'name' => self::$username, + 'name' => self::$displayName, 'roles' => self::$groups, 'permissions' => self::$perms, 'cookies' => [] ); - if (!self::isLoggedIn() || self::isBanned()) + if (!self::$id || self::$banStatus & (ACC_BAN_TEMP | ACC_BAN_PERM)) return $gUser; - $gUser['commentban'] = !self::canComment(); + $gUser['commentban'] = (bool)(self::$banStatus & ACC_BAN_COMMENT); $gUser['canUpvote'] = self::canUpvote(); $gUser['canDownvote'] = self::canDownvote(); - $gUser['canPostReplies'] = self::canReply(); + $gUser['canPostReplies'] = self::canComment(); $gUser['superCommentVotes'] = self::canSupervote(); - $gUser['downvoteRep'] = Cfg::get('REP_REQ_DOWNVOTE'); - $gUser['upvoteRep'] = Cfg::get('REP_REQ_UPVOTE'); + $gUser['downvoteRep'] = CFG_REP_REQ_DOWNVOTE; + $gUser['upvoteRep'] = CFG_REP_REQ_UPVOTE; $gUser['characters'] = self::getCharacters(); - $gUser['completion'] = self::getCompletion(); - $gUser['excludegroups'] = self::$excludeGroups; - - 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, $_); if ($_ = self::getProfiles()) $gUser['profiles'] = $_; - if ($_ = self::getGuides()) - $gUser['guides'] = $_; - if ($_ = self::getWeightScales()) $gUser['weightscales'] = $_; @@ -593,202 +521,75 @@ class User return $gUser; } - public static function getWeightScales() : array + public static function getWeightScales() { - $result = []; - - 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 %in', array_keys($res)); - foreach ($weights as $id => $data) - $result[] = array_merge(['name' => $res[$id], 'id' => $id], $data); - - return $result; - } - - public static function getProfilerExclusions() : array - { - $result = []; - - 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() : array - { - if (!self::loadProfiles()) - return []; - - return self::$profiles->getJSGlobals(PROFILEINFO_CHARACTER); - } - - public static function getProfiles() : array - { - if (!self::loadProfiles()) - return []; - - return self::$profiles->getJSGlobals(PROFILEINFO_PROFILE); - } - - public static function getPinnedCharacter() : array - { - if (!self::loadProfiles()) - return []; - - $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) + + $res = DB::Aowow()->select('SELECT * FROM ?_account_weightscales WHERE userId = ?d', self::$id); + foreach ($res as $i) { - $tc = Type::newList($type, [['id', $ids]]); - if (!$tc || $tc->error) - continue; + $set = array ( + 'name' => $i['name'], + 'id' => $i['id'] + ); - $entities = []; - foreach ($tc->iterate() as $id => $__) - $entities[] = [$id, $tc->getField('name', true, true)]; + $weights = explode(',', $i['weights']); + foreach ($weights as $weight) + { + $w = explode(':', $weight); - if ($entities) - $data[] = ['id' => $type, 'entities' => $entities]; + if ($w[1] === 'undefined') + $w[1] = 0; + + $set[$w[0]] = $w[1]; + } + + $data[] = $set; } return $data; } - public static function getCompletion() : array + public static function getCharacters() { - 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 + // existing chars on realm(s) + $characters = array( + // array( + // 'id' => 22, + // 'name' => 'Example Char', + // 'realmname' => 'Example Realm', + // 'region' => 'eu', + // 'realm' => 'example-realm', + // 'race' => 6, + // 'classs' => 11, + // 'level' => 80, + // 'gender' => 1, + // 'pinned' => 1 + // ) ); - 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; + return $characters; } - private static function loadProfiles() : bool + public static function getProfiles() { - if (!Cfg::get('PROFILER_ENABLE')) - return false; + // chars build in profiler + $profiles = array( + // array('id' => 21, 'name' => 'Example Profile 1', 'race' => 4, 'classs' => 5, 'level' => 72, 'gender' => 1, 'icon' => 'inv_axe_04'), + // array('id' => 23, 'name' => 'Example Profile 2', 'race' => 11, 'classs' => 3, 'level' => 17, 'gender' => 0) + ); - if (self::$profiles === null) - { - $ap = DB::Aowow()->selectCol('SELECT `profileId` FROM ::account_profiles WHERE `accountId` = %i', self::$id); + return $profiles; + } - // 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]; + public static function getCookies() + { + $data = []; - self::$profiles = (new LocalProfileList($conditions)); - } + if (self::$id) + $data = DB::Aowow()->selectCol('SELECT name AS ARRAY_KEY, data FROM ?_account_cookies WHERE userId = ?d', self::$id); - return !!self::$profiles->getFoundIDs(); + return $data; } } diff --git a/includes/utilities.php b/includes/utilities.php index 39e9f297..a4819978 100644 --- a/includes/utilities.php +++ b/includes/utilities.php @@ -1,63 +1,162 @@ $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 + public function addCData($str) { $node = dom_import_simplexml($this); $no = $node->ownerDocument; - $node->appendChild($no->createCDATASection($cData)); + $node->appendChild($no->createCDATASection($str)); return $this; } } - -abstract class Util +class Util { - /* 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; + public static $resistanceFields = array( + null, 'resHoly', 'resFire', 'resNature', 'resFrost', 'resShadow', 'resArcane' + ); - private const GEM_SCORE_BASE_WOTLK = 16; // rare quality wotlk gem score - private const GEM_SCORE_BASE_BC = 8; // rare quality bc gem score + public static $rarityColorStings = array( // zero-indexed + '9d9d9d', 'ffffff', '1eff00', '0070dd', 'a335ee', 'ff8000', 'e5cc80', 'e6cc80' + ); - private static $perfectGems = null; + public static $localeStrings = array( // zero-indexed + 'enus', null, 'frfr', 'dede', null, null, 'eses', null, 'ruru' + ); - public static $regions = array( - 'us', 'eu', 'kr', 'tw', 'cn', 'dev' + 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' + ); + + public static $typeStrings = array( // zero-indexed + null, 'npc', 'object', 'item', 'itemset', 'quest', 'spell', 'zone', 'faction', + 'pet', 'achievement', 'title', 'event', 'class', 'race', 'skill', null, 'currency', + TYPE_USER => 'user' + ); + + public static $combatRatingToItemMod = array( // zero-indexed idx:CR; val:Mod + null, 12, 13, 14, 15, 16, 17, 18, 19, + 20, 21, 22, 23, 24, 25, 26, 27, 28, + 29, 30, null, null, null, 37, 44 + ); + + # todo (high): find a sensible way to write data here on setup + public 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 $lvlIndepRating = array( // rating doesn't scale with level + ITEM_MOD_MANA, ITEM_MOD_HEALTH, ITEM_MOD_ATTACK_POWER, ITEM_MOD_MANA_REGENERATION, ITEM_MOD_SPELL_POWER, + ITEM_MOD_HEALTH_REGEN, ITEM_MOD_SPELL_PENETRATION, ITEM_MOD_BLOCK_VALUE + ); + + public static $questClasses = array( // taken from old aowow: 2 & 3 partially point to pointless mini-areas in front of dungeons + -2 => [ 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 + 129 => [-202010, -202011, -202012] // Riding + ) + ); + + public static $sockets = array( // jsStyle Strings + 'meta', 'red', 'yellow', 'blue' + ); + + 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 $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 $ssdMaskFields = array( @@ -72,77 +171,745 @@ abstract class Util 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(\'\', \'\')'; - public static $tryFilteringEntityString = '$$WH.sprintf(LANG.lvnote_entitiesfound, %s, %s, %s) + LANG.dash + LANG.lvnote_tryfiltering.replace(\'\', \'\')'; public static $tryNarrowingString = '$$WH.sprintf(%s, %s, %s) + LANG.dash + LANG.lvnote_trynarrowing'; + public static $setCriteriaString = "fi_setCriteria(%s, %s, %s);\n"; public static $dfnString = '%s'; public static $mapSelectorString = '%s (%d)'; - public static $expansionString = [null, 'bc', 'wotlk']; + public static $expansionString = array( // 3 & 4 unused .. obviously + null, 'bc', 'wotlk', 'cata', 'mop' + ); + + 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 + ); + + // todo: translate and move to Lang + public static $spellEffectStrings = array( + 0 => 'None', + 1 => 'Instakill', + 2 => 'School Damage', + 3 => 'Dummy', + 4 => 'Portal Teleport', + 5 => 'Teleport Units', + 6 => 'Apply Aura', + 7 => 'Environmental Damage', + 8 => 'Power Drain', + 9 => 'Health Leech', + 10 => 'Heal', + 11 => 'Bind', + 12 => 'Portal', + 13 => 'Ritual Base', + 14 => 'Ritual Specialize', + 15 => 'Ritual Activate Portal', + 16 => 'Quest Complete', + 17 => 'Weapon Damage NoSchool', + 18 => 'Resurrect', + 19 => 'Add Extra Attacks', + 20 => 'Dodge', + 21 => 'Evade', + 22 => 'Parry', + 23 => 'Block', + 24 => 'Create Item', + 25 => 'Can Use Weapon', + 26 => 'Defense', + 27 => 'Persistent Area Aura', + 28 => 'Summon', + 29 => 'Leap', + 30 => 'Energize', + 31 => 'Weapon Damage Percent', + 32 => 'Trigger Missile', + 33 => 'Open Lock', + 34 => 'Summon Change Item', + 35 => 'Apply Area Aura Party', + 36 => 'Learn Spell', + 37 => 'Spell Defense', + 38 => 'Dispel', + 39 => 'Language', + 40 => 'Dual Wield', + 41 => 'Jump', + 42 => 'Jump Dest', + 43 => 'Teleport Units Face Caster', + 44 => 'Skill Step', + 45 => 'Add Honor', + 46 => 'Spawn', + 47 => 'Trade Skill', + 48 => 'Stealth', + 49 => 'Detect', + 50 => 'Trans Door', + 51 => 'Force Critical Hit', + 52 => 'Guarantee Hit', + 53 => 'Enchant Item Permanent', + 54 => 'Enchant Item Temporary', + 55 => 'Tame Creature', + 56 => 'Summon Pet', + 57 => 'Learn Pet Spell', + 58 => 'Weapon Damage Flat', + 59 => 'Create Random Item', + 60 => 'Proficiency', + 61 => 'Send Event', + 62 => 'Power Burn', + 63 => 'Threat', + 64 => 'Trigger Spell', + 65 => 'Apply Area Aura Raid', + 66 => 'Create Mana Gem', + 67 => 'Heal Max Health', + 68 => 'Interrupt Cast', + 69 => 'Distract', + 70 => 'Pull', + 71 => 'Pickpocket', + 72 => 'Add Farsight', + 73 => 'Untrain Talents', + 74 => 'Apply Glyph', + 75 => 'Heal Mechanical', + 76 => 'Summon Object Wild', + 77 => 'Script Effect', + 78 => 'Attack', + 79 => 'Sanctuary', + 80 => 'Add Combo Points', + 81 => 'Create House', + 82 => 'Bind Sight', + 83 => 'Duel', + 84 => 'Stuck', + 85 => 'Summon Player', + 86 => 'Activate Object', + 87 => 'WMO Damage', + 88 => 'WMO Repair', + 89 => 'WMO Change', + 90 => 'Kill Credit', + 91 => 'Threat All', + 92 => 'Enchant Held Item', + 93 => 'Force Deselect', + 94 => 'Self Resurrect', + 95 => 'Skinning', + 96 => 'Charge', + 97 => 'Cast Button', + 98 => 'Knock Back', + 99 => 'Disenchant', + 100 => 'Inebriate', + 101 => 'Feed Pet', + 102 => 'Dismiss Pet', + 103 => 'Reputation', + 104 => 'Summon Object Slot1', + 105 => 'Summon Object Slot2', + 106 => 'Summon Object Slot3', + 107 => 'Summon Object Slot4', + 108 => 'Dispel Mechanic', + 109 => 'Summon Dead Pet', + 110 => 'Destroy All Totems', + 111 => 'Durability Damage', + 112 => 'Summon Demon', + 113 => 'Resurrect Flat', + 114 => 'Attack Me', + 115 => 'Durability Damage Percent', + 116 => 'Skin Player Corpse', + 117 => 'Spirit Heal', + 118 => 'Skill', + 119 => 'Apply Area Aura Pet', + 120 => 'Teleport Graveyard', + 121 => 'Weapon Damage Normalized', + 122 => 'Unknown Effect', + 123 => 'Send Taxi', + 124 => 'Pull Towards', + 125 => 'Modify Threat Percent', + 126 => 'Steal Beneficial Buff', + 127 => 'Prospecting', + 128 => 'Apply Area Aura Friend', + 129 => 'Apply Area Aura Enemy', + 130 => 'Redirect Threat', + 131 => 'Unknown Effect', + 132 => 'Play Music', + 133 => 'Unlearn Specialization', + 134 => 'Kill Credit2', + 135 => 'Call Pet', + 136 => 'Heal Percent', + 137 => 'Energize Percent', + 138 => 'Leap Back', + 139 => 'Clear Quest', + 140 => 'Force Cast', + 141 => 'Force Cast With Value', + 142 => 'Trigger Spell With Value', + 143 => 'Apply Area Aura Owner', + 144 => 'Knock Back Dest', + 145 => 'Pull Towards Dest', + 146 => 'Activate Rune', + 147 => 'Quest Fail', + 148 => 'Unknown Effect', + 149 => 'Charge Dest', + 150 => 'Quest Start', + 151 => 'Trigger Spell 2', + 152 => 'Unknown Effect', + 153 => 'Create Tamed Pet', + 154 => 'Discover Taxi', + 155 => 'Dual Wield 2H Weapons', + 156 => 'Enchant Item Prismatic', + 157 => 'Create Item 2', + 158 => 'Milling', + 159 => 'Allow Rename Pet', + 160 => 'Unknown Effect', + 161 => 'Talent Spec Count', + 162 => 'Talent Spec Select', + 163 => 'Unknown Effect', + 164 => 'Remove Aura' + ); + + public static $spellAuraStrings = array( + 0 => 'None', + 1 => 'Bind Sight', + 2 => 'Mod Possess', + 3 => 'Periodic Damage', + 4 => 'Dummy', + 5 => 'Mod Confuse', + 6 => 'Mod Charm', + 7 => 'Mod Fear', + 8 => 'Periodic Heal', + 9 => 'Mod Attack Speed', + 10 => 'Mod Threat', + 11 => 'Taunt', + 12 => 'Stun', + 13 => 'Mod Damage Done Flat', + 14 => 'Mod Damage Taken Flat', + 15 => 'Damage Shield', + 16 => 'Mod Stealth', + 17 => 'Mod Stealth Detection', + 18 => 'Mod Invisibility', + 19 => 'Mod Invisibility Detection', + 20 => 'Mod Health Percent', + 21 => 'Mod Power Percent', + 22 => 'Mod Resistance Flat', + 23 => 'Periodic Trigger Spell', + 24 => 'Periodic Energize', + 25 => 'Pacify', + 26 => 'Root', + 27 => 'Silence', + 28 => 'Reflect Spells', + 29 => 'Mod Stat Flat', + 30 => 'Mod Skill', + 31 => 'Mod Increase Speed', + 32 => 'Mod Increase Mounted Speed', + 33 => 'Mod Decrease Speed', + 34 => 'Mod Increase Health', + 35 => 'Mod Increase Power', + 36 => 'Shapeshift', + 37 => 'Spell Effect Immunity', + 38 => 'Spell Aura Immunity', + 39 => 'School Immunity', + 40 => 'Damage Immunity', + 41 => 'Dispel Immunity', + 42 => 'Proc Trigger Spell', + 43 => 'Proc Trigger Damage', + 44 => 'Track Creatures', + 45 => 'Track Resources', + 46 => 'Mod Parry Skill', + 47 => 'Mod Parry Percent', + 48 => 'Unknown Aura', + 49 => 'Mod Dodge Percent', + 50 => 'Mod Critical Healing Amount', + 51 => 'Mod Block Percent', + 52 => 'Mod Physical Crit Percent', + 53 => 'Periodic Health Leech', + 54 => 'Mod Hit Chance', + 55 => 'Mod Spell Hit Chance', + 56 => 'Transform', + 57 => 'Mod Spell Crit Chance', + 58 => 'Mod Increase Swim Speed', + 59 => 'Mod Damage Done Versus Creature', + 60 => 'Pacify Silence', + 61 => 'Mod Scale', + 62 => 'Periodic Health Funnel', + 63 => 'Periodic Mana Funnel', + 64 => 'Periodic Mana Leech', + 65 => 'Mod Casting Speed (not stacking)', + 66 => 'Feign Death', + 67 => 'Disarm', + 68 => 'Stalked', + 69 => 'School Absorb', + 70 => 'Extra Attacks', + 71 => 'Mod Spell Crit Chance School', + 72 => 'Mod Power Cost School Percent', + 73 => 'Mod Power Cost School Flat', + 74 => 'Reflect Spells School', + 75 => 'Language', + 76 => 'Far Sight', + 77 => 'Mechanic Immunity', + 78 => 'Mounted', + 79 => 'Mod Damage Done Percent', + 80 => 'Mod Stat Percent', + 81 => 'Split Damage Percent', + 82 => 'Water Breathing', + 83 => 'Mod Base Resistance Flat', + 84 => 'Mod Health Regeneration', + 85 => 'Mod Power Regeneration', + 86 => 'Channel Death Item', + 87 => 'Mod Damage Taken Percent', + 88 => 'Mod Health Regeneration Percent', + 89 => 'Periodic Damage Percent', + 90 => 'Mod Resist Chance', + 91 => 'Mod Detect Range', + 92 => 'Prevent Fleeing', + 93 => 'Unattackable', + 94 => 'Interrupt Regeneration', + 95 => 'Ghost', + 96 => 'Spell Magnet', + 97 => 'Mana Shield', + 98 => 'Mod Skill Value', + 99 => 'Mod Attack Power', + 100 => 'Auras Visible', + 101 => 'Mod Resistance Percent', + 102 => 'Mod Melee Attack Power Versus', + 103 => 'Mod Total Threat', + 104 => 'Water Walk', + 105 => 'Feather Fall', + 106 => 'Hover', + 107 => 'Add Flat Modifier', + 108 => 'Add Percent Modifier', + 109 => 'Add Target Trigger', + 110 => 'Mod Power Regeneration Percent', + 111 => 'Add Caster Hit Trigger', + 112 => 'Override Class Scripts', + 113 => 'Mod Ranged Damage Taken Flat', + 114 => 'Mod Ranged Damage Taken Percent', + 115 => 'Mod Healing', + 116 => 'Mod Regeneration During Combat', + 117 => 'Mod Mechanic Resistance', + 118 => 'Mod Healing Taken Percent', + 119 => 'Share Pet Tracking', + 120 => 'Untrackable', + 121 => 'Empathy', + 122 => 'Mod Offhand Damage Percent', + 123 => 'Mod Target Resistance', + 124 => 'Mod Ranged Attack Power', + 125 => 'Mod Melee Damage Taken Flat', + 126 => 'Mod Melee Damage Taken Percent', + 127 => 'Ranged Attack Power Attacker Bonus', + 128 => 'Possess Pet', + 129 => 'Mod Speed Always', + 130 => 'Mod Mounted Speed Always', + 131 => 'Mod Ranged Attack Power Versus', + 132 => 'Mod Increase Energy Percent', + 133 => 'Mod Increase Health Percent', + 134 => 'Mod Mana Regeneration Interrupt', + 135 => 'Mod Healing Done Flat', + 136 => 'Mod Healing Done Percent', + 137 => 'Mod Total Stat Percentage', + 138 => 'Mod Melee Haste', + 139 => 'Force Reaction', + 140 => 'Mod Ranged Haste', + 141 => 'Mod Ranged Ammo Haste', + 142 => 'Mod Base Resistance Percent', + 143 => 'Mod Resistance Exclusive', + 144 => 'Safe Fall', + 145 => 'Mod Pet Talent Points', + 146 => 'Allow Tame Pet Type', + 147 => 'Mechanic Immunity Mask', + 148 => 'Retain Combo Points', + 149 => 'Reduce Pushback', + 150 => 'Mod Shield Blockvalue Percent', + 151 => 'Track Stealthed', + 152 => 'Mod Detected Range', + 153 => 'Split Damage Flat', + 154 => 'Mod Stealth Level', + 155 => 'Mod Water Breathing', + 156 => 'Mod Reputation Gain', + 157 => 'Pet Damage Multi', + 158 => 'Mod Shield Blockvalue', + 159 => 'No PvP Credit', + 160 => 'Mod AoE Avoidance', + 161 => 'Mod Health Regeneration In Combat', + 162 => 'Power Burn Mana', + 163 => 'Mod Crit Damage Bonus', + 164 => 'Unknown Aura', + 165 => 'Melee Attack Power Attacker Bonus', + 166 => 'Mod Attack Power Percent', + 167 => 'Mod Ranged Attack Power Percent', + 168 => 'Mod Damage Done Versus', + 169 => 'Mod Crit Percent Versus', + 170 => 'Change Model', + 171 => 'Mod Speed (not stacking)', + 172 => 'Mod Mounted Speed (not stacking)', + 173 => 'Unknown Aura', + 174 => 'Mod Spell Damage Of Stat Percent', + 175 => 'Mod Spell Healing Of Stat Percent', + 176 => 'Spirit Of Redemption', + 177 => 'AoE Charm', + 178 => 'Mod Debuff Resistance', + 179 => 'Mod Attacker Spell Crit Chance', + 180 => 'Mod Spell Damage Versus', + 181 => 'Unknown Aura', + 182 => 'Mod Resistance Of Stat Percent', + 183 => 'Mod Critical Threat', + 184 => 'Mod Attacker Melee Hit Chance', + 185 => 'Mod Attacker Ranged Hit Chance', + 186 => 'Mod Attacker Spell Hit Chance', + 187 => 'Mod Attacker Melee Crit Chance', + 188 => 'Mod Attacker Ranged Crit Chance', + 189 => 'Mod Rating', + 190 => 'Mod Faction Reputation Gain', + 191 => 'Use Normal Movement Speed', + 192 => 'Mod Melee Ranged Haste', + 193 => 'Mod Haste', + 194 => 'Mod Target Absorb School', + 195 => 'Mod Target Ability Absorb School', + 196 => 'Mod Cooldown', + 197 => 'Mod Attacker Spell And Weapon Crit Chance', + 198 => 'Unknown Aura', + 199 => 'Mod Increases Spell Percent to Hit', + 200 => 'Mod XP Percent', + 201 => 'Fly', + 202 => 'Ignore Combat Result', + 203 => 'Mod Attacker Melee Crit Damage', + 204 => 'Mod Attacker Ranged Crit Damage', + 205 => 'Mod School Crit Damage Taken', + 206 => 'Mod Increase Vehicle Flight Speed', + 207 => 'Mod Increase Mounted Flight Speed', + 208 => 'Mod Increase Flight Speed', + 209 => 'Mod Mounted Flight Speed Always', + 210 => 'Mod Vehicle Speed Always', + 211 => 'Mod Flight Speed (not stacking)', + 212 => 'Mod Ranged Attack Power Of Stat Percent', + 213 => 'Mod Rage from Damage Dealt', + 214 => 'Tamed Pet Passive', + 215 => 'Arena Preparation', + 216 => 'Haste Spells', + 217 => 'Killing Spree', + 218 => 'Haste Ranged', + 219 => 'Mod Mana Regeneration from Stat', + 220 => 'Mod Rating from Stat', + 221 => 'Ignore Threat', + 222 => 'Unknown Aura', + 223 => 'Raid Proc from Charge', + 224 => 'Unknown Aura', + 225 => 'Raid Proc from Charge With Value', + 226 => 'Periodic Dummy', + 227 => 'Periodic Trigger Spell With Value', + 228 => 'Detect Stealth', + 229 => 'Mod AoE Damage Avoidance', + 230 => 'Mod Increase Health', + 231 => 'Proc Trigger Spell With Value', + 232 => 'Mod Mechanic Duration', + 233 => 'Mod Display Model', + 234 => 'Mod Mechanic Duration (not stacking)', + 235 => 'Mod Dispel Resist', + 236 => 'Control Vehicle', + 237 => 'Mod Spell Damage Of Attack Power', + 238 => 'Mod Spell Healing Of Attack Power', + 239 => 'Mod Scale 2', + 240 => 'Mod Expertise', + 241 => 'Force Move Forward', + 242 => 'Mod Spell Damage from Healing', + 243 => 'Mod Faction', + 244 => 'Comprehend Language', + 245 => 'Mod Aura Duration By Dispel', + 246 => 'Mod Aura Duration By Dispel (not stacking)', + 247 => 'Clone Caster', + 248 => 'Mod Combat Result Chance', + 249 => 'Convert Rune', + 250 => 'Mod Increase Health 2', + 251 => 'Mod Enemy Dodge', + 252 => 'Mod Speed Slow All', + 253 => 'Mod Block Crit Chance', + 254 => 'Mod Disarm Offhand', + 255 => 'Mod Mechanic Damage Taken Percent', + 256 => 'No Reagent Use', + 257 => 'Mod Target Resist By Spell Class', + 258 => 'Mod Spell Visual', + 259 => 'Mod HoT Percent', + 260 => 'Screen Effect', + 261 => 'Phase', + 262 => 'Ability Ignore Aurastate', + 263 => 'Allow Only Ability', + 264 => 'Unknown Aura', + 265 => 'Unknown Aura', + 266 => 'Unknown Aura', + 267 => 'Mod Immune Aura Apply School', + 268 => 'Mod Attack Power Of Stat Percent', + 269 => 'Mod Ignore Target Resist', + 270 => 'Mod Ability Ignore Target Resist', + 271 => 'Mod Damage Taken Percent From Caster', + 272 => 'Ignore Melee Reset', + 273 => 'X Ray', + 274 => 'Ability Consume No Ammo', + 275 => 'Mod Ignore Shapeshift', + 276 => 'Mod Mechanic Damage Done Percent', + 277 => 'Mod Max Affected Targets', + 278 => 'Mod Disarm Ranged', + 279 => 'Initialize Images', + 280 => 'Mod Armor Penetration Percent', + 281 => 'Mod Honor Gain Percent', + 282 => 'Mod Base Health Percent', + 283 => 'Mod Healing Received', + 284 => 'Linked', + 285 => 'Mod Attack Power Of Armor', + 286 => 'Ability Periodic Crit', + 287 => 'Deflect Spells', + 288 => 'Ignore Hit Direction', + 289 => 'Unknown Aura', + 290 => 'Mod Crit Percent', + 291 => 'Mod XP Quest Percent', + 292 => 'Open Stable', + 293 => 'Override Spells', + 294 => 'Prevent Power Regeneration', + 295 => 'Unknown Aura', + 296 => 'Set Vehicle Id', + 297 => 'Block Spell Family', + 298 => 'Strangulate', + 299 => 'Unknown Aura', + 300 => 'Share Damage Percent', + 301 => 'School Heal Absorb', + 302 => 'Unknown Aura', + 303 => 'Mod Damage Done Versus Aurastate', + 304 => 'Mod Fake Inebriate', + 305 => 'Mod Minimum Speed', + 306 => 'Unknown Aura', + 307 => 'Heal Absorb Test', + 308 => 'Hunter Trap', + 309 => 'Unknown Aura', + 310 => 'Mod Creature AoE Damage Avoidance', + 311 => 'Unknown Aura', + 312 => 'Unknown Aura', + 313 => 'Unknown Aura', + 314 => 'Prevent Ressurection', + 315 => 'Underwater Walking', + 316 => 'Periodic Haste' + ); + + 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 $tcEncoding = '0zMcmVokRsaqbdrfwihuGINALpTjnyxtgevElBCDFHJKOPQSUWXYZ123456789'; + public static $wowheadLink = ''; private static $notes = []; - public static function addNote(string $note, int $uGroupMask = U_GROUP_EMPLOYEE, int $level = LOG_LEVEL_ERROR) : void + // creates an announcement; use if minor issues arise + public static function addNote($uGroupMask, $str) { - self::$notes[] = [$note, $uGroupMask, $level]; + // todo (med): log all those errors to DB + self::$notes[] = [$uGroupMask, $str]; } - public static function getNotes() : array + public static function getNotes($restricted = true) { $notes = []; - $severity = LOG_LEVEL_INFO; - foreach (self::$notes as $k => [$note, $uGroup, $level]) + + foreach (self::$notes as $data) + if (!$restricted || !$data[0] || User::isInGroup($data[0])) + $notes[] = $data[1]; + + return $notes; + } + + private static $execTime = 0.0; + + public static function execTime($set = false) + { + if ($set) { - if ($uGroup && !User::isInGroup($uGroup)) + self::$execTime = microTime(true); + return; + } + + if (!self::$execTime) + return; + + $newTime = microTime(true); + $tDiff = $newTime - self::$execTime; + self::$execTime = $newTime; + + return self::formatTime($tDiff * 1000, true); + } + + public static function 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 formatMoney($qty) + { + $money = ''; + + if ($qty >= 10000) + { + $g = floor($qty / 10000); + $money .= ''.$g.' '; + $qty -= $g * 10000; + } + + 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); + } + } + + public static function itemModByRatingMask($mask) + { + if (($mask & 0x1C000) == 0x1C000) // special case resilience + return ITEM_MOD_RESILIENCE_RATING; + + if (($mask & 0x00E0) == 0x00E0) // special case hit rating + return ITEM_MOD_HIT_RATING; + + for ($j = 0; $j < count(self::$combatRatingToItemMod); $j++) + { + if (!self::$combatRatingToItemMod[$j]) continue; - if ($level < $severity) - $severity = $level; + if (!($mask & (1 << $j))) + continue; - $notes[] = $note; - unset(self::$notes[$k]); + return self::$combatRatingToItemMod[$j]; } - return [$notes, $severity]; + return 0; } - public static function formatMoney(int $qty) : string + public static function sideByRaceMask($race) { - if ($qty <= 0) - return ''; + // Any + if (!$race || ($race & RACE_MASK_ALL) == RACE_MASK_ALL) + return SIDE_BOTH; - $parts = []; + // Horde + if ($race & RACE_MASK_HORDE && !($race & RACE_MASK_ALLIANCE)) + return SIDE_HORDE; - if ($g = intdiv($qty, 10000)) - $parts[] = ''.$g.''; + // Alliance + if ($race & RACE_MASK_ALLIANCE && !($race & RACE_MASK_HORDE)) + return SIDE_ALLIANCE; - if ($s = intdiv($qty % 10000, 100)) - $parts[] = ''.$s.''; - - if ($c = ($qty % 100)) - $parts[] = ''.$c.''; - - return implode(' ', $parts); + return SIDE_BOTH; } - // pageTexts, questTexts and mails - public static function parseHtmlText(string|array $text, bool $markdown = false) : string|array + public static function getReputationLevelForPoints($pts) { - if (is_array($text)) - { - foreach ($text as &$t) - $t = self::parseHtmlText($t, $markdown); - - return $text; - } + 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; + } + // 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( @@ -150,61 +917,57 @@ abstract class Util '' => '', '' => '', '' => '', - '

' => $markdown ? '[br]' : '
' + '

' => '
' ); // html may contain 'Pictures' and FlavorImages and "stuff" $text = preg_replace_callback( '/src="([^"]+)"/i', - function ($m) { return sprintf('src="%s/images/wow/%s.png"', Cfg::get('STATIC_URL'), strtr($m[1], ['\\' => '/'])); }, + function ($m) { return 'src="'.STATIC_URL.'/images/wow/'.strtr($m[1], ['\\' => '/']).'.png"'; }, strtr($text, $pairs) ); } else - $text = strtr($text, ["\n" => $markdown ? '[br]' : '
', "\r" => '']); - - // escape fake html-ish tags the browser skipsh dishplaying ...! - $text = preg_replace('/<([^\s\/]+)>/iu', '<\1>', $text); + $text = strtr($text, ["\n" => '
', "\r" => '']); $from = array( - '/\$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.) + '/\|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) '/<([^\"=\/>]+\s[^\"=\/>]+)>/ui', // emotes (workaround: at least one whitespace and never " or = between brackets) - '/\$(\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 + '/\$(\d+)w/ui' // worldState(?)-ref found on some pageTexts $1234w ); - $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( + $to = array( + '', + '\2', '<\1/\2>', - '<'.implode('/', Lang::game('pvpRank', 1)).'>', + '', + '\1', '<\1>', - 'WorldState #\1', - '<'.Lang::game('class').'>', - '<'.Lang::game('race').'>', - '<'.Lang::main('name').'>', - '
' + 'WorldState #\1' ); - $text = preg_replace($from, $markdown ? $toMD : $toHTML, $text); + $text = preg_replace($from, $to, $text); - return Lang::unescapeUISequences($text, $markdown ? Lang::FMT_MARKUP : Lang::FMT_HTML); + $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); } - public static function asHex(int $val) : string + public static function asHex($val) { $_ = decHex($val); while (fMod(strLen($_), 4)) // in 4-blocks @@ -213,20 +976,17 @@ abstract class Util return '0x'.strToUpper($_); } - public static function asBin(int $val) : string + public static function asBin($val) { $_ = decBin($val); while (fMod(strLen($_), 4)) // in 4-blocks $_ = '0'.$_; - return 'b'.$_; + return 'b'.strToUpper($_); } - public static function htmlEscape(string|array|null $data) : string|array + public static function htmlEscape($data) { - if (empty($data)) // null, '', [] and not "0" - return ''; - if (is_array($data)) { foreach ($data as &$v) @@ -234,15 +994,12 @@ abstract class Util return $data; } - - return htmlspecialchars($data, ENT_QUOTES | ENT_DISALLOWED | ENT_HTML5, 'utf-8'); + else + return htmlspecialchars(trim($data), ENT_QUOTES, 'utf-8'); } - public static function jsEscape(string|array|null $data) : string|array + public static function jsEscape($data) { - if (empty($data)) // null, '', [] and not "0" - return ''; - if (is_array($data)) { foreach ($data as &$v) @@ -250,58 +1007,33 @@ abstract class Util return $data; } - - return strtr($data, array( - '/' => '\/', - '\\' => '\\\\', - "'" => "\\'", - '"' => '\\"', - "\r" => '\\r', - "\n" => '\\n' - )); - } - - public static function defStatic(array|string $data) : array|string - { - if (is_array($data)) - { - foreach ($data as &$v) - if ($v) - $v = self::defStatic($v); - - return $data; - } - - return strtr($data, array( - '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') - )); + else + return strtr(trim($data), array( + '\\' => '\\\\', + "'" => "\\'", + '"' => '\\"', + "\r" => '\\r', + "\n" => '\\n' + )); } // default back to enUS if localization unavailable - public static function localizedString(array $data, string $field, bool $silent = false) : string + public static function localizedString($data, $field, $silent = false) { - // 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'.Lang::getLocale()->value])) - return $data[$field.'_loc'.Lang::getLocale()->value]; + if (!empty($data[$field.'_loc'.User::$localeId])) + return $data[$field.'_loc'.User::$localeId]; // locale not enUS; aowow-type localization available; add brackets if not silent - else if (Lang::getLocale() != Locale::EN && !empty($data[$field.'_loc0'])) + else if (User::$localeId != 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 (Lang::getLocale() != Locale::EN && !empty($data[$field])) + else if (User::$localeId != LOCALE_EN && !empty($data[$field])) return $silent ? $data[$field] : '['.$data[$field].']'; // locale enUS; TC localization; return normal - else if (Lang::getLocale() == Locale::EN && !empty($data[$field])) + else if (User::$localeId == LOCALE_EN && !empty($data[$field])) return $data[$field]; // nothing to find; be empty @@ -310,13 +1042,12 @@ abstract class Util } // for item and spells - public static function setRatingLevel(int $level, int $statId, int $val, bool $interactive = false) : string + public static function setRatingLevel($level, $type, $val) { - if (in_array($statId, [Stat::DEFENSE_RTG, Stat::DODGE_RTG, Stat::PARRY_RTG, Stat::BLOCK_RTG, Stat::RESILIENCE_RTG]) && $level < 34) + 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) $level = 34; - $factor = Stat::getRatingPctFactor($statId); - if (!$factor) + if (!isset(Util::$gtCombatRatings[$type])) $result = 0; else { @@ -329,203 +1060,308 @@ abstract class Util else $c = 2 / 52; - // do not use localized number format here! - $result = number_format($val / $factor / $c, 2); + $result = number_format($val / Util::$gtCombatRatings[$type] / $c, 2); } - if (!in_array($statId, [Stat::DEFENSE_RTG, Stat::EXPERTISE_RTG])) + if (!in_array($type, array(ITEM_MOD_DEFENSE_SKILL_RATING, ITEM_MOD_EXPERTISE_RATING))) $result .= '%'; - $result = Lang::item('ratingString', [$statId, $result, $level]); - - return $interactive ? sprintf(self::$setRatingLevelString, $level, $statId, $val, $result) : $result; + return sprintf(Lang::item('ratingString'), ''.$result, ''.$level); } - // 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 + public static function powerUseLocale($domain = 'www') { - $first = mb_substr($str, 0, 1); - $rest = mb_substr($str, 1); + foreach (Util::$localeStrings as $k => $v) + { + if (strstr($v, $domain)) + { + User::useLocale($k); + Lang::load(User::$localeString); + return; + } + } - return mb_strtoupper($first).$rest; + if ($domain == 'www') + { + User::useLocale(LOCALE_EN); + Lang::load(User::$localeString); + } } - public static function ucWords(string $str) : string + // EnchantmentTypes + // 0 => TYPE_NONE dnd stuff; (ignore) + // 1 => TYPE_COMBAT_SPELL proc spell from ObjectX (amountX == procChance?; ignore) + // 2 => TYPE_DAMAGE +AmountX damage + // 3 => TYPE_EQUIP_SPELL Spells from ObjectX (amountX == procChance?) + // 4 => TYPE_RESISTANCE +AmountX resistance for ObjectX School + // 5 => TYPE_STAT +AmountX for Statistic by type of ObjectX + // 6 => TYPE_TOTEM Rockbiter AmountX as Damage (ignore) + // 7 => TYPE_USE_SPELL Engineering gadgets + // 8 => TYPE_PRISMATIC_SOCKET Extra Sockets AmountX as socketCount (ignore) + public static function parseItemEnchantment($ench, $raw = false, &$misc = null) { - return mb_convert_case($str, MB_CASE_TITLE); + if (!$ench) + return []; + + if (is_numeric($ench)) + $ench = [$ench]; + + if (!is_array($ench)) + return []; + + $enchants = DB::Aowow()->select('SELECT *, Id AS ARRAY_KEY FROM ?_itemenchantment WHERE id IN (?a)', $ench); + if (!$enchants) + return []; + + $result = []; + foreach ($enchants as $eId => $e) + { + $misc[$eId] = array( + 'name' => self::localizedString($e, 'text'), + 'text' => array( + 'text_loc0' => $e['text_loc0'], + 'text_loc2' => $e['text_loc2'], + 'text_loc3' => $e['text_loc3'], + 'text_loc6' => $e['text_loc6'], + 'text_loc8' => $e['text_loc8'] + ) + ); + + if ($e['skillLine'] > 0) + $misc[$eId]['reqskill'] = $e['skillLine']; + + if ($e['skillLevel'] > 0) + $misc[$eId]['reqskillrank'] = $e['skillLevel']; + + if ($e['requiredLevel'] > 0) + $misc[$eId]['reqlevel'] = $e['requiredLevel']; + + // parse stats + $jsonStats = []; + for ($h = 1; $h <= 3; $h++) + { + $obj = (int)$e['object'.$h]; + $val = (int)$e['amount'.$h]; + + switch ($e['type'.$h]) + { + case 2: + $obj = ITEM_MOD_WEAPON_DMG; + break; + case 3: + case 7: + $spl = new SpellList(array(['s.id', $obj])); + if (!$spl->error) + Util::arraySumByKey($jsonStats, $spl->getStatGain()[$obj]); + + $obj = null; + break; + case 4: + 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: + break; + default: // skip assignment below + $obj = null; + } + + if ($obj) + { + if (!isset($jsonStats[$obj])) + $jsonStats[$obj] = 0; + + $jsonStats[$obj] += $val; + } + } + + if ($raw) + $result[$eId] = $jsonStats; + else + { + $result[$eId] = []; + foreach ($jsonStats as $k => $v) // check if we use these mods + if ($str = Util::$itemMods[$k]) + $result[$eId][$str] = $v; + } + } + + return $result; } - public static function lower(string $str) : string + // default ucFirst doesn't convert UTF-8 chars + public static function ucFirst($str) { - return mb_strtolower($str); + $len = mb_strlen($str, 'UTF-8') - 1; + $first = mb_substr($str, 0, 1, 'UTF-8'); + $rest = mb_substr($str, 1, $len, 'UTF-8'); + + return mb_strtoupper($first, 'UTF-8').$rest; } - public static function strrev(string $str) : string + public static function ucWords($str) { - $out = ''; - for ($i = 1, $len = mb_strlen($str); $i <= $len; $i++) - $out .= mb_substr($str, -$i, 1); - - return $out; + return mb_convert_case($str, MB_CASE_TITLE, 'UTF-8'); } - // doesn't handle scientific notation .. why would you input 3e3 for 3000..? - public static function checkNumeric(mixed &$data, int $typeCast = NUM_ANY) : bool + // note: valid integer > 32bit are returned as float + public static function checkNumeric(&$data) { if ($data === null) return false; - - if (is_array($data)) + else if (!is_array($data)) { - array_walk($data, function(&$x) use($typeCast) { self::checkNumeric($x, $typeCast); }); - return false; // always false for passed arrays - } + $data = trim($data); - // already in required state - if ((is_float($data) && $typeCast == NUM_REQ_FLOAT) || - (is_int($data) && $typeCast == NUM_REQ_INT)) - return true; + if (is_numeric($data)) + { + $_int = intVal($data); + $_float = floatVal($data); + + $data = ($_int == $_float) ? $_int : $_float; + return true; + } + else if (preg_match('/^\d*,\d+$/', $data)) + { + $data = floatVal(strtr($data, ',', '.')); + return true; + } - // 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; + array_walk($data, function(&$item, $key) { + self::checkNumeric($item); + }); - return true; - } - - // is string string - return false; + return false; // always false for passed arrays } - public static function arraySumByKey(array &$ref, array ...$adds) : void + public static function arraySumByKey(&$ref) { - if (!$adds) + $nArgs = func_num_args(); + if (!is_array($ref) || $nArgs < 2) return; - foreach ($adds as $arr) + for ($i = 1; $i < $nArgs; $i++) { + $arr = func_get_arg($i); + if (!is_array($arr)) + continue; + foreach ($arr as $k => $v) { if (!isset($ref[$k])) - { - if (is_array($v)) - $ref[$k] = []; - else if (is_numeric($v)) - $ref[$k] = 0; - else - continue; - } + $ref[$k] = 0; - 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; + $ref[$k] += $v; } } } - public static function createNumRange(int $min, int $max, string $delim = '', ?callable $fn = null) : string + public static function getTaughtSpells(&$spell) { - 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')) + $extraIds = [-1]; // init with -1 to prevent empty-array errors + $lookup = [-1]; + switch (gettype($spell)) { - 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 - }; + case 'object': + if (get_class($spell) != 'SpellList') + return []; - 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; + $lookup[] = $spell->id; + foreach ($spell->canTeachSpell() as $idx) + $extraIds[] = $spell->getField('effect'.$idx.'TriggerSpell'); - return $errCode ? '' : ($nameMatch[0] ?: $name); + 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 validatePassword(?string $pass, ?int &$errCode = 0) : string + public static function urlize($str) { - if (is_null($pass) || $pass === '') - return ''; + $search = ['<', '>', ' / ', "'", '(', ')']; + $replace = ['<', '>', '-', '', '', '']; + $str = str_replace($search, $replace, $str); - $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:]]+$/'] - }; + $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); + $str = preg_replace('/[^a-z0-9]/i', '-', $str); - if (($min && mb_strlen($pass) < $min) || ($max && mb_strlen($pass) > $max)) - $errCode = 1; - else if ($pattern && !preg_match($pattern, $pass, $passMatch)) - $errCode = 2; + $str = str_replace('--', '-', $str); + $str = str_replace('--', '-', $str); - return $errCode ? '' : ($passMatch[0] ?: $pass); + $str = rtrim($str, '-'); + $str = strtolower($str); + + return $str; } - public static function validateEmail(?string $email) : string + public static function isValidEmail($email) { - 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 ''; + return preg_match('/^([a-z0-9._-]+)(\+[a-z0-9._-]+)?(@[a-z0-9.-]+\.[a-z]{2,4})$/i', $email); } public static function loadStaticFile($file, &$result, $localized = false) @@ -533,8 +1369,8 @@ abstract class Util $success = true; if ($localized) { - if (file_exists('datasets/'.Lang::getLocale()->json().'/'.$file)) - $result .= file_get_contents('datasets/'.Lang::getLocale()->json().'/'.$file); + if (file_exists('datasets/'.User::$localeString.'/'.$file)) + $result .= file_get_contents('datasets/'.User::$localeString.'/'.$file); else if (file_exists('datasets/enus/'.$file)) $result .= file_get_contents('datasets/enus/'.$file); else @@ -551,29 +1387,32 @@ abstract class Util return $success; } - // just some random numbers for unsafe identification purpose - public static function createHash(int $length = 40) : string + public static function createHash($length = 40) // just some random numbers for unsafe identifictaion purpose { - static $seed = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; + static $seed = ".abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; $hash = ''; for ($i = 0; $i < $length; $i++) - $hash .= substr($seed, mt_rand(0, 61), 1); + $hash .= substr($seed, mt_rand(0, 62), 1); return $hash; } - public static function mergeJsGlobals(array &$master, array ...$adds) : bool + public static function mergeJsGlobals(&$master) { - if (!$adds) // insufficient args + $args = func_get_args(); + if (count($args) < 2) // insufficient args return false; - foreach ($adds as $arr) + if (!is_array($master)) + $master = []; + + for ($i = 1; $i < count($args); $i++) // skip first (master) entry { - foreach ($arr as $type => $data) + foreach ($args[$i] as $type => $data) { // bad data or empty - if (!Type::exists($type) || !is_array($data) || !$data) + if (empty(Util::$typeStrings[$type]) || !is_array($data) || !$data) continue; if (!isset($master[$type])) @@ -603,26 +1442,26 @@ abstract class Util switch ($action) { case SITEREP_ACTION_REGISTER: - $x['amount'] = Cfg::get('REP_REWARD_REGISTER'); + $x['amount'] = CFG_REP_REWARD_REGISTER; break; case SITEREP_ACTION_DAILYVISIT: $x['sourceA'] = time(); - $x['amount'] = Cfg::get('REP_REWARD_DAILYVISIT'); + $x['amount'] = CFG_REP_REWARD_DAILYVISIT; break; case SITEREP_ACTION_COMMENT: if (empty($miscData['id'])) return false; $x['sourceA'] = $miscData['id']; // commentId - $x['amount'] = Cfg::get('REP_REWARD_COMMENT'); + $x['amount'] = CFG_REP_REWARD_COMMENT; break; case SITEREP_ACTION_UPVOTED: case SITEREP_ACTION_DOWNVOTED: if (empty($miscData['id']) || empty($miscData['voterId'])) return false; - 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', + 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)', $miscData['id'], $miscData['voterId'], $user, @@ -631,15 +1470,15 @@ abstract class Util $x['sourceA'] = $miscData['id']; // commentId $x['sourceB'] = $miscData['voterId']; - $x['amount'] = $action == SITEREP_ACTION_UPVOTED ? Cfg::get('REP_REWARD_UPVOTED') : Cfg::get('REP_REWARD_DOWNVOTED'); + $x['amount'] = $action == SITEREP_ACTION_UPVOTED ? CFG_REP_REWARD_UPVOTED : CFG_REP_REWARD_DOWNVOTED; break; - case SITEREP_ACTION_SUBMIT_SCREENSHOT: - case SITEREP_ACTION_SUGGEST_VIDEO: + case SITEREP_ACTION_UPLOAD: if (empty($miscData['id']) || empty($miscData['what'])) return false; $x['sourceA'] = $miscData['id']; // screenshotId or videoId - $x['amount'] = $action == SITEREP_ACTION_SUBMIT_SCREENSHOT ? Cfg::get('REP_REWARD_SUBMIT_SCREENSHOT') : Cfg::get('REP_REWARD_SUGGEST_VIDEO'); + $x['sourceB'] = $miscData['what']; // screenshot or video + $x['amount'] = CFG_REP_REWARD_UPLOAD; break; case SITEREP_ACTION_GOOD_REPORT: // NYI case SITEREP_ACTION_BAD_REPORT: @@ -647,14 +1486,14 @@ abstract class Util return false; $x['sourceA'] = $miscData['id']; - $x['amount'] = $action == SITEREP_ACTION_GOOD_REPORT ? Cfg::get('REP_REWARD_GOOD_REPORT') : Cfg::get('REP_REWARD_BAD_REPORT'); + $x['amount'] = $action == SITEREP_ACTION_GOOD_REPORT ? CFG_REP_REWARD_GOOD_REPORT : CFG_REP_REWARD_BAD_REPORT; break; - case SITEREP_ACTION_ARTICLE: - if (empty($miscData['id'])) // guideId + case SITEREP_ACTION_ARTICLE: // NYI + if (empty($miscData['id'])) // reportId return false; $x['sourceA'] = $miscData['id']; - $x['amount'] = Cfg::get('REP_REWARD_ARTICLE'); + $x['amount'] = CFG_REP_REWARD_ARTICLE; break; case SITEREP_ACTION_USER_WARNED: // NYI case SITEREP_ACTION_USER_SUSPENDED: @@ -662,504 +1501,209 @@ abstract class Util return false; $x['sourceA'] = $miscData['id']; - $x['amount'] = $action == SITEREP_ACTION_USER_WARNED ? Cfg::get('REP_REWARD_USER_WARNED') : Cfg::get('REP_REWARD_USER_SUSPENDED'); + $x['amount'] = $action == SITEREP_ACTION_USER_WARNED ? CFG_REP_REWARD_USER_WARNED : CFG_REP_REWARD_USER_SUSPENDED; break; } - $x += array( + $x = array_merge($x, array( 'userId' => $user, 'action' => $action, - 'date' => $miscData['date'] ?? time() - ); + 'date' => time() + )); - return DB::Aowow()->qry('INSERT IGNORE INTO ::account_reputation %v', $x); + return DB::Aowow()->query('INSERT IGNORE INTO ?_account_reputation (?#) VALUES (?a)', array_keys($x), array_values($x)); } - public static function toJSON($data, $forceFlags = 0) + // TYPE => tableName; when handling comments, screenshots or videos + public static function getCCTableParent($type) { - $flags = $forceFlags ?: (JSON_NUMERIC_CHECK | JSON_UNESCAPED_UNICODE); - - 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; - } - - - /*****************/ - /* file handling */ - /*****************/ - - 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")) + // only filtrable types; others don't care about being flagged for having CommunityContent + switch ($type) { - if (fWrite($handle, $content)) - $success = true; - else - trigger_error('could not write to file', E_USER_ERROR); - - fClose($handle); + case TYPE_ACHIEVEMENT: return '?_achievement'; + case TYPE_SPELL: return '?_spell'; + case TYPE_OBJECT: return '?_objects'; + case TYPE_ITEM: return '?_items'; + case TYPE_ITEMSET: return '?_itemset'; + case TYPE_NPC: return '?_creature'; + case TYPE_QUEST: return '?_quests'; + default: return null; } - else - trigger_error('could not create file', E_USER_ERROR); - - if ($success) - @chmod($file, self::FILE_ACCESS); - - return $success; } - public static function writeDir(string $dir, bool &$exist = true) : bool + public static function createShowOnMap() { - // remove multiple slashes; trailing slashes - $dir = preg_replace(['/\/+/', '/\/$/'], ['/', ''], $dir) ?: '.'; - $exist = is_dir($dir); - - if ($exist) - { - 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); - } - - // 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); - return false; + /* + quest: "Quest Givers", + daily: "Quest Givers (Daily)", + alliancequests: "Quest Givers", + hordequests: "Quest Givers", + */ } - - /**************/ - /* Good Skill */ - /**************/ - - public static function getEquipmentScore(int $itemLevel, int $quality, int $slot, int $nSockets = 0) : float + public static function getServerConditions($srcType, $srcGroup = null, $srcEntry = null) { - if ($itemLevel < 0) // can this even happen? - $itemLevel = 0; - - $score = $itemLevel; - - // quality mod - switch ($quality) - { - case ITEM_QUALITY_POOR: - $score = 0; // guessed as crap - break; - case ITEM_QUALITY_NORMAL: - $score = 0; // guessed as crap - break; - case ITEM_QUALITY_UNCOMMON: - $score /= 2.0; - break; - case ITEM_QUALITY_RARE: - $score /= 1.8; - break; - case ITEM_QUALITY_EPIC: - $score /= 1.2; - break; - case ITEM_QUALITY_LEGENDARY: - $score /= 1; - break; - case ITEM_QUALITY_HEIRLOOM: // actual calculation in javascript .. still uses this as some sort of factor..? - break; - case ITEM_QUALITY_ARTIFACT: - break; - } - - switch ($slot) - { - case INVTYPE_WEAPON: - case INVTYPE_WEAPONMAINHAND: - case INVTYPE_WEAPONOFFHAND: - $score *= 27/64; - break; - case INVTYPE_SHIELD: - case INVTYPE_HOLDABLE: - $score *= 9/16; - break; - case INVTYPE_HEAD: - case INVTYPE_CHEST: - case INVTYPE_LEGS: - case INVTYPE_2HWEAPON: - $score *= 1.0; - break; - case INVTYPE_SHOULDERS: - case INVTYPE_HANDS: - case INVTYPE_WAIST: - case INVTYPE_FEET: - $score *= 3/4; - break; - case INVTYPE_WRISTS: - case INVTYPE_NECK: - case INVTYPE_CLOAK: - case INVTYPE_FINGER: - case INVTYPE_TRINKET: - $score *= 9/16; - break; - case INVTYPE_THROWN: - case INVTYPE_RANGED: - case INVTYPE_RELIC: - $score *= 81/256; - break; - default: - $score *= 0.0; - } - - // subtract sockets - if ($nSockets) - { - // 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($score, 4); - } - - 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 = %i', 55534); - - // epic - WotLK - increased stats / profession specific (Dragon's Eyes) - if ($profSpec) - return 32.0; - // epic - WotLK - base stats - if ($itemLevel == 80 && $quality == ITEM_QUALITY_EPIC) - return 20.0; - // rare - WotLK [GEM BASELINE!] - if ($itemLevel == 80 && $quality == ITEM_QUALITY_RARE) - return 16.0; - // uncommon - WotLK - inreased stats - if ($itemId > 0 && in_array($itemId, self::$perfectGems)) - return 14.0; - // uncommon - WotLK - base stats - if ($itemLevel == 70 && $quality == ITEM_QUALITY_UNCOMMON) - return 12.0; - // epic - BC - vendored (PvP) - if ($itemLevel == 60 && $quality == ITEM_QUALITY_EPIC) - return 10.0; - // epic - BC - dropped / crafted - if ($itemLevel == 70 && $quality == ITEM_QUALITY_EPIC) - return 9.0; - // rare - BC - crafted - if ($itemLevel == 70 && $quality == ITEM_QUALITY_RARE) - return 8.0; - // rare - BC - vendored (pvp) - if ($itemLevel == 60 && $quality == ITEM_QUALITY_RARE) - return 7.0; - // uncommon - BC - if ($itemLevel == 60 && $quality == ITEM_QUALITY_UNCOMMON) - return 6.0; - // common - BC - vendored gems - if ($itemLevel == 55 && $quality == ITEM_QUALITY_NORMAL) - return 4.0; - - // dafuq..? - return 0.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; - - if ($profSpec) - return 40.0; - - // other than the constraints (0 - 20 points; 40 for profession perks), everything in here is guesswork - $score = min($itemLevel, 80); - - switch ($quality) - { - case ITEM_QUALITY_HEIRLOOM: // because i say so! - $score = 20.0; - break; - case ITEM_QUALITY_RARE: - $score /= 4.8; - break; - case ITEM_QUALITY_UNCOMMON: - $score /= 6.4; - break; - case ITEM_QUALITY_NORMAL: - $score /= 10.0; - break; - default: - $score /= 4.0; - } - - return round($score, 4); - } - - public static function fixWeaponScores(int $class, array $talents, array $mainHand, array $offHand) : array - { - $mh = 1; - $oh = 1; - - if ($mainHand) { // Main Hand Equipped - if ($offHand) { // Off Hand Equipped - if ($mainHand['slotbak'] == 21 || $mainHand['slotbak'] == 13) { // Main Hand, One Hand - if ($offHand['slotbak'] == 22 || $offHand['slotbak'] == 13) { // Off Hand, One Hand - if ($class == 6 || $class == 3 || $class == 4 || // Death Knight, Hunter, Rogue - ($class == 7 && $talents['spent'][1] > 30 && $talents['spec'] == 2) || // Enhancement Shaman Over 39 - ($class == 1 && $talents['spent'][1] < 51 && $talents['spec'] == 2)) // Fury Warrior Under 60 - { - $mh = 64 / 27; - $oh = 64 / 27; - } - } - else if ($offHand['slotbak'] == 23 || $offHand['slotbak'] == 14) { // Held in Off Hand, Shield - if ($class == 5 || $class == 9 || $class == 8 || // Priest, Warlock, Mage - ($class == 11 && ($talents['spec'] == 1 || $talents['spec'] == 3)) || // Balance Druid, Restoration Druid - ($class == 7 && ($talents['spec'] == 1 || $talents['spec'] == 3)) || // Elemental Shaman, Restoration Shaman - ($class == 2 && ($talents['spec'] == 1 || $talents['spec'] == 2)) || // Holy Paladin, Protection Paladin - ($class == 1 && $talents['spec'] == 3)) // Protection Warrior - { - $mh = 64 / 27; - $oh = 16 / 9; - } - } - } - } - else if ($mainHand['slotbak'] == 17) { // Two Handed - if ($class == 5 || $class == 9 || $class == 8 || // Priest, Warlock, Mage - $class == 11 || $class == 3 || $class == 6 || // Druid, Hunter, Death Knight - ($class == 7 && $talents['spent'][1] < 31 && $talents['spec'] == 2) || // Enhancement Shaman Under 40 - ($class == 2 && $talents['spec'] == 3) || // Retribution Paladin - ($class == 1 && $talents['spec'] == 1)) // Arms Warrior - { - $mh = 2; - $oh = 0; - } - } - } - - return array( - 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) + if (!$srcGroup && !$srcEntry) return []; - $floors = []; - $menu = [[null, "Move Location to..."]]; - foreach ($points as $p) + $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 + ); + + foreach ($conditions as $c) { - 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) + switch ($c['SourceTypeOrReferenceId']) { - 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.')']; + 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 array_values($menu); + return [$result, $jsGlobals]; } - public static function sendMail(string $email, string $tplFile, array $vars = [], int $expiration = 0) : bool + public static function sendNoCacheHeader() { - if (!self::validateEmail($email)) - return false; + 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'); + } - $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; + public static function toJSON($data) + { + $flags = JSON_NUMERIC_CHECK | JSON_UNESCAPED_UNICODE; - $template = file_get_contents('template/mails/'.$tplFile.'_'.$l->value.'.tpl'); - break; - } - } + if (CFG_DEBUG) + $flags |= JSON_PRETTY_PRINT; - if (!$template) - { - trigger_error('Util::SendMail() - mail template not found: '.$tplFile, E_USER_ERROR); - return false; - } + // just a thought: .. about prefixing variables with $ to mark them as function code and retroactively stripping escapes from them + // like it's done already in with listviews for example - [, $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); + return json_encode($data, $flags); } } diff --git a/index.php b/index.php index 6be334f8..20ae26ee 100644 --- a/index.php +++ b/index.php @@ -1,96 +1,128 @@ $param) +// maybe add additional setup checks? +if (!DB::isConnectable(DB_AOWOW) || !DB::isConnectable(DB_WORLD)) + (new GenericPage($pageCall))->maintenance(); + + +$altClass = ''; +switch ($pageCall) { - // could be an array - if (!is_string($param)) - { - $pageCall = ''; // just .. fail + /* 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 'event': + case 'events': + case 'faction': + case 'factions': + // case 'guild': + // case 'guilds': + 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': // db: sounds for zone, creature, spell, ... + // case 'sounds': + case 'spell': + case 'spells': + case 'talent': // tool: talent calculator + case 'title': + case 'titles': + case 'user': + case 'video': + case 'zone': + case 'zones': + if (in_array($pageCall, ['admin', 'account', 'profile'])) + { + if (($_ = (new AjaxHandler($pageParam))->handle($pageCall)) !== null) + { + header('Content-type: application/x-javascript; charset=utf-8'); + die((string)$_); + } + } + + $_ = ($altClass ?: $pageCall).'Page'; + (new $_($pageCall, $pageParam))->display(); + break; + /* other pages */ + case 'whats-new': + case 'searchplugins': + case 'searchbox': + case 'tooltips': + case 'help': + case 'faq': + case 'aboutus': + (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; + /* called by script */ + case 'data': // tool: dataset-loader + case 'cookie': // lossless cookies and user settings + case 'contactus': + case 'comment': + case 'go-to-comment': // find page the comment is on and forward + case 'locale': // subdomain-workaround, change the language + if (($_ = (new AjaxHandler($pageParam))->handle($pageCall)) !== null) + { + header('Content-type: application/x-javascript; charset=utf-8'); + die((string)$_); + } + 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; - } - - // fix page calls - pages like search use the page call directly and expect it as lower case - if (preg_match('/[A-Z]/', $page)) - { - $url = explode('=', $_SERVER['REQUEST_URI'], 2); - $page = Util::lower(array_shift($url)).($url ? '=' . $url[0] : ''); - header('Location: '.$page, true, 302); - exit; - } - - $pageCall = preg_replace('/[^\w\-]/i', '', $page); - $pageParam = $param ?? ''; - break; // only use first k/v-pair to determine page -} - -[$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 deleted file mode 100644 index 98d60c6e..00000000 --- a/localization/datetime.class.php +++ /dev/null @@ -1,222 +0,0 @@ -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 7579c1de..f87ffca8 100644 --- a/localization/lang.class.php +++ b/localization/lang.class.php @@ -1,263 +1,114 @@ json().'.php')) - die('File for locale '.$loc->name.' not found.'); + if (!file_exists('localization/locale_'.$loc.'.php')) + die('File for localization '.strToUpper($loc).' not found.'); else - require 'localization/locale_'.$loc->json().'.php'; + require 'localization/locale_'.$loc.'.php'; foreach ($lang as $k => $v) self::$$k = $v; - // *cough* .. reuse-hacks (because copy-pastaing text for 5 locales sucks) - self::$item['cat'][2][1] = self::$spell['weaponSubClass']; + // *cough* .. reuse-hack + self::$item['cat'][2] = [self::$item['cat'][2], self::$spell['weaponSubClass']]; self::$item['cat'][2][1][14] .= ' ('.self::$item['cat'][2][0].')'; - self::$main['moreTitles']['privilege'] = self::$privileges['_privileges']; - - self::$locale = $loc; } - 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 + // todo: make static props private and access through this + public static function __callStatic($prop, $args) { if (!isset(self::$$prop)) + { + Util::addNote(U_GROUP_STAFF, 'Lang::__callStatic() - tried to use undefined property Lang::$'.$prop); return null; + } - $ref = self::$$prop; - foreach ($args as $a) + $var = self::$$prop; + foreach ($args as $key) { - if (!isset($ref[$a])) + if (!isset($var[$key])) + { + Util::addNote(U_GROUP_STAFF, 'Lang::__callStatic() - undefined key "'.$key.'" in property Lang::$'.$prop.'[\''.implode('\'][\'', $args).'\']'); return null; + } - $ref = $ref[$a]; + $var = $var[$key]; } - return $ref; + return $var; } - public static function concat(array $args, int $concat = self::CONCAT_AND, ?callable $callback = null) : string - { - $buff = ''; - $callback ??= fn($x) => $x; - - reset($args); - - if (count($args) < 2) - return $callback(current($args), key($args)); - - do - { - $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 - $buff .= self::main('or').$item; - } - while ($arg !== false); - - 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); - - $separator = match ($fmt) - { - self::FMT_HTML => '
', - self::FMT_MARKUP => '[br]', - self::FMT_RAW => "\n", - default => "\n" - }; - - return implode($separator, $row); - } - - public static function sort(string $prop, string $group, int $method = SORT_NATURAL) : void + public static function sort($prop, $group, $method = SORT_NATURAL) { if (!isset(self::$$prop)) { - trigger_error('Lang::sort - tried to use undefined property Lang::$'.$prop, E_USER_WARNING); - return; + Util::addNote(U_GROUP_STAFF, 'Lang::sort() - tried to use undefined property Lang::$'.$prop); + return null; } $var = &self::$$prop; if (!isset($var[$group])) { - trigger_error('Lang::sort - tried to use undefined property Lang::$'.$prop.'[\''.$group.'\']', E_USER_WARNING); - return; + Util::addNote(U_GROUP_STAFF, 'Lang::sort() - tried to use undefined property Lang::$'.$prop.'[\''.$group.'\']'); + return null; } asort($var[$group], $method); } // todo: expand - public static function getInfoBoxForFlags(int $cuFlags) : array + public static function getInfoBoxForFlags($flags) { $tmp = []; - if ($cuFlags & CUSTOM_DISABLED) - $tmp[] = '[tooltip name=disabledHint]'.self::main('disabledHint').'[/tooltip][span class=tip tooltip=disabledHint]'.self::main('disabled').'[/span]'; + 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_SERVERSIDE) - $tmp[] = '[tooltip name=serversideHint]'.self::main('serversideHint').'[/tooltip][span class=tip tooltip=serversideHint]'.self::main('serverside').'[/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_UNAVAILABLE) + if ($flags & CUSTOM_UNAVAILABLE) $tmp[] = self::main('unavailable'); - if ($cuFlags & CUSTOM_EXCLUDE_FOR_LISTVIEW && User::isInGroup(U_GROUP_STAFF)) + if ($flags & 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(int $lockId, ?array &$ids = [], bool $interactive = false, int $fmt = self::FMT_HTML) : array + public static function getLocks($lockId, $interactive = false) { $locks = []; - $ids = []; - $lock = DB::Aowow()->selectRow('SELECT * FROM ::lock WHERE `id` = %i', $lockId); + $lock = DB::Aowow()->selectRow('SELECT * FROM ?_lock WHERE id = ?d', $lockId); if (!$lock) return $locks; @@ -267,100 +118,60 @@ class Lang $rank = $lock['reqSkill'.$i]; $name = ''; - switch ($lock['type'.$i]) + if ($lock['type'.$i] == 1) // opened by item { - case LOCK_TYPE_ITEM: - $name = ItemList::getName($prop); - if (!$name) - continue 2; + $name = ItemList::getName($prop); + if (!$name) + continue; - if ($fmt == self::FMT_HTML) - $name = $interactive ? ''.$name.'' : ''.$name.''; - else if ($interactive && $fmt == self::FMT_MARKUP) - { - $name = '[item='.$prop.']'; - $ids[Type::ITEM][] = $prop; - } - - break; - case LOCK_TYPE_SKILL: - $name = self::spell('lockType', $prop); - if (!$name) - continue 2; - - // skills - if (in_array($prop, [1, 2, 3, 20])) - { - $skills = array( - 1 => SKILL_LOCKPICKING, - 2 => SKILL_HERBALISM, - 3 => SKILL_MINING, - 20 => SKILL_INSCRIPTION - ); - - if ($fmt == self::FMT_HTML) - $name = $interactive ? ''.$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; + 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; - $locks[$lock['type'.$i] == LOCK_TYPE_ITEM ? $prop : -$prop] = $name; + $name = self::spell('lockType', $prop); + if (!$name) + continue; + + if ($interactive) + { + $skill = 0; + switch ($prop) + { + case 1: $skill = 633; break; // Lockpicking + case 2: $skill = 182; break; // Herbing + case 3: $skill = 186; break; // Mining + case 20: $skill = 773; break; // Scribing + } + + if ($skill) + $name = ''.$name.''; + } + + if ($rank > 0) + $name .= ' ('.$rank.')'; + } + else + continue; + + $locks[$lock['type'.$i] == 1 ? $prop : -$prop] = sprintf(self::game('requires'), $name); } return $locks; } - public static function getReputationLevelForPoints(int $pts) : string + public static function getReputationLevelForPoints($pts) { - return self::game('rep', Game::getReputationLevelForPoints($pts)); + $_ = Util::getReputationLevelForPoints($pts); + + return self::game('rep', $_); } - public static function getRequiredItems(int $class, int $mask, bool $short = true) : string + public static function getRequiredItems($class, $mask, $short = true) { if (!in_array($class, [ITEM_CLASS_MISC, ITEM_CLASS_ARMOR, ITEM_CLASS_WEAPON])) return ''; @@ -386,7 +197,7 @@ class Lang } if ($class == ITEM_CLASS_MISC) // yeah hardcoded.. sue me! - return self::spell('cat', -5, 0); + return self::spell('cat', -5); $tmp = []; $strs = self::spell($class == ITEM_CLASS_ARMOR ? 'armorSubClass' : 'weaponSubClass'); @@ -394,17 +205,12 @@ class Lang if ($mask & (1 << $k) && $str) $tmp[] = $str; - if (!$tmp && $class == ITEM_CLASS_ARMOR) - return self::spell('cat', -11, 8); - else if (!$tmp && $class == ITEM_CLASS_WEAPON) - return self::spell('cat', -11, 6); - else - return implode(', ', $tmp); + return implode(', ', $tmp); } - public static function getStances(int $stanceMask) : string + public static function getStances($stanceMask) { - $stanceMask &= 0xFF37F6FF; // clamp to available stances/forms.. + $stanceMask &= 0xFC27909F; // clamp to available stances/forms.. $tmp = []; $i = 1; @@ -422,18 +228,12 @@ class Lang return implode(', ', $tmp); } - public static function getMagicSchools(int $schoolMask, bool $short = false) : string + public static function getMagicSchools($schoolMask) { $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)) @@ -447,365 +247,79 @@ class Lang return implode(', ', $tmp); } - public static function getClassString(int $classMask, array &$ids = [], int $fmt = self::FMT_HTML) : string + public static function getClassString($classMask, &$ids = [], &$n = 0, $asHTML = true) { - $classMask &= ChrClass::MASK_ALL; // clamp to available classes.. + $classMask &= CLASS_MASK_ALL; // clamp to available classes.. - if (!$classMask || $classMask == ChrClass::MASK_ALL)// available to all classes - return ''; + if ($classMask == CLASS_MASK_ALL) // available to all classes + return false; - [$base, $br] = match ($fmt) + $tmp = []; + $i = 1; + $base = $asHTML ? '%2$s' : '[class=%d]'; + $br = $asHTML ? '' : '[br]'; + + while ($classMask) { - 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)); + 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++; + } + $n = count($tmp); $ids = array_keys($tmp); return implode(', ', $tmp); } - public static function getRaceString(int $raceMask, array &$ids = [], int $fmt = self::FMT_HTML) : string + public static function getRaceString($raceMask, &$side = 0, &$ids = [], &$n = 0, $asHTML = true) { - $raceMask &= ChrRace::MASK_ALL; // clamp to available races.. + $raceMask &= RACE_MASK_ALL; // clamp to available races.. - if (!$raceMask || $raceMask == ChrRace::MASK_ALL) // available to all races (we don't display 'both factions') - return ''; + if ($raceMask == RACE_MASK_ALL) // available to all races (we don't display 'both factions') + return false; - if ($raceMask == ChrRace::MASK_HORDE) + $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) return self::game('ra', -2); - if ($raceMask == ChrRace::MASK_ALLIANCE) + if ($raceMask == RACE_MASK_ALLIANCE) return self::game('ra', -1); - [$base, $br] = match ($fmt) + while ($raceMask) { - 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)); + 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++; + } + $n = count($tmp); $ids = array_keys($tmp); return implode(', ', $tmp); } - - public static function formatSkillBreakpoints(array $bp, int $fmt = self::FMT_MARKUP) : string - { - $tmp = self::game('difficulty'); - - $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); - } - - 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); - - 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); - - return self::unescapeUISequences($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. - - * |2text - Before vowels outputs d' (with apostrophe) and removes any leading spaces from text, otherwise outputs de (with trailing space) - - * |3-formid(text) - Displays text declined to the specified form (index ranges from 1 to GetNumDeclensionSets()). - - * 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). - **/ - - 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) - { - switch ($fmt) - { - case -1: // default Lang::vspf case - case self::FMT_HTML: - return '
'; - case self::FMT_MARKUP: - return '[br]'; - case self::FMT_RAW: - default: - return ''; - } - }, $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; - } } -?> +?> \ No newline at end of file diff --git a/localization/locale_dede.php b/localization/locale_dede.php index 1ca688b4..68df160f 100644 --- a/localization/locale_dede.php +++ b/localization/locale_dede.php @@ -1,2451 +1,1061 @@ -\World of Warcraft\Data\deDE\patch-deDE-3.MPQ\Interface\FrameXML\GlobalStrings.lua -*/ - -$lang = array( - // page variables - 'timeUnits' => array( - 'sg' => ["Jahr", "Monat", "Woche", "Tag", "Stunde", "Minute", "Sekunde", "Millisekunde"], - '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", - 'signIn' => "Anmelden / Registrieren", - 'jsError' => "Stelle bitte sicher, dass JavaScript aktiviert ist.", - 'language' => "Sprache", - 'feedback' => "Rückmeldung", - '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", - '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: ", - 'related' => "Weiterführende Informationen", - 'contribute' => "Beitragen", - // 'replyingTo' => "Antwort zu einem Kommentar von", - 'submit' => "Absenden", - 'save' => 'Speichern', - 'cancel' => "Abbrechen", - 'rewards' => "Belohnungen", - 'gains' => "Belohnungen", - // 'login' => "Login", - 'forum' => "Forum", - 'siteRep' => "Ruf: ", - 'yourRepHistory'=> "Dein Ruf-Verlauf", - 'aboutUs' => "Über Aowow", - 'and' => " und ", - 'or' => " oder ", - 'back' => "Zurück", - 'reputationTip' => "Rufpunkte", - '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: ", - 'allFilter' => "Alle Filter", - 'oneFilter' => "Mindestens einer", - 'applyFilter' => "Filter anwenden", - 'resetForm' => "Formular zurücksetzen", - 'refineSearch' => 'Tipp: Präzisiere deine Suche mit Durchsuchen einer Unterkategorie.', - 'clear' => "leeren", - 'exactMatch' => "Exakt passend", - '_reqLevel' => "Mindeststufe: ", - - // infobox - 'unavailable' => "Nicht für Spieler verfügbar", - 'disabled' => "Deaktiviert", - 'disabledHint' => "Kann nicht erhalten oder abgeschlossen werden.", - 'serverside' => "Serverseitig", - 'serversideHint'=> "Diese Informationen sind nicht im Client enthalten und wurden gesnifft und/oder erraten.", - - // red buttons - 'links' => "Links", - 'compare' => "Vergleichen", - 'view3D' => "3D-Ansicht", - 'findUpgrades' => "Bessere Gegenstände finden…", - 'report' => "Melden", - 'writeGuide' => "Neuen Leitfaden erstellen", - 'edit' => "Bearbeiten", - 'changelog' => 'Änderungsprotokoll', - - // miscTools - 'errPageTitle' => "Seite nicht gefunden", - 'nfPageTitle' => "Fehler", - 'subscribe' => "Abonnieren", - 'mostComments' => ["Gestern", "Vergangene %d Tage"], - 'utilities' => array( - "Neueste Ergänzungen", "Neueste Artikel", "Neueste Kommentare", "Neueste Screenshots", null, - "Nicht bewertete Kommentare", 11 => "Neueste Videos", 12 => "Meiste Kommentare", 13 => "Fehlende Screenshots" - ), - - // article & infobox - 'langOnly' => "Diese Seite ist nur in %s verfügbar.", - - // calculators - '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: ", - '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:", - - // search - 'search' => "Suche", - 'foundResult' => "Suchergebnisse für", - 'noResult' => "Keine Ergebnisse für", - 'tryAgain' => "Bitte versucht es mit anderen Suchbegriffen oder überprüft deren Schreibweise.", - 'ignoredTerms' => "Die folgenden Wörter wurden in Eurer Suche ignoriert: %s", - - // formating - '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.", - 'intError2' => "Ein interner Fehler ist aufgetreten. (%s)", - '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", - 'whats-new' => "Was gibt's Neues?", - 'searchbox' => "Suchbox", - 'tooltips' => "Tooltips", - 'faq' => "Häufig gestellte Fragen", - 'aboutus' => "Was ist AoWoW?", - 'searchplugins' => "Such-Plugins", - 'privileges' => "Privilegien", - 'top-users' => "Hilfreichste Benutzer", - 'help' => array( - 'commenting-and-you' => "Wie man Kommentare schreibt", 'modelviewer' => "Modellviewer", 'screenshots-tips-tricks' => "Screenshots: Tipps & Tricks", - 'stat-weighting' => "Gewichtung von Werten", 'talent-calculator' => "Talentrechner", 'item-comparison' => "Gegenstandsvergleich", - 'profiler' => "Profiler", 'markup-guide' => "Markup Guide" - ) - ) - ), - '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", - 'viewCharacter' => "Charakter anzeigen", - '_cpHint' => "Der Charakter-Profiler gibt Euch die Möglichkeit, Euren Charakter zu editieren, bessere Ausrüstung zu finden, Eure Gearscore zu überprüfen, und mehr!", - '_cpHelp' => "Um loszulegen, folgt einfach den untenstehenden Schritten. Falls Ihr mehr Informationen benötigt, schaut auf unserer ausführlichen Hilfeseite nach.", - '_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", - '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." - ), - '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( - 'submission' => "Screenshot-Einsendung", - 'selectAll' => "Alles auswählen", - 'cropHint' => "Ihr könnt Euren Screenshot zuschneiden und beschriften.", - 'displayOn' => "Hochgeladen für:[br]%s - [%s=%d]", - 'caption' => "Kurzbeschreibung", - 'charLimit' => "Optional, bis zu 200 Zeichen", - 'thanks' => array( - 'contrib' => "Vielen Dank für Euren Beitrag!", - 'goBack' => 'Klickt hier, um zu der vorherigen Seite zurückzukehren.', - 'note' => "Hinweis: Euer Screenshot muss zunächst zugelassen werden, bevor es auf der Seite erscheint. Dies kann bis zu 72 Stunden dauern." - ), - 'error' => array( - 'unkFormat' => "Unbekanntes Bildformat.", - '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( - // 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", - 'title' => "Titel", // 11 - 'titles' => "Titel", - 'event' => "Weltereignis", // 12 - 'events' => "Weltereignisse", - 'class' => "Klasse", // 13 - 'classes' => "Klassen", - 'race' => "Volk", // 14 - 'races' => "Völker", - 'skill' => "Fertigkeit", // 15 - 'skills' => "Fertigkeiten", - 'currency' => "Währung", // 17 - 'currencies' => "Währungen", - '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", - 'eventShort' => "Ereignis: %s", - 'flags' => "Flags", - 'glyphType' => "Glyphenart: ", - 'level' => "Stufe", - 'mechanic' => "Auswirkung", - 'mechAbbr' => "Ausw.: ", - 'meetingStone' => "Versammlungsstein: ", - 'requires' => "Benötigt %s", - 'requires2' => "Benötigt", - 'reqLevel' => "Benötigt Stufe %s", - 'reqSkillLevel' => "Benötigte Fertigkeitsstufe: ", - 'school' => "Magieart", - 'type' => "Art: ", - 'valueDelim' => " - ", // " bis " - 'target' => "", - - 'pvp' => "PvP", - 'honorPoints' => "Ehrenpunkte", - 'arenaPoints' => "Arenapunkte", - 'heroClass' => "Heldenklasse", - 'resource' => "Ressource: ", - 'resources' => "Ressourcen: ", - 'role' => "Rolle: ", - 'roles' => "Rollen: ", - 'specs' => "Spezialisierungen: ", - '_roles' => ["Heiler", "Nahkampf-DPS", "Distanz-DPS", "Tank"], - - 'phases' => "Phasen", - '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", - "Erfolg", null, "Schwarzmarkt", "Entzaubert", "Geangelt", "Gesammelt", - "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", - 33 => "Gossensprache", 35 => "Draeneiisch", 36 => "Zombie", 37 => "Gnomenbinär", 38 => "Goblinbinär" - ), - 'gl' => [null, "Erhebliche", "Geringe"], - '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", "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", 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", - "Umgebung", "Ghul", "Terrorbärengestalt", "Steves Ghul", "Skelett von Tharon'ja", "Dunkelmond-Test der Stärke", - "BLB Spieler", "Schattentanz", "Kreatur - Bär", "Kreatur - Katze", "Geisterwolf", "Kampfhaltung", - "Verteidigungshaltung", "Berserkerhaltung", "Test", "Zombie", "Metamorphosis", null, - null, "Untotes Wesen", "Raserei", "Schnelle Fluggestalt", "Schattengestalt", "Fluggestalt", - "Verstohlenheit", "Mondkingestalt", "Geist der Erlösung" - ), - 'me' => array( - null, "Bezaubert", "Desorientiert", "Entwaffnet", "Abgelenkt", "Flüchtend", - "Ergriffen", "Unbeweglich", "Befriedet", "Schweigend", "Schlafend", "Verlangsamt", - "Betäubt", "Eingefroren", "Handlungsunfähig", "Blutend", "Heilung", "Verwandelt", - "Verbannt", "Abgeschirmt", "Gefesselt", "Reitend", "Verführt", "Vertrieben", - "Entsetzt", "Unverwundbar", "Unterbrochen", "Benommen", "Entdeckung", "Unverwundbar", - "Kopfnuss", "Wütend" - ), - 'ct' => array( - "Nicht kategorisiert", "Wildtier", "Drachkin", "Dämon", "Elementar", "Riese", - "Untoter", "Humanoid", "Tier", "Mechanisch", "Nicht spezifiziert", "Totem", - "Haustier", "Gaswolke" - ), - 'fa' => array( - 1 => "Wolf", 2 => "Katze", 3 => "Spinne", 4 => "Bär", 5 => "Eber", 6 => "Krokilisk", - 7 => "Aasvogel", 8 => "Krebs", 9 => "Gorilla", 11 => "Raptor", 12 => "Weitschreiter", 20 => "Skorpid", - 21 => "Schildkröte", 24 => "Fledermaus", 25 => "Hyäne", 26 => "Raubvogel", 27 => "Windnatter", 30 => "Drachenfalke", - 31 => "Felshetzer", 32 => "Sphärenjäger", 33 => "Sporensegler", 34 => "Netherrochen", 35 => "Schlange", 37 => "Motte", - 38 => "Schimäre", 39 => "Teufelssaurier", 41 => "Silithid", 42 => "Wurm", 43 => "Rhinozeros", 44 => "Wespe", - 45 => "Kernhund", 46 => "Geisterbestie" - ), - '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", - 'email' => "E-Mail-Adresse", - 'continue' => "Fortsetzen", - 'groups' => array( - -1 => "Keine", "Tester", "Administrator", "Editor", "Moderator", "Bürokrat", - "Entwickler", "VIP", "Blogger", "Premium", "Übersetzer", "Handelsvertreter", - "Screenshot-Verwalter", "Video-Verwalter", "API-Partner", "Ausstehend" - ), - // signIn - 'signIn' => "Anmelden", - 'user' => "Benutzername", - 'pass' => "Kennwort", - 'rememberMe' => "Angemeldet bleiben", - 'forgot' => "Vergessen", - 'forgotUser' => "Benutzername", - 'forgotPass' => "Kennwort", - 'accCreate' => 'Noch kein Konto? Jetzt eins erstellen!', - - // recovery - '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 - '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', - - // bans - 'accBanned' => "Dieses Konto wurde geschlossen", - 'bannedBy' => "Gebannt durch: ", - 'reason' => "Grund: ", - 'ends' => "Endet am: ", - 'permanent' => "Der Bann ist permanent", - 'noReason' => "Es wurde kein Grund angegeben.", - - // form-text - 'emailInvalid' => "Diese E-Mail-Adresse ist ungültig.", // message_emailnotvalid - 'userNotFound' => "Ein Konto mit diesem Namen existiert nicht.", - 'wrongPass' => "Dieses Kennwort ist ungültig.", - // '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.", - 'passCheckFail' => "Die Kennwörter stimmen nicht überein.", // message_passwordsdonotmatch - '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: ", - 'publicDesc' => "Öffentliche Beschreibung", - 'profileTitle' => "Profil von %s", - 'contributions' => "Beiträge", - '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", - '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", - '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.", - '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", - 'focus' => "Zauberfokus", - 'focusDesc' => "Zauber, die diesen Fokus benötigen, können an diesem Objekt gewirkt werden.", - 'trap' => "Falle", - 'triggeredBy' => "Ausgelöst durch", - 'capturePoint' => "Eroberungspunkt", - 'foundIn' => "Dieses Objekt befindet sich in", - 'restock' => "Wird alle %s wieder aufgefüllt.", - 'goFlags' => array( - GO_FLAG_IN_USE => 'In Benutzung', - GO_FLAG_LOCKED => 'Verschlossen', - GO_FLAG_INTERACT_COND => 'Nicht interagierbar', - GO_FLAG_TRANSPORT => 'Transporter', - GO_FLAG_NOT_SELECTABLE => 'Nicht anwählbar', - GO_FLAG_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: %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 %2$s.', - 'seat' => "Sitz", - 'accessory' => "Zusätze", - 'accessoryFor' => "Dieser NPC ist Zusatz für Fahrzeug", - '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: ", - 'resistances' => "Widerstände: ", - 'foundIn' => "Dieser NPC befindet sich in", - 'tameable' => "Zähmbar (%s)", - 'waypoint' => "Wegpunkt", - 'wait' => "Wartezeit", - '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"], - '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: ", - '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", - 'criteriaType' => "Criterium Typ-Id:", - 'itemReward' => "Ihr bekommt", - 'titleReward' => 'Euch wird der Titel "%s" verliehen', - 'slain' => "getötet", - '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.', - '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: ", - 'startZone' => "Startgebiet", - ), - 'maps' => array( - 'maps' => "Karten", - 'linkToThisMap' => "Link zu dieser Karte", - 'clear' => "Zurücksetzen", - 'EasternKingdoms' => "Östliche Königreiche", - 'Kalimdor' => "Kalimdor", - 'Outland' => "Scherbenwelt", - 'Northrend' => "Nordend", - 'Instances' => "Instanzen", - 'Dungeons' => "Dungeons", - 'Raids' => "Schlachtzüge", - 'More' => "Weitere", - 'Battlegrounds' => "Schlachtfelder", - '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.", - 'privilege' => "Privileg", - 'privileges' => "Privilegien", - 'requiredRep' => "Benötigter Ruf", - 'reqPoints' => "Dieses Privileg benötigt %s Rufpunkte.", - '_privileges' => array( - null, "Kommentare einsenden", "Webseitenexterne Links einsenden", null, - "Kein CAPTCHA", "Kommentarbewertungen zählen mehr", null, null, - null, "Mehr Wertungsfreiheit", "Kommentare positiv bewerten", "Kommentare negativ bewerten", - "Kommentarantwort einsenden", "Rahmen: Außergewöhnlich", "Rahmen: Selten", "Rahmen: Episch", - "Rahmen: Legendär", "AoWoW Premium" - ) - ), - 'zone' => array( - 'id' => "Gebiets-ID: ", - 'notFound' => "Dieses Gebiet existiert nicht.", - '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: ", - 'hcAvailable' => "Heroischer Modus verfügbar (%d)", - '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( - "Östliche Königreiche", "Kalimdor", "Dungeons", "Schlachtzüge", "Unbenutzt", null, - "Schlachtfelder", null, "Scherbenwelt", "Arenen", "Nordend" - ) - ), - '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: %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)", - 'providedItem' => "Bereitgestellter Gegenstand", - 'completion' => "Abschluss", - 'description' => "Beschreibung", - '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", - 'monthly' => "Monatlich", - 'sharable' => "Teilbar", - 'notSharable' => "Nicht teilbar", - 'repeatable' => "Wiederholbar", - 'reqQ' => "Benötigt", - 'reqQDesc' => "Um diese Quest zu erhalten, müsst ihr alle der nachfolgenden Quests abschließen", - 'reqOneQ' => "Benötigt eins von", - 'reqOneQDesc' => "Um diese Quest zu erhalten, müsst ihr eines der nachfolgenden Quests abschließen", - 'opensQ' => "Öffnet Quests", - 'opensQDesc' => "Es ist notwendig, diese Quest zu beenden, um die nachfolgenden Quests zu erhalten", - 'closesQ' => "Schließt Quests", - 'closesQDesc' => "Wenn ihr diese Quest beendet, sind die nachfolgenden Quests nicht mehr verfügbar", - 'enablesQ' => "Aktiviert", - 'enablesQDesc' => "Wenn diese Quest aktiv ist, sind die nachfolgenden Quests ebenfalls verfügbar", - '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", - '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", - '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)', - '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", - 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", - 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", - 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", - 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", - -61 => "Hexenmeister", -81 => "Krieger", -82 => "Schamane", -141 => "Paladin", -161 => "Magier", - -162 => "Schurke", -261 => "Jäger", -262 => "Priester", -263 => "Druide", -372 => "Todesritter" - ), - 5 => array( "Berufe", - -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", 3277 => "Kriegshymnenschlucht", 3358 => "Arathibecken", 3820 => "Auge des Sturms", 4384 => "Strand der Uralten", - 4710 => "Insel der Eroberung", -25 => "Schlachtfelder" - ), - 7 => array( "Verschiedenes", - -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( - "Allgemein", "Spieler gegen Spieler", "Ruf", "Dungeon & Schlachtzug", "Quests", "Berufe", "Weltereignisse" - ) - ), - '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", - 9 => "Nebenberufe", 10 => "Sprachen", 11 => "Berufe" - ) - ), - 'currency' => array( - 'id' => "Währungs-ID: ", - 'notFound' => "Diese Währung existiert nicht.", - '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" - ) - ), - 'sound' => array( - 'notFound' => "Dieser Klang existiert nicht.", - 'foundIn' => "Dieser Klang befindet sich in", - 'goToPlaylist' => "Gehe zu meiner Playlist", - 'music' => "Musik", - 'intro' => "Intromusik", - 'ambience' => "Umgebung", - 'cat' => array( - null, "Spells", "User Interface", "Footsteps", "Weapons Impacts", null, "Weapons Misses", null, null, "Pick Up/Put Down", - "NPC Combat", null, "Errors", "Nature", "Objects", null, "Death", "NPC Greetings", null, "Armor", - "Footstep Splash", "Water (Character)", "Water", "Tradeskills", "Misc Ambience", "Doodads", "Spell Fizzle", "NPC Loops", "Zone Music", "Emotes", - "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: ", - 'customRewRate' => "Abweichende Belohnungsraten", - '_transfer' => 'Die Reputation mit dieser Fraktion wird mit dem für %s vertauscht, wenn Ihr zur %s wechselt.', - 'cat' => array( - 1118 => ["Classic", 469 => "Allianz", 169 => "Dampfdruckkartell", 67 => "Horde", 891 => "Streitkräfte der Allianz", 892 => "Streitkräfte der Horde"], - 980 => ["The Burning Crusade", 936 => "Shattrath"], - 1097 => ["Wrath of the Lich King", 1052 => "Expedition der Horde", 1117 => "Sholazarbecken", 1037 => "Vorposten der Allianz"], - 0 => "Sonstige" - ) - ), - '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' => "%d Teile: ", - '_unavailable' => "Dieses Ausrüstungsset ist nicht für Spieler verfügbar.", - '_tag' => "Tag: ", - 'summary' => "Zusammenfassung", - 'notes' => array( - null, "Dungeon-Set 1", "Dungeon-Set 2", "Tier 1 Raid-Set", - "Tier 2 Raid-Set", "Tier 3 Raid-Set", "Level 60 PvP-Set (Rar)", "Level 60 PvP-Set (Rar, alt)", - "Level 60 PvP-Set (Episch)", "Set der Ruinen von Ahn'Qiraj", "Set des Tempels von Ahn'Qiraj", "Set von Zul'Gurub", - "Tier 4 Raid-Set", "Tier 5 Raid-Set", "Dungeon-Set 3", "Set des Arathibeckens", - "Level 70 PvP-Set (Rar)", "Arena-Set Saison 1", "Tier 6 Raid-Set", "Arena-Set Saison 2", - "Arena-Set Saison 3", "Level 70 PvP-Set 2 (Rar)", "Arena-Set Saison 4", "Tier 7 Raid-Set", - "Arena-Set Saison 5", "Tier 8 Raid-Set", "Arena-Set Saison 6", "Tier 9 Raid-Set", - "Arena-Set Saison 7", "Tier 10 Raid-Set", "Arena-Set Saison 8" - ), - 'types' => array( - null, "Stoff", "Leder", "Schwere Rüstung", "Platte", "Dolch", "Ring", - "Faustwaffe", "Einhandaxt", "Einhandstreitkolben", "Einhandschwert", "Schmuck", "Amulett" - ) - ), - 'spell' => array( - 'id' => "Zauber-ID: ", - 'notFound' => "Dieser Zauber existiert nicht.", - '_spellDetails' => "Zauberdetails", - '_cost' => "Kosten", - '_range' => "Reichweite", - '_castTime' => "Zauberzeit", - '_cooldown' => "Abklingzeit", - '_distUnit' => " Meter", - '_forms' => "Gestalten", - '_aura' => "Aura", - '_effect' => "Effekt", - '_none' => "Nichts", - '_gcd' => "GCD", - '_globCD' => "Globale Abklingzeit", - '_gcdCategory' => "GCD-Kategorie", - '_value' => "Wert", - '_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.', - '_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' => "(%.1f Auslösungen pro Minute)", - 'procChance' => "Procchance: %.4g%%", - 'starter' => "Basiszauber", - 'trainingCost' => "Trainingskosten: ", - 'channeled' => "Kanalisiert", - 'range' => "%s Meter Reichweite", - 'meleeRange' => "Nahkampfreichweite", - 'unlimRange' => "Unbegrenzte Reichweite", - 'reagents' => "Reagenzien", - 'tools' => "Extras", - 'home' => "<Gasthaus>", - '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", - 'apMod' => " (AP mod: %.3g)", - 'spMod' => " (ZM mod: %.3g)", - 'instantPhys' => "Sofort", - 'castTime' => array( - "Spontanzauber", - "Wirken in %.3g Sek.", - "Wirken in %.3g Min." - ), - '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", - // powerDisplay - -1 => "Munition", -41 => "Pyrit", -61 => "Dampfdruck", -101 => "Hitze", -121 => "Schlamm", -141 => "Blutmacht", - -142 => "Wrath" - ), - 'relItems' => array( - 'base' => "%s im Zusammenhang mit %s anzeigen", - 'link' => " oder ", - 'recipes' => 'Rezeptgegenstände', - 'crafted' => 'Hergestellte Gegenstände' - ), - 'cat' => array( - 7 => "Klassenfertigkeiten", - -13 => "Glyphen", - -11 => ["Sachverstand", 8 => "Rüstung", 6 => "Waffen", 10 => "Sprachen"], - -4 => "Völkerfertigkeiten", - -2 => "Talente", - -6 => "Haustiere", - -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", - 214 => "Krebs", 212 => "Krokilisk", 775 => "Motte", 764 => "Netherrochen", 217 => "Raptor", 655 => "Raubvogel", 786 => "Rhinozeros", - 251 => "Schildkröte", 780 => "Schimäre", 768 => "Schlange", 783 => "Silithid", 236 => "Skorpid", 766 => "Sphärenjäger", 203 => "Spinne", - 765 => "Sporensegler", 781 => "Teufelssaurier", 218 => "Weitschreiter", 785 => "Wespe", 656 => "Windnatter", 208 => "Wolf", 784 => "Wurm", - 204 => "Leerwandler", 205 => "Sukkubus", 189 => "Teufelsjäger", 761 => "Teufelswache", 188 => "Wichtel", - ), - -7 => ["Begleitertalente", 410 => "Gerissenheit", 411 => "Wildheit", 409 => "Hartnäckigkeit"], - 11 => array( - "Berufe", - 171 => "Alchemie", - 164 => ["Schmiedekunst", 9788 => "Rüstungsschmied", 9787 => "Waffenschmied", 17041 => "Axtschmiedemeister", 17040 => "Hammerschmiedemeister", 17039 => "Schwertschmiedemeister"], - 333 => "Verzauberkunst", - 202 => ["Ingenieurskunst", 20219 => "Gnomeningenieurskunst", 20222 => "Gobliningenieurskunst"], - 182 => "Kräuterkunde", - 773 => "Inschriftenkunde", - 755 => "Juwelenschleifen", - 165 => ["Lederverarbeitung", 10656 => "Drachenschuppenlederverarbeitung", 10658 => "Elementarlederverarbeitung", 10660 => "Stammeslederverarbeitung"], - 186 => "Bergbau", - 393 => "Kürschnerei", - 197 => ["Schneiderei", 26798 => "Mondstoffschneiderei", 26801 => "Schattenstoffschneiderei", 26797 => "Zauberfeuerschneiderei"], - ), - 9 => ["Nebenberufe", 185 => "Kochkunst", 129 => "Erste Hilfe", 356 => "Angeln", 762 => "Reiten"], - -8 => "NPC-Fähigkeiten", - -9 => "GM-Fähigkeiten", - 0 => "Nicht kategorisiert" - ), - 'armorSubClass' => array( - "Sonstiges", "Stoffrüstung", "Lederrüstung", "Schwere Rüstung", "Plattenrüstung", - null, "Schilde", "Buchbände", "Götzen", "Totems", - "Siegel" - ), - 'weaponSubClass' => array( - 15 => "Dolche", 0 => "Einhandäxte", 7 => "Einhandschwerter", 4 => "Einhandstreitkolben", 13 => "Faustwaffen", - 6 => "Stangenwaffen", 10 => "Stäbe", 1 => "Zweihandäxte", 8 => "Zweihandschwerter", 5 => "Zweihandstreitkolben", - 18 => "Armbrüste", 2 => "Bögen", 3 => "Schusswaffen", 16 => "Wurfwaffen", 19 => "Zauberstäbe", - 20 => "Angelruten", 14 => "Diverse" - ), - 'subClassMasks' => array( - 0x02A5F3 => "Nahkampfwaffe", 0x0060 => "Schild", 0x04000C => "Distanzwaffe", 0xA091 => "Einhandnahkampfwaffe" - ), - 'traitShort' => array( - 'atkpwr' => "Angr", 'rgdatkpwr' => "DAngr", 'splpwr' => "ZMacht", 'arcsplpwr' => "ArkM", 'firsplpwr' => "FeuM", - 'frosplpwr' => "FroM", 'holsplpwr' => "HeiM", 'natsplpwr' => "NatM", 'shasplpwr' => "SchM", 'splheal' => "Heil", - 'str' => "Stä", 'agi' => "Bew", 'sta' => "Aus", 'int' => "Int", 'spi' => "Wil" - ), - 'spellModOp' => array( - "Schaden", "Dauer", "Bedrohung", "Effekt 1", "Aufladungen", - "Reichweite", "Radius", "Kritische Trefferchance", "Alle Effekte", "Zauberzeitverlust", - "Zauberzeit", "Abklingzeit", "Effekt 2", "Ignoriere Rüstung", "Kosten", - "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" - ), - 'combatRating' => array( - "Waffenfertigkeit", "Verteidigungsfertigkeit", "Ausweichen", "Parrieren", "Blocken", - "Nahkampftrefferchance", "Fernkampftrefferchance", "Zaubertrefferchance", "kritische Nahkampftrefferchance", "kritische Fernkampftrefferchance", - "kritische Zaubertrefferchance", "erhaltene Nahkampftreffer", "erhaltene Fernkampftreffer", "erhaltene Zaubertreffer", "erhaltene kritische Nahkampftreffer", - "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", - "Schnell öffnen", "Schnell schließen", "Offenes Tüfteln", "Offenes Knien", "Offenes Angreifen", - "Gahz'ridian (DND)", "Schlagen", "PvP öffnen", "PvP schließen", "Angeln", - "Inschriftenkunde", "Vom Fahrzeug öffnen" - ), - 'stealthType' => ["Allgemein", "Falle"], - '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', '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', '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 (%1$d)', - 'auras' => array( -/*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 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' => '%2$s @ Lvl%3$d', - 'heroic' => "Heroisch", - 'startQuest' => "Dieser Gegenstand startet eine Quest", - 'bagSlotString' => '%1$d Platz %2$s', - 'fap' => "Angriffskraft in Tiergestalt", - 'durability' => 'Haltbarkeit %1$d / %2$d', - 'realTime' => "Realzeit", - 'conjured' => "Herbeigezauberter Gegenstand", - 'sellPrice' => "Verkaufspreis", - 'itemLevel' => "Gegenstandsstufe %d", - 'randEnchant' => "<Zufällige Verzauberung>", - 'readClick' => "<Zum Lesen rechtsklicken>", - 'openClick' => "<Zum Öffnen rechtsklicken>", - 'setBonus' => "(%d) Set: %s", - 'setName' => '%1$s (%2$d/%3$d)', - 'partyLoot' => "Gruppenloot", - 'smartLoot' => "Intelligente Beuteverteilung", - 'indestructible'=> "Kann nicht zerstört werden", - 'deprecated' => "Nicht benutzt", - 'useInShape' => "Benutzbar in Gestaltwandlung", - 'useInArena' => "Benutzbar in Arenen", - 'refundable' => "Rückzahlbar", - 'noNeedRoll' => "Kann nicht für Bedarf werfen", - 'atKeyring' => "Passt in den Schlüsselbund", - 'worth' => "Wert: ", - 'consumable' => "Verbrauchbar", - 'nonConsumable' => "Nicht verbrauchbar", - 'accountWide' => "Accountweit", - 'millable' => "Mahlbar", - 'noEquipCD' => "Keine Anlegabklingzeit", - 'prospectable' => "Sondierbar", - 'disenchantable'=> "Kann entzaubert werden", - 'cantDisenchant'=> "Kann nicht entzaubert werden", - 'repairCost' => "Reparaturkosten: ", - 'tool' => "Werkzeug: ", - 'cost' => "Preis", - 'content' => "Inhalt", - '_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: ", - 'buyout' => "Sofortkaufpreis", - 'each' => "Stück", - 'tabOther' => "Anderes", - 'reqMinLevel' => "Benötigt Stufe %d", - 'reqLevelRange' => "Benötigt Stufe %d bis %d (%s)", - 'unique' => ["Einzigartig", "Limitiert (%d)", "Einzigartig: %s (%d)" ], - '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: %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) - 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.", - "Benötigt eine persönliche und eine Teamwertung von %d|nin 3v3- oder 5v5-Turnieren", - "Benötigt eine persönliche und eine Teamwertung von %d|nin 5v5-Turnieren" - ), - 'quality' => array( - "Schlecht", "Verbreitet", "Selten", "Rar", - "Episch", "Legendär", "Artefakt", "Erbstücke", - ), - 'trigger' => array( - "Benutzen: ", "Anlegen: ", "Chance bei Treffer: ", "", "", - "", "" - ), - 'bonding' => array( // ITEM_BIND_* - "Accountgebunden", "Wird beim Aufheben gebunden", "Wird beim Anlegen gebunden", - "Wird bei Benutzung gebunden", "Questgegenstand", "Questgegenstand" - ), - 'bagFamily' => array( - "Tasche", "Köcher", "Munitionsbeutel", "Seelentasche", "Lederertasche", - "Schreibertasche", "Kräutertasche", "Verzauberertasche", "Ingenieurstasche", null, /*Schlüssel*/ - "Edelsteintasche", "Bergbautasche" - ), - 'inventoryType' => array( - null, "Kopf", "Hals", "Schulter", "Hemd", - "Brust", "Taille", "Beine", "Füße", "Handgelenke", - "Hände", "Finger", "Schmuck", "Einhändig", "Schildhand", /*Schild*/ - "Distanz", "Rücken", "Zweihändig", "Tasche", "Wappenrock", - null, /*Robe*/ "Waffenhand", "Schildhand", "In der Schildhand geführt", "Projektil", - "Wurfwaffe", null, /*Ranged2*/ "Köcher", "Relikt" - ), - 'armorSubClass' => array( - "Sonstiges", "Stoff", "Leder", "Schwere Rüstung", "Platte", - null, "Schild", "Buchband", "Götze", "Totem", - "Sigel" - ), - 'weaponSubClass' => array( - "Axt", "Axt", "Bogen", "Schusswaffe", "Streitkolben", - "Streitkolben", "Stangenwaffe", "Schwert", "Schwert", null, - "Stab", null, null, "Faustwaffe", "Diverse", - "Dolche", "Wurfwaffe", null, "Armbrust", "Zauberstab", - "Angelrute" - ), - 'projectileSubClass' => array( - null, null, "Pfeil", "Kugel", null - ), - 'elixirType' => [null, "Kampf", "Wächter"], - 'cat' => array( - 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", - -7 => "Wappenröcke", -3 => "Amulette", -2 => "Ringe", -4 => "Schmuckstücke", 0 => "Verschiedenes (Rüstung)", - )), - 1 => array("Behälter", array( - 0 => "Taschen", 3 => "Verzauberertaschen", 4 => "Ingenieurstaschen", 5 => "Edelsteintaschen", 2 => "Kräutertaschen", 8 => "Schreibertaschen", - 7 => "Lederertaschen", 6 => "Bergbautaschen", 1 => "Seelentaschen" - )), - 0 => array("Verbrauchbar", array( - -3 => "Gegenstandsverzauberungen (Temporäre)", 6 => "Gegenstandsverzauberungen (Dauerhafte)", 2 => ["Elixire", [1 => "Kampfelixire", 2 => "Wächterelixire"]], - 1 => "Tränke", 4 => "Schriftrollen", 7 => "Verbände", 0 => "Verbrauchbar", 3 => "Fläschchen", 5 => "Essen & Trinken", - 8 => "Andere (Verbrauchbar)" - )), - 16 => array("Glyphen", array( - 1 => "Kriegerglyphen", 2 => "Paladinglyphen", 3 => "Jägerglyphen", 4 => "Schurkenglyphen", 5 => "Priesterglyphen", 6 => "Todesritterglyphen", - 7 => "Schamanenglyphen", 8 => "Magierglyphen", 9 => "Hexenmeisterglyphen", 11 => "Druidenglyphen" - )), - 7 => array("Handwerkswaren", array( - 14 => "Rüstungsverzauberungen", 5 => "Stoff", 3 => "Geräte", 10 => "Elementar", 12 => "Verzauberkunst", 2 => "Sprengstoff", - 9 => "Kräuter", 4 => "Juwelenschleifen", 6 => "Leder", 13 => "Materialien", 8 => "Fleisch", 7 => "Metall & Stein", - 1 => "Teile", 15 => "Waffenverzauberungen", 11 => "Andere (Handwerkswaren)" - )), - 6 => ["Projektile", [ 2 => "Pfeile", 3 => "Kugeln" ]], - 11 => ["Köcher", [ 2 => "Köcher", 3 => "Munitionsbeutel" ]], - 9 => array("Rezepte", array( - 0 => "Bücher", 6 => "Alchemierezepte", 4 => "Schmiedekunstpläne", 5 => "Kochrezepte", 8 => "Verzauberkunstformeln", 3 => "Ingenieurschemata", - 7 => "Erste Hilfe-Bücher", 9 => "Angelbücher", 11 => "Inschriftenkundetechniken",10 => "Juwelenschleifen-Vorlagen",1 => "Lederverarbeitungsmuster",12 => "Bergbauleitfäden", - 2 => "Schneidereimuster" - )), - 3 => array("Edelsteine", array( - 6 => "Meta-Edelsteine", 0 => "Rote Edelsteine", 1 => "Blaue Edelsteine", 2 => "Gelbe Edelsteine", 3 => "Violette Edelsteine", 4 => "Grüne Edelsteine", - 5 => "Orange Edelsteine", 8 => "Prismatische Edelsteine", 7 => "Einfache Edelsteine" - )), - 15 => array("Verschiedenes", array( - -2 => "Rüstungsmarken", 3 => "Feiertag", 0 => "Plunder", 1 => "Reagenzien", 5 => "Reittiere", -7 => "Flugtiere", - 2 => "Haustiere", 4 => "Andere (Verschiedenes)" - )), - 10 => "Währung", - 12 => "Quest", - 13 => "Schlüssel", - ), - 'statType' => array( // ITEM_MOD_* - '%1$c%2$d Mana', - '%1$c%2$d Gesundheit', - null, - '%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 Distanztrefferwertung um %d.", - "Erhöht Zaubertrefferwertung um %d.", - "Erhöht kritische Nahkampftrefferwertung um %d.", - "Erhöht kritische Distanztrefferwertung um %d.", - "Erhöht kritische Zaubertrefferwertung um %d.", - "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 Distanztempowertung um %d.", - "Erhöht Zaubertempowertung 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 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 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 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 den Blockwert Eures Schilds um %d.", - "Unbekannter Bonus #%d (%d)" - ) - ) -); - -?> +\World of Warcraft\Data\deDE\patch-deDE-3.MPQ\Interface\FrameXML\GlobalStrings.lua + like: ITEM_MOD_*, POWER_TYPE_*, ITEM_BIND_*, PVP_RANK_* +*/ + +$lang = array( + // page variables + 'timeUnits' => array( + 'sg' => ["Jahr", "Monat", "Woche", "Tag", "Stunde", "Minute", "Sekunde", "Millisekunde"], + 'pl' => ["Jahre", "Monate", "Wochen", "Tage", "Stunden", "Minuten", "Sekunden", "Millisekunden"], + 'ab' => ["J.", "M.", "W.", "Tag", "Std.", "Min.", "Sek.", "Ms."], + 'ago' => 'vor %s' + ), + 'main' => array( + 'name' => "Name", + 'link' => "Link", + 'signIn' => "Anmelden / Registrieren", + 'jsError' => "Stelle bitte sicher, dass JavaScript aktiviert ist.", + 'language' => "Sprache", + 'feedback' => "Rückmeldung", + 'numSQL' => "Anzahl an MySQL-Queries", + 'timeSQL' => "Zeit für MySQL-Queries", + 'noJScript' => 'Diese Seite macht ausgiebigen Gebrauch von JavaScript.
Bitte aktiviert JavaScript in Eurem Browser.', + 'userProfiles' => "Deine Charaktere", + 'pageNotFound' => "Dies %s existiert nicht.", + 'gender' => "Geschlecht", + 'sex' => [null, "Mann", "Frau"], + 'players' => "Spieler", + 'quickFacts' => "Kurzübersicht", + 'screenshots' => "Screenshots", + 'videos' => "Videos", + 'side' => "Seite", + 'related' => "Weiterführende Informationen", + 'contribute' => "Beitragen", + // 'replyingTo' => "Antwort zu einem Kommentar von", + 'submit' => "Absenden", + 'cancel' => "Abbrechen", + 'rewards' => "Belohnungen", + 'gains' => "Belohnungen", + 'login' => "Login", + 'forum' => "Forum", + 'n_a' => "n. v.", + 'siteRep' => "Ruf", + 'aboutUs' => "Über Aowow", + 'and' => " und ", + 'or' => " oder ", + 'back' => "Zurück", + + // filter + 'extSearch' => "Erweiterte Suche", + 'addFilter' => "Weiteren Filter hinzufügen", + 'match' => "Verwendete Filter", + 'allFilter' => "Alle Filters", + 'oneFilter' => "Mindestens einer", + 'applyFilter' => "Filter anwenden", + 'resetForm' => "Formular zurücksetzen", + 'refineSearch' => 'Tipp: Präzisiere deine Suche mit Durchsuchen einer Unterkategorie.', + 'clear' => "leeren", + 'exactMatch' => "Exakt passend", + '_reqLevel' => "Mindeststufe", + + // infobox + 'unavailable' => "Nicht für Spieler verfügbar", + 'disabled' => "Deaktiviert", + 'disabledHint' => "Kann nicht erhalten oder abgeschlossen werden.", + 'serverside' => "Serverseitig", + 'serversideHint'=> "Diese Informationen sind nicht im Client enthalten und wurden gesnifft und/oder erraten.", + + // red buttons + 'links' => "Links", + 'compare' => "Vergleichen", + 'view3D' => "3D-Ansicht", + 'findUpgrades' => "Bessere Gegenstände finden...", + + // miscTools + 'errPageTitle' => "Seite nicht gefunden", + 'nfPageTitle' => "Fehler", + 'subscribe' => "Abonnieren", + 'mostComments' => ["Gestern", "Vergangene %d Tage"], + 'utilities' => array( + "Neueste Ergänzungen", "Neueste Artikel", "Neueste Kommentare", "Neueste Screenshots", null, + "Nicht bewertete Kommentare", 11 => "Neueste Videos", 12 => "Meiste Kommentare", 13 => "Fehlende Screenshots" + ), + + // article & infobox + 'englishOnly' => "Diese Seite ist nur in Englisch verfügbar.", + + // calculators + '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", + '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", + + // profiler + 'realm' => "Realm", + 'region' => "Region", + 'viewCharacter' => "Charakter anzeigen", + '_cpHead' => "Charakter-Profiler", + '_cpHint' => "Der Charakter-Profiler gibt Euch die Möglichkeit, Euren Charakter zu editieren, bessere Ausrüstung zu finden, Eure Gearscore zu überprüfen, und mehr!", + '_cpHelp' => "Um loszulegen, folgt einfach den untenstehenden Schritten. Falls Ihr mehr Informationen benötigt, schaut auf unserer ausführlichen Hilfeseite nach.", + '_cpFooter' => "Falls Ihr eine genauere Suche möchtet, probiert unsere erweiterten Suchoptionen. Ihr könnt außerdem ein neues individuelles Profil erstellen.", + + // help + 'help' => "Hilfe", + 'helpTopics' => array( + "Wie man Kommentare schreibt", "Modellviewer", "Screenshots: Tipps & Tricks", "Gewichtung von Werten", + "Talentrechner", "Gegenstandsvergleich", "Profiler", "Markup Guide" + ), + + // 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.", + 'ignoredTerms' => "Die folgenden Wörter wurden in Eurer Suche ignoriert: %s", + + // formating + 'colon' => ': ', + 'dateFmtShort' => "d.m.Y", + 'dateFmtLong' => "d.m.Y \u\m H:i", + + // error + 'intError' => "Ein interner Fehler ist aufgetreten.", + 'intError2' => "Ein interner Fehler ist aufgetreten. (%s)", + '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 + + // screenshots + 'prepError' => "Bei der Aufbereitung eures Screenshots ist ein Fehler aufgetreten", + 'cropHint' => "Schneidet das Bild zu, indem ihr die Auswahl verschiebt.
Bitte beachtet Screenshots: Tipps & Tricks für eine optimale Darstellung.", + 'caption' => "Kurzbeschreibung", + 'originalSize' => "Originalgröße", + 'targetSize' => "Zielgröße", + 'minSize' => "Mindestgröße", + 'displayOn' => "Hochgeladen für: %s[br][%s=%d]", + 'ssEdit' => "Screenshot bearbeiten", + 'ssUpload' => "Screenshot hochladen", + 'ssSubmit' => "Screenshot einsenden", + 'ssErrors' => array( + 'noUpload' => "Die Datei wurde nicht hochgeladen!", + 'maxSize' => "Die Datei überschreitet die max. Größe von %s!", + 'interrupted' => "Der Vorgang wurde unterbrochen!", + 'noFile' => "Es wurde keine Datei empfangen!", + 'noDest' => "Die Seite auf welcher der Screenshot angezeigt werden sollte existiert nicht!", + 'notAllowed' => "Es ist euch nicht erlaubt einen Screenshot hochzuladen!", + 'noImage' => "Die hochgeladene Datei ist kein Bild!", + 'wrongFormat' => "Das Bild muss im PNG oder JPG-Format sein!", + 'load' => "Das Bild konnte nicht geladen werden!", + 'tooSmall' => "Die Abmessungen sind zu klein! (kleiner als %d x %d)", + 'tooLarge' => "Die Abmessungen sind zu groß! (größer als %d x %d)" + ) + ), + 'game' => array( + 'achievement' => "Erfolg", + 'achievements' => "Erfolge", + 'class' => "Klasse", + 'classes' => "Klassen", + 'currency' => "Währung", + 'currencies' => "Währungen", + 'difficulty' => "Modus", + 'dispelType' => "Bannart", + 'duration' => "Dauer", + 'gameObject' => "Objekt", + 'gameObjects' => "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", + 'item' => "Gegenstand", + 'items' => "Gegenstände", + 'itemset' => "Ausrüstungsset", + 'itemsets' => "Ausrüstungssets", + 'mechanic' => "Auswirkung", + 'mechAbbr' => "Ausw.", + 'meetingStone' => "Versammlungsstein", + 'npc' => "NPC", + 'npcs' => "NPCs", + 'pet' => "Begleiter", + 'pets' => "Begleiter", + 'profile' => "", + 'profiles' => "Profile", + 'quest' => "Quest", + 'quests' => "Quests", + 'requires' => "Benötigt %s", + 'requires2' => "Benötigt", + 'reqLevel' => "Benötigt Stufe %s", + 'reqLevelHlm' => "Benötigt Stufe %s", + 'reqSkillLevel' => "Benötigte Fertigkeitsstufe", + 'level' => "Stufe", + 'school' => "Magieart", + 'skill' => "Fertigkeit", + 'skills' => "Fertigkeiten", + 'spell' => "Zauber", + 'spells' => "Zauber", + 'type' => "Art", + 'valueDelim' => " - ", // " bis " + 'zone' => "Zone", + 'zones' => "Gebiete", + + 'pvp' => "PvP", + 'honorPoints' => "Ehrenpunkte", + 'arenaPoints' => "Arenapunkte", + 'heroClass' => "Heldenklasse", + '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"], + 'expansions' => ["Classic", "The Burning Crusade", "Wrath of the Lich King"], + 'stats' => ["Stärke", "Beweglichkeit", "Ausdauer", "Intelligenz", "Willenskraft"], + 'sources' => array( + "Unbekannt", "Hergestellt", "Drop", "PvP", "Quest", "Händler", + "Lehrer", "Entdeckung", "Einlösung", "Talent", "Startausrüstung", "Ereignis", + "Erfolg", null, "Schwarzmarkt", "Entzaubert", "Geangelt", "Gesammelt", + "Gemahlen", "Abgebaut", "Sondiert", "Aus Taschendiebstahl", "Geborgen", "Gehäutet", + "In-Game-Store" + ), + '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", + 33 => "Gossensprache", 35 => "Draeneiisch", 36 => "Zombie", 37 => "Gnomenbinär", 38 => "Goblinbinär" + ), + 'gl' => [null, "Erhebliche", "Geringe"], + '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"], + '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"], + 'rep' => ["Hasserfüllt", "Feindselig", "Unfreundlich", "Neutral", "Freundlich", "Wohlwollend", "Respektvoll", "Ehrfürchtig"], + 'st' => array( + "Vorgabe", "Katzengestalt", "Baum des Lebens", "Reisegestalt", "Wassergestalt", "Bärengestalt", + null, null, "Terrorbärengestalt", null, null, null, + null, "Schattentanz", null, null, "Geisterwolf", "Kampfhaltung", + "Verteidigungshaltung", "Berserkerhaltung", null, null, "Metamorphosis", null, + null, null, null, "Schnelle Fluggestalt", "Schattengestalt", "Fluggestalt", + "Verstohlenheit", "Mondkingestalt", "Geist der Erlösung" + ), + 'me' => array( + null, "Bezaubert", "Desorientiert", "Entwaffnet", "Abgelenkt", "Flüchtend", + "Ergriffen", "Unbeweglich", "Befriedet", "Schweigend", "Schlafend", "Verlangsamt", + "Betäubt", "Eingefroren", "Handlungsunfähig", "Blutend", "Heilung", "Verwandelt", + "Verbannt", "Abgeschirmt", "Gefesselt", "Reitend", "Verführt", "Vertrieben", + "Entsetzt", "Unverwundbar", "Unterbrochen", "Benommen", "Entdeckung", "Unverwundbar", + "Kopfnuss", "Wütend" + ), + 'ct' => array( + "Nicht kategorisiert", "Wildtier", "Drachkin", "Dämon", "Elementar", "Riese", + "Untoter", "Humanoid", "Tier", "Mechanisch", "Nicht spezifiziert", "Totem", + "Haustier", "Gaswolke" + ), + 'fa' => array( + 1 => "Wolf", 2 => "Katze", 3 => "Spinne", 4 => "Bär", 5 => "Eber", 6 => "Krokilisk", + 7 => "Aasvogel", 8 => "Krebs", 9 => "Gorilla", 11 => "Raptor", 12 => "Weitschreiter", 20 => "Skorpid", + 21 => "Schildkröte", 24 => "Fledermaus", 25 => "Hyäne", 26 => "Raubvogel", 27 => "Windnatter", 30 => "Drachenfalke", + 31 => "Felshetzer", 32 => "Sphärenjäger", 33 => "Sporensegler", 34 => "Netherrochen", 35 => "Schlange", 37 => "Motte", + 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" + ), + ), + 'account' => array( + 'title' => "Aowow-Konto", + 'email' => "E-Mail-Adresse", + 'continue' => "Fortsetzen", + 'groups' => array( + -1 => "Keine", "Tester", "Administrator", "Editor", "Moderator", "Bürokrat", + "Entwickler", "VIP", "Blogger", "Premium", "Übersetzer", "Handelsvertreter", + "Screenshot-Verwalter", "Video-Verwalter" + ), + // signIn + 'doSignIn' => "Mit Eurem AoWoW-Konto anmelden", + 'signIn' => "Anmelden", + 'user' => "Benutzername", + 'pass' => "Kennwort", + 'rememberMe' => "Angemeldet bleiben", + 'forgot' => "Vergessen", + 'forgotUser' => "Benutzername", + 'forgotPass' => "Kennwort", + 'accCreate' => 'Noch kein Konto? Jetzt eins erstellen!', + + // recovery + 'recoverUser' => "Benutzernamenanfrage", + 'recoverPass' => "Kennwort zurücksetzen: Schritt %s von 2", + 'newPass' => "Neues Kennwort", + + // creation + 'register' => "Registrierung: Schritt %s von 2", + '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', + + // bans + 'accBanned' => "Dieses Konto wurde geschlossen", + 'bannedBy' => "Gebannt durch", + '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.", + '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 + ), + 'user' => array( + 'notFound' => "Der Benutzer \"%s\" wurde nicht gefunden!", + 'removed' => "(Entfernt)", + '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."] + ), + 'gameObject' => array( + '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 => ""], + 'unkPosition' => "Der Standort dieses Objekts ist nicht bekannt.", + 'key' => "Schlüssel", + 'focus' => "Zauberfokus", + 'focusDesc' => "Zauber, die diesen Fokus benötigen, können an diesem Objekt gewirkt werden.", + 'trap' => "Falle", + 'triggeredBy' => "Ausgelöst durch", + 'capturePoint' => "Eroberungspunkt", + 'foundIn' => "Dieses Objekt befindet sich in", + 'restock' => "Wird alle %s wieder aufgefüllt." + ), + 'npc' => array( + 'notFound' => "Dieser NPC existiert nicht.", + 'classification'=> "Einstufung", + 'petFamily' => "Tierart", + 'react' => "Reaktion", + 'worth' => "Wert", + 'unkPosition' => "Der Aufenthaltsort dieses NPCs ist nicht bekannt.", + 'difficultyPH' => "Dieser NPC ist ein Platzhalter für einen anderen Modus von", + 'seat' => "Sitz", + 'accessory' => "Zusätze", + 'accessoryFor' => "Dieser NPC ist Zusatz für Fahrzeug", + 'quotes' => "Zitate", + '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", + 'foundIn' => "Dieser NPC befindet sich in", + 'tameable' => "Zähmbar (%s)", + 'waypoint' => "Wegpunkt", + 'wait' => "Wartezeit", + 'respawnIn' => "Wiedereinstieg in", + '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"] + ), + 'cat' => array( + "Nicht kategorisiert", "Wildtiere", "Drachkin", "Dämonen", "Elementare", "Riesen", "Untote", "Humanoide", + "Tiere", "Mechanisch", "Nicht spezifiziert", "Totems", "Haustiere", "Gaswolken" + ) + ), + 'event' => array( + 'notFound' => "Dieses Weltereignis existiert nicht.", + 'start' => "Anfang", + 'end' => "Ende", + 'interval' => "Intervall", + 'inProgress' => "Ereignis findet gerade statt", + 'category' => ["Nicht kategorisiert", "Feiertage", "Wiederkehrend", "Spieler vs. Spieler"] + ), + 'achievement' => array( + '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", + 'rfAvailable' => "Verfügbar auf Realm:", + '_transfer' => 'Dieser Erfolg wird mit %s vertauscht, wenn Ihr zur %s wechselt.', + ), + 'chrClass' => array( + 'notFound' => "Diese Klasse existiert nicht." + ), + 'race' => array( + 'notFound' => "Dieses Volk existiert nicht.", + 'racialLeader' => "Volksanführer", + 'startZone' => "Startgebiet", + ), + 'maps' => array( + 'maps' => "Karten", + 'linkToThisMap' => "Link zu dieser Karte", + 'clear' => "Zurücksetzen", + 'EasternKingdoms' => "Östliche Königreiche", + 'Kalimdor' => "Kalimdor", + 'Outland' => "Scherbenwelt", + 'Northrend' => "Nordend", + 'Instances' => "Instanzen", + 'Dungeons' => "Dungeons", + 'Raids' => "Schlachtzüge", + 'More' => "Weitere", + 'Battlegrounds' => "Schlachtfelder", + 'Miscellaneous' => "Diverse", + 'Azeroth' => "Azeroth", + 'CosmicMap' => "Kosmische Karte", + ), + 'zone' => array( + 'notFound' => "Dieses Gebiet existiert nicht.", + 'attunement' => ["Einstimmung", "Heroische Einstimmung"], + 'key' => ["Schlüssel", "Heroischer Schlüssel"], + 'location' => "Ort", + 'raidFaction' => "Schlachtzugsfraktion", + '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", + 'hcAvailable' => "Heroischer Modus verfügbar (%d)", + 'numPlayers' => "Anzahl an Spielern", + 'noMap' => "Für dieses Gebiet steht keine Karte zur Verfügung.", + 'instanceTypes' => ["Zone", "Durchgang", "Dungeon", "Schlachtzug", "Battleground", "Dungeon", "Arena", "Schlachtzug", "Schlachtzug"], + 'territories' => ["Allianz", "Horde", "Umkämpft", "Sicheres Gebiet", "PvP", "Welt-PvP"], + 'cat' => array( + "Östliche Königreiche", "Kalimdor", "Dungeons", "Schlachtzüge", "Unbenutzt", null, + "Schlachtfelder", null, "Scherbenwelt", "Arenen", "Nordend" + ) + ), + 'quest' => array( + '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", + '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", + 'providedItem' => "Bereitgestellter Gegenstand", + 'completion' => "Abschluss", + 'description' => "Beschreibung", + 'playerSlain' => "Spieler getötet", + 'profession' => "Beruf", + 'timer' => "Zeitbegrenzung", + 'loremaster' => "Meister der Lehren", + 'suggestedPl' => "Empfohlene Spielerzahl", + 'keepsPvpFlag' => "Hält Euch im PvP", + 'daily' => 'Täglich', + 'weekly' => "Wöchentlich", + 'monthly' => "Monatlich", + 'sharable' => "Teilbar", + 'notSharable' => "Nicht teilbar", + 'repeatable' => "Wiederholbar", + 'reqQ' => "Benötigt", + 'reqQDesc' => "Um diese Quest zu erhalten, müsst ihr alle der nachfolgenden Quests abschließen", + 'reqOneQ' => "Benötigt eins von", + 'reqOneQDesc' => "Um diese Quest zu erhalten, müsst ihr eines der nachfolgenden Quests abschließen", + 'opensQ' => "Öffnet Quests", + 'opensQDesc' => "Es ist notwendig, diese Quest zu beenden, um die nachfolgenden Quests zu erhalten", + 'closesQ' => "Schließt Quests", + 'closesQDesc' => "Wenn ihr diese Quest beendet, sind die nachfolgenden Quests nicht mehr verfügbar", + 'enablesQ' => "Aktiviert", + 'enablesQDesc' => "Wenn diese Quest aktiv ist, sind die nachfolgenden Quests ebenfalls verfügbar", + '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", + 'bonusTalents' => "Talentpunkte", + 'spellDisplayed'=> ' (%s wird angezeigt)', + '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 => 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" + ), + 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" + ), + 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" + ), + 4 => array( "Klassen", + -263 => "Druide", -61 => "Hexenmeister", -261 => "Jäger", -81 => "Krieger", -161 => "Magier", + -141 => "Paladin", -262 => "Priester", -82 => "Schamane", -162 => "Schurke", -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" + ), + 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" + ), + 7 => array( "Verschiedenes", + -365 => "Krieg von Ahn'Qiraj", -1010 => "Dungeonfinder", -1 => "Episch", -344 => "Legendär", -367 => "Ruf", + -368 => "Invasion der Geißel", -241 => "Turnier" + ), + -2 => "Nicht kategorisiert" + ) + ), + 'title' => array( + 'notFound' => "Dieser Titel existiert nicht.", + '_transfer' => 'Dieser Titel wird mit %s vertauscht, wenn Ihr zur %s wechselt.', + 'cat' => array( + "Allgemein", "Spieler gegen Spieler", "Ruf", "Dungeon & Schlachtzug", "Quests", "Berufe", "Weltereignisse" + ) + ), + 'skill' => array( + 'notFound' => "Diese Fertigkeit existiert nicht.", + 'cat' => array( + -6 => "Haustiere", -5 => "Reittiere", -4 => "Völkerfertigkeiten", 5 => "Attribute", 6 => "Waffenfertigkeiten", 7 => "Klassenfertigkeiten", 8 => "Rüstungssachverstand", + 9 => "Nebenberufe", 10 => "Sprachen", 11 => "Berufe" + ) + ), + 'currency' => array( + 'notFound' => "Diese Währung existiert nicht.", + '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" + ) + ), + 'pet' => array( + 'notFound' => "Diese Tierart existiert nicht.", + 'exotic' => "Exotisch", + 'cat' => ["Wildheit", "Hartnäckigkeit", "Gerissenheit"] + ), + 'faction' => array( + '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", + 'customRewRate' => "Abweichende Belohnungsraten", + '_transfer' => 'Die Reputation mit dieser Fraktion wird mit dem für %s vertauscht, wenn Ihr zur %s wechselt.', + 'cat' => array( + 1118 => ["Classic", 469 => "Allianz", 169 => "Dampfdruckkartell", 67 => "Horde", 891 => "Streitkräfte der Allianz", 892 => "Streitkräfte der Horde"], + 980 => ["The Burning Crusade", 936 => "Shattrath"], + 1097 => ["Wrath of the Lich King", 1052 => "Expedition der Horde", 1117 => "Sholazarbecken", 1037 => "Vorposten der Allianz"], + 0 => "Sonstige" + ) + ), + 'itemset' => array( + '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", + '_unavailable' => "Dieses Ausrüstungsset ist nicht für Spieler verfügbar.", + '_tag' => "Tag", + 'summary' => "Zusammenfassung", + 'notes' => array( + null, "Dungeon-Set 1", "Dungeon-Set 2", "Tier 1 Raid-Set", + "Tier 2 Raid-Set", "Tier 3 Raid-Set", "Level 60 PvP-Set (Rar)", "Level 60 PvP-Set (Rar, alt)", + "Level 60 PvP-Set (Episch)", "Set der Ruinen von Ahn'Qiraj", "Set des Tempels von Ahn'Qiraj", "Set von Zul'Gurub", + "Tier 4 Raid-Set", "Tier 5 Raid-Set", "Dungeon-Set 3", "Set des Arathibeckens", + "Level 70 PvP-Set (Rar)", "Arena-Set Saison 1", "Tier 6 Raid-Set", "Arena-Set Saison 2", + "Arena-Set Saison 3", "Level 70 PvP-Set 2 (Rar)", "Arena-Set Saison 4", "Tier 7 Raid-Set", + "Arena-Set Saison 5", "Tier 8 Raid-Set", "Arena-Set Saison 6", "Tier 9 Raid-Set", + "Arena-Set Saison 7", "Tier 10 Raid-Set", "Arena-Set Saison 8" + ), + 'types' => array( + null, "Stoff", "Leder", "Schwere Rüstung", "Platte", "Dolch", "Ring", + "Faustwaffe", "Einhandaxt", "Einhandstreitkolben", "Einhandschwert", "Schmuck", "Amulett" + ) + ), + 'spell' => array( + 'notFound' => "Dieser Zauber existiert nicht.", + '_spellDetails' => "Zauberdetails", + '_cost' => "Kosten", + '_range' => "Reichweite", + '_castTime' => "Zauberzeit", + '_cooldown' => "Abklingzeit", + '_distUnit' => "Meter", + '_forms' => "Gestalten", + '_aura' => "Aura", + '_effect' => "Effekt", + '_none' => "Nichts", + '_gcd' => "GCD", + '_globCD' => "Globale Abklingzeit", + '_gcdCategory' => "GCD-Kategorie", + '_value' => "Wert", + '_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.', + 'discovered' => "Durch Geistesblitz erlernt", + 'ppm' => "%s Auslösungen pro Minute", + 'procChance' => "Procchance", + 'starter' => "Basiszauber", + 'trainingCost' => "Trainingskosten", + 'remaining' => "Noch %s", + 'untilCanceled' => "bis Abbruch", + 'castIn' => "Wirken in %s Sek.", + 'instantPhys' => "Sofort", + 'instantMagic' => "Spontanzauber", + 'channeled' => "Kanalisiert", + 'range' => "%s Meter Reichweite", + 'meleeRange' => "Nahkampfreichweite", + 'unlimRange' => "Unbegrenzte Reichweite", + 'reagents' => "Reagenzien", + 'tools' => "Extras", + 'home' => "<Gasthaus>", + 'pctCostOf' => "vom Grund%s", + 'costPerSec' => ", plus %s pro Sekunde", + 'costPerLevel' => ", plus %s pro Stufe", + '_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" + ), + 'powerRunes' => ["Frost", "Unheilig", "Blut", "Tod"], + 'powerTypes' => array( + // conventional + -2 => "Gesundheit", 0 => "Mana", 1 => "Wut", 2 => "Fokus", 3 => "Energie", 4 => "Zufriedenheit", + 5 => "Runen", 6 => "Runenmacht", + // powerDisplay + -1 => "Munition", -41 => "Pyrit", -61 => "Dampfdruck", -101 => "Hitze", -121 => "Schlamm", -141 => "Blutmacht", + -142 => "Wrath" + ), + 'relItems' => array( + 'base' => "%s im Zusammenhang mit %s anzeigen", + 'link' => " oder ", + 'recipes' => 'Rezeptgegenstände', + 'crafted' => 'Hergestellte Gegenstände' + ), + 'cat' => array( + 7 => "Klassenfertigkeiten", + -13 => "Glyphen", + -11 => ["Sachverstand", 8 => "Rüstung", 6 => "Waffen", 10 => "Sprachen"], + -4 => "Völkerfertigkeiten", + -2 => "Talente", + -6 => "Haustiere", + -5 => "Reittiere", + -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", + 214 => "Krebs", 212 => "Krokilisk", 775 => "Motte", 764 => "Netherrochen", 217 => "Raptor", 655 => "Raubvogel", 786 => "Rhinozeros", + 251 => "Schildkröte", 780 => "Schimäre", 768 => "Schlange", 783 => "Silithid", 236 => "Skorpid", 766 => "Sphärenjäger", 203 => "Spinne", + 765 => "Sporensegler", 781 => "Teufelssaurier", 218 => "Weitschreiter", 785 => "Wespe", 656 => "Windnatter", 208 => "Wolf", 784 => "Wurm", + 204 => "Leerwandler", 205 => "Sukkubus", 189 => "Teufelsjäger", 761 => "Teufelswache", 188 => "Wichtel", + ), + -7 => ["Begleitertalente", 410 => "Gerissenheit", 411 => "Wildheit", 409 => "Hartnäckigkeit"], + 11 => array( + "Berufe", + 171 => "Alchemie", + 164 => ["Schmiedekunst", 9788 => "Rüstungsschmied", 9787 => "Waffenschmied", 17041 => "Axtschmiedemeister", 17040 => "Hammerschmiedemeister", 17039 => "Schwertschmiedemeister"], + 333 => "Verzauberkunst", + 202 => ["Ingenieurskunst", 20219 => "Gnomeningenieurskunst", 20222 => "Gobliningenieurskunst"], + 182 => "Kräuterkunde", + 773 => "Inschriftenkunde", + 755 => "Juwelenschleifen", + 165 => ["Lederverarbeitung", 10656 => "Drachenschuppenlederverarbeitung", 10658 => "Elementarlederverarbeitung", 10660 => "Stammeslederverarbeitung"], + 186 => "Bergbau", + 393 => "Kürschnerei", + 197 => ["Schneiderei", 26798 => "Mondstoffschneiderei", 26801 => "Schattenstoffschneiderei", 26797 => "Zauberfeuerschneiderei"], + ), + 9 => ["Nebenberufe", 185 => "Kochkunst", 129 => "Erste Hilfe", 356 => "Angeln", 762 => "Reiten"], + -8 => "NPC-Fähigkeiten", + -9 => "GM-Fähigkeiten", + 0 => "Nicht kategorisiert" + ), + 'armorSubClass' => array( + "Sonstiges", "Stoffrüstung", "Lederrüstung", "Schwere Rüstung", "Plattenrüstung", + null, "Schilde", "Buchbände", "Götzen", "Totems", + "Siegel" + ), + 'weaponSubClass' => array( + 15 => "Dolche", 0 => "Einhandäxte", 7 => "Einhandschwerter", 4 => "Einhandstreitkolben", 13 => "Faustwaffen", + 6 => "Stangenwaffen", 10 => "Stäbe", 1 => "Zweihandäxte", 8 => "Zweihandschwerter", 5 => "Zweihandstreitkolben", + 18 => "Armbrüste", 2 => "Bögen", 3 => "Schusswaffen", 16 => "Wurfwaffen", 19 => "Zauberstäbe", + 20 => "Angelruten", 14 => "Diverse" + ), + 'subClassMasks' => array( + 0x02A5F3 => "Nahkampfwaffe", 0x0060 => "Schild", 0x04000C => "Distanzwaffe", 0xA091 => "Einhandnahkampfwaffe" + ), + 'traitShort' => array( + 'atkpwr' => "Angr", 'rgdatkpwr' => "DAngr", 'splpwr' => "ZMacht", 'arcsplpwr' => "ArkM", 'firsplpwr' => "FeuM", + 'frosplpwr' => "FroM", 'holsplpwr' => "HeiM", 'natsplpwr' => "NatM", 'shasplpwr' => "SchM", 'splheal' => "Heil" + ), + 'spellModOp' => array( + "Schaden", "Dauer", "Bedrohung", "Effekt 1", "Aufladungen", + "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", + "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" + ), + 'combatRating' => array( + "Waffenfertigkeit", "Verteidigungsfertigkeit", "Ausweichen", "Parrieren", "Blocken", + "Nahkampftrefferchance", "Fernkampftrefferchance", "Zaubertrefferchance", "kritische Nahkampftrefferchance", "kritische Fernkampftrefferchance", + "kritische Zaubertrefferchance", "erhaltene Nahkampftreffer", "erhaltene Fernkampftreffer", "erhaltene Zaubertreffer", "erhaltene kritische Nahkampftreffer", + "erhaltene kritische Fernkampftreffer", "erhaltene kritische Zaubertreffer", "Nahkampftempo", "Fernkampftempo", "Zaubertempo", + "Waffenfertigkeit Haupthand", "Waffenfertigkeit Nebenhand", "Waffenfertigkeit Fernkampf", "Waffenkunde", "Rüstungsdurchschlag" + ), + 'lockType' => array( + null, "Schlossknacken", "Kräuterkunde", "Bergbau", "Falle entschärfen", + "Öffnen", "Schatz (DND)", "Verkalkte Elfenedelsteine (DND)", "Schließen", "Falle scharf machen", + "Schnell öffnen", "Schnell schließen", "Offenes Tüfteln", "Offenes Knien", "Offenes Angreifen", + "Gahz'ridian (DND)", "Schlagen", "PvP öffnen", "PvP schließen", "Angeln", + "Inschriftenkunde", "Vom Fahrzeug öffnen" + ), + 'stealthType' => ["Allgemein", "Falle"], + 'invisibilityType' => ["Allgemein", 3 => "Falle", 6 => "Trunkenheit"] + ), + 'item' => array( + 'notFound' => "Dieser Gegenstand existiert nicht .", + 'armor' => "%s Rüstung", + 'block' => "%s Blocken", + 'charges' => "Aufladungen", + 'locked' => "Verschlossen", + 'ratingString' => "%s @ L%s", + 'heroic' => "Heroisch", + 'unique' => "Einzigartig", + 'uniqueEquipped'=> "Einzigartig anlegbar", + 'startQuest' => "Dieser Gegenstand startet eine Quest", + 'bagSlotString' => "%d Platz %s", + 'dps' => "Schaden pro Sekunde", + 'dps2' => "Schaden pro Sekunde", + 'addsDps' => "Adds", + 'fap' => "Angriffskraft in Tiergestalt", + 'durability' => "Haltbarkeit", + 'realTime' => "Realzeit", + 'conjured' => "Herbeigezauberter Gegenstand", + 'damagePhys' => "%s Schaden", + 'damageMagic' => "%s %sschaden", + 'speed' => "Tempo", + 'sellPrice' => "Verkaufspreis", + 'itemLevel' => "Gegenstandsstufe", + 'randEnchant' => "<Zufällige Verzauberung>", + 'readClick' => "<Zum Lesen rechtsklicken>", + 'openClick' => "<Zum Öffnen rechtsklicken>", + 'set' => "Set", + 'partyLoot' => "Gruppenloot", + 'smartLoot' => "Intelligente Beuteverteilung", + 'indestructible'=> "Kann nicht zerstört werden", + 'deprecated' => "Nicht benutzt", + 'useInShape' => "Benutzbar in Gestaltwandlung", + 'useInArena' => "Benutzbar in Arenen", + 'refundable' => "Rückzahlbar", + 'noNeedRoll' => "Kann nicht für Bedarf werfen", + 'atKeyring' => "Passt in den Schlüsselbund", + 'worth' => "Wert", + 'consumable' => "Verbrauchbar", + 'nonConsumable' => "Nicht verbrauchbar", + 'accountWide' => "Accountweit", + 'millable' => "Mahlbar", + 'noEquipCD' => "Keine Anlegabklingzeit", + 'prospectable' => "Sondierbar", + 'disenchantable'=> "Kann entzaubert werden", + 'cantDisenchant'=> "Kann nicht entzaubert werden", + 'repairCost' => "Reparaturkosten", + 'tool' => "Werkzeug", + 'cost' => "Preis", + 'content' => "Inhalt", + '_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", + 'buyout' => "Sofortkaufpreis", + 'each' => "Stück", + 'tabOther' => "Anderes", + 'gems' => "Edelsteine", + 'socketBonus' => "Sockelbonus", + 'socket' => array( + "Metasockel", "Roter Sockel", "Gelber Sockel", "Blauer Sockel", -1 => "Prismatischer Sockel" + ), + 'gemColors' => array( // *_GEM + "Meta", "Rot", "Gelb", "Blau" + ), + 'gemConditions' => array( // ENCHANT_CONDITION_* in GlobalStrings.lua; 2 not in use (use as PH) + 2 => ["weniger als %d Edelstein der Kategorie %s", "weniger als %d Edelsteine der Kategorie %s"], + 3 => "mehr Edelsteine der Kategorie %s als Edelsteine der Kategorie %s", + 5 => ["mindestens %d Edelstein der Kategorie %s", "mindestens %d Edelsteine der Kategorie %s"] + ), + 'reqRating' => array( // ITEM_REQ_ARENA_RATING* + "Benötigt eine persönliche Arenawertung und Teamwertung von %d.", + "Benötigt eine persönliche und eine Teamwertung von %d
in 3v3- oder 5v5-Turnieren", + "Benötigt eine persönliche und eine Teamwertung von %d
in 5v5-Turnieren" + ), + 'quality' => array( + "Schlecht", "Verbreitet", "Selten", "Rar", + "Episch", "Legendär", "Artefakt", "Erbstücke", + ), + 'trigger' => array( + "Benutzen: ", "Anlegen: ", "Chance bei Treffer: ", null, null, + null, null + ), + 'bonding' => array( + "Accountgebunden", "Wird beim Aufheben gebunden", "Wird beim Anlegen gebunden", + "Wird bei Benutzung gebunden", "Questgegenstand", "Questgegenstand" + ), + 'bagFamily' => array( + "Tasche", "Köcher", "Munitionsbeutel", "Seelentasche", "Lederertasche", + "Schreibertasche", "Kräutertasche", "Verzauberertasche", "Ingenieurstasche", null, /*Schlüssel*/ + "Edelsteintasche", "Bergbautasche" + ), + 'inventoryType' => array( + null, "Kopf", "Hals", "Schulter", "Hemd", + "Brust", "Taille", "Beine", "Füße", "Handgelenke", + "Hände", "Finger", "Schmuck", "Einhändig", "Schildhand", /*Schild*/ + "Distanz", "Rücken", "Zweihändig", "Tasche", "Wappenrock", + null, /*Robe*/ "Waffenhand", "Schildhand", "In der Schildhand geführt", "Projektil", + "Wurfwaffe", null, /*Ranged2*/ "Köcher", "Relikt" + ), + 'armorSubClass' => array( + "Sonstiges", "Stoff", "Leder", "Schwere Rüstung", "Platte", + null, "Schild", "Buchband", "Götze", "Totem", + "Sigel" + ), + 'weaponSubClass' => array( + "Axt", "Axt", "Bogen", "Schusswaffe", "Streitkolben", + "Streitkolben", "Stangenwaffe", "Schwert", "Schwert", null, + "Stab", null, null, "Faustwaffe", "Diverse", + "Dolche", "Wurfwaffe", null, "Armbrust", "Zauberstab", + "Angelrute" + ), + 'projectileSubClass' => array( + null, null, "Pfeil", "Kugel", null + ), + 'elixirType' => [null, "Kampf", "Wächter"], + 'cat' => array( + 2 => "Waffen", // self::$spell['weaponSubClass'] + 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", + -7 => "Wappenröcke", -3 => "Amulette", -2 => "Ringe", -4 => "Schmuckstücke", 0 => "Verschiedenes (Rüstung)", + )), + 1 => array("Behälter", array( + 0 => "Taschen", 3 => "Verzauberertaschen", 4 => "Ingenieurstaschen", 5 => "Edelsteintaschen", 2 => "Kräutertaschen", 8 => "Schreibertaschen", + 7 => "Lederertaschen", 6 => "Bergbautaschen", 1 => "Seelentaschen" + )), + 0 => array("Verbrauchbar", array( + -3 => "Gegenstandsverzauberungen (Temporäre)", 6 => "Gegenstandsverzauberungen (Dauerhafte)", 2 => ["Elixire", [1 => "Kampfelixire", 2 => "Wächterelixire"]], + 1 => "Tränke", 4 => "Schriftrollen", 7 => "Verbände", 0 => "Verbrauchbar", 3 => "Fläschchen", 5 => "Essen & Trinken", + 8 => "Andere (Verbrauchbar)" + )), + 16 => array("Glyphen", array( + 1 => "Kriegerglyphen", 2 => "Paladinglyphen", 3 => "Jägerglyphen", 4 => "Schurkenglyphen", 5 => "Priesterglyphen", 6 => "Todesritterglyphen", + 7 => "Schamanenglyphen", 8 => "Magierglyphen", 9 => "Hexenmeisterglyphen", 11 => "Druidenglyphen" + )), + 7 => array("Handwerkswaren", array( + 14 => "Rüstungsverzauberungen", 5 => "Stoff", 3 => "Geräte", 10 => "Elementar", 12 => "Verzauberkunst", 2 => "Sprengstoff", + 9 => "Kräuter", 4 => "Juwelenschleifen", 6 => "Leder", 13 => "Materialien", 8 => "Fleisch", 7 => "Metall & Stein", + 1 => "Teile", 15 => "Waffenverzauberungen", 11 => "Andere (Handwerkswaren)" + )), + 6 => ["Projektile", [ 2 => "Pfeile", 3 => "Kugeln" ]], + 11 => ["Köcher", [ 2 => "Köcher", 3 => "Munitionsbeutel" ]], + 9 => array("Rezepte", array( + 0 => "Bücher", 6 => "Alchemierezepte", 4 => "Schmiedekunstpläne", 5 => "Kochrezepte", 8 => "Verzauberkunstformeln", 3 => "Ingenieurschemata", + 7 => "Erste Hilfe-Bücher", 9 => "Angelbücher", 11 => "Inschriftenkundetechniken",10 => "Juwelenschleifen-Vorlagen",1 => "Lederverarbeitungsmuster",12 => "Bergbauleitfäden", + 2 => "Schneidereimuster" + )), + 3 => array("Edelsteine", array( + 6 => "Meta-Edelsteine", 0 => "Rote Edelsteine", 1 => "Blaue Edelsteine", 2 => "Gelbe Edelsteine", 3 => "Violette Edelsteine", 4 => "Grüne Edelsteine", + 5 => "Orange Edelsteine", 8 => "Prismatische Edelsteine", 7 => "Einfache Edelsteine" + )), + 15 => array("Verschiedenes", array( + -2 => "Rüstungsmarken", 3 => "Feiertag", 0 => "Plunder", 1 => "Reagenzien", 5 => "Reittiere", -7 => "Flugtiere", + 2 => "Haustiere", 4 => "Andere (Verschiedenes)" + )), + 10 => "Währung", + 12 => "Quest", + 13 => "Schlüssel", + ), + 'statType' => array( + "Erhöht Euer Mana um %d.", + "Erhöht Eure Gesundheit um %d.", + null, + "Beweglichkeit", + "Stärke", + "Intelligenz", + "Willenskraft", + "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 Zaubertrefferwertung um %d.", + "Erhöht kritische Nahkampftrefferwertung um %d.", + "Erhöht kritische Fernkampftrefferwertung um %d.", + "Erhöht kritische Zaubertrefferwertung um %d.", + "Erhöht Vermeidungswertung für Nahkampftreffer um +3.", + "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 Zaubertempowertung um %d.", + "Erhöht Eure Trefferwertung um %d.", + "Erhöht Eure 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 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.", + "Stellt alle 5 Sek. %d Mana wieder her.", + "Erhöht Euren Rüstungsdurchschlagwert 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)", + ) + ) +); + +?> diff --git a/localization/locale_enus.php b/localization/locale_enus.php index 6735239a..9eb1ff48 100644 --- a/localization/locale_enus.php +++ b/localization/locale_enus.php @@ -1,2451 +1,1056 @@ - array( - 'sg' => ["year", "month", "week", "day", "hour", "minute", "second", "millisecond"], - 'pl' => ["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", - 'signIn' => "Log in / Register", - 'jsError' => "Please make sure you have javascript enabled.", - 'language' => "Language", - 'feedback' => "Feedback", - '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", - '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: ", - 'related' => "Related", - 'contribute' => "Contribute", - // 'replyingTo' => "The answer to a comment from", - 'submit' => "Submit", - 'save' => 'Save', - 'cancel' => "Cancel", - 'rewards' => "Rewards", - 'gains' => "Gains", - // 'login' => "Login", - 'forum' => "Forum", - 'siteRep' => "Reputation: ", - 'yourRepHistory'=> "Your Reputation History", - 'aboutUs' => "About us & contact", - 'and' => " and ", - 'or' => " or ", - 'back' => "Back", - 'reputationTip' => "Reputation points", - '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: ", - 'allFilter' => "All filters", - 'oneFilter' => "At least one", - 'applyFilter' => "Apply filter", - 'resetForm' => "Reset Form", - 'refineSearch' => 'Tip: Refine your search by browsing a subcategory.', - 'clear' => "clear", - 'exactMatch' => "Exact match", - '_reqLevel' => "Required level: ", - - // infobox - 'unavailable' => "Not available to players", // alternative wording found: "No longer available to players" ... aw screw it <_< - 'disabled' => "Disabled", - 'disabledHint' => "Cannot be attained or completed", - 'serverside' => "Serverside", - 'serversideHint'=> "These informations are not in the Client and have been provided by sniffing and/or guessing.", - - // red buttons - 'links' => "Links", - 'compare' => "Compare", - 'view3D' => "View in 3D", - 'findUpgrades' => "Find upgrades…", - 'report' => "Report", - 'writeGuide' => "Write New Guide", - 'edit' => "Edit", - 'changelog' => 'Changelog', - - // misc Tools - 'errPageTitle' => "Page not found", - 'nfPageTitle' => "Error", - 'subscribe' => "Subscribe", - 'mostComments' => ["Yesterday", "Past %d Days"], - 'utilities' => array( - "Latest Additions", "Latest Articles", "Latest Comments", "Latest Screenshots", null, - "Unrated Comments", 11 => "Latest Videos", 12 => "Most Comments", 13 => "Missing Screenshots" - ), - - // article & infobox - 'langOnly' => "This page is only available in %s.", - - // calculators - '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: ", - '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:", - - // search - 'search' => "Search", - 'foundResult' => "Search Results for", - 'noResult' => "No Results for", - 'tryAgain' => "Please try some different keywords or check your spelling.", - 'ignoredTerms' => "The following words were ignored in your search: %s", - - // formating - 'colon' => ': ', - 'dateFmtShort' => "Y/m/d", - '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.", - 'intError2' => "An internal error has occurred. (%s)", - '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", - 'whats-new' => "What's New", - 'searchbox' => "Search Box", - 'tooltips' => "Tooltips", - 'faq' => "Frequently Asked Questions", - 'aboutus' => "What is AoWoW?", - 'searchplugins' => "Search Plugins", - 'privileges' => "Privileges", - 'top-users' => "Top Users", - 'help' => array( - 'commenting-and-you' => "Commenting and You", 'modelviewer' => "Model Viewer", 'screenshots-tips-tricks' => "Screenshots: Tips & Tricks", - 'stat-weighting' => "Stat Weighting", 'talent-calculator' => "Talent Calculator", 'item-comparison' => "Item Comparison", - 'profiler' => "Profiler", 'markup-guide' => "Markup Guide" - ) - ) - ), - '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", - 'viewCharacter' => "View Character", - '_cpHint' => "The Character Profiler lets you edit your character, find gear upgrades, check your gearscore and more!", - '_cpHelp' => "To get started, just follow the steps below. If you'd like more information, check out our extensive help page.", - '_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", - '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." - ), - '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( - 'submission' => "Screenshot Submission", - 'selectAll' => "Select all", - 'cropHint' => "You may crop your screenshot and enter a caption.", - 'displayOn' => "Displayed on:[br]%s - [%s=%d]", - 'caption' => "Caption", - 'charLimit' => "Optional, up to 200 characters", - 'thanks' => array( - 'contrib' => "Thanks a lot for your contribution!", - 'goBack' => 'Click here to go back to the page you came from.', - 'note' => "Note: Your screenshot will need to be approved before appearing on the site. This can take up to 72 hours." - ), - 'error' => array( - 'unkFormat' => "Unknown image format.", - '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( - // type strings - 'npc' => "NPC", - 'npcs' => "NPCs", - 'object' => "object", - 'objects' => "Objects", - 'item' => "item", - 'items' => "Items", - 'itemset' => "item Set", - 'itemsets' => "Item Sets", - '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: ", - 'school' => "School", - 'type' => "Type: ", - 'valueDelim' => " to ", - '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: ", - '_roles' => ["Healer", "Melee DPS", "Ranged DPS", "Tank"], - - 'phases' => "Phases", - '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( //
' + 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 deleted file mode 100644 index 20822e62..00000000 --- a/setup/tools/filegen/templates/global.js/guide.js +++ /dev/null @@ -1,368 +0,0 @@ -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 = $("
diff --git a/template/localized/delete-account_0.tpl.php b/template/localized/delete-account_0.tpl.php deleted file mode 100644 index bfef2a6f..00000000 --- a/template/localized/delete-account_0.tpl.php +++ /dev/null @@ -1,21 +0,0 @@ - - - diff --git a/template/localized/delete-account_2.tpl.php b/template/localized/delete-account_2.tpl.php deleted file mode 100644 index db677dc0..00000000 --- a/template/localized/delete-account_2.tpl.php +++ /dev/null @@ -1,21 +0,0 @@ - - - diff --git a/template/localized/delete-account_3.tpl.php b/template/localized/delete-account_3.tpl.php deleted file mode 100644 index a7585340..00000000 --- a/template/localized/delete-account_3.tpl.php +++ /dev/null @@ -1,21 +0,0 @@ - - - diff --git a/template/localized/delete-account_4.tpl.php b/template/localized/delete-account_4.tpl.php deleted file mode 100644 index d1c39332..00000000 --- a/template/localized/delete-account_4.tpl.php +++ /dev/null @@ -1,21 +0,0 @@ - - - diff --git a/template/localized/delete-account_6.tpl.php b/template/localized/delete-account_6.tpl.php deleted file mode 100644 index 8a1fd5c0..00000000 --- a/template/localized/delete-account_6.tpl.php +++ /dev/null @@ -1,21 +0,0 @@ - - - diff --git a/template/localized/delete-account_8.tpl.php b/template/localized/delete-account_8.tpl.php deleted file mode 100644 index 5864fdbd..00000000 --- a/template/localized/delete-account_8.tpl.php +++ /dev/null @@ -1,21 +0,0 @@ - - - diff --git a/template/localized/ssReminder_0.tpl.php b/template/localized/ssReminder_0.tpl.php deleted file mode 100644 index c94c311a..00000000 --- a/template/localized/ssReminder_0.tpl.php +++ /dev/null @@ -1,16 +0,0 @@ - - -

Reminder

- Your screenshot will not be approved if it doesn't correspond to the following guidelines. - -
    -
  • Be sure to turn up your graphics settings to make sure the shot looks good!
  • -
  • Model viewer shots are deleted on sight (this also includes character select, typically).
  • -
  • Don't include the onscreen text and the selection circle of a NPC.
  • -
  • Don't include any UI in the shot if you can help it.
  • -
  • Use the screenshot cropping tool to focus on the item as much as possible and reduce any unnecessary surrounding, as to better show off the item in question when reduced to the thumbnail that will be present on the item's page.
  • -
diff --git a/template/localized/ssReminder_2.tpl.php b/template/localized/ssReminder_2.tpl.php deleted file mode 100644 index be667edf..00000000 --- a/template/localized/ssReminder_2.tpl.php +++ /dev/null @@ -1,16 +0,0 @@ - - -

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 deleted file mode 100644 index 953d2eea..00000000 --- a/template/localized/ssReminder_3.tpl.php +++ /dev/null @@ -1,16 +0,0 @@ - - -

Hinweis

- Euer Screenshot wird nicht zugelassen werden, wenn er nicht unseren Richtlinien entspricht. - -
    -
  • Achtet darauf die Qualität Eurer Video Einstellungen zu erhöhen, damit Euer Bild gut aussieht!
  • -
  • Bilder des Modelviewers werden sofort gelöscht (das beinhaltet in der Regel auch Bilder der Charakterauswahl).
  • -
  • Vermeidet Bildschirmtext und die Zielmarkierung an NPCs.
  • -
  • Bezieht das Interface nicht ins Bild mit ein, wenn Ihr es vermeiden könnt.
  • -
  • Benutzt das Zuschneidewerkzeug um den Fokus so weit wie möglich auf das Objekt zu legen und unnötigen Rand zu reduzieren. Dies hilft das Objekt besser in der Vorschau zu erkennen, welche auf der entsprechenden Seite angezeigt wird.
  • -
diff --git a/template/localized/ssReminder_4.tpl.php b/template/localized/ssReminder_4.tpl.php deleted file mode 100644 index 9f7c5bd7..00000000 --- a/template/localized/ssReminder_4.tpl.php +++ /dev/null @@ -1,16 +0,0 @@ - - -

提醒

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

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 deleted file mode 100644 index 81b43f3c..00000000 --- a/template/localized/ssReminder_8.tpl.php +++ /dev/null @@ -1,16 +0,0 @@ - - -

Напоминание

- Ваш скриншот не будет одобрен, если он не соответствует следующим рекомендациям. - -
    -
  • Обязательно увеличьте свои настройки графики, чтобы скриншот выглядел хорошо!
  • -
  • Скриншоты из просмотрщика моделей удаляются сразу (это также касается выбора персонажа).
  • -
  • Не включайте текст на экране и круг выделения NPC.
  • -
  • Не включайте никакой интерфейс пользователя в скриншот, если это возможно.
  • -
  • Используйте инструмент обрезки скриншотов, чтобы максимально сфокусироваться на предмете и уменьшить ненужное окружение, чтобы лучше показать предмет на миниатюре, которая будет на странице предмета.
  • -
diff --git a/template/mails/activate-account_0.tpl b/template/mails/activate-account_0.tpl deleted file mode 100644 index ad662a74..00000000 --- a/template/mails/activate-account_0.tpl +++ /dev/null @@ -1,10 +0,0 @@ -# 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 deleted file mode 100644 index 7394b874..00000000 --- a/template/mails/activate-account_2.tpl +++ /dev/null @@ -1,10 +0,0 @@ -# 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 deleted file mode 100644 index 43be6bcf..00000000 --- a/template/mails/activate-account_3.tpl +++ /dev/null @@ -1,10 +0,0 @@ -# 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 deleted file mode 100644 index c0086607..00000000 --- a/template/mails/activate-account_4.tpl +++ /dev/null @@ -1,10 +0,0 @@ -# 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 deleted file mode 100644 index 0c669570..00000000 --- a/template/mails/activate-account_6.tpl +++ /dev/null @@ -1,10 +0,0 @@ -# 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 deleted file mode 100644 index 1a39e951..00000000 --- a/template/mails/activate-account_8.tpl +++ /dev/null @@ -1,10 +0,0 @@ -# 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 deleted file mode 100644 index 58a92f10..00000000 --- a/template/mails/change-email_0.tpl +++ /dev/null @@ -1,11 +0,0 @@ -# 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 deleted file mode 100644 index c9743068..00000000 --- a/template/mails/change-email_2.tpl +++ /dev/null @@ -1,11 +0,0 @@ -# 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 deleted file mode 100644 index 8abae027..00000000 --- a/template/mails/change-email_3.tpl +++ /dev/null @@ -1,11 +0,0 @@ -# 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 deleted file mode 100644 index 9fbd9efa..00000000 --- a/template/mails/change-email_4.tpl +++ /dev/null @@ -1,11 +0,0 @@ -# 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 deleted file mode 100644 index 16aabc7b..00000000 --- a/template/mails/change-email_6.tpl +++ /dev/null @@ -1,11 +0,0 @@ -# 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 deleted file mode 100644 index aaeeed88..00000000 --- a/template/mails/change-email_8.tpl +++ /dev/null @@ -1,12 +0,0 @@ -# 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 deleted file mode 100644 index 7bbef9db..00000000 --- a/template/mails/delete-account_0.tpl +++ /dev/null @@ -1,21 +0,0 @@ -# 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 deleted file mode 100644 index 7ccda5d2..00000000 --- a/template/mails/delete-account_2.tpl +++ /dev/null @@ -1,21 +0,0 @@ -# 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 deleted file mode 100644 index b8ce03a4..00000000 --- a/template/mails/delete-account_3.tpl +++ /dev/null @@ -1,21 +0,0 @@ -# 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 deleted file mode 100644 index a1738dfe..00000000 --- a/template/mails/delete-account_4.tpl +++ /dev/null @@ -1,21 +0,0 @@ -# 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 deleted file mode 100644 index 421351f6..00000000 --- a/template/mails/delete-account_6.tpl +++ /dev/null @@ -1,21 +0,0 @@ -# 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 deleted file mode 100644 index f9f916c4..00000000 --- a/template/mails/delete-account_8.tpl +++ /dev/null @@ -1,21 +0,0 @@ -# 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 deleted file mode 100644 index 9ab6d562..00000000 --- a/template/mails/recover-user_0.tpl +++ /dev/null @@ -1,7 +0,0 @@ -# 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 deleted file mode 100644 index 6fd398ad..00000000 --- a/template/mails/recover-user_2.tpl +++ /dev/null @@ -1,7 +0,0 @@ -# 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 deleted file mode 100644 index 83046bb5..00000000 --- a/template/mails/recover-user_3.tpl +++ /dev/null @@ -1,7 +0,0 @@ -# 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 deleted file mode 100644 index f571deae..00000000 --- a/template/mails/recover-user_4.tpl +++ /dev/null @@ -1,7 +0,0 @@ -# Legacy import -用户恢复 -请点击此链接登录。 - -HOST_URL?account=signin&key=%s - -如果您没有请求此邮件,请忽略它。 diff --git a/template/mails/recover-user_6.tpl b/template/mails/recover-user_6.tpl deleted file mode 100644 index bf5e173c..00000000 --- a/template/mails/recover-user_6.tpl +++ /dev/null @@ -1,7 +0,0 @@ -# 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 deleted file mode 100644 index 5847f9ac..00000000 --- a/template/mails/recover-user_8.tpl +++ /dev/null @@ -1,7 +0,0 @@ -# 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 deleted file mode 100644 index 7fe9762b..00000000 --- a/template/mails/reset-password_0.tpl +++ /dev/null @@ -1,7 +0,0 @@ -# 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 deleted file mode 100644 index 97a3cd75..00000000 --- a/template/mails/reset-password_2.tpl +++ /dev/null @@ -1,7 +0,0 @@ -# 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 deleted file mode 100644 index 0cf87890..00000000 --- a/template/mails/reset-password_3.tpl +++ /dev/null @@ -1,7 +0,0 @@ -# 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 deleted file mode 100644 index 615da1fb..00000000 --- a/template/mails/reset-password_4.tpl +++ /dev/null @@ -1,7 +0,0 @@ -# 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 deleted file mode 100644 index 83ad31fc..00000000 --- a/template/mails/reset-password_6.tpl +++ /dev/null @@ -1,7 +0,0 @@ -# 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 deleted file mode 100644 index ca1317d8..00000000 --- a/template/mails/reset-password_8.tpl +++ /dev/null @@ -1,7 +0,0 @@ -# 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 deleted file mode 100644 index 881c02f9..00000000 --- a/template/mails/revert-email_0.tpl +++ /dev/null @@ -1,11 +0,0 @@ -# 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 deleted file mode 100644 index b9b37958..00000000 --- a/template/mails/revert-email_2.tpl +++ /dev/null @@ -1,11 +0,0 @@ -# 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 deleted file mode 100644 index 4ccc7af9..00000000 --- a/template/mails/revert-email_3.tpl +++ /dev/null @@ -1,11 +0,0 @@ -# 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 deleted file mode 100644 index ecc86e88..00000000 --- a/template/mails/revert-email_4.tpl +++ /dev/null @@ -1,11 +0,0 @@ -# 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 deleted file mode 100644 index 2fbf927b..00000000 --- a/template/mails/revert-email_6.tpl +++ /dev/null @@ -1,11 +0,0 @@ -# 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 deleted file mode 100644 index b257ad2a..00000000 --- a/template/mails/revert-email_8.tpl +++ /dev/null @@ -1,11 +0,0 @@ -# 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 deleted file mode 100644 index d55404cb..00000000 --- a/template/mails/update-password_0.tpl +++ /dev/null @@ -1,10 +0,0 @@ -# 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 deleted file mode 100644 index e971a03a..00000000 --- a/template/mails/update-password_2.tpl +++ /dev/null @@ -1,10 +0,0 @@ -# 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 deleted file mode 100644 index 348a7abb..00000000 --- a/template/mails/update-password_3.tpl +++ /dev/null @@ -1,10 +0,0 @@ -# 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 deleted file mode 100644 index affa20f4..00000000 --- a/template/mails/update-password_4.tpl +++ /dev/null @@ -1,10 +0,0 @@ -# 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 deleted file mode 100644 index b46dd849..00000000 --- a/template/mails/update-password_6.tpl +++ /dev/null @@ -1,10 +0,0 @@ -# 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 deleted file mode 100644 index 9266fa1d..00000000 --- a/template/mails/update-password_8.tpl +++ /dev/null @@ -1,10 +0,0 @@ -# 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 new file mode 100644 index 00000000..84606e30 --- /dev/null +++ b/template/pages/acc-dashboard.tpl.php @@ -0,0 +1,143 @@ +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 new file mode 100644 index 00000000..3a26c370 --- /dev/null +++ b/template/pages/acc-recover.tpl.php @@ -0,0 +1,134 @@ +brick('header'); ?> + +
+
+
+brick('announcement'); + + $this->brick('pageTemplate'); +?> +
+text)): ?> +
+

head; ?>

+
+
text; ?>
+
+resetPass): ?> + + +
+
+

head; ?>

+
error; ?>
+ + + + + + + + + + + + + + + + + + + +
+ +
+
+ + + + + +
+
+

head; ?>

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

-
+

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

head; ?>

+
+
text; ?>
+
+ -
+
-

-
+

head; ?>

+
error; ?>
- - + + - + - + - - + + - +
/> - +
- +
@@ -104,3 +116,9 @@ + +
+
+
+ +brick('footer'); ?> diff --git a/template/pages/account.tpl.php b/template/pages/account.tpl.php deleted file mode 100644 index e5565ca9..00000000 --- a/template/pages/account.tpl.php +++ /dev/null @@ -1,348 +0,0 @@ -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 86a31978..6c037e67 100644 --- a/template/pages/achievement.tpl.php +++ b/template/pages/achievement.tpl.php @@ -1,12 +1,4 @@ -brick('header'); -?> +brick('header'); ?>
@@ -21,84 +13,103 @@ ?>
+brick('headIcons'); + +$this->brick('redButtons'); +?> +

name; ?>

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

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

\n"; ?> -

h1; ?>

- description.PHP_EOL; ?> -

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

+
+ criteria as $i => $icon): +foreach ($this->criteria['data'] as $i => $cr): + echo ''; + + if (!isset($cr['icon'])): + echo '
  •  
'; + endif; + + echo '
'; + // every odd number of elements - ${'rows' . ($i % 2)} .= $icon->renderContainer(20, $i, true); + 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; + + if (User::isInGroup(U_GROUP_STAFF)): + echo ' ['.$cr['id'].']'; + endif; + + echo '
'; + endif; 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 ($rewItems): - echo '

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

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

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

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

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

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

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

'.PHP_EOL; - echo '
  • '.$rewText.'
'.PHP_EOL; + if (empty($r['title']) && empty($r['item']) && $r['text']): + echo '

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

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

+

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

h1; ?>

-
+
+ - + - +
ucFirst(Lang::main('name')).Lang::main('colon'); ?> - - - + + +
 />  /> />  />
  - - + +
    /> - />    /> - />
-
+
+
-
- /> /> +
+ /> />
- - + +
-renderFilter(12); ?> + brick('lvTabs'); ?> diff --git a/template/pages/admin/reports.tpl.php b/template/pages/admin/reports.tpl.php deleted file mode 100644 index ac203920..00000000 --- a/template/pages/admin/reports.tpl.php +++ /dev/null @@ -1,37 +0,0 @@ -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 e0276d9f..bd39c0e4 100644 --- a/template/pages/admin/screenshots.tpl.php +++ b/template/pages/admin/screenshots.tpl.php @@ -1,23 +1,16 @@ -brick('header'); -?> +brick('header'); ?>
brick('announcement'); +$this->brick('announcement'); - $this->brick('pageTemplate'); +$this->brick('pageTemplate'); ?> -
-

h1; ?>

+

name; ?>

@@ -29,11 +22,17 @@ - - + +
Page: #» Search by Page#» Search by Page

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

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
@@ -109,29 +108,27 @@ $WH.ge('pagetypeid').onkeydown = function(e) { e = $WH.$E(e); - var validKeys = [8, 9, 13, 35, 36, 37, 38, 39, 40, 46, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 173]; + var validKeys = [8, 9, 13, 35, 36, 37, 39, 46, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57]; if (!e.ctrlKey && $WH.in_array(validKeys, e.keyCode) == -1) return false; if (e.keyCode == 13 && this.value != '') - ss_Manage(null, $('#pagetype').val(), parseInt($('#pagetypeid').val()) || 0); + ss_Manage(); return true; } - getAll): - echo ' var ss_getAll = true;'.PHP_EOL; + echo " var ss_getAll = true;\n"; endif; if ($this->ssPages): - echo ' var ssm_screenshotPages = ".$this->json($this->ssPages).";'.PHP_EOL; - echo ' ssm_UpdatePages();'.PHP_EOL; + echo " var ssm_screenshotPages = ".json_encode($this->ssPages, JSON_NUMERIC_CHECK).";\n"; + echo " ssm_UpdatePages();\n"; elseif ($this->ssData): - echo ' var ssm_screenshotData = ".$this->json($this->ssData).";'.PHP_EOL; - echo ' ssm_UpdateList();'.PHP_EOL; + echo " var ssm_screenshotData = ".json_encode($this->ssData, JSON_NUMERIC_CHECK).";\n"; + echo " ssm_UpdateList();\n"; endif; ?> - ss_OnResize();
diff --git a/template/pages/admin/siteconfig.tpl.php b/template/pages/admin/siteconfig.tpl.php deleted file mode 100644 index 6c604cc4..00000000 --- a/template/pages/admin/siteconfig.tpl.php +++ /dev/null @@ -1,291 +0,0 @@ -brick('header'); -?> - - - -
-
-
- -brick('announcement'); - - $this->brick('pageTemplate'); - - $this->brick('lvTabs'); -?> - -
-
-
- -brick('footer'); ?> diff --git a/template/pages/admin/videos.tpl.php b/template/pages/admin/videos.tpl.php deleted file mode 100644 index 5cc7b135..00000000 --- a/template/pages/admin/videos.tpl.php +++ /dev/null @@ -1,141 +0,0 @@ -brick('header'); -?> - -
-
-
- -brick('announcement'); - - $this->brick('pageTemplate'); -?> - -
-

h1; ?>

- - - - - - - - - - - - - -
User: » Search by User
Page: - - #» Search by Page
-
- - - - - - - -
Menu
PagesVideos:
- - - - - - - -
VideoIdTitleDateUploaderStatusOptions
- - -
-
-
- -brick('footer'); ?> diff --git a/template/pages/admin/weight-presets.tpl.php b/template/pages/admin/weight-presets.tpl.php deleted file mode 100644 index c21baf6a..00000000 --- a/template/pages/admin/weight-presets.tpl.php +++ /dev/null @@ -1,585 +0,0 @@ -brick('header'); -?> - - - -
-
-
- -brick('announcement'); - -$this->brick('pageTemplate'); -?> - -
-

h1;?>

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

Edit

-
-
Icon
-
-
-
-
Scale
-
-
-
-
-
-
- -brick('footer'); ?> diff --git a/template/pages/areatriggers.tpl.php b/template/pages/areatriggers.tpl.php deleted file mode 100644 index a663845e..00000000 --- a/template/pages/areatriggers.tpl.php +++ /dev/null @@ -1,78 +0,0 @@ -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 deleted file mode 100644 index c7636499..00000000 --- a/template/pages/arena-teams.tpl.php +++ /dev/null @@ -1,87 +0,0 @@ -brick('header'); - $f = $this->filter->values; // shorthand -?> - -
-
-
- -brick('announcement'); - - $this->brick('pageTemplate', ['fiQuery' => $this->filter->query, 'fiMenuItem' => array_slice($this->pageTemplate['breadcrumb'], 0, 3)]); - -# 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'); ?> - - - - -
 />  />
-
      
     
- -
- -
- - -
- - -
-
- -renderFilter(12); ?> - -brick('lvTabs'); ?> - -
-
-
- -brick('footer'); ?> diff --git a/template/pages/compare.tpl.php b/template/pages/compare.tpl.php index 5f06d246..054392fe 100644 --- a/template/pages/compare.tpl.php +++ b/template/pages/compare.tpl.php @@ -1,10 +1,4 @@ -brick('header'); -?> +brick('header'); ?>
@@ -19,14 +13,12 @@
diff --git a/template/pages/delete.tpl.php b/template/pages/delete.tpl.php deleted file mode 100644 index ddcc3c18..00000000 --- a/template/pages/delete.tpl.php +++ /dev/null @@ -1,31 +0,0 @@ -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 7757c8ff..5a03ee9d 100644 --- a/template/pages/detail-page-generic.tpl.php +++ b/template/pages/detail-page-generic.tpl.php @@ -1,12 +1,4 @@ -brick('header'); -?> +brick('header'); ?>
@@ -21,62 +13,50 @@ ?>
- brick('headIcons'); $this->brick('redButtons'); +?> - if ($this->expansion && $this->h1): - echo '

'.$this->h1.'

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

'.$this->h1.'

'.PHP_EOL; - endif; + expansion) ? ' class="h1-icon">'.$this->name.'' : '>'.$this->name; ?> - $this->brick('markup', ['markup' => $this->article]); - - $this->brick('markup', ['markup' => $this->extraText]); +brick('article'); $this->brick('mapper'); - if ($this->transfer): - echo '
'.PHP_EOL; - echo ' '.$this->transfer.PHP_EOL; - endif; - - $this->brick('markup', ['markup' => $this->smartAI]); - -if ($this->zoneMusic): +if (isset($this->extraText)): ?> +
+ -
- -zoneMusic as [$h3, $data, $divId, $opts]): -?> - -
-

-
- - - - -
- +
-

+if (isset($this->unavailable)): +?> +
+ +transfer)): + echo "
\n ".$this->transfer."\n"; +endif; + +?> +

brick('lvTabs'); + $this->brick('lvTabs', ['relTabs' => true]); $this->brick('contribute'); ?> diff --git a/template/pages/enchantment.tpl.php b/template/pages/enchantment.tpl.php deleted file mode 100644 index f737f555..00000000 --- a/template/pages/enchantment.tpl.php +++ /dev/null @@ -1,138 +0,0 @@ -brick('header'); -?> - -
-
-
- -brick('announcement'); - - $this->brick('pageTemplate'); - - $this->brick('infobox'); -?> - -
- -brick('redButtons'); ?> - -

h1; ?>

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

- - - - - - - - -activation): -?> - - - - - - -effects as $i => $e): -?> - - - - - - - - -
activation; ?>
- -)' : '').''; - - if ($e['value']): - echo '
'.Lang::spell('_value').Lang::main('colon').$e['value']; - endif; - - if ($e['proc']): - echo '
'; - - if ($e['proc'] < 0): - echo Lang::spell('ppm', [-$e['proc']]); - elseif ($e['proc'] < 100.0): - echo Lang::spell('procChance', [$e['proc']]); - endif; - endif; - - echo '
'.PHP_EOL; - - if ($e['tip']): -?> - - - - - - - - renderContainer(0, $i); ?> - - -
- - - - -
- -

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

h1; ?>

-
-
-
- -
- -
- - - - - -
ucFirst(Lang::main('name')).Lang::main('colon'); ?> - - -
 />
-
- -
- -
- /> /> -
- -
- -
- - -
- -
-
-
- -renderFilter(12); ?> - -brick('lvTabs'); ?> - -
-
-
- -brick('footer'); ?> diff --git a/template/pages/guide-edit.tpl.php b/template/pages/guide-edit.tpl.php deleted file mode 100644 index 3d83fb77..00000000 --- a/template/pages/guide-edit.tpl.php +++ /dev/null @@ -1,318 +0,0 @@ -brick('header'); -?> - -
-
-
- -brick('announcement'); - - $this->brick('pageTemplate'); -?> - -
-

h1; ?>

- -brick('markup', ['markup' => $this->article]); -?> - -
-
- -
-
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -*/ -?> - - - - - - - - - - - - -
- - -
- - -
- - -
- - - -
- - -
- - -
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 b/template/pages/guilds.tpl new file mode 100644 index 00000000..c7fe571c --- /dev/null +++ b/template/pages/guilds.tpl @@ -0,0 +1,77 @@ +{include file='header.tpl'} + +
+
+
+{if !empty($announcements)} + {foreach from=$announcements item=item} + {include file='bricks/announcement.tpl' an=$item} + {/foreach} +{/if} + + + +
+
+ + + + + + + + + + + + + + + + + +
{$lang.name}{$lang.colon} + + + + +
  
+
Region{$lang.colon}     Realm{$lang.colon} 
{$lang.side}{$lang.colon}     Level{$lang.colon}  -
+ +
+ +
+
{$lang.refineSearch}
+ +
+
+
+ + + +
+ + +
+
+
+ +{include file='footer.tpl'} diff --git a/template/pages/guilds.tpl.php b/template/pages/guilds.tpl.php deleted file mode 100644 index 312606cf..00000000 --- a/template/pages/guilds.tpl.php +++ /dev/null @@ -1,84 +0,0 @@ -brick('header'); - $f = $this->filter->values; // shorthand -?> - -
-
-
- -brick('announcement'); - - $this->brick('pageTemplate', ['fiQuery' => $this->filter->query, 'fiMenuItem' => array_slice($this->pageTemplate['breadcrumb'], 0, 3)]); - - # 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'); ?> - - - - -
 />  />
-
      
   
- -
- -
- - -
- - -
-
- -renderFilter(12); ?> - -brick('lvTabs'); ?> - -
-
-
- -brick('footer'); ?> diff --git a/template/pages/home.tpl.php b/template/pages/home.tpl.php index 3334e0b2..201b829b 100644 --- a/template/pages/home.tpl.php +++ b/template/pages/home.tpl.php @@ -1,121 +1,66 @@ - - - brick('head'); ?> - +
- -homeTitle): - echo " ".PHP_EOL; -endif; - -if ($this->altHomeLogo): -?> - - - - -
-

concat('title'); ?>

+

Aowow

brick('announcement'); ?>
-oneliner): ?> -

- +news): ?> +
-featuredBox): ?> -
- -featuredBox): -?> - -
- -featuredBox['overlays']): ?> +
+news['overlays']): ?> - -
- - +
brick('pageTemplate'); ?> -localizedBrickIf($this->consentFooter, 'consent'); ?> - - + diff --git a/template/pages/icon.tpl.php b/template/pages/icon.tpl.php deleted file mode 100644 index 59a3a906..00000000 --- a/template/pages/icon.tpl.php +++ /dev/null @@ -1,53 +0,0 @@ -brick('header'); -?> - -
-
-
- -brick('announcement'); - - $this->brick('pageTemplate'); - - $this->brick('infobox'); -?> - -
- -brick('redButtons'); -?> - -

h1; ?>

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

-
- -brick('lvTabs'); - - $this->brick('contribute'); -?> - -
-
-
- -brick('footer'); ?> diff --git a/template/pages/icons.tpl.php b/template/pages/icons.tpl.php deleted file mode 100644 index 35f59368..00000000 --- a/template/pages/icons.tpl.php +++ /dev/null @@ -1,70 +0,0 @@ -brick('header'); - $f = $this->filter->values; // shorthand -?> - -
-
-
- -brick('announcement'); - - $this->brick('pageTemplate', ['fiQuery' => $this->filter->query, 'fiMenuItem' => [31]]); -?> - -
-
-
- -brick('headIcons'); - - $this->brick('redButtons'); -?> - -

h1; ?>

-
- - - - - -
ucFirst(Lang::main('name')).Lang::main('colon'); ?> - - -
 />
-
- -
- -
- /> /> -
- -
- -
- - -
- -
-
-
- -renderFilter(12); ?> - -brick('lvTabs'); ?> - -
-
-
- -brick('footer'); ?> diff --git a/template/pages/image-crop.tpl.php b/template/pages/image-crop.tpl.php deleted file mode 100644 index 6084bf49..00000000 --- a/template/pages/image-crop.tpl.php +++ /dev/null @@ -1,49 +0,0 @@ -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 070fb0b1..9b710a68 100644 --- a/template/pages/item.tpl.php +++ b/template/pages/item.tpl.php @@ -1,12 +1,4 @@ -brick('header'); -?> +brick('header'); ?>
@@ -21,71 +13,70 @@ ?>
- brick('redButtons'); ?> -

h1; ?>

+

name; ?>

unavailable): -?> - -
- -
- -brick('tooltip'); - $this->brick('markup', ['markup' => $this->article]); + $this->brick('article'); -if ($this->map): - echo '

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

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

- +
+ subItems['data'], ceil(count($this->subItems['data']) / 2)) as $columns): +endif; + +if (!empty($this->transfer)): + echo "
\n ".$this->transfer."\n"; +endif; + +if (!empty($this->subItems)): ?> +
+

    - ['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; + foreach ($this->subItems['data'] as $k => $i): + if ($k < (count($this->subItems['data']) / 2)): + echo '
  • ...'.$i['name'].''; + echo ' '.sprintf(Lang::item('_chance'), $i['chance']).'
    '.$i['enchantment'].'
  • '; + endif; endforeach; ?> -
- subItems) > 1): +?> +
+
    +subItems['data'] as $k => $i): + if ($k >= (count($this->subItems['data']) / 2)): + echo '
  • ...'.$i['name'].''; + echo ' '.sprintf(Lang::item('_chance'), $i['chance']).'
    '.$i['enchantment'].'
  • '; + endif; + endforeach; +?> +
+
+brick('book'); ?> -

+

brick('lvTabs'); + $this->brick('lvTabs', ['relTabs' => true]); $this->brick('contribute'); ?> diff --git a/template/pages/items.tpl.php b/template/pages/items.tpl.php index b722653a..311475cf 100644 --- a/template/pages/items.tpl.php +++ b/template/pages/items.tpl.php @@ -1,12 +1,6 @@ brick('header'); - $f = $this->filter->values; // shorthand +$this->brick('header'); +$f = $this->filter; // shorthand ?>
@@ -14,155 +8,169 @@
brick('announcement'); +$this->brick('announcement'); - $this->brick('pageTemplate', ['fiQuery' => $this->filter->query, 'fiMenuItem' => [0]]); +$this->brick('pageTemplate', ['fi' => empty($f['query']) ? null : ['query' => $f['query'], 'menuItem' => 0]]); ?> -
-
-
- -brick('headIcons'); - - $this->brick('redButtons'); -?> - -

h1; ?>

-
+
+
-
- +
+
slotList): +if (!empty($f['slot'])): ?> -
-
- +
+
- + $str): + echo ' \n"; + endforeach; +?>
- typeList): +if (!empty($f['type'])): ?> -
-
- +
+
- + $str): + echo ' \n"; + endforeach; +?>
- - - + + - - + + - +
ucFirst(Lang::main('name')).Lang::main('colon'); ?> /> />
 /> - /> /> - /> - - + +
    /> - />    /> - />
   
-
+
+
-
- - />/> +
+ />/>
-

+

- -makeRadiosList('gb', Lang::main('gb'), $f['gb'] ?? '', 24, fn($v, &$k) => ($k = $k ?: '') || 1); ?> + $str): + if ($k): + echo ' \n"; + else: + echo ' \n"; + endif; +endforeach; +?>
- - + +
- + />
@@ -170,7 +178,14 @@ if ($this->typeList):
-renderFilter(12); ?> + brick('lvTabs'); ?> diff --git a/template/pages/itemset.tpl.php b/template/pages/itemset.tpl.php index 42a3b36f..7ed3cc64 100644 --- a/template/pages/itemset.tpl.php +++ b/template/pages/itemset.tpl.php @@ -1,12 +1,4 @@ -brick('header'); -?> +brick('header'); ?>
@@ -21,93 +13,73 @@ ?>
- brick('redButtons'); +$this->brick('redButtons'); if ($this->expansion): - echo '

'.$this->h1.'

'.PHP_EOL; + echo '

'.$this->name."

\n"; else: - echo '

'.$this->h1.'

'.PHP_EOL; + echo '

'.$this->name."

\n"; endif; -if ($this->unavailable): -?> -
- -
- -brick('markup', ['markup' => $this->article]); +$this->brick('article'); echo $this->description; ?> - - pieces as [, $icon]): - echo $icon->renderContainer(20, $iconIdx, true); +foreach ($this->pieces as $i => $p): + echo ' \n"; endforeach; ?> -
'.$p['name']."
-

bonusExt; ?>

- - -
    - spells as [$nItems, $spellId, $text]): - echo '
  • '.Lang::itemset('_pieces', [$nItems]).''.$text.'
  • '.PHP_EOL; +if ($this->unavailable): +?> +
    + + + +

    bonusExt; ?>

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

    +

    - - -

    +

brick('lvTabs'); +$this->brick('lvTabs', ['relTabs' => true]); $this->brick('contribute'); ?> diff --git a/template/pages/itemsets.tpl.php b/template/pages/itemsets.tpl.php index 3f2d09c9..59ef0d34 100644 --- a/template/pages/itemsets.tpl.php +++ b/template/pages/itemsets.tpl.php @@ -1,12 +1,6 @@ brick('header'); - $f = $this->filter->values; // shorthand +$this->brick('header'); +$f = $this->filter; // shorthand ?>
@@ -14,91 +8,111 @@
brick('announcement'); +$this->brick('announcement'); - $this->brick('pageTemplate', ['fiQuery' => $this->filter->query, 'fiMenuItem' => [2]]); +$this->brick('pageTemplate', ['fi' => empty($f['query']) ? null : ['query' => $f['query'], 'menuItem' => 2]]); ?> -
- -
- -brick('headIcons'); - - $this->brick('redButtons'); -?> - -

h1; ?>

-
+
+
-
- +
+
-
- +
+
- - + + - - + + - +
ucFirst(Lang::main('name')).Lang::main('colon'); ?> /> />
 /> - /> /> - /> - - + +
    /> - />    /> - />
ucFirst(Lang::game('class')).Lang::main('colon'); ?>   - +
        
-
+
+
-
- /> /> +
+ /> />
- - + +
-renderFilter(12); ?> + brick('lvTabs'); ?> diff --git a/template/pages/list-page-generic.tpl.php b/template/pages/list-page-generic.tpl.php index c4e1d1cb..e686b8f5 100644 --- a/template/pages/list-page-generic.tpl.php +++ b/template/pages/list-page-generic.tpl.php @@ -1,12 +1,4 @@ -brick('header'); -?> +brick('header'); ?>
@@ -16,47 +8,21 @@ $this->brick('announcement'); $this->brick('pageTemplate'); -?> - -
- -brick('redButtons'); - - if ($this->h1Link): - echo ' '; - endif; - - if ($this->h1): - echo '

'.$this->h1.'

'; - endif; $this->brick('mapper'); - $this->brick('markup', ['markup' => $this->article]); +if (!empty($this->name) || !empty($this->h1Links) || !empty($this->extraHTML)): + echo '
' . + (!empty($this->h1Links) ? '' : null) . + (!empty($this->name) ? '

'.$this->name.'

' : null) . + (!empty($this->extraHTML) ? $this->extraHTML : null) . + '
'; +endif; - $this->brick('markup', ['markup' => $this->extraText]); - - echo $this->extraHTML ?? ''; - - if ($this->tabsTitle): - echo '

'.$this->tabsTitle.'

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

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

+

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

brick('markup', ['markup' => $this->article]); + $this->brick('article'); if ($this->accessory): echo '
'.Lang::npc('accessoryFor').' '; - echo Lang::concat($this->accessory, true, fn ($v) => ''.$v[1].''); - echo '.
'.PHP_EOL; + + $n = count($this->accessory); + foreach ($this->accessory as $i => $ac): + if ($n > 1 && $i > 0): + echo ($i == $n - 1) ? Lang::main('and') : ', '; + endif; + echo ''.$ac[1].''; + endforeach; + + echo ".
\n"; endif; -if ($this->placeholder): +if (is_array($this->position)): + echo '
'.Lang::npc('difficultyPH').' '.$this->position[1].".
\n"; ?> - -
placeholder);?>
- map): +elseif (!empty($this->map)): $this->brick('mapper'); else: - echo ' '.Lang::npc('unkPosition').''.PHP_EOL; + echo ' '.Lang::npc('unkPosition')."\n"; endif; -if ([$quoteGroups, $count] = $this->quotes): +if ($this->quotes[0]): ?> - -

+

quotes[1]; ?>)

- reputation): ?> - -

- +

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

    +

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

    h1; ?>

    -
    +
    +
    -
    - +
    +
    - petFamPanel): ?>
    -
    +
    - - - + - - + +
    ucFirst(Lang::main('name')).Lang::main('colon'); ?> - - - + + +
     />  /> />  />
     /> - /> /> - /> - - +
             - > - - - + + +
    @@ -85,25 +78,33 @@ $f = $this->filter->values; // shorthand
    -
    +
    +
    -
    - /> /> +
    + /> />
    - - + +
    -renderFilter(12); ?> + brick('lvTabs'); ?> diff --git a/template/pages/object.tpl.php b/template/pages/object.tpl.php index ad44552b..35f59818 100644 --- a/template/pages/object.tpl.php +++ b/template/pages/object.tpl.php @@ -1,12 +1,4 @@ -brick('header'); -?> +brick('header'); ?>
    @@ -21,36 +13,27 @@ ?>
    - brick('redButtons'); ?> -

    h1; ?>

    +

    name; ?>

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

    +

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

    h1; ?>

    -
    +
    + - +
    ucFirst(Lang::main('name')).Lang::main('colon'); ?> />
     />
    -
    +
    +
    -
    - /> /> +
    + /> />
    - - + +
    -renderFilter(12); ?> + brick('lvTabs'); ?> diff --git a/template/pages/privilege.tpl.php b/template/pages/privilege.tpl.php deleted file mode 100644 index f86c7cdb..00000000 --- a/template/pages/privilege.tpl.php +++ /dev/null @@ -1,32 +0,0 @@ -brick('header'); -?> - -
    -
    -
    - -brick('announcement'); - - $this->brick('pageTemplate'); -?> - -
    -

    h1;?>

    -

    privReqPoints;?>


    - -brick('markup', ['markup' => $this->article]); -?> - -
    -
    -
    -
    - -brick('footer'); ?> diff --git a/template/pages/privileges.tpl.php b/template/pages/privileges.tpl.php deleted file mode 100644 index 5f4005eb..00000000 --- a/template/pages/privileges.tpl.php +++ /dev/null @@ -1,43 +0,0 @@ -brick('header'); -?> - -
    -
    -
    - -brick('announcement'); - - $this->brick('pageTemplate'); -?> - -
    -

    -
    -

    -

    - - - - -privileges as $id => [$earned, $name, $value]): - echo ' '.PHP_EOL; - endforeach; -?> - - -
     
    '.$name.'
    '.Lang::nf($value).'
    -
    -
    -
    -
    - -brick('footer'); ?> diff --git a/template/pages/profile.tpl.php b/template/pages/profile.tpl.php index fed434d7..74a0e159 100644 --- a/template/pages/profile.tpl.php +++ b/template/pages/profile.tpl.php @@ -1,10 +1,4 @@ -brick('header'); -?> +brick('header'); ?>
    @@ -19,8 +13,8 @@
    diff --git a/template/pages/profiler.tpl.php b/template/pages/profiler.tpl.php index 8880f490..6f3b8800 100644 --- a/template/pages/profiler.tpl.php +++ b/template/pages/profiler.tpl.php @@ -1,12 +1,4 @@ -brick('header'); -?> +brick('header'); ?>
    @@ -19,46 +11,41 @@ ?>
    -

    +

    -

    +

    -

    +

    -

    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; -}); ?> +

    + +
    -

    +

    - - + +
    -

    +

    brick('lvTabs'); ?> diff --git a/template/pages/quest.tpl.php b/template/pages/quest.tpl.php index 104827e9..4ae5ae37 100644 --- a/template/pages/quest.tpl.php +++ b/template/pages/quest.tpl.php @@ -1,12 +1,4 @@ -brick('header'); -?> +brick('header'); ?>
    @@ -21,207 +13,193 @@ ?>
    - brick('redButtons'); ?> -

    h1; ?>

    - +

    name; ?>

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

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

    '.PHP_EOL; - echo $this->requestItems.PHP_EOL; + echo '

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

    \n"; + echo $this->requestItems."\n"; elseif ($this->offerReward): - echo '

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

    '.PHP_EOL; - echo $this->offerReward.PHP_EOL; + echo '

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

    \n"; + echo $this->offerReward."\n"; endif; -$iconOffset = 0; -if ($this->end || $this->objectiveList): +if ($e = $this->end): ?> - - + +suggestedPl): ?> + + +

     

     

    +
    objectiveList as $objective): - if (is_string($objective)): // just text line - echo '

     

    '.$objective.''.PHP_EOL; - elseif (is_array($objective)): // proxy npc data - ['id' => $id, 'text' => $text, 'qty' => $qty, 'proxy' => $proxies] = $objective; - echo '

     

    '.$text.''.($qty ? ' ('.$qty.')' : '').' brick('lvTabs'); +$this->brick('lvTabs', ['relTabs' => true]); $this->brick('contribute'); ?> diff --git a/template/pages/quests.tpl.php b/template/pages/quests.tpl.php index 1dbeb100..053a8659 100644 --- a/template/pages/quests.tpl.php +++ b/template/pages/quests.tpl.php @@ -1,12 +1,6 @@ brick('header'); - $f = $this->filter->values; // shorthand +$this->brick('header'); +$f = $this->filter; // shorthand ?>
    @@ -14,79 +8,85 @@
    brick('announcement'); +$this->brick('announcement'); - $this->brick('pageTemplate', ['fiQuery' => $this->filter->query, 'fiMenuItem' => [3]]); +$this->brick('pageTemplate', ['fi' => empty($f['query']) ? null : ['query' => $f['query'], 'menuItem' => 3]]); ?> -
    -
    -
    - -brick('headIcons'); - - $this->brick('redButtons'); -?> - -

    h1; ?>

    -
    +
    +
    -
    +
    - + - - + + - +
    ucFirst(Lang::main('name')).Lang::main('colon'); ?> - - - + + +
     />  /> />  />
     /> - /> /> - /> - - + +
        /> - />    /> - />
     
    -
    +
    +
    -
    - /> /> +
    + /> />
    - - + +
    -renderFilter(12); ?> + brick('lvTabs'); ?> diff --git a/template/pages/roster.tpl.php b/template/pages/roster.tpl.php deleted file mode 100644 index 7159bf1b..00000000 --- a/template/pages/roster.tpl.php +++ /dev/null @@ -1,41 +0,0 @@ -brick('header'); -?> - -
    -
    -
    - -brick('announcement'); - - $this->brick('pageTemplate'); -?> - - - -
    - -brick('redButtons'); ?> -

    h1; ?>

    - -extraHTML ?? ''; -?> - -
    - -brick('lvTabs'); -?> - -
    -
    -
    - -brick('footer'); ?> diff --git a/template/pages/screenshot.tpl.php b/template/pages/screenshot.tpl.php index 60559162..e6d1083f 100644 --- a/template/pages/screenshot.tpl.php +++ b/template/pages/screenshot.tpl.php @@ -1,64 +1,70 @@ -brick('header'); -?> +brick('header'); ?>
    brick('announcement'); +$this->brick('announcement'); - $this->brick('pageTemplate'); +$this->brick('pageTemplate'); - $this->brick('infobox'); +$this->brick('infobox'); + +if (isset($this->error)): ?> +
    +
    +

    +
    error; ?>
    +
    -

    h1; ?>

    +

    name; ?>

    - -
    -
    - -
    - -
    - -
    - -
    -
    +mode == 'add'): +?> +
    +
    +
    - -localizedBrick('ssReminder'); ?> - - - - - +
    +
    + +
    text counter ph
    +
    + + +
    +
    + +
    +
    diff --git a/template/pages/search.tpl.php b/template/pages/search.tpl.php index a66adbed..3c4522a1 100644 --- a/template/pages/search.tpl.php +++ b/template/pages/search.tpl.php @@ -1,55 +1,42 @@ -brick('header'); -?> +brick('header'); ?>
    brick('announcement'); +$this->brick('announcement'); - $this->brick('pageTemplate'); +$this->brick('pageTemplate'); ?>
    - + WowheadWowhead brick('redButtons'); -if (count($this->lvTabs)): - echo '

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

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

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

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

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

    '.PHP_EOL; + echo "\n"; ?> -
    -
    diff --git a/template/pages/sound-playlist.tpl.php b/template/pages/sound-playlist.tpl.php deleted file mode 100644 index 59ea456a..00000000 --- a/template/pages/sound-playlist.tpl.php +++ /dev/null @@ -1,69 +0,0 @@ -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 deleted file mode 100644 index 619a7f28..00000000 --- a/template/pages/sound.tpl.php +++ /dev/null @@ -1,106 +0,0 @@ -brick('header'); -?> - -
    -
    -
    - -brick('announcement'); - - $this->brick('pageTemplate'); -?> - -
    - -brick('redButtons'); -?> - -

    h1; ?>

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

      - -
      - -brick('lvTabs'); - - $this->brick('contribute'); -?> - -
      -
      -
      - -brick('footer'); ?> diff --git a/template/pages/sounds.tpl.php b/template/pages/sounds.tpl.php deleted file mode 100644 index 1d9729a2..00000000 --- a/template/pages/sounds.tpl.php +++ /dev/null @@ -1,70 +0,0 @@ -brick('header'); - $f = $this->filter->values; // shorthand -?> - -
      -
      -
      - -brick('announcement'); - - $this->brick('pageTemplate', ['fiQuery' => $this->filter->query, 'fiMenuItem' => [19]]); -?> - -
      -
      -
      - -brick('headIcons'); - - $this->brick('redButtons'); -?> - -

      h1; ?>

      -
      -
      -
      - -
      - -
      - - - - - -
      ucFirst(Lang::main('name')).Lang::main('colon'); ?> - - -
       />
      -
      - -
      - -
      - - -
      - -
      -
      -
      - -brick('lvTabs'); ?> - -
      -
      -
      - -brick('footer'); ?> diff --git a/template/pages/spell.tpl.php b/template/pages/spell.tpl.php index 1487db0d..fd58e553 100644 --- a/template/pages/spell.tpl.php +++ b/template/pages/spell.tpl.php @@ -1,14 +1,4 @@ -brick('header'); - - $iconOffset = 0; -?> +brick('header'); ?>
      @@ -26,67 +16,60 @@ brick('redButtons'); ?> -

      h1; ?>

      +

      name; ?>

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

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

      - tools as $icon): - echo $icon->renderContainer(20, $iconOffset, true); - endforeach; + foreach ($this->tools as $i => $t): + echo ' \n"; + endforeach; ?> -
      '.$t['name']."
      - reagents[0]): - echo '
      '.PHP_EOL; + if ($this->reagents[0]): + echo "
      \n"; + endif; endif; endif; ?> -
      -brick('markup', ['markup' => $this->article]); +brick('article'); ?> -if ($this->transfer): - echo '
      '.PHP_EOL; - echo ' '.$this->transfer.PHP_EOL; +transfer)): + echo "
      \n ".$this->transfer."\n"; endif; ?> -

      +

      @@ -102,221 +85,153 @@ endif; - - + + - - + + - - + + - - + + - - + +
      duration ?: ''.Lang::main('n_a').'');?>duration) ? $this->duration : ''.Lang::main('n_a').''; ?>
      school ?: ''.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').''; ?>
      mechanic ?:''.Lang::main('n_a').'');?>mechanic) ? $this->mechanic : ''.Lang::main('n_a').''; ?>
      dispel ?: ''.Lang::main('n_a').'');?>dispel) ? $this->dispel : ''.Lang::main('n_a').''; ?>
      gcdCat ?: ''.Lang::main('n_a').'');?>gcdCat) ? $this->gcdCat : ''.Lang::main('n_a').''; ?>
      - - powerCost ?: Lang::spell('_none'));?> + + powerCost) ? $this->powerCost : Lang::spell('_none'); ?> - - range.Lang::spell('_distUnit').' ('.$this->rangeName.')';?> + + range.' '.Lang::spell('_distUnit').' ('.$this->rangeName; ?>) - - castTime;?> + + castTime; ?> - - cooldown ?: ''.Lang::main('n_a').'');?> + + cooldown) ? $this->cooldown : ''.Lang::main('n_a').''; ?> - '.Lang::spell('_gcd');?> - gcd;?> + '.Lang::spell('_gcd'); ?> + gcd; ?> +scaling), [[-1, -1, 0, 0], [0, 0, 0, 0]])): +?> + + + stances): + foreach ($this->scaling as $k => $s): + if ($s > 0): + echo ' '.sprintf(Lang::spell('scaling', $k), $s * 100)."
      \n"; + endif; + endforeach; ?> - - - - stances;?> + - items): +if (!empty($this->stances)): ?> - - - items;?> + + stances; ?> + +items)): +?> + + + items[0]), $this->items[1]) : $this->items[1]; ?> - effects as $i => $e): ?> - - + - ' . + (isset($e['value']) ? '
      '.Lang::spell('_value') .Lang::main('colon').$e['value'] : null) . + (isset($e['radius']) ? '
      '.Lang::spell('_radius') .Lang::main('colon').$e['radius'].' '.Lang::spell('_distUnit') : null) . + (isset($e['interval']) ? '
      '.Lang::spell('_interval').Lang::main('colon').$e['interval'] : null) . + (isset($e['mechanic']) ? '
      '.Lang::game('mechanic') .Lang::main('colon').$e['mechanic'] : null); - if ($e['footer']): - echo '
      '.implode('
      ', $e['footer']).'
      '.PHP_EOL; + if (isset($e['procData'])): + echo '
      '; + + if ($e['procData'][0] < 0): + echo sprintf(Lang::spell('ppm'), -$e['procData'][0]); + elseif ($e['procData'][0] < 100.0): + echo Lang::spell('procChance').Lang::main('colon').$e['procData'][0].'%'; + endif; + + if ($e['procData'][1]): + if ($e['procData'][0] < 100.0): + echo '
      '; + endif; + echo sprintf(Lang::game('cooldown'), $e['procData'][1]); + endif; endif; - if ($e['markup']): - echo '
      '; - endif; + echo "\n"; - if ($e['icon']): + if (isset($e['icon'])): ?> - - renderContainer(iconIdxOffset: $iconTabIdx); ?> + +'.$e['icon']['name']."\n"; + else: + echo ' \n"; + endif; +?>
      '.(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']; +endif; ?> - - - - renderContainer(0, $iconTabIdx, true); ?>
      - - - - - -
      - -'.Lang::spell('_seeMore').' brick('lvTabs'); +$this->brick('lvTabs', ['relTabs' => true]); $this->brick('contribute'); ?> -
      diff --git a/template/pages/spells.tpl.php b/template/pages/spells.tpl.php index e3452808..1360c72d 100644 --- a/template/pages/spells.tpl.php +++ b/template/pages/spells.tpl.php @@ -1,12 +1,6 @@ brick('header'); - $f = $this->filter->values; // shorthand +$this->brick('header'); +$f = $this->filter; // shorthand ?>
      @@ -14,122 +8,151 @@
      brick('announcement'); +$this->brick('announcement'); - $this->brick('pageTemplate', ['fiQuery' => $this->filter->query, 'fiMenuItem' => [1]]); +$this->brick('pageTemplate', ['fi' => empty($f['query']) ? null : ['query' => $f['query'], 'menuItem' => 1]]); ?> -
      -
      -
      - -brick('headIcons'); - - $this->brick('redButtons'); -?> - -

      h1; ?>

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

      chooseType; ?>

      +
      +
      +

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

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

      notFound['title']; ?>

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

      '.$this->h1.'

      ' : '');?> +

      name; ?>

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

      h1; ?>

      - - -

      h1; ?>

      - - - -

      -
      description): -?> - -
      - - -
      - - -lvTabs)): ?> - - - - -
      - - - -brick('lvTabs'); ?> - -
      -
      -
      - -brick('footer'); ?> +brick('header'); ?> + +
      +
      +
      + +brick('announcement'); + + $this->brick('pageTemplate'); + + $this->brick('infobox'); +?> + + + +
      +
      + +

      name; ?>

      +
      + +

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

      h1; ?>

      - -

      viTitle;?>

      -
      -
      - - -
      - -
      -
      -
      - - -
      - - -
      -
      -
      - -brick('footer'); ?>