1 Commits

Author SHA1 Message Date
Sarjuuk
e7d49befdf Updated README with installation instructions
thats it folks .. lets call it v1.0
2015-06-27 21:40:43 +02:00
705 changed files with 34891 additions and 96759 deletions

14
.gitattributes vendored
View File

@@ -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

View File

@@ -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

20
.gitignore vendored
View File

@@ -1,44 +1,30 @@
# Git
*.orig
# cache # cache
/cache/template/* /cache/template/*
/cache/alphaMaps/* /setup/generated/alphaMaps/*.png
/cache/setup/*
# extract from MPQ
/setup/mpqdata/*
# generated files # generated files
/static/js/profile_all.js /static/js/profile_all.js
/static/js/locale.js /static/js/locale.js
/static/js/Markup.js
/static/widgets/power.js /static/widgets/power.js
/static/widgets/power/demo.html /static/widgets/power/demo.html
/static/widgets/searchbox.js /static/widgets/searchbox.js
/static/widgets/searchbox/searchbox.html /static/widgets/searchbox/searchbox.html
/static/download/searchplugins/aowow.xml /static/download/searchplugins/aowow.xml
/config/config.php /config/config.php
/datasets/*
!/datasets/zones
# /datasets/item-scaling # /datasets/item-scaling
# extracted sounds
/static/wowsounds/*
# extracted images # extracted images
/static/images/wow/icons/large/* /static/images/wow/icons/large/*
/static/images/wow/icons/medium/* /static/images/wow/icons/medium/*
/static/images/wow/icons/small/* /static/images/wow/icons/small/*
/static/images/wow/icons/tiny/* /static/images/wow/icons/tiny/*
!/static/images/wow/icons/tiny/quest_* !/static/images/wow/icons/tiny/quest_*
/static/images/wow/hunterpettalents/* /static/images/wow/hunterpettalents/icons*
/static/images/wow/Interface/* /static/images/wow/interface/*
/static/images/wow/loadingscreens/* /static/images/wow/loadingscreens/*
/static/images/wow/maps/* /static/images/wow/maps/*
!/static/images/wow/maps/overlay* !/static/images/wow/maps/overlay*
/static/images/wow/talents/icons/* /static/images/wow/talents/icons/*
/static/images/wow/talents/backgrounds/*
# modelviewer (~7GB data) # modelviewer (~7GB data)
/static/modelviewer/models/* /static/modelviewer/models/*

6
.gitmodules vendored Normal file
View File

@@ -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

View File

@@ -1,19 +1,11 @@
Order Deny,Allow Order Deny,Allow
<FilesMatch "\.(conf|php|in)$"> <FilesMatch "\.(conf|php|in)|aowow$">
Deny from all Deny from all
</FilesMatch> </FilesMatch>
<FilesMatch "^(index)\.php$"> <FilesMatch "^(index)\.php$">
Allow from all Allow from all
</FilesMatch> </FilesMatch>
<Files "aowow">
ForceType application/x-httpd-php
</Files>
<Files "prQueue">
ForceType application/x-httpd-php
</Files>
# Block view of some folders # Block view of some folders
Options -Indexes Options -Indexes
DirectoryIndex index.php DirectoryIndex index.php
@@ -21,13 +13,13 @@ DirectoryIndex index.php
# Support for UTF8 # Support for UTF8
AddDefaultCharset utf8 AddDefaultCharset utf8
<IfModule mod_charset.c> <IfModule mod_charset.c>
CharsetDisable on CharsetDisable on
CharsetRecodeMultipartForms Off CharsetRecodeMultipartForms Off
</IfModule> </IfModule>
# UHD screenshots can get pretty large (cannot be set in config) # 5MB should be enough for the largest screenshots in the land
php_value upload_max_filesize 20M php_value default_charset UTF-8
php_value post_max_size 25M php_value upload_max_filesize 5M
RewriteEngine on RewriteEngine on
# RewriteBase /~user/localPath/ # enable if the rules do not work, when they should # RewriteBase /~user/localPath/ # enable if the rules do not work, when they should

BIN
README Normal file

Binary file not shown.

140
README.md
View File

@@ -1,140 +0,0 @@
![logo](static/images/logos/home.png)
## Build Status
![fuck it ship it](http://forthebadge.com/images/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)
+ [GNU Multiple Precision](https://www.php.net/manual/en/book.gmp.php) (When using TrinityCore as auth source)
+ MySQL ≥ 5.7.0 OR MariaDB ≥ 10.6.4 OR similar
+ [TDB 335.21101](https://github.com/TrinityCore/TrinityCore/releases/tag/TDB335.21101) (no other other providers are supported at this time)
+ 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 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 database you are going to reference.
Import `setup/db_structure.sql` into the AoWoW database `mysql -p {your-db-here} < setup/db_structure.sql`
#### 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:
> \<localeCode>/DBFilesClient/
> \<localeCode>/Interface/WorldMap/
> \<localeCode>/Interface/FrameXML/GlobalStrings.lua
.. once is enough (still apply the localeCode though):
> \<localeCode>/Interface/TalentFrame/
> \<localeCode>/Interface/Icons/
> \<localeCode>/Interface/Spellbook/
> \<localeCode>/Interface/PaperDoll/
> \<localeCode>/Interface/Glues/CharacterCreate/
> \<localeCode>/Interface/Pictures
> \<localeCode>/Interface/PvPRankBadges
> \<localeCode>/Interface/FlavorImages
> \<localeCode>/Interface/Calendar/Holidays/
> \<localeCode>/Sound/
.. optionally (not used in AoWoW):
> \<localeCode>/Interface/Glues/Loadingscreens/
> \<localeCode>/Interface/Glues/Credits/
#### 5. Reencode the audio files
WAV-files need to be reencoded as `ogg/vorbis` and some MP3s may identify themselves as `application/octet-stream` instead of `audio/mpeg`.
* [example for WIN](https://gist.github.com/Sarjuuk/d77b203f7b71d191509afddabad5fc9f)
* [example for \*nix](https://gist.github.com/Sarjuuk/1f05ef2affe49a7e7ca0fad7b01c081d)
#### 6. Run the initial setup from the CLI
`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 --siteconfig`
Q: Fatal error: Can't inherit abstract function \<functionName> (previously declared abstract in \<className>) in \<path>
A: You are using 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, some more exotic 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](http://forthebadge.com/images/badges/uses-badges.svg)

17
aowow Executable file → Normal file
View File

@@ -1,15 +1,12 @@
#!/usr/bin/env php
<?php <?php
if (PHP_SAPI !== 'cli') require 'includes/shared.php';
if (!CLI)
die("this script must be run from CLI\n"); die("this script must be run from CLI\n");
if (PHP_SAPI === 'cli' && getcwd().DIRECTORY_SEPARATOR.'aowow' != __FILE__) if (CLI && getcwd().DIRECTORY_SEPARATOR.'aowow' != __FILE__)
die("this script must be run from the aowow root directory\n"); die("this script must be run from root directory\n");
else
require_once 'includes/kernel.php'; require 'setup/setup.php';
require_once 'includes/setup/cli.class.php';
require_once 'includes/setup/timer.class.php';
require_once 'setup/setup.php';
?> ?>

49
config/config.php.in Normal file
View File

@@ -0,0 +1,49 @@
<?php
if (!defined('AOWOW_REVISION'))
die('illegal access');
// use as example. File is generated from setup.
// -- Aowow Database --
// contains world-data, user-data and logs
$AoWoWconf['aowow'] = array(
'host' => '127.0.0.1',
'user' => '<user>',
'pass' => '<pass>',
'db' => 'world',
'prefix' => 'aowow_'
);
// -- World Database --
// used to generate data-tables
$AoWoWconf['world'] = array(
'host' => '127.0.0.1',
'user' => '<user>',
'pass' => '<pass>',
'db' => 'world',
'prefix' => ''
);
// -- Auth Database --
// used to generate user-tables
$AoWoWconf['auth'] = array(
'host' => '127.0.0.1',
'user' => '<user>',
'pass' => '<pass>',
'db' => 'auth',
'prefix' => ''
);
// -- Characters Database --
// used to display profiles
$AoWoWconf['characters'][<realmId>] = array(
'host' => '127.0.0.1',
'user' => '<user>',
'pass' => '<pass>',
'db' => 'characters',
'prefix' => ''
);
?>

View File

@@ -4,16 +4,13 @@ if (!defined('AOWOW_REVISION'))
die('illegal access'); die('illegal access');
function extAuth(string &$usernameOrEmail, string $password, int &$userId = 0, int &$userGroup = -1) : int function extAuth($user, $pass, &$userId = 0)
{ {
/* /*
insert some auth mechanism here insert some auth mechanism here
set usernameOrEmail to a valid username, do not pass back the email if used for login see defines for usable return values
set userId to uid from external auth provider for identification set userId for identification
(optional) set userGroup to a valid userGroup (see U_GROUP_* defines)
return an AUTH_* result (see defines)
*/ */
return AUTH_INTERNAL_ERR; return AUTH_INTERNAL_ERR;

74
datasets/weight-presets Normal file
View File

@@ -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}
}
}
};

46
datasets/zones Normal file
View File

@@ -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
*/

File diff suppressed because it is too large Load Diff

View File

@@ -1,173 +0,0 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
class AjaxAccount extends AjaxHandler
{
protected $validParams = ['exclude', 'weightscales', 'favorites'];
protected $_post = array(
'groups' => ['filter' => FILTER_SANITIZE_NUMBER_INT],
'save' => ['filter' => FILTER_SANITIZE_NUMBER_INT],
'delete' => ['filter' => FILTER_SANITIZE_NUMBER_INT],
'id' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\AjaxHandler::checkIdList'],
'name' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\AjaxAccount::checkName' ],
'scale' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\AjaxAccount::checkScale' ],
'reset' => ['filter' => FILTER_SANITIZE_NUMBER_INT],
'mode' => ['filter' => FILTER_SANITIZE_NUMBER_INT],
'type' => ['filter' => FILTER_SANITIZE_NUMBER_INT],
'add' => ['filter' => FILTER_SANITIZE_NUMBER_INT],
'remove' => ['filter' => FILTER_SANITIZE_NUMBER_INT],
// 'sessionKey' => ['filter' => FILTER_SANITIZE_NUMBER_INT]
);
public function __construct(array $params)
{
parent::__construct($params);
if (!$this->params || !User::isLoggedIn())
return;
// select handler
if ($this->params[0] == 'exclude')
$this->handler = 'handleExclude';
else if ($this->params[0] == 'weightscales')
$this->handler = 'handleWeightscales';
else if ($this->params[0] == 'favorites')
$this->handler = 'handleFavorites';
}
protected function handleExclude() : void
{
if ($this->_post['mode'] == 1) // directly set exludes
{
$type = $this->_post['type'];
$ids = $this->_post['id'];
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` = ?d AND `typeId` IN (?a)', $this->_post['type'], $validIds);
foreach ($validIds as $typeId)
DB::Aowow()->query('INSERT INTO ?_account_excludes (`userId`, `type`, `typeId`, `mode`) VALUES (?a) ON DUPLICATE KEY UPDATE `mode` = (`mode` ^ 0x3)',
[User::$id, $this->_post['type'], $typeId, in_array($typeId, $includes) ? 2 : 1]
);
}
else
trigger_error('AjaxAccount::handleExclude - invalid type #'.$type.(empty($ids) ? ' or id-list empty' : ''), E_USER_ERROR);
return;
}
else if ($this->_post['reset'] == 1) // defaults to unavailable
{
$mask = PR_EXCLUDE_GROUP_UNAVAILABLE;
DB::Aowow()->query('DELETE FROM ?_account_excludes WHERE userId = ?d', User::$id);
}
else // clamp to real groups
$mask = $this->_post['groups'] & PR_EXCLUDE_GROUP_ANY;
DB::Aowow()->query('UPDATE ?_account SET excludeGroups = ?d WHERE id = ?d', $mask, User::$id);
}
protected function handleWeightscales() : string
{
if ($this->_post['save'])
{
if (!$this->_post['scale'])
{
trigger_error('AjaxAccount::handleWeightscales - scaleId empty', E_USER_ERROR);
return '0';
}
$id = 0;
if ($this->_post['id'] && ($id = $this->_post['id'][0]))
{
if (!DB::Aowow()->selectCell('SELECT 1 FROM ?_account_weightscales WHERE `userId` = ?d AND `id` = ?d', User::$id, $id))
{
trigger_error('AjaxAccount::handleWeightscales - scale #'.$id.' not in db or owned by user #'.User::$id, E_USER_ERROR);
return '0';
}
DB::Aowow()->query('UPDATE ?_account_weightscales SET `name` = ? WHERE `id` = ?d', $this->_post['name'], $id);
}
else
{
$nScales = DB::Aowow()->selectCell('SELECT COUNT(`id`) FROM ?_account_weightscales WHERE `userId` = ?d', User::$id);
if ($nScales >= 5) // more or less hard-defined in LANG.message_weightscalesaveerror
return '0';
$id = DB::Aowow()->query('INSERT INTO ?_account_weightscales (`userId`, `name`) VALUES (?d, ?)', User::$id, $this->_post['name']);
}
DB::Aowow()->query('DELETE FROM ?_account_weightscale_data WHERE `id` = ?d', $id);
foreach (explode(',', $this->_post['scale']) as $s)
{
[$k, $v] = explode(':', $s);
if (!in_array($k, Util::$weightScales) || $v < 1)
continue;
DB::Aowow()->query('INSERT INTO ?_account_weightscale_data VALUES (?d, ?, ?d)', $id, $k, $v);
}
return (string)$id;
}
else if ($this->_post['delete'] && $this->_post['id'] && $this->_post['id'][0])
{
DB::Aowow()->query('DELETE FROM ?_account_weightscales WHERE `id` = ?d AND `userId` = ?d', $this->_post['id'][0], User::$id);
return '';
}
trigger_error('AjaxAccount::handleWeightscales - malformed request received', E_USER_ERROR);
return '0';
}
protected function handleFavorites() : void
{
// omit usage of sessionKey
if (count($this->_post['id']) != 1 || empty($this->_post['id'][0]))
{
trigger_error('AjaxAccount::handleFavorites - malformed request received', E_USER_ERROR);
return;
}
$typeId = $this->_post['id'][0];
if ($type = $this->_post['add'])
{
if (!Type::validateIds($this->_post['add'], $this->_post['id']))
{
trigger_error('AjaxAccount::handleFavorites - invalid typeId #'.$typeId.' for type #'.$type, E_USER_ERROR);
return;
}
DB::Aowow()->query('INSERT INTO ?_account_favorites (`userId`, `type`, `typeId`) VALUES (?d, ?d, ?d)', User::$id, $type, $typeId);
}
else if ($type = $this->_post['remove'])
DB::Aowow()->query('DELETE FROM ?_account_favorites WHERE `userId` = ?d AND `type` = ?d AND `typeId` = ?d', User::$id, $type, $typeId);
}
protected static function checkScale(string $val) : string
{
if (preg_match('/^((\w+:\d+)(,\w+:\d+)*)$/', $val))
return $val;
return '';
}
protected static function checkName(string $val) : string
{
$var = trim(urldecode($val));
return filter_var($var, FILTER_SANITIZE_SPECIAL_CHARS, FILTER_FLAG_STRIP_LOW);
}
}
?>

View File

@@ -1,557 +0,0 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
class AjaxAdmin extends AjaxHandler
{
protected $validParams = ['screenshots', 'siteconfig', 'weight-presets', 'spawn-override', 'guide', 'comment'];
protected $_get = array(
'action' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\AjaxHandler::checkTextLine' ],
'id' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\AjaxHandler::checkIdListUnsigned'],
'key' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\AjaxAdmin::checkKey' ],
'all' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\AjaxHandler::checkEmptySet' ],
'type' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\AjaxHandler::checkInt' ],
'typeid' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\AjaxHandler::checkInt' ],
'user' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\AjaxAdmin::checkUser' ],
'val' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\AjaxHandler::checkTextBlob' ],
'guid' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\AjaxHandler::checkInt' ],
'area' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\AjaxHandler::checkInt' ],
'floor' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\AjaxHandler::checkInt' ]
);
protected $_post = array(
'alt' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\AjaxHandler::checkTextBlob'],
'id' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\AjaxHandler::checkInt' ],
'scale' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\AjaxAdmin::checkScale' ],
'__icon' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\AjaxAdmin::checkKey' ],
'status' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\AjaxHandler::checkInt' ],
'msg' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\AjaxHandler::checkTextBlob']
);
public function __construct(array $params)
{
parent::__construct($params);
if (!$this->params)
return;
if ($this->params[0] == 'screenshots' && $this->_get['action'])
{
if (!User::isInGroup(U_GROUP_ADMIN | U_GROUP_BUREAU | U_GROUP_SCREENSHOT))
return;
if ($this->_get['action'] == 'list')
$this->handler = 'ssList';
else if ($this->_get['action'] == 'manage')
$this->handler = 'ssManage';
else if ($this->_get['action'] == 'editalt')
$this->handler = 'ssEditAlt';
else if ($this->_get['action'] == 'approve')
$this->handler = 'ssApprove';
else if ($this->_get['action'] == 'sticky')
$this->handler = 'ssSticky';
else if ($this->_get['action'] == 'delete')
$this->handler = 'ssDelete';
else if ($this->_get['action'] == 'relocate')
$this->handler = 'ssRelocate';
}
else if ($this->params[0] == 'siteconfig' && $this->_get['action'])
{
if (!User::isInGroup(U_GROUP_DEV | U_GROUP_ADMIN))
return;
if ($this->_get['action'] == 'add')
$this->handler = 'confAdd';
else if ($this->_get['action'] == 'remove')
$this->handler = 'confRemove';
else if ($this->_get['action'] == 'update')
$this->handler = 'confUpdate';
}
else if ($this->params[0] == 'weight-presets' && $this->_get['action'])
{
if (!User::isInGroup(U_GROUP_DEV | U_GROUP_ADMIN | U_GROUP_BUREAU))
return;
if ($this->_get['action'] == 'save')
$this->handler = 'wtSave';
}
else if ($this->params[0] == 'spawn-override')
{
if (!User::isInGroup(U_GROUP_MODERATOR))
return;
$this->handler = 'spawnPosFix';
}
else if ($this->params[0] == 'guide')
{
if (!User::isInGroup(U_GROUP_STAFF))
return;
$this->handler = 'guideManage';
}
else if ($this->params[0] == 'comment')
{
if (!User::isInGroup(U_GROUP_ADMIN | U_GROUP_BUREAU | U_GROUP_MOD))
return;
$this->handler = 'commentOutOfDate';
}
}
// get all => null (optional)
// evaled response .. UNK
protected function ssList() : string
{
// ssm_screenshotPages
// ssm_numPagesFound
$pages = CommunityContent::getScreenshotPagesForManager($this->_get['all'], $nPages);
$buff = 'ssm_screenshotPages = '.Util::toJSON($pages).";\n";
$buff .= 'ssm_numPagesFound = '.$nPages.';';
return $buff;
}
// get: [type => type, typeId => typeId] || [user => username]
// evaled response .. UNK
protected function ssManage() : string
{
$res = [];
if ($this->_get['type'] && $this->_get['type'] && $this->_get['typeid'] && $this->_get['typeid'])
$res = CommunityContent::getScreenshotsForManager($this->_get['type'], $this->_get['typeid']);
else if ($this->_get['user'])
if ($uId = DB::Aowow()->selectCell('SELECT `id` FROM ?_account WHERE LOWER(`username`) = LOWER(?)', $this->_get['user']))
$res = CommunityContent::getScreenshotsForManager(0, 0, $uId);
return 'ssm_screenshotData = '.Util::toJSON($res);
}
// get: id => SSid
// resp: ''
protected function ssEditAlt() : void
{
// doesn't need to be htmlEscaped, ths javascript does that
if ($this->_get['id'] && $this->_post['alt'] !== null)
DB::Aowow()->query('UPDATE ?_screenshots SET caption = ? WHERE id = ?d', trim($this->_post['alt']), $this->_get['id'][0]);
}
// get: id => comma-separated SSids
// resp: ''
protected function ssApprove() : void
{
if (!$this->reqGET('id'))
{
trigger_error('AjaxAdmin::ssApprove - screenshotId empty', E_USER_ERROR);
return;
}
// create resized and thumb version of screenshot
$resized = [772, 618];
$thumb = [150, 150];
$path = 'static/uploads/screenshots/%s/%d.jpg';
foreach ($this->_get['id'] as $id)
{
// must not be already approved
if ($ssEntry = DB::Aowow()->selectRow('SELECT userIdOwner, date, type, typeId FROM ?_screenshots WHERE (status & ?d) = 0 AND id = ?d', CC_FLAG_APPROVED, $id))
{
// should also error-log
if (!file_exists(sprintf($path, 'pending', $id)))
{
trigger_error('AjaxAdmin::ssApprove - screenshot #'.$id.' exists in db but not as file', E_USER_ERROR);
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($ssEntry['userIdOwner'], SITEREP_ACTION_UPLOAD, ['id' => $id, 'what' => 1, 'date' => $ssEntry['date']]);
// flag DB entry as having screenshots
if ($tbl = Type::getClassAttrib($ssEntry['type'], 'dataTable'))
DB::Aowow()->query('UPDATE '.$tbl.' SET cuFlags = cuFlags | ?d WHERE id = ?d', CUSTOM_HAS_SCREENSHOT, $ssEntry['typeId']);
}
else
trigger_error('AjaxAdmin::ssApprove - screenshot #'.$id.' not in db or already approved', E_USER_ERROR);
}
return;
}
// get: id => comma-separated SSids
// resp: ''
protected function ssSticky() : void
{
if (!$this->reqGET('id'))
{
trigger_error('AjaxAdmin::ssSticky - screenshotId empty', E_USER_ERROR);
return;
}
// approve soon to be sticky screenshots
$this->ssApprove();
// this one is a bit strange: as far as i've seen, the only thing a 'sticky' screenshot does is show up in the infobox
// this also means, that only one screenshot per page should be sticky
// so, handle it one by one and the last one affecting one particular type/typId-key gets the cake
foreach ($this->_get['id'] as $id)
{
// reset all others
DB::Aowow()->query('UPDATE ?_screenshots a, ?_screenshots b SET a.status = a.status & ~?d WHERE a.type = b.type AND a.typeId = b.typeId AND a.id <> b.id AND b.id = ?d', CC_FLAG_STICKY, $id);
// toggle sticky status
DB::Aowow()->query('UPDATE ?_screenshots SET `status` = IF(`status` & ?d, `status` & ~?d, `status` | ?d) WHERE id = ?d AND `status` & ?d', CC_FLAG_STICKY, CC_FLAG_STICKY, CC_FLAG_STICKY, $id, CC_FLAG_APPROVED);
}
}
// get: id => comma-separated SSids
// resp: ''
// 2 steps: 1) remove from sight, 2) remove from disk
protected function ssDelete() : void
{
if (!$this->reqGET('id'))
{
trigger_error('AjaxAdmin::ssDelete - screenshotId empty', E_USER_ERROR);
return;
}
$path = 'static/uploads/screenshots/%s/%d.jpg';
foreach ($this->_get['id'] as $id)
{
// irrevocably remove already deleted files
if (User::isInGroup(U_GROUP_ADMIN) && DB::Aowow()->selectCell('SELECT 1 FROM ?_screenshots WHERE status & ?d AND id = ?d', CC_FLAG_DELETED, $id))
{
DB::Aowow()->query('DELETE FROM ?_screenshots WHERE id = ?d', $id);
if (file_exists(sprintf($path, 'pending', $id)))
unlink(sprintf($path, 'pending', $id));
continue;
}
// move pending or normal to pending
if (file_exists(sprintf($path, 'normal', $id)))
rename(sprintf($path, 'normal', $id), sprintf($path, 'pending', $id));
// remove resized and thumb
if (file_exists(sprintf($path, 'thumb', $id)))
unlink(sprintf($path, 'thumb', $id));
if (file_exists(sprintf($path, 'resized', $id)))
unlink(sprintf($path, 'resized', $id));
}
// flag as deleted if not aready
$oldEntries = DB::Aowow()->selectCol('SELECT `type` AS ARRAY_KEY, GROUP_CONCAT(typeId) FROM ?_screenshots WHERE id IN (?a) GROUP BY `type`', $this->_get['id']);
DB::Aowow()->query('UPDATE ?_screenshots SET status = ?d, userIdDelete = ?d WHERE id IN (?a)', CC_FLAG_DELETED, User::$id, $this->_get['id']);
// deflag db entry as having screenshots
foreach ($oldEntries as $type => $typeIds)
{
$typeIds = explode(',', $typeIds);
$toUnflag = DB::Aowow()->selectCol('SELECT typeId AS ARRAY_KEY, IF(BIT_OR(`status`) & ?d, 1, 0) AS hasMore FROM ?_screenshots WHERE `type` = ?d AND typeId IN (?a) GROUP BY typeId HAVING hasMore = 0', CC_FLAG_APPROVED, $type, $typeIds);
if ($toUnflag && ($tbl = Type::getClassAttrib($type, 'dataTable')))
DB::Aowow()->query('UPDATE '.$tbl.' SET cuFlags = cuFlags & ~?d WHERE id IN (?a)', CUSTOM_HAS_SCREENSHOT, array_keys($toUnflag));
}
}
// get: id => ssId, typeid => typeId (but not type..?)
// resp: ''
protected function ssRelocate() : void
{
if (!$this->reqGET('id', 'typeid'))
{
trigger_error('AjaxAdmin::ssRelocate - screenshotId or typeId empty', E_USER_ERROR);
return;
}
$id = $this->_get['id'][0];
[$type, $oldTypeId] = array_values(DB::Aowow()->selectRow('SELECT type, typeId FROM ?_screenshots WHERE id = ?d', $id));
$typeId = (int)$this->_get['typeid'];
$tc = Type::newList($type, [['id', $typeId]]);
if ($tc && !$tc->error)
{
// move screenshot
DB::Aowow()->query('UPDATE ?_screenshots SET typeId = ?d WHERE id = ?d', $typeId, $id);
// flag target as having screenshot
DB::Aowow()->query('UPDATE '.$tc::$dataTable.' SET cuFlags = cuFlags | ?d WHERE id = ?d', CUSTOM_HAS_SCREENSHOT, $typeId);
// deflag source for having had screenshots (maybe)
$ssInfo = DB::Aowow()->selectRow('SELECT IF(BIT_OR(~status) & ?d, 1, 0) AS hasMore FROM ?_screenshots WHERE `status`& ?d AND `type` = ?d AND typeId = ?d', CC_FLAG_DELETED, CC_FLAG_APPROVED, $type, $oldTypeId);
if($ssInfo || !$ssInfo['hasMore'])
DB::Aowow()->query('UPDATE '.$tc::$dataTable.' SET cuFlags = cuFlags & ~?d WHERE id = ?d', CUSTOM_HAS_SCREENSHOT, $oldTypeId);
}
else
trigger_error('AjaxAdmin::ssRelocate - invalid typeId #'.$typeId.' for type #'.$type, E_USER_ERROR);
}
protected function confAdd() : string
{
$key = trim($this->_get['key']);
$val = trim(urldecode($this->_get['val']));
return Cfg::add($key, $val);
}
protected function confRemove() : string
{
if (!$this->reqGET('key'))
return 'invalid configuration option given';
return Cfg::delete($this->_get['key']);
}
protected function confUpdate() : string
{
$key = trim($this->_get['key']);
$val = trim(urldecode($this->_get['val']));
return Cfg::set($key, $val);
}
protected function wtSave() : string
{
if (!$this->reqPOST('id', '__icon'))
return '3';
// save to db
DB::Aowow()->query('DELETE FROM ?_account_weightscale_data WHERE id = ?d', $this->_post['id']);
DB::Aowow()->query('UPDATE ?_account_weightscales SET `icon`= ? WHERE `id` = ?d', $this->_post['__icon'], $this->_post['id']);
foreach (explode(',', $this->_post['scale']) as $s)
{
[$k, $v] = explode(':', $s);
if (!in_array($k, Util::$weightScales) || $v < 1)
continue;
if (DB::Aowow()->query('INSERT INTO ?_account_weightscale_data VALUES (?d, ?, ?d)', $this->_post['id'], $k, $v) === null)
return '1';
}
// write dataset
exec('php aowow --build=weightPresets', $out);
foreach ($out as $o)
if (strstr($o, 'ERR'))
return '2';
// all done
return '0';
}
protected function spawnPosFix() : string
{
if (!$this->reqGET('type', 'guid', 'area', 'floor'))
return '-4';
$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]))
return '-3';
DB::Aowow()->query('REPLACE INTO ?_spawns_override VALUES (?d, ?d, ?d, ?d, ?d)', $type, $guid, $area, $floor, AOWOW_REVISION);
if ($wPos = WorldPosition::getForGUID($type, $guid))
{
if ($point = WorldPosition::toZonePos($wPos[$guid]['mapId'], $wPos[$guid]['posX'], $wPos[$guid]['posY'], $area, $floor))
{
$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)
{
$jobs = array(
'SELECT -w.id AS `entry`, w.point AS `pointId`, w.position_x AS `posX`, w.position_y AS `posY` FROM creature_addon ca JOIN waypoint_data w ON w.id = ca.path_id WHERE ca.guid = ?d AND ca.path_id <> 0',
'SELECT `entry`, `pointId`, `location_x` AS `posX`, `location_y` AS `posY` FROM `script_waypoint` WHERE `entry` = ?d',
'SELECT `entry`, `pointId`, `position_x` AS `posX`, `position_y` AS `posY` FROM `waypoints` WHERE `entry` = ?d'
);
foreach ($jobs as $idx => $job)
{
if ($swp = DB::World()->select($job, $idx ? $wPos[$guid]['id'] : $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()->query('UPDATE ?_creature_waypoints SET ?a WHERE `creatureOrPath` = ?d AND `point` = ?d', $p, $w['entry'], $w['pointId']);
}
}
}
}
// also move linked vehicle accessories (on the very same position)
$updGUIDs = array_merge($updGUIDs, DB::Aowow()->selectCol('SELECT s2.guid FROM ?_spawns s1 JOIN ?_spawns s2 ON s1.posX = s2.posX AND s1.posY = s2.posY AND
s1.areaId = s2.areaId AND s1.floor = s2.floor AND s2.guid < 0 WHERE s1.guid = ?d', $guid));
}
DB::Aowow()->query('UPDATE ?_spawns SET ?a WHERE `type` = ?d AND `guid` IN (?a)', $newPos, $type, $updGUIDs);
return '1';
}
return '-2';
}
return '-1';
}
protected function guideManage() : string
{
$update = function (int $id, int $status, ?string $msg = null) : bool
{
if (!DB::Aowow()->query('UPDATE ?_guides SET `status` = ?d WHERE `id` = ?d', $status, $id))
return false;
// set display rev to latest
if ($status == GUIDE_STATUS_APPROVED)
DB::Aowow()->query('UPDATE ?_guides SET `rev` = (SELECT `rev` FROM ?_articles WHERE `type` = ?d AND `typeId` = ?d ORDER BY `rev` DESC LIMIT 1), `approveUserId` = ?d, `approveDate` = ?d WHERE `id` = ?d', Type::GUIDE, $id, User::$id, time(), $id);
DB::Aowow()->query('INSERT INTO ?_guides_changelog (`id`, `date`, `userId`, `status`) VALUES (?d, ?d, ?d, ?d)', $id, time(), User::$id, $status);
if ($msg)
DB::Aowow()->query('INSERT INTO ?_guides_changelog (`id`, `date`, `userId`, `msg`) VALUES (?d, ?d, ?d, ?)', $id, time(), User::$id, $msg);
return true;
};
if (!$this->_post['id'])
trigger_error('AjaxHander::guideManage - malformed request: id: '.$this->_post['id'].', status: '.$this->_post['status']);
else
{
$guide = DB::Aowow()->selectRow('SELECT `userId`, `status` FROM ?_guides WHERE `id` = ?d', $this->_post['id']);
if (!$guide)
trigger_error('AjaxHander::guideManage - guide #'.$this->_post['id'].' not found');
else
{
if ($this->_post['status'] == $guide['status'])
trigger_error('AjaxHander::guideManage - guide #'.$this->_post['id'].' already has status #'.$this->_post['status']);
else
{
if ($this->_post['status'] == GUIDE_STATUS_APPROVED)
{
if ($update($this->_post['id'], GUIDE_STATUS_APPROVED, $this->_post['msg']))
{
Util::gainSiteReputation($guide['userId'], SITEREP_ACTION_ARTICLE, ['id' => $this->_post['id']]);
return '1';
}
else
return '-2';
}
else if ($this->_post['status'] == GUIDE_STATUS_REJECTED)
return $update($this->_post['id'], GUIDE_STATUS_REJECTED, $this->_post['msg']) ? '1' : '-2';
else
trigger_error('AjaxHander::guideManage - unhandled status change request');
}
}
}
return '-1';
}
protected function commentOutOfDate() : string
{
$ok = false;
switch ($this->_post['status'])
{
case 0: // up to date
if ($ok = DB::Aowow()->query('UPDATE ?_comments SET `flags` = `flags` & ~?d WHERE `id` = ?d', 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);
break;
case 1: // outdated, mark as deleted and clear other flags (sticky + outdated)
if ($ok = DB::Aowow()->query('UPDATE ?_comments SET `flags` = ?d, `deleteUserId` = ?d, `deleteDate` = ?d WHERE `id` = ?d', CC_FLAG_DELETED, User::$id, time(), $this->_post['id']))
if ($rep = new Report(Report::MODE_COMMENT, Report::CO_OUT_OF_DATE, $this->_post['id']))
$rep->close(Report::STATUS_CLOSED_SOLVED);
break;
default:
trigger_error('AjaxHandler::comentOutOfDate - called with invalid status');
}
return $ok ? '1' : '0';
}
/***************************/
/* additional input filter */
/***************************/
protected static function checkKey(string $val) : string
{
// expecting string
if (preg_match(Cfg::PATTERN_INV_CONF_KEY, $val))
return '';
return strtolower($val);
}
protected static function checkUser($val) : string
{
$n = Util::lower(trim(urldecode($val)));
if (User::isValidName($n))
return $n;
return '';
}
protected static function checkScale($val) : string
{
if (preg_match('/^((\w+:\d+)(,\w+:\d+)*)$/', $val))
return $val;
return '';
}
}
?>

View File

@@ -1,71 +0,0 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
class AjaxHandler
{
use TrRequestData;
protected $validParams = [];
protected $params = [];
protected $handler;
protected $contentType = MIME_TYPE_JSON;
public $doRedirect = false;
public function __construct(array $params)
{
$this->params = $params;
$this->initRequestData();
}
public function handle(string &$out) : bool
{
if (!$this->handler)
return false;
if ($this->validParams)
{
if (count($this->params) != 1)
return false;
if (!in_array($this->params[0], $this->validParams))
return false;
}
$out = $this->{$this->handler}() ?? '';
return true;
}
public function getContentType() : string
{
return $this->contentType;
}
protected function reqPOST(string ...$keys) : bool
{
foreach ($keys as $k)
if (!isset($this->_post[$k]) || $this->_post[$k] === null || $this->_post[$k] === '')
return false;
return true;
}
protected function reqGET(string ...$keys) : bool
{
foreach ($keys as $k)
if (!isset($this->_get[$k]) || $this->_get[$k] === null || $this->_get[$k] === '')
return false;
return true;
}
}
?>

View File

@@ -1,83 +0,0 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
class AjaxArenaTeam extends AjaxHandler
{
protected $validParams = ['resync', 'status'];
protected $_get = array(
'id' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\AjaxHandler::checkIdList' ],
'profile' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\AjaxHandler::checkEmptySet'],
);
public function __construct(array $params)
{
parent::__construct($params);
if (!$this->params)
return;
switch ($this->params[0])
{
case 'resync':
$this->handler = 'handleResync';
break;
case 'status':
$this->handler = 'handleStatus';
break;
}
}
/* params
id: <prId1,prId2,..,prIdN>
user: <string> [optional, not used]
profile: <empty> [optional, also get related chars]
return: 1
*/
protected function handleResync() : string
{
if ($teams = DB::Aowow()->select('SELECT realm, realmGUID FROM ?_profiler_arena_team WHERE id IN (?a)', $this->_get['id']))
foreach ($teams as $t)
Profiler::scheduleResync(Type::ARENA_TEAM, $t['realm'], $t['realmGUID']);
if ($this->_get['profile'])
if ($chars = DB::Aowow()->select('SELECT realm, realmGUID FROM ?_profiler_profiles p JOIN ?_profiler_arena_team_member atm ON atm.profileId = p.id WHERE atm.arenaTeamId IN (?a)', $this->_get['id']))
foreach ($chars as $c)
Profiler::scheduleResync(Type::PROFILE, $c['realm'], $c['realmGUID']);
return '1';
}
/* params
id: <prId1,prId2,..,prIdN>
return
<status object>
[
nQueueProcesses,
[statusCode, timeToRefresh, curQueuePos, errorCode, nResyncTries],
[<anotherStatus>]
...
]
not all fields are required, if zero they are omitted
statusCode:
0: end the request
1: waiting
2: working...
3: ready; click to view
4: error / retry
errorCode:
0: unk error
1: char does not exist
2: armory gone
*/
protected function handleStatus() : string
{
return Profiler::resyncStatus(Type::ARENA_TEAM, $this->_get['id']);
}
}
?>

View File

@@ -1,485 +0,0 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
class AjaxComment extends AjaxHandler
{
const COMMENT_LENGTH_MIN = 10;
const COMMENT_LENGTH_MAX = 7500;
const REPLY_LENGTH_MIN = 15;
const REPLY_LENGTH_MAX = 600;
protected $_post = array(
'id' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\AjaxHandler::checkIdListUnsigned'],
'body' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\AjaxHandler::checkTextBlob' ],
'commentbody' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\AjaxHandler::checkTextBlob' ],
'response' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\AjaxHandler::checkTextBlob' ],
'reason' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\AjaxHandler::checkTextBlob' ],
'remove' => ['filter' => FILTER_SANITIZE_NUMBER_INT ],
'commentId' => ['filter' => FILTER_SANITIZE_NUMBER_INT ],
'replyId' => ['filter' => FILTER_SANITIZE_NUMBER_INT ],
'sticky' => ['filter' => FILTER_SANITIZE_NUMBER_INT ],
// 'username' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\AjaxHandler::checkTextLine' ]
);
protected $_get = array(
'id' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\AjaxHandler::checkInt'],
'type' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\AjaxHandler::checkInt'],
'typeid' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\AjaxHandler::checkInt'],
'rating' => ['filter' => FILTER_SANITIZE_NUMBER_INT ]
);
public function __construct(array $params)
{
parent::__construct($params);
if (!$this->params || count($this->params) != 1)
return;
// note: return values must be formated as STRICT json!
// select handler
if ($this->params[0] == 'add')
$this->handler = 'handleCommentAdd';
else if ($this->params[0] == 'edit')
$this->handler = 'handleCommentEdit';
else if ($this->params[0] == 'delete')
$this->handler = 'handleCommentDelete';
else if ($this->params[0] == 'undelete')
$this->handler = 'handleCommentUndelete';
else if ($this->params[0] == 'rating') // up/down - distribution
$this->handler = 'handleCommentRating';
else if ($this->params[0] == 'vote') // up, down and remove
$this->handler = 'handleCommentVote';
else if ($this->params[0] == 'sticky') // toggle flag
$this->handler = 'handleCommentSticky';
else if ($this->params[0] == 'out-of-date') // toggle flag
$this->handler = 'handleCommentOutOfDate';
else if ($this->params[0] == 'show-replies')
$this->handler = 'handleCommentShowReplies';
else if ($this->params[0] == 'add-reply') // also returns all replies on success
$this->handler = 'handleReplyAdd';
else if ($this->params[0] == 'edit-reply') // also returns all replies on success
$this->handler = 'handleReplyEdit';
else if ($this->params[0] == 'detach-reply')
$this->handler = 'handleReplyDetach';
else if ($this->params[0] == 'delete-reply')
$this->handler = 'handleReplyDelete';
else if ($this->params[0] == 'flag-reply')
$this->handler = 'handleReplyFlag';
else if ($this->params[0] == 'upvote-reply')
$this->handler = 'handleReplyUpvote';
else if ($this->params[0] == 'downvote-reply')
$this->handler = 'handleReplyDownvote';
}
// i .. have problems believing, that everything uses nifty ajax while adding comments requires a brutal header(Loacation: <wherever>), yet, thats how it is
protected function handleCommentAdd() : string
{
if (!$this->_get['typeid'] || !$this->_get['type'] || !Type::exists($this->_get['type']))
{
trigger_error('AjaxComment::handleCommentAdd - malforemd request received', E_USER_ERROR);
return ''; // whatever, we cant even send him back
}
// this type cannot be commented on
if (!Type::checkClassAttrib($this->_get['type'], 'contribute', CONTRIBUTE_CO))
{
trigger_error('AjaxComment::handleCommentAdd - tried to comment on unsupported type #'.$this->_get['type'], E_USER_ERROR);
return '';
}
// trim to max length
if (!User::isInGroup(U_GROUP_MODERATOR) && mb_strlen($this->_post['commentbody']) > (self::COMMENT_LENGTH_MAX * (User::isPremium() ? 3 : 1)))
$this->_post['commentbody'] = mb_substr($this->_post['commentbody'], 0, (self::COMMENT_LENGTH_MAX * (User::isPremium() ? 3 : 1)));
if (User::canComment())
{
if (!empty($this->_post['commentbody']) && mb_strlen($this->_post['commentbody']) >= self::COMMENT_LENGTH_MIN)
{
if ($postIdx = DB::Aowow()->query('INSERT INTO ?_comments (type, typeId, userId, roles, body, date) VALUES (?d, ?d, ?d, ?d, ?, UNIX_TIMESTAMP())', $this->_get['type'], $this->_get['typeid'], User::$id, User::$groups, $this->_post['commentbody']))
{
Util::gainSiteReputation(User::$id, SITEREP_ACTION_COMMENT, ['id' => $postIdx]);
// every comment starts with a rating of +1 and i guess the simplest thing to do is create a db-entry with the system as owner
DB::Aowow()->query('INSERT INTO ?_user_ratings (`type`, `entry`, `userId`, `value`) VALUES (?d, ?d, 0, 1)', RATING_COMMENT, $postIdx);
// flag target with hasComment
if ($tbl = Type::getClassAttrib($this->_get['type'], 'dataTable'))
DB::Aowow()->query('UPDATE '.$tbl.' SET cuFlags = cuFlags | ?d WHERE id = ?d', CUSTOM_HAS_COMMENT, $this->_get['typeid']);
}
else
{
$_SESSION['error']['co'] = Lang::main('intError');
trigger_error('AjaxComment::handleCommentAdd - write to db failed', E_USER_ERROR);
}
}
else
$_SESSION['error']['co'] = Lang::main('textLength', [mb_strlen($this->_post['commentbody']), self::COMMENT_LENGTH_MIN, self::COMMENT_LENGTH_MAX]);
}
else
$_SESSION['error']['co'] = Lang::main('cannotComment');
$this->doRedirect = true;
$idOrUrl = $this->_get['typeid'];
if ($this->_get['type'] == Type::GUIDE)
if ($_ = DB::Aowow()->selectCell('SELECT `url` FROM ?_guides WHERE `id` = ?d', $this->_get['typeid']))
$idOrUrl = $_;
return '?'.Type::getFileString($this->_get['type']).'='.$idOrUrl.'#comments';
}
protected function handleCommentEdit() : void
{
if (!User::canComment() && !User::isInGroup(U_GROUP_MODERATOR))
{
trigger_error('AjaxComment::handleCommentEdit - user #'.User::$id.' not allowed to edit', E_USER_ERROR);
return;
}
if (!$this->_get['id'] || !$this->_post['body'])
{
trigger_error('AjaxComment::handleCommentEdit - malforemd request received', E_USER_ERROR);
return;
}
if (mb_strlen($this->_post['body']) < self::COMMENT_LENGTH_MIN)
return; // no point in reporting this trifle
// trim to max length
if (!User::isInGroup(U_GROUP_MODERATOR) && mb_strlen($this->_post['body']) > (self::COMMENT_LENGTH_MAX * (User::isPremium() ? 3 : 1)))
$this->_post['body'] = mb_substr($this->_post['body'], 0, (self::COMMENT_LENGTH_MAX * (User::isPremium() ? 3 : 1)));
$update = array(
'body' => $this->_post['body'],
'editUserId' => User::$id,
'editDate' => time()
);
if (User::isInGroup(U_GROUP_MODERATOR))
{
$update['responseBody'] = !$this->_post['response'] ? '' : $this->_post['response'];
$update['responseUserId'] = !$this->_post['response'] ? 0 : User::$id;
$update['responseRoles'] = !$this->_post['response'] ? 0 : User::$groups;
}
DB::Aowow()->query('UPDATE ?_comments SET editCount = editCount + 1, ?a WHERE id = ?d', $update, $this->_get['id']);
}
protected function handleCommentDelete() : void
{
if (!$this->_post['id'] || !User::isLoggedIn())
{
trigger_error('AjaxComment::handleCommentDelete - commentId empty or user not logged in', E_USER_ERROR);
return;
}
// in theory, there is a username passed alongside... lets just use the current user (see user.js)
$ok = DB::Aowow()->query('UPDATE ?_comments SET `flags` = `flags` | ?d, `deleteUserId` = ?d, `deleteDate` = UNIX_TIMESTAMP() WHERE `id` IN (?a){ AND `userId` = ?d}',
CC_FLAG_DELETED,
User::$id,
$this->_post['id'],
User::isInGroup(U_GROUP_MODERATOR) ? DBSIMPLE_SKIP : User::$id
);
// deflag hasComment
if ($ok)
{
$coInfo = DB::Aowow()->select('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` IN (?a) GROUP BY b.`type`, b.`typeId`',
CC_FLAG_DELETED,
$this->_post['id']
);
foreach ($coInfo as $co)
if (!$co['hasMore'] && ($tbl = Type::getClassAttrib($co['type'], 'dataTable')))
DB::Aowow()->query('UPDATE ?# SET `cuFlags` = `cuFlags` & ~?d WHERE `id` = ?d', $tbl, CUSTOM_HAS_COMMENT, $co['typeId']);
}
else
trigger_error('AjaxComment::handleCommentDelete - user #'.User::$id.' could not flag comment #'.$this->_post['id'].' as deleted', E_USER_ERROR);
}
protected function handleCommentUndelete() : void
{
if (!$this->_post['id'] || !User::isLoggedIn())
{
trigger_error('AjaxComment::handleCommentUndelete - commentId empty or user not logged in', E_USER_ERROR);
return;
}
// in theory, there is a username passed alongside... lets just use the current user (see user.js)
$ok = DB::Aowow()->query('UPDATE ?_comments SET `flags` = `flags` & ~?d WHERE `id` IN (?a){ AND `userId` = `deleteUserId` AND `deleteUserId` = ?d}',
CC_FLAG_DELETED,
$this->_post['id'],
User::isInGroup(U_GROUP_MODERATOR) ? DBSIMPLE_SKIP : User::$id
);
// reflag hasComment
if ($ok)
{
$coInfo = DB::Aowow()->select('SELECT `type`, `typeId` FROM ?_comments WHERE `id` IN (?a) GROUP BY `type`, `typeId`', $this->_post['id']);
foreach ($coInfo as $co)
if ($tbl = Type::getClassAttrib($co['type'], 'dataTable'))
DB::Aowow()->query('UPDATE ?# SET `cuFlags` = `cuFlags` | ?d WHERE `id` = ?d', $tbl, CUSTOM_HAS_COMMENT, $co['typeId']);
}
else
trigger_error('AjaxComment::handleCommentUndelete - user #'.User::$id.' could not unflag comment #'.$this->_post['id'].' as deleted', E_USER_ERROR);
}
protected function handleCommentRating() : string
{
if (!$this->_get['id'])
return Util::toJSON(['success' => 0]);
if ($votes = DB::Aowow()->selectRow('SELECT 1 AS success, SUM(IF(`value` > 0, `value`, 0)) AS up, SUM(IF(`value` < 0, -`value`, 0)) AS down FROM ?_user_ratings WHERE `type` = ?d AND `entry` = ?d AND userId <> 0 GROUP BY `entry`', RATING_COMMENT, $this->_get['id']))
return Util::toJSON($votes);
else
return Util::toJSON(['success' => 1, 'up' => 0, 'down' => 0]);
}
protected function handleCommentVote() : string
{
if (!User::isLoggedIn() || !$this->_get['id'] || !$this->_get['rating'])
return Util::toJSON(['error' => 1, 'message' => Lang::main('genericError')]);
$target = DB::Aowow()->selectRow('SELECT c.`userId` AS owner, ur.`value` FROM ?_comments c LEFT JOIN ?_user_ratings ur ON ur.`type` = ?d AND ur.`entry` = c.id AND ur.`userId` = ?d WHERE c.id = ?d', RATING_COMMENT, User::$id, $this->_get['id']);
$val = User::canSupervote() ? 2 : 1;
if ($this->_get['rating'] < 0)
$val *= -1;
if (User::getCurrentDailyVotes() <= 0)
return Util::toJSON(['error' => 1, 'message' => Lang::main('tooManyVotes')]);
else if (!$target || $val != $this->_get['rating'])
return Util::toJSON(['error' => 1, 'message' => Lang::main('genericError')]);
else if (($val > 0 && !User::canUpvote()) || ($val < 0 && !User::canDownvote()))
return Util::toJSON(['error' => 1, 'message' => Lang::main('bannedRating')]);
$ok = false;
// old and new have same sign; undo vote (user may have gained/lost access to superVote in the meantime)
if ($target['value'] && ($target['value'] < 0) == ($val < 0))
$ok = DB::Aowow()->query('DELETE FROM ?_user_ratings WHERE `type` = ?d AND `entry` = ?d AND `userId` = ?d', RATING_COMMENT, $this->_get['id'], User::$id);
else // replace, because we may be overwriting an old, opposing vote
if ($ok = DB::Aowow()->query('REPLACE INTO ?_user_ratings (`type`, `entry`, `userId`, `value`) VALUES (?d, ?d, ?d, ?d)', RATING_COMMENT, (int)$this->_get['id'], User::$id, $val))
User::decrementDailyVotes(); // do not refund retracted votes!
if (!$ok)
return Util::toJSON(['error' => 1, 'message' => Lang::main('genericError')]);
if ($val > 0) // gain rep
Util::gainSiteReputation($target['owner'], SITEREP_ACTION_UPVOTED, ['id' => $this->_get['id'], 'voterId' => User::$id]);
else if ($val < 0)
Util::gainSiteReputation($target['owner'], SITEREP_ACTION_DOWNVOTED, ['id' => $this->_get['id'], 'voterId' => User::$id]);
return Util::toJSON(['error' => 0]);
}
protected function handleCommentSticky() : void
{
if (!$this->_post['id'] || !User::isInGroup(U_GROUP_MODERATOR))
{
trigger_error('AjaxComment::handleCommentSticky - commentId empty or user #'.User::$id.' not moderator', E_USER_ERROR);
return;
}
if ($this->_post['sticky'])
DB::Aowow()->query('UPDATE ?_comments SET flags = flags | ?d WHERE id = ?d', CC_FLAG_STICKY, $this->_post['id'][0]);
else
DB::Aowow()->query('UPDATE ?_comments SET flags = flags & ~?d WHERE id = ?d', CC_FLAG_STICKY, $this->_post['id'][0]);
}
protected function handleCommentOutOfDate() : string
{
$this->contentType = MIME_TYPE_TEXT;
if (!$this->_post['id'])
{
trigger_error('AjaxComment::handleCommentOutOfDate - commentId empty', E_USER_ERROR);
return Lang::main('intError');
}
$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 | ?d WHERE id = ?d', CC_FLAG_OUTDATED, $this->_post['id'][0]);
else
$ok = DB::Aowow()->query('UPDATE ?_comments SET flags = flags & ~?d WHERE id = ?d', CC_FLAG_OUTDATED, $this->_post['id'][0]);
}
else // try to report as outdated
{
$report = new Report(Report::MODE_COMMENT, Report::CO_OUT_OF_DATE, $this->_post['id'][0]);
if ($report->create($this->_post['reason']))
$ok = true; // the script expects the actual characters 'ok' not some json string like "ok"
else
return Lang::main('intError');
if (count($report->getSimilar()) >= 5) // 5 or more reports on the same comment: trigger flag
$ok = DB::Aowow()->query('UPDATE ?_comments SET flags = flags | ?d WHERE id = ?d', CC_FLAG_OUTDATED, $this->_post['id'][0]);
}
if ($ok)
return 'ok';
else
trigger_error('AjaxComment::handleCommentOutOfDate - failed to update comment in db', E_USER_ERROR);
return Lang::main('intError');
}
protected function handleCommentShowReplies() : string
{
return Util::toJSON(!$this->_get['id'] ? [] : CommunityContent::getCommentReplies($this->_get['id']));
}
protected function handleReplyAdd() : string
{
$this->contentType = MIME_TYPE_TEXT;
if (!User::canComment())
return Lang::main('cannotComment');
if (!$this->_post['commentId'] || !DB::Aowow()->selectCell('SELECT 1 FROM ?_comments WHERE id = ?d', $this->_post['commentId']))
{
trigger_error('AjaxComment::handleReplyAdd - comment #'.$this->_post['commentId'].' does not exist', E_USER_ERROR);
return Lang::main('intError');
}
if (!$this->_post['body'] || mb_strlen($this->_post['body']) < self::REPLY_LENGTH_MIN || mb_strlen($this->_post['body']) > self::REPLY_LENGTH_MAX)
return Lang::main('textLength', [mb_strlen($this->_post['body']), self::REPLY_LENGTH_MIN, self::REPLY_LENGTH_MAX]);
if (DB::Aowow()->query('INSERT INTO ?_comments (`userId`, `roles`, `body`, `date`, `replyTo`) VALUES (?d, ?d, ?, UNIX_TIMESTAMP(), ?d)', User::$id, User::$groups, $this->_post['body'], $this->_post['commentId']))
return Util::toJSON(CommunityContent::getCommentReplies($this->_post['commentId']));
trigger_error('AjaxComment::handleReplyAdd - write to db failed', E_USER_ERROR);
return Lang::main('intError');
}
protected function handleReplyEdit() : string
{
$this->contentType = MIME_TYPE_TEXT;
if (!User::canComment())
return Lang::main('cannotComment');
if ((!$this->_post['replyId'] || !$this->_post['commentId']) && DB::Aowow()->selectCell('SELECT COUNT(1) FROM ?_comments WHERE id IN (?a)', [$this->_post['replyId'], $this->_post['commentId']]))
{
trigger_error('AjaxComment::handleReplyEdit - comment #'.$this->_post['commentId'].' or reply #'.$this->_post['replyId'].' does not exist', E_USER_ERROR);
return Lang::main('intError');
}
if (!$this->_post['body'] || mb_strlen($this->_post['body']) < self::REPLY_LENGTH_MIN || mb_strlen($this->_post['body']) > self::REPLY_LENGTH_MAX)
return Lang::main('textLength', [mb_strlen($this->_post['body']), self::REPLY_LENGTH_MIN, self::REPLY_LENGTH_MAX]);
if (DB::Aowow()->query('UPDATE ?_comments SET body = ?, editUserId = ?d, editDate = UNIX_TIMESTAMP(), editCount = editCount + 1 WHERE id = ?d AND replyTo = ?d{ AND userId = ?d}',
$this->_post['body'], User::$id, $this->_post['replyId'], $this->_post['commentId'], User::isInGroup(U_GROUP_MODERATOR) ? DBSIMPLE_SKIP : User::$id))
return Util::toJSON(CommunityContent::getCommentReplies($this->_post['commentId']));
trigger_error('AjaxComment::handleReplyEdit - write to db failed', E_USER_ERROR);
return Lang::main('intError');
}
protected function handleReplyDetach() : void
{
if (!$this->_post['id'] || !User::isInGroup(U_GROUP_MODERATOR))
{
trigger_error('AjaxComment::handleReplyDetach - commentId empty or user #'.User::$id.' not moderator', E_USER_ERROR);
return;
}
DB::Aowow()->query('UPDATE ?_comments c1, ?_comments c2 SET c1.replyTo = 0, c1.type = c2.type, c1.typeId = c2.typeId WHERE c1.replyTo = c2.id AND c1.id = ?d', $this->_post['id'][0]);
}
protected function handleReplyDelete() : void
{
if (!User::isLoggedIn() || !$this->_post['id'])
{
trigger_error('AjaxComment::handleReplyDelete - commentId empty or user not logged in', E_USER_ERROR);
return;
}
if (DB::Aowow()->query('DELETE FROM ?_comments WHERE id = ?d{ AND userId = ?d}', $this->_post['id'][0], User::isInGroup(U_GROUP_MODERATOR) ? DBSIMPLE_SKIP : User::$id))
DB::Aowow()->query('DELETE FROM ?_user_ratings WHERE `type` = ?d AND `entry` = ?d', RATING_COMMENT, $this->_post['id'][0]);
else
trigger_error('AjaxComment::handleReplyDelete - deleting comment #'.$this->_post['id'][0].' by user #'.User::$id.' from db failed', E_USER_ERROR);
}
protected function handleReplyFlag() : void
{
if (!User::isLoggedIn() || !$this->_post['id'])
{
trigger_error('AjaxComment::handleReplyFlag - commentId empty or user not logged in', E_USER_ERROR);
return;
}
$report = new Report(Report::MODE_COMMENT, Report::CO_INAPPROPRIATE, $this->_post['id'][0]);
$report->create('Report Reply Button Click');
}
protected function handleReplyUpvote() : void
{
if (!$this->_post['id'] || !User::canUpvote())
{
trigger_error('AjaxComment::handleReplyUpvote - commentId empty or user not allowed to vote', E_USER_ERROR);
return;
}
$owner = DB::Aowow()->selectCell('SELECT userId FROM ?_comments WHERE id = ?d', $this->_post['id'][0]);
if (!$owner)
{
trigger_error('AjaxComment::handleReplyUpvote - comment #'.$this->_post['id'][0].' not found in db', E_USER_ERROR);
return;
}
$ok = DB::Aowow()->query(
'INSERT INTO ?_user_ratings (`type`, `entry`, `userId`, `value`) VALUES (?d, ?d, ?d, ?d)',
RATING_COMMENT,
$this->_post['id'][0],
User::$id,
User::canSupervote() ? 2 : 1
);
if ($ok)
{
Util::gainSiteReputation($owner, SITEREP_ACTION_UPVOTED, ['id' => $this->_post['id'][0], 'voterId' => User::$id]);
User::decrementDailyVotes();
}
else
trigger_error('AjaxComment::handleReplyUpvote - write to db failed', E_USER_ERROR);
}
protected function handleReplyDownvote() : void
{
if (!$this->_post['id'] || !User::canDownvote())
{
trigger_error('AjaxComment::handleReplyDownvote - commentId empty or user not allowed to vote', E_USER_ERROR);
return;
}
$owner = DB::Aowow()->selectCell('SELECT userId FROM ?_comments WHERE id = ?d', $this->_post['id'][0]);
if (!$owner)
{
trigger_error('AjaxComment::handleReplyDownvote - comment #'.$this->_post['id'][0].' not found in db', E_USER_ERROR);
return;
}
$ok = DB::Aowow()->query(
'INSERT INTO ?_user_ratings (`type`, `entry`, `userId`, `value`) VALUES (?d, ?d, ?d, ?d)',
RATING_COMMENT,
$this->_post['id'][0],
User::$id,
User::canSupervote() ? -2 : -1
);
if ($ok)
{
Util::gainSiteReputation($owner, SITEREP_ACTION_DOWNVOTED, ['id' => $this->_post['id'][0], 'voterId' => User::$id]);
User::decrementDailyVotes();
}
else
trigger_error('AjaxComment::handleReplyDownvote - write to db failed', E_USER_ERROR);
}
}
?>

View File

@@ -1,50 +0,0 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
class AjaxContactus extends AjaxHandler
{
protected $_post = array(
'mode' => ['filter' => FILTER_SANITIZE_NUMBER_INT ],
'reason' => ['filter' => FILTER_SANITIZE_NUMBER_INT ],
'ua' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\AjaxHandler::checkTextLine'],
'appname' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\AjaxHandler::checkTextLine'],
'page' => ['filter' => FILTER_SANITIZE_URL ],
'desc' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\AjaxHandler::checkTextBlob'],
'id' => ['filter' => FILTER_SANITIZE_NUMBER_INT ],
'relatedurl' => ['filter' => FILTER_SANITIZE_URL ],
'email' => ['filter' => FILTER_SANITIZE_EMAIL ]
);
public function __construct(array $params)
{
parent::__construct($params);
// always this one
$this->handler = 'handleContactUs';
}
/* responses
0: success
1: captcha invalid
2: description too long
3: reason missing
7: already reported
$: prints response
*/
protected function handleContactUs() : string
{
$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']))
return 0;
else if (($e = $report->getError()) > 0)
return $e;
else
return Lang::main('intError');
}
}
?>

View File

@@ -1,47 +0,0 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
class AjaxCookie extends AjaxHandler
{
public function __construct(array $params)
{
// note that parent::__construct has to come after this
if (!$params || !User::isLoggedIn())
return;
$this->_get = array(
$params[0] => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\AjaxHandler::checkTextLine'],
);
// NOW we know, what to expect and sanitize
parent::__construct($params);
// always this one
$this->handler = 'handleCookie';
}
/* responses
0: success
$: silent error
*/
protected function handleCookie() : string
{
if (User::isLoggedIn() && $this->params && $this->_get[$this->params[0]])
{
if (DB::Aowow()->query('REPLACE INTO ?_account_cookies VALUES (?d, ?, ?)', User::$id, $this->params[0], $this->_get[$this->params[0]]))
return '0';
else
trigger_error('AjaxCookie::handleCookie - write to db failed', E_USER_ERROR);
}
else
trigger_error('AjaxCookie::handleCookie - malformed request received', E_USER_ERROR);
return '';
}
}
?>

View File

@@ -1,143 +0,0 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
class AjaxData extends AjaxHandler
{
protected $_get = array(
'locale' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\Locale::tryFrom' ],
't' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\AjaxHandler::checkTextLine'],
'catg' => ['filter' => FILTER_SANITIZE_NUMBER_INT ],
'skill' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\AjaxData::checkSkill' ],
'class' => ['filter' => FILTER_SANITIZE_NUMBER_INT ],
'callback' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\AjaxData::checkCallback' ]
);
public function __construct(array $params)
{
parent::__construct($params);
if ($this->_get['locale']?->validate())
Lang::load($this->_get['locale']);
// always this one
$this->handler = 'handleData';
}
/* responses
<string>
*/
protected function handleData() : string
{
$result = '';
// different data can be strung together
foreach ($this->params as $set)
{
// requires valid token to hinder automated access
if ($set != 'item-scaling' && (!$this->_get['t'] || empty($_SESSION['dataKey']) || $this->_get['t'] != $_SESSION['dataKey']))
{
trigger_error('AjaxData::handleData - session data key empty or expired', E_USER_ERROR);
continue;
}
switch ($set)
{
/* issue on no initial data:
when we loadOnDemand, the jScript tries to generate the catg-tree before it is initialized
it cant be initialized, without loading the data as empty catg are omitted
loading the data triggers the generation of the catg-tree
*/
case 'factions':
$result .= $this->loadProfilerData($set);
break;
case 'companions':
$result .= $this->loadProfilerData($set, SKILL_COMPANIONS);
break;
case 'mounts':
$result .= $this->loadProfilerData($set, SKILL_MOUNTS);
break;
case 'quests':
$catg = isset($this->_get['catg']) ? $this->_get['catg'] : 'null';
if ($catg == 'null')
Util::loadStaticFile('p-'.$set, $result, false);
else
Util::loadStaticFile('p-'.$set.'-'.$catg, $result, true);
$result .= "\n\$WowheadProfiler.loadOnDemand('".$set."', ".$catg.");\n";
break;
case 'recipes':
if (!$this->_get['callback'] || !$this->_get['skill'])
break;
foreach ($this->_get['skill'] as $s)
Util::loadStaticFile('p-recipes-'.$s, $result, true);
Util::loadStaticFile('p-recipes-sec', $result, true);
$result .= "\n\$WowheadProfiler.loadOnDemand('recipes', null);\n";
break;
// locale independent
case 'quick-excludes':
case 'weight-presets':
case 'item-scaling':
case 'realms':
case 'statistics':
if (!Util::loadStaticFile($set, $result) && Cfg::get('DEBUG'))
$result .= "alert('could not fetch static data: ".$set."');";
$result .= "\n\n";
break;
// localized
case 'talents':
if ($_ = $this->_get['class'])
$set .= "-".$_;
case 'achievements':
case 'pet-talents':
case 'glyphs':
case 'gems':
case 'enchants':
case 'itemsets':
case 'pets':
case 'zones':
if (!Util::loadStaticFile($set, $result, true) && Cfg::get('DEBUG'))
$result .= "alert('could not fetch static data: ".$set." for locale: ".Lang::getLocale()->json()."');";
$result .= "\n\n";
break;
default:
trigger_error('AjaxData::handleData - invalid file "'.$set.'" in request', E_USER_ERROR);
break;
}
}
return $result;
}
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';
}
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.");\n";
return $result;
}
}
?>

View File

@@ -1,82 +0,0 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
class AjaxEdit extends AjaxHandler
{
protected $_get = array(
'qqfile' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\AjaxHandler::checkTextLine'],
'guide' => ['filter' => FILTER_SANITIZE_NUMBER_INT ]
);
public function __construct(array $params)
{
parent::__construct($params);
if (!$params)
return;
if ($params[0] == 'image')
$this->handler = 'handleUpload';
else if ($params[0] == 'article') // has it's own editor page
$this->handler = null;
}
/*
success: bool
id: image enumerator
type: 3 ? png : jpg
name: old filename
error: errString
*/
protected function handleUpload() : string
{
if (!User::canWriteGuide() || $this->_get['guide'] != 1)
return Util::toJSON(['success' => false, 'error' => '']);
require_once('includes/libs/qqFileUploader.class.php');
$targetPath = 'static/uploads/guide/images/';
$tmpPath = 'static/uploads/temp/';
$tmpFile = User::$username.'-'.Type::GUIDE.'-0-'.Util::createHash(16);
$uploader = new \qqFileUploader(['jpg', 'jpeg', 'png'], 10 * 1024 * 1024);
$result = $uploader->handleUpload($tmpPath, $tmpFile, true);
if (isset($result['success']))
{
$finfo = new \finfo(FILEINFO_MIME);
$mime = $finfo->file($tmpPath.$result['newFilename']);
if (preg_match('/^image\/(png|jpe?g)/i', $mime, $m))
{
$i = 1; // image index
if ($files = scandir($targetPath, SCANDIR_SORT_DESCENDING))
if (rsort($files, SORT_NATURAL) && $files[0] != '.' && $files[0] != '..')
$i = explode('.', $files[0])[0] + 1;
$targetFile = $i . ($m[1] == 'png' ? '.png' : '.jpg');
// move to final location
if (!rename($tmpPath.$result['newFilename'], $targetPath.$targetFile))
return Util::toJSON(['error' => Lang::main('intError')]);
// send success
return Util::toJSON(array(
'success' => true,
'id' => $i,
'type' => $m[1] == 'png' ? 3 : 2,
'name' => $this->_get['qqfile']
));
}
return Util::toJSON(['error' => Lang::screenshot('error', 'unkFormat')]);
}
return Util::toJSON($result);
}
}
?>

View File

@@ -1,71 +0,0 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
class AjaxFilter extends AjaxHandler
{
public $doRedirect = true;
private $cat = [];
private $page = '';
private $filter = null;
public function __construct(array $params)
{
if (!$params)
return;
parent::__construct($params);
$p = explode('=', $params[0]);
$this->page = $p[0];
if (isset($p[1]))
$this->cat[] = $p[1];
if (count($params) > 1)
for ($i = 1; $i < count($params); $i++)
$this->cat[] = $params[$i];
$opts = ['parentCats' => $this->cat];
// 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() ?
$this->filter = Type::newFilter($fileStr, $_POST, $opts);
// always this one
$this->handler = 'handleFilter';
}
protected function handleFilter() : string
{
$url = '?'.$this->page;
$this->filter?->mergeCat($this->cat);
if ($this->cat)
$url .= '='.implode('.', $this->cat);
if ($x = $this->filter?->buildGETParam())
$url .= '&filter='.$x;
if ($this->filter?->error)
$_SESSION['error']['fi'] = get_class($this->filter);
// do get request
return $url;
}
}
?>

View File

@@ -1,37 +0,0 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
class AjaxGetdescription extends AjaxHandler
{
protected $_post = array(
'description' => [FILTER_CALLBACK, ['options' => 'Aowow\AjaxHandler::checkTextBlob']]
);
public function __construct(array $params)
{
parent::__construct($params);
if (!$params || $params[0]) // should be empty
return;
$this->handler = 'handleDescription';
}
protected function handleDescription() : string
{
$this->contentType = MIME_TYPE_TEXT;
if (!User::canWriteGuide())
return '';
$desc = Markup::stripTags($this->_post['description']);
return Lang::trimTextClean($desc, 120);
}
}
?>

View File

@@ -1,40 +0,0 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
class AjaxGotocomment extends AjaxHandler
{
protected $_get = array(
'id' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\AjaxHandler::checkInt']
);
public function __construct(array $params)
{
parent::__construct($params);
// always this one
$this->handler = 'handleGoToComment';
$this->doRedirect = true;
}
/* responses
header()
*/
protected function handleGoToComment() : string
{
if (!$this->_get['id'])
return '.'; // go home
if ($_ = DB::Aowow()->selectRow('SELECT IFNULL(c2.`id`, c1.`id`) AS "id", IFNULL(c2.`type`, c1.`type`) AS "type", IFNULL(c2.`typeId`, c1.`typeId`) AS "typeId" FROM ?_comments c1 LEFT JOIN ?_comments c2 ON c1.`replyTo` = c2.`id` WHERE c1.`id` = ?d', $this->_get['id']))
return '?'.Type::getFileString(intVal($_['type'])).'='.$_['typeId'].'#comments:id='.$_['id'].($_['id'] != $this->_get['id'] ? ':reply='.$this->_get['id'] : null);
else
trigger_error('AjaxGotocomment::handleGoToComment - could not find comment #'.$this->_get['id'], E_USER_ERROR);
return '.';
}
}
?>

View File

@@ -1,63 +0,0 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
class AjaxGuide extends AjaxHandler
{
protected $_post = array(
'id' => [FILTER_SANITIZE_NUMBER_INT, null],
'rating' => [FILTER_SANITIZE_NUMBER_INT, null]
);
public function __construct(array $params)
{
parent::__construct($params);
if (!$this->params || count($this->params) != 1)
return;
$this->contentType = MIME_TYPE_TEXT;
// select handler
if ($this->params[0] == 'vote')
$this->handler = 'voteGuide';
}
protected function voteGuide() : string
{
if (!$this->_post['id'] || $this->_post['rating'] < 0 || $this->_post['rating'] > 5)
{
header('HTTP/1.0 404 Not Found', true, 404);
return '';
}
else if (!User::canUpvote() || !User::canDownvote()) // same logic as comments?
{
header('HTTP/1.0 403 Forbidden', true, 403);
return '';
}
// by id, not own, published
if ($g = DB::Aowow()->selectRow('SELECT `userId`, `cuFlags` FROM ?_guides WHERE `id` = ?d AND (`status` = ?d OR `rev` > 0)', $this->_post['id'], GUIDE_STATUS_APPROVED))
{
if ($g['cuFlags'] & GUIDE_CU_NO_RATING || $g['userId'] == User::$id)
{
header('HTTP/1.0 403 Forbidden', true, 403);
return '';
}
if (!$this->_post['rating'])
DB::Aowow()->query('DELETE FROM ?_user_ratings WHERE `type` = ?d AND `entry` = ?d AND `userId` = ?d', RATING_GUIDE, $this->_post['id'], User::$id);
else
DB::Aowow()->query('REPLACE INTO ?_user_ratings VALUES (?d, ?d, ?d, ?d)', RATING_GUIDE, $this->_post['id'], User::$id, $this->_post['rating']);
$res = DB::Aowow()->selectRow('SELECT IFNULL(SUM(`value`), 0) AS `t`, IFNULL(COUNT(*), 0) AS `n` FROM ?_user_ratings WHERE `type` = ?d AND `entry` = ?d', RATING_GUIDE, $this->_post['id']);
return Util::toJSON($res['n'] ? ['rating' => $res['t'] / $res['n'], 'nvotes' => $res['n']] : ['rating' => 0, 'nvotes' => 0]);
}
return Util::toJSON(['rating' => 0, 'nvotes' => 0]);
}
}
?>

View File

@@ -1,83 +0,0 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
class AjaxGuild extends AjaxHandler
{
protected $validParams = ['resync', 'status'];
protected $_get = array(
'id' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\AjaxHandler::checkIdList' ],
'profile' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\AjaxHandler::checkEmptySet'],
);
public function __construct(array $params)
{
parent::__construct($params);
if (!$this->params)
return;
switch ($this->params[0])
{
case 'resync':
$this->handler = 'handleResync';
break;
case 'status':
$this->handler = 'handleStatus';
break;
}
}
/* params
id: <prId1,prId2,..,prIdN>
user: <string> [optional, not used]
profile: <empty> [optional, also get related chars]
return: 1
*/
protected function handleResync() : string
{
if ($guilds = DB::Aowow()->select('SELECT realm, realmGUID FROM ?_profiler_guild WHERE id IN (?a)', $this->_get['id']))
foreach ($guilds as $g)
Profiler::scheduleResync(Type::GUILD, $g['realm'], $g['realmGUID']);
if ($this->_get['profile'])
if ($chars = DB::Aowow()->select('SELECT realm, realmGUID FROM ?_profiler_profiles WHERE guild IN (?a)', $this->_get['id']))
foreach ($chars as $c)
Profiler::scheduleResync(Type::PROFILE, $c['realm'], $c['realmGUID']);
return '1';
}
/* params
id: <prId1,prId2,..,prIdN>
return
<status object>
[
nQueueProcesses,
[statusCode, timeToRefresh, curQueuePos, errorCode, nResyncTries],
[<anotherStatus>]
...
]
not all fields are required, if zero they are omitted
statusCode:
0: end the request
1: waiting
2: working...
3: ready; click to view
4: error / retry
errorCode:
0: unk error
1: char does not exist
2: armory gone
*/
protected function handleStatus() : string
{
return Profiler::resyncStatus(Type::GUILD, $this->_get['id']);
}
}
?>

View File

@@ -1,38 +0,0 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
class AjaxLocale extends AjaxHandler
{
protected $_get = array(
'locale' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\Locale::tryFrom']
);
public function __construct(array $params)
{
parent::__construct($params);
// always this one
$this->handler = 'handleLocale';
$this->doRedirect = true;
}
/* responses
header()
*/
protected function handleLocale() : string
{
if ($this->_get['locale']?->validate())
{
User::$preferedLoc = $this->_get['locale'];
User::save(true);
}
return isset($_SERVER['HTTP_REFERER']) ? $_SERVER['HTTP_REFERER'] : '.';
}
}
?>

View File

@@ -1,780 +0,0 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
class AjaxProfile extends AjaxHandler
{
private $undo = false;
protected $validParams = ['link', 'unlink', 'pin', 'unpin', 'public', 'private', 'avatar', 'resync', 'status', 'save', 'delete', 'purge', 'summary', 'load'];
protected $_get = array(
'id' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\AjaxHandler::checkIdList' ],
'items' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\AjaxProfile::checkItemList'],
'size' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\AjaxHandler::checkTextLine'],
'guild' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\AjaxHandler::checkEmptySet'],
'arena-team' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\AjaxHandler::checkEmptySet'],
'user' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\AjaxProfile::checkUser' ]
);
protected $_post = array(
'name' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\AjaxHandler::checkTextLine'],
'level' => ['filter' => FILTER_SANITIZE_NUMBER_INT],
'class' => ['filter' => FILTER_SANITIZE_NUMBER_INT],
'race' => ['filter' => FILTER_SANITIZE_NUMBER_INT],
'gender' => ['filter' => FILTER_SANITIZE_NUMBER_INT],
'nomodel' => ['filter' => FILTER_SANITIZE_NUMBER_INT],
'talenttree1' => ['filter' => FILTER_SANITIZE_NUMBER_INT],
'talenttree2' => ['filter' => FILTER_SANITIZE_NUMBER_INT],
'talenttree3' => ['filter' => FILTER_SANITIZE_NUMBER_INT],
'activespec' => ['filter' => FILTER_SANITIZE_NUMBER_INT],
'talentbuild1' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\AjaxProfile::checkTalentString'],
'glyphs1' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\AjaxProfile::checkGlyphString' ],
'talentbuild2' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\AjaxProfile::checkTalentString'],
'glyphs2' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\AjaxProfile::checkGlyphString' ],
'icon' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\AjaxHandler::checkTextLine' ],
'description' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\AjaxHandler::checkTextBlob' ],
'source' => ['filter' => FILTER_SANITIZE_NUMBER_INT],
'copy' => ['filter' => FILTER_SANITIZE_NUMBER_INT],
'public' => ['filter' => FILTER_SANITIZE_NUMBER_INT],
'gearscore' => ['filter' => FILTER_SANITIZE_NUMBER_INT],
'inv' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\AjaxHandler::checkIdListUnsigned', 'flags' => FILTER_REQUIRE_ARRAY],
);
public function __construct(array $params)
{
parent::__construct($params);
if (!$this->params)
return;
if (!Cfg::get('PROFILER_ENABLE'))
return;
switch ($this->params[0])
{
case 'unlink':
$this->undo = true;
case 'link':
$this->handler = 'handleLink'; // always returns null
break;
case 'unpin':
$this->undo = true;
case 'pin':
$this->handler = 'handlePin'; // always returns null
break;
case 'private':
$this->undo = true;
case 'public':
$this->handler = 'handlePrivacy'; // always returns null
break;
case 'avatar':
$this->handler = 'handleAvatar'; // sets an image header
break; // so it has to die here or another header will be set
case 'resync':
$this->handler = 'handleResync'; // always returns "1"
break;
case 'status':
$this->handler = 'handleStatus'; // returns status object
break;
case 'save':
$this->handler = 'handleSave';
break;
case 'delete':
$this->handler = 'handleDelete';
break;
case 'purge':
$this->handler = 'handlePurge';
break;
case 'summary': // page is generated by jScript
die(); // just be empty
case 'load':
$this->handler = 'handleLoad';
break;
}
}
/* params
id: <prId1,prId2,..,prIdN>
user: <string> [optional]
return: null
*/
protected function handleLink() : void // links char with account
{
if (!User::isLoggedIn() || empty($this->_get['id']))
{
trigger_error('AjaxProfile::handleLink - profileId empty or user not logged in', E_USER_ERROR);
return;
}
$uid = User::$id;
if ($this->_get['user'] && User::isInGroup(U_GROUP_ADMIN | U_GROUP_BUREAU))
{
if (!($uid = DB::Aowow()->selectCell('SELECT `id` FROM ?_account WHERE LOWER(`username`) = LOWER(?)', $this->_get['user'])))
{
trigger_error('AjaxProfile::handleLink - user "'.$this->_get['user'].'" does not exist', E_USER_ERROR);
return;
}
}
if ($this->undo)
DB::Aowow()->query('DELETE FROM ?_account_profiles WHERE `accountId` = ?d AND `profileId` IN (?a)', $uid, $this->_get['id']);
else
{
foreach ($this->_get['id'] as $prId) // only link characters, not custom profiles
{
if ($prId = DB::Aowow()->selectCell('SELECT `id` FROM ?_profiler_profiles WHERE `id` = ?d AND `realm` IS NOT NULL', $prId))
DB::Aowow()->query('INSERT IGNORE INTO ?_account_profiles VALUES (?d, ?d, 0)', $uid, $prId);
else
{
trigger_error('AjaxProfile::handleLink - profile #'.$prId.' is custom or does not exist', E_USER_ERROR);
return;
}
}
}
}
/* params
id: <prId1,prId2,..,prIdN>
user: <string> [optional]
return: null
*/
protected function handlePin() : void // (un)favorite
{
if (!User::isLoggedIn() || empty($this->_get['id'][0]))
{
trigger_error('AjaxProfile::handlePin - profileId empty or user not logged in', E_USER_ERROR);
return;
}
$uid = User::$id;
if ($this->_get['user'] && User::isInGroup(U_GROUP_ADMIN | U_GROUP_BUREAU))
{
if (!($uid = DB::Aowow()->selectCell('SELECT `id` FROM ?_account WHERE LOWER(`username`) = LOWER(?)', $this->_get['user'])))
{
trigger_error('AjaxProfile::handlePin - 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()->query('UPDATE ?_account_profiles SET `extraFlags` = `extraFlags` & ?d WHERE `accountId` = ?d', ~PROFILER_CU_PINNED, $uid);
// and set a single char if necessary
if (!$this->undo)
DB::Aowow()->query('UPDATE ?_account_profiles SET `extraFlags` = `extraFlags` | ?d WHERE `profileId` = ?d AND `accountId` = ?d', PROFILER_CU_PINNED, $this->_get['id'][0], $uid);
}
/* params
id: <prId1,prId2,..,prIdN>
user: <string> [optional]
return: null
*/
protected function handlePrivacy() : void // public visibility
{
if (!User::isLoggedIn() || empty($this->_get['id'][0]))
{
trigger_error('AjaxProfile::handlePrivacy - profileId empty or user not logged in', E_USER_ERROR);
return;
}
$uid = User::$id;
if ($this->_get['user'] && User::isInGroup(U_GROUP_ADMIN | U_GROUP_BUREAU))
{
if (!($uid = DB::Aowow()->selectCell('SELECT `id` FROM ?_account WHERE LOWER(`username`) = LOWER(?)', $this->_get['user'])))
{
trigger_error('AjaxProfile::handlePrivacy - user "'.$this->_get['user'].'" does not exist', E_USER_ERROR);
return;
}
}
if ($this->undo)
{
DB::Aowow()->query('UPDATE ?_account_profiles SET `extraFlags` = `extraFlags` & ?d WHERE `profileId` IN (?a) AND `accountId` = ?d', ~PROFILER_CU_PUBLISHED, $this->_get['id'], $uid);
DB::Aowow()->query('UPDATE ?_profiler_profiles SET `cuFlags` = `cuFlags` & ?d WHERE `id` IN (?a) AND `user` = ?d', ~PROFILER_CU_PUBLISHED, $this->_get['id'], $uid);
}
else
{
DB::Aowow()->query('UPDATE ?_account_profiles SET `extraFlags` = `extraFlags` | ?d WHERE `profileId` IN (?a) AND `accountId` = ?d', PROFILER_CU_PUBLISHED, $this->_get['id'], $uid);
DB::Aowow()->query('UPDATE ?_profiler_profiles SET `cuFlags` = `cuFlags` | ?d WHERE `id` IN (?a) AND `user` = ?d', PROFILER_CU_PUBLISHED, $this->_get['id'], $uid);
}
}
/* params
id: <prId>
size: <string> [optional]
return: image-header
*/
protected function handleAvatar() : void // image
{
// something happened in the last years: those textures do not include tiny icons
$sizes = [/* 'tiny' => 15, */'small' => 18, 'medium' => 36, 'large' => 56];
$aPath = 'uploads/avatars/%d.jpg';
$s = $this->_get['size'] ?: 'medium';
if (!$this->_get['id'] || !preg_match('/^([0-9]+)\.(jpg|gif)$/', $this->_get['id'][0], $matches) || !in_array($s, array_keys($sizes)))
{
trigger_error('AjaxProfile::handleAvatar - malformed request received', E_USER_ERROR);
return;
}
$this->contentType = $matches[2] == 'png' ? MIME_TYPE_PNG : MIME_TYPE_JPEG;
$id = $matches[1];
$dest = imageCreateTruecolor($sizes[$s], $sizes[$s]);
if (file_exists(sprintf($aPath, $id)))
{
$offsetX = $offsetY = 0;
switch ($s)
{
case 'tiny':
$offsetX += $sizes['small'];
case 'small':
$offsetY += $sizes['medium'];
case 'medium':
$offsetX += $sizes['large'];
}
$src = imageCreateFromJpeg(printf($aPath, $id));
imagecopymerge($dest, $src, 0, 0, $offsetX, $offsetY, $sizes[$s], $sizes[$s], 100);
}
else
trigger_error('AjaxProfile::handleAvatar - avatar file #'.$id.' not found', E_USER_ERROR);
if ($matches[2] == 'gif')
imageGif($dest);
else
imageJpeg($dest);
}
/* params
id: <prId1,prId2,..,prIdN>
user: <string> [optional, not used]
return: 1
*/
protected function handleResync() : string
{
if ($chars = DB::Aowow()->select('SELECT realm, realmGUID FROM ?_profiler_profiles WHERE id IN (?a)', $this->_get['id']))
{
foreach ($chars as $c)
Profiler::scheduleResync(Type::PROFILE, $c['realm'], $c['realmGUID']);
}
else
trigger_error('AjaxProfile::handleResync - profiles '.implode(', ', $this->_get['id']).' not found in db', E_USER_ERROR);
return '1';
}
/* params
id: <prId1,prId2,..,prIdN>
return
<status object>
[
nQueueProcesses,
[statusCode, timeToRefresh, curQueuePos, errorCode, nResyncTries],
[<anotherStatus>]
...
]
not all fields are required, if zero they are omitted
statusCode:
0: end the request
1: waiting
2: working...
3: ready; click to view
4: error / retry
errorCode:
0: unk error
1: char does not exist
2: armory gone
*/
protected function handleStatus() : string
{
// roster resync for this guild was requested -> get char list
if ($this->_get['guild'])
$ids = DB::Aowow()->selectCol('SELECT id FROM ?_profiler_profiles WHERE guild IN (?a)', $this->_get['id']);
else if ($this->_get['arena-team'])
$ids = DB::Aowow()->selectCol('SELECT profileId FROM ?_profiler_arena_team_member WHERE arenaTeamId IN (?a)', $this->_get['id']);
else
$ids = $this->_get['id'];
if (!$ids)
{
trigger_error('AjaxProfile::handleStatus - no profileIds to resync'.($this->_get['guild'] ? ' for guild #'.$this->_get['guild'] : ($this->_get['arena-team'] ? ' for areana team #'.$this->_get['arena-team'] : '')), E_USER_ERROR);
return Util::toJSON([1, [PR_QUEUE_STATUS_ERROR, 0, 0, PR_QUEUE_ERROR_CHAR]]);
}
return Profiler::resyncStatus(Type::PROFILE, $ids);
}
/* params (get))
id: <prId1,0> [0: new profile]
params (post)
<various char data> [see below]
return:
proileId [onSuccess]
-1 [onError]
*/
protected function handleSave() : string // unKill a profile
{
// todo (med): detail check this post-data
$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'],
'cuFlags' => PROFILER_CU_PROFILE | ($this->_post['public'] ? PROFILER_CU_PUBLISHED : 0)
);
if (strstr($cuProfile['icon'], 'profile=avatar')) // how the profiler is supposed to handle icons is beyond me
$cuProfile['icon'] = '';
if ($_ = $this->_post['description'])
$cuProfile['description'] = $_;
if ($_ = $this->_post['source']) // should i also set sourcename?
$cuProfile['sourceId'] = $_;
if ($_ = $this->_post['copy']) // gets set to source profileId when "save as" is clicked. Whats the difference to 'source' though?
{
// get character origin info if possible
if ($r = DB::Aowow()->selectCell('SELECT realm FROM ?_profiler_profiles WHERE id = ?d AND realm IS NOT NULL', $_))
$cuProfile['realm'] = $r;
$cuProfile['sourceId'] = $_;
}
if (!empty($cuProfile['sourceId']))
$cuProfile['sourceName'] = DB::Aowow()->selectCell('SELECT name FROM ?_profiler_profiles WHERE id = ?d', $cuProfile['sourceId']);
$charId = -1;
if ($id = $this->_get['id'][0]) // update
{
if ($charId = DB::Aowow()->selectCell('SELECT id FROM ?_profiler_profiles WHERE id = ?d', $id))
DB::Aowow()->query('UPDATE ?_profiler_profiles SET ?a WHERE id = ?d', $cuProfile, $id);
}
else // new
{
$nProfiles = DB::Aowow()->selectCell('SELECT COUNT(*) FROM ?_profiler_profiles WHERE user = ?d AND (cuFlags & ?d) = 0 AND realmGUID IS NULL', User::$id, PROFILER_CU_DELETED);
if ($nProfiles < 10 || User::isPremium())
if ($newId = DB::Aowow()->query('INSERT INTO ?_profiler_profiles (?#) VALUES (?a)', array_keys($cuProfile), array_values($cuProfile)))
$charId = $newId;
}
// update items
if ($charId != -1)
{
// ok, 'funny' thing: wether an item has en extra prismatic sockel is determined contextual
// either the socket is -1 or it has an itemId in a socket where there shouldn't be one
$keys = ['id', 'slot', 'item', 'subitem', 'permEnchant', 'tempEnchant', 'gem1', 'gem2', 'gem3', 'gem4'];
// validate Enchantments
$enchIds = array_merge(
array_column($this->_post['inv'], 3), // perm enchantments
array_column($this->_post['inv'], 4) // temp enchantments (not used..?)
);
$enchs = new EnchantmentList(array(['id', $enchIds]));
// validate items
$itemIds = array_merge(
array_column($this->_post['inv'], 1), // base item
array_column($this->_post['inv'], 5), // gem slot 1
array_column($this->_post['inv'], 6), // gem slot 2
array_column($this->_post['inv'], 7), // gem slot 3
array_column($this->_post['inv'], 8) // gem slot 4
);
$items = new ItemList(array(['id', $itemIds]));
if (!$items->error)
{
foreach ($this->_post['inv'] as $slot => $itemData)
{
if ($slot + 1 == array_sum($itemData)) // only slot definition set => empty slot
{
DB::Aowow()->query('DELETE FROM ?_profiler_items WHERE id = ?d AND slot = ?d', $charId, $itemData[0]);
continue;
}
// item does not exist
if (!$items->getEntry($itemData[1]))
continue;
// sub-item check
if (!$items->getRandEnchantForItem($itemData[1]))
$itemData[2] = 0;
// item sockets are fubar
$nSockets = $items->json[$itemData[1]]['nsockets'] ?? 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()->query('REPLACE INTO ?_profiler_items (?#) VALUES (?a)', $keys, $itemData);
}
}
}
return (string)$charId;
}
/* params
id: <prId1,prId2,..,prIdN>
return
null
*/
protected function handleDelete() : void // kill a profile
{
if (!User::isLoggedIn() || !$this->_get['id'])
{
trigger_error('AjaxProfile::handleDelete - profileId empty or user not logged in', E_USER_ERROR);
return;
}
// only flag as deleted; only custom profiles
DB::Aowow()->query(
'UPDATE ?_profiler_profiles SET cuFlags = cuFlags | ?d WHERE id IN (?a) AND cuFlags & ?d {AND user = ?d}',
PROFILER_CU_DELETED,
$this->_get['id'],
PROFILER_CU_PROFILE,
User::isInGroup(U_GROUP_ADMIN | U_GROUP_BUREAU) ? DBSIMPLE_SKIP : User::$id
);
}
/* params
id: profileId
items: string [itemIds.join(':')]
unnamed: unixtime [only to force the browser to reload instead of cache]
return
lots...
*/
protected function handleLoad() : string
{
// titles, achievements, characterData, talents, pets
// and some onLoad-hook to .. load it registerProfile($data)
// everything else goes through data.php .. strangely enough
if (!$this->_get['id'])
{
trigger_error('AjaxProfile::handleLoad - 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 = ?d', $this->_get['id'][0]);
if (!$pBase)
{
trigger_error('Profiler::handleLoad - called with invalid profileId #'.$this->_get['id'][0], E_USER_WARNING);
return '';
}
if (($pBase['cuFlags'] & PROFILER_CU_DELETED) && !User::isInGroup(U_GROUP_ADMIN | U_GROUP_BUREAU))
return '';
$rData = [];
foreach (Profiler::getRealms() as $rId => $rData)
if ($rId == $pBase['realm'])
break;
if (!$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['cuFlags'] & PROFILER_CU_PROFILE)
{
// this parameter is _really_ strange .. probably still not doing this right
$profile['source'] = $pBase['realm'] ? $pBase['sourceId'] : 0;
$profile['sourcename'] = $pBase['sourceName'];
$profile['description'] = $pBase['description'];
$profile['user'] = $pBase['user'];
$profile['username'] = DB::Aowow()->selectCell('SELECT `username` FROM ?_account WHERE `id` = ?d', $pBase['user']);
}
// custom profiles inherit this when copied from real char :(
if ($pBase['realm'])
{
$profile['region'] = [$rData['region'], Lang::profiler('regions', $rData['region'])];
$profile['battlegroup'] = [Profiler::urlize(Cfg::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 = ?d', $pBase['id']))
$profile['bookmarks'] = $_;
// arena teams - [size(2|3|5) => name]; name gets urlized to use as link
if ($at = DB::Aowow()->selectCol('SELECT type AS ARRAY_KEY, name FROM ?_profiler_arena_team at JOIN ?_profiler_arena_team_member atm ON atm.arenaTeamId = at.id WHERE atm.profileId = ?d', $pBase['id']))
$profile['arenateams'] = $at;
// pets if hunter fields: [name:name, family:petFamily, npc:npcId, displayId:modelId, talents:talentString]
if ($pets = DB::Aowow()->select('SELECT `name`, `family`, `npc`, `displayId`, CONCAT("$\"", `talents`, "\"") AS "talents" FROM ?_profiler_pets WHERE `owner` = ?d', $pBase['id']))
$profile['pets'] = $pets;
// source for custom profiles; profileId => [name, ownerId, iconString(optional)]
if ($customs = DB::Aowow()->select('SELECT id AS ARRAY_KEY, name, user, icon FROM ?_profiler_profiles WHERE sourceId = ?d AND sourceId <> id {AND (cuFlags & ?d) = 0}', $pBase['id'], User::isInGroup(U_GROUP_STAFF) ? DBSIMPLE_SKIP : PROFILER_CU_DELETED))
{
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` = ?d', $pBase['id']))
{
$qList = new QuestList(array(['id', $quests], Cfg::get('SQL_LIMIT_NONE')));
if (!$qList->error)
foreach ($qList->iterate() as $id => $__)
$profile['quests'][$id] = [$qList->getField('cat1'), $qList->getField('cat2')];
}
// skillId => [value, max]
$profile['skills'] = DB::Aowow()->select('SELECT `skillId` AS ARRAY_KEY, `value` AS "0", `max` AS "1" FROM ?_profiler_completion_skills WHERE `id` = ?d', $pBase['id']);
// factionId => amount
$profile['reputation'] = DB::Aowow()->selectCol('SELECT `factionId` AS ARRAY_KEY, `standing` FROM ?_profiler_completion_reputation WHERE `id` = ?d', $pBase['id']);
// titleId => 1
$profile['titles'] = DB::Aowow()->selectCol('SELECT `titleId` AS ARRAY_KEY, 1 FROM ?_profiler_completion_titles WHERE `id` = ?d', $pBase['id']);
// achievementId => js date object
$profile['achievements'] = DB::Aowow()->selectCol('SELECT `achievementId` AS ARRAY_KEY, CONCAT("$new Date(", `date` * 1000, ")") FROM ?_profiler_completion_achievements WHERE `id` = ?d', $pBase['id']);
// just points
$profile['achievementpoints'] = $profile['achievements'] ? DB::Aowow()->selectCell('SELECT SUM(`points`) FROM ?_achievement WHERE `id` IN (?a)', array_keys($profile['achievements'])) : 0;
// achievementId => counter
$profile['statistics'] = DB::Aowow()->selectCol('SELECT `achievementId` AS ARRAY_KEY, `counter` FROM ?_profiler_completion_statistics WHERE `id` = ?d', $pBase['id']);
// achievementId => 1
$profile['activity'] = DB::Aowow()->selectCol('SELECT `achievementId` AS ARRAY_KEY, 1 FROM ?_profiler_completion_statistics WHERE `id` = ?d AND `date` > ?d', $pBase['id'], time() - MONTH);
// spellId => 1
$profile['spells'] = DB::Aowow()->selectCol('SELECT `spellId` AS ARRAY_KEY, 1 FROM ?_profiler_completion_spells WHERE `id` = ?d', $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()->select('SELECT * FROM ?_profiler_items WHERE id = ?d', $pBase['id']))
{
$itemz = new ItemList(array(['id', array_column($items, 'item')], Cfg::get('SQL_LIMIT_NONE')));
if (!$itemz->error)
{
$data = $itemz->getListviewData(ITEMINFO_JSON | ITEMINFO_SUBITEMS);
foreach ($items as $i)
{
if ($itemz->getEntry($i['item']) && !in_array($i['slot'], $usedSlots))
{
// get and apply inventory
$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')], Cfg::get('SQL_LIMIT_NONE')));
// $dataz = $auraz->getListviewData();
// $modz = $auraz->getProfilerMods();
// // get and apply aura-mods
// foreach ($dataz as $id => $data)
// {
// $mods = [];
// if (!empty($modz[$id]))
// {
// foreach ($modz[$id] as $k => $v)
// {
// if (is_array($v))
// $mods[] = $v;
// else if ($str = @Game::$itemMods[$k])
// $mods[$str] = $v;
// }
// }
// $buff .= 'g_spells.add('.$id.", {id:".$id.", name:'".Util::jsEscape(mb_substr($data['name'], 1))."', icon:'".$data['icon']."', 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).");";
return $buff."\n";
}
/* params
id: <prId>
data: <mode> [string, tabName]
return
null
*/
protected function handlePurge() : void { } // removes completion data (as uploaded by the wowhead client) Just fail silently if someone triggers this manually
protected static function checkItemList($val) : array
{
// expecting item-list
if (preg_match('/\d+(:\d+)*/', $val))
return array_map('intVal', explode(':', $val));
return [];
}
protected static function checkUser(string $val) : string
{
if (User::isValidName($val))
return $val;
return '';
}
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 '';
}
}
?>

View File

@@ -1,451 +0,0 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
class Cfg
{
public const PATTERN_CONF_KEY = '/[a-z0-9_\.\-]/i';
public const PATTERN_INV_CONF_KEY = '/[^a-z0-9_\.\-]/i';
public const PATTERN_INVALID_CHARS = '/\p{C}/ui';
// config flags
public const FLAG_TYPE_INT = 0x001; // validate with intVal()
public const FLAG_TYPE_FLOAT = 0x002; // validate with floatVal()
public const FLAG_TYPE_BOOL = 0x004; // 0 || 1
public const FLAG_TYPE_STRING = 0x008; //
public const FLAG_OPT_LIST = 0x010; // single option
public const FLAG_BITMASK = 0x020; // multiple options
public const FLAG_PHP = 0x040; // applied with ini_set() [restrictions apply!]
public const FLAG_PERSISTENT = 0x080; // can not be deleted
public const FLAG_REQUIRED = 0x100; // required to have non-empty value
public const FLAG_ON_LOAD_FN = 0x200; // run static function of the same name after load
public const FLAG_ON_SET_FN = 0x400; // run static function of the same name as validator
public const FLAG_INTERNAL = 0x800; // can not be configures, automaticly calculated, skip on lists
public const CAT_MISCELLANEOUS = 0;
public const CAT_SITE = 1;
public const CAT_CACHE = 2;
public const CAT_ACCOUNT = 3;
public const CAT_SESSION = 4;
public const CAT_SITE_REPUTATION = 5;
public const CAT_ANALYTICS = 6;
public const CAT_PROFILER = 7;
public static $categories = array( // don't mind the ordering ... please?
1 => '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_unco' => ['global'], // currently not a template or buildScript
// 'rep_req_border_rare' => ['global'],
// 'rep_req_border_epic' => ['global'],
// 'rep_req_border_lege' => ['global'],
'profiler_enable' => ['realms', 'realmMenu'],
'battlegroup' => ['realms', 'realmMenu'],
'name_short' => ['searchplugin', 'searchboxBody', 'searchboxScript', 'demo'],
'site_host' => ['searchplugin', 'searchboxBody', 'searchboxScript', 'demo', 'power'],
'static_host' => ['searchplugin', 'searchboxBody', 'searchboxScript', 'power'],
'contact_email' => ['markup'],
'locales' => ['locales']
);
public static function load() : void
{
if (!DB::isConnectable(DB_AOWOW))
return;
$sets = DB::Aowow()->select('SELECT `key` AS ARRAY_KEY, `value` AS "0", `flags` AS "1", `cat` AS "2", `default` AS "3", `comment` AS "4" FROM ?_config ORDER BY `key` ASC');
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_INV_CONF_KEY, $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 (!DB::Aowow()->query('INSERT IGNORE INTO ?_config (`key`, `value`, `cat`, `flags`) VALUES (?, ?, ?d, ?d)', $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()->query('DELETE FROM ?_config WHERE `key` = ? AND (`flags` & ?d) = 0 AND (`flags` & ?d) > 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` = ?', $key));
if ($fromDB)
return DB::Aowow()->selectCell('SELECT `value` FROM ?_config WHERE `key` = ?', $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()->query('UPDATE ?_config SET `value` = ? WHERE `key` = ?', $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()->query('UPDATE ?_config SET `value` = ? WHERE `key` = ?', $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()->query('UPDATE ?_config SET `value` = ? WHERE `key` = ?', $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()->query('UPDATE ?_config SET `value` = ? WHERE `key` = ?', $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) : string
{
return preg_replace_callback(
['/CFG_([A-Z_]+)/', '/((HOST|STATIC)_URL)/'],
function ($m) {
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) ? 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 = (bool)$value;
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]."<br />\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 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 useSSL() : bool
{
return (($_SERVER['HTTPS'] ?? 'off') != 'off') || (self::$store['force_ssl'][self::IDX_VALUE] ?? 0);
}
}
?>

View File

@@ -0,0 +1,526 @@
<?php
if (!defined('AOWOW_REVISION'))
die('illegal access');
/************
* get Community Content
************/
/*
{id:115,user:'Ciderhelm',date:'2010/05/10 19:14:18',caption:'TankSpot\'s Guide to the Fury Warrior (Part 1)',videoType:1,videoId:'VUvxFvVmttg',type:13,typeId:1},
{id:116,user:'Ciderhelm',date:'2010/05/10 19:14:18',caption:'TankSpot\'s Guide to the Fury Warrior (Part 2)',videoType:1,videoId:'VEfnuIcq7n8',type:13,typeId:1},
{id:117,user:'Ciderhelm',date:'2010/05/10 19:14:18',caption:'TankSpot\'s Protection Warrior Guide',videoType:1,videoId:'vF-7kmvJZXY',type:13,typeId:1,sticky:1}
*/
/* todo: administration of content */
class CommunityContent
{
private static $jsGlobals = [];
private static $subjCache = [];
private static $commentQuery = '
SELECT
c.*,
a1.displayName AS user,
a2.displayName AS editUser,
a3.displayName AS deleteUser,
a4.displayName AS responseUser,
IFNULL(SUM(cr.value), 0) AS rating,
SUM(IF (cr.userId = ?d, value, 0)) AS userRating,
SUM(IF (r.userId = ?d, 1, 0)) AS userReported
FROM
?_comments c
JOIN
?_account a1 ON c.userId = a1.id
LEFT JOIN
?_account a2 ON c.editUserId = a2.id
LEFT JOIN
?_account a3 ON c.deleteUserId = a3.id
LEFT JOIN
?_account a4 ON c.responseUserId = a4.id
LEFT JOIN
?_comments_rates cr ON c.id = cr.commentId
LEFT JOIN
?_reports r ON r.subject = c.id AND r.mode = 1 AND r.reason = 19
WHERE
c.replyTo = ?d AND c.type = ?d AND c.typeId = ?d AND
((c.flags & ?d) = 0 OR c.userId = ?d OR ?d)
GROUP BY
c.id
ORDER BY
rating ASC
';
private static $previewQuery = '
SELECT
c.id,
c.body AS preview,
c.date,
c.replyTo AS commentid,
UNIX_TIMESTAMP() - c.date AS elapsed,
IF(c.flags & ?d, 1, 0) AS deleted,
IF(c.type <> 0, c.type, c2.type) AS type,
IF(c.typeId <> 0, c.typeId, c2.typeId) AS typeId,
IFNULL(SUM(cr.value), 0) AS rating,
a.displayName AS user
FROM
?_comments c
JOIN
?_account a ON c.userId = a.id
LEFT JOIN
?_comments_rates cr ON cr.commentId = c.id
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;
}
}
?>

View File

@@ -1,746 +0,0 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
// TrinityCore - Condition System
class Conditions
{
// enum TypeID
private const TYPEID_OBJECT = 0;
private const TYPEID_ITEM = 1;
private const TYPEID_CONTAINER = 2;
private const TYPEID_UNIT = 3;
private const TYPEID_PLAYER = 4;
private const TYPEID_GAMEOBJECT = 5;
private const TYPEID_DYNAMICOBJECT = 6;
private const TYPEID_CORPSE = 7;
public const OP_E = 0; // ==
public const OP_GT = 1; // >
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
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]
);
private $jsGlobals = [];
private $rows = [];
private $result = [];
private $resultExtra = [];
/******/
/* IN */
/******/
public function getBySourceEntry(int $entry, int ...$srcType) : self
{
$this->rows = array_merge($this->rows, DB::World()->select(
'SELECT `SourceTypeOrReferenceId`, `SourceEntry`, `SourceGroup`, `SourceId`, `ElseGroup`,
`ConditionTypeOrReference`, `ConditionTarget`, `ConditionValue1`, `ConditionValue2`, `ConditionValue3`, `NegativeCondition`
FROM conditions
WHERE `SourceTypeOrReferenceId` IN (?a) AND `SourceEntry` = ?d
ORDER BY `SourceTypeOrReferenceId`, `SourceEntry`, `SourceGroup`, `ElseGroup` ASC',
$srcType, $entry
));
return $this;
}
public function getBySourceGroup(int $group, int ...$srcType) : self
{
$this->rows = array_merge($this->rows, DB::World()->select(
'SELECT `SourceTypeOrReferenceId`, `SourceEntry`, `SourceGroup`, `SourceId`, `ElseGroup`,
`ConditionTypeOrReference`, `ConditionTarget`, `ConditionValue1`, `ConditionValue2`, `ConditionValue3`, `NegativeCondition`
FROM conditions
WHERE `SourceTypeOrReferenceId` IN (?a) AND `SourceGroup` = ?d
ORDER BY `SourceTypeOrReferenceId`, `SourceEntry`, `SourceGroup`, `ElseGroup` ASC',
$srcType, $group
));
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[] = sprintf("(c2.`ConditionTypeOrReference` = %d AND (c2.`ConditionValue1` & %d) > 0)", $cId, 1 << ($typeId - 1));
else
$lookups[] = sprintf("(c2.`ConditionTypeOrReference` = %d AND c2.`ConditionValue1` = %d)", $cId, $typeId);
}
if (!$lookups)
return $this;
$this->rows = array_merge($this->rows, DB::World()->select(sprintf(
'SELECT c1.`SourceTypeOrReferenceId`, c1.`SourceEntry`, c1.`SourceGroup`, c1.`SourceId`, c1.`ElseGroup`,
c1.`ConditionTypeOrReference`, c1.`ConditionTarget`, c1.`ConditionValue1`, c1.`ConditionValue2`, c1.`ConditionValue3`, c1.`NegativeCondition`
FROM conditions c1
JOIN conditions c2 ON c1.SourceTypeOrReferenceId = c2.SourceTypeOrReferenceId AND c1.SourceEntry = c2.SourceEntry AND c1.SourceGroup = c2.SourceGroup AND c1.SourceId = c2.SourceId
WHERE %s
GROUP BY `SourceTypeOrReferenceId`,`SourceGroup`,`SourceEntry`,`SourceId`,`ElseGroup`,`ConditionTypeOrReference`,`ConditionTarget`,`ConditionValue1`,`ConditionValue2`,`ConditionValue3`
ORDER BY `SourceTypeOrReferenceId`, `SourceEntry`, `SourceGroup`, `ElseGroup` ASC',
implode(' OR ', $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] = array_pad($condition, 5, 0);
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))
{
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 = "<script type=\"text/javascript\">\n" .
" var markup = ConditionList.createTab(".Util::toJSON($out).");\n" .
" Markup.printHtml(markup, 'tab-".$id."', { allow: Markup.CLASS_STAFF })\n" .
"</script>";
$tab = array(
'data' => $data,
'id' => $id,
'name' => ($name ?: '$LANG.tab_conditions') . '+" ('.$nCnd.')"'
);
return [null, $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 getJsGlobals() : array
{
return $this->jsGlobals;
}
/*********/
/* Other */
/*********/
public static function lootTableToConditionSource(string $lootTable) : int
{
switch ($lootTable)
{
case LOOT_FISHING: return self::SRC_FISHING_LOOT_TEMPLATE;
case LOOT_CREATURE: return self::SRC_CREATURE_LOOT_TEMPLATE;
case LOOT_GAMEOBJECT: return self::SRC_GAMEOBJECT_LOOT_TEMPLATE;
case LOOT_ITEM: return self::SRC_ITEM_LOOT_TEMPLATE;
case LOOT_DISENCHANT: return self::SRC_DISENCHANT_LOOT_TEMPLATE;
case LOOT_PROSPECTING: return self::SRC_PROSPECTING_LOOT_TEMPLATE;
case LOOT_MILLING: return self::SRC_MILLING_LOOT_TEMPLATE;
case LOOT_PICKPOCKET: return self::SRC_PICKPOCKETING_LOOT_TEMPLATE;
case LOOT_SKINNING: return self::SRC_SKINNING_LOOT_TEMPLATE;
case LOOT_MAIL: return self::SRC_MAIL_LOOT_TEMPLATE;
case LOOT_SPELL: return self::SRC_SPELL_LOOT_TEMPLATE;
case LOOT_REFERENCE: return self::SRC_REFERENCE_LOOT_TEMPLATE;
default: return self::SRC_NONE;
}
}
public static function extendListviewRow(array &$lvRow, int $srcType, int $groupKey, array $condition) : bool
{
if (!isset(self::$source[$srcType]))
return false;
[$cId, $cVal1, $cVal2, $cVal3] = array_pad($condition, 5, 0);
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))
$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']
);
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) : array
{
if ($fn = self::$conditions[abs($cId)][self::IDX_CND_FN])
if (!$this->$fn(abs($cId), $cVal1, $cVal2, $cVal3))
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
}
return $result;
}
private function factionToSide($cndId, &$cVal1, $cVal2, $cVal3) : 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) : 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` = ?d AND `parentArea` = 0 AND (`cuFlags` & ?d) = 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) : 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) : bool
{
if ($cVal1 == self::TYPEID_UNIT)
{
if ($cVal3 && ($_ = DB::Aowow()->selectCell('SELECT `typeId` FROM ?_spawns WHERE `type` = ?d AND `guid` = ?d', 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` = ?d AND `guid` = ?d', 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` = ?d', $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` = ?d', $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` = ?d', $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` = ?d', $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` = ?d', $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` = ?d', $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;
}
}
?>

View File

@@ -1,756 +0,0 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
// TrinityCore - SmartAI
trait SmartHelper
{
private function resolveGuid(int $type, int $guid) : ?int
{
if ($_ = DB::Aowow()->selectCell('SELECT `typeId` FROM ?_spawns WHERE `type` = ?d AND `guid` = ?d', $type, $guid))
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 (!$min && !$max)
return '';
$str = $isTime ? Util::formatTime($min, true) : $min;
if ($max > $min)
$str .= ' &ndash; '.($isTime ? Util::formatTime($max, true) : $max);
return $str;
}
private function formatTime(int $time, int $_, bool $isMilliSec) : string
{
if (!$time)
return '';
return Util::formatTime($time * ($isMilliSec ? 1 : 1000), false);
}
private function castFlags(int $flags) : string
{
$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
{
$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
{
$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
{
$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
{
$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
{
$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 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
private array $jsGlobals = [];
private array $rawData = [];
private array $result = [];
private array $tabs = [];
private array $itr = [];
private array $quotes = [];
// misc data
public readonly int $baseEntry; // I'm a timed action list belonging to this entry
public readonly string $title; // title appendix for the [toggle]
public readonly int $teleportTargetArea; // precalculated areaId so we don't have to look it up right now
public function __construct(public readonly int $srcType = 0, public readonly int $entry = 0, array $miscData = [])
{
$this->baseEntry = $miscData['baseEntry'] ?? 0;
$this->title = $miscData['title'] ?? '';
$this->teleportTargetArea = $miscData['teleportTargetArea'] ?? 0;
$raw = DB::World()->select(
'SELECT `id`, `link`,
`event_type`, `event_phase_mask`, `event_chance`, `event_flags`, `event_param1`, `event_param2`, `event_param3`, `event_param4`, `event_param5`,
`action_type`, `action_param1`, `action_param2`, `action_param3`, `action_param4`, `action_param5`, `action_param6`,
`target_type`, `target_param1`, `target_param2`, `target_param3`, `target_param4`, `target_x`, `target_y`, `target_z`, `target_o`
FROM smart_scripts
WHERE `entryorguid` = ?d AND `source_type` = ?d
ORDER BY `id` ASC',
$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)
);
}
}
/*********************/
/* 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` = ?d AND `typeId` = ?d', Type::NPC, $npcId))
if ($groups = DB::World()->selectCol('SELECT `groupId` FROM spawn_group WHERE `spawnType` = 0 AND `spawnId` IN (?a)', $npcGuids))
foreach ($groups as $g)
$lookup[SmartAction::ACTION_SPAWN_SPAWNGROUP][1] = $g;
$result = self::getActionOwner($lookup, $typeFilter);
// can skip lookups for SmartAction::ACTION_SUMMON_CREATURE_GROUP as creature_summon_groups already contains summoner info
if ($sgs = DB::World()->select('SELECT `summonerType` AS "0", `summonerId` AS "1" FROM creature_summon_groups WHERE `entry` = ?d', $npcId))
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` = ?d AND `typeId` = ?d', Type::OBJECT, $objectId))
if ($groups = DB::World()->selectCol('SELECT `groupId` FROM spawn_group WHERE `spawnType` = 1 AND `spawnId` IN (?a)', $objGuids))
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;
}
foreach ($lookup as $action => $params)
{
$aq = '(`action_type` = '.(int)$action.' AND (';
$pq = [];
foreach ($params as $idx => $p)
$pq[] = '`action_param'.(int)$idx.'` = '.(int)$p;
if ($pq)
$qParts[] = $aq.implode(' OR ', $pq).'))';
}
$smartS = DB::World()->select(sprintf('SELECT `source_type` AS "0", `entryOrGUID` AS "1" FROM smart_scripts WHERE (%s){ AND `source_type` IN (?a)}', $qParts ? implode(' OR ', $qParts) : '0'), $genFilter ?: DBSIMPLE_SKIP);
// filter for TAL shenanigans
if ($smartTAL = array_filter($smartS, fn($x) => $x[0] == self::SRC_TYPE_ACTIONLIST))
{
$smartS = array_diff_key($smartS, $smartTAL);
$q = [];
foreach ($smartTAL as [, $eog])
{
// SmartAction::ACTION_CALL_TIMED_ACTIONLIST
$q[] = '`action_type` = '.SmartAction::ACTION_CALL_TIMED_ACTIONLIST.' AND `action_param1` = '.$eog;
// SmartAction::ACTION_CALL_RANDOM_TIMED_ACTIONLIST
$q[] = '`action_type` = '.SmartAction::ACTION_CALL_RANDOM_TIMED_ACTIONLIST.' AND (`action_param1` = '.$eog.' OR `action_param2` = '.$eog.' OR `action_param3` = '.$eog.' OR `action_param4` = '.$eog.' OR `action_param5` = '.$eog.')';
// SmartAction::ACTION_CALL_RANDOM_RANGE_TIMED_ACTIONLIST
$q[] = '`action_type` = '.SmartAction::ACTION_CALL_RANDOM_RANGE_TIMED_ACTIONLIST.' AND `action_param1` <= '.$eog.' AND `action_param2` >= '.$eog;
}
if ($_ = DB::World()->select(sprintf('SELECT `source_type` AS "0", `entryOrGUID` AS "1" FROM smart_scripts WHERE ((%s)){ AND `source_type` IN (?a)}', $q ? implode(') OR (', $q) : '0'), $talFilter ?: DBSIMPLE_SKIP))
$smartS = array_merge($smartS, $_);
}
// filter guids for entries
if ($smartG = array_filter($smartS, fn($x) => $x[1] < 0))
{
$smartS = array_diff_key($smartS, $smartG);
$q = [];
foreach ($smartG as [$st, $eog])
{
if ($st == self::SRC_TYPE_CREATURE)
$q[] = '`type` = '.Type::NPC.' AND `guid` = '.-$eog;
else if ($st == self::SRC_TYPE_OBJECT)
$q[] = '`type` = '.Type::OBJECT.' AND `guid` = '.-$eog;
}
if ($q)
{
$owner = DB::Aowow()->select(sprintf('SELECT `type`, `typeId` FROM ?_spawns WHERE (%s)', implode(') OR (', $q)));
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` = ?d AND `summonerId` = ?d', $st, $entry))
$result = array_merge($result, $csg);
}
if (!empty($moreInfo[SmartAction::ACTION_SPAWN_SPAWNGROUP]))
{
$grp = $moreInfo[SmartAction::ACTION_SPAWN_SPAWNGROUP];
if ($sgs = DB::World()->selectCol('SELECT `spawnId` FROM spawn_group WHERE `spawnType` = ?d AND `groupId` IN (?a)', SUMMONER_TYPE_CREATURE, $grp))
if ($ids = DB::Aowow()->selectCol('SELECT DISTINCT `typeId` FROM ?_spawns WHERE `type` = ?d AND `guid` IN (?a)', Type::NPC, $sgs))
$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` = ?d AND `groupId` IN (?a)', SUMMONER_TYPE_GAMEOBJECT, $grp))
if ($ids = DB::Aowow()->selectCol('SELECT DISTINCT `typeId` FROM ?_spawns WHERE `type` = ?d AND `guid` IN (?a)', Type::OBJECT, $sgs))
$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` = ?d AND `action_type` IN (?a) AND `entryOrGUID` IN (?a)';
$smartScripts = DB::World()->select($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()->select($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;
$hidePhase =
$hideChance = true;
foreach ($this->iterate() as $id => $__)
{
$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())
$hidePhase = false;
if ($this->itr['event']->chance != 100)
$hideChance = false;
$this->result[] = array(
$this->itr['id'],
implode(', ', Util::mask2bits($this->itr['event']->phaseMask, 1)),
$evtBody.($evtFooter ? '[div float=right margin=0px clear=both][i][small class=q0]'.$evtFooter.'[/small][/i][/div]' : null),
$this->itr['event']->chance.'%',
$actBody.($actFooter ? '[div float=right margin=0px clear=both][i][small class=q0]'.$actFooter.'[/small][/i][/div]' : null)
);
}
$th = array(
'#' => 16,
'Phase' => 32,
'Event' => 350,
'Chance' => 24,
'Action' => 0
);
if ($hidePhase)
{
unset($th['Phase']);
foreach ($this->result as &$r)
unset($r[1]);
}
unset($r);
if ($hideChance)
{
unset($th['Chance']);
foreach ($this->result as &$r)
unset($r[3]);
}
unset($r);
$tbl = '[tr]';
foreach ($th as $n => $w)
$tbl .= '[td header '.($w ? 'width='.$w.'px' : null).']'.$n.'[/td]';
$tbl .= '[/tr]';
foreach ($this->result as $r)
$tbl .= '[tr][td]'.implode('[/td][td]', $r).'[/td][/tr]';
if ($this->srcType == self::SRC_TYPE_ACTIONLIST)
$this->tabs[$this->entry] = $tbl;
else
$this->tabs[0] = $tbl;
return true;
}
public function getMarkdown() : string
{
# id | event (footer phase) | chance | action + target
if (!$this->rawData)
return '';
$return = '[style]#text-generic .grid { clear:left; } #text-generic .tabbed-contents { padding:0px; clear:left; }[/style][pad][h3][toggler id=sai]SmartAI'.$this->title.'[/toggler][/h3][div id=sai clear=left]%s[/div]';
if (count($this->tabs) > 1)
{
$wrapper = '[tabs name=sai width=942px]%s[/tabs]';
$return = '[script]function TalTabClick(id) { $(\'#dsf67g4d-sai\').find(\\\'[href=\\\\\'#sai-actionlist-\' + id + \'\\\\\']\\\').click(); }[/script]' . $return;
$tabs = '';
foreach ($this->tabs as $guid => $data)
{
$buff = '[tab name=\"'.($guid ? 'ActionList #'.$guid : 'Main').'\"][table class=grid width=940px]'.$data.'[/table][/tab]';
if ($guid)
$tabs .= $buff;
else
$tabs = $buff . $tabs;
}
return sprintf($return, sprintf($wrapper, $tabs));
}
else
return sprintf($return, '[table class=grid width=940px]'.$this->tabs[0].'[/table]');
}
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 [];
}
}
?>

View File

@@ -1,748 +0,0 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
// TrinityCore - SmartAI
class SmartAction
{
use SmartHelper;
public const ACTION_NONE = 0; // Do nothing
public const ACTION_TALK = 1; // Param2 in Milliseconds.
public const ACTION_SET_FACTION = 2; // Sets faction to creature.
public const ACTION_MORPH_TO_ENTRY_OR_MODEL = 3; // Take DisplayID of creature (param1) OR Turn to DisplayID (param2) OR Both = 0 for Demorph
public const ACTION_SOUND = 4; // TextRange = 0 only sends sound to self, TextRange = 1 sends sound to everyone in visibility range
public const ACTION_PLAY_EMOTE = 5; // Play Emote
public const ACTION_FAIL_QUEST = 6; // Fail Quest of Target
public const ACTION_OFFER_QUEST = 7; // Add Quest to Target
public const ACTION_SET_REACT_STATE = 8; // React State. Can be Passive (0), Defensive (1), Aggressive (2), Assist (3).
public const ACTION_ACTIVATE_GOBJECT = 9; // Activate Object
public const ACTION_RANDOM_EMOTE = 10; // Play Random Emote
public const ACTION_CAST = 11; // Cast Spell ID at Target
public const ACTION_SUMMON_CREATURE = 12; // Summon Unit
public const ACTION_THREAT_SINGLE_PCT = 13; // Change Threat Percentage for Single Target
public const ACTION_THREAT_ALL_PCT = 14; // Change Threat Percentage for All Enemies
public const ACTION_CALL_AREAEXPLOREDOREVENTHAPPENS = 15; //
public const ACTION_SET_INGAME_PHASE_ID = 16; // [RESERVED] For 4.3.4 + only
public const ACTION_SET_EMOTE_STATE = 17; // Play Emote Continuously
public const ACTION_SET_UNIT_FLAG = 18; // [DEPRECATED] Can set Multi-able flags at once
public const ACTION_REMOVE_UNIT_FLAG = 19; // [DEPRECATED] Can Remove Multi-able flags at once
public const ACTION_AUTO_ATTACK = 20; // Stop or Continue Automatic Attack.
public const ACTION_ALLOW_COMBAT_MOVEMENT = 21; // Allow or Disable Combat Movement
public const ACTION_SET_EVENT_PHASE = 22; //
public const ACTION_INC_EVENT_PHASE = 23; // Set param1 OR param2 (not both). Value 0 has no effect.
public const ACTION_EVADE = 24; // Evade Incoming Attack
public const ACTION_FLEE_FOR_ASSIST = 25; // If you want the fleeing NPC to say '%s attempts to run away in fear' on flee, use 1 on param1. 0 for no message.
public const ACTION_CALL_GROUPEVENTHAPPENS = 26; //
public const ACTION_COMBAT_STOP = 27; //
public const ACTION_REMOVEAURASFROMSPELL = 28; // 0 removes all auras
public const ACTION_FOLLOW = 29; // Follow Target
public const ACTION_RANDOM_PHASE = 30; //
public const ACTION_RANDOM_PHASE_RANGE = 31; //
public const ACTION_RESET_GOBJECT = 32; // Reset Gameobject
public const ACTION_CALL_KILLEDMONSTER = 33; // This is the ID from quest_template.RequiredNpcOrGo
public const ACTION_SET_INST_DATA = 34; // Set Instance Data
public const ACTION_SET_INST_DATA64 = 35; // Set Instance Data uint64
public const ACTION_UPDATE_TEMPLATE = 36; // Updates creature_template to given entry
public const ACTION_DIE = 37; // Kill Target
public const ACTION_SET_IN_COMBAT_WITH_ZONE = 38; //
public const ACTION_CALL_FOR_HELP = 39; // If you want the NPC to say '%s calls for help!'. Use 1 on param1, 0 for no message.
public const ACTION_SET_SHEATH = 40; //
public const ACTION_FORCE_DESPAWN = 41; // Despawn Target after param1 in Milliseconds. If you want to set respawn time set param2 in seconds.
public const ACTION_SET_INVINCIBILITY_HP_LEVEL = 42; // If you use both params, only percent will be used.
public const ACTION_MOUNT_TO_ENTRY_OR_MODEL = 43; // Mount to Creature Entry (param1) OR Mount to Creature Display (param2) Or both = 0 for Unmount
public const ACTION_SET_INGAME_PHASE_MASK = 44; //
public const ACTION_SET_DATA = 45; // Set Data For Target, can be used with SMART_EVENT_DATA_SET
public const ACTION_ATTACK_STOP = 46; //
public const ACTION_SET_VISIBILITY = 47; // Makes creature Visible = 1 or Invisible = 0
public const ACTION_SET_ACTIVE = 48; //
public const ACTION_ATTACK_START = 49; // Allows basic melee swings to creature.
public const ACTION_SUMMON_GO = 50; // Spawns Gameobject, use target_type to set spawn position.
public const ACTION_KILL_UNIT = 51; // Kills Creature.
public const ACTION_ACTIVATE_TAXI = 52; // Sends player to flight path. You have to be close to Flight Master, which gives Taxi ID you need.
public const ACTION_WP_START = 53; // Creature starts Waypoint Movement. Use waypoints table to create movement.
public const ACTION_WP_PAUSE = 54; // Creature pauses its Waypoint Movement for given time.
public const ACTION_WP_STOP = 55; // Creature stops its Waypoint Movement.
public const ACTION_ADD_ITEM = 56; // Adds item(s) to player.
public const ACTION_REMOVE_ITEM = 57; // Removes item(s) from player.
public const ACTION_INSTALL_AI_TEMPLATE = 58; // [DEPRECATED]
public const ACTION_SET_RUN = 59; //
public const ACTION_SET_DISABLE_GRAVITY = 60; // Only works for creatures with inhabit air.
public const ACTION_SET_SWIM = 61; // [DEPRECATED]
public const ACTION_TELEPORT = 62; // Continue this action with the TARGET_TYPE column. Use any target_type (except 0), and use target_x, target_y, target_z, target_o as the coordinates
public const ACTION_SET_COUNTER = 63; //
public const ACTION_STORE_TARGET_LIST = 64; //
public const ACTION_WP_RESUME = 65; // Creature continues in its Waypoint Movement.
public const ACTION_SET_ORIENTATION = 66; //
public const ACTION_CREATE_TIMED_EVENT = 67; //
public const ACTION_PLAYMOVIE = 68; //
public const ACTION_MOVE_TO_POS = 69; // PointId is called by SMART_EVENT_MOVEMENTINFORM. Continue this action with the TARGET_TYPE column. Use any target_type, and use target_x, target_y, target_z, target_o as the coordinates
public const ACTION_ENABLE_TEMP_GOBJ = 70; // param1 = duration
public const ACTION_EQUIP = 71; // 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 Param1 is set
public const ACTION_CLOSE_GOSSIP = 72; // Closes gossip window.
public const ACTION_TRIGGER_TIMED_EVENT = 73; //
public const ACTION_REMOVE_TIMED_EVENT = 74; //
public const ACTION_ADD_AURA = 75; // [DEPRECATED] Adds aura to player(s). Use target_type 17 to make AoE aura.
public const ACTION_OVERRIDE_SCRIPT_BASE_OBJECT = 76; // [DEPRECATED] WARNING: CAN CRASH CORE, do not use if you dont know what you are doing
public const ACTION_RESET_SCRIPT_BASE_OBJECT = 77; // [DEPRECATED]
public const ACTION_CALL_SCRIPT_RESET = 78; //
public const ACTION_SET_RANGED_MOVEMENT = 79; // Sets movement to follow at a specific range to the target.
public const ACTION_CALL_TIMED_ACTIONLIST = 80; //
public const ACTION_SET_NPC_FLAG = 81; //
public const ACTION_ADD_NPC_FLAG = 82; //
public const ACTION_REMOVE_NPC_FLAG = 83; //
public const ACTION_SIMPLE_TALK = 84; // Makes a player say text. SMART_EVENT_TEXT_OVER is not triggered and whispers can not be used.
public const ACTION_SELF_CAST = 85; // spellID, castFlags
public const ACTION_CROSS_CAST = 86; // This action is used to make selected caster (in CasterTargetType) to cast spell. Actual target is entered in target_type as normally.
public const ACTION_CALL_RANDOM_TIMED_ACTIONLIST = 87; // Will select one entry from the ones provided. 0 is ignored.
public const ACTION_CALL_RANDOM_RANGE_TIMED_ACTIONLIST = 88; // 0 is ignored.
public const ACTION_RANDOM_MOVE = 89; // Creature moves to random position in given radius.
public const ACTION_SET_UNIT_FIELD_BYTES_1 = 90; //
public const ACTION_REMOVE_UNIT_FIELD_BYTES_1 = 91; //
public const ACTION_INTERRUPT_SPELL = 92; // This action allows you to interrupt the current spell being cast. If you do not set the spellId, the core will find the current spell depending on the withDelay and the withInstant values.
public const ACTION_SEND_GO_CUSTOM_ANIM = 93; // [DEPRECATED] oldFlag = newFlag
public const ACTION_SET_DYNAMIC_FLAG = 94; // [DEPRECATED] oldFlag |= newFlag
public const ACTION_ADD_DYNAMIC_FLAG = 95; // [DEPRECATED] oldFlag &= ~newFlag
public const ACTION_REMOVE_DYNAMIC_FLAG = 96; // [DEPRECATED]
public const ACTION_JUMP_TO_POS = 97; //
public const ACTION_SEND_GOSSIP_MENU = 98; // Can be used together with 'SMART_EVENT_GOSSIP_HELLO' to set custom gossip.
public const ACTION_GO_SET_LOOT_STATE = 99; //
public const ACTION_SEND_TARGET_TO_TARGET = 100; // Send targets previously stored with SMART_ACTION_STORE_TARGET, to another npc/go, the other npc/go can then access them as if it was its own stored list
public const ACTION_SET_HOME_POS = 101; // Use with SMART_TARGET_SELF or SMART_TARGET_POSITION
public const ACTION_SET_HEALTH_REGEN = 102; // Sets the current creatures health regen on or off.
public const ACTION_SET_ROOT = 103; // Enables or disables creature movement
public const ACTION_SET_GO_FLAG = 104; // [DEPRECATED] oldFlag = newFlag
public const ACTION_ADD_GO_FLAG = 105; // [DEPRECATED] oldFlag |= newFlag
public const ACTION_REMOVE_GO_FLAG = 106; // [DEPRECATED] oldFlag &= ~newFlag
public const ACTION_SUMMON_CREATURE_GROUP = 107; // Use creature_summon_groups table. SAI target has no effect, use 0
public const ACTION_SET_POWER = 108; //
public const ACTION_ADD_POWER = 109; //
public const ACTION_REMOVE_POWER = 110; //
public const ACTION_GAME_EVENT_STOP = 111; //
public const ACTION_GAME_EVENT_START = 112; //
public const ACTION_START_CLOSEST_WAYPOINT = 113; // Make target follow closest waypoint to its location
public const ACTION_MOVE_OFFSET = 114; // Use target_x, target_y, target_z With target_type=1
public const ACTION_RANDOM_SOUND = 115; //
public const ACTION_SET_CORPSE_DELAY = 116; //
public const ACTION_DISABLE_EVADE = 117; //
public const ACTION_GO_SET_GO_STATE = 118; //
public const ACTION_SET_CAN_FLY = 119; // [DEPRECATED]
public const ACTION_REMOVE_AURAS_BY_TYPE = 120; // [DEPRECATED]
public const ACTION_SET_SIGHT_DIST = 121; // [DEPRECATED]
public const ACTION_FLEE = 122; // [DEPRECATED]
public const ACTION_ADD_THREAT = 123; //
public const ACTION_LOAD_EQUIPMENT = 124; //
public const ACTION_TRIGGER_RANDOM_TIMED_EVENT = 125; //
public const ACTION_REMOVE_ALL_GAMEOBJECTS = 126; // [DEPRECATED]
public const ACTION_PAUSE_MOVEMENT = 127; // MovementSlot (default = 0, active = 1, controlled = 2), PauseTime (ms), Force
public const ACTION_PLAY_ANIMKIT = 128; // [RESERVED] don't use on 3.3.5a
public const ACTION_SCENE_PLAY = 129; // [RESERVED] don't use on 3.3.5a
public const ACTION_SCENE_CANCEL = 130; // [RESERVED] don't use on 3.3.5a
public const ACTION_SPAWN_SPAWNGROUP = 131; //
public const ACTION_DESPAWN_SPAWNGROUP = 132; //
public const ACTION_RESPAWN_BY_SPAWNID = 133; // type, typeGuid - Use to respawn npcs and gobs, the target in this case is always=1 and only a single unit could be a target via the spawnId (action_param1, action_param2)
public const ACTION_INVOKER_CAST = 134; // spellID, castFlags
public const ACTION_PLAY_CINEMATIC = 135; // cinematic
public const ACTION_SET_MOVEMENT_SPEED = 136; // movementType, speedInteger, speedFraction
public const ACTION_PLAY_SPELL_VISUAL_KIT = 137; // [RESERVED] spellVisualKitId
public const ACTION_OVERRIDE_LIGHT = 138; // zoneId, areaLightId, overrideLightID, transitionMilliseconds
public const ACTION_OVERRIDE_WEATHER = 139; // zoneId, weatherId, intensity
public const ACTION_SET_AI_ANIM_KIT = 140; // [RESERVED]
public const ACTION_SET_HOVER = 141; // Enable/Disable hover for target units.
public const ACTION_SET_HEALTH_PCT = 142; // Set current health percentage of target units.
public const ACTION_CREATE_CONVERSATION = 143; // [RESERVED]
public const ACTION_SET_IMMUNE_PC = 144; // Enable/Disable immunity to players of target units.
public const ACTION_SET_IMMUNE_NPC = 145; // Enable/Disable immunity to creatures of target units.
public const ACTION_SET_UNINTERACTIBLE = 146; // Make/Reset target units uninteractible.
public const ACTION_ACTIVATE_GAMEOBJECT = 147; // Activate target gameobjects, using given action.
public const ACTION_ADD_TO_STORED_TARGET_LIST = 148; // Add selected targets to varID for later use.
public const ACTION_BECOME_PERSONAL_CLONE_FOR_PLAYER = 149; // [RESERVED]
public const ACTION_TRIGGER_GAME_EVENT = 150; // [RESERVED]
public const ACTION_DO_ACTION = 151; // [RESERVED]
public const ACTION_ALL_SPELLCASTS = [self::ACTION_CAST, self::ACTION_ADD_AURA, self::ACTION_INVOKER_CAST, self::ACTION_SELF_CAST, self::ACTION_CROSS_CAST];
public const ACTION_ALL_TIMED_ACTION_LISTS = [self::ACTION_CALL_TIMED_ACTIONLIST, self::ACTION_CALL_RANDOM_TIMED_ACTIONLIST, self::ACTION_CALL_RANDOM_RANGE_TIMED_ACTIONLIST];
private const ACTION_CELL_TPL = '[tooltip name=a-#rowIdx#]%1$s[/tooltip][span tooltip=a-#rowIdx#]%2$s[/span]';
private const TAL_TAB_ANCHOR = '[url=#sai-actionlist-%1$d onclick=TalTabClick(%1$d)]#%1$d[/url]';
private array $data = array(
self::ACTION_NONE => [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` = ?d', $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?d AS start_loc?d, tn2.`name_loc0` AS "end_loc0", tn2.name_loc?d AS end_loc?d
FROM ?_taxipath tp
JOIN ?_taxinodes tn1 ON tp.`startNodeId` = tn1.`id`
JOIN ?_taxinodes tn2 ON tp.`endNodeId` = tn2.`id`
WHERE tp.`id` = ?d',
Lang::getLocale()->value, Lang::getLocale()->value, Lang::getLocale()->value, Lang::getLocale()->value, $this->param[0]
);
$this->param[10] = Util::jsEscape(Util::localizedString($nodes, 'start'));
$this->param[11] = Util::jsEscape(Util::localizedString($nodes, 'end'));
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` = ?d', $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` = ?d AND `ID` = ?d', $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` = ?d AND `ID` = ?d', $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();
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 <param1> 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();
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();
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` = ?d 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` = ?d', $this->param[0]));
$entities = DB::World()->select('SELECT `spawnType` AS "0", `spawnId` AS "1" FROM spawn_group WHERE `groupId` = ?d', $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
case self::ACTION_SIMPLE_TALK: // 84 -> any target
$noSrc = false;
if ($npcId = $this->smartAI->getTarget()->getTalkSource($noSrc))
{
if ($quotes = $this->smartAI->getQuote($npcId, $this->param[0], $npcSrc))
foreach ($quotes as ['text' => $text, 'prefix' => $prefix])
$this->param[10] .= sprintf($text, $noSrc ? '' : sprintf($prefix, $npcSrc), $npcSrc);
}
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];
}
}
?>

View File

@@ -1,382 +0,0 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
// TrinityCore - SmartAI
class SmartEvent
{
use SmartHelper;
public const EVENT_UPDATE_IC = 0; // In combat.
public const EVENT_UPDATE_OOC = 1; // Out of combat.
public const EVENT_HEALTH_PCT = 2; // Health Percentage
public const EVENT_MANA_PCT = 3; // Mana Percentage
public const EVENT_AGGRO = 4; // On Creature Aggro
public const EVENT_KILL = 5; // On Creature Kill
public const EVENT_DEATH = 6; // On Creature Death
public const EVENT_EVADE = 7; // On Creature Evade Attack
public const EVENT_SPELLHIT = 8; // On Creature/Gameobject Spell Hit
public const EVENT_RANGE = 9; // On Target In Range
public const EVENT_OOC_LOS = 10; // On Target In Distance Out of Combat
public const EVENT_RESPAWN = 11; // On Creature/Gameobject Respawn
public const EVENT_TARGET_HEALTH_PCT = 12; // [DEPRECATED] On Target Health Percentage
public const EVENT_VICTIM_CASTING = 13; // On Target Casting Spell
public const EVENT_FRIENDLY_HEALTH = 14; // [DEPRECATED] On Friendly Health Deficit
public const EVENT_FRIENDLY_IS_CC = 15; //
public const EVENT_FRIENDLY_MISSING_BUFF = 16; // On Friendly Lost Buff
public const EVENT_SUMMONED_UNIT = 17; // On Creature/Gameobject Summoned Unit
public const EVENT_TARGET_MANA_PCT = 18; // [DEPRECATED] On Target Mana Percentage
public const EVENT_ACCEPTED_QUEST = 19; // On Target Accepted Quest
public const EVENT_REWARD_QUEST = 20; // On Target Rewarded Quest
public const EVENT_REACHED_HOME = 21; // On Creature Reached Home
public const EVENT_RECEIVE_EMOTE = 22; // On Receive Emote.
public const EVENT_HAS_AURA = 23; // On Creature Has Aura
public const EVENT_TARGET_BUFFED = 24; // On Target Buffed With Spell
public const EVENT_RESET = 25; // After Combat, On Respawn or Spawn
public const EVENT_IC_LOS = 26; // On Target In Distance In Combat
public const EVENT_PASSENGER_BOARDED = 27; //
public const EVENT_PASSENGER_REMOVED = 28; //
public const EVENT_CHARMED = 29; // On Creature Charmed
public const EVENT_CHARMED_TARGET = 30; // [DEPRECATED] On Target Charmed
public const EVENT_SPELLHIT_TARGET = 31; // On Target Spell Hit
public const EVENT_DAMAGED = 32; // On Creature Damaged
public const EVENT_DAMAGED_TARGET = 33; // On Target Damaged
public const EVENT_MOVEMENTINFORM = 34; // WAYPOINT_MOTION_TYPE = 2, POINT_MOTION_TYPE = 8
public const EVENT_SUMMON_DESPAWNED = 35; // On Summoned Unit Despawned
public const EVENT_CORPSE_REMOVED = 36; // On Creature Corpse Removed
public const EVENT_AI_INIT = 37; //
public const EVENT_DATA_SET = 38; // On Creature/Gameobject Data Set, Can be used with SMART_ACTION_SET_DATA
public const EVENT_WAYPOINT_START = 39; // [DEPRECATED] On Creature Waypoint ID Started
public const EVENT_WAYPOINT_REACHED = 40; // On Creature Waypoint ID Reached
public const EVENT_TRANSPORT_ADDPLAYER = 41; // [RESERVED]
public const EVENT_TRANSPORT_ADDCREATURE = 42; // [RESERVED]
public const EVENT_TRANSPORT_REMOVE_PLAYER = 43; // [RESERVED]
public const EVENT_TRANSPORT_RELOCATE = 44; // [RESERVED]
public const EVENT_INSTANCE_PLAYER_ENTER = 45; // [RESERVED]
public const EVENT_AREATRIGGER_ONTRIGGER = 46; //
public const EVENT_QUEST_ACCEPTED = 47; // [RESERVED] On Target Quest Accepted
public const EVENT_QUEST_OBJ_COMPLETION = 48; // [RESERVED] On Target Quest Objective Completed
public const EVENT_QUEST_COMPLETION = 49; // [RESERVED] On Target Quest Completed
public const EVENT_QUEST_REWARDED = 50; // [RESERVED] On Target Quest Rewarded
public const EVENT_QUEST_FAIL = 51; // [RESERVED] On Target Quest Field
public const EVENT_TEXT_OVER = 52; // On TEXT_OVER Event Triggered After SMART_ACTION_TALK
public const EVENT_RECEIVE_HEAL = 53; // On Creature Received Healing
public const EVENT_JUST_SUMMONED = 54; // On Creature Just spawned
public const EVENT_WAYPOINT_PAUSED = 55; // On Creature Paused at Waypoint ID
public const EVENT_WAYPOINT_RESUMED = 56; // On Creature Resumed after Waypoint ID
public const EVENT_WAYPOINT_STOPPED = 57; // On Creature Stopped On Waypoint ID
public const EVENT_WAYPOINT_ENDED = 58; // On Creature Waypoint Path Ended
public const EVENT_TIMED_EVENT_TRIGGERED = 59; //
public const EVENT_UPDATE = 60; //
public const EVENT_LINK = 61; // Used to link together multiple events as a chain of events.
public const EVENT_GOSSIP_SELECT = 62; // On gossip clicked (gossip_menu_option335).
public const EVENT_JUST_CREATED = 63; //
public const EVENT_GOSSIP_HELLO = 64; // On Right-Click Creature/Gameobject that have gossip enabled.
public const EVENT_FOLLOW_COMPLETED = 65; //
public const EVENT_EVENT_PHASE_CHANGE = 66; // [DEPRECATED] On event phase mask set
public const EVENT_IS_BEHIND_TARGET = 67; // [DEPRECATED] On Creature is behind target.
public const EVENT_GAME_EVENT_START = 68; // On game_event started.
public const EVENT_GAME_EVENT_END = 69; // On game_event ended.
public const EVENT_GO_LOOT_STATE_CHANGED = 70; //
public const EVENT_GO_EVENT_INFORM = 71; //
public const EVENT_ACTION_DONE = 72; //
public const EVENT_ON_SPELLCLICK = 73; //
public const EVENT_FRIENDLY_HEALTH_PCT = 74; //
public const EVENT_DISTANCE_CREATURE = 75; // On creature guid OR any instance of creature entry is within distance.
public const EVENT_DISTANCE_GAMEOBJECT = 76; // On gameobject guid OR any instance of gameobject entry is within distance.
public const EVENT_COUNTER_SET = 77; // If the value of specified counterID is equal to a specified value
public const EVENT_SCENE_START = 78; // [RESERVED] don't use on 3.3.5a
public const EVENT_SCENE_TRIGGER = 79; // [RESERVED] don't use on 3.3.5a
public const EVENT_SCENE_CANCEL = 80; // [RESERVED] don't use on 3.3.5a
public const EVENT_SCENE_COMPLETE = 81; // [RESERVED] don't use on 3.3.5a
public const EVENT_SUMMONED_UNIT_DIES = 82; //
public const EVENT_ON_SPELL_CAST = 83; // On Spell::cast
public const EVENT_ON_SPELL_FAILED = 84; // On Unit::InterruptSpell
public const EVENT_ON_SPELL_START = 85; // On Spell::prapare
public const EVENT_ON_DESPAWN = 86; // On before creature removed
public const FLAG_NO_REPEAT = 0x0001;
public const FLAG_DIFFICULTY_0 = 0x0002;
public const FLAG_DIFFICULTY_1 = 0x0004;
public const FLAG_DIFFICULTY_2 = 0x0008;
public const FLAG_DIFFICULTY_3 = 0x0010;
public const FLAG_DEBUG_ONLY = 0x0080;
public const FLAG_NO_RESET = 0x0100;
public const FLAG_WHILE_CHARMED = 0x0200;
public const FLAG_ALL_DIFFICULTIES = self::FLAG_DIFFICULTY_0 | self::FLAG_DIFFICULTY_1 | self::FLAG_DIFFICULTY_2 | self::FLAG_DIFFICULTY_3;
private const EVENT_CELL_TPL = '[tooltip name=e-#rowIdx#]%1$s[/tooltip][span tooltip=e-#rowIdx#]%2$s[/span]';
private array $data = array( // param 1-5 - int > 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
);
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` = ?d', $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` = ?d AND `entryorguid` = ?d AND `source_type` = ?d', $this->id, $this->smartAI->entry, $this->smartAI->srcType))
$this->param[10] = Lang::concat($links, Lang::CONCAT_OR, fn($x) => "#[b]".$x."[/b]");
break;
case self::EVENT_GOSSIP_SELECT: // 62 - On gossip clicked (gossip_menu_option335).
$gmo = DB::World()->selectRow(
'SELECT gmo.`OptionText` AS "text_loc0" {, gmol.`OptionText` AS text_loc?d }
FROM gossip_menu_option gmo
LEFT JOIN gossip_menu_option_locale gmol ON gmo.`MenuID` = gmol.`MenuID` AND gmo.`OptionID` = gmol.`OptionID` AND gmol.`Locale` = ?d
WHERE gmo.`MenuId` = ?d AND gmo.`OptionID` = ?d',
Lang::getLocale() != Locale::EN ? Lang::getLocale()->value : DBSIMPLE_SKIP,
Lang::getLocale()->json(),
$this->param[0], $this->param[1]
);
if ($gmo)
$this->param[10] = Util::jsEscape(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 == 0;
}
private function formatFlags() : string
{
$flags = $this->flags;
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);
}
}
?>

View File

@@ -1,185 +0,0 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
// TrinityCore - SmartAI
class SmartTarget
{
use SmartHelper;
public const TARGET_NONE = 0; // None.
public const TARGET_SELF = 1; // Self cast.
public const TARGET_VICTIM = 2; // Our current target. (ie: highest aggro)
public const TARGET_HOSTILE_SECOND_AGGRO = 3; // Second highest aggro.
public const TARGET_HOSTILE_LAST_AGGRO = 4; // Dead last on aggro.
public const TARGET_HOSTILE_RANDOM = 5; // Just any random target on our threat list.
public const TARGET_HOSTILE_RANDOM_NOT_TOP = 6; // Any random target except top threat.
public const TARGET_ACTION_INVOKER = 7; // Unit who caused this Event to occur.
public const TARGET_POSITION = 8; // Use xyz from event params.
public const TARGET_CREATURE_RANGE = 9; // (Random?) creature with specified ID within specified range.
public const TARGET_CREATURE_GUID = 10; // Creature with specified GUID.
public const TARGET_CREATURE_DISTANCE = 11; // Creature with specified ID within distance. (Different from #9?)
public const TARGET_STORED = 12; // Uses pre-stored target(list)
public const TARGET_GAMEOBJECT_RANGE = 13; // (Random?) object with specified ID within specified range.
public const TARGET_GAMEOBJECT_GUID = 14; // Object with specified GUID.
public const TARGET_GAMEOBJECT_DISTANCE = 15; // Object with specified ID within distance. (Different from #13?)
public const TARGET_INVOKER_PARTY = 16; // Invoker's party members
public const TARGET_PLAYER_RANGE = 17; // (Random?) player within specified range.
public const TARGET_PLAYER_DISTANCE = 18; // (Random?) player within specified distance. (Different from #17?)
public const TARGET_CLOSEST_CREATURE = 19; // Closest creature with specified ID within specified range.
public const TARGET_CLOSEST_GAMEOBJECT = 20; // Closest object with specified ID within specified range.
public const TARGET_CLOSEST_PLAYER = 21; // Closest player within specified range.
public const TARGET_ACTION_INVOKER_VEHICLE = 22; // Unit's vehicle who caused this Event to occur
public const TARGET_OWNER_OR_SUMMONER = 23; // Unit's owner or summoner
public const TARGET_THREAT_LIST = 24; // All units on creature's threat list
public const TARGET_CLOSEST_ENEMY = 25; // Any attackable target (creature or player) within maxDist
public const TARGET_CLOSEST_FRIENDLY = 26; // Any friendly unit (creature, player or pet) within maxDist
public const TARGET_LOOT_RECIPIENTS = 27; // All tagging players
public const TARGET_FARTHEST = 28; // Farthest unit on the threat list
public const TARGET_VEHICLE_PASSENGER = 29; // Vehicle can target unit in given seat
public const TARGET_CLOSEST_UNSPAWNED_GO = 30; // entry(0any), maxDist
private const TARGET_TPL = '[tooltip name=t-#rowIdx#]%1$s[/tooltip][span class=tip tooltip=t-#rowIdx#]%2$s[/span]';
private array $targets = array(
self::TARGET_NONE => [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()
};
}
}
?>

View File

@@ -1,546 +0,0 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
/************
* get Community Content
************/
/*
{id:115,user:'Ciderhelm',date:'2010/05/10 19:14:18',caption:'TankSpot\'s Guide to the Fury Warrior (Part 1)',videoType:1,videoId:'VUvxFvVmttg',type:13,typeId:1},
{id:116,user:'Ciderhelm',date:'2010/05/10 19:14:18',caption:'TankSpot\'s Guide to the Fury Warrior (Part 2)',videoType:1,videoId:'VEfnuIcq7n8',type:13,typeId:1},
{id:117,user:'Ciderhelm',date:'2010/05/10 19:14:18',caption:'TankSpot\'s Protection Warrior Guide',videoType:1,videoId:'vF-7kmvJZXY',type:13,typeId:1,sticky:1}
*/
/* todo: administration of content */
class CommunityContent
{
private static array $jsGlobals = [];
private static array $subjCache = [];
private static string $coCountQuery =
'SELECT COUNT(1)
FROM ?_comments c
WHERE c.`replyTo` = ?d AND c.`type` = ?d AND c.`typeId` = ?d AND
((c.`flags` & ?d) = 0 OR c.`userId` = ?d OR ?d)';
private static string $coQuery =
'SELECT c.*,
a1.`username` AS "user",
a2.`username` AS "editUser",
a3.`username` AS "deleteUser",
a4.`username` AS "responseUser",
IFNULL(SUM(ur.`value`), 0) AS "rating",
SUM(IF(ur.`userId` > 0 AND ur.`userId` = ?d, ur.`value`, 0)) AS "userRating",
IF(r.`id` IS NULL, 0, 1) AS "userReported"
FROM ?_comments c
JOIN ?_account a1 ON c.`userId` = a1.`id`
LEFT JOIN ?_account a2 ON c.`editUserId` = a2.`id`
LEFT JOIN ?_account a3 ON c.`deleteUserId` = a3.`id`
LEFT JOIN ?_account a4 ON c.`responseUserId` = a4.`id`
LEFT JOIN ?_user_ratings ur ON c.`id` = ur.`entry` AND ur.`type` = ?d
LEFT JOIN ?_reports r ON r.`subject` = c.`id` AND r.`mode` = ?d AND r.`userId` = ?d
WHERE c.`replyTo` = ?d AND c.`type` = ?d AND c.`typeId` = ?d AND
((c.`flags` & ?d) = 0 OR c.`userId` = ?d OR ?d)
GROUP BY c.`id`
ORDER BY c.`date` ASC';
private static string $ssQuery =
'SELECT s.`id` AS ARRAY_KEY, s.`id`, a.`username` AS "user", s.`date`, s.`width`, s.`height`, s.`caption`, IF(s.`status` & ?d, 1, 0) AS "sticky", s.`type`, s.`typeId`
FROM ?_screenshots s
LEFT JOIN ?_account a ON s.`userIdOwner` = a.`id`
WHERE { s.`userIdOwner` = ?d AND }{ s.`type` = ? AND }{ s.`typeId` = ? AND } s.`status` & ?d AND (s.`status` & ?d) = 0
{ ORDER BY ?# DESC }
{ LIMIT ?d }';
private static string $viQuery =
'SELECT v.`id` AS ARRAY_KEY, v.`id`, a.`username` AS "user", v.`date`, v.`videoId`, v.`caption`, IF(v.`status` & ?d, 1, 0) AS "sticky", v.`type`, v.`typeId`
FROM ?_videos v
LEFT JOIN ?_account a ON v.`userIdOwner` = a.`id`
WHERE { v.`userIdOwner` = ?d AND }{ v.`type` = ? AND }{ v.`typeId` = ? AND } v.`status` & ?d AND (v.`status` & ?d) = 0
{ ORDER BY ?# DESC }
{ LIMIT ?d }';
private static string $previewQuery =
'SELECT c.`id`,
c.`body` AS "preview",
c.`date`,
c.`replyTo` AS "commentid",
IF(c.`flags` & ?d, 1, 0) AS "deleted",
IF(c.`type` <> 0, c.`type`, c2.`type`) AS "type",
IF(c.`typeId` <> 0, c.`typeId`, c2.`typeId`) AS "typeId",
IFNULL(SUM(ur.`value`), 0) AS "rating",
a.`username` AS "user"
FROM ?_comments c
JOIN ?_account a ON c.`userId` = a.`id`
LEFT JOIN ?_user_ratings ur ON ur.`entry` = c.`id` AND ur.`userId` <> 0 AND ur.`type` = 1
LEFT JOIN ?_comments c2 ON c.`replyTo` = c2.`id`
WHERE %s
((c.`flags` & ?d) = 0 OR c.`userId` = ?d OR ?d)
GROUP BY c.`id`
ORDER BY c.`date` DESC
{ LIMIT ?d }';
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, [Cfg::get('SQL_LIMIT_NONE'), ['id', $_]]);
if (!$obj)
continue;
foreach ($obj->iterate() as $id => $__)
self::$subjCache[$type][$id] = $obj->getField('name', true);
}
}
public static function getCommentPreviews(array $opt = [], ?int &$nFound = 0, bool $dateFmt = true) : 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];
$w = [];
if ($opt['user'])
$w[] = sprintf('c.userId = %d AND', $opt['user']);
if ($opt['unrated'])
$w[] = 'ur.entry IS NULL AND';
if ($opt['comments'] && !$opt['replies'])
$w[] = 'c.replyTo = 0 AND';
else if (!$opt['comments'] && $opt['replies'])
$w[] = 'c.replyTo <> 0 AND';
// else
// pick both and no extra constraint needed for that
$query = sprintf(self::$previewQuery, implode(' ', $w));
$comments = DB::Aowow()->select(
$query,
CC_FLAG_DELETED,
CC_FLAG_DELETED,
User::$id,
User::isInGroup(U_GROUP_COMMENTS_MODERATOR),
Cfg::get('SQL_LIMIT_DEFAULT')
);
if (!$comments)
return [];
$nFound = DB::Aowow()->selectCell(
substr_replace($query, 'SELECT COUNT(*) ', 0, strpos($query, 'FROM')),
CC_FLAG_DELETED,
User::$id,
User::isInGroup(U_GROUP_COMMENTS_MODERATOR),
DBSIMPLE_SKIP
);
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 nonexistant subject.', E_USER_NOTICE);
unset($comments[$idx]);
}
}
return array_values($comments);
}
public static function getCommentReplies(int $commentId, int $limit = 0, ?int &$nFound = 0) : array
{
$replies = [];
$query = $limit > 0 ? self::$coQuery.' LIMIT '.$limit : self::$coQuery;
// get replies
if ($results = DB::Aowow()->select($query, User::$id, RATING_COMMENT, Report::MODE_COMMENT, User::$id, $commentId, 0, 0, CC_FLAG_DELETED, User::$id, User::isInGroup(U_GROUP_COMMENTS_MODERATOR)))
{
$nFound = DB::Aowow()->selectCell(self::$coCountQuery, $commentId, 0, 0, CC_FLAG_DELETED, User::$id, User::isInGroup(U_GROUP_COMMENTS_MODERATOR));
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 getScreenshotsForManager($type, $typeId, $userId = 0)
{
$screenshots = DB::Aowow()->select(
'SELECT s.`id`, a.`username` AS "user", s.`date`, s.`width`, s.`height`, s.`type`, s.`typeId`, s.`caption`, s.`status`, s.`status` AS "flags"
FROM ?_screenshots s
LEFT JOIN ?_account a ON s.`userIdOwner` = a.`id`
WHERE
{ s.`type` = ?d}
{ AND s.`typeId` = ?d}
{ s.`userIdOwner` = ?d}
LIMIT 100',
$userId ? DBSIMPLE_SKIP : $type,
$userId ? DBSIMPLE_SKIP : $typeId,
$userId ? $userId : DBSIMPLE_SKIP
);
$num = [];
foreach ($screenshots as $s)
{
if (empty($num[$s['type']][$s['typeId']]))
$num[$s['type']][$s['typeId']] = 1;
else
$num[$s['type']][$s['typeId']]++;
}
// format data to meet requirements of the js
foreach ($screenshots as $idx => &$s)
{
$s['date'] = date(Util::$dateFormatInternal, $s['date']);
$s['name'] = "Screenshot #".$s['id']; // what should we REALLY name it?
if (isset($screenshots[$idx - 1]))
$s['prev'] = $idx - 1;
if (isset($screenshots[$idx + 1]))
$s['next'] = $idx + 1;
// order gives priority for 'status'
if (!($s['flags'] & CC_FLAG_APPROVED))
{
$s['pending'] = 1;
$s['status'] = 0;
}
else
$s['status'] = 100;
if ($s['flags'] & CC_FLAG_STICKY)
{
$s['sticky'] = 1;
$s['status'] = 105;
}
if ($s['flags'] & CC_FLAG_DELETED)
{
$s['deleted'] = 1;
$s['status'] = 999;
}
// something todo with massSelect .. am i doing this right?
if ($num[$s['type']][$s['typeId']] == 1)
$s['unique'] = 1;
if (!$s['user'])
unset($s['user']);
}
return $screenshots;
}
public static function getScreenshotPagesForManager($all, &$nFound)
{
// i GUESS .. ss_getALL ? everything : pending
$nFound = 0;
$pages = DB::Aowow()->select(
'SELECT s.`type`, s.`typeId`, COUNT(1) AS "count", MIN(s.`date`) AS "date"
FROM ?_screenshots s
{ WHERE (s.`status` & ?d) = 0 }
GROUP BY s.`type`, s.`typeId`',
$all ? DBSIMPLE_SKIP : CC_FLAG_APPROVED | CC_FLAG_DELETED
);
if ($pages)
{
// limit to one actually existing type each
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, [Cfg::get('SQL_LIMIT_NONE'), ['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('Screenshot linked to nonexistant type/typeId combination: '.$p['type'].'/'.$p['typeId'], E_USER_NOTICE);
unset($p);
}
else
{
$nFound += $p['count'];
$p['date'] = date(Util::$dateFormatInternal, $p['date']);
}
}
}
return $pages;
}
public static function getComments(int $type, int $typeId) : array
{
$results = DB::Aowow()->query(self::$coQuery, User::$id, RATING_COMMENT, Report::MODE_COMMENT, User::$id, 0, $type, $typeId, CC_FLAG_DELETED, User::$id, (int)User::isInGroup(U_GROUP_COMMENTS_MODERATOR));
$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) : array
{
$videos = DB::Aowow()->select(self::$viQuery,
CC_FLAG_STICKY,
$typeOrUser < 0 ? -$typeOrUser : DBSIMPLE_SKIP,
$typeOrUser > 0 ? $typeOrUser : DBSIMPLE_SKIP,
$typeOrUser > 0 ? $typeId : DBSIMPLE_SKIP,
CC_FLAG_APPROVED,
CC_FLAG_DELETED,
!$typeOrUser ? 'date' : DBSIMPLE_SKIP,
!$typeOrUser ? Cfg::get('SQL_LIMIT_SEARCH') : DBSIMPLE_SKIP
);
if (!$videos)
return [];
$nFound = DB::Aowow()->selectCell(
substr_replace(self::$viQuery, 'SELECT COUNT(*) ', 0, strpos(self::$viQuery, 'FROM')),
$typeOrUser < 0 ? -$typeOrUser : DBSIMPLE_SKIP,
$typeOrUser > 0 ? $typeOrUser : DBSIMPLE_SKIP,
$typeOrUser > 0 ? $typeId : DBSIMPLE_SKIP,
CC_FLAG_APPROVED,
CC_FLAG_DELETED,
!$typeOrUser ? 'date' : DBSIMPLE_SKIP,
DBSIMPLE_SKIP
);
if ($typeOrUser <= 0) // not for search by type/typeId
{
foreach ($videos as $v)
self::addSubject($v['type'], $v['typeId']);
self::getSubjects();
}
// format data to meet requirements of the js
foreach ($videos as &$v)
{
if ($typeOrUser <= 0) // not for search by type/typeId
{
if (!empty(self::$subjCache[$v['type']][$v['typeId']]) && !is_numeric(self::$subjCache[$v['type']][$v['typeId']]))
$v['subject'] = self::$subjCache[$v['type']][$v['typeId']];
else
$v['subject'] = Lang::user('removed');
}
$v['date'] = $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) : array
{
$screenshots = DB::Aowow()->select(self::$ssQuery,
CC_FLAG_STICKY,
$typeOrUser < 0 ? -$typeOrUser : DBSIMPLE_SKIP,
$typeOrUser > 0 ? $typeOrUser : DBSIMPLE_SKIP,
$typeOrUser > 0 ? $typeId : DBSIMPLE_SKIP,
CC_FLAG_APPROVED,
CC_FLAG_DELETED,
!$typeOrUser ? 'date' : DBSIMPLE_SKIP,
!$typeOrUser ? Cfg::get('SQL_LIMIT_SEARCH') : DBSIMPLE_SKIP
);
if (!$screenshots)
return [];
$nFound = DB::Aowow()->selectCell(
substr_replace(self::$ssQuery, 'SELECT COUNT(*) ', 0, strpos(self::$ssQuery, 'FROM')),
$typeOrUser < 0 ? -$typeOrUser : DBSIMPLE_SKIP,
$typeOrUser > 0 ? $typeOrUser : DBSIMPLE_SKIP,
$typeOrUser > 0 ? $typeId : DBSIMPLE_SKIP,
CC_FLAG_APPROVED,
CC_FLAG_DELETED,
!$typeOrUser ? 'date' : DBSIMPLE_SKIP,
DBSIMPLE_SKIP
);
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 getAll(int $type, int $typeId, array &$jsg) : array
{
$result = array(
'vi' => self::getVideos($type, $typeId),
'ss' => self::getScreenshots($type, $typeId),
'co' => self::getComments($type, $typeId)
);
Util::mergeJsGlobals($jsg, self::$jsGlobals);
return $result;
}
public static function getJSGlobals() : array
{
return self::$jsGlobals;
}
}
?>

View File

@@ -1,726 +0,0 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die("illegal access");
trait TrProfilerFilter
{
protected array $parentCats = []; // used to validate ty-filter
protected function cbRegionCheck(string &$v) : bool
{
if (in_array($v, Util::$regions))
{
$this->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;
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}]+$/i';
protected const ENUM_FACTION = array( 469, 1037, 1106, 529, 1012, 87, 21, 910, 609, 942, 909, 530, 69, 577, 930, 1068, 1104, 729, 369, 92,
54, 946, 67, 1052, 749, 47, 989, 1090, 1098, 978, 1011, 93, 1015, 1038, 76, 470, 349, 1031, 1077, 809,
911, 890, 970, 169, 730, 72, 70, 932, 1156, 933, 510, 1126, 1067, 1073, 509, 941, 1105, 990, 934, 935,
1094, 1119, 1124, 1064, 967, 1091, 59, 947, 81, 576, 922, 68, 1050, 1085, 889, 589, 270);
protected const ENUM_CURRENCY = array(32572, 32569, 29736, 44128, 20560, 20559, 29434, 37829, 23247, 44990, 24368, 52027, 52030, 43016, 41596, 34052, 45624, 49426, 40752, 47241,
40753, 29024, 24245, 26045, 26044, 38425, 29735, 24579, 24581, 32897, 22484, 52026, 52029, 4291, 28558, 43228, 34664, 47242, 52025, 52028,
37836, 20558, 34597, 43589);
protected const ENUM_EVENT = array( 372, 283, 285, 353, 420, 400, 284, 201, 374, 409, 141, 324, 321, 424, 423, 327, 341, 181, 404, 398,
301);
protected const ENUM_ZONE = array( 4494, 36, 2597, 3358, 45, 331, 3790, 4277, 16, 3524, 3, 3959, 719, 1584, 25, 1583, 2677, 3702, 3522, 4,
3525, 3537, 46, 1941, 2918, 3905, 4024, 2817, 4395, 4378, 148, 393, 1657, 41, 2257, 405, 2557, 65, 4196, 1,
14, 10, 15, 139, 12, 3430, 3820, 361, 357, 3433, 721, 394, 3923, 4416, 2917, 4272, 4820, 4264, 3483, 3562,
267, 495, 4742, 3606, 210, 4812, 1537, 4710, 4080, 3457, 38, 4131, 3836, 3792, 2100, 2717, 493, 215, 3518, 3698,
3456, 3523, 2367, 2159, 1637, 4813, 4298, 2437, 722, 491, 44, 3429, 3968, 796, 2057, 51, 3607, 3791, 3789, 209,
3520, 3703, 3711, 1377, 3487, 130, 3679, 406, 1519, 4384, 33, 2017, 1477, 4075, 8, 440, 141, 3428, 3519, 3848,
17, 2366, 3840, 3713, 3847, 3775, 4100, 1581, 3557, 3845, 4500, 4809, 47, 3849, 4265, 4493, 4228, 3698, 4406, 3714,
3717, 3715, 717, 67, 3716, 457, 4415, 400, 1638, 1216, 85, 4723, 4722, 1337, 4273, 490, 1497, 206, 1196, 4603,
718, 3277, 28, 40, 11, 4197, 618, 3521, 3805, 66, 1176, 1977);
protected const ENUM_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; // erroneous search fields
// 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 = [];
/* genericFilter: [FILTER_TYPE, colOrFnName, param1, param2]
[self::CR_BOOLEAN, <string:colName>, <bool:isString>, null]
[self::CR_FLAG, <string:colName>, <int:testBit>, <bool:matchAny>] # default param2: matchExact
[self::CR_NUMERIC, <string:colName>, <int:NUM_FLAGS>, <bool:addExtraCol>]
[self::CR_STRING, <string:colName>, <int:STR_FLAGS>, null]
[self::CR_ENUM, <string:colName>, <bool:ANYNONE>, <bool:isEnumVal>] # param3 ? crv is val in enum : key in enum
[self::CR_STAFFFLAG, <string:colName>, null, null]
[self::CR_CALLBACK, <string:fnName>, <mixed:param1>, <mixed:param2>]
[self::CR_NYI_PH, null, <int:returnVal>, param2] # mostly 1: to ignore this criterium; 0: to fail the whole query
*/
protected array $genericFilter = [];
protected array $inputFields = []; // list of input fields defined per page - fieldName => [checkType, checkValue[, fieldIsArray]]
protected array $enums = []; // validation for opt lists per page - criteriumID => [validOptionList]
protected string $type = ''; // set by child
protected array $parentCats = []; // used to validate ty-filter
// 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 = []; // old fiData['v']
public array $criteria = []; // old fiData['c']
// 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 ($this->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();
}
public function mergeCat(array &$cats) : void
{
foreach ($this->parentCats as $idx => $cat)
$cats[$idx] = $cat;
}
private function &criteriaIterator() : \Generator
{
if (!$this->criteria)
return;
for ($i = 0; $i < count($this->criteria['cr']); $i++)
{
// throws a notice if yielded directly "Only variable references should be yielded by reference"
$v = [&$this->criteria['cr'][$i], &$this->criteria['crs'][$i], &$this->criteria['crv'][$i]];
yield $i => $v;
}
}
/***********************/
/* get prepared values */
/***********************/
public function buildGETParam(array $override = [], array $addCr = []) : string
{
$get = [];
foreach (array_merge($this->criteria, $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
foreach ($this->criteriaIterator() as &$_cr)
if ($cnd = $this->createSQLForCriterium(...$_cr))
$this->cndSet[] = $cnd;
if ($this->cndSet) // Note: TYPE_SOUND does not use 'match any'
array_unshift($this->cndSet, empty($this->values['ma']) ? 'AND' : 'OR');
}
return $this->cndSet;
}
public function getSetCriteria(int ...$cr) : array
{
if (!$cr || !$this->fiSetCriteria)
return $this->fiSetCriteria;
return array_intersect($this->fiSetCriteria['cr'], $cr);
}
/**********************/
/* input sanitization */
/**********************/
private function transformGET(string $get) : array
{
if (!$get)
return [];
$data = [];
foreach (explode(';', $get) as $field)
{
if (!strstr($field, '='))
{
trigger_error('Filter::transformGET - malformed GET string', E_USER_NOTICE);
$this->error = true;
continue;
}
[$k, $v] = explode('=', $field);
if (!isset($this->inputFields[$k]))
{
trigger_error('Filter::transformGET - GET param not in filter: '.$k, E_USER_NOTICE);
$this->error = true;
continue;
}
$asArray = $this->inputFields[$k][2];
$data[$k] = $asArray ? explode(':', $v) : $v;
}
return $data;
}
private function initFields() : void
{
foreach ($this->inputFields as $inp => [$type, $valid, $asArray])
{
$var = in_array($inp, ['cr', 'crs', 'crv']) ? 'criteria' : 'values';
if (!isset($this->rawData[$inp]) || $this->rawData[$inp] === '')
{
$this->$var[$inp] = $asArray ? [] : null;
continue;
}
$val = $this->rawData[$inp];
if ($asArray)
{
// quirk: in the POST step criteria can be [[''], null, null] if not selected.
$buff = [];
foreach ((array)$val as $v) // can be string|int in POST step if only one value present
if ($v !== '' && $this->checkInput($type, $valid, $v))
$buff[] = $v;
$this->$var[$inp] = $buff;
}
else
$this->$var[$inp] = $this->checkInput($type, $valid, $val) ? $val : null;
}
}
public function evalCriteria() : void // [cr]iterium, [cr].[s]ign, [cr].[v]alue
{
if (empty($this->criteria['cr']) && empty($this->criteria['crs']) && empty($this->criteria['crv']))
return;
else if (empty($this->criteria['cr']) || empty($this->criteria['crs']) || empty($this->criteria['crv']))
{
unset($this->criteria['cr']);
unset($this->criteria['crs']);
unset($this->criteria['crv']);
trigger_error('Filter::setCriteria - one of cr, crs, crv is missing', E_USER_NOTICE);
$this->error = true;
return;
}
$_cr = &$this->criteria['cr'];
$_crs = &$this->criteria['crs'];
$_crv = &$this->criteria['crv'];
if (count($_cr) != count($_crv) || count($_cr) != count($_crs) || count($_cr) > 5 || count($_crs) > 5 /*|| count($_crv) > 5*/)
{
// use min provided criterion as basis; 5 criteria at most
$min = max(5, min(count($_cr), count($_crv), count($_crs)));
if (count($_cr) > $min)
array_splice($_cr, $min);
if (count($_crv) > $min)
array_splice($_crv, $min);
if (count($_crs) > $min)
array_splice($_crs, $min);
trigger_error('Filter::setCriteria - cr, crs, crv are imbalanced', E_USER_NOTICE);
$this->error = true;
}
for ($i = 0; $i < count($_cr); $i++)
{
// conduct filter specific checks & casts here
$unsetme = false;
if (isset($this->genericFilter[$_cr[$i]]))
{
$gf = $this->genericFilter[$_cr[$i]];
switch ($gf[0])
{
case self::CR_NUMERIC:
$_ = $_crs[$i];
if (!Util::checkNumeric($_crv[$i], $gf[2]) || !$this->int2Op($_))
$unsetme = true;
break;
case self::CR_BOOLEAN:
case self::CR_FLAG:
$_ = $_crs[$i];
if (!$this->int2Bool($_))
$unsetme = true;
break;
case self::CR_ENUM:
case self::CR_STAFFFLAG:
if (!Util::checkNumeric($_crs[$i], NUM_CAST_INT))
$unsetme = true;
break;
}
}
if (!$unsetme && intval($_cr[$i]) && $_crs[$i] !== '' && $_crv[$i] !== '')
continue;
unset($_cr[$i]);
unset($_crs[$i]);
unset($_crv[$i]);
trigger_error('Filter::setCriteria - generic check failed ["'.$_cr[$i].'", "'.$_crs[$i].'", "'.$_crv[$i].'"]', E_USER_NOTICE);
$this->error = true;
}
$this->fiSetCriteria = array(
'cr' => $_cr,
'crs' => $_crs,
'crv' => $_crv
);
}
public function evalWeights() : void
{
// both empty: not in use; not an error
if (!$this->values['wt'] && !$this->values['wtv'])
return;
// one empty: erroneous manual input?
if (!$this->values['wt'] || !$this->values['wtv'])
{
unset($this->values['wt']);
unset($this->values['wtv']);
trigger_error('Filter::setWeights - one of wt, wtv is missing', E_USER_NOTICE);
$this->error = true;
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 = 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 $valid, mixed &$val, bool $recursive = false) : bool
{
switch ($type)
{
case self::V_EQUAL:
if (gettype($valid) == 'integer')
$val = intval($val);
else if (gettype($valid) == 'double')
$val = floatval($val);
else /* if (gettype($valid) == 'string') */
$val = strval($val);
if ($valid == $val)
return true;
break;
case self::V_LIST:
if (!Util::checkNumeric($val, NUM_CAST_INT))
return false;
foreach ($valid as $k => $v)
{
if (gettype($v) != 'array')
continue;
if ($this->checkInput(self::V_RANGE, $v, $val, true))
return true;
unset($valid[$k]);
}
if (in_array($val, $valid))
return true;
break;
case self::V_RANGE:
if (Util::checkNumeric($val, NUM_CAST_INT) && $val >= $valid[0] && $val <= $valid[1])
return true;
break;
case self::V_CALLBACK:
if ($this->$valid($val))
return true;
break;
case self::V_REGEX:
if (!preg_match($valid, $val))
return true;
break;
}
if (!$recursive)
{
trigger_error('Filter::checkInput - check failed [type: '.$type.' valid: '.((string)$valid).' val: '.((string)$val).']', E_USER_NOTICE);
$this->error = true;
}
return false;
}
protected function transformToken(string $string, bool $exact) : string
{
// escape manually entered _; entering % should be prohibited
$string = str_replace('_', '\\_', $string);
// now replace search wildcards with sql wildcards
$string = strtr($string, self::$wCards);
return sprintf($exact ? '%s' : '%%%s%%', $string);
}
protected function tokenizeString(array $fields, string $string = '', bool $exact = false, bool $shortStr = false) : array
{
if (!$string && $this->values['na'])
$string = $this->values['na'];
$qry = [];
foreach ($fields as $f)
{
$sub = [];
$parts = $exact ? [$string] : array_filter(explode(' ', $string));
foreach ($parts as $p)
{
if ($p[0] == '-' && (mb_strlen($p) > 3 || $shortStr))
$sub[] = [$f, $this->transformToken(mb_substr($p, 1), $exact), '!'];
else if ($p[0] != '-' && (mb_strlen($p) > 2 || $shortStr))
$sub[] = [$f, $this->transformToken($p, $exact)];
}
// single cnd?
if (!$sub)
continue;
else if (count($sub) > 1)
array_unshift($sub, 'AND');
else
$sub = $sub[0];
$qry[] = $sub;
}
// single cnd?
if (!$qry)
{
trigger_error('Filter::tokenizeString - could not tokenize string: '.$string, E_USER_NOTICE);
$this->error = true;
}
else if (count($qry) > 1)
array_unshift($qry, 'OR');
else
$qry = $qry[0];
return $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, string $value, ?int $strFlags) : ?array
{
$strFlags ??= 0x0;
if ($strFlags & STR_LOCALIZED)
$field .= '_loc'.Lang::getLocale()->value;
return $this->tokenizeString([$field], $value, $strFlags & STR_MATCH_EXACT, $strFlags & STR_ALLOW_SHORT);
}
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;
}
private function genericCriterion(int $cr, int $crs, string $crv) : ?array
{
[$crType, $colOrFn, $param1, $param2] = array_pad($this->genericFilter[$cr], 4, null);
$result = null;
switch ($crType)
{
case self::CR_NUMERIC:
$result = $this->genericNumeric($colOrFn, $crv, $crs, $param1);
break;
case self::CR_FLAG:
$result = $this->genericBooleanFlags($colOrFn, $param1, $crs, $param2);
break;
case self::CR_STAFFFLAG:
if (User::isInGroup(U_GROUP_EMPLOYEE) && $crs > 0)
$result = $this->genericBooleanFlags($colOrFn, (1 << ($crs - 1)), true);
break;
case self::CR_BOOLEAN:
$result = $this->genericBoolean($colOrFn, $crs, !empty($param1));
break;
case self::CR_STRING:
$result = $this->genericString($colOrFn, $crv, $param1);
break;
case self::CR_ENUM:
if (!$param2 && isset($this->enums[$cr][$crs]))
$result = $this->genericEnum($colOrFn, $this->enums[$cr][$crs]);
if ($param2 && in_array($crs, $this->enums[$cr]))
$result = $this->genericEnum($colOrFn, $crs);
else if ($param1 && ($crs == self::ENUM_ANY || $crs == self::ENUM_NONE))
$result = $this->genericEnum($colOrFn, $crs);
break;
case self::CR_CALLBACK:
$result = $this->{$colOrFn}($cr, $crs, $crv, $param1, $param2);
break;
case self::CR_NYI_PH: // do not limit with not implemented filters
if (is_int($param1))
return [$param1];
// for nonsensical values; compare against 0
if ($this->int2Op($crs) && Util::checkNumeric($crv))
{
if ($crs == '=')
$crs = '==';
return eval('return ('.$crv.' '.$crs.' 0);') ? [1] : [0];
}
else
return [0];
}
if ($result && $crType == self::CR_NUMERIC && !empty($param2))
$this->fiExtraCols[] = $cr;
return $result;
}
/***********************************/
/* create conditions from */
/* non-generic values and criteria */
/***********************************/
protected function createSQLForCriterium(int &$cr, int &$crs, string &$crv) : array
{
if (!$this->genericFilter) // criteria not in use - no error
return [];
if (isset($this->genericFilter[$cr]))
if ($genCr = $this->genericCriterion($cr, $crs, $crv))
return $genCr;
trigger_error('Filter::createSQLForCriterium - received unhandled criterium: ["'.$cr.'", "'.$crs.'", "'.$crv.'"]', E_USER_NOTICE);
$this->error = true;
unset($cr, $crs, $crv);
return [];
}
abstract protected function createSQLForValues() : array;
}
?>

View File

@@ -1,69 +0,0 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
class Announcement implements \JsonSerializable
{
public const MODE_PAGE_TOP = 0;
public const MODE_CONTENT_TOP = 1;
public const STATUS_DISABLED = 0;
public const STATUS_ENABLED = 1;
public const STATUS_DELETED = 2;
public readonly int $status;
private bool $editable = false;
public function __construct(
public readonly int $id,
private string $name,
private LocString $text,
private int $mode = self::MODE_CONTENT_TOP,
int $status = self::STATUS_ENABLED,
private string $style = '')
{
// a negative id displays ENABLE/DISABLE and DELETE links for this announcement
// TODO - the ugroup check mirrors the js. Add other checks like ownership status? (ownership currently not stored)
if (User::isInGroup(U_GROUP_ADMIN | U_GROUP_BUREAU) /* && User::$id == $authorId */)
$this->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-' . abs($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";
}
}
?>

View File

@@ -1,50 +0,0 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
class Book implements \JsonSerializable
{
public function __construct(
private array $pages, // js:array of html
private string $parent = 'book-generic', // HTMLNode.id
private ?int $page = null) // start page; defaults to 1
{
if (!$this->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";
}
}
?>

View File

@@ -1,159 +0,0 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
class IconElement
{
public const SIZE_SMALL = 0;
public const SIZE_MEDIUM = 1;
public const SIZE_LARGE = 2;
private const CREATE_ICON_TPL = "\$WH.ge('%s%d').appendChild(%s.createIcon(%s));\n";
private int $idx = 0;
private string $href = '';
private bool $noIcon = false;
public readonly string $quality;
public readonly ?string $align;
public readonly int $size;
public function __construct(
public readonly int $type,
public readonly int $typeId,
public readonly string $text,
public readonly int|string $num = '',
public readonly int|string $qty = '',
?string $quality = null,
int $size = self::SIZE_MEDIUM,
bool $link = true,
string $url = '',
?string $align = null,
public readonly string $element = 'icontab-icon',
public ?string $extraText = null
)
{
if (is_numeric($quality))
$this->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;
// 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', $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_numeric($this->num) ? $this->num : "'".$this->num."'";
if ($this->qty)
$params[] = is_numeric($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));
}
}
?>

View File

@@ -1,49 +0,0 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
class InfoboxMarkup extends Markup
{
public function __construct(private array $items = [], array $opts, string $parent = '')
{
parent::__construct('', $opts, $parent);
}
public function addItem(string $item, ?int $pos = null) : void
{
if (is_null($pos) || $pos >= count($this->items))
$this->items[] = $item;
else
array_splice($this->items, $pos, 0, $item);
}
public function append(string $text) : self
{
if ($this->items && !$this->__text)
$this->replace('[ul][li]' . implode('[/li][li]', $this->items) . '[/li][/ul]');
return parent::append($text);
}
public function __toString() : string
{
if ($this->items && !$this->__text)
$this->replace('[ul][li]' . implode('[/li][li]', $this->items) . '[/li][/ul]');
return parent::__toString();
}
public function getJsGlobals() : array
{
if ($this->items && !$this->__text)
$this->replace('[ul][li]' . implode('[/li][li]', $this->items) . '[/li][/ul]');
return parent::getJsGlobals();
}
}
?>

View File

@@ -1,174 +0,0 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
class Listview implements \JsonSerializable
{
public const MODE_DEFAULT = 0;
public const MODE_CHECKBOX = 1;
public const MODE_DIV = 2;
public const MODE_TILED = 3;
public const MODE_CALENDAR = 4;
public const MODE_FLEXGRID = 5;
private const TEMPLATES = array(
'achievement' => ['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 <RowDefinitions>
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:<width>, h:<height>}
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 <ColumnDefinition>
private null|array|string $visibleCols = null; // js:callable or js:array of string <colIds>
private null|array|string $hiddenCols = null; // js:callable or js:array of string <colIds>
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:<colId>, name:<name>, hidden:<bool>, type:"text", sortFunc:<callable>}
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';
}
public function &iterate() : \Generator
{
reset($this->data);
foreach ($this->data as $idx => &$row)
yield $idx => $row;
}
public function getTemplate() : string
{
return $this->template;
}
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() : void
{
$this->_errors = 1;
}
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
{
if ($this->__addIn)
include($this->__addIn);
return "new Listview(".Util::toJSON($this).");\n";
}
}
?>

View File

@@ -1,291 +0,0 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
class Markup implements \JsonSerializable
{
private const DB_TAG_PATTERN = '/(?<!\\\\)\[(npc|object|item|itemset|quest|spell|zone|faction|pet|achievement|statistic|title|event|class|race|skill|currency|emote|enchantment|money|sound|icondb)=(-?\d+)[^\]]*\]/i';
// const val
public const MARKUP_MODE_COMMENT = 1;
public const MARKUP_MODE_ARTICLE = 2;
public const MARKUP_MODE_QUICKFACTS = 3;
public const MARKUP_MODE_SIGNATURE = 4;
public const MARKUP_MODE_REPLY = 5;
// js var
public const MODE_COMMENT = '$Markup.MODE_COMMENT';
public const MODE_ARTICLE = '$Markup.MODE_ARTICLE';
public const MODE_QUICKFACTS = '$Markup.MODE_QUICKFACTS';
public const MODE_SIGNATURE = '$Markup.MODE_SIGNATURE';
public const MODE_REPLY = '$Markup.MODE_REPLY';
// const val
public const MARKUP_CLASS_ADMIN = 40;
public const MARKUP_CLASS_STAFF = 30;
public const MARKUP_CLASS_PREMIUM = 20;
public const MARKUP_CLASS_USER = 10;
public const MARKUP_CLASS_PENDING = 1;
// js var
public const CLASS_ADMIN = '$Markup.CLASS_ADMIN';
public const CLASS_STAFF = '$Markup.CLASS_STAFF';
public const CLASS_PREMIUM = '$Markup.CLASS_PREMIUM';
public const CLASS_USER = '$Markup.CLASS_USER';
public const CLASS_PENDING = '$Markup.CLASS_PENDING';
// options
private ?string $prepend = null; // html in front of article
private ?string $append = null; // html trailing the article
private ?int $locale = null; // forces tooltips in the article to adhere to another locale
private ?int $inBlog = null; // js:bool; unused by aowow
private ?string $mode = null; // defaults to Markup.MODE_ARTICLE, which is what we want.
private ?string $allow = null; // defaults to Markup.CLASS_STAFF
private ?int $roles = null; // if allow is null, get allow from roles (user group); also mode will be set to MODE_ARTICLE for staff groups
private ?int $stopAtBreak = null; // js:bool; only parses text until substring "[break]" is encountered; some debug option...?
private ?string $highlight = null; // HTMLNode selector
private ?int $skipReset = null; // js:bool; unsure, if TRUE the next settings in this block get skipped
private ?string $uid = null; // defaults to 'abc'; unsure, key under which media is stored and referenced in g_screenshots and g_videos
private ?string $root = null; // unsure, something to with Markup Tags that need to be subordinate to other tags (e.g.: [li] to [ol])
private ?int $preview = null; // unsure, appends '-preview' to the div created by the [tabs] tag and prevents scrolling. Forum feature?
private ?int $dbpage = null; // js:bool; set on db type detail pages; adds article edit links to admin menu
protected string $__text;
private string $__parent = 'article-generic';
public function __construct(string $text, array $opts, string $parent = '')
{
foreach ($opts as $k => $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';
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";
}
}
?>

View File

@@ -1,70 +0,0 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
class Summary implements \JsonSerializable
{
private string $id = ''; // HTMLNode.id
private ?string $parent = ''; // HTMLNode.id; if set $id is created and attached here instead of searched for
private string $template = ''; //
private ?int $editable = null; // js:bool; defaults to TRUE
private ?int $draggable = null; // js:bool; defaults to $editable
private ?int $searchable = null; // js:bool; defaults to $editable && $draggable
private ?int $weightable = null; // js:bool; defaults to $editable
private ?int $textable = null; // js:bool; defaults to FALSE
private ?int $enhanceable = null; // js:bool; defaults to $editable
private ?int $level = null; // js:int; defaults to 80
private array $groups = []; // js:array; defaults to GET-params
private ?array $weights = null; // js:array; defaults to GET-params
public function __construct(array $opts)
{
foreach ($opts as $k => $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";
}
}
?>

View File

@@ -1,142 +0,0 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
class Tabs implements \JsonSerializable, \Countable
{
private array $__tabs = [];
private string $parent = ''; // HTMLNode
private ?int $poundable = null; // js:bool
private ?int $forceScroll = null; // js:bool
private ?int $noScroll = null; // js:bool
private ?string $trackable = null; // String to track in Google Analytics .. often a DB Type
private ?string $onLoad = null; // js::callable
private ?string $onShow = null; // js::callable
private ?string $onHide = null; // js::callable
public function __construct(array $opts, public readonly string $__tabVar = 'myTabs', private bool $__forceTabs = false)
{
foreach ($opts as $k => $v)
{
if (property_exists($this, $k))
$this->$k = $v;
else
trigger_error(self::class.'::__construct - unrecognized option: ' . $k);
}
}
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 '<div class="text tabbed-contents" id="tab-'.$tab['id'].'" style="display:none;">'.$tab['data'].'</div>';
}
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";
}
}
?>

View File

@@ -1,60 +0,0 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
class Tooltip implements \JsonSerializable
{
private ?string $name = null;
private ?string $tooltip = null;
private ?\StdClass $map = null; // secondary tooltip
private ?string $icon = null;
private ?int $quality = null; // icon border color coded
private ?bool $daily = null;
private ?array $spells = null;
private ?string $buff = null;
private ?array $buffspells = null;
public function __construct(private string $__powerTpl, private string $__subject, array $opts = [])
{
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, Util::toJSON($this->__subject, JSON_AOWOW_POWER), Lang::getLocale()->value, Util::toJSON($this, JSON_AOWOW_POWER))."\n";
}
}
?>

View File

@@ -1,61 +0,0 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
class LocString
{
private \WeakMap $store;
public function __construct(array $data, string $key = 'name', ?callable $callback = null)
{
$this->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)
$this->store[$l] = (string)$callback($data[$key.'_loc'.$l->value] ?? '');
}
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;
}
}
?>

File diff suppressed because it is too large Load Diff

View File

@@ -1,274 +0,0 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
class Report
{
public const MODE_GENERAL = 0;
public const MODE_COMMENT = 1;
public const MODE_FORUM_POST = 2;
public const MODE_SCREENSHOT = 3;
public const MODE_CHARACTER = 4;
public const MODE_VIDEO = 5;
public const MODE_GUIDE = 6;
public const GEN_FEEDBACK = 1;
public const GEN_BUG_REPORT = 2;
public const GEN_TYPO_TRANSLATION = 3;
public const GEN_OP_ADVERTISING = 4;
public const GEN_OP_PARTNERSHIP = 5;
public const GEN_PRESS_INQUIRY = 6;
public const GEN_MISCELLANEOUS = 7;
public const GEN_MISINFORMATION = 8;
public const CO_ADVERTISING = 15;
public const CO_INACCURATE = 16;
public const CO_OUT_OF_DATE = 17;
public const CO_SPAM = 18;
public const CO_INAPPROPRIATE = 19;
public const CO_MISCELLANEOUS = 20;
public const FO_ADVERTISING = 30;
public const FO_AVATAR = 31;
public const FO_INACCURATE = 32;
public const FO_OUT_OF_DATE = 33;
public const FO_SPAM = 34;
public const FO_STICKY_REQUEST = 35;
public const FO_INAPPROPRIATE = 36;
public const FO_MISCELLANEOUS = 37;
public const SS_INACCURATE = 45;
public const SS_OUT_OF_DATE = 46;
public const SS_INAPPROPRIATE = 47;
public const SS_MISCELLANEOUS = 48;
public const PR_INACCURATE_DATA = 60;
public const PR_MISCELLANEOUS = 61;
public const VI_INACCURATE = 45;
public const VI_OUT_OF_DATE = 46;
public const VI_INAPPROPRIATE = 47;
public const VI_MISCELLANEOUS = 48;
public const AR_INACCURATE = 45;
public const AR_OUT_OF_DATE = 46;
public const AR_MISCELLANEOUS = 48;
private array $context = array(
self::MODE_GENERAL => 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() : int
{
// check already reported
$field = User::isLoggedIn() ? 'userId' : 'ip';
if (DB::Aowow()->selectCell('SELECT 1 FROM ?_reports WHERE `mode` = ?d AND `reason`= ?d AND `subject` = ?d AND ?# = ?', $this->mode, $this->reason, $this->subject, $field, User::$id ?: User::$ip))
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` = ?d', $this->subject);
// else if if ($this->mode == self::MODE_FORUM_POST)
// $roles = DB::Aowow()->selectCell('SELECT `roles` FROM ?_forum_posts WHERE `id` = ?d', $this->subject);
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;
}
if($err = $this->checkTargetContext())
{
$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()->query('INSERT INTO ?_reports (?#) VALUES (?a)', array_keys($update), array_values($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()->select('SELECT `id` AS ARRAY_KEY, r.* FROM ?_reports r WHERE {`status` IN (?a) AND }`mode` = ?d AND `reason` = ?d AND `subject` = ?d',
$status ?: DBSIMPLE_SKIP, $this->mode, $this->reason, $this->subject);
}
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 (?a) AND `mode` = ?d AND `reason` = ?d AND `subject` = ?d',
$fromStatus, $this->mode, $this->reason, $this->subject))
{
DB::Aowow()->query('UPDATE ?_reports SET `status` = ?d, `assigned` = 0 WHERE `id` IN (?a)', $closeStatus, array_keys($reports));
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;
}
}
?>

View File

@@ -1,7 +1,5 @@
<?php <?php
namespace Aowow;
if (!defined('AOWOW_REVISION')) if (!defined('AOWOW_REVISION'))
die('illegal access'); die('illegal access');
@@ -13,173 +11,111 @@ if (!defined('AOWOW_REVISION'))
class DB class DB
{ {
private static array $interfaceCache = []; private static $interfaceCache = [];
private static array $optionsCache = []; private static $optionsCache = [];
private static array $logs = []; private static $connectionCache = [];
private static function createConnectSyntax(array &$options) : string private static function createConnectSyntax(&$options)
{ {
return 'mysqli://'.$options['user'].':'.$options['pass'].'@'.$options['host'].'/'.$options['db']; return 'mysqli://'.$options['user'].':'.$options['pass'].'@'.$options['host'].'/'.$options['db'];
} }
public static function connect(int $idx) : void public static function connect($idx)
{ {
if (self::isConnected($idx)) if (self::isConnected($idx))
{ return;
self::$interfaceCache[$idx]->link->close();
self::$interfaceCache[$idx] = null;
}
$options = &self::$optionsCache[$idx]; $options = &self::$optionsCache[$idx];
$interface = \DbSimple_Generic::connect(self::createConnectSyntax($options)); $interface = DbSimple_Generic::connect(self::createConnectSyntax($options));
$interface->setErrorHandler(self::errorHandler(...)); if (!$interface || $interface->error)
die('Failed to connect to database.');
$interface->setErrorHandler(['DB', 'errorHandler']);
$interface->query('SET NAMES ?', 'utf8');
if ($options['prefix']) if ($options['prefix'])
$interface->setIdentPrefix($options['prefix']); $interface->setIdentPrefix($options['prefix']);
self::$interfaceCache[$idx] = &$interface; self::$interfaceCache[$idx] = &$interface;
self::$connectionCache[$idx] = true;
// should be caught by registered error handler
if (!$interface || !$interface->link)
return;
$interface->query('SET NAMES ?', 'utf8mb4');
// disable STRICT_TRANS_TABLES and STRICT_ALL_TABLES off. It prevents usage of implicit default values.
// disable ONLY_FULL_GROUP_BY (Allows for non-aggregated selects in a group-by query)
$extraModes = ['STRICT_TRANS_TABLES', 'STRICT_ALL_TABLES', 'ONLY_FULL_GROUP_BY', 'NO_ZERO_DATE', 'NO_ZERO_IN_DATE', 'ERROR_FOR_DIVISION_BY_ZERO'];
$oldModes = explode(',', $interface->selectCell('SELECT @@sql_mode'));
$newModes = array_diff($oldModes, $extraModes);
if ($oldModes != $newModes)
$interface->query("SET SESSION sql_mode = ?", implode(',', $newModes));
} }
public static function test(array $options, ?string &$err = '') : bool public static function errorHandler($message, $data)
{
$defPort = ini_get('mysqli.default_port');
$port = 0;
if (strstr($options['host'], ':'))
[$options['host'], $port] = explode(':', $options['host']);
if ($link = mysqli_connect($options['host'], $options['user'], $options['pass'], $options['db'], $port ?: $defPort))
{
mysqli_close($link);
return true;
}
$err = '['.mysqli_connect_errno().'] '.mysqli_connect_error();
return false;
}
public static function errorHandler(string $message, array $data) : void
{ {
if (!error_reporting()) if (!error_reporting())
return; return;
// continue on warning, end on error $error = "DB ERROR:<br /><br />\n\n<pre>".print_r($data, true)."</pre>";
$isError = $data['code'] > 0;
// make number sensible again echo CLI ? strip_tags($error) : $error;
$data['code'] = abs($data['code']); exit;
if (Cfg::get('DEBUG') >= LOG_LEVEL_INFO)
{
echo "\nDB ERROR\n";
foreach ($data as $k => $v)
echo ' '.str_pad($k.':', 10).$v."\n";
}
trigger_error($message, $isError ? E_USER_ERROR : E_USER_WARNING);
} }
public static function profiler(mixed $self, string $query, mixed $trace) : void public static function getDB($idx)
{
if ($trace) // actual query
self::$logs[] = [str_replace("\n", ' ', $query)];
else // the statistics
{
end(self::$logs);
self::$logs[key(self::$logs)][] = substr(explode(';', $query)[0], 5);
}
}
public static function getProfiles() : string
{
$out = '<pre><table style="font-size:12;"><tr><th></th><th>Time</th><th>Query</th></tr>';
foreach (self::$logs as $i => [$l, $t])
{
$c = 'inherit';
preg_match('/(\d+)/', $t, $m);
if ($m[1] > 100)
$c = '#FFA0A0';
else if ($m[1] > 20)
$c = '#FFFFA0';
$out .= '<tr><td>'.$i.'.</td><td style="background-color:'.$c.';">'.$t.'</td><td>'.$l.'</td></tr>';
}
return Util::jsEscape($out).'</table></pre>';
}
public static function getDB(int $idx) : ?\DbSimple_Mysqli
{ {
return self::$interfaceCache[$idx]; return self::$interfaceCache[$idx];
} }
public static function isConnected(int $idx) : bool public static function isConnected($idx)
{ {
return isset(self::$interfaceCache[$idx]) && self::$interfaceCache[$idx]->link; return isset(self::$connectionCache[$idx]);
} }
public static function isConnectable(int $idx) : bool public static function isConnectable($idx)
{ {
return isset(self::$optionsCache[$idx]); return isset(self::$optionsCache[$idx]);
} }
/** private static function safeGetDB($idx)
* @static
* @return DbSimple_Mysqli
*/
public static function Characters(int $realmId) : ?\DbSimple_Mysqli
{ {
if (!isset(self::$optionsCache[DB_CHARACTERS.$realmId])) if (!self::isConnected($idx))
die('Connection info not found for live database of realm #'.$realmId.'. Aborted.'); self::connect($idx);
return self::getDB(DB_CHARACTERS.$realmId); return self::getDB($idx);
} }
/** /**
* @static * @static
* @return DbSimple_Mysqli * @return DbSimple_Mysql
*/ */
public static function Auth() : ?\DbSimple_Mysqli public static function Characters($realm)
{ {
return self::getDB(DB_AUTH); 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 * @static
* @return DbSimple_Mysqli * @return DbSimple_Mysql
*/ */
public static function World() : ?\DbSimple_Mysqli public static function Auth()
{ {
return self::getDB(DB_WORLD); return self::safeGetDB(DB_AUTH);
} }
/** /**
* @static * @static
* @return DbSimple_Mysqli * @return DbSimple_Mysql
*/ */
public static function Aowow() : ?\DbSimple_Mysqli public static function World()
{ {
return self::getDB(DB_AOWOW); return self::safeGetDB(DB_WORLD);
} }
public static function load(int $idx, array $config) : void /**
* @static
* @return DbSimple_Mysql
*/
public static function Aowow()
{
return self::safeGetDB(DB_AOWOW);
}
public static function load($idx, $config)
{ {
self::$optionsCache[$idx] = $config; self::$optionsCache[$idx] = $config;
self::connect($idx);
} }
} }

File diff suppressed because it is too large Load Diff

View File

@@ -1,79 +0,0 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
enum ChrClass : int
{
case WARRIOR = 1;
case PALADIN = 2;
case HUNTER = 3;
case ROGUE = 4;
case PRIEST = 5;
case DEATHKNIGHT = 6;
case SHAMAN = 7;
case MAGE = 8;
case WARLOCK = 9;
case DRUID = 11;
public const MASK_ALL = 0x5FF;
public function matches(int $classMask) : bool
{
return !$classMask || $this->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
};
}
}
?>

View File

@@ -1,132 +0,0 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
enum ChrRace : int
{
case HUMAN = 1;
case ORC = 2;
case DWARF = 3;
case NIGHTELF = 4;
case UNDEAD = 5;
case TAUREN = 6;
case GNOME = 7;
case TROLL = 8;
case BLOODELF = 10;
case DRAENEI = 11;
public const MASK_ALLIANCE = 0x44D;
public const MASK_HORDE = 0x2B2;
public const MASK_ALL = 0x6FF;
public function matches(int $raceMask) : bool
{
return !$raceMask || $this->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 => 'undead',
self::TAUREN => 'tauren',
self::GNOME => 'gnome',
self::TROLL => 'troll',
self::BLOODELF => 'bloodelf',
self::DRAENEI => 'draenei'
};
}
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;
}
}
?>

View File

@@ -1,723 +0,0 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
abstract class Stat // based on g_statToJson
{
public const HEALTH = 1;
public const MANA = 2; // note: mana is on idx 0 from 5.0 onwards; idx 2 is empty
public const AGILITY = 3;
public const STRENGTH = 4;
public const INTELLECT = 5;
public const SPIRIT = 6;
public const STAMINA = 7;
public const ENERGY = 8;
public const RAGE = 9;
public const FOCUS = 10;
public const RUNIC_POWER = 11;
public const DEFENSE_RTG = 12;
public const DODGE_RTG = 13;
public const PARRY_RTG = 14;
public const BLOCK_RTG = 15;
public const MELEE_HIT_RTG = 16;
public const RANGED_HIT_RTG = 17;
public const SPELL_HIT_RTG = 18;
public const MELEE_CRIT_RTG = 19;
public const RANGED_CRIT_RTG = 20;
public const SPELL_CRIT_RTG = 21;
public const MELEE_HIT_TAKEN_RTG = 22;
public const RANGED_HIT_TAKEN_RTG = 23;
public const SPELL_HIT_TAKEN_RTG = 24;
public const MELEE_CRIT_TAKEN_RTG = 25;
public const RANGED_CRIT_TAKEN_RTG = 26;
public const SPELL_CRIT_TAKEN_RTG = 27;
public const MELEE_HASTE_RTG = 28;
public const RANGED_HASTE_RTG = 29;
public const SPELL_HASTE_RTG = 30;
public const HIT_RTG = 31;
public const CRIT_RTG = 32;
public const HIT_TAKEN_RTG = 33;
public const CRIT_TAKEN_RTG = 34;
public const RESILIENCE_RTG = 35;
public const HASTE_RTG = 36;
public const EXPERTISE_RTG = 37;
public const ATTACK_POWER = 38;
public const RANGED_ATTACK_POWER = 39;
public const FERAL_ATTACK_POWER = 40; // unused in wow-3.3.5
public const HEALING_SPELL_POWER = 41; // deprecated
public const DAMAGE_SPELL_POWER = 42; // deprecated
public const MANA_REGENERATION = 43;
public const ARMOR_PENETRATION_RTG = 44;
public const SPELL_POWER = 45;
public const HEALTH_REGENERATION = 46; // no differentiation between IC (item mods / spells) and OOC (from spirit) for Profiler
public const SPELL_PENETRATION = 47;
public const BLOCK = 48;
// public const MASTERY_RTG = 49; // not in wow-3.3.5
public const ARMOR = 50;
public const FIRE_RESISTANCE = 51;
public const FROST_RESISTANCE = 52;
public const HOLY_RESISTANCE = 53;
public const SHADOW_RESISTANCE = 54;
public const NATURE_RESISTANCE = 55;
public const ARCANE_RESISTANCE = 56;
public const FIRE_SPELL_POWER = 57;
public const FROST_SPELL_POWER = 58;
public const HOLY_SPELL_POWER = 59;
public const SHADOW_SPELL_POWER = 60;
public const NATURE_SPELL_POWER = 61;
public const ARCANE_SPELL_POWER = 62;
// v for stats lookups v
public const WEAPON_DAMAGE = 200; // +weapon dmg from enchantments
public const WEAPON_DAMAGE_TYPE = 201;
public const WEAPON_DAMAGE_MIN = 202;
public const WEAPON_DAMAGE_MAX = 203;
public const WEAPON_SPEED = 204;
public const WEAPON_DPS = 205; // also +weapon dps from enchantments (rockbiter)
public const MELEE_DAMAGE_MIN = 206;
public const MELEE_DAMAGE_MAX = 207;
public const MELEE_SPEED = 208;
public const MELEE_DPS = 209;
public const RANGED_DAMAGE_MIN = 210;
public const RANGED_DAMAGE_MAX = 211;
public const RANGED_SPEED = 212;
public const RANGED_DPS = 213;
public const EXTRA_SOCKETS = 214;
public const ARMOR_BONUS = 215;
public const MELEE_ATTACK_POWER = 216;
// v only seen in profiler v
public const EXPERTISE = 500;
public const ARMOR_PENETRATION_PCT = 501;
public const MELEE_HIT_PCT = 502;
public const MELEE_CRIT_PCT = 503;
public const MELEE_HASTE_PCT = 504;
public const RANGED_HIT_PCT = 505;
public const RANGED_CRIT_PCT = 506;
public const RANGED_HASTE_PCT = 507;
public const SPELL_HIT_PCT = 508;
public const SPELL_CRIT_PCT = 509;
public const SPELL_HASTE_PCT = 510;
public const MANA_REGENERATION_SPI = 511; // mp5 from spirit, excluding other sources
public const MANA_REGENERATION_OC = 512; // mp5 out of combat, excluding other sources
public const MANA_REGENERATION_IC = 513; // mp5 in combat, excluding other sources
public const ARMOR_TOTAL = 514; // ARMOR + ARMOR_BONUS meta category .. can be skipped here like pet* stats?
public const DEFENSE = 515;
public const DODGE_PCT = 516;
public const PARRY_PCT = 517;
public const BLOCK_PCT = 518;
public const RESILIENCE_PCT = 519;
public const FLAG_NONE = 0x00;
public const FLAG_ITEM = 0x01; // found on items
public const FLAG_SERVERSIDE = 0x02; // not included in g_statToJson
public const FLAG_PROFILER = 0x04; // stat used in profiler only
public const FLAG_LVL_SCALING = 0x08; // rating effectivenes scales with level
public const FLAG_FLOAT_VALUE = 0x10; // not an int
public const IDX_JSON_STR = 0;
public const IDX_ITEM_MOD = 1; // granted by items
public const IDX_COMBAT_RATING = 2; // granted by spells + enchantments
public const IDX_FILTER_CR_ID = 3; // also references listview cols
public const IDX_FLAGS = 4;
private static /* array */ $data = array(
self::HEALTH => ['health', ITEM_MOD_HEALTH, null, 115, self::FLAG_ITEM],
self::MANA => ['mana', ITEM_MOD_MANA, null, 116, self::FLAG_ITEM],
self::AGILITY => ['agi', ITEM_MOD_AGILITY, null, 21, self::FLAG_ITEM],
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', null, CR_MASTERY, null, self::FLAG_NONE],
self::ARMOR => ['armor', null, null, 41, self::FLAG_ITEM],
self::FIRE_RESISTANCE => ['firres', null, null, 26, self::FLAG_ITEM],
self::FROST_RESISTANCE => ['frores', null, null, 28, self::FLAG_ITEM],
self::HOLY_RESISTANCE => ['holres', null, null, 30, self::FLAG_ITEM],
self::SHADOW_RESISTANCE => ['shares', null, null, 29, self::FLAG_ITEM],
self::NATURE_RESISTANCE => ['natres', null, null, 27, self::FLAG_ITEM],
self::ARCANE_RESISTANCE => ['arcres', null, null, 25, self::FLAG_ITEM],
self::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::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::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 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 $match) : int
{
$i = array_search($match, array_column(self::$data, $idx));
if ($i === false)
return 0;
return array_keys(self::$data)[$i];
}
}
class StatsContainer implements \Countable
{
private $store = [];
private $relSpells = [];
private $relEnchantments = [];
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) : self
{
if (!$spell)
return $this;
// if spells grant an equal, non-zero amount of SPELL_DAMAGE and SPELL_HEALING, combine them to SPELL_POWER
// this probably does not affect enchantments
$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
foreach ($this->convertSpellEffect($aura, $mVal, $amt) as $idx)
Util::arraySumByKey($tmpStore, [$idx => $amt]);
}
if (!empty($tmpStore[Stat::HEALING_SPELL_POWER]) && !empty($tmpStore[Stat::DAMAGE_SPELL_POWER]) && $tmpStore[Stat::HEALING_SPELL_POWER] == $tmpStore[Stat::DAMAGE_SPELL_POWER])
{
Util::arraySumByKey($tmpStore, [Stat::SPELL_POWER => $tmpStore[Stat::HEALING_SPELL_POWER]]);
unset($tmpStore[Stat::HEALING_SPELL_POWER]);
unset($tmpStore[Stat::DAMAGE_SPELL_POWER]);
}
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 (?#) FROM ?_item_stats WHERE `type` = ?d AND `typeId` = ?d', 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) : array
{
$out = [];
foreach ($this->store as $stat => $amt)
if (!$outFlags || (Stat::getFlags($stat) & $outFlags))
$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
{
if ($enchantmentId <= 0 || !isset($this->relEnchantments[$enchantmentId]))
return [];
return $this->relEnchantments[$enchantmentId];
}
private function relS(int $spellId) : array
{
if ($spellId <= 0 || !isset($this->relSpells[$spellId]))
return [];
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:
if ($object == SPELL_SCHOOL_NORMAL)
return [Stat::ARMOR];
if ($object == SPELL_SCHOOL_HOLY)
return [Stat::HOLY_RESISTANCE];
if ($object == SPELL_SCHOOL_FIRE)
return [Stat::FIRE_RESISTANCE];
if ($object == SPELL_SCHOOL_NATURE)
return [Stat::NATURE_RESISTANCE];
if ($object == SPELL_SCHOOL_FROST)
return [Stat::FROST_RESISTANCE];
if ($object == SPELL_SCHOOL_SHADOW)
return [Stat::SHADOW_RESISTANCE];
if ($object == SPELL_SCHOOL_ARCANE)
return [Stat::ARCANE_RESISTANCE];
return [];
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:
if ($miscValue < 0) // all stats
return [Stat::AGILITY, Stat::STRENGTH, Stat::INTELLECT, Stat::SPIRIT, Stat::STAMINA];
if ($miscValue == STAT_STRENGTH) // one stat
return [Stat::STRENGTH];
if ($miscValue == STAT_AGILITY)
return [Stat::AGILITY];
if ($miscValue == STAT_STAMINA)
return [Stat::STAMINA];
if ($miscValue == STAT_INTELLECT)
return [Stat::INTELLECT];
if ($miscValue == STAT_SPIRIT)
return [Stat::SPIRIT];
return []; // one bullshit
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];
// HolySpellpower (deprecated; still used in randomproperties)
if ($miscValue & (1 << SPELL_SCHOOL_HOLY))
$stats[] = Stat::HOLY_SPELL_POWER;
// FireSpellpower (deprecated; still used in randomproperties)
if ($miscValue & (1 << SPELL_SCHOOL_FIRE))
$stats[] = Stat::FIRE_SPELL_POWER;
// NatureSpellpower (deprecated; still used in randomproperties)
if ($miscValue & (1 << SPELL_SCHOOL_NATURE))
$stats[] = Stat::NATURE_SPELL_POWER;
// FrostSpellpower (deprecated; still used in randomproperties)
if ($miscValue & (1 << SPELL_SCHOOL_FROST))
$stats[] = Stat::FROST_SPELL_POWER;
// ShadowSpellpower (deprecated; still used in randomproperties)
if ($miscValue & (1 << SPELL_SCHOOL_SHADOW))
$stats[] = Stat::SHADOW_SPELL_POWER;
// ArcaneSpellpower (deprecated; still used in randomproperties)
if ($miscValue & (1 << SPELL_SCHOOL_ARCANE))
$stats[] = Stat::ARCANE_SPELL_POWER;
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
if ($miscValue == POWER_ENERGY)
return [Stat::ENERGY];
if ($miscValue == POWER_RAGE)
return [Stat::RAGE];
if ($miscValue == POWER_MANA)
return [Stat::MANA];
if ($miscValue == POWER_RUNIC_POWER)
return [Stat::RUNIC_POWER];
return [];
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::ATTACK_POWER/*, Stat::RANGED_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 [];
}
}
?>

View File

@@ -1,348 +0,0 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
class Game
{
public static $resistanceFields = array(
null, 'resHoly', 'resFire', 'resNature', 'resFrost', 'resShadow', 'resArcane'
);
public static $rarityColorStings = array( // zero-indexed
'9d9d9d', 'ffffff', '1eff00', '0070dd', 'a335ee', 'ff8000', 'e5cc80', 'e6cc80'
);
public static $specIconStrings = array(
-1 => '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 static $questClasses = 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]
);
// zoneorsort for quests need updating
// partially points non-instanced area with identical name for instance quests
public static $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 $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 $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 $sockets = array( // jsStyle Strings
'meta', 'red', 'yellow', 'blue'
);
public static function getReputationLevelForPoints($pts)
{
if ($pts >= 41999)
return REP_EXALTED;
else if ($pts >= 20999)
return REP_REVERED;
else if ($pts >= 8999)
return REP_HONORED;
else if ($pts >= 2999)
return REP_FRIENDLY;
else if ($pts >= 0)
return REP_NEUTRAL;
else if ($pts >= -3000)
return REP_UNFRIENDLY;
else if ($pts >= -6000)
return REP_HOSTILE;
else
return REP_HATED;
}
public static function getTaughtSpells(&$spell)
{
$extraIds = [-1]; // init with -1 to prevent empty-array errors
$lookup = [-1];
switch (gettype($spell))
{
case 'object':
if (get_class($spell) != __NAMESPACE__.'\SpellList')
return [];
$lookup[] = $spell->id;
foreach ($spell->canTeachSpell() as $idx)
$extraIds[] = $spell->getField('effect'.$idx.'TriggerSpell');
break;
case 'integer':
$lookup[] = $spell;
break;
case 'array':
$lookup = $spell;
break;
default:
return [];
}
// note: omits required spell and chance in skill_discovery_template
$data = array_merge(
DB::World()->selectCol('SELECT spellId FROM spell_learn_spell WHERE entry IN (?a)', $lookup),
DB::World()->selectCol('SELECT spellId FROM skill_discovery_template WHERE reqSpell IN (?a)', $lookup),
$extraIds
);
// return list of integers, not strings
$data = array_map('intVal', $data);
return $data;
}
public static function getPageText($ptId)
{
$pages = [];
while ($ptId)
{
if ($row = DB::World()->selectRow('SELECT ptl.Text AS Text_loc?d, pt.* FROM page_text pt LEFT JOIN page_text_locale ptl ON pt.ID = ptl.ID AND locale = ? WHERE pt.ID = ?d', Lang::getLocale()->value, Lang::getLocale()->json(), $ptId))
{
$ptId = $row['NextPageID'];
$pages[] = Util::parseHtmlText(Util::localizedString($row, 'Text'));
}
else
{
trigger_error('Referenced PageTextId #'.$ptId.' is not in DB', E_USER_WARNING);
break;
}
}
return $pages;
}
public static function getQuotesForCreature(int $creatureId, bool $asHTML = false, string $talkSource = '') : array
{
$nQuotes = 0;
$quotes = [];
$soundIds = [];
$quoteSrc = DB::World()->select(
'SELECT ct.`GroupID` AS ARRAY_KEY, ct.`ID` AS ARRAY_KEY2, ct.`Type` AS "talkType", ct.TextRange AS "range",
IFNULL(bct.`LanguageID`, ct.`Language`) AS "lang",
IFNULL(NULLIF(bct.`Text`, ""), IFNULL(NULLIF(bct.`Text1`, ""), IFNULL(ct.`Text`, ""))) AS "text_loc0",
{ IFNULL(NULLIF(bctl.`Text`, ""), IFNULL(NULLIF(bctl.`Text1`, ""), IFNULL(ctl.`Text`, ""))) AS text_loc?d, }
IF(bct.`SoundEntriesID` > 0, bct.`SoundEntriesID`, ct.`Sound`) AS "soundId"
FROM creature_text ct
{ LEFT JOIN creature_text_locale ctl ON ct.`CreatureID` = ctl.`CreatureID` AND ct.`GroupID` = ctl.`GroupID` AND ct.`ID` = ctl.`ID` AND ctl.`Locale` = ? }
LEFT JOIN broadcast_text bct ON ct.`BroadcastTextId` = bct.`ID`
{ LEFT JOIN broadcast_text_locale bctl ON ct.`BroadcastTextId` = bctl.`ID` AND bctl.`locale` = ? }
WHERE ct.`CreatureID` = ?d',
Lang::getLocale()->value ?: DBSIMPLE_SKIP,
Lang::getLocale()->value ? Lang::getLocale()->json() : DBSIMPLE_SKIP,
Lang::getLocale()->value ? Lang::getLocale()->json() : DBSIMPLE_SKIP,
$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);
// make type css compatible
switch ($t['talkType'])
{
case 1: // yell:
case 14: $t['talkType'] = 1; break; // - dark red
case 2: // emote:
case 16: // "
case 3: // boss emote:
case 41: $t['talkType'] = 4; break; // - orange
case 4: // whisper:
case 15: // "
case 5: // boss whisper:
case 42: $t['talkType'] = 3; break; // - pink-ish
default: $t['talkType'] = 2; // [type: 0, 12] say: yellow-ish
}
// prefix
$pre = '';
if ($t['talkType'] != 4)
$pre = ($talkSource ?: '%s').' '.Lang::npc('textTypes', $t['talkType']).Lang::main('colon').($t['lang'] ? '['.Lang::game('languages', $t['lang']).'] ' : null);
if ($asHTML)
$msg = '<div><span class="s'.$t['talkType'].'">%s'.($t['range'] ? sprintf(Util::$dfnString, Lang::npc('textRanges', $t['range']), $msg) : $msg).'</span></div>';
else
$msg = '[div][span class=s'.$t['talkType'].']%s'.html_entity_decode($msg).'[/span][/div]';
$line = array(
'range' => $t['range'],
'text' => $msg,
'prefix' => $pre
);
$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];
}
}
}
?>

View File

@@ -1,160 +0,0 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
abstract class WorldPosition
{
private static array $alphaMapCache = [];
private static array $capitalCities = array( // capitals take precedence over their surrounding area
1497, 1637, 1638, 3487, // Undercity, Ogrimmar, Thunder Bluff, Silvermoon City
1519, 1537, 1657, 3557, // Stormwind City, Ironforge, Darnassus, The Exodar
3703, 4395 // Shattrath City, Dalaran
);
private static function alphaMapCheck(int $areaId, array &$set) : bool
{
$file = 'cache/alphaMaps/'.$areaId.'.png';
if (!file_exists($file)) // file does not exist (probably instanced area)
return false;
// invalid and corner cases (literally)
if (empty($set['posX']) || empty($set['posY']) || $set['posX'] >= 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, function ($a, $b) { return ($a['dist'] < $b['dist']) ? -1 : 1; });
$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()->select('SELECT `guid` AS ARRAY_KEY, `id`, `map` AS `mapId`, `position_x` AS `posX`, `position_y` AS `posY` FROM creature WHERE `guid` IN (?a)', $guids);
break;
case Type::OBJECT:
$result = DB::World()->select('SELECT `guid` AS ARRAY_KEY, `id`, `map` AS `mapId`, `position_x` AS `posX`, `position_y` AS `posY` FROM gameobject WHERE `guid` IN (?a)', $guids);
break;
case Type::SOUND:
$result = DB::AoWoW()->select('SELECT `id` AS ARRAY_KEY, `soundId` AS `id`, `mapId`, `posX`, `posY` FROM ?_soundemitters WHERE `id` IN (?a)', $guids);
break;
case Type::ZONE:
$result = DB::Aowow()->select('SELECT -`id` AS ARRAY_KEY, `id`, `parentMapId` AS `mapId`, `parentX` AS `posX`, `parentY` AS `posY` FROM ?_zones WHERE -`id` IN (?a)', $guids);
break;
case Type::AREATRIGGER:
$result = [];
if ($base = array_filter($guids, fn($x) => $x > 0))
$result = array_replace($result, DB::AoWoW()->select('SELECT `id` AS ARRAY_KEY, `id`, `mapId`, `posX`, `posY` FROM ?_areatrigger WHERE `id` IN (?a)', $base));
if ($endpoints = array_filter($guids, fn($x) => $x < 0))
$result = array_replace($result, DB::World()->select(
'SELECT -`ID` AS ARRAY_KEY, ID AS `id`, `target_map` AS `mapId`, `target_position_x` AS `posX`, `target_position_y` AS `posY` FROM areatrigger_teleport WHERE -`id` IN (?a) UNION
SELECT -`entryorguid` AS ARRAY_KEY, entryorguid AS `id`, `action_param1` AS `mapId`, `target_x` AS `posX`, `target_y` AS `posY` FROM smart_scripts WHERE -`entryorguid` IN (?a) AND `source_type` = ?d AND `action_type` = ?d',
$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 [];
$query =
'SELECT
x.`id`,
x.`areaId`,
IF(x.`defaultDungeonMapId` < 0, x.`floor` + 1, x.`floor`) AS `floor`,
IF(dm.`id` IS NOT NULL OR x.`defaultDungeonMapId` < 0, 1, 0) AS `multifloor`,
ROUND((x.`maxY` - ?d) * 100 / (x.`maxY` - x.`minY`), 1) AS `posX`,
ROUND((x.`maxX` - ?d) * 100 / (x.`maxX` - x.`minX`), 1) AS `posY`,
SQRT(POWER(ABS((x.`maxY` - ?d) * 100 / (x.`maxY` - x.`minY`) - 50), 2) +
POWER(ABS((x.`maxX` - ?d) * 100 / (x.`maxX` - x.`minX`) - 50), 2)) AS `dist`
FROM
(SELECT 0 AS `id`, `areaId`, `mapId`, `right` AS `minY`, `left` AS `maxY`, `top` AS `maxX`, `bottom` AS `minX`, 0 AS `floor`, 0 AS `worldMapAreaId`, `defaultDungeonMapId` FROM ?_worldmaparea wma UNION
SELECT dm.`id`, `areaId`, wma.`mapId`, `minY`, `maxY`, `maxX`, `minX`, `floor`, `worldMapAreaId`, `defaultDungeonMapId` FROM ?_worldmaparea wma
JOIN ?_dungeonmap dm ON dm.`mapId` = wma.`mapId` WHERE wma.`mapId` NOT IN (0, 1, 530, 571) OR wma.`areaId` = 4395) x
LEFT JOIN
?_dungeonmap dm ON dm.`mapId` = x.`mapId` AND dm.`worldMapAreaId` = x.`worldMapAreaId` AND dm.`floor` <> x.`floor` AND dm.`worldMapAreaId` > 0
WHERE
x.`mapId` = ?d AND IF(?d, x.`areaId` = ?d, x.`areaId` <> 0){ AND x.`floor` = ?d - IF(x.`defaultDungeonMapId` < 0, 1, 0)}
GROUP BY
x.`id`, x.`areaId`
HAVING
(`posX` BETWEEN 0.1 AND 99.9 AND `posY` BETWEEN 0.1 AND 99.9)
ORDER BY
`multifloor` DESC, `dist` ASC';
// dist BETWEEN 0 (center) AND 70.7 (corner)
$points = DB::Aowow()->select($query, $mapY, $mapX, $mapY, $mapX, $mapId, $preferedAreaId, $preferedAreaId, $preferedFloor < 0 ? DBSIMPLE_SKIP : $preferedFloor);
if (!$points) // retry: pre-instance subareas belong to the instance-maps but are displayed on the outside. There also cases where the zone reaches outside it's own map.
$points = DB::Aowow()->select($query, $mapY, $mapX, $mapY, $mapX, $mapId, 0, 0, DBSIMPLE_SKIP);
if (!is_array($points))
{
trigger_error('WorldPosition::toZonePos - query failed', E_USER_ERROR);
return [];
}
return $points;
}
}
?>

View File

@@ -1,243 +1,54 @@
<?php <?php
namespace Aowow; if (!defined('AOWOW_REVISION'))
die('illegal access');
mb_internal_encoding('UTF-8');
error_reporting(E_ALL);
mysqli_report(MYSQLI_REPORT_ERROR);
define('AOWOW_REVISION', 40);
define('OS_WIN', substr(PHP_OS, 0, 3) == 'WIN'); // OS_WIN as per compile info of php
define('CLI', PHP_SAPI === 'cli');
define('CLI_HAS_E', CLI && // WIN10 and later usually support ANSI escape sequences
(!OS_WIN || (function_exists('sapi_windows_vt100_support') && sapi_windows_vt100_support(STDOUT))));
$reqExt = ['SimpleXML', 'gd', 'mysqli', 'mbstring', 'fileinfo'/*, 'gmp'*/];
$badExt = [];
$error = '';
if ($ext = array_filter($reqExt, fn($x) => !extension_loaded($x)))
$error .= 'Required Extension <b>'.implode(', ', $ext)."</b> was not found. Please check if it should exist, using \"<i>php -m</i>\"\n\n";
if ($ext = array_filter($badExt, fn($x) => extension_loaded($x)))
$error .= 'Loaded Extension <b>'.implode(', ', $ext)."</b> is incompatible and must be disabled.\n\n";
if (version_compare(PHP_VERSION, '8.2.0') < 0)
$error .= 'PHP Version <b>8.2</b> or higher required! Your version is <b>'.PHP_VERSION."</b>.\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 'includes/libs/DbSimple/Generic.php'; // Libraray: http://en.dklab.ru/lib/DbSimple (using variant: https://github.com/ivan1986/DbSimple/tree/master)
require_once 'includes/database.class.php'; // wrap DBSimple
require_once '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';
});
// 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';
});
// 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/types/'.$cl.'.class.php'))
{
require_once 'includes/types/basetype.class.php';
require_once 'includes/types/'.$cl.'.class.php';
}
else
throw new \Exception('could not register type class: '.$cl);
});
// endpoint loader
spl_autoload_register(function (string $class) : void
{
if ($i = strrpos($class, '\\'))
$class = substr($class, $i + 1);
if (preg_match('/[^\w]/i', $class))
return;
$class = strtolower($class);
if (stripos($class, 'ajax') === 0) // handles ajax and jsonp requests
{
if (file_exists('includes/ajaxHandler/'.strtr($class, ['ajax' => '']).'.class.php'))
{
require_once 'includes/ajaxHandler/ajaxHandler.class.php';
require_once 'includes/ajaxHandler/'.strtr($class, ['ajax' => '']).'.class.php';
}
else
throw new \Exception('could not register ajaxHandler class: '.$class);
return;
}
else if (stripos($class, 'page')) // handles templated pages
{
if (file_exists('pages/'.strtr($class, ['page' => '']).'.php'))
{
require_once 'pages/genericPage.class.php';
require_once 'pages/'.strtr($class, ['page' => '']).'.php';
}
else if ($class == 'genericpage') // may be called directly in fatal error case
require_once 'pages/genericPage.class.php';
}
});
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;
// 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 (DB::isConnected(DB_AOWOW))
DB::Aowow()->query('INSERT INTO ?_errors (`date`, `version`, `phpError`, `file`, `line`, `query`, `post`, `userGroups`, `message`) VALUES (UNIX_TIMESTAMP(), ?d, ?d, ?, ?d, ?, ?, ?d, ?) ON DUPLICATE KEY UPDATE `date` = UNIX_TIMESTAMP()',
AOWOW_REVISION, $errNo, $errFile, $errLine, CLI ? 'CLI' : substr($_SERVER['QUERY_STRING'] ?? '', 0, 250), empty($_POST) ? '' : http_build_query($_POST), User::$groups, $errStr
);
if (CLI)
CLI::write($errName.' - '.$errStr.' @ '.$errFile. ':'.$errLine, $logLevel);
else if (Cfg::get('DEBUG') >= $logLevel)
Util::addNote($errName.' - '.$errStr.' @ '.$errFile. ':'.$errLine, U_GROUP_EMPLOYEE, $logLevel);
return true;
}, E_ALL);
// handle exceptions
set_exception_handler(function (\Throwable $e) : void
{
if (DB::isConnected(DB_AOWOW))
DB::Aowow()->query('INSERT INTO ?_errors (`date`, `version`, `phpError`, `file`, `line`, `query`, `post`, `userGroups`, `message`) VALUES (UNIX_TIMESTAMP(), ?d, ?d, ?, ?d, ?, ?, ?d, ?) ON DUPLICATE KEY UPDATE `date` = UNIX_TIMESTAMP()',
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 GenericPage())->error();
}
});
// handle fatal errors
register_shutdown_function(function() : void
{
if ($e = error_get_last())
{
if (DB::isConnected(DB_AOWOW))
DB::Aowow()->query('INSERT INTO ?_errors (`date`, `version`, `phpError`, `file`, `line`, `query`, `post`, `userGroups`, `message`) VALUES (UNIX_TIMESTAMP(), ?d, ?d, ?, ?d, ?, ?, ?d, ?) ON DUPLICATE KEY UPDATE `date` = UNIX_TIMESTAMP()',
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')) if (file_exists('config/config.php'))
require_once 'config/config.php'; require_once 'config/config.php';
else else
$AoWoWconf = []; $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'])) if (!empty($AoWoWconf['aowow']['db']))
DB::load(DB_AOWOW, $AoWoWconf['aowow']); DB::load(DB_AOWOW, $AoWoWconf['aowow']);
@@ -252,63 +63,133 @@ if (!empty($AoWoWconf['characters']))
if (!empty($charDBInfo)) if (!empty($charDBInfo))
DB::load(DB_CHARACTERS . $realm, $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 // handle occuring errors
Lang::load(Locale::EN); 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 if (User::isInGroup(U_GROUP_STAFF))
Cfg::load(); {
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) 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 GenericPage())->maintenance();
// Setup Session // Setup Session
$cacheDir = Cfg::get('SESSION_CACHE_DIR'); session_set_cookie_params(15 * YEAR, '/', '', $secure, true);
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_cache_limiter('private'); session_cache_limiter('private');
if (!session_start()) session_start();
{ if (!empty($AoWoWconf['aowow']) && User::init())
trigger_error('failed to start session', E_USER_ERROR);
(new GenericPage())->error();
}
if (User::init())
User::save(); // save user-variables in session User::save(); // save user-variables in session
// hard override locale for this call (should this be here..?) // todo: (low) - move to setup web-interface (when it begins its existance)
if (isset($_GET['locale']) && ($loc = Locale::tryFrom((int)$_GET['locale']))) if (!defined('CFG_SITE_HOST') || !defined('CFG_STATIC_HOST'))
Lang::load($loc);
else
Lang::load(User::$preferedLoc);
// set up some logging (some queries will execute before we init the user and load the config)
if (Cfg::get('DEBUG') >= LOG_LEVEL_INFO && User::isInGroup(U_GROUP_DEV | U_GROUP_ADMIN))
{ {
DB::Aowow()->setLogger(DB::profiler(...)); $host = substr($_SERVER['SERVER_NAME'].strtr($_SERVER['SCRIPT_NAME'], ['index.php' => '']), 0, -1);
DB::World()->setLogger(DB::profiler(...));
if (DB::isConnected(DB_AUTH))
DB::Auth()->setLogger(DB::profiler(...));
if (!empty($AoWoWconf['characters'])) define('HOST_URL', ($secure ? 'https://' : 'http://').$host);
foreach ($AoWoWconf['characters'] as $idx => $__) define('STATIC_URL', ($secure ? 'https://' : 'http://').$host.'/static');
if (DB::isConnected(DB_CHARACTERS . $idx))
DB::Characters($idx)->setLogger(DB::profiler(...)); 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! // parse page-parameters .. sanitize before use!
$str = explode('&', $_SERVER['QUERY_STRING'] ?? '', 2)[0]; $str = explode('&', $_SERVER['QUERY_STRING'], 2)[0];
$_ = explode('=', $str, 2); $_ = explode('=', $str, 2);
$pageCall = mb_strtolower($_[0]); $pageCall = $_[0];
$pageParam = $_[1] ?? ''; $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
?> ?>

View File

@@ -159,8 +159,9 @@ class DbSimple_Connect
* *
* @param string $query запрос * @param string $query запрос
*/ */
public function addInit(...$args) public function addInit($query)
{ {
$args = func_get_args();
if ($this->DbSimple !== null) if ($this->DbSimple !== null)
return call_user_func_array(array(&$this->DbSimple, 'query'), $args); return call_user_func_array(array(&$this->DbSimple, 'query'), $args);
$this->init[] = $args; $this->init[] = $args;

View File

@@ -93,8 +93,6 @@ require_once __DIR__ . '/CacherImpl.php';
*/ */
abstract class DbSimple_Database extends DbSimple_LastError abstract class DbSimple_Database extends DbSimple_LastError
{ {
private $attributes;
/** /**
* Public methods. * Public methods.
*/ */
@@ -146,8 +144,9 @@ abstract class DbSimple_Database extends DbSimple_LastError
* mixed select(string $query [, $arg1] [,$arg2] ...) * mixed select(string $query [, $arg1] [,$arg2] ...)
* Execute query and return the result. * Execute query and return the result.
*/ */
public function select(...$args) public function select($query)
{ {
$args = func_get_args();
$total = false; $total = false;
return $this->_query($args, $total); return $this->_query($args, $total);
} }
@@ -158,8 +157,10 @@ abstract class DbSimple_Database extends DbSimple_LastError
* Total number of found rows (independent to LIMIT) is returned in $total * Total number of found rows (independent to LIMIT) is returned in $total
* (in most cases second query is performed to calculate $total). * (in most cases second query is performed to calculate $total).
*/ */
public function selectPage(&$total, ...$args) public function selectPage(&$total, $query)
{ {
$args = func_get_args();
array_shift($args);
$total = true; $total = true;
return $this->_query($args, $total); return $this->_query($args, $total);
} }
@@ -172,8 +173,9 @@ abstract class DbSimple_Database extends DbSimple_LastError
* because PHP DOES NOT generates notice on $row['abc'] if $row === null * because PHP DOES NOT generates notice on $row['abc'] if $row === null
* or $row === false (but, if $row is empty array, notice is generated). * or $row === false (but, if $row is empty array, notice is generated).
*/ */
public function selectRow(...$args) public function selectRow()
{ {
$args = func_get_args();
$total = false; $total = false;
$rows = $this->_query($args, $total); $rows = $this->_query($args, $total);
if (!is_array($rows)) return $rows; if (!is_array($rows)) return $rows;
@@ -186,8 +188,9 @@ abstract class DbSimple_Database extends DbSimple_LastError
* array selectCol(string $query [, $arg1] [,$arg2] ...) * array selectCol(string $query [, $arg1] [,$arg2] ...)
* Return the first column of query result as array. * Return the first column of query result as array.
*/ */
public function selectCol(...$args) public function selectCol()
{ {
$args = func_get_args();
$total = false; $total = false;
$rows = $this->_query($args, $total); $rows = $this->_query($args, $total);
if (!is_array($rows)) return $rows; if (!is_array($rows)) return $rows;
@@ -200,8 +203,9 @@ abstract class DbSimple_Database extends DbSimple_LastError
* Return the first cell of the first column of query result. * Return the first cell of the first column of query result.
* If no one row selected, return null. * If no one row selected, return null.
*/ */
public function selectCell(...$args) public function selectCell()
{ {
$args = func_get_args();
$total = false; $total = false;
$rows = $this->_query($args, $total); $rows = $this->_query($args, $total);
if (!is_array($rows)) return $rows; if (!is_array($rows)) return $rows;
@@ -217,8 +221,9 @@ abstract class DbSimple_Database extends DbSimple_LastError
* mixed query(string $query [, $arg1] [,$arg2] ...) * mixed query(string $query [, $arg1] [,$arg2] ...)
* Alias for select(). May be used for INSERT or UPDATE queries. * Alias for select(). May be used for INSERT or UPDATE queries.
*/ */
public function query(...$args) public function query()
{ {
$args = func_get_args();
$total = false; $total = false;
return $this->_query($args, $total); return $this->_query($args, $total);
} }
@@ -241,8 +246,9 @@ abstract class DbSimple_Database extends DbSimple_LastError
* Нужно для сложных запросов, состоящих из кусков, которые полезно сохранить * Нужно для сложных запросов, состоящих из кусков, которые полезно сохранить
* *
*/ */
public function subquery(...$args) public function subquery()
{ {
$args = func_get_args();
$this->_expandPlaceholders($args,$this->_placeholderNativeArgs !== null); $this->_expandPlaceholders($args,$this->_placeholderNativeArgs !== null);
return new DbSimple_SubQuery($args); return new DbSimple_SubQuery($args);
} }
@@ -1144,7 +1150,7 @@ abstract class DbSimple_Database extends DbSimple_LastError
$len = 0; $len = 0;
$values = array(); $values = array();
foreach ($rows[0] as $k=>$v) { foreach ($rows[0] as $k=>$v) {
$len += strlen($v ?? ''); $len += strlen($v);
if ($len > $this->MAX_LOG_ROW_LEN) { if ($len > $this->MAX_LOG_ROW_LEN) {
break; break;
} }

View File

@@ -26,13 +26,11 @@ class DbSimple_Mysqli extends DbSimple_Database
{ {
var $link; var $link;
private $_lastQuery;
/** /**
* constructor(string $dsn) * constructor(string $dsn)
* Connect to MySQL server. * Connect to MySQL server.
*/ */
function __construct($dsn) function DbSimple_Mysqli($dsn)
{ {
if (!is_callable("mysqli_connect")) if (!is_callable("mysqli_connect"))
@@ -161,21 +159,9 @@ class DbSimple_Mysqli extends DbSimple_Database
{ {
$this->_lastQuery = $queryMain; $this->_lastQuery = $queryMain;
$this->_expandPlaceholders($queryMain, false); $this->_expandPlaceholders($queryMain, false);
mysqli_ping($this->link);
$result = mysqli_query($this->link, $queryMain[0]); $result = mysqli_query($this->link, $queryMain[0]);
if ($result === false) if ($result === false)
return $this->_setDbError($queryMain[0]); return $this->_setDbError($queryMain[0]);
if ($this->link->warning_count) {
if ($warn = $this->link->query("SHOW WARNINGS")) {
while ($warnRow = $warn->fetch_row())
if ($warnRow[0] === 'Warning')
$this->_setLastError(-$warnRow[1], $warnRow[2], $queryMain[0]);
$warn->close();
}
}
if (!is_object($result)) { if (!is_object($result)) {
if (preg_match('/^\s* INSERT \s+/six', $queryMain[0])) if (preg_match('/^\s* INSERT \s+/six', $queryMain[0]))
{ {
@@ -192,7 +178,7 @@ class DbSimple_Mysqli extends DbSimple_Database
protected function _performFetch($result) protected function _performFetch($result)
{ {
$row = mysqli_fetch_assoc($result); $row = mysqli_fetch_assoc($result);
if (mysqli_error($this->link)) return $this->_setDbError($this->_lastQuery); if (mysql_error()) return $this->_setDbError($this->_lastQuery);
if ($row === false) return null; if ($row === false) return null;
return $row; return $row;
} }

View File

@@ -1,187 +0,0 @@
<?php
/****************************************
Example of how to use this uploader class...
You can uncomment the following lines (minus the require) to use these as your defaults.
// list of valid extensions, ex. array("jpeg", "xml", "bmp")
$allowedExtensions = array();
// max file size in bytes
$sizeLimit = 10 * 1024 * 1024;
require('valums-file-uploader/server/php.php');
$uploader = new qqFileUploader($allowedExtensions, $sizeLimit);
// Call handleUpload() with the name of the folder, relative to PHP's getcwd()
$result = $uploader->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 = trim($str);
$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'];
}
}

View File

@@ -1,179 +0,0 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
enum Locale : int
{ // unused by TC WoW self
case EN = 0; // enGB, enUS
case KR = 1; // koKR ?
case FR = 2; // frFR
case DE = 3; // deDE
case CN = 4; // zhCN, enCN
case TW = 5; // zhTW, enTW x
case ES = 6; // esES
case MX = 7; // esMX x
case RU = 8; // ruRU
case JP = 9; // jaJP x x x
case PT = 10; // ptPT, ptBR x ?
case IT = 11; // itIT x x x
private const MASK_ALL = 0b000101011101; // technically supported locales
public function domain() : string // our subdomain / locale in web context
{
return match ($this)
{
self::EN => '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;
}
}
?>

View File

@@ -1,9 +1,7 @@
<?php <?php
namespace Aowow;
if (!defined('AOWOW_REVISION')) if (!defined('AOWOW_REVISION'))
die('illegal access'); die('invalid access');
/* from TC wiki /* from TC wiki
@@ -27,7 +25,6 @@ class Loot
private $entry = 0; // depending on the lookup itemId oder templateId private $entry = 0; // depending on the lookup itemId oder templateId
private $results = []; private $results = [];
private $chanceMods = [];
private $lootTemplates = array( private $lootTemplates = array(
LOOT_REFERENCE, // internal LOOT_REFERENCE, // internal
LOOT_ITEM, // item LOOT_ITEM, // item
@@ -43,33 +40,33 @@ class Loot
LOOT_SPELL // spell LOOT_SPELL // spell
); );
public function &iterate() : iterable public function &iterate()
{ {
reset($this->results); reset($this->results);
foreach ($this->results as $k => $__) while (list($k, $__) = each($this->results))
yield $k => $this->results[$k]; yield $k => $this->results[$k];
} }
public function getResult() : array public function getResult()
{ {
return $this->results; return $this->results;
} }
private function createStack(array $l) : string // issue: TC always has an equal distribution between min/max 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']) if (empty($l['min']) || empty($l['max']) || $l['max'] <= $l['min'])
return ''; return null;
$stack = []; $stack = [];
for ($i = $l['min']; $i <= $l['max']; $i++) for ($i = $l['min']; $i <= $l['max']; $i++)
$stack[$i] = round(100 / (1 + $l['max'] - $l['min']), 3); $stack[$i] = round(100 / (1 + $l['max'] - $l['min']), 3);
// yes, it wants a string .. how weired is that.. // yes, it wants a string .. how weired is that..
return json_encode($stack, JSON_NUMERIC_CHECK); // do not replace with Util::toJSON ! return json_encode($stack, JSON_NUMERIC_CHECK);
} }
private function storeJSGlobals(array $data) : void private function storeJSGlobals($data)
{ {
foreach ($data as $type => $jsData) foreach ($data as $type => $jsData)
{ {
@@ -84,91 +81,29 @@ class Loot
} }
} }
private function calcChance(array $refs, array $parents = []) : array private function getByContainerRecursive($tableName, $lootId, &$handledRefs, $groupId = 0, $baseChance = 1.0)
{
$retData = [];
$retKeys = [];
foreach ($refs as $rId => $ref)
{
// check for possible database inconsistencies
if (!$ref['chance'] && !$ref['isGrouped'])
trigger_error('Loot by Item: Ungrouped Item/Ref '.$ref['item'].' has 0% chance assigned!', E_USER_WARNING);
if ($ref['isGrouped'] && $ref['sumChance'] > 100)
trigger_error('Loot by Item: Group with Item/Ref '.$ref['item'].' has '.number_format($ref['sumChance'], 2).'% total chance! Some items cannot drop!', E_USER_WARNING);
if ($ref['isGrouped'] && $ref['sumChance'] >= 100 && !$ref['chance'])
trigger_error('Loot by Item: Item/Ref '.$ref['item'].' with adaptive chance cannot drop. Group already at 100%!', E_USER_WARNING);
$chance = abs($ref['chance'] ?: (100 - $ref['sumChance']) / $ref['nZeroItems']) / 100;
// apply inherited chanceMods
if (isset($this->chanceMods[$ref['item']]))
{
$chance *= $this->chanceMods[$ref['item']][0];
$chance = 1 - pow(1 - $chance, $this->chanceMods[$ref['item']][1]);
}
// save chance for parent-ref
$this->chanceMods[$rId] = [$chance, $ref['multiplier']];
// refTemplate doesn't point to a new ref -> we are done
if (!in_array($rId, $parents))
{
$data = array(
'percent' => $chance,
'stack' => [$ref['min'], $ref['max']],
'count' => 1 // ..and one for the sort script
);
if ($_ = self::createStack($ref))
$data['pctstack'] = $_;
// sort highest chances first
$i = 0;
for (; $i < count($retData); $i++)
if ($retData[$i]['percent'] < $data['percent'])
break;
array_splice($retData, $i, 0, [$data]);
array_splice($retKeys, $i, 0, [$rId]);
}
}
return array_combine($retKeys, $retData);
}
private function getByContainerRecursive(string $tableName, int $lootId, array &$handledRefs, int $groupId = 0, float $baseChance = 1.0) : array
{ {
$loot = []; $loot = [];
$rawItems = []; $rawItems = [];
if (!$tableName || !$lootId) if (!$tableName || !$lootId)
return [null, null]; return null;
$rows = DB::World()->select('SELECT * FROM ?# WHERE entry = ?d{ AND groupid = ?d}', $tableName, $lootId, $groupId ?: DBSIMPLE_SKIP); $rows = DB::World()->select('SELECT * FROM ?# WHERE entry = ?d{ AND groupid = ?d}', $tableName, $lootId, $groupId ?: DBSIMPLE_SKIP);
if (!$rows) if (!$rows)
return [null, null]; return null;
$groupChances = []; $groupChances = [];
$nGroupEquals = []; $nGroupEquals = [];
$cnd = new Conditions();
foreach ($rows as $entry) foreach ($rows as $entry)
{ {
$set = array( $set = array(
'quest' => $entry['QuestRequired'], 'quest' => $entry['QuestRequired'],
'group' => $entry['GroupId'], 'group' => $entry['GroupId'],
'parentRef' => $tableName == LOOT_REFERENCE ? $lootId : 0, 'parentRef' => $tableName == LOOT_REFERENCE ? $lootId : 0,
'realChanceMod' => $baseChance, 'realChanceMod' => $baseChance
'groupChance' => 0
); );
if ($entry['QuestRequired'])
foreach (DB::Aowow()->selectCol('SELECT id FROM ?_quests WHERE (`reqSourceItemId1` = ?d OR `reqSourceItemId2` = ?d OR `reqSourceItemId3` = ?d OR `reqSourceItemId4` = ?d OR `reqItemId1` = ?d OR `reqItemId2` = ?d OR `reqItemId3` = ?d OR `reqItemId4` = ?d OR `reqItemId5` = ?d OR `reqItemId6` = ?d) AND (`cuFlags` & ?d) = 0',
$entry['Item'], $entry['Item'], $entry['Item'], $entry['Item'], $entry['Item'], $entry['Item'], $entry['Item'], $entry['Item'], $entry['Item'], $entry['Item'], CUSTOM_EXCLUDE_FOR_LISTVIEW | CUSTOM_UNAVAILABLE) as $questId)
$cnd->addExternalCondition(Conditions::lootTableToConditionSource($tableName), $lootId . ':' . $entry['Item'], [Conditions::QUESTTAKEN, $questId], true);
// if ($entry['LootMode'] > 1) // if ($entry['LootMode'] > 1)
// { // {
$buff = []; $buff = [];
@@ -199,7 +134,7 @@ class Loot
// bandaid.. remove when propperly handling lootmodes // bandaid.. remove when propperly handling lootmodes
if (!in_array($entry['Reference'], $handledRefs)) if (!in_array($entry['Reference'], $handledRefs))
{ // todo (high): find out, why i used this in the first place. (don't do drugs, kids) { // todo (high): find out, why i used this in the first place. (don't do drugs, kids)
[$data, $raw] = self::getByContainerRecursive(LOOT_REFERENCE, $entry['Reference'], $handledRefs, /*$entry['GroupId'],*/ 0, $entry['Chance'] / 100); list($data, $raw) = self::getByContainerRecursive(LOOT_REFERENCE, $entry['Reference'], $handledRefs, /*$entry['GroupId'],*/ 0, $entry['Chance'] / 100);
$handledRefs[] = $entry['Reference']; $handledRefs[] = $entry['Reference'];
@@ -232,19 +167,15 @@ class Loot
} }
else if ($entry['GroupId'] && $entry['Chance']) else if ($entry['GroupId'] && $entry['Chance'])
{ {
if (empty($groupChances[$entry['GroupId']]))
$groupChances[$entry['GroupId']] = 0;
$groupChances[$entry['GroupId']] += $entry['Chance'];
$set['groupChance'] = $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 else // shouldn't have happened
{ {
trigger_error('Unhandled case in calculating chance for item '.$entry['Item'].'!', E_USER_WARNING); Util::addNote(U_GROUP_EMPLOYEE, 'Loot::getByContainerRecursive: unhandled case in calculating chance for item '.$entry['Item'].'!');
continue; continue;
} }
@@ -258,30 +189,30 @@ class Loot
$sum = 0; $sum = 0;
else if ($sum >= 100.01) 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); 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; $sum = 100;
} }
// is applied as backReference to items with 0-chance
$groupChances[$k] = (100 - $sum) / ($nGroupEquals[$k] ?: 1);
}
if ($cnd->getBySourceGroup($lootId, Conditions::lootTableToConditionSource($tableName))->prepare()) $cnt = empty($nGroupEquals[$k]) ? 1 : $nGroupEquals[$k];
{
self::storeJSGlobals($cnd->getJsGlobals()); $groupChances[$k] = (100 - $sum) / $cnt; // is applied as backReference to items with 0-chance
$cnd->toListviewColumn($loot, $this->extraCols, $lootId, 'content');
} }
return [$loot, array_unique($rawItems)]; return [$loot, array_unique($rawItems)];
} }
public function getByContainer(string $table, int $entry): bool public function getByContainer($table, $entry)
{ {
$this->entry = intVal($entry); $this->entry = intVal($entry);
if (!in_array($table, $this->lootTemplates) || !$this->entry) if (!in_array($table, $this->lootTemplates) || !$this->entry)
return false; 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]) // if (is_array($this->entry) && in_array($table, [LOOT_CREATURE, LOOT_GAMEOBJECT])
// iterate over the 4 available difficulties and assign modes // iterate over the 4 available difficulties and assign modes
@@ -289,16 +220,16 @@ class Loot
modes:{"mode":1,"1":{"count":4408,"outof":16013},"4":{"count":4408,"outof":22531}} modes:{"mode":1,"1":{"count":4408,"outof":16013},"4":{"count":4408,"outof":22531}}
*/ */
$handledRefs = []; $handledRefs = [];
[$lootRows, $itemIds] = self::getByContainerRecursive($table, $this->entry, $handledRefs); $struct = self::getByContainerRecursive($table, $this->entry, $handledRefs);
if (!$lootRows) if (!$struct)
return false; return false;
$items = new ItemList(array(['i.id', $itemIds], Cfg::get('SQL_LIMIT_NONE'))); $items = new ItemList(array(['i.id', $struct[1]], CFG_SQL_LIMIT_NONE));
self::storeJSGlobals($items->getJSGlobals(GLOBALINFO_SELF | GLOBALINFO_RELATED)); $this->jsGlobals = $items->getJSGlobals(GLOBALINFO_SELF | GLOBALINFO_RELATED);
$foo = $items->getListviewData(); $foo = $items->getListviewData();
// assign listview LV rows to loot rows, not the other way round! The same item may be contained multiple times // assign listview LV rows to loot rows, not the other way round! The same item may be contained multiple times
foreach ($lootRows as $loot) foreach ($struct[0] as $loot)
{ {
$base = array( $base = array(
'percent' => round($loot['groupChance'] * $loot['realChanceMod'], 3), 'percent' => round($loot['groupChance'] * $loot['realChanceMod'], 3),
@@ -313,20 +244,11 @@ class Loot
if ($_ = $loot['parentRef']) if ($_ = $loot['parentRef'])
$base['reference'] = $_; $base['reference'] = $_;
if (isset($loot['condition']))
$base['condition'] = $loot['condition'];
if ($_ = self::createStack($loot)) if ($_ = self::createStack($loot))
$base['pctstack'] = $_; $base['pctstack'] = $_;
if (empty($loot['reference'])) // regular drop if (empty($loot['reference'])) // regular drop
{ {
if (!isset($foo[$loot['content']]))
{
trigger_error('Item #'.$loot['content'].' referenced by loot does not exist!', E_USER_WARNING);
continue;
}
if (!User::isInGroup(U_GROUP_EMPLOYEE)) if (!User::isInGroup(U_GROUP_EMPLOYEE))
{ {
if (!isset($this->results[$loot['content']])) if (!isset($this->results[$loot['content']]))
@@ -347,7 +269,7 @@ class Loot
); );
$this->results[] = array_merge($base, $data); $this->results[] = array_merge($base, $data);
$this->jsGlobals[Type::ITEM][$loot['reference']] = $data; $this->jsGlobals[TYPE_ITEM][$loot['reference']] = $data;
} }
} }
@@ -390,53 +312,51 @@ class Loot
break; break;
} }
$this->extraCols[] = "\$Listview.funcBox.createSimpleCol('group', 'Group', '7%', 'group')"; $this->extraCols[] = "Listview.funcBox.createSimpleCol('group', 'Group', '7%', 'group')";
foreach ($fields as $idx => $field) foreach ($fields as $idx => $field)
if ($set & (1 << $idx)) if ($set & (1 << $idx))
$this->extraCols[] = "\$Listview.funcBox.createSimpleCol('".$field."', '".Util::ucFirst($field)."', '7%', '".$field."')"; $this->extraCols[] = "Listview.funcBox.createSimpleCol('".$field."', '".Util::ucFirst($field)."', '7%', '".$field."')";
} }
return true; return true;
} }
public function getByItem(int $entry, int $maxResults = -1, array $lootTableList = []) : bool public function getByItem($entry, $maxResults = CFG_SQL_LIMIT_DEFAULT, $lootTableList = [])
{ {
$this->entry = $entry; $this->entry = intVal($entry);
if (!$this->entry) if (!$this->entry)
return false; return false;
if ($maxResults < 0)
$maxResults = Cfg::get('SQL_LIMIT_DEFAULT');
// [fileName, tabData, tabName, tabId, extraCols, hiddenCols, visibleCols] // [fileName, tabData, tabName, tabId, extraCols, hiddenCols, visibleCols]
$tabsFinal = array( $tabsFinal = array(
[Type::ITEM, [], '$LANG.tab_containedin', 'contained-in-item', [], [], []], ['item', [], '$LANG.tab_containedin', 'contained-in-item', [], [], []],
[Type::ITEM, [], '$LANG.tab_disenchantedfrom', 'disenchanted-from', [], [], []], ['item', [], '$LANG.tab_disenchantedfrom', 'disenchanted-from', [], [], []],
[Type::ITEM, [], '$LANG.tab_prospectedfrom', 'prospected-from', [], [], []], ['item', [], '$LANG.tab_prospectedfrom', 'prospected-from', [], [], []],
[Type::ITEM, [], '$LANG.tab_milledfrom', 'milled-from', [], [], []], ['item', [], '$LANG.tab_milledfrom', 'milled-from', [], [], []],
[Type::NPC, [], '$LANG.tab_droppedby', 'dropped-by', [], [], []], ['creature', [], '$LANG.tab_droppedby', 'dropped-by', [], [], []],
[Type::NPC, [], '$LANG.tab_pickpocketedfrom', 'pickpocketed-from', [], [], []], ['creature', [], '$LANG.tab_pickpocketedfrom', 'pickpocketed-from', [], [], []],
[Type::NPC, [], '$LANG.tab_skinnedfrom', 'skinned-from', [], [], []], ['creature', [], '$LANG.tab_skinnedfrom', 'skinned-from', [], [], []],
[Type::NPC, [], '$LANG.tab_minedfromnpc', 'mined-from-npc', [], [], []], ['creature', [], '$LANG.tab_minedfromnpc', 'mined-from-npc', [], [], []],
[Type::NPC, [], '$LANG.tab_salvagedfrom', 'salvaged-from', [], [], []], ['creature', [], '$LANG.tab_salvagedfrom', 'salvaged-from', [], [], []],
[Type::NPC, [], '$LANG.tab_gatheredfromnpc', 'gathered-from-npc', [], [], []], ['creature', [], '$LANG.tab_gatheredfromnpc', 'gathered-from-npc', [], [], []],
[Type::QUEST, [], '$LANG.tab_rewardfrom', 'reward-from-quest', [], [], []], ['quest', [], '$LANG.tab_rewardfrom', 'reward-from-quest', [], [], []],
[Type::ZONE, [], '$LANG.tab_fishedin', 'fished-in-zone', [], [], []], ['zone', [], '$LANG.tab_fishedin', 'fished-in-zone', [], [], []],
[Type::OBJECT, [], '$LANG.tab_containedin', 'contained-in-object', [], [], []], ['object', [], '$LANG.tab_containedin', 'contained-in-object', [], [], []],
[Type::OBJECT, [], '$LANG.tab_minedfrom', 'mined-from-object', [], [], []], ['object', [], '$LANG.tab_minedfrom', 'mined-from-object', [], [], []],
[Type::OBJECT, [], '$LANG.tab_gatheredfrom', 'gathered-from-object', [], [], []], ['object', [], '$LANG.tab_gatheredfrom', 'gathered-from-object', [], [], []],
[Type::OBJECT, [], '$LANG.tab_fishedin', 'fished-in-object', [], [], []], ['object', [], '$LANG.tab_fishedin', 'fished-in-object', [], [], []],
[Type::SPELL, [], '$LANG.tab_createdby', 'created-by', [], [], []], ['spell', [], '$LANG.tab_createdby', 'created-by', [], [], []],
[Type::ACHIEVEMENT, [], '$LANG.tab_rewardfrom', 'reward-from-achievement', [], [], []] ['achievement', [], '$LANG.tab_rewardfrom', 'reward-from-achievement', [], [], []]
); );
$refResults = []; $refResults = [];
$chanceMods = [];
$query = 'SELECT $query = 'SELECT
lt1.entry AS ARRAY_KEY, lt1.entry AS ARRAY_KEY,
IF(lt1.reference = 0, lt1.item, lt1.reference) AS item, IF(lt1.reference = 0, lt1.item, lt1.reference) AS item,
lt1.chance, lt1.chance,
SUM(IF(lt2.chance = 0, 1, 0)) AS nZeroItems, SUM(IF(lt2.chance = 0, 1, 0)) AS nZeroItems,
SUM(IF(lt2.reference = 0, lt2.chance, 0)) AS sumChance, SUM(lt2.chance) AS sumChance,
IF(lt1.groupid > 0, 1, 0) AS isGrouped, IF(lt1.groupid > 0, 1, 0) AS isGrouped,
IF(lt1.reference = 0, lt1.mincount, 1) AS min, 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 max,
@@ -449,6 +369,61 @@ class Loot
%s %s
GROUP BY lt2.entry, lt2.groupid'; 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 get references containing the item
*/ */
@@ -458,16 +433,6 @@ class Loot
$this->entry $this->entry
); );
/* i'm currently not seeing a reasonable way to blend this into creature/gobject/etc tabs as one entity may drop the same item multiple times, with and without conditions.
if ($newRefs)
{
$cnd = new Conditions();
if ($cnd->getBySourceEntry($this->entry, Conditions::SRC_REFERENCE_LOOT_TEMPLATE))
if ($cnd->toListviewColumn($newRefs, $x, $this->entry))
self::storejsGlobals($cnd->getJsGlobals());
}
*/
while ($newRefs) while ($newRefs)
{ {
$curRefs = $newRefs; $curRefs = $newRefs;
@@ -477,23 +442,20 @@ class Loot
array_keys($curRefs) array_keys($curRefs)
); );
$refResults += $this->calcChance($curRefs, array_column($newRefs, 'item')); $refResults += $calcChance($curRefs, array_column($newRefs, 'item'));
} }
/* /*
search the real loot-templates for the itemId and gathered refds search the real loot-templates for the itemId and gathered refds
*/ */
foreach ($this->lootTemplates as $lootTemplate) for ($i = 1; $i < count($this->lootTemplates); $i++)
{ {
if ($lootTableList && !in_array($lootTemplate, $lootTableList)) if ($lootTableList && !in_array($this->lootTemplates[$i], $lootTableList))
continue; continue;
if ($lootTemplate == LOOT_REFERENCE) $result = $calcChance(DB::World()->select(
continue;
$result = $this->calcChance(DB::World()->select(
sprintf($query, '{lt1.reference IN (?a) OR }(lt1.reference = 0 AND lt1.item = ?d)'), sprintf($query, '{lt1.reference IN (?a) OR }(lt1.reference = 0 AND lt1.item = ?d)'),
$lootTemplate, $lootTemplate, $this->lootTemplates[$i], $this->lootTemplates[$i],
$refResults ? array_keys($refResults) : DBSIMPLE_SKIP, $refResults ? array_keys($refResults) : DBSIMPLE_SKIP,
$this->entry $this->entry
)); ));
@@ -509,10 +471,10 @@ class Loot
} }
// cap fetched entries to the sql-limit to guarantee, that the highest chance items get selected first // cap fetched entries to the sql-limit to guarantee, that the highest chance items get selected first
// screws with GO-loot and skinning-loot as these templates are shared for several tabs (fish, herb, ore) (herb, ore, leather) // 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); $ids = array_slice(array_keys($result), 0, $maxResults);
switch ($lootTemplate) switch ($this->lootTemplates[$i])
{ {
case LOOT_CREATURE: $field = 'lootId'; $tabId = 4; break; case LOOT_CREATURE: $field = 'lootId'; $tabId = 4; break;
case LOOT_PICKPOCKET: $field = 'pickpocketLootId'; $tabId = 5; break; case LOOT_PICKPOCKET: $field = 'pickpocketLootId'; $tabId = 5; break;
@@ -532,7 +494,7 @@ class Loot
$srcData = $srcObj->getListviewData(); $srcData = $srcObj->getListviewData();
foreach ($srcObj->iterate() as $curTpl) foreach ($srcObj->iterate() as $__id => $curTpl)
{ {
switch ($curTpl['typeCat']) switch ($curTpl['typeCat'])
{ {
@@ -543,7 +505,7 @@ class Loot
} }
$tabsFinal[$tabId][1][] = array_merge($srcData[$srcObj->id], $result[$srcObj->getField('lootId')]); $tabsFinal[$tabId][1][] = array_merge($srcData[$srcObj->id], $result[$srcObj->getField('lootId')]);
$tabsFinal[$tabId][4][] = '$Listview.extraCols.percent'; $tabsFinal[$tabId][4][] = 'Listview.extraCols.percent';
if ($tabId != 15) if ($tabId != 15)
$tabsFinal[$tabId][6][] = 'skill'; $tabsFinal[$tabId][6][] = 'skill';
} }
@@ -568,7 +530,7 @@ class Loot
// achievement part // achievement part
$conditions = array(['itemExtra', $this->entry]); $conditions = array(['itemExtra', $this->entry]);
if ($ar = DB::World()->selectCol('SELECT ID FROM achievement_reward WHERE ItemID = ?d{ OR MailTemplateID IN (?a)}', $this->entry, $ids ?: DBSIMPLE_SKIP)) 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'); array_push($conditions, ['id', $ar], 'OR');
$srcObj = new AchievementList($conditions); $srcObj = new AchievementList($conditions);
@@ -587,9 +549,9 @@ class Loot
case LOOT_SPELL: case LOOT_SPELL:
$conditions = array( $conditions = array(
'OR', 'OR',
['AND', ['effect1CreateItemId', $this->entry], ['OR', ['effect1Id', SpellList::EFFECTS_ITEM_CREATE], ['effect1AuraId', SpellList::AURAS_ITEM_CREATE]]], ['AND', ['effect1CreateItemId', $this->entry], ['OR', ['effect1Id', SpellList::$effects['itemCreate']], ['effect1AuraId', SpellList::$auras['itemCreate']]]],
['AND', ['effect2CreateItemId', $this->entry], ['OR', ['effect2Id', SpellList::EFFECTS_ITEM_CREATE], ['effect2AuraId', SpellList::AURAS_ITEM_CREATE]]], ['AND', ['effect2CreateItemId', $this->entry], ['OR', ['effect2Id', SpellList::$effects['itemCreate']], ['effect2AuraId', SpellList::$auras['itemCreate']]]],
['AND', ['effect3CreateItemId', $this->entry], ['OR', ['effect3Id', SpellList::EFFECTS_ITEM_CREATE], ['effect3AuraId', SpellList::AURAS_ITEM_CREATE]]], ['AND', ['effect3CreateItemId', $this->entry], ['OR', ['effect3Id', SpellList::$effects['itemCreate']], ['effect3AuraId', SpellList::$auras['itemCreate']]]],
); );
if ($ids) if ($ids)
$conditions[] = ['id', $ids]; $conditions[] = ['id', $ids];
@@ -601,9 +563,9 @@ class Loot
$srcData = $srcObj->getListviewData(); $srcData = $srcObj->getListviewData();
if (!empty($result)) if (!empty($result))
$tabsFinal[16][4][] = '$Listview.extraCols.percent'; $tabsFinal[16][4][] = 'Listview.extraCols.percent';
if ($srcObj->hasSetFields('reagent1', 'reagent2', 'reagent3', 'reagent4', 'reagent5', 'reagent6', 'reagent7', 'reagent8')) if ($srcObj->hasSetFields(['reagent1']))
$tabsFinal[16][6][] = 'reagents'; $tabsFinal[16][6][] = 'reagents';
foreach ($srcObj->iterate() as $_) foreach ($srcObj->iterate() as $_)
@@ -615,28 +577,13 @@ class Loot
if (!$ids) if (!$ids)
continue; continue;
$parentData = [];
switch ($tabsFinal[abs($tabId)][0]) switch ($tabsFinal[abs($tabId)][0])
{ {
case TYPE::NPC: // new CreatureList case 'creature': // new CreatureList
if ($baseIds = DB::Aowow()->selectCol( case 'item': // new ItemList
'SELECT `difficultyEntry1` AS ARRAY_KEY, `id` FROM ?_creature WHERE difficultyEntry1 IN (?a) UNION case 'zone': // new ZoneList
SELECT `difficultyEntry2` AS ARRAY_KEY, `id` FROM ?_creature WHERE difficultyEntry2 IN (?a) UNION $oName = ucFirst($tabsFinal[abs($tabId)][0]).'List';
SELECT `difficultyEntry3` AS ARRAY_KEY, `id` FROM ?_creature WHERE difficultyEntry3 IN (?a)', $srcObj = new $oName(array([$field, $ids]));
$ids, $ids, $ids))
{
$parentObj = new CreatureList(array(['id', $baseIds]));
if (!$parentObj->error)
{
self::storeJSGlobals($parentObj->getJSGlobals());
$parentData = $parentObj->getListviewData();
$ids = array_diff($ids, $baseIds);
}
}
case Type::ITEM: // new ItemList
case Type::ZONE: // new ZoneList
$srcObj = Type::newList($tabsFinal[abs($tabId)][0], array([$field, $ids]));
if (!$srcObj->error) if (!$srcObj->error)
{ {
$srcData = $srcObj->getListviewData(); $srcData = $srcObj->getListviewData();
@@ -644,46 +591,24 @@ class Loot
foreach ($srcObj->iterate() as $curTpl) foreach ($srcObj->iterate() as $curTpl)
{ {
if ($tabId < 0 && $curTpl['typeFlags'] & NPC_TYPEFLAG_SKIN_WITH_HERBALISM) if ($tabId < 0 && $curTpl['typeFlags'] & NPC_TYPEFLAG_HERBLOOT)
$tabId = 9; $tabId = 9;
else if ($tabId < 0 && $curTpl['typeFlags'] & NPC_TYPEFLAG_SKIN_WITH_ENGINEERING) else if ($tabId < 0 && $curTpl['typeFlags'] & NPC_TYPEFLAG_ENGINEERLOOT)
$tabId = 8; $tabId = 8;
else if ($tabId < 0 && $curTpl['typeFlags'] & NPC_TYPEFLAG_SKIN_WITH_MINING) else if ($tabId < 0 && $curTpl['typeFlags'] & NPC_TYPEFLAG_MININGLOOT)
$tabId = 7; $tabId = 7;
else if ($tabId < 0) else if ($tabId < 0)
$tabId = abs($tabId); // general case (skinning) $tabId = abs($tabId); // general case (skinning)
if (($p = $srcObj->getField('parentId')) && ($d = $parentData[$p] ?? null)) $tabsFinal[$tabId][1][] = array_merge($srcData[$srcObj->id], $result[$srcObj->getField($field)]);
$tabsFinal[$tabId][1][] = array_merge($d, $result[$srcObj->getField($field)]); $tabsFinal[$tabId][4][] = 'Listview.extraCols.percent';
else
$tabsFinal[$tabId][1][] = array_merge($srcData[$srcObj->id], $result[$srcObj->getField($field)]);
$tabsFinal[$tabId][4][] = '$Listview.extraCols.percent';
} }
} }
break; break;
} }
} }
foreach ($tabsFinal as $tabId => $data) $this->results = $tabsFinal;
{
$tabData = array(
'data' => $data[1],
'name' => $data[2],
'id' => $data[3]
);
if ($data[4])
$tabData['extraCols'] = array_unique($data[4]);
if ($data[5])
$tabData['hiddenCols'] = array_unique($data[5]);
if ($data[6])
$tabData['visibleCols'] = array_unique($data[6]);
$this->results[$tabId] = [Type::getFileString($data[0]), $tabData];
}
return true; return true;
} }

50
includes/markup.class.php Normal file
View File

@@ -0,0 +1,50 @@
<?php
if (!defined('AOWOW_REVISION'))
die('invalid access');
/*
this is just a skeleton for now
at some point i'll need to (at least rudamentary) parse
back and forth between markup and html
*/
class Markup
{
private $text = '';
private $jsGlobals = [];
public function __construct($text)
{
$this->text = $text;
}
public function parseGlobalsFromText(&$jsg = [])
{
if (preg_match_all('/(?<!\\\\)\[(npc|object|item|itemset|quest|spell|zone|faction|pet|achievement|statistic|title|event|class|race|skill|currency)=(\d+)[^\]]*\]/i', $this->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()
{
}
}
?>

View File

@@ -1,355 +0,0 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
abstract class CLI
{
private const CHR_BELL = 7;
private const CHR_BACK = 8;
private const CHR_TAB = 9;
private const CHR_LF = 10;
private const CHR_CR = 13;
private const CHR_ESC = 27;
private const CHR_BACKSPACE = 127;
public const LOG_NONE = -1;
public const LOG_BLANK = 0;
public const LOG_ERROR = LOG_LEVEL_ERROR;
public const LOG_WARN = LOG_LEVEL_WARN;
public const LOG_INFO = LOG_LEVEL_INFO;
public const LOG_OK = 4;
private static $logHandle = null;
private static $hasReadline = null;
private static $overwriteLast = false;
/********************/
/* formatted output */
/********************/
public static function writeTable(array $out, bool $timestamp = false, bool $headless = false) : void
{
if (!$out)
return;
$pads = [];
$nCols = 0;
foreach ($out as $i => $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);
switch ($lvl)
{
case self::LOG_ERROR: // red critical error
$msg .= '['.self::red('ERR').'] ';
break;
case self::LOG_WARN: // yellow notice
$msg .= '['.self::yellow('WARN').'] ';
break;
case self::LOG_OK: // green success
$msg .= '['.self::green('OK').'] ';
break;
case self::LOG_INFO: // blue info
$msg .= '['.self::blue('INFO').'] ';
break;
case self::LOG_BLANK:
$msg .= ' ';
break;
}
$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 <Enter> (wich also appends a \r)
as such <ESC> 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));
$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;
}
}
?>

View File

@@ -1,39 +0,0 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
class Timer
{
private $t_cur = 0;
private $t_new = 0;
private $intv = 0;
public function __construct(int $intervall)
{
$this->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;
}
}
?>

26
includes/shared.php Normal file
View File

@@ -0,0 +1,26 @@
<?php
define('AOWOW_REVISION', 1);
define('CLI', PHP_SAPI === 'cli');
$reqExt = ['SimpleXML', 'gd', 'mysqli', 'mbstring'];
$error = '';
foreach ($reqExt as $r)
if (!extension_loaded($r))
$error .= 'Required Extension <b>'.$r."</b> was not found. Please check if it should exist, using \"<i>php -m</i>\"\n\n";
if (version_compare(PHP_VERSION, '5.5.0') < 0)
$error .= 'PHP Version <b>5.5.0</b> or higher required! Your version is <b>'.PHP_VERSION."</b>.\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';
?>

View File

@@ -1,260 +0,0 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
abstract class Type
{
public const NPC = 1;
public const OBJECT = 2;
public const ITEM = 3;
public const ITEMSET = 4;
public const QUEST = 5;
public const SPELL = 6;
public const ZONE = 7;
public const FACTION = 8;
public const PET = 9;
public const ACHIEVEMENT = 10;
public const TITLE = 11;
public const WORLDEVENT = 12;
public const CHR_CLASS = 13;
public const CHR_RACE = 14;
public const SKILL = 15;
public const STATISTIC = 16;
public const CURRENCY = 17;
// PROJECT = 18;
public const SOUND = 19;
// BUILDING = 20;
// FOLLOWER = 21;
// MISSION_ABILITY = 22;
// MISSION = 23;
// SHIP = 25;
// THREAT = 26;
// RESOURCE = 27;
// CHAMPION = 28;
public const ICON = 29;
// ORDER_ADVANCEMENT = 30;
// FOLLOWER_ALLIANCE = 31;
// FOLLOWER_HORDE = 32;
// SHIP_ALLIANCE = 33;
// SHIP_HORDE = 34;
// CHAMPION_ALLIANCE = 35;
// CHAMPION_HORDE = 36;
// TRANSMOG_ITEM = 37;
// BFA_CHAMPION = 38;
// BFA_CHAMPION_ALLIANCE = 39;
// AFFIX = 40;
// BFA_CHAMPION_HORDE = 41;
// AZERITE_ESSENCE_POWER = 42;
// AZERITE_ESSENCE = 43;
// STORYLINE = 44;
// ADVENTURE_COMBATANT_ABILITY = 46;
// ENCOUNTER = 47;
// COVENANT = 48;
// SOULBIND = 49;
// DI_ITEM = 50;
// GATHERER_SCREENSHOT = 91;
// GATHERER_GUIDE_IMAGE = 98;
public const PROFILE = 100;
// our own things
public const GUILD = 101;
// TRANSMOG_SET = 101; // future conflict inc.
public const ARENA_TEAM = 102;
// OUTFIT = 110;
// GEAR_SET = 111;
// GATHERER_LISTVIEW = 158;
// GATHERER_SURVEY_COVENANTS = 161;
// NEWS_POST = 162;
// BATTLE_PET_ABILITY = 200;
public const GUIDE = 300; // should have been 100, but conflicts with old version of Profile/List
public const USER = 500;
public const EMOTE = 501;
public const ENCHANTMENT = 502;
public const AREATRIGGER = 503;
public const MAIL = 504;
// Blizzard API things
// MOUNT = -1000;
// RECIPE = -1001;
// BATTLE_PET = -1002;
public const FLAG_NONE = 0x0;
public const FLAG_RANDOM_SEARCHABLE = 0x1;
public const FLAG_FILTRABLE = 0x2;
public const FLAG_DB_TYPE = 0x4;
public const FLAG_HAS_ICON = 0x8;
public const IDX_LIST_OBJ = 0;
public const IDX_FILE_STR = 1;
public const IDX_JSG_TPL = 2;
public const IDX_FLAGS = 3;
private static array $data = array(
self::NPC => [__NAMESPACE__ . '\CreatureList', 'npc', 'g_npcs', self::FLAG_RANDOM_SEARCHABLE | self::FLAG_FILTRABLE | self::FLAG_DB_TYPE],
self::OBJECT => [__NAMESPACE__ . '\GameObjectList', 'object', 'g_objects', self::FLAG_RANDOM_SEARCHABLE | self::FLAG_FILTRABLE | self::FLAG_DB_TYPE],
self::ITEM => [__NAMESPACE__ . '\ItemList', 'item', 'g_items', self::FLAG_RANDOM_SEARCHABLE | self::FLAG_FILTRABLE | self::FLAG_DB_TYPE | self::FLAG_HAS_ICON],
self::ITEMSET => [__NAMESPACE__ . '\ItemsetList', 'itemset', 'g_itemsets', self::FLAG_RANDOM_SEARCHABLE | self::FLAG_FILTRABLE | self::FLAG_DB_TYPE],
self::QUEST => [__NAMESPACE__ . '\QuestList', 'quest', 'g_quests', self::FLAG_RANDOM_SEARCHABLE | self::FLAG_FILTRABLE | self::FLAG_DB_TYPE],
self::SPELL => [__NAMESPACE__ . '\SpellList', 'spell', 'g_spells', self::FLAG_RANDOM_SEARCHABLE | self::FLAG_FILTRABLE | self::FLAG_DB_TYPE | self::FLAG_HAS_ICON],
self::ZONE => [__NAMESPACE__ . '\ZoneList', 'zone', 'g_gatheredzones', self::FLAG_RANDOM_SEARCHABLE | self::FLAG_DB_TYPE],
self::FACTION => [__NAMESPACE__ . '\FactionList', 'faction', 'g_factions', self::FLAG_RANDOM_SEARCHABLE | self::FLAG_DB_TYPE],
self::PET => [__NAMESPACE__ . '\PetList', 'pet', 'g_pets', self::FLAG_RANDOM_SEARCHABLE | self::FLAG_DB_TYPE | self::FLAG_HAS_ICON],
self::ACHIEVEMENT => [__NAMESPACE__ . '\AchievementList', 'achievement', 'g_achievements', self::FLAG_RANDOM_SEARCHABLE | self::FLAG_FILTRABLE | self::FLAG_DB_TYPE | self::FLAG_HAS_ICON],
self::TITLE => [__NAMESPACE__ . '\TitleList', 'title', 'g_titles', self::FLAG_RANDOM_SEARCHABLE | self::FLAG_DB_TYPE],
self::WORLDEVENT => [__NAMESPACE__ . '\WorldEventList', 'event', 'g_holidays', self::FLAG_RANDOM_SEARCHABLE | self::FLAG_DB_TYPE | self::FLAG_HAS_ICON],
self::CHR_CLASS => [__NAMESPACE__ . '\CharClassList', 'class', 'g_classes', self::FLAG_RANDOM_SEARCHABLE | self::FLAG_DB_TYPE],
self::CHR_RACE => [__NAMESPACE__ . '\CharRaceList', 'race', 'g_races', self::FLAG_RANDOM_SEARCHABLE | self::FLAG_DB_TYPE],
self::SKILL => [__NAMESPACE__ . '\SkillList', 'skill', 'g_skills', self::FLAG_RANDOM_SEARCHABLE | self::FLAG_DB_TYPE | self::FLAG_HAS_ICON],
self::STATISTIC => [__NAMESPACE__ . '\AchievementList', 'achievement', 'g_achievements', self::FLAG_NONE], // alias for achievements; exists only for Markup
self::CURRENCY => [__NAMESPACE__ . '\CurrencyList', 'currency', 'g_gatheredcurrencies', self::FLAG_RANDOM_SEARCHABLE | self::FLAG_DB_TYPE | self::FLAG_HAS_ICON],
self::SOUND => [__NAMESPACE__ . '\SoundList', 'sound', 'g_sounds', self::FLAG_RANDOM_SEARCHABLE | self::FLAG_FILTRABLE | self::FLAG_DB_TYPE],
self::ICON => [__NAMESPACE__ . '\IconList', 'icon', 'g_icons', self::FLAG_RANDOM_SEARCHABLE | self::FLAG_FILTRABLE | self::FLAG_DB_TYPE | self::FLAG_HAS_ICON],
self::GUIDE => [__NAMESPACE__ . '\GuideList', 'guide', '', self::FLAG_NONE],
self::PROFILE => [__NAMESPACE__ . '\ProfileList', 'profile', '', self::FLAG_FILTRABLE], // x - not known in javascript
self::GUILD => [__NAMESPACE__ . '\GuildList', 'guild', '', self::FLAG_FILTRABLE], // x
self::ARENA_TEAM => [__NAMESPACE__ . '\ArenaTeamList', 'arena-team', '', self::FLAG_FILTRABLE], // x
self::USER => [__NAMESPACE__ . '\UserList', 'user', 'g_users', self::FLAG_NONE], // x
self::EMOTE => [__NAMESPACE__ . '\EmoteList', 'emote', 'g_emotes', self::FLAG_RANDOM_SEARCHABLE | self::FLAG_DB_TYPE],
self::ENCHANTMENT => [__NAMESPACE__ . '\EnchantmentList', 'enchantment', 'g_enchantments', self::FLAG_RANDOM_SEARCHABLE | self::FLAG_FILTRABLE | self::FLAG_DB_TYPE],
self::AREATRIGGER => [__NAMESPACE__ . '\AreatriggerList', 'areatrigger', '', self::FLAG_FILTRABLE | self::FLAG_DB_TYPE],
self::MAIL => [__NAMESPACE__ . '\MailList', 'mail', '', self::FLAG_RANDOM_SEARCHABLE | self::FLAG_DB_TYPE]
);
/********************/
/* Field Operations */
/********************/
public static function newList(int $type, array $conditions = []) : ?BaseType
{
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 ?# WHERE `id` IN (?a)', 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;
}
}
?>

View File

@@ -1,7 +1,5 @@
<?php <?php
namespace Aowow;
if (!defined('AOWOW_REVISION')) if (!defined('AOWOW_REVISION'))
die('illegal access'); die('illegal access');
@@ -10,24 +8,23 @@ class AchievementList extends BaseType
{ {
use listviewHelper; use listviewHelper;
public static $type = Type::ACHIEVEMENT; public static $type = TYPE_ACHIEVEMENT;
public static $brickFile = 'achievement'; public static $brickFile = 'achievement';
public static $dataTable = '?_achievement';
public $criteria = []; public $criteria = [];
protected $queryBase = 'SELECT `a`.*, `a`.`id` AS ARRAY_KEY FROM ?_achievement a'; protected $queryBase = 'SELECT `a`.*, `a`.`id` AS ARRAY_KEY FROM ?_achievement a';
protected $queryOpts = array( protected $queryOpts = array(
'a' => [['ic'], 'o' => 'orderInGroup ASC'], 'a' => [['si'], 'o' => 'orderInGroup ASC'],
'ic' => ['j' => ['?_icons ic ON ic.id = a.iconId', true], 's' => ', ic.name AS iconString'], '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`'] '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 todo: evaluate TC custom-data-tables: a*_criteria_data should be merged on installation
*/ */
public function __construct(array $conditions = [], array $miscData = []) public function __construct($conditions = [], $miscData = null)
{ {
parent::__construct($conditions, $miscData); parent::__construct($conditions, $miscData);
@@ -36,24 +33,7 @@ class AchievementList extends BaseType
// post processing // post processing
$rewards = DB::World()->select(' $rewards = DB::World()->select('
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)',
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 (?a)',
$this->getFoundIDs() $this->getFoundIDs()
); );
@@ -65,33 +45,31 @@ class AchievementList extends BaseType
{ {
$_curTpl = array_merge($rewards[$_id], $_curTpl); $_curTpl = array_merge($rewards[$_id], $_curTpl);
$_curTpl['mailTemplate'] = $rewards[$_id]['MailTemplateID']; if ($rewards[$_id]['mailTemplate'])
if ($rewards[$_id]['MailTemplateID'])
{ {
// using class Loot creates an inifinite loop cirling between Loot, ItemList and SpellList or something // using class Loot creates an inifinite loop cirling between Loot, ItemList and SpellList or something
// $mailSrc = new Loot(); // $mailSrc = new Loot();
// $mailSrc->getByContainer(LOOT_MAIL, $rewards[$_id]['MailTemplateID']); // $mailSrc->getByContainer(LOOT_MAIL, $rewards[$_id]['mailTemplate']);
// foreach ($mailSrc->iterate() as $loot) // foreach ($mailSrc->iterate() as $loot)
// $_curTpl['rewards'][] = [Type::ITEM, $loot['id']]; // $_curTpl['rewards'][] = [TYPE_ITEM, $loot['id']];
// lets just assume for now, that mailRewards for achievements do not contain references // lets just assume for now, that mailRewards for achievements do not contain references
$mailRew = DB::World()->selectCol('SELECT Item FROM mail_loot_template WHERE Reference <= 0 AND entry = ?d', $rewards[$_id]['MailTemplateID']); $mailRew = DB::World()->selectCol('SELECT Item FROM mail_loot_template WHERE Reference <= 0 AND entry = ?d', $rewards[$_id]['mailTemplate']);
foreach ($mailRew AS $mr) foreach ($mailRew AS $mr)
$_curTpl['rewards'][] = [Type::ITEM, $mr]; $_curTpl['rewards'][] = [TYPE_ITEM, $mr];
} }
} }
//"rewards":[[11,137],[3,138]] [type, typeId] //"rewards":[[11,137],[3,138]] [type, typeId]
if (!empty($_curTpl['ItemID'])) if (!empty($_curTpl['item']))
$_curTpl['rewards'][] = [Type::ITEM, $_curTpl['ItemID']]; $_curTpl['rewards'][] = [TYPE_ITEM, $_curTpl['item']];
if (!empty($_curTpl['itemExtra'])) if (!empty($_curTpl['itemExtra']))
$_curTpl['rewards'][] = [Type::ITEM, $_curTpl['itemExtra']]; $_curTpl['rewards'][] = [TYPE_ITEM, $_curTpl['itemExtra']];
if (!empty($_curTpl['TitleA'])) if (!empty($_curTpl['title_A']))
$_curTpl['rewards'][] = [Type::TITLE, $_curTpl['TitleA']]; $_curTpl['rewards'][] = [TYPE_TITLE, $_curTpl['title_A']];
if (!empty($_curTpl['TitleH'])) if (!empty($_curTpl['title_H']))
if (empty($_curTpl['TitleA']) || $_curTpl['TitleA'] != $_curTpl['TitleH']) if (empty($_curTpl['title_A']) || $_curTpl['title_A'] != $_curTpl['title_H'])
$_curTpl['rewards'][] = [Type::TITLE, $_curTpl['TitleH']]; $_curTpl['rewards'][] = [TYPE_TITLE, $_curTpl['title_H']];
// icon // icon
$_curTpl['iconString'] = $_curTpl['iconString'] ?: 'trade_engineering'; $_curTpl['iconString'] = $_curTpl['iconString'] ?: 'trade_engineering';
@@ -105,7 +83,7 @@ class AchievementList extends BaseType
foreach ($this->iterate() as $__) foreach ($this->iterate() as $__)
{ {
if ($addMask & GLOBALINFO_SELF) if ($addMask & GLOBALINFO_SELF)
$data[Type::ACHIEVEMENT][$this->id] = ['icon' => $this->curTpl['iconString'], 'name' => $this->getField('name', true)]; $data[TYPE_ACHIEVEMENT][$this->id] = ['icon' => $this->curTpl['iconString'], 'name' => $this->getField('name', true)];
if ($addMask & GLOBALINFO_REWARDS) if ($addMask & GLOBALINFO_REWARDS)
foreach ($this->curTpl['rewards'] as $_) foreach ($this->curTpl['rewards'] as $_)
@@ -153,7 +131,7 @@ class AchievementList extends BaseType
if (isset($this->criteria[$this->id])) if (isset($this->criteria[$this->id]))
return $this->criteria[$this->id]; return $this->criteria[$this->id];
$result = DB::Aowow()->Select('SELECT * FROM ?_achievementcriteria WHERE `refAchievementId` = ?d ORDER BY `order` ASC', $this->curTpl['refAchievement'] ?: $this->id); $result = DB::Aowow()->Select('SELECT * FROM ?_achievementcriteria WHERE `refAchievementId` = ?d ORDER BY `order` ASC', $this->id);
if (!$result) if (!$result)
return []; return [];
@@ -228,25 +206,23 @@ class AchievementList extends BaseType
break; break;
} }
$criteria .= '<!--cr'.$crt['id'].':'.$crt['type'].':'.$crt['value1'].'-->- '.$crtName;
if ($crt['completionFlags'] & ACHIEVEMENT_CRITERIA_FLAG_MONEY_COUNTER) if ($crt['completionFlags'] & ACHIEVEMENT_CRITERIA_FLAG_MONEY_COUNTER)
$criteria .= '&nbsp;<span class="moneygold">'.Lang::nf($crt['value2' ] / 10000).'</span>'; $criteria .= '- '.Util::jsEscape($crtName).' <span class="moneygold">'.number_format($crt['value2' ] / 10000).'</span><br />';
else
$criteria .= '<br />'; $criteria .= '- '.Util::jsEscape($crtName).'<br />';
if (++$i == round(count($rows)/2)) if (++$i == round(count($rows)/2))
$criteria .= '</small></td><th class="q0" style="white-space: nowrap; text-align: left"><small>'; $criteria .= '</small></td><th class="q0" style="white-space: nowrap; text-align: left"><small>';
} }
$x = '<table><tr><td><b class="q">'; $x = '<table><tr><td><b class="q">';
$x .= $name; $x .= Util::jsEscape($name);
$x .= '</b></td></tr></table>'; $x .= '</b></td></tr></table>';
if ($description || $criteria) if ($description || $criteria)
$x .= '<table><tr><td>'; $x .= '<table><tr><td>';
if ($description) if ($description)
$x .= '<br />'.$description.'<br />'; $x .= '<br />'.Util::jsEscape($description).'<br />';
if ($criteria) if ($criteria)
{ {
@@ -259,19 +235,16 @@ class AchievementList extends BaseType
return $x; return $x;
} }
public function getSourceData(int $id = 0) : array public function getSourceData()
{ {
$data = []; $data = [];
foreach ($this->iterate() as $__) foreach ($this->iterate() as $__)
{ {
if ($id && $id != $this->id)
continue;
$data[$this->id] = array( $data[$this->id] = array(
"n" => $this->getField('name', true), "n" => $this->getField('name', true),
"s" => $this->curTpl['faction'], "s" => $this->curTpl['faction'],
"t" => Type::ACHIEVEMENT, "t" => TYPE_ACHIEVEMENT,
"ti" => $this->id "ti" => $this->id
); );
} }
@@ -283,12 +256,11 @@ class AchievementList extends BaseType
class AchievementListFilter extends Filter class AchievementListFilter extends Filter
{ {
protected string $type = 'achievements';
protected array $enums = array( protected $enums = array(
4 => parent::ENUM_ZONE, // location
11 => array( 11 => array(
327 => 160, // Lunar Festival 327 => 160, // Lunar Festival
423 => 187, // Love is in the Air 335 => 187, // Love is in the Air
181 => 159, // Noblegarden 181 => 159, // Noblegarden
201 => 163, // Children's Week 201 => 163, // Children's Week
341 => 161, // Midsummer Fire Festival 341 => 161, // Midsummer Fire Festival
@@ -298,110 +270,134 @@ class AchievementListFilter extends Filter
141 => 156, // Feast of Winter Veil 141 => 156, // Feast of Winter Veil
409 => -3456, // Day of the Dead 409 => -3456, // Day of the Dead
398 => -3457, // Pirates' Day 398 => -3457, // Pirates' Day
parent::ENUM_ANY => true, FILTER_ENUM_ANY => true,
parent::ENUM_NONE => false, FILTER_ENUM_NONE => false,
283 => -1, // valid events without achievements 283 => -1, // valid events without achievements
285 => -1, 353 => -1, 420 => -1, 285 => -1, 353 => -1, 420 => -1,
400 => -1, 284 => -1, 374 => -1, 400 => -1, 284 => -1, 374 => -1,
321 => -1, 424 => -1, 301 => -1 321 => -1, 424 => -1, 301 => -1
) )
); );
protected $genericFilter = array( // misc (bool): _NUMERIC => useFloat; _STRING => localized; _FLAG => match Value; _BOOLEAN => stringSet
protected array $genericFilter = array( 2 => [FILTER_CR_BOOLEAN, 'reward_loc0', true ], // givesreward
2 => [parent::CR_BOOLEAN, 'reward_loc0', true ], // givesreward 3 => [FILTER_CR_STRING, 'reward', true ], // rewardtext
3 => [parent::CR_STRING, 'reward', STR_LOCALIZED ], // rewardtext 7 => [FILTER_CR_BOOLEAN, 'chainId', ], // partseries
4 => [parent::CR_NYI_PH, null, 1, ], // location [enum] 9 => [FILTER_CR_NUMERIC, 'id', null, true], // id
5 => [parent::CR_CALLBACK, 'cbSeries', ACHIEVEMENT_CU_FIRST_SERIES, null], // first in series [yn] 10 => [FILTER_CR_STRING, 'si.iconString', ], // icon
6 => [parent::CR_CALLBACK, 'cbSeries', ACHIEVEMENT_CU_LAST_SERIES, null], // last in series [yn] 18 => [FILTER_CR_STAFFFLAG, 'flags', ], // flags
7 => [parent::CR_BOOLEAN, 'chainId', ], // partseries 14 => [FILTER_CR_FLAG, 'cuFlags', CUSTOM_HAS_COMMENT ], // hascomments
9 => [parent::CR_NUMERIC, 'id', NUM_CAST_INT, true], // id 15 => [FILTER_CR_FLAG, 'cuFlags', CUSTOM_HAS_SCREENSHOT ], // hasscreenshots
10 => [parent::CR_STRING, 'ic.name', ], // icon 16 => [FILTER_CR_FLAG, 'cuFlags', CUSTOM_HAS_VIDEO ], // hasvideos
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 array $inputFields = array( protected function createSQLForCriterium(&$cr)
'cr' => [parent::V_RANGE, [2, 18], true ], // criteria ids {
'crs' => [parent::V_LIST, [parent::ENUM_NONE, parent::ENUM_ANY, [0, 99999]], true ], // criteria operators if (in_array($cr[0], array_keys($this->genericFilter)))
'crv' => [parent::V_REGEX, parent::PATTERN_CRV, true ], // criteria values - only printable chars, no delimiters {
'na' => [parent::V_REGEX, parent::PATTERN_NAME, false], // name / description - only printable chars, no delimiter if ($genCr = $this->genericCriterion($cr))
'ex' => [parent::V_EQUAL, 'on', false], // extended name search return $genCr;
'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 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 = []; $parts = [];
$_v = &$this->values; $_v = &$this->fiData['v'];
// name ex: +description, +rewards // name ex: +description, +rewards
if ($_v['na']) if (isset($_v['na']))
{ {
$_ = []; $_ = [];
if ($_v['ex'] == 'on') if (isset($_v['ex']) && $_v['ex'] == 'on')
$_ = $this->tokenizeString(['name_loc'.Lang::getLocale()->value, 'reward_loc'.Lang::getLocale()->value, 'description_loc'.Lang::getLocale()->value]); $_ = $this->modularizeString(['name_loc'.User::$localeId, 'reward_loc'.User::$localeId, 'description_loc'.User::$localeId]);
else else
$_ = $this->tokenizeString(['name_loc'.Lang::getLocale()->value]); $_ = $this->modularizeString(['name_loc'.User::$localeId]);
if ($_) if ($_)
$parts[] = $_; $parts[] = $_;
} }
// points min // points min
if ($_v['minpt']) if (isset($_v['minpt']))
$parts[] = ['points', $_v['minpt'], '>=']; {
if ($this->isSaneNumeric($_v['minpt']))
$parts[] = ['points', $_v['minpt'], '>='];
else
unset($_v['minpt']);
}
// points max // points max
if ($_v['maxpt']) if (isset($_v['maxpt']))
$parts[] = ['points', $_v['maxpt'], '<=']; {
if ($this->isSaneNumeric($_v['maxpt']))
$parts[] = ['points', $_v['maxpt'], '<='];
else
unset($_v['maxpt']);
}
// faction (side) // faction (side)
if ($_v['si']) if (isset($_v['si']))
{ {
$parts[] = match ($_v['si']) switch ($_v['si'])
{ {
-SIDE_ALLIANCE, // equals faction case 3: // both
-SIDE_HORDE => ['faction', -$_v['si']], $parts[] = ['faction', 0];
SIDE_ALLIANCE, // includes faction break;
SIDE_HORDE, case -1: // faction, exclusive both
SIDE_BOTH => ['faction', $_v['si'], '&'] 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; return $parts;
} }
protected function cbRelEvent(int $cr, int $crs, string $crv) : ?array
{
if (!isset($this->enums[$cr][$crs]))
return null;
$_ = $this->enums[$cr][$crs];
if (is_int($_))
return ($_ > 0) ? ['category', $_] : ['id', abs($_)];
else
{
$ids = array_filter($this->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 ? ['AND', ['chainId', 0, '!'], ['cuFlags', $seriesFlag, '&']] : ['AND', ['chainId', 0, '!'], [['cuFlags', $seriesFlag, '&'], 0]];
return null;
}
} }
?> ?>

View File

@@ -1,95 +0,0 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
class AreaTriggerList extends BaseType
{
use spawnHelper;
public static $type = Type::AREATRIGGER;
public static $brickFile = 'areatrigger';
public static $dataTable = '?_areatrigger';
public static $contribute = CONTRIBUTE_CO;
protected $queryBase = 'SELECT a.*, a.id AS ARRAY_KEY FROM ?_areatrigger a';
protected $queryOpts = array(
'a' => [['s']], // guid < 0 are teleporter targets, so exclude them here
's' => ['j' => ['?_spawns s ON s.`type` = 503 AND s.`typeId` = a.`id` AND s.`guid` > 0', true], 's' => ', GROUP_CONCAT(s.`areaId`) AS "areaId"', 'g' => 'a.`id`']
);
public function __construct(array $conditions = [], array $miscData = [])
{
parent::__construct($conditions, $miscData);
foreach ($this->iterate() as $id => &$_curTpl)
if (!$_curTpl['name'])
$_curTpl['name'] = 'Unnamed Areatrigger #' . $id;
}
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($addMask = GLOBALINFO_ANY)
{
return [];
}
public function renderTooltip() { }
}
class AreaTriggerListFilter extends Filter
{
protected string $type = 'areatrigger';
protected array $genericFilter = array(
2 => [parent::CR_NUMERIC, 'id', NUM_CAST_INT] // id
);
// fieldId => [checkType, checkValue[, fieldIsArray]]
protected 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_REGEX, parent::PATTERN_NAME, 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->tokenizeString(['name']))
$parts[] = $_;
// type [list]
if ($_v['ty'])
$parts[] = ['type', $_v['ty']];
return $parts;
}
}
?>

View File

@@ -1,383 +0,0 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
class ArenaTeamList extends BaseType
{
use profilerHelper, listviewHelper;
private $rankOrder = [];
public static $contribute = CONTRIBUTE_NONE;
public function getListviewData()
{
$data = [];
foreach ($this->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 array_values($data);
}
public function renderTooltip() {}
public function getJSGlobals($addMask = 0) {}
}
class ArenaTeamListFilter extends Filter
{
use TrProfilerFilter;
protected string $type = 'arenateams';
protected array $genericFilter = [];
protected array $inputFields = array(
'na' => [parent::V_REGEX, parent::PATTERN_NAME, false], // name - only printable chars, no delimiter
'ma' => [parent::V_EQUAL, 1, false], // match any / all filter
'ex' => [parent::V_EQUAL, 'on', false], // only match exact
'si' => [parent::V_LIST, [1, 2], false], // side
'sz' => [parent::V_LIST, [2, 3, 5], false], // tema size
'rg' => [parent::V_CALLBACK, 'cbRegionCheck', false], // region
'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->tokenizeString(['at.name'], $_v['na'], $_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 $queryBase = 'SELECT `at`.*, `at`.`arenaTeamId` AS ARRAY_KEY FROM arena_team at';
protected $queryOpts = array(
'at' => [['atm', 'c'], 'g' => 'ARRAY_KEY', 'o' => 'rating DESC'],
'atm' => ['j' => 'arena_team_member atm ON atm.arenaTeamId = at.arenaTeamId'],
'c' => ['j' => 'characters c ON c.guid = atm.guid AND c.deleteInfos_Account IS NULL AND c.level <= 80 AND (c.extra_flags & '.Profiler::CHAR_GMFLAGS.') = 0', 's' => ', BIT_OR(IF(c.race IN (1, 3, 4, 7, 11), 1, 2)) - 1 AS faction']
);
private $members = [];
private $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` = ?d ORDER BY rating DESC', $type);
reset($this->dbNames); // only use when querying single realm
$realmId = key($this->dbNames);
$realms = Profiler::getRealms();
$distrib = [];
// post processing
foreach ($this->iterate() as $guid => &$curTpl)
{
// battlegroup
$curTpl['battlegroup'] = Cfg::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 nonexistant 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)->select('
SELECT
at.arenaTeamId AS ARRAY_KEY, c.guid AS ARRAY_KEY2, c.name AS "0", c.class AS "1", IF(at.captainguid = c.guid, 1, 0) AS "2"
FROM
arena_team at
JOIN
arena_team_member atm ON atm.arenaTeamId = at.arenaTeamId JOIN characters c ON c.guid = atm.guid
WHERE
at.arenaTeamId IN (?a) AND
c.deleteInfos_Account IS NULL AND
c.level <= ?d AND
(c.extra_flags & ?d) = 0',
$teams,
MAX_LEVEL,
Profiler::CHAR_GMFLAGS
);
// equalize subject distribution across realms
foreach ($conditions as $c)
if (is_int($c))
$limit = $c;
$limit ??= Cfg::get('SQL_LIMIT_DEFAULT');
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], Cfg::get('SQL_LIMIT_NONE')), ['sv' => $realmId]);
if (!$profiles[$realmId]->error)
$profiles[$realmId]->initializeLocalEntries();
}
$data = [];
foreach ($this->iterate() as $guid => $__)
{
$data[$guid] = array(
'realm' => $this->getField('realm'),
'realmGUID' => $this->getField('arenaTeamId'),
'name' => $this->getField('name'),
'nameUrl' => Profiler::urlize($this->getField('name')),
'type' => $this->getField('type'),
'rating' => $this->getField('rating'),
'cuFlags' => PROFILER_CU_NEEDS_RESYNC
);
}
// basic arena team data
foreach (Util::createSqlBatchInsert($data) as $ins)
DB::Aowow()->query('INSERT INTO ?_profiler_arena_team (?#) VALUES '.$ins.' ON DUPLICATE KEY UPDATE `id` = `id`', array_keys(reset($data)));
// merge back local ids
$localIds = DB::Aowow()->selectCol(
'SELECT CONCAT(realm, ":", realmGUID) AS ARRAY_KEY, id FROM ?_profiler_arena_team WHERE realm IN (?a) AND realmGUID IN (?a)',
array_column($data, 'realm'),
array_column($data, 'realmGUID')
);
foreach ($this->iterate() as $guid => &$_curTpl)
if (isset($localIds[$guid]))
$_curTpl['id'] = $localIds[$guid];
// profiler_arena_team_member requires profiles and arena teams to be filled
foreach ($this->members as $realmId => $teams)
{
if (empty($profiles[$realmId]))
continue;
$memberData = [];
foreach ($teams as $teamId => $team)
{
$clearMembers = [];
foreach ($team as $memberId => $member)
{
$clearMembers[] = $profiles[$realmId]->getEntry($realmId.':'.$memberId)['id'];
$memberData[] = array(
'arenaTeamId' => $localIds[$realmId.':'.$teamId],
'profileId' => $profiles[$realmId]->getEntry($realmId.':'.$memberId)['id'],
'captain' => $member[2]
);
}
// Delete members from other teams of the same type
DB::Aowow()->query(
'DELETE atm
FROM ?_profiler_arena_team_member atm
JOIN ?_profiler_arena_team at ON atm.`arenaTeamId` = at.`id` AND at.`type` = ?d
WHERE atm.`profileId` IN (?a)',
$data[$realmId.':'.$teamId]['type'] ?? 0,
$clearMembers
);
}
foreach (Util::createSqlBatchInsert($memberData) as $ins)
DB::Aowow()->query('INSERT INTO ?_profiler_arena_team_member (?#) VALUES '.$ins.' ON DUPLICATE KEY UPDATE `profileId` = `profileId`', array_keys(reset($memberData)));
}
}
}
class LocalArenaTeamList extends ArenaTeamList
{
protected $queryBase = 'SELECT at.*, at.id AS ARRAY_KEY FROM ?_profiler_arena_team at';
protected $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, 'AND');
$conditions = ['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()->select(
'SELECT `arenaTeamId` AS ARRAY_KEY, p.`id` AS ARRAY_KEY2, p.`name` AS "0", p.`class` AS "1", atm.`captain` AS "2"
FROM ?_profiler_arena_team_member atm
JOIN ?_profiler_profiles p ON p.`id` = atm.`profileId`
WHERE `arenaTeamId` IN (?a)',
$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()
{
$url = '?arena-team=';
return $url.implode('.', array(
Profiler::urlize($this->getField('region')),
Profiler::urlize($this->getField('realmName')),
Profiler::urlize($this->getField('name'))
));
}
}
?>

File diff suppressed because it is too large Load Diff

View File

@@ -1,22 +1,19 @@
<?php <?php
namespace Aowow;
if (!defined('AOWOW_REVISION')) if (!defined('AOWOW_REVISION'))
die('illegal access'); die('illegal access');
class CharClassList extends BaseType class CharClassList extends BaseType
{ {
public static $type = Type::CHR_CLASS; public static $type = TYPE_CLASS;
public static $brickFile = 'class'; public static $brickFile = 'class';
public static $dataTable = '?_classes';
protected $queryBase = 'SELECT c.*, id AS ARRAY_KEY FROM ?_classes c'; 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) foreach ($this->iterate() as $k => &$_curTpl)
$_curTpl['skills'] = explode(' ', $_curTpl['skills']); $_curTpl['skills'] = explode(' ', $_curTpl['skills']);
@@ -58,6 +55,7 @@ class CharClassList extends BaseType
return $data; return $data;
} }
public function addRewardsToJScript(&$ref) { }
public function renderTooltip() { } public function renderTooltip() { }
} }

View File

@@ -1,18 +1,15 @@
<?php <?php
namespace Aowow;
if (!defined('AOWOW_REVISION')) if (!defined('AOWOW_REVISION'))
die('illegal access'); die('illegal access');
class CharRaceList extends BaseType class CharRaceList extends BaseType
{ {
public static $type = Type::CHR_RACE; public static $type = TYPE_RACE;
public static $brickFile = 'race'; public static $brickFile = 'race';
public static $dataTable = '?_races';
protected $queryBase = 'SELECT r.*, id AS ARRAY_KEY FROM ?_races r'; protected $queryBase = 'SELECT *, id AS ARRAY_KEY FROM ?_races r';
public function getListviewData() public function getListviewData()
{ {
@@ -42,11 +39,12 @@ class CharRaceList extends BaseType
$data = []; $data = [];
foreach ($this->iterate() as $__) foreach ($this->iterate() as $__)
$data[Type::CHR_RACE][$this->id] = ['name' => $this->getField('name', true)]; $data[TYPE_RACE][$this->id] = ['name' => $this->getField('name', true)];
return $data; return $data;
} }
public function addRewardsToJScript(&$ref) { }
public function renderTooltip() { } public function renderTooltip() { }
} }

View File

@@ -1,7 +1,5 @@
<?php <?php
namespace Aowow;
if (!defined('AOWOW_REVISION')) if (!defined('AOWOW_REVISION'))
die('illegal access'); die('illegal access');
@@ -10,13 +8,12 @@ class CreatureList extends BaseType
{ {
use spawnHelper; use spawnHelper;
public static $type = Type::NPC; public static $type = TYPE_NPC;
public static $brickFile = 'npc'; public static $brickFile = 'creature';
public static $dataTable = '?_creature';
protected $queryBase = 'SELECT ct.*, ct.id AS ARRAY_KEY FROM ?_creature ct'; protected $queryBase = 'SELECT ct.*, ct.id AS ARRAY_KEY FROM ?_creature ct';
public $queryOpts = array( public $queryOpts = array(
'ct' => [['ft', 'qse', 'dct1', 'dct2', 'dct3'], 's' => ', IFNULL(dct1.id, IFNULL(dct2.id, IFNULL(dct3.id, 0))) AS parentId, IFNULL(dct1.name_loc0, IFNULL(dct2.name_loc0, IFNULL(dct3.name_loc0, ""))) AS parent_loc0, IFNULL(dct1.name_loc2, IFNULL(dct2.name_loc2, IFNULL(dct3.name_loc2, ""))) AS parent_loc2, IFNULL(dct1.name_loc3, IFNULL(dct2.name_loc3, IFNULL(dct3.name_loc3, ""))) AS parent_loc3, IFNULL(dct1.name_loc4, IFNULL(dct2.name_loc4, IFNULL(dct3.name_loc4, ""))) AS parent_loc4, IFNULL(dct1.name_loc6, IFNULL(dct2.name_loc6, IFNULL(dct3.name_loc6, ""))) AS parent_loc6, IFNULL(dct1.name_loc8, IFNULL(dct2.name_loc8, IFNULL(dct3.name_loc8, ""))) AS parent_loc8, IF(dct1.difficultyEntry1 = ct.id, 1, IF(dct2.difficultyEntry2 = ct.id, 2, IF(dct3.difficultyEntry3 = ct.id, 3, 0))) AS difficultyMode'], 'ct' => [['ft', 'qse', 'dct1', 'dct2', 'dct3'], 's' => ', IFNULL(dct1.id, IFNULL(dct2.id, IFNULL(dct3.id, 0))) AS parentId, IFNULL(dct1.name_loc0, IFNULL(dct2.name_loc0, IFNULL(dct3.name_loc0, ""))) AS parent_loc0, IFNULL(dct1.name_loc2, IFNULL(dct2.name_loc2, IFNULL(dct3.name_loc2, ""))) AS parent_loc2, IFNULL(dct1.name_loc3, IFNULL(dct2.name_loc3, IFNULL(dct3.name_loc3, ""))) AS parent_loc3, IFNULL(dct1.name_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]], '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]], '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]], 'dct3' => ['j' => ['?_creature dct3 ON ct.cuFlags & 0x02 AND dct3.difficultyEntry3 = ct.id', true]],
@@ -26,7 +23,7 @@ class CreatureList extends BaseType
's' => ['j' => ['?_spawns s ON s.type = 1 AND s.typeId = ct.id', true]] 's' => ['j' => ['?_spawns s ON s.type = 1 AND s.typeId = ct.id', true]]
); );
public function __construct(array $conditions = [], array $miscData = []) public function __construct($conditions = [], $miscData = null)
{ {
parent::__construct($conditions, $miscData); parent::__construct($conditions, $miscData);
@@ -51,7 +48,7 @@ class CreatureList extends BaseType
public static function getName($id) public static function getName($id)
{ {
$n = DB::Aowow()->SelectRow('SELECT name_loc0, name_loc2, name_loc3, name_loc4, name_loc6, name_loc8 FROM ?_creature WHERE id = ?d', $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'); return Util::localizedString($n, 'name');
} }
@@ -79,14 +76,13 @@ class CreatureList extends BaseType
if ($type) if ($type)
$row3[] = Lang::game('ct', $type); $row3[] = Lang::game('ct', $type);
if ($_ = Lang::npc('rank', $this->curTpl['rank'])) $row3[] = '('.Lang::npc('rank', $this->curTpl['rank']).')';
$row3[] = '('.$_.')';
$x = '<table>'; $x = '<table>';
$x .= '<tr><td><b class="q">'.Util::htmlEscape($this->getField('name', true)).'</b></td></tr>'; $x .= '<tr><td><b class="q">'.$this->getField('name', true).'</b></td></tr>';
if ($sn = $this->getField('subname', true)) if ($sn = $this->getField('subname', true))
$x .= '<tr><td>'.Util::htmlEscape($sn).'</td></tr>'; $x .= '<tr><td>'.$sn.'</td></tr>';
$x .= '<tr><td>'.implode(' ', $row3).'</td></tr>'; $x .= '<tr><td>'.implode(' ', $row3).'</td></tr>';
@@ -104,22 +100,27 @@ class CreatureList extends BaseType
public function getRandomModelId() public function getRandomModelId()
{ {
// dwarf?? [null, 30754, 30753, 30755, 30736]
// totems use hardcoded models, tauren model is base // totems use hardcoded models, tauren model is base
$totems = [null, 4589, 4588, 4587, 4590]; // slot => modelId $totems = array( // tauren => [orc, dwarf(?!), troll, tauren, draenei]
$data = []; 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++) for ($i = 1; $i < 5; $i++)
if ($_ = $this->curTpl['displayId'.$i]) if ($_ = $this->curTpl['displayId'.$i])
$data[] = $_; $data[] = $_;
if (count($data) == 1 && ($slotId = array_search($data[0], $totems))) if (count($data) == 1 && in_array($data[0], array_keys($totems)))
$data = DB::World()->selectCol('SELECT DisplayId FROM player_totem_model WHERE TotemSlot = ?d', $slotId); $data = $totems[$data[0]];
return !$data ? 0 : $data[array_rand($data)]; return !$data ? 0 : $data[array_rand($data)];
} }
public function getBaseStats(string $type) : array public function getBaseStats($type)
{ {
// i'm aware of the BaseVariance/RangedVariance fields ... i'm just totaly unsure about the whole damage calculation // i'm aware of the BaseVariance/RangedVariance fields ... i'm just totaly unsure about the whole damage calculation
switch ($type) switch ($type)
@@ -144,14 +145,8 @@ class CreatureList extends BaseType
$rngMin = ($this->getField('dmgMin') + ($this->getField('rngAtkPwrMin') / 14)) * $this->getField('dmgMultiplier') * $this->getField('rngAtkSpeed'); $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'); $rngMax = ($this->getField('dmgMax') * 1.5 + ($this->getField('rngAtkPwrMax') / 14)) * $this->getField('dmgMultiplier') * $this->getField('rngAtkSpeed');
return [$rngMin, $rngMax]; 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: default:
return []; return [0, 0];
} }
} }
@@ -166,22 +161,9 @@ class CreatureList extends BaseType
* *
* NPCINFO_TAMEABLE (0x1): include texture & react * NPCINFO_TAMEABLE (0x1): include texture & react
* NPCINFO_MODEL (0x2): * NPCINFO_MODEL (0x2):
* NPCINFO_REP (0x4): include repreward
*/ */
$data = []; $data = [];
$rewRep = [];
if ($addInfoMask & NPCINFO_REP && $this->getFoundIDs())
{
$rewRep = DB::World()->selectCol('
SELECT creature_id AS ARRAY_KEY, RewOnKillRepFaction1 AS ARRAY_KEY2, RewOnKillRepValue1 FROM creature_onkill_reputation WHERE creature_id IN (?a) AND RewOnKillRepFaction1 > 0 UNION
SELECT creature_id AS ARRAY_KEY, RewOnKillRepFaction2 AS ARRAY_KEY2, RewOnKillRepValue2 FROM creature_onkill_reputation WHERE creature_id IN (?a) AND RewOnKillRepFaction2 > 0',
$this->getFoundIDs(),
$this->getFoundIDs()
);
}
foreach ($this->iterate() as $__) foreach ($this->iterate() as $__)
{ {
@@ -225,7 +207,6 @@ class CreatureList extends BaseType
'react' => [$this->curTpl['A'], $this->curTpl['H']], 'react' => [$this->curTpl['A'], $this->curTpl['H']],
); );
if ($this->getField('startsQuests')) if ($this->getField('startsQuests'))
$data[$this->id]['hasQuests'] = 1; $data[$this->id]['hasQuests'] = 1;
@@ -234,14 +215,6 @@ class CreatureList extends BaseType
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 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']; $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];
}
} }
} }
@@ -254,291 +227,353 @@ class CreatureList extends BaseType
$data = []; $data = [];
foreach ($this->iterate() as $__) foreach ($this->iterate() as $__)
$data[Type::NPC][$this->id] = ['name' => $this->getField('name', true)]; $data[TYPE_NPC][$this->id] = ['name' => $this->getField('name', true)];
return $data; return $data;
} }
public function getSourceData(int $id = 0) : array public function getSourceData()
{ {
$data = []; $data = [];
foreach ($this->iterate() as $__) foreach ($this->iterate() as $__)
{ {
if ($id && $id != $this->id)
continue;
$data[$this->id] = array( $data[$this->id] = array(
'n' => $this->getField('parentId') ? $this->getField('parent', true) : $this->getField('name', true), 'n' => $this->getField('parentId') ? $this->getField('parent', true) : $this->getField('name', true),
't' => Type::NPC, 't' => TYPE_NPC,
'ti' => $this->getField('parentId') ?: $this->id '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; return $data;
} }
public function addRewardsToJScript(&$refs) { }
} }
class CreatureListFilter extends Filter class CreatureListFilter extends Filter
{ {
protected string $type = 'npcs'; public $extraOpts = null;
protected array $enums = array( protected $enums = array(
3 => parent::ENUM_FACTION, // faction 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,
6 => parent::ENUM_ZONE, // foundin 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,
42 => parent::ENUM_FACTION, // increasesrepwith 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,
43 => parent::ENUM_FACTION, // decreasesrepwith 589, 270)
38 => parent::ENUM_EVENT // relatedevent
); );
protected array $genericFilter = array( // cr => [type, field, misc, extraCol]
1 => [parent::CR_CALLBACK, 'cbHealthMana', 'healthMax', 'healthMin'], // health [num] protected $genericFilter = array( // misc (bool): _NUMERIC => useFloat; _STRING => localized; _FLAG => match Value; _BOOLEAN => stringSet
2 => [parent::CR_CALLBACK, 'cbHealthMana', 'manaMin', 'manaMax' ], // mana [num] 5 => [FILTER_CR_FLAG, 'npcflag', NPC_FLAG_REPAIRER ], // canrepair
3 => [parent::CR_CALLBACK, 'cbFaction', null, null ], // faction [enum] 6 => [FILTER_CR_ENUM, 's.areaId', null ], // foundin
5 => [parent::CR_FLAG, 'npcflag', NPC_FLAG_REPAIRER ], // canrepair 9 => [FILTER_CR_BOOLEAN, 'lootId', ], // lootable
6 => [parent::CR_ENUM, 's.areaId', false, true ], // foundin 11 => [FILTER_CR_BOOLEAN, 'pickpocketLootId', ], // pickpocketable
7 => [parent::CR_CALLBACK, 'cbQuestRelation', 'startsQuests', 0x1 ], // startsquest [enum] 18 => [FILTER_CR_FLAG, 'npcflag', NPC_FLAG_AUCTIONEER ], // auctioneer
8 => [parent::CR_CALLBACK, 'cbQuestRelation', 'endsQuests', 0x2 ], // endsquest [enum] 19 => [FILTER_CR_FLAG, 'npcflag', NPC_FLAG_BANKER ], // banker
9 => [parent::CR_BOOLEAN, 'lootId', ], // lootable 20 => [FILTER_CR_FLAG, 'npcflag', NPC_FLAG_BATTLEMASTER ], // battlemaster
10 => [parent::CR_CALLBACK, 'cbRegularSkinLoot', NPC_TYPEFLAG_SPECIALLOOT ], // skinnable [yn] 21 => [FILTER_CR_FLAG, 'npcflag', NPC_FLAG_FLIGHT_MASTER ], // flightmaster
11 => [parent::CR_BOOLEAN, 'pickpocketLootId', ], // pickpocketable 22 => [FILTER_CR_FLAG, 'npcflag', NPC_FLAG_GUILD_MASTER ], // guildmaster
12 => [parent::CR_CALLBACK, 'cbMoneyDrop', null, null ], // averagemoneydropped [op] [int] 23 => [FILTER_CR_FLAG, 'npcflag', NPC_FLAG_INNKEEPER ], // innkeeper
15 => [parent::CR_CALLBACK, 'cbSpecialSkinLoot', NPC_TYPEFLAG_SKIN_WITH_HERBALISM, null ], // gatherable [yn] 24 => [FILTER_CR_FLAG, 'npcflag', NPC_FLAG_CLASS_TRAINER ], // talentunlearner
16 => [parent::CR_CALLBACK, 'cbSpecialSkinLoot', NPC_TYPEFLAG_SKIN_WITH_MINING, null ], // minable [yn] 25 => [FILTER_CR_FLAG, 'npcflag', NPC_FLAG_GUILD_MASTER ], // tabardvendor
18 => [parent::CR_FLAG, 'npcflag', NPC_FLAG_AUCTIONEER ], // auctioneer 27 => [FILTER_CR_FLAG, 'npcflag', NPC_FLAG_STABLE_MASTER ], // stablemaster
19 => [parent::CR_FLAG, 'npcflag', NPC_FLAG_BANKER ], // banker 28 => [FILTER_CR_FLAG, 'npcflag', NPC_FLAG_TRAINER ], // trainer
20 => [parent::CR_FLAG, 'npcflag', NPC_FLAG_BATTLEMASTER ], // battlemaster 29 => [FILTER_CR_FLAG, 'npcflag', NPC_FLAG_VENDOR ], // vendor
21 => [parent::CR_FLAG, 'npcflag', NPC_FLAG_FLIGHT_MASTER ], // flightmaster 19 => [FILTER_CR_FLAG, 'npcflag', NPC_FLAG_BANKER ], // banker
22 => [parent::CR_FLAG, 'npcflag', NPC_FLAG_GUILD_MASTER ], // guildmaster 37 => [FILTER_CR_NUMERIC, 'id', null, true], // id
23 => [parent::CR_FLAG, 'npcflag', NPC_FLAG_INNKEEPER ], // innkeeper 35 => [FILTER_CR_STRING, 'textureString' ], // useskin
24 => [parent::CR_FLAG, 'npcflag', NPC_FLAG_CLASS_TRAINER ], // talentunlearner 32 => [FILTER_CR_FLAG, 'cuFlags', NPC_CU_INSTANCE_BOSS ], // instanceboss
25 => [parent::CR_FLAG, 'npcflag', NPC_FLAG_GUILD_MASTER ], // tabardvendor 33 => [FILTER_CR_FLAG, 'cuFlags', CUSTOM_HAS_COMMENT ], // hascomments
27 => [parent::CR_FLAG, 'npcflag', NPC_FLAG_STABLE_MASTER ], // stablemaster 31 => [FILTER_CR_FLAG, 'cuFlags', CUSTOM_HAS_SCREENSHOT ], // hasscreenshots
28 => [parent::CR_FLAG, 'npcflag', NPC_FLAG_TRAINER ], // trainer 40 => [FILTER_CR_FLAG, 'cuFlags', CUSTOM_HAS_VIDEO ], // hasvideos
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_STRING, 'modelId', STR_MATCH_EXACT | STR_ALLOW_SHORT ], // usemodel [str] (wants int in string fmt <_<)
35 => [parent::CR_STRING, 'textureString' ], // useskin [str]
37 => [parent::CR_NUMERIC, 'id', NUM_CAST_INT, true ], // id
38 => [parent::CR_CALLBACK, 'cbRelEvent', null, null ], // relatedevent [enum]
40 => [parent::CR_FLAG, 'cuFlags', CUSTOM_HAS_VIDEO ], // hasvideos
41 => [parent::CR_NYI_PH, 1, null ], // haslocation [yn] [staff]
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 array $inputFields = array( protected function createSQLForCriterium(&$cr)
'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 if (in_array($cr[0], array_keys($this->genericFilter)))
'crv' => [parent::V_REGEX, parent::PATTERN_CRV, true ], // criteria values - only printable chars, no delimiter {
'na' => [parent::V_REGEX, parent::PATTERN_NAME, false], // name / subname - only printable chars, no delimiter if ($genCr = $this->genericCriterion($cr))
'ex' => [parent::V_EQUAL, 'on', false], // also match subname return $genCr;
'ma' => [parent::V_EQUAL, 1, false], // match any / all filter
'fa' => [parent::V_CALLBACK, 'cbPetFamily', true ], // pet family [list] - cat[0] == 1
'minle' => [parent::V_RANGE, [1, 99], false], // min level [int]
'maxle' => [parent::V_RANGE, [1, 99], false], // max level [int]
'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 = []; unset($cr);
$this->error = true;
return [1];
}
protected function createSQLForValues() : array 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 = []; $parts = [];
$_v = &$this->values; $_v = &$this->fiData['v'];
// name [str] // name [str]
if ($_v['na']) if (isset($_v['na']))
{ {
$_ = []; $_ = [];
if ($_v['ex'] == 'on') if (isset($_v['ex']) && $_v['ex'] == 'on')
$_ = $this->tokenizeString(['name_loc'.Lang::getLocale()->value, 'subname_loc'.Lang::getLocale()->value]); $_ = $this->modularizeString(['name_loc'.User::$localeId, 'subname_loc'.User::$localeId]);
else else
$_ = $this->tokenizeString(['name_loc'.Lang::getLocale()->value]); $_ = $this->modularizeString(['name_loc'.User::$localeId]);
if ($_) if ($_)
$parts[] = $_; $parts[] = $_;
} }
// pet family [list] // pet family [list]
if ($_v['fa']) if (isset($_v['fa']))
$parts[] = ['family', $_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] // creatureLevel min [int]
if ($_v['minle']) if (isset($_v['minle']))
$parts[] = ['minLevel', $_v['minle'], '>=']; {
if (is_int($_v['minle']) && $_v['minle'] > 0)
$parts[] = ['minLevel', $_v['minle'], '>='];
else
unset($_v['minle']);
}
// creatureLevel max [int] // creatureLevel max [int]
if ($_v['maxle']) if (isset($_v['maxle']))
$parts[] = ['maxLevel', $_v['maxle'], '<=']; {
if (is_int($_v['maxle']) && $_v['maxle'] > 0)
$parts[] = ['maxLevel', $_v['maxle'], '<='];
else
unset($_v['maxle']);
}
// classification [list] // classification [list]
if ($_v['cl']) if (isset($_v['cl']))
$parts[] = ['rank', $_v['cl']]; {
$_ = (array)$_v['cl'];
if (!array_diff($_, [0, 1, 2, 3, 4]))
$parts[] = ['rank', $_];
else
unset($_v['cl']);
}
// react Alliance [int] // react Alliance [int]
if ($_v['ra']) if (isset($_v['ra']))
$parts[] = ['ft.A', $_v['ra']]; {
$_ = (int)$_v['ra'];
if (in_array($_, [-1, 0, 1]))
$parts[] = ['ft.A', $_];
else
unset($_v['ra']);
}
// react Horde [int] // react Horde [int]
if ($_v['rh']) if (isset($_v['rh']))
$parts[] = ['ft.H', $_v['rh']]; {
$_ = (int)$_v['rh'];
if (in_array($_, [-1, 0, 1]))
$parts[] = ['ft.H', $_];
else
unset($_v['rh']);
}
return $parts; 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 (?a)', $eventIds))
return ['s.guid', $cGuids];
return [0];
}
else if ($crs == parent::ENUM_NONE)
{
if ($eventIds = DB::Aowow()->selectCol('SELECT `id` FROM ?_events WHERE `holidayId` <> 0'))
if ($cGuids = DB::World()->selectCol('SELECT DISTINCT `guid` FROM game_event_creature WHERE `eventEntry` IN (?a)', $eventIds))
return ['s.guid', $cGuids, '!'];
return [0];
}
else if (in_array($crs, $this->enums[$cr]))
{
if ($eventIds = DB::Aowow()->selectCol('SELECT `id` FROM ?_events WHERE `holidayId` = ?d', $crs))
if ($cGuids = DB::World()->selectCol('SELECT DISTINCT `guid` FROM `game_event_creature` WHERE `eventEntry` IN (?a)', $eventIds))
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 ['AND', ['((minGold + maxGold) / 2)', $crv, $crs]];
}
protected function cbQuestRelation(int $cr, int $crs, string $crv, $field, $val) : ?array
{
switch ($crs)
{
case 1: // any
return ['AND', ['qse.method', $val, '&'], ['qse.questId', null, '!']];
case 2: // alliance
return ['AND', ['qse.method', $val, '&'], ['qse.questId', null, '!'], [['qt.reqRaceMask', ChrRace::MASK_HORDE, '&'], 0], ['qt.reqRaceMask', ChrRace::MASK_ALLIANCE, '&']];
case 3: // horde
return ['AND', ['qse.method', $val, '&'], ['qse.questId', null, '!'], [['qt.reqRaceMask', ChrRace::MASK_ALLIANCE, '&'], 0], ['qt.reqRaceMask', ChrRace::MASK_HORDE, '&']];
case 4: // both
return ['AND', ['qse.method', $val, '&'], ['qse.questId', null, '!'], ['OR', ['AND', ['qt.reqRaceMask', ChrRace::MASK_ALLIANCE, '&'], ['qt.reqRaceMask', ChrRace::MASK_HORDE, '&']], ['qt.reqRaceMask', 0]]];
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 ['AND', ['skinLootId', 0, '>'], ['typeFlags', $typeFlag, '&']];
else
return ['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 ['AND', ['skinLootId', 0, '>'], [['typeFlags', $typeFlag, '&'], 0]];
else
return ['OR', ['skinLootId', 0], ['typeFlags', $typeFlag, '&']];
}
protected function cbReputation(int $cr, int $crs, string $crv, $op) : ?array
{
if (!in_array($crs, $this->enums[$cr]))
return null;
if ($_ = DB::Aowow()->selectRow('SELECT * FROM ?_factions WHERE `id` = ?d', $crs))
$this->fiReputationCols[] = [$crs, Util::localizedString($_, 'name')];
if ($cIds = DB::World()->selectCol('SELECT `creature_id` FROM creature_onkill_reputation WHERE (`RewOnKillRepFaction1` = ?d AND `RewOnKillRepValue1` '.$op.' 0) OR (`RewOnKillRepFaction2` = ?d AND `RewOnKillRepValue2` '.$op.' 0)', $crs, $crs))
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, $this->enums[$cr]))
return null;
$facTpls = [];
$facs = new FactionList(array('OR', ['parentFactionId', $crs], ['id', $crs]));
foreach ($facs->iterate() as $__)
$facTpls = array_merge($facTpls, $facs->getField('templateIds'));
return $facTpls ? ['faction', $facTpls] : [0];
}
} }
?> ?>

View File

@@ -1,31 +1,19 @@
<?php <?php
namespace Aowow;
if (!defined('AOWOW_REVISION')) if (!defined('AOWOW_REVISION'))
die('illegal access'); die('illegal access');
class CurrencyList extends BaseType class CurrencyList extends BaseType
{ {
public static $type = Type::CURRENCY; public static $type = TYPE_CURRENCY;
public static $brickFile = 'currency'; public static $brickFile = 'currency';
public static $dataTable = '?_currencies';
protected $queryBase = 'SELECT c.*, c.id AS ARRAY_KEY FROM ?_currencies c';
protected $queryOpts = array(
'c' => [['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;
}
protected $queryBase = 'SELECT *, c.id AS ARRAY_KEY FROM ?_currencies c';
protected $queryOpts = array(
'c' => [['ic']],
'ic' => ['j' => ['?_icons ic ON ic.id = c.iconId', true], 's' => ', ic.iconString']
);
public function getListviewData() public function getListviewData()
{ {
@@ -52,37 +40,19 @@ class CurrencyList extends BaseType
{ {
// todo (low): find out, why i did this in the first place // todo (low): find out, why i did this in the first place
if ($this->id == 104) // in case of honor commit sebbuku if ($this->id == 104) // in case of honor commit sebbuku
$icon = ['pvp-currency-alliance', 'pvp-currency-horde']; $icon = ['inv_bannerpvp_02', 'inv_bannerpvp_01']; // ['alliance', 'horde'];
else if ($this->id == 103) // also arena-icon diffs from item-icon else if ($this->id == 103) // also arena-icon diffs from item-icon
$icon = ['pvp-arenapoints-icon', 'pvp-arenapoints-icon']; $icon = ['money_arena', 'money_arena'];
else else
$icon = [$this->curTpl['iconString'], $this->curTpl['iconString']]; $icon = [$this->curTpl['iconString'], $this->curTpl['iconString']];
$data[Type::CURRENCY][$this->id] = ['name' => $this->getField('name', true), 'icon' => $icon]; $data[TYPE_CURRENCY][$this->id] = ['name' => $this->getField('name', true), 'icon' => $icon];
} }
return $data; return $data;
} }
public function renderTooltip() public function renderTooltip() { }
{
if (!$this->curTpl)
return array();
$x = '<table><tr><td>';
$x .= '<b>'.$this->getField('name', true).'</b><br>';
// cata+ (or go fill it by hand)
if ($_ = $this->getField('description', true))
$x .= '<div style="max-width: 300px" class="q">'.$_.'</div>';
if ($_ = $this->getField('cap'))
$x .= '<br><span class="q">'.Lang::currency('cap').Lang::main('colon').'</span>'.Lang::nf($_).'<br>';
$x .= '</td></tr></table>';
return $x;
}
} }
?> ?>

View File

@@ -1,60 +0,0 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
class EmoteList extends BaseType
{
public static $type = Type::EMOTE;
public static $brickFile = 'emote';
public static $dataTable = '?_emotes';
protected $queryBase = 'SELECT e.*, e.id AS ARRAY_KEY FROM ?_emotes e';
public function __construct(array $conditions = [], array $miscData = [])
{
parent::__construct($conditions, $miscData);
// post processing
foreach ($this->iterate() as &$curTpl)
{
// remap for generic access
$curTpl['name'] = $curTpl['cmd'];
}
}
public function getListviewData()
{
$data = [];
foreach ($this->iterate() as $__)
{
$data[$this->id] = array(
'id' => $this->curTpl['id'],
'name' => $this->curTpl['cmd'],
'preview' => Util::parseHtmlText($this->getField('meToExt', true) ?: $this->getField('meToNone', true) ?: $this->getField('extToMe', true) ?: $this->getField('extToExt', true) ?: $this->getField('extToNone', true), true)
);
// [nyi] sounds
}
return $data;
}
public function getJSGlobals($addMask = GLOBALINFO_ANY)
{
$data = [];
foreach ($this->iterate() as $__)
$data[Type::EMOTE][$this->id] = ['name' => $this->getField('cmd')];
return $data;
}
public function renderTooltip() { }
}
?>

View File

@@ -1,265 +0,0 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
class EnchantmentList extends BaseType
{
use listviewHelper;
public static $type = Type::ENCHANTMENT;
public static $brickFile = 'enchantment';
public static $dataTable = '?_itemenchantment';
private array $jsonStats = [];
private ?SpellList $relSpells = null;
private array $triggerIds = [];
protected $queryBase = 'SELECT ie.*, ie.id AS ARRAY_KEY FROM ?_itemenchantment ie';
protected $queryOpts = array( // 502 => 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;
}
}
// issue with scaling stats enchantments
// stats are stored as NOT NULL to be usable by the search filters and such become indistinguishable from scaling enchantments that _actually_ use the value 0
// so filter the stats container and if it is empty, rebuild from self. .. there are no mixed scaling/static enchantments, right!?
$this->jsonStats[$this->id] = (new StatsContainer())->fromJson($curTpl, true)->filter();
if (!count($this->jsonStats[$this->id]))
$this->jsonStats[$this->id]->fromEnchantment($curTpl);
}
if ($relSpells)
$this->relSpells = new SpellList(array(['id', $relSpells]));
}
// use if you JUST need the name
public static function getName($id)
{
$n = DB::Aowow()->SelectRow('SELECT `name_loc0`, `name_loc2`, `name_loc3`, `name_loc4`, `name_loc6`, `name_loc8` FROM ?_itemenchantment WHERE `id` = ?d', $id );
return Util::localizedString($n, 'name');
}
// end static use
public function getListviewData($addInfoMask = 0x0)
{
$data = [];
foreach ($this->iterate() as $__)
{
$data[$this->id] = array(
'id' => $this->id,
'name' => $this->getField('name', true),
'spells' => []
);
if ($this->curTpl['skillLine'] > 0)
$data[$this->id]['reqskill'] = $this->curTpl['skillLine'];
if ($this->curTpl['skillLevel'] > 0)
$data[$this->id]['reqskillrank'] = $this->curTpl['skillLevel'];
if ($this->curTpl['requiredLevel'] > 0)
$data[$this->id]['reqlevel'] = $this->curTpl['requiredLevel'];
foreach ($this->curTpl['spells'] as [$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());
}
return $data;
}
public function getStatGainForCurrent() : array
{
return $this->jsonStats[$this->id]->toJson();
}
public function getRelSpell($id)
{
if ($this->relSpells)
return $this->relSpells->getEntry($id);
return null;
}
public function getJSGlobals($addMask = GLOBALINFO_ANY)
{
$data = [];
if ($addMask & GLOBALINFO_SELF)
foreach ($this->iterate() as $__)
$data[Type::ENCHANTMENT][$this->id] = ['name' => $this->getField('name', true)];
if ($addMask & GLOBALINFO_RELATED)
{
if ($this->relSpells)
$data = $this->relSpells->getJSGlobals(GLOBALINFO_SELF);
foreach ($this->triggerIds as $tId)
if (empty($data[Type::SPELL][$tId]))
$data[Type::SPELL][$tId] = $tId;
}
return $data;
}
public function renderTooltip() { }
}
class EnchantmentListFilter extends Filter
{
protected string $type = 'enchantments';
protected array $enums = array(
3 => parent::ENUM_PROFESSION // requiresprof
);
protected 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 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_REGEX, parent::PATTERN_NAME, 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->tokenizeString(['name_loc'.Lang::getLocale()->value]))
$parts[] = $_;
// type
if ($_v['ty'])
$parts[] = ['OR', ['type1', $_v['ty']], ['type2', $_v['ty']], ['type3', $_v['ty']]];
return $parts;
}
}
?>

View File

@@ -1,27 +1,24 @@
<?php <?php
namespace Aowow;
if (!defined('AOWOW_REVISION')) if (!defined('AOWOW_REVISION'))
die('illegal access'); die('illegal access');
class FactionList extends BaseType class FactionList extends BaseType
{ {
public static $type = Type::FACTION; public static $type = TYPE_FACTION;
public static $brickFile = 'faction'; public static $brickFile = 'faction';
public static $dataTable = '?_factions';
protected $queryBase = 'SELECT f.*, f.parentFactionId AS cat, f.id AS ARRAY_KEY FROM ?_factions f'; protected $queryBase = 'SELECT f.*, f.parentFactionId AS cat, f.id AS ARRAY_KEY FROM ?_factions f';
protected $queryOpts = array( protected $queryOpts = array(
'f' => [['f2']], 'f' => [['f2']],
'f2' => ['j' => ['?_factions f2 ON f.parentFactionId = f2.id', true], 's' => ', IFNULL(f2.parentFactionId, 0) AS cat2'], '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'] '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) if ($this->error)
return; return;
@@ -39,7 +36,7 @@ class FactionList extends BaseType
public static function getName($id) public static function getName($id)
{ {
$n = DB::Aowow()->SelectRow('SELECT name_loc0, name_loc2, name_loc3, name_loc4, name_loc6, name_loc8 FROM ?_factions WHERE id = ?d', $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'); return Util::localizedString($n, 'name');
} }
@@ -77,7 +74,7 @@ class FactionList extends BaseType
$data = []; $data = [];
foreach ($this->iterate() as $__) 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; return $data;
} }

View File

@@ -1,7 +1,5 @@
<?php <?php
namespace Aowow;
if (!defined('AOWOW_REVISION')) if (!defined('AOWOW_REVISION'))
die('illegal access'); die('illegal access');
@@ -10,12 +8,11 @@ class GameObjectList extends BaseType
{ {
use listviewHelper, spawnHelper; use listviewHelper, spawnHelper;
public static $type = Type::OBJECT; public static $type = TYPE_OBJECT;
public static $brickFile = 'object'; public static $brickFile = 'object';
public static $dataTable = '?_objects';
protected $queryBase = 'SELECT o.*, o.id AS ARRAY_KEY FROM ?_objects o'; protected $queryBase = 'SELECT o.*, o.id AS ARRAY_KEY FROM ?_objects o';
protected $queryOpts = array( protected $queryOpts = array(
'o' => [['ft', 'qse']], 'o' => [['ft', 'qse']],
'ft' => ['j' => ['?_factiontemplate ft ON ft.id = o.faction', true], 's' => ', ft.factionId, ft.A, ft.H'], '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'], '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'],
@@ -23,7 +20,7 @@ class GameObjectList extends BaseType
's' => ['j' => '?_spawns s ON s.type = 2 AND s.typeId = o.id'] 's' => ['j' => '?_spawns s ON s.type = 2 AND s.typeId = o.id']
); );
public function __construct(array $conditions = [], array $miscData = []) public function __construct($conditions = [], $miscData = null)
{ {
parent::__construct($conditions, $miscData); parent::__construct($conditions, $miscData);
@@ -33,9 +30,6 @@ class GameObjectList extends BaseType
// post processing // post processing
foreach ($this->iterate() as $_id => &$curTpl) foreach ($this->iterate() as $_id => &$curTpl)
{ {
if (!$curTpl['name_loc0'])
$curTpl['name_loc0'] = 'Unnamed Object #' . $_id;
// unpack miscInfo // unpack miscInfo
$curTpl['lootStack'] = []; $curTpl['lootStack'] = [];
$curTpl['spells'] = []; $curTpl['spells'] = [];
@@ -64,7 +58,7 @@ class GameObjectList extends BaseType
public static function getName($id) public static function getName($id)
{ {
$n = DB::Aowow()->SelectRow('SELECT name_loc0, name_loc2, name_loc3, name_loc4, name_loc6, name_loc8 FROM ?_objects WHERE id = ?d', $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'); return Util::localizedString($n, 'name');
} }
@@ -75,7 +69,7 @@ class GameObjectList extends BaseType
{ {
$data[$this->id] = array( $data[$this->id] = array(
'id' => $this->id, 'id' => $this->id,
'name' => Lang::unescapeUISequences($this->getField('name', true), Lang::FMT_RAW), 'name' => $this->getField('name', true),
'type' => $this->curTpl['typeCat'], 'type' => $this->curTpl['typeCat'],
'location' => $this->getSpawns(SPAWNINFO_ZONES) 'location' => $this->getSpawns(SPAWNINFO_ZONES)
); );
@@ -97,15 +91,14 @@ class GameObjectList extends BaseType
return array(); return array();
$x = '<table>'; $x = '<table>';
$x .= '<tr><td><b class="q">'.Lang::unescapeUISequences($this->getField('name', true), Lang::FMT_HTML).'</b></td></tr>'; $x .= '<tr><td><b class="q">'.$this->getField('name', true).'</b></td></tr>';
if ($this->curTpl['typeCat']) if ($_ = Lang::gameObject('type', $this->curTpl['typeCat']))
if ($_ = Lang::gameObject('type', $this->curTpl['typeCat'])) $x .= '<tr><td>'.$_.'</td></tr>';
$x .= '<tr><td>'.$_.'</td></tr>';
if (isset($this->curTpl['lockId'])) if (isset($this->curTpl['lockId']))
if ($locks = Lang::getLocks($this->curTpl['lockId'])) if ($locks = Lang::getLocks($this->curTpl['lockId']))
foreach ($locks as $l) foreach ($locks as $l)
$x .= '<tr><td>'.sprintf(Lang::game('requires'), $l).'</td></tr>'; $x .= '<tr><td>'.$l.'</td></tr>';
$x .= '</table>'; $x .= '</table>';
@@ -117,24 +110,23 @@ class GameObjectList extends BaseType
$data = []; $data = [];
foreach ($this->iterate() as $__) foreach ($this->iterate() as $__)
$data[Type::OBJECT][$this->id] = ['name' => Lang::unescapeUISequences($this->getField('name', true), Lang::FMT_RAW)]; $data[TYPE_OBJECT][$this->id] = ['name' => $this->getField('name', true)];
return $data; return $data;
} }
public function getSourceData(int $id = 0) : array public function getSourceData()
{ {
$data = []; $data = [];
foreach ($this->iterate() as $__) foreach ($this->iterate() as $__)
{ {
if ($id && $id != $this->id)
continue;
$data[$this->id] = array( $data[$this->id] = array(
'n' => $this->getField('name', true), 'n' => $this->getField('name', true),
't' => Type::OBJECT, 't' => TYPE_OBJECT,
'ti' => $this->id 'ti' => $this->id
// 'bd' => bossdrop
// 'dd' => dungeondifficulty
); );
} }
@@ -145,108 +137,116 @@ class GameObjectList extends BaseType
class GameObjectListFilter extends Filter class GameObjectListFilter extends Filter
{ {
protected string $type = 'objects'; public $extraOpts = [];
protected array $enums = array(
1 => parent::ENUM_ZONE, protected $genericFilter = array(
16 => parent::ENUM_EVENT, 1 => [FILTER_CR_ENUM, 's.areaId', null ], // foundin
50 => [1, 2, 3, 4, 663, 883] 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 array $genericFilter = array( protected function createSQLForCriterium(&$cr)
1 => [parent::CR_ENUM, 's.areaId', false, true], // foundin {
2 => [parent::CR_CALLBACK, 'cbQuestRelation', 'startsQuests', 0x1 ], // startsquest [side] if (in_array($cr[0], array_keys($this->genericFilter)))
3 => [parent::CR_CALLBACK, 'cbQuestRelation', 'endsQuests', 0x2 ], // endsquest [side] {
4 => [parent::CR_CALLBACK, 'cbOpenable', null, null], // openable [yn] if ($genCR = $this->genericCriterion($cr))
5 => [parent::CR_NYI_PH, null, 0 ], // averagemoneycontained [op] [int] - GOs don't contain money, match against 0 return $genCR;
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 array $inputFields = array( unset($cr);
'cr' => [parent::V_LIST, [[1, 5], 7, 11, 13, 15, 16, 18, 50], true ], // criteria ids $this->error = true;
'crs' => [parent::V_LIST, [parent::ENUM_NONE, parent::ENUM_ANY, [0, 5000]], true ], // criteria operators return [1];
'crv' => [parent::V_REGEX, parent::PATTERN_INT, true ], // criteria values - only numeric input values expected }
'na' => [parent::V_REGEX, parent::PATTERN_NAME, false], // name - only printable chars, no delimiter
'ma' => [parent::V_EQUAL, 1, false] // match any / all filter
);
public array $extraOpts = []; switch ($cr[0])
{
case 4:
if (!$this->int2Bool($cr[1]))
break;
protected function createSQLForValues() : array 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 = []; $parts = [];
$_v = $this->values; $_v = $this->fiData['v'];
// name // name
if ($_v['na']) if (isset($_v['na']))
if ($_ = $this->tokenizeString(['name_loc'.Lang::getLocale()->value])) if ($_ = $this->modularizeString(['name_loc'.User::$localeId]))
$parts[] = $_; $parts[] = $_;
return $parts; return $parts;
} }
protected function cbOpenable(int $cr, int $crs, string $crv) : ?array
{
if ($this->int2Bool($crs))
return $crs ? ['OR', ['flags', 0x2, '&'], ['type', 3]] : ['AND', [['flags', 0x2, '&'], 0], ['type', 3, '!']];
return null;
}
protected function cbQuestRelation(int $cr, int $crs, string $crv, $field, $value) : ?array
{
switch ($crs)
{
case 1: // any
return ['AND', ['qse.method', $value, '&'], ['qse.questId', null, '!']];
case 2: // alliance only
return ['AND', ['qse.method', $value, '&'], ['qse.questId', null, '!'], [['qt.reqRaceMask', ChrRace::MASK_HORDE, '&'], 0], ['qt.reqRaceMask', ChrRace::MASK_ALLIANCE, '&']];
case 3: // horde only
return ['AND', ['qse.method', $value, '&'], ['qse.questId', null, '!'], [['qt.reqRaceMask', ChrRace::MASK_ALLIANCE, '&'], 0], ['qt.reqRaceMask', ChrRace::MASK_HORDE, '&']];
case 4: // both
return ['AND', ['qse.method', $value, '&'], ['qse.questId', null, '!'], ['OR', ['AND', ['qt.reqRaceMask', ChrRace::MASK_ALLIANCE, '&'], ['qt.reqRaceMask', ChrRace::MASK_HORDE, '&']], ['qt.reqRaceMask', 0]]];
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 (?a)', $eventIds))
return ['s.guid', $goGuids];
return [0];
}
else if ($crs == parent::ENUM_NONE)
{
if ($eventIds = DB::Aowow()->selectCol('SELECT `id` FROM ?_events WHERE `holidayId` <> 0'))
if ($goGuids = DB::World()->selectCol('SELECT DISTINCT `guid` FROM game_event_gameobject WHERE `eventEntry` IN (?a)', $eventIds))
return ['s.guid', $goGuids, '!'];
return [0];
}
else if (in_array($crs, $this->enums[$cr]))
{
if ($eventIds = DB::Aowow()->selectCol('SELECT `id` FROM ?_events WHERE `holidayId` = ?d', $crs))
if ($goGuids = DB::World()->selectCol('SELECT DISTINCT `guid` FROM game_event_gameobject WHERE `eventEntry` IN (?a)', $eventIds))
return ['s.guid', $goGuids];
return [0];
}
return null;
}
} }
?> ?>

View File

@@ -1,175 +0,0 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
class GuideList extends BaseType
{
use ListviewHelper;
public const STATUS_COLORS = array(
GUIDE_STATUS_DRAFT => '#71D5FF',
GUIDE_STATUS_REVIEW => '#FFFF00',
GUIDE_STATUS_APPROVED => '#1EFF00',
GUIDE_STATUS_REJECTED => '#FF4040',
GUIDE_STATUS_ARCHIVED => '#FFD100'
);
public static $type = Type::GUIDE;
public static $brickFile = 'guide';
public static $dataTable = '?_guides';
public static $contribute = CONTRIBUTE_CO;
private $article = [];
private $jsGlobals = [];
protected $queryBase = 'SELECT g.*, g.id AS ARRAY_KEY FROM ?_guides g';
protected $queryOpts = array(
'g' => [['a', 'c'], '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"']
);
public function __construct(array $conditions = [], array $miscData = [])
{
parent::__construct($conditions, $miscData);
if ($this->error)
return;
$ratings = DB::Aowow()->select('SELECT `entry` AS ARRAY_KEY, IFNULL(SUM(`value`), 0) AS `t`, IFNULL(COUNT(*), 0) AS `n`, IFNULL(MAX(IF(`userId` = ?d, `value`, 0)), 0) AS `s` FROM ?_user_ratings WHERE `type` = ?d AND `entry` IN (?a)', User::$id, RATING_GUIDE, $this->getFoundIDs());
// post processing
foreach ($this->iterate() as $id => &$_curTpl)
{
if (isset($ratings[$id]))
{
$_curTpl['nvotes'] = $ratings[$id]['n'];
$_curTpl['rating'] = $ratings[$id]['n'] < 5 ? -1 : $ratings[$id]['t'] / $ratings[$id]['n'];
$_curTpl['_self'] = $ratings[$id]['s'];
}
else
{
$_curTpl['nvotes'] = 0;
$_curTpl['rating'] = -1;
}
}
}
public function getArticle(int $rev = -1) : string
{
if ($rev < -1)
$rev = -1;
if (empty($this->article[$rev]))
{
$a = DB::Aowow()->selectRow('SELECT `article`, `rev` FROM ?_articles WHERE ((`type` = ?d AND `typeId` = ?d){ OR `url` = ?}){ AND `rev`= ?d} ORDER BY `rev` DESC LIMIT 1',
Type::GUIDE, $this->id, $this->getField('url') ?: DBSIMPLE_SKIP, $rev < 0 ? DBSIMPLE_SKIP : $rev);
$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') == GUIDE_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') != GUIDE_STATUS_ARCHIVED;
}
public function getJSGlobals($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 .= '&nbsp;&nbsp;&nbsp;&nbsp;<span class="icontiny c'.$c.'" style="background-image: url('.Cfg::get('STATIC_URL').'/images/wow/icons/tiny/class_'.ChrClass::tryFrom($c)->json().'.gif)">%s</span>';
if (($s = $this->getField('specId')) > -1)
{
$i = Game::$specIconStrings[$c][$s];
$n = '';
$specStr .= '<span class="icontiny c'.$c.'" style="background-image: url('.Cfg::get('STATIC_URL').'/images/wow/icons/tiny/'.$i.'.gif)">'.Lang::game('classSpecs', $c, $s).'</span>';
}
$specStr = sprintf($specStr, $n);
}
}
$tt = '<table><tr><td><div style="max-width: 320px"><b class="q">'.$this->getField('title').'</b><br>';
$tt .= '<table width="100%"><tr><td>'.Lang::game('guide').'</td><th>'.Lang::guide('byAuthor', [$this->getField('author')]).'</th></tr></table>';
$tt .= '<table width="100%"><tr><td>'.Lang::guide('category', $this->getField('category')).$specStr.'</td><th>'.Lang::guide('patch').' 3.3.5</th></tr></table>';
$tt .= '<div class="q" style="margin: 0.25em 0">'.$this->getField('description').'</div>';
$tt .= '</div></td></tr></table>';
return $tt;
}
}
?>

View File

@@ -1,313 +0,0 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
class GuildList extends BaseType
{
use profilerHelper, listviewHelper;
public static $contribute = CONTRIBUTE_NONE;
public function getListviewData()
{
$this->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 array_values($data);
}
private function getGuildScores()
{
/*
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()->select('SELECT `guild` AS ARRAY_KEY, `id` AS ARRAY_KEY2, `level`, `gearscore`, `achievementpoints`, IF(`cuFlags` & ?d, 0, 1) AS "synced" FROM ?_profiler_profiles WHERE `guild` IN (?a) ORDER BY `gearscore` DESC', PROFILER_CU_NEEDS_RESYNC, $guilds);
foreach ($this->iterate() as &$_curTpl)
{
$id = $_curTpl['id'];
if (empty($stats[$id]))
continue;
$guildStats = array_filter($stats[$id], function ($x) { return $x['synced']; } );
if (!$guildStats)
continue;
$nMaxLevel = count(array_filter($stats[$id], function ($x) { return $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 function renderTooltip() {}
public function getJSGlobals($addMask = 0) {}
}
class GuildListFilter extends Filter
{
use TrProfilerFilter;
protected string $type = 'guilds';
protected array $genericFilter = [];
protected array $inputFields = array(
'na' => [parent::V_REGEX, parent::PATTERN_NAME, false], // name - only printable chars, no delimiter
'ma' => [parent::V_EQUAL, 1, false], // match any / all filter
'ex' => [parent::V_EQUAL, 'on', false], // only match exact
'si' => [parent::V_LIST, [SIDE_ALLIANCE, SIDE_HORDE], false], // side
'rg' => [parent::V_CALLBACK, 'cbRegionCheck', false], // region
'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->tokenizeString(['g.name'], $_v['na'], $_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 $queryBase = 'SELECT `g`.*, `g`.`guildid` AS ARRAY_KEY FROM guild g';
protected $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
$realmId = key($this->dbNames);
$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 nonexistant 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']]++;
}
foreach ($conditions as $c)
if (is_int($c))
$limit = $c;
$limit ??= Cfg::get('SQL_LIMIT_DEFAULT');
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()
{
$data = [];
foreach ($this->iterate() as $guid => $__)
{
$data[$guid] = array(
'realm' => $this->getField('realm'),
'realmGUID' => $this->getField('guildid'),
'name' => $this->getField('name'),
'nameUrl' => Profiler::urlize($this->getField('name')),
'cuFlags' => PROFILER_CU_NEEDS_RESYNC
);
}
// basic guild data
foreach (Util::createSqlBatchInsert($data) as $ins)
DB::Aowow()->query('INSERT INTO ?_profiler_guild (?#) VALUES '.$ins.' ON DUPLICATE KEY UPDATE `id` = `id`', array_keys(reset($data)));
// merge back local ids
$localIds = DB::Aowow()->selectCol(
'SELECT CONCAT(realm, ":", realmGUID) AS ARRAY_KEY, id FROM ?_profiler_guild WHERE realm IN (?a) AND realmGUID IN (?a)',
array_column($data, 'realm'),
array_column($data, 'realmGUID')
);
foreach ($this->iterate() as $guid => &$_curTpl)
if (isset($localIds[$guid]))
$_curTpl['id'] = $localIds[$guid];
}
}
class LocalGuildList extends GuildList
{
protected $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, 'AND');
$conditions = ['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()
{
$url = '?guild=';
return $url.implode('.', array(
Profiler::urlize($this->getField('region')),
Profiler::urlize($this->getField('realmName')),
Profiler::urlize($this->getField('name'))
));
}
}
?>

View File

@@ -1,229 +0,0 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
class IconList extends BaseType
{
use listviewHelper;
public static $type = Type::ICON;
public static $brickFile = 'icongallery';
public static $dataTable = '?_icons';
public static $contribute = CONTRIBUTE_CO;
private $pseudoQry = 'SELECT `iconId` AS ARRAY_KEY, COUNT(*) FROM ?# WHERE `iconId` IN (?a) GROUP BY `iconId`';
private $pseudoJoin = array(
'nItems' => '?_items',
'nSpells' => '?_spell',
'nAchievements' => '?_achievement',
'nCurrencies' => '?_currencies',
'nPets' => '?_pet'
);
protected $queryBase = 'SELECT ic.*, ic.id AS ARRAY_KEY FROM ?_icons ic';
/* this works, but takes ~100x more time than i'm comfortable with .. kept as reference
protected $queryOpts = array( // 29 => Type::ICON
'ic' => [['s', 'i', 'a', 'c', 'p'], 'g' => 'ic.id'],
'i' => ['j' => ['?_items `i` ON `i`.`iconId` = `ic`.`id`', true], 's' => ', COUNT(DISTINCT `i`.`id`) AS nItems'],
's' => ['j' => ['?_spell `s` ON `s`.`iconId` = `ic`.`id`', true], 's' => ', COUNT(DISTINCT `s`.`id`) AS nSpells'],
'a' => ['j' => ['?_achievement `a` ON `a`.`iconId` = `ic`.`id`', true], 's' => ', COUNT(DISTINCT `a`.`id`) AS nAchievements'],
'c' => ['j' => ['?_currencies `c` ON `c`.`iconId` = `ic`.`id`', true], 's' => ', COUNT(DISTINCT `c`.`id`) AS nCurrencies'],
'p' => ['j' => ['?_pet `p` ON `p`.`iconId` = `ic`.`id`', true], 's' => ', COUNT(DISTINCT `p`.`id`) AS nPets']
);
*/
public function __construct(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;
}
}
// use if you JUST need the name
public static function getName($id)
{
$n = DB::Aowow()->SelectRow('SELECT `name` FROM ?_icons WHERE `id` = ?d', $id );
return Util::localizedString($n, 'name');
}
// end static use
public function getListviewData($addInfoMask = 0x0)
{
$data = [];
foreach ($this->iterate() as $__)
{
$data[$this->id] = array(
'id' => $this->id,
'name' => $this->getField('name', true, true),
'icon' => $this->getField('name', true, true),
'itemcount' => (int)$this->getField('nItems'),
'spellcount' => (int)$this->getField('nSpells'),
'achievementcount' => (int)$this->getField('nAchievements'),
'npccount' => 0, // UNUSED
'petabilitycount' => 0, // UNUSED
'currencycount' => (int)$this->getField('nCurrencies'),
'missionabilitycount' => 0, // UNUSED
'buildingcount' => 0, // UNUSED
'petcount' => (int)$this->getField('nPets'),
'threatcount' => 0, // UNUSED
'classcount' => 0 // class icons are hardcoded and not referenced in dbc
);
}
return $data;
}
public function getJSGlobals($addMask = GLOBALINFO_ANY)
{
$data = [];
foreach ($this->iterate() as $__)
$data[Type::ICON][$this->id] = ['name' => $this->getField('name', true, true), 'icon' => $this->getField('name', true, true)];
return $data;
}
public function renderTooltip() { }
}
class IconListFilter extends Filter
{
private array $totalUses = [];
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 [num]
13 => '' // used [num]
);
protected string $type = 'icons';
protected array $genericFilter = array(
1 => [parent::CR_CALLBACK, 'cbUseAny' ], // items [num]
2 => [parent::CR_CALLBACK, 'cbUseAny' ], // spells [num]
3 => [parent::CR_CALLBACK, 'cbUseAny' ], // achievements [num]
6 => [parent::CR_CALLBACK, 'cbUseAny' ], // currencies [num]
9 => [parent::CR_CALLBACK, 'cbUseAny' ], // hunterpets [num]
11 => [parent::CR_NYI_PH, null, 0], // classes [num]
13 => [parent::CR_CALLBACK, 'cbUseAll' ] // used [num]
);
protected 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_REGEX, parent::PATTERN_NAME, false], // name - only printable chars, no delimiter
'ma' => [parent::V_EQUAL, 1, false] // match any / all filter
);
public array $extraOpts = [];
private function _getCnd(string $op, int $val, string $tbl) : ?array
{
switch ($op)
{
case '>':
case '>=':
case '=':
$ids = DB::Aowow()->selectCol('SELECT `iconId` AS ARRAY_KEY, COUNT(*) AS "n" FROM ?# GROUP BY `iconId` HAVING n '.$op.' '.$val, $tbl);
return $ids ? ['id', array_keys($ids)] : [1];
case '<=':
if ($val)
$op = '>';
break;
case '<':
if ($val)
$op = '>=';
break;
case '!=':
if ($val)
$op = '=';
break;
default:
return null;
}
$ids = DB::Aowow()->selectCol('SELECT `iconId` AS ARRAY_KEY, COUNT(*) AS "n" FROM ?# GROUP BY `iconId` HAVING n '.$op.' '.$val, $tbl);
return $ids ? ['id', array_keys($ids), '!'] : [1];
}
protected function createSQLForValues() : array
{
$parts = [];
$_v = &$this->values;
//string
if ($_v['na'])
if ($_ = $this->tokenizeString(['name']))
$parts[] = $_;
return $parts;
}
protected function cbUseAny(int $cr, int $crs, string $crv) : ?array
{
if (Util::checkNumeric($crv, NUM_CAST_INT) && $this->int2Op($crs))
return $this->_getCnd($crs, $crv, $this->criterion2field[$cr]);
return null;
}
protected function cbUseAll(int $cr, int $crs, string $crv) : ?array
{
if (!Util::checkNumeric($crv, NUM_CAST_INT) || !$this->int2Op($crs))
return null;
if (!$this->totalUses)
{
foreach ($this->criterion2field as $tbl)
{
if (!$tbl)
continue;
$res = DB::Aowow()->selectCol('SELECT `iconId` AS ARRAY_KEY, COUNT(*) AS "n" FROM ?# GROUP BY `iconId`', $tbl);
Util::arraySumByKey($this->totalUses, $res);
}
}
if ($crs == '=')
$crs = '==';
$op = $crs;
if ($crs == '<=' && $crv)
$op = '>';
else if ($crs == '<' && $crv)
$op = '>=';
else if ($crs == '!=' && $crv)
$op = '==';
$ids = array_filter($this->totalUses, fn($x) => eval('return '.$x.' '.$op.' '.$crv.';'));
if ($crs != $op)
return $ids ? ['id', array_keys($ids), '!'] : [1];
else
return $ids ? ['id', array_keys($ids)] : ['id', array_keys($this->totalUses), '!'];
}
}
?>

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,5 @@
<?php <?php
namespace Aowow;
if (!defined('AOWOW_REVISION')) if (!defined('AOWOW_REVISION'))
die('illegal access'); die('illegal access');
@@ -10,31 +8,33 @@ class ItemsetList extends BaseType
{ {
use ListviewHelper; use ListviewHelper;
public static $type = Type::ITEMSET; public static $type = TYPE_ITEMSET;
public static $brickFile = 'itemset'; public static $brickFile = 'itemset';
public static $dataTable = '?_itemset';
public $pieceToSet = []; // used to build g_items and search public $pieceToSet = []; // used to build g_items and search
private $classes = []; // used to build g_classes private $classes = []; // used to build g_classes
protected $queryBase = 'SELECT `set`.*, `set`.id AS ARRAY_KEY FROM ?_itemset `set`'; protected $queryBase = 'SELECT *, id AS ARRAY_KEY FROM ?_itemset `set`';
protected $queryOpts = array( protected $queryOpts = ['set' => ['o' => 'maxlevel DESC']];
'set' => ['o' => 'maxlevel DESC'],
'e' => ['j' => ['?_events e ON `e`.`id` = `set`.`eventId`', true], 's' => ', e.holidayId'],
'src' => ['j' => ['?_source src ON `src`.`typeId` = `set`.`id` AND `src`.`type` = 4', true], 's' => ', src1, src2, src3, src4, src5, src6, src7, src8, src9, src10, src11, src12, src13, src14, src15, src16, src17, src18, src19, src20, src21, src22, src23, src24']
);
public function __construct(array $conditions = [], array $miscData = []) public function __construct($conditions = [])
{ {
parent::__construct($conditions, $miscData); parent::__construct($conditions);
// post processing // post processing
foreach ($this->iterate() as &$_curTpl) foreach ($this->iterate() as &$_curTpl)
{ {
$_curTpl['classes'] = ChrClass::fromMask($_curTpl['classMask']); $_curTpl['classes'] = [];
$this->classes = array_merge($this->classes, $_curTpl['classes']);
$_curTpl['pieces'] = []; $_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++) for ($i = 1; $i < 10; $i++)
{ {
if ($piece = $_curTpl['item'.$i]) if ($piece = $_curTpl['item'.$i])
@@ -76,184 +76,134 @@ class ItemsetList extends BaseType
$data = []; $data = [];
if ($this->classes && ($addMask & GLOBALINFO_RELATED)) if ($this->classes && ($addMask & GLOBALINFO_RELATED))
$data[Type::CHR_CLASS] = array_combine($this->classes, $this->classes); $data[TYPE_CLASS] = array_combine($this->classes, $this->classes);
if ($this->pieceToSet && ($addMask & GLOBALINFO_SELF)) if ($this->pieceToSet && ($addMask & GLOBALINFO_SELF))
$data[Type::ITEM] = array_combine(array_keys($this->pieceToSet), array_keys($this->pieceToSet)); $data[TYPE_ITEM] = array_combine(array_keys($this->pieceToSet), array_keys($this->pieceToSet));
if ($addMask & GLOBALINFO_SELF) if ($addMask & GLOBALINFO_SELF)
foreach ($this->iterate() as $id => $__) foreach ($this->iterate() as $id => $__)
$data[Type::ITEMSET][$id] = ['name' => $this->getField('name', true)]; $data[TYPE_ITEMSET][$id] = ['name' => $this->getField('name', true)];
return $data; return $data;
} }
public function renderTooltip() public function renderTooltip() { }
{
if (!$this->curTpl)
return array();
$x = '<table><tr><td>';
$x .= '<span class="q'.$this->getField('quality').'">'.$this->getField('name', true).'</span><br />';
$nCl = 0;
if ($_ = $this->getField('classMask'))
{
$jsg = [];
$cl = Lang::getClassString($_, $jsg);
$nCl = count($jsg);
$x .= Util::ucFirst($nCl > 1 ? Lang::game('classes') : Lang::game('class')).Lang::main('colon').$cl.'<br />';
}
if ($_ = $this->getField('contentGroup'))
$x .= Lang::itemset('notes', $_).($this->getField('heroic') ? ' <i class="q2">('.Lang::item('heroic').')</i>' : '').'<br />';
if (!$nCl || !$this->getField('type'))
$x.= Lang::itemset('types', $this->getField('type')).'<br />';
if ($bonuses = $this->getBonuses())
{
$x .= '<span>';
foreach ($bonuses as $b)
$x .= '<br /><span class="q13">'.$b['bonus'].' '.Lang::itemset('_pieces').Lang::main('colon').'</span>'.$b['desc'];
$x .= '</span>';
}
$x .= '</td></tr></table>';
return $x;
}
public function getBonuses()
{
$spells = [];
for ($i = 1; $i < 9; $i++)
{
$spl = $this->getField('spell'.$i);
$qty = $this->getField('bonus'.$i);
// cant use spell as index, would change order
if ($spl && $qty)
$spells[] = ['id' => $spl, 'bonus' => $qty];
}
// sort by required pieces ASC
usort($spells, function($a, $b) {
if ($a['bonus'] == $b['bonus'])
return 0;
return ($a['bonus'] > $b['bonus']) ? 1 : -1;
});
$setSpells = new SpellList(array(['s.id', array_column($spells, 'id')]));
foreach ($setSpells->iterate() as $spellId => $__)
{
foreach ($spells as &$s)
{
if ($spellId != $s['id'])
continue;
$s['desc'] = $setSpells->parseText('description', $this->getField('reqLevel') ?: MAX_LEVEL)[0];
}
}
return $spells;
}
} }
// missing filter: "Available to Players" // missing filter: "Available to Players"
class ItemsetListFilter extends Filter class ItemsetListFilter extends Filter
{ {
protected string $type = 'itemsets'; // cr => [type, field, misc, extraCol]
protected array $enums = array( protected $genericFilter = array( // misc (bool): _NUMERIC => useFloat; _STRING => localized; _FLAG => match Value; _BOOLEAN => stringSet
6 => parent::ENUM_EVENT 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 array $genericFilter = array( protected function createSQLForCriterium(&$cr)
2 => [parent::CR_NUMERIC, 'id', NUM_CAST_INT, true], // id {
3 => [parent::CR_NUMERIC, 'npieces', NUM_CAST_INT ], // pieces if (in_array($cr[0], array_keys($this->genericFilter)))
4 => [parent::CR_STRING, 'bonusText', STR_LOCALIZED ], // bonustext {
5 => [parent::CR_BOOLEAN, 'heroic' ], // heroic if ($genCR = $this->genericCriterion($cr))
6 => [parent::CR_ENUM, 'e.holidayId', true, true], // relatedevent return $genCR;
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 array $inputFields = array( unset($cr);
'cr' => [parent::V_RANGE, [2, 12], true ], // criteria ids $this->error = true;
'crs' => [parent::V_LIST, [parent::ENUM_NONE, parent::ENUM_ANY, [0, 424]], true ], // criteria operators return [1];
'crv' => [parent::V_REGEX, parent::PATTERN_CRV, true ], // criteria values - only printable chars, no delimiters }
'na' => [parent::V_REGEX, parent::PATTERN_NAME, false], // name / description - only printable chars, no delimiter
'ma' => [parent::V_EQUAL, 1, false], // match any / all filter
'qu' => [parent::V_RANGE, [0, 7], true ], // quality
'ty' => [parent::V_RANGE, [1, 12], true ], // set type
'minle' => [parent::V_RANGE, [1, 999], false], // min item level
'maxle' => [parent::V_RANGE, [1, 999], false], // max itemlevel
'minrl' => [parent::V_RANGE, [1, MAX_LEVEL], false], // min required level
'maxrl' => [parent::V_RANGE, [1, MAX_LEVEL], false], // max required level
'cl' => [parent::V_LIST, [[1, 9], 11], false], // class
'ta' => [parent::V_RANGE, [1, 30], false] // tag / content group
);
protected function createSQLForValues() : array 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 = []; $parts = [];
$_v = &$this->values; $_v = &$this->fiData['v'];
// name [str] // name [str]
if ($_v['na']) if (isset($_v['na']))
if ($_ = $this->tokenizeString(['name_loc'.Lang::getLocale()->value])) if ($_ = $this->modularizeString(['name_loc'.User::$localeId]))
$parts[] = $_; $parts[] = $_;
// quality [enum] // quality [enum]
if ($_v['qu']) if (isset($_v['qu']))
$parts[] = ['quality', $_v['qu']]; $parts[] = ['quality', (array)$_v['qu']];
// type [enum] // type [enum]
if ($_v['ty']) if (isset($_v['ty']))
$parts[] = ['type', $_v['ty']]; $parts[] = ['type', (array)$_v['ty']];
// itemLevel min [int] // itemLevel min [int]
if ($_v['minle']) if (isset($_v['minle']))
$parts[] = ['minLevel', $_v['minle'], '>=']; {
if (is_int($_v['minle']) && $_v['minle'] > 0)
$parts[] = ['minLevel', $_v['minle'], '>='];
else
unset($_v['minle']);
}
// itemLevel max [int] // itemLevel max [int]
if ($_v['maxle']) if (isset($_v['maxle']))
$parts[] = ['maxLevel', $_v['maxle'], '<=']; {
if (is_int($_v['maxle']) && $_v['maxle'] > 0)
$parts[] = ['maxLevel', $_v['maxle'], '<='];
else
unset($_v['maxle']);
}
// reqLevel min [int] // reqLevel min [int]
if ($_v['minrl']) if (isset($_v['minrl']))
$parts[] = ['reqLevel', $_v['minrl'], '>=']; {
if (is_int($_v['minrl']) && $_v['minrl'] > 0)
$parts[] = ['reqLevel', $_v['minrl'], '>='];
else
unset($_v['minrl']);
}
// reqLevel max [int] // reqLevel max [int]
if ($_v['maxrl']) if (isset($_v['maxrl']))
$parts[] = ['reqLevel', $_v['maxrl'], '<=']; {
if (is_int($_v['maxrl']) && $_v['maxrl'] > 0)
$parts[] = ['reqLevel', $_v['maxrl'], '<='];
else
unset($_v['maxrl']);
}
// class [enum] // class [enum]
if ($_v['cl']) if (isset($_v['cl']))
$parts[] = ['classMask', $this->list2Mask([$_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] // tag [enum]
if ($_v['ta']) if (isset($_v['ta']))
$parts[] = ['contentGroup', intVal($_v['ta'])]; {
if ($_v['ta'] > 0 && $_v['ta'] < 31)
$parts[] = ['contentGroup', intVal($_v['ta'])];
else
unset($_v['ta']);
}
return $parts; 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
};
}
} }
?> ?>

View File

@@ -1,76 +0,0 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
class MailList extends BaseType
{
public static $type = Type::MAIL;
public static $brickFile = 'mail';
public static $dataTable = '?_mails';
protected $queryBase = 'SELECT m.*, m.id AS ARRAY_KEY FROM ?_mails m';
protected $queryOpts = [];
public function __construct(array $conditions = [], array $miscData = [])
{
parent::__construct($conditions, $miscData);
if ($this->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($id)
{
$n = DB::Aowow()->SelectRow('SELECT subject_loc0, subject_loc2, subject_loc3, subject_loc4, subject_loc6, subject_loc8 FROM ?_mails WHERE id = ?d', $id);
return Util::localizedString($n, 'subject');
}
public function getListviewData()
{
$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($addMask = 0)
{
$data = [];
foreach ($this->iterate() as $__)
if ($a = $this->curTpl['attachment'])
$data[Type::ITEM][$a] = $a;
return $data;
}
public function renderTooltip() { }
}
?>

View File

@@ -1,7 +1,5 @@
<?php <?php
namespace Aowow;
if (!defined('AOWOW_REVISION')) if (!defined('AOWOW_REVISION'))
die('illegal access'); die('illegal access');
@@ -10,15 +8,10 @@ class PetList extends BaseType
{ {
use ListviewHelper; use ListviewHelper;
public static $type = Type::PET; public static $type = TYPE_PET;
public static $brickFile = 'pet'; public static $brickFile = 'pet';
public static $dataTable = '?_pet';
protected $queryBase = 'SELECT p.*, p.id AS ARRAY_KEY FROM ?_pet p'; protected $queryBase = 'SELECT *, id AS ARRAY_KEY FROM ?_pet p';
protected $queryOpts = array(
'p' => [['ic']],
'ic' => ['j' => ['?_icons ic ON p.iconId = ic.id', true], 's' => ', ic.name AS iconString'],
);
public function getListviewData() public function getListviewData()
{ {
@@ -61,10 +54,10 @@ class PetList extends BaseType
if ($addMask & GLOBALINFO_RELATED) if ($addMask & GLOBALINFO_RELATED)
for ($i = 1; $i <= 4; $i++) for ($i = 1; $i <= 4; $i++)
if ($this->curTpl['spellId'.$i] > 0) 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) if ($addMask & GLOBALINFO_SELF)
$data[Type::PET][$this->id] = ['icon' => $this->curTpl['iconString']]; $data[TYPE_PET][$this->id] = ['icon' => $this->curTpl['iconString']];
} }
return $data; return $data;

View File

@@ -1,763 +1,228 @@
<?php <?php
namespace Aowow;
if (!defined('AOWOW_REVISION')) if (!defined('AOWOW_REVISION'))
die('illegal access'); die('illegal access');
// class CharacterList extends BaseType // new profiler-related parent: ProfilerType?; maybe a trait is enough => use ProfileHelper;
// class GuildList extends BaseType
// class ArenaTeamList extends BaseType
class ProfileList extends BaseType class ProfileList extends BaseType
{ {
use profilerHelper, listviewHelper; public static $type = 0; // profiles dont actually have one
public static $brickFile = 'profile';
public static $contribute = CONTRIBUTE_NONE; 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 getListviewData($addInfo = 0, array $reqCols = []) 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 = []; $data = [];
foreach ($this->iterate() as $__) foreach ($this->iterate() as $__)
{ {
if (!$this->isVisibleToUser()) $tDistrib = $this->getTalentDistribution();
continue;
if (($addInfo & PROFILEINFO_PROFILE) && !$this->isCustom())
continue;
if (($addInfo & PROFILEINFO_CHARACTER) && $this->isCustom())
continue;
$data[$this->id] = array( $data[$this->id] = array(
'id' => $this->getField('id'), 'id' => 0,
'name' => $this->getField('name'), 'name' => $this->curTpl['name'],
'race' => $this->getField('race'), 'achievementpoints' => $this->curTpl['achievementpoints'],
'classs' => $this->getField('class'), 'guild' => $this->curTpl['guild'], // 0 if none
'gender' => $this->getField('gender'), 'guildRank' => -1,
'level' => $this->getField('level'), 'realm' => $this->curTpl['realm'][0],
'faction' => ChrRace::tryFrom($this->getField('race'))?->isAlliance() ? 0 : 1, 'realmname' => $this->curTpl['realm'][1],
'talenttree1' => $this->getField('talenttree1'), 'battlegroup' => $this->curTpl['battlegroup'][0],
'talenttree2' => $this->getField('talenttree2'), 'battlegroupname' => $this->curTpl['battlegroup'][0],
'talenttree3' => $this->getField('talenttree3'), 'region' => $this->curTpl['region'][0],
'talentspec' => $this->getField('activespec') + 1, // 0 => 1; 1 => 2 'level' => $this->curTpl['level'],
'achievementpoints' => $this->getField('achievementpoints'), 'race' => $this->curTpl['race'],
'guild' => $this->curTpl['guildname'] ? '$"'.str_replace ('"', '', $this->curTpl['guildname']).'"' : '', // force this to be a string 'gender' => $this->curTpl['gender'],
'guildrank' => $this->getField('guildrank'), 'classs' => $this->curTpl['classs'],
'realm' => Profiler::urlize($this->getField('realmName'), true), 'faction' => $this->curTpl['faction'],
'realmname' => $this->getField('realmName'), 'talenttree1' => $tDistrib[0],
// 'battlegroup' => Profiler::urlize($this->getField('battlegroup')), // was renamed to subregion somewhere around cata release 'talenttree2' => $tDistrib[1],
// 'battlegroupname' => $this->getField('battlegroup'), 'talenttree3' => $tDistrib[2],
'gearscore' => $this->getField('gearscore') 'talentspec' => $this->curTpl['talents']['active']
); );
if ($addInfo & PROFILEINFO_USER) if (!empty($this->curTpl['description']))
$data[$this->id]['published'] = (int)!!($this->getField('cuFlags') & PROFILER_CU_PUBLISHED); $data[$this->id]['description'] = $this->curTpl['description'];
// for the lv this determins if the link is profile=<id> or profile=<region>.<realm>.<name> if (!empty($this->curTpl['icon']))
if (!$this->isCustom()) $data[$this->id]['icon'] = $this->curTpl['icon'];
$data[$this->id]['region'] = Profiler::urlize($this->getField('region'));
if ($addInfo & PROFILEINFO_ARENA) if ($this->curTpl['cuFlags'] & PROFILE_CU_PUBLISHED)
{ $data[$this->id]['published'] = 1;
$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 if ($this->curTpl['cuFlags'] & PROFILE_CU_PINNED)
foreach ($reqCols as $col)
$data[$this->id][$col] = $this->getField($col);
if ($addInfo & PROFILEINFO_PROFILE)
{
if ($_ = $this->getField('description'))
$data[$this->id]['description'] = $_;
if ($_ = $this->getField('icon'))
$data[$this->id]['icon'] = $_;
}
if ($addInfo & PROFILEINFO_CHARACTER)
if ($_ = $this->getField('renameItr'))
$data[$this->id]['renameItr'] = $_;
if ($this->getField('cuFlags') & PROFILER_CU_PINNED)
$data[$this->id]['pinned'] = 1; $data[$this->id]['pinned'] = 1;
if ($this->getField('cuFlags') & PROFILER_CU_DELETED) if ($this->curTpl['cuFlags'] & PROFILE_CU_DELETED)
$data[$this->id]['deleted'] = 1; $data[$this->id]['deleted'] = 1;
} }
return array_values($data);
}
public function renderTooltip()
{
if (!$this->curTpl)
return [];
$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 = '<table>';
$x .= '<tr><td><b class="q">'.$name.'</b></td></tr>';
if ($g = $this->getField('guildname'))
$x .= '<tr><td>&lt;'.$g.'&gt;</td></tr>';
else if ($d = $this->getField('description'))
$x .= '<tr><td>'.$d.'</td></tr>';
$x .= '<tr><td>'.Lang::game('level').' '.$this->getField('level').' '.Lang::game('ra', $this->getField('race')).' '.Lang::game('cl', $this->getField('class')).'</td></tr>';
$x .= '</table>';
return $x;
}
public function getJSGlobals($addMask = 0)
{
$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; return $data;
} }
public function isCustom() public function renderTooltip($interactive = false)
{ {
return $this->getField('cuFlags') & PROFILER_CU_PROFILE; if (!$this->curTpl)
return [];
$x = '<table>';
$x .= '<tr><td><b class="q">'.$this->getField('name').'</b></td></tr>';
if ($g = $this->getField('name'))
$x .= '<tr><td>&lt;'.$g.'&gt; ('.$this->getField('guildrank').')</td></tr>';
else if ($d = $this->getField('description'))
$x .= '<tr><td>'.$d.'</td></tr>';
$x .= '<tr><td>'.Lang::game('level').' '.$this->getField('level').' '.Lang::game('ra', $this->curTpl['race']).' '.Lang::game('cl', $this->curTpl['classs']).'</td></tr>';
$x .= '</table>';
return $x;
} }
public function isVisibleToUser() public function getJSGlobals($addMask = 0) {}
private function getTalentDistribution()
{ {
if (!$this->isCustom() || User::isInGroup(U_GROUP_ADMIN | U_GROUP_BUREAU)) if (!empty($this->tDistribution))
return true; $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));
if ($this->getField('cuFlags') & PROFILER_CU_DELETED) $result = [];
return false; $start = 0;
foreach ($this->tDistribution[$this->curTpl['classId']] as $len)
if (User::$id == $this->getField('user'))
return true;
return (bool)($this->getField('cuFlags') & PROFILER_CU_PUBLISHED);
}
public function getIcon()
{
if ($_ = $this->getField('icon'))
return $_;
$str = 'chr_';
switch ($this->getField('race'))
{ {
case 1: $str .= 'human_'; break; $result[] = array_sum(str_split(substr($this->curTpl['talentString'], $start, $len)));
case 2: $str .= 'orc_'; break; $start += $len;
case 3: $str .= 'dwarf_'; break;
case 4: $str .= 'nightelf_'; break;
case 5: $str .= 'scourge_'; break;
case 6: $str .= 'tauren_'; break;
case 7: $str .= 'gnome_'; break;
case 8: $str .= 'troll_'; break;
case 10: $str .= 'bloodelf_'; break;
case 11: $str .= 'draenei_'; break;
} }
switch ($this->getField('gender')) return $result;
{
case 0: $str .= 'male_'; break;
case 1: $str .= 'female_'; break;
}
switch ($this->getField('class'))
{
case 1: $str .= 'warrior0'; break;
case 2: $str .= 'paladin0'; break;
case 3: $str .= 'hunter0'; break;
case 4: $str .= 'rogue0'; break;
case 5: $str .= 'priest0'; break;
case 6: $str .= 'deathknight0'; break;
case 7: $str .= 'shaman0'; break;
case 8: $str .= 'mage0'; break;
case 9: $str .= 'warlock0'; break;
case 11: $str .= 'druid0'; break;
}
$level = $this->getField('level');
if ($level > 59)
$str .= floor(($level - 60) / 10) + 2;
else
$str .= 1;
return $str;
} }
} }
class ProfileListFilter extends Filter class ProfileListFilter extends Filter
{ {
use TrProfilerFilter; public $extraOpts = [];
protected string $type = 'profiles'; protected $genericFilter = array(
protected 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 array $inputFields = array( protected function createSQLForCriterium(&$cr)
'cr' => [parent::V_RANGE, [1, 36], true ], // criteria ids
'crs' => [parent::V_LIST, [parent::ENUM_NONE, parent::ENUM_ANY, [0, 5000]], true ], // criteria operators
'crv' => [parent::V_REGEX, parent::PATTERN_CRV, true ], // criteria values
'na' => [parent::V_REGEX, parent::PATTERN_NAME, false], // name - only printable chars, no delimiter
'ma' => [parent::V_EQUAL, 1, false], // match any / all filter
'ex' => [parent::V_EQUAL, 'on', false], // only match exact
'si' => [parent::V_LIST, [SIDE_ALLIANCE, SIDE_HORDE], false], // side
'ra' => [parent::V_LIST, [[1, 8], 10, 11], true ], // race
'cl' => [parent::V_LIST, [[1, 9], 11], true ], // class
'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
'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 (in_array($cr[0], array_keys($this->genericFilter)))
if (!empty($this->criteria['cr']))
if (array_intersect($this->criteria['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] - the table is case sensitive. Since i don't want to destroy indizes, lets alter the search terms
if ($_v['na'])
{ {
$lower = $this->tokenizeString([$k.'.name'], Util::lower($_v['na']), $_v['ex'] == 'on', true); if ($genCR = $this->genericCriterion($cr))
$proper = $this->tokenizeString([$k.'.name'], Util::ucWords($_v['na']), $_v['ex'] == 'on', true); return $genCR;
$parts[] = ['OR', $lower, $proper]; unset($cr);
$this->error = true;
return [1];
} }
// side [list] switch ($cr[0])
if ($_v['si'] == SIDE_ALLIANCE) {
$parts[] = [$k.'.race', ChrRace::fromMask(ChrRace::MASK_ALLIANCE)]; default:
else if ($_v['si'] == SIDE_HORDE) break;
$parts[] = [$k.'.race', ChrRace::fromMask(ChrRace::MASK_HORDE)]; }
// race [list] unset($cr);
if ($_v['ra']) $this->error = 1;
$parts[] = [$k.'.race', $_v['ra']]; return [1];
}
// class [list] protected function createSQLForValues()
if ($_v['cl']) {
$parts[] = [$k.'.class', $_v['cl']]; $parts = [];
$_v = $this->fiData['v'];
// min level [int] // name
if ($_v['minle']) if (isset($_v['na']))
$parts[] = [$k.'.level', $_v['minle'], '>=']; if ($_ = $this->modularizeString(['name_loc'.User::$localeId]))
$parts[] = $_;
// max level [int]
if ($_v['maxle'])
$parts[] = [$k.'.level', $_v['maxle'], '<='];
return $parts; 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
{
if ($_ = $this->tokenizeString(['at.name'], $crv))
return ['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 ['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 $queryBase = 'SELECT `c`.*, `c`.`guid` AS ARRAY_KEY FROM characters c';
protected $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 $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 nonexistant 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 = ?d AND realmGUID IS NOT NULL AND name = ?', $r, $curTpl['name']) ?: 0;
// already saved as "pending rename"
if ($rnItr = DB::Aowow()->selectCell('SELECT renameItr FROM ?_profiler_profiles WHERE realm = ?d AND realmGUID = ?d', $r, $g))
$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 (?a)', array_keys($chars));
$talentSpells = DB::Aowow()->select('SELECT spell AS ARRAY_KEY, tab, `rank` FROM ?_talents WHERE class IN (?a)', array_unique($talentSpells));
foreach ($conditions as $c)
if (is_int($c))
$limit = $c;
$limit ??= Cfg::get('SQL_LIMIT_DEFAULT');
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($addInfoMask = 0, array $reqCols = [])
{
$data = parent::getListviewData($addInfoMask, $reqCols);
// not wanted on server list
foreach ($data as &$d)
unset($d['published']);
return $data;
}
public function initializeLocalEntries()
{
$baseData = $guildData = [];
foreach ($this->iterate() as $guid => $__)
{
$realmId = $this->getField('realm');
$guildGUID = $this->getField('guild');
$baseData[$guid] = array(
'realm' => $realmId,
'realmGUID' => $this->getField('guid'),
'name' => $this->getField('name'),
'renameItr' => $this->getField('renameItr'),
'race' => $this->getField('race'),
'class' => $this->getField('class'),
'level' => $this->getField('level'),
'gender' => $this->getField('gender'),
'guild' => $guildGUID ?: null,
'guildrank' => $guildGUID ? $this->getField('guildrank') : null,
'cuFlags' => PROFILER_CU_NEEDS_RESYNC
);
if ($guildGUID && empty($guildData[$realmId.'-'.$guildGUID]))
$guildData[$realmId.'-'.$guildGUID] = array(
'realm' => $realmId,
'realmGUID' => $guildGUID,
'name' => $this->getField('guildname'),
'nameUrl' => Profiler::urlize($this->getField('guildname')),
'cuFlags' => PROFILER_CU_NEEDS_RESYNC
);
}
// basic guild data (satisfying table constraints)
if ($guildData)
{
foreach (Util::createSqlBatchInsert($guildData) as $ins)
DB::Aowow()->query('INSERT INTO ?_profiler_guild (?#) VALUES '.$ins.' ON DUPLICATE KEY UPDATE `id` = `id`', array_keys(reset($guildData)));
// merge back local ids
$localGuilds = DB::Aowow()->selectCol('SELECT realm AS ARRAY_KEY, realmGUID AS ARRAY_KEY2, id FROM ?_profiler_guild WHERE realm IN (?a) AND realmGUID IN (?a)',
array_column($guildData, 'realm'), array_column($guildData, 'realmGUID')
);
foreach ($baseData as &$bd)
if ($bd['guild'])
$bd['guild'] = $localGuilds[$bd['realm']][$bd['guild']];
}
// basic char data (enough for tooltips)
if ($baseData)
{
foreach ($baseData as $ins)
DB::Aowow()->query('INSERT INTO ?_profiler_profiles (?#) VALUES (?a) ON DUPLICATE KEY UPDATE `name` = ?, `renameItr` = ?d', array_keys($ins), array_values($ins), $ins['name'], $ins['renameItr']);
// merge back local ids
$localIds = DB::Aowow()->select(
'SELECT CONCAT(realm, ":", realmGUID) AS ARRAY_KEY, id, gearscore FROM ?_profiler_profiles WHERE (cuFlags & ?d) = 0 AND realm IN (?a) AND realmGUID IN (?a)',
PROFILER_CU_PROFILE,
array_column($baseData, 'realm'),
array_column($baseData, 'realmGUID')
);
foreach ($this->iterate() as $guid => &$_curTpl)
if (isset($localIds[$guid]))
$_curTpl = array_merge($_curTpl, $localIds[$guid]);
}
}
}
class LocalProfileList extends ProfileList
{
protected $queryBase = 'SELECT p.*, p.id AS ARRAY_KEY FROM ?_profiler_profiles p';
protected $queryOpts = array(
'p' => [['g'], 'g' => 'p.id'],
'ap' => ['j' => ['?_account_profiles ap ON ap.profileId = p.id', true], 's' => ', (IFNULL(ap.ExtraFlags, 0) | p.cuFlags) AS cuFlags'],
'atm' => ['j' => ['?_profiler_arena_team_member atm ON atm.profileId = p.id', true], 's' => ', atm.captain, atm.personalRating AS rating, atm.seasonGames, atm.seasonWins'],
'at' => [['atm'], 'j' => ['?_profiler_arena_team at ON at.id = atm.arenaTeamId', true], 's' => ', at.type'],
'g' => ['j' => ['?_profiler_guild g ON g.id = p.guild', true], 's' => ', g.name AS guildname']
);
public function __construct(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, 'AND');
$conditions = ['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()
{
$url = '?profile=';
if ($this->isCustom())
return $url.$this->getField('id');
return $url.implode('.', array(
Profiler::urlize($this->getField('region')),
Profiler::urlize($this->getField('realmName')),
urlencode($this->getField('name'))
));
}
}
?> ?>

View File

@@ -1,16 +1,13 @@
<?php <?php
namespace Aowow;
if (!defined('AOWOW_REVISION')) if (!defined('AOWOW_REVISION'))
die('illegal access'); die('illegal access');
class QuestList extends BaseType class QuestList extends BaseType
{ {
public static $type = Type::QUEST; public static $type = TYPE_QUEST;
public static $brickFile = 'quest'; public static $brickFile = 'quest';
public static $dataTable = '?_quests';
public $requires = []; public $requires = [];
public $rewards = []; public $rewards = [];
@@ -21,10 +18,9 @@ class QuestList extends BaseType
'q' => [], 'q' => [],
'rsc' => ['j' => '?_spell rsc ON q.rewardSpellCast = rsc.id'], // limit rewardSpellCasts '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..? '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 = []) public function __construct($conditions = [], $miscData = null)
{ {
parent::__construct($conditions, $miscData); parent::__construct($conditions, $miscData);
@@ -37,7 +33,7 @@ class QuestList extends BaseType
$_curTpl['cat1'] = $_curTpl['zoneOrSort']; // should probably be in a method... $_curTpl['cat1'] = $_curTpl['zoneOrSort']; // should probably be in a method...
$_curTpl['cat2'] = 0; $_curTpl['cat2'] = 0;
foreach (Game::$questClasses as $k => $arr) foreach (Util::$questClasses as $k => $arr)
{ {
if (in_array($_curTpl['cat1'], $arr)) if (in_array($_curTpl['cat1'], $arr))
{ {
@@ -51,18 +47,18 @@ class QuestList extends BaseType
for ($i = 1; $i < 7; $i++) for ($i = 1; $i < 7; $i++)
{ {
if ($_ = $_curTpl['reqItemId'.$i]) if ($_ = $_curTpl['reqItemId'.$i])
$requires[Type::ITEM][] = $_; $requires[TYPE_ITEM][] = $_;
if ($i > 4) if ($i > 4)
continue; continue;
if ($_curTpl['reqNpcOrGo'.$i] > 0) if ($_curTpl['reqNpcOrGo'.$i] > 0)
$requires[Type::NPC][] = $_curTpl['reqNpcOrGo'.$i]; $requires[TYPE_NPC][] = $_curTpl['reqNpcOrGo'.$i];
else if ($_curTpl['reqNpcOrGo'.$i] < 0) else if ($_curTpl['reqNpcOrGo'.$i] < 0)
$requires[Type::OBJECT][] = -$_curTpl['reqNpcOrGo'.$i]; $requires[TYPE_OBJECT][] = -$_curTpl['reqNpcOrGo'.$i];
if ($_ = $_curTpl['reqSourceItemId'.$i]) if ($_ = $_curTpl['reqSourceItemId'.$i])
$requires[Type::ITEM][] = $_; $requires[TYPE_ITEM][] = $_;
} }
if ($requires) if ($requires)
$this->requires[$id] = $requires; $this->requires[$id] = $requires;
@@ -72,24 +68,24 @@ class QuestList extends BaseType
$choices = []; $choices = [];
if ($_ = $_curTpl['rewardTitleId']) if ($_ = $_curTpl['rewardTitleId'])
$rewards[Type::TITLE][] = $_; $rewards[TYPE_TITLE][] = $_;
if ($_ = $_curTpl['rewardHonorPoints']) if ($_ = $_curTpl['rewardHonorPoints'])
$rewards[Type::CURRENCY][104] = $_; $rewards[TYPE_CURRENCY][104] = $_;
if ($_ = $_curTpl['rewardArenaPoints']) if ($_ = $_curTpl['rewardArenaPoints'])
$rewards[Type::CURRENCY][103] = $_; $rewards[TYPE_CURRENCY][103] = $_;
for ($i = 1; $i < 7; $i++) for ($i = 1; $i < 7; $i++)
{ {
if ($_ = $_curTpl['rewardChoiceItemId'.$i]) if ($_ = $_curTpl['rewardChoiceItemId'.$i])
$choices[Type::ITEM][$_] = $_curTpl['rewardChoiceItemCount'.$i]; $choices[TYPE_ITEM][$_] = $_curTpl['rewardChoiceItemCount'.$i];
if ($i > 5) if ($i > 5)
continue; continue;
if ($_ = $_curTpl['rewardFactionId'.$i]) if ($_ = $_curTpl['rewardFactionId'.$i])
$rewards[Type::FACTION][$_] = $_curTpl['rewardFactionValue'.$i]; $rewards[TYPE_FACTION][$_] = $_curTpl['rewardFactionValue'.$i];
if ($i > 4) if ($i > 4)
continue; continue;
@@ -98,9 +94,9 @@ class QuestList extends BaseType
{ {
$qty = $_curTpl['rewardItemCount'.$i]; $qty = $_curTpl['rewardItemCount'.$i];
if (in_array($_, $currencies)) if (in_array($_, $currencies))
$rewards[Type::CURRENCY][array_search($_, $currencies)] = $qty; $rewards[TYPE_CURRENCY][array_search($_, $currencies)] = $qty;
else else
$rewards[Type::ITEM][$_] = $qty; $rewards[TYPE_ITEM][$_] = $qty;
} }
} }
if ($rewards) if ($rewards)
@@ -114,7 +110,7 @@ class QuestList extends BaseType
// static use START // static use START
public static function getName($id) public static function getName($id)
{ {
$n = DB::Aowow()->SelectRow('SELECT name_loc0, name_loc2, name_loc3, name_loc4, name_loc6, name_loc8 FROM ?_quests WHERE id = ?d', $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'); return Util::localizedString($n, 'name');
} }
// static use END // static use END
@@ -124,18 +120,12 @@ class QuestList extends BaseType
return $this->curTpl['flags'] & QUEST_FLAG_REPEATABLE || $this->curTpl['specialFlags'] & QUEST_FLAG_SPECIAL_REPEATABLE; return $this->curTpl['flags'] & QUEST_FLAG_REPEATABLE || $this->curTpl['specialFlags'] & QUEST_FLAG_SPECIAL_REPEATABLE;
} }
public function isDaily() public function isDaily($strict = false)
{ {
if ($this->curTpl['flags'] & QUEST_FLAG_DAILY) if ($strict)
return 1; return $this->curTpl['flags'] & QUEST_FLAG_DAILY;
else
if ($this->curTpl['flags'] & QUEST_FLAG_WEEKLY) return $this->curTpl['flags'] & (QUEST_FLAG_DAILY | QUEST_FLAG_WEEKLY) || $this->curTpl['specialFlags'] & QUEST_FLAG_SPECIAL_MONTHLY;
return 2;
if ($this->curTpl['specialFlags'] & QUEST_FLAG_SPECIAL_MONTHLY)
return 3;
return 0;
} }
// using reqPlayerKills and rewardHonor as a crutch .. has TC this even implemented..? // using reqPlayerKills and rewardHonor as a crutch .. has TC this even implemented..?
@@ -150,18 +140,15 @@ class QuestList extends BaseType
return in_array($this->getField('zoneOrSortBak'), [-22, -284, -366, -369, -370, -376, -374]) && !$this->isRepeatable(); return in_array($this->getField('zoneOrSortBak'), [-22, -284, -366, -369, -370, -376, -374]) && !$this->isRepeatable();
} }
public function getSourceData(int $id = 0) : array public function getSourceData()
{ {
$data = []; $data = [];
foreach ($this->iterate() as $__) foreach ($this->iterate() as $__)
{ {
if ($id && $id != $this->id)
continue;
$data[$this->id] = array( $data[$this->id] = array(
"n" => $this->getField('name', true), "n" => $this->getField('name', true),
"t" => Type::QUEST, "t" => TYPE_QUEST,
"ti" => $this->id, "ti" => $this->id,
"c" => $this->curTpl['cat1'], "c" => $this->curTpl['cat1'],
"c2" => $this->curTpl['cat2'] "c2" => $this->curTpl['cat2']
@@ -177,10 +164,10 @@ class QuestList extends BaseType
foreach ($this->iterate() as $__) foreach ($this->iterate() as $__)
{ {
if (!(ChrRace::sideFromMask($this->curTpl['reqRaceMask']) & $side)) if (!(Util::sideByRaceMask($this->curTpl['reqRaceMask']) & $side))
continue; continue;
[$series, $first] = DB::Aowow()->SelectRow( 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', '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 $this->id
); );
@@ -213,22 +200,22 @@ class QuestList extends BaseType
'id' => $this->id, 'id' => $this->id,
'level' => $this->curTpl['level'], 'level' => $this->curTpl['level'],
'reqlevel' => $this->curTpl['minLevel'], 'reqlevel' => $this->curTpl['minLevel'],
'name' => Lang::unescapeUISequences($this->getField('name', true), Lang::FMT_RAW), 'name' => $this->getField('name', true),
'side' => ChrRace::sideFromMask($this->curTpl['reqRaceMask']), 'side' => Util::sideByRaceMask($this->curTpl['reqRaceMask']),
'wflags' => 0x0, 'wflags' => 0x0,
'xp' => $this->curTpl['rewardXP'] 'xp' => $this->curTpl['rewardXP']
); );
if (!empty($this->rewards[$this->id][Type::CURRENCY])) if (!empty($this->rewards[$this->id][TYPE_CURRENCY]))
foreach ($this->rewards[$this->id][Type::CURRENCY] as $iId => $qty) foreach ($this->rewards[$this->id][TYPE_CURRENCY] as $iId => $qty)
$data[$this->id]['currencyrewards'][] = [$iId, $qty]; $data[$this->id]['currencyrewards'][] = [$iId, $qty];
if (!empty($this->rewards[$this->id][Type::ITEM])) if (!empty($this->rewards[$this->id][TYPE_ITEM]))
foreach ($this->rewards[$this->id][Type::ITEM] as $iId => $qty) foreach ($this->rewards[$this->id][TYPE_ITEM] as $iId => $qty)
$data[$this->id]['itemrewards'][] = [$iId, $qty]; $data[$this->id]['itemrewards'][] = [$iId, $qty];
if (!empty($this->choices[$this->id][Type::ITEM])) if (!empty($this->choices[$this->id][TYPE_ITEM]))
foreach ($this->choices[$this->id][Type::ITEM] as $iId => $qty) foreach ($this->choices[$this->id][TYPE_ITEM] as $iId => $qty)
$data[$this->id]['itemchoices'][] = [$iId, $qty]; $data[$this->id]['itemchoices'][] = [$iId, $qty];
if ($_ = $this->curTpl['rewardTitleId']) if ($_ = $this->curTpl['rewardTitleId'])
@@ -240,8 +227,8 @@ class QuestList extends BaseType
if ($_ = $this->curTpl['reqClassMask']) if ($_ = $this->curTpl['reqClassMask'])
$data[$this->id]['reqclass'] = $_; $data[$this->id]['reqclass'] = $_;
if ($_ = ($this->curTpl['reqRaceMask'] & ChrRace::MASK_ALL)) if ($_ = ($this->curTpl['reqRaceMask'] & RACE_MASK_ALL))
if ((($_ & ChrRace::MASK_ALLIANCE) != ChrRace::MASK_ALLIANCE) && (($_ & ChrRace::MASK_HORDE) != ChrRace::MASK_HORDE)) if ((($_ & RACE_MASK_ALLIANCE) != RACE_MASK_ALLIANCE) && (($_ & RACE_MASK_HORDE) != RACE_MASK_HORDE))
$data[$this->id]['reqrace'] = $_; $data[$this->id]['reqrace'] = $_;
if ($_ = $this->curTpl['rewardOrReqMoney']) if ($_ = $this->curTpl['rewardOrReqMoney'])
@@ -254,8 +241,6 @@ class QuestList extends BaseType
// if ($this->isRepeatable()) // dafuque..? says repeatable and is used as 'disabled'..? // if ($this->isRepeatable()) // dafuque..? says repeatable and is used as 'disabled'..?
// $data[$this->id]['wflags'] |= QUEST_CU_REPEATABLE; // $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) if ($this->curTpl['flags'] & QUEST_FLAG_DAILY)
{ {
@@ -318,7 +303,7 @@ class QuestList extends BaseType
if (!$this->curTpl) if (!$this->curTpl)
return null; return null;
$title = Lang::unescapeUISequences(Util::htmlEscape($this->getField('name', true)), Lang::FMT_HTML); $title = Util::jsEscape($this->getField('name', true));
$level = $this->curTpl['level']; $level = $this->curTpl['level'];
if ($level < 0) if ($level < 0)
$level = 0; $level = 0;
@@ -337,7 +322,7 @@ class QuestList extends BaseType
$x .= '<table><tr><td><b class="q">'.$title.'</b></td></tr></table>'; $x .= '<table><tr><td><b class="q">'.$title.'</b></td></tr></table>';
$x .= '<table><tr><td><br />'.$this->parseText('objectives', false); $x .= '<table><tr><td><br />'.$this->parseText('objectives');
$xReq = ''; $xReq = '';
@@ -353,9 +338,9 @@ class QuestList extends BaseType
if ($ot) if ($ot)
$name = $ot; $name = $ot;
else else
$name = $rng > 0 ? CreatureList::getName($rng) : Lang::unescapeUISequences(GameObjectList::getName(-$rng), Lang::FMT_HTML); $name = $rng > 0 ? CreatureList::getName($rng) : GameObjectList::getName(-$rng);
$xReq .= '<br /> - '.$name.($rngQty > 1 ? ' x '.$rngQty : null); $xReq .= '<br /> - '.Util::jsEscape($name).($rngQty > 1 ? ' x '.$rngQty : null);
} }
for ($i = 1; $i < 7; $i++) for ($i = 1; $i < 7; $i++)
@@ -366,11 +351,11 @@ class QuestList extends BaseType
if (!$ri || $riQty < 1) if (!$ri || $riQty < 1)
continue; continue;
$xReq .= '<br /> - '.Lang::unescapeUISequences(ItemList::getName($ri), Lang::FMT_HTML).($riQty > 1 ? ' x '.$riQty : null); $xReq .= '<br /> - '.Util::jsEscape(ItemList::getName($ri)).($riQty > 1 ? ' x '.$riQty : null);
} }
if ($et = $this->getField('end', true)) if ($et = $this->getField('end', true))
$xReq .= '<br /> - '.$et; $xReq .= '<br /> - '.Util::jsEscape($et);
if ($_ = $this->getField('rewardOrReqMoney')) if ($_ = $this->getField('rewardOrReqMoney'))
if ($_ < 0) if ($_ < 0)
@@ -395,31 +380,33 @@ class QuestList extends BaseType
// items // items
for ($i = 1; $i < 5; $i++) for ($i = 1; $i < 5; $i++)
if ($this->curTpl['rewardItemId'.$i] > 0) if ($this->curTpl['rewardItemId'.$i] > 0)
$data[Type::ITEM][$this->curTpl['rewardItemId'.$i]] = $this->curTpl['rewardItemId'.$i]; $data[TYPE_ITEM][$this->curTpl['rewardItemId'.$i]] = $this->curTpl['rewardItemId'.$i];
for ($i = 1; $i < 7; $i++) for ($i = 1; $i < 7; $i++)
if ($this->curTpl['rewardChoiceItemId'.$i] > 0) if ($this->curTpl['rewardChoiceItemId'.$i] > 0)
$data[Type::ITEM][$this->curTpl['rewardChoiceItemId'.$i]] = $this->curTpl['rewardChoiceItemId'.$i]; $data[TYPE_ITEM][$this->curTpl['rewardChoiceItemId'.$i]] = $this->curTpl['rewardChoiceItemId'.$i];
// spells // spells
if ($this->curTpl['rewardSpell'] > 0) if ($this->curTpl['rewardSpell'] > 0)
$data[Type::SPELL][$this->curTpl['rewardSpell']] = $this->curTpl['rewardSpell']; $data[TYPE_SPELL][$this->curTpl['rewardSpell']] = $this->curTpl['rewardSpell'];
if ($this->curTpl['rewardSpellCast'] > 0) if ($this->curTpl['rewardSpellCast'] > 0)
$data[Type::SPELL][$this->curTpl['rewardSpellCast']] = $this->curTpl['rewardSpellCast']; $data[TYPE_SPELL][$this->curTpl['rewardSpellCast']] = $this->curTpl['rewardSpellCast'];
// titles // titles
if ($this->curTpl['rewardTitleId'] > 0) if ($this->curTpl['rewardTitleId'] > 0)
$data[Type::TITLE][$this->curTpl['rewardTitleId']] = $this->curTpl['rewardTitleId']; $data[TYPE_TITLE][$this->curTpl['rewardTitleId']] = $this->curTpl['rewardTitleId'];
// currencies // currencies
if (!empty($this->rewards[$this->id][Type::CURRENCY])) if (!empty($this->rewards[$this->id][TYPE_CURRENCY]))
foreach ($this->rewards[$this->id][Type::CURRENCY] as $id => $__) {
$data[Type::CURRENCY][$id] = $id; $_ = $this->rewards[$this->id][TYPE_CURRENCY];
$data[TYPE_CURRENCY] = array_combine(array_keys($_), array_keys($_));
}
} }
if ($addMask & GLOBALINFO_SELF) if ($addMask & GLOBALINFO_SELF)
$data[Type::QUEST][$this->id] = ['name' => $this->getField('name', true)]; $data[TYPE_QUEST][$this->id] = ['name' => $this->getField('name', true)];
} }
return $data; return $data;
@@ -429,295 +416,292 @@ class QuestList extends BaseType
class QuestListFilter extends Filter class QuestListFilter extends Filter
{ {
protected string $type = 'quests'; public $extraOpts = [];
protected array $enums = array( 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 => parent::ENUM_CLASSS, // classspecific 37 => [null, 1, 2, 3, 4, 5, 6, 7, 8, 9, null, 11, true, false],
38 => parent::ENUM_RACE, // racespecific 38 => [null, 1, 2, 3, 4, 5, 6, 7, 8, null, 10, 11, true, false],
9 => parent::ENUM_FACTION, // objectiveearnrepwith );
33 => parent::ENUM_EVENT, // relatedevent protected $genericFilter = array(
43 => parent::ENUM_CURRENCY, // currencyrewarded 27 => [FILTER_CR_FLAG, 'flags', QUEST_FLAG_DAILY ], // daily
1 => parent::ENUM_FACTION, // increasesrepwith 28 => [FILTER_CR_FLAG, 'flags', QUEST_FLAG_WEEKLY ], // weekly
10 => parent::ENUM_FACTION // decreasesrepwith 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 array $genericFilter = array( protected function createSQLForCriterium(&$cr)
1 => [parent::CR_CALLBACK, 'cbReputation', '>', null], // increasesrepwith {
2 => [parent::CR_NUMERIC, 'rewardXP', NUM_CAST_INT ], // experiencegained if (in_array($cr[0], array_keys($this->genericFilter)))
3 => [parent::CR_NUMERIC, 'rewardOrReqMoney', NUM_CAST_INT ], // moneyrewarded {
4 => [parent::CR_CALLBACK, 'cbSpellRewards', null, null], // spellrewarded [yn] if ($genCr = $this->genericCriterion($cr))
5 => [parent::CR_FLAG, 'flags', QUEST_FLAG_SHARABLE ], // sharable return $genCr;
6 => [parent::CR_NUMERIC, 'timeLimit', NUM_CAST_INT ], // timer
7 => [parent::CR_FLAG, 'cuFlags', QUEST_CU_FIRST_SERIES ], // firstquestseries
9 => [parent::CR_CALLBACK, 'cbEarnReputation', null, null], // objectiveearnrepwith [enum]
10 => [parent::CR_CALLBACK, 'cbReputation', '<', null], // decreasesrepwith
11 => [parent::CR_NUMERIC, 'suggestedPlayers', NUM_CAST_INT ], // suggestedplayers
15 => [parent::CR_FLAG, 'cuFlags', QUEST_CU_LAST_SERIES ], // lastquestseries
16 => [parent::CR_FLAG, 'cuFlags', QUEST_CU_PART_OF_SERIES ], // partseries
18 => [parent::CR_FLAG, 'cuFlags', CUSTOM_HAS_SCREENSHOT ], // hasscreenshots
19 => [parent::CR_CALLBACK, 'cbQuestRelation', 0x1, null], // startsfrom [enum]
21 => [parent::CR_CALLBACK, 'cbQuestRelation', 0x2, null], // endsat [enum]
22 => [parent::CR_CALLBACK, 'cbItemRewards', null, null], // itemrewards [op] [int]
23 => [parent::CR_CALLBACK, 'cbItemChoices', null, null], // itemchoices [op] [int]
24 => [parent::CR_CALLBACK, 'cbLacksStartEnd', null, null], // lacksstartend [yn]
25 => [parent::CR_FLAG, 'cuFlags', CUSTOM_HAS_COMMENT ], // hascomments
27 => [parent::CR_FLAG, 'flags', QUEST_FLAG_DAILY ], // daily
28 => [parent::CR_FLAG, 'flags', QUEST_FLAG_WEEKLY ], // weekly
29 => [parent::CR_CALLBACK, 'cbRepeatable', null ], // repeatable
30 => [parent::CR_NUMERIC, 'id', NUM_CAST_INT, true], // id
33 => [parent::CR_ENUM, 'e.holidayId', true, true], // relatedevent
34 => [parent::CR_CALLBACK, 'cbAvailable', null, null], // availabletoplayers [yn]
36 => [parent::CR_FLAG, 'cuFlags', CUSTOM_HAS_VIDEO ], // hasvideos
37 => [parent::CR_CALLBACK, 'cbClassSpec', null, null], // classspecific [enum]
38 => [parent::CR_CALLBACK, 'cbRaceSpec', null, null], // racespecific [enum]
42 => [parent::CR_STAFFFLAG, 'flags' ], // flags
43 => [parent::CR_CALLBACK, 'cbCurrencyReward', null, null], // currencyrewarded [enum]
44 => [parent::CR_CALLBACK, 'cbLoremaster', null, null], // countsforloremaster_stc [yn]
45 => [parent::CR_BOOLEAN, 'rewardTitleId' ] // titlerewarded
);
protected array $inputFields = array( unset($cr);
'cr' => [parent::V_RANGE, [1, 45], true ], // criteria ids $this->error = true;
'crs' => [parent::V_LIST, [parent::ENUM_NONE, parent::ENUM_ANY, [0, 99999]], true ], // criteria operators return [1];
'crv' => [parent::V_REGEX, parent::PATTERN_INT, true ], // criteria values - only numerals }
'na' => [parent::V_REGEX, parent::PATTERN_NAME, false], // name / text - only printable chars, no delimiter
'ex' => [parent::V_EQUAL, 'on', false], // also match subname
'ma' => [parent::V_EQUAL, 1, false], // match any / all filter
'minle' => [parent::V_RANGE, [1, 99], false], // min quest level
'maxle' => [parent::V_RANGE, [1, 99], false], // max quest level
'minrl' => [parent::V_RANGE, [1, 99], false], // min required level
'maxrl' => [parent::V_RANGE, [1, 99], false], // max required level
'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 = []; 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;
protected function createSQLForValues() : array $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 = []; $parts = [];
$_v = $this->values; $_v = $this->fiData['v'];
// name // name
if ($_v['na']) if (isset($_v['na']))
{ {
$_ = []; $_ = [];
if ($_v['ex'] == 'on') if (isset($_v['ex']) && $_v['ex'] == 'on')
$_ = $this->tokenizeString(['name_loc'.Lang::getLocale()->value, 'objectives_loc'.Lang::getLocale()->value, 'details_loc'.Lang::getLocale()->value]); $_ = $this->modularizeString(['name_loc'.User::$localeId, 'objectives_loc'.User::$localeId, 'details_loc'.User::$localeId]);
else else
$_ = $this->tokenizeString(['name_loc'.Lang::getLocale()->value]); $_ = $this->modularizeString(['name_loc'.User::$localeId]);
if ($_) if ($_)
$parts[] = $_; $parts[] = $_;
} }
// level min // level min
if ($_v['minle']) if (isset($_v['minle']))
$parts[] = ['level', $_v['minle'], '>=']; // not considering quests that are always at player level (-1) {
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 // level max
if ($_v['maxle']) if (isset($_v['maxle']))
$parts[] = ['level', $_v['maxle'], '<=']; {
if (is_int($_v['maxle']) && $_v['maxle'] > 0)
$parts[] = ['level', $_v['maxle'], '<='];
else
unset($_v['maxle']);
}
// reqLevel min // reqLevel min
if ($_v['minrl']) if (isset($_v['minrl']))
$parts[] = ['minLevel', $_v['minrl'], '>=']; // ignoring maxLevel {
if (is_int($_v['minrl']) && $_v['minrl'] > 0)
$parts[] = ['minLevel', $_v['minrl'], '>='];// ignoring maxLevel
else
unset($_v['minrl']);
}
// reqLevel max // reqLevel max
if ($_v['maxrl']) if (isset($_v['maxrl']))
$parts[] = ['minLevel', $_v['maxrl'], '<=']; // ignoring maxLevel {
if (is_int($_v['maxrl']) && $_v['maxrl'] > 0)
$parts[] = ['minLevel', $_v['maxrl'], '<='];// ignoring maxLevel
else
unset($_v['maxrl']);
}
// side // side
if ($_v['si']) if (isset($_v['si']))
{ {
$excl = [['reqRaceMask', ChrRace::MASK_ALL, '&'], ChrRace::MASK_ALL, '!']; $ex = [['reqRaceMask', RACE_MASK_ALL, '&'], RACE_MASK_ALL, '!'];
$incl = ['OR', ['reqRaceMask', 0], [['reqRaceMask', ChrRace::MASK_ALL, '&'], ChrRace::MASK_ALL]]; $notEx = ['OR', ['reqRaceMask', 0], [['reqRaceMask', RACE_MASK_ALL, '&'], RACE_MASK_ALL]];
$parts[] = match ($_v['si']) switch ($_v['si'])
{ {
SIDE_BOTH => $incl, case 3:
SIDE_HORDE => ['OR', $incl, ['reqRaceMask', ChrRace::MASK_HORDE, '&']], $parts[] = $notEx;
-SIDE_HORDE => ['AND', $excl, ['reqRaceMask', ChrRace::MASK_HORDE, '&']], break;
SIDE_ALLIANCE => ['OR', $incl, ['reqRaceMask', ChrRace::MASK_ALLIANCE, '&']], case 2:
-SIDE_ALLIANCE => ['AND', $excl, ['reqRaceMask', ChrRace::MASK_ALLIANCE, '&']] $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] // type [list]
if ($_v['ty'] !== null) if (isset($_v['ty']))
$parts[] = ['type', $_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; 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, $this->enums[$cr]))
return null;
if ($_ = DB::Aowow()->selectRow('SELECT * FROM ?_factions WHERE `id` = ?d', $crs))
$this->fiReputationCols[] = [$crs, Util::localizedString($_, 'name')];
return [
'OR',
['AND', ['rewardFactionId1', $crs], ['rewardFactionValue1', 0, $sign]],
['AND', ['rewardFactionId2', $crs], ['rewardFactionValue2', 0, $sign]],
['AND', ['rewardFactionId3', $crs], ['rewardFactionValue3', 0, $sign]],
['AND', ['rewardFactionId4', $crs], ['rewardFactionValue4', 0, $sign]],
['AND', ['rewardFactionId5', $crs], ['rewardFactionValue5', 0, $sign]]
];
}
protected function cbQuestRelation(int $cr, int $crs, string $crv, $flags) : ?array
{
return match ($crs)
{
Type::NPC,
Type::OBJECT,
Type::ITEM => ['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, $this->enums[$cr]))
return null;
return [
'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 cbRepeatable(int $cr, int $crs, string $crv) : ?array
{
if (!$this->int2Bool($crs))
return null;
if ($crs)
return ['OR', ['flags', QUEST_FLAG_REPEATABLE, '&'], ['specialFlags', QUEST_FLAG_SPECIAL_REPEATABLE, '&']];
else
return ['AND', [['flags', QUEST_FLAG_REPEATABLE, '&'], 0], [['specialFlags', QUEST_FLAG_SPECIAL_REPEATABLE, '&'], 0]];
}
protected function cbItemChoices(int $cr, int $crs, string $crv) : ?array
{
if (!Util::checkNumeric($crv, NUM_CAST_INT) || !$this->int2Op($crs))
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 ['AND', ['zoneOrSort', 0, '>'], [['flags', QUEST_FLAG_DAILY | QUEST_FLAG_WEEKLY | QUEST_FLAG_REPEATABLE, '&'], 0], [['specialFlags', QUEST_FLAG_SPECIAL_REPEATABLE | QUEST_FLAG_SPECIAL_MONTHLY, '&'], 0]];
else
return ['OR', ['zoneOrSort', 0, '<'], ['flags', QUEST_FLAG_DAILY | QUEST_FLAG_WEEKLY | QUEST_FLAG_REPEATABLE, '&'], ['specialFlags', QUEST_FLAG_SPECIAL_REPEATABLE | QUEST_FLAG_SPECIAL_MONTHLY, '&']];
}
protected function cbSpellRewards(int $cr, int $crs, string $crv) : ?array
{
if (!$this->int2Bool($crs))
return null;
if ($crs)
return ['OR', ['sourceSpellId', 0, '>'], ['rewardSpell', 0, '>'], ['rsc.effect1Id', SpellList::EFFECTS_TEACH], ['rsc.effect2Id', SpellList::EFFECTS_TEACH], ['rsc.effect3Id', SpellList::EFFECTS_TEACH]];
else
return ['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 ['OR', ['reqFactionId1', 0, '>'], ['reqFactionId2', 0, '>']];
else if ($crs == parent::ENUM_NONE)
return ['AND', ['reqFactionId1', 0], ['reqFactionId2', 0]];
else if (in_array($crs, $this->enums[$cr]))
return ['OR', ['reqFactionId1', $crs], ['reqFactionId2', $crs]];
return null;
}
protected function cbClassSpec(int $cr, int $crs, string $crv) : ?array
{
if (!isset($this->enums[$cr][$crs]))
return null;
$_ = $this->enums[$cr][$crs];
if ($_ === true)
return ['AND', ['reqClassMask', 0, '!'], [['reqClassMask', ChrClass::MASK_ALL, '&'], ChrClass::MASK_ALL, '!']];
else if ($_ === false)
return ['OR', ['reqClassMask', 0], [['reqClassMask', ChrClass::MASK_ALL, '&'], ChrClass::MASK_ALL]];
else if (is_int($_))
return ['AND', ['reqClassMask', ChrClass::from($_)->toMask(), '&'], [['reqClassMask', ChrClass::MASK_ALL, '&'], ChrClass::MASK_ALL, '!']];
return null;
}
protected function cbRaceSpec(int $cr, int $crs, string $crv) : ?array
{
if (!isset($this->enums[$cr][$crs]))
return null;
$_ = $this->enums[$cr][$crs];
if ($_ === true)
return ['AND', ['reqRaceMask', 0, '!'], [['reqRaceMask', ChrRace::MASK_ALL, '&'], ChrRace::MASK_ALL, '!'], [['reqRaceMask', ChrRace::MASK_ALLIANCE, '&'], ChrRace::MASK_ALLIANCE, '!'], [['reqRaceMask', ChrRace::MASK_HORDE, '&'], ChrRace::MASK_HORDE, '!']];
else if ($_ === false)
return ['OR', ['reqRaceMask', 0], ['reqRaceMask', ChrRace::MASK_ALL], ['reqRaceMask', ChrRace::MASK_ALLIANCE], ['reqRaceMask', ChrRace::MASK_HORDE]];
else if (is_int($_))
return ['AND', ['reqRaceMask', ChrRace::from($_)->toMask(), '&'], [['reqRaceMask', ChrRace::MASK_ALLIANCE, '&'], ChrRace::MASK_ALLIANCE, '!'], [['reqRaceMask', ChrRace::MASK_HORDE, '&'], ChrRace::MASK_HORDE, '!']];
return 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, '!'];
}
} }

View File

@@ -1,26 +1,23 @@
<?php <?php
namespace Aowow;
if (!defined('AOWOW_REVISION')) if (!defined('AOWOW_REVISION'))
die('illegal access'); die('illegal access');
class SkillList extends BaseType class SkillList extends BaseType
{ {
public static $type = Type::SKILL; public static $type = TYPE_SKILL;
public static $brickFile = 'skill'; public static $brickFile = 'skill';
public static $dataTable = '?_skillline';
protected $queryBase = 'SELECT sl.*, sl.id AS ARRAY_KEY FROM ?_skillline sl'; protected $queryBase = 'SELECT *, sl.id AS ARRAY_KEY FROM ?_skillline sl';
protected $queryOpts = array( protected $queryOpts = array(
'sl' => [['ic']], 'sl' => [['si']],
'ic' => ['j' => ['?_icons ic ON ic.id = sl.iconId', true], 's' => ', ic.name AS iconString'], 'si' => ['j' => '?_icons si ON si.id = sl.iconId', 's' => ', si.iconString'],
); );
public function __construct(array $conditions = [], array $miscData = []) public function __construct($conditions = [])
{ {
parent::__construct($conditions, $miscData); parent::__construct($conditions);
// post processing // post processing
foreach ($this->iterate() as &$_curTpl) foreach ($this->iterate() as &$_curTpl)
@@ -34,15 +31,12 @@ class SkillList extends BaseType
while (count($_) < 5) while (count($_) < 5)
$_[] = 0; $_[] = 0;
} }
if (!$_curTpl['iconId'])
$_curTpl['iconString'] = DEFAULT_ICON;
} }
} }
public static function getName($id) public static function getName($id)
{ {
$n = DB::Aowow()->SelectRow('SELECT name_loc0, name_loc2, name_loc3, name_loc4, name_loc6, name_loc8 FROM ?_skillline WHERE id = ?d', $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'); return Util::localizedString($n, 'name');
} }
@@ -56,11 +50,11 @@ class SkillList extends BaseType
'category' => $this->curTpl['typeCat'], 'category' => $this->curTpl['typeCat'],
'categorybak' => $this->curTpl['categoryId'], 'categorybak' => $this->curTpl['categoryId'],
'id' => $this->id, 'id' => $this->id,
'name' => $this->getField('name', true), 'name' => Util::jsEscape($this->getField('name', true)),
'profession' => $this->curTpl['professionMask'], 'profession' => $this->curTpl['professionMask'],
'recipeSubclass' => $this->curTpl['recipeSubClass'], 'recipeSubclass' => $this->curTpl['recipeSubClass'],
'specializations' => Util::toJSON($this->curTpl['specializations'], JSON_NUMERIC_CHECK), 'specializations' => Util::toJSON($this->curTpl['specializations']),
'icon' => $this->curTpl['iconString'] 'icon' => Util::jsEscape($this->curTpl['iconString'])
); );
} }
@@ -72,7 +66,7 @@ class SkillList extends BaseType
$data = []; $data = [];
foreach ($this->iterate() as $__) foreach ($this->iterate() as $__)
$data[self::$type][$this->id] = ['name' => $this->getField('name', true), 'icon' => $this->curTpl['iconString']]; $data[self::$type][$this->id] = ['name' => Util::jsEscape($this->getField('name', true)), 'icon' => Util::jsEscape($this->curTpl['iconString'])];
return $data; return $data;
} }

View File

@@ -1,125 +0,0 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
class SoundList extends BaseType
{
use spawnHelper;
public static $type = Type::SOUND;
public static $brickFile = 'sound';
public static $dataTable = '?_sounds';
public static $contribute = CONTRIBUTE_CO;
protected $queryBase = 'SELECT s.*, s.id AS ARRAY_KEY FROM ?_sounds s';
private $fileBuffer = [];
private static $fileTypes = array(
SOUND_TYPE_OGG => 'audio/ogg; codecs="vorbis"',
SOUND_TYPE_MP3 => 'audio/mpeg'
);
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()->select('SELECT `id` AS ARRAY_KEY, `id`, `file` AS "title", CAST(`type` AS UNSIGNED) AS "type", `path` FROM ?_sounds_files sf WHERE `id` IN (?a)', array_keys($this->fileBuffer));
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 function getListviewData()
{
$data = [];
foreach ($this->iterate() as $__)
{
$data[$this->id] = array(
'id' => $this->id,
'type' => $this->getField('cat'),
'name' => $this->getField('name'),
'files' => array_values(array_filter($this->getField('files')))
);
}
return $data;
}
public function getJSGlobals($addMask = 0)
{
$data = [];
foreach ($this->iterate() as $__)
$data[self::$type][$this->id] = array(
'name' => $this->getField('name', true),
'type' => $this->getField('cat'),
'files' => array_values(array_filter($this->getField('files')))
);
return $data;
}
public function renderTooltip() { }
}
class SoundListFilter extends Filter
{
protected string $type = 'sounds';
protected array $inputFields = array(
'na' => [parent::V_REGEX, parent::PATTERN_NAME, 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->tokenizeString(['name']))
$parts[] = $_;
// type [list]
if ($_v['ty'])
$parts[] = ['cat', $_v['ty']];
return $parts;
}
}
?>

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,5 @@
<?php <?php
namespace Aowow;
if (!defined('AOWOW_REVISION')) if (!defined('AOWOW_REVISION'))
die('illegal access'); die('illegal access');
@@ -10,47 +8,45 @@ class TitleList extends BaseType
{ {
use listviewHelper; use listviewHelper;
public static $type = Type::TITLE; public static $type = TYPE_TITLE;
public static $brickFile = 'title'; public static $brickFile = 'title';
public static $dataTable = '?_titles';
public $sources = []; public $sources = [];
protected $queryBase = 'SELECT t.*, t.id AS ARRAY_KEY FROM ?_titles t'; protected $queryBase = 'SELECT t.*, id AS ARRAY_KEY FROM ?_titles t';
protected $queryOpts = array( protected $queryOpts = array(
't' => [['src']], // 11: Type::TITLE 't' => [['src']], // 11: TYPE_TITLE
'src' => ['j' => ['?_source src ON type = 11 AND typeId = t.id', true], 's' => ', src13, moreType, moreTypeId'] 'src' => ['j' => ['?_source src ON type = 11 AND typeId = t.id', true], 's' => ', src13, moreType, moreTypeId']
); );
public function __construct(array $conditions = [], array $miscData = []) public function __construct($conditions = [])
{ {
parent::__construct($conditions, $miscData); parent::__construct($conditions);
// post processing // post processing
foreach ($this->iterate() as $id => &$_curTpl) 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 // 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) if ($_curTpl['moreType'] == TYPE_ACHIEVEMENT)
$this->sources[$this->id][SRC_ACHIEVEMENT][] = $_curTpl['moreTypeId']; $this->sources[$this->id][12][] = $_curTpl['moreTypeId'];
else if ($_curTpl['moreType'] == Type::QUEST) else if ($_curTpl['moreType'] == TYPE_QUEST)
$this->sources[$this->id][SRC_QUEST][] = $_curTpl['moreTypeId']; $this->sources[$this->id][4][] = $_curTpl['moreTypeId'];
else if ($_curTpl['src13']) else if ($_curTpl['src13'])
$this->sources[$this->id][SRC_CUSTOM_STRING][] = $_curTpl['src13']; $this->sources[$this->id][13][] = $_curTpl['src13'];
// titles display up to two achievements at once // titles display up to two achievements at once
if ($_curTpl['src12Ext']) if ($_curTpl['src12Ext'])
$this->sources[$this->id][SRC_ACHIEVEMENT][] = $_curTpl['src12Ext']; $this->sources[$this->id][12][] = $_curTpl['src12Ext'];
unset($_curTpl['src12Ext']); unset($_curTpl['src12Ext']);
unset($_curTpl['moreType']); unset($_curTpl['moreType']);
unset($_curTpl['moreTypeId']); unset($_curTpl['moreTypeId']);
unset($_curTpl['src3']); unset($_curTpl['src3']);
// shorthand for more generic access; required by CommunityContent to determine subject // shorthand for more generic access
foreach (Locale::cases() as $loc) foreach (Util::$localeStrings as $i => $str)
if ($loc->validate()) if ($str)
$_curTpl['name'] = new LocString($_curTpl, 'male', fn($x) => trim(str_replace('%s', '', $x))); $_curTpl['name_loc'.$i] = trim(str_replace('%s', '', $_curTpl['male_loc'.$i]));
// $_curTpl['name_loc'.$loc->value] = trim(str_replace('%s', '', $_curTpl['male_loc'.$loc->value]));
} }
} }
@@ -84,10 +80,10 @@ class TitleList extends BaseType
foreach ($this->iterate() as $__) foreach ($this->iterate() as $__)
{ {
$data[Type::TITLE][$this->id]['name'] = $this->getField('male', true); $data[TYPE_TITLE][$this->id]['name'] = $this->getField('male', true);
if ($_ = $this->getField('female', true)) if ($_ = $this->getField('female', true))
$data[Type::TITLE][$this->id]['namefemale'] = $_; $data[TYPE_TITLE][$this->id]['namefemale'] = $_;
} }
return $data; return $data;
@@ -96,9 +92,9 @@ class TitleList extends BaseType
private function createSource() private function createSource()
{ {
$sources = array( $sources = array(
SRC_QUEST => [], 4 => [], // Quest
SRC_ACHIEVEMENT => [], 12 => [], // Achievements
SRC_CUSTOM_STRING => [] 13 => [] // simple text
); );
foreach ($this->iterate() as $__) foreach ($this->iterate() as $__)
@@ -112,43 +108,46 @@ class TitleList extends BaseType
} }
// fill in the details // fill in the details
if (!empty($sources[SRC_QUEST])) if (!empty($sources[4]))
$sources[SRC_QUEST] = (new QuestList(array(['id', $sources[SRC_QUEST]])))->getSourceData(); $sources[4] = (new QuestList(array(['id', $sources[4]])))->getSourceData();
if (!empty($sources[SRC_ACHIEVEMENT])) if (!empty($sources[12]))
$sources[SRC_ACHIEVEMENT] = (new AchievementList(array(['id', $sources[SRC_ACHIEVEMENT]])))->getSourceData(); $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) foreach ($this->sources as $Id => $src)
{ {
$tmp = []; $tmp = [];
// Quest-source // Quest-source
if (isset($src[SRC_QUEST])) if (isset($src[4]))
{ {
foreach ($src[SRC_QUEST] as $s) foreach ($src[4] as $s)
{ {
if (isset($sources[SRC_QUEST][$s]['s'])) if (isset($sources[4][$s]['s']))
$this->faction2Side($sources[SRC_QUEST][$s]['s']); $this->faction2Side($sources[4][$s]['s']);
$tmp[SRC_QUEST][] = $sources[SRC_QUEST][$s]; $tmp[4][] = $sources[4][$s];
} }
} }
// Achievement-source // Achievement-source
if (isset($src[SRC_ACHIEVEMENT])) if (isset($src[12]))
{ {
foreach ($src[SRC_ACHIEVEMENT] as $s) foreach ($src[12] as $s)
{ {
if (isset($sources[SRC_ACHIEVEMENT][$s]['s'])) if (isset($sources[12][$s]['s']))
$this->faction2Side($sources[SRC_ACHIEVEMENT][$s]['s']); $this->faction2Side($sources[12][$s]['s']);
$tmp[SRC_ACHIEVEMENT][] = $sources[SRC_ACHIEVEMENT][$s]; $tmp[12][] = $sources[12][$s];
} }
} }
// other source (only one item possible, so no iteration needed) // other source (only one item possible, so no iteration needed)
if (isset($src[SRC_CUSTOM_STRING])) if (isset($src[13]))
$tmp[SRC_CUSTOM_STRING] = [Lang::game('pvpSources', $Id)]; $tmp[13] = [Util::localizedString($sources[13][$this->sources[$Id][13][0]], 'source')];
$this->templates[$Id]['source'] = $tmp; $this->templates[$Id]['source'] = $tmp;
} }

View File

@@ -1,25 +1,21 @@
<?php <?php
namespace Aowow;
if (!defined('AOWOW_REVISION')) if (!defined('AOWOW_REVISION'))
die('illegal access'); die('illegal access');
class UserList extends BaseType class UserList extends BaseType
{ {
public static $type = Type::USER; public static $type = TYPE_USER;
public static $brickFile = 'user'; public static $brickFile = 'user';
public static $dataTable = ''; // doesn't have community content
public static $contribute = CONTRIBUTE_NONE;
public $sources = []; public $sources = [];
protected $queryBase = 'SELECT *, a.id AS ARRAY_KEY FROM ?_account a'; protected $queryBase = 'SELECT *, a.id AS ARRAY_KEY FROM ?_account a';
protected $queryOpts = array( protected $queryOpts = array(
'a' => [['r']], 'a' => [['r']],
'r' => ['j' => ['?_account_reputation r ON r.`userId` = a.`id`', true], 's' => ', IFNULL(SUM(r.`amount`), 0) AS "reputation"', 'g' => 'a.`id`'] 'r' => ['j' => ['?_account_reputation r ON r.userId = a.id', true], 's' => ', IFNULL(SUM(r.amount), 0) AS reputation', 'g' => 'a.id']
); );
public function getListviewData() { } public function getListviewData() { }
@@ -29,7 +25,7 @@ class UserList extends BaseType
foreach ($this->iterate() as $__) foreach ($this->iterate() as $__)
{ {
$data[$this->curTpl['username']] = array( $data[$this->curTpl['displayName']] = array(
'border' => 0, // border around avatar (rarityColors) 'border' => 0, // border around avatar (rarityColors)
'roles' => $this->curTpl['userGroups'], 'roles' => $this->curTpl['userGroups'],
'joined' => date(Util::$dateFormatInternal, $this->curTpl['joinDate']), 'joined' => date(Util::$dateFormatInternal, $this->curTpl['joinDate']),
@@ -40,22 +36,22 @@ class UserList extends BaseType
'reputation' => $this->curTpl['reputation'] 'reputation' => $this->curTpl['reputation']
); );
// custom titles (only seen on user page..?) // custom titles (only ssen on user page..?)
if ($_ = $this->curTpl['title']) if ($_ = $this->curTpl['title'])
$data[$this->curTpl['username']]['title'] = $_; $data[$this->curTpl['displayName']]['title'] = $_;
if ($_ = $this->curTpl['avatar']) if ($_ = $this->curTpl['avatar'])
{ {
$data[$this->curTpl['username']]['avatar'] = is_numeric($_) ? 2 : 1; $data[$this->curTpl['displayName']]['avatar'] = is_numeric($_) ? 2 : 1;
$data[$this->curTpl['username']]['avatarmore'] = $_; $data[$this->curTpl['displayName']]['avatarmore'] = $_;
} }
// more optional data // more optional data
// sig: markdown formated string (only used in forum?) // 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 patreon-status) // 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]; return [TYPE_USER => $data];
} }
public function renderTooltip() { } public function renderTooltip() { }

View File

@@ -1,26 +1,23 @@
<?php <?php
namespace Aowow;
if (!defined('AOWOW_REVISION')) if (!defined('AOWOW_REVISION'))
die('illegal access'); die('illegal access');
class WorldEventList extends BaseType class WorldEventList extends BaseType
{ {
public static $type = Type::WORLDEVENT; public static $type = TYPE_WORLDEVENT;
public static $brickFile = 'event'; public static $brickFile = 'event';
public static $dataTable = '?_events';
protected $queryBase = 'SELECT e.holidayId, e.cuFlags, e.startTime, e.endTime, e.occurence, e.length, e.requires, e.description AS nameINT, e.id AS eventId, e.id AS ARRAY_KEY, h.* FROM ?_events e'; protected $queryBase = 'SELECT *, -e.id as id, -e.id AS ARRAY_KEY FROM ?_events e';
protected $queryOpts = array( protected $queryOpts = array(
'e' => [['h']], 'e' => [['h']],
'h' => ['j' => ['?_holidays h ON e.holidayId = h.id', true], 'o' => '-e.id ASC'] 'h' => ['j' => ['?_holidays h ON e.holidayId = h.id', true], 'o' => '-e.id ASC']
); );
public function __construct(array $conditions = [], array $miscData = []) public function __construct($conditions = [])
{ {
parent::__construct($conditions, $miscData); parent::__construct($conditions);
// unseting elements while we iterate over the array will cause the pointer to reset // unseting elements while we iterate over the array will cause the pointer to reset
$replace = []; $replace = [];
@@ -43,18 +40,22 @@ class WorldEventList extends BaseType
if ($this->curTpl['requires']) if ($this->curTpl['requires'])
$this->curTpl['requires'] = explode(' ', $this->curTpl['requires']); $this->curTpl['requires'] = explode(' ', $this->curTpl['requires']);
$this->curTpl['eventBak'] = -$this->curTpl['id'];
// change Ids if holiday is set // change Ids if holiday is set
if ($this->curTpl['holidayId'] > 0) if ($this->curTpl['holidayId'] > 0)
{ {
$this->curTpl['id'] = $this->curTpl['holidayId'];
$this->curTpl['name'] = $this->getField('name', true); $this->curTpl['name'] = $this->getField('name', true);
$replace[$this->id] = $this->curTpl; $replace[$this->id] = $this->curTpl;
unset($this->curTpl['description']);
} }
else // set a name if holiday is missing else // set a name if holiday is missing
{ {
// template // template
$this->curTpl['name_loc0'] = $this->curTpl['nameINT']; $this->curTpl['name_loc0'] = $this->curTpl['description'];
$this->curTpl['iconString'] = 'trade_engineering'; $this->curTpl['iconString'] = 'trade_engineering';
$this->curTpl['name'] = '(SERVERSIDE) '.$this->getField('nameINT', true); $this->curTpl['name'] = '(SERVERSIDE) '.$this->getField('description', true);
$replace[$this->id] = $this->curTpl; $replace[$this->id] = $this->curTpl;
} }
} }
@@ -62,28 +63,16 @@ class WorldEventList extends BaseType
foreach ($replace as $old => $data) foreach ($replace as $old => $data)
{ {
unset($this->templates[$old]); unset($this->templates[$old]);
$this->templates[$data['eventId']] = $data; $this->templates[$data['id']] = $data;
} }
} }
public static function getName($id) public static function getName($id)
{ {
$row = DB::Aowow()->SelectRow(' if ($id > 0)
SELECT $row = DB::Aowow()->SelectRow('SELECT * FROM ?_holidays WHERE Id = ?d', intVal($id));
IFNULL(h.name_loc0, e.description) AS name_loc0, else
h.name_loc2, $row = DB::Aowow()->SelectRow('SELECT description as name FROM ?_events WHERE Id = ?d', intVal(-$id));
h.name_loc3,
h.name_loc4,
h.name_loc6,
h.name_loc8
FROM
?_events e
LEFT JOIN
?_holidays h ON e.holidayId = h.id
WHERE
e.id = ?d',
$id
);
return Util::localizedString($row, 'name'); return Util::localizedString($row, 'name');
} }
@@ -164,36 +153,12 @@ class WorldEventList extends BaseType
$data = []; $data = [];
foreach ($this->iterate() as $__) foreach ($this->iterate() as $__)
$data[Type::WORLDEVENT][$this->id] = ['name' => $this->getField('name', true), 'icon' => $this->curTpl['iconString']]; $data[TYPE_WORLDEVENT][$this->id] = ['name' => $this->getField('name', true), 'icon' => $this->curTpl['iconString']];
return $data; return $data;
} }
public function renderTooltip() public function renderTooltip() { }
{
if (!$this->curTpl)
return null;
$x = '<table><tr><td>';
// head v that extra % is nesecary because we are using sprintf later on
$x .= '<table width="100%%"><tr><td><b>'.$this->getField('name', true).'</b></td><th><b class="q0">'.Lang::event('category', $this->getField('category')).'</b></th></tr></table>';
// use string-placeholder for dates
// start
$x .= Lang::event('start').Lang::main('colon').'%s<br>';
// end
$x .= Lang::event('end').Lang::main('colon').'%s';
$x .= '</td></tr></table>';
// desc
if ($this->getField('holidayId'))
if ($_ = $this->getField('description', true))
$x .= '<table><tr><td><span class="q">'.$_.'</span></td></tr></table>';
return $x;
}
} }
?> ?>

View File

@@ -1,7 +1,5 @@
<?php <?php
namespace Aowow;
if (!defined('AOWOW_REVISION')) if (!defined('AOWOW_REVISION'))
die('illegal access'); die('illegal access');
@@ -10,13 +8,12 @@ class ZoneList extends BaseType
{ {
use listviewHelper; use listviewHelper;
public static $type = Type::ZONE; public static $type = TYPE_ZONE;
public static $brickFile = 'zone'; public static $brickFile = 'zone';
public static $dataTable = '?_zones';
protected $queryBase = 'SELECT z.*, z.id AS ARRAY_KEY FROM ?_zones z'; protected $queryBase = 'SELECT *, id AS ARRAY_KEY FROM ?_zones z';
public function __construct(array $conditions = [], array $miscData = []) public function __construct($conditions = [], $miscData = null)
{ {
parent::__construct($conditions, $miscData); parent::__construct($conditions, $miscData);
@@ -56,7 +53,7 @@ class ZoneList extends BaseType
// use if you JUST need the name // use if you JUST need the name
public static function getName($id) public static function getName($id)
{ {
$n = DB::Aowow()->selectRow('SELECT name_loc0, name_loc2, name_loc3, name_loc4, name_loc6, name_loc8 FROM ?_zones WHERE id = ?d', $id ); $n = DB::Aowow()->selectRow('SELECT name_loc0, name_loc2, name_loc3, name_loc6, name_loc8 FROM ?_zones WHERE id = ?d', $id );
return Util::localizedString($n, 'name'); return Util::localizedString($n, 'name');
} }
@@ -102,7 +99,7 @@ class ZoneList extends BaseType
$data = []; $data = [];
foreach ($this->iterate() as $__) 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; return $data;
} }

File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More