386 Commits

Author SHA1 Message Date
Sarjuuk
ef981c93e2 DB/Fixup: database dump ddl dml inconsistency (#453, #452)
* fix: `db_structure.sql` dump missed DML change after `aowow_account.allowExpire` field removed (in `a99fff46`)
* fix: `db_structure.sql` dump missed DML change after `aowow_articles.quickInfo` field removed (in `112acb22`)

---------

Co-authored-by: Михаил Драгункин <contact@md.land>
2025-09-26 15:35:24 +02:00
Sarjuuk
10c70209e7 Markup/Fixup
* fix replacing tags with names
2025-09-25 19:23:39 +02:00
Sarjuuk
8243be8d8e Spells/Tooltips
* allow spells to scale up to its maxLevel instead of max player level
   (e.g. spell 42891)
2025-09-18 20:20:25 +02:00
Sarjuuk
a4a3876cdc Items/Fixup
* casting the icon string to int may be considered suboptimal
 * fixes eb3b4ca5ec
2025-08-11 23:57:15 +02:00
Sarjuuk
4c89c9061e Filter/Fixup
* lost changes to spells from 16eabb90b6
 * fix criteria access for icons
2025-08-11 19:49:48 +02:00
Sarjuuk
7d8ffdd7da ItemFilter/Fixup
* fix slot panel after 16eabb90b6
2025-08-08 22:22:38 +02:00
Sarjuuk
eb3b4ca5ec Core/Optimization
* avoid using expensive numeric cast for anything but user inputs
   especially within nested loops
 * CharStats aggregation should be about x5 faster
2025-08-08 22:10:55 +02:00
Sarjuuk
0753bfbcf6 ItemFilter/Fixup 16eabb90b6
* fix lost item grouping
 * fix type selector in filter form
 * fix item upgrade search
2025-08-06 14:19:17 +02:00
Sarjuuk
8d7c95378c Core/Cleanup
* set type declarations in DB Wrapper
2025-08-06 01:10:24 +02:00
Sarjuuk
16eabb90b6 Core/Cleanup
* move DBType Filter base to its own file under components
 * modernize class and its children
2025-08-05 21:12:23 +02:00
Sarjuuk
08f0ae711e Misc/Fixup
* fix fetching areatriggers from DB and calculating found matches
 * fix Lang concatenating an array of strings with len < 2
 * don't show debug-id col in picker windows (Summary/Profiler) and sort them by score if able
2025-08-05 21:11:02 +02:00
Sarjuuk
569c9efca4 Setup/Source
* fix copy/paste error breaking item - pvp source query
2025-07-31 18:02:32 +02:00
Sarjuuk
6d3b3e1fcb Misc/Cleanup
* modernize extAuth template and add more help text
 * type declarations + cleanup in kernel
 * respect max col size when logging errors
2025-07-28 19:47:31 +02:00
Sarjuuk
112acb2216 Zones/QuickFacts
* unlink quick facts from articles and store per-row
 * new system allows generic and manual QuickFacts to coexist
 * fill new table with data for zones
 * if someone used static quickFacts .. uh .. good luck?
2025-07-28 00:30:04 +02:00
Sarjuuk
ceec228718 Util/Mails
* make sendMail a member of Util
 * move mail templates from strings to template files
 * enabled debug output to page
2025-07-27 18:37:40 +02:00
Sarjuuk
40b5c992e2 Misc/Fixup
* make time formatting js compatible
 * add auto-discovery for rss feeds
 * fix typos
 * define more magic numbers
2025-07-27 16:42:15 +02:00
Sarjuuk
b35ab67360 Items/Stats
* fixed stats for random enchanted items with scaling enchantments
 * don't send unused 'chance' to Summary tool
2025-07-27 16:42:14 +02:00
Sarjuuk
a99fff46aa User/Sessions
* implement tracking
    * FUTURE: log out all devices for user
    * generally store less info in _SESSION
2025-07-27 16:42:13 +02:00
Sarjuuk
086760b9b1 User/Cleanup
* the great unfuckening of user and displayName
    * `login` is purely used as login with AUTH_MODE_SELF
    * `email` may now also be used to log in (if the system knows it)
    * `username` is purely used for display around the site, and lookups from web context
    * both must exist because of external logins
        a) that may be not unique
        b) you may not want to share with the rest of the world
    * todo: implement rename ( because of b) )
2025-07-27 16:42:13 +02:00
Sarjuuk
bffdb9672e Future/Frontend
* create php classes, each mirroring a js object
 * each frontend class implements __toString and json_serialize and as such can be directly used by the template
 * also allows for sane object creation before js screams in agony

 * usage TBD
2025-07-27 16:42:12 +02:00
Sarjuuk
58412e0491 Titles/Name
* partially revert 398b93e9a7
 * generic "name" is required by CommunityContent system
2025-07-27 16:42:12 +02:00
Sarjuuk
5de9759b90 DBType
* extend functions
   * FilterFactory
   * test ::hasIcon()
   * test ::isRandomSearchable()
2025-07-27 16:42:12 +02:00
Sarjuuk
967841fcb9 Spell/DetailPage
* finally set GCDCat var
 * (a smooth decade later and it turns out it was StartRecoveryCategory all along)
2025-07-27 16:42:12 +02:00
Sarjuuk
3f0d6c2de6 JS
* add clickToCopy functionality
2025-07-26 23:27:11 +02:00
Sarjuuk
0928b1b430 User
* slightly modernize static class
2025-07-26 23:18:59 +02:00
Sarjuuk
0562989196 Fix LocString serialization
.. just implement __serialize, d'uh!
2025-07-26 23:16:07 +02:00
Sarjuuk
06bd7aa665 More/Searchbox
* modernize article
 * (clicktocopy pending cherry-pick)
2025-07-26 23:04:23 +02:00
Sarjuuk
a7cf96307c JS/CSS
* remove vendor-specific styles and replace with generics where necessary
 * remove browser hacks as far as possible (Presto/Trident)
 * remove FontAwesome reference
 * minor cleanup
2025-07-26 20:36:01 +02:00
Sarjuuk
8403f9ffd9 Comments/Replies
* fix load on demand for more than 5 replies per comment
2025-07-26 20:19:21 +02:00
Sarjuuk
fbfb81cd25 Profiler/Talents
* use icon from g_file_specs instead of weightscale data
 * fix detecting tank subspec for class 6
2025-07-24 19:43:09 +02:00
Sarjuuk
7a803a8783 Profiler/Talents
* fix grabbing unused gyph items for spec (like 44432)
2025-07-24 19:43:08 +02:00
Sarjuuk
dfefa914af Misc/Fixup
* remove vintage copy-paste error (that probably didn't do anything) in SmartAI
 * fix well-riped copy-paste error (that definitely did something) in Conditions
2025-07-18 02:29:20 +02:00
Sarjuuk
707dc32495 Util/Sanity
* break closing html tags contained in js
2025-07-07 17:55:11 +02:00
smansesdottk
e7baa27e27 Misc/Typos
* fix typos in README.md
 * cherry-picked from smansesdottk/www-aowow
2025-07-05 20:53:15 +02:00
Sarjuuk
f826e4d68a NPCs/Vendors
* fix npc_vendor self-referencing itself
 * game_event_npc_vendor seems to not allow for self references
2025-07-04 22:29:15 +02:00
Sarjuuk
e173de9a97 Quests/Series
* apply cuFlags to quest series
 * implement firstquestseries, lastquestseries, partseries filters using said flags
2025-06-22 19:31:22 +02:00
Sarjuuk
7c527e6144 Quest/Series
* fix series steps not displaying quests running in parallel for this step
2025-06-20 19:04:22 +02:00
Sarjuuk
74c0727bdb Profiler/ArenaTeams
* filter unused teams explicitly by played game count instead of implicitly by rating
2025-06-17 18:02:56 +02:00
Sarjuuk
cd94a2fa4e Profiler/Misc
* fix realm selection in profiler filters
 * minor cleanup of prQueue
2025-06-17 17:45:49 +02:00
Sarjuuk
91bb53aa1d Profiler/Realms
* skip out of List construction if realms are empty or do not match preselection
 * also prefilter Guilds and Prolfiles Lists for server or region (unless custom profiles)
 * discard ProfileList entries for inaccessible realms
2025-06-13 21:04:32 +02:00
Sarjuuk
069ca27b35 Localization/esES
* having ChatGPT translate missing strings
    * cherry picked examples seem fine, even those with control sequences
2025-06-03 20:50:13 +02:00
Sarjuuk
c3048fe1f8 Misc/Fixup
* test arrays first before use, not the other way round
 * do not try to init local arena team entries if there are no entries in list.
 * fix equally distributing chars/guilds/arenateams across realms for unlimited (0) lists
 * fix double declaration of realms in ArenateamList
2025-06-03 17:13:44 +02:00
Sarjuuk
ee02e70571 Profiler/Sync
* only truncate arena team members for OTHER teams of the given size
2025-06-03 17:02:24 +02:00
Sarjuuk
af69aa8e94 Profiler/ArenaTeams
* fixed LocalArenaTeamList being incompatible with it's inherited getListviewData()
 * also apply region and server limiters in LocalArenaTeamList
2025-06-03 17:02:23 +02:00
Sarjuuk
1de0535629 Mapper/Fixup
* no longer need to provide empty mapperData object on ZoneListPage
2025-05-23 19:59:51 +02:00
Sarjuuk
9b6ed672c4 Misc/Fixup
* FCK BOM
 * ItemsetDetailPage is visible again
2025-05-23 19:04:49 +02:00
Sarjuuk
9d704c5ff9 ItemDetailPage/Vendors
* show mapper for vendor locations
2025-05-20 20:07:32 +02:00
Sarjuuk
6475a9d181 SpellDetailPage/SpellGroups
* get a spells first rank from TC table if able
 * fix display of spell group stack rules tab
2025-05-17 22:01:21 +02:00
Sarjuuk
a8e1e3cf19 Misc/Fixup
* fixed typo when trying to include file for CharStats handling
2025-05-14 21:53:19 +02:00
Sarjuuk
44e0b6c62d Spells/Racial Skills
* fix manually set race mask for expansion races (and one class mask)
 * show applicable classes in race detail page listing
2025-05-13 20:11:20 +02:00
Sarjuuk
b837e55edc Misc/Fixups
* copy/paste errors in MiscValue handling for SpellEffect: skinning
2025-04-23 01:28:53 +02:00
Sarjuuk
d75aa56b38 Spells/Attributes
* update spell attribute descriptions primary from TC, secondary from Wowdev Wiki (not guaranteed to be correct for 335)
 * show all attributes on SpellDetailPage
 * update links from attributes to SpellFilter
   * unsure: should the attribute filters work purely on attributes or also consider other factors?
 * implement some of the client side modifiers on the spell tooltips and buffs
2025-04-22 21:38:58 +02:00
Sarjuuk
9c73f3cf95 Style/Fixup
* vertical align tinyicons in SpellMod array on SpellDetailPage
2025-04-13 00:37:36 +02:00
Sarjuuk
9a893e03d7 Misc/External Links
* try to prompt the brwoser to not send a referrer header when leaving the site.
2025-04-11 04:52:22 +02:00
Sarjuuk
43970189a6 Misc/Fixup
* happy three quarters of a new year .. or something
2025-04-02 23:34:36 +02:00
Sarjuuk
682b315e17 User/Misc
* floating changes
 * codify user checks into functions
2025-04-02 23:26:01 +02:00
Sarjuuk
3078763ec3 Misc/Fixup
* floating changes
 * condense several switch() into match() constructs
 * fix ucFirst not making the rest of the word lowercase
2025-04-02 22:22:57 +02:00
Sarjuuk
44ff43c113 Misc/Fixup
* floating changes against account ajax
 * make validation of DBtype/typeIds a method of Type
2025-04-02 22:14:45 +02:00
Sarjuuk
790264ba08 Misc/Fixup
* floating changes against profiler
 * no functional changes
2025-04-02 21:49:25 +02:00
Sarjuuk
e29d1e69fe Template/Fixup
* those js snippets don't really care what namespace they are included in
 * .. why are they even php files...?
2025-04-01 23:18:24 +02:00
Sarjuuk
2689fba992 SpellDetailPage/Source
* do not acces sources directly
 * fixes source display in infobox
2025-04-01 23:10:24 +02:00
Sarjuuk
db1d3ccace Core/Cleanup
* try to give included files a logical structure
 * move objects from Util and Game to their own files
 * make non-essential files auto-loaded
2025-04-01 22:33:36 +02:00
Sarjuuk
3a6c86092b Core/Compat
* create namespace Aowow to avoid naming conflicts
 * inclues/libs/ is outside of the Aowow namespace
2025-04-01 22:32:37 +02:00
Sarjuuk
4ccf917707 Listview/ReplyPreview
* fix links to parent comment
 * implement go-to-reply redirect
2025-03-29 14:15:59 +01:00
Sarjuuk
b347794ce5 Guides/Editor
* fix language selector
 * fix changelog
2025-03-29 13:52:36 +01:00
Sarjuuk
ed25f1f5f5 Misc/Fixup
* add type guide (300) to js so comments can be linked to guides
2025-03-26 21:22:37 +01:00
Sarjuuk
390801f53c NpcDetailPage/Skinning Skill
* fix displayed skill requirement for non-standard skinning (engineering, herbalism)
2025-03-25 17:30:33 +01:00
Sarjuuk
73f4a69a41 Setup/CustomData
* fix entry for item 33147
2025-03-22 19:24:01 +01:00
Sarjuuk
197b097ee5 Filters/StaffFlags
* fixed flag filters visibility for spells
 * fixed implementation of staff flag filters. Now they acutually do something.
 * fixed localization
2025-03-20 21:30:01 +01:00
Sarjuuk
f2bbb87eef SpellDetailPage/Effects
* readd lost markup used by EFFECT_PLAY_SOUND, EFFECT_PLAY_MUSIC and AURA_OVERRIDE_SPELLS, AURA_SCREEN_EFFECT
2025-03-20 19:07:21 +01:00
Sarjuuk
676c8617a5 Misc/Fixup
* fixed an exception for using an enum as string when generating log message.
2025-03-19 13:50:28 +01:00
Sarjuuk
6f8b72c980 Spells/Tooltips
* use cooldown strings from future clients to avoid scientific notation for long cooldowns hidden by the 335 client
2025-03-16 01:11:32 +01:00
Sarjuuk
b2d3bc1076 Types/Filters
* followup on 748a78c3c7
 * fix return type of criteria filter callbacks (mostly in case of faulty input)
 * spread the criteria var into criteria filter callbacks
 * some magic numbers to constants and type declarations for params/return types
2025-03-12 00:29:23 +01:00
Sarjuuk
9345309c07 ItemDetailPage/Tabs
* add filter result prompt to "disenchanted from" tab
2025-03-11 22:16:43 +01:00
Sarjuuk
50a5ccbaf6 Items/Summary
* Fixed itemset bonus calculation for sets with interchangeable items.
 * Fixed multiple items for the same slot activating set boni
2025-03-07 16:29:14 +01:00
Sarjuuk
460f4857ad Items/Filters
* add 'fished' as valid item source
 * item source 'none' now properly selects items without source, not items without source from this list
 * fixed edgecases where filter callback might have no return value
2025-03-06 18:51:51 +01:00
Sarjuuk
3b0d288418 Profiler/Fixup
* profiler expects teamId, not sideId
   (yes i know they are basically the same thing offset by 1!)
2025-03-06 02:00:28 +01:00
Sarjuuk
c712d1234b WorldEvents/Query
* try to fix generated aggregate query by not selecting multiple cols as `id`
2025-03-04 23:49:32 +01:00
Sarjuuk
31ad2e4944 Misc/Locale
* coerce malformed locale values from GET to int instead of failing outright
2025-03-04 23:49:27 +01:00
Sarjuuk
6249f2957e User/Fixup
* user linked characters where also uninitialized
2025-03-04 23:49:15 +01:00
Sarjuuk
efc8b51c8f Enchantment/Spells
*  initialize SpellList as null so it can be referenced
2025-03-04 00:29:08 +01:00
Sarjuuk
44bd9f521b Misc/Fixup
* fixed return type declaration to match inherited class
2025-03-04 00:22:13 +01:00
Sarjuuk
8b59551905 Locale/Search
* remove obsolete and broken logographic check.
 * this fixes searches with "too short" search tokens
2025-03-03 19:42:00 +01:00
Sarjuuk
4fe930cdca Quest/Condition
* fix quest display for reputation restricted quests
 * also check sources from externaly added conditions
2025-03-01 22:00:12 +01:00
Sarjuuk
337eddcc0b Fixup/Spells
* fixed variable reuse
2025-03-01 21:17:34 +01:00
Sarjuuk
013f1845b5 Fixup/Class/Race
* matching bitmasks on IDs may be considered counterproductive
2025-02-28 19:17:17 +01:00
Sarjuuk
4c6d93881c Spells/Fixup
* format effect cooldown as time
2025-02-28 14:36:49 +01:00
Sarjuuk
fa33467610 Misc/Cleanup
* more trait properties to const as they were always intended to be
2025-02-28 14:05:04 +01:00
Sarjuuk
1f59e6fe2d Util/Enums
* create helper enum classes ChrClass & ChrRace
 * replace various iterators and checks with said enums
2025-02-28 14:04:19 +01:00
Sarjuuk
a5bd6ddc8a Misc/Fixup
* fix external links by escaping quest name as javascript
2025-02-27 16:25:25 +01:00
Sarjuuk
6660125154 Items/Tooltips
* fix scaling string in item tooltips for scaling & non-interactive tooltips
2025-02-27 16:25:25 +01:00
Sarjuuk
9abe9b0d56 Loot/Conditions
* do not link questitems for multiple quests with 'and'
2025-02-27 16:25:25 +01:00
Sarjuuk
83b99c47d2 Auth/Fixup
* provide empty values for non-default db fields when initializing external accounts
 * removed browser check for username maxlength as it depends on auth method
2025-02-27 16:25:24 +01:00
Sarjuuk
cd4c023c61 SpellDetailPage/Fixup
* fix extraCols from spell_loot bleeding into other tabs
 * fix linking to effect spell from glyph_properties
2025-02-27 16:25:24 +01:00
Sarjuuk
870cbea2ca SmartAI/Update
* update events and actions to match TrinityCore again
   * removed events and actions have been kept but marked as deprecated
 * general rewording and use of UIES for better readability
 * move constants to respective classes
 * reevaluate usage of UNIT_FIELD_BYTES1 content
2025-02-27 16:25:24 +01:00
Sarjuuk
748a78c3c7 Filters/Cleanup
* move filter constants to class Filter
 * use less magic numbers
 * add type declarations
2025-02-27 16:25:23 +01:00
Sarjuuk
398b93e9a7 Locale
* detatch from User and Util and move to its own enum class
 * added definitions for all locales the 12340 client could in theory have
 * this is incompatble with the Intl extension
 * version bump and php requirement bump
2025-02-27 16:25:23 +01:00
Sarjuuk
40c2c63d1b NPC/MapModes
* try to improve map mode detection for NPCs again...
2025-01-27 23:09:45 +01:00
Sarjuuk
9da1e1575f Locale/Typo
* fixed title categories for locale 8
 * .. after a decade
2025-01-27 10:31:23 +01:00
Sarjuuk
88c066a8f5 ErrorHandling/Fixup
* do not write to STDERR from web context
2025-01-27 10:31:23 +01:00
Sarjuuk
88da3588e5 Compat/SQL
* make ON DUPLICATE KEY UPDATE queries compatible with both MySQL8 and MariaDB by providing update values from php
2025-01-26 13:53:26 +01:00
Sarjuuk
5309843d77 Misc/Robots
* add sensible robots.txt expected by configuration self test
2025-01-20 07:09:48 +01:00
Sarjuuk
452e056499 Misc/Date Format
* use 12h format when using AM/PM suffix
2025-01-20 06:41:32 +01:00
Sarjuuk
d86936f6f5 Setup/ItemStats
* fixed array structure when converting equal amounts of SPELL_DAMAGE and SPELL_HEALING to SPELL_POWER
2025-01-20 06:34:38 +01:00
Sarjuuk
c1eecb4c22 ItemStats/Filters
* do not use NULL on item stats as it prevents searching for an amount of 0 (except for stats that certain items just cant have)
 * fix stats from spells granting spell power and spell healing separately
 * define and use some item subclasses
2024-09-10 16:54:49 +02:00
Sarjuuk
a62f24b97c Locales/Domain
* entirely switch over to write 'en' for locale 0 and accept both 'www' and 'en' for locale 0 when receiving
2024-09-10 14:42:27 +02:00
Sarjuuk
e3fc4ebd62 Updates/Fixup
* DROP COLUMN IF EXIST is MariaDB specific and should not be used.
2024-09-08 21:46:59 +02:00
Sarjuuk
79aa8fda7e Profiler/Fixup
* fix index error in ArenaTeam member update query
2024-08-27 22:56:33 +02:00
Sarjuuk
fdf8d783b1 AjaxHandler/Fixup
* fix invalid return type in handleWeightscales() for action: delete
2024-08-27 22:34:52 +02:00
Sarjuuk
48ce7267e7 ItemFilter/Fixup
* fix invalid return type in createConditionsForWeights()
2024-08-27 22:34:52 +02:00
Sarjuuk
5270e70cd1 ListviewTabs/Fixup
* filter empty tabs before trying to display, case in point
 * do not always assign conditions tab if empty
2024-08-27 22:34:52 +02:00
Sarjuuk
50f5af07f3 SpellEffects/Icons
* fix icon formater: num/qty can be strings
2024-08-27 22:34:52 +02:00
Sarjuuk
64fb86f3a9 Reports/Fixup
* reports can have 'null' subject (general bug reports, general feedback)
2024-08-27 22:34:52 +02:00
Sarjuuk
778c21e817 BaseType/Fixup
* fix totals query. Rather treat original query as subquery than trying to modify it
 * fixes queries utilizing HAVING and GROUP BY
2024-08-27 20:46:06 +02:00
Sarjuuk
af303f447a CommunityContent/Fixup
* fix totals query params after 481a3dc63f
2024-08-27 20:44:43 +02:00
Sarjuuk
03cf3a5918 UtilityPage/Random
* fix invalid type error
 * simplyfy query building (there are no longer tables with an index named `entry`)
2024-08-27 20:42:06 +02:00
Sarjuuk
3aede18926 SpellDetailPage/Fixup
* fix empty scaling bar
 * fix setting max level for scaling tooltips
 * fix displayed periodic power gain value for runic power and rage
 * attempt to fix spell coefficient calculation (dear god...)
 * append % sign to value for two more auras
 * display value per combo point
2024-08-24 20:02:51 +02:00
Sarjuuk
075e15ba0c Setup/Fixup
* even more forgotten renames in help text
 * see 07e001ee9c
2024-08-24 15:11:31 +02:00
Sarjuuk
7d2e306f0c Game/Spawns
* fix spawn point calculation on instance maps without dungeonmap entry
   (can't wait to see what breaks elsewhere)
2024-08-24 14:49:22 +02:00
Sarjuuk
b11c1125f6 Filters/Wildcards
* fix usage of generic search wildcards: ? *
 * only transform search form fields expected to be text
 * some cleanup (magic numbers to define, fn return types, nullsafe assignemnts)
2024-08-22 16:15:07 +02:00
Sarjuuk
57ad861da7 Items/Arena
* changed logic to not display required arena rating in tooltip or infobox if there are sources that don't require it.
 * also, should there be a difference in sources, display lowest required rating, not not highest.
2024-08-22 01:40:03 +02:00
Sarjuuk
d2e109d818 SpellDetailPage
* attempted cleanup in effect generation and template
 * display summon properties
 * display unit of effect value
 * fix spell effect layout, typos, missing tools, ...
2024-08-19 21:35:36 +02:00
Sarjuuk
dab110475c Items/Tooltips
* do not display crafted item tooltip inside recipe tooltip if the craft spell doesn't create an item.
   (notably: enchantments were displaying enchantment scrolls)
 * always display reagent cost if set
2024-07-31 22:02:15 +02:00
Sarjuuk
b330f88699 Itemstats/Ammunition
* don't use 'delay' to calc added dps from ammunition
2024-07-31 02:38:25 +02:00
Sarjuuk
481a3dc63f MySQL/Compat
* fixed several deprecation notices and warnings from MySQL8, most notably:
   - SQL_CALC_FOUND_ROWS: stopped using DBSimple::selectPage and query 'SELECT COUNT(*) ...' separately where needed
   - ON DUPLICATE KEY UPDATE ... VALUES(): use row alias for new values instead of VALUES function
   - boolean shorthands to long form (&& -> AND, etc)
2024-07-31 02:38:19 +02:00
Sarjuuk
1b2b773663 PHP/Compat
* avoid using "echo" to write to CLI as php mistakes it for sent headers (see php-src #12303)
 * as we are using fwrite now, errors are written to STDERR instead of STDOUT
 * fixes an issue where reloading the config would cause "ini_set(): Session ini settings cannot be changed after headers have already been sent" to be spammed
2024-07-31 00:51:10 +02:00
Sarjuuk
b5c2f7a296 Git/Misc
* force text files eol to \n
2024-07-31 00:50:46 +02:00
Sarjuuk
07e001ee9c Setup/Fixup
* fixed help text referencing renamed parameter
2024-07-31 00:50:46 +02:00
Sarjuuk
c30e68d86f Enchantments/ProcSpells
* fixed resolving procSpells with a triggerSpell of 0
 * spells are now always displayed in listview
2024-07-14 17:48:30 +02:00
Sarjuuk
7d545167df Errors/Log
* also log POST data
2024-07-14 17:18:22 +02:00
Sarjuuk
31928d56a7 Localization/Fixup
* kill an antique typo causing an error whenever a tooltip for a hearthstoneesque effect is displayed for Locale FR an RU
2024-07-14 16:23:43 +02:00
Sarjuuk
99a95f3995 Profiler/Completions
* removed old workaround for js date object creation
2024-07-13 19:22:25 +02:00
Sarjuuk
a9f1832b6d GearScore/Enchants
* reapply lost /4 modifier for enchantment scores
2024-07-11 01:48:20 +02:00
Sarjuuk
b0a51f4746 Spells/SpellClassMask
* fix confusion of SpellClassMask fields
 * no functional change
2024-07-10 18:04:35 +02:00
Sarjuuk
f55945780b Setup/Fixup
* fix syntax of db_structure.sql
 * not to self: SHOW CREATE TABLE doesn't return a terminated statement
 * fixes #431
2024-07-09 21:10:17 +02:00
Sarjuuk
81078bcf3d Comments/Date
* add missing elapsed value to comment and reply rows to make them sortable
2024-07-09 14:41:05 +02:00
Sarjuuk
c14d53067b Comments/Replies
* add default values to comments table so replying doesn't cause errors
2024-07-09 13:30:51 +02:00
Sarjuuk
460615c112 Profiler/Completion
* split completion table into it's subcomponents
 * this should save some disk space as some keys and null fields have been optimized out and col sizes have been reduced
 * sort ICC raid bosses first to last
2024-07-08 23:56:43 +02:00
Sarjuuk
98b1771850 Config/Locales
* flag locales as 'required'
 * allow bitmasks to be empty unless required
 * skip empty check in javascript
2024-07-08 17:16:10 +02:00
Sarjuuk
37def70f6a Setup/Realms
* force use of Locale EN instead of last used locale (usually RU)
2024-07-08 16:51:44 +02:00
Sarjuuk
f4364099c6 DB/CustomData
* alter value column from varchar to text to prevent truncated data
2024-07-08 16:51:44 +02:00
Sarjuuk
92a6e0122f Misc/Fixup
* Profiler: handle items with invalid enchantments
 * Profiler: convert (last?) forgotten INSERT IGNORE to ON DUPLICATE KEY UPDATE id=id
 * Areatrigger: fix null related tabs
 * LatestComments: fix lv template for replies
 * Conditions: fixed tab names for locale DE
2024-07-08 16:51:08 +02:00
Sarjuuk
828fa40d4b Loot/LootLink
* link Ahune and Majordomo Executus to their chests
2024-07-07 23:02:44 +02:00
Sarjuuk
815f13e530 Pages/TopUsers
* hide 'reports' column as reputation for reports is NYI
2024-07-07 20:55:00 +02:00
Sarjuuk
9435c0fc2e EventDetailPage/RelItems
* fixed getting related items from QuestList
2024-07-07 20:50:22 +02:00
Sarjuuk
b8898797ed SoundDetailPage/Conditions
* fix error assigning a worldstate conditional from child zone to parent zone
2024-07-07 14:18:08 +02:00
Sarjuuk
be7a84a651 Core/Conditions
* minor rework
 * fixed columns of tab item loot
 * fixed lookup of classes/races (as they are the only type used as bitmask)
 * implemented reverse lookups everywhere (arguably class, race and skill are too spammy)
 * reverse lookups no longer contain redundant data
 * changed how the groupKey is set, so there are no more cases that can't be looked up
 * fixes #273

 * title: added tab 'criteria-of'
2024-07-06 04:03:56 +02:00
Sarjuuk
0f6b8015a1 Misc/Fixup
* Loc EN: removed unnessecary 'to translate' backets
 * npc: add missing index to loot tabs to kill a warning
2024-07-05 22:10:46 +02:00
Sarjuuk
bd5200de85 Mapper/Quests
* sort zones by number of pins, most to least
 * always preselect zone with most pins
2024-07-05 22:10:36 +02:00
Sarjuuk
f21e8045b2 Misc/Fixup
* spell:    resolve MiscValue for SPELL_AURA_MOD_INVISIBILITY
 * search:   do not double escape page title
 * profiler: fixed typo, causing shaman spirit weapons to be applied _everywhere_
2024-07-05 18:15:05 +02:00
Sarjuuk
8d885a5a67 Setup/WIN
* fix paths for WIN ... and try to not use slashes in the future...
 * fixes #430
2024-07-05 18:15:05 +02:00
Sarjuuk
a4bcb33ba4 NPCs/DetailPage
* added item to infobox when npc is a spirit (only visible when dead)
2024-07-05 18:01:49 +02:00
Sarjuuk
8b46607c29 Setup/Spawns
* handle erronous zone data from TDB creature/gameobject tables
2024-07-04 23:22:29 +02:00
Sarjuuk
c3bae7fe5e Localization/zhCN
* add lost string lookups
2024-07-04 21:31:52 +02:00
Sarjuuk
2e9b503c59 Loot/LinkedLoot
* rework npc <-> chest loot linking
 * difficulty is now directly stored
 * should fix multiple issues where loot tabs had wrong difficulty or null title
2024-07-04 21:31:48 +02:00
Sarjuuk
02e33b4038 Profiler/Tooltips
* do not transform page parameters to lowercase. The tooltip Javascript expects the server response in the same case it got sent.
 * transform to lowercase just for lookups
 * fixes #394
2024-07-04 16:27:00 +02:00
Sarjuuk
c3347b8e9c CLI/WIN
* read input as a whole line under WIN instead as stream_get_contents always blocks
 * fixes #429
2024-07-04 15:20:07 +02:00
Sarjuuk
cd4e049680 DB/Engine
* drop usage of MyISAM and switch to InnoDB.
2024-07-03 18:45:52 +02:00
Sarjuuk
2bd588045a PHP/Compat
* remove sources of deprecation warnings. Mostly dynamic creation of object properties.
 * some string function no longer accept null as string
2024-07-03 18:38:28 +02:00
Sarjuuk
05c036bd9f Misc/Fixup
* fix Conditions derived from spell_area
 * Type Statistic should not be random searchable and excluded from "missing screenshots" UtilityPage
 * do not cut off querys in log
2024-07-03 18:33:25 +02:00
Sarjuuk
d93b5df5bc Misc/Fixup
* added datetime increment to dbversion table, lost from 69de457108
2024-07-02 16:12:13 +02:00
Sarjuuk
f12d16ea5b Localiziation/Fixup
* fixed cast time strings for locale enUS
2024-07-01 22:25:06 +02:00
Sarjuuk
d2277d1034 Setup/Icons
* do not consider the setup step failed if icons are missing. There are a couple void references in dbc
2024-07-01 19:44:31 +02:00
Sarjuuk
4d306e64fb CLI/Config
* fixed skipping the value in oneline mode being interpreted as empty string
 * fixed entering infinite loop in oneline mode, when passed value was invalid
 * fixed displaying config hint, when passed value was valid
2024-07-01 19:05:33 +02:00
Sarjuuk
8016802ec6 CLISetup/Fixup
* also convert Cfg test function, forgotten in 2386e35207
2024-06-30 16:55:22 +02:00
Sarjuuk
2386e35207 CLISetup/locales
* removed redundant declaration of locales to process
2024-06-29 13:03:18 +02:00
Sarjuuk
5d4051928a Setup/Fixup
* spell:  do not create a temporary copy of the spell.dbc, but merge serverside spells directly into aowow_spell.
           This sidesteps a mysql issue where a temp table can't be read multiple times.
 * spawns: fixed lost var rename in log output
 * items:  use UNSIGNED instead of INT when typecasting in query so mysql can also understand it
2024-06-29 12:18:13 +02:00
Sarjuuk
33d2192431 JS/Mapper
* generally allow zone links on a map on the zone detail page
 * manually disable it for the main map
2024-06-28 01:04:48 +02:00
Sarjuuk
ae54e5e213 Setup/Spawns
* restore info on manually moved spawn points
 * don't use worldmaparea zone dimensions for maps that don't use them
2024-06-28 01:04:42 +02:00
Sarjuuk
cdf06deb90 Misc/Fixup
* restore display of creature targets on quest detail page
 * fix ShowOnMap utility for quest displays
2024-06-28 01:04:36 +02:00
Sarjuuk
69de457108 Map/Spawns
* move areatrigger teleport endpoints and instance entrance points to spawns. This makes them editable like spawn points.
 * since instance entrances aren't shown on maps they are moved through the admin menu on the instances page.
 * rewrote spawns SetupScript. Individual groups can now be recalculated separately.
2024-06-28 01:04:29 +02:00
Sarjuuk
040cac41a1 Setup/Help
* created a generic help display for SetupScripts with sub commands
2024-06-28 01:04:24 +02:00
Sarjuuk
abbedf9ae4 Misc/README
* extracting Glues/Credits/ is optional. They are not used by aowow itself.
2024-06-27 14:51:16 +02:00
Sarjuuk
22b0f8c1c1 Setup/Update
* setup step names are allowed to have dashes and underscores
2024-06-26 16:52:45 +02:00
Sarjuuk
a4c734435e Map/ShowOnMap
* fix pins being combined across multiple floors
2024-06-26 16:52:45 +02:00
Sarjuuk
88c5127ab5 Map/Spawns
* fix spawns for multifloor dungeonmaps that use their worldmaparea entry for coordinates
 * fix mapper feature: move visible spawn point
 * resolve confused X/Y coordinate remapping from a time before i knew world coordinated are rotated by 90°
 * PS: Ulduar floor indizes were funky. No idea why.
 * restore old define order for g_zone_areas
2024-06-26 16:52:00 +02:00
Sarjuuk
a08a713dcc Misc/Fixup
* fixed typo in locale deDE
 * fixed markup not processing emotes with negative id
2024-06-26 16:42:04 +02:00
Sarjuuk
79764ced60 Core/ErrorHandler
* do not handle errors outside of the registered handlers
 * always handle all errors otherwise they get stored for error_get_last
 * always print errors to CLI
 * shutdown function handler should not be picky about what errors it gets to report
 * removed some mostly unused error strings
2024-06-24 18:10:44 +02:00
Sarjuuk
e614f415a9 Util/FileHandler
* try to create directory if file is to be written into nonexistent directory
2024-06-24 17:13:50 +02:00
Sarjuuk
10de320616 CLI/readline
* added checks if STDIN is available and open before accessing it.
2024-06-23 15:54:58 +02:00
Sarjuuk
9bb5afd9a7 Setup/CustomData
* removed unused option to have custom data defined in code
 * table `aowow_setup_custom_data` should be used instead
 * this kills a "creation of dynamic property" deprecation notice
2024-06-23 14:42:16 +02:00
Sarjuuk
615c203c7a Setup/img-maps
* script cleanup
 * fixed subzone generation and made color less garish
 * fixed alphamap generation and alphamapcheck not pointing to the same path
 * fixed padding UtilityScript args with unexpected var types
2024-06-21 16:57:33 +02:00
Sarjuuk
931a90f5c8 Misc/Fixup
* revision bump
 * minor log format fix
2024-06-20 20:49:11 +02:00
Sarjuuk
bf184e7555 Core/Setup
* rewritten to be able to dynamicly load it's components
   - CLISetup -> checks for UtilityScripts (config, setup, dbc reader, etc.) -> checks for SetupScripts (individual sql/file generators)
   - each step may now have a help prompt attached. If none are provided, the containing script may provide it's help.
   - all Scripts are self contained modules. No more editing of 3+ files if some component is added/removed
 * removed intermediaries FileGen & SqlGen
 * functional changes
   - allow providing CLI arguments to siteconfig and account UtilityScript and skip the interactive prompts
   - set slot for consumable enchantment items so they are filtrable
   - zones dataset is now localized and generated from GlobalStrings.lua and DungeonMap.dbc. Related data dumps removed.
   - 'aowow' and 'prQueue' executables now have shebangs

    WARNING - command line options have been renamed!
2024-06-20 18:10:12 +02:00
Sarjuuk
ab4cf67e80 Misc/Fixups
* Util::writeTable .. border length, toned down colors
 * Cfg::get .. empty strings and 0 can be valid, return null on failure
 * Game::getWorldPosForGUID .. fixed referencing soundemitters by soundId instead if index
2024-06-20 16:28:29 +02:00
Sarjuuk
7412a518a5 Setup/Classes
* fix skill aggregate
 * ya cant sum strings, ya dummy
2024-06-19 02:52:43 +02:00
Sarjuuk
3d84870db8 DBC/Fixup
* fix broken locale string extraction after e164023b8a
2024-06-19 01:34:35 +02:00
Sarjuuk
b5b62a5a62 Localization/GlobalStrings
* apply more strings from GLobalStrings.lua to localization
 * mostly ITEM_MOD_* and PVP_RANK_*
 * some fixes in the process
2024-06-18 22:59:20 +02:00
Sarjuuk
7e0be11323 Stats/CombatRatings
* define magic numbers from combat ratings
 * move forgotten rating base values from Util to Stats
 * fix localized rating strings for item and spell tooltips (expected float, got string)
 * some cleanup in item/spell types to make it more clear when an internal stat is being handled, not an itemMod or combatRating
2024-06-18 22:34:50 +02:00
Sarjuuk
e164023b8a Setup/DBC
* move dbc structures to separate files and allow loading a specific build
 * handle localized single string fields
 * add cli option for output table name
 * add cli option for wowbuild
2024-06-17 19:59:51 +02:00
Sarjuuk
12ddc6fe82 Misc/Fixup
* prevent direct access to some internal files where applicable and reword error for others
 * if CFG_DEBUG is set also enable debug in client javascript
 * non-breaking formatting changes
 * define regions and missing locales for later use
2024-06-17 19:59:51 +02:00
Sarjuuk
06ffba0239 CLI/readline
* do not reuse prompt variable for use input
2024-06-17 19:59:51 +02:00
Sarjuuk
d03448b053 CLI/FileAccess
* fix deep file/dir creation
 * also edited file permissions again and left a note so it doen't happen again .. maybe
2024-06-17 18:13:03 +02:00
Sarjuuk
0117916da9 CLI
* make tables more legible
2024-06-17 18:13:03 +02:00
Sarjuuk
c2bbfe17a6 Setup/DataStores
* do not rely on temporary converted dbc tables during runtime
 * create permanent tables instead
2024-06-17 18:12:59 +02:00
Sarjuuk
7b924a197e Mapper/Spawns
* do not try to place spawns assigned by instanced map <=> zoneId association. They have (0,0) as coordinates
2024-06-17 13:36:45 +02:00
Sarjuuk
8fe18ed41c Articles/Classes
* add class articles from 8e5bdebea0 db_structure.sql
2024-06-16 22:16:38 +02:00
Sarjuuk
d16b08bb29 Core/CharStats
* unify stat handling. If there are discrepancies left at least they are now centralized.
   - health should no longer pretend to be mana
   - stats are no longer capped to 32.7k points as items can have multiple of the same stat and handily exceed smallints
 * weight scale fixes:
   - "repair cost" is no longer a weight scale option. Why was it one in the first place? Also it wasn't even accessible before. (that was a bug)
   - "bonus armor" is now searchable and only applied to armor pieces
 * removed unused parsed stats from itemsets
2024-06-16 19:28:26 +02:00
Sarjuuk
cb3c7d4ef0 Misc/Util
* relax numeric type requirements when working with filters
 * restore smart type casting functionality of Util::checkNumeric when used with NUM_ANY
 * enable Util::arraySumByKey to work recursively
 * fix source display in listview
2024-06-16 17:22:08 +02:00
AthenaSui
a4d05dc036 Localization/zhCN
* cherry-picked translation from 820c88d412
2024-06-14 02:01:54 +02:00
Sarjuuk
ce0e57e390 Setup/Fixup
* run TDB checks agains word db as they are supposed to.
 * fixes #423
2024-06-10 21:07:57 +02:00
Sarjuuk
33ee358e0c Setup/DBconfig
* test for known world db formats and throw appropriate errors
 * test for imported aowow db and throw error if missing
 * make DB connection reloadable
2024-06-09 15:02:31 +02:00
Sarjuuk
82c04c9ed5 CLI/readline
* there are still terminals out there sending \r as line terminator...
2024-06-07 17:15:01 +02:00
Sarjuuk
fc86825b15 Pages/Item
* fixed fractional buy price for items sold in stacks in infobox (fixes #362)
 * fixed filter criteria enums being generally invalid
2024-06-07 16:40:26 +02:00
Sarjuuk
e873d8cbd0 CLI/readline
* ignore \e sequences except the single \e chars read to leave the current loop
2024-06-07 15:32:35 +02:00
Sarjuuk
69fe0b5c7d Cfg/Fixup
* also trigger first time load as soon as db is set up
2024-06-04 23:15:18 +02:00
Sarjuuk
e734b41632 Cfg/Fixup
* only throw errors if there is actually a config to work with. (like, not when you are just setting up for the first time)
 * do not use trigger_error() in CLI mode as it can cause a lockup as the error_handler tries to throw more errors.
 * assign lost validator fn flag to settings
 * also use validator onReset
2024-06-04 22:15:52 +02:00
Sarjuuk
5c1e9747c6 Misc/Cleanup
* remove some derelict code
2024-06-03 15:58:41 +02:00
Sarjuuk
f861886fdf Misc/Fixup
* Conditions: loot rows initially have no 'id'
 * fixed building talent string for hunter pets (different talents can occupy the same row/col spot)
 * added keys loot cols on creature table
 * fixed trying to show itemset type for itemsets without type
 * fixed waypoint calculation when moving entity between floors
2024-06-02 21:15:29 +02:00
Sarjuuk
efab0bad32 Misc/Fixup
* missed const from f77d676a19
 * missing start of string anchor in Util::checkNumeric
2024-06-02 02:45:29 +02:00
Sarjuuk
bc7d561da2 Core/Conditions
* rewritten and moved to its own class, should be easier to expand in the future
 * add missing sources and types from TrinityCore
 * implement conditions on Areatrigger and Loot containers
 * implement reverse lookups (e.g. a spell is a conditional for something else)
 * general beautification pass .. should be more legible in general

NOTE:
 * texts have been changed, so the existing translation for esES ist gone
 * selecting and describing condition targets is still wonky
2024-06-01 02:47:58 +02:00
Sarjuuk
84555afae3 Page/Notes
* also color code notes according to message severity
2024-06-01 02:47:58 +02:00
Sarjuuk
f77d676a19 Core/Config
* convert configuration from list of constants to object
 * fixes config changes not applying on cli whithout closing and reopening again
 * config variables are no longer embedded in localization text
2024-05-30 20:50:44 +02:00
Sarjuuk
454e09cc78 DB/Errors
* don't need to handle sql warnings in batch when the error handler doesn't use exit() any longer
 * display warnings as [WARN] and use consts instead of magic numbers
2024-05-28 22:40:46 +02:00
Sarjuuk
2d5caba814 DB/Structure
* some more corrections to field types
   - quests: rewardArenaPoints - unsigned -> signed
   - events: unify event id - tinyint -> smallint
   - objects: unify quest id - smallint -> mediumint
   - item_stats: stat cols - smallint -> mediumint (Tester Ring has 64k HP+MP)
2024-05-28 22:37:59 +02:00
Sarjuuk
f422c4ecfb Setup/Spawns
* implement SpawnedByDefault from TC
 * maybe fix a longstanding issue where multiple parent areas per instanced map lead to wrong spawn points
    map  should be                  can also be
    36   The Deadmines              The Great Sea, Unused Ironcladcove
    109  The Temple of Atal'Hakkar  Sunken Temple
    540  The Shattered Halls        Hellfire Citadel
    560  Old Hillsbrad Foothills    Hyjal Past
    631  Icecrown Citadel           The Frost Queen's Lair, Putricide's Laboratory of Alchemical Horrors and Fun, The Crimson Hall, The Frozen Throne, The Sanctum of Blood
2024-05-28 21:58:44 +02:00
Sarjuuk
a87b869896 Setup/BLP
* fix using replacement patch files as images
2024-05-28 20:17:16 +02:00
Sarjuuk
2c451b8deb Locales/Search
* do not apply minimum string length limiter to logographic languages
2024-05-27 17:54:58 +02:00
Sarjuuk
f6565ea924 DB/Structure
* fix data types and data length and add default values where necessary
 * data should no longer get truncated
 * misc fixes
2024-05-22 20:35:02 +02:00
Sarjuuk
7d5930865c DB/Setup
* fix erronous foreign key constraint revealed by MySQL 8.4
2024-05-21 17:46:23 +02:00
Sarjuuk
90b04865f5 Setup/DBC
* use utf8mb4 for dbc_* tables
2024-05-17 23:52:47 +02:00
Sarjuuk
a03d44ae67 ZoneDetailPage/Fishing
* if subzone has no entry in skill_fishing_base_level, try to look up parent entry
2024-05-17 23:27:03 +02:00
Sarjuuk
a97cede1e0 Profiler/Realms
* generally allow all realm types
 * filter visibility/access by userGroup
2024-05-16 22:33:01 +02:00
Sarjuuk
b2690aea08 Setup/Update
* underscores are valid chars for script names
2024-05-15 00:08:45 +02:00
Sarjuuk
c87178c791 PHP/Compat
* spreading a nested array prevents it from being passed as reference
 * this worked previously under php v7.2
2024-05-11 00:32:01 +02:00
Sarjuuk
b5829cc954 Misc/Fixup
* fix reference error from 9a1cb5f2f9
2024-05-10 17:37:13 +02:00
Sarjuuk
9a1cb5f2f9 Setup/ImageGen
* consider preconverted png images when initially checking resources
 * fixed minor bugs
2024-05-09 16:05:23 +02:00
Sarjuuk
d7fa4a900e Admin/Screenshots
* whelp, didn't see the onKeydown handler for the input field
 * fix the fix in bf06c418d4
 * allow "ArrowUp", "ArrowDown" and "-" as inputs
 * also fix search by pressing <Return>
2024-05-07 19:26:06 +02:00
Sarjuuk
bf06c418d4 Misc/Fixup
* do not urlencode mysqli uris  ..  can't wait for this to cause issues again.
 * input[type=number] does not allow inputing dashes to signify negative numbers. Use input[type=text] instead and cast to int afterwards.
2024-05-07 16:43:35 +02:00
Sarjuuk
937bec3d84 Spells/Tooltips
* enable level scaling for spells with RealPointsPerLevel
 * note: While BasePoint vars ($m, $M) can scale, they are often involved in formulas that would have to be recalculated in javascript. This is currently impossible. So this var is skipped for now.
2024-05-06 01:06:31 +02:00
Sarjuuk
99eca2661f Items/Tooltips
* do not display charges if they can't be used
2024-05-05 19:45:27 +02:00
Sarjuuk
6b25288e2b Spells/Reagents
* always check all reagent fields, they are not set first to last
2024-05-05 14:31:15 +02:00
Sarjuuk
92c51237c6 Misc/Fixup
* lost changes from 5f4c62644d
2024-05-04 18:52:56 +02:00
Sarjuuk
5bb277bf2f Tooltips/CombatRatings
* modenize interactive rating string
2024-05-03 21:01:24 +02:00
Sarjuuk
5f4c62644d DB/Errors
* improve db error handling
 * web view should always result in an user friendly error if the db connection is missing or erronous
 * cli use should never fatal if the db connection is erronous. How are you going to fix it, d'uh.
 * if some CLISetup script requires a db connection check it individually
2024-05-03 20:58:30 +02:00
Sarjuuk
41c0af16b3 Misc/Fixup
* drop build steps possibly scheduled to sql update after de2fa377 (they can never be executed)
2024-04-21 22:27:15 +02:00
Sarjuuk
a4c15653d8 DB/SqlModes (#406)
* drop more modes that depend on previously dropped STRICT_TRANS_TABLES
2024-04-21 15:10:35 +02:00
Sarjuuk
42d284dce0 CLI/CR
* always skip \r inputs.
* May fix weird issues when accessing a *nix container from WIN and MacOS hasn't been a thing for decades...
2024-04-20 23:39:29 +02:00
Sarjuuk
d084e6072b DB/SqlModes
* only update sql_mode if strictly necessary
* keep other modes set in my.cfg / my.ini
2024-04-18 19:14:47 +02:00
Sarjuuk
67d4f12cfe Loot/Errors
* gracefully handle loot referencing nonexistent items
2024-04-18 14:12:20 +02:00
Sarjuuk
97d59dbb98 Tooltips/Icons
* properly format icons in tooltips after b125e55a4a
2024-04-03 21:53:52 +02:00
Sarjuuk
f35adfeb3a Misc/Fixups
* fix rogue letter in zhCN localization
 * remove unused redirects from powered tooltips, ocasionally breaking locale detection (preserved for posterity)
2024-04-03 19:31:11 +02:00
Sarjuuk
e5e4446366 Items/Icon
* set a default icon for items so they don't break listviews
2024-04-03 17:49:54 +02:00
Sarjuuk
e01c3ac205 Misc/Fixups
* fix line terminators in update file
2024-04-02 02:03:39 +02:00
Sarjuuk
e09e3a7260 Spells/Effects
* also list affected spells directly in the spell effect
 * allow more spell auras to display affected spells
2024-04-02 02:00:22 +02:00
Sarjuuk
7b43739dbc Localization/NumberFormat
* use narrow non-breaking space (&#8239) to separate thousands blocks for locales frFR and ruRU
2024-03-30 00:37:22 +01:00
SrzmGit
8e5bdebea0 Articles/Class (#397)
* Added german version of the class articles
2024-03-29 20:19:56 +01:00
Sarjuuk
555a6211bd Misc/Fixup
* no idea
2024-03-26 13:33:33 +01:00
Sarjuuk
6249ac6b2a Misc/Fixup
* STDOUT is only defined if php is in run in cli mode
2024-03-26 12:07:45 +01:00
Sarjuuk
494328cf53 DetailPage/Articles
* initialize articles null as originally expected
 * properly sort found articles or the wrong locale may get selected
2024-03-25 14:52:46 +01:00
Sarjuuk
e2e0a0295b ItemDetailPage/Infobox
* properly line break arena requirements after 8bf7b3ee06
 * fix ancient typo in unused code
2024-03-24 22:11:54 +01:00
Sarjuuk
676a7ef24e Misc/Formating
* removed excess ;
2024-03-16 16:38:02 +01:00
Sarjuuk
c01c9ce901 Misc/Fixups
* use built in function to determine if CLI can use escape codes
 * define _post, _get and _cookie in all cases
 * do not apply 8 regexes to a string that doesn't even contain a UI Sequence
2024-03-16 00:17:19 +01:00
Sarjuuk
d37eb9a59b Maps/Repsawn (#172)
* use more verbose time formater for repsawn time
2024-03-16 00:09:56 +01:00
Sarjuuk
d4a0abf704 Types/Objects
* make traps their own category
 * move "trap for" from infobox to listview tabs as there can be a couple hundred GOs pointing to the same trap.
2024-03-15 23:49:58 +01:00
Sarjuuk
ec1a2afc5f PHP/Compat
* fixed misc issues Intellisense was nice enough to highlight.
 * mostly deprecated usage of uninitialized parameters
 * class GenericPage still needs a long, hard look and a refactor
2024-03-11 23:20:17 +01:00
Sarjuuk
3e6e43fd68 MySQL/Compat
* fix col name / function name conflict
2024-03-11 23:20:06 +01:00
Sarjuuk
c37d2fd594 CLI/WIN
* support ansi escape codes for win 10 and up
 * also kill a warning
2024-03-11 23:16:32 +01:00
Sarjuuk
25b5928a22 ItemFilter/TabLinks
* always display FilterResult prompt on currency-for tabs
 * fixed display of FilterResult prompt on item currency-for tab
2024-03-09 22:44:55 +01:00
Sarjuuk
88b62730e1 SmartAI/Misc
* fix footer for ACTION_FOLLOW
 * use \u003A for : so it doesn't get evaluated as a switch
2024-03-06 21:26:39 +01:00
Sarjuuk
f00ccedbd3 Localization/Invisibility
* invisibility type General is on index 0 not index 1.
2024-03-06 21:25:44 +01:00
Sarjuuk
de2fa3770b JS/Tooltips
* try to get current domain from Locale when used on the site itself and no other params are given
2024-02-29 18:12:46 +01:00
Sarjuuk
be06b1e0cf Items/ExtraLoot
* corrected calculation after writing a simulator and some consultation
 * sad thing is, this will not even be visible after rounding for display
2024-02-29 17:33:55 +01:00
Sarjuuk
29f80f9a76 Items/ExtraLoot
* break down chances for loot from skill_extra_item_template
 * rename tab to make it clear that its content is a bonus
2024-02-29 01:25:34 +01:00
Sarjuuk
e85a9e9d6a Setup/Zones
* instances with with a entrance touple of (0, 0, 0) will no longer be displayed somewhere in Alterac (at the map 0 origin point)
2024-02-28 22:17:07 +01:00
Sarjuuk
54b224d929 Achievements/Rewards
* fix createing bogus ItemLists or TitleLists when displaying achievement rewards
 * don't display empty related-achievements tab on Emotes DetailPage
2024-02-28 21:52:34 +01:00
Sarjuuk
d0e5bec845 Setup/FileGen (#390)
* directories should probably have write permissions
 * add forgotten directory for guide image storage
2024-02-28 21:18:05 +01:00
Sarjuuk
b125e55a4a Custom/UiEscapes
* so, apparently the client allows ui escape sequences in the weirdest of places. Guess i have to follow suit.
 * also, the sequences can be multiline
 * also also, the file extension for icons is optional
2024-02-28 21:04:18 +01:00
Sarjuuk
611d2c40bd Profiler/Access
* load css/js even when profiler is disabled
 * do not display page speciffic announcements for overridden displays (e.g. Profiler help on maintenance, error, etc. display)
2024-02-28 19:05:21 +01:00
Sarjuuk
8b1fd3ac79 Setup/Source
* setting an unsigned column -1 is pretty pointless, isn't it?
 * using a recently truncated, not yet repopulated table to determine if items can be obtained is pretty pointless, RIGHT!?
   (items are not longer blanket tagged as unobtainable)
2024-02-28 19:05:17 +01:00
Sarjuuk
9831038729 Misc/Fixups
* remove w from umask for generated files
 * fixed warning in setup dbconfig
 * added lost Markup.js generation to initial setup
2024-02-28 19:05:17 +01:00
Sarjuuk
8bbffae837 Setup/DB
* fixed syntax error in initial db setup
 * corrected class role custom data
2024-02-28 16:28:06 +01:00
Amandil
b2ca072120 Misc/Fixups (#391)
* lost spell effect/aura declaration changes from 0e0116b27
     * case-sensitivity conflict (ID => id) caused by 77f81c1bd
2024-02-28 15:57:55 +01:00
Sarjuuk
9aeb2177cf Pages/SpellDetailPage
* fixed "modefied-by" and "modifies" tab having different results
 * evalueate some more spell effects
 * replace magic numbers with defined strings
 * get model info from some more spell auras
2024-02-26 01:23:35 +01:00
Sarjuuk
3dfdc300c5 DetailPage/Links (#388)
* add missing trailing slash to url in links list
2024-02-25 22:41:16 +01:00
Sarjuuk
4e65f0a955 Merge remote-tracking branch 'github/master' into ghMaster 2024-02-25 22:31:33 +01:00
Sarjuuk
dd9eaf49ff Page/Listview
* refer to Listview template by default name if able
2024-02-25 21:34:03 +01:00
Sarjuuk
6b0f617d1b Page/Screenshots
* handle more error cases more gracefully when uploading screenshots
2024-02-25 21:28:36 +01:00
Sarjuuk
ba53a5c760 Page/WH
* handle backlink in Page instead of Util
 * update event/holiday link
2024-02-25 21:26:30 +01:00
Sarjuuk
6958efcd0f Items/Tooltip
* do not show cooldown for passive onEquip spells
2024-02-25 21:24:36 +01:00
Sarjuuk
85e8175338 Spells/Misc
* try to handle bogus data from creature_template_addon.auras
2024-02-25 21:23:33 +01:00
Sarjuuk
a14b5e2be1 Libs/DBSimple
* also collect sql warnings for error handling
 * urlescape user/password on the db connection url (mysqli://)
2024-02-25 21:23:09 +01:00
Sarjuuk
d8a6f67688 Misc/Timer
* calling reset() on the Timer no longer breaks the interval
2024-02-25 21:19:10 +01:00
Sarjuuk
979a21afae Localization/Caching
* store source lists as object so different locales can be fetched
 * store parsed spell texts locale dependent so the locale isn't fixed
2024-02-25 21:19:04 +01:00
Sarjuuk
8269d4946f Misc/Home
* update currentyear
2024-02-25 21:18:52 +01:00
Sarjuuk
a39881e73f Misc/Fixup
* fixed borked Lang::load in setup after d0d2451ff51157728e622142c3be7ae0ba7dcebe
2024-02-25 21:17:33 +01:00
Sarjuuk
cfa5030f85 Profiler/ArenaTeams
* remove profiles from existing teams of the same type they are going to be added to.
 * should prevent characters being stuck in old teams.
2024-02-25 21:15:33 +01:00
Sarjuuk
c84d1181bb Localization
* add translation for dungeon floors from GlobalStrings.lua
 * fix SpellMod mistranslation
2024-01-08 00:12:30 +01:00
Sarjuuk
d92b17a386 Loot/Difficulty
* fixed encoding Dungeon Difficulty in source
 * resolve difficulty dummy loot sources to base creature if able
 * added listview note for difficulty source on item detail page
 * misc: fixed parsing color UI escape sequence
2024-01-08 00:06:30 +01:00
Sarjuuk
9e857035bd Creatures/Filter
* implement filter #34 (modelId)
 * thats the actual modelId .. not displayId
2024-01-08 00:05:32 +01:00
Sarjuuk
79383fc83a Search/Opensearch
* return results with correct mime type.
2024-01-08 00:05:32 +01:00
Sarjuuk
0e0116b274 Misc/Defines
* declare and use Spell Effects and Spell Auras
2024-01-08 00:05:32 +01:00
Sarjuuk
deba5325a7 Spells/DetailPage
* display proc info for dummy auras
2024-01-08 00:05:32 +01:00
Sarjuuk
a73d71b966 Profiler/Pets
* fix talent distribution being converted to int
* store pets tamed in heroic dungeons/raids with its base entry
2024-01-08 00:05:32 +01:00
Sarjuuk
06ecfd93d5 Setup/DBC
* add Item.dbc (maybe use later for creature eqipment display)
2024-01-08 00:05:32 +01:00
Sarjuuk
ac34b47c26 Spells/Sounds
* get sounds from ScreenEffect.dbc and link to type Spell and type Sound
 * resolve ScreenEffect name in SpellDetailPage
Fixup
 * fix warning in UI escape sequence parsing
2024-01-08 00:05:32 +01:00
Sarjuuk
9b16f2d84a Localization/WoW strings
* generalize WoW UI escape sequence handling
 * implement use of declinated words from dbc for locale 8 (ruRU)
2024-01-08 00:05:24 +01:00
Sarjuuk
cc594e3415 Skills/Requirements
* fix requirements  display for low level skinning
 * display fishing skill requirements on zone detail page in fishing tab
2024-01-07 23:04:03 +01:00
Sarjuuk
4d6fb4975e Emotes
* have creature emotes in the same system (on a negeative index) as SAI links were uselessly pointing to player emotes before
 * player emotes are now gendered
 * display two more cases of who points at whom when using a player emote
 * use and link event sound from emote
 * display more misc info
2024-01-07 23:03:21 +01:00
Sarjuuk
0f186576d3 Filter/Fixups
* fixed wrong indizes for gem color picker in locale 3 (deDE)
 * fixed interpretation of filter enums. Some are providing their value directly, others have an intermediate key.
 * createSQLForCriterium is no longer abstract. (was implemented in exactly the same way everywhere)
 * erronous criteria are now logged and discarded entirely)
2024-01-07 22:53:38 +01:00
Sarjuuk
837fdf78a0 Zone/DetailPage
* fix quests tab not displaying quests from zone
 * only include special quests (QuestSortID set) and quests directly related to zone instead of any quest in tab
 * adjust quest rewards tab accordingly
 * fix breadcrumbs for dungeon quests with sub category
2023-06-08 21:09:48 +02:00
Sarjuuk
6382302a30 Defines/SpellTrigger
* convert magic numbers to define and use as spell trigger types
2023-06-08 15:35:51 +02:00
Sarjuuk
da8943095b Localization/Typo
* introduced about 10 years ago *nostagia*
2023-06-08 15:35:42 +02:00
Sarjuuk
26da03f029 Libs/jQuery
* upgrade from v1.12.4 to v3.7.0
2023-06-08 15:35:31 +02:00
Sarjuuk
73dd745fe8 Profiler/Achievementpoints
* remove redundant total calculation as it is already stored with the profile
 * do not include statistic in sum calculation. (ffs why do they even have points assigned)
2023-06-08 15:35:19 +02:00
Sarjuuk
eca3e09482 Types/Filter
* do not split strings at \s if match:exact is set.
2023-06-08 15:35:07 +02:00
Sarjuuk
d8d2676596 Profiler/Completion
* move spells w/o source to excludeGroup unavailable
   shouldn't hide. They may have been available in the past.
2023-06-08 15:34:59 +02:00
Sarjuuk
a6f6e0b05d Itemset/Listpage
* implement filter: available to players
2023-06-08 15:30:44 +02:00
Sarjuuk
e9622e6991 Filter/Cleanup
* move shared criteria enums to parent
 * define shared regex patterns filter
 * set missing enum checks
 * fixed some filters
2023-06-08 15:28:57 +02:00
Sarjuuk
fcf24b3a45 Misc
* define and use some more magic numbers
 * move id-based custom data from spell SetupScript to aowow_setup_custom_data table
 * hide on unused glyph while at it
2023-06-08 15:27:49 +02:00
Sarjuuk
77f81c1bde Setup/Sources
* rewrote SetupSrcipt
 * implemented 'zone' parameter
 * implemented 'bossdrop' parameter
 * implemented 'dungeondifficulty' parameter
 * implemented item filter relying on zone information (dropsInX)
 * fixed world random drops showing a single loot source
 * extended Source column of spells to the same functionality as items

ToDo:
 * apply new 'commondrop' parameter on loot listviews
 * gather difficuly versions of gameobjects and apply the same logic as for creatures
 * implement fake spawns so npcs can get linked to a zone
2023-06-08 15:07:13 +02:00
Sarjuuk
fc7a526a67 Items/ListPage
* fixed displaying icon for currencies in vendor related columns
2023-06-08 14:16:29 +02:00
Sarjuuk
e71da620c6 Comments/Replies
* also show replies on latest-comments utility page
 * sort replies by score DESC
2023-06-08 14:16:03 +02:00
Sarjuuk
0d6a6e163c Profiler/Statistics
* use spells for skill modifying racials instead of hardcoded values
 * get baseline statistics from player_levelstats instead of hardcoded values
2023-06-08 14:01:36 +02:00
Sarjuuk
1c5e43d378 Profiler/Completion
* apply factions from player_factionchange_spells to all spells
 * filter companion pets without suitable item source
2023-06-08 14:00:32 +02:00
Sarjuuk
d16d685b70 Guides/Markup
* promote user guides to blogger level so the markup promised by the editor is actually appied.
2023-06-08 13:58:53 +02:00
Sarjuuk
856a98d875 Profiler/Characters
* fixed position of pending rename note
2023-06-08 13:55:35 +02:00
Sarjuuk
bc3ba23162 Guides/Fixups
* fix urls in user menu
 * strip anchors from tooltip title
 * prevent line breaks in description
 * make only in english info popup modular
2023-06-08 13:53:48 +02:00
Sarjuuk
0e9ca3ff90 Quests/Filter
* filter "repeatable" now also considers specialFlags
2023-06-08 13:48:56 +02:00
Sarjuuk
24d683332d Profiler/Completion
* fix partial loading of quests in profiler
 * provide inital category totals on a per class & race basis
2023-06-08 13:45:55 +02:00
Sarjuuk
ebc20be508 Spells/Localization
* use more GlobalStrings in spell context (as far as possible)
 * fixed rune cost order
 * fixes agains locale based time formater
2023-06-08 13:03:05 +02:00
Sarjuuk
8bf7b3ee06 Lang/cleanup
* fixed break and trim functions not handling text shorter than break length
 * add option to output raw text besides html, markup format
 * decalare return types and parameter types
 * cleanup
2023-06-08 12:56:36 +02:00
Sarjuuk
78f7f6b9cf Page/Profile
* fixed hang on profile load without base data
2023-06-08 12:55:06 +02:00
Sarjuuk
bfb7abb843 Profiler/Progression
* reorient icon texture so instance icons no longer appear multiple times on long progress bars
2023-06-08 12:52:54 +02:00
Sarjuuk
70a93d9905 Misc/Lang
* remove some more square brackets denoting missing translations
2023-06-07 19:46:52 +02:00
Stefano Borzì
153d489400 Lang/zhCN (#384)
* cherry pick missing translation from https://github.com/azerothcore/aowow/pull/36
---------

Co-authored-by: yuanf225 <43561197+yuanf225@users.noreply.github.com>
2023-06-06 21:49:23 +02:00
Sarjuuk
40014755e2 Setup/Log
* allow overwriting generic/fine log output
 * fix some errors
 * can't catch notices generated by mysqli_connect
 * removed some unnessecary ORDER from querys
2023-04-25 23:26:25 +02:00
Sarjuuk
acb9c60c9a Profiler/QuickInfo
* remove padding from profiles icon list
2023-04-25 23:23:49 +02:00
Sarjuuk
934066fed8 Tooltips/Style
* also fix width for secondary tooltips
2023-04-25 23:23:41 +02:00
Sarjuuk
e916deaafc Setup/Fixups
* sanity check slot / invtype realtion on setup (and hide offenders)
 * hide internal/unused items by name part
 * always truncate table to get rid of old data
 * fixing one->many relation revealed by replacing REPLACE with INSERT in creature setup
2023-04-25 23:23:19 +02:00
Sarjuuk
1130581152 Items/Stats
* resolve more col name conflicts .. should probably rename the cols in the table itself...
2023-04-20 22:04:25 +02:00
Sarjuuk
5c227c01e4 Currency/PvP
* convert and assign correct icons for honor points and arena points from /Interface/PvPFrame
2023-04-20 19:53:22 +02:00
Sarjuuk
a6897ad2f8 Misc/Fixups
* fixed more error pages without styles/scripts
 * fixed sourcemore for items if more then 300 items where requested at once
 * fixed item name localization in rare cases
2023-04-20 19:51:46 +02:00
Sarjuuk
5be5c2b59e Guides/Listview
* display class/spec in category if applicable
 * make class/spec searchable
 * unify class/spec display with tooltips
2023-04-18 17:20:40 +02:00
Sarjuuk
9c8656f4b5 Admin/Guides
* log approver / time as intended
2023-04-18 15:45:01 +02:00
Sarjuuk
14658a5016 Pages/Scripts
* do not skip generic page constructor ... ever
2023-04-15 11:46:21 +02:00
wodim
5f708470fc Localization/Typo (#354)
* small typo
2023-04-13 22:04:24 +02:00
Sarjuuk
f2a0e75bb1 Scripts/Fixup
*  fix tooltip localization after 9f1cbc0233
2023-04-13 19:26:23 +02:00
Sarjuuk
4f13c492f3 Pages/Icon (#360)
* fixed missing icons in used-by-spell tab
2023-04-13 17:48:10 +02:00
Sarjuuk
6123b6bafc JS/jQuery
* workaround jQuery.htmlPrefilter vulnerability
2023-04-13 17:34:52 +02:00
Sarjuuk
2c142c506c Search/Forms (#383)
* do not prefil search form with unchecked user input
 * thx @Endalaust
2023-04-13 17:34:39 +02:00
Sarjuuk
9f1cbc0233 HTML/Scripts
* append a filemtime timestamp to js/css files to work around browser caching after update
 * shuffle script handling around a bit
 * also user pages cant have community content
 * also fix breadcrumbs on items page
2023-04-13 17:30:23 +02:00
Sarjuuk
b06d1a5c2c Pages/Caching (#380)
* move localized option sorting to postCache() to prevent real order display in cached versions
2023-04-13 17:26:27 +02:00
Sarjuuk
beb32da3b4 Itemset/Summary (#351)
* disable compare button for empty item sets
 * do not display summary for empty item sets
2023-04-13 17:25:42 +02:00
Sarjuuk
2e82bf84d2 Pages/Spell (#378)
* fixed effect index offset on DetailPage
2023-04-13 17:25:31 +02:00
Sarjuuk
25ddb0ca99 Items/FAP
* show Feral Attack Power on weapons a druid can actually use (incuding 1H-weapons, excluding swords & axes (usually))
2023-04-13 17:25:17 +02:00
Sarjuuk
02239b4f74 Misc/Fixups
* fix text vars in tooltips for missing entities
 * fix notice in weapon GS calculation
 * reduce tooltip max width slightly to prevent overlap with secondary tooltip
2023-04-13 17:21:55 +02:00
Sarjuuk
e0a3c44776 Setup/Spells (#374)
* fixed rarity color for spells that create items
2023-04-13 17:20:50 +02:00
Sarjuuk
e513e01b29 Setup/TalentIcons (#382)
* fix icon order for pet talents
2023-04-13 17:16:49 +02:00
Sarjuuk
138dbbc8a5 Game/Text
* try to catch more html-like constructs in text blobs and escape them
 * ...<hic>!
2023-04-13 17:16:01 +02:00
Sarjuuk
6f87870e09 Item/Tooltips (#368)
* fixed broken link to required event
2023-04-13 17:15:53 +02:00
Sarjuuk
2210c0c4c5 Spells/Auras (#379)
* get auras from creature_template_addon for tabs
   - npc: abilities
   - spell: used by npc
2023-04-13 17:15:28 +02:00
Sarjuuk
8ab8eee1f4 Tooltips
* revert e092a69175 as it causes the tooltip to grow when moving the mouse.
 * use CSS to solve
2023-04-13 17:13:38 +02:00
Sarjuuk
d77e459da3 Tooltips/TimeFMT
* use strings from globalstrings.lua to format time in item and spell tooltips
 * fixed item filter ItemCooldown
 * fixed timeAgo format
 * move item duration to correct position in tooltip
2023-04-13 17:12:58 +02:00
Sarjuuk
6ee0d63766 Update external links to point to wotlk speciffic database 2023-04-13 17:12:02 +02:00
Sarjuuk
0c47f262ea Userdata
* replace input filter FILTER_UNSAFE_RAW (+ STRIP flags) with regex checks to preserve \n and utf8 chars
2023-04-13 17:07:59 +02:00
Sarjuuk
ffa4cf5b29 Misc/Fixes
* resolve more spellEffects/Auras
 * fixed item filter with multiple upgrade items
 * localized unknown spellAura/Effects text
 * remove unreferenced error code from image setup
2023-04-13 17:05:30 +02:00
Sarjuuk
ca26955ac2 Setup/SQL
* change sql batching to account for non continuous indizes in world tables
2023-04-13 16:48:52 +02:00
Sarjuuk
f05fe60030 Spells/Tooltips
* fix level slider for spells with buffs but without tooltips
2023-04-13 16:31:09 +02:00
Sarjuuk
2b15c13e88 User/Guides
* fixed copy/paste fail
2023-04-13 16:16:58 +02:00
Sarjuuk
df1ba841c5 Item/Tooltip
* display hidden/cosmetic spells for staff
2022-06-10 16:58:18 +02:00
Sarjuuk
47da18b717 Profiler/Caching
* redirect to search for subjects without cache, instead of hanging on an empty profile
 * todo: fix properly
2022-06-10 16:41:03 +02:00
Sarjuuk
117ab617b6 SAI/Teleport (#346)
* fixed resolving teleport target for NPCs
2022-06-10 16:00:33 +02:00
Sarjuuk
7e5659f49f NPC/Waypoints
* improve display for NPCs traversing zones and/or floors (e.g.: npc 844)
2022-06-10 15:15:28 +02:00
Sarjuuk
e493acca0d Item/XML
* fix output for currency, quest and key subclasses
2022-06-10 14:40:30 +02:00
Sarjuuk
6de6853cfe Core/PHP
* bump version requirements to v8.0
2022-06-10 14:00:35 +02:00
Sarjuuk
8ec6cc548d Merge remote-tracking branch 'origin/master' 2022-06-10 13:58:55 +02:00
Sarjuuk
c0e9159c1e Misc/Fixups
* colAddins should be null if not in use
 * fix index warnings in search + profiler
 * cast url param 'locale' to int for all uses
 * fix breadcrumbs for sounds page
 * fix determining actionOwner for SmartAI
2022-06-10 13:58:13 +02:00
Dima
1bd752a60f Typo/Spells (#343)
* fixed legacy typo in spell power calculation
2022-05-30 16:27:56 +02:00
Sarjuuk
df3694b539 Setup/Mails (#340)
* fix borked setup after b3e8f5e50f
 * thx @jackpoz for the research
2022-05-08 00:22:10 +02:00
Sarjuuk
6594d6fa42 Spells/Tooltips
* htmlify \n in spell tooltips & buffs
 * allow recursion of Util::parseHtmlText()
2022-04-05 18:32:41 +02:00
Sarjuuk
8425eeb685 Reports
* move related functions to separate class
 * implement out of date comment handling shorthand
 * implement admin interface to work with reports
   - listing based on user group
   - assign to self / unassign from self
   - close with reason
   - comment functionality
   - reward reputation to creator based on resolution
2022-04-05 15:45:56 +02:00
HelloKitty
2daa724720 README/Typo (#337)
Fix typo in the Special Thanks section.
2022-03-30 13:18:16 +02:00
Sarjuuk
6db77ed4f2 CommunityContent/Listing
* generally use indexed/non-asssociative arrays when returning content
2022-03-28 16:25:09 +02:00
Sarjuuk
b08d30d043 Screenshots/Crop
* fixed coordinate filter broken after a8edf6c912
2022-03-28 00:58:43 +02:00
Sarjuuk
b3e8f5e50f DB/Mails
* add field cuFlags to Type:Mail storage (RandomPage search no longer randomly fails, when randomly selecting mails)
2022-03-25 16:41:58 +01:00
Sarjuuk
1f5e2645f0 Misc/Fixups
* forgotten rename fails
2022-03-25 16:41:58 +01:00
Sarjuuk
04e55b5498 NPCs/Vendors
* show restock time if available
2022-03-25 16:41:49 +01:00
Sarjuuk
32b4c451e4 Misc/Fixup
* remove unnecessary tab components from ?unrated-comments page
 * fix urls generated by guides listview
2022-03-24 10:38:45 +01:00
Sarjuuk
05f6d68070 Utility
* implemented page: unrated-comment
2022-03-23 19:28:40 +01:00
Sarjuuk
e572967c08 Misc/Fixup
* lost changes to timestamps from a8edf6c912
* dates on comment, screenshot, video previews are now formatted correctly
2022-03-23 11:19:00 +01:00
Sarjuuk
1dc8d50289 Misc/Fixup
* move guide localization, so Lang::typeName can pick it up
 * rename forgotten TYPE_* definitions
2022-03-22 22:27:19 +01:00
Sarjuuk
7caabc0fa8 Misc/Fixup
* obligatory after-the-fact fixups
2022-03-22 19:07:47 +01:00
Sarjuuk
d819bf60f5 Merge remote-tracking branch 'origin/master' 2022-03-22 15:57:18 +01:00
Sarjuuk
65bfd93761 Profiler
* truncate local profile if sync with game server fails (entry deleted)
 * do not use chars/guilds/arena teams with empty names
2022-03-13 15:03:11 +01:00
Sarjuuk
3a98201837 Zones/Locations
* fixed Quick Info defaulting expansion maps to have [0, 0, 0] as parent
 * corrected looking for custom excluded flag when collecting instances for area
2022-03-08 18:52:01 +01:00
511 changed files with 46009 additions and 31573 deletions

10
.gitattributes vendored
View File

@@ -1,4 +1,14 @@
* text=input
*.php text eol=lf
*.js text eol=lf
*.css text eol=lf
*.sql text eol=lf
aowow text eol=lf
prQueue text eol=lf
*.png binary
*.jpg binary
*.gif binary
*.ttf binary
*.swf binary

4
.gitignore vendored
View File

@@ -3,8 +3,8 @@
# cache
/cache/template/*
/setup/generated/alphaMaps/*.png
/cache/firstrun
/cache/alphaMaps/*
/cache/setup/*
# extract from MPQ
/setup/mpqdata/*

View File

@@ -13,20 +13,20 @@ While the first releases can be found as early as 2008, today it is impossible t
This is a complete rewrite of the serverside php code and update to the clientside javascripts from 2008 to something 2013ish.
I myself take no credit for the clientside scripting, design and layout that these php-scripts cater to.
Also, this project is not meant to be used for commercial puposes of any kind!
Also, this project is not meant to be used for commercial purposes of any kind!
## Requirements
+ Webserver running PHP ≥ 7.4 — 8.0 including extensions:
+ 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.5.30
+ [TDB 335.21101](https://github.com/TrinityCore/TrinityCore/releases/tag/TDB335.21101)
+ 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))
@@ -39,7 +39,7 @@ audio processing may require [lame](https://sourceforge.net/projects/lame/files/
#### Highly Recommended
+ setting the following configuration values on your TrinityCore server will greatly increase the accuracy of spawn points
> Calculate.Creature.Zone.Area.Data = 1
> Calculate.Gameoject.Zone.Area.Data = 1
> Calculate.Gameobject.Zone.Area.Data = 1
## Install
@@ -72,19 +72,19 @@ Extract the following directories from the client archives into `setup/mpqdata/`
.. once is enough (still apply the localeCode though):
> \<localeCode>/Interface/TalentFrame/
> \<localeCode>/Interface/Glues/Credits/
> \<localeCode>/Interface/Icons/
> \<localeCode>/Interface/Spellbook/
> \<localeCode>/Interface/PaperDoll/
> \<localeCode>/Interface/GLUES/CHARACTERCREATE/
> \<localeCode>/Interface/Glues/CharacterCreate/
> \<localeCode>/Interface/Pictures
> \<localeCode>/Interface/PvPRankBadges
> \<localeCode>/Interface/FlavorImages
> \<localeCode>/Interface/Calendar/Holidays/
> \<localeCode>/Sound/
.. optionaly (not used in AoWoW):
> \<localeCode>/Interface/GLUES/LOADINGSCREENS/
.. 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`.
@@ -100,24 +100,24 @@ When you've created your admin account you are done.
## Troubleshooting
Q: The Page appears white, without any styles.
A: The static content is not being displayed. You are either using SSL and AoWoW is unable to detect it or STATIC_HOST is not defined poperly. Either way this can be fixed via config `php aowow --siteconfig`
A: The static content is not being displayed. You are either using SSL and AoWoW is unable to detect it or STATIC_HOST is not defined properly. Either way this can be fixed via config `php aowow --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 confict with each other. (Zend OPcache, XCache, ..) Disable all but one.
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 priorize png files over blp files.
AoWoW will prioritize png files over blp files.
Q: How can i get the modelviewer to work?
A: You can't anymore. Wowhead switched from Flash to WebGL (as they should) and moved or deleted the old files in the process.
Q: I'm getting random javascript errors!
A: Some server configurations or external services (like Cloudflare) come with modules, that automaticly minify js and css files. Sometimes they break in the process. Disable the module in this case.
A: Some server configurations or external services (like Cloudflare) come with modules, that automatically minify js and css files. Sometimes they break in the process. Disable the module in this case.
Q: Some search results within the profiler act rather strange. How does it work?
A: Whenever you try to view a new character, AoWoW needs to fetch it first. Since the data is structured for the needs of TrinityCore and not for easy viewing, AoWoW needs to save and restructure it locally. To this end, every char request is placed in a queue. While the queue is not empty, a single instance of `prQueue` is run in the background as not to overwhelm the characters database with requests. This also means, some more exotic search queries can't be run agains the characters database and have to use the incomplete/outdated cached profiles of AoWoW.
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.
@@ -134,7 +134,7 @@ A: A search is only conducted against the currently used locale. You may have on
## Special Thanks
Said website with the red smiling rocket, for providing this beautifull website!
Please do not reagard this project as blatant rip-off, rather as "We do really liked your presentation, but since time and content progresses, you are sadly no longer supplying the data we need".
Said website with the red smiling rocket, for providing this beautiful website!
Please do not regard this project as blatant rip-off, rather as "We do really liked your presentation, but since time and content progresses, you are sadly no longer supplying the data we need".
![uses badges](http://forthebadge.com/images/badges/uses-badges.svg)

17
aowow
View File

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

View File

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

View File

@@ -1,46 +0,0 @@
Mapper.multiLevelZones = {
206: ['206-1', '206-2', '206-3'],
209: ['209-1', '209-2', '209-3', '209-4', '209-5', '209-6', '209-7'],
616: ['616-1', '616_1', '616_2'],
719: ['719-1', '719-2', '719-3'],
721: ['721-1', '721-2', '721-3', '721-4'],
796: ['796-1', '796-2', '796-3', '796-4'],
1196: ['1196-1', '1196-2'],
1337: ['1337-1', '1337-2'],
1581: ['1581-1', '1581-2'],
1583: ['1583-1', '1583-2', '1583-3', '1583-4', '1583-5', '1583-6', '1583-7'],
1584: ['1584-1', '1584-2'],
2017: ['2017-1', '2017-2'],
2057: ['2057-1', '2057-2', '2057-3', '2057-4'],
2100: ['2100-1', '2100-2'],
2557: ['2557-1', '2557-2', '2557-3', '2557-4', '2557-5', '2557-6'],
2677: ['2677-1', '2677-2', '2677-3', '2677-4'],
3959: ['3959', '3959-1', '3959-2', '3959-3', '3959-4', '3959-5', '3959-6', '3959-7'],
3428: ['3428-1', '3428-2', '3428-3'],
3456: ['3456-1', '3456-2', '3456-3', '3456-4', '3456-5', '3456-6'],
3457: ['3457-1', '3457-2', '3457-3', '3457-4', '3457-5', '3457-6', '3457-7', '3457-8', '3457-9', '3457-10', '3457-11', '3457-12', '3457-13', '3457-14', '3457-15', '3457-16', '3457-17'],
3477: ['3477-1', '3477-2', '3477-3'],
3715: ['3715-1', '3715-2'],
3790: ['3790-1', '3790-2'],
3791: ['3791-1', '3791-2'],
3848: ['3848-1', '3848-2', '3848-3'],
3849: ['3849-1', '3849-2'],
4075: ['4075', '4075-1'],
4100: ['4100-1', '4100-2'],
4131: ['4131-1', '4131-2'],
4196: ['4196-1', '4196-2'],
4228: ['4228-1', '4228-2', '4228-3', '4228-4'],
4272: ['4272-1', '4272-2'],
4273: ['4273-0', '4273-1', '4273-2', '4273-3', '4273-4', '4273-5'],
4277: ['4277-1', '4277-2', '4277-3'],
4395: ['4395-1', '4395-2'],
4494: ['4494-1', '4494-2'],
4714: ['4714-1', '4714_1', '4714_2', '4714_3'],
4722: ['4722-1', '4722-2'],
4812: ['4812-1', '4812-2', '4812-3', '4812-4', '4812-5', '4812-6', '4812-7', '4812-8'],
};
/*
var g_zone_areas = {};
in locale files
*/

View File

@@ -1,5 +1,7 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
@@ -10,9 +12,9 @@ class AjaxAccount extends AjaxHandler
'groups' => ['filter' => FILTER_SANITIZE_NUMBER_INT],
'save' => ['filter' => FILTER_SANITIZE_NUMBER_INT],
'delete' => ['filter' => FILTER_SANITIZE_NUMBER_INT],
'id' => ['filter' => FILTER_CALLBACK, 'options' => 'AjaxHandler::checkIdList'],
'name' => ['filter' => FILTER_CALLBACK, 'options' => 'AjaxAccount::checkName' ],
'scale' => ['filter' => FILTER_CALLBACK, 'options' => 'AjaxAccount::checkScale' ],
'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],
@@ -20,18 +22,12 @@ class AjaxAccount extends AjaxHandler
'remove' => ['filter' => FILTER_SANITIZE_NUMBER_INT],
// 'sessionKey' => ['filter' => FILTER_SANITIZE_NUMBER_INT]
);
protected $_get = array(
'locale' => ['filter' => FILTER_CALLBACK, 'options' => 'AjaxHandler::checkLocale']
);
public function __construct(array $params)
{
parent::__construct($params);
if (is_numeric($this->_get['locale']))
User::useLocale($this->_get['locale']);
if (!$this->params || !User::$id)
if (!$this->params || !User::isLoggedIn())
return;
// select handler
@@ -50,22 +46,21 @@ class AjaxAccount extends AjaxHandler
$type = $this->_post['type'];
$ids = $this->_post['id'];
if (!Type::exists($type) || empty($ids))
if ($validIds = Type::validateIds($this->_post['type'], $this->_post['id']))
{
trigger_error('AjaxAccount::handleExclude - invalid type #'.$type.(empty($ids) ? ' or id-list empty' : ''), E_USER_ERROR);
return;
// ready for some bullshit? here it comes!
// we don't get signaled whether an id should be added to or removed from either includes or excludes
// so we throw everything into one table and toggle the mode if its already in here
$includes = DB::Aowow()->selectCol('SELECT `typeId` FROM ?_profiler_excludes WHERE `type` = ?d AND `typeId` IN (?a)', $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]
);
}
// ready for some bullshit? here it comes!
// we don't get signaled whether an id should be added to or removed from either includes or excludes
// so we throw everything into one table and toggle the mode if its already in here
$includes = DB::Aowow()->selectCol('SELECT typeId FROM ?_profiler_excludes WHERE type = ?d AND typeId IN (?a)', $type, $ids);
foreach ($ids as $typeId)
DB::Aowow()->query('INSERT INTO ?_account_excludes (`userId`, `type`, `typeId`, `mode`) VALUES (?a) ON DUPLICATE KEY UPDATE mode = (mode ^ 0x3)', array(
User::$id, $type, $typeId, in_array($includes, $typeId) ? 2 : 1
));
else
trigger_error('AjaxAccount::handleExclude - invalid type #'.$type.(empty($ids) ? ' or id-list empty' : ''), E_USER_ERROR);
return;
}
@@ -94,24 +89,24 @@ class AjaxAccount extends AjaxHandler
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))
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);
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);
$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);
DB::Aowow()->query('DELETE FROM ?_account_weightscale_data WHERE `id` = ?d', $id);
foreach (explode(',', $this->_post['scale']) as $s)
{
@@ -125,12 +120,13 @@ class AjaxAccount extends AjaxHandler
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);
else
{
trigger_error('AjaxAccount::handleWeightscales - malformed request received', E_USER_ERROR);
return '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
@@ -146,8 +142,7 @@ class AjaxAccount extends AjaxHandler
if ($type = $this->_post['add'])
{
$tc = Type::newList($type, [['id', $typeId]]);
if (!$tc || $tc->error)
if (!Type::validateIds($this->_post['add'], $this->_post['id']))
{
trigger_error('AjaxAccount::handleFavorites - invalid typeId #'.$typeId.' for type #'.$type, E_USER_ERROR);
return;
@@ -171,7 +166,7 @@ class AjaxAccount extends AjaxHandler
{
$var = trim(urldecode($val));
return filter_var($var, FILTER_UNSAFE_RAW, FILTER_FLAG_STRIP_AOWOW);
return filter_var($var, FILTER_SANITIZE_SPECIAL_CHARS, FILTER_FLAG_STRIP_LOW);
}
}

View File

@@ -1,31 +1,33 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
class AjaxAdmin extends AjaxHandler
{
protected $validParams = ['screenshots', 'siteconfig', 'weight-presets', 'spawn-override', 'guide'];
protected $validParams = ['screenshots', 'siteconfig', 'weight-presets', 'spawn-override', 'guide', 'comment'];
protected $_get = array(
'action' => ['filter' => FILTER_UNSAFE_RAW, 'flags' => FILTER_FLAG_STRIP_AOWOW ],
'id' => ['filter' => FILTER_CALLBACK, 'options' => 'AjaxHandler::checkIdListUnsigned'],
'key' => ['filter' => FILTER_CALLBACK, 'options' => 'AjaxAdmin::checkKey' ],
'all' => ['filter' => FILTER_CALLBACK, 'options' => 'AjaxHandler::checkFulltext' ],
'type' => ['filter' => FILTER_CALLBACK, 'options' => 'AjaxHandler::checkInt' ],
'typeid' => ['filter' => FILTER_CALLBACK, 'options' => 'AjaxHandler::checkInt' ],
'user' => ['filter' => FILTER_CALLBACK, 'options' => 'AjaxAdmin::checkUser' ],
'val' => ['filter' => FILTER_CALLBACK, 'options' => 'AjaxHandler::checkFulltext' ],
'guid' => ['filter' => FILTER_CALLBACK, 'options' => 'AjaxHandler::checkInt' ],
'area' => ['filter' => FILTER_CALLBACK, 'options' => 'AjaxHandler::checkInt' ],
'floor' => ['filter' => FILTER_CALLBACK, 'options' => 'AjaxHandler::checkInt' ]
'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_UNSAFE_RAW, 'flags' => FILTER_FLAG_STRIP_AOWOW],
'id' => ['filter' => FILTER_CALLBACK, 'options' => 'AjaxHandler::checkInt'],
'scale' => ['filter' => FILTER_CALLBACK, 'options' => 'AjaxAdmin::checkScale'],
'__icon' => ['filter' => FILTER_CALLBACK, 'options' => 'AjaxAdmin::checkKey' ],
'status' => ['filter' => FILTER_CALLBACK, 'options' => 'AjaxHandler::checkInt'],
'msg' => ['filter' => FILTER_UNSAFE_RAW, 'flags' => FILTER_FLAG_STRIP_AOWOW]
'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)
@@ -89,6 +91,13 @@ class AjaxAdmin extends AjaxHandler
$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)
@@ -114,7 +123,7 @@ class AjaxAdmin extends AjaxHandler
if ($this->_get['type'] && $this->_get['type'] && $this->_get['typeid'] && $this->_get['typeid'])
$res = CommunityContent::getScreenshotsForManager($this->_get['type'], $this->_get['typeid']);
else if ($this->_get['user'])
if ($uId = DB::Aowow()->selectCell('SELECT id FROM ?_account WHERE displayName = ?', $this->_get['user']))
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);
@@ -317,20 +326,7 @@ class AjaxAdmin extends AjaxHandler
$key = trim($this->_get['key']);
$val = trim(urldecode($this->_get['val']));
if ($key === null)
return 'empty option name given';
if (!strlen($key))
return 'invalid chars in option name: [a-z 0-9 _ . -] are allowed';
if (ini_get($key) === false || ini_set($key, $val) === false)
return 'this configuration option cannot be set';
if (DB::Aowow()->selectCell('SELECT 1 FROM ?_config WHERE `flags` & ?d AND `key` = ?', CON_FLAG_PHP, $key))
return 'this configuration option is already in use';
DB::Aowow()->query('INSERT IGNORE INTO ?_config (`key`, `value`, `cat`, `flags`) VALUES (?, ?, 0, ?d)', $key, $val, CON_FLAG_TYPE_STRING | CON_FLAG_PHP);
return '';
return Cfg::add($key, $val);
}
protected function confRemove() : string
@@ -338,39 +334,15 @@ class AjaxAdmin extends AjaxHandler
if (!$this->reqGET('key'))
return 'invalid configuration option given';
if (DB::Aowow()->query('DELETE FROM ?_config WHERE `key` = ? AND (`flags` & ?d) = 0', $this->_get['key'], CON_FLAG_PERSISTENT))
return '';
else
return 'option name is either protected or was not found';
return Cfg::delete($this->_get['key']);
}
protected function confUpdate() : string
{
$key = trim($this->_get['key']);
$val = trim(urldecode($this->_get['val']));
$msg = '';
if (!strlen($key))
return 'empty option name given';
$cfg = DB::Aowow()->selectRow('SELECT `flags`, `value` FROM ?_config WHERE `key` = ?', $key);
if (!$cfg)
return 'configuration option not found';
if (!($cfg['flags'] & CON_FLAG_TYPE_STRING) && !strlen($val))
return 'empty value given';
else if ($cfg['flags'] & CON_FLAG_TYPE_INT && !preg_match('/^-?\d+$/i', $val))
return "value must be integer";
else if ($cfg['flags'] & CON_FLAG_TYPE_FLOAT && !preg_match('/^-?\d*(,|.)?\d+$/i', $val))
return "value must be float";
else if ($cfg['flags'] & CON_FLAG_TYPE_BOOL && $val != '1')
$val = '0';
DB::Aowow()->query('UPDATE ?_config SET `value` = ? WHERE `key` = ?', $val, $key);
if (!$this->confOnChange($key, $val, $msg))
DB::Aowow()->query('UPDATE ?_config SET `value` = ? WHERE `key` = ?', $cfg['value'], $key);
return $msg;
return Cfg::set($key, $val);
}
protected function wtSave() : string
@@ -413,14 +385,14 @@ class AjaxAdmin extends AjaxHandler
$area = $this->_get['area'];
$floor = $this->_get['floor'];
if (!in_array($type, [Type::NPC, Type::OBJECT, Type::SOUND, Type::AREATRIGGER]))
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 = Game::getWorldPosForGUID($type, $guid))
if ($wPos = WorldPosition::getForGUID($type, $guid))
{
if ($point = Game::worldPosToZonePos($wPos[$guid]['mapId'], $wPos[$guid]['posX'], $wPos[$guid]['posY'], $area, $floor))
if ($point = WorldPosition::toZonePos($wPos[$guid]['mapId'], $wPos[$guid]['posX'], $wPos[$guid]['posY'], $area, $floor))
{
$updGUIDs = [$guid];
$newPos = array(
@@ -434,9 +406,9 @@ class AjaxAdmin extends AjaxHandler
if ($type == Type::NPC)
{
$jobs = array(
'SELECT -w.id AS `entry`, w.point AS `pointId`, w.position_y AS `posX`, w.position_x 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_y` AS `posX`, `location_x` AS `posY` FROM `script_waypoint` WHERE `entry` = ?d',
'SELECT `entry`, `pointId`, `position_y` AS `posX`, `position_x` AS `posY` FROM `waypoints` WHERE `entry` = ?d'
'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)
@@ -445,7 +417,7 @@ class AjaxAdmin extends AjaxHandler
{
foreach ($swp as $w)
{
if ($point = Game::worldPosToZonePos($wPos[$guid]['mapId'], $w['posX'], $w['posY'], $area, $floor))
if ($point = WorldPosition::toZonePos($wPos[$guid]['mapId'], $w['posX'], $w['posY'], $area, $floor))
{
$p = array(
'posX' => $point[0]['posX'],
@@ -453,8 +425,9 @@ class AjaxAdmin extends AjaxHandler
'areaId' => $point[0]['areaId'],
'floor' => $point[0]['floor']
);
DB::Aowow()->query('UPDATE ?_creature_waypoints SET ?a WHERE `creatureOrPath` = ?d AND `point` = ?d', $p, $w['entry'], $w['pointId']);
}
DB::Aowow()->query('UPDATE ?_creature_waypoints SET ?a WHERE `creatureOrPath` = ?d AND `point` = ?d', $p, $w['entry'], $w['pointId']);
}
}
}
@@ -484,11 +457,11 @@ class AjaxAdmin extends AjaxHandler
// 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) WHERE `id` = ?d', Type::GUIDE, $id, $id);
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);
DB::Aowow()->query('INSERT INTO ?_guides_changelog (`id`, `date`, `userId`, `msg`) VALUES (?d, ?d, ?d, ?)', $id, time(), User::$id, $msg);
return true;
};
@@ -526,6 +499,28 @@ class AjaxAdmin extends AjaxHandler
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 */
@@ -534,7 +529,7 @@ class AjaxAdmin extends AjaxHandler
protected static function checkKey(string $val) : string
{
// expecting string
if (preg_match('/[^a-z0-9_\.\-]/i', $val))
if (preg_match(Cfg::PATTERN_INV_CONF_KEY, $val))
return '';
return strtolower($val);
@@ -557,73 +552,6 @@ class AjaxAdmin extends AjaxHandler
return '';
}
/**********/
/* helper */
/**********/
private static function confOnChange(string $key, string $val, string &$msg) : bool
{
$fn = $buildList = null;
switch ($key)
{
case 'battlegroup':
$buildList = 'realms,realmMenu';
break;
case 'name_short':
$buildList = 'searchboxBody,demo,searchplugin';
break;
case 'site_host':
$buildList = 'searchplugin,demo,power,searchboxBody';
break;
case 'static_host':
$buildList = 'searchplugin,power,searchboxBody,searchboxScript';
break;
case 'contact_email':
$buildList = 'markup';
break;
case 'locales':
$buildList = 'locales';
$msg .= ' * remember to rebuild all static files for the language you just added.<br />';
$msg .= ' * you can speed this up by supplying the regionCode to the setup: <pre class="q1">--locales=<regionCodes,> -f</pre>';
break;
case 'profiler_enable':
$buildList = 'realms,realmMenu';
$fn = function($x) use (&$msg) {
if (!$x)
return true;
return Profiler::queueStart($msg);
};
break;
case 'acc_auth_mode':
$fn = function($x) use (&$msg) {
if ($x == 1 && !extension_loaded('gmp'))
{
$msg .= 'PHP extension GMP is required to use TrinityCore as auth source, but is not currently enabled.<br />';
return false;
}
return true;
};
break;
default: // nothing to do, everything is fine
return true;
}
if ($buildList)
{
// we need to use exec as build() can only be run from CLI
exec('php aowow --build='.$buildList, $out);
foreach ($out as $o)
if (strstr($o, 'ERR'))
$msg .= explode('0m]', $o)[1]."<br />\n";
}
return $fn ? $fn($val) : true;
}
}
?>

View File

@@ -1,5 +1,7 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
@@ -37,10 +39,7 @@ class AjaxHandler
return false;
}
$h = $this->handler;
$out = $this->$h();
if ($out === null)
$out = '';
$out = $this->{$this->handler}() ?? '';
return true;
}
@@ -68,4 +67,5 @@ class AjaxHandler
return true;
}
}
?>

View File

@@ -1,14 +1,16 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('invalid access');
die('illegal access');
class AjaxArenaTeam extends AjaxHandler
{
protected $validParams = ['resync', 'status'];
protected $_get = array(
'id' => ['filter' => FILTER_CALLBACK, 'options' => 'AjaxHandler::checkIdList' ],
'profile' => ['filter' => FILTER_CALLBACK, 'options' => 'AjaxHandler::checkEmptySet'],
'id' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\AjaxHandler::checkIdList' ],
'profile' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\AjaxHandler::checkEmptySet'],
);
public function __construct(array $params)
@@ -74,8 +76,7 @@ class AjaxArenaTeam extends AjaxHandler
*/
protected function handleStatus() : string
{
$response = Profiler::resyncStatus(Type::ARENA_TEAM, $this->_get['id']);
return Util::toJSON($response);
return Profiler::resyncStatus(Type::ARENA_TEAM, $this->_get['id']);
}
}

View File

@@ -1,5 +1,7 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
@@ -11,23 +13,23 @@ class AjaxComment extends AjaxHandler
const REPLY_LENGTH_MAX = 600;
protected $_post = array(
'id' => ['filter' => FILTER_CALLBACK, 'options' => 'AjaxHandler::checkIdListUnsigned'],
'body' => ['filter' => FILTER_CALLBACK, 'options' => 'AjaxHandler::checkFulltext' ],
'commentbody' => ['filter' => FILTER_CALLBACK, 'options' => 'AjaxHandler::checkFulltext' ],
'response' => ['filter' => FILTER_UNSAFE_RAW, 'flags' => FILTER_FLAG_STRIP_AOWOW ],
'reason' => ['filter' => FILTER_UNSAFE_RAW, 'flags' => FILTER_FLAG_STRIP_AOWOW ],
'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_UNSAFE_RAW, 'flags' => FILTER_FLAG_STRIP_AOWOW ]
'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' => 'AjaxHandler::checkInt'],
'type' => ['filter' => FILTER_CALLBACK, 'options' => 'AjaxHandler::checkInt'],
'typeid' => ['filter' => FILTER_CALLBACK, 'options' => 'AjaxHandler::checkInt'],
'rating' => ['filter' => FILTER_SANITIZE_NUMBER_INT]
'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)
@@ -170,14 +172,14 @@ class AjaxComment extends AjaxHandler
protected function handleCommentDelete() : void
{
if (!$this->_post['id'] || !User::$id)
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}',
$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'],
@@ -187,13 +189,14 @@ class AjaxComment extends AjaxHandler
// deflag hasComment
if ($ok)
{
$coInfo = DB::Aowow()->selectRow('SELECT IF(BIT_OR(~b.flags) & ?d, 1, 0) as hasMore, b.type, b.typeId FROM ?_comments a JOIN ?_comments b ON a.type = b.type AND a.typeId = b.typeId WHERE a.id = ?d',
$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']
);
if (!$coInfo['hasMore'] && ($tbl = Type::getClassAttrib($coInfo['type'], 'dataTable')))
DB::Aowow()->query('UPDATE '.$tbl.' SET cuFlags = cuFlags & ~?d WHERE id = ?d', CUSTOM_HAS_COMMENT, $coInfo['typeId']);
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);
@@ -201,14 +204,14 @@ class AjaxComment extends AjaxHandler
protected function handleCommentUndelete() : void
{
if (!$this->_post['id'] || !User::$id)
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}',
$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
@@ -217,9 +220,10 @@ class AjaxComment extends AjaxHandler
// reflag hasComment
if ($ok)
{
$coInfo = DB::Aowow()->selectRow('SELECT type, typeId FROM ?_comments WHERE id = ?d', $this->_post['id']);
if ($tbl = Type::getClassAttrib($coInfo['type'], 'dataTable'))
DB::Aowow()->query('UPDATE '.$tbl.' SET cuFlags = cuFlags | ?d WHERE id = ?d', CUSTOM_HAS_COMMENT, $coInfo['typeId']);
$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);
@@ -238,7 +242,7 @@ class AjaxComment extends AjaxHandler
protected function handleCommentVote() : string
{
if (!User::$id || !$this->_get['id'] || !$this->_get['rating'])
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']);
@@ -246,7 +250,7 @@ class AjaxComment extends AjaxHandler
if ($this->_get['rating'] < 0)
$val *= -1;
if (User::getCurDailyVotes() <= 0)
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')]);
@@ -300,19 +304,24 @@ class AjaxComment extends AjaxHandler
if (User::isInGroup(U_GROUP_MODERATOR)) // directly mark as outdated
{
if (!$this->_post['remove'])
$ok = DB::Aowow()->query('UPDATE ?_comments SET flags = flags | 0x4 WHERE id = ?d', $this->_post['id'][0]);
$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 & ~0x4 WHERE id = ?d', $this->_post['id'][0]);
$ok = DB::Aowow()->query('UPDATE ?_comments SET flags = flags & ~?d WHERE id = ?d', CC_FLAG_OUTDATED, $this->_post['id'][0]);
}
else if (DB::Aowow()->selectCell('SELECT 1 FROM ?_reports WHERE `mode` = ?d AND `reason`= ?d AND `subject` = ?d AND `userId` = ?d', 1, 17, $this->_post['id'][0], User::$id))
return Lang::main('alreadyReport');
else if (User::$id && !$this->_post['reason'] || mb_strlen($this->_post['reason']) < self::REPLY_LENGTH_MIN)
return Lang::main('textTooShort');
else if (User::$id) // only report as outdated
$ok = Util::createReport(1, 17, $this->_post['id'][0], '[Outdated Comment] '.$this->_post['reason']);
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 ($ok) // this one is very special; as in: completely retarded
return 'ok'; // the script expects the actual characters 'ok' not some string like "ok"
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);
@@ -384,7 +393,7 @@ class AjaxComment extends AjaxHandler
protected function handleReplyDelete() : void
{
if (!User::$id || !$this->_post['id'])
if (!User::isLoggedIn() || !$this->_post['id'])
{
trigger_error('AjaxComment::handleReplyDelete - commentId empty or user not logged in', E_USER_ERROR);
return;
@@ -398,13 +407,14 @@ class AjaxComment extends AjaxHandler
protected function handleReplyFlag() : void
{
if (!User::$id || !$this->_post['id'])
if (!User::isLoggedIn() || !$this->_post['id'])
{
trigger_error('AjaxComment::handleReplyFlag - commentId empty or user not logged in', E_USER_ERROR);
return;
}
Util::createReport(1, 19, $this->_post['id'][0], '[General Reply Report]');
$report = new Report(Report::MODE_COMMENT, Report::CO_INAPPROPRIATE, $this->_post['id'][0]);
$report->create('Report Reply Button Click');
}
protected function handleReplyUpvote() : void

View File

@@ -1,20 +1,22 @@
<?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_UNSAFE_RAW, 'flags' => FILTER_FLAG_STRIP_AOWOW],
'appname' => ['filter' => FILTER_UNSAFE_RAW, 'flags' => FILTER_FLAG_STRIP_AOWOW],
'page' => ['filter' => FILTER_SANITIZE_URL],
'desc' => ['filter' => FILTER_UNSAFE_RAW, 'flags' => FILTER_FLAG_STRIP_AOWOW],
'id' => ['filter' => FILTER_SANITIZE_NUMBER_INT],
'relatedurl' => ['filter' => FILTER_SANITIZE_URL],
'email' => ['filter' => FILTER_SANITIZE_EMAIL]
'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)
@@ -35,58 +37,13 @@ class AjaxContactus extends AjaxHandler
*/
protected function handleContactUs() : string
{
$mode = $this->_post['mode'];
$rsn = $this->_post['reason'];
$ua = $this->_post['ua'];
$app = $this->_post['appname'];
$url = $this->_post['page'];
$desc = $this->_post['desc'];
$subj = $this->_post['id'];
$contexts = array(
[1, 2, 3, 4, 5, 6, 7, 8],
[15, 16, 17, 18, 19, 20],
[30, 31, 32, 33, 34, 35, 36, 37],
[45, 46, 47, 48],
[60, 61],
[45, 46, 47, 48],
[45, 46, 48]
);
if ($mode === null || $rsn === null || $ua === null || $app === null || $url === null)
{
trigger_error('AjaxContactus::handleContactUs - malformed contact request received', E_USER_ERROR);
return Lang::main('intError');
}
if (!isset($contexts[$mode]) || !in_array($rsn, $contexts[$mode]))
{
trigger_error('AjaxContactus::handleContactUs - report has invalid context (mode:'.$mode.' / reason:'.$rsn.')', E_USER_ERROR);
return Lang::main('intError');
}
if (!$desc)
return 3;
if (mb_strlen($desc) > 500)
return 2;
if (!User::$id && !User::$ip)
{
trigger_error('AjaxContactus::handleContactUs - could not determine IP for anonymous user', E_USER_ERROR);
return Lang::main('intError');
}
// check already reported
$field = User::$id ? 'userId' : 'ip';
if (DB::Aowow()->selectCell('SELECT 1 FROM ?_reports WHERE `mode` = ?d AND `reason`= ?d AND `subject` = ?d AND ?# = ?', $mode, $rsn, $subj, $field, User::$id ?: User::$ip))
return 7;
if (Util::createReport($mode, $rsn, $subj, $desc, $ua, $app, $url, $this->_post['relatedurl'], $this->_post['email']))
$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;
trigger_error('AjaxContactus::handleContactUs - write to db failed', E_USER_ERROR);
return Lang::main('intError');
else if (($e = $report->getError()) > 0)
return $e;
else
return Lang::main('intError');
}
}

View File

@@ -1,5 +1,7 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
@@ -8,11 +10,11 @@ class AjaxCookie extends AjaxHandler
public function __construct(array $params)
{
// note that parent::__construct has to come after this
if (!$params || !User::$id)
if (!$params || !User::isLoggedIn())
return;
$this->_get = array(
$params[0] => ['filter' => FILTER_UNSAFE_RAW, 'flags' => FILTER_FLAG_STRIP_AOWOW],
$params[0] => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\AjaxHandler::checkTextLine'],
);
// NOW we know, what to expect and sanitize
@@ -28,7 +30,7 @@ class AjaxCookie extends AjaxHandler
*/
protected function handleCookie() : string
{
if (User::$id && $this->params && $this->_get[$this->params[0]])
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';

View File

@@ -1,25 +1,27 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
class AjaxData extends AjaxHandler
{
protected $_get = array(
'locale' => ['filter' => FILTER_CALLBACK, 'options' => 'AjaxHandler::checkLocale'],
't' => ['filter' => FILTER_UNSAFE_RAW, 'flags' => FILTER_FLAG_STRIP_AOWOW ],
'catg' => ['filter' => FILTER_SANITIZE_NUMBER_INT ],
'skill' => ['filter' => FILTER_CALLBACK, 'options' => 'AjaxData::checkSkill' ],
'class' => ['filter' => FILTER_SANITIZE_NUMBER_INT ],
'callback' => ['filter' => FILTER_CALLBACK, 'options' => 'AjaxData::checkCallback' ]
'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 (is_numeric($this->_get['locale']))
User::useLocale($this->_get['locale']);
if ($this->_get['locale']?->validate())
Lang::load($this->_get['locale']);
// always this one
$this->handler = 'handleData';
@@ -53,20 +55,19 @@ class AjaxData extends AjaxHandler
$result .= $this->loadProfilerData($set);
break;
case 'companions':
$result .= $this->loadProfilerData($set, '778');
$result .= $this->loadProfilerData($set, SKILL_COMPANIONS);
break;
case 'mounts':
$result .= $this->loadProfilerData($set, '777');
$result .= $this->loadProfilerData($set, SKILL_MOUNTS);
break;
case 'quests':
// &partial: im not doing this right
// it expects a full quest dump on first lookup but will query subCats again if clicked..?
// for now omiting the detail clicks with empty results and just set catg update
$catg = isset($this->_get['catg']) ? $this->_get['catg'] : 'null';
if ($catg == 'null')
$result .= $this->loadProfilerData($set);
else if ($this->_get['callback'])
$result .= "\n\$WowheadProfiler.loadOnDemand('quests', ".$catg.");\n";
Util::loadStaticFile('p-'.$set, $result, false);
else
Util::loadStaticFile('p-'.$set.'-'.$catg, $result, true);
$result .= "\n\$WowheadProfiler.loadOnDemand('".$set."', ".$catg.");\n";
break;
case 'recipes':
@@ -80,14 +81,13 @@ class AjaxData extends AjaxHandler
$result .= "\n\$WowheadProfiler.loadOnDemand('recipes', null);\n";
break;
// locale independant
// locale independent
case 'quick-excludes':
case 'zones':
case 'weight-presets':
case 'item-scaling':
case 'realms':
case 'statistics':
if (!Util::loadStaticFile($set, $result) && CFG_DEBUG)
if (!Util::loadStaticFile($set, $result) && Cfg::get('DEBUG'))
$result .= "alert('could not fetch static data: ".$set."');";
$result .= "\n\n";
@@ -103,8 +103,9 @@ class AjaxData extends AjaxHandler
case 'enchants':
case 'itemsets':
case 'pets':
if (!Util::loadStaticFile($set, $result, true) && CFG_DEBUG)
$result .= "alert('could not fetch static data: ".$set." for locale: ".User::$localeString."');";
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;
@@ -119,7 +120,7 @@ class AjaxData extends AjaxHandler
protected static function checkSkill(string $val) : array
{
return array_intersect([171, 164, 333, 202, 182, 773, 755, 165, 186, 393, 197, 185, 129, 356], explode(',', $val));
return array_intersect(array_merge(SKILLS_TRADE_PRIMARY, [SKILL_FIRST_AID, SKILL_COOKING, SKILL_FISHING]), explode(',', $val));
}
protected static function checkCallback(string $val) : bool

View File

@@ -1,13 +1,15 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
class AjaxEdit extends AjaxHandler
{
protected $_get = array(
'qqfile' => ['filter' => FILTER_UNSAFE_RAW, 'flags' => FILTER_FLAG_STRIP_AOWOW],
'guide' => ['filter' => FILTER_SANITIZE_NUMBER_INT]
'qqfile' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\AjaxHandler::checkTextLine'],
'guide' => ['filter' => FILTER_SANITIZE_NUMBER_INT ]
);
public function __construct(array $params)
@@ -32,21 +34,21 @@ class AjaxEdit extends AjaxHandler
*/
protected function handleUpload() : string
{
if (!User::$id || $this->_get['guide'] != 1)
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::$displayName.'-'.Type::GUIDE.'-0-'.Util::createHash(16);
$tmpFile = User::$username.'-'.Type::GUIDE.'-0-'.Util::createHash(16);
$uploader = new qqFileUploader(['jpg', 'jpeg', 'png'], 10 * 1024 * 1024);
$uploader = new \qqFileUploader(['jpg', 'jpeg', 'png'], 10 * 1024 * 1024);
$result = $uploader->handleUpload($tmpPath, $tmpFile, true);
if (isset($result['success']))
{
$finfo = new finfo(FILEINFO_MIME);
$finfo = new \finfo(FILEINFO_MIME);
$mime = $finfo->file($tmpPath.$result['newFilename']);
if (preg_match('/^image\/(png|jpe?g)/i', $mime, $m))
{

View File

@@ -1,5 +1,7 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
@@ -17,6 +19,8 @@ class AjaxFilter extends AjaxHandler
if (!$params)
return;
parent::__construct($params);
$p = explode('=', $params[0]);
$this->page = $p[0];
@@ -30,55 +34,15 @@ class AjaxFilter extends AjaxHandler
$opts = ['parentCats' => $this->cat];
switch ($p[0])
// so usually the page call is just the DBTypes file string with a plural 's' .. but then there are currencies
$fileStr = match ($this->page)
{
case 'achievements':
$this->filter = (new AchievementListFilter(true, $opts));
break;
case 'areatriggers':
$this->filter = (new AreaTriggerListFilter(true, $opts));
break;
case 'enchantments':
$this->filter = (new EnchantmentListFilter(true, $opts));
break;
case 'icons':
$this->filter = (new IconListFilter(true, $opts));
break;
case 'items':
$this->filter = (new ItemListFilter(true, $opts));
break;
case 'itemsets':
$this->filter = (new ItemsetListFilter(true, $opts));
break;
case 'npcs':
$this->filter = (new CreatureListFilter(true, $opts));
break;
case 'objects':
$this->filter = (new GameObjectListFilter(true, $opts));
break;
case 'quests':
$this->filter = (new QuestListFilter(true, $opts));
break;
case 'sounds':
$this->filter = (new SoundListFilter(true, $opts));
break;
case 'spells':
$this->filter = (new SpellListFilter(true, $opts));
break;
case 'profiles':
$this->filter = (new ProfileListFilter(true, $opts));
break;
case 'guilds':
$this->filter = (new GuildListFilter(true, $opts));
break;
case 'arena-teams':
$this->filter = (new ArenaTeamListFilter(true, $opts));
break;
default:
return;
}
'currencies' => 'currency',
default => substr($this->page, 0, -1)
};
parent::__construct($params);
// 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';
@@ -88,20 +52,16 @@ class AjaxFilter extends AjaxHandler
{
$url = '?'.$this->page;
$this->filter->mergeCat($this->cat);
$this->filter?->mergeCat($this->cat);
if ($this->cat)
$url .= '='.implode('.', $this->cat);
$fi = [];
if ($x = $this->filter->getFilterString())
if ($x = $this->filter?->buildGETParam())
$url .= '&filter='.$x;
if ($this->filter->error)
$_SESSION['fiError'] = get_class($this->filter);
if ($fi)
$url .= '&filter='.implode(';', $fi);
if ($this->filter?->error)
$_SESSION['error']['fi'] = get_class($this->filter);
// do get request
return $url;

View File

@@ -1,12 +1,14 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
class AjaxGetdescription extends AjaxHandler
{
protected $_post = array(
'description' => [FILTER_CALLBACK, ['options' => 'AjaxHandler::checkFulltext']]
'description' => [FILTER_CALLBACK, ['options' => 'Aowow\AjaxHandler::checkTextBlob']]
);
public function __construct(array $params)
@@ -23,10 +25,10 @@ class AjaxGetdescription extends AjaxHandler
{
$this->contentType = MIME_TYPE_TEXT;
if (!User::$id)
if (!User::canWriteGuide())
return '';
$desc = (new Markup($this->_post['description']))->stripTags();
$desc = Markup::stripTags($this->_post['description']);
return Lang::trimTextClean($desc, 120);
}

View File

@@ -1,12 +1,14 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
class AjaxGotocomment extends AjaxHandler
{
protected $_get = array(
'id' => ['filter' => FILTER_CALLBACK, 'options' => 'AjaxHandler::checkInt']
'id' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\AjaxHandler::checkInt']
);
public function __construct(array $params)
@@ -26,10 +28,10 @@ class AjaxGotocomment extends AjaxHandler
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']))
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);
trigger_error('AjaxGotocomment::handleGoToComment - could not find comment #'.$this->_get['id'], E_USER_ERROR);
return '.';
}

View File

@@ -1,5 +1,7 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');

View File

@@ -1,14 +1,16 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('invalid access');
die('illegal access');
class AjaxGuild extends AjaxHandler
{
protected $validParams = ['resync', 'status'];
protected $_get = array(
'id' => ['filter' => FILTER_CALLBACK, 'options' => 'AjaxHandler::checkIdList' ],
'profile' => ['filter' => FILTER_CALLBACK, 'options' => 'AjaxHandler::checkEmptySet'],
'id' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\AjaxHandler::checkIdList' ],
'profile' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\AjaxHandler::checkEmptySet'],
);
public function __construct(array $params)
@@ -74,8 +76,7 @@ class AjaxGuild extends AjaxHandler
*/
protected function handleStatus() : string
{
$response = Profiler::resyncStatus(Type::GUILD, $this->_get['id']);
return Util::toJSON($response);
return Profiler::resyncStatus(Type::GUILD, $this->_get['id']);
}
}

View File

@@ -1,12 +1,14 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
class AjaxLocale extends AjaxHandler
{
protected $_get = array(
'locale' => ['filter' => FILTER_CALLBACK, 'options' => 'AjaxHandler::checkLocale']
'locale' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\Locale::tryFrom']
);
public function __construct(array $params)
@@ -23,8 +25,11 @@ class AjaxLocale extends AjaxHandler
*/
protected function handleLocale() : string
{
User::setLocale($this->_get['locale']);
User::save();
if ($this->_get['locale']?->validate())
{
User::$preferedLoc = $this->_get['locale'];
User::save(true);
}
return isset($_SERVER['HTTP_REFERER']) ? $_SERVER['HTTP_REFERER'] : '.';
}

View File

@@ -1,5 +1,7 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
@@ -9,16 +11,16 @@ class AjaxProfile extends AjaxHandler
protected $validParams = ['link', 'unlink', 'pin', 'unpin', 'public', 'private', 'avatar', 'resync', 'status', 'save', 'delete', 'purge', 'summary', 'load'];
protected $_get = array(
'id' => ['filter' => FILTER_CALLBACK, 'options' => 'AjaxHandler::checkIdList' ],
'items' => ['filter' => FILTER_CALLBACK, 'options' => 'AjaxProfile::checkItemList'],
'size' => ['filter' => FILTER_UNSAFE_RAW, 'flags' => FILTER_FLAG_STRIP_AOWOW ],
'guild' => ['filter' => FILTER_CALLBACK, 'options' => 'AjaxHandler::checkEmptySet'],
'arena-team' => ['filter' => FILTER_CALLBACK, 'options' => 'AjaxHandler::checkEmptySet'],
'user' => ['filter' => FILTER_CALLBACK, 'options' => 'AjaxProfile::checkUser' ]
'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' => 'AjaxHandler::checkFulltext'],
'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],
@@ -28,17 +30,17 @@ class AjaxProfile extends AjaxHandler
'talenttree2' => ['filter' => FILTER_SANITIZE_NUMBER_INT],
'talenttree3' => ['filter' => FILTER_SANITIZE_NUMBER_INT],
'activespec' => ['filter' => FILTER_SANITIZE_NUMBER_INT],
'talentbuild1' => ['filter' => FILTER_UNSAFE_RAW, 'flags' => FILTER_FLAG_STRIP_AOWOW],
'glyphs1' => ['filter' => FILTER_UNSAFE_RAW, 'flags' => FILTER_FLAG_STRIP_AOWOW],
'talentbuild2' => ['filter' => FILTER_UNSAFE_RAW, 'flags' => FILTER_FLAG_STRIP_AOWOW],
'glyphs2' => ['filter' => FILTER_UNSAFE_RAW, 'flags' => FILTER_FLAG_STRIP_AOWOW],
'icon' => ['filter' => FILTER_UNSAFE_RAW, 'flags' => FILTER_FLAG_STRIP_AOWOW],
'description' => ['filter' => FILTER_CALLBACK, 'options' => 'AjaxHandler::checkFulltext'],
'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' => 'AjaxHandler::checkIdListUnsigned', 'flags' => FILTER_REQUIRE_ARRAY],
'inv' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\AjaxHandler::checkIdListUnsigned', 'flags' => FILTER_REQUIRE_ARRAY],
);
public function __construct(array $params)
@@ -48,7 +50,7 @@ class AjaxProfile extends AjaxHandler
if (!$this->params)
return;
if (!CFG_PROFILER_ENABLE)
if (!Cfg::get('PROFILER_ENABLE'))
return;
switch ($this->params[0])
@@ -101,7 +103,7 @@ class AjaxProfile extends AjaxHandler
*/
protected function handleLink() : void // links char with account
{
if (!User::$id || empty($this->_get['id']))
if (!User::isLoggedIn() || empty($this->_get['id']))
{
trigger_error('AjaxProfile::handleLink - profileId empty or user not logged in', E_USER_ERROR);
return;
@@ -110,7 +112,7 @@ class AjaxProfile extends AjaxHandler
$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 user = ?', $this->_get['user'])))
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;
@@ -118,12 +120,12 @@ class AjaxProfile extends AjaxHandler
}
if ($this->undo)
DB::Aowow()->query('DELETE FROM ?_account_profiles WHERE accountId = ?d AND profileId IN (?a)', $uid, $this->_get['id']);
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))
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
{
@@ -141,7 +143,7 @@ class AjaxProfile extends AjaxHandler
*/
protected function handlePin() : void // (un)favorite
{
if (!User::$id || empty($this->_get['id'][0]))
if (!User::isLoggedIn() || empty($this->_get['id'][0]))
{
trigger_error('AjaxProfile::handlePin - profileId empty or user not logged in', E_USER_ERROR);
return;
@@ -150,7 +152,7 @@ class AjaxProfile extends AjaxHandler
$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 user = ?', $this->_get['user'])))
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;
@@ -158,10 +160,10 @@ class AjaxProfile extends AjaxHandler
}
// since only one character can be pinned at a time we can reset everything
DB::Aowow()->query('UPDATE ?_account_profiles SET extraFlags = extraFlags & ?d WHERE accountId = ?d', ~PROFILER_CU_PINNED, $uid);
DB::Aowow()->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);
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
@@ -171,7 +173,7 @@ class AjaxProfile extends AjaxHandler
*/
protected function handlePrivacy() : void // public visibility
{
if (!User::$id || empty($this->_get['id'][0]))
if (!User::isLoggedIn() || empty($this->_get['id'][0]))
{
trigger_error('AjaxProfile::handlePrivacy - profileId empty or user not logged in', E_USER_ERROR);
return;
@@ -180,7 +182,7 @@ class AjaxProfile extends AjaxHandler
$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 user = ?', $this->_get['user'])))
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;
@@ -189,13 +191,13 @@ class AjaxProfile extends AjaxHandler
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);
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);
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);
}
}
@@ -305,8 +307,7 @@ class AjaxProfile extends AjaxHandler
return Util::toJSON([1, [PR_QUEUE_STATUS_ERROR, 0, 0, PR_QUEUE_ERROR_CHAR]]);
}
$response = Profiler::resyncStatus(Type::PROFILE, $ids);
return Util::toJSON($response);
return Profiler::resyncStatus(Type::PROFILE, $ids);
}
/* params (get))
@@ -322,7 +323,7 @@ class AjaxProfile extends AjaxHandler
// todo (med): detail check this post-data
$cuProfile = array(
'user' => User::$id,
// 'userName' => User::$displayName,
// 'userName' => User::$username,
'name' => $this->_post['name'],
'level' => $this->_post['level'],
'class' => $this->_post['class'],
@@ -360,7 +361,7 @@ class AjaxProfile extends AjaxHandler
$cuProfile['sourceId'] = $_;
}
if ($cuProfile['sourceId'])
if (!empty($cuProfile['sourceId']))
$cuProfile['sourceName'] = DB::Aowow()->selectCell('SELECT name FROM ?_profiler_profiles WHERE id = ?d', $cuProfile['sourceId']);
$charId = -1;
@@ -420,7 +421,7 @@ class AjaxProfile extends AjaxHandler
$itemData[2] = 0;
// item sockets are fubar
$nSockets = $items->json[$itemData[1]]['nsockets'];
$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)))
@@ -450,7 +451,7 @@ class AjaxProfile extends AjaxHandler
*/
protected function handleDelete() : void // kill a profile
{
if (!User::$id || !$this->_get['id'])
if (!User::isLoggedIn() || !$this->_get['id'])
{
trigger_error('AjaxProfile::handleDelete - profileId empty or user not logged in', E_USER_ERROR);
return;
@@ -501,13 +502,16 @@ class AjaxProfile extends AjaxHandler
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' => Game::sideByRaceMask(1 << ($pBase['race'] - 1)) - 1,
'faction' => ChrRace::tryFrom($pBase['race'])?->getTeam() ?? TEAM_NEUTRAL,
'gender' => $pBase['gender'],
'skincolor' => $pBase['skincolor'],
'hairstyle' => $pBase['hairstyle'],
@@ -553,14 +557,14 @@ class AjaxProfile extends AjaxHandler
$profile['sourcename'] = $pBase['sourceName'];
$profile['description'] = $pBase['description'];
$profile['user'] = $pBase['user'];
$profile['username'] = DB::Aowow()->selectCell('SELECT displayName FROM ?_account WHERE id = ?d', $pBase['user']);
$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_BATTLEGROUP), CFG_BATTLEGROUP];
$profile['battlegroup'] = [Profiler::urlize(Cfg::get('BATTLEGROUP')), Cfg::get('BATTLEGROUP')];
$profile['realm'] = [Profiler::urlize($rData['name'], true), $rData['name']];
}
@@ -568,12 +572,12 @@ class AjaxProfile extends AjaxHandler
if ($_ = DB::Aowow()->selectCol('SELECT accountId FROM ?_account_profiles WHERE profileId = ?d', $pBase['id']))
$profile['bookmarks'] = $_;
// arena teams - [size(2|3|5) => DisplayName]; DisplayName gets urlized to use as link
// 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, talents FROM ?_profiler_pets WHERE owner = ?d', $pBase['id']))
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)]
@@ -598,60 +602,42 @@ class AjaxProfile extends AjaxHandler
*/
$completion = DB::Aowow()->select('SELECT type AS ARRAY_KEY, typeId AS ARRAY_KEY2, cur, max FROM ?_profiler_completion WHERE id = ?d', $pBase['id']);
foreach ($completion as $type => $data)
// questId => [cat1, cat2]
$profile['quests'] = [];
if ($quests = DB::Aowow()->selectCol('SELECT `questId` FROM ?_profiler_completion_quests WHERE `id` = ?d', $pBase['id']))
{
switch ($type)
{
case Type::FACTION: // factionId => amount
$profile['reputation'] = array_combine(array_keys($data), array_column($data, 'cur'));
break;
case Type::TITLE:
foreach ($data as &$d)
$d = 1;
$profile['titles'] = $data;
break;
case Type::QUEST:
foreach ($data as &$d)
$d = 1;
$profile['quests'] = $data;
break;
case Type::SPELL:
foreach ($data as &$d)
$d = 1;
$profile['spells'] = $data;
break;
case Type::ACHIEVEMENT:
$achievements = array_filter($data, function ($x) { return $x['max'] === null; });
$statistics = array_filter($data, function ($x) { return $x['max'] !== null; });
// achievements
$profile['achievements'] = array_combine(array_keys($achievements), array_column($achievements, 'cur'));
$profile['achievementpoints'] = DB::Aowow()->selectCell('SELECT SUM(points) FROM ?_achievement WHERE id IN (?a)', array_keys($achievements));
// raid progression
$activity = array_filter($statistics, function ($x) { return $x['cur'] > (time() - MONTH); });
foreach ($activity as &$r)
$r = 1;
// ony .. subtract 10-man from 25-man
$profile['statistics'] = array_combine(array_keys($statistics), array_column($statistics, 'max'));
$profile['activity'] = $activity;
break;
case Type::SKILL:
foreach ($data as &$d)
$d = [$d['cur'], $d['max']];
$profile['skills'] = $data;
break;
}
$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')];
}
$buff = '';
// 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'])
@@ -668,7 +654,12 @@ class AjaxProfile extends AjaxHandler
if (in_array($sl, $invTypes) && !in_array($slot, $usedSlots))
{
// get and apply inventory
$buff .= 'g_items.add('.$iId.', {name_'.User::$localeString.":'".Util::jsEscape($phItems->getField('name', true))."', quality:".$phItems->getField('quality').", icon:'".$phItems->getField('iconString')."', jsonequip:".Util::toJSON($data[$iId])."});\n";
$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;
@@ -681,30 +672,36 @@ class AjaxProfile extends AjaxHandler
if ($items = DB::Aowow()->select('SELECT * FROM ?_profiler_items WHERE id = ?d', $pBase['id']))
{
$itemz = new ItemList(array(['id', array_column($items, 'item')], CFG_SQL_LIMIT_NONE));
$itemz = new ItemList(array(['id', array_column($items, 'item')], Cfg::get('SQL_LIMIT_NONE')));
if (!$itemz->error)
{
$data = $itemz->getListviewData(ITEMINFO_JSON | ITEMINFO_SUBITEMS);
$data = $itemz->getListviewData(ITEMINFO_JSON | ITEMINFO_SUBITEMS);
foreach ($items as $i)
{
if ($itemz->getEntry($i['item']) && !in_array($i['slot'], $usedSlots))
{
// get and apply inventory
$buff .= 'g_items.add('.$i['item'].', {name_'.User::$localeString.":'".Util::jsEscape($itemz->getField('name', true))."', quality:".$itemz->getField('quality').", icon:'".$itemz->getField('iconString')."', jsonequip:".Util::toJSON($data[$i['item']])."});\n";
$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']];
}
}
}
}
if ($buff)
$buff .= "\n";
$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_SQL_LIMIT_NONE));
// $auraz = new SpellList(array(['id', $char->getField('auras')], Cfg::get('SQL_LIMIT_NONE')));
// $dataz = $auraz->getListviewData();
// $modz = $auraz->getProfilerMods();
@@ -723,7 +720,7 @@ class AjaxProfile extends AjaxHandler
// }
// }
// $buff .= 'g_spells.add('.$id.", {id:".$id.", name:'".Util::jsEscape(mb_substr($data['name'], 1))."', icon:'".$data['icon']."', modifier:".Util::toJSON($mods)."});\n";
// $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";
// }
@@ -762,6 +759,22 @@ class AjaxProfile extends AjaxHandler
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 '';
}
}
?>

451
includes/cfg.class.php Normal file
View File

@@ -0,0 +1,451 @@
<?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,746 @@
<?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

@@ -0,0 +1,756 @@
<?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

@@ -0,0 +1,748 @@
<?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

@@ -0,0 +1,382 @@
<?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

@@ -0,0 +1,185 @@
<?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,5 +1,7 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
@@ -21,88 +23,68 @@ class CommunityContent
private static array $jsGlobals = [];
private static array $subjCache = [];
private static string $coQuery = '
SELECT
c.*,
a1.displayName AS user,
a2.displayName AS editUser,
a3.displayName AS deleteUser,
a4.displayName AS responseUser,
IFNULL(SUM(ur.value), 0) AS rating,
SUM(IF(ur.userId > 0 AND ur.userId = ?d, ur.value, 0)) AS userRating,
SUM(IF( r.userId > 0 AND r.userId = ?d, 1, 0)) AS userReported
FROM
?_comments c
JOIN
?_account a1 ON c.userId = a1.id
LEFT JOIN
?_account a2 ON c.editUserId = a2.id
LEFT JOIN
?_account a3 ON c.deleteUserId = a3.id
LEFT JOIN
?_account a4 ON c.responseUserId = a4.id
LEFT JOIN
?_user_ratings ur ON c.id = ur.entry AND ur.type = ?d
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 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 $ssQuery = '
SELECT s.id AS ARRAY_KEY, s.id, a.displayName AS user, s.date, s.width, s.height, s.caption, IF(s.status & ?d, 1, 0) AS "sticky", s.type, s.typeId
FROM ?_screenshots s
LEFT JOIN ?_account a ON s.userIdOwner = a.id
WHERE {s.userIdOwner = ?d AND }{s.type = ? AND }{s.typeId = ? AND }s.status & ?d AND (s.status & ?d) = 0
{ORDER BY ?# DESC}
{LIMIT ?d}
';
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 $viQuery = '
SELECT v.id AS ARRAY_KEY, v.id, a.displayName AS user, v.date, v.videoId, v.caption, IF(v.status & ?d, 1, 0) AS "sticky", v.type, v.typeId
FROM ?_videos v
LEFT JOIN ?_account a ON v.userIdOwner = a.id
WHERE {v.userIdOwner = ?d AND }{v.type = ? AND }{v.typeId = ? AND }v.status & ?d AND (v.status & ?d) = 0
{ORDER BY ?# DESC}
{LIMIT ?d}
';
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 $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.displayName 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
{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 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
{
@@ -118,7 +100,7 @@ class CommunityContent
if (!$_)
continue;
$obj = Type::newList($type, [CFG_SQL_LIMIT_NONE, ['id', $_]]);
$obj = Type::newList($type, [Cfg::get('SQL_LIMIT_NONE'), ['id', $_]]);
if (!$obj)
continue;
@@ -127,24 +109,48 @@ class CommunityContent
}
}
public static function getCommentPreviews(array $params = [], ?int &$nFound = 0, bool $dateFmt = true) : array
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
*/
$comments = DB::Aowow()->selectPage(
$nFound,
self::$previewQuery,
// 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,
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
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)
@@ -160,10 +166,11 @@ class CommunityContent
$c['subject'] = self::$subjCache[$c['type']][$c['typeId']];
// format date
$c['date'] = $dateFmt ? date(Util::$dateFormatInternal, $c['date']) : intVal($c['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($params['replies']))
if (empty($opt['replies']))
unset($c['commentid']);
// format text for listview
@@ -176,7 +183,7 @@ class CommunityContent
}
}
return $comments;
return array_values($comments);
}
public static function getCommentReplies(int $commentId, int $limit = 0, ?int &$nFound = 0) : array
@@ -185,31 +192,35 @@ class CommunityContent
$query = $limit > 0 ? self::$coQuery.' LIMIT '.$limit : self::$coQuery;
// get replies
$results = DB::Aowow()->selectPage($nFound, $query, User::$id, User::$id, RATING_COMMENT, $commentId, 0, 0, CC_FLAG_DELETED, User::$id, User::isInGroup(U_GROUP_COMMENTS_MODERATOR));
foreach ($results as $r)
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)))
{
(new Markup($r['body']))->parseGlobalsFromText(self::$jsGlobals);
$nFound = DB::Aowow()->selectCell(self::$coCountQuery, $commentId, 0, 0, CC_FLAG_DELETED, User::$id, User::isInGroup(U_GROUP_COMMENTS_MODERATOR));
$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']
);
foreach ($results as $r)
{
Markup::parseTags($r['body'], self::$jsGlobals);
if ($r['userReported'])
$reply['reportedByUser'] = true;
$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['userRating'] > 0)
$reply['votedByUser'] = true;
else if ($r['userRating'] < 0)
$reply['downvotedByUser'] = true;
if ($r['userReported'])
$reply['reportedByUser'] = true;
$replies[] = $reply;
if ($r['userRating'] > 0)
$reply['votedByUser'] = true;
else if ($r['userRating'] < 0)
$reply['downvotedByUser'] = true;
$replies[] = $reply;
}
}
return $replies;
@@ -217,14 +228,14 @@ class CommunityContent
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"
$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
LEFT JOIN ?_account a ON s.`userIdOwner` = a.`id`
WHERE
{ s.type = ?d}
{ AND s.typeId = ?d}
{ s.userIdOwner = ?d}
{ s.`type` = ?d}
{ AND s.`typeId` = ?d}
{ s.`userIdOwner` = ?d}
LIMIT 100',
$userId ? DBSIMPLE_SKIP : $type,
$userId ? DBSIMPLE_SKIP : $typeId,
@@ -289,11 +300,11 @@ class CommunityContent
{
// 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`',
$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
);
@@ -310,7 +321,7 @@ class CommunityContent
if (!$ids)
continue;
$obj = Type::newList($t, [CFG_SQL_LIMIT_NONE, ['id', $ids]]);
$obj = Type::newList($t, [Cfg::get('SQL_LIMIT_NONE'), ['id', $ids]]);
if (!$obj || $obj->error)
continue;
@@ -341,14 +352,14 @@ class CommunityContent
public static function getComments(int $type, int $typeId) : array
{
$results = DB::Aowow()->query(self::$coQuery, User::$id, User::$id, RATING_COMMENT, 0, $type, $typeId, CC_FLAG_DELETED, User::$id, (int)User::isInGroup(U_GROUP_COMMENTS_MODERATOR));
$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)
{
(new Markup($r['body']))->parseGlobalsFromText(self::$jsGlobals);
Markup::parseTags($r['body'], self::$jsGlobals);
self::$jsGlobals[Type::USER][$r['userId']] = $r['userId'];
@@ -362,6 +373,7 @@ class CommunityContent
'rating' => $r['rating'],
'userRating' => $r['userRating'],
'user' => $r['user'],
'nreplies' => 0
);
$c['replies'] = self::getCommentReplies($r['id'], 5, $c['nreplies']);
@@ -372,7 +384,7 @@ class CommunityContent
$c['responseroles'] = $r['responseRoles'];
$c['responseuser'] = $r['responseUser'];
(new Markup($r['responseBody']))->parseGlobalsFromText(self::$jsGlobals);
Markup::parseTags($r['responseBody'], self::$jsGlobals);
}
if ($r['editCount']) // lastEdit
@@ -396,17 +408,31 @@ class CommunityContent
return $comments;
}
public static function getVideos(int $typeOrUser = 0, int $typeId = 0, int &$nFound = 0, bool $dateFmt = true) : array
public static function getVideos(int $typeOrUser = 0, int $typeId = 0, ?int &$nFound = 0, bool $dateFmt = true) : array
{
$videos = DB::Aowow()->selectPage($nFound, self::$viQuery,
$videos = DB::Aowow()->select(self::$viQuery,
CC_FLAG_STICKY,
$typeOrUser < 0 ? -$typeOrUser : DBSIMPLE_SKIP,
$typeOrUser > 0 ? $typeOrUser : DBSIMPLE_SKIP,
$typeOrUser > 0 ? $typeId : DBSIMPLE_SKIP,
$typeOrUser < 0 ? -$typeOrUser : DBSIMPLE_SKIP,
$typeOrUser > 0 ? $typeOrUser : DBSIMPLE_SKIP,
$typeOrUser > 0 ? $typeId : DBSIMPLE_SKIP,
CC_FLAG_APPROVED,
CC_FLAG_DELETED,
!$typeOrUser ? 'date' : DBSIMPLE_SKIP,
!$typeOrUser ? CFG_SQL_LIMIT_SEARCH : DBSIMPLE_SKIP
!$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
@@ -438,20 +464,34 @@ class CommunityContent
unset($v['user']);
}
return $videos;
return array_values($videos);
}
public static function getScreenshots(int $typeOrUser = 0, int $typeId = 0, int &$nFound = 0, bool $dateFmt = true) : array
public static function getScreenshots(int $typeOrUser = 0, int $typeId = 0, ?int &$nFound = 0, bool $dateFmt = true) : array
{
$screenshots = DB::Aowow()->selectPage($nFound, self::$ssQuery,
$screenshots = DB::Aowow()->select(self::$ssQuery,
CC_FLAG_STICKY,
$typeOrUser < 0 ? -$typeOrUser : DBSIMPLE_SKIP,
$typeOrUser > 0 ? $typeOrUser : DBSIMPLE_SKIP,
$typeOrUser > 0 ? $typeId : DBSIMPLE_SKIP,
$typeOrUser < 0 ? -$typeOrUser : DBSIMPLE_SKIP,
$typeOrUser > 0 ? $typeOrUser : DBSIMPLE_SKIP,
$typeOrUser > 0 ? $typeId : DBSIMPLE_SKIP,
CC_FLAG_APPROVED,
CC_FLAG_DELETED,
!$typeOrUser ? 'date' : DBSIMPLE_SKIP,
!$typeOrUser ? CFG_SQL_LIMIT_SEARCH : DBSIMPLE_SKIP
!$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
@@ -482,7 +522,7 @@ class CommunityContent
unset($s['user']);
}
return $screenshots;
return array_values($screenshots);
}
public static function getAll(int $type, int $typeId, array &$jsg) : array

View File

@@ -0,0 +1,726 @@
<?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

@@ -0,0 +1,69 @@
<?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

@@ -0,0 +1,50 @@
<?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

@@ -0,0 +1,159 @@
<?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

@@ -0,0 +1,49 @@
<?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

@@ -0,0 +1,174 @@
<?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

@@ -0,0 +1,291 @@
<?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

@@ -0,0 +1,70 @@
<?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

@@ -0,0 +1,142 @@
<?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

@@ -0,0 +1,60 @@
<?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

@@ -0,0 +1,61 @@
<?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;
}
}
?>

View File

@@ -1,13 +1,24 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
class Profiler
{
const PID_FILE = 'config/pr-queue-pid';
const CHAR_GMFLAGS = 0x1 | 0x8 | 0x10 | 0x20; // PLAYER_EXTRA_ :: GM_ON | TAXICHEAT | GM_INVISIBLE | GM_CHAT
public const PID_FILE = 'config/pr-queue-pid';
public const CHAR_GMFLAGS = 0x1 | 0x8 | 0x10 | 0x20; // PLAYER_EXTRA_ :: GM_ON | TAXICHEAT | GM_INVISIBLE | GM_CHAT
public const REGIONS = array( // see cfg_categories.dbc
'us' => [2, 3, 4, 5], // US (us, oceanic, latin america, americas - tournament)
'kr' => [6, 7], // KR (kr, tournament)
'eu' => [8, 9, 10, 11, 12, 13], // EU (english, german, french, spanish, russian, eu - tournament)
'tw' => [14, 15], // TW (tw, tournament)
'cn' => [16, 17, 18, 19, 20, 21, 22, 23, 24, 25], // CN (cn, CN1-8, tournament)
'dev' => [1, 26, 27, 28, 30] // Development, Test Server, Test Server - tournament, QA Server, Test Server 2
);
private static $realms = [];
@@ -178,35 +189,58 @@ class Profiler
return $str;
}
public static function getRealms()
public static function getRealms() : array
{
if (DB::isConnectable(DB_AUTH) && !self::$realms)
if (!DB::isConnectable(DB_AUTH) || self::$realms)
return self::$realms;
self::$realms = DB::Auth()->select(
'SELECT `id` AS ARRAY_KEY,
`name`,
CASE WHEN `timezone` BETWEEN 2 AND 5 THEN "us" # US, Oceanic, Latin America, Americas-Tournament
WHEN `timezone` BETWEEN 6 AND 7 THEN "kr" # KR, KR-Tournament
WHEN `timezone` BETWEEN 8 AND 13 THEN "eu" # GB, DE, FR, ES, RU, EU-Tournament
WHEN `timezone` BETWEEN 14 AND 15 THEN "tw" # TW, TW-Tournament
WHEN `timezone` BETWEEN 16 AND 25 THEN "cn" # CN, CN1-8, CN-Tournament
ELSE "dev" END AS "region", # 1: Dev, 26: Test, 27: Test Tournament, 28: QA, 30: Test2, 31+: misc
`allowedSecurityLevel` AS "access"
FROM `realmlist`
WHERE `gamebuild` = ?d',
WOW_BUILD
);
foreach (self::$realms as $rId => &$rData)
{
self::$realms = DB::Auth()->select('SELECT
id AS ARRAY_KEY,
`name`,
CASE
WHEN timezone IN (2, 3, 4) THEN "us"
WHEN timezone IN (8, 9, 10, 11, 12) THEN "eu"
WHEN timezone = 6 THEN "kr"
WHEN timezone = 14 THEN "tw"
WHEN timezone = 16 THEN "cn"
END AS region
FROM
realmlist
WHERE
allowedSecurityLevel = 0 AND
gamebuild = ?d',
WOW_BUILD
);
foreach (self::$realms as $rId => $rData)
// realm in db but no connection info set
if (!DB::isConnectable(DB_CHARACTERS . $rId))
{
if (DB::isConnectable(DB_CHARACTERS . $rId))
continue;
// realm in db but no connection info set
unset(self::$realms[$rId]);
continue;
}
// filter by access level
if ($rData['access'] == SEC_ADMINISTRATOR && (CLI || User::isInGroup(U_GROUP_DEV | U_GROUP_ADMIN)))
$rData['access'] = U_GROUP_DEV | U_GROUP_ADMIN;
else if ($rData['access'] == SEC_GAMEMASTER && (CLI || User::isInGroup(U_GROUP_DEV | U_GROUP_ADMIN | U_GROUP_MOD)))
$rData['access'] = U_GROUP_DEV | U_GROUP_ADMIN | U_GROUP_MOD;
else if ($rData['access'] == SEC_MODERATOR && (CLI || User::isInGroup(U_GROUP_DEV | U_GROUP_ADMIN | U_GROUP_MOD | U_GROUP_BUREAU)))
$rData['access'] = U_GROUP_DEV | U_GROUP_ADMIN | U_GROUP_MOD | U_GROUP_BUREAU;
else if ($rData['access'] > SEC_PLAYER && !CLI)
{
unset(self::$realms[$rId]);
continue;
}
// filter dev realms
if ($rData['region'] === 'dev')
{
if (CLI || User::isInGroup(U_GROUP_DEV | U_GROUP_ADMIN))
$rData['access'] = U_GROUP_DEV | U_GROUP_ADMIN;
else
{
unset(self::$realms[$rId]);
continue;
}
}
}
@@ -215,17 +249,17 @@ class Profiler
private static function queueInsert($realmId, $guid, $type, $localId)
{
if ($rData = DB::Aowow()->selectRow('SELECT requestTime AS time, status FROM ?_profiler_sync WHERE realm = ?d AND realmGUID = ?d AND `type` = ?d AND typeId = ?d AND status <> ?d', $realmId, $guid, $type, $localId, PR_QUEUE_STATUS_WORKING))
if ($rData = DB::Aowow()->selectRow('SELECT `requestTime` AS "time", `status` FROM ?_profiler_sync WHERE `realm` = ?d AND `realmGUID` = ?d AND `type` = ?d AND `typeId` = ?d AND `status` <> ?d', $realmId, $guid, $type, $localId, PR_QUEUE_STATUS_WORKING))
{
// not on already scheduled - recalc time and set status to PR_QUEUE_STATUS_WAITING
if ($rData['status'] != PR_QUEUE_STATUS_WAITING)
{
$newTime = CFG_DEBUG ? time() : max($rData['time'] + CFG_PROFILER_RESYNC_DELAY, time());
DB::Aowow()->query('UPDATE ?_profiler_sync SET requestTime = ?d, status = ?d, errorCode = 0 WHERE realm = ?d AND realmGUID = ?d AND `type` = ?d AND typeId = ?d', $newTime, PR_QUEUE_STATUS_WAITING, $realmId, $guid, $type, $localId);
$newTime = Cfg::get('DEBUG') ? time() : max($rData['time'] + Cfg::get('PROFILER_RESYNC_DELAY'), time());
DB::Aowow()->query('UPDATE ?_profiler_sync SET `requestTime` = ?d, `status` = ?d, `errorCode` = 0 WHERE `realm` = ?d AND `realmGUID` = ?d AND `type` = ?d AND `typeId` = ?d', $newTime, PR_QUEUE_STATUS_WAITING, $realmId, $guid, $type, $localId);
}
}
else
DB::Aowow()->query('REPLACE INTO ?_profiler_sync (realm, realmGUID, `type`, typeId, requestTime, status, errorCode) VALUES (?d, ?d, ?d, ?d, UNIX_TIMESTAMP(), ?d, 0)', $realmId, $guid, $type, $localId, PR_QUEUE_STATUS_WAITING);
DB::Aowow()->query('REPLACE INTO ?_profiler_sync (`realm`, `realmGUID`, `type`, `typeId`, `requestTime`, `status`, `errorCode`) VALUES (?d, ?d, ?d, ?d, UNIX_TIMESTAMP(), ?d, 0)', $realmId, $guid, $type, $localId, PR_QUEUE_STATUS_WAITING);
}
public static function scheduleResync($type, $realmId, $guid)
@@ -235,17 +269,17 @@ class Profiler
switch ($type)
{
case Type::PROFILE:
if ($newId = DB::Aowow()->selectCell('SELECT id FROM ?_profiler_profiles WHERE realm = ?d AND realmGUID = ?d', $realmId, $guid))
if ($newId = DB::Aowow()->selectCell('SELECT `id` FROM ?_profiler_profiles WHERE `realm` = ?d AND `realmGUID` = ?d', $realmId, $guid))
self::queueInsert($realmId, $guid, Type::PROFILE, $newId);
break;
case Type::GUILD:
if ($newId = DB::Aowow()->selectCell('SELECT id FROM ?_profiler_guild WHERE realm = ?d AND realmGUID = ?d', $realmId, $guid))
if ($newId = DB::Aowow()->selectCell('SELECT `id` FROM ?_profiler_guild WHERE `realm` = ?d AND `realmGUID` = ?d', $realmId, $guid))
self::queueInsert($realmId, $guid, Type::GUILD, $newId);
break;
case Type::ARENA_TEAM:
if ($newId = DB::Aowow()->selectCell('SELECT id FROM ?_profiler_arena_team WHERE realm = ?d AND realmGUID = ?d', $realmId, $guid))
if ($newId = DB::Aowow()->selectCell('SELECT `id` FROM ?_profiler_arena_team WHERE `realm` = ?d AND `realmGUID` = ?d', $realmId, $guid))
self::queueInsert($realmId, $guid, Type::ARENA_TEAM, $newId);
break;
@@ -262,18 +296,43 @@ class Profiler
return $newId;
}
public static function resyncStatus($type, array $subjectGUIDs)
/* return
<status object> [
nQueueProcesses,
[statusCode, timeToRefresh, curQueuePos, errorCode, nResyncs],
[<anotherStatus>]
...
]
statusCode:
0: end the request
1: waiting
2: working...
3: ready; click to view
4: error / retry
timeToRefresh:
msec till the client may ask for another update
curQueuePos:
position in the queue
errorCode:
0: unk error
1: char does not exist
2: armory gone
nResyncs:
??? .. if !nResyncs && !timeToRefresh prints "Adding to queue..." but will not ping the server for updates...?
*/
public static function resyncStatus(int $type, array $subjectGUIDs) : string
{
$response = [CFG_PROFILER_ENABLE ? 2 : 0]; // in theory you could have multiple queues; used as divisor for: (15 / x) + 2
$response = [Cfg::get('PROFILER_ENABLE') ? 2 : 0]; // in theory you could have multiple queues; used as divisor in wait time estimation: (15 / x) + 2
if (!$subjectGUIDs)
$response[] = [PR_QUEUE_STATUS_ENDED, 0, 0, PR_QUEUE_ERROR_CHAR];
else
{
// error out all profiles with status WORKING, that are older than 60sec
DB::Aowow()->query('UPDATE ?_profiler_sync SET status = ?d, errorCode = ?d WHERE status = ?d AND requestTime < ?d', PR_QUEUE_STATUS_ERROR, PR_QUEUE_ERROR_UNK, PR_QUEUE_STATUS_WORKING, time() - MINUTE);
DB::Aowow()->query('UPDATE ?_profiler_sync SET `status` = ?d, `errorCode` = ?d WHERE `status` = ?d AND `requestTime` < ?d', PR_QUEUE_STATUS_ERROR, PR_QUEUE_ERROR_UNK, PR_QUEUE_STATUS_WORKING, time() - MINUTE);
$subjectStatus = DB::Aowow()->select('SELECT typeId AS ARRAY_KEY, status, realm, errorCode FROM ?_profiler_sync WHERE `type` = ?d AND typeId IN (?a)', $type, $subjectGUIDs);
$queue = DB::Aowow()->selectCol('SELECT CONCAT(type, ":", typeId) FROM ?_profiler_sync WHERE status = ?d AND requestTime < UNIX_TIMESTAMP() ORDER BY requestTime ASC', PR_QUEUE_STATUS_WAITING);
$subjectStatus = DB::Aowow()->select('SELECT `typeId` AS ARRAY_KEY, `status`, `realm`, `errorCode` FROM ?_profiler_sync WHERE `type` = ?d AND `typeId` IN (?a)', $type, $subjectGUIDs);
$queue = DB::Aowow()->selectCol('SELECT CONCAT(`type`, ":", `typeId`) FROM ?_profiler_sync WHERE `status` = ?d AND `requestTime` < UNIX_TIMESTAMP() ORDER BY `requestTime` ASC', PR_QUEUE_STATUS_WAITING);
foreach ($subjectGUIDs as $guid)
{
if (empty($subjectStatus[$guid])) // whelp, thats some error..
@@ -283,25 +342,31 @@ class Profiler
else
$response[] = array(
$subjectStatus[$guid]['status'],
$subjectStatus[$guid]['status'] != PR_QUEUE_STATUS_READY ? CFG_PROFILER_RESYNC_PING : 0,
$subjectStatus[$guid]['status'] != PR_QUEUE_STATUS_READY ? Cfg::get('PROFILER_RESYNC_PING') : 0,
array_search($type.':'.$guid, $queue) + 1,
0,
1 // nResycTries - unsure about this one
1 // nResyncs - unsure about this one
);
}
}
return $response;
return Util::toJSON($response);
}
public static function getCharFromRealm($realmId, $charGuid)
{
$char = DB::Characters($realmId)->selectRow('SELECT c.* FROM characters c WHERE c.guid = ?d', $charGuid);
$char = DB::Characters($realmId)->selectRow('SELECT c.* FROM characters c WHERE c.`guid` = ?d', $charGuid);
if (!$char)
return false;
if (!$char['name'])
{
trigger_error('char #'.$charGuid.' on realm #'.$realmId.' has empty name. skipping...', E_USER_WARNING);
return false;
}
// reminder: this query should not fail: a placeholder entry is created as soon as a char listview is created or profile detail page is called
$profile = DB::Aowow()->selectRow('SELECT id, lastupdated FROM ?_profiler_profiles WHERE realm = ?d AND realmGUID = ?d', $realmId, $char['guid']);
$profile = DB::Aowow()->selectRow('SELECT `id`, `lastupdated` FROM ?_profiler_profiles WHERE `realm` = ?d AND `realmGUID` = ?d', $realmId, $char['guid']);
if (!$profile)
return false; // well ... it failed
@@ -311,15 +376,16 @@ class Profiler
if (!$char['online'] && $char['logout_time'] <= $profile['lastupdated'])
{
DB::Aowow()->query('UPDATE ?_profiler_profiles SET lastupdated = ?d WHERE id = ?d', time(), $profileId);
DB::Aowow()->query('UPDATE ?_profiler_profiles SET `lastupdated` = ?d WHERE `id` = ?d', time(), $profileId);
CLI::write('char did not log in since last update. skipping...');
return true;
}
CLI::write('writing...');
$ra = (1 << ($char['race'] - 1));
$cl = (1 << ($char['class'] - 1));
$ra = ChrRace::from($char['race']);
$cl = ChrClass::from($char['class']);
/*************/
/* equipment */
@@ -337,8 +403,8 @@ class Profiler
*/
DB::Aowow()->query('DELETE FROM ?_profiler_items WHERE id = ?d', $profileId);
$items = DB::Characters($realmId)->select('SELECT ci.slot AS ARRAY_KEY, ii.itemEntry, ii.enchantments, ii.randomPropertyId FROM character_inventory ci JOIN item_instance ii ON ci.item = ii.guid WHERE ci.guid = ?d AND bag = 0 AND slot BETWEEN 0 AND 18', $char['guid']);
DB::Aowow()->query('DELETE FROM ?_profiler_items WHERE `id` = ?d', $profileId);
$items = DB::Characters($realmId)->select('SELECT ci.`slot` AS ARRAY_KEY, ii.`itemEntry`, ii.`enchantments`, ii.`randomPropertyId` FROM character_inventory ci JOIN item_instance ii ON ci.`item` = ii.`guid` WHERE ci.`guid` = ?d AND `bag` = 0 AND `slot` BETWEEN 0 AND 18', $char['guid']);
$gemItems = [];
$permEnch = [];
@@ -355,7 +421,7 @@ class Profiler
if ($gEnch)
{
$gi = DB::Aowow()->selectCol('SELECT gemEnchantmentId AS ARRAY_KEY, id FROM ?_items WHERE class = 3 AND gemEnchantmentId IN (?a)', $gEnch);
$gi = DB::Aowow()->selectCol('SELECT `gemEnchantmentId` AS ARRAY_KEY, `id` FROM ?_items WHERE `class` = ?d AND `gemEnchantmentId` IN (?a)', ITEM_CLASS_GEM, $gEnch);
foreach ($gEnch as $eId)
{
if (isset($gemItems[$eId]))
@@ -411,7 +477,7 @@ class Profiler
'hairstyle' => $char['hairStyle'],
'haircolor' => $char['hairColor'],
'features' => $char['facialStyle'], // maybe facetype
'title' => $char['chosenTitle'] ? DB::Aowow()->selectCell('SELECT id FROM ?_titles WHERE bitIdx = ?d', $char['chosenTitle']) : 0,
'title' => $char['chosenTitle'] ? DB::Aowow()->selectCell('SELECT `id` FROM ?_titles WHERE `bitIdx` = ?d', $char['chosenTitle']) : 0,
'playedtime' => $char['totaltime'],
'nomodelMask' => ($char['playerFlags'] & 0x400 ? (1 << SLOT_HEAD) : 0) | ($char['playerFlags'] & 0x800 ? (1 << SLOT_BACK) : 0),
'talenttree1' => 0,
@@ -431,22 +497,23 @@ class Profiler
// char is flagged for rename
if ($char['at_login'] & 0x1)
{
$ri = DB::Aowow()->selectCell('SELECT MAX(renameItr) FROM ?_profiler_profiles WHERE realm = ?d AND realmGUID = ?d AND name = ?', $realmId, $charGuid, $char['name']);
$ri = DB::Aowow()->selectCell('SELECT MAX(`renameItr`) FROM ?_profiler_profiles WHERE `realm` = ?d AND `realmGUID` IS NOT NULL AND `name` = ?', $realmId, $char['name']);
$data['renameItr'] = $ri ? ++$ri : 1;
}
/********************/
/* talents + glyphs */
/********************/
$t = DB::Characters($realmId)->selectCol('SELECT talentGroup AS ARRAY_KEY, spell AS ARRAY_KEY2, spell FROM character_talent WHERE guid = ?d', $char['guid']);
$g = DB::Characters($realmId)->select('SELECT talentGroup AS ARRAY_KEY, glyph1 AS g1, glyph2 AS g4, glyph3 AS g5, glyph4 AS g2, glyph5 AS g3, glyph6 AS g6 FROM character_glyphs WHERE guid = ?d', $char['guid']);
$t = DB::Characters($realmId)->selectCol('SELECT `talentGroup` AS ARRAY_KEY, `spell` AS ARRAY_KEY2, `spell` FROM character_talent WHERE `guid` = ?d', $char['guid']);
$g = DB::Characters($realmId)->select('SELECT `talentGroup` AS ARRAY_KEY, `glyph1` AS "g1", `glyph2` AS "g4", `glyph3` AS "g5", `glyph4` AS "g2", `glyph5` AS "g3", `glyph6` AS "g6" FROM character_glyphs WHERE `guid` = ?d', $char['guid']);
for ($i = 0; $i < 2; $i++)
{
// talents
for ($j = 0; $j < 3; $j++)
{
$_ = DB::Aowow()->selectCol('SELECT spell AS ARRAY_KEY, MAX(IF(spell IN (?a), `rank`, 0)) FROM ?_talents WHERE class = ?d AND tab = ?d GROUP BY id ORDER BY `row`, `col` ASC', !empty($t[$i]) ? $t[$i] : [0], $char['class'], $j);
$_ = DB::Aowow()->selectCol('SELECT `spell` AS ARRAY_KEY, MAX(IF(`spell` IN (?a), `rank`, 0)) FROM ?_talents WHERE `class` = ?d AND `tab` = ?d GROUP BY `id` ORDER BY `row`, `col` ASC', $t[$i] ?? [0], $cl->value, $j);
$data['talentbuild'.($i + 1)] .= implode('', $_);
if ($data['activespec'] == $i)
$data['talenttree'.($j + 1)] = array_sum($_);
@@ -461,8 +528,19 @@ class Profiler
$gProps[$j] = $g[$i]['g'.$j];
if ($gProps)
if ($gItems = DB::Aowow()->selectCol('SELECT i.id FROM ?_glyphproperties gp JOIN ?_spell s ON s.effect1MiscValue = gp.id AND s.effect1Id = 74 JOIN ?_items i ON i.class = 16 AND i.spellId1 = s.id WHERE gp.id IN (?a)', $gProps))
{
$gItems = DB::Aowow()->selectCol(
'SELECT i.`id`
FROM ?_glyphproperties gp
JOIN ?_spell s ON s.`effect1MiscValue` = gp.`id` AND s.`effect1Id` = ?d
JOIN ?_items i ON i.`class` = ?d AND i.`spellId1` = s.`id` AND (i.`cuFlags` & ?d) = 0
WHERE gp.`id` IN (?a)',
SPELL_EFFECT_APPLY_GLYPH, ITEM_CLASS_GLYPH, CUSTOM_DISABLED | CUSTOM_UNAVAILABLE | CUSTOM_EXCLUDE_FOR_LISTVIEW, $gProps
);
if ($gItems)
$data['glyphs'.($i + 1)] = implode(':', $gItems);
}
}
}
@@ -494,9 +572,15 @@ class Profiler
// enchantId => multiple spells => multiple items with varying itemlevels, quality, whatevs
// cant reasonably get to the castItem from enchantId and slot
$profSpec = DB::Aowow()->selectCol('SELECT id AS ARRAY_KEY, skillLevel AS "1", skillLine AS "0" FROM ?_itemenchantment WHERE id IN (?a)', $permEnch);
foreach ($permEnch as $eId)
$profSpec = DB::Aowow()->selectCol('SELECT `id` AS ARRAY_KEY, `skillLevel` AS "1", `skillLine` AS "0" FROM ?_itemenchantment WHERE `id` IN (?a)', $permEnch);
foreach ($permEnch as $slot => $eId)
{
if (!isset($profSpec[$eId]))
{
trigger_error('char #'.$charGuid.' on realm #'.$realmId.' has item in slot #'.$slot.' with invalid perm enchantment #'.CLI::bold($eId), E_USER_WARNING);
continue;
}
if ($x = Util::getEnchantmentScore(0, 0, !!$profSpec[$eId][1], $eId))
$data['gearscore'] += $x;
else if ($profSpec[$eId][0] != 776) // not runeforging
@@ -513,22 +597,31 @@ class Profiler
/* hunter pets */
/***************/
if ($cl == CLASS_HUNTER)
if ($cl == ChrClass::HUNTER)
{
DB::Aowow()->query('DELETE FROM ?_profiler_pets WHERE owner = ?d', $profileId);
$pets = DB::Characters($realmId)->select('SELECT id AS ARRAY_KEY, id, entry, modelId, name FROM character_pet WHERE owner = ?d', $charGuid);
DB::Aowow()->query('DELETE FROM ?_profiler_pets WHERE `owner` = ?d', $profileId);
$pets = DB::Characters($realmId)->select('SELECT `id` AS ARRAY_KEY, `entry`, `modelId`, `name` FROM character_pet WHERE `owner` = ?d', $charGuid);
foreach ($pets as $petGuid => $petData)
{
$morePet = DB::Aowow()->selectRow('SELECT p.`type`, c.family FROM ?_pet p JOIN ?_creature c ON c.family = p.id WHERE c.id = ?d', $petData['entry']);
$petSpells = DB::Characters($realmId)->selectCol('SELECT spell FROM pet_spell WHERE guid = ?d', $petGuid);
$petSpells = DB::Characters($realmId)->selectCol('SELECT `spell` FROM pet_spell WHERE `guid` = ?d', $petGuid);
$morePet = DB::Aowow()->selectRow(
'SELECT IFNULL(c3.`id`, IFNULL(c2.`id`, IFNULL(c1.`id`, c.`id`))) AS "entry", p.`type`, c.`family`
FROM ?_pet p
JOIN ?_creature c ON c.`family` = p.`id`
LEFT JOIN ?_creature c1 ON c1.`difficultyEntry1` = c.`id`
LEFT JOIN ?_creature c2 ON c2.`difficultyEntry2` = c.`id`
LEFT JOIN ?_creature c3 ON c3.`difficultyEntry3` = c.`id`
WHERE c.`id` = ?d',
$petData['entry']
);
$_ = DB::Aowow()->selectCol('SELECT spell AS ARRAY_KEY, MAX(IF(spell IN (?a), `rank`, 0)) FROM ?_talents WHERE class = 0 AND petTypeMask = ?d GROUP BY id ORDER BY row, col ASC', $petSpells ?: [0], 1 << $morePet['type']);
$_ = DB::Aowow()->selectCol('SELECT `spell` AS ARRAY_KEY, MAX(IF(`spell` IN (?a), `rank`, 0)) FROM ?_talents WHERE `class` = 0 AND `petTypeMask` = ?d GROUP BY `row`, `col` ORDER BY `row`, `col` ASC', $petSpells ?: [0], 1 << $morePet['type']);
$pet = array(
'id' => $petGuid,
'owner' => $profileId,
'name' => $petData['name'],
'family' => $morePet['family'],
'npc' => $petData['entry'],
'npc' => $morePet['entry'],
'displayId' => $petData['modelId'],
'talents' => implode('', $_)
);
@@ -544,114 +637,91 @@ class Profiler
/* completion data */
/*******************/
DB::Aowow()->query('DELETE FROM ?_profiler_completion WHERE id = ?d', $profileId);
// done quests //
// done quests
if ($quests = DB::Characters($realmId)->select('SELECT ?d AS id, ?d AS `type`, quest AS typeId FROM character_queststatus_rewarded WHERE guid = ?d', $profileId, Type::QUEST, $char['guid']))
DB::Aowow()->query('DELETE FROM ?_profiler_completion_quests WHERE `id` = ?d', $profileId);
if ($quests = DB::Characters($realmId)->select('SELECT ?d AS `id`, `quest` AS `questId` FROM character_queststatus_rewarded WHERE `guid` = ?d', $profileId, $char['guid']))
foreach (Util::createSqlBatchInsert($quests) as $q)
DB::Aowow()->query('INSERT INTO ?_profiler_completion (?#) VALUES '.$q, array_keys($quests[0]));
DB::Aowow()->query('INSERT INTO ?_profiler_completion_quests (?#) VALUES '.$q, array_keys($quests[0]));
CLI::write(' ..quests');
// known skills (professions only)
$skAllowed = DB::Aowow()->selectCol('SELECT id FROM ?_skillline WHERE typeCat IN (9, 11) AND (cuFlags & ?d) = 0', CUSTOM_EXCLUDE_FOR_LISTVIEW);
$skills = DB::Characters($realmId)->select('SELECT ?d AS id, ?d AS `type`, skill AS typeId, `value` AS cur, max FROM character_skills WHERE guid = ?d AND skill IN (?a)', $profileId, Type::SKILL, $char['guid'], $skAllowed);
// known skills (professions only) //
// manually apply racial profession bonuses
foreach ($skills as &$sk)
DB::Aowow()->query('DELETE FROM ?_profiler_completion_skills WHERE `id` = ?d', $profileId);
$skAllowed = DB::Aowow()->selectCol('SELECT `id` FROM ?_skillline WHERE `typeCat` IN (9, 11) AND (`cuFlags` & ?d) = 0', CUSTOM_EXCLUDE_FOR_LISTVIEW);
$skills = DB::Characters($realmId)->select('SELECT ?d AS `id`, `skill` AS `skillId`, `value`, `max` FROM character_skills WHERE `guid` = ?d AND `skill` IN (?a)', $profileId, $char['guid'], $skAllowed);
$racials = DB::Aowow()->select('SELECT `effect1MiscValue` AS ARRAY_KEY, `effect1DieSides` + `effect1BasePoints` AS qty, `reqRaceMask`, `reqClassMask` FROM ?_spell WHERE `typeCat` = -4 AND `effect1Id` = ?d AND `effect1AuraId` = ?d', SPELL_EFFECT_APPLY_AURA, SPELL_AURA_MOD_SKILL_TALENT);
foreach ($skills as &$sk) // apply racial profession bonuses
{
// Blood Elves - Arcane Affinity
if ($sk['typeId'] == 333 && $char['race'] == 10)
if (!isset($racials[$sk['skillId']]))
continue;
$r = $racials[$sk['skillId']];
if ($ra->matches($r['reqRaceMask']) && $cl->matches($r['reqClassMask']))
{
$sk['cur'] += 10;
$sk['max'] += 10;
}
// Draenei - Gemcutting
if ($sk['typeId'] == 755 && $char['race'] == 11)
{
$sk['cur'] += 5;
$sk['max'] += 5;
}
// Tauren - Cultivation
// Gnomes - Engineering Specialization
if (($sk['typeId'] == 182 && $char['race'] == 6) ||
($sk['typeId'] == 202 && $char['race'] == 7))
{
$sk['cur'] += 15;
$sk['max'] += 15;
$sk['value'] += $r['qty'];
$sk['max'] += $r['qty'];
}
}
unset($sk);
if ($skills)
{
// apply auto-learned trade skills
DB::Aowow()->query('
INSERT INTO ?_profiler_completion
SELECT ?d, ?d, spellId, NULL, NULL
FROM dbc_skilllineability
WHERE skillLineId IN (?a) AND
acquireMethod = 1 AND
(reqRaceMask = 0 OR reqRaceMask & ?d) AND
(reqClassMask = 0 OR reqClassMask & ?d)',
$profileId, Type::SPELL,
array_column($skills, 'typeId'),
1 << ($char['race'] - 1),
1 << ($char['class'] - 1)
);
foreach (Util::createSqlBatchInsert($skills) as $sk)
DB::Aowow()->query('INSERT INTO ?_profiler_completion (?#) VALUES '.$sk, array_keys($skills[0]));
}
DB::Aowow()->query('INSERT INTO ?_profiler_completion_skills (?#) VALUES '.$sk, array_keys($skills[0]));
CLI::write(' ..professions');
// reputation
// reputation //
DB::Aowow()->query('DELETE FROM ?_profiler_completion_reputation WHERE `id` = ?d', $profileId);
// get base values for this race/class
$reputation = [];
$baseRep = DB::Aowow()->selectCol('
SELECT id AS ARRAY_KEY, baseRepValue1 FROM aowow_factions WHERE baseRepValue1 && (baseRepRaceMask1 & ?d || (!baseRepRaceMask1 AND baseRepClassMask1)) &&
((baseRepClassMask1 & ?d) || !baseRepClassMask1) UNION
SELECT id AS ARRAY_KEY, baseRepValue2 FROM aowow_factions WHERE baseRepValue2 && (baseRepRaceMask2 & ?d || (!baseRepRaceMask2 AND baseRepClassMask2)) &&
((baseRepClassMask2 & ?d) || !baseRepClassMask2) UNION
SELECT id AS ARRAY_KEY, baseRepValue3 FROM aowow_factions WHERE baseRepValue3 && (baseRepRaceMask3 & ?d || (!baseRepRaceMask3 AND baseRepClassMask3)) &&
((baseRepClassMask3 & ?d) || !baseRepClassMask3) UNION
SELECT id AS ARRAY_KEY, baseRepValue4 FROM aowow_factions WHERE baseRepValue4 && (baseRepRaceMask4 & ?d || (!baseRepRaceMask4 AND baseRepClassMask4)) &&
((baseRepClassMask4 & ?d) || !baseRepClassMask4)
', $ra, $cl, $ra, $cl, $ra, $cl, $ra, $cl);
$baseRep = DB::Aowow()->selectCol(
'SELECT `id` AS ARRAY_KEY, `baseRepValue1` FROM aowow_factions WHERE `baseRepValue1` AND (`baseRepRaceMask1` & ?d OR (`baseRepClassMask1` AND NOT `baseRepRaceMask1`)) AND ((`baseRepClassMask1` & ?d) OR NOT `baseRepClassMask1`) UNION
SELECT `id` AS ARRAY_KEY, `baseRepValue2` FROM aowow_factions WHERE `baseRepValue2` AND (`baseRepRaceMask2` & ?d OR (`baseRepClassMask2` AND NOT `baseRepRaceMask2`)) AND ((`baseRepClassMask2` & ?d) OR NOT `baseRepClassMask2`) UNION
SELECT `id` AS ARRAY_KEY, `baseRepValue3` FROM aowow_factions WHERE `baseRepValue3` AND (`baseRepRaceMask3` & ?d OR (`baseRepClassMask3` AND NOT `baseRepRaceMask3`)) AND ((`baseRepClassMask3` & ?d) OR NOT `baseRepClassMask3`) UNION
SELECT `id` AS ARRAY_KEY, `baseRepValue4` FROM aowow_factions WHERE `baseRepValue4` AND (`baseRepRaceMask4` & ?d OR (`baseRepClassMask4` AND NOT `baseRepRaceMask4`)) AND ((`baseRepClassMask4` & ?d) OR NOT `baseRepClassMask4`)',
$ra->toMask(), $cl->toMask(), $ra->toMask(), $cl->toMask(), $ra->toMask(), $cl->toMask(), $ra->toMask(), $cl->toMask()
);
if ($reputation = DB::Characters($realmId)->select('SELECT ?d AS id, ?d AS `type`, faction AS typeId, standing AS cur FROM character_reputation WHERE guid = ?d AND (flags & 0x4) = 0', $profileId, Type::FACTION, $char['guid']))
if ($reputation = DB::Characters($realmId)->select('SELECT ?d AS `id`, `faction` AS `factionId`, `standing` FROM character_reputation WHERE `guid` = ?d AND (`flags` & 0x4) = 0', $profileId, $char['guid']))
{
// merge back base values for encountered factions
foreach ($reputation as &$set)
{
if (empty($baseRep[$set['typeId']]))
if (empty($baseRep[$set['factionId']]))
continue;
$set['cur'] += $baseRep[$set['typeId']];
unset($baseRep[$set['typeId']]);
$set['standing'] += $baseRep[$set['factionId']];
unset($baseRep[$set['factionId']]);
}
}
// insert base values for not yet encountered factions
foreach ($baseRep as $id => $val)
$reputation[] = array(
'id' => $profileId,
'type' => Type::FACTION,
'typeId' => $id,
'cur' => $val
'id' => $profileId,
'factionId' => $id,
'standing' => $val
);
foreach (Util::createSqlBatchInsert($reputation) as $rep)
DB::Aowow()->query('INSERT INTO ?_profiler_completion (?#) VALUES '.$rep, array_keys($reputation[0]));
DB::Aowow()->query('INSERT INTO ?_profiler_completion_reputation (?#) VALUES '.$rep, array_keys($reputation[0]));
CLI::write(' ..reputation');
// known titles
// known titles //
DB::Aowow()->query('DELETE FROM ?_profiler_completion_titles WHERE `id` = ?d', $profileId);
$tBlocks = explode(' ', $char['knownTitles']);
$indizes = [];
for ($i = 0; $i < 6; $i++)
@@ -660,38 +730,63 @@ class Profiler
$indizes[] = $j + ($i * 32);
if ($indizes)
DB::Aowow()->query('INSERT INTO ?_profiler_completion SELECT ?d, ?d, id, NULL, NULL FROM ?_titles WHERE bitIdx IN (?a)', $profileId, Type::TITLE, $indizes);
DB::Aowow()->query('INSERT INTO ?_profiler_completion_titles SELECT ?d, `id` FROM ?_titles WHERE `bitIdx` IN (?a)', $profileId, $indizes);
CLI::write(' ..titles');
// achievements
if ($achievements = DB::Characters($realmId)->select('SELECT ?d AS id, ?d AS `type`, achievement AS typeId, date AS cur FROM character_achievement WHERE guid = ?d', $profileId, Type::ACHIEVEMENT, $char['guid']))
// achievements //
DB::Aowow()->query('DELETE FROM ?_profiler_completion_achievements WHERE `id` = ?d', $profileId);
if ($achievements = DB::Characters($realmId)->select('SELECT ?d AS id, `achievement` AS `achievementId`, `date` FROM character_achievement WHERE `guid` = ?d', $profileId, $char['guid']))
{
foreach (Util::createSqlBatchInsert($achievements) as $a)
DB::Aowow()->query('INSERT INTO ?_profiler_completion (?#) VALUES '.$a, array_keys($achievements[0]));
DB::Aowow()->query('INSERT INTO ?_profiler_completion_achievements (?#) VALUES '.$a, array_keys($achievements[0]));
$data['achievementpoints'] = DB::Aowow()->selectCell('SELECT SUM(points) FROM ?_achievement WHERE id IN (?a)', array_column($achievements, 'typeId'));
$data['achievementpoints'] = DB::Aowow()->selectCell('SELECT SUM(`points`) FROM ?_achievement WHERE `id` IN (?a) AND (`flags` & ?d) = 0', array_column($achievements, 'achievementId'), ACHIEVEMENT_FLAG_COUNTER);
}
CLI::write(' ..achievements');
// raid progression
if ($progress = DB::Characters($realmId)->select('SELECT ?d AS id, ?d AS `type`, criteria AS typeId, date AS cur, counter AS `max` FROM character_achievement_progress WHERE guid = ?d AND criteria IN (?a)', $profileId, Type::ACHIEVEMENT, $char['guid'], self::$raidProgression))
// raid progression //
DB::Aowow()->query('DELETE FROM ?_profiler_completion_statistics WHERE `id` = ?d', $profileId);
if ($progress = DB::Characters($realmId)->select('SELECT ?d AS `id`, `criteria` AS `achievementId`, `date`, `counter` FROM character_achievement_progress WHERE `guid` = ?d AND `criteria` IN (?a)', $profileId, $char['guid'], self::$raidProgression))
{
array_walk($progress, function (&$val) { $val['typeId'] = array_search($val['typeId'], self::$raidProgression); });
array_walk($progress, function (&$val) { $val['achievementId'] = array_search($val['achievementId'], self::$raidProgression); });
foreach (Util::createSqlBatchInsert($progress) as $p)
DB::Aowow()->query('INSERT INTO ?_profiler_completion (?#) VALUES '.$p, array_keys($progress[0]));
DB::Aowow()->query('INSERT INTO ?_profiler_completion_statistics (?#) VALUES '.$p, array_keys($progress[0]));
}
CLI::write(' ..raid progression');
// known spells
if ($spells = DB::Characters($realmId)->select('SELECT ?d AS id, ?d AS `type`, spell AS typeId FROM character_spell WHERE guid = ?d AND disabled = 0', $profileId, Type::SPELL, $char['guid']))
// known spells //
DB::Aowow()->query('DELETE FROM ?_profiler_completion_spells WHERE `id` = ?d', $profileId);
if ($spells = DB::Characters($realmId)->select('SELECT ?d AS `id`, `spell` AS `spellId` FROM character_spell WHERE `guid` = ?d AND `disabled` = 0', $profileId, $char['guid']))
foreach (Util::createSqlBatchInsert($spells) as $s)
DB::Aowow()->query('INSERT INTO ?_profiler_completion (?#) VALUES '.$s, array_keys($spells[0]));
DB::Aowow()->query('INSERT INTO ?_profiler_completion_spells (?#) VALUES '.$s, array_keys($spells[0]));
// apply auto-learned spells from trade skills
if ($skills)
DB::Aowow()->query(
'INSERT INTO ?_profiler_completion_spells
SELECT ?d, `spellId`
FROM ?_skilllineability
WHERE `skillLineId` IN (?a) AND
`acquireMethod` = 1 AND
(`reqRaceMask` = 0 OR `reqRaceMask` & ?d) AND
(`reqClassMask` = 0 OR `reqClassMask` & ?d)',
$profileId,
array_column($skills, 'skillId'),
$ra->toMask(),
$cl->toMask()
);
CLI::write(' ..known spells (vanity pets & mounts)');
@@ -701,7 +796,7 @@ class Profiler
/****************/
// guilds
if ($guild = DB::Characters($realmId)->selectRow('SELECT g.name AS name, g.guildid AS id, gm.rank FROM guild_member gm JOIN guild g ON g.guildid = gm.guildid WHERE gm.guid = ?d', $char['guid']))
if ($guild = DB::Characters($realmId)->selectRow('SELECT g.`name` AS `name`, g.`guildid` AS `id`, gm.`rank` FROM guild_member gm JOIN guild g ON g.`guildid` = gm.`guildid` WHERE gm.`guid` = ?d', $char['guid']))
{
$guildId = 0;
if (!($guildId = DB::Aowow()->selectCell('SELECT id FROM ?_profiler_guild WHERE realm = ?d AND realmGUID = ?d', $realmId, $guild['id'])))
@@ -721,13 +816,15 @@ class Profiler
$data['guildRank'] = $guild['rank'];
}
CLI::write(' ..basic guild data');
// arena teams
$teams = DB::Characters($realmId)->select('SELECT at.arenaTeamId AS ARRAY_KEY, at.name, at.type, IF(at.captainGuid = atm.guid, 1, 0) AS captain, atm.* FROM arena_team at JOIN arena_team_member atm ON atm.arenaTeamId = at.arenaTeamId WHERE atm.guid = ?d', $char['guid']);
$teams = DB::Characters($realmId)->select('SELECT at.`arenaTeamId` AS ARRAY_KEY, at.`name`, at.`type`, IF(at.`captainGuid` = atm.`guid`, 1, 0) AS `captain`, atm.* FROM arena_team at JOIN arena_team_member atm ON atm.`arenaTeamId` = at.`arenaTeamId` WHERE atm.`guid` = ?d', $char['guid']);
foreach ($teams as $rGuid => $t)
{
$teamId = 0;
if (!($teamId = DB::Aowow()->selectCell('SELECT id FROM ?_profiler_arena_team WHERE realm = ?d AND realmGUID = ?d', $realmId, $rGuid)))
if (!($teamId = DB::Aowow()->selectCell('SELECT `id` FROM ?_profiler_arena_team WHERE `realm` = ?d AND `realmGUID` = ?d', $realmId, $rGuid)))
{
$team = array( // only most basic data
'realm' => $realmId,
@@ -752,6 +849,15 @@ class Profiler
'personalRating' => $t['personalRating']
);
// 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` = ?d AND atm.`arenaTeamId` <> ?d',
$t['type'], $profileId, $teamId
);
DB::Aowow()->query('INSERT INTO ?_profiler_arena_team_member (?#) VALUES (?a) ON DUPLICATE KEY UPDATE ?a', array_keys($member), array_values($member), array_slice($member, 2));
}
@@ -761,20 +867,26 @@ class Profiler
/* mark char as done */
/*********************/
if (DB::Aowow()->query('UPDATE ?_profiler_profiles SET ?a WHERE realm = ?d AND realmGUID = ?d', $data, $realmId, $charGuid) !== null)
DB::Aowow()->query('UPDATE ?_profiler_profiles SET cuFlags = cuFlags & ?d WHERE id = ?d', ~PROFILER_CU_NEEDS_RESYNC, $profileId);
if (DB::Aowow()->query('UPDATE ?_profiler_profiles SET ?a WHERE `realm` = ?d AND `realmGUID` = ?d', $data, $realmId, $charGuid) !== null)
DB::Aowow()->query('UPDATE ?_profiler_profiles SET `cuFlags` = `cuFlags` & ?d WHERE `id` = ?d', ~PROFILER_CU_NEEDS_RESYNC, $profileId);
return true;
}
public static function getGuildFromRealm($realmId, $guildGuid)
{
$guild = DB::Characters($realmId)->selectRow('SELECT guildId, name, createDate, info, backgroundColor, emblemStyle, emblemColor, borderStyle, borderColor FROM guild WHERE guildId = ?d', $guildGuid);
$guild = DB::Characters($realmId)->selectRow('SELECT `guildId`, `name`, `createDate`, `info`, `backgroundColor`, `emblemStyle`, `emblemColor`, `borderStyle`, `borderColor` FROM guild WHERE `guildId` = ?d', $guildGuid);
if (!$guild)
return false;
if (!$guild['name'])
{
trigger_error('guild #'.$guildGuid.' on realm #'.$realmId.' has empty name. skipping...', E_USER_WARNING);
return false;
}
// reminder: this query should not fail: a placeholder entry is created as soon as a team listview is created or team detail page is called
$guildId = DB::Aowow()->selectCell('SELECT id FROM ?_profiler_guild WHERE realm = ?d AND realmGUID = ?d', $realmId, $guild['guildId']);
$guildId = DB::Aowow()->selectCell('SELECT `id` FROM ?_profiler_guild WHERE `realm` = ?d AND `realmGUID` = ?d', $realmId, $guild['guildId']);
CLI::write('fetching guild #'.$guildGuid.' from realm #'.$realmId);
CLI::write('writing...');
@@ -787,11 +899,11 @@ class Profiler
unset($guild['guildId']);
$guild['nameUrl'] = self::urlize($guild['name']);
DB::Aowow()->query('UPDATE ?_profiler_guild SET ?a WHERE realm = ?d AND realmGUID = ?d', $guild, $realmId, $guildGuid);
DB::Aowow()->query('UPDATE ?_profiler_guild SET ?a WHERE `realm` = ?d AND `realmGUID` = ?d', $guild, $realmId, $guildGuid);
// ranks
DB::Aowow()->query('DELETE FROM ?_profiler_guild_rank WHERE guildId = ?d', $guildId);
if ($ranks = DB::Characters($realmId)->select('SELECT ?d AS guildId, rid AS `rank`, rname AS name FROM guild_rank WHERE guildid = ?d', $guildId, $guildGuid))
DB::Aowow()->query('DELETE FROM ?_profiler_guild_rank WHERE `guildId` = ?d', $guildId);
if ($ranks = DB::Characters($realmId)->select('SELECT ?d AS `guildId`, `rid` AS "rank", `rname` AS "name" FROM guild_rank WHERE `guildid` = ?d', $guildId, $guildGuid))
foreach (Util::createSqlBatchInsert($ranks) as $r)
DB::Aowow()->query('INSERT INTO ?_profiler_guild_rank (?#) VALUES '.$r, array_keys(reset($ranks)));
@@ -823,19 +935,25 @@ class Profiler
/* mark guild as done */
/*********************/
DB::Aowow()->query('UPDATE ?_profiler_guild SET cuFlags = cuFlags & ?d WHERE id = ?d', ~PROFILER_CU_NEEDS_RESYNC, $guildId);
DB::Aowow()->query('UPDATE ?_profiler_guild SET `cuFlags` = `cuFlags` & ?d WHERE `id` = ?d', ~PROFILER_CU_NEEDS_RESYNC, $guildId);
return true;
}
public static function getArenaTeamFromRealm($realmId, $teamGuid)
{
$team = DB::Characters($realmId)->selectRow('SELECT arenaTeamId, name, type, captainGuid, rating, seasonGames, seasonWins, weekGames, weekWins, `rank`, backgroundColor, emblemStyle, emblemColor, borderStyle, borderColor FROM arena_team WHERE arenaTeamId = ?d', $teamGuid);
$team = DB::Characters($realmId)->selectRow('SELECT `arenaTeamId`, `name`, `type`, `captainGuid`, `rating`, `seasonGames`, `seasonWins`, `weekGames`, `weekWins`, `rank`, `backgroundColor`, `emblemStyle`, `emblemColor`, `borderStyle`, `borderColor` FROM arena_team WHERE `arenaTeamId` = ?d', $teamGuid);
if (!$team)
return false;
if (!$team['name'])
{
trigger_error('arena team #'.$teamGuid.' on realm #'.$realmId.' has empty name. skipping...', E_USER_WARNING);
return false;
}
// reminder: this query should not fail: a placeholder entry is created as soon as a team listview is created or team detail page is called
$teamId = DB::Aowow()->selectCell('SELECT id FROM ?_profiler_arena_team WHERE realm = ?d AND realmGUID = ?d', $realmId, $team['arenaTeamId']);
$teamId = DB::Aowow()->selectCell('SELECT `id` FROM ?_profiler_arena_team WHERE `realm` = ?d AND `realmGUID` = ?d', $realmId, $team['arenaTeamId']);
CLI::write('fetching arena team #'.$teamGuid.' from realm #'.$realmId);
CLI::write('writing...');
@@ -850,7 +968,7 @@ class Profiler
unset($team['arenaTeamId']);
$team['nameUrl'] = self::urlize($team['name']);
DB::Aowow()->query('UPDATE ?_profiler_arena_team SET ?a WHERE realm = ?d AND realmGUID = ?d', $team, $realmId, $teamGuid);
DB::Aowow()->query('UPDATE ?_profiler_arena_team SET ?a WHERE `realm` = ?d AND `realmGUID` = ?d', $team, $realmId, $teamGuid);
CLI::write(' ..team data');
@@ -859,18 +977,14 @@ class Profiler
/* Member Data */
/***************/
$members = DB::Characters($realmId)->select('
SELECT
atm.guid AS ARRAY_KEY, atm.arenaTeamId, atm.weekGames, atm.weekWins, atm.seasonGames, atm.seasonWins, atm.personalrating
FROM
arena_team_member atm
JOIN
characters c ON c.guid = atm.guid AND
c.deleteInfos_Account IS NULL AND
c.level <= ?d AND
(c.extra_flags & ?d) = 0
WHERE
arenaTeamId = ?d',
$members = DB::Characters($realmId)->select(
'SELECT atm.`guid` AS ARRAY_KEY, atm.`arenaTeamId`, atm.`weekGames`, atm.`weekWins`, atm.`seasonGames`, atm.`seasonWins`, atm.`personalrating`
FROM arena_team_member atm
JOIN characters c ON c.`guid` = atm.`guid` AND
c.`deleteInfos_Account` IS NULL AND
c.`level` <= ?d AND
(c.`extra_flags` & ?d) = 0
WHERE `arenaTeamId` = ?d',
MAX_LEVEL,
self::CHAR_GMFLAGS,
$teamGuid
@@ -884,35 +998,44 @@ class Profiler
);
$mProfiles = new RemoteProfileList($conditions, ['sv' => $realmId]);
if (!$mProfiles->error)
{
$mProfiles->initializeLocalEntries();
foreach ($mProfiles->iterate() as $__)
{
$mGuid = $mProfiles->getField('guid');
$members[$mGuid]['arenaTeamId'] = $teamId;
$members[$mGuid]['captain'] = (int)($mGuid == $captain);
$members[$mGuid]['profileId'] = $mProfiles->getField('id');
}
DB::Aowow()->query('DELETE FROM ?_profiler_arena_team_member WHERE arenaTeamId = ?d', $teamId);
foreach (Util::createSqlBatchInsert($members) as $m)
DB::Aowow()->query('INSERT INTO ?_profiler_arena_team_member (?#) VALUES '.$m, array_keys(reset($members)));
}
else
if ($mProfiles->error)
return false;
$mProfiles->initializeLocalEntries();
foreach ($mProfiles->iterate() as $__)
{
$mGuid = $mProfiles->getField('guid');
$members[$mGuid]['arenaTeamId'] = $teamId;
$members[$mGuid]['captain'] = (int)($mGuid == $captain);
$members[$mGuid]['profileId'] = $mProfiles->getField('id');
}
// Delete members from other teams of the same type...
DB::Aowow()->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)',
$team['type'],
array_column($members, 'profileId')
);
// ...and purge this teams member
DB::Aowow()->query('DELETE FROM ?_profiler_arena_team_member WHERE `arenaTeamId` = ?d', $teamId);
foreach (Util::createSqlBatchInsert($members) as $m)
DB::Aowow()->query('INSERT INTO ?_profiler_arena_team_member (?#) VALUES '.$m, array_keys(reset($members)));
CLI::write(' ..team members');
/*********************/
/* mark team as done */
/*********************/
DB::Aowow()->query('UPDATE ?_profiler_arena_team SET cuFlags = cuFlags & ?d WHERE id = ?d', ~PROFILER_CU_NEEDS_RESYNC, $teamId);
DB::Aowow()->query('UPDATE ?_profiler_arena_team SET `cuFlags` = `cuFlags` & ?d WHERE `id` = ?d', ~PROFILER_CU_NEEDS_RESYNC, $teamId);
return true;
}

View File

@@ -0,0 +1,274 @@
<?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,5 +1,7 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
@@ -11,42 +13,46 @@ if (!defined('AOWOW_REVISION'))
class DB
{
private static $interfaceCache = [];
private static $optionsCache = [];
private static $connectionCache = [];
private static array $interfaceCache = [];
private static array $optionsCache = [];
private static array $logs = [];
private static $logs = [];
private static function createConnectSyntax(&$options)
private static function createConnectSyntax(array &$options) : string
{
return 'mysqli://'.$options['user'].':'.$options['pass'].'@'.$options['host'].'/'.$options['db'];
}
public static function connect($idx)
public static function connect(int $idx) : void
{
if (self::isConnected($idx))
return;
{
self::$interfaceCache[$idx]->link->close();
self::$interfaceCache[$idx] = null;
}
$options = &self::$optionsCache[$idx];
$interface = DbSimple_Generic::connect(self::createConnectSyntax($options));
$interface = \DbSimple_Generic::connect(self::createConnectSyntax($options));
if (!$interface || $interface->error)
die('Failed to connect to database on index #'.$idx.".\n");
$interface->setErrorHandler(['DB', 'errorHandler']);
$interface->query('SET NAMES ?', 'utf8mb4');
$interface->setErrorHandler(self::errorHandler(...));
if ($options['prefix'])
$interface->setIdentPrefix($options['prefix']);
// disable STRICT_TRANS_TABLES and STRICT_ALL_TABLES off. It prevents usage of implicit default values.
if ($idx == DB_AOWOW)
$interface->query("SET SESSION sql_mode = 'NO_ENGINE_SUBSTITUTION'");
// disable ONLY_FULL_GROUP_BY (Allows for non-aggregated selects in a group-by query)
else
$interface->query("SET SESSION sql_mode = ''");
self::$interfaceCache[$idx] = &$interface;
self::$connectionCache[$idx] = true;
// 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
@@ -56,33 +62,41 @@ class DB
if (strstr($options['host'], ':'))
[$options['host'], $port] = explode(':', $options['host']);
try {
$link = @mysqli_connect($options['host'], $options['user'], $options['pass'], $options['db'], $port ?: $defPort);
mysqli_close($link);
}
catch (Exception $e)
if ($link = mysqli_connect($options['host'], $options['user'], $options['pass'], $options['db'], $port ?: $defPort))
{
$err = '['.mysqli_connect_errno().'] '.mysqli_connect_error();
return false;
}
mysqli_close($link);
return true;
}
$err = '['.mysqli_connect_errno().'] '.mysqli_connect_error();
return false;
}
public static function errorHandler($message, $data)
public static function errorHandler(string $message, array $data) : void
{
if (!error_reporting())
return;
$error = "DB ERROR:<br /><br />\n\n<pre>".print_r($data, true)."</pre>";
// continue on warning, end on error
$isError = $data['code'] > 0;
echo CLI ? strip_tags($error) : $error;
exit;
// make number sensible again
$data['code'] = abs($data['code']);
if (Cfg::get('DEBUG') >= LOG_LEVEL_INFO)
{
echo "\nDB ERROR\n";
foreach ($data as $k => $v)
echo ' '.str_pad($k.':', 10).$v."\n";
}
trigger_error($message, $isError ? E_USER_ERROR : E_USER_WARNING);
}
public static function logger($self, $query, $trace)
public static function profiler(mixed $self, string $query, mixed $trace) : void
{
if ($trace) // actual query
self::$logs[] = [substr(str_replace("\n", ' ', $query), 0, 200)];
self::$logs[] = [str_replace("\n", ' ', $query)];
else // the statistics
{
end(self::$logs);
@@ -90,7 +104,7 @@ class DB
}
}
public static function getLogs()
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])
@@ -108,71 +122,64 @@ class DB
return Util::jsEscape($out).'</table></pre>';
}
public static function getDB($idx)
public static function getDB(int $idx) : ?\DbSimple_Mysqli
{
return self::$interfaceCache[$idx];
}
public static function isConnected($idx)
public static function isConnected(int $idx) : bool
{
return isset(self::$connectionCache[$idx]);
return isset(self::$interfaceCache[$idx]) && self::$interfaceCache[$idx]->link;
}
public static function isConnectable($idx)
public static function isConnectable(int $idx) : bool
{
return isset(self::$optionsCache[$idx]);
}
private static function safeGetDB($idx)
/**
* @static
* @return DbSimple_Mysqli
*/
public static function Characters(int $realmId) : ?\DbSimple_Mysqli
{
if (!self::isConnected($idx))
self::connect($idx);
if (!isset(self::$optionsCache[DB_CHARACTERS.$realmId]))
die('Connection info not found for live database of realm #'.$realmId.'. Aborted.');
return self::getDB($idx);
return self::getDB(DB_CHARACTERS.$realmId);
}
/**
* @static
* @return DbSimple_Mysql
* @return DbSimple_Mysqli
*/
public static function Characters($realm)
public static function Auth() : ?\DbSimple_Mysqli
{
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);
return self::getDB(DB_AUTH);
}
/**
* @static
* @return DbSimple_Mysql
* @return DbSimple_Mysqli
*/
public static function Auth()
public static function World() : ?\DbSimple_Mysqli
{
return self::safeGetDB(DB_AUTH);
return self::getDB(DB_WORLD);
}
/**
* @static
* @return DbSimple_Mysql
* @return DbSimple_Mysqli
*/
public static function World()
public static function Aowow() : ?\DbSimple_Mysqli
{
return self::safeGetDB(DB_WORLD);
return self::getDB(DB_AOWOW);
}
/**
* @static
* @return DbSimple_Mysql
*/
public static function Aowow()
{
return self::safeGetDB(DB_AOWOW);
}
public static function load($idx, $config)
public static function load(int $idx, array $config) : void
{
self::$optionsCache[$idx] = $config;
self::connect($idx);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,490 +0,0 @@
<?php
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 $classFileStrings = array(
null, 'warrior', 'paladin', 'hunter', 'rogue', 'priest', 'deathknight', 'shaman', 'mage', 'warlock', null, 'druid'
);
private static $combatRatingToItemMod = array( // zero-indexed idx:CR; val:Mod
null, 12, 13, 14, 15, 16, 17, 18, 19,
20, 21, 22, 23, 24, 25, 26, 27, 28,
29, 30, null, null, null, 37, 44
);
public static $lvlIndepRating = array( // rating doesn't scale with level
ITEM_MOD_MANA, ITEM_MOD_HEALTH, ITEM_MOD_ATTACK_POWER, ITEM_MOD_MANA_REGENERATION, ITEM_MOD_SPELL_POWER,
ITEM_MOD_HEALTH_REGEN, ITEM_MOD_SPELL_PENETRATION, ITEM_MOD_BLOCK_VALUE
);
public static $questClasses = array(
-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, 1417, 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]
);
/* 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'
);
// 'replicates' $WH.g_statToJson
public static $itemMods = array( // zero-indexed; "mastrtng": unused mastery; _[a-z] => taken mods..
'dmg', 'mana', 'health', 'agi', 'str', 'int', 'spi',
'sta', 'energy', 'rage', 'focus', 'runicpwr', 'defrtng', 'dodgertng',
'parryrtng', 'blockrtng', 'mlehitrtng', 'rgdhitrtng', 'splhitrtng', 'mlecritstrkrtng', 'rgdcritstrkrtng',
'splcritstrkrtng', '_mlehitrtng', '_rgdhitrtng', '_splhitrtng', '_mlecritstrkrtng', '_rgdcritstrkrtng', '_splcritstrkrtng',
'mlehastertng', 'rgdhastertng', 'splhastertng', 'hitrtng', 'critstrkrtng', '_hitrtng', '_critstrkrtng',
'resirtng', 'hastertng', 'exprtng', 'atkpwr', 'rgdatkpwr', 'feratkpwr', 'splheal',
'spldmg', 'manargn', 'armorpenrtng', 'splpwr', 'healthrgn', 'splpen', 'block', // ITEM_MOD_BLOCK_VALUE
'mastrtng', 'armor', 'firres', 'frores', 'holres', 'shares', 'natres',
'arcres', 'firsplpwr', 'frosplpwr', 'holsplpwr', 'shasplpwr', 'natsplpwr', 'arcsplpwr'
);
public static $class2SpellFamily = array(
// null Warrior Paladin Hunter Rogue Priest DK Shaman Mage Warlock null Druid
null, 4, 10, 9, 8, 6, 15, 11, 3, 5, null, 7
);
public static $areaFloors = array(
206 => 3, 209 => 7, 719 => 3, 721 => 4, 796 => 4, 1196 => 2, 1337 => 2, 1581 => 2, 1583 => 7, 1584 => 2,
2017 => 2, 2057 => 4, 2100 => 2, 2557 => 6, 2677 => 4, 3428 => 3, 3457 => 17, 3790 => 2, 3791 => 2, 3959 => 8,
3456 => 6, 3715 => 2, 3848 => 3, 3849 => 2, 4075 => 2, 4100 => 2, 4131 => 2, 4196 => 2, 4228 => 4, 4272 => 2,
4273 => 6, 4277 => 3, 4395 => 2, 4494 => 2, 4722 => 2, 4812 => 8
);
public static function itemModByRatingMask($mask)
{
if (($mask & 0x1C000) == 0x1C000) // special case resilience
return ITEM_MOD_RESILIENCE_RATING;
if (($mask & 0x00E0) == 0x00E0) // hit rating - all subcats (mle, rgd, spl)
return ITEM_MOD_HIT_RATING;
if (($mask & 0x0700) == 0x0700) // crit rating - all subcats (mle, rgd, spl)
return ITEM_MOD_CRIT_RATING;
for ($j = 0; $j < count(self::$combatRatingToItemMod); $j++)
{
if (!self::$combatRatingToItemMod[$j])
continue;
if (!($mask & (1 << $j)))
continue;
return self::$combatRatingToItemMod[$j];
}
return 0;
}
public static function sideByRaceMask($race)
{
// Any
if (!$race || ($race & RACE_MASK_ALL) == RACE_MASK_ALL)
return SIDE_BOTH;
// Horde
if ($race & RACE_MASK_HORDE && !($race & RACE_MASK_ALLIANCE))
return SIDE_HORDE;
// Alliance
if ($race & RACE_MASK_ALLIANCE && !($race & RACE_MASK_HORDE))
return SIDE_ALLIANCE;
return SIDE_BOTH;
}
public static function getReputationLevelForPoints($pts)
{
if ($pts >= 41999)
return REP_EXALTED;
else if ($pts >= 20999)
return REP_REVERED;
else if ($pts >= 8999)
return REP_HONORED;
else if ($pts >= 2999)
return REP_FRIENDLY;
else if ($pts >= 0)
return REP_NEUTRAL;
else if ($pts >= -3000)
return REP_UNFRIENDLY;
else if ($pts >= -6000)
return REP_HOSTILE;
else
return REP_HATED;
}
public static function getTaughtSpells(&$spell)
{
$extraIds = [-1]; // init with -1 to prevent empty-array errors
$lookup = [-1];
switch (gettype($spell))
{
case 'object':
if (get_class($spell) != 'SpellList')
return [];
$lookup[] = $spell->id;
foreach ($spell->canTeachSpell() as $idx)
$extraIds[] = $spell->getField('effect'.$idx.'TriggerSpell');
break;
case 'integer':
$lookup[] = $spell;
break;
case 'array':
$lookup = $spell;
break;
default:
return [];
}
// note: omits required spell and chance in skill_discovery_template
$data = array_merge(
DB::World()->selectCol('SELECT spellId FROM spell_learn_spell WHERE entry IN (?a)', $lookup),
DB::World()->selectCol('SELECT spellId FROM skill_discovery_template WHERE reqSpell IN (?a)', $lookup),
$extraIds
);
// return list of integers, not strings
$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', User::$localeId, User::$localeString, $ptId))
{
$ptId = $row['NextPageID'];
$pages[] = Util::parseHtmlText(Util::localizedString($row, 'Text'));
}
else
{
trigger_error('Referenced PageTextId #'.$ptId.' is not in DB', E_USER_WARNING);
break;
}
}
return $pages;
}
/*********************/
/* World Pos. Checks */
/*********************/
private static $alphaMapCache = [];
private static function alphaMapCheck(int $areaId, array &$set) : bool
{
$file = 'setup/generated/alphaMaps/'.$areaId.'.png';
if (!file_exists($file)) // file does not exist (probably instanced area)
return false;
// invalid and corner cases (literally)
if (!is_array($set) || 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 checkCoords(array $points) : array
{
$result = [];
$capitals = array( // capitals take precedence over their surroundings
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
);
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];
}
else if (in_array($res['areaId'], $capitals)) // capitals (auto-discovered) and no hand-made alphaMap available
return $res;
else if (empty($result)) // add with lowest quality if alpha map is missing
$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 getWorldPosForGUID(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_y` AS `posX`, `position_x` 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_y` AS `posX`, `position_x` AS `posY` FROM gameobject WHERE `guid` IN (?a)', $guids);
break;
case Type::SOUND:
$result = DB::AoWoW()->select('SELECT `soundId` AS ARRAY_KEY, `soundId` AS `id`, `mapId`, `posX`, `posY` FROM dbc_soundemitters WHERE `soundId` IN (?a)', $guids);
break;
case Type::AREATRIGGER:
$result = DB::AoWoW()->select('SELECT `id` AS ARRAY_KEY, `id`, `mapId`, `posX`, `posY` FROM dbc_areatrigger WHERE `id` IN (?a)', $guids);
break;
default:
trigger_error('Game::getWorldPosForGUID - instanced with unsupported TYPE '.$type, E_USER_WARNING);
}
return $result;
}
public static function worldPosToZonePos(int $mapId, float $posX, float $posY, int $areaId = 0, int $floor = -1) : array
{
if (!$mapId < 0)
return [];
$query = 'SELECT
dm.id,
wma.areaId,
IFNULL(dm.floor, 0) AS floor,
100 - ROUND(IF(dm.id IS NOT NULL, (?f - dm.minY) * 100 / (dm.maxY - dm.minY), (?f - wma.right) * 100 / (wma.left - wma.right)), 1) AS `posX`,
100 - ROUND(IF(dm.id IS NOT NULL, (?f - dm.minX) * 100 / (dm.maxX - dm.minX), (?f - wma.bottom) * 100 / (wma.top - wma.bottom)), 1) AS `posY`,
SQRT(POWER(abs(IF(dm.id IS NOT NULL, (?f - dm.minY) * 100 / (dm.maxY - dm.minY), (?f - wma.right) * 100 / (wma.left - wma.right)) - 50), 2) +
POWER(abs(IF(dm.id IS NOT NULL, (?f - dm.minX) * 100 / (dm.maxX - dm.minX), (?f - wma.bottom) * 100 / (wma.top - wma.bottom)) - 50), 2)) AS `dist`
FROM
dbc_worldmaparea wma
LEFT JOIN
dbc_dungeonmap dm ON dm.mapId = IF(?d AND (wma.mapId NOT IN (0, 1, 530, 571) OR wma.areaId = 4395), wma.mapId, -1)
WHERE
wma.mapId = ?d AND IF(?d, wma.areaId = ?d, wma.areaId <> 0){ AND IF(dm.floor IS NULL, 1, dm.floor = ?d)}
HAVING
(`posX` BETWEEN 0.1 AND 99.9 AND `posY` BETWEEN 0.1 AND 99.9)
ORDER BY
`dist` ASC';
// dist BETWEEN 0 (center) AND 70.7 (corner)
$points = DB::Aowow()->select($query, $posX, $posX, $posY, $posY, $posX, $posX, $posY, $posY, 1, $mapId, $areaId, $areaId, $floor < 0 ? DBSIMPLE_SKIP : $floor);
if (!$points) // retry: TC counts pre-instance subareas as instance-maps .. which have no map file
$points = DB::Aowow()->select($query, $posX, $posX, $posY, $posY, $posX, $posX, $posY, $posY, 0, $mapId, 0, 0, DBSIMPLE_SKIP);
if (!is_array($points))
{
trigger_error('Game::worldPosToZonePos - dbc query failed', E_USER_ERROR);
return [];
}
return $points;
}
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',
User::$localeId ?: DBSIMPLE_SKIP,
User::$localeId ? Util::$localeStrings[User::$localeId] : DBSIMPLE_SKIP,
User::$localeId ? Util::$localeStrings[User::$localeId] : 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-insensivity
$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
{
switch ($skillId)
{
case SKILL_HERBALISM:
case SKILL_LOCKPICKING:
case SKILL_JEWELCRAFTING:
case SKILL_INSCRIPTION:
case SKILL_SKINNING:
case SKILL_MINING:
$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

@@ -0,0 +1,79 @@
<?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

@@ -0,0 +1,132 @@
<?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

@@ -0,0 +1,723 @@
<?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,5 +1,7 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
@@ -137,20 +139,21 @@ class Loot
return array_combine($retKeys, $retData);
}
private function getByContainerRecursive(string $tableName, int $lootId, array &$handledRefs, int $groupId = 0, float $baseChance = 1.0) : ?array
private function getByContainerRecursive(string $tableName, int $lootId, array &$handledRefs, int $groupId = 0, float $baseChance = 1.0) : array
{
$loot = [];
$rawItems = [];
if (!$tableName || !$lootId)
return null;
return [null, null];
$rows = DB::World()->select('SELECT * FROM ?# WHERE entry = ?d{ AND groupid = ?d}', $tableName, $lootId, $groupId ?: DBSIMPLE_SKIP);
if (!$rows)
return null;
return [null, null];
$groupChances = [];
$nGroupEquals = [];
$cnd = new Conditions();
foreach ($rows as $entry)
{
$set = array(
@@ -161,6 +164,11 @@ class Loot
'groupChance' => 0
);
if ($entry['QuestRequired'])
foreach (DB::Aowow()->selectCol('SELECT id FROM ?_quests WHERE (`reqSourceItemId1` = ?d OR `reqSourceItemId2` = ?d OR `reqSourceItemId3` = ?d OR `reqSourceItemId4` = ?d OR `reqItemId1` = ?d OR `reqItemId2` = ?d OR `reqItemId3` = ?d OR `reqItemId4` = ?d OR `reqItemId5` = ?d OR `reqItemId6` = ?d) AND (`cuFlags` & ?d) = 0',
$entry['Item'], $entry['Item'], $entry['Item'], $entry['Item'], $entry['Item'], $entry['Item'], $entry['Item'], $entry['Item'], $entry['Item'], $entry['Item'], CUSTOM_EXCLUDE_FOR_LISTVIEW | CUSTOM_UNAVAILABLE) as $questId)
$cnd->addExternalCondition(Conditions::lootTableToConditionSource($tableName), $lootId . ':' . $entry['Item'], [Conditions::QUESTTAKEN, $questId], true);
// if ($entry['LootMode'] > 1)
// {
$buff = [];
@@ -257,6 +265,12 @@ class Loot
$groupChances[$k] = (100 - $sum) / ($nGroupEquals[$k] ?: 1);
}
if ($cnd->getBySourceGroup($lootId, Conditions::lootTableToConditionSource($tableName))->prepare())
{
self::storeJSGlobals($cnd->getJsGlobals());
$cnd->toListviewColumn($loot, $this->extraCols, $lootId, 'content');
}
return [$loot, array_unique($rawItems)];
}
@@ -268,10 +282,6 @@ class Loot
return false;
/*
todo (high): implement conditions on loot (and conditions in general)
also
// if (is_array($this->entry) && in_array($table, [LOOT_CREATURE, LOOT_GAMEOBJECT])
// iterate over the 4 available difficulties and assign modes
@@ -279,16 +289,16 @@ class Loot
modes:{"mode":1,"1":{"count":4408,"outof":16013},"4":{"count":4408,"outof":22531}}
*/
$handledRefs = [];
$struct = self::getByContainerRecursive($table, $this->entry, $handledRefs);
if (!$struct)
[$lootRows, $itemIds] = self::getByContainerRecursive($table, $this->entry, $handledRefs);
if (!$lootRows)
return false;
$items = new ItemList(array(['i.id', $struct[1]], CFG_SQL_LIMIT_NONE));
$this->jsGlobals = $items->getJSGlobals(GLOBALINFO_SELF | GLOBALINFO_RELATED);
$items = new ItemList(array(['i.id', $itemIds], Cfg::get('SQL_LIMIT_NONE')));
self::storeJSGlobals($items->getJSGlobals(GLOBALINFO_SELF | GLOBALINFO_RELATED));
$foo = $items->getListviewData();
// assign listview LV rows to loot rows, not the other way round! The same item may be contained multiple times
foreach ($struct[0] as $loot)
foreach ($lootRows as $loot)
{
$base = array(
'percent' => round($loot['groupChance'] * $loot['realChanceMod'], 3),
@@ -303,11 +313,20 @@ class Loot
if ($_ = $loot['parentRef'])
$base['reference'] = $_;
if (isset($loot['condition']))
$base['condition'] = $loot['condition'];
if ($_ = self::createStack($loot))
$base['pctstack'] = $_;
if (empty($loot['reference'])) // regular drop
{
if (!isset($foo[$loot['content']]))
{
trigger_error('Item #'.$loot['content'].' referenced by loot does not exist!', E_USER_WARNING);
continue;
}
if (!User::isInGroup(U_GROUP_EMPLOYEE))
{
if (!isset($this->results[$loot['content']]))
@@ -380,33 +399,36 @@ class Loot
return true;
}
public function getByItem(int $entry, int $maxResults = CFG_SQL_LIMIT_DEFAULT, array $lootTableList = []) : bool
public function getByItem(int $entry, int $maxResults = -1, array $lootTableList = []) : bool
{
$this->entry = intVal($entry);
$this->entry = $entry;
if (!$this->entry)
return false;
if ($maxResults < 0)
$maxResults = Cfg::get('SQL_LIMIT_DEFAULT');
// [fileName, tabData, tabName, tabId, extraCols, hiddenCols, visibleCols]
$tabsFinal = array(
['item', [], '$LANG.tab_containedin', 'contained-in-item', [], [], []],
['item', [], '$LANG.tab_disenchantedfrom', 'disenchanted-from', [], [], []],
['item', [], '$LANG.tab_prospectedfrom', 'prospected-from', [], [], []],
['item', [], '$LANG.tab_milledfrom', 'milled-from', [], [], []],
['creature', [], '$LANG.tab_droppedby', 'dropped-by', [], [], []],
['creature', [], '$LANG.tab_pickpocketedfrom', 'pickpocketed-from', [], [], []],
['creature', [], '$LANG.tab_skinnedfrom', 'skinned-from', [], [], []],
['creature', [], '$LANG.tab_minedfromnpc', 'mined-from-npc', [], [], []],
['creature', [], '$LANG.tab_salvagedfrom', 'salvaged-from', [], [], []],
['creature', [], '$LANG.tab_gatheredfromnpc', 'gathered-from-npc', [], [], []],
['quest', [], '$LANG.tab_rewardfrom', 'reward-from-quest', [], [], []],
['zone', [], '$LANG.tab_fishedin', 'fished-in-zone', [], [], []],
['object', [], '$LANG.tab_containedin', 'contained-in-object', [], [], []],
['object', [], '$LANG.tab_minedfrom', 'mined-from-object', [], [], []],
['object', [], '$LANG.tab_gatheredfrom', 'gathered-from-object', [], [], []],
['object', [], '$LANG.tab_fishedin', 'fished-in-object', [], [], []],
['spell', [], '$LANG.tab_createdby', 'created-by', [], [], []],
['achievement', [], '$LANG.tab_rewardfrom', 'reward-from-achievement', [], [], []]
[Type::ITEM, [], '$LANG.tab_containedin', 'contained-in-item', [], [], []],
[Type::ITEM, [], '$LANG.tab_disenchantedfrom', 'disenchanted-from', [], [], []],
[Type::ITEM, [], '$LANG.tab_prospectedfrom', 'prospected-from', [], [], []],
[Type::ITEM, [], '$LANG.tab_milledfrom', 'milled-from', [], [], []],
[Type::NPC, [], '$LANG.tab_droppedby', 'dropped-by', [], [], []],
[Type::NPC, [], '$LANG.tab_pickpocketedfrom', 'pickpocketed-from', [], [], []],
[Type::NPC, [], '$LANG.tab_skinnedfrom', 'skinned-from', [], [], []],
[Type::NPC, [], '$LANG.tab_minedfromnpc', 'mined-from-npc', [], [], []],
[Type::NPC, [], '$LANG.tab_salvagedfrom', 'salvaged-from', [], [], []],
[Type::NPC, [], '$LANG.tab_gatheredfromnpc', 'gathered-from-npc', [], [], []],
[Type::QUEST, [], '$LANG.tab_rewardfrom', 'reward-from-quest', [], [], []],
[Type::ZONE, [], '$LANG.tab_fishedin', 'fished-in-zone', [], [], []],
[Type::OBJECT, [], '$LANG.tab_containedin', 'contained-in-object', [], [], []],
[Type::OBJECT, [], '$LANG.tab_minedfrom', 'mined-from-object', [], [], []],
[Type::OBJECT, [], '$LANG.tab_gatheredfrom', 'gathered-from-object', [], [], []],
[Type::OBJECT, [], '$LANG.tab_fishedin', 'fished-in-object', [], [], []],
[Type::SPELL, [], '$LANG.tab_createdby', 'created-by', [], [], []],
[Type::ACHIEVEMENT, [], '$LANG.tab_rewardfrom', 'reward-from-achievement', [], [], []]
);
$refResults = [];
$query = 'SELECT
@@ -436,6 +458,16 @@ class Loot
$this->entry
);
/* i'm currently not seeing a reasonable way to blend this into creature/gobject/etc tabs as one entity may drop the same item multiple times, with and without conditions.
if ($newRefs)
{
$cnd = new Conditions();
if ($cnd->getBySourceEntry($this->entry, Conditions::SRC_REFERENCE_LOOT_TEMPLATE))
if ($cnd->toListviewColumn($newRefs, $x, $this->entry))
self::storejsGlobals($cnd->getJsGlobals());
}
*/
while ($newRefs)
{
$curRefs = $newRefs;
@@ -451,14 +483,17 @@ class Loot
/*
search the real loot-templates for the itemId and gathered refds
*/
for ($i = 1; $i < count($this->lootTemplates); $i++)
foreach ($this->lootTemplates as $lootTemplate)
{
if ($lootTableList && !in_array($this->lootTemplates[$i], $lootTableList))
if ($lootTableList && !in_array($lootTemplate, $lootTableList))
continue;
if ($lootTemplate == LOOT_REFERENCE)
continue;
$result = $this->calcChance(DB::World()->select(
sprintf($query, '{lt1.reference IN (?a) OR }(lt1.reference = 0 AND lt1.item = ?d)'),
$this->lootTemplates[$i], $this->lootTemplates[$i],
$lootTemplate, $lootTemplate,
$refResults ? array_keys($refResults) : DBSIMPLE_SKIP,
$this->entry
));
@@ -474,10 +509,10 @@ class Loot
}
// cap fetched entries to the sql-limit to guarantee, that the highest chance items get selected first
// screws with GO-loot and skinnig-loot as these templates are shared for several tabs (fish, herb, ore) (herb, ore, leather)
// screws with GO-loot and skinning-loot as these templates are shared for several tabs (fish, herb, ore) (herb, ore, leather)
$ids = array_slice(array_keys($result), 0, $maxResults);
switch ($this->lootTemplates[$i])
switch ($lootTemplate)
{
case LOOT_CREATURE: $field = 'lootId'; $tabId = 4; break;
case LOOT_PICKPOCKET: $field = 'pickpocketLootId'; $tabId = 5; break;
@@ -552,9 +587,9 @@ class Loot
case LOOT_SPELL:
$conditions = array(
'OR',
['AND', ['effect1CreateItemId', $this->entry], ['OR', ['effect1Id', SpellList::$effects['itemCreate']], ['effect1AuraId', SpellList::$auras['itemCreate']]]],
['AND', ['effect2CreateItemId', $this->entry], ['OR', ['effect2Id', SpellList::$effects['itemCreate']], ['effect2AuraId', SpellList::$auras['itemCreate']]]],
['AND', ['effect3CreateItemId', $this->entry], ['OR', ['effect3Id', SpellList::$effects['itemCreate']], ['effect3AuraId', SpellList::$auras['itemCreate']]]],
['AND', ['effect1CreateItemId', $this->entry], ['OR', ['effect1Id', SpellList::EFFECTS_ITEM_CREATE], ['effect1AuraId', SpellList::AURAS_ITEM_CREATE]]],
['AND', ['effect2CreateItemId', $this->entry], ['OR', ['effect2Id', SpellList::EFFECTS_ITEM_CREATE], ['effect2AuraId', SpellList::AURAS_ITEM_CREATE]]],
['AND', ['effect3CreateItemId', $this->entry], ['OR', ['effect3Id', SpellList::EFFECTS_ITEM_CREATE], ['effect3AuraId', SpellList::AURAS_ITEM_CREATE]]],
);
if ($ids)
$conditions[] = ['id', $ids];
@@ -568,7 +603,7 @@ class Loot
if (!empty($result))
$tabsFinal[16][4][] = '$Listview.extraCols.percent';
if ($srcObj->hasSetFields(['reagent1']))
if ($srcObj->hasSetFields('reagent1', 'reagent2', 'reagent3', 'reagent4', 'reagent5', 'reagent6', 'reagent7', 'reagent8'))
$tabsFinal[16][6][] = 'reagents';
foreach ($srcObj->iterate() as $_)
@@ -580,13 +615,28 @@ class Loot
if (!$ids)
continue;
$parentData = [];
switch ($tabsFinal[abs($tabId)][0])
{
case 'creature': // new CreatureList
case 'item': // new ItemList
case 'zone': // new ZoneList
$oName = ucFirst($tabsFinal[abs($tabId)][0]).'List';
$srcObj = new $oName(array([$field, $ids]));
case TYPE::NPC: // new CreatureList
if ($baseIds = DB::Aowow()->selectCol(
'SELECT `difficultyEntry1` AS ARRAY_KEY, `id` FROM ?_creature WHERE difficultyEntry1 IN (?a) UNION
SELECT `difficultyEntry2` AS ARRAY_KEY, `id` FROM ?_creature WHERE difficultyEntry2 IN (?a) UNION
SELECT `difficultyEntry3` AS ARRAY_KEY, `id` FROM ?_creature WHERE difficultyEntry3 IN (?a)',
$ids, $ids, $ids))
{
$parentObj = new CreatureList(array(['id', $baseIds]));
if (!$parentObj->error)
{
self::storeJSGlobals($parentObj->getJSGlobals());
$parentData = $parentObj->getListviewData();
$ids = array_diff($ids, $baseIds);
}
}
case Type::ITEM: // new ItemList
case Type::ZONE: // new ZoneList
$srcObj = Type::newList($tabsFinal[abs($tabId)][0], array([$field, $ids]));
if (!$srcObj->error)
{
$srcData = $srcObj->getListviewData();
@@ -594,16 +644,20 @@ class Loot
foreach ($srcObj->iterate() as $curTpl)
{
if ($tabId < 0 && $curTpl['typeFlags'] & NPC_TYPEFLAG_HERBLOOT)
if ($tabId < 0 && $curTpl['typeFlags'] & NPC_TYPEFLAG_SKIN_WITH_HERBALISM)
$tabId = 9;
else if ($tabId < 0 && $curTpl['typeFlags'] & NPC_TYPEFLAG_ENGINEERLOOT)
else if ($tabId < 0 && $curTpl['typeFlags'] & NPC_TYPEFLAG_SKIN_WITH_ENGINEERING)
$tabId = 8;
else if ($tabId < 0 && $curTpl['typeFlags'] & NPC_TYPEFLAG_MININGLOOT)
else if ($tabId < 0 && $curTpl['typeFlags'] & NPC_TYPEFLAG_SKIN_WITH_MINING)
$tabId = 7;
else if ($tabId < 0)
$tabId = abs($tabId); // general case (skinning)
$tabsFinal[$tabId][1][] = array_merge($srcData[$srcObj->id], $result[$srcObj->getField($field)]);
if (($p = $srcObj->getField('parentId')) && ($d = $parentData[$p] ?? null))
$tabsFinal[$tabId][1][] = array_merge($d, $result[$srcObj->getField($field)]);
else
$tabsFinal[$tabId][1][] = array_merge($srcData[$srcObj->id], $result[$srcObj->getField($field)]);
$tabsFinal[$tabId][4][] = '$Listview.extraCols.percent';
}
}
@@ -620,15 +674,15 @@ class Loot
);
if ($data[4])
$tabData['extraCols'] = array_unique($data[4]);
$tabData['extraCols'] = array_unique($data[4]);
if ($data[5])
$tabData['hiddenCols'] = array_unique($data[5]);
$tabData['hiddenCols'] = array_unique($data[5]);
if ($data[6])
$tabData['visibleCols'] = array_unique($data[6]);
$this->results[$tabId] = [$data[0], $tabData];
$this->results[$tabId] = [Type::getFileString($data[0]), $tabData];
}
return true;

348
includes/game/misc.php Normal file
View File

@@ -0,0 +1,348 @@
<?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

@@ -0,0 +1,160 @@
<?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,82 +1,243 @@
<?php
if (!defined('AOWOW_REVISION'))
die('illegal access');
namespace Aowow;
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'))
require_once 'config/config.php';
else
$AoWoWconf = [];
mb_internal_encoding('UTF-8');
define('OS_WIN', substr(PHP_OS, 0, 3) == 'WIN');
require_once 'includes/defines.php';
require_once 'includes/libs/DbSimple/Generic.php'; // Libraray: http://en.dklab.ru/lib/DbSimple (using variant: https://github.com/ivan1986/DbSimple/tree/master)
require_once 'includes/utilities.php'; // helper functions
require_once 'includes/game.php'; // game related data & functions
require_once 'includes/profiler.class.php';
require_once 'includes/user.class.php';
require_once 'includes/markup.class.php'; // manipulate markup text
require_once 'includes/database.class.php'; // wrap DBSimple
require_once 'includes/community.class.php'; // handle comments, screenshots and videos
require_once 'includes/loot.class.php'; // build lv-tabs containing loot-information
require_once 'includes/smartAI.class.php';
require_once 'localization/lang.class.php';
require_once 'pages/genericPage.class.php';
// autoload List-classes, associated filters and pages
spl_autoload_register(function ($class) {
$class = strtolower(str_replace('ListFilter', 'List', $class));
if (class_exists($class)) // already registered
return;
if (preg_match('/[^\w]/i', $class)) // name should contain only letters
return;
if (stripos($class, 'list'))
{
require_once 'includes/basetype.class.php';
$cl = strtr($class, ['list' => '']);
if ($cl == 'remoteprofile' || $cl == 'localprofile')
$cl = 'profile';
if ($cl == 'remotearenateam' || $cl == 'localarenateam')
$cl = 'arenateam';
if ($cl == 'remoteguild' || $cl == 'localguild')
$cl = 'guild';
if (file_exists('includes/types/'.$cl.'.class.php'))
require_once 'includes/types/'.$cl.'.class.php';
else
throw new Exception('could not register type class: '.$cl);
return;
}
else if (stripos($class, 'ajax') === 0)
{
require_once 'includes/ajaxHandler.class.php'; // handles ajax and jsonp requests
if (file_exists('includes/ajaxHandler/'.strtr($class, ['ajax' => '']).'.class.php'))
require_once 'includes/ajaxHandler/'.strtr($class, ['ajax' => '']).'.class.php';
else
throw new Exception('could not register ajaxHandler class: '.$class);
return;
}
else if (file_exists('pages/'.strtr($class, ['page' => '']).'.php'))
require_once 'pages/'.strtr($class, ['page' => '']).'.php';
});
// Setup DB-Wrapper
if (!empty($AoWoWconf['aowow']['db']))
DB::load(DB_AOWOW, $AoWoWconf['aowow']);
@@ -91,188 +252,63 @@ if (!empty($AoWoWconf['characters']))
if (!empty($charDBInfo))
DB::load(DB_CHARACTERS . $realm, $charDBInfo);
$AoWoWconf = null; // empty auths
// load config to constants
function loadConfig(bool $noPHP = false) : void
{
$sets = DB::isConnectable(DB_AOWOW) ? DB::Aowow()->select('SELECT `key` AS ARRAY_KEY, `value`, `flags` FROM ?_config') : [];
foreach ($sets as $k => $v)
{
$php = $v['flags'] & CON_FLAG_PHP;
if ($php && $noPHP)
continue;
// this should not have been possible
if (!strlen($v['value']) && !($v['flags'] & CON_FLAG_TYPE_STRING) && !$php)
{
trigger_error('Aowow config value CFG_'.strtoupper($k).' is empty - config will not be used!', E_USER_ERROR);
continue;
}
// for CLI and early errors in erb context
Lang::load(Locale::EN);
if ($v['flags'] & CON_FLAG_TYPE_INT)
$val = intVal($v['value']);
else if ($v['flags'] & CON_FLAG_TYPE_FLOAT)
$val = floatVal($v['value']);
else if ($v['flags'] & CON_FLAG_TYPE_BOOL)
$val = (bool)$v['value'];
else if ($v['flags'] & CON_FLAG_TYPE_STRING)
$val = preg_replace("/[\p{C}]/ui", '', $v['value']);
else if ($php)
{
trigger_error('PHP config value '.strtolower($k).' has no type set - config will not be used!', E_USER_ERROR);
continue;
}
else // if (!$php)
{
trigger_error('Aowow config value CFG_'.strtoupper($k).' has no type set - value forced to 0!', E_USER_ERROR);
$val = 0;
}
if ($php)
ini_set(strtolower($k), $val);
else if (!defined('CFG_'.strtoupper($k)))
define('CFG_'.strtoupper($k), $val);
}
}
loadConfig();
// handle non-fatal errors and notices
error_reporting(!empty($AoWoWconf['aowow']) && CFG_DEBUG ? E_AOWOW : 0);
set_error_handler(function($errNo, $errStr, $errFile, $errLine)
{
$errName = 'unknown error'; // errors not in this list can not be handled by set_error_handler (as per documentation) or are ignored
$uGroup = U_GROUP_EMPLOYEE;
if ($errNo == E_WARNING) // 0x0002
$errName = 'E_WARNING';
else if ($errNo == E_PARSE) // 0x0004
$errName = 'E_PARSE';
else if ($errNo == E_NOTICE) // 0x0008
$errName = 'E_NOTICE';
else if ($errNo == E_USER_ERROR) // 0x0100
$errName = 'E_USER_ERROR';
else if ($errNo == E_USER_WARNING) // 0x0200
$errName = 'E_USER_WARNING';
else if ($errNo == E_USER_NOTICE) // 0x0400
{
$errName = 'E_USER_NOTICE';
$uGroup = U_GROUP_STAFF;
}
else if ($errNo == E_RECOVERABLE_ERROR) // 0x1000
$errName = 'E_RECOVERABLE_ERROR';
Util::addNote($uGroup, $errName.' - '.$errStr.' @ '.$errFile. ':'.$errLine);
if (CLI)
CLI::write($errName.' - '.$errStr.' @ '.$errFile. ':'.$errLine, $errNo & 0x40A ? CLI::LOG_WARN : CLI::LOG_ERROR);
if (DB::isConnectable(DB_AOWOW))
DB::Aowow()->query('INSERT INTO ?_errors (`date`, `version`, `phpError`, `file`, `line`, `query`, `userGroups`, `message`) VALUES (UNIX_TIMESTAMP(), ?d, ?d, ?, ?d, ?, ?d, ?) ON DUPLICATE KEY UPDATE `date` = UNIX_TIMESTAMP()',
AOWOW_REVISION, $errNo, $errFile, $errLine, CLI ? 'CLI' : ($_SERVER['QUERY_STRING'] ?? ''), User::$groups, $errStr
);
return true;
}, E_AOWOW);
// handle exceptions
set_exception_handler(function ($ex)
{
Util::addNote(U_GROUP_EMPLOYEE, 'Exception - '.$ex->getMessage().' @ '.$ex->getFile(). ':'.$ex->getLine()."\n".$ex->getTraceAsString());
if (DB::isConnectable(DB_AOWOW))
DB::Aowow()->query('INSERT INTO ?_errors (`date`, `version`, `phpError`, `file`, `line`, `query`, `userGroups`, `message`) VALUES (UNIX_TIMESTAMP(), ?d, ?d, ?, ?d, ?, ?d, ?) ON DUPLICATE KEY UPDATE `date` = UNIX_TIMESTAMP()',
AOWOW_REVISION, $ex->getCode(), $ex->getFile(), $ex->getLine(), CLI ? 'CLI' : ($_SERVER['QUERY_STRING'] ?? ''), User::$groups, $ex->getMessage()
);
if (!CLI)
(new GenericPage())->error();
else
echo 'Exception - '.$ex->getMessage()."\n ".$ex->getFile(). '('.$ex->getLine().")\n".$ex->getTraceAsString()."\n";
});
// handle fatal errors
register_shutdown_function(function()
{
if (($e = error_get_last()) && $e['type'] & (E_ERROR | E_COMPILE_ERROR | E_CORE_ERROR))
{
Util::addNote(U_GROUP_EMPLOYEE, 'Fatal Error - '.$e['message'].' @ '.$e['file']. ':'.$e['line']);
if (DB::isConnectable(DB_AOWOW))
DB::Aowow()->query('INSERT INTO ?_errors (`date`, `version`, `phpError`, `file`, `line`, `query`, `userGroups`, `message`) VALUES (UNIX_TIMESTAMP(), ?d, ?d, ?, ?d, ?, ?d, ?) ON DUPLICATE KEY UPDATE `date` = UNIX_TIMESTAMP()',
AOWOW_REVISION, $e['type'], $e['file'], $e['line'], CLI ? 'CLI' : ($_SERVER['QUERY_STRING'] ?? ''), User::$groups, $e['message']
);
if (CLI)
echo 'Fatal Error - '.$e['message'].' @ '.$e['file']. ':'.$e['line']."\n";
// cant generate a page for web view :(
die();
}
});
$secure = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] != 'off') || (!empty($AoWoWconf['aowow']) && CFG_FORCE_SSL);
if (defined('CFG_STATIC_HOST')) // points js to images & scripts
define('STATIC_URL', ($secure ? 'https://' : 'http://').CFG_STATIC_HOST);
if (defined('CFG_SITE_HOST')) // points js to executable files
define('HOST_URL', ($secure ? 'https://' : 'http://').CFG_SITE_HOST);
// load config from DB
Cfg::load();
if (!CLI)
{
if (!defined('CFG_SITE_HOST') || !defined('CFG_STATIC_HOST'))
die('error: SITE_HOST or STATIC_HOST not configured');
// not displaying the brb gnomes as static_host is missing, but eh...
if (!DB::isConnected(DB_AOWOW) || !DB::isConnected(DB_WORLD) || !Cfg::get('HOST_URL') || !Cfg::get('STATIC_URL'))
(new GenericPage())->maintenance();
// Setup Session
if (CFG_SESSION_CACHE_DIR && Util::writeDir(CFG_SESSION_CACHE_DIR))
session_save_path(getcwd().'/'.CFG_SESSION_CACHE_DIR);
$cacheDir = Cfg::get('SESSION_CACHE_DIR');
if ($cacheDir && Util::writeDir($cacheDir))
session_save_path(getcwd().'/'.$cacheDir);
session_set_cookie_params(15 * YEAR, '/', '', $secure, true);
session_set_cookie_params(15 * YEAR, '/', '', (($_SERVER['HTTPS'] ?? 'off') != 'off') || Cfg::get('FORCE_SSL'), true);
session_cache_limiter('private');
if (!session_start())
{
trigger_error('failed to start session', E_USER_ERROR);
exit;
(new GenericPage())->error();
}
if (!empty($AoWoWconf['aowow']) && User::init())
if (User::init())
User::save(); // save user-variables in session
// set up some logging (~10 queries will execute before we init the user and load the config)
if (CFG_DEBUG && User::isInGroup(U_GROUP_DEV | U_GROUP_ADMIN))
// hard override locale for this call (should this be here..?)
if (isset($_GET['locale']) && ($loc = Locale::tryFrom((int)$_GET['locale'])))
Lang::load($loc);
else
Lang::load(User::$preferedLoc);
// 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', 'logger']);
DB::World()->setLogger(['DB', 'logger']);
DB::Aowow()->setLogger(DB::profiler(...));
DB::World()->setLogger(DB::profiler(...));
if (DB::isConnected(DB_AUTH))
DB::Auth()->setLogger(['DB', 'logger']);
DB::Auth()->setLogger(DB::profiler(...));
if (!empty($AoWoWconf['characters']))
foreach ($AoWoWconf['characters'] as $idx => $__)
if (DB::isConnected(DB_CHARACTERS . $idx))
DB::Characters($idx)->setLogger(['DB', 'logger']);
}
// hard-override locale for this call (should this be here..?)
// all strings attached..
if (!empty($AoWoWconf['aowow']))
{
if (isset($_GET['locale']) && (int)$_GET['locale'] <= MAX_LOCALES && (int)$_GET['locale'] >= 0)
if (CFG_LOCALES & (1 << $_GET['locale']))
User::useLocale($_GET['locale']);
Lang::load(User::$localeString);
DB::Characters($idx)->setLogger(DB::profiler(...));
}
// parse page-parameters .. sanitize before use!
$str = explode('&', mb_strtolower($_SERVER['QUERY_STRING'] ?? ''), 2)[0];
$str = explode('&', $_SERVER['QUERY_STRING'] ?? '', 2)[0];
$_ = explode('=', $str, 2);
$pageCall = $_[0];
$pageCall = mb_strtolower($_[0]);
$pageParam = $_[1] ?? '';
Util::$wowheadLink = 'http://'.Util::$subDomains[User::$localeId].'.wowhead.com/'.$str;
}
else if (!empty($AoWoWconf['aowow']))
Lang::load('enus');
$AoWoWconf = null; // empty auths
?>

View File

@@ -93,6 +93,8 @@ require_once __DIR__ . '/CacherImpl.php';
*/
abstract class DbSimple_Database extends DbSimple_LastError
{
private $attributes;
/**
* Public methods.
*/
@@ -1142,7 +1144,7 @@ abstract class DbSimple_Database extends DbSimple_LastError
$len = 0;
$values = array();
foreach ($rows[0] as $k=>$v) {
$len += strlen($v);
$len += strlen($v ?? '');
if ($len > $this->MAX_LOG_ROW_LEN) {
break;
}

View File

@@ -26,6 +26,8 @@ class DbSimple_Mysqli extends DbSimple_Database
{
var $link;
private $_lastQuery;
/**
* constructor(string $dsn)
* Connect to MySQL server.
@@ -163,6 +165,17 @@ class DbSimple_Mysqli extends DbSimple_Database
$result = mysqli_query($this->link, $queryMain[0]);
if ($result === false)
return $this->_setDbError($queryMain[0]);
if ($this->link->warning_count) {
if ($warn = $this->link->query("SHOW WARNINGS")) {
while ($warnRow = $warn->fetch_row())
if ($warnRow[0] === 'Warning')
$this->_setLastError(-$warnRow[1], $warnRow[2], $queryMain[0]);
$warn->close();
}
}
if (!is_object($result)) {
if (preg_match('/^\s* INSERT \s+/six', $queryMain[0]))
{

179
includes/locale.class.php Normal file
View File

@@ -0,0 +1,179 @@
<?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,148 +0,0 @@
<?php
if (!defined('AOWOW_REVISION'))
die('illegal 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 = [];
private static $dbTagPattern = '/(?<!\\\\)\[(npc|object|item|itemset|quest|spell|zone|faction|pet|achievement|statistic|title|event|class|race|skill|currency|emote|enchantment|money|sound|icondb)=(-?\d+)[^\]]*\]/i';
public function __construct($text)
{
$this->text = $text;
}
public function parseGlobalsFromText(&$jsg = [])
{
if (preg_match_all(self::$dbTagPattern, $this->text, $matches, PREG_SET_ORDER))
{
foreach ($matches as $match)
{
if ($match[1] == 'statistic')
$match[1] = 'achievement';
else if ($match[1] == 'icondb')
$match[1] = 'icon';
if ($match[1] == 'money')
{
if (stripos($match[0], 'items'))
{
if (preg_match('/items=([0-9,]+)/i', $match[0], $submatch))
{
$sm = explode(',', $submatch[1]);
for ($i = 0; $i < count($sm); $i+=2)
$this->jsGlobals[Type::ITEM][$sm[$i]] = $sm[$i];
}
}
if (stripos($match[0], 'currency'))
{
if (preg_match('/currency=([0-9,]+)/i', $match[0], $submatch))
{
$sm = explode(',', $submatch[1]);
for ($i = 0; $i < count($sm); $i+=2)
$this->jsGlobals[Type::CURRENCY][$sm[$i]] = $sm[$i];
}
}
}
else if ($type = Type::getIndexFrom(Type::IDX_FILE_STR, $match[1]))
$this->jsGlobals[$type][$match[2]] = $match[2];
}
}
Util::mergeJsGlobals($jsg, $this->jsGlobals);
return $this->jsGlobals;
}
public function stripTags($globals = [])
{
// since this is an article the db-tags should already be parsed
$text = preg_replace_callback(self::$dbTagPattern, function ($match) use ($globals) {
if ($match[1] == 'statistic')
$match[1] = 'achievement';
else if ($match[1] == 'icondb')
$match[1] = 'icon';
else if ($match[1] == 'money')
{
$moneys = [];
if (stripos($match[0], 'items'))
{
if (preg_match('/items=([0-9,]+)/i', $match[0], $submatch))
{
$sm = explode(',', $submatch[1]);
for ($i = 0; $i < count($sm); $i += 2)
{
if (!empty($globals[Type::ITEM][1][$sm[$i]]))
$moneys[] = $globals[Type::ITEM][1][$sm[$i]]['name'];
else
$moneys[] = Util::ucFirst(Lang::game('item')).' #'.$sm[$i];
}
}
}
if (stripos($match[0], 'currency'))
{
if (preg_match('/currency=([0-9,]+)/i', $match[0], $submatch))
{
$sm = explode(',', $submatch[1]);
for ($i = 0; $i < count($sm); $i += 2)
{
if (!empty($globals[Type::CURRENCY][1][$sm[$i]]))
$moneys[] = $globals[Type::CURRENCY][1][$sm[$i]]['name'];
else
$moneys[] = Util::ucFirst(Lang::game('curency')).' #'.$sm[$i];
}
}
}
return Lang::concat($moneys);
}
if ($type = Type::getIndexFrom(Type::IDX_FILE_STR, $match[1]))
{
if (!empty($globals[$type][1][$match[2]]))
return $globals[$type][1][$match[2]]['name'];
else
return Util::ucFirst(Lang::game($match[1])).' #'.$match[2];
}
trigger_error('Markup::stripTags() - encountered unhandled db-tag: '.var_export($match));
return '';
}, $this->text);
$text = str_replace('[br]', "\n", $text);
$stripped = '';
$inTag = false;
for ($i = 0; $i < strlen($text); $i++)
{
if ($text[$i] == '[')
$inTag = true;
if (!$inTag)
$stripped .= $text[$i];
if ($text[$i] == ']')
$inTag = false;
}
return $stripped;
}
public function fromHtml()
{
}
public function toHtml()
{
}
}
?>

View File

@@ -0,0 +1,355 @@
<?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

@@ -0,0 +1,39 @@
<?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;
}
}
?>

View File

@@ -1,26 +0,0 @@
<?php
define('AOWOW_REVISION', 33);
define('CLI', PHP_SAPI === 'cli');
$reqExt = ['SimpleXML', 'gd', 'mysqli', 'mbstring', 'fileinfo'/*, 'gmp'*/];
$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, '7.4.0') < 0)
$error .= 'PHP Version <b>7.4</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';
?>

File diff suppressed because it is too large Load Diff

260
includes/type.class.php Normal file
View File

@@ -0,0 +1,260 @@
<?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,5 +1,7 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
@@ -25,7 +27,7 @@ class AchievementList extends BaseType
todo: evaluate TC custom-data-tables: a*_criteria_data should be merged on installation
*/
public function __construct($conditions = [], $miscData = null)
public function __construct(array $conditions = [], array $miscData = [])
{
parent::__construct($conditions, $miscData);
@@ -257,12 +259,15 @@ class AchievementList extends BaseType
return $x;
}
public function getSourceData()
public function getSourceData(int $id = 0) : array
{
$data = [];
foreach ($this->iterate() as $__)
{
if ($id && $id != $this->id)
continue;
$data[$this->id] = array(
"n" => $this->getField('name', true),
"s" => $this->curTpl['faction'],
@@ -278,11 +283,12 @@ class AchievementList extends BaseType
class AchievementListFilter extends Filter
{
protected $enums = array(
protected string $type = 'achievements';
protected array $enums = array(
4 => parent::ENUM_ZONE, // location
11 => array(
327 => 160, // Lunar Festival
335 => 187, // Love is in the Air
423 => 187, // Love is in the Air
181 => 159, // Noblegarden
201 => 163, // Children's Week
341 => 161, // Midsummer Fire Festival
@@ -292,8 +298,8 @@ class AchievementListFilter extends Filter
141 => 156, // Feast of Winter Veil
409 => -3456, // Day of the Dead
398 => -3457, // Pirates' Day
FILTER_ENUM_ANY => true,
FILTER_ENUM_NONE => false,
parent::ENUM_ANY => true,
parent::ENUM_NONE => false,
283 => -1, // valid events without achievements
285 => -1, 353 => -1, 420 => -1,
400 => -1, 284 => -1, 374 => -1,
@@ -301,116 +307,100 @@ class AchievementListFilter extends Filter
)
);
protected $genericFilter = array( // misc (bool): _NUMERIC => useFloat; _STRING => localized; _FLAG => match Value; _BOOLEAN => stringSet
2 => [FILTER_CR_BOOLEAN, 'reward_loc0', true ], // givesreward
3 => [FILTER_CR_STRING, 'reward', STR_LOCALIZED ], // rewardtext
4 => [FILTER_CR_NYI_PH, null, 1, ], // location [enum]
5 => [FILTER_CR_CALLBACK, 'cbSeries', ACHIEVEMENT_CU_FIRST_SERIES, null], // first in series [yn]
6 => [FILTER_CR_CALLBACK, 'cbSeries', ACHIEVEMENT_CU_LAST_SERIES, null], // last in series [yn]
7 => [FILTER_CR_BOOLEAN, 'chainId', ], // partseries
9 => [FILTER_CR_NUMERIC, 'id', NUM_CAST_INT, true], // id
10 => [FILTER_CR_STRING, 'ic.name', ], // icon
11 => [FILTER_CR_CALLBACK, 'cbRelEvent', null, null], // related event [enum]
14 => [FILTER_CR_FLAG, 'cuFlags', CUSTOM_HAS_COMMENT ], // hascomments
15 => [FILTER_CR_FLAG, 'cuFlags', CUSTOM_HAS_SCREENSHOT ], // hasscreenshots
16 => [FILTER_CR_FLAG, 'cuFlags', CUSTOM_HAS_VIDEO ], // hasvideos
18 => [FILTER_CR_STAFFFLAG, 'flags', ] // flags
protected array $genericFilter = array(
2 => [parent::CR_BOOLEAN, 'reward_loc0', true ], // givesreward
3 => [parent::CR_STRING, 'reward', STR_LOCALIZED ], // rewardtext
4 => [parent::CR_NYI_PH, null, 1, ], // location [enum]
5 => [parent::CR_CALLBACK, 'cbSeries', ACHIEVEMENT_CU_FIRST_SERIES, null], // first in series [yn]
6 => [parent::CR_CALLBACK, 'cbSeries', ACHIEVEMENT_CU_LAST_SERIES, null], // last in series [yn]
7 => [parent::CR_BOOLEAN, 'chainId', ], // partseries
9 => [parent::CR_NUMERIC, 'id', NUM_CAST_INT, true], // id
10 => [parent::CR_STRING, 'ic.name', ], // icon
11 => [parent::CR_CALLBACK, 'cbRelEvent', null, null], // related event [enum]
14 => [parent::CR_FLAG, 'cuFlags', CUSTOM_HAS_COMMENT ], // hascomments
15 => [parent::CR_FLAG, 'cuFlags', CUSTOM_HAS_SCREENSHOT ], // hasscreenshots
16 => [parent::CR_FLAG, 'cuFlags', CUSTOM_HAS_VIDEO ], // hasvideos
18 => [parent::CR_STAFFFLAG, 'flags', ] // flags
);
// fieldId => [checkType, checkValue[, fieldIsArray]]
protected $inputFields = array(
'cr' => [FILTER_V_RANGE, [2, 18], true ], // criteria ids
'crs' => [FILTER_V_LIST, [FILTER_ENUM_NONE, FILTER_ENUM_ANY, [0, 99999]], true ], // criteria operators
'crv' => [FILTER_V_REGEX, '/[\p{C};:%\\\\]/ui', true ], // criteria values - only printable chars, no delimiters
'na' => [FILTER_V_REGEX, '/[\p{C};%\\\\]/ui', false], // name / description - only printable chars, no delimiter
'ex' => [FILTER_V_EQUAL, 'on', false], // extended name search
'ma' => [FILTER_V_EQUAL, 1, false], // match any / all filter
'si' => [FILTER_V_LIST, [1, 2, 3, -1, -2], false], // side
'minpt' => [FILTER_V_RANGE, [1, 99], false], // required level min
'maxpt' => [FILTER_V_RANGE, [1, 99], false] // required level max
protected array $inputFields = array(
'cr' => [parent::V_RANGE, [2, 18], true ], // criteria ids
'crs' => [parent::V_LIST, [parent::ENUM_NONE, parent::ENUM_ANY, [0, 99999]], true ], // criteria operators
'crv' => [parent::V_REGEX, parent::PATTERN_CRV, true ], // criteria values - only printable chars, no delimiters
'na' => [parent::V_REGEX, parent::PATTERN_NAME, false], // name / description - only printable chars, no delimiter
'ex' => [parent::V_EQUAL, 'on', false], // extended name search
'ma' => [parent::V_EQUAL, 1, false], // match any / all filter
'si' => [parent::V_LIST, [SIDE_ALLIANCE, SIDE_HORDE, SIDE_BOTH, -SIDE_ALLIANCE, -SIDE_HORDE], false], // side
'minpt' => [parent::V_RANGE, [1, 99], false], // required level min
'maxpt' => [parent::V_RANGE, [1, 99], false] // required level max
);
protected function createSQLForCriterium(&$cr)
{
if (in_array($cr[0], array_keys($this->genericFilter)))
if ($genCr = $this->genericCriterion($cr))
return $genCr;
unset($cr);
$this->error = true;
return [1];
}
protected function createSQLForValues()
protected function createSQLForValues() : array
{
$parts = [];
$_v = &$this->fiData['v'];
$_v = &$this->values;
// name ex: +description, +rewards
if (isset($_v['na']))
if ($_v['na'])
{
$_ = [];
if (isset($_v['ex']) && $_v['ex'] == 'on')
$_ = $this->modularizeString(['name_loc'.User::$localeId, 'reward_loc'.User::$localeId, 'description_loc'.User::$localeId]);
if ($_v['ex'] == 'on')
$_ = $this->tokenizeString(['name_loc'.Lang::getLocale()->value, 'reward_loc'.Lang::getLocale()->value, 'description_loc'.Lang::getLocale()->value]);
else
$_ = $this->modularizeString(['name_loc'.User::$localeId]);
$_ = $this->tokenizeString(['name_loc'.Lang::getLocale()->value]);
if ($_)
$parts[] = $_;
}
// points min
if (isset($_v['minpt']))
if ($_v['minpt'])
$parts[] = ['points', $_v['minpt'], '>='];
// points max
if (isset($_v['maxpt']))
if ($_v['maxpt'])
$parts[] = ['points', $_v['maxpt'], '<='];
// faction (side)
if (isset($_v['si']))
if ($_v['si'])
{
switch ($_v['si'])
$parts[] = match ($_v['si'])
{
case -1: // faction, exclusive both
case -2:
$parts[] = ['faction', -$_v['si']];
break;
case 1: // faction, inclusive both
case 2:
case 3: // both
$parts[] = ['faction', $_v['si'], '&'];
break;
}
-SIDE_ALLIANCE, // equals faction
-SIDE_HORDE => ['faction', -$_v['si']],
SIDE_ALLIANCE, // includes faction
SIDE_HORDE,
SIDE_BOTH => ['faction', $_v['si'], '&']
};
}
return $parts;
}
protected function cbRelEvent($cr, $value)
protected function cbRelEvent(int $cr, int $crs, string $crv) : ?array
{
if (!isset($this->enums[$cr[0]][$cr[1]]))
return false;
if (!isset($this->enums[$cr][$crs]))
return null;
$_ = $this->enums[$cr[0]][$cr[1]];
$_ = $this->enums[$cr][$crs];
if (is_int($_))
return ($_ > 0) ? ['category', $_] : ['id', abs($_)];
else
{
$ids = array_filter($this->enums[$cr[0]], function($x) { return is_int($x) && $x > 0; });
$ids = array_filter($this->enums[$cr], fn($x) => is_int($x) && $x > 0);
return ['category', $ids, $_ ? null : '!'];
}
return false;
return null;
}
protected function cbSeries($cr, $value)
protected function cbSeries(int $cr, int $crs, string $crv, int $seriesFlag) : ?array
{
if ($this->int2Bool($cr[1]))
return $cr[1] ? ['AND', ['chainId', 0, '!'], ['cuFlags', $value, '&']] : ['AND', ['chainId', 0, '!'], [['cuFlags', $value, '&'], 0]];
if ($this->int2Bool($crs))
return $crs ? ['AND', ['chainId', 0, '!'], ['cuFlags', $seriesFlag, '&']] : ['AND', ['chainId', 0, '!'], [['cuFlags', $seriesFlag, '&'], 0]];
return false;
return null;
}
}

View File

@@ -1,5 +1,7 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
@@ -8,19 +10,20 @@ class AreaTriggerList extends BaseType
{
use spawnHelper;
public static $type = Type::AREATRIGGER;
public static $brickFile = 'areatrigger';
public static $dataTable = '?_areatrigger';
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']],
's' => ['j' => ['?_spawns s ON s.type = 503 AND s.typeId = a.id', true], 's' => ', s.areaId']
'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($conditions)
public function __construct(array $conditions = [], array $miscData = [])
{
parent::__construct($conditions);
parent::__construct($conditions, $miscData);
foreach ($this->iterate() as $id => &$_curTpl)
if (!$_curTpl['name'])
@@ -40,7 +43,7 @@ class AreaTriggerList extends BaseType
);
if ($_ = $this->curTpl['areaId'])
$data[$this->id]['location'] = [$_];
$data[$this->id]['location'] = explode(',', $_);
}
return $data;
@@ -56,43 +59,33 @@ class AreaTriggerList extends BaseType
class AreaTriggerListFilter extends Filter
{
protected $genericFilter = array(
2 => [FILTER_CR_NUMERIC, 'id', NUM_CAST_INT] // id
protected string $type = 'areatrigger';
protected array $genericFilter = array(
2 => [parent::CR_NUMERIC, 'id', NUM_CAST_INT] // id
);
// fieldId => [checkType, checkValue[, fieldIsArray]]
protected $inputFields = array(
'cr' => [FILTER_V_LIST, [2], true ], // criteria ids
'crs' => [FILTER_V_RANGE, [1, 6], true ], // criteria operators
'crv' => [FILTER_V_RANGE, [0, 99999], true ], // criteria values - all criteria are numeric here
'na' => [FILTER_V_REGEX, '/[\p{C};\\\\]/ui', false], // name - only printable chars, no delimiter
'ma' => [FILTER_V_EQUAL, 1, false], // match any / all filter
'ty' => [FILTER_V_RANGE, [0, 5], true ] // types
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 createSQLForCriterium(&$cr)
{
if (in_array($cr[0], array_keys($this->genericFilter)))
if ($genCr = $this->genericCriterion($cr))
return $genCr;
unset($cr);
$this->error = true;
return [1];
}
protected function createSQLForValues()
protected function createSQLForValues() : array
{
$parts = [];
$_v = &$this->fiData['v'];
$_v = &$this->values;
// name [str]
if (isset($_v['na']))
if ($_ = $this->modularizeString(['name']))
if ($_v['na'])
if ($_ = $this->tokenizeString(['name']))
$parts[] = $_;
// type [list]
if (isset($_v['ty']))
if ($_v['ty'])
$parts[] = ['type', $_v['ty']];
return $parts;

View File

@@ -1,5 +1,7 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
@@ -10,6 +12,8 @@ class ArenaTeamList extends BaseType
private $rankOrder = [];
public static $contribute = CONTRIBUTE_NONE;
public function getListviewData()
{
$data = [];
@@ -42,76 +46,46 @@ class ArenaTeamList extends BaseType
class ArenaTeamListFilter extends Filter
{
public $extraOpts = [];
protected $genericFilter = [];
use TrProfilerFilter;
// fieldId => [checkType, checkValue[, fieldIsArray]]
protected $inputFields = array(
'na' => [FILTER_V_REGEX, '/[\p{C};%\\\\]/ui', false], // name - only printable chars, no delimiter
'ma' => [FILTER_V_EQUAL, 1, false], // match any / all filter
'ex' => [FILTER_V_EQUAL, 'on', false], // only match exact
'si' => [FILTER_V_LIST, [1, 2], false], // side
'sz' => [FILTER_V_LIST, [2, 3, 5], false], // tema size
'rg' => [FILTER_V_CALLBACK, 'cbRegionCheck', false], // region
'sv' => [FILTER_V_CALLBACK, 'cbServerCheck', false], // server
protected 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
);
protected function createSQLForCriterium(&$cr) { }
public array $extraOpts = [];
protected function createSQLForValues()
protected function createSQLForValues() : array
{
$parts = [];
$_v = $this->fiData['v'];
$_v = $this->values;
// region (rg), battlegroup (bg) and server (sv) are passed to ArenaTeamList as miscData and handled there
// name [str]
if (!empty($_v['na']))
if ($_ = $this->modularizeString(['at.name'], $_v['na'], !empty($_v['ex']) && $_v['ex'] == 'on'))
if ($_v['na'])
if ($_ = $this->tokenizeString(['at.name'], $_v['na'], $_v['ex'] == 'on'))
$parts[] = $_;
// side [list]
if (!empty($_v['si']))
{
if ($_v['si'] == 1)
$parts[] = ['c.race', [1, 3, 4, 7, 11]];
else if ($_v['si'] == 2)
$parts[] = ['c.race', [2, 5, 6, 8, 10]];
}
if ($_v['si'] == SIDE_ALLIANCE)
$parts[] = ['c.race', ChrRace::fromMask(ChrRace::MASK_ALLIANCE)];
else if ($_v['si'] == SIDE_HORDE)
$parts[] = ['c.race', ChrRace::fromMask(ChrRace::MASK_HORDE)];
// size [int]
if (!empty($_v['sz']))
if ($_v['sz'])
$parts[] = ['at.type', $_v['sz']];
return $parts;
}
protected function cbRegionCheck(&$v)
{
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(&$v)
{
foreach (Profiler::getRealms() as $realm)
if ($realm['name'] == $v)
{
$this->parentCats[1] = Profiler::urlize($v);// directly redirect onto this server
$v = ''; // remove from filter
return true;
}
return false;
}
}
@@ -125,13 +99,14 @@ class RemoteArenaTeamList extends ArenaTeamList
);
private $members = [];
private $rankOrder = [];
public function __construct($conditions = [], $miscData = null)
public function __construct(array $conditions = [], array $miscData = [])
{
// select DB by realm
if (!$this->selectRealms($miscData))
{
trigger_error('no access to auth-db or table realmlist is empty', E_USER_WARNING);
trigger_error('RemoteArenaTeamList::__construct - cannot access any realm.', E_USER_WARNING);
return;
}
@@ -154,7 +129,7 @@ class RemoteArenaTeamList extends ArenaTeamList
foreach ($this->iterate() as $guid => &$curTpl)
{
// battlegroup
$curTpl['battlegroup'] = CFG_BATTLEGROUP;
$curTpl['battlegroup'] = Cfg::get('BATTLEGROUP');
// realm, rank
$r = explode(':', $guid);
@@ -167,7 +142,15 @@ class RemoteArenaTeamList extends ArenaTeamList
}
else
{
trigger_error('arena team "'.$curTpl['name'].'" belongs to nonexistant realm #'.$r, E_USER_WARNING);
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;
}
@@ -202,11 +185,14 @@ class RemoteArenaTeamList extends ArenaTeamList
);
// equalize subject distribution across realms
$limit = CFG_SQL_LIMIT_DEFAULT;
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);
@@ -228,8 +214,11 @@ class RemoteArenaTeamList extends ArenaTeamList
}
}
public function initializeLocalEntries()
public function initializeLocalEntries() : void
{
if (!$this->templates)
return;
$profiles = [];
// init members for tooltips
foreach ($this->members as $realmId => $teams)
@@ -238,7 +227,7 @@ class RemoteArenaTeamList extends ArenaTeamList
foreach ($teams as $team)
$gladiators = array_merge($gladiators, array_keys($team));
$profiles[$realmId] = new RemoteProfileList(array(['c.guid', $gladiators], CFG_SQL_LIMIT_NONE), ['sv' => $realmId]);
$profiles[$realmId] = new RemoteProfileList(array(['c.guid', $gladiators], Cfg::get('SQL_LIMIT_NONE')), ['sv' => $realmId]);
if (!$profiles[$realmId]->error)
$profiles[$realmId]->initializeLocalEntries();
@@ -260,7 +249,7 @@ class RemoteArenaTeamList extends ArenaTeamList
// basic arena team data
foreach (Util::createSqlBatchInsert($data) as $ins)
DB::Aowow()->query('INSERT IGNORE INTO ?_profiler_arena_team (?#) VALUES '.$ins, array_keys(reset($data)));
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(
@@ -282,15 +271,31 @@ class RemoteArenaTeamList extends ArenaTeamList
$memberData = [];
foreach ($teams as $teamId => $team)
{
$clearMembers = [];
foreach ($team as $memberId => $member)
$memberData[] = array(
{
$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 IGNORE INTO ?_profiler_arena_team_member (?#) VALUES '.$ins, array_keys(reset($memberData)));
DB::Aowow()->query('INSERT INTO ?_profiler_arena_team_member (?#) VALUES '.$ins.' ON DUPLICATE KEY UPDATE `profileId` = `profileId`', array_keys(reset($memberData)));
}
}
}
@@ -298,19 +303,51 @@ class RemoteArenaTeamList extends ArenaTeamList
class LocalArenaTeamList extends ArenaTeamList
{
protected $queryBase = 'SELECT at.*, at.id AS ARRAY_KEY FROM ?_profiler_arena_team at';
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($conditions = [], $miscData = null)
public function __construct(array $conditions = [], array $miscData = [])
{
$realms = Profiler::getRealms();
// graft realm selection from miscData onto conditions
if (isset($miscData['sv']))
$realms = array_filter($realms, fn($x) => Profiler::urlize($x['name']) == Profiler::urlize($miscData['sv']));
if (isset($miscData['rg']))
$realms = array_filter($realms, fn($x) => $x['region'] == $miscData['rg']);
if (!$realms)
{
trigger_error('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;
$realms = Profiler::getRealms();
// post processing
$members = DB::Aowow()->selectCol('SELECT *, arenaTeamId AS ARRAY_KEY, profileId AS ARRAY_KEY2 FROM ?_profiler_arena_team_member WHERE arenaTeamId IN (?a)', $this->getFoundIDs());
$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)
{
@@ -324,9 +361,9 @@ class LocalArenaTeamList extends ArenaTeamList
}
// battlegroup
$curTpl['battlegroup'] = CFG_BATTLEGROUP;
$curTpl['battlegroup'] = Cfg::get('BATTLEGROUP');
$curTpl['members'] = $members[$id];
$curTpl['members'] = array_values($members[$id]);
}
}

View File

@@ -1,5 +1,7 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
@@ -12,9 +14,9 @@ class CharClassList extends BaseType
protected $queryBase = 'SELECT c.*, id AS ARRAY_KEY FROM ?_classes c';
public function __construct($conditions = [])
public function __construct($conditions = [], array $miscData = [])
{
parent::__construct($conditions);
parent::__construct($conditions, $miscData);
foreach ($this->iterate() as $k => &$_curTpl)
$_curTpl['skills'] = explode(' ', $_curTpl['skills']);
@@ -56,7 +58,6 @@ class CharClassList extends BaseType
return $data;
}
public function addRewardsToJScript(&$ref) { }
public function renderTooltip() { }
}

View File

@@ -1,5 +1,7 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
@@ -45,7 +47,6 @@ class CharRaceList extends BaseType
return $data;
}
public function addRewardsToJScript(&$ref) { }
public function renderTooltip() { }
}

View File

@@ -1,5 +1,7 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
@@ -9,7 +11,7 @@ class CreatureList extends BaseType
use spawnHelper;
public static $type = Type::NPC;
public static $brickFile = 'creature';
public static $brickFile = 'npc';
public static $dataTable = '?_creature';
protected $queryBase = 'SELECT ct.*, ct.id AS ARRAY_KEY FROM ?_creature ct';
@@ -24,7 +26,7 @@ class CreatureList extends BaseType
's' => ['j' => ['?_spawns s ON s.type = 1 AND s.typeId = ct.id', true]]
);
public function __construct($conditions = [], $miscData = null)
public function __construct(array $conditions = [], array $miscData = [])
{
parent::__construct($conditions, $miscData);
@@ -257,238 +259,227 @@ class CreatureList extends BaseType
return $data;
}
public function getSourceData()
public function getSourceData(int $id = 0) : array
{
$data = [];
foreach ($this->iterate() as $__)
{
if ($id && $id != $this->id)
continue;
$data[$this->id] = array(
'n' => $this->getField('parentId') ? $this->getField('parent', true) : $this->getField('name', true),
't' => Type::NPC,
'ti' => $this->getField('parentId') ?: $this->id,
// 'bd' => (int)($this->curTpl['cuFlags'] & NPC_CU_INSTANCE_BOSS || ($this->curTpl['typeFlags'] & 0x4 && $this->curTpl['rank']))
// 'z' where am i spawned
// 'dd' DungeonDifficulty requires 'z'
'ti' => $this->getField('parentId') ?: $this->id
);
}
return $data;
}
public function addRewardsToJScript(&$refs) { }
}
class CreatureListFilter extends Filter
{
public $extraOpts = null;
protected $enums = array(
3 => array( 469, 1037, 1106, 529, 1012, 87, 21, 910, 609, 942, 909, 530, 69, 577, 930, 1068, 1104, 729, 369, 92, 54, 946, 67, 1052, 749,
47, 989, 1090, 1098, 978, 1011, 93, 1015, 1038, 76, 470, 349, 1031, 1077, 809, 911, 890, 970, 169, 730, 72, 70, 932, 1156, 933,
510, 1126, 1067, 1073, 509, 941, 1105, 990, 934, 935, 1094, 1119, 1124, 1064, 967, 1091, 59, 947, 81, 576, 922, 68, 1050, 1085, 889,
589, 270)
protected string $type = 'npcs';
protected array $enums = array(
3 => parent::ENUM_FACTION, // faction
6 => parent::ENUM_ZONE, // foundin
42 => parent::ENUM_FACTION, // increasesrepwith
43 => parent::ENUM_FACTION, // decreasesrepwith
38 => parent::ENUM_EVENT // relatedevent
);
// cr => [type, field, misc, extraCol]
protected $genericFilter = array( // misc (bool): _NUMERIC => useFloat; _STRING => localized; _FLAG => match Value; _BOOLEAN => stringSet
1 => [FILTER_CR_CALLBACK, 'cbHealthMana', 'healthMax', 'healthMin'], // health [num]
2 => [FILTER_CR_CALLBACK, 'cbHealthMana', 'manaMin', 'manaMax' ], // mana [num]
3 => [FILTER_CR_CALLBACK, 'cbFaction', null, null ], // faction [enum]
5 => [FILTER_CR_FLAG, 'npcflag', NPC_FLAG_REPAIRER ], // canrepair
6 => [FILTER_CR_ENUM, 's.areaId', null ], // foundin
7 => [FILTER_CR_CALLBACK, 'cbQuestRelation', 'startsQuests', 0x1 ], // startsquest [enum]
8 => [FILTER_CR_CALLBACK, 'cbQuestRelation', 'endsQuests', 0x2 ], // endsquest [enum]
9 => [FILTER_CR_BOOLEAN, 'lootId', ], // lootable
10 => [FILTER_CR_CALLBACK, 'cbRegularSkinLoot', NPC_TYPEFLAG_SPECIALLOOT ], // skinnable [yn]
11 => [FILTER_CR_BOOLEAN, 'pickpocketLootId', ], // pickpocketable
12 => [FILTER_CR_CALLBACK, 'cbMoneyDrop', null, null ], // averagemoneydropped [op] [int]
15 => [FILTER_CR_CALLBACK, 'cbSpecialSkinLoot', NPC_TYPEFLAG_HERBLOOT, null ], // gatherable [yn]
16 => [FILTER_CR_CALLBACK, 'cbSpecialSkinLoot', NPC_TYPEFLAG_MININGLOOT, null ], // minable [yn]
18 => [FILTER_CR_FLAG, 'npcflag', NPC_FLAG_AUCTIONEER ], // auctioneer
19 => [FILTER_CR_FLAG, 'npcflag', NPC_FLAG_BANKER ], // banker
20 => [FILTER_CR_FLAG, 'npcflag', NPC_FLAG_BATTLEMASTER ], // battlemaster
21 => [FILTER_CR_FLAG, 'npcflag', NPC_FLAG_FLIGHT_MASTER ], // flightmaster
22 => [FILTER_CR_FLAG, 'npcflag', NPC_FLAG_GUILD_MASTER ], // guildmaster
23 => [FILTER_CR_FLAG, 'npcflag', NPC_FLAG_INNKEEPER ], // innkeeper
24 => [FILTER_CR_FLAG, 'npcflag', NPC_FLAG_CLASS_TRAINER ], // talentunlearner
25 => [FILTER_CR_FLAG, 'npcflag', NPC_FLAG_GUILD_MASTER ], // tabardvendor
27 => [FILTER_CR_FLAG, 'npcflag', NPC_FLAG_STABLE_MASTER ], // stablemaster
28 => [FILTER_CR_FLAG, 'npcflag', NPC_FLAG_TRAINER ], // trainer
29 => [FILTER_CR_FLAG, 'npcflag', NPC_FLAG_VENDOR ], // vendor
31 => [FILTER_CR_FLAG, 'cuFlags', CUSTOM_HAS_SCREENSHOT ], // hasscreenshots
32 => [FILTER_CR_FLAG, 'cuFlags', NPC_CU_INSTANCE_BOSS ], // instanceboss
33 => [FILTER_CR_FLAG, 'cuFlags', CUSTOM_HAS_COMMENT ], // hascomments
34 => [FILTER_CR_NYI_PH, 1, null ], // usemodel [str] - displayId -> id:creatureDisplayInfo.dbc/model -> id:cratureModelData.dbc/modelPath
35 => [FILTER_CR_STRING, 'textureString' ], // useskin
37 => [FILTER_CR_NUMERIC, 'id', NUM_CAST_INT, true ], // id
38 => [FILTER_CR_CALLBACK, 'cbRelEvent', null, null ], // relatedevent [enum]
40 => [FILTER_CR_FLAG, 'cuFlags', CUSTOM_HAS_VIDEO ], // hasvideos
41 => [FILTER_CR_NYI_PH, 1, null ], // haslocation [yn] [staff]
42 => [FILTER_CR_CALLBACK, 'cbReputation', '>', null ], // increasesrepwith [enum]
43 => [FILTER_CR_CALLBACK, 'cbReputation', '<', null ], // decreasesrepwith [enum]
44 => [FILTER_CR_CALLBACK, 'cbSpecialSkinLoot', NPC_TYPEFLAG_ENGINEERLOOT, null ] // salvageable [yn]
protected array $genericFilter = array(
1 => [parent::CR_CALLBACK, 'cbHealthMana', 'healthMax', 'healthMin'], // health [num]
2 => [parent::CR_CALLBACK, 'cbHealthMana', 'manaMin', 'manaMax' ], // mana [num]
3 => [parent::CR_CALLBACK, 'cbFaction', null, null ], // faction [enum]
5 => [parent::CR_FLAG, 'npcflag', NPC_FLAG_REPAIRER ], // canrepair
6 => [parent::CR_ENUM, 's.areaId', false, true ], // foundin
7 => [parent::CR_CALLBACK, 'cbQuestRelation', 'startsQuests', 0x1 ], // startsquest [enum]
8 => [parent::CR_CALLBACK, 'cbQuestRelation', 'endsQuests', 0x2 ], // endsquest [enum]
9 => [parent::CR_BOOLEAN, 'lootId', ], // lootable
10 => [parent::CR_CALLBACK, 'cbRegularSkinLoot', NPC_TYPEFLAG_SPECIALLOOT ], // skinnable [yn]
11 => [parent::CR_BOOLEAN, 'pickpocketLootId', ], // pickpocketable
12 => [parent::CR_CALLBACK, 'cbMoneyDrop', null, null ], // averagemoneydropped [op] [int]
15 => [parent::CR_CALLBACK, 'cbSpecialSkinLoot', NPC_TYPEFLAG_SKIN_WITH_HERBALISM, null ], // gatherable [yn]
16 => [parent::CR_CALLBACK, 'cbSpecialSkinLoot', NPC_TYPEFLAG_SKIN_WITH_MINING, null ], // minable [yn]
18 => [parent::CR_FLAG, 'npcflag', NPC_FLAG_AUCTIONEER ], // auctioneer
19 => [parent::CR_FLAG, 'npcflag', NPC_FLAG_BANKER ], // banker
20 => [parent::CR_FLAG, 'npcflag', NPC_FLAG_BATTLEMASTER ], // battlemaster
21 => [parent::CR_FLAG, 'npcflag', NPC_FLAG_FLIGHT_MASTER ], // flightmaster
22 => [parent::CR_FLAG, 'npcflag', NPC_FLAG_GUILD_MASTER ], // guildmaster
23 => [parent::CR_FLAG, 'npcflag', NPC_FLAG_INNKEEPER ], // innkeeper
24 => [parent::CR_FLAG, 'npcflag', NPC_FLAG_CLASS_TRAINER ], // talentunlearner
25 => [parent::CR_FLAG, 'npcflag', NPC_FLAG_GUILD_MASTER ], // tabardvendor
27 => [parent::CR_FLAG, 'npcflag', NPC_FLAG_STABLE_MASTER ], // stablemaster
28 => [parent::CR_FLAG, 'npcflag', NPC_FLAG_TRAINER ], // trainer
29 => [parent::CR_FLAG, 'npcflag', NPC_FLAG_VENDOR ], // vendor
31 => [parent::CR_FLAG, 'cuFlags', CUSTOM_HAS_SCREENSHOT ], // hasscreenshots
32 => [parent::CR_FLAG, 'cuFlags', NPC_CU_INSTANCE_BOSS ], // instanceboss
33 => [parent::CR_FLAG, 'cuFlags', CUSTOM_HAS_COMMENT ], // hascomments
34 => [parent::CR_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]
);
// fieldId => [checkType, checkValue[, fieldIsArray]]
protected $inputFields = array(
'cr' => [FILTER_V_LIST, [[1, 3],[5, 12], 15, 16, [18, 25], [27, 29], [31, 35], 37, 38, [40, 44]], true ], // criteria ids
'crs' => [FILTER_V_LIST, [FILTER_ENUM_NONE, FILTER_ENUM_ANY, [0, 9999]], true ], // criteria operators
'crv' => [FILTER_V_REGEX, '/[\p{C}:;%\\\\]/ui', true ], // criteria values - only printable chars, no delimiter
'na' => [FILTER_V_REGEX, '/[\p{C};%\\\\]/ui', false], // name / subname - only printable chars, no delimiter
'ex' => [FILTER_V_EQUAL, 'on', false], // also match subname
'ma' => [FILTER_V_EQUAL, 1, false], // match any / all filter
'fa' => [FILTER_V_CALLBACK, 'cbPetFamily', true ], // pet family [list] - cat[0] == 1
'minle' => [FILTER_V_RANGE, [1, 99], false], // min level [int]
'maxle' => [FILTER_V_RANGE, [1, 99], false], // max level [int]
'cl' => [FILTER_V_RANGE, [0, 4], true ], // classification [list]
'ra' => [FILTER_V_LIST, [-1, 0, 1], false], // react alliance [int]
'rh' => [FILTER_V_LIST, [-1, 0, 1], false] // react horde [int]
protected array $inputFields = array(
'cr' => [parent::V_LIST, [[1, 3],[5, 12], 15, 16, [18, 25], [27, 29], [31, 35], 37, 38, [40, 44]], true ], // criteria ids
'crs' => [parent::V_LIST, [parent::ENUM_NONE, parent::ENUM_ANY, [0, 9999]], true ], // criteria operators
'crv' => [parent::V_REGEX, parent::PATTERN_CRV, true ], // criteria values - only printable chars, no delimiter
'na' => [parent::V_REGEX, parent::PATTERN_NAME, false], // name / subname - only printable chars, no delimiter
'ex' => [parent::V_EQUAL, 'on', false], // also match subname
'ma' => [parent::V_EQUAL, 1, false], // match any / all filter
'fa' => [parent::V_CALLBACK, 'cbPetFamily', true ], // pet family [list] - cat[0] == 1
'minle' => [parent::V_RANGE, [1, 99], false], // min level [int]
'maxle' => [parent::V_RANGE, [1, 99], false], // max level [int]
'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]
);
protected function createSQLForCriterium(&$cr)
{
if (in_array($cr[0], array_keys($this->genericFilter)))
if ($genCr = $this->genericCriterion($cr))
return $genCr;
public array $extraOpts = [];
unset($cr);
$this->error = true;
return [1];
}
protected function createSQLForValues()
protected function createSQLForValues() : array
{
$parts = [];
$_v = &$this->fiData['v'];
$_v = &$this->values;
// name [str]
if (isset($_v['na']))
if ($_v['na'])
{
$_ = [];
if (isset($_v['ex']) && $_v['ex'] == 'on')
$_ = $this->modularizeString(['name_loc'.User::$localeId, 'subname_loc'.User::$localeId]);
if ($_v['ex'] == 'on')
$_ = $this->tokenizeString(['name_loc'.Lang::getLocale()->value, 'subname_loc'.Lang::getLocale()->value]);
else
$_ = $this->modularizeString(['name_loc'.User::$localeId]);
$_ = $this->tokenizeString(['name_loc'.Lang::getLocale()->value]);
if ($_)
$parts[] = $_;
}
// pet family [list]
if (isset($_v['fa']))
if ($_v['fa'])
$parts[] = ['family', $_v['fa']];
// creatureLevel min [int]
if (isset($_v['minle']))
if ($_v['minle'])
$parts[] = ['minLevel', $_v['minle'], '>='];
// creatureLevel max [int]
if (isset($_v['maxle']))
if ($_v['maxle'])
$parts[] = ['maxLevel', $_v['maxle'], '<='];
// classification [list]
if (isset($_v['cl']))
if ($_v['cl'])
$parts[] = ['rank', $_v['cl']];
// react Alliance [int]
if (isset($_v['ra']))
if ($_v['ra'])
$parts[] = ['ft.A', $_v['ra']];
// react Horde [int]
if (isset($_v['rh']))
if ($_v['rh'])
$parts[] = ['ft.H', $_v['rh']];
return $parts;
}
protected function cbPetFamily(&$val)
protected function cbPetFamily(string &$val) : bool
{
if (!$this->parentCats || $this->parentCats[0] != 1)
return false;
if (!Util::checkNumeric($val, NUM_REQ_INT))
if (!Util::checkNumeric($val, NUM_CAST_INT))
return false;
$type = FILTER_V_LIST;
$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($cr)
protected function cbRelEvent(int $cr, int $crs, string $crv) : ?array
{
if (!Util::checkNumeric($cr[1], NUM_REQ_INT))
return false;
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];
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];
return [0];
}
else if ($cr[1] == FILTER_ENUM_NONE)
else if ($crs == parent::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, '!'];
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 ($cr[1])
else if (in_array($crs, $this->enums[$cr]))
{
$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];
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 false;
return null;
}
protected function cbMoneyDrop($cr)
protected function cbMoneyDrop(int $cr, int $crs, string $crv) : ?array
{
if (!Util::checkNumeric($cr[2], NUM_CAST_INT) || !$this->int2Op($cr[1]))
return false;
if (!Util::checkNumeric($crv, NUM_CAST_INT) || !$this->int2Op($crs))
return null;
return ['AND', ['((minGold + maxGold) / 2)', $cr[2], $cr[1]]];
return ['AND', ['((minGold + maxGold) / 2)', $crv, $crs]];
}
protected function cbQuestRelation($cr, $field, $val)
protected function cbQuestRelation(int $cr, int $crs, string $crv, $field, $val) : ?array
{
switch ($cr[1])
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', RACE_MASK_HORDE, '&'], 0], ['qt.reqRaceMask', RACE_MASK_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', RACE_MASK_ALLIANCE, '&'], 0], ['qt.reqRaceMask', RACE_MASK_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', RACE_MASK_ALLIANCE, '&'], ['qt.reqRaceMask', RACE_MASK_HORDE, '&']], ['qt.reqRaceMask', 0]]];
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 false;
return null;
}
protected function cbHealthMana($cr, $minField, $maxField)
protected function cbHealthMana(int $cr, int $crs, string $crv, $minField, $maxField) : ?array
{
if (!Util::checkNumeric($cr[2], NUM_CAST_INT) || !$this->int2Op($cr[1]))
return false;
if (!Util::checkNumeric($crv, NUM_CAST_INT) || !$this->int2Op($crs))
return null;
// remap OP for this special case
switch ($cr[1])
switch ($crs)
{
case '=': // min > max is totally possible
$this->extraOpts['ct']['h'][] = $minField.' = '.$maxField.' AND '.$minField.' = '.$cr[2];
$this->extraOpts['ct']['h'][] = $minField.' = '.$maxField.' AND '.$minField.' = '.$crv;
break;
case '>':
case '>=':
case '<':
case '<=':
$this->extraOpts['ct']['h'][] = 'IF('.$minField.' > '.$maxField.', '.$maxField.', '.$minField.') '.$cr[1].' '.$cr[2];
$this->extraOpts['ct']['h'][] = 'IF('.$minField.' > '.$maxField.', '.$maxField.', '.$minField.') '.$crs.' '.$crv;
break;
}
@@ -496,54 +487,53 @@ class CreatureListFilter extends Filter
return [1]; // always true, use post-filter
}
protected function cbSpecialSkinLoot($cr, $typeFlag)
protected function cbSpecialSkinLoot(int $cr, int $crs, string $crv, $typeFlag) : ?array
{
if (!$this->int2Bool($cr[1]))
return false;
if (!$this->int2Bool($crs))
return null;
if ($cr[1])
if ($crs)
return ['AND', ['skinLootId', 0, '>'], ['typeFlags', $typeFlag, '&']];
else
return ['OR', ['skinLootId', 0], [['typeFlags', $typeFlag, '&'], 0]];
}
protected function cbRegularSkinLoot($cr, $typeFlag)
protected function cbRegularSkinLoot(int $cr, int $crs, string $crv, $typeFlag) : ?array
{
if (!$this->int2Bool($cr[1]))
return false;
if (!$this->int2Bool($crs))
return null;
if ($cr[1])
if ($crs)
return ['AND', ['skinLootId', 0, '>'], [['typeFlags', $typeFlag, '&'], 0]];
else
return ['OR', ['skinLootId', 0], ['typeFlags', $typeFlag, '&']];
}
protected function cbReputation($cr, $op)
protected function cbReputation(int $cr, int $crs, string $crv, $op) : ?array
{
if (!in_array($cr[1], $this->enums[3])) // reuse
return false;
if (!in_array($crs, $this->enums[$cr]))
return null;
if ($_ = DB::Aowow()->selectRow('SELECT * FROM ?_factions WHERE id = ?d', $cr[1]))
$this->formData['reputationCols'][] = [$cr[1], Util::localizedString($_, 'name')];
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)', $cr[1], $cr[1]))
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($cr)
protected function cbFaction(int $cr, int $crs, string $crv) : ?array
{
if (!Util::checkNumeric($cr[1], NUM_REQ_INT))
return false;
if (!in_array($cr[1], $this->enums[$cr[0]]))
return false;
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', $cr[1]], ['id', $cr[1]]));
$facs = new FactionList(array('OR', ['parentFactionId', $crs], ['id', $crs]));
foreach ($facs->iterate() as $__)
$facTpls = array_merge($facTpls, $facs->getField('templateIds'));

View File

@@ -1,5 +1,7 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
@@ -16,13 +18,12 @@ class CurrencyList extends BaseType
'ic' => ['j' => ['?_icons ic ON ic.id = c.iconId', true], 's' => ', ic.name AS iconString']
);
public function __construct($conditions = [])
public function __construct(array $conditions = [], array $miscData = [])
{
parent::__construct($conditions);
parent::__construct($conditions, $miscData);
foreach ($this->iterate() as &$_curTpl)
if (!$_curTpl['iconString'])
$_curTpl['iconString'] = 'inv_misc_questionmark';
$_curTpl['iconString'] = $_curTpl['iconString'] ?: DEFAULT_ICON;
}
@@ -51,9 +52,9 @@ class CurrencyList extends BaseType
{
// todo (low): find out, why i did this in the first place
if ($this->id == 104) // in case of honor commit sebbuku
$icon = ['inv_bannerpvp_02', 'inv_bannerpvp_01']; // ['alliance', 'horde'];
$icon = ['pvp-currency-alliance', 'pvp-currency-horde'];
else if ($this->id == 103) // also arena-icon diffs from item-icon
$icon = ['money_arena', 'money_arena'];
$icon = ['pvp-arenapoints-icon', 'pvp-arenapoints-icon'];
else
$icon = [$this->curTpl['iconString'], $this->curTpl['iconString']];

View File

@@ -1,5 +1,7 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
@@ -12,9 +14,9 @@ class EmoteList extends BaseType
protected $queryBase = 'SELECT e.*, e.id AS ARRAY_KEY FROM ?_emotes e';
public function __construct($conditions = [])
public function __construct(array $conditions = [], array $miscData = [])
{
parent::__construct($conditions);
parent::__construct($conditions, $miscData);
// post processing
foreach ($this->iterate() as &$curTpl)
@@ -33,7 +35,7 @@ class EmoteList extends BaseType
$data[$this->id] = array(
'id' => $this->curTpl['id'],
'name' => $this->curTpl['cmd'],
'preview' => $this->getField('self', true) ?: ($this->getField('noTarget', true) ?: $this->getField('target', true))
'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

View File

@@ -1,5 +1,7 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
@@ -8,23 +10,25 @@ class EnchantmentList extends BaseType
{
use listviewHelper;
public static $type = Type::ENCHANTMENT;
public static $brickFile = 'enchantment';
public static $dataTable = '?_itemenchantment';
public static $type = Type::ENCHANTMENT;
public static $brickFile = 'enchantment';
public static $dataTable = '?_itemenchantment';
private $jsonStats = [];
private $relSpells = [];
private $triggerIds = [];
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
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($conditions = [])
public function __construct(array $conditions = [], array $miscData = [])
{
parent::__construct($conditions);
parent::__construct($conditions, $miscData);
$relSpells = [];
// post processing
foreach ($this->iterate() as &$curTpl)
@@ -35,45 +39,40 @@ class EnchantmentList extends BaseType
if ($curTpl['object'.$i] <= 0)
continue;
switch ($curTpl['type'.$i])
switch ($curTpl['type'.$i]) // SPELL_TRIGGER_* just reused for wording
{
case 1:
case ENCHANTMENT_TYPE_COMBAT_SPELL:
$proc = -$this->getField('ppmRate') ?: ($this->getField('procChance') ?: $this->getField('amount'.$i));
$curTpl['spells'][$i] = [$curTpl['object'.$i], 2, $curTpl['charges'], $proc];
$this->relSpells[] = $curTpl['object'.$i];
$curTpl['spells'][$i] = [$curTpl['object'.$i], SPELL_TRIGGER_HIT, $curTpl['charges'], $proc];
$relSpells[] = $curTpl['object'.$i];
break;
case 3:
$curTpl['spells'][$i] = [$curTpl['object'.$i], 1, $curTpl['charges'], 0];
$this->relSpells[] = $curTpl['object'.$i];
case ENCHANTMENT_TYPE_EQUIP_SPELL:
$curTpl['spells'][$i] = [$curTpl['object'.$i], SPELL_TRIGGER_EQUIP, $curTpl['charges'], 0];
$relSpells[] = $curTpl['object'.$i];
break;
case 7:
$curTpl['spells'][$i] = [$curTpl['object'.$i], 0, $curTpl['charges'], 0];
$this->relSpells[] = $curTpl['object'.$i];
case ENCHANTMENT_TYPE_USE_SPELL:
$curTpl['spells'][$i] = [$curTpl['object'.$i], SPELL_TRIGGER_USE, $curTpl['charges'], 0];
$relSpells[] = $curTpl['object'.$i];
break;
}
}
// floats are fetched as string from db :<
$curTpl['dmg'] = floatVal($curTpl['dmg']);
$curTpl['dps'] = floatVal($curTpl['dps']);
// remove zero-stats
foreach (Game::$itemMods as $str)
if ($curTpl[$str] == 0) // empty(0.0f) => true .. yeah, sure
unset($curTpl[$str]);
if ($curTpl['dps'] == 0)
unset($curTpl['dps']);
// 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 ($this->relSpells)
$this->relSpells = new SpellList(array(['id', $this->relSpells]));
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 );
$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
@@ -99,107 +98,39 @@ class EnchantmentList extends BaseType
if ($this->curTpl['requiredLevel'] > 0)
$data[$this->id]['reqlevel'] = $this->curTpl['requiredLevel'];
foreach ($this->curTpl['spells'] as $s)
foreach ($this->curTpl['spells'] as [$spellId, $trigger, $charges, $procChance])
{
// enchant is procing or onUse
if ($s[1] == 2 || $s[1] == 0)
$data[$this->id]['spells'][$s[0]] = $s[2];
// spell is procing
else if ($this->relSpells && $this->relSpells->getEntry($s[0]) && ($_ = $this->relSpells->canTriggerSpell()))
$trgSpell = 0;
if ($this->relSpells && $this->relSpells->getEntry($spellId) && ($_ = $this->relSpells->canTriggerSpell()))
{
foreach ($_ as $idx)
{
$this->triggerIds[] = $this->relSpells->getField('effect'.$idx.'TriggerSpell');
$data[$this->id]['spells'][$this->relSpells->getField('effect'.$idx.'TriggerSpell')] = $s[2];
if ($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->getStatGain());
Util::arraySumByKey($data[$this->id], $this->jsonStats[$this->id]->toJson());
}
return $data;
}
public function getStatGain($addScalingKeys = false)
public function getStatGainForCurrent() : array
{
$data = [];
foreach (Game::$itemMods as $str)
if (isset($this->curTpl[$str]))
$data[$str] = $this->curTpl[$str];
if (isset($this->curTpl['dps']))
$data['dps'] = $this->curTpl['dps'];
// scaling enchantments are saved as 0 to item_stats, thus return empty
if ($addScalingKeys)
{
$spellStats = [];
if ($this->relSpells)
$spellStats = $this->relSpells->getStatGain();
for ($h = 1; $h <= 3; $h++)
{
$obj = (int)$this->curTpl['object'.$h];
switch ($this->curTpl['type'.$h])
{
case 3: // TYPE_EQUIP_SPELL Spells from ObjectX (use of amountX?)
if (!empty($spellStats[$obj]))
foreach ($spellStats[$obj] as $mod => $_)
if ($str = Game::$itemMods[$mod])
Util::arraySumByKey($data, [$str => 0]);
$obj = null;
break;
case 4: // TYPE_RESISTANCE +AmountX resistance for ObjectX School
switch ($obj)
{
case 0: // Physical
$obj = ITEM_MOD_ARMOR;
break;
case 1: // Holy
$obj = ITEM_MOD_HOLY_RESISTANCE;
break;
case 2: // Fire
$obj = ITEM_MOD_FIRE_RESISTANCE;
break;
case 3: // Nature
$obj = ITEM_MOD_NATURE_RESISTANCE;
break;
case 4: // Frost
$obj = ITEM_MOD_FROST_RESISTANCE;
break;
case 5: // Shadow
$obj = ITEM_MOD_SHADOW_RESISTANCE;
break;
case 6: // Arcane
$obj = ITEM_MOD_ARCANE_RESISTANCE;
break;
default:
$obj = null;
}
break;
case 5: // TYPE_STAT +AmountX for Statistic by type of ObjectX
if ($obj < 2) // [mana, health] are on [0, 1] respectively and are expected on [1, 2] ..
$obj++; // 0 is weaponDmg .. ehh .. i messed up somewhere
break; // stats are directly assigned below
default: // TYPE_NONE dnd stuff; skip assignment below
$obj = null;
}
if ($obj !== null)
if ($str = Game::$itemMods[$obj]) // check if we use these mods
Util::arraySumByKey($data, [$str => 0]);
}
}
return $data;
return $this->jsonStats[$this->id]->toJson();
}
public function getRelSpell($id)
@@ -237,107 +168,94 @@ class EnchantmentList extends BaseType
class EnchantmentListFilter extends Filter
{
protected $enums = array(
3 => array( // requiresprof
null, 171, 164, 185, 333, 202, 129, 755, 165, 186, 197, true, false, 356, 182, 773
)
protected string $type = 'enchantments';
protected array $enums = array(
3 => parent::ENUM_PROFESSION // requiresprof
);
protected $genericFilter = array( // misc (bool): _NUMERIC => useFloat; _STRING => localized; _FLAG => match Value; _BOOLEAN => stringSet
2 => [FILTER_CR_NUMERIC, 'id', NUM_CAST_INT, true], // id
3 => [FILTER_CR_ENUM, 'skillLine' ], // requiresprof
4 => [FILTER_CR_NUMERIC, 'skillLevel', NUM_CAST_INT ], // reqskillrank
5 => [FILTER_CR_BOOLEAN, 'conditionId' ], // hascondition
10 => [FILTER_CR_FLAG, 'cuFlags', CUSTOM_HAS_COMMENT ], // hascomments
11 => [FILTER_CR_FLAG, 'cuFlags', CUSTOM_HAS_SCREENSHOT ], // hasscreenshots
12 => [FILTER_CR_FLAG, 'cuFlags', CUSTOM_HAS_VIDEO ], // hasvideos
20 => [FILTER_CR_NUMERIC, 'is.str', NUM_CAST_INT, true], // str
21 => [FILTER_CR_NUMERIC, 'is.agi', NUM_CAST_INT, true], // agi
22 => [FILTER_CR_NUMERIC, 'is.sta', NUM_CAST_INT, true], // sta
23 => [FILTER_CR_NUMERIC, 'is.int', NUM_CAST_INT, true], // int
24 => [FILTER_CR_NUMERIC, 'is.spi', NUM_CAST_INT, true], // spi
25 => [FILTER_CR_NUMERIC, 'is.arcres', NUM_CAST_INT, true], // arcres
26 => [FILTER_CR_NUMERIC, 'is.firres', NUM_CAST_INT, true], // firres
27 => [FILTER_CR_NUMERIC, 'is.natres', NUM_CAST_INT, true], // natres
28 => [FILTER_CR_NUMERIC, 'is.frores', NUM_CAST_INT, true], // frores
29 => [FILTER_CR_NUMERIC, 'is.shares', NUM_CAST_INT, true], // shares
30 => [FILTER_CR_NUMERIC, 'is.holres', NUM_CAST_INT, true], // holres
32 => [FILTER_CR_NUMERIC, 'is.dps', NUM_CAST_FLOAT, true], // dps
34 => [FILTER_CR_NUMERIC, 'is.dmg', NUM_CAST_FLOAT, true], // dmg
37 => [FILTER_CR_NUMERIC, 'is.mleatkpwr', NUM_CAST_INT, true], // mleatkpwr
38 => [FILTER_CR_NUMERIC, 'is.rgdatkpwr', NUM_CAST_INT, true], // rgdatkpwr
39 => [FILTER_CR_NUMERIC, 'is.rgdhitrtng', NUM_CAST_INT, true], // rgdhitrtng
40 => [FILTER_CR_NUMERIC, 'is.rgdcritstrkrtng', NUM_CAST_INT, true], // rgdcritstrkrtng
41 => [FILTER_CR_NUMERIC, 'is.armor' , NUM_CAST_INT, true], // armor
42 => [FILTER_CR_NUMERIC, 'is.defrtng', NUM_CAST_INT, true], // defrtng
43 => [FILTER_CR_NUMERIC, 'is.block', NUM_CAST_INT, true], // block
44 => [FILTER_CR_NUMERIC, 'is.blockrtng', NUM_CAST_INT, true], // blockrtng
45 => [FILTER_CR_NUMERIC, 'is.dodgertng', NUM_CAST_INT, true], // dodgertng
46 => [FILTER_CR_NUMERIC, 'is.parryrtng', NUM_CAST_INT, true], // parryrtng
48 => [FILTER_CR_NUMERIC, 'is.splhitrtng', NUM_CAST_INT, true], // splhitrtng
49 => [FILTER_CR_NUMERIC, 'is.splcritstrkrtng', NUM_CAST_INT, true], // splcritstrkrtng
50 => [FILTER_CR_NUMERIC, 'is.splheal', NUM_CAST_INT, true], // splheal
51 => [FILTER_CR_NUMERIC, 'is.spldmg', NUM_CAST_INT, true], // spldmg
52 => [FILTER_CR_NUMERIC, 'is.arcsplpwr', NUM_CAST_INT, true], // arcsplpwr
53 => [FILTER_CR_NUMERIC, 'is.firsplpwr', NUM_CAST_INT, true], // firsplpwr
54 => [FILTER_CR_NUMERIC, 'is.frosplpwr', NUM_CAST_INT, true], // frosplpwr
55 => [FILTER_CR_NUMERIC, 'is.holsplpwr', NUM_CAST_INT, true], // holsplpwr
56 => [FILTER_CR_NUMERIC, 'is.natsplpwr', NUM_CAST_INT, true], // natsplpwr
57 => [FILTER_CR_NUMERIC, 'is.shasplpwr', NUM_CAST_INT, true], // shasplpwr
60 => [FILTER_CR_NUMERIC, 'is.healthrgn', NUM_CAST_INT, true], // healthrgn
61 => [FILTER_CR_NUMERIC, 'is.manargn', NUM_CAST_INT, true], // manargn
77 => [FILTER_CR_NUMERIC, 'is.atkpwr', NUM_CAST_INT, true], // atkpwr
78 => [FILTER_CR_NUMERIC, 'is.mlehastertng', NUM_CAST_INT, true], // mlehastertng
79 => [FILTER_CR_NUMERIC, 'is.resirtng', NUM_CAST_INT, true], // resirtng
84 => [FILTER_CR_NUMERIC, 'is.mlecritstrkrtng', NUM_CAST_INT, true], // mlecritstrkrtng
94 => [FILTER_CR_NUMERIC, 'is.splpen', NUM_CAST_INT, true], // splpen
95 => [FILTER_CR_NUMERIC, 'is.mlehitrtng', NUM_CAST_INT, true], // mlehitrtng
96 => [FILTER_CR_NUMERIC, 'is.critstrkrtng', NUM_CAST_INT, true], // critstrkrtng
97 => [FILTER_CR_NUMERIC, 'is.feratkpwr', NUM_CAST_INT, true], // feratkpwr
101 => [FILTER_CR_NUMERIC, 'is.rgdhastertng', NUM_CAST_INT, true], // rgdhastertng
102 => [FILTER_CR_NUMERIC, 'is.splhastertng', NUM_CAST_INT, true], // splhastertng
103 => [FILTER_CR_NUMERIC, 'is.hastertng', NUM_CAST_INT, true], // hastertng
114 => [FILTER_CR_NUMERIC, 'is.armorpenrtng', NUM_CAST_INT, true], // armorpenrtng
115 => [FILTER_CR_NUMERIC, 'is.health', NUM_CAST_INT, true], // health
116 => [FILTER_CR_NUMERIC, 'is.mana', NUM_CAST_INT, true], // mana
117 => [FILTER_CR_NUMERIC, 'is.exprtng', NUM_CAST_INT, true], // exprtng
119 => [FILTER_CR_NUMERIC, 'is.hitrtng', NUM_CAST_INT, true], // hitrtng
123 => [FILTER_CR_NUMERIC, 'is.splpwr', NUM_CAST_INT, true] // splpwr
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
);
// fieldId => [checkType, checkValue[, fieldIsArray]]
protected $inputFields = array(
'cr' => [FILTER_V_RANGE, [2, 123], true ], // criteria ids
'crs' => [FILTER_V_RANGE, [1, 15], true ], // criteria operators
'crv' => [FILTER_V_RANGE, [0, 99999], true ], // criteria values - only numerals
'na' => [FILTER_V_REGEX, '/[\p{C};%\\\\]/ui', false], // name - only printable chars, no delimiter
'ma' => [FILTER_V_EQUAL, 1, false], // match any / all filter
'ty' => [FILTER_V_RANGE, [1, 8], true ] // types
protected 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 createSQLForCriterium(&$cr)
{
if (in_array($cr[0], array_keys($this->genericFilter)))
if ($genCr = $this->genericCriterion($cr))
return $genCr;
unset($cr);
$this->error = true;
return [1];
}
protected function createSQLForValues()
protected function createSQLForValues() : array
{
$parts = [];
$_v = &$this->fiData['v'];
$_v = &$this->values;
//string
if (isset($_v['na']))
if ($_ = $this->modularizeString(['name_loc'.User::$localeId]))
if ($_v['na'])
if ($_ = $this->tokenizeString(['name_loc'.Lang::getLocale()->value]))
$parts[] = $_;
// type
if (isset($_v['ty']))
if ($_v['ty'])
$parts[] = ['OR', ['type1', $_v['ty']], ['type2', $_v['ty']], ['type3', $_v['ty']]];
return $parts;

View File

@@ -1,5 +1,7 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
@@ -17,9 +19,9 @@ class FactionList extends BaseType
'ft' => ['j' => '?_factiontemplate ft ON ft.factionId = f.id']
);
public function __construct($conditions = [])
public function __construct(array $conditions = [], array $miscData = [])
{
parent::__construct($conditions);
parent::__construct($conditions, $miscData);
if ($this->error)
return;

View File

@@ -1,5 +1,7 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
@@ -21,7 +23,7 @@ class GameObjectList extends BaseType
's' => ['j' => '?_spawns s ON s.type = 2 AND s.typeId = o.id']
);
public function __construct($conditions = [], $miscData = null)
public function __construct(array $conditions = [], array $miscData = [])
{
parent::__construct($conditions, $miscData);
@@ -73,7 +75,7 @@ class GameObjectList extends BaseType
{
$data[$this->id] = array(
'id' => $this->id,
'name' => $this->getField('name', true),
'name' => Lang::unescapeUISequences($this->getField('name', true), Lang::FMT_RAW),
'type' => $this->curTpl['typeCat'],
'location' => $this->getSpawns(SPAWNINFO_ZONES)
);
@@ -95,7 +97,7 @@ class GameObjectList extends BaseType
return array();
$x = '<table>';
$x .= '<tr><td><b class="q">'.$this->getField('name', true).'</b></td></tr>';
$x .= '<tr><td><b class="q">'.Lang::unescapeUISequences($this->getField('name', true), Lang::FMT_HTML).'</b></td></tr>';
if ($this->curTpl['typeCat'])
if ($_ = Lang::gameObject('type', $this->curTpl['typeCat']))
$x .= '<tr><td>'.$_.'</td></tr>';
@@ -115,23 +117,24 @@ class GameObjectList extends BaseType
$data = [];
foreach ($this->iterate() as $__)
$data[Type::OBJECT][$this->id] = ['name' => $this->getField('name', true)];
$data[Type::OBJECT][$this->id] = ['name' => Lang::unescapeUISequences($this->getField('name', true), Lang::FMT_RAW)];
return $data;
}
public function getSourceData()
public function getSourceData(int $id = 0) : array
{
$data = [];
foreach ($this->iterate() as $__)
{
if ($id && $id != $this->id)
continue;
$data[$this->id] = array(
'n' => $this->getField('name', true),
't' => Type::OBJECT,
'ti' => $this->id
// 'bd' => bossdrop
// 'dd' => dungeondifficulty
);
}
@@ -142,118 +145,107 @@ class GameObjectList extends BaseType
class GameObjectListFilter extends Filter
{
public $extraOpts = [];
protected $enums = array(
50 => array(
null, 1, 2, 3, 4,
663 => 663,
883 => 883,
FILTER_ENUM_ANY => true,
FILTER_ENUM_NONE => false
)
protected string $type = 'objects';
protected array $enums = array(
1 => parent::ENUM_ZONE,
16 => parent::ENUM_EVENT,
50 => [1, 2, 3, 4, 663, 883]
);
protected $genericFilter = array(
1 => [FILTER_CR_ENUM, 's.areaId', null ], // foundin
2 => [FILTER_CR_CALLBACK, 'cbQuestRelation', 'startsQuests', 0x1 ], // startsquest [side]
3 => [FILTER_CR_CALLBACK, 'cbQuestRelation', 'endsQuests', 0x2 ], // endsquest [side]
4 => [FILTER_CR_CALLBACK, 'cbOpenable', null, null], // openable [yn]
5 => [FILTER_CR_NYI_PH, null, null ], // averagemoneycontained [op] [int] - GOs don't contain money, match against 0
7 => [FILTER_CR_NUMERIC, 'reqSkill', NUM_CAST_INT ], // requiredskilllevel
11 => [FILTER_CR_FLAG, 'cuFlags', CUSTOM_HAS_SCREENSHOT ], // hasscreenshots
13 => [FILTER_CR_FLAG, 'cuFlags', CUSTOM_HAS_COMMENT ], // hascomments
15 => [FILTER_CR_NUMERIC, 'id', NUM_CAST_INT ], // id
16 => [FILTER_CR_CALLBACK, 'cbRelEvent', null, null], // relatedevent (ignore removed by event)
18 => [FILTER_CR_FLAG, 'cuFlags', CUSTOM_HAS_VIDEO ], // hasvideos
50 => [FILTER_CR_ENUM, 'spellFocusId', null, ], // SpellFocus
protected array $genericFilter = array(
1 => [parent::CR_ENUM, 's.areaId', false, true], // foundin
2 => [parent::CR_CALLBACK, 'cbQuestRelation', 'startsQuests', 0x1 ], // startsquest [side]
3 => [parent::CR_CALLBACK, 'cbQuestRelation', 'endsQuests', 0x2 ], // endsquest [side]
4 => [parent::CR_CALLBACK, 'cbOpenable', null, null], // openable [yn]
5 => [parent::CR_NYI_PH, null, 0 ], // averagemoneycontained [op] [int] - GOs don't contain money, match against 0
7 => [parent::CR_NUMERIC, 'reqSkill', NUM_CAST_INT ], // requiredskilllevel
11 => [parent::CR_FLAG, 'cuFlags', CUSTOM_HAS_SCREENSHOT ], // hasscreenshots
13 => [parent::CR_FLAG, 'cuFlags', CUSTOM_HAS_COMMENT ], // hascomments
15 => [parent::CR_NUMERIC, 'id', NUM_CAST_INT ], // id
16 => [parent::CR_CALLBACK, 'cbRelEvent', null, null], // relatedevent (ignore removed by event)
18 => [parent::CR_FLAG, 'cuFlags', CUSTOM_HAS_VIDEO ], // hasvideos
50 => [parent::CR_ENUM, 'spellFocusId', true, true], // spellfocus
);
// fieldId => [checkType, checkValue[, fieldIsArray]]
protected $inputFields = array(
'cr' => [FILTER_V_LIST, [[1, 5], 7, 11, 13, 15, 16, 18, 50], true ], // criteria ids
'crs' => [FILTER_V_LIST, [FILTER_ENUM_NONE, FILTER_ENUM_ANY, [0, 5000]], true ], // criteria operators
'crv' => [FILTER_V_RANGE, [0, 99999], true ], // criteria values - only numeric input values expected
'na' => [FILTER_V_REGEX, '/[\p{C};%\\\\]/ui', false], // name - only printable chars, no delimiter
'ma' => [FILTER_V_EQUAL, 1, false] // match any / all filter
protected array $inputFields = array(
'cr' => [parent::V_LIST, [[1, 5], 7, 11, 13, 15, 16, 18, 50], true ], // criteria ids
'crs' => [parent::V_LIST, [parent::ENUM_NONE, parent::ENUM_ANY, [0, 5000]], true ], // criteria operators
'crv' => [parent::V_REGEX, parent::PATTERN_INT, true ], // criteria values - only numeric input values expected
'na' => [parent::V_REGEX, parent::PATTERN_NAME, false], // name - only printable chars, no delimiter
'ma' => [parent::V_EQUAL, 1, false] // match any / all filter
);
protected function createSQLForCriterium(&$cr)
{
if (in_array($cr[0], array_keys($this->genericFilter)))
if ($genCR = $this->genericCriterion($cr))
return $genCR;
public array $extraOpts = [];
unset($cr);
$this->error = true;
return [1];
}
protected function createSQLForValues()
protected function createSQLForValues() : array
{
$parts = [];
$_v = $this->fiData['v'];
$_v = $this->values;
// name
if (isset($_v['na']))
if ($_ = $this->modularizeString(['name_loc'.User::$localeId]))
if ($_v['na'])
if ($_ = $this->tokenizeString(['name_loc'.Lang::getLocale()->value]))
$parts[] = $_;
return $parts;
}
protected function cbOpenable($cr)
protected function cbOpenable(int $cr, int $crs, string $crv) : ?array
{
if ($this->int2Bool($cr[1]))
return $cr[1] ? ['OR', ['flags', 0x2, '&'], ['type', 3]] : ['AND', [['flags', 0x2, '&'], 0], ['type', 3, '!']];
if ($this->int2Bool($crs))
return $crs ? ['OR', ['flags', 0x2, '&'], ['type', 3]] : ['AND', [['flags', 0x2, '&'], 0], ['type', 3, '!']];
return false;
return null;
}
protected function cbQuestRelation($cr, $field, $value)
protected function cbQuestRelation(int $cr, int $crs, string $crv, $field, $value) : ?array
{
switch ($cr[1])
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', RACE_MASK_HORDE, '&'], 0], ['qt.reqRaceMask', RACE_MASK_ALLIANCE, '&']];
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', RACE_MASK_ALLIANCE, '&'], 0], ['qt.reqRaceMask', RACE_MASK_HORDE, '&']];
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', RACE_MASK_ALLIANCE, '&'], ['qt.reqRaceMask', RACE_MASK_HORDE, '&']], ['qt.reqRaceMask', 0]]];
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 false;
return null;
}
protected function cbRelEvent($cr)
protected function cbRelEvent(int $cr, int $crs, string $crv) : ?array
{
if (!Util::checkNumeric($cr[1], NUM_REQ_INT))
return false;;
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];
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];
return [0];
}
else if ($cr[1] == FILTER_ENUM_NONE)
else if ($crs == parent::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, '!'];
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 ($cr[1])
else if (in_array($crs, $this->enums[$cr]))
{
$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];
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 false;
return null;
}
}

View File

@@ -1,5 +1,7 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
@@ -16,9 +18,10 @@ class GuideList extends BaseType
GUIDE_STATUS_ARCHIVED => '#FFD100'
);
public static $type = Type::GUIDE;
public static $brickFile = 'guide';
public static $dataTable = '?_guides';
public static $type = Type::GUIDE;
public static $brickFile = 'guide';
public static $dataTable = '?_guides';
public static $contribute = CONTRIBUTE_CO;
private $article = [];
private $jsGlobals = [];
@@ -26,13 +29,13 @@ class GuideList extends BaseType
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.displayName, "") 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`']
'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($conditions = [])
public function __construct(array $conditions = [], array $miscData = [])
{
parent::__construct($conditions);
parent::__construct($conditions, $miscData);
if ($this->error)
return;
@@ -69,7 +72,7 @@ class GuideList extends BaseType
$this->article[$a['rev']] = $a['article'];
if ($this->article[$a['rev']])
{
(new Markup($this->article[$a['rev']]))->parseGlobalsFromText($this->jsGlobals);
Markup::parseTags($this->article[$a['rev']], $this->jsGlobals);
return $this->article[$a['rev']];
}
else
@@ -92,7 +95,7 @@ class GuideList extends BaseType
'description' => $this->getField('description'),
'sticky' => !!($this->getField('cuFlags') & CC_FLAG_STICKY),
'nvotes' => $this->getField('nvotes'),
'url' => '/?guide=' . ($this->getField('url') ?: $this->id),
'url' => '?guide=' . ($this->getField('url') ?: $this->id),
'status' => $this->getField('status'),
'author' => $this->getField('author'),
'authorroles' => $this->getField('roles'),
@@ -104,7 +107,11 @@ class GuideList extends BaseType
'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;
@@ -139,23 +146,24 @@ class GuideList extends BaseType
if ($this->getField('classId') && $this->getField('category') == 1)
{
$c = $this->getField('classId');
if (($s = $this->getField('specId')) > -1)
if ($c = $this->getField('classId'))
{
$i = Game::$specIconStrings[$c][$s];
$n = Lang::game('classSpecs', $c, $s);
}
else
{
$i = 'class_'.Game::$classFileStrings[$c];
$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>';
$specStr = '&nbsp;&nbsp;&nbsp;&nbsp;<span class="icontiny c'.$c.'" style="background-image: url('.STATIC_URL.'/images/wow/icons/tiny/'.$i.'.gif)">'.$n.'</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::guide('guide').'</td><th>'.Lang::guide('byAuthor', [$this->getField('author')]).'</th></tr></table>';
$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>';

View File

@@ -1,5 +1,7 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
@@ -8,6 +10,8 @@ class GuildList extends BaseType
{
use profilerHelper, listviewHelper;
public static $contribute = CONTRIBUTE_NONE;
public function getListviewData()
{
$this->getGuildScores();
@@ -44,7 +48,7 @@ class GuildList extends BaseType
if (!$guilds)
return;
$stats = DB::Aowow()->select('SELECT guild AS ARRAY_KEY, id AS ARRAY_KEY2, level, gearscore, achievementpoints, IF(cuFlags & ?d, 0, 1) AS synced FROM ?_profiler_profiles WHERE guild IN (?a) ORDER BY gearscore DESC', PROFILER_CU_NEEDS_RESYNC, $guilds);
$stats = DB::Aowow()->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'];
@@ -85,71 +89,41 @@ class GuildList extends BaseType
class GuildListFilter extends Filter
{
public $extraOpts = [];
protected $genericFilter = [];
use TrProfilerFilter;
// fieldId => [checkType, checkValue[, fieldIsArray]]
protected $inputFields = array(
'na' => [FILTER_V_REGEX, '/[\p{C};%\\\\]/ui', false], // name - only printable chars, no delimiter
'ma' => [FILTER_V_EQUAL, 1, false], // match any / all filter
'ex' => [FILTER_V_EQUAL, 'on', false], // only match exact
'si' => [FILTER_V_LIST, [1, 2], false], // side
'rg' => [FILTER_V_CALLBACK, 'cbRegionCheck', false], // region
'sv' => [FILTER_V_CALLBACK, 'cbServerCheck', false], // server
protected string $type = 'guilds';
protected 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
);
protected function createSQLForCriterium(&$cr) { }
public array $extraOpts = [];
protected function createSQLForValues()
protected function createSQLForValues() : array
{
$parts = [];
$_v = $this->fiData['v'];
$_v = $this->values;
// region (rg), battlegroup (bg) and server (sv) are passed to GuildList as miscData and handled there
// name [str]
if (!empty($_v['na']))
if ($_ = $this->modularizeString(['g.name'], $_v['na'], !empty($_v['ex']) && $_v['ex'] == 'on'))
if ($_v['na'])
if ($_ = $this->tokenizeString(['g.name'], $_v['na'], $_v['ex'] == 'on'))
$parts[] = $_;
// side [list]
if (!empty($_v['si']))
{
if ($_v['si'] == 1)
$parts[] = ['c.race', [1, 3, 4, 7, 11]];
else if ($_v['si'] == 2)
$parts[] = ['c.race', [2, 5, 6, 8, 10]];
}
if ($_v['si'] == SIDE_ALLIANCE)
$parts[] = ['c.race', ChrRace::fromMask(ChrRace::MASK_ALLIANCE)];
else if ($_v['si'] == SIDE_HORDE)
$parts[] = ['c.race', ChrRace::fromMask(ChrRace::MASK_HORDE)];
return $parts;
}
protected function cbRegionCheck(&$v)
{
if (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(&$v)
{
foreach (Profiler::getRealms() as $realm)
if ($realm['name'] == $v)
{
$this->parentCats[1] = Profiler::urlize($v);// directly redirect onto this server
$v = ''; // remove from filter
return true;
}
return false;
}
}
@@ -162,12 +136,12 @@ class RemoteGuildList extends GuildList
'c' => ['j' => 'characters c ON c.guid = gm.guid', 's' => ', BIT_OR(IF(c.race IN (1, 3, 4, 7, 11), 1, 2)) - 1 AS faction']
);
public function __construct($conditions = [], $miscData = null)
public function __construct(array $conditions = [], array $miscData = [])
{
// select DB by realm
if (!$this->selectRealms($miscData))
{
trigger_error('no access to auth-db or table realmlist is empty', E_USER_WARNING);
trigger_error('RemoteGuildList::__construct - cannot access any realm.', E_USER_WARNING);
return;
}
@@ -185,7 +159,7 @@ class RemoteGuildList extends GuildList
foreach ($this->iterate() as $guid => &$curTpl)
{
// battlegroup
$curTpl['battlegroup'] = CFG_BATTLEGROUP;
$curTpl['battlegroup'] = Cfg::get('BATTLEGROUP');
$r = explode(':', $guid)[0];
if (!empty($realms[$r]))
@@ -196,7 +170,15 @@ class RemoteGuildList extends GuildList
}
else
{
trigger_error('character "'.$curTpl['name'].'" belongs to nonexistant realm #'.$r, E_USER_WARNING);
trigger_error('guild #'.$guid.' belongs to 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;
}
@@ -208,11 +190,14 @@ class RemoteGuildList extends GuildList
$distrib[$curTpl['realm']]++;
}
$limit = CFG_SQL_LIMIT_DEFAULT;
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);
@@ -246,7 +231,7 @@ class RemoteGuildList extends GuildList
// basic guild data
foreach (Util::createSqlBatchInsert($data) as $ins)
DB::Aowow()->query('INSERT IGNORE INTO ?_profiler_guild (?#) VALUES '.$ins, array_keys(reset($data)));
DB::Aowow()->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(
@@ -266,15 +251,36 @@ class LocalGuildList extends GuildList
{
protected $queryBase = 'SELECT g.*, g.id AS ARRAY_KEY FROM ?_profiler_guild g';
public function __construct($conditions = [], $miscData = null)
public function __construct(array $conditions = [], array $miscData = [])
{
$realms = Profiler::getRealms();
// graft realm selection from miscData onto conditions
if (isset($miscData['sv']))
$realms = array_filter($realms, fn($x) => Profiler::urlize($x['name']) == Profiler::urlize($miscData['sv']));
if (isset($miscData['rg']))
$realms = array_filter($realms, fn($x) => $x['region'] == $miscData['rg']);
if (!$realms)
{
trigger_error('LocalGuildList::__construct - cannot access any realm.', E_USER_WARNING);
return;
}
if ($conditions)
{
array_unshift($conditions, 'AND');
$conditions = ['AND', ['realm', array_keys($realms)], $conditions];
}
else
$conditions = [['realm', array_keys($realms)]];
parent::__construct($conditions, $miscData);
if ($this->error)
return;
$realms = Profiler::getRealms();
foreach ($this->iterate() as $id => &$curTpl)
{
if ($curTpl['realm'] && !isset($realms[$curTpl['realm']]))
@@ -287,7 +293,7 @@ class LocalGuildList extends GuildList
}
// battlegroup
$curTpl['battlegroup'] = CFG_BATTLEGROUP;
$curTpl['battlegroup'] = Cfg::get('BATTLEGROUP');
}
}

View File

@@ -1,5 +1,7 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
@@ -9,11 +11,11 @@ class IconList extends BaseType
use listviewHelper;
public static $type = Type::ICON;
public static $brickFile = '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 $pseudoQry = 'SELECT `iconId` AS ARRAY_KEY, COUNT(*) FROM ?# WHERE `iconId` IN (?a) GROUP BY `iconId`';
private $pseudoJoin = array(
'nItems' => '?_items',
'nSpells' => '?_spell',
@@ -34,9 +36,9 @@ class IconList extends BaseType
);
*/
public function __construct($conditions)
public function __construct(array $conditions = [], array $miscData = [])
{
parent::__construct($conditions);
parent::__construct($conditions, $miscData);
if (!$this->getFoundIDs())
return;
@@ -53,7 +55,7 @@ class IconList extends BaseType
// use if you JUST need the name
public static function getName($id)
{
$n = DB::Aowow()->SelectRow('SELECT name FROM ?_icons WHERE id = ?d', $id );
$n = DB::Aowow()->SelectRow('SELECT `name` FROM ?_icons WHERE `id` = ?d', $id );
return Util::localizedString($n, 'name');
}
// end static use
@@ -101,10 +103,8 @@ class IconList extends BaseType
class IconListFilter extends Filter
{
public $extraOpts = null;
// cr => [type, field, misc, extraCol]
private $criterion2field = array(
private array $totalUses = [];
private array $criterion2field = array(
1 => '?_items', // items [num]
2 => '?_spell', // spells [num]
3 => '?_achievement', // achievements [num]
@@ -118,35 +118,36 @@ class IconListFilter extends Filter
11 => '', // classes [num]
13 => '' // used [num]
);
private $totalUses = [];
protected $genericFilter = array(
1 => [FILTER_CR_CALLBACK, 'cbUseAny' ], // items [num]
2 => [FILTER_CR_CALLBACK, 'cbUseAny' ], // spells [num]
3 => [FILTER_CR_CALLBACK, 'cbUseAny' ], // achievements [num]
6 => [FILTER_CR_CALLBACK, 'cbUseAny' ], // currencies [num]
9 => [FILTER_CR_CALLBACK, 'cbUseAny' ], // hunterpets [num]
11 => [FILTER_CR_NYI_PH, null, null], // classes [num]
13 => [FILTER_CR_CALLBACK, 'cbUseAll' ] // used [num]
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]
);
// fieldId => [checkType, checkValue[, fieldIsArray]]
protected $inputFields = array(
'cr' => [FILTER_V_LIST, [1, 2, 3, 6, 9, 11, 13], true ], // criteria ids
'crs' => [FILTER_V_RANGE, [1, 6], true ], // criteria operators
'crv' => [FILTER_V_RANGE, [0, 99999], true ], // criteria values - all criteria are numeric here
'na' => [FILTER_V_REGEX, '/[\p{C};%\\\\]/ui', false], // name - only printable chars, no delimiter
'ma' => [FILTER_V_EQUAL, 1, false] // match any / all filter
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
);
private function _getCnd($op, $val, $tbl)
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);
$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)
@@ -160,48 +161,39 @@ class IconListFilter extends Filter
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);
$ids = DB::Aowow()->selectCol('SELECT `iconId` AS ARRAY_KEY, COUNT(*) AS "n" FROM ?# GROUP BY `iconId` HAVING n '.$op.' '.$val, $tbl);
return $ids ? ['id', array_keys($ids), '!'] : [1];
}
protected function createSQLForCriterium(&$cr)
{
if (in_array($cr[0], array_keys($this->genericFilter)))
if ($genCr = $this->genericCriterion($cr))
return $genCr;
unset($cr);
$this->error = true;
return [1];
}
protected function createSQLForValues()
protected function createSQLForValues() : array
{
$parts = [];
$_v = &$this->fiData['v'];
$_v = &$this->values;
//string
if (isset($_v['na']))
if ($_ = $this->modularizeString(['name']))
if ($_v['na'])
if ($_ = $this->tokenizeString(['name']))
$parts[] = $_;
return $parts;
}
protected function cbUseAny($cr, $value)
protected function cbUseAny(int $cr, int $crs, string $crv) : ?array
{
if (Util::checkNumeric($cr[2], NUM_CAST_INT) && $this->int2Op($cr[1]))
return $this->_getCnd($cr[1], $cr[2], $this->criterion2field[$cr[0]]);
if (Util::checkNumeric($crv, NUM_CAST_INT) && $this->int2Op($crs))
return $this->_getCnd($crs, $crv, $this->criterion2field[$cr]);
return false;
return null;
}
protected function cbUseAll($cr)
protected function cbUseAll(int $cr, int $crs, string $crv) : ?array
{
if (!Util::checkNumeric($cr[2], NUM_CAST_INT) || !$this->int2Op($cr[1]))
return false;
if (!Util::checkNumeric($crv, NUM_CAST_INT) || !$this->int2Op($crs))
return null;
if (!$this->totalUses)
{
@@ -210,24 +202,24 @@ class IconListFilter extends Filter
if (!$tbl)
continue;
$res = DB::Aowow()->selectCol('SELECT iconId AS ARRAY_KEY, COUNT(*) AS n FROM ?# GROUP BY iconId', $tbl);
$res = DB::Aowow()->selectCol('SELECT `iconId` AS ARRAY_KEY, COUNT(*) AS "n" FROM ?# GROUP BY `iconId`', $tbl);
Util::arraySumByKey($this->totalUses, $res);
}
}
if ($cr[1] == '=')
$cr[1] = '==';
if ($crs == '=')
$crs = '==';
$op = $cr[1];
if ($cr[1] == '<=' && $cr[2])
$op = $crs;
if ($crs == '<=' && $crv)
$op = '>';
else if ($cr[1] == '<' && $cr[2])
else if ($crs == '<' && $crv)
$op = '>=';
else if ($cr[1] == '!=' && $cr[2])
else if ($crs == '!=' && $crv)
$op = '==';
$ids = array_filter($this->totalUses, function ($x) use ($op, $cr) { return eval('return '.$x.' '.$op.' '.$cr[2].';'); });
$ids = array_filter($this->totalUses, fn($x) => eval('return '.$x.' '.$op.' '.$crv.';'));
if ($cr[1] != $op)
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,5 +1,7 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
@@ -18,27 +20,21 @@ class ItemsetList extends BaseType
protected $queryBase = 'SELECT `set`.*, `set`.id AS ARRAY_KEY FROM ?_itemset `set`';
protected $queryOpts = array(
'set' => ['o' => 'maxlevel DESC'],
'e' => ['j' => ['?_events e ON e.id = `set`.eventId', true], 's' => ', e.holidayId']
'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($conditions = [])
public function __construct(array $conditions = [], array $miscData = [])
{
parent::__construct($conditions);
parent::__construct($conditions, $miscData);
// post processing
foreach ($this->iterate() as &$_curTpl)
{
$_curTpl['classes'] = [];
$_curTpl['pieces'] = [];
for ($i = 1; $i < 12; $i++)
{
if ($_curTpl['classMask'] & (1 << ($i - 1)))
{
$this->classes[] = $i;
$_curTpl['classes'][] = $i;
}
}
$_curTpl['classes'] = ChrClass::fromMask($_curTpl['classMask']);
$this->classes = array_merge($this->classes, $_curTpl['classes']);
$_curTpl['pieces'] = [];
for ($i = 1; $i < 10; $i++)
{
if ($piece = $_curTpl['item'.$i])
@@ -112,7 +108,7 @@ class ItemsetList extends BaseType
if ($_ = $this->getField('contentGroup'))
$x .= Lang::itemset('notes', $_).($this->getField('heroic') ? ' <i class="q2">('.Lang::item('heroic').')</i>' : '').'<br />';
if (!$nCl || !$this->getField('contentGroup'))
if (!$nCl || !$this->getField('type'))
$x.= Lang::itemset('types', $this->getField('type')).'<br />';
if ($bonuses = $this->getBonuses())
@@ -171,91 +167,93 @@ class ItemsetList extends BaseType
// missing filter: "Available to Players"
class ItemsetListFilter extends Filter
{
// cr => [type, field, misc, extraCol]
protected $genericFilter = array( // misc (bool): _NUMERIC => useFloat; _STRING => localized; _FLAG => match Value; _BOOLEAN => stringSet
2 => [FILTER_CR_NUMERIC, 'id', NUM_CAST_INT, true], // id
3 => [FILTER_CR_NUMERIC, 'npieces', NUM_CAST_INT ], // pieces
4 => [FILTER_CR_STRING, 'bonusText', STR_LOCALIZED ], // bonustext
5 => [FILTER_CR_BOOLEAN, 'heroic', ], // heroic
6 => [FILTER_CR_ENUM, 'e.holidayId', ], // relatedevent
8 => [FILTER_CR_FLAG, 'cuFlags', CUSTOM_HAS_COMMENT ], // hascomments
9 => [FILTER_CR_FLAG, 'cuFlags', CUSTOM_HAS_SCREENSHOT ], // hasscreenshots
10 => [FILTER_CR_FLAG, 'cuFlags', CUSTOM_HAS_VIDEO ], // hasvideos
12 => [FILTER_CR_NYI_PH, null, 1 ] // available to players [yn] - ugh .. scan loot, quest and vendor templates and write to ?_itemset
protected string $type = 'itemsets';
protected array $enums = array(
6 => parent::ENUM_EVENT
);
// fieldId => [checkType, checkValue[, fieldIsArray]]
protected $inputFields = array(
'cr' => [FILTER_V_RANGE, [2, 12], true ], // criteria ids
'crs' => [FILTER_V_LIST, [FILTER_ENUM_NONE, FILTER_ENUM_ANY, [0, 424]], true ], // criteria operators
'crv' => [FILTER_V_REGEX, '/[\p{C};:%\\\\]/ui', true ], // criteria values - only printable chars, no delimiters
'na' => [FILTER_V_REGEX, '/[\p{C};%\\\\]/ui', false], // name / description - only printable chars, no delimiter
'ma' => [FILTER_V_EQUAL, 1, false], // match any / all filter
'qu' => [FILTER_V_RANGE, [0, 7], true ], // quality
'ty' => [FILTER_V_RANGE, [1, 12], true ], // set type
'minle' => [FILTER_V_RANGE, [1, 999], false], // min item level
'maxle' => [FILTER_V_RANGE, [1, 999], false], // max itemlevel
'minrl' => [FILTER_V_RANGE, [1, MAX_LEVEL], false], // min required level
'maxrl' => [FILTER_V_RANGE, [1, MAX_LEVEL], false], // max required level
'cl' => [FILTER_V_LIST, [[1, 9], 11], false], // class
'ta' => [FILTER_V_RANGE, [1, 30], false] // tag / content group
protected array $genericFilter = array(
2 => [parent::CR_NUMERIC, 'id', NUM_CAST_INT, true], // id
3 => [parent::CR_NUMERIC, 'npieces', NUM_CAST_INT ], // pieces
4 => [parent::CR_STRING, 'bonusText', STR_LOCALIZED ], // bonustext
5 => [parent::CR_BOOLEAN, 'heroic' ], // heroic
6 => [parent::CR_ENUM, 'e.holidayId', true, true], // relatedevent
8 => [parent::CR_FLAG, 'cuFlags', CUSTOM_HAS_COMMENT ], // hascomments
9 => [parent::CR_FLAG, 'cuFlags', CUSTOM_HAS_SCREENSHOT ], // hasscreenshots
10 => [parent::CR_FLAG, 'cuFlags', CUSTOM_HAS_VIDEO ], // hasvideos
12 => [parent::CR_CALLBACK, 'cbAvaliable', ] // available to players [yn]
);
protected function createSQLForCriterium(&$cr)
{
if (in_array($cr[0], array_keys($this->genericFilter)))
if ($genCR = $this->genericCriterion($cr))
return $genCR;
protected array $inputFields = array(
'cr' => [parent::V_RANGE, [2, 12], true ], // criteria ids
'crs' => [parent::V_LIST, [parent::ENUM_NONE, parent::ENUM_ANY, [0, 424]], true ], // criteria operators
'crv' => [parent::V_REGEX, parent::PATTERN_CRV, true ], // criteria values - only printable chars, no delimiters
'na' => [parent::V_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
);
unset($cr);
$this->error = true;
return [1];
}
protected function createSQLForValues()
protected function createSQLForValues() : array
{
$parts = [];
$_v = &$this->fiData['v'];
$_v = &$this->values;
// name [str]
if (isset($_v['na']))
if ($_ = $this->modularizeString(['name_loc'.User::$localeId]))
if ($_v['na'])
if ($_ = $this->tokenizeString(['name_loc'.Lang::getLocale()->value]))
$parts[] = $_;
// quality [enum]
if (isset($_v['qu']))
if ($_v['qu'])
$parts[] = ['quality', $_v['qu']];
// type [enum]
if (isset($_v['ty']))
if ($_v['ty'])
$parts[] = ['type', $_v['ty']];
// itemLevel min [int]
if (isset($_v['minle']))
if ($_v['minle'])
$parts[] = ['minLevel', $_v['minle'], '>='];
// itemLevel max [int]
if (isset($_v['maxle']))
if ($_v['maxle'])
$parts[] = ['maxLevel', $_v['maxle'], '<='];
// reqLevel min [int]
if (isset($_v['minrl']))
if ($_v['minrl'])
$parts[] = ['reqLevel', $_v['minrl'], '>='];
// reqLevel max [int]
if (isset($_v['maxrl']))
if ($_v['maxrl'])
$parts[] = ['reqLevel', $_v['maxrl'], '<='];
// class [enum]
if (isset($_v['cl']))
if ($_v['cl'])
$parts[] = ['classMask', $this->list2Mask([$_v['cl']]), '&'];
// tag [enum]
if (isset($_v['ta']))
if ($_v['ta'])
$parts[] = ['contentGroup', intVal($_v['ta'])];
return $parts;
}
protected function cbAvaliable(int $cr, int $crs, string $crv) : ?array
{
return match ($crs)
{
1 => ['src.typeId', null, '!'], // Yes
2 => ['src.typeId', null], // No
default => null
};
}
}
?>

View File

@@ -1,5 +1,7 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
@@ -13,9 +15,9 @@ class MailList extends BaseType
protected $queryBase = 'SELECT m.*, m.id AS ARRAY_KEY FROM ?_mails m';
protected $queryOpts = [];
public function __construct($conditions = [])
public function __construct(array $conditions = [], array $miscData = [])
{
parent::__construct($conditions);
parent::__construct($conditions, $miscData);
if ($this->error)
return;

View File

@@ -1,5 +1,7 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');

View File

@@ -1,5 +1,7 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
@@ -8,6 +10,8 @@ class ProfileList extends BaseType
{
use profilerHelper, listviewHelper;
public static $contribute = CONTRIBUTE_NONE;
public function getListviewData($addInfo = 0, array $reqCols = [])
{
$data = [];
@@ -29,13 +33,13 @@ class ProfileList extends BaseType
'classs' => $this->getField('class'),
'gender' => $this->getField('gender'),
'level' => $this->getField('level'),
'faction' => (1 << ($this->getField('race') - 1)) & RACE_MASK_ALLIANCE ? 0 : 1,
'faction' => ChrRace::tryFrom($this->getField('race'))?->isAlliance() ? 0 : 1,
'talenttree1' => $this->getField('talenttree1'),
'talenttree2' => $this->getField('talenttree2'),
'talenttree3' => $this->getField('talenttree3'),
'talentspec' => $this->getField('activespec') + 1, // 0 => 1; 1 => 2
'achievementpoints' => $this->getField('achievementpoints'),
'guild' => '$"'.str_replace ('"', '', $this->curTpl['guildname']).'"',// force this to be a string
'guild' => $this->curTpl['guildname'] ? '$"'.str_replace ('"', '', $this->curTpl['guildname']).'"' : '', // force this to be a string
'guildrank' => $this->getField('guildrank'),
'realm' => Profiler::urlize($this->getField('realmName'), true),
'realmname' => $this->getField('realmName'),
@@ -235,286 +239,230 @@ class ProfileList extends BaseType
class ProfileListFilter extends Filter
{
public $useLocalList = false;
public $extraOpts = [];
use TrProfilerFilter;
private $realms = [];
protected $enums = array(
-1 => array( // arena team sizes
// by name by rating by contrib
12 => 2, 13 => 2, 14 => 2,
15 => 3, 16 => 3, 17 => 3,
18 => 5, 19 => 5, 20 => 5
)
protected string $type = 'profiles';
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 $genericFilter = array( // misc (bool): _NUMERIC => useFloat; _STRING => localized; _FLAG => match Value; _BOOLEAN => stringSet
2 => [FILTER_CR_NUMERIC, 'gearscore', NUM_CAST_INT ], // gearscore [num]
3 => [FILTER_CR_CALLBACK, 'cbAchievs', null, null], // achievementpoints [num]
5 => [FILTER_CR_NUMERIC, 'talenttree1', NUM_CAST_INT ], // talenttree1 [num]
6 => [FILTER_CR_NUMERIC, 'talenttree2', NUM_CAST_INT ], // talenttree2 [num]
7 => [FILTER_CR_NUMERIC, 'talenttree3', NUM_CAST_INT ], // talenttree3 [num]
9 => [FILTER_CR_STRING, 'g.name', ], // guildname
10 => [FILTER_CR_CALLBACK, 'cbHasGuildRank', null, null], // guildrank
12 => [FILTER_CR_CALLBACK, 'cbTeamName', null, null], // teamname2v2
15 => [FILTER_CR_CALLBACK, 'cbTeamName', null, null], // teamname3v3
18 => [FILTER_CR_CALLBACK, 'cbTeamName', null, null], // teamname5v5
13 => [FILTER_CR_CALLBACK, 'cbTeamRating', null, null], // teamrtng2v2
16 => [FILTER_CR_CALLBACK, 'cbTeamRating', null, null], // teamrtng3v3
19 => [FILTER_CR_CALLBACK, 'cbTeamRating', null, null], // teamrtng5v5
14 => [FILTER_CR_NYI_PH, 0 ], // teamcontrib2v2 [num]
17 => [FILTER_CR_NYI_PH, 0 ], // teamcontrib3v3 [num]
20 => [FILTER_CR_NYI_PH, 0 ], // teamcontrib5v5 [num]
21 => [FILTER_CR_CALLBACK, 'cbWearsItems', null, null], // wearingitem [str]
23 => [FILTER_CR_CALLBACK, 'cbCompletedAcv', null, null], // completedachievement
25 => [FILTER_CR_CALLBACK, 'cbProfession', SKILL_ALCHEMY, null], // alchemy [num]
26 => [FILTER_CR_CALLBACK, 'cbProfession', SKILL_BLACKSMITHING, null], // blacksmithing [num]
27 => [FILTER_CR_CALLBACK, 'cbProfession', SKILL_ENCHANTING, null], // enchanting [num]
28 => [FILTER_CR_CALLBACK, 'cbProfession', SKILL_ENGINEERING, null], // engineering [num]
29 => [FILTER_CR_CALLBACK, 'cbProfession', SKILL_HERBALISM, null], // herbalism [num]
30 => [FILTER_CR_CALLBACK, 'cbProfession', SKILL_INSCRIPTION, null], // inscription [num]
31 => [FILTER_CR_CALLBACK, 'cbProfession', SKILL_JEWELCRAFTING, null], // jewelcrafting [num]
32 => [FILTER_CR_CALLBACK, 'cbProfession', SKILL_LEATHERWORKING, null], // leatherworking [num]
33 => [FILTER_CR_CALLBACK, 'cbProfession', SKILL_MINING, null], // mining [num]
34 => [FILTER_CR_CALLBACK, 'cbProfession', SKILL_SKINNING, null], // skinning [num]
35 => [FILTER_CR_CALLBACK, 'cbProfession', SKILL_TAILORING, null], // tailoring [num]
36 => [FILTER_CR_CALLBACK, 'cbHasGuild', null, null] // hasguild [yn]
protected array $inputFields = array(
'cr' => [parent::V_RANGE, [1, 36], true ], // criteria ids
'crs' => [parent::V_LIST, [parent::ENUM_NONE, parent::ENUM_ANY, [0, 5000]], true ], // criteria operators
'crv' => [parent::V_REGEX, parent::PATTERN_CRV, true ], // criteria values
'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
);
// fieldId => [checkType, checkValue[, fieldIsArray]]
protected $inputFields = array(
'cr' => [FILTER_V_RANGE, [1, 36], true ], // criteria ids
'crs' => [FILTER_V_LIST, [FILTER_ENUM_NONE, FILTER_ENUM_ANY, [0, 5000]], true ], // criteria operators
'crv' => [FILTER_V_REGEX, '/[\p{C}:;%\\\\]/ui', true ], // criteria values
'na' => [FILTER_V_REGEX, '/[\p{C};%\\\\]/ui', false], // name - only printable chars, no delimiter
'ma' => [FILTER_V_EQUAL, 1, false], // match any / all filter
'ex' => [FILTER_V_EQUAL, 'on', false], // only match exact
'si' => [FILTER_V_LIST, [1, 2], false], // side
'ra' => [FILTER_V_LIST, [[1, 8], 10, 11], true ], // race
'cl' => [FILTER_V_LIST, [[1, 9], 11], true ], // class
'minle' => [FILTER_V_RANGE, [1, MAX_LEVEL], false], // min level
'maxle' => [FILTER_V_RANGE, [1, MAX_LEVEL], false], // max level
'rg' => [FILTER_V_CALLBACK, 'cbRegionCheck', false], // region
'sv' => [FILTER_V_CALLBACK, 'cbServerCheck', false], // server
);
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($fromPOST = false, $opts = [])
public function __construct(string|array $data, array $opts = [])
{
if (!empty($opts['realms']))
$this->realms = $opts['realms'];
else
$this->realms = array_keys(Profiler::getRealms());
parent::__construct($data, $opts);
parent::__construct($fromPOST, $opts);
if (!empty($this->fiData['c']['cr']))
if (array_intersect($this->fiData['c']['cr'], [2, 5, 6, 7, 21]))
if (!empty($this->criteria['cr']))
if (array_intersect($this->criteria['cr'], [2, 5, 6, 7, 21]))
$this->useLocalList = true;
}
protected function createSQLForCriterium(&$cr)
{
if (in_array($cr[0], array_keys($this->genericFilter)))
if ($genCR = $this->genericCriterion($cr))
return $genCR;
unset($cr);
$this->error = true;
return [1];
}
protected function createSQLForValues()
protected function createSQLForValues() : array
{
$parts = [];
$_v = $this->fiData['v'];
$_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 down't want to destroy indizes, lets alter the search terms
if (!empty($_v['na']))
// 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->modularizeString([$k.'.name'], Util::lower($_v['na']), !empty($_v['ex']) && $_v['ex'] == 'on', true);
$proper = $this->modularizeString([$k.'.name'], Util::ucWords($_v['na']), !empty($_v['ex']) && $_v['ex'] == 'on', true);
$lower = $this->tokenizeString([$k.'.name'], Util::lower($_v['na']), $_v['ex'] == 'on', true);
$proper = $this->tokenizeString([$k.'.name'], Util::ucWords($_v['na']), $_v['ex'] == 'on', true);
$parts[] = ['OR', $lower, $proper];
}
// side [list]
if (!empty($_v['si']))
{
if ($_v['si'] == 1)
$parts[] = [$k.'.race', [1, 3, 4, 7, 11]];
else if ($_v['si'] == 2)
$parts[] = [$k.'.race', [2, 5, 6, 8, 10]];
}
if ($_v['si'] == SIDE_ALLIANCE)
$parts[] = [$k.'.race', ChrRace::fromMask(ChrRace::MASK_ALLIANCE)];
else if ($_v['si'] == SIDE_HORDE)
$parts[] = [$k.'.race', ChrRace::fromMask(ChrRace::MASK_HORDE)];
// race [list]
if (!empty($_v['ra']))
if ($_v['ra'])
$parts[] = [$k.'.race', $_v['ra']];
// class [list]
if (!empty($_v['cl']))
if ($_v['cl'])
$parts[] = [$k.'.class', $_v['cl']];
// min level [int]
if (isset($_v['minle']))
if ($_v['minle'])
$parts[] = [$k.'.level', $_v['minle'], '>='];
// max level [int]
if (isset($_v['maxle']))
if ($_v['maxle'])
$parts[] = [$k.'.level', $_v['maxle'], '<='];
return $parts;
}
protected function cbRegionCheck(&$v)
protected function cbProfession(int $cr, int $crs, string $crv, $skillId) : ?array
{
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(&$v)
{
foreach (Profiler::getRealms() as $realm)
if ($realm['name'] == $v)
{
$this->parentCats[1] = Profiler::urlize($v);// directly redirect onto this server
$v = ''; // remove from filter
return true;
}
return false;
}
protected function cbProfession($cr, $skillId)
{
if (!Util::checkNumeric($cr[2], NUM_CAST_INT) || !$this->int2Op($cr[1]))
return;
if (!Util::checkNumeric($crv, NUM_CAST_INT) || !$this->int2Op($crs))
return null;
$k = 'sk_'.Util::createHash(12);
$col = 'skill'.$skillId;
$col = 'skill-'.$skillId;
$this->formData['extraCols'][$skillId] = $col;
$this->fiExtraCols[$skillId] = $col;
if ($this->useLocalList)
{
$this->extraOpts[$k] = array(
'j' => ['?_profiler_completion '.$k.' ON '.$k.'.id = p.id AND '.$k.'.`type` = '.Type::SKILL.' AND '.$k.'.typeId = '.$skillId.' AND '.$k.'.cur '.$cr[1].' '.$cr[2], true],
's' => [', '.$k.'.cur AS '.$col]
'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.'.typeId', null, '!'];
return [$k.'.skillId', null, '!'];
}
else
{
$this->extraOpts[$k] = array(
'j' => ['character_skills '.$k.' ON '.$k.'.guid = c.guid AND '.$k.'.skill = '.$skillId.' AND '.$k.'.value '.$cr[1].' '.$cr[2], true],
's' => [', '.$k.'.value AS '.$col]
'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($cr)
protected function cbCompletedAcv(int $cr, int $crs, string $crv) : ?array
{
if (!Util::checkNumeric($cr[2], NUM_CAST_INT))
return false;
if (!Util::checkNumeric($crv, NUM_CAST_INT))
return null;
if (!DB::Aowow()->selectCell('SELECT 1 FROM ?_achievement WHERE id = ?d', $cr[2]))
return false;
if (!Type::validateIds(Type::ACHIEVEMENT, $crv))
return null;
$k = 'acv_'.Util::createHash(12);
if ($this->useLocalList)
{
$this->extraOpts[$k] = ['j' => ['?_profiler_completion '.$k.' ON '.$k.'.id = p.id AND '.$k.'.`type` = '.Type::ACHIEVEMENT.' AND '.$k.'.typeId = '.$cr[2], true]];
return [$k.'.typeId', null, '!'];
$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' => ['character_achievement '.$k.' ON '.$k.'.guid = c.guid AND '.$k.'.achievement = '.$cr[2], true]];
$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($cr)
protected function cbWearsItems(int $cr, int $crs, string $crv) : ?array
{
if (!Util::checkNumeric($cr[2], NUM_CAST_INT))
return false;
if (!Util::checkNumeric($crv, NUM_CAST_INT))
return null;
if (!DB::Aowow()->selectCell('SELECT 1 FROM ?_items WHERE id = ?d', $cr[2]))
return false;
if (!Type::validateIds(Type::ITEM, $crv))
return null;
$k = 'i_'.Util::createHash(12);
$this->extraOpts[$k] = ['j' => ['?_profiler_items '.$k.' ON '.$k.'.id = p.id AND '.$k.'.item = '.$cr[2], true]];
$this->extraOpts[$k] = ['j' => [sprintf('?_profiler_items %1$s ON `%1$s`.`id` = p.`id` AND `%1$s`.`item` = %2$d', $k, $crv), true]];
return [$k.'.item', null, '!'];
}
protected function cbHasGuild($cr)
protected function cbHasGuild(int $cr, int $crs, string $crv) : ?array
{
if (!$this->int2Bool($cr[1]))
return false;
if (!$this->int2Bool($crs))
return null;
if ($this->useLocalList)
return ['p.guild', null, $cr[1] ? '!' : null];
return ['p.guild', null, $crs ? '!' : null];
else
return ['gm.guildId', null, $cr[1] ? '!' : null];
return ['gm.guildId', null, $crs ? '!' : null];
}
protected function cbHasGuildRank($cr)
protected function cbHasGuildRank(int $cr, int $crs, string $crv) : ?array
{
if (!Util::checkNumeric($cr[2], NUM_CAST_INT) || !$this->int2Op($cr[1]))
return false;
if (!Util::checkNumeric($crv, NUM_CAST_INT) || !$this->int2Op($crs))
return null;
if ($this->useLocalList)
return ['p.guildrank', $cr[2], $cr[1]];
return ['p.guildrank', $crv, $crs];
else
return ['gm.rank', $cr[2], $cr[1]];
return ['gm.rank', $crv, $crs];
}
protected function cbTeamName($cr)
protected function cbTeamName(int $cr, int $crs, string $crv, $size) : ?array
{
if ($_ = $this->modularizeString(['at.name'], $cr[2]))
return ['AND', ['at.type', $this->enums[-1][$cr[0]]], $_];
if ($_ = $this->tokenizeString(['at.name'], $crv))
return ['AND', ['at.type', $size], $_];
return false;
return null;
}
protected function cbTeamRating($cr)
protected function cbTeamRating(int $cr, int $crs, string $crv, $size) : ?array
{
if (!Util::checkNumeric($cr[2], NUM_CAST_INT) || !$this->int2Op($cr[1]))
return false;
if (!Util::checkNumeric($crv, NUM_CAST_INT) || !$this->int2Op($crs))
return null;
return ['AND', ['at.type', $this->enums[-1][$cr[0]]], ['at.rating', $cr[2], $cr[1]]];
return ['AND', ['at.type', $size], ['at.rating', $crv, $crs]];
}
protected function cbAchievs($cr)
protected function cbAchievs(int $cr, int $crs, string $crv) : ?array
{
if (!Util::checkNumeric($cr[2], NUM_CAST_INT) || !$this->int2Op($cr[1]))
return false;
if (!Util::checkNumeric($crv, NUM_CAST_INT) || !$this->int2Op($crs))
return null;
if ($this->useLocalList)
return ['p.achievementpoints', $cr[2], $cr[1]];
return ['p.achievementpoints', $crv, $crs];
else
return ['cap.counter', $cr[2], $cr[1]];
return ['cap.counter', $crv, $crs];
}
}
@@ -531,12 +479,14 @@ class RemoteProfileList extends ProfileList
'at' => [['atm'], 'j' => 'arena_team at ON atm.arenaTeamId = at.arenaTeamId', 's' => ', at.name AS arenateam, IF(at.captainGuid = c.guid, 1, 0) AS captain']
);
public function __construct($conditions = [], $miscData = null)
private $rnItr = []; // rename iterator [name => nCharsWithThisName]
public function __construct(array $conditions = [], array $miscData = [])
{
// select DB by realm
if (!$this->selectRealms($miscData))
{
trigger_error('no access to auth-db or table realmlist is empty', E_USER_WARNING);
trigger_error('RemoteProfileList::__construct - cannot access any realm.', E_USER_WARNING);
return;
}
@@ -550,18 +500,13 @@ class RemoteProfileList extends ProfileList
$realms = Profiler::getRealms();
$talentSpells = [];
$talentLookup = [];
$distrib = null;
$limit = CFG_SQL_LIMIT_DEFAULT;
foreach ($conditions as $c)
if (is_int($c))
$limit = $c;
$distrib = [];
// post processing
foreach ($this->iterate() as $guid => &$curTpl)
{
// battlegroup
$curTpl['battlegroup'] = CFG_BATTLEGROUP;
$curTpl['battlegroup'] = Cfg::get('BATTLEGROUP');
// realm
[$r, $g] = explode(':', $guid);
@@ -573,7 +518,15 @@ class RemoteProfileList extends ProfileList
}
else
{
trigger_error('character "'.$curTpl['name'].'" belongs to nonexistant realm #'.$r, E_USER_WARNING);
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;
}
@@ -587,13 +540,10 @@ class RemoteProfileList extends ProfileList
$curTpl['activespec'] = $curTpl['activeTalentGroup'];
// equalize distribution
if ($limit != CFG_SQL_LIMIT_NONE)
{
if (empty($distrib[$curTpl['realm']]))
$distrib[$curTpl['realm']] = 1;
else
$distrib[$curTpl['realm']]++;
}
if (empty($distrib[$curTpl['realm']]))
$distrib[$curTpl['realm']] = 1;
else
$distrib[$curTpl['realm']]++;
// char is pending rename
if ($curTpl['at_login'] & 0x1)
@@ -619,16 +569,21 @@ class RemoteProfileList extends ProfileList
$talentSpells = DB::Aowow()->select('SELECT spell AS ARRAY_KEY, tab, `rank` FROM ?_talents WHERE class IN (?a)', array_unique($talentSpells));
if ($distrib !== null)
{
$total = array_sum($distrib);
foreach ($distrib as &$d)
$d = ceil($limit * $d / $total);
}
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 !== null)
if ($distrib)
{
if ($limit <= 0 || $distrib[$curTpl['realm']] <= 0)
{
@@ -671,8 +626,11 @@ class RemoteProfileList extends ProfileList
$baseData = $guildData = [];
foreach ($this->iterate() as $guid => $__)
{
$realmId = $this->getField('realm');
$guildGUID = $this->getField('guild');
$baseData[$guid] = array(
'realm' => $this->getField('realm'),
'realm' => $realmId,
'realmGUID' => $this->getField('guid'),
'name' => $this->getField('name'),
'renameItr' => $this->getField('renameItr'),
@@ -680,15 +638,15 @@ class RemoteProfileList extends ProfileList
'class' => $this->getField('class'),
'level' => $this->getField('level'),
'gender' => $this->getField('gender'),
'guild' => $this->getField('guild') ?: null,
'guildrank' => $this->getField('guild') ? $this->getField('guildrank') : null,
'guild' => $guildGUID ?: null,
'guildrank' => $guildGUID ? $this->getField('guildrank') : null,
'cuFlags' => PROFILER_CU_NEEDS_RESYNC
);
if ($this->getField('guild'))
$guildData[] = array(
'realm' => $this->getField('realm'),
'realmGUID' => $this->getField('guild'),
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
@@ -699,7 +657,7 @@ class RemoteProfileList extends ProfileList
if ($guildData)
{
foreach (Util::createSqlBatchInsert($guildData) as $ins)
DB::Aowow()->query('INSERT IGNORE INTO ?_profiler_guild (?#) VALUES '.$ins, array_keys(reset($guildData)));
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)',
@@ -714,8 +672,8 @@ class RemoteProfileList extends ProfileList
// basic char data (enough for tooltips)
if ($baseData)
{
foreach (Util::createSqlBatchInsert($baseData) as $ins)
DB::Aowow()->query('INSERT INTO ?_profiler_profiles (?#) VALUES '.$ins.' ON DUPLICATE KEY UPDATE name = VALUES(name), renameItr = VALUES(renameItr)', array_keys(reset($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(
@@ -744,33 +702,45 @@ class LocalProfileList extends ProfileList
'g' => ['j' => ['?_profiler_guild g ON g.id = p.guild', true], 's' => ', g.name AS guildname']
);
public function __construct($conditions = [], $miscData = null)
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;
$realms = Profiler::getRealms();
// post processing
$acvPoints = DB::Aowow()->selectCol('SELECT pc.id AS ARRAY_KEY, SUM(a.points) FROM ?_profiler_completion pc LEFT JOIN ?_achievement a ON a.id = pc.typeId WHERE pc.`type` = ?d AND pc.id IN (?a) GROUP BY pc.id', Type::ACHIEVEMENT, $this->getFoundIDs());
foreach ($this->iterate() as $id => &$curTpl)
{
if ($curTpl['realm'] && !isset($realms[$curTpl['realm']]))
if (!$curTpl['realm']) // custom profile w/o realminfo
continue;
if (isset($realms[$curTpl['realm']]))
if (!isset($realms[$curTpl['realm']]))
{
$curTpl['realmName'] = $realms[$curTpl['realm']]['name'];
$curTpl['region'] = $realms[$curTpl['realm']]['region'];
unset($this->templates[$id]);
continue;
}
// battlegroup
$curTpl['battlegroup'] = CFG_BATTLEGROUP;
$curTpl['achievementpoints'] = isset($acvPoints[$id]) ? $acvPoints[$id] : 0;
$curTpl['realmName'] = $realms[$curTpl['realm']]['name'];
$curTpl['region'] = $realms[$curTpl['realm']]['region'];
$curTpl['battlegroup'] = Cfg::get('BATTLEGROUP');
}
}

View File

@@ -1,5 +1,7 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
@@ -22,7 +24,7 @@ class QuestList extends BaseType
'e' => ['j' => ['?_events e ON e.id = `q`.eventId', true], 's' => ', e.holidayId']
);
public function __construct($conditions = [], $miscData = null)
public function __construct(array $conditions = [], array $miscData = [])
{
parent::__construct($conditions, $miscData);
@@ -148,12 +150,15 @@ class QuestList extends BaseType
return in_array($this->getField('zoneOrSortBak'), [-22, -284, -366, -369, -370, -376, -374]) && !$this->isRepeatable();
}
public function getSourceData()
public function getSourceData(int $id = 0) : array
{
$data = [];
foreach ($this->iterate() as $__)
{
if ($id && $id != $this->id)
continue;
$data[$this->id] = array(
"n" => $this->getField('name', true),
"t" => Type::QUEST,
@@ -172,7 +177,7 @@ class QuestList extends BaseType
foreach ($this->iterate() as $__)
{
if (!(Game::sideByRaceMask($this->curTpl['reqRaceMask']) & $side))
if (!(ChrRace::sideFromMask($this->curTpl['reqRaceMask']) & $side))
continue;
[$series, $first] = DB::Aowow()->SelectRow(
@@ -208,8 +213,8 @@ class QuestList extends BaseType
'id' => $this->id,
'level' => $this->curTpl['level'],
'reqlevel' => $this->curTpl['minLevel'],
'name' => $this->getField('name', true),
'side' => Game::sideByRaceMask($this->curTpl['reqRaceMask']),
'name' => Lang::unescapeUISequences($this->getField('name', true), Lang::FMT_RAW),
'side' => ChrRace::sideFromMask($this->curTpl['reqRaceMask']),
'wflags' => 0x0,
'xp' => $this->curTpl['rewardXP']
);
@@ -235,8 +240,8 @@ class QuestList extends BaseType
if ($_ = $this->curTpl['reqClassMask'])
$data[$this->id]['reqclass'] = $_;
if ($_ = ($this->curTpl['reqRaceMask'] & RACE_MASK_ALL))
if ((($_ & RACE_MASK_ALLIANCE) != RACE_MASK_ALLIANCE) && (($_ & RACE_MASK_HORDE) != RACE_MASK_HORDE))
if ($_ = ($this->curTpl['reqRaceMask'] & ChrRace::MASK_ALL))
if ((($_ & ChrRace::MASK_ALLIANCE) != ChrRace::MASK_ALLIANCE) && (($_ & ChrRace::MASK_HORDE) != ChrRace::MASK_HORDE))
$data[$this->id]['reqrace'] = $_;
if ($_ = $this->curTpl['rewardOrReqMoney'])
@@ -313,7 +318,7 @@ class QuestList extends BaseType
if (!$this->curTpl)
return null;
$title = htmlentities($this->getField('name', true));
$title = Lang::unescapeUISequences(Util::htmlEscape($this->getField('name', true)), Lang::FMT_HTML);
$level = $this->curTpl['level'];
if ($level < 0)
$level = 0;
@@ -348,7 +353,7 @@ class QuestList extends BaseType
if ($ot)
$name = $ot;
else
$name = $rng > 0 ? CreatureList::getName($rng) : GameObjectList::getName(-$rng);
$name = $rng > 0 ? CreatureList::getName($rng) : Lang::unescapeUISequences(GameObjectList::getName(-$rng), Lang::FMT_HTML);
$xReq .= '<br /> - '.$name.($rngQty > 1 ? ' x '.$rngQty : null);
}
@@ -361,7 +366,7 @@ class QuestList extends BaseType
if (!$ri || $riQty < 1)
continue;
$xReq .= '<br /> - '.ItemList::getName($ri).($riQty > 1 ? ' x '.$riQty : null);
$xReq .= '<br /> - '.Lang::unescapeUISequences(ItemList::getName($ri), Lang::FMT_HTML).($riQty > 1 ? ' x '.$riQty : null);
}
if ($et = $this->getField('end', true))
@@ -424,292 +429,291 @@ class QuestList extends BaseType
class QuestListFilter extends Filter
{
public $extraOpts = [];
protected $enums = array( // massive enums could be put here, if you want to restrict inputs further to be valid IDs instead of just integers
37 => [null, 1, 2, 3, 4, 5, 6, 7, 8, 9, null, 11, true, false],
38 => [null, 1, 2, 3, 4, 5, 6, 7, 8, null, 10, 11, true, false],
);
protected $genericFilter = array(
1 => [FILTER_CR_CALLBACK, 'cbReputation', '>', null], // increasesrepwith
2 => [FILTER_CR_NUMERIC, 'rewardXP', NUM_CAST_INT ], // experiencegained
3 => [FILTER_CR_NUMERIC, 'rewardOrReqMoney', NUM_CAST_INT ], // moneyrewarded
4 => [FILTER_CR_CALLBACK, 'cbSpellRewards', null, null], // spellrewarded [yn]
5 => [FILTER_CR_FLAG, 'flags', QUEST_FLAG_SHARABLE ], // sharable
6 => [FILTER_CR_NUMERIC, 'timeLimit', NUM_CAST_INT ], // timer
7 => [FILTER_CR_NYI_PH, null, 1 ], // firstquestseries
9 => [FILTER_CR_CALLBACK, 'cbEarnReputation', null, null], // objectiveearnrepwith [enum]
10 => [FILTER_CR_CALLBACK, 'cbReputation', '<', null], // decreasesrepwith
11 => [FILTER_CR_NUMERIC, 'suggestedPlayers', NUM_CAST_INT ], // suggestedplayers
15 => [FILTER_CR_NYI_PH, null, 1 ], // lastquestseries
16 => [FILTER_CR_NYI_PH, null, 1 ], // partseries
18 => [FILTER_CR_FLAG, 'cuFlags', CUSTOM_HAS_SCREENSHOT ], // hasscreenshots
19 => [FILTER_CR_CALLBACK, 'cbQuestRelation', 0x1, null], // startsfrom [enum]
21 => [FILTER_CR_CALLBACK, 'cbQuestRelation', 0x2, null], // endsat [enum]
22 => [FILTER_CR_CALLBACK, 'cbItemRewards', null, null], // itemrewards [op] [int]
23 => [FILTER_CR_CALLBACK, 'cbItemChoices', null, null], // itemchoices [op] [int]
24 => [FILTER_CR_CALLBACK, 'cbLacksStartEnd', null, null], // lacksstartend [yn]
25 => [FILTER_CR_FLAG, 'cuFlags', CUSTOM_HAS_COMMENT ], // hascomments
27 => [FILTER_CR_FLAG, 'flags', QUEST_FLAG_DAILY ], // daily
28 => [FILTER_CR_FLAG, 'flags', QUEST_FLAG_WEEKLY ], // weekly
29 => [FILTER_CR_FLAG, 'flags', QUEST_FLAG_REPEATABLE ], // repeatable
30 => [FILTER_CR_NUMERIC, 'id', NUM_CAST_INT, true], // id
33 => [FILTER_CR_ENUM, 'e.holidayId' ], // relatedevent
34 => [FILTER_CR_CALLBACK, 'cbAvailable', null, null], // availabletoplayers [yn]
36 => [FILTER_CR_FLAG, 'cuFlags', CUSTOM_HAS_VIDEO ], // hasvideos
37 => [FILTER_CR_CALLBACK, 'cbClassSpec', null, null], // classspecific [enum]
38 => [FILTER_CR_CALLBACK, 'cbRaceSpec', null, null], // racespecific [enum]
42 => [FILTER_CR_STAFFFLAG, 'flags' ], // flags
43 => [FILTER_CR_CALLBACK, 'cbCurrencyReward', null, null], // currencyrewarded [enum]
44 => [FILTER_CR_CALLBACK, 'cbLoremaster', null, null], // countsforloremaster_stc [yn]
45 => [FILTER_CR_BOOLEAN, 'rewardTitleId' ] // titlerewarded
protected string $type = 'quests';
protected array $enums = array(
37 => parent::ENUM_CLASSS, // classspecific
38 => parent::ENUM_RACE, // racespecific
9 => parent::ENUM_FACTION, // objectiveearnrepwith
33 => parent::ENUM_EVENT, // relatedevent
43 => parent::ENUM_CURRENCY, // currencyrewarded
1 => parent::ENUM_FACTION, // increasesrepwith
10 => parent::ENUM_FACTION // decreasesrepwith
);
// fieldId => [checkType, checkValue[, fieldIsArray]]
protected $inputFields = array(
'cr' => [FILTER_V_RANGE, [1, 45], true ], // criteria ids
'crs' => [FILTER_V_LIST, [FILTER_ENUM_NONE, FILTER_ENUM_ANY, [0, 99999]], true ], // criteria operators
'crv' => [FILTER_V_REGEX, '/\D/', true ], // criteria values - only numerals
'na' => [FILTER_V_REGEX, '/[\p{C};%\\\\]/ui', false], // name / text - only printable chars, no delimiter
'ex' => [FILTER_V_EQUAL, 'on', false], // also match subname
'ma' => [FILTER_V_EQUAL, 1, false], // match any / all filter
'minle' => [FILTER_V_RANGE, [1, 99], false], // min quest level
'maxle' => [FILTER_V_RANGE, [1, 99], false], // max quest level
'minrl' => [FILTER_V_RANGE, [1, 99], false], // min required level
'maxrl' => [FILTER_V_RANGE, [1, 99], false], // max required level
'si' => [FILTER_V_LIST, [-2, -1, 1, 2, 3], false], // siede
'ty' => [FILTER_V_LIST, [0, 1, 21, 41, 62, [81, 85], 88, 89], true ] // type
protected array $genericFilter = array(
1 => [parent::CR_CALLBACK, 'cbReputation', '>', null], // increasesrepwith
2 => [parent::CR_NUMERIC, 'rewardXP', NUM_CAST_INT ], // experiencegained
3 => [parent::CR_NUMERIC, 'rewardOrReqMoney', NUM_CAST_INT ], // moneyrewarded
4 => [parent::CR_CALLBACK, 'cbSpellRewards', null, null], // spellrewarded [yn]
5 => [parent::CR_FLAG, 'flags', QUEST_FLAG_SHARABLE ], // sharable
6 => [parent::CR_NUMERIC, 'timeLimit', NUM_CAST_INT ], // timer
7 => [parent::CR_FLAG, 'cuFlags', QUEST_CU_FIRST_SERIES ], // firstquestseries
9 => [parent::CR_CALLBACK, 'cbEarnReputation', null, null], // objectiveearnrepwith [enum]
10 => [parent::CR_CALLBACK, 'cbReputation', '<', null], // decreasesrepwith
11 => [parent::CR_NUMERIC, 'suggestedPlayers', NUM_CAST_INT ], // suggestedplayers
15 => [parent::CR_FLAG, 'cuFlags', QUEST_CU_LAST_SERIES ], // lastquestseries
16 => [parent::CR_FLAG, 'cuFlags', QUEST_CU_PART_OF_SERIES ], // partseries
18 => [parent::CR_FLAG, 'cuFlags', CUSTOM_HAS_SCREENSHOT ], // hasscreenshots
19 => [parent::CR_CALLBACK, 'cbQuestRelation', 0x1, null], // startsfrom [enum]
21 => [parent::CR_CALLBACK, 'cbQuestRelation', 0x2, null], // endsat [enum]
22 => [parent::CR_CALLBACK, 'cbItemRewards', null, null], // itemrewards [op] [int]
23 => [parent::CR_CALLBACK, 'cbItemChoices', null, null], // itemchoices [op] [int]
24 => [parent::CR_CALLBACK, 'cbLacksStartEnd', null, null], // lacksstartend [yn]
25 => [parent::CR_FLAG, 'cuFlags', CUSTOM_HAS_COMMENT ], // hascomments
27 => [parent::CR_FLAG, 'flags', QUEST_FLAG_DAILY ], // daily
28 => [parent::CR_FLAG, 'flags', QUEST_FLAG_WEEKLY ], // weekly
29 => [parent::CR_CALLBACK, 'cbRepeatable', null ], // repeatable
30 => [parent::CR_NUMERIC, 'id', NUM_CAST_INT, true], // id
33 => [parent::CR_ENUM, 'e.holidayId', true, true], // relatedevent
34 => [parent::CR_CALLBACK, 'cbAvailable', null, null], // availabletoplayers [yn]
36 => [parent::CR_FLAG, 'cuFlags', CUSTOM_HAS_VIDEO ], // hasvideos
37 => [parent::CR_CALLBACK, 'cbClassSpec', null, null], // classspecific [enum]
38 => [parent::CR_CALLBACK, 'cbRaceSpec', null, null], // racespecific [enum]
42 => [parent::CR_STAFFFLAG, 'flags' ], // flags
43 => [parent::CR_CALLBACK, 'cbCurrencyReward', null, null], // currencyrewarded [enum]
44 => [parent::CR_CALLBACK, 'cbLoremaster', null, null], // countsforloremaster_stc [yn]
45 => [parent::CR_BOOLEAN, 'rewardTitleId' ] // titlerewarded
);
protected function createSQLForCriterium(&$cr)
{
if (in_array($cr[0], array_keys($this->genericFilter)))
if ($genCr = $this->genericCriterion($cr))
return $genCr;
protected array $inputFields = array(
'cr' => [parent::V_RANGE, [1, 45], true ], // criteria ids
'crs' => [parent::V_LIST, [parent::ENUM_NONE, parent::ENUM_ANY, [0, 99999]], true ], // criteria operators
'crv' => [parent::V_REGEX, parent::PATTERN_INT, true ], // criteria values - only numerals
'na' => [parent::V_REGEX, parent::PATTERN_NAME, false], // name / text - only printable chars, no delimiter
'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
);
unset($cr);
$this->error = true;
return [1];
}
public array $extraOpts = [];
protected function createSQLForValues()
protected function createSQLForValues() : array
{
$parts = [];
$_v = $this->fiData['v'];
$_v = $this->values;
// name
if (isset($_v['na']))
if ($_v['na'])
{
$_ = [];
if (isset($_v['ex']) && $_v['ex'] == 'on')
$_ = $this->modularizeString(['name_loc'.User::$localeId, 'objectives_loc'.User::$localeId, 'details_loc'.User::$localeId]);
if ($_v['ex'] == 'on')
$_ = $this->tokenizeString(['name_loc'.Lang::getLocale()->value, 'objectives_loc'.Lang::getLocale()->value, 'details_loc'.Lang::getLocale()->value]);
else
$_ = $this->modularizeString(['name_loc'.User::$localeId]);
$_ = $this->tokenizeString(['name_loc'.Lang::getLocale()->value]);
if ($_)
$parts[] = $_;
}
// level min
if (isset($_v['minle']))
if ($_v['minle'])
$parts[] = ['level', $_v['minle'], '>=']; // not considering quests that are always at player level (-1)
// level max
if (isset($_v['maxle']))
if ($_v['maxle'])
$parts[] = ['level', $_v['maxle'], '<='];
// reqLevel min
if (isset($_v['minrl']))
if ($_v['minrl'])
$parts[] = ['minLevel', $_v['minrl'], '>=']; // ignoring maxLevel
// reqLevel max
if (isset($_v['maxrl']))
if ($_v['maxrl'])
$parts[] = ['minLevel', $_v['maxrl'], '<=']; // ignoring maxLevel
// side
if (isset($_v['si']))
if ($_v['si'])
{
$ex = [['reqRaceMask', RACE_MASK_ALL, '&'], RACE_MASK_ALL, '!'];
$notEx = ['OR', ['reqRaceMask', 0], [['reqRaceMask', RACE_MASK_ALL, '&'], RACE_MASK_ALL]];
$excl = [['reqRaceMask', ChrRace::MASK_ALL, '&'], ChrRace::MASK_ALL, '!'];
$incl = ['OR', ['reqRaceMask', 0], [['reqRaceMask', ChrRace::MASK_ALL, '&'], ChrRace::MASK_ALL]];
switch ($_v['si'])
$parts[] = match ($_v['si'])
{
case 3:
$parts[] = $notEx;
break;
case 2:
$parts[] = ['OR', $notEx, ['reqRaceMask', RACE_MASK_HORDE, '&']];
break;
case -2:
$parts[] = ['AND', $ex, ['reqRaceMask', RACE_MASK_HORDE, '&']];
break;
case 1:
$parts[] = ['OR', $notEx, ['reqRaceMask', RACE_MASK_ALLIANCE, '&']];
break;
case -1:
$parts[] = ['AND', $ex, ['reqRaceMask', RACE_MASK_ALLIANCE, '&']];
break;
}
SIDE_BOTH => $incl,
SIDE_HORDE => ['OR', $incl, ['reqRaceMask', ChrRace::MASK_HORDE, '&']],
-SIDE_HORDE => ['AND', $excl, ['reqRaceMask', ChrRace::MASK_HORDE, '&']],
SIDE_ALLIANCE => ['OR', $incl, ['reqRaceMask', ChrRace::MASK_ALLIANCE, '&']],
-SIDE_ALLIANCE => ['AND', $excl, ['reqRaceMask', ChrRace::MASK_ALLIANCE, '&']]
};
}
// type [list]
if (isset($_v['ty']))
if ($_v['ty'] !== null)
$parts[] = ['type', $_v['ty']];
return $parts;
}
protected function cbReputation($cr, $sign)
protected function cbReputation(int $cr, int $crs, string $crv, string $sign) : ?array
{
if (!Util::checkNumeric($cr[1], NUM_REQ_INT) || $cr[1] <= 0)
return false;
if (!Util::checkNumeric($crs, NUM_CAST_INT))
return null;
if ($_ = DB::Aowow()->selectRow('SELECT * FROM ?_factions WHERE id = ?d', $cr[1]))
$this->formData['reputationCols'][] = [$cr[1], Util::localizedString($_, 'name')];
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', $cr[1]], ['rewardFactionValue1', 0, $sign]],
['AND', ['rewardFactionId2', $cr[1]], ['rewardFactionValue2', 0, $sign]],
['AND', ['rewardFactionId3', $cr[1]], ['rewardFactionValue3', 0, $sign]],
['AND', ['rewardFactionId4', $cr[1]], ['rewardFactionValue4', 0, $sign]],
['AND', ['rewardFactionId5', $cr[1]], ['rewardFactionValue5', 0, $sign]]
['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($cr, $flags)
protected function cbQuestRelation(int $cr, int $crs, string $crv, $flags) : ?array
{
switch ($cr[1])
return match ($crs)
{
case 1: // npc
return ['AND', ['qse.type', Type::NPC], ['qse.method', $flags, '&']];
case 2: // object
return ['AND', ['qse.type', Type::OBJECT], ['qse.method', $flags, '&']];
case 3: // item
return ['AND', ['qse.type', Type::ITEM], ['qse.method', $flags, '&']];
}
return false;
Type::NPC,
Type::OBJECT,
Type::ITEM => ['AND', ['qse.type', $crs], ['qse.method', $flags, '&']],
default => null
};
}
protected function cbCurrencyReward($cr)
protected function cbCurrencyReward(int $cr, int $crs, string $crv) : ?array
{
if (!Util::checkNumeric($cr[1], NUM_REQ_INT) || $cr[1] <= 0)
return false;
if (!Util::checkNumeric($crs, NUM_CAST_INT))
return null;
if (!in_array($crs, $this->enums[$cr]))
return null;
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]]
['rewardItemId1', $crs], ['rewardItemId2', $crs], ['rewardItemId3', $crs], ['rewardItemId4', $crs],
['rewardChoiceItemId1', $crs], ['rewardChoiceItemId2', $crs], ['rewardChoiceItemId3', $crs], ['rewardChoiceItemId4', $crs], ['rewardChoiceItemId5', $crs], ['rewardChoiceItemId6', $crs]
];
}
protected function cbAvailable($cr)
protected function cbAvailable(int $cr, int $crs, string $crv) : ?array
{
if (!$this->int2Bool($cr[1]))
return false;
if (!$this->int2Bool($crs))
return null;
if ($cr[1])
if ($crs)
return [['cuFlags', CUSTOM_UNAVAILABLE | CUSTOM_DISABLED, '&'], 0];
else
return ['cuFlags', CUSTOM_UNAVAILABLE | CUSTOM_DISABLED, '&'];
}
protected function cbItemChoices($cr)
protected function cbRepeatable(int $cr, int $crs, string $crv) : ?array
{
if (!Util::checkNumeric($cr[2], NUM_CAST_INT) || !$this->int2Op($cr[1]))
return false;
if (!$this->int2Bool($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 '.$cr[1].' '.$cr[2];
return [1];
}
protected function cbItemRewards($cr)
{
if (!Util::checkNumeric($cr[2], NUM_CAST_INT) || !$this->int2Op($cr[1]))
return false;
$this->extraOpts['q']['s'][] = ', (IF(rewardItemId1, 1, 0) + IF(rewardItemId2, 1, 0) + IF(rewardItemId3, 1, 0) + IF(rewardItemId4, 1, 0)) as numRewards';
$this->extraOpts['q']['h'][] = 'numRewards '.$cr[1].' '.$cr[2];
return [1];
}
protected function cbLoremaster($cr)
{
if (!$this->int2Bool($cr[1]))
return false;
if ($cr[1])
return ['AND', ['zoneOrSort', 0, '>'], [['flags', QUEST_FLAG_DAILY | QUEST_FLAG_WEEKLY | QUEST_FLAG_REPEATABLE , '&'], 0], [['specialFlags', QUEST_FLAG_SPECIAL_REPEATABLE | QUEST_FLAG_SPECIAL_MONTHLY , '&'], 0]];
if ($crs)
return ['OR', ['flags', QUEST_FLAG_REPEATABLE, '&'], ['specialFlags', QUEST_FLAG_SPECIAL_REPEATABLE, '&']];
else
return ['OR', ['zoneOrSort', 0, '<'], ['flags', QUEST_FLAG_DAILY | QUEST_FLAG_WEEKLY | QUEST_FLAG_REPEATABLE , '&'], ['specialFlags', QUEST_FLAG_SPECIAL_REPEATABLE | QUEST_FLAG_SPECIAL_MONTHLY , '&']];;
return ['AND', [['flags', QUEST_FLAG_REPEATABLE, '&'], 0], [['specialFlags', QUEST_FLAG_SPECIAL_REPEATABLE, '&'], 0]];
}
protected function cbSpellRewards($cr)
protected function cbItemChoices(int $cr, int $crs, string $crv) : ?array
{
if (!$this->int2Bool($cr[1]))
return false;
if (!Util::checkNumeric($crv, NUM_CAST_INT) || !$this->int2Op($crs))
return null;
if ($cr[1])
return ['OR', ['sourceSpellId', 0, '>'], ['rewardSpell', 0, '>'], ['rsc.effect1Id', SpellList::$effects['teach']], ['rsc.effect2Id', SpellList::$effects['teach']], ['rsc.effect3Id', SpellList::$effects['teach']]];
$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($cr)
protected function cbEarnReputation(int $cr, int $crs, string $crv) : ?array
{
if (!Util::checkNumeric($cr[1], NUM_REQ_INT))
return false;
if (!Util::checkNumeric($crs, NUM_CAST_INT))
return null;
if ($cr[1] > 0)
return ['OR', ['reqFactionId1', $cr[1]], ['reqFactionId2', $cr[1]]];
else if ($cr[1] == FILTER_ENUM_ANY) // any
if ($crs == parent::ENUM_ANY)
return ['OR', ['reqFactionId1', 0, '>'], ['reqFactionId2', 0, '>']];
else if ($cr[1] == FILTER_ENUM_NONE) // none
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 false;
return null;
}
protected function cbClassSpec($cr)
protected function cbClassSpec(int $cr, int $crs, string $crv) : ?array
{
if (!isset($this->enums[$cr[0]][$cr[1]]))
return false;
if (!isset($this->enums[$cr][$crs]))
return null;
$_ = $this->enums[$cr[0]][$cr[1]];
$_ = $this->enums[$cr][$crs];
if ($_ === true)
return ['AND', ['reqClassMask', 0, '!'], [['reqClassMask', CLASS_MASK_ALL, '&'], CLASS_MASK_ALL, '!']];
return ['AND', ['reqClassMask', 0, '!'], [['reqClassMask', ChrClass::MASK_ALL, '&'], ChrClass::MASK_ALL, '!']];
else if ($_ === false)
return ['OR', ['reqClassMask', 0], [['reqClassMask', CLASS_MASK_ALL, '&'], CLASS_MASK_ALL]];
return ['OR', ['reqClassMask', 0], [['reqClassMask', ChrClass::MASK_ALL, '&'], ChrClass::MASK_ALL]];
else if (is_int($_))
return ['AND', ['reqClassMask', (1 << ($_ - 1)), '&'], [['reqClassMask', CLASS_MASK_ALL, '&'], CLASS_MASK_ALL, '!']];
return ['AND', ['reqClassMask', ChrClass::from($_)->toMask(), '&'], [['reqClassMask', ChrClass::MASK_ALL, '&'], ChrClass::MASK_ALL, '!']];
return false;
return null;
}
protected function cbRaceSpec($cr)
protected function cbRaceSpec(int $cr, int $crs, string $crv) : ?array
{
if (!isset($this->enums[$cr[0]][$cr[1]]))
return false;
if (!isset($this->enums[$cr][$crs]))
return null;
$_ = $this->enums[$cr[0]][$cr[1]];
$_ = $this->enums[$cr][$crs];
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, '!']];
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', RACE_MASK_ALL], ['reqRaceMask', RACE_MASK_ALLIANCE], ['reqRaceMask', RACE_MASK_HORDE]];
return ['OR', ['reqRaceMask', 0], ['reqRaceMask', ChrRace::MASK_ALL], ['reqRaceMask', ChrRace::MASK_ALLIANCE], ['reqRaceMask', ChrRace::MASK_HORDE]];
else if (is_int($_))
return ['AND', ['reqRaceMask', (1 << ($_ - 1)), '&'], [['reqRaceMask', RACE_MASK_ALLIANCE, '&'], RACE_MASK_ALLIANCE, '!'], [['reqRaceMask', RACE_MASK_HORDE, '&'], RACE_MASK_HORDE, '!']];
return ['AND', ['reqRaceMask', ChrRace::from($_)->toMask(), '&'], [['reqRaceMask', ChrRace::MASK_ALLIANCE, '&'], ChrRace::MASK_ALLIANCE, '!'], [['reqRaceMask', ChrRace::MASK_HORDE, '&'], ChrRace::MASK_HORDE, '!']];
return false;
return null;
}
protected function cbLacksStartEnd($cr)
protected function cbLacksStartEnd(int $cr, int $crs, string $crv) : ?array
{
if (!$this->int2Bool($cr[1]))
return false;
if (!$this->int2Bool($crs))
return null;
$missing = DB::Aowow()->selectCol('SELECT questId, max(method) a, min(method) b FROM ?_quests_startend GROUP BY questId HAVING (a | b) <> 3');
if ($cr[1])
$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,5 +1,7 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
@@ -16,9 +18,9 @@ class SkillList extends BaseType
'ic' => ['j' => ['?_icons ic ON ic.id = sl.iconId', true], 's' => ', ic.name AS iconString'],
);
public function __construct($conditions = [])
public function __construct(array $conditions = [], array $miscData = [])
{
parent::__construct($conditions);
parent::__construct($conditions, $miscData);
// post processing
foreach ($this->iterate() as &$_curTpl)
@@ -34,7 +36,7 @@ class SkillList extends BaseType
}
if (!$_curTpl['iconId'])
$_curTpl['iconString'] = 'inv_misc_questionmark';
$_curTpl['iconString'] = DEFAULT_ICON;
}
}

View File

@@ -1,5 +1,7 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
@@ -21,9 +23,9 @@ class SoundList extends BaseType
SOUND_TYPE_MP3 => 'audio/mpeg'
);
public function __construct($conditions = [])
public function __construct(array $conditions = [], array $miscData = [])
{
parent::__construct($conditions);
parent::__construct($conditions, $miscData);
// post processing
foreach ($this->iterate() as $id => &$_curTpl)
@@ -43,17 +45,17 @@ class SoundList extends BaseType
if ($this->fileBuffer)
{
$files = DB::Aowow()->select('SELECT id AS ARRAY_KEY, `id`, `file` AS title, `type`, `path` FROM ?_sounds_files sf WHERE id IN (?a)', array_keys($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']);
// skipp file extension
// skip file extension
$data['title'] = substr($data['title'], 0, -4);
// enum to string
$data['type'] = self::$fileTypes[$data['type']];
// get real url
$data['url'] = STATIC_URL . '/wowsounds/' . $data['id'];
$data['url'] = Cfg::get('STATIC_URL') . '/wowsounds/' . $data['id'];
// v push v
$this->fileBuffer[$id] = $data;
}
@@ -96,32 +98,24 @@ class SoundList extends BaseType
class SoundListFilter extends Filter
{
// fieldId => [checkType, checkValue[, fieldIsArray]]
protected $inputFields = array(
'na' => [FILTER_V_REGEX, '/[\p{C};%\\\\]/ui', false], // name - only printable chars, no delimiter
'ty' => [FILTER_V_LIST, [[1, 4], 6, 9, 10, 12, 13, 14, 16, 17, [19, 23], [25, 31], 50, 52, 53], true ] // type
protected 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
);
// we have no criteria for this one...
protected function createSQLForCriterium(&$cr)
{
unset($cr);
$this->error = true;
return [1];
}
protected function createSQLForValues()
protected function createSQLForValues() : array
{
$parts = [];
$_v = &$this->fiData['v'];
$_v = &$this->values;
// name [str]
if (isset($_v['na']))
if ($_ = $this->modularizeString(['name']))
if ($_v['na'])
if ($_ = $this->tokenizeString(['name']))
$parts[] = $_;
// type [list]
if (isset($_v['ty']))
if ($_v['ty'])
$parts[] = ['cat', $_v['ty']];
return $parts;

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,7 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
@@ -14,40 +16,41 @@ class TitleList extends BaseType
public $sources = [];
protected $queryBase = 'SELECT t.*, id AS ARRAY_KEY FROM ?_titles t';
protected $queryBase = 'SELECT t.*, t.id AS ARRAY_KEY FROM ?_titles t';
protected $queryOpts = array(
't' => [['src']], // 11: Type::TITLE
'src' => ['j' => ['?_source src ON type = 11 AND typeId = t.id', true], 's' => ', src13, moreType, moreTypeId']
);
public function __construct($conditions = [])
public function __construct(array $conditions = [], array $miscData = [])
{
parent::__construct($conditions);
parent::__construct($conditions, $miscData);
// post processing
foreach ($this->iterate() as $id => &$_curTpl)
{
// preparse sources - notice: under this system titles can't have more than one source (or two for achivements), which is enough for standard TC cases but may break custom cases
if ($_curTpl['moreType'] == Type::ACHIEVEMENT)
$this->sources[$this->id][12][] = $_curTpl['moreTypeId'];
$this->sources[$this->id][SRC_ACHIEVEMENT][] = $_curTpl['moreTypeId'];
else if ($_curTpl['moreType'] == Type::QUEST)
$this->sources[$this->id][4][] = $_curTpl['moreTypeId'];
$this->sources[$this->id][SRC_QUEST][] = $_curTpl['moreTypeId'];
else if ($_curTpl['src13'])
$this->sources[$this->id][13][] = $_curTpl['src13'];
$this->sources[$this->id][SRC_CUSTOM_STRING][] = $_curTpl['src13'];
// titles display up to two achievements at once
if ($_curTpl['src12Ext'])
$this->sources[$this->id][12][] = $_curTpl['src12Ext'];
$this->sources[$this->id][SRC_ACHIEVEMENT][] = $_curTpl['src12Ext'];
unset($_curTpl['src12Ext']);
unset($_curTpl['moreType']);
unset($_curTpl['moreTypeId']);
unset($_curTpl['src3']);
// shorthand for more generic access
foreach (Util::$localeStrings as $i => $str)
if ($str)
$_curTpl['name_loc'.$i] = trim(str_replace('%s', '', $_curTpl['male_loc'.$i]));
// shorthand for more generic access; required by CommunityContent to determine subject
foreach (Locale::cases() as $loc)
if ($loc->validate())
$_curTpl['name'] = new LocString($_curTpl, 'male', fn($x) => trim(str_replace('%s', '', $x)));
// $_curTpl['name_loc'.$loc->value] = trim(str_replace('%s', '', $_curTpl['male_loc'.$loc->value]));
}
}
@@ -93,9 +96,9 @@ class TitleList extends BaseType
private function createSource()
{
$sources = array(
4 => [], // Quest
12 => [], // Achievements
13 => [] // simple text
SRC_QUEST => [],
SRC_ACHIEVEMENT => [],
SRC_CUSTOM_STRING => []
);
foreach ($this->iterate() as $__)
@@ -109,43 +112,43 @@ class TitleList extends BaseType
}
// fill in the details
if (!empty($sources[4]))
$sources[4] = (new QuestList(array(['id', $sources[4]])))->getSourceData();
if (!empty($sources[SRC_QUEST]))
$sources[SRC_QUEST] = (new QuestList(array(['id', $sources[SRC_QUEST]])))->getSourceData();
if (!empty($sources[12]))
$sources[12] = (new AchievementList(array(['id', $sources[12]])))->getSourceData();
if (!empty($sources[SRC_ACHIEVEMENT]))
$sources[SRC_ACHIEVEMENT] = (new AchievementList(array(['id', $sources[SRC_ACHIEVEMENT]])))->getSourceData();
foreach ($this->sources as $Id => $src)
{
$tmp = [];
// Quest-source
if (isset($src[4]))
if (isset($src[SRC_QUEST]))
{
foreach ($src[4] as $s)
foreach ($src[SRC_QUEST] as $s)
{
if (isset($sources[4][$s]['s']))
$this->faction2Side($sources[4][$s]['s']);
if (isset($sources[SRC_QUEST][$s]['s']))
$this->faction2Side($sources[SRC_QUEST][$s]['s']);
$tmp[4][] = $sources[4][$s];
$tmp[SRC_QUEST][] = $sources[SRC_QUEST][$s];
}
}
// Achievement-source
if (isset($src[12]))
if (isset($src[SRC_ACHIEVEMENT]))
{
foreach ($src[12] as $s)
foreach ($src[SRC_ACHIEVEMENT] as $s)
{
if (isset($sources[12][$s]['s']))
$this->faction2Side($sources[12][$s]['s']);
if (isset($sources[SRC_ACHIEVEMENT][$s]['s']))
$this->faction2Side($sources[SRC_ACHIEVEMENT][$s]['s']);
$tmp[12][] = $sources[12][$s];
$tmp[SRC_ACHIEVEMENT][] = $sources[SRC_ACHIEVEMENT][$s];
}
}
// other source (only one item possible, so no iteration needed)
if (isset($src[13]))
$tmp[13] = [Lang::game('pvpSources', $this->sources[$Id][13][0])];
if (isset($src[SRC_CUSTOM_STRING]))
$tmp[SRC_CUSTOM_STRING] = [Lang::game('pvpSources', $Id)];
$this->templates[$Id]['source'] = $tmp;
}

View File

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

View File

@@ -1,5 +1,7 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
@@ -10,15 +12,15 @@ class WorldEventList extends BaseType
public static $brickFile = 'event';
public static $dataTable = '?_events';
protected $queryBase = 'SELECT e.*, h.*, e.description AS nameINT, e.id AS id, e.id AS ARRAY_KEY FROM ?_events e';
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 $queryOpts = array(
'e' => [['h']],
'h' => ['j' => ['?_holidays h ON e.holidayId = h.id', true], 'o' => '-e.id ASC']
);
public function __construct($conditions = [])
public function __construct(array $conditions = [], array $miscData = [])
{
parent::__construct($conditions);
parent::__construct($conditions, $miscData);
// unseting elements while we iterate over the array will cause the pointer to reset
$replace = [];
@@ -60,7 +62,7 @@ class WorldEventList extends BaseType
foreach ($replace as $old => $data)
{
unset($this->templates[$old]);
$this->templates[$data['id']] = $data;
$this->templates[$data['eventId']] = $data;
}
}

View File

@@ -1,5 +1,7 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
@@ -12,9 +14,9 @@ class ZoneList extends BaseType
public static $brickFile = 'zone';
public static $dataTable = '?_zones';
protected $queryBase = 'SELECT z.*, id AS ARRAY_KEY FROM ?_zones z';
protected $queryBase = 'SELECT z.*, z.id AS ARRAY_KEY FROM ?_zones z';
public function __construct($conditions = [], $miscData = null)
public function __construct(array $conditions = [], array $miscData = [])
{
parent::__construct($conditions, $miscData);

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,16 +1,13 @@
<?php
require 'includes/shared.php';
namespace Aowow;
require 'includes/kernel.php';
if (CLI)
die("this script must not be run from CLI.\nto setup aowow use 'php aowow'\n");
// maybe add additional setup checks?
if (!DB::isConnectable(DB_AOWOW) || !DB::isConnectable(DB_WORLD))
(new GenericPage($pageCall))->maintenance();
$altClass = '';
switch ($pageCall)
{
@@ -94,13 +91,16 @@ switch ($pageCall)
case 'edit': // guide editor: targeted by QQ fileuploader, detail-page article editor
case 'get-description': // guide editor: shorten fulltext into description
case 'filter': // pre-evaluate filter POST-data; sanitize and forward as GET-data
case 'go-to-reply': // find page the reply is on and forward
if ($pageCall == 'go-to-reply')
$altClass = 'go-to-comment';
case 'go-to-comment': // find page the comment is on and forward
case 'locale': // subdomain-workaround, change the language
$cleanName = str_replace(['-', '_'], '', ucFirst($altClass ?: $pageCall));
try // can it be handled as ajax?
{
$out = '';
$class = 'Ajax'.$cleanName;
$class = __NAMESPACE__.'\\'.'Ajax'.$cleanName;
$ajax = new $class(explode('.', $pageParam));
if ($ajax->handle($out))
@@ -116,17 +116,17 @@ switch ($pageCall)
}
}
else
throw new Exception('not handled as ajax');
throw new \Exception('not handled as ajax');
}
catch (Exception $e) // no, apparently not..
catch (\Exception $e) // no, apparently not..
{
$class = $cleanName.'Page';
$class = __NAMESPACE__.'\\'.$cleanName.'Page';
$classInstance = new $class($pageCall, $pageParam);
if (is_callable([$classInstance, 'display']))
$classInstance->display();
else if (isset($_GET['power']))
die('$WowheadPower.register(0, '.User::$localeId.', {})');
die('$WowheadPower.register(0, '.Lang::getLocale()->value.', {})');
else // in conjunction with a proper rewriteRule in .htaccess...
(new GenericPage($pageCall))->error();
}
@@ -158,7 +158,7 @@ switch ($pageCall)
break;
default: // unk parameter given -> ErrorPage
if (isset($_GET['power']))
die('$WowheadPower.register(0, '.User::$localeId.', {})');
die('$WowheadPower.register(0, '.Lang::getLocale()->value.', {})');
else // in conjunction with a proper rewriteRule in .htaccess...
(new GenericPage($pageCall))->error();
break;

View File

@@ -1,8 +1,14 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
class Lang
{
private static $timeUnits;
private static $lang;
private static $main;
private static $account;
private static $user;
@@ -40,21 +46,25 @@ class Lang
private static $emote;
private static $enchantment;
private static $locales = array(
LOCALE_EN => 'English',
LOCALE_FR => 'Français',
LOCALE_DE => 'Deutsch',
LOCALE_CN => '简体中文',
LOCALE_ES => 'Español',
LOCALE_RU => 'Русский'
);
private static ?Locale $locale = null;
public static function load($loc)
public const FMT_RAW = 0;
public const FMT_HTML = 1;
public const FMT_MARKUP = 2;
public const CONCAT_NONE = 0;
public const CONCAT_AND = 1;
public const CONCAT_OR = 2;
public static function load(Locale $loc) : void
{
if (!file_exists('localization/locale_'.$loc.'.php'))
die('File for localization '.strToUpper($loc).' not found.');
if (self::$locale == $loc)
return;
if (!file_exists('localization/locale_'.$loc->json().'.php'))
die('File for locale '.$loc->name.' not found.');
else
require 'localization/locale_'.$loc.'.php';
require 'localization/locale_'.$loc->json().'.php';
foreach ($lang as $k => $v)
self::$$k = $v;
@@ -63,72 +73,79 @@ class Lang
self::$item['cat'][2] = [self::$item['cat'][2], self::$spell['weaponSubClass']];
self::$item['cat'][2][1][14] .= ' ('.self::$item['cat'][2][0].')';
self::$main['moreTitles']['privilege'] = self::$privileges['_privileges'];
self::$locale = $loc;
}
public static function __callStatic($prop, $args)
public static function getLocale() : Locale
{
return self::$locale;
}
public static function __callStatic(string $prop, ?array $args = []) : string|array|null
{
$vspfArgs = [];
foreach ($args as $i => $arg)
{
if (!is_array($arg))
continue;
$vspfArgs = $arg;
unset($args[$i]);
}
if (($x = self::exist($prop, ...$args)) !== null)
return self::vspf($x, $vspfArgs);
$dbt = debug_backtrace()[0];
$file = explode(DIRECTORY_SEPARATOR, $dbt['file']);
trigger_error('Lang - undefined property Lang::$'.$prop.'[\''.implode('\'][\'', $args).'\'], called in '.array_pop($file).':'.$dbt['line'], E_USER_WARNING);
return null;
}
public static function exist(string $prop, string ...$args) : string|array|null
{
if (!isset(self::$$prop))
{
$dbt = debug_backtrace()[0];
$file = explode(DIRECTORY_SEPARATOR, $dbt['file']);
trigger_error('Lang - tried to use undefined property Lang::$'.$prop.', called in '.array_pop($file).':'.$dbt['line'], E_USER_WARNING);
return null;
}
$vspfArgs = [];
$var = self::$$prop;
foreach ($args as $arg)
$ref = self::$$prop;
foreach ($args as $a)
{
if (is_array($arg))
{
$vspfArgs = $arg;
continue;
}
else if (!isset($var[$arg]))
{
$dbt = debug_backtrace()[0];
$file = explode(DIRECTORY_SEPARATOR, $dbt['file']);
trigger_error('Lang - undefined property Lang::$'.$prop.'[\''.implode('\'][\'', $args).'\'], called in '.array_pop($file).':'.$dbt['line'], E_USER_WARNING);
if (!isset($ref[$a]))
return null;
}
$var = $var[$arg];
$ref = $ref[$a];
}
// meh :x
if ($var === null && $prop == 'spell' && count($args) == 1)
{
if ($args[0] == 'effects')
$var = self::$$prop['unkEffect'];
else if ($args[0] == 'auras')
$var = self::$$prop['unkAura'];
}
return self::vspf($var, $vspfArgs);
return $ref;
}
public static function concat($args, $useAnd = true, $callback = null)
public static function concat(array $args, int $concat = self::CONCAT_AND, ?callable $callback = null) : string
{
$b = '';
$i = 0;
$n = count($args);
foreach ($args as $k => $arg)
$buff = '';
$callback ??= fn($x) => $x;
reset($args);
if (count($args) < 2)
return $callback(current($args), key($args));
do
{
if (is_callable($callback))
$b .= $callback($arg, $k);
$item = $callback(current($args), key($args));
$arg = next($args);
if ($arg !== false || $concat == self::CONCAT_NONE)
$buff .= ', '.$item;
else if ($concat == self::CONCAT_AND)
$buff .= self::main('and').' '.$item;
else
$b .= $arg;
if ($n > 1 && $i < ($n - 2))
$b .= ', ';
else if ($n > 1 && $i == $n - 2)
$b .= Lang::main($useAnd ? 'and' : 'or');
$i++;
$buff .= self::main('or').' '.$item;
}
while ($arg !== false);
return $b;
return substr($buff, 2);
}
// truncate string after X chars. If X is inside a word truncate behind it.
@@ -140,26 +157,24 @@ class Lang
// limit whitespaces to one at a time
$text = preg_replace('/\s+/', ' ', trim($text));
if ($len > 0 && mb_strlen($text) > $len)
{
$n = 0;
$b = [];
$parts = explode(' ', $text);
while ($n < $len && $parts)
{
$_ = array_shift($parts);
$n += mb_strlen($_);
$b[] = $_;
}
if ($len <= 0 || mb_strlen($text) <= $len)
return $text;
$text = implode(' ', $b).'…';
$n = 0;
$b = [];
$parts = explode(' ', $text);
while ($n < $len && $parts)
{
$_ = array_shift($parts);
$n += mb_strlen($_);
$b[] = $_;
}
return $text;
return implode(' ', $b).'…';
}
// add line breaks to string after X chars. If X is inside a word break behind it.
public static function breakTextClean(string $text, int $len = 30, bool $asHTML = true) : string
public static function breakTextClean(string $text, int $len = 30, int $fmt = self::FMT_HTML) : string
{
// remove line breaks
$text = strtr($text, ["\n" => ' ', "\r" => ' ']);
@@ -167,74 +182,81 @@ class Lang
// limit whitespaces to one at a time
$text = preg_replace('/\s+/', ' ', trim($text));
if ($len <= 0 || mb_strlen($text) <= $len)
return $text;
$row = [];
if ($len > 0 && mb_strlen($text) > $len)
$i = 0;
$n = 0;
foreach (explode(' ', $text) as $p)
{
$i = 0;
$row[$i][] = $p;
$n += (mb_strlen($p) + 1);
if ($n < $len)
continue;
$n = 0;
$parts = explode(' ', $text);
foreach ($parts as $p)
{
$row[$i][] = $p;
$n += (mb_strlen($p) + 1);
if ($n < $len)
continue;
$n = 0;
$i++;
}
foreach ($row as &$r)
$r = implode(' ', $r);
$i++;
}
foreach ($row as &$r)
$r = implode(' ', $r);
return implode($asHTML ? '<br />' : '[br]', $row);
$separator = match ($fmt)
{
self::FMT_HTML => '<br />',
self::FMT_MARKUP => '[br]',
self::FMT_RAW => "\n",
default => "\n"
};
return implode($separator, $row);
}
public static function sort($prop, $group, $method = SORT_NATURAL)
public static function sort(string $prop, string $group, int $method = SORT_NATURAL) : void
{
if (!isset(self::$$prop))
{
trigger_error('Lang::sort - tried to use undefined property Lang::$'.$prop, E_USER_WARNING);
return null;
return;
}
$var = &self::$$prop;
if (!isset($var[$group]))
{
trigger_error('Lang::sort - tried to use undefined property Lang::$'.$prop.'[\''.$group.'\']', E_USER_WARNING);
return null;
return;
}
asort($var[$group], $method);
}
// todo: expand
public static function getInfoBoxForFlags($flags)
public static function getInfoBoxForFlags(int $cuFlags) : array
{
$tmp = [];
if ($flags & CUSTOM_DISABLED)
if ($cuFlags & CUSTOM_DISABLED)
$tmp[] = '[tooltip name=disabledHint]'.Util::jsEscape(self::main('disabledHint')).'[/tooltip][span class=tip tooltip=disabledHint]'.Util::jsEscape(self::main('disabled')).'[/span]';
if ($flags & CUSTOM_SERVERSIDE)
if ($cuFlags & CUSTOM_SERVERSIDE)
$tmp[] = '[tooltip name=serversideHint]'.Util::jsEscape(self::main('serversideHint')).'[/tooltip][span class=tip tooltip=serversideHint]'.Util::jsEscape(self::main('serverside')).'[/span]';
if ($flags & CUSTOM_UNAVAILABLE)
if ($cuFlags & CUSTOM_UNAVAILABLE)
$tmp[] = self::main('unavailable');
if ($flags & CUSTOM_EXCLUDE_FOR_LISTVIEW && User::isInGroup(U_GROUP_STAFF))
if ($cuFlags & CUSTOM_EXCLUDE_FOR_LISTVIEW && User::isInGroup(U_GROUP_STAFF))
$tmp[] = '[tooltip name=excludedHint]This entry is excluded from lists and is not searchable.[/tooltip][span tooltip=excludedHint class="tip q10"]Hidden[/span]';
return $tmp;
}
public static function getLocks(int $lockId, ?array &$ids = [], bool $interactive = false, bool $asHTML = false) : array
public static function getLocks(int $lockId, ?array &$ids = [], bool $interactive = false, int $fmt = self::FMT_HTML) : array
{
$locks = [];
$ids = [];
$lock = DB::Aowow()->selectRow('SELECT * FROM ?_lock WHERE id = ?d', $lockId);
$lock = DB::Aowow()->selectRow('SELECT * FROM ?_lock WHERE `id` = ?d', $lockId);
if (!$lock)
return $locks;
@@ -250,13 +272,16 @@ class Lang
if (!$name)
continue;
if ($interactive && $asHTML)
$name = '<a class="q1" href="?item='.$prop.'">'.$name.'</a>';
else if ($interactive && !$asHTML)
if ($fmt == self::FMT_HTML)
$name = $interactive ? '<a class="q1" href="?item='.$prop.'">'.$name.'</a>' : '<span class="q1">'.$name.'</span>';
else if ($interactive && $fmt == self::FMT_MARKUP)
{
$name = '[item='.$prop.']';
$ids[Type::ITEM][] = $prop;
}
else
$name = $prop;
}
else if ($lock['type'.$i] == LOCK_TYPE_SKILL)
{
@@ -272,15 +297,17 @@ class Lang
2 => SKILL_HERBALISM,
3 => SKILL_MINING,
20 => SKILL_INSCRIPTION
);
);
if ($interactive && $asHTML)
$name = '<a href="?skill='.$skills[$prop].'">'.$name.'</a>';
else if ($interactive && !$asHTML)
if ($fmt == self::FMT_HTML)
$name = $interactive ? '<a href="?skill='.$skills[$prop].'">'.$name.'</a>' : '<span class="q1">'.$name.'</span>';
else if ($interactive && $fmt == self::FMT_MARKUP)
{
$name = '[skill='.$skills[$prop].']';
$ids[Type::SKILL][] = $skills[$prop];
}
else
$name = $skills[$prop];
if ($rank > 0)
$name .= ' ('.$rank.')';
@@ -288,13 +315,14 @@ class Lang
// Lockpicking
else if ($prop == 4)
{
if ($interactive && $asHTML)
$name = '<a href="?spell=1842">'.$name.'</a>';
else if ($interactive && !$asHTML)
if ($fmt == self::FMT_HTML)
$name = $interactive ? '<a href="?spell=1842">'.$name.'</a>' : '<span class="q1">'.$name.'</span>';
else if ($interactive && $fmt == self::FMT_MARKUP)
{
$name = '[spell=1842]';
$ids[Type::SPELL][] = 1842;
}
// else $name = $name
}
// exclude unusual stuff
else if (User::isInGroup(U_GROUP_STAFF))
@@ -314,14 +342,12 @@ class Lang
return $locks;
}
public static function getReputationLevelForPoints($pts)
public static function getReputationLevelForPoints(int $pts) : string
{
$_ = Game::getReputationLevelForPoints($pts);
return self::game('rep', $_);
return self::game('rep', Game::getReputationLevelForPoints($pts));
}
public static function getRequiredItems($class, $mask, $short = true)
public static function getRequiredItems(int $class, int $mask, bool $short = true) : string
{
if (!in_array($class, [ITEM_CLASS_MISC, ITEM_CLASS_ARMOR, ITEM_CLASS_WEAPON]))
return '';
@@ -363,7 +389,7 @@ class Lang
return implode(', ', $tmp);
}
public static function getStances($stanceMask)
public static function getStances(int $stanceMask) : string
{
$stanceMask &= 0xFF37F6FF; // clamp to available stances/forms..
@@ -383,12 +409,18 @@ class Lang
return implode(', ', $tmp);
}
public static function getMagicSchools($schoolMask)
public static function getMagicSchools(int $schoolMask, bool $short = false) : string
{
$schoolMask &= SPELL_ALL_SCHOOLS; // clamp to available schools..
$tmp = [];
$i = 0;
if ($short && $schoolMask == SPELL_ALL_SCHOOLS)
return self::main('all');
if ($short && $schoolMask == SPELL_MAGIC_SCHOOLS)
return self::main('all').' ('.self::game('dt', 1).')';
while ($schoolMask)
{
if ($schoolMask & (1 << $i))
@@ -402,93 +434,82 @@ class Lang
return implode(', ', $tmp);
}
public static function getClassString(int $classMask, array &$ids = [], bool $asHTML = true) : string
public static function getClassString(int $classMask, array &$ids = [], int $fmt = self::FMT_HTML) : string
{
$classMask &= CLASS_MASK_ALL; // clamp to available classes..
$classMask &= ChrClass::MASK_ALL; // clamp to available classes..
if ($classMask == CLASS_MASK_ALL) // available to all classes
return false;
if (!$classMask || $classMask == ChrClass::MASK_ALL)// available to all classes
return '';
$tmp = [];
$i = 1;
$base = $asHTML ? '<a href="?class=%d" class="c%1$d">%2$s</a>' : '[class=%d]';
$br = $asHTML ? '' : '[br]';
while ($classMask)
[$base, $br] = match ($fmt)
{
if ($classMask & (1 << ($i - 1)))
{
$tmp[$i] = (!fMod(count($tmp) + 1, 3) ? $br : null).sprintf($base, $i, self::game('cl', $i));
$classMask &= ~(1 << ($i - 1));
}
$i++;
}
self::FMT_HTML => ['<a href="?class=%1$d" class="c%1$d">%2$s</a>', ''],
self::FMT_MARKUP => ['[class=%1$d]', '[br]'],
self::FMT_RAW => ['%2$s', ''],
default => ['%2$s', '']
};
$tmp = [];
foreach (ChrClass::fromMask($classMask) as $c)
$tmp[$c] = (!fMod(count($tmp) + 1, 3) ? $br : null).sprintf($base, $c, self::game('cl', $c));
$ids = array_keys($tmp);
return implode(', ', $tmp);
}
public static function getRaceString(int $raceMask, array &$ids = [], bool $asHTML = true) : string
public static function getRaceString(int $raceMask, array &$ids = [], int $fmt = self::FMT_HTML) : string
{
$raceMask &= RACE_MASK_ALL; // clamp to available races..
$raceMask &= ChrRace::MASK_ALL; // clamp to available races..
if ($raceMask == RACE_MASK_ALL) // available to all races (we don't display 'both factions')
return false;
if (!$raceMask || $raceMask == ChrRace::MASK_ALL) // available to all races (we don't display 'both factions')
return '';
if (!$raceMask)
return false;
$tmp = [];
$i = 1;
$base = $asHTML ? '<a href="?race=%d" class="q1">%s</a>' : '[race=%d]';
$br = $asHTML ? '' : '[br]';
if ($raceMask == RACE_MASK_HORDE)
if ($raceMask == ChrRace::MASK_HORDE)
return self::game('ra', -2);
if ($raceMask == RACE_MASK_ALLIANCE)
if ($raceMask == ChrRace::MASK_ALLIANCE)
return self::game('ra', -1);
while ($raceMask)
[$base, $br] = match ($fmt)
{
if ($raceMask & (1 << ($i - 1)))
{
$tmp[$i] = (!fMod(count($tmp) + 1, 3) ? $br : null).sprintf($base, $i, self::game('ra', $i));
$raceMask &= ~(1 << ($i - 1));
}
$i++;
}
self::FMT_HTML => ['<a href="?race=%1$d" class="q1">%2$s</a>', ''],
self::FMT_MARKUP => ['[race=%1$d]', '[br]'],
self::FMT_RAW => ['%2$s', ''],
default => ['%2$s', '']
};
$tmp = [];
foreach (ChrRace::fromMask($raceMask) as $r)
$tmp[$r] = (!fMod(count($tmp) + 1, 3) ? $br : '').sprintf($base, $r, self::game('ra', $r));
$ids = array_keys($tmp);
return implode(', ', $tmp);
}
public static function formatSkillBreakpoints(array $bp, bool $html = false) : string
public static function formatSkillBreakpoints(array $bp, int $fmt = self::FMT_MARKUP) : string
{
$tmp = Lang::game('difficulty').Lang::main('colon');
$tmp = self::game('difficulty').self::main('colon');
$base = match ($fmt)
{
self::FMT_HTML => '<span class="r%1$d">%2$s</span> ',
self::FMT_MARKUP => '[color=r%1$d]%2$s[/color] ',
self::FMT_RAW => '%2$s ',
default => '%2$s '
};
for ($i = 0; $i < 4; $i++)
if (!empty($bp[$i]))
$tmp .= $html ? '<span class="r'.($i + 1).'">'.$bp[$i].'</span> ' : '[color=r'.($i + 1).']'.$bp[$i].'[/color] ';
$tmp .= sprintf($base, $i + 1, $bp[$i]);
return trim($tmp);
}
public static function nf($number, $decimals = 0, $no1k = false)
public static function nf(float $number, int $decimals = 0, bool $no1k = false) : string
{
// [decimal, thousand]
$seps = array(
LOCALE_EN => [',', '.'],
LOCALE_FR => [' ', ','],
LOCALE_DE => ['.', ','],
LOCALE_CN => [',', '.'],
LOCALE_ES => ['.', ','],
LOCALE_RU => [' ', ',']
);
return number_format($number, $decimals, $seps[User::$localeId][1], $no1k ? '' : $seps[User::$localeId][0]);
return number_format($number, $decimals, self::main('nfSeparators', 1), $no1k ? '' : self::main('nfSeparators', 0));
}
public static function typeName(int $type) : string
@@ -496,99 +517,287 @@ class Lang
return Util::ucFirst(self::game(Type::getFileString($type)));
}
public static function formatTime(int $msec, string $prop = 'game', string $src = 'timeAbbrev', bool $concat = false) : string
{
if ($msec < 0)
$msec = 0;
private static function vspf($var, $args)
$time = Util::parseTime($msec); // [$ms, $s, $m, $h, $d]
$mult = [0, 1000, 60, 60, 24];
$total = 0;
$ref = [];
$result = [];
if (is_array(self::$$prop[$src]))
$ref = &self::$$prop[$src];
else
{
trigger_error('Lang::formatTime - tried to access undefined property Lang::$'.$prop, E_USER_WARNING);
return '';
}
if (!$msec)
return self::vspf($ref[0], [0]);
if ($concat)
{
for ($i = 4; $i > 0; $i--)
{
$total += $time[$i];
if (isset($ref[$i]) && ($total || ($i == 1 && !$result)))
{
$result[] = self::vspf($ref[$i], [$total]);
$total = 0;
}
else
$total *= $mult[$i];
}
return implode(', ', $result);
}
for ($i = 4; $i > 0; $i--)
{
$total += $time[$i];
if (isset($ref[$i]) && ($total || $i == 1))
return self::vspf($ref[$i], [$total + ($time[$i-1] ?? 0) / $mult[$i]]);
else
$total *= $mult[$i];
}
return '';
}
private static function vspf(null|array|string $var, array $args = []) : null|array|string
{
if (is_array($var))
{
foreach ($var as &$v)
$v == self::vspf($v, $args);
$v = self::vspf($v, $args);
return $var;
}
if (!$var) // may be null or empty. Handled differently depending on context
return $var;
$var = Cfg::applyToString($var);
if ($args)
$var = vsprintf($var, $args);
// line break
// |n
$var = str_replace('|n', '<br />', $var);
return self::unescapeUISequences($var);
}
// color
// |c<aarrggbb><word>|r
$var = preg_replace('/\|cff([a-f0-9]{6})(.+?)\|r/i', '<span style="color: #$1;">$2</span>', $var);
/* Quoted from WoWWiki - UI Escape Sequences (https://wowwiki-archive.fandom.com/wiki/UI_escape_sequences)
* number |1singular;plural;
Will choose a word depending on whether the digit preceding it is 0/1 or not (i.e. 1,11,21 return the first string, as will 0,10,40). Note that unlike |4 singular and plural forms are separated by semi-colon.
// icon
// |T<imgPath>:0:0:0:-1|t - not used, skip if found
$var = preg_replace('/\|T[^\|]+\|t/', '', $var);
* |2text
Before vowels outputs d' (with apostrophe) and removes any leading spaces from text, otherwise outputs de (with trailing space)
// hyperlink
// |H<hyperlinkStruct>|h<name>|h - not used, truncate structure if found
$var = preg_replace('/\|H[^\|]+\|h([^\|]+)\|h/', '$1', $var);
* |3-formid(text)
Displays text declined to the specified form (index ranges from 1 to GetNumDeclensionSets()).
// french preposition : de
// |2 <word>
$var = preg_replace_callback('/\|2\s(\w)/i', function ($m) {
if (in_array(strtolower($m[1]), ['a', 'e', 'h', 'i', 'o', 'u']))
return "d'".$m[1];
else
return 'de '.$m[1];
}, $var);
* number |4singular:plural; -or- number |4singular:plural1:plural2;
Will choose a form based on the number preceding it. More than two forms (separated by colons) may be required by locale 8 (ruRU).
**/
// russian word cunjugation thingy
// |3-<number>(<word>)
$var = preg_replace_callback('/\|3-(\d)\(([^\)]+)\)/i', function ($m) {
switch ($m[0])
public static function unescapeUISequences(string $var, int $fmt = -1) : string
{
if (strpos($var, '|') === false)
return $var;
// line break |n
$var = preg_replace_callback('/\|n/i', function ($m) use ($fmt)
{
case 1: // seen cases
case 2:
case 3:
case 4:
case 5:
case 6:
case 7:
default: // passthrough .. unk case
return $m[1];
}
}, $var);
// numeric switch
// <number> |4<singular>:<plural>[:<plural2>];
$var = preg_replace_callback('/([\d\.\,]+)([^\d]*)\|4([^:]*):([^;]*);/i', function ($m) {
$plurals = explode(':', $m[4]);
$result = '';
if (count($plurals) == 2) // special case: ruRU
{
switch (substr($m[1], -1)) // check last digit of number
switch ($fmt)
{
case 1:
// but not 11 (teen number)
if (!in_array($m[1], [11]))
{
$result = $m[3];
break;
}
case 2:
case 3:
case 4:
// but not 12, 13, 14 (teen number) [11 is passthrough]
if (!in_array($m[1], [11, 12, 13, 14]))
{
$result = $plurals[0];
break;
}
break;
case -1: // default Lang::vspf case
case self::FMT_HTML:
return '<br />';
case self::FMT_MARKUP:
return '[br]';
case self::FMT_RAW:
default:
$result = $plurals[1];
return '';
}
}
else
$result = ($m[1] == 1 ? $m[3] : $plurals[0]);
}, $var);
return $m[1].$m[2].$result;
}, $var);
// color |c<aarrggbb><word>|r
$var = preg_replace_callback('/\|c([[:xdigit:]]{2})([[:xdigit:]]{6})(.+?)\|r/is', function ($m) use ($fmt)
{
[$_, $a, $rgb, $text] = $m;
switch ($fmt)
{
case -1: // default Lang::vspf case
case self::FMT_HTML:
return sprintf('<span style="color: #%1$s%2$s;">%3$s</span>', $rgb, $a, $text);
case self::FMT_MARKUP:
return sprintf('[span color=#%1$s]%3$s[/span]', $rgb, $a, $text); // doesn't support alpha
case self::FMT_RAW:
default:
return $text;
}
}, $var);
// icon |T<imgPath+File.blp>:0:0:0:-1|t
$var = preg_replace_callback('/\|T([\w]+\\\)*([^\.:]+)(?:\.[bB][lL][pP])?:([^\|]+)\|t/', function ($m) use ($fmt)
{
/* iconParam - size1, size2, xoffset, yoffset
size1 == 0; size2 omitted: Width = Height = TextHeight (always square!)
size1 > 0; size2 omitted: Width = Height = size1 (always square!)
size1 == 0; size2 == 0 : Width = Height = TextHeight (always square!)
size1 > 0; size2 == 0 : Width = TextHeight; Height = size1 (size1 is height!!!)
size1 == 0; size2 > 0 : Width = size2 * TextHeight; Height = TextHeight (size2 is an aspect ratio and defines width!!!)
size1 > 0; size2 > 0 : Width = size1; Height = size2
*/
[$_, $iconPath, $iconName, $iconParam] = $m;
switch ($fmt)
{
case self::FMT_HTML:
return '<span class="icontiny" style="background-image: url('.Cfg::get('STATIC_URL').'/images/wow/icons/tiny/'.Util::lower($iconName).'.gif)">';
case self::FMT_MARKUP:
return '[icon name='.Util::lower($iconName).']';
case self::FMT_RAW:
default:
return '';
}
}, $var);
// hyperlink |H<hyperlinkStruct>|h<name>|h
$var = preg_replace_callback('/\|H([^:]+):([^\|]+)\|h([^\|]+)\|h/i', function ($m) use ($fmt)
{
/* type Params
|Hchannel channelName, channelname == CHANNEL ? channelNr : null
|Hachievement AchievementID, PlayerGUID, isComplete, Month, Day, Year, criteriaMask1, criteriaMask2, criteriaMask3, criteriaMask4 - 32bit masks of Achievement_criteria.dbc/UIOrder only for achievements that display a todo list
|Hquest QuestID, QuestLevel
|Hitem itemId enchantId gemId1 gemId2 gemId3 gemId4 suffixId uniqueId linkLevel
|Henchant SpellID (from craftwindow)
|Htalent TalentID, TalentRank
|Hspell SpellID, PlayerLevel?
|Htrade SpellID, curSkill, maxSkill, PlayerGUID, base64_encode(known recipes bitmask)
|Hplayer Name
|Hunit GUID ? - combatlog
|Hicon ? "source"|"dest" - combatlog
|Haction ? - combatlog
*/
[$_, $linkType, $linkVars, $text] = $m;
$linkVars = explode(':', $linkVars);
$spfVars = ['', $linkVars[0], $text];
switch ($linkType)
{
case 'trade':
case 'enchant':
$linkType = 'spell';
case 'achievement': // markdown COULD implement completed status
case 'quest':
case 'item': // markdown COULD implement enchantments/gems
case 'spell':
$spfVars[0] = $linkType;
break;
case 'talent':
if ($spell = DB::Aowow()->selectCell('SELECT `spell` FROM ?_talents WHERE `id` = ?d AND `rank` = ?d', $linkVars[0], $linkVars[1]))
{
$spfVars[0] = 'spell';
$spfVars[1] = $spell;
break;
}
default:
return '';
}
switch ($fmt)
{
case self::FMT_HTML:
return sprintf('<a href="?%s=%d">%s</a>', $spfVars);
case self::FMT_MARKUP:
return sprintf('[%s=%d]', $spfVars);
case self::FMT_RAW:
default:
return sprintf('(%s #%d) %s', $spfVars);
}
}, $var);
// |1 - digit singular/plural <number> |1<singular;<plural>;
$var = preg_replace_callback('/(\d+)\s*\|1([^;]+);([^;]+);/is', function ($m)
{
[$_, $num, $singular, $plural] = $m;
switch ($num[-1])
{
case 0:
case 1:
return $num . ' ' . $singular;
default:
return $num . ' ' . $plural;
}
}, $var);
// |2 - frFR preposition: de |2 <word>
$var = preg_replace_callback('/\|2\s?(\w)/i', function ($m)
{
[$_, $word] = $m;
switch (strtolower($word[1]))
{
case 'h':
if (self::$locale != Locale::FR)
return 'de ' . $word;
case 'a':
case 'e':
case 'i':
case 'o':
case 'u':
return "d'" . $word;
default:
return 'de ' . $word;
}
}, $var);
// |3 - ruRU declinations |3-<caseIdx>(<word>)
$var = preg_replace_callback('/\|3-(\d)\(([^\)]+)\)/iu', function ($m)
{
[$_, $caseIdx, $word] = $m;
if ($caseIdx > 11 || $caseIdx < 1) // max caseIdx seen in DeclinedWordCases.dbc
return $word;
if (preg_match('/\P{Cyrillic}/iu', $word)) // not in cyrillic script
return $word;
if ($declWord = DB::Aowow()->selectCell('SELECT dwc.word FROM ?_declinedwordcases dwc JOIN ?_declinedword dc ON dwc.wordId = dc.id WHERE dwc.caseIdx = ?d AND dc.word = ?', $caseIdx, $word))
return $declWord;
return $word;
}, $var);
// |4 - numeric switch <number> |4<singular>:<plural>[:<plural2>];
$var = preg_replace_callback('/([\d\.\,]+)([^\d]*)\|4([^:]*):([^:;]+)(?::([^;]+))?;/is', function ($m)
{
[$_, $num, $pad, $singular, $plural1, $plural2] = array_pad($m, 6, null);
if (self::$locale != Locale::RU || !$plural2)
return $num . $pad . ($num == 1 ? $singular : $plural1);
// singular - ends in 1, but not teen number
if ($num[-1] == 1 && $num != 11)
return $num . $pad . $singular;
// genitive singular - ends in 2, 3, 4, but not teen number
if (($num[-1] == 2 && $num != 12) || ($num[-1] == 3 && $num != 13) || ($num[-1] == 4 && $num != 14))
return $num . $pad . $plural1;
// genitive plural - everything else
return $num . $pad . $plural2;
}, $var);
return $var;
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,7 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
@@ -7,9 +9,19 @@ if (!defined('AOWOW_REVISION'))
// exclude & weightscales are handled as Ajax
class AccountPage extends GenericPage
{
protected $text = '';
protected $head = '';
protected $token = '';
protected $infobox = [];
protected $resetPass = false;
protected $forceTabs = false;
protected $tpl = 'acc-dashboard';
protected $js = [[JS_FILE, 'user.js'], [JS_FILE, 'profile.js']];
protected $css = [[CSS_FILE, 'Profiler.css']];
protected $scripts = array(
[SC_JS_FILE, 'js/user.js'],
[SC_JS_FILE, 'js/profile.js'],
[SC_CSS_FILE, 'css/Profiler.css']
);
protected $mode = CACHE_TYPE_NONE;
protected $category = null;
protected $validCats = array(
@@ -34,10 +46,10 @@ class AccountPage extends GenericPage
protected $_post = array(
'username' => ['filter' => FILTER_SANITIZE_SPECIAL_CHARS, 'flags' => FILTER_FLAG_STRIP_AOWOW],
'password' => ['filter' => FILTER_UNSAFE_RAW],
'c_password' => ['filter' => FILTER_UNSAFE_RAW],
'token' => ['filter' => FILTER_UNSAFE_RAW],
'remember_me' => ['filter' => FILTER_CALLBACK, 'options' => 'AccountPage::rememberCallback'],
'password' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\GenericPage::checkTextLine'],
'c_password' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\GenericPage::checkTextLine'],
'token' => ['filter' => FILTER_SANITIZE_SPECIAL_CHARS, 'flags' => FILTER_FLAG_STRIP_AOWOW],
'remember_me' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\AccountPage::rememberCallback'],
'email' => ['filter' => FILTER_SANITIZE_EMAIL]
);
@@ -51,10 +63,10 @@ class AccountPage extends GenericPage
if ($pageParam)
{
// requires auth && not authed
if ($this->validCats[$pageParam][0] && !User::$id)
if ($this->validCats[$pageParam][0] && !User::isLoggedIn())
$this->forwardToSignIn('account='.$pageParam);
// doesn't require auth && authed
else if (!$this->validCats[$pageParam][0] && User::$id)
else if (!$this->validCats[$pageParam][0] && User::isLoggedIn())
header('Location: ?account', true, 302); // goto dashboard
}
}
@@ -75,10 +87,10 @@ class AccountPage extends GenericPage
switch ($this->category[0])
{
case 'forgotpassword':
if (CFG_ACC_AUTH_MODE != AUTH_MODE_SELF)
if (Cfg::get('ACC_AUTH_MODE') != AUTH_MODE_SELF)
{
if (CFG_ACC_EXT_RECOVER_URL)
header('Location: '.CFG_ACC_EXT_RECOVER_URL, true, 302);
if (Cfg::get('ACC_EXT_RECOVER_URL'))
header('Location: '.Cfg::get('ACC_EXT_RECOVER_URL'), true, 302);
else
$this->error();
}
@@ -92,10 +104,10 @@ class AccountPage extends GenericPage
$this->head = sprintf(Lang::account('recoverPass'), $nStep);
break;
case 'forgotusername':
if (CFG_ACC_AUTH_MODE != AUTH_MODE_SELF)
if (Cfg::get('ACC_AUTH_MODE') != AUTH_MODE_SELF)
{
if (CFG_ACC_EXT_RECOVER_URL)
header('Location: '.CFG_ACC_EXT_RECOVER_URL, true, 302);
if (Cfg::get('ACC_EXT_RECOVER_URL'))
header('Location: '.Cfg::get('ACC_EXT_RECOVER_URL'), true, 302);
else
$this->error();
}
@@ -125,23 +137,20 @@ class AccountPage extends GenericPage
if ($err = $this->doSignIn())
$this->error = $err;
else
{
session_regenerate_id(true); // user status changed => regenerate id
header('Location: '.$this->getNext(true), true, 302);
}
}
else if ($this->_get['token'] && ($_ = DB::Aowow()->selectCell('SELECT user FROM ?_account WHERE status IN (?a) AND token = ? AND statusTimer > UNIX_TIMESTAMP()', [ACC_STATUS_RECOVER_USER, ACC_STATUS_OK], $this->_get['token'])))
else if ($this->_get['token'] && ($_ = DB::Aowow()->selectCell('SELECT `username` FROM ?_account WHERE `status` IN (?a) AND `token` = ? AND `statusTimer` > UNIX_TIMESTAMP()', [ACC_STATUS_RECOVER_USER, ACC_STATUS_OK], $this->_get['token'])))
$this->user = $_;
break;
case 'signup':
if (!CFG_ACC_ALLOW_REGISTER)
if (!Cfg::get('ACC_ALLOW_REGISTER'))
$this->error();
if (CFG_ACC_AUTH_MODE != AUTH_MODE_SELF)
if (Cfg::get('ACC_AUTH_MODE') != AUTH_MODE_SELF)
{
if (CFG_ACC_EXT_CREATE_URL)
header('Location: '.CFG_ACC_EXT_CREATE_URL, true, 302);
if (Cfg::get('ACC_EXT_CREATE_URL'))
header('Location: '.Cfg::get('ACC_EXT_CREATE_URL'), true, 302);
else
$this->error();
}
@@ -162,7 +171,7 @@ class AccountPage extends GenericPage
{
$nStep = 2;
DB::Aowow()->query('UPDATE ?_account SET status = ?d, statusTimer = 0, token = 0, userGroups = ?d WHERE token = ?', ACC_STATUS_OK, U_GROUP_NONE, $this->_get['token']);
DB::Aowow()->query('REPLACE INTO ?_account_bannedips (ip, type, count, unbanDate) VALUES (?, 1, ?d + 1, UNIX_TIMESTAMP() + ?d)', User::$ip, CFG_ACC_FAILED_AUTH_COUNT, CFG_ACC_FAILED_AUTH_BLOCK);
DB::Aowow()->query('REPLACE INTO ?_account_bannedips (ip, type, count, unbanDate) VALUES (?, 1, ?d + 1, UNIX_TIMESTAMP() + ?d)', User::$ip, Cfg::get('ACC_FAILED_AUTH_COUNT'), Cfg::get('ACC_FAILED_AUTH_BLOCK'));
$this->text = sprintf(Lang::account('accActivated'), $this->_get['token']);
}
@@ -172,6 +181,8 @@ class AccountPage extends GenericPage
$this->head = sprintf(Lang::account('register'), $nStep);
break;
case 'signout':
DB::Aowow()->query('UPDATE ?_account_sessions SET `touched` = ?d, `status` = ?d WHERE `sessionId` = ?', time(), SESSION_LOGOUT, session_id());
User::destroy();
default:
header('Location: '.$this->getNext(true), true, 302);
@@ -188,11 +199,11 @@ class AccountPage extends GenericPage
private function createDashboard()
{
if (!User::$id)
if (!User::isLoggedIn())
$this->forwardToSignIn('account');
$user = DB::Aowow()->selectRow('SELECT * FROM ?_account WHERE id = ?d', User::$id);
$bans = DB::Aowow()->select('SELECT ab.*, a.displayName, ab.id AS ARRAY_KEY FROM ?_account_banned ab LEFT JOIN ?_account a ON a.id = ab.staffId WHERE ab.userId = ?d', User::$id);
$user = DB::Aowow()->selectRow('SELECT * FROM ?_account WHERE `id` = ?d', User::$id);
$bans = DB::Aowow()->select('SELECT ab.*, a.`username`, ab.`id` AS ARRAY_KEY FROM ?_account_banned ab LEFT JOIN ?_account a ON a.`id` = ab.`staffId` WHERE ab.`userId` = ?d', User::$id);
/***********/
/* Infobox */
@@ -224,7 +235,7 @@ class AccountPage extends GenericPage
continue;
$this->banned = array(
'by' => [$b['staffId'], $b['displayName']],
'by' => [$b['staffId'], $b['username']],
'end' => $b['end'],
'reason' => $b['reason']
);
@@ -248,7 +259,7 @@ class AccountPage extends GenericPage
}
// comments
if ($_ = CommunityContent::getCommentPreviews(['user' => User::$id, 'replies' => false]))
if ($_ = CommunityContent::getCommentPreviews(['user' => User::$id, 'comments' => true]))
{
// needs foundCount for params
// _totalCount: 377,
@@ -346,22 +357,27 @@ Markup.printHtml("description text here", "description-generic", { allow: Markup
if (!User::isValidPass($this->_post['password']))
return Lang::account('wrongPass');
switch (User::Auth($this->_post['username'], $this->_post['password']))
switch (User::authenticate($this->_post['username'], $this->_post['password']))
{
case AUTH_OK:
if (!User::$ip)
return Lang::main('intError');
// reset account status, update expiration
DB::Aowow()->query('UPDATE ?_account SET prevIP = IF(curIp = ?, prevIP, curIP), curIP = IF(curIp = ?, curIP, ?), allowExpire = ?d, status = IF(status = ?d, status, 0), statusTimer = IF(status = ?d, statusTimer, 0), token = IF(status = ?d, token, "") WHERE user = ?',
DB::Aowow()->query('UPDATE ?_account SET `prevIP` = IF(`curIp` = ?, `prevIP`, `curIP`), `curIP` = IF(`curIp` = ?, `curIP`, ?), `status` = IF(`status` = ?d, `status`, 0), `statusTimer` = IF(`status` = ?d, `statusTimer`, 0), `token` = IF(`status` = ?d, `token`, "") WHERE LOWER(`username`) = LOWER(?)',
User::$ip, User::$ip, User::$ip,
$this->_post['remember_me'] != 'yes',
ACC_STATUS_NEW, ACC_STATUS_NEW, ACC_STATUS_NEW,
$this->_post['username']
);
if (User::init())
User::save(); // overwrites the current user
session_regenerate_id(true); // user status changed => regenerate id
// create new session entry
DB::Aowow()->query('INSERT INTO ?_account_sessions (`userId`, `sessionId`, `created`, `expires`, `touched`, `deviceInfo`, `ip`, `status`) VALUES (?d, ?, ?d, ?d, ?d, ?, ?, ?d)',
User::$id, session_id(), time(), $this->_post['remember_me'] ? 0 : time() + Cfg::get('SESSION_TIMEOUT_DELAY'), time(), User::$agent, User::$ip, SESSION_ACTIVE);
if (User::init()) // reinitialize the user
User::save();
return;
case AUTH_BANNED:
@@ -376,7 +392,7 @@ Markup.printHtml("description text here", "description-generic", { allow: Markup
return Lang::account('wrongPass');
case AUTH_IPBANNED:
User::destroy();
return sprintf(Lang::account('loginExceeded'), Util::formatTime(CFG_ACC_FAILED_AUTH_BLOCK * 1000));
return sprintf(Lang::account('loginExceeded'), Util::formatTime(Cfg::get('ACC_FAILED_AUTH_BLOCK') * 1000));
case AUTH_INTERNAL_ERR:
User::destroy();
return Lang::main('intError');
@@ -407,56 +423,55 @@ Markup.printHtml("description text here", "description-generic", { allow: Markup
return Lang::main('intError');
// limit account creation
$ip = DB::Aowow()->selectRow('SELECT ip, count, unbanDate FROM ?_account_bannedips WHERE type = 1 AND ip = ?', User::$ip);
if ($ip && $ip['count'] >= CFG_ACC_FAILED_AUTH_COUNT && $ip['unbanDate'] >= time())
$ip = DB::Aowow()->selectRow('SELECT `ip`, `count`, `unbanDate` FROM ?_account_bannedips WHERE `type` = 1 AND `ip` = ?', User::$ip);
if ($ip && $ip['count'] >= Cfg::get('ACC_FAILED_AUTH_COUNT') && $ip['unbanDate'] >= time())
{
DB::Aowow()->query('UPDATE ?_account_bannedips SET count = count + 1, unbanDate = UNIX_TIMESTAMP() + ?d WHERE ip = ? AND type = 1', CFG_ACC_FAILED_AUTH_BLOCK, User::$ip);
return sprintf(Lang::account('signupExceeded'), Util::formatTime(CFG_ACC_FAILED_AUTH_BLOCK * 1000));
DB::Aowow()->query('UPDATE ?_account_bannedips SET `count` = `count` + 1, `unbanDate` = UNIX_TIMESTAMP() + ?d WHERE `ip` = ? AND `type` = 1', Cfg::get('ACC_FAILED_AUTH_BLOCK'), User::$ip);
return sprintf(Lang::account('signupExceeded'), Util::formatTime(Cfg::get('ACC_FAILED_AUTH_BLOCK') * 1000));
}
// username taken
if ($_ = DB::Aowow()->SelectCell('SELECT user FROM ?_account WHERE (user = ? OR email = ?) AND (status <> ?d OR (status = ?d AND statusTimer > UNIX_TIMESTAMP()))', $this->_post['username'], $this->_post['email'], ACC_STATUS_NEW, ACC_STATUS_NEW))
if ($_ = DB::Aowow()->SelectCell('SELECT `username` FROM ?_account WHERE (`username` = ? OR `email` = ?) AND (`status` <> ?d OR (`status` = ?d AND `statusTimer` > UNIX_TIMESTAMP()))', $this->_post['username'], $this->_post['email'], ACC_STATUS_NEW, ACC_STATUS_NEW))
return $_ == $this->_post['username'] ? Lang::account('nameInUse') : Lang::account('mailInUse');
// create..
$token = Util::createHash();
$ok = DB::Aowow()->query('REPLACE INTO ?_account (user, passHash, displayName, email, joindate, curIP, allowExpire, locale, userGroups, status, statusTimer, token) VALUES (?, ?, ?, ?, UNIX_TIMESTAMP(), ?, ?d, ?d, ?d, ?d, UNIX_TIMESTAMP() + ?d, ?)',
$ok = DB::Aowow()->query('REPLACE INTO ?_account (`login`, `passHash`, `username`, `email`, `joindate`, `curIP`, `locale`, `userGroups`, `status`, `statusTimer`, `token`) VALUES (?, ?, ?, ?, UNIX_TIMESTAMP(), ?, ?d, ?d, ?d, ?d, UNIX_TIMESTAMP() + ?d, ?)',
$this->_post['username'],
User::hashCrypt($this->_post['password']),
Util::ucFirst($this->_post['username']),
$this->_post['username'],
$this->_post['email'],
User::$ip,
$this->_post['remember_me'] != 'yes',
User::$localeId,
Lang::getLocale()->value,
U_GROUP_PENDING,
ACC_STATUS_NEW,
CFG_ACC_CREATE_SAVE_DECAY,
Cfg::get('ACC_CREATE_SAVE_DECAY'),
$token
);
if (!$ok)
return Lang::main('intError');
else if ($_ = $this->sendMail(Lang::user('accConfirm', 0), sprintf(Lang::user('accConfirm', 1), $token), CFG_ACC_CREATE_SAVE_DECAY))
{
if ($id = DB::Aowow()->selectCell('SELECT id FROM ?_account WHERE token = ?', $token))
Util::gainSiteReputation($id, SITEREP_ACTION_REGISTER);
// success:: update ip-bans
if (!$ip || $ip['unbanDate'] < time())
DB::Aowow()->query('REPLACE INTO ?_account_bannedips (ip, type, count, unbanDate) VALUES (?, 1, 1, UNIX_TIMESTAMP() + ?d)', User::$ip, CFG_ACC_FAILED_AUTH_BLOCK);
else
DB::Aowow()->query('UPDATE ?_account_bannedips SET count = count + 1, unbanDate = UNIX_TIMESTAMP() + ?d WHERE ip = ? AND type = 1', CFG_ACC_FAILED_AUTH_BLOCK, User::$ip);
if (!Util::sendMail($this->_post['email'], 'activate-account', [$token], Cfg::get('ACC_RECOVERY_DECAY')))
return Lang::main('intError2', ['send mail']);
return $_;
}
if ($id = DB::Aowow()->selectCell('SELECT id FROM ?_account WHERE token = ?', $token))
Util::gainSiteReputation($id, SITEREP_ACTION_REGISTER);
// success:: update ip-bans
if (!$ip || $ip['unbanDate'] < time())
DB::Aowow()->query('REPLACE INTO ?_account_bannedips (ip, type, count, unbanDate) VALUES (?, 1, 1, UNIX_TIMESTAMP() + ?d)', User::$ip, Cfg::get('ACC_FAILED_AUTH_BLOCK'));
else
DB::Aowow()->query('UPDATE ?_account_bannedips SET count = count + 1, unbanDate = UNIX_TIMESTAMP() + ?d WHERE ip = ? AND type = 1', Cfg::get('ACC_FAILED_AUTH_BLOCK'), User::$ip);
}
private function doRecoverPass()
{
if ($_ = $this->initRecovery(ACC_STATUS_RECOVER_PASS, CFG_ACC_RECOVERY_DECAY, $token))
if ($_ = $this->initRecovery(ACC_STATUS_RECOVER_PASS, Cfg::get('ACC_RECOVERY_DECAY'), $token))
return $_;
// send recovery mail
return $this->sendMail(Lang::user('resetPass', 0), sprintf(Lang::user('resetPass', 1), $token), CFG_ACC_RECOVERY_DECAY);
if (!Util::sendMail($this->_post['email'], 'reset-password', [$token], Cfg::get('ACC_RECOVERY_DECAY')))
return Lang::main('intError2', ['send mail']);
}
private function doResetPass()
@@ -467,28 +482,28 @@ Markup.printHtml("description text here", "description-generic", { allow: Markup
if (!Util::isValidEmail($this->_post['email']))
return Lang::account('emailInvalid');
$uId = DB::Aowow()->selectCell('SELECT id FROM ?_account WHERE token = ? AND email = ? AND status = ?d AND statusTimer > UNIX_TIMESTAMP()',
$userData = DB::Aowow()->selectRow('SELECT `id, `passHash` FROM ?_account WHERE `token` = ? AND `email` = ? AND `status` = ?d AND `statusTimer` > UNIX_TIMESTAMP()',
$this->_post['token'],
$this->_post['email'],
ACC_STATUS_RECOVER_PASS
);
if (!$uId)
if (!$userData)
return Lang::account('emailNotFound'); // assume they didn't meddle with the token
if (!User::verifyCrypt($this->_post['c_password']))
if (!User::verifyCrypt($this->_post['c_password'], $userData['passHash']))
return Lang::account('newPassDiff');
if (!DB::Aowow()->query('UPDATE ?_account SET passHash = ?, status = ?d WHERE id = ?d', User::hashCrypt($this->_post['c_password']), ACC_STATUS_OK, $uId))
if (!DB::Aowow()->query('UPDATE ?_account SET `passHash` = ?, `status` = ?d WHERE `id` = ?d', User::hashCrypt($this->_post['c_password']), ACC_STATUS_OK, $userData['id']))
return Lang::main('intError');
}
private function doRecoverUser()
{
if ($_ = $this->initRecovery(ACC_STATUS_RECOVER_USER, CFG_ACC_RECOVERY_DECAY, $token))
if ($_ = $this->initRecovery(ACC_STATUS_RECOVER_USER, Cfg::get('ACC_RECOVERY_DECAY'), $token))
return $_;
// send recovery mail
return $this->sendMail(Lang::user('recoverUser', 0), sprintf(Lang::user('recoverUser', 1), $token), CFG_ACC_RECOVERY_DECAY);
if (!Util::sendMail($this->_post['email'], 'recover-user', [$token], Cfg::get('ACC_RECOVERY_DECAY')))
return Lang::main('intError2', ['send mail']);
}
private function initRecovery($type, $delay, &$token)
@@ -506,19 +521,6 @@ Markup.printHtml("description text here", "description-generic", { allow: Markup
return Lang::main('intError');
}
private function sendMail($subj, $msg, $delay = 300)
{
// send recovery mail
$subj = CFG_NAME_SHORT.Lang::main('colon') . $subj;
$msg .= "\r\n\r\n".sprintf(Lang::user('tokenExpires'), Util::formatTime($delay * 1000))."\r\n";
$header = 'From: '.CFG_CONTACT_EMAIL . "\r\n" .
'Reply-To: '.CFG_CONTACT_EMAIL . "\r\n" .
'X-Mailer: PHP/' . phpversion();
if (!mail($this->_post['email'], $subj, $msg, $header))
return sprintf(Lang::main('intError2'), 'send mail');
}
private function getNext($forHeader = false)
{
$next = $forHeader ? '.' : '';

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