From 36aa33ac265ea5d78623878ea366017c3341e9f5 Mon Sep 17 00:00:00 2001 From: Yrito Date: Sun, 23 Jun 2024 17:10:45 +0200 Subject: [PATCH 001/260] Setup/CLI * Allow starting setup at specific step --- setup/tools/CLISetup.class.php | 1 + setup/tools/clisetup/setup.us.php | 66 ++++++++++++++++++++----------- 2 files changed, 44 insertions(+), 23 deletions(-) diff --git a/setup/tools/CLISetup.class.php b/setup/tools/CLISetup.class.php index 5529f9dd..670274bd 100644 --- a/setup/tools/CLISetup.class.php +++ b/setup/tools/CLISetup.class.php @@ -46,6 +46,7 @@ class CLISetup 'force' => [self::OPT_GRP_MISC, ['f'], self::ARGV_NONE, 'Force existing files to be overwritten.', '' ], 'locales' => [self::OPT_GRP_MISC, [], self::ARGV_ARRAY | self::ARGV_OPTIONAL, 'Limit setup to enUS, frFR, deDE, zhCN, esES and/or ruRU. (does not override config settings)', '='], 'datasrc' => [self::OPT_GRP_MISC, [], self::ARGV_OPTIONAL, 'Manually point to directory with extracted mpq files. This is limited to setup/ (default: setup/mpqdata/)', '=path/' ], + 'step' => [self::OPT_GRP_MISC, [], self::ARGV_REQUIRED, 'Start setup at given step (can be used to better automate the setup process).', '=step' ], ); private static $utilScriptRefs = []; diff --git a/setup/tools/clisetup/setup.us.php b/setup/tools/clisetup/setup.us.php index 0c84b0a1..60c312e2 100644 --- a/setup/tools/clisetup/setup.us.php +++ b/setup/tools/clisetup/setup.us.php @@ -25,9 +25,8 @@ CLISetup::registerUtility(new class extends UtilityScript public const SITE_LOCK = CLISetup::LOCK_ON; - private $startStep = 0; - private $dynArgs = ['doSql' => [], 'doBuild' => []]; // ref to pass commands from 'update' to 'sync' - private $steps = array( + private $dynArgs = ['doSql' => [], 'doBuild' => []]; // ref to pass commands from 'update' to 'sync' + private $steps = array( // [staticUS, $name, [...args]] ['database', '', []], ['configure', '', []], @@ -39,21 +38,21 @@ CLISetup::registerUtility(new class extends UtilityScript private const STEP_FILE = 'cache/setup/firstrun'; - // Note! Must be loaded after all SetupScripts have been registered - public function __construct() + private function getSavedStartStep() : int { - /********************/ - /* get current step */ - /********************/ - if (file_exists(self::STEP_FILE)) { $rows = file(self::STEP_FILE); if ((int)$rows[0] == AOWOW_REVISION) - $this->startStep = (int)$rows[1]; + return (int)$rows[1]; } + return 0; + } + // Note! Must be loaded after all SetupScripts have been registered + public function __construct() + { /****************/ /* define steps */ /****************/ @@ -78,23 +77,44 @@ CLISetup::registerUtility(new class extends UtilityScript // args: null, null, null, null // nnnn public function run(&$args) : bool { - if ($this->startStep) + /******************/ + /* get start step */ + /******************/ + + $startStep = 0; + if (($cliStartStep = CLISetup::getOpt('step')) !== false) { - CLI::write('[setup] found firstrun progression info. (Halted on subscript: '.($this->steps[$this->startStep][1] ?: $this->steps[$this->startStep][0]).')', CLI::LOG_INFO); - $msg = ''; - if (!CLI::read(['x' => ['continue setup? (y/n)', true, true, '/y|n/i']], $uiN) || !$uiN || strtolower($uiN['x']) == 'n') + $startStep = ((int)$cliStartStep) - 1; + if ($startStep < 0 || $startStep >= count($this->steps)) { - $msg = '[setup] starting from scratch...'; - $this->startStep = 0; + CLI::write('Invalid step number. Use --step <1-'.count($this->steps).'>', CLI::LOG_ERROR); + return false; + } + + CLI::write('[setup] starting from step '.($startStep + 1).'...'); + } + elseif (($startStep = $this->getSavedStartStep()) !== 0) + { + CLI::write('[setup] found firstrun progression info. (Halted on subscript: '.($this->steps[$startStep][1] ?: $this->steps[$startStep][0]).')', CLI::LOG_INFO); + if (!CLI::read(['x' => ['continue setup? (y/n)', true, true, '/y|n/i']], $uiN)) + { + CLI::write('Failed to read answer. Use --step in a non-interactive environment.', CLI::LOG_ERROR); + return false; } - else - $msg = '[setup] resuming from step '.($this->startStep + 1).'...'; CLI::write(); - CLI::write($msg); + if (strtolower($uiN['x']) == 'n') + { + $startStep = 0; + CLI::write('[setup] starting from scratch...'); + } + else + CLI::write('[setup] resuming from step '.($startStep + 1).'...'); + sleep(1); } + // init temp setup dir if ($info = new \SplFileInfo(self::STEP_FILE)) CLISetup::writeDir($info->getPath()); @@ -106,7 +126,7 @@ CLISetup::registerUtility(new class extends UtilityScript foreach ($this->steps as $idx => [$usName, , $param]) { - if ($this->startStep > $idx) + if ($startStep > $idx) continue; while (true) @@ -147,17 +167,17 @@ CLISetup::registerUtility(new class extends UtilityScript public function writeCLIHelp() : bool { - CLI::write(' usage: php aowow --setup [--locales: --datasrc:]', -1, false); + CLI::write(' usage: php aowow --setup [--locales: --datasrc:] [--step=]', -1, false); CLI::write(); CLI::write(' Initially essential connection information are set up and basic connectivity tests run afterwards.', -1, false); CLI::write(' In the main stage dbc and world data is compiled into the database and required sound, image and data files are generated.', -1, false); CLI::write(' This should not require further input and will take about 15-20 minutes, plus 10 minutes per additional locale.', -1, false); CLI::write(' Lastly pending updates are applied and you are prompted to create an administrator account.', -1, false); - if ($this->startStep) + if (($startStep = $this->getSavedStartStep()) !== 0) { CLI::write(); - CLI::write(' You are currently on step '.($this->startStep + 1).' / '.count($this->steps).' ('.($this->steps[$this->startStep][1] ?: $this->steps[$this->startStep][0]).'). You can resume or restart the setup process.', -1, false); + CLI::write(' You are currently on step '.($startStep + 1).' / '.count($this->steps).' ('.($this->steps[$startStep][1] ?: $this->steps[$startStep][0]).'). You can resume or restart the setup process.', -1, false); } CLI::write(); From bd1f139c2ec48e8fe470479d303c85b7d98cc8c8 Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Thu, 2 Oct 2025 16:16:49 +0200 Subject: [PATCH 002/260] CLI/Misc * handle error case where setup is run automated and receives no input on STDIN --- includes/setup/cli.class.php | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/includes/setup/cli.class.php b/includes/setup/cli.class.php index 14cda6cf..47a05ee6 100644 --- a/includes/setup/cli.class.php +++ b/includes/setup/cli.class.php @@ -288,7 +288,12 @@ abstract class CLI continue; // stream_get_contents is always blocking under WIN - fgets should work similary as php always receives a terminated line of text - $chars = str_split(OS_WIN ? fgets(STDIN) : stream_get_contents(STDIN)); + $chars = str_split(OS_WIN ? fgets(STDIN) : stream_get_contents(STDIN)); + + // $chars can be empty if used non-interactive + if (!$chars) + return false; + $ordinals = array_map('ord', $chars); if ($ordinals[0] == self::CHR_ESC) From 6d86f880f4ce405f31cd1845d975095fc6f5986a Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Thu, 2 Oct 2025 17:23:54 +0200 Subject: [PATCH 003/260] Analytics/Cookies * don't ask users to consent on GA tracking if we don't use GA tracking --- includes/components/pagetemplate.class.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/components/pagetemplate.class.php b/includes/components/pagetemplate.class.php index b1360f23..7eb11c8c 100644 --- a/includes/components/pagetemplate.class.php +++ b/includes/components/pagetemplate.class.php @@ -470,7 +470,7 @@ class PageTemplate private function update() : void { // analytics + consent - if (!isset($_COOKIE['consent'])) + if ($this->analyticsTag && !isset($_COOKIE['consent'])) { $this->addScript(SC_CSS_FILE, 'css/consent.css'); $this->addScript(SC_JS_FILE, 'js/consent.js'); From bc112b2b161303ba2040655b2582373ee1c97e14 Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Fri, 3 Oct 2025 17:41:07 +0200 Subject: [PATCH 004/260] Template/Fixup * fix directly adding dataloader to PageTemplate --- includes/components/response/templateresponse.class.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/components/response/templateresponse.class.php b/includes/components/response/templateresponse.class.php index a8760bd4..9d3b2ec7 100644 --- a/includes/components/response/templateresponse.class.php +++ b/includes/components/response/templateresponse.class.php @@ -217,7 +217,7 @@ class TemplateResponse extends BaseResponse if (!$this->result) $this->dataLoader = array_merge($this->dataLoader, $dataFiles); else - $this->result->addDataLoader($dataFiles); + $this->result->addDataLoader(...$dataFiles); } public static function pageStatsHook(Template\PageTemplate &$pt, array &$stats) : void From 60eb816002acac0ba89bd158c8b6997f43da02dc Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Fri, 3 Oct 2025 00:47:57 +0200 Subject: [PATCH 005/260] Quickfacts * where applicable: - show typeId - show english name if currently localized - show percentage attained by synced profiles * fixed some typos --- endpoints/achievement/achievement.php | 22 ++++- endpoints/class/class.php | 7 ++ endpoints/currency/currency.php | 9 +- endpoints/emote/emote.php | 3 + endpoints/enchantment/enchantment.php | 7 ++ endpoints/event/event.php | 13 ++- endpoints/faction/faction.php | 20 +++++ endpoints/guide/changelog.php | 2 +- endpoints/item/item.php | 9 +- endpoints/itemset/itemset.php | 7 ++ endpoints/mail/mail.php | 3 + endpoints/npc/npc.php | 7 ++ endpoints/object/object.php | 7 ++ endpoints/pet/pet.php | 9 +- endpoints/quest/quest.php | 20 +++++ endpoints/race/race.php | 7 ++ endpoints/skill/skill.php | 7 ++ endpoints/spell/spell.php | 22 ++++- endpoints/title/title.php | 20 +++++ endpoints/zone/zone.php | 7 ++ localization/locale_dede.php | 23 ++++- localization/locale_enus.php | 23 ++++- localization/locale_eses.php | 27 +++++- localization/locale_frfr.php | 33 ++++++-- localization/locale_ruru.php | 23 ++++- localization/locale_zhcn.php | 23 ++++- setup/sql/01-db_structure.sql | 4 + setup/sql/updates/1759504522_01.sql | 11 +++ setup/sql/updates/1759504522_02.sql | 1 + .../templates/global.js/clicktocopy.js | 2 +- .../filegen/templates/global.js/markup.js | 84 +++++++++++++++++++ static/css/aowow.css | 11 +++ static/js/locale_dede.js | 2 + static/js/locale_enus.js | 2 + static/js/locale_eses.js | 2 + static/js/locale_frfr.js | 2 + static/js/locale_ruru.js | 2 + static/js/locale_zhcn.js | 2 + 38 files changed, 462 insertions(+), 23 deletions(-) create mode 100644 setup/sql/updates/1759504522_01.sql create mode 100644 setup/sql/updates/1759504522_02.sql diff --git a/endpoints/achievement/achievement.php b/endpoints/achievement/achievement.php index 3d36494b..0cb6df45 100644 --- a/endpoints/achievement/achievement.php +++ b/endpoints/achievement/achievement.php @@ -107,13 +107,33 @@ class AchievementBaseResponse extends TemplateResponse implements ICache default => Lang::game('si', SIDE_BOTH) // 0, 3 }; + // id + $infobox[] = Lang::achievement('id') . $this->typeId; + // icon if ($_ = $this->subject->getField('iconId')) { - $infobox[] = Util::ucFirst(lang::game('icon')).Lang::main('colon').'[icondb='.$_.' name=true]'; + $infobox[] = Util::ucFirst(Lang::game('icon')).Lang::main('colon').'[icondb='.$_.' name=true]'; $this->extendGlobalIds(Type::ICON, $_); } + // profiler relateed (note that this is part of the cache. I don't think this is important enough to calc for every view) + if (Cfg::get('PROFILER_ENABLE') && !($this->subject->getField('flags') & ACHIEVEMENT_FLAG_COUNTER)) + { + $x = DB::Aowow()->selectCell('SELECT COUNT(1) FROM ?_profiler_completion_achievements WHERE `achievementId` = ?d', $this->typeId); + $y = DB::Aowow()->selectCell('SELECT COUNT(1) FROM ?_profiler_profiles WHERE `realm` IS NOT NULL AND `realmGUID` IS NOT NULL'); + $infobox[] = Lang::profiler('attainedBy', [round(($x ?: 0) * 100 / ($y ?: 1))]); + + // - js component missing; + // - can't yet assign styles to li element + // if (User::getPinnedCharacter()) + // $infobox[] = Lang::profiler('completion') . '[span class="compact-completion-display"][/span]'; // [li style="display:none"]...[/li] + } + + // original name + if (Lang::getLocale() != Locale::EN) + $infobox[] = Util::ucFirst(Lang::lang(Locale::EN->value) . Lang::main('colon')) . '[copy button=false]'.$this->subject->getField('name_loc0').'[/copy][/li]'; + if ($infobox) $this->infobox = new InfoboxMarkup($infobox, ['allow' => Markup::CLASS_STAFF, 'dbpage' => true], 'infobox-contents0'); diff --git a/endpoints/class/class.php b/endpoints/class/class.php index 50ca4666..f983c679 100644 --- a/endpoints/class/class.php +++ b/endpoints/class/class.php @@ -103,6 +103,13 @@ class ClassBaseResponse extends TemplateResponse implements ICache if ($specList) $infobox[] = Lang::game('specs').'[ul][li]'.implode('[/li][li]', $specList).'[/li][/ul]'; + // id + $infobox[] = Lang::chrClass('id') . $this->typeId; + + // original name + if (Lang::getLocale() != Locale::EN) + $infobox[] = Util::ucFirst(Lang::lang(Locale::EN->value) . Lang::main('colon')) . '[copy button=false]'.$this->subject->getField('name_loc0').'[/copy][/li]'; + if ($infobox) $this->infobox = new InfoboxMarkup($infobox, ['allow' => Markup::CLASS_STAFF, 'dbpage' => true], 'infobox-contents0'); diff --git a/endpoints/currency/currency.php b/endpoints/currency/currency.php index 31bc6823..cc65b9e8 100644 --- a/endpoints/currency/currency.php +++ b/endpoints/currency/currency.php @@ -72,13 +72,20 @@ class CurrencyBaseResponse extends TemplateResponse implements ICache if ($_ = $this->subject->getField('cap')) $infobox[] = Lang::currency('cap').Lang::nf($_); + // id + $infobox[] = Lang::currency('id') . $this->typeId; + // icon if ($_ = $this->subject->getField('iconId')) { - $infobox[] = Util::ucFirst(lang::game('icon')).Lang::main('colon').'[icondb='.$_.' name=true]'; + $infobox[] = Util::ucFirst(Lang::game('icon')).Lang::main('colon').'[icondb='.$_.' name=true]'; $this->extendGlobalIds(Type::ICON, $_); } + // original name + if (Lang::getLocale() != Locale::EN) + $infobox[] = Util::ucFirst(Lang::lang(Locale::EN->value) . Lang::main('colon')) . '[copy button=false]'.$this->subject->getField('name_loc0').'[/copy][/li]'; + if ($infobox) $this->infobox = new InfoboxMarkup($infobox, ['allow' => Markup::CLASS_STAFF, 'dbpage' => true], 'infobox-contents0'); diff --git a/endpoints/emote/emote.php b/endpoints/emote/emote.php index d5e16070..317d0130 100644 --- a/endpoints/emote/emote.php +++ b/endpoints/emote/emote.php @@ -94,6 +94,9 @@ class EmoteBaseResponse extends TemplateResponse implements ICache } } + // id + $infobox[] = Lang::emote('id') . $this->typeId; + if ($infobox) $this->infobox = new InfoboxMarkup($infobox, ['allow' => Markup::CLASS_STAFF, 'dbpage' => true], 'infobox-contents0'); diff --git a/endpoints/enchantment/enchantment.php b/endpoints/enchantment/enchantment.php index ab874512..b836cbd1 100644 --- a/endpoints/enchantment/enchantment.php +++ b/endpoints/enchantment/enchantment.php @@ -85,6 +85,13 @@ class EnchantmentBaseResponse extends TemplateResponse implements ICache $infobox[] = $foo; } + // id + $infobox[] = Lang::enchantment('id') . $this->typeId; + + // original name + if (Lang::getLocale() != Locale::EN) + $infobox[] = Util::ucFirst(Lang::lang(Locale::EN->value) . Lang::main('colon')) . '[copy button=false]'.$this->subject->getField('name_loc0').'[/copy][/li]'; + if ($infobox) $this->infobox = new InfoboxMarkup($infobox, ['allow' => Markup::CLASS_STAFF, 'dbpage' => true], 'infobox-contents0'); diff --git a/endpoints/event/event.php b/endpoints/event/event.php index 5e1530b8..71a722e6 100644 --- a/endpoints/event/event.php +++ b/endpoints/event/event.php @@ -88,9 +88,16 @@ class EventBaseResponse extends TemplateResponse implements ICache $infobox[] = Lang::npc('rank', 3).Lang::main('colon').'[npc='.$_.']'; } - // display internal id to staff - if (User::isInGroup(U_GROUP_STAFF)) - $infobox[] = 'Event-Id'.Lang::main('colon').$this->typeId; + // id + $infobox[] = Lang::event('id') . $this->typeId; + + // display holiday id to staff + if ($_holidayId && User::isInGroup(U_GROUP_STAFF)) + $infobox[] = 'Holiday ID'.Lang::main('colon').$_holidayId; + + // original name + if (Lang::getLocale() != Locale::EN) + $infobox[] = Util::ucFirst(Lang::lang(Locale::EN->value) . Lang::main('colon')) . '[copy button=false]'.$this->subject->getField('name_loc0').'[/copy][/li]'; if ($infobox) $this->infobox = new InfoboxMarkup($infobox, ['allow' => Markup::CLASS_STAFF, 'dbpage' => true], 'infobox-contents0'); diff --git a/endpoints/faction/faction.php b/endpoints/faction/faction.php index 1279b62b..868b9481 100644 --- a/endpoints/faction/faction.php +++ b/endpoints/faction/faction.php @@ -96,6 +96,26 @@ class FactionBaseResponse extends TemplateResponse implements ICache if ($_ = $this->subject->getField('side')) $infobox[] = Lang::main('side').'[span class=icon-'.($_ == SIDE_ALLIANCE ? 'alliance' : 'horde').']'.Lang::game('si', $_).'[/span]'; + // id + $infobox[] = Lang::faction('id') . $this->typeId; + + // profiler relateed (note that this is part of the cache. I don't think this is important enough to calc for every view) + if (Cfg::get('PROFILER_ENABLE') && !($this->subject->getField('cuFlags') & CUSTOM_EXCLUDE_FOR_LISTVIEW)) + { + $x = DB::Aowow()->selectCell('SELECT COUNT(1) FROM ?_profiler_completion_reputation WHERE `standing` >= ?d AND `factionId` = ?d', 42000, $this->typeId); + $y = DB::Aowow()->selectCell('SELECT COUNT(1) FROM ?_profiler_profiles WHERE `realm` IS NOT NULL AND `realmGUID` IS NOT NULL'); + $infobox[] = Lang::profiler('attainedBy', [round(($x ?: 0) * 100 / ($y ?: 1))]); + + // - js component missing; + // - can't yet assign styles to li element + // if (User::getPinnedCharacter()) + // $infobox[] = Lang::profiler('completion') . '[span class="compact-completion-display"][/span]'; // [li style="display:none"]...[/li] + } + + // original name + if (Lang::getLocale() != Locale::EN) + $infobox[] = Util::ucFirst(Lang::lang(Locale::EN->value) . Lang::main('colon')) . '[copy button=false]'.$this->subject->getField('name_loc0').'[/copy][/li]'; + if ($infobox) $this->infobox = new InfoboxMarkup($infobox, ['allow' => Markup::CLASS_STAFF, 'dbpage' => true], 'infobox-contents0'); diff --git a/endpoints/guide/changelog.php b/endpoints/guide/changelog.php index 685dfad3..614af6d6 100644 --- a/endpoints/guide/changelog.php +++ b/endpoints/guide/changelog.php @@ -33,7 +33,7 @@ class GuideChangelogResponse extends TemplateResponse if (!$guide->canBeViewed() && !$guide->userCanView()) $this->forward('?guides='.$guide->getField('category')); - $this->h1 = lang::guide('clTitle', [$this->_get['id'], $guide->getField('title')]); + $this->h1 = Lang::guide('clTitle', [$this->_get['id'], $guide->getField('title')]); if (!$this->h1) $this->h1 = $guide->getField('name'); diff --git a/endpoints/item/item.php b/endpoints/item/item.php index f476b5b9..2e5880fe 100644 --- a/endpoints/item/item.php +++ b/endpoints/item/item.php @@ -103,10 +103,13 @@ class ItemBaseResponse extends TemplateResponse implements ICache SIDE_BOTH => Lang::game('si', SIDE_BOTH) }; + // id + $infobox[] = Lang::item('id') . $this->typeId; + // icon if ($_ = $this->subject->getField('iconId')) { - $infobox[] = Util::ucFirst(lang::game('icon')).Lang::main('colon').'[icondb='.$_.' name=true]'; + $infobox[] = Util::ucFirst(Lang::game('icon')).Lang::main('colon').'[icondb='.$_.' name=true]'; $this->extendGlobalIds(Type::ICON, $_); } @@ -316,6 +319,10 @@ class ItemBaseResponse extends TemplateResponse implements ICache if ($_bagFamily & 0x0100) $infobox[] = Lang::item('atKeyring'); + // original name + if (Lang::getLocale() != Locale::EN) + $infobox[] = Util::ucFirst(Lang::lang(Locale::EN->value) . Lang::main('colon')) . '[copy button=false]'.$this->subject->getField('name_loc0').'[/copy][/li]'; + if ($infobox) $this->infobox = new InfoboxMarkup($infobox, ['allow' => Markup::CLASS_STAFF, 'dbpage' => true], 'infobox-contents0'); diff --git a/endpoints/itemset/itemset.php b/endpoints/itemset/itemset.php index 398fe55e..2cd46422 100644 --- a/endpoints/itemset/itemset.php +++ b/endpoints/itemset/itemset.php @@ -125,6 +125,13 @@ class ItemsetBaseResponse extends TemplateResponse implements ICache if ($_ta) $infobox[] = Lang::itemset('_tag').'[url=?itemsets&filter=ta='.$_ta.']'.Lang::itemset('notes', $_ta).'[/url]'; + // id + $infobox[] = Lang::itemset('id') . $this->subject->getField('refSetId'); + + // original name + if (Lang::getLocale() != Locale::EN) + $infobox[] = Util::ucFirst(Lang::lang(Locale::EN->value) . Lang::main('colon')) . '[copy button=false]'.$this->subject->getField('name_loc0').'[/copy][/li]'; + if ($infobox) $this->infobox = new InfoboxMarkup($infobox, ['allow' => Markup::CLASS_STAFF, 'dbpage' => true], 'infobox-contents0'); diff --git a/endpoints/mail/mail.php b/endpoints/mail/mail.php index 705c6aca..5179b6e8 100644 --- a/endpoints/mail/mail.php +++ b/endpoints/mail/mail.php @@ -108,6 +108,9 @@ class MailBaseResponse extends TemplateResponse implements ICache } } + // id + $infobox[] = Lang::mail('id') . $this->typeId; + if ($infobox) $this->infobox = new InfoboxMarkup($infobox, ['allow' => Markup::CLASS_STAFF, 'dbpage' => true], 'infobox-contents0'); diff --git a/endpoints/npc/npc.php b/endpoints/npc/npc.php index 3d4624e2..25e7cf72 100644 --- a/endpoints/npc/npc.php +++ b/endpoints/npc/npc.php @@ -194,6 +194,13 @@ class NpcBaseResponse extends TemplateResponse implements ICache if ($this->subject->getField('npcflag') & (NPC_FLAG_SPIRIT_HEALER | NPC_FLAG_SPIRIT_GUIDE)) $infobox[] = Lang::npc('extraFlags', CREATURE_FLAG_EXTRA_GHOST_VISIBILITY); + // id + $infobox[] = Lang::npc('id') . $this->typeId; + + // original name + if (Lang::getLocale() != Locale::EN) + $infobox[] = Util::ucFirst(Lang::lang(Locale::EN->value) . Lang::main('colon')) . '[copy button=false]'.$this->subject->getField('name_loc0').'[/copy][/li]'; + if (User::isInGroup(U_GROUP_EMPLOYEE)) { // AI diff --git a/endpoints/object/object.php b/endpoints/object/object.php index 9c6a1ba2..6ebfbe6f 100644 --- a/endpoints/object/object.php +++ b/endpoints/object/object.php @@ -196,6 +196,13 @@ class ObjectBaseResponse extends TemplateResponse implements ICache $infobox[] = $buff; } + // id + $infobox[] = Lang::gameObject('id') . $this->typeId; + + // original name + if (Lang::getLocale() != Locale::EN) + $infobox[] = Util::ucFirst(Lang::lang(Locale::EN->value) . Lang::main('colon')) . '[copy button=false]'.$this->subject->getField('name_loc0').'[/copy][/li]'; + // AI if (User::isInGroup(U_GROUP_EMPLOYEE)) if ($_ = $this->subject->getField('ScriptOrAI')) diff --git a/endpoints/pet/pet.php b/endpoints/pet/pet.php index f383a0f7..30d0db14 100644 --- a/endpoints/pet/pet.php +++ b/endpoints/pet/pet.php @@ -73,13 +73,20 @@ class PetBaseResponse extends TemplateResponse implements ICache if ($this->subject->getField('exotic')) $infobox[] = '[url=?spell=53270]'.Lang::pet('exotic').'[/url]'; + // id + $infobox[] = Lang::pet('id') . $this->typeId; + // icon if ($_ = $this->subject->getField('iconId')) { - $infobox[] = Util::ucFirst(lang::game('icon')).Lang::main('colon').'[icondb='.$_.' name=true]'; + $infobox[] = Util::ucFirst(Lang::game('icon')).Lang::main('colon').'[icondb='.$_.' name=true]'; $this->extendGlobalIds(Type::ICON, $_); } + // original name + if (Lang::getLocale() != Locale::EN) + $infobox[] = Util::ucFirst(Lang::lang(Locale::EN->value) . Lang::main('colon')) . '[copy button=false]'.$this->subject->getField('name_loc0').'[/copy][/li]'; + if ($infobox) $this->infobox = new InfoboxMarkup($infobox, ['allow' => Markup::CLASS_STAFF, 'dbpage' => true], 'infobox-contents0'); diff --git a/endpoints/quest/quest.php b/endpoints/quest/quest.php index 305900d6..daf291f1 100644 --- a/endpoints/quest/quest.php +++ b/endpoints/quest/quest.php @@ -271,6 +271,26 @@ class QuestBaseResponse extends TemplateResponse implements ICache $infobox[] = Lang::game('difficulty').implode('[small]  [/small]', $_); } + // id + $infobox[] = Lang::quest('id') . $this->typeId; + + // profiler relateed (note that this is part of the cache. I don't think this is important enough to calc for every view) + if (Cfg::get('PROFILER_ENABLE') && !($_flags & QUEST_FLAG_UNAVAILABLE || $this->subject->getField('cuFlags') & CUSTOM_EXCLUDE_FOR_LISTVIEW)) + { + $x = DB::Aowow()->selectCell('SELECT COUNT(1) FROM ?_profiler_completion_quests WHERE `questId` = ?d', $this->typeId); + $y = DB::Aowow()->selectCell('SELECT COUNT(1) FROM ?_profiler_profiles WHERE `realm` IS NOT NULL AND `realmGUID` IS NOT NULL'); + $infobox[] = Lang::profiler('attainedBy', [round(($x ?: 0) * 100 / ($y ?: 1))]); + + // - js component missing; + // - can't yet assign styles to li element + // if (User::getPinnedCharacter()) + // $infobox[] = Lang::profiler('completion') . '[span class="compact-completion-display"][/span]'; // [li style="display:none"]...[/li] + } + + // original name + if (Lang::getLocale() != Locale::EN) + $infobox[] = Util::ucFirst(Lang::lang(Locale::EN->value) . Lang::main('colon')) . '[copy button=false]'.$this->subject->getField('name_loc0').'[/copy][/li]'; + if ($infobox) $this->infobox = new InfoboxMarkup($infobox, ['allow' => Markup::CLASS_STAFF, 'dbpage' => true], 'infobox-contents0'); diff --git a/endpoints/race/race.php b/endpoints/race/race.php index 1bb261c6..75c5cd47 100644 --- a/endpoints/race/race.php +++ b/endpoints/race/race.php @@ -99,6 +99,13 @@ class RaceBaseResponse extends TemplateResponse implements ICache $infobox[] = Lang::race('startZone').Lang::main('colon').'[zone='.$_.']'; } + // id + $infobox[] = Lang::race('id') . $this->typeId; + + // original name + if (Lang::getLocale() != Locale::EN) + $infobox[] = Util::ucFirst(Lang::lang(Locale::EN->value) . Lang::main('colon')) . '[copy button=false]'.$this->subject->getField('name_loc0').'[/copy][/li]'; + if ($infobox) $this->infobox = new InfoboxMarkup($infobox, ['allow' => Markup::CLASS_STAFF, 'dbpage' => true], 'infobox-contents0'); diff --git a/endpoints/skill/skill.php b/endpoints/skill/skill.php index 2277c66b..18fb8d8e 100644 --- a/endpoints/skill/skill.php +++ b/endpoints/skill/skill.php @@ -70,6 +70,9 @@ class SkillBaseResponse extends TemplateResponse implements ICache $infobox = Lang::getInfoBoxForFlags($this->subject->getField('cuFlags')); + // id + $infobox[] = Lang::skill('id') . $this->typeId; + // icon if ($_ = $this->subject->getField('iconId')) { @@ -77,6 +80,10 @@ class SkillBaseResponse extends TemplateResponse implements ICache $this->extendGlobalIds(Type::ICON, $_); } + // original name + if (Lang::getLocale() != Locale::EN) + $infobox[] = Util::ucFirst(Lang::lang(Locale::EN->value) . Lang::main('colon')) . '[copy button=false]'.$this->subject->getField('name_loc0').'[/copy][/li]'; + if ($infobox) $this->infobox = new InfoboxMarkup($infobox, ['allow' => Markup::CLASS_STAFF, 'dbpage' => true], 'infobox-contents0'); diff --git a/endpoints/spell/spell.php b/endpoints/spell/spell.php index 8f1e6645..ad9fd262 100644 --- a/endpoints/spell/spell.php +++ b/endpoints/spell/spell.php @@ -2345,13 +2345,33 @@ class SpellBaseResponse extends TemplateResponse implements ICache if ($cost = $this->subject->getField('trainingCost')) $infobox[] = Lang::spell('trainingCost').'[money='.$cost.']'; + // id + $infobox[] = Lang::spell('id') . $this->typeId; + // icon if ($_ = $this->subject->getField('iconId')) { + $infobox[] = Util::ucFirst(Lang::game('icon')).Lang::main('colon').'[icondb='.$_.' name=true]'; $this->extendGlobalIds(Type::ICON, $_); - $infobox[] = Util::ucFirst(lang::game('icon')).Lang::main('colon').'[icondb='.$_.' name=true]'; } + // profiler relateed (note that this is part of the cache. I don't think this is important enough to calc for every view) + if (Cfg::get('PROFILER_ENABLE') && in_array($this->subject->getField('typeCat'), [-5, -6]) && !($this->subject->getField('cuFlags') & CUSTOM_EXCLUDE_FOR_LISTVIEW)) + { + $x = DB::Aowow()->selectCell('SELECT COUNT(1) FROM ?_profiler_completion_spells WHERE `spellId` = ?d', $this->typeId); + $y = DB::Aowow()->selectCell('SELECT COUNT(1) FROM ?_profiler_profiles WHERE `realm` IS NOT NULL AND `realmGUID` IS NOT NULL'); + $infobox[] = Lang::profiler('attainedBy', [round(($x ?: 0) * 100 / ($y ?: 1))]); + + // - js component missing; + // - can't yet assign styles to li element + // if (User::getPinnedCharacter()) + // $infobox[] = Lang::profiler('completion') . '[span class="compact-completion-display"][/span]'; // [li style="display:none"]...[/li] + } + + // original name + if (Lang::getLocale() != Locale::EN) + $infobox[] = Util::ucFirst(Lang::lang(Locale::EN->value) . Lang::main('colon')) . '[copy button=false]'.$this->subject->getField('name_loc0').'[/copy][/li]'; + // used in mode foreach ($this->difficulties as $n => $id) if ($id == $this->typeId) diff --git a/endpoints/title/title.php b/endpoints/title/title.php index 84783c94..382ce66b 100644 --- a/endpoints/title/title.php +++ b/endpoints/title/title.php @@ -84,6 +84,26 @@ class TitleBaseResponse extends TemplateResponse implements ICache $infobox[] = Lang::game('eventShort', ['[event='.$eId.']']); } + // id + $infobox[] = Lang::title('id') . $this->typeId; + + // profiler relateed (note that this is part of the cache. I don't think this is important enough to calc for every view) + if (Cfg::get('PROFILER_ENABLE')) + { + $x = DB::Aowow()->selectCell('SELECT COUNT(1) FROM ?_profiler_completion_titles WHERE `titleId` = ?d', $this->typeId); + $y = DB::Aowow()->selectCell('SELECT COUNT(1) FROM ?_profiler_profiles WHERE `realm` IS NOT NULL AND `realmGUID` IS NOT NULL'); + $infobox[] = Lang::profiler('attainedBy', [round(($x ?: 0) * 100 / ($y ?: 1))]); + + // - js component missing; + // - can't yet assign styles to li element + // if (User::getPinnedCharacter()) + // $infobox[] = Lang::profiler('completion') . '[span class="compact-completion-display"][/span]'; // [li style="display:none"]...[/li] + } + + // original name + if (Lang::getLocale() != Locale::EN) + $infobox[] = Util::ucFirst(Lang::lang(Locale::EN->value) . Lang::main('colon')) . '[copy button=false]'.$this->subject->getField('name_loc0').'[/copy][/li]'; + if ($infobox) $this->infobox = new InfoboxMarkup($infobox, ['allow' => Markup::CLASS_STAFF, 'dbpage' => true], 'infobox-contents0'); diff --git a/endpoints/zone/zone.php b/endpoints/zone/zone.php index 548b4a0d..2039efc9 100644 --- a/endpoints/zone/zone.php +++ b/endpoints/zone/zone.php @@ -188,6 +188,13 @@ class ZoneBaseResponse extends TemplateResponse implements ICache } } + // id + $infobox[] = Lang::zone('id') . $this->typeId; + + // original name + if (Lang::getLocale() != Locale::EN) + $infobox[] = Util::ucFirst(Lang::lang(Locale::EN->value) . Lang::main('colon')) . '[copy button=false]'.$this->subject->getField('name_loc0').'[/copy][/li]'; + if ($botRows = array_filter($quickFactsRows, fn($x) => $x > 0, ARRAY_FILTER_USE_KEY)) $infobox = array_merge($infobox, $botRows); diff --git a/localization/locale_dede.php b/localization/locale_dede.php index af068ae1..1ca2f222 100644 --- a/localization/locale_dede.php +++ b/localization/locale_dede.php @@ -236,6 +236,8 @@ $lang = array( 'atCaptain' => "Teamkapitän", 'atSize' => "Größe: ", 'profiler' => "Charakter-Profiler", + 'completion' => "Vervollständigung: ", + 'attainedBy' => "Erlangt von %d%% der Profile", 'notFound' => array( 'guild' => "Diese Gilde existiert nicht oder wurde noch nicht in die Datenbank übernommen.", 'arenateam' => "Dieses Arena Team existiert nicht oder wurde noch nicht in die Datenbank übernommen.", @@ -1070,6 +1072,7 @@ $lang = array( 'posts' => "Forenbeiträge: " ), 'emote' => array( + 'id' => "Emote-ID: ", 'notFound' => "Dieses Emote existiert nicht.", // 'self' => "An Euch selbst", // 'target' => "An Andere mit Ziel", @@ -1102,9 +1105,10 @@ $lang = array( 'state' => ['Einmalig', 'Stetiger Zustand', 'Stetiges Emote'] ), 'enchantment' => array( + 'id' => "Verzauberungs-ID: ", + 'notFound' => "Diese Verzauberung existiert nicht.", 'details' => "Details", 'activation' => "Aktivierung", - 'notFound' => "Diese Verzauberung existiert nicht.", 'types' => array( 1 => "Zauber (Auslösung)", 3 => "Zauber (Anlegen)", 7 => "Zauber (Benutzen)", 8 => "Prismatischer Sockel", 5 => "Statistik", 2 => "Waffenschaden", 6 => "DPS", 4 => "Verteidigung" @@ -1116,6 +1120,7 @@ $lang = array( 'types' => ['Unbenutzt', 'Gasthaus', 'Teleporter', 'Questziel', 'Smarter Trigger', 'Script'] ), 'gameObject' => array( + 'id' => "Objekt-ID: ", 'notFound' => "Dieses Objekt existiert nicht.", 'cat' => [0 => "Anderes", 3 => "Behälter", 6 => "Fallen", 9 => "Bücher", 25 => "Fischschwärme", -5 => "Truhen", -3 => "Kräuter", -4 => "Erzadern", -2 => "Quest", -6 => "Werkzeuge"], 'type' => [ 3 => "Behälter", 6 => "", 9 => "Buch", 25 => "", -5 => "Truhe", -3 => "Kraut", -4 => "Erzvorkommen", -2 => "Quest", -6 => ""], @@ -1148,6 +1153,7 @@ $lang = array( ) ), 'npc' => array( + 'id' => "NPC-ID: ", 'notFound' => "Dieser NPC existiert nicht.", 'classification'=> "Einstufung: %s", 'petFamily' => "Tierart: ", @@ -1243,6 +1249,7 @@ $lang = array( ) ), 'event' => array( + 'id' => "Weltereignis-ID: ", 'notFound' => "Dieses Weltereignis existiert nicht.", 'start' => "Anfang: ", 'end' => "Ende: ", @@ -1251,6 +1258,7 @@ $lang = array( 'category' => ["Nicht kategorisiert", "Feiertage", "Wiederkehrend", "Spieler vs. Spieler"] ), 'achievement' => array( + 'id' => "Erfolgs-ID: ", 'notFound' => "Dieser Erfolg existiert nicht.", 'criteria' => "Kriterien", 'points' => "Punkte", @@ -1309,9 +1317,11 @@ $lang = array( ) ), 'chrClass' => array( + 'id' => "Klassen-ID: ", 'notFound' => "Diese Klasse existiert nicht." ), 'race' => array( + 'id' => "Volks-ID: ", 'notFound' => "Dieses Volk existiert nicht.", 'racialLeader' => "Volksanführer: ", 'startZone' => "Startgebiet", @@ -1349,6 +1359,7 @@ $lang = array( ) ), 'zone' => array( + 'id' => "Gebiets-ID: ", 'notFound' => "Dieses Gebiet existiert nicht.", 'attunement' => ["Einstimmung: ", "Heroische Einstimmung: "], 'key' => ["Schlüssel: ", "Heroischer Schlüssel: "], @@ -1377,6 +1388,7 @@ $lang = array( ) ), 'quest' => array( + 'id' => "Quest-ID: ", 'notFound' => "Diese Quest existiert nicht.", '_transfer' => 'Dieses Quest wird mit %s vertauscht, wenn Ihr zur %s wechselt.', 'questLevel' => "Stufe %s", @@ -1514,6 +1526,7 @@ $lang = array( 'notFound' => "Dieses Icon existiert nicht." ), 'title' => array( + 'id' => "Titel-ID: ", 'notFound' => "Dieser Titel existiert nicht.", '_transfer' => 'Dieser Titel wird mit %s vertauscht, wenn Ihr zur %s wechselt.', 'cat' => array( @@ -1521,6 +1534,7 @@ $lang = array( ) ), 'skill' => array( + 'id' => "Fertigkeits-ID: ", 'notFound' => "Diese Fertigkeit existiert nicht.", 'cat' => array( -6 => "Haustiere", -5 => "Reittiere", -4 => "Völkerfertigkeiten", 5 => "Attribute", 6 => "Waffenfertigkeiten", 7 => "Klassenfertigkeiten", 8 => "Rüstungssachverstand", @@ -1528,6 +1542,7 @@ $lang = array( ) ), 'currency' => array( + 'id' => "Währungs-ID: ", 'notFound' => "Diese Währung existiert nicht.", 'cap' => "Obergrenze: ", 'cat' => array( @@ -1549,6 +1564,7 @@ $lang = array( ) ), 'mail' => array( + 'id' => "Brief-ID: ", 'notFound' => "Dieser Brief existiert nicht.", 'attachment' => "Anlage", 'mailDelivery' => 'Ihr werdet diesen Brief%s%s erhalten', @@ -1559,12 +1575,14 @@ $lang = array( 'untitled' => "Unbetitelter Brief #%d" ), 'pet' => array( + 'id' => "Tierart-ID: ", 'notFound' => "Diese Tierart existiert nicht.", 'exotic' => "Exotisch", 'cat' => ["Wildheit", "Hartnäckigkeit", "Gerissenheit"], 'food' => ["Fleisch", "Fisch", "Käse", "Brot", "Fungus", "Obst", "Rohes Fleisch", "Roher Fisch"] ), 'faction' => array( + 'id' => "Fraktions-ID: ", 'notFound' => "Diese Fraktion existiert nicht.", 'spillover' => "Reputationsüberlauf", 'spilloverDesc' => "Für diese Fraktion erhaltener Ruf wird zusätzlich mit den unten aufgeführten Fraktionen anteilig verrechnet.", @@ -1580,6 +1598,7 @@ $lang = array( ) ), 'itemset' => array( + 'id' => "Ausrüstungsset-ID: ", 'notFound' => "Dieses Ausrüstungsset existiert nicht.", '_desc' => "%s ist das %s. Es enthält %s Teile.", '_descTagless' => "%s ist ein Ausrüstungsset, das %s Teile enthält.", @@ -1605,6 +1624,7 @@ $lang = array( ) ), 'spell' => array( + 'id' => "Zauber-ID: ", 'notFound' => "Dieser Zauber existiert nicht.", '_spellDetails' => "Zauberdetails", '_cost' => "Kosten", @@ -2168,6 +2188,7 @@ $lang = array( ) ), 'item' => array( + 'id' => "Gegenstands-ID: ", 'notFound' => "Dieser Gegenstand existiert nicht .", 'armor' => "%s Rüstung", 'block' => "%s Blocken", diff --git a/localization/locale_enus.php b/localization/locale_enus.php index 0901c76b..6e1bc7e6 100644 --- a/localization/locale_enus.php +++ b/localization/locale_enus.php @@ -236,6 +236,8 @@ $lang = array( 'atCaptain' => "Arena Team Captain", 'atSize' => "Size: ", 'profiler' => "Character Profiler", + 'completion' => "Completion: ", + 'attainedBy' => "Attained by %d%% of profiles", 'notFound' => array( 'guild' => "This Guild doesn't exist or is not yet in the database.", 'arenateam' => "This Arena Team doesn't exist or is not yet in the database.", @@ -1070,6 +1072,7 @@ $lang = array( 'posts' => "Forum posts: " ), 'emote' => array( + 'id' => "Emote ID: ", 'notFound' => "This Emote doesn't exist.", // 'self' => "To Yourself", // 'target' => "To others with a target", @@ -1102,9 +1105,10 @@ $lang = array( 'state' => ['Oneshot', 'Continuous State', 'Continuous Emote'] ), 'enchantment' => array( + 'id' => "Enchantment ID: ", + 'notFound' => "This enchantment doesn't exist.", 'details' => "Details", 'activation' => "Activation", - 'notFound' => "This enchantment doesn't exist.", 'types' => array( 1 => "Proc Spell", 3 => "Equip Spell", 7 => "Use Spell", 8 => "Prismatic Socket", 5 => "Statistics", 2 => "Weapon Damage", 6 => "DPS", 4 => "Defense" @@ -1116,6 +1120,7 @@ $lang = array( 'types' => ['Unused', 'Tavern', 'Teleporter', 'Quest Objective', 'Smart Trigger', 'Script'] ), 'gameObject' => array( + 'id' => "Object ID: ", 'notFound' => "This object doesn't exist.", 'cat' => [0 => "Other", 3 => "Containers", 6 => "Traps", 9 => "Books", 25 => "Fishing Pools", -5 => "Chests", -3 => "Herbs", -4 => "Mineral Veins", -2 => "Quest", -6 => "Tools"], 'type' => [ 3 => "Container", 6 => "", 9 => "Book", 25 => "", -5 => "Chest", -3 => "Herb", -4 => "Mineral Vein", -2 => "Quest", -6 => ""], // used for tooltip @@ -1148,6 +1153,7 @@ $lang = array( ) ), 'npc' => array( + 'id' => "NPC ID: ", 'notFound' => "This NPC doesn't exist.", 'classification'=> "Classification: %s", 'petFamily' => "Pet familiy: ", @@ -1243,6 +1249,7 @@ $lang = array( ) ), 'event' => array( + 'id' => "World Event ID: ", 'notFound' => "This world event doesn't exist.", 'start' => "Start: ", 'end' => "End: ", @@ -1251,6 +1258,7 @@ $lang = array( 'category' => ["Uncategorized", "Holidays", "Recurring", "Player vs. Player"] ), 'achievement' => array( + 'id' => "Achievement ID: ", 'notFound' => "This achievement doesn't exist.", 'criteria' => "Criteria", 'points' => "Points", @@ -1309,9 +1317,11 @@ $lang = array( ) ), 'chrClass' => array( + 'id' => "Class ID: ", 'notFound' => "This class doesn't exist." ), 'race' => array( + 'id' => "Race ID: ", 'notFound' => "This race doesn't exist.", 'racialLeader' => "Racial leader: ", 'startZone' => "Starting zone", @@ -1349,6 +1359,7 @@ $lang = array( ) ), 'zone' => array( + 'id' => "Zone ID: ", 'notFound' => "This zone doesn't exist.", 'attunement' => ["Attunement: ", "Heroic attunement: "], 'key' => ["Key: ", "Heroic key: "], @@ -1377,6 +1388,7 @@ $lang = array( ) ), 'quest' => array( + 'id' => "Quest ID: ", 'notFound' => "This quest doesn't exist.", '_transfer' => 'This quest will be converted to %s if you transfer to %s.', 'questLevel' => "Level %s", @@ -1514,6 +1526,7 @@ $lang = array( 'notFound' => "This icon doesn't exist." ), 'title' => array( + 'id' => "Title ID: ", 'notFound' => "This title doesn't exist.", '_transfer' => 'This title will be converted to %s if you transfer to %s.', 'cat' => array( @@ -1521,6 +1534,7 @@ $lang = array( ) ), 'skill' => array( + 'id' => "Skill ID: ", 'notFound' => "This skill doesn't exist.", 'cat' => array( -6 => "Companions", -5 => "Mounts", -4 => "Racial Traits", 5 => "Attributes", 6 => "Weapon Skills", 7 => "Class Skills", 8 => "Armor Proficiencies", @@ -1528,6 +1542,7 @@ $lang = array( ) ), 'currency' => array( + 'id' => "Currency ID: ", 'notFound' => "This currency doesn't exist.", 'cap' => "Total cap: ", 'cat' => array( @@ -1549,6 +1564,7 @@ $lang = array( ) ), 'mail' => array( + 'id' => "Mail ID: ", 'notFound' => "This mail doesn't exist.", 'attachment' => "Attachment", 'mailDelivery' => 'You will receive this letter%s%s', @@ -1559,12 +1575,14 @@ $lang = array( 'untitled' => "Untitled Mail #%d" ), 'pet' => array( + 'id' => "Pet family ID: ", 'notFound' => "This pet family doesn't exist.", 'exotic' => "Exotic", 'cat' => ["Ferocity", "Tenacity", "Cunning"], 'food' => ["Meat", "Fish", "Cheese", "Bread", "Fungus", "Fruit", "Raw Meat", "Raw Fish"] // ItemPetFood.dbc ), 'faction' => array( + 'id' => "Faction ID: ", 'notFound' => "This faction doesn't exist.", 'spillover' => "Reputation Spillover", 'spilloverDesc' => "Gaining reputation with this faction also yields a proportional gain with the factions listed below.", @@ -1580,6 +1598,7 @@ $lang = array( ) ), 'itemset' => array( + 'id' => "Item Set ID: ", 'notFound' => "This item set doesn't exist.", '_desc' => "%s is the %s. It contains %s pieces.", '_descTagless' => "%s is an item set that contains %s pieces.", @@ -1605,6 +1624,7 @@ $lang = array( ) ), 'spell' => array( + 'id' => "Spell ID: ", 'notFound' => "This spell doesn't exist.", '_spellDetails' => "Spell Details", '_cost' => "Cost", @@ -2168,6 +2188,7 @@ $lang = array( ) ), 'item' => array( + 'id' => "Item ID: ", 'notFound' => "This item doesn't exist.", 'armor' => "%s Armor", // ARMOR_TEMPLATE 'block' => "%s Block", // SHIELD_BLOCK_TEMPLATE diff --git a/localization/locale_eses.php b/localization/locale_eses.php index 0f75fb34..e1f772ac 100644 --- a/localization/locale_eses.php +++ b/localization/locale_eses.php @@ -236,6 +236,8 @@ $lang = array( 'atCaptain' => "Capitán de equipo de arena", 'atSize' => "Tamaño: ", 'profiler' => "Gestor de Perfiles", // Perfiles de Personaje? (character profiler) + 'completion' => "Terminación: ", + 'attainedBy' => "Obtenido por %d%% de perfiles", 'notFound' => array( 'guild' => "Esta hermandad no existe o aún no está en la base de datos.", 'arenateam' => "Este equipo de arena no existe o aún no está en la base de datos.", @@ -315,7 +317,7 @@ $lang = array( 'achievements' => "Logros", 'title' => "título", 'titles' => "Títulos", - 'event' => "Suceso mundial ", + 'event' => "Suceso mundial", 'events' => "Eventos del mundo", 'class' => "clase", 'classes' => "Clases", @@ -323,7 +325,7 @@ $lang = array( 'races' => "Razas", 'skill' => "habilidad", 'skills' => "Habilidades", - 'currency' => "monedas", + 'currency' => "moneda", 'currencies' => "Monedas", 'sound' => "sonido", 'sounds' => "Sonidos", @@ -1070,6 +1072,7 @@ $lang = array( 'posts' => "Mensajes en los foros: " ), 'emote' => array( + 'id' => "ID de Emoticón: ", 'notFound' => "Este emoticón no existe.", // 'self' => "Para Usted", // 'target' => "Para otros con un objetivo", @@ -1102,9 +1105,10 @@ $lang = array( 'state' => ['Oneshot', 'Estado continuo', 'Emoticono continuo'] ), 'enchantment' => array( + 'id' => "ID de Encantamiento: ", + 'notFound' => "Este encantamiento no existe.", 'details' => "Detalles", 'activation' => "Activación", - 'notFound' => "Este encantamiento no existe.", 'types' => array( 1 => "Prob. Hechizo", 3 => "Equipar Hechizo", 7 => "Usar Hechizo", 8 => "Ranura prismática", 5 => "Atributos", 2 => "Daño de arma", 6 => "DPS", 4 => "Defensa" @@ -1116,6 +1120,7 @@ $lang = array( 'types' => ['Sin usar', 'Taberna', 'Teletransportador', 'Objetivo de misión', 'Activador inteligente', 'Script'] ), 'gameObject' => array( + 'id' => "ID de Entidad: ", 'notFound' => "Esta entidad no existe.", 'cat' => [0 => "Otros", 3 => "Contenedores", 6 => "Trampas", 9 => "Libros", 25 => "Bancos de peces", -5 => "Cofres", -3 => "Hierbas", -4 => "Venas de minerales", -2 => "Misiones", -6 => "Herramientas"], 'type' => [ 3 => "Contenedore", 6 => "", 9 => "Libro", 25 => "", -5 => "Cofre", -3 => "Hierba", -4 => "Filóne de mineral", -2 => "Misión", -6 => ""], @@ -1148,6 +1153,7 @@ $lang = array( ) ), 'npc' => array( + 'id' => "ID de PNJ: ", 'notFound' => "Este PNJ no existe.", 'classification'=> "Clasificación: %s", 'petFamily' => "Familia de mascota: ", @@ -1243,6 +1249,7 @@ $lang = array( ) ), 'event' => array( + 'id' => "ID de Suceso mundial: ", 'notFound' => "Este evento del mundo no existe.", 'start' => "Empieza: ", 'end' => "Termina: ", @@ -1251,6 +1258,7 @@ $lang = array( 'category' => ["Sin categoría", "Vacacionales", "Periódicos", "Jugador contra Jugador"] ), 'achievement' => array( + 'id' => "ID de Logro: ", 'notFound' => "Este logro no existe.", 'criteria' => "Requisitos", 'points' => "Puntos", @@ -1309,9 +1317,11 @@ $lang = array( ) ), 'chrClass' => array( + 'id' => "ID de Clase: ", 'notFound' => "Esta clase no existe." ), 'race' => array( + 'id' => "ID de Raza: ", 'notFound' => "Esta raza no existe.", 'racialLeader' => "Lider racial: ", 'startZone' => "Zona de inicio", @@ -1349,6 +1359,7 @@ $lang = array( ) ), 'zone' => array( + 'id' => "ID de Zona: ", 'notFound' => "Esta zona no existe.", 'attunement' => ["Requisito: ", "Requisito heroica: "], 'key' => ["Llave: ", "Llave heroica: "], @@ -1377,6 +1388,7 @@ $lang = array( ) ), 'quest' => array( + 'id' => "ID de Misión: ", 'notFound' => "Esta misión no existe.", '_transfer' => 'Esta misión será convertido a %s si lo transfieres a la %s.', 'questLevel' => 'Nivel %s', @@ -1514,6 +1526,7 @@ $lang = array( 'notFound' => "Este icono no existe." ), 'title' => array( + 'id' => "ID de Título: ", 'notFound' => "Este título no existe.", '_transfer' => 'Este título será convertido a %s si lo transfieres a la %s.', 'cat' => array( @@ -1521,6 +1534,7 @@ $lang = array( ) ), 'skill' => array( + 'id' => "ID de Habilidad: ", 'notFound' => "Esta habilidad no existe.", 'cat' => array( -6 => "Compañeros", -5 => "Monturas", -4 => "Habilidades de raza", 5 => "Atributos", 6 => "Habilidades con armas", 7 => "Habilidades de clase", 8 => "Armaduras disponibles", @@ -1528,6 +1542,7 @@ $lang = array( ) ), 'currency' => array( + 'id' => "ID de Moneda: ", 'notFound' => "Esta moneda no existe.", 'cap' => "Límite total: ", 'cat' => array( @@ -1549,6 +1564,7 @@ $lang = array( ) ), 'mail' => array( + 'id' => "ID de Correo: ", 'notFound' => "Este correo no existe.", 'attachment' => "Adjunto", 'mailDelivery' => "Usted recibirá esta carta%s%s", @@ -1559,12 +1575,14 @@ $lang = array( 'untitled' => "Correo sin título #%d" ), 'pet' => array( + 'id' => "ID de Familia de mascotas: ", 'notFound' => "Esta familia de mascotas no existe.", 'exotic' => "Exótica", 'cat' => ["Ferocidad", "Tenacidad", "Astucia"], 'food' => ["Carne", "Pescado", "Queso", "Pan", "Hongo", "Fruta", "Carne cruda", "Pescado crudo"] ), 'faction' => array( + 'id' => "ID de Facción: ", 'notFound' => "Esta facción no existe.", 'spillover' => "Excedente de reputación", 'spilloverDesc' => "Ganar reputación con esta facción tambien una proporción ganada con las facciones listadas a continuación.", @@ -1580,6 +1598,7 @@ $lang = array( ) ), 'itemset' => array( + 'id' => "ID de Conjunto de objetos: ", 'notFound' => "Este conjunto de objetos no existe.", '_desc' => "%s es el %s. Contiene %s piezas.", '_descTagless' => "%s es un conjunto de objetos que tiene %s piezas.", @@ -1605,6 +1624,7 @@ $lang = array( ) ), 'spell' => array( + 'id' => "ID de Hechizo: ", 'notFound' => "Este hechizo no existe.", '_spellDetails' => "Detalles de hechizos", '_cost' => "Costo", @@ -2168,6 +2188,7 @@ $lang = array( ) ), 'item' => array( + 'id' => "ID de Objeto: ", 'notFound' => "Este objeto no existe.", 'armor' => "%s armadura", 'block' => "%s bloqueo", diff --git a/localization/locale_frfr.php b/localization/locale_frfr.php index 1a717c01..62f31429 100644 --- a/localization/locale_frfr.php +++ b/localization/locale_frfr.php @@ -236,6 +236,8 @@ $lang = array( 'atCaptain' => "Capitaine d'équipe", 'atSize' => "Type : ", 'profiler' => "Profiler de Personnage", + 'completion' => "Achèvement : ", + 'attainedBy' => "Obtenu par %d%% des profils", 'notFound' => array( 'guild' => "[This Guild doesn't exist or is not yet in the database.]", 'arenateam' => "[This Arena Team doesn't exist or is not yet in the database.]", @@ -323,7 +325,7 @@ $lang = array( 'races' => "Races", 'skill' => "compétence", 'skills' => "Compétences", - 'currency' => "monnaies", + 'currency' => "monnaie", 'currencies' => "Monnaies", 'sound' => "son", 'sounds' => "Sons", @@ -1070,6 +1072,7 @@ $lang = array( 'posts' => "Messages sur le forum : " ), 'emote' => array( + 'id' => "ID Emote : ", 'notFound' => "[This Emote doesn't exist.]", // 'self' => "Vers vous-même", // 'target' => "Vers les autres avec une cible", @@ -1102,9 +1105,10 @@ $lang = array( 'state' => ['[Oneshot]', '[Continuous State]', '[Continuous Emote]'] ), 'enchantment' => array( + 'id' => "ID Enchantement : ", + 'notFound' => "Cet enchantement n'existe pas.", 'details' => "En détail", 'activation' => "Activation", - 'notFound' => "Cet enchantement n'existe pas.", 'types' => array( 1 => "Sort proc", 3 => "Sort équipé", 7 => "Sort utilisé", 8 => "Châsse prismatique", 5 => "Statistiques", 2 => "Dégâts d'arme", 6 => "DPS", 4 => "Défense" @@ -1116,6 +1120,7 @@ $lang = array( 'types' => ['Unused', 'Tavern', 'Teleporter', 'Quest Objective', 'Smart Trigger', 'Script'] ), 'gameObject' => array( + 'id' => "ID Entité: ", 'notFound' => "Cette entité n'existe pas.", 'cat' => [0 => "Autre", 3 => "Conteneurs", 6 => "Pièges", 9 => "Livres", 25 => "Bancs de poissons", -5 => "Coffres", -3 => "Herbes", -4 => "Filons de minerai", -2 => "Quêtes", -6 => "Outils"], 'type' => [ 3 => "Conteneur", 6 => "", 9 => "Livre", 25 => "", -5 => "Coffre", -3 => "Herbe", -4 => "Filon de minerai", -2 => "Quête", -6 => ""], @@ -1148,6 +1153,7 @@ $lang = array( ) ), 'npc' => array( + 'id' => "ID PNJ : ", 'notFound' => "Ce PNJ n'existe pas.", 'classification'=> "Classification : %s", 'petFamily' => "Familier : ", @@ -1243,6 +1249,7 @@ $lang = array( ) ), 'event' => array( + 'id' => "ID Évènement mondial : ", 'notFound' => "Cet évènement mondial n'existe pas.", 'start' => "Début : ", 'end' => "Fin : ", @@ -1251,6 +1258,7 @@ $lang = array( 'category' => ["Non classés", "Vacances", "Récurrent", "Joueur ctr. Joueur"] ), 'achievement' => array( + 'id' => "ID Haut fait : ", 'notFound' => "Ce haut fait n'existe pas.", 'criteria' => "Critères", 'points' => "Points", @@ -1309,9 +1317,11 @@ $lang = array( ) ), 'chrClass' => array( + 'id' => "ID Classe : ", 'notFound' => "Cette classe n'existe pas." ), 'race' => array( + 'id' => "ID Race : ", 'notFound' => "Cette race n'existe pas.", 'racialLeader' => "Leader racial : ", 'startZone' => "Zone initiales", @@ -1349,6 +1359,7 @@ $lang = array( ) ), 'zone' => array( + 'id' => "ID Zone : ", 'notFound' => "Cette zone n'existe pas.", 'attunement' => ["Accès : ", "Accès Héroïque : "], 'key' => ["Clef : ", "Clef Héroïque : "], @@ -1377,6 +1388,7 @@ $lang = array( ) ), 'quest' => array( + 'id' => "ID Quête : ", 'notFound' => "Cette quête n'existe pas.", '_transfer' => 'Cette quête sera converti en %s si vous transférez en %s.', 'questLevel' => "Niveau %s", @@ -1514,6 +1526,7 @@ $lang = array( 'notFound' => "Cette icône n'existe pas." ), 'title' => array( + 'id' => "ID Titre : ", 'notFound' => "Ce titre n'existe pas.", '_transfer' => 'Ce titre sera converti en %s si vous transférez en %s.', 'cat' => array( @@ -1521,6 +1534,7 @@ $lang = array( ) ), 'skill' => array( + 'id' => "ID Compétence : ", 'notFound' => "Cette compétence n'existe pas.", 'cat' => array( -6 => "Compagnons", -5 => "Montures", -4 => "Traits raciaux", 5 => "Caractéristiques", 6 => "Compétences d'armes", 7 => "Compétences de classe", 8 => "Armures utilisables", @@ -1528,6 +1542,7 @@ $lang = array( ) ), 'currency' => array( + 'id' => "ID Monnaie : ", 'notFound' => "Cette monnaie n'existe pas.", 'cap' => "Maximum total : ", 'cat' => array( @@ -1549,22 +1564,25 @@ $lang = array( ) ), 'mail' => array( - 'notFound' => "This mail doesn't exist.", + 'id' => "[Mail ID] : ", + 'notFound' => "[This mail doesn't exist.]", 'attachment' => "[Attachment]", 'mailDelivery' => "Vous recevrez cette lettre%s%s", 'mailBy' => ' de %s', 'mailIn' => " après %s", - 'delay' => "Delay : %s", - 'sender' => "Sender : %s", - 'untitled' => "Untitled Mail #%d" + 'delay' => "[Delay] : %s", + 'sender' => "[Sender] : %s", + 'untitled' => "[Untitled Mail] #%d" ), 'pet' => array( + 'id' => "ID Famille de familiers : ", 'notFound' => "Cette famille de familiers n'existe pas.", 'exotic' => "Exotique", 'cat' => ["Férocité", "Tenacité", "Ruse"], 'food' => ["Viande", "Poisson", "Fromage", "Pain", "Champignon", "Fruit", "Viande crue", "Poisson cru"] ), 'faction' => array( + 'id' => "ID Faction : ", 'notFound' => "Cette faction n'existe pas.", 'spillover' => "Partage de réputations", 'spilloverDesc' => "Gagner de la réputation avec cette faction fourni une réputation proportionnelle avec les factions ci-dessous.", @@ -1580,6 +1598,7 @@ $lang = array( ) ), 'itemset' => array( + 'id' => "ID d'ensemble d'objets : ", 'notFound' => "Cet ensemble d'objets n'existe pas.", '_desc' => "%s est le %s. Il contient %s pièces.", '_descTagless' => "%s est un ensemble d'objet qui contient %s pièces.", @@ -1605,6 +1624,7 @@ $lang = array( ) ), 'spell' => array( + 'id' => "ID Sort: ", 'notFound' => "Ce sort n'existe pas.", '_spellDetails' => "Détails sur le sort", '_cost' => "Coût", @@ -2168,6 +2188,7 @@ $lang = array( ) ), 'item' => array( + 'id' => "ID Objet : ", 'notFound' => "Cet objet n'existe pas.", 'armor' => "Armure : %s", 'block' => "Bloquer : %s", diff --git a/localization/locale_ruru.php b/localization/locale_ruru.php index 81e51b44..e9587064 100644 --- a/localization/locale_ruru.php +++ b/localization/locale_ruru.php @@ -236,6 +236,8 @@ $lang = array( 'atCaptain' => "Капитан команды арены", 'atSize' => "Численности: ", 'profiler' => "Профили персонажей", + 'completion' => "Завершено: ", + 'attainedBy' => "Получено %d%% пользователей с профилями", 'notFound' => array( 'profile' => "Этот персонаж не существует, либо еще не добавлен в базу данных.", 'arenateam' => "[This Arena Team doesn't exist or is not yet in the database.]", @@ -1070,6 +1072,7 @@ $lang = array( 'posts' => "Сообщений на форумах: " ), 'emote' => array( + 'id' => "Эмоция ID: ", 'notFound' => "[This Emote doesn't exist.]", // 'self' => "[To Yourself]", // 'target' => "[To others with a target]", @@ -1102,9 +1105,10 @@ $lang = array( 'state' => ['[Oneshot]', '[Continuous State]', '[Continuous Emote]'] ), 'enchantment' => array( + 'id' => "Улучшение ID: ", + 'notFound' => "Такой улучшение не существует.", 'details' => "Подробности", 'activation' => "Активации", - 'notFound' => "Такой улучшение не существует.", 'types' => array( 1 => "[Proc Spell]", 3 => "[Equip Spell]", 7 => "[Use Spell]", 8 => "Бесцветное гнездо", 5 => "Характеристики", 2 => "Урон оружия", 6 => "УВС", 4 => "Защита" @@ -1116,6 +1120,7 @@ $lang = array( 'types' => ['Unused', 'Tavern', 'Teleporter', 'Quest Objective', 'Smart Trigger', 'Script'] ), 'gameObject' => array( + 'id' => "Объект ID: ", 'notFound' => "Такой объект не существует.", 'cat' => [0 => "Другое", 3 => "Контейнеры", 6 => "Ловушки", 9 => "Книги", 25 => "Рыболовные лунки", -5 => "Сундуки", -3 => "Травы", -4 => "Полезные ископаемые", -2 => "Задания", -6 => "Инструменты"], 'type' => [ 3 => "Контейнер", 6 => "", 9 => "Книга", 25 => "", -5 => "Сундук", -3 => "Растение", -4 => "Полезное ископаемое", -2 => "Задание", -6 => ""], @@ -1148,6 +1153,7 @@ $lang = array( ) ), 'npc' => array( + 'id' => "НИП ID: ", 'notFound' => "Такой НИП не существует.", 'classification'=> "Классификация: %s", 'petFamily' => "Семейство питомца: ", @@ -1243,6 +1249,7 @@ $lang = array( ) ), 'event' => array( + 'id' => "Игровое событие ID: ", 'notFound' => "Это игровое событие не существует.", 'start' => "Начало: ", 'end' => "Конец: ", @@ -1251,6 +1258,7 @@ $lang = array( 'category' => array("Разное", "Праздники", "Периодические", "PvP") ), 'achievement' => array( + 'id' => "Достижение ID: ", 'notFound' => "Такое достижение не существует.", 'criteria' => "Критерий", 'points' => "Очки", @@ -1309,9 +1317,11 @@ $lang = array( ) ), 'chrClass' => array( + 'id' => "Класс ID: ", 'notFound' => "Такой класс не существует." ), 'race' => array( + 'id' => "Раса ID: ", 'notFound' => "Такая раса не существует.", 'racialLeader' => "Лидер расы: ", 'startZone' => "Начальная локация", @@ -1349,6 +1359,7 @@ $lang = array( ) ), 'zone' => array( + 'id' => "Игровая зона ID: ", 'notFound' => "Такая местность не существует.", 'attunement' => ["[Attunement]: ", "[Heroic attunement]: "], 'key' => ["[Key]: ", "[Heroic key]: "], @@ -1377,6 +1388,7 @@ $lang = array( ) ), 'quest' => array( + 'id' => "Задание ID: ", 'notFound' => "Такое задание не существует.", '_transfer' => 'Этот предмет превратится в %s, если вы перейдете за %s.', 'questLevel' => "%s-го уровня", @@ -1514,6 +1526,7 @@ $lang = array( 'notFound' => "Этой иконки не существует" ), 'title' => array( + 'id' => "Звание ID: ", 'notFound' => "Такое звание не существует.", '_transfer' => 'Этот предмет превратится в %s, если вы перейдете за %s.', 'cat' => array( @@ -1521,6 +1534,7 @@ $lang = array( ) ), 'skill' => array( + 'id' => "Навык ID: ", 'notFound' => "Этот навык не существует.", 'cat' => array( -6 => "Спутники", -5 => "Транспорт", -4 => "Классовые навыки", 5 => "Характеристики", 6 => "Оружейные навыки", 7 => "Классовые навыки", 8 => "Доспехи", @@ -1528,6 +1542,7 @@ $lang = array( ) ), 'currency' => array( + 'id' => "Валюта ID: ", 'notFound' => "Такая валюта не существует.", 'cap' => "Максимум всего: ", 'cat' => array( @@ -1549,6 +1564,7 @@ $lang = array( ) ), 'mail' => array( + 'id' => "[Mail ID]: ", 'notFound' => "[This mail doesn't exist].", 'attachment' => "[Attachment]", 'mailDelivery' => "Вы получите это письмо%s%s", @@ -1559,12 +1575,14 @@ $lang = array( 'untitled' => "[Untitled Mail] #%d" ), 'pet' => array( + 'id' => "Питомец ID: ", 'notFound' => "Такой породы питомцев не существует.", 'exotic' => "Экзотический", 'cat' => ["Свирепость", "Упорство", "Хитрость"], 'food' => ["Мясо", "Рыба", "Сыр", "Хлеб", "Грибы", "Фрукты", "Сырое мясо", "Сырая рыба"] ), 'faction' => array( + 'id' => "Фракция ID: ", 'notFound' => "Такая фракция не существует.", 'spillover' => "Распространение репутации", 'spilloverDesc' => "Получение репутации у этой фракции также дает пропорциональный выигрыш по отношению к фракциям, перечисленным ниже.", @@ -1580,6 +1598,7 @@ $lang = array( ) ), 'itemset' => array( + 'id' => "Комплект ID: ", 'notFound' => "Такой комплект не существует.", '_desc' => "%s%s. Он состоит из %s предметов.", '_descTagless' => "%s — набор из %s предметов.", @@ -1605,6 +1624,7 @@ $lang = array( ) ), 'spell' => array( + 'id' => "Заклинание ID: ", 'notFound' => "Такое заклинание не существует.", '_spellDetails' => "Описание заклинания", '_cost' => "Цена", @@ -2168,6 +2188,7 @@ $lang = array( ) ), 'item' => array( + 'id' => "Предмет ID: ", 'notFound' => "Такой предмет не существует.", 'armor' => "Броня: %s", 'block' => "Блок: %s", diff --git a/localization/locale_zhcn.php b/localization/locale_zhcn.php index 05c1d150..7819c9ea 100644 --- a/localization/locale_zhcn.php +++ b/localization/locale_zhcn.php @@ -236,6 +236,8 @@ $lang = array( 'atCaptain' => "竞技场战队队长", 'atSize' => "团队规模:", 'profiler' => "角色概况", + 'completion' => "达成:", + 'attainedBy' => "%d%%角色已完成", 'notFound' => array( 'guild' => "该公会不存在或尚未被收录到数据库中。", 'arenateam' => "该竞技场战队不存在或尚未被收录到数据库中。", @@ -1070,6 +1072,7 @@ $lang = array( 'posts' => "论坛帖子:" ), 'emote' => array( + 'id' => "表情ID:", 'notFound' => "这个表情不存在。", // 'self' => "对你自己", // 'target' => "对别人并且选择了目标", @@ -1102,9 +1105,10 @@ $lang = array( 'state' => ['[Oneshot]', '[Continuous State]', '[Continuous Emote]'] ), 'enchantment' => array( + 'id' => "附魔ID:", + 'notFound' => "这个附魔不存在。", 'details' => "细节", 'activation' => "激活", - 'notFound' => "这个附魔不存在。", 'types' => array( 1 => "触发法术", 3 => "装备法术", 7 => "使用法术", 8 => "棱形插槽", 5 => "统计", 2 => "武器伤害", 6 => "DPS", 4 => "防御" @@ -1116,6 +1120,7 @@ $lang = array( 'types' => ['未使用', '酒馆', '传送门', '任务目标', 'Smart Trigger', '脚本'] ), 'gameObject' => array( + 'id' => "对象ID:", 'notFound' => "这个对象不存在。", 'cat' => [0 => "其他", 3 => "容器", 6 => "陷阱", 9 => "书籍", 25 => "钓鱼点", -5 => "宝箱", -3 => "草药", -4 => "矿脉", -2 => "任务", -6 => "工具"], 'type' => [ 3 => "容器", 6 => "", 9 => "书籍", 25 => "", -5 => "宝箱", -3 => "草药", -4 => "矿脉", -2 => "任务", -6 => ""], @@ -1148,6 +1153,7 @@ $lang = array( ) ), 'npc' => array( + 'id' => "NPC ID:", 'notFound' => "这个NPC不存在。", 'classification'=> "分类:%s", 'petFamily' => "宠物家族:", @@ -1243,6 +1249,7 @@ $lang = array( ) ), 'event' => array( + 'id' => "世界事件ID:", 'notFound' => "这个世界事件不存在。", 'start' => "开始:", 'end' => "结束:", @@ -1251,6 +1258,7 @@ $lang = array( 'category' => ["未分类", "节日", "循环", "PvP"] ), 'achievement' => array( + 'id' => "成就ID:", 'notFound' => "这个成就不存在。", 'criteria' => "达成条件", 'points' => "点数", @@ -1309,9 +1317,11 @@ $lang = array( ) ), 'chrClass' => array( + 'id' => "职业ID:", 'notFound' => "这个职业不存在。" ), 'race' => array( + 'id' => "种族ID:", 'notFound' => "这个种族不存在。", 'racialLeader' => "种族领袖:", 'startZone' => "起始区域" @@ -1349,6 +1359,7 @@ $lang = array( ) ), 'zone' => array( + 'id' => "区域ID:", 'notFound' => "这个区域不存在。", 'attunement' => ["调整:", "英雄调整:"], 'key' => ["钥匙:", "英雄钥匙:"], @@ -1377,6 +1388,7 @@ $lang = array( ) ), 'quest' => array( + 'id' => "任务ID:", 'notFound' => "这个任务不存在。", '_transfer' => '这个任务将被转换到%s,如果你转移到%s。', 'questLevel' => "等级%s", @@ -1514,6 +1526,7 @@ $lang = array( 'notFound' => "这个图标不存在。" ), 'title' => array( + 'id' => "头衔ID:", 'notFound' => "这个头衔不存在。", '_transfer' => '这个头衔将被转换到%s,如果你转移到%s。', 'cat' => array( @@ -1521,6 +1534,7 @@ $lang = array( ) ), 'skill' => array( + 'id' => "技能ID:", 'notFound' => "这个技能不存在。", 'cat' => array( -6 => "伙伴", -5 => "坐骑", -4 => "种族特性", 5 => "属性", 6 => "武器技能", 7 => "职业技能", 8 => "护甲精通", @@ -1528,6 +1542,7 @@ $lang = array( ) ), 'currency' => array( + 'id' => "货币ID:", 'notFound' => "这个货币不存在。", 'cap' => "总共上限:", 'cat' => array( @@ -1549,6 +1564,7 @@ $lang = array( ) ), 'mail' => array( + 'id' => "邮件ID:", 'notFound' => "该邮件不存在。", 'attachment' => "附件", 'mailDelivery' => '你将收到 这封信%s%s', // "你会收到这封信%s%s", @@ -1559,12 +1575,14 @@ $lang = array( 'untitled' => "无标题邮件 #%d" ), 'pet' => array( + 'id' => "猎人宠物ID:", 'notFound' => "这个宠物家族不存在。", 'exotic' => "异域的", 'cat' => ["狂野", "坚韧", "狡诈"], 'food' => ["肉", "鱼", "奶酪", "面包", "蘑菇", "水果", "生肉", "生鱼"] ), 'faction' => array( + 'id' => "阵营ID:", 'notFound' => "这个阵营不存在。", 'spillover' => "声望额外效果", 'spilloverDesc' => "获得这个阵营的声望也将按比例获得下列阵营的声望。", @@ -1580,6 +1598,7 @@ $lang = array( ) ), 'itemset' => array( + 'id' => "套装ID:", 'notFound' => "这个物品套装不存在。", '_desc' => "%s%s。它包含%s件。", '_descTagless' => "%s是物品套装包含%s件。", @@ -1605,6 +1624,7 @@ $lang = array( ) ), 'spell' => array( + 'id' => "法术ID:", 'notFound' => "这个法术不存在。", '_spellDetails' => "法术细节", '_cost' => "花费", @@ -2168,6 +2188,7 @@ $lang = array( ) ), 'item' => array( + 'id' => "物品ID:", 'notFound' => "这个物品不存在。", 'armor' => "%d点护甲", 'block' => "%d格挡", diff --git a/setup/sql/01-db_structure.sql b/setup/sql/01-db_structure.sql index d3709dca..6063d5fb 100644 --- a/setup/sql/01-db_structure.sql +++ b/setup/sql/01-db_structure.sql @@ -1729,6 +1729,7 @@ CREATE TABLE `aowow_profiler_completion_quests` ( `id` int(10) unsigned NOT NULL, `questId` mediumint(8) unsigned NOT NULL, KEY `id` (`id`), + KEY `typeId` (`questId`), CONSTRAINT `FK_pr_completion_quests` FOREIGN KEY (`id`) REFERENCES `aowow_profiler_profiles` (`id`) ON DELETE CASCADE ON UPDATE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; @@ -1745,6 +1746,7 @@ CREATE TABLE `aowow_profiler_completion_reputation` ( `factionId` smallint(5) unsigned NOT NULL, `standing` mediumint(9) DEFAULT NULL, KEY `id` (`id`), + KEY `typeId` (`factionId`), CONSTRAINT `FK_pr_completion_reputation` FOREIGN KEY (`id`) REFERENCES `aowow_profiler_profiles` (`id`) ON DELETE CASCADE ON UPDATE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; @@ -1778,6 +1780,7 @@ CREATE TABLE `aowow_profiler_completion_spells` ( `id` int(10) unsigned NOT NULL, `spellId` mediumint(8) unsigned NOT NULL, KEY `id` (`id`), + KEY `typeId` (`spellId`), CONSTRAINT `FK_pr_completion_spells` FOREIGN KEY (`id`) REFERENCES `aowow_profiler_profiles` (`id`) ON DELETE CASCADE ON UPDATE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; @@ -1811,6 +1814,7 @@ CREATE TABLE `aowow_profiler_completion_titles` ( `id` int(10) unsigned NOT NULL, `titleId` tinyint(3) unsigned NOT NULL, KEY `id` (`id`), + KEY `typeId` (`titleId`), CONSTRAINT `FK_pr_completion_titles` FOREIGN KEY (`id`) REFERENCES `aowow_profiler_profiles` (`id`) ON DELETE CASCADE ON UPDATE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; diff --git a/setup/sql/updates/1759504522_01.sql b/setup/sql/updates/1759504522_01.sql new file mode 100644 index 00000000..12872d0e --- /dev/null +++ b/setup/sql/updates/1759504522_01.sql @@ -0,0 +1,11 @@ +ALTER TABLE aowow_profiler_completion_quests + ADD KEY `typeId` (`questId`); + +ALTER TABLE aowow_profiler_completion_reputation + ADD KEY `typeId` (`factionId`); + +ALTER TABLE aowow_profiler_completion_spells + ADD KEY `typeId` (`spellId`); + +ALTER TABLE aowow_profiler_completion_titles + ADD KEY `typeId` (`titleId`); diff --git a/setup/sql/updates/1759504522_02.sql b/setup/sql/updates/1759504522_02.sql new file mode 100644 index 00000000..03adf418 --- /dev/null +++ b/setup/sql/updates/1759504522_02.sql @@ -0,0 +1 @@ +UPDATE `aowow_dbversion` SET `build` = CONCAT(IFNULL(`build`, ''), ' globaljs'); diff --git a/setup/tools/filegen/templates/global.js/clicktocopy.js b/setup/tools/filegen/templates/global.js/clicktocopy.js index 9225b6f4..d8c20bfd 100644 --- a/setup/tools/filegen/templates/global.js/clicktocopy.js +++ b/setup/tools/filegen/templates/global.js/clicktocopy.js @@ -23,7 +23,7 @@ $WH.clickToCopy = function (el, textOrFn, opt) $WH.clickToCopy.getTooltip.bind(null, false, opt), undefined, // { - /* byCursor: */ !opt.attachToElement, + /* byCursor: ! */ opt.attachToElement, // stopPropagation: opt.overrideOtherTooltips // } ); diff --git a/setup/tools/filegen/templates/global.js/markup.js b/setup/tools/filegen/templates/global.js/markup.js index e72e5dd2..97f2e128 100644 --- a/setup/tools/filegen/templates/global.js/markup.js +++ b/setup/tools/filegen/templates/global.js/markup.js @@ -465,6 +465,90 @@ var Markup = { return [str, '']; } }, + copy: + { + empty: false, + attr: + { + unnamed: { req: false }, + button: { req: false, valid: /^false$/ }, + size: { req: false, valid: /^large$/ } + }, + allowedClass: MARKUP_CLASS_STAFF, + allowedChildren: { '': 1 }, + // invisibleContent: true, + // rawText: true, + toHtml: function(attr) + { + let title = attr.unnamed ? LANG.copy_format.replace('%s', attr.unnamed) : LANG.copy_clipboard; + // let text = attr._contents.textContent; + let text = attr._textContents; + + // aowow - can't access dom nodes from here .. hopefully this does not become relevant + // while (attr._contents.hasChildNodes()) + // attr._contents.removeChild(attr._contents.firstChild); + + if (attr.button === 'false') + { + // aowow - must return string, not node + // let span = $WH.ce('span', { className: 'tip' }, $WH.ct(text)); + // $WH.clickToCopy(span, text); + // return span; + + let rand = Math.random().toString().substr(2); + let html = ''; + setTimeout(() => { + span = $WH.ge('ctc-' + rand); + $WH.clickToCopy(span, span.textContent); + }, 500); + + return [html, '']; + } + + /* aowow - red buttons aren't yet + * let btn = $WH.ce('button', { className: 'btn btn-site fa fa-clipboard', type: 'button' }, $WH.ct(title)); + * + * if (attr.size !== 'large') + * btn.classList.add('btn-small'); + * + * $WH.aE(btn, 'click', () => { + * $WH.copyToClipboard(text); + * btn.classList.remove('fa-clipboard'); + * btn.classList.add('fa-check'); + * setTimeout(() => { + * btn.classList.remove('fa-check'); + * btn.classList.add('fa-clipboard'); + * }, 3 * 1000); + * }); + * + * return btn; + */ + + let rand = Math.random().toString().substr(2); + let btn = RedButton.create(title); + + btn.id = 'ctc-' + rand; + btn.classList.add('fa-clipboard'); + + btn.style.float = 'initial'; + btn.style.display = 'inline-block'; + + setTimeout(() => { + btn = $WH.ge('ctc-' + rand); + $WH.aE(btn, 'click', () => { + $WH.copyToClipboard(text); + btn.classList.remove('fa-clipboard'); + btn.classList.add('fa-check'); + setTimeout(() => { + btn.classList.remove('fa-check'); + btn.classList.add('fa-clipboard'); + }, 3 * 1000); + }, 500); + }); + + return [btn.outerHTML]; + } + }, currency: { empty: true, diff --git a/static/css/aowow.css b/static/css/aowow.css index 7692cdd9..64c3fa45 100644 --- a/static/css/aowow.css +++ b/static/css/aowow.css @@ -4232,6 +4232,17 @@ input.button-copy:hover { background-color: #444; } +/* clicktocopy fa-replacement on redbutton custom */ +a.button-red.fa-check > em > span { + padding-left: 19px; + background: url(../images/icons/tick.png) no-repeat left center / 12px; +} + +a.button-red.fa-clipboard > em > span { + padding-left: 19px; + background: url(../images/icons/pages.gif) no-repeat left center / 12px; +} + /* favicon fa-replacement custom */ .fav-star { cursor:pointer; diff --git a/static/js/locale_dede.js b/static/js/locale_dede.js index 055c8497..1f075acb 100644 --- a/static/js/locale_dede.js +++ b/static/js/locale_dede.js @@ -4904,6 +4904,8 @@ var LANG = { copied: 'Kopiert', clickToCopy: 'Klicke zum Kopieren', nothingToCopy_tip: 'Nichts zu kopieren!', + copy_clipboard: 'Kopieren', + copy_format: 'Kopiere %s', // TC conditions display tab_conditions: 'Konditionen', diff --git a/static/js/locale_enus.js b/static/js/locale_enus.js index 417ee9ab..4f9911a4 100644 --- a/static/js/locale_enus.js +++ b/static/js/locale_enus.js @@ -4952,6 +4952,8 @@ var LANG = { copied: 'Copied', clickToCopy: 'Click to Copy', nothingToCopy_tip: 'Nothing to copy!', + copy_clipboard: 'Copy', + copy_format: 'Copy %s', // TC conditions display tab_conditions: 'Conditions', diff --git a/static/js/locale_eses.js b/static/js/locale_eses.js index ca03d740..53edb97d 100644 --- a/static/js/locale_eses.js +++ b/static/js/locale_eses.js @@ -4906,6 +4906,8 @@ var LANG = { copied: 'Copiado', clickToCopy: 'Click para copiar', nothingToCopy_tip: '[Nothing to copy!]', + copy_clipboard: 'Copiar', + copy_format: 'Copiar %s', // TC conditions display tab_conditions: 'Condiciones', diff --git a/static/js/locale_frfr.js b/static/js/locale_frfr.js index 04be890c..1cf62a7c 100644 --- a/static/js/locale_frfr.js +++ b/static/js/locale_frfr.js @@ -4906,6 +4906,8 @@ var LANG = { copied: 'Copié', clickToCopy: 'Cliquer pour Copier', nothingToCopy_tip: 'Rien à copier !', + copy_clipboard: 'Copier', + copy_format: 'Copier %s', // TC conditions display tab_conditions: '[Conditions]', diff --git a/static/js/locale_ruru.js b/static/js/locale_ruru.js index 117fd9cc..7fb5aa8b 100644 --- a/static/js/locale_ruru.js +++ b/static/js/locale_ruru.js @@ -4908,6 +4908,8 @@ var LANG = { copied: 'Скопировано', clickToCopy: 'Нажмите, чтобы скопировать', nothingToCopy_tip: 'Нет данных для копирования!', + copy_clipboard: 'Скопировать', + copy_format: 'Скопировать %s', // TC conditions display tab_conditions: '[Conditions]', diff --git a/static/js/locale_zhcn.js b/static/js/locale_zhcn.js index 7399cae5..1aee50eb 100644 --- a/static/js/locale_zhcn.js +++ b/static/js/locale_zhcn.js @@ -4932,6 +4932,8 @@ var LANG = { copied: '已复制', clickToCopy: '点击复制', nothingToCopy_tip: '[Nothing to copy!]', + copy_clipboard: '复制', + copy_format: '[Copy %s]', // TC conditions display tab_conditions: '[Conditions]', From 6263ccd92a7bb8f2e6a5c3c288d50815168d6cdc Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Fri, 3 Oct 2025 18:02:34 +0200 Subject: [PATCH 006/260] SkillDetailPage/Tabs * add tab for spells modifying skill value --- endpoints/skill/skill.php | 20 ++++++++++++++++++++ endpoints/spell/spell.php | 6 ++++-- 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/endpoints/skill/skill.php b/endpoints/skill/skill.php index 18fb8d8e..005bc0cf 100644 --- a/endpoints/skill/skill.php +++ b/endpoints/skill/skill.php @@ -227,6 +227,26 @@ class SkillBaseResponse extends TemplateResponse implements ICache } } + // tab: modified by [spell] + $conditions = array( + 'OR', + ['AND', ['effect1AuraId', [SPELL_AURA_MOD_SKILL, SPELL_AURA_MOD_SKILL_TALENT]], ['effect1MiscValue', $this->typeId]], + ['AND', ['effect2AuraId', [SPELL_AURA_MOD_SKILL, SPELL_AURA_MOD_SKILL_TALENT]], ['effect2MiscValue', $this->typeId]], + ['AND', ['effect3AuraId', [SPELL_AURA_MOD_SKILL, SPELL_AURA_MOD_SKILL_TALENT]], ['effect3MiscValue', $this->typeId]] + ); + $modBy = new SpellList($conditions); + if (!$modBy->error) + { + $this->extendGlobalData($modBy->getJSGlobals()); + + $this->lvTabs->addListviewTab(new Listview(array( + 'data' => $modBy->getListviewData(), + 'id' => 'modified-by', + 'name' => '$LANG.tab_modifiedby', + 'hiddenCols' => ['skill'], + ), SpellList::$brickFile)); + } + // tab: spells [spells] (exclude first tab) $reqClass = 0x0; $reqRace = 0x0; diff --git a/endpoints/spell/spell.php b/endpoints/spell/spell.php index ad9fd262..9d224044 100644 --- a/endpoints/spell/spell.php +++ b/endpoints/spell/spell.php @@ -1892,6 +1892,7 @@ class SpellBaseResponse extends TemplateResponse implements ICache $_nameMV = $this->fmtStaffTip($_, 'MiscValue: '.$effMV); break; case SPELL_AURA_MOD_LANGUAGE: + case SPELL_AURA_COMPREHEND_LANGUAGE: if ($_ = Lang::game('languages', $effMV)) $_nameMV = $this->fmtStaffTip($_, 'MiscValue: '.$effMV); break; @@ -1967,8 +1968,9 @@ class SpellBaseResponse extends TemplateResponse implements ICache if ($_ = Lang::getMagicSchools($effMV)) $_nameMV = $this->fmtStaffTip($_, 'MiscValue: '.Util::asHex($effMV)); break; - case SPELL_AURA_MOD_SKILL: - case SPELL_AURA_MOD_SKILL_TALENT: + case SPELL_AURA_MOD_SKILL: // temp + case SPELL_AURA_MOD_SKILL_TALENT: // perm + $valueFmt = '%+d'; if ($a = SkillList::makeLink($effMV)) $_nameMV = $a; else From ff690770b5f0b5951b2c1b4c466d5e8253412797 Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Sat, 4 Oct 2025 00:15:38 +0200 Subject: [PATCH 007/260] Misc/Fixup * type error when declaring listview --- endpoints/npc/npc.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/endpoints/npc/npc.php b/endpoints/npc/npc.php index 25e7cf72..72f4500e 100644 --- a/endpoints/npc/npc.php +++ b/endpoints/npc/npc.php @@ -533,7 +533,7 @@ class NpcBaseResponse extends TemplateResponse implements ICache $soldItems = new ItemList(array(['id', $sells])); if (!$soldItems->error) { - $colAddIn = null; + $colAddIn = ''; $extraCols = ["\$Listview.funcBox.createSimpleCol('stack', 'stack', '10%', 'stack')", '$Listview.extraCols.cost']; $lvData = $soldItems->getListviewData(ITEMINFO_VENDOR, [Type::NPC => [$this->typeId]]); From a6108be400e622e5aa311476bbe67b07509a024a Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Sat, 4 Oct 2025 00:50:27 +0200 Subject: [PATCH 008/260] Spells/Parsing * bandaid fix parsing deeply nested formulas in non-interactive mode (should rethink how/when formulas get flagged as un-evalable) --- includes/dbtypes/spell.class.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/includes/dbtypes/spell.class.php b/includes/dbtypes/spell.class.php index 344fa8d2..f41e8161 100644 --- a/includes/dbtypes/spell.class.php +++ b/includes/dbtypes/spell.class.php @@ -910,7 +910,11 @@ class SpellList extends DBTypeList $formula = preg_replace('/(\+|-|\*|\/)(\+|-|\*|\/)/i', '\1 \2', $formula); // there should not be any letters without a leading $ - return eval('return '.$formula.';'); + try { $formula = eval('return '.$formula.';'); } + // but there can be if we are non-interactive + catch (\Throwable $e) { } + + return $formula; } // description-, buff-parsing component From 9fc84cdf9e604717a4900b7d28ee29190f131ce1 Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Sat, 4 Oct 2025 15:39:44 +0200 Subject: [PATCH 009/260] Comments/Fixup * fix false error when voting on relies --- endpoints/comment/downvote-reply.php | 2 +- endpoints/comment/upvote-reply.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/endpoints/comment/downvote-reply.php b/endpoints/comment/downvote-reply.php index 541a0d9b..9637bdc3 100644 --- a/endpoints/comment/downvote-reply.php +++ b/endpoints/comment/downvote-reply.php @@ -47,7 +47,7 @@ class CommentDownvotereplyResponse extends TextResponse User::canSupervote() ? -2 : -1 ); - if (!$ok) + if (!is_int($ok)) { trigger_error('CommentDownvotereplyResponse - write to db failed', E_USER_ERROR); $this->generate404(User::isInGroup(U_GROUP_STAFF) ? 'write to db failed' : ''); diff --git a/endpoints/comment/upvote-reply.php b/endpoints/comment/upvote-reply.php index d40b5b75..91fedf47 100644 --- a/endpoints/comment/upvote-reply.php +++ b/endpoints/comment/upvote-reply.php @@ -47,7 +47,7 @@ class CommentUpvotereplyResponse extends TextResponse User::canSupervote() ? 2 : 1 ); - if (!$ok) + if (!is_int($ok)) { trigger_error('CommentUpvotereplyResponse - write to db failed', E_USER_ERROR); $this->generate404(User::isInGroup(U_GROUP_STAFF) ? 'write to db failed' : ''); From 53559890152c452a9f456d990a6f87a1b2dfe97e Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Sat, 4 Oct 2025 16:56:32 +0200 Subject: [PATCH 010/260] Logging/Misc * don't log passwords to DB (and neither check_passwords) --- includes/kernel.php | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/includes/kernel.php b/includes/kernel.php index cb4c6ac4..809460c9 100644 --- a/includes/kernel.php +++ b/includes/kernel.php @@ -149,6 +149,11 @@ set_error_handler(function(int $errNo, string $errStr, string $errFile, int $err default => 'UNKNOWN_ERROR' // errors not in this list can not be handled by set_error_handler (as per documentation) or are ignored }; + if (!empty($_POST['password'])) + $_POST['password'] = '******'; + if (!empty($_POST['c_password'])) + $_POST['c_password'] = '******'; + if (DB::isConnected(DB_AOWOW)) DB::Aowow()->query('INSERT INTO ?_errors (`date`, `version`, `phpError`, `file`, `line`, `query`, `post`, `userGroups`, `message`) VALUES (UNIX_TIMESTAMP(), ?d, ?d, ?, ?d, ?, ?, ?d, ?) ON DUPLICATE KEY UPDATE `date` = UNIX_TIMESTAMP()', AOWOW_REVISION, $errNo, $errFile, $errLine, CLI ? 'CLI' : substr($_SERVER['QUERY_STRING'] ?? '', 0, 250), empty($_POST) ? '' : http_build_query($_POST), User::$groups, $errStr @@ -165,6 +170,11 @@ set_error_handler(function(int $errNo, string $errStr, string $errFile, int $err // handle exceptions set_exception_handler(function (\Throwable $e) : void { + if (!empty($_POST['password'])) + $_POST['password'] = '******'; + if (!empty($_POST['c_password'])) + $_POST['c_password'] = '******'; + if (DB::isConnected(DB_AOWOW)) DB::Aowow()->query('INSERT INTO ?_errors (`date`, `version`, `phpError`, `file`, `line`, `query`, `post`, `userGroups`, `message`) VALUES (UNIX_TIMESTAMP(), ?d, ?d, ?, ?d, ?, ?, ?d, ?) ON DUPLICATE KEY UPDATE `date` = UNIX_TIMESTAMP()', AOWOW_REVISION, $e->getCode(), $e->getFile(), $e->getLine(), CLI ? 'CLI' : substr($_SERVER['QUERY_STRING'] ?? '', 0, 250), empty($_POST) ? '' : http_build_query($_POST), User::$groups, $e->getMessage() @@ -188,6 +198,11 @@ register_shutdown_function(function() : void if ($e = error_get_last()) { + if (!empty($_POST['password'])) + $_POST['password'] = '******'; + if (!empty($_POST['c_password'])) + $_POST['c_password'] = '******'; + if (DB::isConnected(DB_AOWOW)) DB::Aowow()->query('INSERT INTO ?_errors (`date`, `version`, `phpError`, `file`, `line`, `query`, `post`, `userGroups`, `message`) VALUES (UNIX_TIMESTAMP(), ?d, ?d, ?, ?d, ?, ?, ?d, ?) ON DUPLICATE KEY UPDATE `date` = UNIX_TIMESTAMP()', 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'] From 4fe35d9e3ce378f4e205c7ffda60ffbaa1e636c2 Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Sat, 4 Oct 2025 16:24:13 +0200 Subject: [PATCH 011/260] PageTemplate/Fixup * fix merging jsGlobals from comments/etc. into existing PageTemplate --- includes/components/response/templateresponse.class.php | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/includes/components/response/templateresponse.class.php b/includes/components/response/templateresponse.class.php index 9d3b2ec7..bc5ff8f6 100644 --- a/includes/components/response/templateresponse.class.php +++ b/includes/components/response/templateresponse.class.php @@ -625,7 +625,14 @@ class TemplateResponse extends BaseResponse // as this may be loaded from cache, it will be unlinked from its response if ($ptJSG = $this->result->jsGlobals) { - Util::mergeJsGlobals($ptJSG, $this->jsGlobals); + foreach ($this->jsGlobals as $type => [, $data, ]) + { + if (!isset($ptJSG[$type]) || $type == Type::USER) + $ptJSGs[$type] = $this->jsGlobals[$type]; + else + Util::mergeJsGlobals($ptJSG[$type][1], $data); + } + $this->result->jsGlobals = $ptJSG; } else if ($this->jsGlobals) From eb95b03e31e79b90768959c5ec715f903550d0d1 Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Sat, 4 Oct 2025 20:02:34 +0200 Subject: [PATCH 012/260] SmartAI/Update * implement events added in https://github.com/TrinityCore/TrinityCore/commit/3bb4f5677359e3af9fe4b80e42157816786dca49 --- includes/components/SmartAI/SmartEvent.class.php | 10 +++++++++- localization/locale_dede.php | 10 +++++++--- localization/locale_enus.php | 4 ++++ localization/locale_eses.php | 4 ++++ localization/locale_frfr.php | 4 ++++ localization/locale_ruru.php | 4 ++++ localization/locale_zhcn.php | 4 ++++ 7 files changed, 36 insertions(+), 4 deletions(-) diff --git a/includes/components/SmartAI/SmartEvent.class.php b/includes/components/SmartAI/SmartEvent.class.php index 722179dc..a9f1f3f8 100644 --- a/includes/components/SmartAI/SmartEvent.class.php +++ b/includes/components/SmartAI/SmartEvent.class.php @@ -98,6 +98,10 @@ class SmartEvent public const EVENT_ON_SPELL_FAILED = 84; // On Unit::InterruptSpell public const EVENT_ON_SPELL_START = 85; // On Spell::prapare public const EVENT_ON_DESPAWN = 86; // On before creature removed + public const EVENT_SEND_EVENT_TRIGGER = 87; // [RESERVED] UNUSED NEEDS CHERRYPICK + public const EVENT_AREATRIGGER_EXIT = 88; // [RESERVED] don't use on 3.3.5a + public const EVENT_ON_AURA_APPLIED = 89; // + public const EVENT_ON_AURA_REMOVED = 90; // public const FLAG_NO_REPEAT = 0x0001; public const FLAG_DIFFICULTY_0 = 0x0002; @@ -198,7 +202,11 @@ class SmartEvent self::EVENT_ON_SPELL_CAST => [Type::SPELL, ['numRange', -1, true], null, null, null, 0], // SpellID, CooldownMin, CooldownMax self::EVENT_ON_SPELL_FAILED => [Type::SPELL, ['numRange', -1, true], null, null, null, 0], // SpellID, CooldownMin, CooldownMax self::EVENT_ON_SPELL_START => [Type::SPELL, ['numRange', -1, true], null, null, null, 0], // SpellID, CooldownMin, CooldownMax - self::EVENT_ON_DESPAWN => [null, null, null, null, null, 0] // NONE + self::EVENT_ON_DESPAWN => [null, null, null, null, null, 0], // NONE + self::EVENT_SEND_EVENT_TRIGGER => [null, null, null, null, null, 2], // UNUSED NEEDS CHERRYPICK + self::EVENT_AREATRIGGER_EXIT => [null, null, null, null, null, 2], // don't use on 3.3.5a + self::EVENT_ON_AURA_APPLIED => [Type::SPELL, ['numRange', -1, true], null, null, null, 0], // SpellID, CooldownMin, CooldownMax + self::EVENT_ON_AURA_REMOVED => [Type::SPELL, ['numRange', -1, true], null, null, null, 0] // SpellID, CooldownMin, CooldownMax ); private array $jsGlobals = []; diff --git a/localization/locale_dede.php b/localization/locale_dede.php index 1ca2f222..2a47572c 100644 --- a/localization/locale_dede.php +++ b/localization/locale_dede.php @@ -651,10 +651,14 @@ $lang = array( /* 80*/ SmartEvent::EVENT_SCENE_CANCEL => null, SmartEvent::EVENT_SCENE_COMPLETE => null, SmartEvent::EVENT_SUMMONED_UNIT_DIES => ['Durch mich beschworener (%1$d)?[npc=%1$d]:NPC; stirbt', 'Abklingzeit: %s'], - SmartEvent::EVENT_ON_SPELL_CAST => ['Bei \'cast success\' von [spell=%1$d] ', 'Abklingzeit: %s'], - SmartEvent::EVENT_ON_SPELL_FAILED => ['Bei \'cast failed\' von [spell=%1$d] ', 'Abklingzeit: %s'], - SmartEvent::EVENT_ON_SPELL_START => ['Bei \'cast start\' von [spell=%1$d] ', 'Abklingzeit: %s'], + SmartEvent::EVENT_ON_SPELL_CAST => ['Bei \'cast success\' von [spell=%1$d]', 'Abklingzeit: %s'], + SmartEvent::EVENT_ON_SPELL_FAILED => ['Bei \'cast failed\' von [spell=%1$d]', 'Abklingzeit: %s'], + SmartEvent::EVENT_ON_SPELL_START => ['Bei \'cast start\' von [spell=%1$d]', 'Abklingzeit: %s'], SmartEvent::EVENT_ON_DESPAWN => ['Beim Verschwinden', ''], + SmartEvent::EVENT_SEND_EVENT_TRIGGER => null, + SmartEvent::EVENT_AREATRIGGER_EXIT => null, + SmartEvent::EVENT_ON_AURA_APPLIED => ['Wenn Aura [spell=%1$d] angewendet wird', 'Abklingzeit: %s'], + SmartEvent::EVENT_ON_AURA_REMOVED => ['Wenn Aura [spell=%1$d] endet', 'Abklingzeit: %s'] ), 'eventFlags' => array( SmartEvent::FLAG_NO_REPEAT => 'Nicht wiederholbar', diff --git a/localization/locale_enus.php b/localization/locale_enus.php index 6e1bc7e6..99d09f1d 100644 --- a/localization/locale_enus.php +++ b/localization/locale_enus.php @@ -655,6 +655,10 @@ $lang = array( SmartEvent::EVENT_ON_SPELL_FAILED => ['On [spell=%1$d] cast failed', 'Cooldown: %s'], SmartEvent::EVENT_ON_SPELL_START => ['On [spell=%1$d] cast start', 'Cooldown: %s'], SmartEvent::EVENT_ON_DESPAWN => ['On despawn', ''], + SmartEvent::EVENT_SEND_EVENT_TRIGGER => null, + SmartEvent::EVENT_AREATRIGGER_EXIT => null, + SmartEvent::EVENT_ON_AURA_APPLIED => ['On aura [spell=%1$d] applied', 'Cooldown: %s'], + SmartEvent::EVENT_ON_AURA_REMOVED => ['On aura [spell=%1$d] removed', 'Cooldown: %s'] ), 'eventFlags' => array( SmartEvent::FLAG_NO_REPEAT => 'No Repeat', diff --git a/localization/locale_eses.php b/localization/locale_eses.php index e1f772ac..e88d151e 100644 --- a/localization/locale_eses.php +++ b/localization/locale_eses.php @@ -655,6 +655,10 @@ $lang = array( SmartEvent::EVENT_ON_SPELL_FAILED => ['Al fallar [spell=%1$d]', 'Enfriamiento: %s'], SmartEvent::EVENT_ON_SPELL_START => ['Al comenzar a lanzar [spell=%1$d]', 'Enfriamiento: %s'], SmartEvent::EVENT_ON_DESPAWN => ['Al desaparecer', ''], + SmartEvent::EVENT_SEND_EVENT_TRIGGER => null, + SmartEvent::EVENT_AREATRIGGER_EXIT => null, + SmartEvent::EVENT_ON_AURA_APPLIED => ['Al aplicar el aura [spell=%1$d]', 'Enfriamiento: %s'], + SmartEvent::EVENT_ON_AURA_REMOVED => ['Al eliminar el aura [spell=%1$d]', 'Enfriamiento: %s'] ), 'eventFlags' => array( SmartEvent::FLAG_NO_REPEAT => 'Sin repetir', diff --git a/localization/locale_frfr.php b/localization/locale_frfr.php index 62f31429..58ac1bbf 100644 --- a/localization/locale_frfr.php +++ b/localization/locale_frfr.php @@ -655,6 +655,10 @@ $lang = array( SmartEvent::EVENT_ON_SPELL_FAILED => ['On [spell=%1$d] cast failed', 'Cooldown: %s'], SmartEvent::EVENT_ON_SPELL_START => ['On [spell=%1$d] cast start', 'Cooldown: %s'], SmartEvent::EVENT_ON_DESPAWN => ['On despawn', ''], + SmartEvent::EVENT_SEND_EVENT_TRIGGER => null, + SmartEvent::EVENT_AREATRIGGER_EXIT => null, + SmartEvent::EVENT_ON_AURA_APPLIED => ['On aura [spell=%1$d] applied', 'Cooldown: %s'], + SmartEvent::EVENT_ON_AURA_REMOVED => ['On aura [spell=%1$d] removed', 'Cooldown: %s'] ), 'eventFlags' => array( SmartEvent::FLAG_NO_REPEAT => 'No Repeat', diff --git a/localization/locale_ruru.php b/localization/locale_ruru.php index e9587064..57620310 100644 --- a/localization/locale_ruru.php +++ b/localization/locale_ruru.php @@ -655,6 +655,10 @@ $lang = array( SmartEvent::EVENT_ON_SPELL_FAILED => ['On [spell=%1$d] cast failed', 'Cooldown: %s'], SmartEvent::EVENT_ON_SPELL_START => ['On [spell=%1$d] cast start', 'Cooldown: %s'], SmartEvent::EVENT_ON_DESPAWN => ['On despawn', ''], + SmartEvent::EVENT_SEND_EVENT_TRIGGER => null, + SmartEvent::EVENT_AREATRIGGER_EXIT => null, + SmartEvent::EVENT_ON_AURA_APPLIED => ['On aura [spell=%1$d] applied', 'Cooldown: %s'], + SmartEvent::EVENT_ON_AURA_REMOVED => ['On aura [spell=%1$d] removed', 'Cooldown: %s'] ), 'eventFlags' => array( SmartEvent::FLAG_NO_REPEAT => 'No Repeat', diff --git a/localization/locale_zhcn.php b/localization/locale_zhcn.php index 7819c9ea..87276104 100644 --- a/localization/locale_zhcn.php +++ b/localization/locale_zhcn.php @@ -655,6 +655,10 @@ $lang = array( SmartEvent::EVENT_ON_SPELL_FAILED => ['On [spell=%1$d] cast failed', 'Cooldown: %s'], SmartEvent::EVENT_ON_SPELL_START => ['On [spell=%1$d] cast start', 'Cooldown: %s'], SmartEvent::EVENT_ON_DESPAWN => ['On despawn', ''], + SmartEvent::EVENT_SEND_EVENT_TRIGGER => null, + SmartEvent::EVENT_AREATRIGGER_EXIT => null, + SmartEvent::EVENT_ON_AURA_APPLIED => ['On aura [spell=%1$d] applied', 'Cooldown: %s'], + SmartEvent::EVENT_ON_AURA_REMOVED => ['On aura [spell=%1$d] removed', 'Cooldown: %s'] ), 'eventFlags' => array( SmartEvent::FLAG_NO_REPEAT => 'No Repeat', From baf4ba5b98515cb4f4ff3adb0f33b0bad6f84a5b Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Sun, 5 Oct 2025 00:28:27 +0200 Subject: [PATCH 013/260] Codestyle/Cleanup --- includes/game/chrstatistics.php | 79 ++++++++++++--------------------- includes/game/misc.php | 34 +++++++------- includes/setup/cli.class.php | 24 +++------- includes/setup/timer.class.php | 6 +-- 4 files changed, 55 insertions(+), 88 deletions(-) diff --git a/includes/game/chrstatistics.php b/includes/game/chrstatistics.php index c1819501..136eeb2d 100644 --- a/includes/game/chrstatistics.php +++ b/includes/game/chrstatistics.php @@ -546,22 +546,17 @@ class StatsContainer implements \Countable case ENCHANTMENT_TYPE_STAT: // ITEM_MOD_* return [Stat::getIndexFrom(Stat::IDX_ITEM_MOD, $object)]; case ENCHANTMENT_TYPE_RESISTANCE: - if ($object == SPELL_SCHOOL_NORMAL) - return [Stat::ARMOR]; - if ($object == SPELL_SCHOOL_HOLY) - return [Stat::HOLY_RESISTANCE]; - if ($object == SPELL_SCHOOL_FIRE) - return [Stat::FIRE_RESISTANCE]; - if ($object == SPELL_SCHOOL_NATURE) - return [Stat::NATURE_RESISTANCE]; - if ($object == SPELL_SCHOOL_FROST) - return [Stat::FROST_RESISTANCE]; - if ($object == SPELL_SCHOOL_SHADOW) - return [Stat::SHADOW_RESISTANCE]; - if ($object == SPELL_SCHOOL_ARCANE) - return [Stat::ARCANE_RESISTANCE]; - - return []; + return match ($object) + { + SPELL_SCHOOL_NORMAL => [Stat::ARMOR], + SPELL_SCHOOL_HOLY => [Stat::HOLY_RESISTANCE], + SPELL_SCHOOL_FIRE => [Stat::FIRE_RESISTANCE], + SPELL_SCHOOL_NATURE => [Stat::NATURE_RESISTANCE], + SPELL_SCHOOL_FROST => [Stat::FROST_RESISTANCE], + SPELL_SCHOOL_SHADOW => [Stat::SHADOW_RESISTANCE], + SPELL_SCHOOL_ARCANE => [Stat::ARCANE_RESISTANCE], + default => [] + }; case ENCHANTMENT_TYPE_EQUIP_SPELL: // handled one level up case ENCHANTMENT_TYPE_COMBAT_SPELL: // we do not average effects, so skip case ENCHANTMENT_TYPE_USE_SPELL: @@ -602,20 +597,15 @@ class StatsContainer implements \Countable switch ($auraId) { case SPELL_AURA_MOD_STAT: - if ($miscValue < 0) // all stats - return [Stat::AGILITY, Stat::STRENGTH, Stat::INTELLECT, Stat::SPIRIT, Stat::STAMINA]; - if ($miscValue == STAT_STRENGTH) // one stat - return [Stat::STRENGTH]; - if ($miscValue == STAT_AGILITY) - return [Stat::AGILITY]; - if ($miscValue == STAT_STAMINA) - return [Stat::STAMINA]; - if ($miscValue == STAT_INTELLECT) - return [Stat::INTELLECT]; - if ($miscValue == STAT_SPIRIT) - return [Stat::SPIRIT]; - - return []; // one bullshit + return match ($miscValue) + { + STAT_STRENGTH => [Stat::STRENGTH], + STAT_AGILITY => [Stat::AGILITY], + STAT_STAMINA => [Stat::STAMINA], + STAT_INTELLECT => [Stat::INTELLECT], + STAT_SPIRIT => [Stat::SPIRIT], + default => $miscValue < 0 ? [Stat::AGILITY, Stat::STRENGTH, Stat::INTELLECT, Stat::SPIRIT, Stat::STAMINA] : [] + }; case SPELL_AURA_MOD_INCREASE_HEALTH: case SPELL_AURA_MOD_INCREASE_HEALTH_NONSTACK: case SPELL_AURA_MOD_INCREASE_HEALTH_2: @@ -629,27 +619,16 @@ class StatsContainer implements \Countable if ($miscValue == SPELL_MAGIC_SCHOOLS) return [Stat::DAMAGE_SPELL_POWER]; - // HolySpellpower (deprecated; still used in randomproperties) if ($miscValue & (1 << SPELL_SCHOOL_HOLY)) $stats[] = Stat::HOLY_SPELL_POWER; - - // FireSpellpower (deprecated; still used in randomproperties) if ($miscValue & (1 << SPELL_SCHOOL_FIRE)) $stats[] = Stat::FIRE_SPELL_POWER; - - // NatureSpellpower (deprecated; still used in randomproperties) if ($miscValue & (1 << SPELL_SCHOOL_NATURE)) $stats[] = Stat::NATURE_SPELL_POWER; - - // FrostSpellpower (deprecated; still used in randomproperties) if ($miscValue & (1 << SPELL_SCHOOL_FROST)) $stats[] = Stat::FROST_SPELL_POWER; - - // ShadowSpellpower (deprecated; still used in randomproperties) if ($miscValue & (1 << SPELL_SCHOOL_SHADOW)) $stats[] = Stat::SHADOW_SPELL_POWER; - - // ArcaneSpellpower (deprecated; still used in randomproperties) if ($miscValue & (1 << SPELL_SCHOOL_ARCANE)) $stats[] = Stat::ARCANE_SPELL_POWER; @@ -657,16 +636,14 @@ class StatsContainer implements \Countable case SPELL_AURA_MOD_HEALING_DONE: // not as a mask.. return [Stat::HEALING_SPELL_POWER]; case SPELL_AURA_MOD_INCREASE_ENERGY: // MiscVal:type see defined Powers only energy/mana in use - if ($miscValue == POWER_ENERGY) - return [Stat::ENERGY]; - if ($miscValue == POWER_RAGE) - return [Stat::RAGE]; - if ($miscValue == POWER_MANA) - return [Stat::MANA]; - if ($miscValue == POWER_RUNIC_POWER) - return [Stat::RUNIC_POWER]; - - return []; + return match ($miscValue) + { + POWER_ENERGY => [Stat::ENERGY], + POWER_RAGE => [Stat::RAGE], + POWER_MANA => [Stat::MANA], + POWER_RUNIC_POWER => [Stat::RUNIC_POWER], + default => [] + }; case SPELL_AURA_MOD_RATING: case SPELL_AURA_MOD_RATING_FROM_STAT: if ($stat = self::convertCombatRating($miscValue)) diff --git a/includes/game/misc.php b/includes/game/misc.php index 67bfc553..0902b3c6 100644 --- a/includes/game/misc.php +++ b/includes/game/misc.php @@ -8,15 +8,15 @@ if (!defined('AOWOW_REVISION')) class Game { - public static $resistanceFields = array( + public static array $resistanceFields = array( null, 'resHoly', 'resFire', 'resNature', 'resFrost', 'resShadow', 'resArcane' ); - public static $rarityColorStings = array( // zero-indexed + public static array $rarityColorStings = array( // zero-indexed '9d9d9d', 'ffffff', '1eff00', '0070dd', 'a335ee', 'ff8000', 'e5cc80', 'e6cc80' ); - public static $specIconStrings = array( + public static array $specIconStrings = array( -1 => 'inv_misc_questionmark', 0 => 'spell_nature_elementalabsorption', 6 => ['spell_deathknight_bloodpresence', 'spell_deathknight_frostpresence', 'spell_deathknight_unholypresence' ], @@ -48,7 +48,7 @@ class Game // zoneorsort for quests need updating // partially points non-instanced area with identical name for instance quests - public static $questSortFix = array( + public static array $questSortFix = array( -221 => 440, // Treasure Map => Tanaris -284 => 0, // Special => Misc (some quests get shuffled into seasonal) 151 => 0, // Designer Island => Misc @@ -86,7 +86,7 @@ class Game 1417 => 1477 // Sunken Temple ); - public static $questSubCats = array( + public static array $questSubCats = array( 1 => [132], // Dun Morogh: Coldridge Valley 12 => [9], // Elwynn Forest: Northshire Valley 141 => [188], // Teldrassil: Shadowglen @@ -113,7 +113,7 @@ class Game 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] + public static array $skillLineMask = array( // idx => [familyId, skillLineId] -1 => array( // Pets (Hunter) [ 1, 208], [ 2, 209], [ 3, 203], [ 4, 210], [ 5, 211], [ 6, 212], [ 7, 213], // Wolf, Cat, Spider, Bear, Boar, Crocolisk, Carrion Bird [ 8, 214], [ 9, 215], [11, 217], [12, 218], [20, 236], [21, 251], [24, 653], // Crab, Gorilla, Raptor, Tallstrider, Scorpid, Turtle, Bat @@ -129,31 +129,31 @@ class Game ) ); - public static $sockets = array( // jsStyle Strings + public static array $sockets = array( // jsStyle Strings 'meta', 'red', 'yellow', 'blue' ); - public static function getReputationLevelForPoints($pts) + public static function getReputationLevelForPoints(int $pts) : int { if ($pts >= 41999) return REP_EXALTED; - else if ($pts >= 20999) + if ($pts >= 20999) return REP_REVERED; - else if ($pts >= 8999) + if ($pts >= 8999) return REP_HONORED; - else if ($pts >= 2999) + if ($pts >= 2999) return REP_FRIENDLY; - else if ($pts >= 0) + if ($pts >= 0) return REP_NEUTRAL; - else if ($pts >= -3000) + if ($pts >= -3000) return REP_UNFRIENDLY; - else if ($pts >= -6000) + if ($pts >= -6000) return REP_HOSTILE; - else - return REP_HATED; + + return REP_HATED; } - public static function getTaughtSpells(&$spell) + public static function getTaughtSpells(mixed &$spell) : array { $extraIds = [-1]; // init with -1 to prevent empty-array errors $lookup = [-1]; diff --git a/includes/setup/cli.class.php b/includes/setup/cli.class.php index 47a05ee6..ef16296f 100644 --- a/includes/setup/cli.class.php +++ b/includes/setup/cli.class.php @@ -152,24 +152,14 @@ abstract class CLI if ($timestamp) $msg = str_pad(date('H:i:s'), 10); - switch ($lvl) + $msg .= match ($lvl) { - case self::LOG_ERROR: // red critical error - $msg .= '['.self::red('ERR').'] '; - break; - case self::LOG_WARN: // yellow notice - $msg .= '['.self::yellow('WARN').'] '; - break; - case self::LOG_OK: // green success - $msg .= '['.self::green('OK').'] '; - break; - case self::LOG_INFO: // blue info - $msg .= '['.self::blue('INFO').'] '; - break; - case self::LOG_BLANK: - $msg .= ' '; - break; - } + self::LOG_ERROR => '['.self::red('ERR').'] ', // red critical error + self::LOG_WARN => '['.self::yellow('WARN').'] ', // yellow notice + self::LOG_OK => '['.self::green('OK').'] ', // green success + self::LOG_INFO => '['.self::blue('INFO').'] ', // blue info + default => ' ' + }; $msg .= $txt; } diff --git a/includes/setup/timer.class.php b/includes/setup/timer.class.php index a9c22c15..7c36620d 100644 --- a/includes/setup/timer.class.php +++ b/includes/setup/timer.class.php @@ -8,9 +8,9 @@ if (!defined('AOWOW_REVISION')) class Timer { - private $t_cur = 0; - private $t_new = 0; - private $intv = 0; + private float $t_cur = 0; + private float $t_new = 0; + private float $intv = 0; public function __construct(int $intervall) { From 7b752143a0173b938e81a4caae9eddaa898d7944 Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Sun, 5 Oct 2025 18:53:40 +0200 Subject: [PATCH 014/260] Creature/Quotes * don't add superfluous creature name placeholder to emotes --- includes/game/misc.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/game/misc.php b/includes/game/misc.php index 0902b3c6..f146ae48 100644 --- a/includes/game/misc.php +++ b/includes/game/misc.php @@ -268,7 +268,7 @@ class Game }; // prefix - $prefix = '%s '; + $prefix = ''; if ($t['talkType'] != 4) $prefix = ($talkSource ?: '%s').' '.Lang::npc('textTypes', $t['talkType']).Lang::main('colon').($t['lang'] ? '['.Lang::game('languages', $t['lang']).'] ' : ' '); From aa7c0186fce1289022ed02eb94bc752611fffd40 Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Sun, 5 Oct 2025 19:57:42 +0200 Subject: [PATCH 015/260] Profiler/Cleanup * gracefully handle DB errors when fetching realms instead of crashing --- includes/components/profiler.class.php | 52 ++++++++++++-------------- 1 file changed, 24 insertions(+), 28 deletions(-) diff --git a/includes/components/profiler.class.php b/includes/components/profiler.class.php index 6d18ec7d..48a8d0f1 100644 --- a/includes/components/profiler.class.php +++ b/includes/components/profiler.class.php @@ -8,10 +8,10 @@ if (!defined('AOWOW_REVISION')) class Profiler { - public const PID_FILE = 'config/pr-queue-pid'; - public const CHAR_GMFLAGS = 0x1 | 0x8 | 0x10 | 0x20; // PLAYER_EXTRA_ :: GM_ON | TAXICHEAT | GM_INVISIBLE | GM_CHAT + public const /* string */ PID_FILE = 'config/pr-queue-pid'; + public const /* int */ CHAR_GMFLAGS = 0x1 | 0x8 | 0x10 | 0x20; // PLAYER_EXTRA_ :: GM_ON | TAXICHEAT | GM_INVISIBLE | GM_CHAT - public const REGIONS = array( // see cfg_categories.dbc + public const /* array */ REGIONS = array( // see cfg_categories.dbc 'us' => [2, 3, 4, 5], // US (us, oceanic, latin america, americas - tournament) 'kr' => [6, 7], // KR (kr, tournament) 'eu' => [8, 9, 10, 11, 12, 13], // EU (english, german, french, spanish, russian, eu - tournament) @@ -20,9 +20,9 @@ class Profiler 'dev' => [1, 26, 27, 28, 30] // Development, Test Server, Test Server - tournament, QA Server, Test Server 2 ); - private static $realms = []; + private static array $realms = []; - public static $slot2InvType = array( + public static array $slot2InvType = array( 1 => [INVTYPE_HEAD], // head 2 => [INVTYPE_NECK], // neck 3 => [INVTYPE_SHOULDERS], // shoulder @@ -44,7 +44,7 @@ class Profiler 19 => [INVTYPE_TABARD], // tabard ); - public static $raidProgression = array( // statisticAchievement => relevantCriterium ; don't forget to enable this in /js/Profiler.js as well + public static array $raidProgression = array( // statisticAchievement => relevantCriterium ; don't forget to enable this in /js/Profiler.js as well 1361 => 5100, 1362 => 5101, 1363 => 5102, 1365 => 5104, 1366 => 5108, 1364 => 5110, 1369 => 5112, 1370 => 5113, 1371 => 5114, 1372 => 5117, 1373 => 5119, 1374 => 5120, 1375 => 7805, 1376 => 5122, 1377 => 5123, // Naxxramas 10 1367 => 5103, 1368 => 5111, 1378 => 5124, 1379 => 5125, 1380 => 5126, 1381 => 5127, 1382 => 5128, 1383 => 7806, 1384 => 5130, 1385 => 5131, 1386 => 5132, 1387 => 5133, 1388 => 5134, 1389 => 5135, 1390 => 5136, // Naxxramas 25 2856 => 9938, 2857 => 9939, 2858 => 9940, 2859 => 9941, 2861 => 9943, 2865 => 9947, 2866 => 9948, 2868 => 9950, 2869 => 9951, 2870 => 9952, 2863 => 10558, 2864 => 10559, 2862 => 10560, 2867 => 10565, 2860 => 10580, // Ulduar 10 @@ -65,7 +65,7 @@ class Profiler 4821 => 13466, // Ruby Sanctum 10 nh ); - public static function getBuyoutForItem($itemId) + public static function getBuyoutForItem(int $itemId) : int { if (!$itemId) return 0; @@ -75,7 +75,7 @@ class Profiler return 0; } - public static function queueStart(&$msg = '') + public static function queueStart(?string &$msg = '') : bool { $queuePID = self::queueStatus(); @@ -100,7 +100,7 @@ class Profiler } } - public static function queueStatus() + public static function queueStatus() : int { if (!file_exists(self::PID_FILE)) return 0; @@ -117,7 +117,7 @@ class Profiler return 0; } - public static function queueLock($pid) + public static function queueLock(int $pid) : bool { $queuePID = self::queueStatus(); if ($queuePID && $queuePID != $pid) @@ -139,12 +139,12 @@ class Profiler return $ok; } - public static function queueFree() + public static function queueFree() : void { unlink(self::PID_FILE); } - public static function urlize($str, $allowLocales = false, $profile = false) + public static function urlize(string $str, bool $allowLocales = false, bool $profile = false) : string { $search = ['<', '>', ' / ', "'"]; $replace = ['<', '>', '-', '' ]; @@ -194,7 +194,7 @@ class Profiler if (!DB::isConnectable(DB_AUTH) || self::$realms) return self::$realms; - self::$realms = DB::Auth()->select( + $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 @@ -209,14 +209,14 @@ class Profiler WOW_BUILD ); - foreach (self::$realms as $rId => &$rData) + if (!$realms) + return []; + + foreach ($realms as $rId => $rData) { // realm in db but no connection info set if (!DB::isConnectable(DB_CHARACTERS . $rId)) - { - unset(self::$realms[$rId]); continue; - } // filter by access level if ($rData['access'] == SEC_ADMINISTRATOR && (CLI || User::isInGroup(U_GROUP_DEV | U_GROUP_ADMIN))) @@ -226,10 +226,7 @@ class Profiler else if ($rData['access'] == SEC_MODERATOR && (CLI || User::isInGroup(U_GROUP_DEV | U_GROUP_ADMIN | U_GROUP_MOD | U_GROUP_BUREAU))) $rData['access'] = U_GROUP_DEV | U_GROUP_ADMIN | U_GROUP_MOD | U_GROUP_BUREAU; else if ($rData['access'] > SEC_PLAYER && !CLI) - { - unset(self::$realms[$rId]); continue; - } // filter dev realms if ($rData['region'] === 'dev') @@ -237,11 +234,10 @@ class Profiler if (CLI || User::isInGroup(U_GROUP_DEV | U_GROUP_ADMIN)) $rData['access'] = U_GROUP_DEV | U_GROUP_ADMIN; else - { - unset(self::$realms[$rId]); continue; - } } + + self::$realms[$rId] = $rData; } return self::$realms; @@ -255,7 +251,7 @@ class Profiler return array_unique(array_column(self::$realms, 'region')); } - private static function queueInsert($realmId, $guid, $type, $localId) + private static function queueInsert(int $realmId, int $guid, int $type, int $localId) : void { if ($rData = DB::Aowow()->selectRow('SELECT `requestTime` AS "time", `status` FROM ?_profiler_sync WHERE `realm` = ?d AND `realmGUID` = ?d AND `type` = ?d AND `typeId` = ?d AND `status` <> ?d', $realmId, $guid, $type, $localId, PR_QUEUE_STATUS_WORKING)) { @@ -270,7 +266,7 @@ class Profiler 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) + public static function scheduleResync(int $type, int $realmId, int $guid) : int { $newId = 0; @@ -361,7 +357,7 @@ class Profiler return Util::toJSON($response); } - public static function getCharFromRealm($realmId, $charGuid) + public static function getCharFromRealm(int $realmId, int $charGuid) : bool { $char = DB::Characters($realmId)->selectRow('SELECT c.* FROM characters c WHERE c.`guid` = ?d', $charGuid); if (!$char) @@ -881,7 +877,7 @@ class Profiler return true; } - public static function getGuildFromRealm($realmId, $guildGuid) + public static function getGuildFromRealm(int $realmId, int $guildGuid) : bool { $guild = DB::Characters($realmId)->selectRow('SELECT `guildId`, `name`, `createDate`, `info`, `backgroundColor`, `emblemStyle`, `emblemColor`, `borderStyle`, `borderColor` FROM guild WHERE `guildId` = ?d', $guildGuid); if (!$guild) @@ -948,7 +944,7 @@ class Profiler return true; } - public static function getArenaTeamFromRealm($realmId, $teamGuid) + public static function getArenaTeamFromRealm(int $realmId, int $teamGuid) : bool { $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) From 7b429811a92714811620e2d40c9e9864ed11810f Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Sun, 5 Oct 2025 19:33:34 +0200 Subject: [PATCH 016/260] Defines/Races * add unplayable races to ChrRace enum so RaceDetailPage can display them. * don't show empty icons for unplayable races --- endpoints/race/race.php | 4 +++- includes/game/chrrace.class.php | 40 +++++++++++++++++++++------------ 2 files changed, 29 insertions(+), 15 deletions(-) diff --git a/endpoints/race/race.php b/endpoints/race/race.php index 75c5cd47..6da89fc8 100644 --- a/endpoints/race/race.php +++ b/endpoints/race/race.php @@ -115,12 +115,14 @@ class RaceBaseResponse extends TemplateResponse implements ICache /****************/ $this->expansion = Util::$expansionString[$this->subject->getField('expansion')]; - $this->headIcons = ['race_'.$ra->json().'_male', 'race_'.$ra->json().'_female']; $this->redButtons = array( BUTTON_WOWHEAD => true, BUTTON_LINKS => ['type' => $this->type, 'typeId' => $this->typeId] ); + if ($_ = $ra->json()) + $this->headIcons = ['race_'.$_.'_male', 'race_'.$_.'_female']; + /**************/ /* Extra Tabs */ diff --git a/includes/game/chrrace.class.php b/includes/game/chrrace.class.php index aab28ed7..0cbe80bd 100644 --- a/includes/game/chrrace.class.php +++ b/includes/game/chrrace.class.php @@ -8,20 +8,31 @@ if (!defined('AOWOW_REVISION')) enum ChrRace : int { - case HUMAN = 1; - case ORC = 2; - case DWARF = 3; - case NIGHTELF = 4; - case UNDEAD = 5; - case TAUREN = 6; - case GNOME = 7; - case TROLL = 8; - case BLOODELF = 10; - case DRAENEI = 11; + case HUMAN = 1; + case ORC = 2; + case DWARF = 3; + case NIGHTELF = 4; + case UNDEAD = 5; + case TAUREN = 6; + case GNOME = 7; + case TROLL = 8; + case GOBLIN = 9; + case BLOODELF = 10; + case DRAENEI = 11; + case FEL_ORC = 12; + case NAGA = 13; + case BROKEN = 14; + case SKELETON = 15; + case VRYKUL = 16; + case TUSKARR = 17; + case FOREST_TROLL = 18; + case TAUNKA = 19; + case NORTHREND_SKELETON = 20; + case ICE_TROLL = 21; - public const MASK_ALLIANCE = 0x44D; - public const MASK_HORDE = 0x2B2; - public const MASK_ALL = 0x6FF; + public const MASK_ALLIANCE = 0x44D; // HUMAN, DWARF, NIGHTELF, GNOME, DRAENEI + public const MASK_HORDE = 0x2B2; // ORC, UNDEAD, TAUREN, TROLL, BLOODELF + public const MASK_ALL = self::MASK_ALLIANCE | self::MASK_HORDE; public function matches(int $raceMask) : bool { @@ -80,7 +91,8 @@ enum ChrRace : int self::GNOME => 'gnome', self::TROLL => 'troll', self::BLOODELF => 'bloodelf', - self::DRAENEI => 'draenei' + self::DRAENEI => 'draenei', + default => '' }; } From 045c16c2413c9f2dff71dc6d0efd100e9155baa9 Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Mon, 6 Oct 2025 15:00:47 +0200 Subject: [PATCH 017/260] PageTemplate/Profiler * don't skip running parent::generate for incomplete profiles --- endpoints/profile/profile.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/endpoints/profile/profile.php b/endpoints/profile/profile.php index 284ed283..2e41350c 100644 --- a/endpoints/profile/profile.php +++ b/endpoints/profile/profile.php @@ -122,7 +122,10 @@ class ProfileBaseResponse extends TemplateResponse protected function generate() : void { if ($this->doResync) + { + parent::generate(); return; + } if ($this->typeId) { From 704894c1e3669869570e8b2220117a2989746a58 Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Mon, 6 Oct 2025 15:20:54 +0200 Subject: [PATCH 018/260] Spells/Fixup * skillLines can be empty for unused glyphs etc. --- endpoints/spell/spell.php | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/endpoints/spell/spell.php b/endpoints/spell/spell.php index 9d224044..29e95f0a 100644 --- a/endpoints/spell/spell.php +++ b/endpoints/spell/spell.php @@ -2203,6 +2203,7 @@ class SpellBaseResponse extends TemplateResponse implements ICache { $cat = $this->subject->getField('typeCat'); $cf = $this->subject->getField('cuFlags'); + $sl = $this->subject->getField('skillLines'); $this->breadcrumb[] = $cat; @@ -2224,14 +2225,15 @@ class SpellBaseResponse extends TemplateResponse implements ICache if ($cat == -13) $this->breadcrumb[] = ($cf & (SPELL_CU_GLYPH_MAJOR | SPELL_CU_GLYPH_MINOR)) >> 6; - else - $this->breadcrumb[] = $this->subject->getField('skillLines')[0]; + else if ($sl) + $this->breadcrumb[] = $sl[0]; break; case 9: case -3: case 11: - $this->breadcrumb[] = $this->subject->getField('skillLines')[0]; + if ($sl) + $this->breadcrumb[] = $sl[0]; if ($cat == 11) if ($_ = $this->subject->getField('reqSpellId')) @@ -2240,7 +2242,7 @@ class SpellBaseResponse extends TemplateResponse implements ICache break; case -11: foreach (SpellList::$skillLines as $line => $skills) - if (in_array($this->subject->getField('skillLines')[0], $skills)) + if (in_array($sl[0] ?? [], $skills)) $this->breadcrumb[] = $line; break; case -7: // only spells unique in skillLineAbility will always point to the right skillLine :/ @@ -2311,11 +2313,11 @@ class SpellBaseResponse extends TemplateResponse implements ICache if (in_array($typeCat, [9, 11])) { // skill - if ($_ = $this->subject->getField('skillLines')[0]) + if ($_ = $this->subject->getField('skillLines')) { - $this->extendGlobalIds(Type::SKILL, $_); + $this->extendGlobalIds(Type::SKILL, $_[0]); - $bar = Lang::game('requires', [' [skill='.$_.']']); + $bar = Lang::game('requires', [' [skill='.$_[0].']']); if ($_ = $this->subject->getField('learnedAt')) $bar .= ' ('.$_.')'; From 452615a92dafd5b714b74c53b21db1eb7f8d6ab2 Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Mon, 6 Oct 2025 15:50:33 +0200 Subject: [PATCH 019/260] Filters/Misc * be a bit more lenient on level inputs * fix displaying array of requirements on error --- includes/components/filter.class.php | 12 +++++------- includes/dbtypes/creature.class.php | 4 ++-- includes/dbtypes/item.class.php | 8 ++++---- includes/dbtypes/itemset.class.php | 8 ++++---- includes/dbtypes/quest.class.php | 8 ++++---- includes/dbtypes/spell.class.php | 8 ++++---- includes/utilities.php | 21 +++++++++++++++++++++ 7 files changed, 44 insertions(+), 25 deletions(-) diff --git a/includes/components/filter.class.php b/includes/components/filter.class.php index 9f7db3c6..8ea36189 100644 --- a/includes/components/filter.class.php +++ b/includes/components/filter.class.php @@ -452,20 +452,18 @@ abstract class Filter if (!Util::checkNumeric($val, NUM_CAST_INT)) return false; - foreach ($valid as $k => $v) + if (in_array($val, $valid)) + return true; + + foreach ($valid as $v) { if (gettype($v) != 'array') continue; if ($this->checkInput(self::V_RANGE, $v, $val, true)) return true; - - unset($valid[$k]); } - if (in_array($val, $valid)) - return true; - break; case self::V_RANGE: if (Util::checkNumeric($val, NUM_CAST_INT) && $val >= $valid[0] && $val <= $valid[1]) @@ -486,7 +484,7 @@ abstract class Filter if (!$recursive) { - trigger_error('Filter::checkInput - check failed [type: '.$type.' valid: '.((string)$valid).' val: '.((string)$val).']', E_USER_NOTICE); + trigger_error('Filter::checkInput - check failed [type: '.$type.' valid: '.Util::toString($valid).' val: '.((string)$val).']', E_USER_NOTICE); $this->error = true; } diff --git a/includes/dbtypes/creature.class.php b/includes/dbtypes/creature.class.php index f06d4910..5eb4edd8 100644 --- a/includes/dbtypes/creature.class.php +++ b/includes/dbtypes/creature.class.php @@ -332,8 +332,8 @@ class CreatureListFilter extends Filter 'ex' => [parent::V_EQUAL, 'on', false], // also match subname 'ma' => [parent::V_EQUAL, 1, false], // match any / all filter 'fa' => [parent::V_CALLBACK, 'cbPetFamily', true ], // pet family [list] - cat[0] == 1 - 'minle' => [parent::V_RANGE, [1, 99], false], // min level [int] - 'maxle' => [parent::V_RANGE, [1, 99], false], // max level [int] + 'minle' => [parent::V_RANGE, [0, 99], false], // min level [int] + 'maxle' => [parent::V_RANGE, [0, 99], false], // max level [int] 'cl' => [parent::V_RANGE, [0, 4], true ], // classification [list] 'ra' => [parent::V_LIST, [-1, 0, 1], false], // react alliance [int] 'rh' => [parent::V_LIST, [-1, 0, 1], false] // react horde [int] diff --git a/includes/dbtypes/item.class.php b/includes/dbtypes/item.class.php index d8e11659..595c468a 100644 --- a/includes/dbtypes/item.class.php +++ b/includes/dbtypes/item.class.php @@ -2021,10 +2021,10 @@ class ItemListFilter extends Filter 'ty' => [parent::V_CALLBACK, 'cbTypeCheck', true ], // item type - dynamic by current group 'sl' => [parent::V_CALLBACK, 'cbSlotCheck', true ], // item slot - dynamic by current group 'si' => [parent::V_LIST, [-SIDE_HORDE, -SIDE_ALLIANCE, SIDE_ALLIANCE, SIDE_HORDE, SIDE_BOTH], false], // side - 'minle' => [parent::V_RANGE, [1, 999], false], // item level min - 'maxle' => [parent::V_RANGE, [1, 999], false], // item level max - 'minrl' => [parent::V_RANGE, [1, MAX_LEVEL], false], // required level min - 'maxrl' => [parent::V_RANGE, [1, MAX_LEVEL], false] // required level max + 'minle' => [parent::V_RANGE, [0, 999], false], // item level min + 'maxle' => [parent::V_RANGE, [0, 999], false], // item level max + 'minrl' => [parent::V_RANGE, [0, MAX_LEVEL], false], // required level min + 'maxrl' => [parent::V_RANGE, [0, MAX_LEVEL], false] // required level max ); public array $extraOpts = []; // score for statWeights diff --git a/includes/dbtypes/itemset.class.php b/includes/dbtypes/itemset.class.php index 58988b63..0dfcb07c 100644 --- a/includes/dbtypes/itemset.class.php +++ b/includes/dbtypes/itemset.class.php @@ -187,10 +187,10 @@ class ItemsetListFilter extends Filter 'ma' => [parent::V_EQUAL, 1, false], // match any / all filter 'qu' => [parent::V_RANGE, [0, 7], true ], // quality 'ty' => [parent::V_RANGE, [1, 12], true ], // set type - 'minle' => [parent::V_RANGE, [1, 999], false], // min item level - 'maxle' => [parent::V_RANGE, [1, 999], false], // max itemlevel - 'minrl' => [parent::V_RANGE, [1, MAX_LEVEL], false], // min required level - 'maxrl' => [parent::V_RANGE, [1, MAX_LEVEL], false], // max required level + 'minle' => [parent::V_RANGE, [0, 999], false], // min item level + 'maxle' => [parent::V_RANGE, [0, 999], false], // max itemlevel + 'minrl' => [parent::V_RANGE, [0, MAX_LEVEL], false], // min required level + 'maxrl' => [parent::V_RANGE, [0, MAX_LEVEL], false], // max required level 'cl' => [parent::V_LIST, [[1, 9], 11], false], // class 'ta' => [parent::V_RANGE, [1, 30], false] // tag / content group ); diff --git a/includes/dbtypes/quest.class.php b/includes/dbtypes/quest.class.php index 5cd13bb7..f7798935 100644 --- a/includes/dbtypes/quest.class.php +++ b/includes/dbtypes/quest.class.php @@ -478,10 +478,10 @@ class QuestListFilter extends Filter '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 + 'minle' => [parent::V_RANGE, [0, 99], false], // min quest level + 'maxle' => [parent::V_RANGE, [0, 99], false], // max quest level + 'minrl' => [parent::V_RANGE, [0, 99], false], // min required level + 'maxrl' => [parent::V_RANGE, [0, 99], false], // max required level 'si' => [parent::V_LIST, [-SIDE_HORDE, -SIDE_ALLIANCE, SIDE_ALLIANCE, SIDE_HORDE, SIDE_BOTH], false], // side 'ty' => [parent::V_LIST, [0, 1, 21, 41, 62, [81, 85], 88, 89], true ] // type ); diff --git a/includes/dbtypes/spell.class.php b/includes/dbtypes/spell.class.php index f41e8161..935f6fd4 100644 --- a/includes/dbtypes/spell.class.php +++ b/includes/dbtypes/spell.class.php @@ -2527,10 +2527,10 @@ class SpellListFilter extends Filter 'na' => [parent::V_REGEX, parent::PATTERN_NAME, false], // name / text - only printable chars, no delimiter 'ex' => [parent::V_EQUAL, 'on', false], // extended name search 'ma' => [parent::V_EQUAL, 1, false], // match any / all filter - 'minle' => [parent::V_RANGE, [1, 99], false], // spell level min - 'maxle' => [parent::V_RANGE, [1, 99], false], // spell level max - 'minrs' => [parent::V_RANGE, [1, 999], false], // required skill level min - 'maxrs' => [parent::V_RANGE, [1, 999], false], // required skill level max + 'minle' => [parent::V_RANGE, [0, 99], false], // spell level min + 'maxle' => [parent::V_RANGE, [0, 99], false], // spell level max + 'minrs' => [parent::V_RANGE, [0, 999], false], // required skill level min + 'maxrs' => [parent::V_RANGE, [0, 999], false], // required skill level max 'ra' => [parent::V_LIST, [[1, 8], 10, 11], false], // races 'cl' => [parent::V_CALLBACK, 'cbClasses', true ], // classes 'gl' => [parent::V_CALLBACK, 'cbGlyphs', true ], // glyph type diff --git a/includes/utilities.php b/includes/utilities.php index 61084146..2f3db816 100644 --- a/includes/utilities.php +++ b/includes/utilities.php @@ -1142,6 +1142,27 @@ abstract class Util return $bits; } + public static function toString(mixed $var) : string + { + if (is_array($var)) + return '[' . implode(', ', array_map(self::toString(...), $var)) . ']'; + + if (is_object($var)) + { + // hm, respect object stringability? + // if ($var instanceof Stringable) + // return (string)$var; + + $buff = []; + foreach ($var as $k => $v) + $buff[] = $k.':'.self::toString($v); + + return '{' . implode(', ', $buff) . '}'; + } + + return (string)$var; + } + public static function buildPosFixMenu(int $mapId, float $posX, float $posY, int $type, int $guid, int $parentArea = 0, int $parentFloor = 0) : array { $points = WorldPosition::toZonePos($mapId, $posX, $posY); From c40bd3851bbc764e86a671268ba4d398e871ad0e Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Mon, 6 Oct 2025 16:20:55 +0200 Subject: [PATCH 020/260] Profiler/Fixup * fix scoring perm enchantments --- includes/components/profiler.class.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/components/profiler.class.php b/includes/components/profiler.class.php index 48a8d0f1..943a986d 100644 --- a/includes/components/profiler.class.php +++ b/includes/components/profiler.class.php @@ -576,7 +576,7 @@ class Profiler // enchantId => multiple spells => multiple items with varying itemlevels, quality, whatevs // cant reasonably get to the castItem from enchantId and slot - $profSpec = DB::Aowow()->selectCol('SELECT `id` AS ARRAY_KEY, `skillLevel` AS "1", `skillLine` AS "0" FROM ?_itemenchantment WHERE `id` IN (?a)', $permEnch); + $profSpec = DB::Aowow()->select('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])) From e37620c01b3ec2c273523814e1db8b6fea4203f0 Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Mon, 6 Oct 2025 16:30:48 +0200 Subject: [PATCH 021/260] Search/Fixup * fix pruning empty tokens from search --- includes/components/search.class.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/components/search.class.php b/includes/components/search.class.php index 734a45a6..6aa8f6fd 100644 --- a/includes/components/search.class.php +++ b/includes/components/search.class.php @@ -108,7 +108,7 @@ class Search { $clean = str_replace(['\\', '%'], '', $raw); - if (!$clean === '') + if ($clean === '') continue; if ($clean[0] == '-') From 05f5b0ed34db0e0ed2c4ea804581e1b4a4eb3bdb Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Mon, 6 Oct 2025 16:55:29 +0200 Subject: [PATCH 022/260] Response/Params * so we can't directly use BackedEnum::tryFrom as validator, because if the Enum is of and the string is not what php considers numeric, we get a straight TypeError Exception instead of null for failing the tryFrom. --- endpoints/data/data.php | 2 +- endpoints/guide/edit.php | 2 +- endpoints/locale/locale.php | 2 +- includes/components/response/baseresponse.class.php | 7 +++++++ 4 files changed, 10 insertions(+), 3 deletions(-) diff --git a/endpoints/data/data.php b/endpoints/data/data.php index 174e24c2..2376b38c 100644 --- a/endpoints/data/data.php +++ b/endpoints/data/data.php @@ -9,7 +9,7 @@ if (!defined('AOWOW_REVISION')) class DataBaseResponse extends TextResponse { protected array $expectedGET = array( - 'locale' => ['filter' => FILTER_CALLBACK, 'options' => [Locale::class, 'tryFrom'] ], + 'locale' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkLocale' ]], 't' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkTextLine' ]], 'catg' => ['filter' => FILTER_VALIDATE_INT ], 'skill' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkSkill' ]], diff --git a/endpoints/guide/edit.php b/endpoints/guide/edit.php index b2897167..9f7af2b2 100644 --- a/endpoints/guide/edit.php +++ b/endpoints/guide/edit.php @@ -45,7 +45,7 @@ class GuideEditResponse extends TemplateResponse 'description' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkDescription'] ], 'changelog' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkTextBlob'] ], 'body' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkTextBlob'] ], - 'locale' => ['filter' => FILTER_CALLBACK, 'options' => [Locale::class, 'tryFrom'] ], + 'locale' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkLocale'] ], 'category' => ['filter' => FILTER_VALIDATE_INT, 'options' => ['min_value' => 1, 'max_value' => 9] ], 'specId' => ['filter' => FILTER_VALIDATE_INT, 'options' => ['min_value' => -1, 'max_value' => 2, 'default' => -1]], 'classId' => ['filter' => FILTER_VALIDATE_INT, 'options' => ['min_value' => 1, 'max_value' => 11, 'default' => 0]] diff --git a/endpoints/locale/locale.php b/endpoints/locale/locale.php index fccf2962..260ae500 100644 --- a/endpoints/locale/locale.php +++ b/endpoints/locale/locale.php @@ -9,7 +9,7 @@ if (!defined('AOWOW_REVISION')) class LocaleBaseResponse extends TextResponse { protected array $expectedGET = array( - 'locale' => ['filter' => FILTER_CALLBACK, 'options' => [Locale::class, 'tryFrom']] + 'locale' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkLocale']] ); protected function generate() : void diff --git a/includes/components/response/baseresponse.class.php b/includes/components/response/baseresponse.class.php index 986fa509..99d3a8dd 100644 --- a/includes/components/response/baseresponse.class.php +++ b/includes/components/response/baseresponse.class.php @@ -645,6 +645,13 @@ abstract class BaseResponse return preg_replace('/ +/', ' ', trim($str)); } + protected static function checkLocale(string $localeId) : ?Locale + { + if (Util::checkNumeric($localeId, NUM_CAST_INT)) + return Locale::tryFrom($localeId); + return null; + } + /********************/ /* child implements */ From a7e9ac2cf2e1adb31577b130feecd21e8fc006b2 Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Mon, 6 Oct 2025 17:16:41 +0200 Subject: [PATCH 023/260] Misc/Fixup * HTTP_USER_AGENT is not guaranteed to be set --- includes/user.class.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/user.class.php b/includes/user.class.php index e310239f..cfa8673a 100644 --- a/includes/user.class.php +++ b/includes/user.class.php @@ -43,7 +43,7 @@ class User $_SESSION['dataKey'] = Util::createHash(); // just some random numbers for identification purpose self::$dataKey = $_SESSION['dataKey']; - self::$agent = $_SERVER['HTTP_USER_AGENT']; + self::$agent = $_SERVER['HTTP_USER_AGENT'] ?? ''; if (!self::$ip) return false; From e300086cc8720d014ba709c8f46eb80d1d891444 Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Mon, 6 Oct 2025 23:03:30 +0200 Subject: [PATCH 024/260] IconElement/Fixup * a DOMElements text value must be escaped manually (e.g. Foror & Tigule) --- includes/components/frontend/iconelement.class.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/components/frontend/iconelement.class.php b/includes/components/frontend/iconelement.class.php index a9fdac67..d5ab685e 100644 --- a/includes/components/frontend/iconelement.class.php +++ b/includes/components/frontend/iconelement.class.php @@ -102,7 +102,7 @@ class IconElement } if ($this->href) - ($a = $dom->createElement('a', $this->text))->setAttribute('href', $this->href); + ($a = $dom->createElement('a', htmlentities($this->text)))->setAttribute('href', $this->href); else $a = $dom->createTextNode($this->text); From d79742d59968e5342868208a79896f4f3534d6b6 Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Mon, 6 Oct 2025 23:14:53 +0200 Subject: [PATCH 025/260] Profiler/Fixup * dont use unsynced profile stubs in attained % calculation --- endpoints/achievement/achievement.php | 2 +- endpoints/faction/faction.php | 2 +- endpoints/quest/quest.php | 2 +- endpoints/spell/spell.php | 2 +- endpoints/title/title.php | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/endpoints/achievement/achievement.php b/endpoints/achievement/achievement.php index 0cb6df45..6fb270a1 100644 --- a/endpoints/achievement/achievement.php +++ b/endpoints/achievement/achievement.php @@ -121,7 +121,7 @@ class AchievementBaseResponse extends TemplateResponse implements ICache if (Cfg::get('PROFILER_ENABLE') && !($this->subject->getField('flags') & ACHIEVEMENT_FLAG_COUNTER)) { $x = DB::Aowow()->selectCell('SELECT COUNT(1) FROM ?_profiler_completion_achievements WHERE `achievementId` = ?d', $this->typeId); - $y = DB::Aowow()->selectCell('SELECT COUNT(1) FROM ?_profiler_profiles WHERE `realm` IS NOT NULL AND `realmGUID` IS NOT NULL'); + $y = DB::Aowow()->selectCell('SELECT COUNT(1) FROM ?_profiler_profiles WHERE `realm` IS NOT NULL AND `realmGUID` IS NOT NULL AND `cuFlags` & ?d = 0', PROFILER_CU_NEEDS_RESYNC); $infobox[] = Lang::profiler('attainedBy', [round(($x ?: 0) * 100 / ($y ?: 1))]); // - js component missing; diff --git a/endpoints/faction/faction.php b/endpoints/faction/faction.php index 868b9481..12ce8592 100644 --- a/endpoints/faction/faction.php +++ b/endpoints/faction/faction.php @@ -103,7 +103,7 @@ class FactionBaseResponse extends TemplateResponse implements ICache if (Cfg::get('PROFILER_ENABLE') && !($this->subject->getField('cuFlags') & CUSTOM_EXCLUDE_FOR_LISTVIEW)) { $x = DB::Aowow()->selectCell('SELECT COUNT(1) FROM ?_profiler_completion_reputation WHERE `standing` >= ?d AND `factionId` = ?d', 42000, $this->typeId); - $y = DB::Aowow()->selectCell('SELECT COUNT(1) FROM ?_profiler_profiles WHERE `realm` IS NOT NULL AND `realmGUID` IS NOT NULL'); + $y = DB::Aowow()->selectCell('SELECT COUNT(1) FROM ?_profiler_profiles WHERE `realm` IS NOT NULL AND `realmGUID` IS NOT NULL AND `cuFlags` & ?d = 0', PROFILER_CU_NEEDS_RESYNC); $infobox[] = Lang::profiler('attainedBy', [round(($x ?: 0) * 100 / ($y ?: 1))]); // - js component missing; diff --git a/endpoints/quest/quest.php b/endpoints/quest/quest.php index daf291f1..68a51839 100644 --- a/endpoints/quest/quest.php +++ b/endpoints/quest/quest.php @@ -278,7 +278,7 @@ class QuestBaseResponse extends TemplateResponse implements ICache if (Cfg::get('PROFILER_ENABLE') && !($_flags & QUEST_FLAG_UNAVAILABLE || $this->subject->getField('cuFlags') & CUSTOM_EXCLUDE_FOR_LISTVIEW)) { $x = DB::Aowow()->selectCell('SELECT COUNT(1) FROM ?_profiler_completion_quests WHERE `questId` = ?d', $this->typeId); - $y = DB::Aowow()->selectCell('SELECT COUNT(1) FROM ?_profiler_profiles WHERE `realm` IS NOT NULL AND `realmGUID` IS NOT NULL'); + $y = DB::Aowow()->selectCell('SELECT COUNT(1) FROM ?_profiler_profiles WHERE `realm` IS NOT NULL AND `realmGUID` IS NOT NULL AND `cuFlags` & ?d = 0', PROFILER_CU_NEEDS_RESYNC); $infobox[] = Lang::profiler('attainedBy', [round(($x ?: 0) * 100 / ($y ?: 1))]); // - js component missing; diff --git a/endpoints/spell/spell.php b/endpoints/spell/spell.php index 29e95f0a..c96a1739 100644 --- a/endpoints/spell/spell.php +++ b/endpoints/spell/spell.php @@ -2363,7 +2363,7 @@ class SpellBaseResponse extends TemplateResponse implements ICache if (Cfg::get('PROFILER_ENABLE') && in_array($this->subject->getField('typeCat'), [-5, -6]) && !($this->subject->getField('cuFlags') & CUSTOM_EXCLUDE_FOR_LISTVIEW)) { $x = DB::Aowow()->selectCell('SELECT COUNT(1) FROM ?_profiler_completion_spells WHERE `spellId` = ?d', $this->typeId); - $y = DB::Aowow()->selectCell('SELECT COUNT(1) FROM ?_profiler_profiles WHERE `realm` IS NOT NULL AND `realmGUID` IS NOT NULL'); + $y = DB::Aowow()->selectCell('SELECT COUNT(1) FROM ?_profiler_profiles WHERE `realm` IS NOT NULL AND `realmGUID` IS NOT NULL AND `cuFlags` & ?d = 0', PROFILER_CU_NEEDS_RESYNC); $infobox[] = Lang::profiler('attainedBy', [round(($x ?: 0) * 100 / ($y ?: 1))]); // - js component missing; diff --git a/endpoints/title/title.php b/endpoints/title/title.php index 382ce66b..a2805040 100644 --- a/endpoints/title/title.php +++ b/endpoints/title/title.php @@ -91,7 +91,7 @@ class TitleBaseResponse extends TemplateResponse implements ICache if (Cfg::get('PROFILER_ENABLE')) { $x = DB::Aowow()->selectCell('SELECT COUNT(1) FROM ?_profiler_completion_titles WHERE `titleId` = ?d', $this->typeId); - $y = DB::Aowow()->selectCell('SELECT COUNT(1) FROM ?_profiler_profiles WHERE `realm` IS NOT NULL AND `realmGUID` IS NOT NULL'); + $y = DB::Aowow()->selectCell('SELECT COUNT(1) FROM ?_profiler_profiles WHERE `realm` IS NOT NULL AND `realmGUID` IS NOT NULL AND `cuFlags` & ?d = 0', PROFILER_CU_NEEDS_RESYNC); $infobox[] = Lang::profiler('attainedBy', [round(($x ?: 0) * 100 / ($y ?: 1))]); // - js component missing; From 95ee9d2c25adbea7643c96272700a84bb8af33ab Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Tue, 7 Oct 2025 15:29:26 +0200 Subject: [PATCH 026/260] NPCs/Fixup * fix exception when displaying NPC with elemental resistances in base version but not difficulty modes --- endpoints/npc/npc.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/endpoints/npc/npc.php b/endpoints/npc/npc.php index 72f4500e..c911aee7 100644 --- a/endpoints/npc/npc.php +++ b/endpoints/npc/npc.php @@ -1006,10 +1006,13 @@ class NpcBaseResponse extends TemplateResponse implements ICache $modes['ranged'][] = sprintf($modeRow, $m, Lang::nf($ranged[0]).' - '.Lang::nf($ranged[1])); } + // todo: resistances can be present/missing in either $stats or $modes + // should be handled separately..? + if ($modes) foreach ($stats as $k => $v) if ($v) - $stats[$k] = sprintf($hint, implode('[/tr][tr]', $modes[$k]), $v, $k); + $stats[$k] = isset($modes[$k]) ? sprintf($hint, implode('[/tr][tr]', $modes[$k]), $v, $k) : $v; return $stats; } From 3edac3c77a10990465713629d4372e0624cb1800 Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Wed, 8 Oct 2025 18:20:26 +0200 Subject: [PATCH 027/260] Endpoints/Cache * fix cache id collision when category == dbTypeId for a given dbType * increment version number to invalidate existing caches * maps endpoint doesn't need caching. It is entirely static content. --- endpoints/achievement/achievement.php | 2 +- endpoints/achievements/achievements.php | 2 +- endpoints/areatrigger/areatrigger.php | 2 +- endpoints/areatriggers/areatriggers.php | 2 +- endpoints/class/class.php | 2 +- endpoints/classes/classes.php | 2 +- endpoints/currencies/currencies.php | 2 +- endpoints/currency/currency.php | 2 +- endpoints/emote/emote.php | 2 +- endpoints/emotes/emotes.php | 2 +- endpoints/enchantment/enchantment.php | 2 +- endpoints/enchantments/enchantments.php | 2 +- endpoints/event/event.php | 2 +- endpoints/events/events.php | 2 +- endpoints/faction/faction.php | 2 +- endpoints/factions/factions.php | 2 +- endpoints/guide/guide.php | 2 +- endpoints/guides/guides.php | 2 +- endpoints/icon/icon.php | 2 +- endpoints/icons/icons.php | 2 +- endpoints/item/item.php | 2 +- endpoints/item/item_xml.php | 3 +-- endpoints/items/items.php | 2 +- endpoints/itemset/itemset.php | 2 +- endpoints/itemsets/itemsets.php | 2 +- endpoints/mail/mail.php | 2 +- endpoints/mails/mails.php | 2 +- endpoints/maps/maps.php | 2 -- endpoints/npc/npc.php | 2 +- endpoints/npcs/npcs.php | 2 +- endpoints/object/object.php | 2 +- endpoints/objects/objects.php | 2 +- endpoints/pet/pet.php | 2 +- endpoints/pets/pets.php | 2 +- endpoints/quest/quest.php | 2 +- endpoints/quests/quests.php | 2 +- endpoints/race/race.php | 2 +- endpoints/races/races.php | 2 +- endpoints/skill/skill.php | 2 +- endpoints/skills/skills.php | 2 +- endpoints/sound/sound.php | 2 +- endpoints/sounds/sounds.php | 2 +- endpoints/spell/spell.php | 2 +- endpoints/spells/spells.php | 2 +- endpoints/title/title.php | 2 +- endpoints/titles/titles.php | 2 +- endpoints/zone/zone.php | 2 +- endpoints/zones/zones.php | 2 +- .../response/baseresponse.class.php | 19 ++++++++----------- .../response/templateresponse.class.php | 6 ++---- .../response/textresponse.class.php | 3 +-- includes/defines.php | 2 ++ includes/kernel.php | 2 +- 53 files changed, 61 insertions(+), 68 deletions(-) diff --git a/endpoints/achievement/achievement.php b/endpoints/achievement/achievement.php index 6fb270a1..7ab7fb86 100644 --- a/endpoints/achievement/achievement.php +++ b/endpoints/achievement/achievement.php @@ -21,7 +21,7 @@ class AchievementBaseResponse extends TemplateResponse implements ICache { use TrDetailPage, TrCache; - protected int $cacheType = CACHE_TYPE_PAGE; + protected int $cacheType = CACHE_TYPE_DETAIL_PAGE; protected string $template = 'achievement'; protected string $pageName = 'achievement'; diff --git a/endpoints/achievements/achievements.php b/endpoints/achievements/achievements.php index 70fe460e..98c2676a 100644 --- a/endpoints/achievements/achievements.php +++ b/endpoints/achievements/achievements.php @@ -11,7 +11,7 @@ class AchievementsBaseResponse extends TemplateResponse implements ICache use TrListPage, TrCache; protected int $type = Type::ACHIEVEMENT; - protected int $cacheType = CACHE_TYPE_PAGE; + protected int $cacheType = CACHE_TYPE_LIST_PAGE; protected string $template = 'achievements'; protected string $pageName = 'achievements'; diff --git a/endpoints/areatrigger/areatrigger.php b/endpoints/areatrigger/areatrigger.php index b0ce07ca..edd991cf 100644 --- a/endpoints/areatrigger/areatrigger.php +++ b/endpoints/areatrigger/areatrigger.php @@ -10,7 +10,7 @@ class AreatriggerBaseResponse extends TemplateResponse implements ICache { use TrDetailPage, TrCache; - protected int $cacheType = CACHE_TYPE_PAGE; + protected int $cacheType = CACHE_TYPE_DETAIL_PAGE; protected int $requiredUserGroup = U_GROUP_STAFF; protected string $template = 'detail-page-generic'; diff --git a/endpoints/areatriggers/areatriggers.php b/endpoints/areatriggers/areatriggers.php index 04b06d46..55ffa720 100644 --- a/endpoints/areatriggers/areatriggers.php +++ b/endpoints/areatriggers/areatriggers.php @@ -11,7 +11,7 @@ class AreatriggersBaseResponse extends TemplateResponse implements ICache use TrListPage, TrCache; protected int $type = Type::AREATRIGGER; - protected int $cacheType = CACHE_TYPE_PAGE; + protected int $cacheType = CACHE_TYPE_LIST_PAGE; protected int $requiredUserGroup = U_GROUP_STAFF; protected string $template = 'areatriggers'; diff --git a/endpoints/class/class.php b/endpoints/class/class.php index f983c679..b7ce50d7 100644 --- a/endpoints/class/class.php +++ b/endpoints/class/class.php @@ -12,7 +12,7 @@ class ClassBaseResponse extends TemplateResponse implements ICache private const TC_CLASS_IDS = [null, 8, 3, 1, 5, 4, 9, 6, 2, 7, null, 0]; // see TalentCalc.js - protected int $cacheType = CACHE_TYPE_PAGE; + protected int $cacheType = CACHE_TYPE_DETAIL_PAGE; protected string $template = 'detail-page-generic'; protected string $pageName = 'class'; diff --git a/endpoints/classes/classes.php b/endpoints/classes/classes.php index 3e7251a0..46959c9f 100644 --- a/endpoints/classes/classes.php +++ b/endpoints/classes/classes.php @@ -11,7 +11,7 @@ class ClassesBaseResponse extends TemplateResponse implements ICache use TrListPage, TrCache; protected int $type = Type::CHR_CLASS; - protected int $cacheType = CACHE_TYPE_PAGE; + protected int $cacheType = CACHE_TYPE_LIST_PAGE; protected string $template = 'list-page-generic'; protected string $pageName = 'classes'; diff --git a/endpoints/currencies/currencies.php b/endpoints/currencies/currencies.php index 0a5aec25..931cd885 100644 --- a/endpoints/currencies/currencies.php +++ b/endpoints/currencies/currencies.php @@ -11,7 +11,7 @@ class CurrenciesBaseResponse extends TemplateResponse implements ICache use TrListPage, TrCache; protected int $type = Type::CURRENCY; - protected int $cacheType = CACHE_TYPE_PAGE; + protected int $cacheType = CACHE_TYPE_LIST_PAGE; protected string $template = 'list-page-generic'; protected string $pageName = 'currencies'; diff --git a/endpoints/currency/currency.php b/endpoints/currency/currency.php index cc65b9e8..346a71bd 100644 --- a/endpoints/currency/currency.php +++ b/endpoints/currency/currency.php @@ -10,7 +10,7 @@ class CurrencyBaseResponse extends TemplateResponse implements ICache { use TrDetailPage, TrCache; - protected int $cacheType = CACHE_TYPE_PAGE; + protected int $cacheType = CACHE_TYPE_DETAIL_PAGE; protected string $template = 'detail-page-generic'; protected string $pageName = 'currency'; diff --git a/endpoints/emote/emote.php b/endpoints/emote/emote.php index 317d0130..b5b64f9e 100644 --- a/endpoints/emote/emote.php +++ b/endpoints/emote/emote.php @@ -10,7 +10,7 @@ class EmoteBaseResponse extends TemplateResponse implements ICache { use TrDetailPage, TrCache; - protected int $cacheType = CACHE_TYPE_PAGE; + protected int $cacheType = CACHE_TYPE_DETAIL_PAGE; protected string $template = 'detail-page-generic'; protected string $pageName = 'emote'; diff --git a/endpoints/emotes/emotes.php b/endpoints/emotes/emotes.php index f3103995..b303aa5f 100644 --- a/endpoints/emotes/emotes.php +++ b/endpoints/emotes/emotes.php @@ -10,7 +10,7 @@ class EmotesBaseResponse extends TemplateResponse implements ICache { use TrListPage, TrCache; - protected int $cacheType = CACHE_TYPE_PAGE; + protected int $cacheType = CACHE_TYPE_LIST_PAGE; protected int $type = Type::EMOTE; protected string $template = 'list-page-generic'; diff --git a/endpoints/enchantment/enchantment.php b/endpoints/enchantment/enchantment.php index b836cbd1..52a38cc6 100644 --- a/endpoints/enchantment/enchantment.php +++ b/endpoints/enchantment/enchantment.php @@ -10,7 +10,7 @@ class EnchantmentBaseResponse extends TemplateResponse implements ICache { use TrDetailPage, TrCache; - protected int $cacheType = CACHE_TYPE_PAGE; + protected int $cacheType = CACHE_TYPE_DETAIL_PAGE; protected string $template = 'enchantment'; protected string $pageName = 'enchantment'; diff --git a/endpoints/enchantments/enchantments.php b/endpoints/enchantments/enchantments.php index 1d35f2b2..2ca90fdb 100644 --- a/endpoints/enchantments/enchantments.php +++ b/endpoints/enchantments/enchantments.php @@ -11,7 +11,7 @@ class EnchantmentsBaseResponse extends TemplateResponse implements ICache use TrListPage, TrCache; protected int $type = Type::ENCHANTMENT; - protected int $cacheType = CACHE_TYPE_PAGE; + protected int $cacheType = CACHE_TYPE_LIST_PAGE; protected string $template = 'enchantments'; protected string $pageName = 'enchantments'; diff --git a/endpoints/event/event.php b/endpoints/event/event.php index 71a722e6..ac3b5f31 100644 --- a/endpoints/event/event.php +++ b/endpoints/event/event.php @@ -10,7 +10,7 @@ class EventBaseResponse extends TemplateResponse implements ICache { use TrDetailPage, TrCache; - protected int $cacheType = CACHE_TYPE_PAGE; + protected int $cacheType = CACHE_TYPE_DETAIL_PAGE; protected string $template = 'detail-page-generic'; protected string $pageName = 'event'; diff --git a/endpoints/events/events.php b/endpoints/events/events.php index 2ae4a9b9..c724234f 100644 --- a/endpoints/events/events.php +++ b/endpoints/events/events.php @@ -11,7 +11,7 @@ class EventsBaseResponse extends TemplateResponse implements ICache use TrListPage, TrCache; protected int $type = Type::WORLDEVENT; - protected int $cacheType = CACHE_TYPE_PAGE; + protected int $cacheType = CACHE_TYPE_LIST_PAGE; protected string $template = 'list-page-generic'; protected string $pageName = 'events'; diff --git a/endpoints/faction/faction.php b/endpoints/faction/faction.php index 12ce8592..934a191d 100644 --- a/endpoints/faction/faction.php +++ b/endpoints/faction/faction.php @@ -10,7 +10,7 @@ class FactionBaseResponse extends TemplateResponse implements ICache { use TrDetailPage, TrCache; - protected int $cacheType = CACHE_TYPE_PAGE; + protected int $cacheType = CACHE_TYPE_DETAIL_PAGE; protected string $template = 'detail-page-generic'; protected string $pageName = 'faction'; diff --git a/endpoints/factions/factions.php b/endpoints/factions/factions.php index db3caeb6..bf21c587 100644 --- a/endpoints/factions/factions.php +++ b/endpoints/factions/factions.php @@ -11,7 +11,7 @@ class FactionsBaseResponse extends TemplateResponse implements ICache use TrListPage, TrCache; protected int $type = Type::FACTION; - protected int $cacheType = CACHE_TYPE_PAGE; + protected int $cacheType = CACHE_TYPE_LIST_PAGE; protected string $template = 'list-page-generic'; protected string $pageName = 'factions'; diff --git a/endpoints/guide/guide.php b/endpoints/guide/guide.php index 90de7eb8..9bf5836a 100644 --- a/endpoints/guide/guide.php +++ b/endpoints/guide/guide.php @@ -10,7 +10,7 @@ class GuideBaseResponse extends TemplateResponse implements ICache { use TrDetailPage, TrCache; - protected int $cacheType = CACHE_TYPE_PAGE; + protected int $cacheType = CACHE_TYPE_DETAIL_PAGE; protected string $template = 'detail-page-generic'; protected string $pageName = 'guide'; diff --git a/endpoints/guides/guides.php b/endpoints/guides/guides.php index 9c0db0ee..5216111c 100644 --- a/endpoints/guides/guides.php +++ b/endpoints/guides/guides.php @@ -10,7 +10,7 @@ class GuidesBaseResponse extends TemplateResponse // implements ICache { use TrListPage/* , TrCache */; - // protected int $cacheType = CACHE_TYPE_PAGE; // really do? cache would need to be destroyed externally with each guide status update + // protected int $cacheType = CACHE_TYPE_LIST_PAGE; // really do? cache would need to be destroyed externally with each guide status update protected int $type = Type::GUIDE; protected string $template = 'list-page-generic'; diff --git a/endpoints/icon/icon.php b/endpoints/icon/icon.php index 2a67b53e..e57236c2 100644 --- a/endpoints/icon/icon.php +++ b/endpoints/icon/icon.php @@ -10,7 +10,7 @@ class IconBaseResponse extends TemplateResponse implements ICache { use TrDetailPage, TrCache; - protected int $cacheType = CACHE_TYPE_PAGE; + protected int $cacheType = CACHE_TYPE_DETAIL_PAGE; protected string $template = 'icon'; protected string $pageName = 'icon'; diff --git a/endpoints/icons/icons.php b/endpoints/icons/icons.php index 7613240e..7b2cf323 100644 --- a/endpoints/icons/icons.php +++ b/endpoints/icons/icons.php @@ -11,7 +11,7 @@ class IconsBaseResponse extends TemplateResponse implements ICache use TrListPage, TrCache; protected int $type = Type::ICON; - protected int $cacheType = CACHE_TYPE_PAGE; + protected int $cacheType = CACHE_TYPE_LIST_PAGE; protected string $template = 'icons'; protected string $pageName = 'icons'; diff --git a/endpoints/item/item.php b/endpoints/item/item.php index 2e5880fe..03f5da42 100644 --- a/endpoints/item/item.php +++ b/endpoints/item/item.php @@ -10,7 +10,7 @@ class ItemBaseResponse extends TemplateResponse implements ICache { use TrDetailPage, TrCache; - protected int $cacheType = CACHE_TYPE_PAGE; + protected int $cacheType = CACHE_TYPE_DETAIL_PAGE; protected string $template = 'item'; protected string $pageName = 'item'; diff --git a/endpoints/item/item_xml.php b/endpoints/item/item_xml.php index 5599be5c..ee71200b 100644 --- a/endpoints/item/item_xml.php +++ b/endpoints/item/item_xml.php @@ -220,8 +220,7 @@ class ItemXmlResponse extends TextResponse implements ICache { return array( $this->type, // DBType - $this->typeId, // DBTypeId - -1, // category + $this->typeId, // DBTypeId/category -1, // staff mask (content does not diff) '' // misc (unused) ); diff --git a/endpoints/items/items.php b/endpoints/items/items.php index 21aff287..442f0c08 100644 --- a/endpoints/items/items.php +++ b/endpoints/items/items.php @@ -11,7 +11,7 @@ class ItemsBaseResponse extends TemplateResponse implements ICache use TrListPage, TrCache; protected int $type = Type::ITEM; - protected int $cacheType = CACHE_TYPE_PAGE; + protected int $cacheType = CACHE_TYPE_LIST_PAGE; protected string $template = 'items'; protected string $pageName = 'items'; diff --git a/endpoints/itemset/itemset.php b/endpoints/itemset/itemset.php index 2cd46422..317637b3 100644 --- a/endpoints/itemset/itemset.php +++ b/endpoints/itemset/itemset.php @@ -10,7 +10,7 @@ class ItemsetBaseResponse extends TemplateResponse implements ICache { use TrDetailPage, TrCache; - protected int $cacheType = CACHE_TYPE_PAGE; + protected int $cacheType = CACHE_TYPE_DETAIL_PAGE; protected string $template = 'itemset'; protected string $pageName = 'itemset'; diff --git a/endpoints/itemsets/itemsets.php b/endpoints/itemsets/itemsets.php index a0acd354..d1905239 100644 --- a/endpoints/itemsets/itemsets.php +++ b/endpoints/itemsets/itemsets.php @@ -11,7 +11,7 @@ class ItemsetsBaseResponse extends TemplateResponse implements ICache use TrListPage, TrCache; protected int $type = Type::ITEMSET; - protected int $cacheType = CACHE_TYPE_PAGE; + protected int $cacheType = CACHE_TYPE_LIST_PAGE; protected string $template = 'itemsets'; protected string $pageName = 'itemsets'; diff --git a/endpoints/mail/mail.php b/endpoints/mail/mail.php index 5179b6e8..73bc132e 100644 --- a/endpoints/mail/mail.php +++ b/endpoints/mail/mail.php @@ -10,7 +10,7 @@ class MailBaseResponse extends TemplateResponse implements ICache { use TrDetailPage, TrCache; - protected int $cacheType = CACHE_TYPE_PAGE; + protected int $cacheType = CACHE_TYPE_DETAIL_PAGE; protected string $template = 'detail-page-generic'; protected string $pageName = 'mail'; diff --git a/endpoints/mails/mails.php b/endpoints/mails/mails.php index e892948f..0e7e4c60 100644 --- a/endpoints/mails/mails.php +++ b/endpoints/mails/mails.php @@ -11,7 +11,7 @@ class MailsBaseResponse extends TemplateResponse implements ICache use TrListPage, TrCache; protected int $type = Type::MAIL; - protected int $cacheType = CACHE_TYPE_PAGE; + protected int $cacheType = CACHE_TYPE_LIST_PAGE; protected string $template = 'list-page-generic'; protected string $pageName = 'mails'; diff --git a/endpoints/maps/maps.php b/endpoints/maps/maps.php index de62e4bc..0f99f470 100644 --- a/endpoints/maps/maps.php +++ b/endpoints/maps/maps.php @@ -8,8 +8,6 @@ if (!defined('AOWOW_REVISION')) class MapsBaseResponse extends TemplateResponse { - protected int $cacheType = CACHE_TYPE_PAGE; - protected string $template = 'maps'; protected string $pageName = 'maps'; protected ?int $activeTab = parent::TAB_TOOLS; diff --git a/endpoints/npc/npc.php b/endpoints/npc/npc.php index c911aee7..ba489abc 100644 --- a/endpoints/npc/npc.php +++ b/endpoints/npc/npc.php @@ -10,7 +10,7 @@ class NpcBaseResponse extends TemplateResponse implements ICache { use TrDetailPage, TrCache; - protected int $cacheType = CACHE_TYPE_PAGE; + protected int $cacheType = CACHE_TYPE_DETAIL_PAGE; protected string $template = 'npc'; protected string $pageName = 'npc'; diff --git a/endpoints/npcs/npcs.php b/endpoints/npcs/npcs.php index 1bd05bfb..5a325144 100644 --- a/endpoints/npcs/npcs.php +++ b/endpoints/npcs/npcs.php @@ -11,7 +11,7 @@ class NpcsBaseResponse extends TemplateResponse implements ICache use TrListPage, TrCache; protected int $type = Type::NPC; - protected int $cacheType = CACHE_TYPE_PAGE; + protected int $cacheType = CACHE_TYPE_LIST_PAGE; protected string $template = 'npcs'; protected string $pageName = 'npcs'; diff --git a/endpoints/object/object.php b/endpoints/object/object.php index 6ebfbe6f..de643a4d 100644 --- a/endpoints/object/object.php +++ b/endpoints/object/object.php @@ -10,7 +10,7 @@ class ObjectBaseResponse extends TemplateResponse implements ICache { use TrDetailPage, TrCache; - protected int $cacheType = CACHE_TYPE_PAGE; + protected int $cacheType = CACHE_TYPE_DETAIL_PAGE; protected string $template = 'object'; protected string $pageName = 'object'; diff --git a/endpoints/objects/objects.php b/endpoints/objects/objects.php index e08e5782..00b3d463 100644 --- a/endpoints/objects/objects.php +++ b/endpoints/objects/objects.php @@ -11,7 +11,7 @@ class ObjectsBaseResponse extends TemplateResponse implements ICache use TrListPage, TrCache; protected int $type = Type::OBJECT; - protected int $cacheType = CACHE_TYPE_PAGE; + protected int $cacheType = CACHE_TYPE_LIST_PAGE; protected string $template = 'objects'; protected string $pageName = 'objects'; diff --git a/endpoints/pet/pet.php b/endpoints/pet/pet.php index 30d0db14..63460f3e 100644 --- a/endpoints/pet/pet.php +++ b/endpoints/pet/pet.php @@ -10,7 +10,7 @@ class PetBaseResponse extends TemplateResponse implements ICache { use TrDetailPage, TrCache; - protected int $cacheType = CACHE_TYPE_PAGE; + protected int $cacheType = CACHE_TYPE_DETAIL_PAGE; protected string $template = 'detail-page-generic'; protected string $pageName = 'pet'; diff --git a/endpoints/pets/pets.php b/endpoints/pets/pets.php index 714bd151..7565805f 100644 --- a/endpoints/pets/pets.php +++ b/endpoints/pets/pets.php @@ -11,7 +11,7 @@ class PetsBaseResponse extends TemplateResponse implements ICache use TrListPage, TrCache; protected int $type = Type::PET; - protected int $cacheType = CACHE_TYPE_PAGE; + protected int $cacheType = CACHE_TYPE_LIST_PAGE; protected string $template = 'list-page-generic'; protected string $pageName = 'pets'; diff --git a/endpoints/quest/quest.php b/endpoints/quest/quest.php index 68a51839..a18c5d7e 100644 --- a/endpoints/quest/quest.php +++ b/endpoints/quest/quest.php @@ -10,7 +10,7 @@ class QuestBaseResponse extends TemplateResponse implements ICache { use TrDetailPage, TrCache; - protected int $cacheType = CACHE_TYPE_PAGE; + protected int $cacheType = CACHE_TYPE_DETAIL_PAGE; protected string $template = 'quest'; protected string $pageName = 'quest'; diff --git a/endpoints/quests/quests.php b/endpoints/quests/quests.php index fb4d944f..78fac93e 100644 --- a/endpoints/quests/quests.php +++ b/endpoints/quests/quests.php @@ -32,7 +32,7 @@ class QuestsBaseResponse extends TemplateResponse implements ICache ); protected int $type = Type::QUEST; - protected int $cacheType = CACHE_TYPE_PAGE; + protected int $cacheType = CACHE_TYPE_LIST_PAGE; protected string $template = 'quests'; protected string $pageName = 'quests'; diff --git a/endpoints/race/race.php b/endpoints/race/race.php index 6da89fc8..220bf369 100644 --- a/endpoints/race/race.php +++ b/endpoints/race/race.php @@ -16,7 +16,7 @@ class RaceBaseResponse extends TemplateResponse implements ICache [7952, 33554], null, [16264, 33557], [17584, 33657] ); - protected int $cacheType = CACHE_TYPE_PAGE; + protected int $cacheType = CACHE_TYPE_DETAIL_PAGE; protected string $template = 'detail-page-generic'; protected string $pageName = 'race'; diff --git a/endpoints/races/races.php b/endpoints/races/races.php index e5c67324..b9ae8ecb 100644 --- a/endpoints/races/races.php +++ b/endpoints/races/races.php @@ -11,7 +11,7 @@ class RacesBaseResponse extends TemplateResponse implements ICache use TrListPage, TrCache; protected int $type = Type::CHR_RACE; - protected int $cacheType = CACHE_TYPE_PAGE; + protected int $cacheType = CACHE_TYPE_LIST_PAGE; protected string $template = 'list-page-generic'; protected string $pageName = 'races'; diff --git a/endpoints/skill/skill.php b/endpoints/skill/skill.php index 005bc0cf..d68936e0 100644 --- a/endpoints/skill/skill.php +++ b/endpoints/skill/skill.php @@ -10,7 +10,7 @@ class SkillBaseResponse extends TemplateResponse implements ICache { use TrDetailPage, TrCache; - protected int $cacheType = CACHE_TYPE_PAGE; + protected int $cacheType = CACHE_TYPE_DETAIL_PAGE; protected string $template = 'detail-page-generic'; protected string $pageName = 'skill'; diff --git a/endpoints/skills/skills.php b/endpoints/skills/skills.php index f6275491..f60076d7 100644 --- a/endpoints/skills/skills.php +++ b/endpoints/skills/skills.php @@ -11,7 +11,7 @@ class SkillsBaseResponse extends TemplateResponse implements ICache use TrListPage, TrCache; protected int $type = Type::SKILL; - protected int $cacheType = CACHE_TYPE_PAGE; + protected int $cacheType = CACHE_TYPE_LIST_PAGE; protected string $template = 'list-page-generic'; protected string $pageName = 'skills'; diff --git a/endpoints/sound/sound.php b/endpoints/sound/sound.php index 6207cf2a..e2512285 100644 --- a/endpoints/sound/sound.php +++ b/endpoints/sound/sound.php @@ -10,7 +10,7 @@ class SoundBaseResponse extends TemplateResponse implements ICache { use TrDetailPage, TrCache; - protected int $cacheType = CACHE_TYPE_PAGE; + protected int $cacheType = CACHE_TYPE_DETAIL_PAGE; protected string $template = 'sound'; protected string $pageName = 'sound'; diff --git a/endpoints/sounds/sounds.php b/endpoints/sounds/sounds.php index 3ed6eaa6..9132c40f 100644 --- a/endpoints/sounds/sounds.php +++ b/endpoints/sounds/sounds.php @@ -11,7 +11,7 @@ class SoundsBaseResponse extends TemplateResponse implements ICache use TrListPage, TrCache; protected int $type = Type::SOUND; - protected int $cacheType = CACHE_TYPE_PAGE; + protected int $cacheType = CACHE_TYPE_LIST_PAGE; protected string $template = 'sounds'; protected string $pageName = 'sounds'; diff --git a/endpoints/spell/spell.php b/endpoints/spell/spell.php index c96a1739..abb5a783 100644 --- a/endpoints/spell/spell.php +++ b/endpoints/spell/spell.php @@ -16,7 +16,7 @@ class SpellBaseResponse extends TemplateResponse implements ICache SPELL_AURA_MOD_IGNORE_SHAPESHIFT, SPELL_AURA_PERIODIC_HASTE, SPELL_AURA_OVERRIDE_CLASS_SCRIPTS, SPELL_AURA_MOD_DAMAGE_FROM_CASTER, SPELL_AURA_ADD_TARGET_TRIGGER, /* SPELL_AURA_DUMMY ? */]; - protected int $cacheType = CACHE_TYPE_PAGE; + protected int $cacheType = CACHE_TYPE_DETAIL_PAGE; protected string $template = 'spell'; protected string $pageName = 'spell'; diff --git a/endpoints/spells/spells.php b/endpoints/spells/spells.php index db58d8e9..fa6c4533 100644 --- a/endpoints/spells/spells.php +++ b/endpoints/spells/spells.php @@ -26,7 +26,7 @@ class SpellsBaseResponse extends TemplateResponse implements ICache ); protected int $type = Type::SPELL; - protected int $cacheType = CACHE_TYPE_PAGE; + protected int $cacheType = CACHE_TYPE_LIST_PAGE; protected string $template = 'spells'; protected string $pageName = 'spells'; diff --git a/endpoints/title/title.php b/endpoints/title/title.php index a2805040..7bc4ae01 100644 --- a/endpoints/title/title.php +++ b/endpoints/title/title.php @@ -10,7 +10,7 @@ class TitleBaseResponse extends TemplateResponse implements ICache { use TrDetailPage, TrCache; - protected int $cacheType = CACHE_TYPE_PAGE; + protected int $cacheType = CACHE_TYPE_DETAIL_PAGE; protected string $template = 'detail-page-generic'; protected array $breadcrumb = [0, 10]; diff --git a/endpoints/titles/titles.php b/endpoints/titles/titles.php index a9b916d6..61e4dba3 100644 --- a/endpoints/titles/titles.php +++ b/endpoints/titles/titles.php @@ -11,7 +11,7 @@ class TitlesBaseResponse extends TemplateResponse implements ICache use TrListPage, TrCache; protected int $type = Type::TITLE; - protected int $cacheType = CACHE_TYPE_PAGE; + protected int $cacheType = CACHE_TYPE_LIST_PAGE; protected string $template = 'list-page-generic'; protected string $pageName = 'titles'; diff --git a/endpoints/zone/zone.php b/endpoints/zone/zone.php index 2039efc9..a26ddbab 100644 --- a/endpoints/zone/zone.php +++ b/endpoints/zone/zone.php @@ -10,7 +10,7 @@ class ZoneBaseResponse extends TemplateResponse implements ICache { use TrDetailPage, TrCache; - protected int $cacheType = CACHE_TYPE_PAGE; + protected int $cacheType = CACHE_TYPE_DETAIL_PAGE; protected string $template = 'detail-page-generic'; protected string $pageName = 'zone'; diff --git a/endpoints/zones/zones.php b/endpoints/zones/zones.php index 77247254..925486fc 100644 --- a/endpoints/zones/zones.php +++ b/endpoints/zones/zones.php @@ -11,7 +11,7 @@ class ZonesBaseResponse extends TemplateResponse implements ICache use TrListPage, TrCache; protected int $type = Type::ZONE; - protected int $cacheType = CACHE_TYPE_PAGE; + protected int $cacheType = CACHE_TYPE_LIST_PAGE; protected string $template = 'list-page-generic'; protected string $pageName = 'zones'; diff --git a/includes/components/response/baseresponse.class.php b/includes/components/response/baseresponse.class.php index 99d3a8dd..137741d8 100644 --- a/includes/components/response/baseresponse.class.php +++ b/includes/components/response/baseresponse.class.php @@ -201,9 +201,9 @@ trait TrCache { $this->initCache(); - // type+typeId+catg; 3+6+10 + // type+typeId/catg+mode; 3+10+1 $cKey = $this->formatCacheKey(); - $cKey[2] = substr($cKey[2], 0, 19); + $cKey[2] = substr($cKey[2], 0, 13); if ($modeMask & CACHE_MODE_MEMCACHED) foreach ($this->memcached()?->getAllKeys() ?? [] as $k) @@ -265,29 +265,27 @@ trait TrCache // https://stackoverflow.com/questions/466521 private function formatCacheKey() : array { - [$dbType, $dbTypeId, $category, $staffMask, $miscInfo] = $this->getCacheKeyComponents(); + [$dbType, $dbTypeIdOrCat, $staffMask, $miscInfo] = $this->getCacheKeyComponents(); $fileKey = ''; // DBType: 3 $fileKey .= str_pad(dechex($dbType & 0xFFF), 3, 0, STR_PAD_LEFT); - // DBTypeId: 6 - $fileKey .= str_pad(dechex($dbTypeId & 0xFFFFFF), 6, 0, STR_PAD_LEFT); - // category: (2+4+4) - $fileKey .= str_pad(dechex($category & 0xFFFFFFFFFF), 2+4+4, 0, STR_PAD_LEFT); + // DBTypeId: 6 / category: (2+4+4) + $fileKey .= str_pad(dechex($dbTypeIdOrCat & 0xFFFFFFFFFF), 2+4+4, 0, STR_PAD_LEFT); // cacheType: 1 $fileKey .= str_pad(dechex($this->_cacheType & 0xF), 1, 0, STR_PAD_LEFT); // localeId: 2, $fileKey .= str_pad(dechex(Lang::getLocale()->value & 0xFF), 2, 0, STR_PAD_LEFT); // staff mask: 4 $fileKey .= str_pad(dechex($staffMask & 0xFFFFFFFF), 4, 0, STR_PAD_LEFT); - // optioal: miscInfo + // optional: miscInfo if ($miscInfo) $fileKey .= '-'.$miscInfo; // topDir, 2ndDir, file return array( str_pad(dechex($dbType & 0xFF), 2, 0, STR_PAD_LEFT), - str_pad(dechex(($dbTypeId > 0 ? $dbTypeId : $category) & 0xFF), 2, 0, STR_PAD_LEFT), + str_pad(dechex(($dbTypeIdOrCat) & 0xFF), 2, 0, STR_PAD_LEFT), $fileKey ); } @@ -327,8 +325,7 @@ trait TrSearch { return array( -1, // DBType - -1, // DBTypeId - $this->searchMask, // category + $this->searchMask, // DBTypeId/category User::$groups, // staff mask md5($this->query) // misc (here search query) ); diff --git a/includes/components/response/templateresponse.class.php b/includes/components/response/templateresponse.class.php index bc5ff8f6..d5050ad0 100644 --- a/includes/components/response/templateresponse.class.php +++ b/includes/components/response/templateresponse.class.php @@ -21,8 +21,7 @@ trait TrDetailPage { return array( $this->type, // DBType - $this->typeId, // DBTypeId - -1, // category + $this->typeId, // DBTypeId/category User::$groups, // staff mask '' // misc (here unused) ); @@ -54,8 +53,7 @@ trait TrListPage return array( $this->type, // DBType - -1, // DBTypeId - $catg ?? -1, // category + $catg ?? -1, // DBTypeId/category User::$groups, // staff mask $misc ?? '' // misc (here filter) ); diff --git a/includes/components/response/textresponse.class.php b/includes/components/response/textresponse.class.php index aef1843d..7885af27 100644 --- a/includes/components/response/textresponse.class.php +++ b/includes/components/response/textresponse.class.php @@ -14,8 +14,7 @@ trait TrTooltip { $key = array( $this->type, // DBType - $this->typeId, // DBTypeId - -1, // category + $this->typeId, // DBTypeId/category User::$groups, // staff mask '' // misc (here tooltip) ); diff --git a/includes/defines.php b/includes/defines.php index e73a84d0..5084912c 100644 --- a/includes/defines.php +++ b/includes/defines.php @@ -41,6 +41,8 @@ define('CACHE_TYPE_PAGE', 1); define('CACHE_TYPE_TOOLTIP', 2); define('CACHE_TYPE_SEARCH', 3); define('CACHE_TYPE_XML', 4); // only used by items +define('CACHE_TYPE_LIST_PAGE', 5); +define('CACHE_TYPE_DETAIL_PAGE', 6); define('CACHE_MODE_FILECACHE', 0x1); define('CACHE_MODE_MEMCACHED', 0x2); diff --git a/includes/kernel.php b/includes/kernel.php index 809460c9..00a4414d 100644 --- a/includes/kernel.php +++ b/includes/kernel.php @@ -6,7 +6,7 @@ mb_internal_encoding('UTF-8'); error_reporting(E_ALL); mysqli_report(MYSQLI_REPORT_ERROR); -define('AOWOW_REVISION', 41); +define('AOWOW_REVISION', 42); 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 From a9ed897ea6182265ca75223a00201bf0f6805de4 Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Wed, 8 Oct 2025 18:35:38 +0200 Subject: [PATCH 028/260] Filter/Fixup * fix evaluating imbalanced criteria --- includes/components/filter.class.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/components/filter.class.php b/includes/components/filter.class.php index 8ea36189..a84e6ca3 100644 --- a/includes/components/filter.class.php +++ b/includes/components/filter.class.php @@ -335,7 +335,7 @@ abstract class Filter if (count($_cr) != count($_crv) || count($_cr) != count($_crs) || count($_cr) > 5 || count($_crs) > 5 /*|| count($_crv) > 5*/) { // use min provided criterion as basis; 5 criteria at most - $min = max(5, min(count($_cr), count($_crv), count($_crs))); + $min = min(5, count($_cr), count($_crv), count($_crs)); if (count($_cr) > $min) array_splice($_cr, $min); From 215ad39cc6bbf9df4a83a18bad094a59a1677eef Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Wed, 8 Oct 2025 18:52:24 +0200 Subject: [PATCH 029/260] NPC/Reputation * try to fix reputation spillover * fix extra colon on reputation gains --- endpoints/npc/npc.php | 4 ++-- template/pages/npc.tpl.php | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/endpoints/npc/npc.php b/endpoints/npc/npc.php index ba489abc..b7faf170 100644 --- a/endpoints/npc/npc.php +++ b/endpoints/npc/npc.php @@ -832,7 +832,7 @@ class NpcBaseResponse extends TemplateResponse implements ICache $spill[0][1] = $set[1][1] / 2; $spillover[$factions->getField('cat')] = $spill; - $set[6] = $factions->getField('cat'); // set spillover + $set[5] = $factions->getField('cat'); // set spillover } $result[] = $set; @@ -872,7 +872,7 @@ class NpcBaseResponse extends TemplateResponse implements ICache foreach ($reputation as $i => [, $data]) { - foreach ($data as [$factionId, , , , , , $spillover]) + foreach ($data as [$factionId, , , , , $spillover]) { if (!$spillover) continue; diff --git a/template/pages/npc.tpl.php b/template/pages/npc.tpl.php index a6dfeac1..ba68c727 100644 --- a/template/pages/npc.tpl.php +++ b/template/pages/npc.tpl.php @@ -70,7 +70,7 @@ if ($this->reputation): ?>

reputation as [$mode, $data]): if (count($this->reputation) > 1): From 1dcdf9623b41b3b4e33dfa78a0f0f01e51bd30ce Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Wed, 8 Oct 2025 20:01:24 +0200 Subject: [PATCH 030/260] Endpoints/User * do not display user page for internal system user --- endpoints/user/user.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/endpoints/user/user.php b/endpoints/user/user.php index c89a6cf2..72b87671 100644 --- a/endpoints/user/user.php +++ b/endpoints/user/user.php @@ -42,6 +42,10 @@ class UserBaseResponse extends TemplateResponse $this->user = $user; else $this->generateNotFound(Lang::user('notFound', [$pageParam])); + + // do not display system account + if (!$this->user['id']) + $this->generateNotFound(Lang::user('notFound', [$pageParam])); } protected function generate() : void From 5d2fd00358f9ab421825a4481ce4b96d52e657aa Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Fri, 10 Oct 2025 17:41:32 +0200 Subject: [PATCH 031/260] Profiler/Save * fixed inventory definitions not allowing for negative ids (random enchantments) * added handling invalid inventory definitions --- endpoints/profile/save.php | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/endpoints/profile/save.php b/endpoints/profile/save.php index b063cd39..0dd3eabc 100644 --- a/endpoints/profile/save.php +++ b/endpoints/profile/save.php @@ -32,7 +32,7 @@ class ProfileSaveResponse extends TextResponse 'copy' => ['filter' => FILTER_VALIDATE_INT ], 'public' => ['filter' => FILTER_VALIDATE_INT ], 'gearscore' => ['filter' => FILTER_VALIDATE_INT ], - 'inv' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkIdListUnsigned'], 'flags' => FILTER_REQUIRE_ARRAY] + 'inv' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkIdList'], 'flags' => FILTER_REQUIRE_ARRAY] ); public function __construct(string $pageParam) @@ -139,6 +139,12 @@ class ProfileSaveResponse extends TextResponse { foreach ($this->_post['inv'] as $slot => $itemData) { + if (!$itemData) + { + trigger_error('ProfileSaveResponse::generate - skipping malformed inventory definition for slot #'.$slot.': '.Util::toString($itemData), E_USER_NOTICE); + continue; + } + if ($slot + 1 == array_sum($itemData)) // only slot definition set => empty slot { DB::Aowow()->query('DELETE FROM ?_profiler_items WHERE `id` = ?d AND `slot` = ?d', $charId, $itemData[0]); From 204d4b8ae2462dca187dc78604e7755844ce2fc8 Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Fri, 10 Oct 2025 18:05:12 +0200 Subject: [PATCH 032/260] Profiler/Sync * fix SQL FK error when creating guild or arenateam stub. * Urlized name field is non-optional --- endpoints/arena-team/arena-team.php | 1 + endpoints/guild/guild.php | 1 + endpoints/profile/profile.php | 4 ++-- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/endpoints/arena-team/arena-team.php b/endpoints/arena-team/arena-team.php index 2d40ce97..2ade7851 100644 --- a/endpoints/arena-team/arena-team.php +++ b/endpoints/arena-team/arena-team.php @@ -61,6 +61,7 @@ class ArenateamBaseResponse extends TemplateResponse { $subject['realm'] = $this->realmId; $subject['cuFlags'] = PROFILER_CU_NEEDS_RESYNC; + $subject['nameUrl'] = Profiler::urlize($subject['name']); // create entry from realm with basic info DB::Aowow()->query('INSERT IGNORE INTO ?_profiler_arena_team (?#) VALUES (?a)', array_keys($subject), array_values($subject)); diff --git a/endpoints/guild/guild.php b/endpoints/guild/guild.php index 8855b26c..f4ec2335 100644 --- a/endpoints/guild/guild.php +++ b/endpoints/guild/guild.php @@ -61,6 +61,7 @@ class GuildBaseResponse extends TemplateResponse { $subject['realm'] = $this->realmId; $subject['cuFlags'] = PROFILER_CU_NEEDS_RESYNC; + $subject['nameUrl'] = Profiler::urlize($subject['name']); // create entry from realm with basic info DB::Aowow()->query('INSERT IGNORE INTO ?_profiler_guild (?#) VALUES (?a)', array_keys($subject), array_values($subject)); diff --git a/endpoints/profile/profile.php b/endpoints/profile/profile.php index 2e41350c..958ffaae 100644 --- a/endpoints/profile/profile.php +++ b/endpoints/profile/profile.php @@ -99,10 +99,10 @@ class ProfileBaseResponse extends TemplateResponse if ($subject['guildGUID']) { - // create empty guild if nessecary to satisfy foreign keys + // create empty guild if necessary to satisfy foreign keys $subject['guild'] = DB::Aowow()->selectCell('SELECT `id` FROM ?_profiler_guild WHERE `realm` = ?d AND `realmGUID` = ?d', $this->realmId, $subject['guildGUID']); if (!$subject['guild']) - $subject['guild'] = DB::Aowow()->query('INSERT INTO ?_profiler_guild (`realm`, `realmGUID`, `cuFlags`, `name`) VALUES (?d, ?d, ?d, ?)', $this->realmId, $subject['guildGUID'], PROFILER_CU_NEEDS_RESYNC, $subject['guildName']); + $subject['guild'] = DB::Aowow()->query('INSERT INTO ?_profiler_guild (`realm`, `realmGUID`, `cuFlags`, `name`, `nameUrl`) VALUES (?d, ?d, ?d, ?, ?)', $this->realmId, $subject['guildGUID'], PROFILER_CU_NEEDS_RESYNC, $subject['guildName'], Profiler::urlize($subject['guildName'])); } unset($subject['guildGUID'], $subject['guildName'], $subject['at_login']); From 196f60f1762a66e13972df4d6fe9cb33a480c447 Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Fri, 10 Oct 2025 19:36:41 +0200 Subject: [PATCH 033/260] Filter/Zones * add missing Ruby Sanctum to zones dropdown * sort zones alphabetically --- includes/components/filter.class.php | 2 +- static/js/locale_dede.js | 68 +++++++++++++-------------- static/js/locale_enus.js | 62 ++++++++++++------------ static/js/locale_eses.js | 68 +++++++++++++-------------- static/js/locale_frfr.js | 70 ++++++++++++++-------------- static/js/locale_ruru.js | 70 ++++++++++++++-------------- static/js/locale_zhcn.js | 70 ++++++++++++++-------------- 7 files changed, 205 insertions(+), 205 deletions(-) diff --git a/includes/components/filter.class.php b/includes/components/filter.class.php index a84e6ca3..c9927cd5 100644 --- a/includes/components/filter.class.php +++ b/includes/components/filter.class.php @@ -82,7 +82,7 @@ abstract class Filter 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); + 718, 3277, 28, 40, 11, 4197, 618, 3521, 3805, 66, 1176, 1977, 4987); 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); diff --git a/static/js/locale_dede.js b/static/js/locale_dede.js index 1f075acb..1e46333b 100644 --- a/static/js/locale_dede.js +++ b/static/js/locale_dede.js @@ -3630,41 +3630,41 @@ var LANG = { [589, "Wintersäblerausbilder"], [609, "Zirkel des Cenarius"] ], zone: [ - [4494, "Ahn'kahet: Das Alte Königreich"],[3428, "Tempel von Ahn'Qiraj"], [36, "Alteracgebirge"], [2597, "Alteractal"], [3358, "Arathibecken"], - [45, "Arathihochland"], [4603, "Archavons Kammer"], [3702, "Arena des Schergrats"], [4378, "Arena von Dalaran"], [3698, "Arena von Nagrand"], - [3790, "Auchenaikrypta"], [3820, "Auge des Sturms"], [4277, "Azjol-Nerub"], [16, "Azshara"], [3524, "Azurmythosinsel"], - [3525, "Blutmythosinsel"], [3537, "Boreanische Tundra"], [17, "Brachland"], [46, "Brennende Steppe"], [209, "Burg Schattenfang"], - [206, "Burg Utgarde"], [4395, "Dalaran"], [1657, "Darnassus"], [4500, "Das Auge der Ewigkeit"], [4100, "Das Ausmerzen von Stratholme"], - [4493, "Das Obsidiansanktum"], [4228, "Das Oculus"], [796, "Das Scharlachrote Kloster"], [457, "Das verhüllte Meer"], [717, "Das Verlies"], - [3713, "Der Blutkessel"], [3905, "Der Echsenkessel"], [2437, "Der Flammenschlund"], [495, "Der heulende Fjord"], [4265, "Der Nexus"], - [4406, "Der Ring der Ehre"], [3638, "Der Ring der Prüfung"], [2366, "Der schwarze Morast"], [3959, "Der Schwarze Tempel"], [3840, "Der Schwarze Tempel"], - [25, "Der Schwarzfels"], [1477, "Der Tempel von Atal'Hakkar"], [3716, "Der Tiefensumpf"], [3775, "Der Zirkel des Blutes"], [405, "Desolace"], + [4494, "Ahn'kahet: Das Alte Königreich"], [36, "Alteracgebirge"], [2597, "Alteractal"], [3358, "Arathibecken"], [ 45, "Arathihochland"], + [4603, "Archavons Kammer"], [3702, "Arena des Schergrats"], [4378, "Arena von Dalaran"], [3698, "Arena von Nagrand"], [3790, "Auchenaikrypta"], + [3820, "Auge des Sturms"], [4277, "Azjol-Nerub"], [ 16, "Azshara"], [3524, "Azurmythosinsel"], [3525, "Blutmythosinsel"], + [3537, "Boreanische Tundra"], [ 17, "Brachland"], [ 46, "Brennende Steppe"], [ 209, "Burg Schattenfang"], [ 206, "Burg Utgarde"], + [4395, "Dalaran"], [1657, "Darnassus"], [4500, "Das Auge der Ewigkeit"], [4100, "Das Ausmerzen von Stratholme"], [4493, "Das Obsidiansanktum"], + [4228, "Das Oculus"], [4987, "Das Rubinsanktum"], [ 796, "Das Scharlachrote Kloster"], [ 717, "Das Verlies"], [ 457, "Das verhüllte Meer"], + [3713, "Der Blutkessel"], [3905, "Der Echsenkessel"], [2437, "Der Flammenschlund"], [4265, "Der Nexus"], [4406, "Der Ring der Ehre"], + [3638, "Der Ring der Prüfung"], [3959, "Der Schwarze Tempel"], [3840, "Der Schwarze Tempel"], [ 25, "Der Schwarzfels"], [1477, "Der Tempel von Atal'Hakkar"], + [3716, "Der Tiefensumpf"], [3775, "Der Zirkel des Blutes"], [ 495, "Der heulende Fjord"], [2366, "Der schwarze Morast"], [ 405, "Desolace"], [3848, "Die Arkatraz"], [3847, "Die Botanika"], [3715, "Die Dampfkammer"], [3557, "Die Exodar"], [4272, "Die Hallen der Blitze"], - [4264, "Die Hallen des Steins"], [718, "Die Höhlen des Wehklagens"], [3849, "Die Mechanar"], [4809, "Die Seelenschmiede"], [3717, "Die Sklavenunterkünfte"], - [67, "Die Sturmgipfel"], [2257, "Die Tiefenbahn"], [1581, "Die Todesminen"], [4415, "Die Violette Festung"], [3714, "Die zerschmetterten Hallen"], - [1638, "Donnerfels"], [65, "Drachenöde"], [148, "Dunkelküste"], [1, "Dun Morogh"], [14, "Durotar"], - [10, "Dämmerwald"], [2557, "Düsterbruch"], [15, "Düstermarschen"], [1537, "Eisenschmiede"], [210, "Eiskrone"], - [4812, "Eiskronenzitadelle"], [331, "Eschental"], [357, "Feralas"], [4196, "Feste Drak'Tharon"], [3845, "Festung der Stürme"], - [41, "Gebirgspass der Totenwinde"], [3433, "Geisterlande"], [2717, "Geschmolzener Kern"], [721, "Gnomeregan"], [394, "Grizzlyhügel"], - [4813, "Grube von Saron"], [3923, "Gruuls Unterschlupf"], [4416, "Gundrak"], [2918, "Halle der Champions"], [2917, "Halle der Legenden"], - [4820, "Hallen der Reflexion"], [47, "Hinterland"], [1216, "Holzschlundfeste"], [4742, "Hrothgars Landestelle"], [3606, "Hyjalgipfel"], - [3607, "Höhle des Schlangenschreins"], [1941, "Höhlen der Zeit"], [3562, "Höllenfeuerbollwerk"], [3483, "Höllenfeuerhalbinsel"], [722, "Hügel der Klingenhauer"], - [3430, "Immersangwald"], [4710, "Insel der Eroberung"], [4080, "Insel von Quel'Danas"], [4024, "Kaltarra"], [3457, "Karazhan"], - [491, "Kral der Klingenhauer"], [490, "Krater von Un'Goro"], [3277, "Kriegshymnenschlucht"], [2817, "Kristallsangwald"], [38, "Loch Modan"], - [3836, "Magtheridons Kammer"], [3792, "Managruft"], [2100, "Maraudon"], [493, "Mondlichtung"], [215, "Mulgore"], - [3518, "Nagrand"], [3456, "Naxxramas"], [3523, "Nethersturm"], [2159, "Onyxias Hort"], [1637, "Orgrimmar"], - [2677, "Pechschwingenhort"], [4298, "Die Scharlachrote Enklave"], [4723, "Prüfung des Champions"], [4722, "Prüfung des Kreuzfahrers"], [44, "Rotkammgebirge"], - [3429, "Ruinen von Ahn'Qiraj"], [3968, "Ruinen von Lordaeron"], [3789, "Schattenlabyrinth"], [3520, "Schattenmondtal"], [3522, "Schergrat"], - [33, "Schlingendorntal"], [2057, "Scholomance"], [1583, "Schwarzfelsspitze"], [1584, "Schwarzfelstiefen"], [51, "Sengende Schlucht"], - [3791, "Sethekkhallen"], [3703, "Shattrath"], [3711, "Sholazarbecken"], [3487, "Silbermond"], [130, "Silberwald"], - [1377, "Silithus"], [3679, "Skettis"], [4075, "Sonnenbrunnenplateau"], [406, "Steinkrallengebirge"], [393, "Strand der Dunkelspeere"], - [4384, "Strand der Uralten"], [2017, "Stratholme"], [1519, "Sturmwind"], [11, "Sumpfland"], [8, "Sümpfe des Elends"], - [440, "Tanaris"], [400, "Tausend Nadeln"], [4197, "Tausendwintersee"], [141, "Teldrassil"], [4131, "Terrasse der Magister"], - [361, "Teufelswald"], [719, "Tiefschwarze Grotte"], [85, "Tirisfal"], [1196, "Turm Utgarde"], [1337, "Uldaman"], - [4273, "Ulduar"], [1497, "Unterstadt"], [4, "Verwüstete Lande"], [2367, "Vorgebirge des Alten Hügellands"], - [267, "Vorgebirge des Hügellands"], [12, "Wald von Elwynn"], [40, "Westfall"], [28, "Westliche Pestländer"], [618, "Winterquell"], - [3519, "Wälder von Terokkar"], [3521, "Zangarmarschen"], [3805, "Zul'Aman"], [66, "Zul'Drak"], [1176, "Zul'Farrak"], - [1977, "Zul'Gurub"], [3, "Ödland"], [139, "Östliche Pestländer"] + [4264, "Die Hallen des Steins"], [ 718, "Die Höhlen des Wehklagens"], [3849, "Die Mechanar"], [4298, "Die Scharlachrote Enklave"], [4809, "Die Seelenschmiede"], + [3717, "Die Sklavenunterkünfte"], [ 67, "Die Sturmgipfel"], [2257, "Die Tiefenbahn"], [1581, "Die Todesminen"], [4415, "Die Violette Festung"], + [3714, "Die zerschmetterten Hallen"], [1638, "Donnerfels"], [ 65, "Drachenöde"], [ 1, "Dun Morogh"], [ 148, "Dunkelküste"], + [ 14, "Durotar"], [ 10, "Dämmerwald"], [2557, "Düsterbruch"], [ 15, "Düstermarschen"], [1537, "Eisenschmiede"], + [ 210, "Eiskrone"], [4812, "Eiskronenzitadelle"], [ 331, "Eschental"], [ 357, "Feralas"], [4196, "Feste Drak'Tharon"], + [3845, "Festung der Stürme"], [ 41, "Gebirgspass der Totenwinde"], [3433, "Geisterlande"], [2717, "Geschmolzener Kern"], [ 721, "Gnomeregan"], + [ 394, "Grizzlyhügel"], [4813, "Grube von Saron"], [3923, "Gruuls Unterschlupf"], [4416, "Gundrak"], [2918, "Halle der Champions"], + [2917, "Halle der Legenden"], [4820, "Hallen der Reflexion"], [ 47, "Hinterland"], [1216, "Holzschlundfeste"], [4742, "Hrothgars Landestelle"], + [3606, "Hyjalgipfel"], [3607, "Höhle des Schlangenschreins"], [1941, "Höhlen der Zeit"], [3562, "Höllenfeuerbollwerk"], [3483, "Höllenfeuerhalbinsel"], + [ 722, "Hügel der Klingenhauer"], [3430, "Immersangwald"], [4710, "Insel der Eroberung"], [4080, "Insel von Quel'Danas"], [4024, "Kaltarra"], + [3457, "Karazhan"], [ 491, "Kral der Klingenhauer"], [ 490, "Krater von Un'Goro"], [3277, "Kriegshymnenschlucht"], [2817, "Kristallsangwald"], + [ 38, "Loch Modan"], [3836, "Magtheridons Kammer"], [3792, "Managruft"], [2100, "Maraudon"], [ 493, "Mondlichtung"], + [ 215, "Mulgore"], [3518, "Nagrand"], [3456, "Naxxramas"], [3523, "Nethersturm"], [2159, "Onyxias Hort"], + [1637, "Orgrimmar"], [ 3, "Ödland"], [ 139, "Östliche Pestländer"], [2677, "Pechschwingenhort"], [4723, "Prüfung des Champions"], + [4722, "Prüfung des Kreuzfahrers"], [ 44, "Rotkammgebirge"], [3429, "Ruinen von Ahn'Qiraj"], [3968, "Ruinen von Lordaeron"], [3789, "Schattenlabyrinth"], + [3520, "Schattenmondtal"], [3522, "Schergrat"], [ 33, "Schlingendorntal"], [2057, "Scholomance"], [1583, "Schwarzfelsspitze"], + [1584, "Schwarzfelstiefen"], [ 51, "Sengende Schlucht"], [3791, "Sethekkhallen"], [3703, "Shattrath"], [3711, "Sholazarbecken"], + [3487, "Silbermond"], [ 130, "Silberwald"], [1377, "Silithus"], [3679, "Skettis"], [4075, "Sonnenbrunnenplateau"], + [ 406, "Steinkrallengebirge"], [ 393, "Strand der Dunkelspeere"], [4384, "Strand der Uralten"], [2017, "Stratholme"], [1519, "Sturmwind"], + [ 11, "Sumpfland"], [ 8, "Sümpfe des Elends"], [ 440, "Tanaris"], [ 400, "Tausend Nadeln"], [4197, "Tausendwintersee"], + [ 141, "Teldrassil"], [3428, "Tempel von Ahn'Qiraj"], [4131, "Terrasse der Magister"], [ 361, "Teufelswald"], [ 719, "Tiefschwarze Grotte"], + [ 85, "Tirisfal"], [1196, "Turm Utgarde"], [1337, "Uldaman"], [4273, "Ulduar"], [1497, "Unterstadt"], + [ 4, "Verwüstete Lande"], [2367, "Vorgebirge des Alten Hügellands"], [267, "Vorgebirge des Hügellands"], [ 12, "Wald von Elwynn"], [ 40, "Westfall"], + [ 28, "Westliche Pestländer"], [ 618, "Winterquell"], [3519, "Wälder von Terokkar"], [3521, "Zangarmarschen"], [3805, "Zul'Aman"], + [ 66, "Zul'Drak"], [1176, "Zul'Farrak"], [1977, "Zul'Gurub"] ], resistance: [ [6, "Arkan"], [2, "Feuer"], [3, "Natur"], [4, "Frost"], [5, "Schatten"], diff --git a/static/js/locale_enus.js b/static/js/locale_enus.js index 4f9911a4..3f0292ec 100644 --- a/static/js/locale_enus.js +++ b/static/js/locale_enus.js @@ -3679,41 +3679,41 @@ var LANG = { [589, "Wintersaber Trainers"], [270, "Zandalar Tribe"] ], zone: [ - [4494, "Ahn'kahet: The Old Kingdom"], [36, "Alterac Mountains"], [2597, "Alterac Valley"], [3358, "Arathi Basin"], [45, "Arathi Highlands"], - [331, "Ashenvale"], [3790, "Auchenai Crypts"], [4277, "Azjol-Nerub"], [16, "Azshara"], [3524, "Azuremyst Isle"], - [3, "Badlands"], [3959, "Black Temple"], [719, "Blackfathom Deeps"], [1584, "Blackrock Depths"], [25, "Blackrock Mountain"], - [1583, "Blackrock Spire"], [2677, "Blackwing Lair"], [3702, "Blade's Edge Arena"], [3522, "Blade's Edge Mountains"], [4, "Blasted Lands"], - [3525, "Bloodmyst Isle"], [3537, "Borean Tundra"], [46, "Burning Steppes"], [1941, "Caverns of Time"], [2918, "Champions' Hall"], + [4494, "Ahn'kahet: The Old Kingdom"], [ 36, "Alterac Mountains"], [2597, "Alterac Valley"], [3358, "Arathi Basin"], [ 45, "Arathi Highlands"], + [ 331, "Ashenvale"], [3790, "Auchenai Crypts"], [4277, "Azjol-Nerub"], [ 16, "Azshara"], [3524, "Azuremyst Isle"], + [ 3, "Badlands"], [3959, "Black Temple"], [ 719, "Blackfathom Deeps"], [1584, "Blackrock Depths"], [ 25, "Blackrock Mountain"], + [1583, "Blackrock Spire"], [2677, "Blackwing Lair"], [3702, "Blade's Edge Arena"], [3522, "Blade's Edge Mountains"], [ 4, "Blasted Lands"], + [3525, "Bloodmyst Isle"], [3537, "Borean Tundra"], [ 46, "Burning Steppes"], [1941, "Caverns of Time"], [2918, "Champions' Hall"], [3905, "Coilfang Reservoir"], [4024, "Coldarra"], [2817, "Crystalsong Forest"], [4395, "Dalaran"], [4378, "Dalaran Sewers"], - [148, "Darkshore"], [393, "Darkspear Strand"], [1657, "Darnassus"], [41, "Deadwind Pass"], [2257, "Deeprun Tram"], - [405, "Desolace"], [2557, "Dire Maul"], [65, "Dragonblight"], [4196, "Drak'Tharon Keep"], [1, "Dun Morogh"], - [14, "Durotar"], [10, "Duskwood"], [15, "Dustwallow Marsh"], [139, "Eastern Plaguelands"], [12, "Elwynn Forest"], - [3430, "Eversong Woods"], [3820, "Eye of the Storm"], [361, "Felwood"], [357, "Feralas"], [3433, "Ghostlands"], - [721, "Gnomeregan"], [394, "Grizzly Hills"], [3923, "Gruul's Lair"], [4416, "Gundrak"], [2917, "Hall of Legends"], + [ 148, "Darkshore"], [ 393, "Darkspear Strand"], [1657, "Darnassus"], [ 41, "Deadwind Pass"], [2257, "Deeprun Tram"], + [ 405, "Desolace"], [2557, "Dire Maul"], [ 65, "Dragonblight"], [4196, "Drak'Tharon Keep"], [ 1, "Dun Morogh"], + [ 14, "Durotar"], [ 10, "Duskwood"], [ 15, "Dustwallow Marsh"], [ 139, "Eastern Plaguelands"], [ 12, "Elwynn Forest"], + [3430, "Eversong Woods"], [3820, "Eye of the Storm"], [ 361, "Felwood"], [ 357, "Feralas"], [3433, "Ghostlands"], + [ 721, "Gnomeregan"], [ 394, "Grizzly Hills"], [3923, "Gruul's Lair"], [4416, "Gundrak"], [2917, "Hall of Legends"], [4272, "Halls of Lightning"], [4820, "Halls of Reflection"], [4264, "Halls of Stone"], [3483, "Hellfire Peninsula"], [3562, "Hellfire Ramparts"], - [267, "Hillsbrad Foothills"], [495, "Howling Fjord"], [4742, "Hrothgar's Landing"], [3606, "Hyjal Summit"], [210, "Icecrown"], + [ 267, "Hillsbrad Foothills"], [ 495, "Howling Fjord"], [4742, "Hrothgar's Landing"], [3606, "Hyjal Summit"], [ 210, "Icecrown"], [4812, "Icecrown Citadel"], [1537, "Ironforge"], [4710, "Isle of Conquest"], [4080, "Isle of Quel'Danas"], [3457, "Karazhan"], - [38, "Loch Modan"], [4131, "Magisters' Terrace"], [3836, "Magtheridon's Lair"], [3792, "Mana-Tombs"], [2100, "Maraudon"], - [2717, "Molten Core"], [493, "Moonglade"], [215, "Mulgore"], [3518, "Nagrand"], [3698, "Nagrand Arena"], + [ 38, "Loch Modan"], [4131, "Magisters' Terrace"], [3836, "Magtheridon's Lair"], [3792, "Mana-Tombs"], [2100, "Maraudon"], + [2717, "Molten Core"], [ 493, "Moonglade"], [ 215, "Mulgore"], [3518, "Nagrand"], [3698, "Nagrand Arena"], [3456, "Naxxramas"], [3523, "Netherstorm"], [2367, "Old Hillsbrad Foothills"], [2159, "Onyxia's Lair"], [1637, "Orgrimmar"], - [4813, "Pit of Saron"], [4298, "The Scarlet Enclave"], [2437, "Ragefire Chasm"], [722, "Razorfen Downs"], - [491, "Razorfen Kraul"], [44, "Redridge Mountains"], [3429, "Ruins of Ahn'Qiraj"], [3968, "Ruins of Lordaeron"], [796, "Scarlet Monastery"], - [2057, "Scholomance"], [51, "Searing Gorge"], [3607, "Serpentshrine Cavern"], [3791, "Sethekk Halls"], [3789, "Shadow Labyrinth"], - [209, "Shadowfang Keep"], [3520, "Shadowmoon Valley"], [3703, "Shattrath City"], [3711, "Sholazar Basin"], [1377, "Silithus"], - [3487, "Silvermoon City"], [130, "Silverpine Forest"], [3679, "Skettis"], [406, "Stonetalon Mountains"], [1519, "Stormwind City"], - [4384, "Strand of the Ancients"], [33, "Stranglethorn Vale"], [2017, "Stratholme"], [1477, "Sunken Temple"], [4075, "Sunwell Plateau"], - [8, "Swamp of Sorrows"], [440, "Tanaris"], [141, "Teldrassil"], [3428, "Temple of Ahn'Qiraj"], [3519, "Terokkar Forest"], - [3848, "The Arcatraz"], [17, "The Barrens"], [2366, "The Black Morass"], [3840, "The Black Temple"], [3713, "The Blood Furnace"], - [3847, "The Botanica"], [3775, "The Circle of Blood"], [4100, "The Culling of Stratholme"], [1581, "The Deadmines"], [3557, "The Exodar"], - [3845, "The Eye"], [4500, "The Eye of Eternity"], [4809, "The Forge of Souls"], [47, "The Hinterlands"], [3849, "The Mechanar"], - [4265, "The Nexus"], [4493, "The Obsidian Sanctum"], [4228, "The Oculus"], [3698, "The Ring of Trials"], [4406, "The Ring of Valor"], - [3714, "The Shattered Halls"], [3717, "The Slave Pens"], [3715, "The Steamvault"], [717, "The Stockade"], [67, "The Storm Peaks"], - [3716, "The Underbog"], [457, "The Veiled Sea"], [4415, "The Violet Hold"], [400, "Thousand Needles"], [1638, "Thunder Bluff"], - [1216, "Timbermaw Hold"], [85, "Tirisfal Glades"], [4723, "Trial of the Champion"], [4722, "Trial of the Crusader"], [1337, "Uldaman"], - [4273, "Ulduar"], [490, "Un'Goro Crater"], [1497, "Undercity"], [206, "Utgarde Keep"], [1196, "Utgarde Pinnacle"], - [4603, "Vault of Archavon"], [718, "Wailing Caverns"], [3277, "Warsong Gulch"], [28, "Western Plaguelands"], [40, "Westfall"], - [11, "Wetlands"], [4197, "Wintergrasp"], [618, "Winterspring"], [3521, "Zangarmarsh"], [3805, "Zul'Aman"], - [66, "Zul'Drak"], [1176, "Zul'Farrak"], [1977, "Zul'Gurub"] + [4813, "Pit of Saron"], [2437, "Ragefire Chasm"], [ 722, "Razorfen Downs"], [ 491, "Razorfen Kraul"], [ 44, "Redridge Mountains"], + [3429, "Ruins of Ahn'Qiraj"], [3968, "Ruins of Lordaeron"], [ 796, "Scarlet Monastery"], [2057, "Scholomance"], [ 51, "Searing Gorge"], + [3607, "Serpentshrine Cavern"], [3791, "Sethekk Halls"], [3789, "Shadow Labyrinth"], [ 209, "Shadowfang Keep"], [3520, "Shadowmoon Valley"], + [3703, "Shattrath City"], [3711, "Sholazar Basin"], [1377, "Silithus"], [3487, "Silvermoon City"], [ 130, "Silverpine Forest"], + [3679, "Skettis"], [ 406, "Stonetalon Mountains"], [1519, "Stormwind City"], [4384, "Strand of the Ancients"], [ 33, "Stranglethorn Vale"], + [2017, "Stratholme"], [1477, "Sunken Temple"], [4075, "Sunwell Plateau"], [ 8, "Swamp of Sorrows"], [ 440, "Tanaris"], + [ 141, "Teldrassil"], [3428, "Temple of Ahn'Qiraj"], [3519, "Terokkar Forest"], [3848, "The Arcatraz"], [ 17, "The Barrens"], + [2366, "The Black Morass"], [3840, "The Black Temple"], [3713, "The Blood Furnace"], [3847, "The Botanica"], [3775, "The Circle of Blood"], + [4100, "The Culling of Stratholme"], [1581, "The Deadmines"], [3557, "The Exodar"], [3845, "The Eye"], [4500, "The Eye of Eternity"], + [4809, "The Forge of Souls"], [ 47, "The Hinterlands"], [3849, "The Mechanar"], [4265, "The Nexus"], [4493, "The Obsidian Sanctum"], + [4228, "The Oculus"], [3698, "The Ring of Trials"], [4406, "The Ring of Valor"], [4987, "The Ruby Sanctum"], [4298, "The Scarlet Enclave"], + [3714, "The Shattered Halls"], [3717, "The Slave Pens"], [3715, "The Steamvault"], [ 717, "The Stockade"], [ 67, "The Storm Peaks"], + [3716, "The Underbog"], [ 457, "The Veiled Sea"], [4415, "The Violet Hold"], [ 400, "Thousand Needles"], [1638, "Thunder Bluff"], + [1216, "Timbermaw Hold"], [ 85, "Tirisfal Glades"], [4723, "Trial of the Champion"], [4722, "Trial of the Crusader"], [1337, "Uldaman"], + [4273, "Ulduar"], [ 490, "Un'Goro Crater"], [1497, "Undercity"], [ 206, "Utgarde Keep"], [1196, "Utgarde Pinnacle"], + [4603, "Vault of Archavon"], [ 718, "Wailing Caverns"], [3277, "Warsong Gulch"], [ 28, "Western Plaguelands"], [ 40, "Westfall"], + [ 11, "Wetlands"], [4197, "Wintergrasp"], [ 618, "Winterspring"], [3521, "Zangarmarsh"], [3805, "Zul'Aman"], + [ 66, "Zul'Drak"], [1176, "Zul'Farrak"], [1977, "Zul'Gurub"] ], resistance: [ [6, "Arcane"], [2, "Fire"], [3, "Nature"], [4, "Frost"], [5, "Shadow"], diff --git a/static/js/locale_eses.js b/static/js/locale_eses.js index 53edb97d..d5e3722a 100644 --- a/static/js/locale_eses.js +++ b/static/js/locale_eses.js @@ -3630,41 +3630,41 @@ var LANG = { [72, "Ventormenta"], [989, "Vigilantes del Tiempo"], [577, "Vista Eterna"] ], zone: [ - [4494, "Ahn'kahet: El Antiguo Reino"], [3428, "Ahn'Qiraj"], [3775, "Anillo de Sangre"], [2367, "Antiguas Laderas de Trabalomas"],[4378, "Arena de Dalaran"], - [3698, "Arena de Nagrand"], [3702, "Arena Filospada"], [4277, "Azjol-Nerub"], [16, "Azshara"], [4131, "Bancal del Magister"], - [1216, "Bastión Fauces de Madera"], [3430, "Bosque Canción Eterna"], [2817, "Bosque Canto de Cristal"], [130, "Bosque de Argénteos"], [12, "Bosque de Elwynn"], - [10, "Bosque del Ocaso"], [3519, "Bosque de Terokkar"], [209, "Castillo de Colmillo Oscuro"], [3607, "Caverna Santuario Serpiente"], [719, "Cavernas de Brazanegra"], - [1941, "Cavernas del Tiempo"], [65, "Cementerio de Dragones"], [1638, "Cima del Trueno"], [3487, "Ciudad de Lunargenta"], [3703, "Ciudad de Shattrath"], - [1519, "Ciudad de Ventormenta"], [4812, "Ciudadela de la Corona de Hielo"],[493, "Claro de la Luna"], [85, "Claros de Tirisfal"], [394, "Colinas Pardas"], - [4197, "Conquista del Invierno"], [210, "Corona de Hielo"], [148, "Costa Oscura"], [3790, "Criptas Auchenai"], [490, "Cráter de Un'Goro"], - [3358, "Cuenca de Arathi"], [3711, "Cuenca de Sholazar"], [718, "Cuevas de los Lamentos"], [1583, "Cumbre de Roca Negra"], [618, "Cuna del Invierno"], + [3428, "Ahn'Qiraj"], [4494, "Ahn'kahet: El Antiguo Reino"], [3775, "Anillo de Sangre"], [2367, "Antiguas Laderas de Trabalomas"], [3702, "Arena Filospada"], + [4378, "Arena de Dalaran"], [3698, "Arena de Nagrand"], [4277, "Azjol-Nerub"], [ 16, "Azshara"], [4131, "Bancal del Magister"], + [1216, "Bastión Fauces de Madera"], [3430, "Bosque Canción Eterna"], [2817, "Bosque Canto de Cristal"], [ 130, "Bosque de Argénteos"], [ 12, "Bosque de Elwynn"], + [3519, "Bosque de Terokkar"], [ 10, "Bosque del Ocaso"], [ 209, "Castillo de Colmillo Oscuro"], [3607, "Caverna Santuario Serpiente"], [ 719, "Cavernas de Brazanegra"], + [1941, "Cavernas del Tiempo"], [ 65, "Cementerio de Dragones"], [1638, "Cima del Trueno"], [3487, "Ciudad de Lunargenta"], [3703, "Ciudad de Shattrath"], + [1519, "Ciudad de Ventormenta"], [4812, "Ciudadela de la Corona de Hielo"],[493, "Claro de la Luna"], [ 85, "Claros de Tirisfal"], [ 394, "Colinas Pardas"], + [4197, "Conquista del Invierno"], [ 210, "Corona de Hielo"], [ 148, "Costa Oscura"], [3790, "Criptas Auchenai"], [ 490, "Cráter de Un'Goro"], + [3358, "Cuenca de Arathi"], [3711, "Cuenca de Sholazar"], [ 718, "Cuevas de los Lamentos"], [1583, "Cumbre de Roca Negra"], [ 618, "Cuna del Invierno"], [4264, "Cámaras de Piedra"], [4820, "Cámaras de Reflexión"], [4272, "Cámaras de Relámpagos"], [4395, "Dalaran"], [1657, "Darnassus"], - [4742, "Desembarco de Hrothgar"], [405, "Desolace"], [1, "Dun Morogh"], [14, "Durotar"], [3848, "El Arcatraz"], - [4415, "El Bastión Violeta"], [3845, "El Castillo de la Tempestad"], [3638, "El Círculo de los Retos"], [4406, "El Círculo del Valor"], [3557, "El Exodar"], - [3713, "El Horno de Sangre"], [3847, "El Invernáculo"], [3849, "El Mechanar"], [4265, "El Nexo"], [4228, "El Oculus"], - [4500, "El Ojo de la Eternidad"], [4493, "El Sagrario Obsidiana"], [1477, "El Templo de Atal'Hakkar"], [3840, "El Templo Oscuro"], [1497, "Entrañas"], - [357, "Feralas"], [495, "Fiordo Aquilonal"], [1537, "Forjaz"], [4196, "Fortaleza de Drak'Tharon"], [206, "Fortaleza de Utgarde"], - [4813, "Foso de Saron"], [361, "Frondavil"], [3277, "Garganta Grito de Guerra"], [4024, "Gelidar"], [721, "Gnomeregan"], - [2677, "Guarida de Alanegra"], [3923, "Guarida de Gruul"], [3836, "Guarida de Magtheridon"], [2159, "Guarida de Onyxia"], [4416, "Gundrak"], - [491, "Horado Rajacieno"], [3524, "Isla Bruma Azur"], [3525, "Isla Bruma de Sangre"], [4710, "Isla de la Conquista"], [4080, "Isla de Quel'Danas"], - [3457, "Karazhan"], [3789, "Laberinto de las Sombras"], [3606, "La Cima Hyjal"], [2366, "La Ciénaga Negra"], [4603, "La Cámara de Archavon"], - [3715, "La Cámara de Vapor"], [267, "Laderas de Trabalomas"], [4809, "La Forja de Almas"], [51, "La Garganta de Fuego"], [2557, "La Masacre"], - [4100, "La Matanza de Stratholme"], [67, "Las Cumbres Tormentosas"], [46, "Las Estepas Ardientes"], [717, "Las Mazmorras"], [400, "Las Mil Agujas"], - [1581, "Las Minas de la Muerte"], [3716, "La Sotiénaga"], [3714, "Las Salas Arrasadas"], [4, "Las Tierras Devastadas"], [38, "Loch Modan"], - [17, "Los Baldíos"], [11, "Los Humedales"], [2100, "Maraudon"], [457, "Mar de la Bruma"], [3521, "Marisma de Zangar"], - [15, "Marjal Revolcafango"], [4075, "Meseta de La Fuente del Sol"], [796, "Monasterio Escarlata"], [25, "Montaña Roca Negra"], [44, "Montañas Crestagrana"], - [36, "Montañas de Alterac"], [3522, "Montañas Filospada"], [215, "Mulgore"], [3562, "Murallas del Fuego Infernal"], [3518, "Nagrand"], - [3456, "Naxxramas"], [2717, "Núcleo de Magma"], [3820, "Ojo de la Tormenta"], [1637, "Orgrimmar"], [8, "Pantano de las Penas"], - [41, "Paso de la Muerte"], [3483, "Península del Fuego Infernal"], [1196, "Pináculo de Utgarde"], [4384, "Playa de los Ancestros"], [393, "Playa Lanza Negra"], - [1584, "Profundidades de Roca Negra"], [4723, "Prueba del Campeón"], [4722, "Prueba del Cruzado"], [40, "Páramos de Poniente"], [3717, "Recinto de los Esclavos"], - [3905, "Reserva Colmillo Torcido"], [3429, "Ruinas de Ahn'Qiraj"], [3968, "Ruinas de Lordaeron"], [2917, "Sala de las Leyendas"], [2918, "Sala de los Campeones"], - [3791, "Salas Sethekk"], [2057, "Scholomance"], [406, "Sierra Espolón"], [1377, "Silithus"], [2437, "Sima Ígnea"], - [3679, "Skettis"], [2017, "Stratholme"], [440, "Tanaris"], [141, "Teldrassil"], [3959, "Templo Oscuro"], - [45, "Tierras Altas de Arathi"], [4298, "El Enclave Escarlata"], [139, "Tierras de la Peste del Este"], [28, "Tierras de la Peste del Oeste"], [47, "Tierras del Interior"], - [3433, "Tierras Fantasma"], [3, "Tierras Inhóspitas"], [3523, "Tormenta Abisal"], [2257, "Tranvía Subterráneo"], [3792, "Tumbas de Maná"], - [3537, "Tundra Boreal"], [1337, "Uldaman"], [4273, "Ulduar"], [2597, "Valle de Alterac"], [331, "Vallefresno"], - [3520, "Valle Sombraluna"], [33, "Vega de Tuercespina"], [722, "Zahúrda Rajacieno"], [3805, "Zul'Aman"], [66, "Zul'Drak"], - [1176, "Zul'Farrak"], [1977, "Zul'Gurub"] + [4742, "Desembarco de Hrothgar"], [ 405, "Desolace"], [ 1, "Dun Morogh"], [ 14, "Durotar"], [3848, "El Arcatraz"], + [4415, "El Bastión Violeta"], [3845, "El Castillo de la Tempestad"], [3638, "El Círculo de los Retos"], [4406, "El Círculo del Valor"], [4298, "El Enclave Escarlata"], + [3557, "El Exodar"], [3713, "El Horno de Sangre"], [3847, "El Invernáculo"], [3849, "El Mechanar"], [4265, "El Nexo"], + [4228, "El Oculus"], [4500, "El Ojo de la Eternidad"], [4493, "El Sagrario Obsidiana"], [4987, "El Sagrario Rubí"], [3840, "El Templo Oscuro"], + [1477, "El Templo de Atal'Hakkar"], [1497, "Entrañas"], [ 357, "Feralas"], [ 495, "Fiordo Aquilonal"], [1537, "Forjaz"], + [4196, "Fortaleza de Drak'Tharon"], [ 206, "Fortaleza de Utgarde"], [4813, "Foso de Saron"], [ 361, "Frondavil"], [3277, "Garganta Grito de Guerra"], + [4024, "Gelidar"], [ 721, "Gnomeregan"], [2677, "Guarida de Alanegra"], [3923, "Guarida de Gruul"], [3836, "Guarida de Magtheridon"], + [2159, "Guarida de Onyxia"], [4416, "Gundrak"], [ 491, "Horado Rajacieno"], [3524, "Isla Bruma Azur"], [3525, "Isla Bruma de Sangre"], + [4080, "Isla de Quel'Danas"], [4710, "Isla de la Conquista"], [3457, "Karazhan"], [3606, "La Cima Hyjal"], [2366, "La Ciénaga Negra"], + [4603, "La Cámara de Archavon"], [3715, "La Cámara de Vapor"], [4809, "La Forja de Almas"], [ 51, "La Garganta de Fuego"], [2557, "La Masacre"], + [4100, "La Matanza de Stratholme"], [3716, "La Sotiénaga"], [3789, "Laberinto de las Sombras"], [ 267, "Laderas de Trabalomas"], [ 67, "Las Cumbres Tormentosas"], + [ 46, "Las Estepas Ardientes"], [ 717, "Las Mazmorras"], [ 400, "Las Mil Agujas"], [1581, "Las Minas de la Muerte"], [3714, "Las Salas Arrasadas"], + [ 4, "Las Tierras Devastadas"], [ 38, "Loch Modan"], [ 17, "Los Baldíos"], [ 11, "Los Humedales"], [ 457, "Mar de la Bruma"], + [2100, "Maraudon"], [3521, "Marisma de Zangar"], [ 15, "Marjal Revolcafango"], [4075, "Meseta de La Fuente del Sol"], [ 796, "Monasterio Escarlata"], + [ 25, "Montaña Roca Negra"], [ 44, "Montañas Crestagrana"], [3522, "Montañas Filospada"], [ 36, "Montañas de Alterac"], [ 215, "Mulgore"], + [3562, "Murallas del Fuego Infernal"], [3518, "Nagrand"], [3456, "Naxxramas"], [2717, "Núcleo de Magma"], [3820, "Ojo de la Tormenta"], + [1637, "Orgrimmar"], [ 8, "Pantano de las Penas"], [ 41, "Paso de la Muerte"], [3483, "Península del Fuego Infernal"], [1196, "Pináculo de Utgarde"], + [ 393, "Playa Lanza Negra"], [4384, "Playa de los Ancestros"], [1584, "Profundidades de Roca Negra"], [4723, "Prueba del Campeón"], [4722, "Prueba del Cruzado"], + [ 40, "Páramos de Poniente"], [3717, "Recinto de los Esclavos"], [3905, "Reserva Colmillo Torcido"], [3429, "Ruinas de Ahn'Qiraj"], [3968, "Ruinas de Lordaeron"], + [2917, "Sala de las Leyendas"], [2918, "Sala de los Campeones"], [3791, "Salas Sethekk"], [2057, "Scholomance"], [ 406, "Sierra Espolón"], + [1377, "Silithus"], [2437, "Sima Ígnea"], [3679, "Skettis"], [2017, "Stratholme"], [ 440, "Tanaris"], + [ 141, "Teldrassil"], [3959, "Templo Oscuro"], [ 45, "Tierras Altas de Arathi"], [3433, "Tierras Fantasma"], [ 3, "Tierras Inhóspitas"], + [ 139, "Tierras de la Peste del Este"], [ 28, "Tierras de la Peste del Oeste"],[ 47, "Tierras del Interior"], [3523, "Tormenta Abisal"], [2257, "Tranvía Subterráneo"], + [3792, "Tumbas de Maná"], [3537, "Tundra Boreal"], [1337, "Uldaman"], [4273, "Ulduar"], [3520, "Valle Sombraluna"], + [2597, "Valle de Alterac"], [ 331, "Vallefresno"], [ 33, "Vega de Tuercespina"], [ 722, "Zahúrda Rajacieno"], [3805, "Zul'Aman"], + [ 66, "Zul'Drak"], [1176, "Zul'Farrak"], [1977, "Zul'Gurub"] ], resistance: [ [6, "Arcano"], [2, "Fuego"], [3, "Naturaleza"], [4, "Escarcha"], [5, "Sombras"], diff --git a/static/js/locale_frfr.js b/static/js/locale_frfr.js index 1cf62a7c..bc23f10c 100644 --- a/static/js/locale_frfr.js +++ b/static/js/locale_frfr.js @@ -3631,41 +3631,41 @@ var LANG = { [889, "Voltigeurs Chanteguerre"], [589, "Éleveurs de sabres-d'hiver"] ], zone: [ - [4742, "Accostage de Hrothgar"], [4494, "Ahn'kahet : l'Ancien royaume"], [3428, "Ahn'Qiraj"], [4378, "Arène de Dalaran"], [3698, "Arène de Nagrand"], - [3702, "Arène des Tranchantes"], [4277, "Azjol-Nérub"], [16, "Azshara"], [3358, "Bassin d'Arathi"], [3711, "Bassin de Sholazar"], - [618, "Berceau-de-l'Hiver"], [10, "Bois de la Pénombre"], [3430, "Bois des Chants éternels"], [4603, "Caveau d'Archavon"], [3607, "Caverne du sanctuaire du Serpent"], - [718, "Cavernes des lamentations"], [3775, "Cercle de sang"], [1196, "Cime d'Utgarde"], [4812, "Citadelle de la Couronne de glace"], - [85, "Clairières de Tirisfal"], [267, "Contreforts de Hautebrande"], [2367, "Contreforts de Hautebrande d'antan"], [490, "Cratère d'Un'Goro"], - [3790, "Cryptes Auchenaï"], [2717, "Cœur du Magma"], [4395, "Dalaran"], [1657, "Darnassus"], [209, "Donjon d'Ombrecroc"], - [206, "Donjon d'Utgarde"], [4196, "Donjon de Drak'Tharon"], [3845, "Donjon de la Tempête"], [1, "Dun Morogh"], [14, "Durotar"], - [41, "Défilé de Deuillevent"], [405, "Désolace"], [65, "Désolation des dragons"], [495, "Fjord Hurlant"], [1537, "Forgefer"], - [12, "Forêt d'Elwynn"], [130, "Forêt des Pins argentés"], [3519, "Forêt de Terokkar"], [2817, "Forêt du Chant de cristal"], [4813, "Fosse de Saron"], - [1497, "Fossoyeuse"], [4024, "Frimarra"], [357, "Féralas"], [361, "Gangrebois"], [721, "Gnomeregan"], - [51, "Gorge des Vents brûlants"], [2437, "Gouffre de Ragefeu"], [3277, "Goulet des Chanteguerres"], [1941, "Grottes du temps"], [393, "Grève des Sombrelances"], - [4416, "Gundrak"], [2557, "Hache-tripes"], [2918, "Hall des Champions"], [2917, "Hall des Légendes"], [45, "Hautes-terres d'Arathi"], - [1519, "Hurlevent"], [3524, "Île de Brume-azur"], [3525, "Île de Brume-sang"], [4080, "Île de Quel'Danas"], [4710, "Île des Conquérants"], - [4197, "Joug-d'hiver"], [3457, "Karazhan"], [491, "Kraal de Tranchebauge"], [3848, "L'Arcatraz"], [4406, "L'Arène des valeureux"], - [3638, "L'arène des épreuves"], [3557, "L'Exodar"], [4228, "L'Oculus"], [4100, "L'Épuration de Stratholme"], [4723, "L'épreuve du champion"], - [4722, "L'épreuve du croisé"], [4500, "L'Œil de l'éternité"], [3820, "L'Œil du cyclone"], [3716, "La Basse-tourbière"], [3847, "La Botanica"], - [3789, "Labyrinthe des ombres"], [210, "La Couronne de glace"], [4809, "La Forge des âmes"], [3713, "La Fournaise du sang"], [457, "La Mer voilée"], - [717, "La Prison"], [3715, "Le Caveau de la vapeur"], [4415, "Le fort Pourpre"], [3849, "Le Méchanar"], [4265, "Le Nexus"], - [2366, "Le Noir Marécage"], [3836, "Le repaire de Magtheridon"], [4493, "Le sanctum Obsidien"], [44, "Les Carmines"], [3717, "Les enclos aux esclaves"], - [394, "Les Grisonnes"], [47, "Les Hinterlands"], [1581, "Les Mortemines"], [11, "Les Paluns"], [67, "Les pics Foudroyés"], - [1638, "Les Pitons du Tonnerre"], [3714, "Les Salles brisées"], [4272, "Les salles de Foudre"], [4264, "Les salles de Pierre"], [3791, "Les salles des Sethekk"], - [406, "Les Serres-Rocheuses"], [17, "Les Tarides"], [3433, "Les Terres fantômes"], [3522, "Les Tranchantes"], [1477, "Le temple d'Atal'Hakkar"], - [3840, "Le Temple noir"], [38, "Loch Modan"], [3487, "Lune-d'argent"], [4298, "L'enclave Écarlate"], [139, "Maleterres de l'est"], - [28, "Maleterres de l'ouest"], [8, "Marais des Chagrins"], [2100, "Maraudon"], [40, "Marche de l'Ouest"], [15, "Marécage d'Âprefange"], - [3521, "Marécage de Zangar"], [400, "Mille pointes"], [796, "Monastère écarlate"], [36, "Montagnes d'Alterac"], [25, "Mont Rochenoire"], - [215, "Mulgore"], [3518, "Nagrand"], [3456, "Naxxramas"], [1637, "Orgrimmar"], [331, "Orneval"], - [1583, "Pic Rochenoire"], [4075, "Plateau du Puits de soleil"], [719, "Profondeurs de Brassenoire"], [1584, "Profondeurs de Rochenoire"], [3483, "Péninsule des Flammes infernales"], - [3523, "Raz-de-Néant"], [493, "Reflet-de-Lune"], [3562, "Remparts des Flammes infernales"],[2159, "Repaire d'Onyxia"], [3923, "Repaire de Gruul"], - [2677, "Repaire de l'Aile noire"], [1216, "Repaire des Grumegueules"], [4384, "Rivage des Anciens"], [3429, "Ruines d'Ahn'Qiraj"], [3968, "Ruines de Lordaeron"], - [3905, "Réservoir de Glissecroc"], [4820, "Salles des Reflets"], [2057, "Scholomance"], [3703, "Shattrath"], [1377, "Silithus"], - [3679, "Skettis"], [148, "Sombrivage"], [3606, "Sommet d'Hyjal"], [722, "Souilles de Tranchebauge"], [46, "Steppes ardentes"], - [2017, "Stratholme"], [440, "Tanaris"], [141, "Teldrassil"], [3959, "Temple noir"], [4131, "Terrasse des Magistères"], - [4, "Terres foudroyées"], [3, "Terres ingrates"], [3792, "Tombes-mana"], [3537, "Toundra Boréenne"], [2257, "Tram des profondeurs"], - [1337, "Uldaman"], [4273, "Ulduar"], [2597, "Vallée d'Alterac"], [3520, "Vallée d'Ombrelune"], [33, "Vallée de Strangleronce"], - [3805, "Zul'Aman"], [66, "Zul'Drak"], [1176, "Zul'Farrak"], [1977, "Zul'Gurub"] + [4742, "Accostage de Hrothgar"], [3428, "Ahn'Qiraj"], [4494, "Ahn'kahet : l'Ancien royaume"], [4378, "Arène de Dalaran"], [3698, "Arène de Nagrand"], + [3702, "Arène des Tranchantes"], [4277, "Azjol-Nérub"], [ 16, "Azshara"], [3358, "Bassin d'Arathi"], [3711, "Bassin de Sholazar"], + [ 618, "Berceau-de-l'Hiver"], [ 10, "Bois de la Pénombre"], [3430, "Bois des Chants éternels"], [4603, "Caveau d'Archavon"], [3607, "Caverne du sanctuaire du Serpent"], + [ 718, "Cavernes des lamentations"], [3775, "Cercle de sang"], [1196, "Cime d'Utgarde"], [4812, "Citadelle de la Couronne de glace"], [ 85, "Clairières de Tirisfal"], + [ 267, "Contreforts de Hautebrande"], [2367, "Contreforts de Hautebrande d'antan"], [ 490, "Cratère d'Un'Goro"], [3790, "Cryptes Auchenaï"], [2717, "Cœur du Magma"], + [4395, "Dalaran"], [1657, "Darnassus"], [ 209, "Donjon d'Ombrecroc"], [ 206, "Donjon d'Utgarde"], [4196, "Donjon de Drak'Tharon"], + [3845, "Donjon de la Tempête"], [ 1, "Dun Morogh"], [ 14, "Durotar"], [ 41, "Défilé de Deuillevent"], [ 405, "Désolace"], + [ 65, "Désolation des dragons"], [ 495, "Fjord Hurlant"], [1537, "Forgefer"], [ 12, "Forêt d'Elwynn"], [3519, "Forêt de Terokkar"], + [ 130, "Forêt des Pins argentés"], [2817, "Forêt du Chant de cristal"], [4813, "Fosse de Saron"], [1497, "Fossoyeuse"], [4024, "Frimarra"], + [ 357, "Féralas"], [ 361, "Gangrebois"], [ 721, "Gnomeregan"], [ 51, "Gorge des Vents brûlants"], [2437, "Gouffre de Ragefeu"], + [3277, "Goulet des Chanteguerres"], [1941, "Grottes du temps"], [ 393, "Grève des Sombrelances"], [4416, "Gundrak"], [2557, "Hache-tripes"], + [2918, "Hall des Champions"], [2917, "Hall des Légendes"], [ 45, "Hautes-terres d'Arathi"], [1519, "Hurlevent"], [3524, "Île de Brume-azur"], + [3525, "Île de Brume-sang"], [4080, "Île de Quel'Danas"], [4710, "Île des Conquérants"], [4197, "Joug-d'hiver"], [3457, "Karazhan"], + [ 491, "Kraal de Tranchebauge"], [3848, "L'Arcatraz"], [4406, "L'Arène des valeureux"], [3557, "L'Exodar"], [4228, "L'Oculus"], + [3638, "L'arène des épreuves"], [4298, "L'enclave Écarlate"], [4100, "L'Épuration de Stratholme"], [4723, "L'épreuve du champion"], [4722, "L'épreuve du croisé"], + [4500, "L'Œil de l'éternité"], [3820, "L'Œil du cyclone"], [3716, "La Basse-tourbière"], [3847, "La Botanica"], [ 210, "La Couronne de glace"], + [4809, "La Forge des âmes"], [3713, "La Fournaise du sang"], [ 457, "La Mer voilée"], [ 717, "La Prison"], [3789, "Labyrinthe des ombres"], + [3715, "Le Caveau de la vapeur"], [3849, "Le Méchanar"], [4265, "Le Nexus"], [2366, "Le Noir Marécage"], [3840, "Le Temple noir"], + [4415, "Le fort Pourpre"], [3836, "Le repaire de Magtheridon"], [4493, "Le sanctum Obsidien"], [4987, "Le sanctum Rubis"], [1477, "Le temple d'Atal'Hakkar"], + [ 44, "Les Carmines"], [ 394, "Les Grisonnes"], [ 47, "Les Hinterlands"], [1581, "Les Mortemines"], [ 11, "Les Paluns"], + [1638, "Les Pitons du Tonnerre"], [3714, "Les Salles brisées"], [ 406, "Les Serres-Rocheuses"], [ 17, "Les Tarides"], [3433, "Les Terres fantômes"], + [3522, "Les Tranchantes"], [3717, "Les enclos aux esclaves"], [ 67, "Les pics Foudroyés"], [4272, "Les salles de Foudre"], [4264, "Les salles de Pierre"], + [3791, "Les salles des Sethekk"], [ 38, "Loch Modan"], [3487, "Lune-d'argent"], [ 139, "Maleterres de l'est"], [ 28, "Maleterres de l'ouest"], + [ 8, "Marais des Chagrins"], [2100, "Maraudon"], [ 40, "Marche de l'Ouest"], [ 15, "Marécage d'Âprefange"], [3521, "Marécage de Zangar"], + [ 400, "Mille pointes"], [ 796, "Monastère écarlate"], [ 25, "Mont Rochenoire"], [ 36, "Montagnes d'Alterac"], [ 215, "Mulgore"], + [3518, "Nagrand"], [3456, "Naxxramas"], [1637, "Orgrimmar"], [ 331, "Orneval"], [1583, "Pic Rochenoire"], + [4075, "Plateau du Puits de soleil"], [ 719, "Profondeurs de Brassenoire"], [1584, "Profondeurs de Rochenoire"], [3483, "Péninsule des Flammes infernales"], [3523, "Raz-de-Néant"], + [ 493, "Reflet-de-Lune"], [3562, "Remparts des Flammes infernales"], [2159, "Repaire d'Onyxia"], [3923, "Repaire de Gruul"], [2677, "Repaire de l'Aile noire"], + [1216, "Repaire des Grumegueules"], [4384, "Rivage des Anciens"], [3429, "Ruines d'Ahn'Qiraj"], [3968, "Ruines de Lordaeron"], [3905, "Réservoir de Glissecroc"], + [4820, "Salles des Reflets"], [2057, "Scholomance"], [3703, "Shattrath"], [1377, "Silithus"], [3679, "Skettis"], + [ 148, "Sombrivage"], [3606, "Sommet d'Hyjal"], [ 722, "Souilles de Tranchebauge"], [ 46, "Steppes ardentes"], [2017, "Stratholme"], + [ 440, "Tanaris"], [ 141, "Teldrassil"], [3959, "Temple noir"], [4131, "Terrasse des Magistères"], [ 4, "Terres foudroyées"], + [ 3, "Terres ingrates"], [3792, "Tombes-mana"], [3537, "Toundra Boréenne"], [2257, "Tram des profondeurs"], [1337, "Uldaman"], + [4273, "Ulduar"], [2597, "Vallée d'Alterac"], [3520, "Vallée d'Ombrelune"], [ 33, "Vallée de Strangleronce"], [3805, "Zul'Aman"], + [ 66, "Zul'Drak"], [1176, "Zul'Farrak"], [1977, "Zul'Gurub"] ], resistance: [ [6, "Arcane"], [2, "Feu"], [3, "Nature"], [4, "Givre"], [5, "Ombre"], diff --git a/static/js/locale_ruru.js b/static/js/locale_ruru.js index 7fb5aa8b..b5487d57 100644 --- a/static/js/locale_ruru.js +++ b/static/js/locale_ruru.js @@ -3631,41 +3631,41 @@ var LANG = { [1050, "Экспедиция Отважных"], [909, "Ярмарка Новолуния"] ], zone: [ - [4277, "Азжол-Неруб"], [16, "Азшара"], [2597, "Альтеракская долина"], [36, "Альтеракские горы"], [4415, "Аметистовая крепость"], - [3428, "Ан'Кираж"], [4494, "Ан'кахет: Старое Королевство"], [4378, "Арена Даларана"], [4406, "Арена Доблести"], [3698, "Арена Награнда"], - [3702, "Арена Острогорья"], [3848, "Аркатрац"], [3790, "Аукенайские гробницы"], [3562, "Бастионы Адского Пламени"], [4384, "Берег Древних"], - [3, "Бесплодные земли"], [11, "Болотина"], [8, "Болото Печали"], [3537, "Борейская тундра"], [3847, "Ботаника"], - [1196, "Вершина Утгард"], [3606, "Вершина Хиджала"], [47, "Внутренние земли"], [139, "Восточные Чумные земли"], [4, "Выжженные земли"], - [1584, "Глубины Черной горы"], [721, "Гномреган"], [3792, "Гробницы Маны"], [67, "Грозовая Гряда"], [1638, "Громовой Утес"], - [4416, "Гундрак"], [4395, "Даларан"], [1657, "Дарнас"], [3520, "Долина Призрачной Луны"], [65, "Драконий Погост"], - [1, "Дун Морог"], [14, "Дуротар"], [2557, "Забытый Город"], [2918, "Зал Защитника"], [2917, "Зал Легенд"], - [4820, "Залы Отражений"], [3521, "Зангартопь"], [28, "Западные Чумные земли"], [40, "Западный Край"], [618, "Зимние Ключи"], - [3607, "Змеиное святилище"], [3805, "Зул'Аман"], [1977, "Зул'Гуруб"], [66, "Зул'Драк"], [1176, "Зул'Фаррак"], - [4722, "Испытание крестоносца"], [4723, "Испытание чемпиона"], [3457, "Каражан"], [406, "Когтистые горы"], [44, "Красногорье"], - [490, "Кратер Ун'Горо"], [3845, "Крепость Бурь"], [4196, "Крепость Драк'Тарон"], [1216, "Крепость Древобрюхов"], [209, "Крепость Темного Клыка"], - [206, "Крепость Утгард"], [3638, "Круг Испытаний"], [3775, "Круг Крови"], [4809, "Кузня Душ"], [3713, "Кузня Крови"], - [722, "Курганы Иглошкурых"], [491, "Лабиринты Иглошкурых"], [4742, "Лагерь Хротгара"], [210, "Ледяная Корона"], [3519, "Лес Тероккар"], - [2817, "Лес Хрустальной Песни"], [3430, "Леса Вечной Песни"], [3923, "Логово Груула"], [2677, "Логово Крыла Тьмы"], [3836, "Логово Магтеридона"], - [2159, "Логово Ониксии"], [38, "Лок Модан"], [493, "Лунная поляна"], [3487, "Луносвет"], [2100, "Мародон"], - [1581, "Мертвые копи"], [3849, "Механар"], [796, "Монастырь Алого ордена"], [215, "Мулгор"], [45, "Нагорье Арати"], - [3518, "Награнд"], [3456, "Наксрамас"], [2057, "Некроситет"], [4265, "Нексус"], [719, "Непроглядная Пучина"], - [3716, "Нижетопь"], [3358, "Низина Арати"], [3711, "Низина Шолазар"], [4493, "Обсидиановое святилище"], [2437, "Огненная пропасть"], - [2717, "Огненные Недра"], [4197, "Озеро Ледяных Оков"], [3820, "Око Бури"], [4500, "Око Вечности"], [4228, "Окулус"], - [1637, "Оргриммар"], [361, "Оскверненный лес"], [4710, "Остров Завоеваний"], [4080, "Остров Кель'Данас"], [3525, "Остров Кровавой Дымки"], - [3524, "Остров Лазурной Дымки"], [3522, "Острогорье"], [4100, "Очищение Стратхольма"], [3715, "Паровое подземелье"], [41, "Перевал Мертвого Ветра"], - [1941, "Пещеры Времени"], [718, "Пещеры Стенаний"], [1583, "Пик Черной горы"], [4075, "Плато Солнечного Колодца"], [393, "Побережье Черного Копья"], - [1497, "Подгород"], [2257, "Подземный поезд"], [3483, "Полуостров Адского Пламени"], [267, "Предгорья Хилсбрада"], [3433, "Призрачные земли"], - [3523, "Пустоверть"], [405, "Пустоши"], [46, "Пылающие степи"], [15, "Пылевые топи"], [3714, "Разрушенные залы"], - [495, "Ревущий фьорд"], [3905, "Резервуар Кривого Клыка"], [3429, "Руины Ан'Киража"], [3968, "Руины Лордерона"], [394, "Седые холмы"], - [130, "Серебряный бор"], [3791, "Сетеккские залы"], [1377, "Силитус"], [3679, "Скеттис"], [4603, "Склеп Аркавона"], - [457, "Сокрытое море"], [1537, "Стальгорн"], [2367, "Старые предгорья Хилсбрада"], [17, "Степи"], [2017, "Стратхольм"], - [10, "Сумеречный лес"], [440, "Танарис"], [141, "Тельдрассил"], [148, "Темные берега"], [3789, "Темный лабиринт"], - [33, "Тернистая долина"], [4131, "Терраса Магистров"], [85, "Тирисфальские леса"], [51, "Тлеющее ущелье"], [400, "Тысяча Игл"], - [717, "Тюрьма"], [3717, "Узилище"], [1337, "Ульдаман"], [4273, "Ульдуар"], [3277, "Ущелье Песни Войны"], - [357, "Фералас"], [4024, "Хладарра"], [1477, "Храм Атал'Хаккара"], [4812, "Цитадель Ледяной Короны"], [25, "Черная гора"], - [2366, "Черные топи"], [3840, "Черный храм"], [3959, "Черный храм"], [4264, "Чертоги Камня"], [4272, "Чертоги Молний"], - [4298, "Анклав Алого ордена"], [3703, "Шаттрат"], [1519, "Штормград"], [3557, "Экзодар"], [12, "Элвиннский лес"], - [4813, "Яма Сарона"], [331, "Ясеневый лес"] + [4277, "Азжол-Неруб"], [ 16, "Азшара"], [2597, "Альтеракская долина"], [ 36, "Альтеракские горы"], [4415, "Аметистовая крепость"], + [3428, "Ан'Кираж"], [4494, "Ан'кахет: Старое Королевство"], [4298, "Анклав Алого ордена"], [4378, "Арена Даларана"], [4406, "Арена Доблести"], + [3698, "Арена Награнда"], [3702, "Арена Острогорья"], [3848, "Аркатрац"], [3790, "Аукенайские гробницы"], [3562, "Бастионы Адского Пламени"], + [4384, "Берег Древних"], [ 3, "Бесплодные земли"], [ 11, "Болотина"], [ 8, "Болото Печали"], [3537, "Борейская тундра"], + [3847, "Ботаника"], [1196, "Вершина Утгард"], [3606, "Вершина Хиджала"], [ 47, "Внутренние земли"], [ 139, "Восточные Чумные земли"], + [ 4, "Выжженные земли"], [1584, "Глубины Черной горы"], [ 721, "Гномреган"], [3792, "Гробницы Маны"], [ 67, "Грозовая Гряда"], + [1638, "Громовой Утес"], [4416, "Гундрак"], [4395, "Даларан"], [1657, "Дарнас"], [3520, "Долина Призрачной Луны"], + [ 65, "Драконий Погост"], [ 1, "Дун Морог"], [ 14, "Дуротар"], [2557, "Забытый Город"], [2918, "Зал Защитника"], + [2917, "Зал Легенд"], [4820, "Залы Отражений"], [3521, "Зангартопь"], [ 28, "Западные Чумные земли"], [ 40, "Западный Край"], + [ 618, "Зимние Ключи"], [3607, "Змеиное святилище"], [3805, "Зул'Аман"], [1977, "Зул'Гуруб"], [ 66, "Зул'Драк"], + [1176, "Зул'Фаррак"], [4722, "Испытание крестоносца"], [4723, "Испытание чемпиона"], [3457, "Каражан"], [ 406, "Когтистые горы"], + [ 44, "Красногорье"], [ 490, "Кратер Ун'Горо"], [3845, "Крепость Бурь"], [4196, "Крепость Драк'Тарон"], [1216, "Крепость Древобрюхов"], + [ 209, "Крепость Темного Клыка"], [ 206, "Крепость Утгард"], [3638, "Круг Испытаний"], [3775, "Круг Крови"], [4809, "Кузня Душ"], + [3713, "Кузня Крови"], [ 722, "Курганы Иглошкурых"], [ 491, "Лабиринты Иглошкурых"], [4742, "Лагерь Хротгара"], [ 210, "Ледяная Корона"], + [3519, "Лес Тероккар"], [2817, "Лес Хрустальной Песни"], [3430, "Леса Вечной Песни"], [3923, "Логово Груула"], [2677, "Логово Крыла Тьмы"], + [3836, "Логово Магтеридона"], [2159, "Логово Ониксии"], [ 38, "Лок Модан"], [ 493, "Лунная поляна"], [3487, "Луносвет"], + [2100, "Мародон"], [1581, "Мертвые копи"], [3849, "Механар"], [ 796, "Монастырь Алого ордена"], [ 215, "Мулгор"], + [ 45, "Нагорье Арати"], [3518, "Награнд"], [3456, "Наксрамас"], [2057, "Некроситет"], [4265, "Нексус"], + [ 719, "Непроглядная Пучина"], [3716, "Нижетопь"], [3358, "Низина Арати"], [3711, "Низина Шолазар"], [4493, "Обсидиановое святилище"], + [2437, "Огненная пропасть"], [2717, "Огненные Недра"], [4197, "Озеро Ледяных Оков"], [3820, "Око Бури"], [4500, "Око Вечности"], + [4228, "Окулус"], [1637, "Оргриммар"], [ 361, "Оскверненный лес"], [4710, "Остров Завоеваний"], [4080, "Остров Кель'Данас"], + [3525, "Остров Кровавой Дымки"], [3524, "Остров Лазурной Дымки"], [3522, "Острогорье"], [4100, "Очищение Стратхольма"], [3715, "Паровое подземелье"], + [ 41, "Перевал Мертвого Ветра"], [1941, "Пещеры Времени"], [ 718, "Пещеры Стенаний"], [1583, "Пик Черной горы"], [4075, "Плато Солнечного Колодца"], + [ 393, "Побережье Черного Копья"], [1497, "Подгород"], [2257, "Подземный поезд"], [3483, "Полуостров Адского Пламени"], [ 267, "Предгорья Хилсбрада"], + [3433, "Призрачные земли"], [3523, "Пустоверть"], [ 405, "Пустоши"], [ 46, "Пылающие степи"], [ 15, "Пылевые топи"], + [3714, "Разрушенные залы"], [ 495, "Ревущий фьорд"], [3905, "Резервуар Кривого Клыка"], [4987, "Рубиновое святилище"], [3429, "Руины Ан'Киража"], + [3968, "Руины Лордерона"], [ 394, "Седые холмы"], [ 130, "Серебряный бор"], [3791, "Сетеккские залы"], [1377, "Силитус"], + [3679, "Скеттис"], [4603, "Склеп Аркавона"], [ 457, "Сокрытое море"], [1537, "Стальгорн"], [2367, "Старые предгорья Хилсбрада"], + [ 17, "Степи"], [2017, "Стратхольм"], [ 10, "Сумеречный лес"], [ 440, "Танарис"], [ 141, "Тельдрассил"], + [ 148, "Темные берега"], [3789, "Темный лабиринт"], [ 33, "Тернистая долина"], [4131, "Терраса Магистров"], [ 85, "Тирисфальские леса"], + [ 51, "Тлеющее ущелье"], [ 400, "Тысяча Игл"], [ 717, "Тюрьма"], [3717, "Узилище"], [1337, "Ульдаман"], + [4273, "Ульдуар"], [3277, "Ущелье Песни Войны"], [ 357, "Фералас"], [4024, "Хладарра"], [1477, "Храм Атал'Хаккара"], + [4812, "Цитадель Ледяной Короны"], [ 25, "Черная гора"], [2366, "Черные топи"], [3840, "Черный храм"], [3959, "Черный храм"], + [4264, "Чертоги Камня"], [4272, "Чертоги Молний"], [3703, "Шаттрат"], [1519, "Штормград"], [3557, "Экзодар"], + [ 12, "Элвиннский лес"], [4813, "Яма Сарона"], [ 331, "Ясеневый лес"] ], resistance: [ [6, "Тайная магия"], [2, "Огонь"], [3, "природа"], [4, "Лед"], [5, "Тьма"], diff --git a/static/js/locale_zhcn.js b/static/js/locale_zhcn.js index 1aee50eb..a0b0c801 100644 --- a/static/js/locale_zhcn.js +++ b/static/js/locale_zhcn.js @@ -3679,41 +3679,41 @@ var LANG = { [1126, "霜脉矮人"], [1156, "灰烬审判军"] ], zone: [ - [1, "丹莫罗"], [3, "荒芜之地"], [4, "诅咒之地"], [8, "悲伤沼泽"], [10, "暮色森林"], - [11, "湿地"], [12, "艾尔文森林"], [14, "杜隆塔尔"], [15, "尘泥沼泽"], [16, "艾萨拉"], - [17, "贫瘠之地"], [25, "黑石山"], [28, "西瘟疫之地"], [33, "荆棘谷"], [36, "奥特兰克山脉"], - [38, "洛克莫丹"], [40, "西部荒野"], [41, "逆风小径"], [44, "赤脊山"], [45, "阿拉希高地"], - [46, "燃烧平原"], [47, "辛特兰"], [51, "灼热峡谷"], [65, "龙骨荒野"], [66, "祖达克"], - [67, "风暴峭壁"], [85, "提瑞斯法林地"], [130, "银松森林"], [139, "东瘟疫之地"], [141, "泰达希尔"], - [148, "黑海岸"], [206, "乌特加德城堡"], [209, "影牙城堡"], [210, "冰冠冰川"], [215, "莫高雷"], - [267, "希尔斯布莱德丘陵"], [331, "灰谷"], [357, "菲拉斯"], [361, "费伍德森林"], [393, "暗矛海滩"], - [394, "灰熊丘陵"], [400, "千针石林"], [405, "凄凉之地"], [406, "石爪山脉"], [440, "塔纳利斯"], - [457, "迷雾之海"], [490, "安戈洛环形山"], [491, "剃刀沼泽"], [493, "月光林地"], [495, "嚎风峡湾"], - [618, "冬泉谷"], [717, "监狱"], [718, "哀嚎洞穴"], [719, "黑暗深渊"], [721, "诺莫瑞根"], - [722, "剃刀高地"], [796, "血色修道院"], [1176, "祖尔法拉克"], [1196, "乌特加德之巅"], [1216, "木喉要塞"], - [1337, "奥达曼"], [1377, "希利苏斯"], [1477, "阿塔哈卡神庙"], [1497, "幽暗城"], [1519, "暴风城"], - [1537, "铁炉堡"], [1581, "死亡矿井"], [1583, "黑石塔"], [1584, "黑石深渊"], [1637, "奥格瑞玛"], - [1638, "雷霆崖"], [1657, "达纳苏斯"], [1941, "时光之穴"], [1977, "祖尔格拉布"], [2017, "斯坦索姆"], - [2057, "通灵学院"], [2100, "玛拉顿"], [2159, "奥妮克希亚的巢穴"], [2257, "矿道地铁"], [2366, "黑色沼泽"], - [2367, "旧希尔斯布莱德丘陵"], [2437, "怒焰裂谷"], [2557, "厄运之槌"], [2597, "奥特兰克山谷"], [2677, "黑翼之巢"], - [2717, "熔火之心"], [2817, "晶歌森林"], [2917, "传说大厅"], [2918, "勇士大厅"], [3277, "战歌峡谷"], - [3358, "阿拉希盆地"], [3428, "安其拉"], [3429, "安其拉废墟"], [3430, "永歌森林"], [3433, "幽魂之地"], - [3456, "纳克萨玛斯"], [3457, "卡拉赞"], [3483, "地狱火半岛"], [3487, "银月城"], [3518, "纳格兰"], - [3519, "泰罗卡森林"], [3520, "影月谷"], [3521, "赞加沼泽"], [3522, "刀锋山"], [3523, "虚空风暴"], - [3524, "秘蓝岛"], [3525, "秘血岛"], [3537, "北风苔原"], [3557, "埃索达"], [3562, "地狱火城墙"], - [3606, "海加尔峰"], [3607, "毒蛇神殿"], [3679, "斯克提斯"], [3698, "纳格兰竞技场"], [3702, "刀锋山竞技场"], - [3703, "沙塔斯城"], [3711, "索拉查盆地"], [3713, "鲜血熔炉"], [3714, "破碎大厅"], [3715, "蒸汽地窟"], - [3716, "幽暗沼泽"], [3717, "奴隶围栏"], [3775, "鲜血之环"], [3789, "暗影迷宫"], [3790, "奥金尼地穴"], - [3791, "塞泰克大厅"], [3792, "法力陵墓"], [3805, "祖阿曼"], [3820, "风暴之眼"], [3836, "玛瑟里顿的巢穴"], - [3840, "黑暗神殿"], [3845, "风暴要塞"], [3847, "生态船"], [3848, "禁魔监狱"], [3849, "能源舰"], - [3905, "盘牙水库"], [3923, "格鲁尔的巢穴"], [3959, "黑暗神殿"], [3968, "洛丹伦废墟"], [4024, "考达拉"], - [4075, "太阳之井高地"], [4080, "奎尔丹纳斯岛"], [4100, "净化斯坦索姆"], [4131, "魔导师平台"], [4196, "达克萨隆要塞"], - [4197, "冬拥湖"], [4228, "魔环"], [4264, "岩石大厅"], [4265, "魔枢"], [4272, "闪电大厅"], - [4273, "奥杜尔"], [4277, "艾卓-尼鲁布"], [4298, "血色领地"], [4378, "达拉然竞技场"], [4384, "远古海滩"], - [4395, "达拉然"], [4406, "勇气竞技场"], [4415, "紫罗兰监狱"], [4416, "古达克"], [4493, "黑曜石圣殿"], - [4494, "安卡赫特:古代王国"], [4500, "永恒之眼"], [4603, "阿尔卡冯的宝库"], [4710, "征服之岛"], [4722, "十字军的试炼"], - [4723, "冠军的试炼"], [4742, "洛斯加尔登陆点"], [4809, "灵魂洪炉"], [4812, "冰冠堡垒"], [4813, "萨隆矿坑"], - [4820, "映像大厅"] + [ 139, "东瘟疫之地"], [ 1, "丹莫罗"], [1196, "乌特加德之巅"], [ 206, "乌特加德城堡"], [2917, "传说大厅"], + [4723, "冠军的试炼"], [4197, "冬拥湖"], [ 618, "冬泉谷"], [ 210, "冰冠冰川"], [4812, "冰冠堡垒"], + [4100, "净化斯坦索姆"], [ 405, "凄凉之地"], [3522, "刀锋山"], [3702, "刀锋山竞技场"], [ 491, "剃刀沼泽"], + [ 722, "剃刀高地"], [2918, "勇士大厅"], [4406, "勇气竞技场"], [3537, "北风苔原"], [4722, "十字军的试炼"], + [ 400, "千针石林"], [3457, "卡拉赞"], [2557, "厄运之槌"], [4416, "古达克"], [ 718, "哀嚎洞穴"], + [ 495, "嚎风峡湾"], [3483, "地狱火半岛"], [3562, "地狱火城墙"], [3557, "埃索达"], [ 440, "塔纳利斯"], + [3791, "塞泰克大厅"], [4075, "太阳之井高地"], [4080, "奎尔丹纳斯岛"], [2159, "奥妮克希亚的巢穴"], [4273, "奥杜尔"], + [1637, "奥格瑞玛"], [ 36, "奥特兰克山脉"], [2597, "奥特兰克山谷"], [1337, "奥达曼"], [3790, "奥金尼地穴"], + [3717, "奴隶围栏"], [3428, "安其拉"], [3429, "安其拉废墟"], [4494, "安卡赫特:古代王国"], [ 490, "安戈洛环形山"], + [ 15, "尘泥沼泽"], [4264, "岩石大厅"], [1377, "希利苏斯"], [ 267, "希尔斯布莱德丘陵"], [1497, "幽暗城"], + [3716, "幽暗沼泽"], [3433, "幽魂之地"], [3520, "影月谷"], [ 209, "影牙城堡"], [4710, "征服之岛"], + [2437, "怒焰裂谷"], [ 8, "悲伤沼泽"], [3277, "战歌峡谷"], [ 85, "提瑞斯法林地"], [3679, "斯克提斯"], + [2017, "斯坦索姆"], [2367, "旧希尔斯布莱德丘陵"], [1941, "时光之穴"], [4820, "映像大厅"], [2817, "晶歌森林"], + [3789, "暗影迷宫"], [ 393, "暗矛海滩"], [ 10, "暮色森林"], [1519, "暴风城"], [ 493, "月光林地"], + [1216, "木喉要塞"], [ 14, "杜隆塔尔"], [3923, "格鲁尔的巢穴"], [1581, "死亡矿井"], [3607, "毒蛇神殿"], + [4500, "永恒之眼"], [3430, "永歌森林"], [3703, "沙塔斯城"], [3792, "法力陵墓"], [3519, "泰罗卡森林"], + [ 141, "泰达希尔"], [3968, "洛丹伦废墟"], [ 38, "洛克莫丹"], [4742, "洛斯加尔登陆点"], [3606, "海加尔峰"], + [ 11, "湿地"], [ 394, "灰熊丘陵"], [ 331, "灰谷"], [4809, "灵魂洪炉"], [ 51, "灼热峡谷"], + [2717, "熔火之心"], [ 46, "燃烧平原"], [2100, "玛拉顿"], [3836, "玛瑟里顿的巢穴"], [3847, "生态船"], + [ 717, "监狱"], [3905, "盘牙水库"], [ 406, "石爪山脉"], [2257, "矿道地铁"], [3714, "破碎大厅"], + [1977, "祖尔格拉布"], [1176, "祖尔法拉克"], [ 66, "祖达克"], [3805, "祖阿曼"], [3848, "禁魔监狱"], + [3524, "秘蓝岛"], [3525, "秘血岛"], [3711, "索拉查盆地"], [4415, "紫罗兰监狱"], [4987, "红玉圣殿"], + [3456, "纳克萨玛斯"], [3518, "纳格兰"], [3698, "纳格兰竞技场"], [4024, "考达拉"], [3849, "能源舰"], + [4277, "艾卓-尼鲁布"], [ 12, "艾尔文森林"], [ 16, "艾萨拉"], [ 33, "荆棘谷"], [ 3, "荒芜之地"], + [ 215, "莫高雷"], [ 357, "菲拉斯"], [4813, "萨隆矿坑"], [3715, "蒸汽地窟"], [3523, "虚空风暴"], + [ 796, "血色修道院"], [4298, "血色领地"], [ 28, "西瘟疫之地"], [ 40, "西部荒野"], [ 4, "诅咒之地"], + [ 721, "诺莫瑞根"], [ 17, "贫瘠之地"], [ 361, "费伍德森林"], [3521, "赞加沼泽"], [ 44, "赤脊山"], + [ 47, "辛特兰"], [4196, "达克萨隆要塞"], [4395, "达拉然"], [4378, "达拉然竞技场"], [1657, "达纳苏斯"], + [4384, "远古海滩"], [ 457, "迷雾之海"], [ 41, "逆风小径"], [2057, "通灵学院"], [1537, "铁炉堡"], + [3487, "银月城"], [ 130, "银松森林"], [4272, "闪电大厅"], [1477, "阿塔哈卡神庙"], [4603, "阿尔卡冯的宝库"], + [3358, "阿拉希盆地"], [ 45, "阿拉希高地"], [1638, "雷霆崖"], [3820, "风暴之眼"], [ 67, "风暴峭壁"], + [3845, "风暴要塞"], [4131, "魔导师平台"], [4265, "魔枢"], [4228, "魔环"], [3775, "鲜血之环"], + [3713, "鲜血熔炉"], [ 719, "黑暗深渊"], [3840, "黑暗神殿"], [3959, "黑暗神殿"], [4493, "黑曜石圣殿"], + [ 148, "黑海岸"], [1583, "黑石塔"], [ 25, "黑石山"], [1584, "黑石深渊"], [2677, "黑翼之巢"], + [2366, "黑色沼泽"], [ 65, "龙骨荒野"] ], resistance: [ [6, "奥术"], [2, "火焰"], [3, "自然"], [4, "冰霜"], [5, "暗影"], From b832fc172c3254545cee3a50d138a816e58c8916 Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Fri, 10 Oct 2025 19:50:35 +0200 Subject: [PATCH 034/260] Items/Gearscore * fix warning in GS calculation --- includes/dbtypes/item.class.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/dbtypes/item.class.php b/includes/dbtypes/item.class.php index 595c468a..881bd40f 100644 --- a/includes/dbtypes/item.class.php +++ b/includes/dbtypes/item.class.php @@ -1664,7 +1664,7 @@ class ItemList extends DBTypeList $mh = $j; else if ($j['id'] == $ohItem) $oh = $j; - else if ($j['gearscore']) + else if (!empty($j['gearscore'])) { if ($j['slot'] == INVTYPE_RELIC) $score += 20; From a96f6c4cdfedb98bbc4b2e8eb9789f84e6f9200b Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Fri, 10 Oct 2025 20:21:41 +0200 Subject: [PATCH 035/260] PageTemplate/Fixup * really fix merging jsGlobals from comments/etc. into existing PageTemplate --- includes/components/response/templateresponse.class.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/components/response/templateresponse.class.php b/includes/components/response/templateresponse.class.php index d5050ad0..7ceffdcf 100644 --- a/includes/components/response/templateresponse.class.php +++ b/includes/components/response/templateresponse.class.php @@ -626,7 +626,7 @@ class TemplateResponse extends BaseResponse foreach ($this->jsGlobals as $type => [, $data, ]) { if (!isset($ptJSG[$type]) || $type == Type::USER) - $ptJSGs[$type] = $this->jsGlobals[$type]; + $ptJSG[$type] = $this->jsGlobals[$type]; else Util::mergeJsGlobals($ptJSG[$type][1], $data); } From 63053757c94caf15a01d9692813bd0ea0c19806f Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Fri, 10 Oct 2025 20:03:46 +0200 Subject: [PATCH 036/260] Guides/Fixup * fix showing wrong guide version to staff * fix sticky icon offset --- endpoints/guide/guide.php | 12 ++++++------ .../components/response/templateresponse.class.php | 2 +- static/css/global.css | 2 ++ 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/endpoints/guide/guide.php b/endpoints/guide/guide.php index 9bf5836a..215bdb53 100644 --- a/endpoints/guide/guide.php +++ b/endpoints/guide/guide.php @@ -68,12 +68,12 @@ class GuideBaseResponse extends TemplateResponse implements ICache $this->contribute = CONTRIBUTE_NONE; } - if ($this->articleUrl) + // owner or staff and manual rev passed + if ($this->subject->userCanView() && $this->_get['rev']) + $this->guideRevision = $this->_get['rev']; + // has publicly viewable version + else if ($this->subject->canBeViewed()) $this->guideRevision = $this->subject->getField('rev'); - else if ($this->subject->userCanView()) - $this->guideRevision = $this->_get['rev'] ?? $this->subject->getField('latest'); - else - $this->subject->getField('rev'); $this->h1 = $this->subject->getField('name'); @@ -127,7 +127,7 @@ class GuideBaseResponse extends TemplateResponse implements ICache $this->lvTabs = new Tabs(['parent' => "\$\$WH.ge('tabs-generic')"], __forceTabs: true); - // the article text itself is added by PageTemplate::addArticle() + // the article text itself is added by TemplateResponse::addArticle() parent::generate(); $this->result->registerDisplayHook('infobox', [self::class, 'infoboxHook']); diff --git a/includes/components/response/templateresponse.class.php b/includes/components/response/templateresponse.class.php index 7ceffdcf..4b165c82 100644 --- a/includes/components/response/templateresponse.class.php +++ b/includes/components/response/templateresponse.class.php @@ -418,7 +418,7 @@ class TemplateResponse extends BaseResponse if (isset($this->guideRevision)) $article = DB::Aowow()->selectRow('SELECT `article`, `locale`, `editAccess` FROM ?_articles WHERE `type` = ?d AND `typeId` = ?d AND `rev` = ?d', Type::GUIDE, $this->typeId, $this->guideRevision); - if (!$article && $this->gPageInfo['articleUrl']) + if (!$article && !empty($this->gPageInfo['articleUrl'])) $article = DB::Aowow()->selectRow('SELECT `article`, `locale`, `editAccess` FROM ?_articles WHERE `url` = ? AND `locale` IN (?a) ORDER BY `locale` DESC, `rev` DESC LIMIT 1', $this->gPageInfo['articleUrl'], [Lang::getLocale()->value, Locale::EN->value]); if (!$article && !empty($this->type) && isset($this->typeId)) diff --git a/static/css/global.css b/static/css/global.css index b80fe0f7..231c7068 100644 --- a/static/css/global.css +++ b/static/css/global.css @@ -422,10 +422,12 @@ padding-left: 21px; } +/* aowow - clips in the list marker .infobox .guide-sticky { position: relative; left: -6px; } +*/ #guiderating { display: block; From 465e019eaa0cbf1ec1b41b2b7c40f0240be97537 Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Fri, 10 Oct 2025 22:28:20 +0200 Subject: [PATCH 037/260] FactionDetailPage/Tabs * only offer 'filter result' prompt on tabs for factions that are filtrable --- endpoints/faction/faction.php | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/endpoints/faction/faction.php b/endpoints/faction/faction.php index 934a191d..84ba340f 100644 --- a/endpoints/faction/faction.php +++ b/endpoints/faction/faction.php @@ -228,7 +228,8 @@ class FactionBaseResponse extends TemplateResponse implements ICache ); if ($items->getMatches() > Cfg::get('SQL_LIMIT_DEFAULT')) - $tabData['note'] = sprintf(Util::$filterResultString, '?items&filter=cr=17;crs='.$this->typeId.';crv=0'); + if (!is_null(ItemListFilter::getCriteriaIndex(17, $this->typeId))) + $tabData['note'] = sprintf(Util::$filterResultString, '?items&filter=cr=17;crs='.$this->typeId.';crv=0'); $this->lvTabs->addListviewTab(new Listview($tabData, ItemList::$brickFile, 'itemStandingCol')); } @@ -262,7 +263,8 @@ class FactionBaseResponse extends TemplateResponse implements ICache ); if ($killCreatures->getMatches() > Cfg::get('SQL_LIMIT_DEFAULT')) - $tabData['note'] = sprintf(Util::$filterResultString, '?npcs&filter=cr=42;crs='.$this->typeId.';crv=0'); + if (!is_null(CreatureListFilter::getCriteriaIndex(42, $this->typeId))) + $tabData['note'] = sprintf(Util::$filterResultString, '?npcs&filter=cr=42;crs='.$this->typeId.';crv=0'); $this->addDataLoader('zones'); $this->lvTabs->addListviewTab(new Listview($tabData, CreatureList::$brickFile, 'npcRepCol')); @@ -283,7 +285,8 @@ class FactionBaseResponse extends TemplateResponse implements ICache ); if ($members->getMatches() > Cfg::get('SQL_LIMIT_DEFAULT')) - $tabData['note'] = sprintf(Util::$filterResultString, '?npcs&filter=cr=3;crs='.$this->typeId.';crv=0'); + if (!is_null(CreatureListFilter::getCriteriaIndex(3, $this->typeId))) + $tabData['note'] = sprintf(Util::$filterResultString, '?npcs&filter=cr=3;crs='.$this->typeId.';crv=0'); $this->addDataLoader('zones'); $this->lvTabs->addListviewTab(new Listview($tabData, CreatureList::$brickFile)); @@ -321,7 +324,8 @@ class FactionBaseResponse extends TemplateResponse implements ICache ); if ($quests->getMatches() > Cfg::get('SQL_LIMIT_DEFAULT')) - $tabData['note'] = sprintf(Util::$filterResultString, '?quests&filter=cr=1;crs='.$this->typeId.';crv=0'); + if (!is_null(QuestListFilter::getCriteriaIndex(1, $this->typeId))) + $tabData['note'] = sprintf(Util::$filterResultString, '?quests&filter=cr=1;crs='.$this->typeId.';crv=0'); $this->lvTabs->addListviewTab(new Listview($tabData, QuestList::$brickFile, 'questRepCol')); } From 77f2a0c21d5e8f281f50422e977e702ecfa1dbc0 Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Fri, 10 Oct 2025 22:23:33 +0200 Subject: [PATCH 038/260] Profiler/Resync * fix logging ids on resync failure --- endpoints/profile/status.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/endpoints/profile/status.php b/endpoints/profile/status.php index 15d0d781..dd5e9b44 100644 --- a/endpoints/profile/status.php +++ b/endpoints/profile/status.php @@ -39,7 +39,7 @@ class ProfileStatusResponse extends TextResponse if (!$ids) { - trigger_error('ProfileStatusResponse - no profileIds to resync'.($this->_get['guild'] ? ' for guild #'.$this->_get['guild'] : ($this->_get['arena-team'] ? ' for areana team #'.$this->_get['arena-team'] : '')), E_USER_ERROR); + trigger_error('ProfileStatusResponse - no profileIds to resync'.($this->_get['guild'] ? ' for guild #' : ($this->_get['arena-team'] ? ' for areana team #' : ' #')).Util::toString($this->_get['id']), E_USER_WARNING); $this->result = Util::toJSON([1, [PR_QUEUE_STATUS_ERROR, 0, 0, PR_QUEUE_ERROR_CHAR]]); } From 40e98081c912761de6283d969c4682ee3118d792 Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Sat, 11 Oct 2025 19:52:56 +0200 Subject: [PATCH 039/260] LatestComments/RSS * fix url format for replies --- endpoints/latest-comments/latest-comments_rss.php | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/endpoints/latest-comments/latest-comments_rss.php b/endpoints/latest-comments/latest-comments_rss.php index d3611151..22389005 100644 --- a/endpoints/latest-comments/latest-comments_rss.php +++ b/endpoints/latest-comments/latest-comments_rss.php @@ -14,15 +14,20 @@ class LatestcommentsRssResponse extends TextResponse protected function generate() : void { - foreach (CommunityContent::getCommentPreviews(dateFmt: false) as $comment) + foreach (CommunityContent::getCommentPreviews(['comments' => 1, 'replies' => 1], dateFmt: false) as $comment) { + if (empty($comment['commentid'])) + $url = Cfg::get('HOST_URL').'/?go-to-comment&id='.$comment['id']; + else + $url = Cfg::get('HOST_URL').'/?go-to-reply&id='.$comment['id']; + // todo (low): preview should be html-formated $this->feedData[] = array( 'title' => [true, [], Lang::typeName($comment['type']).Lang::main('colon').htmlentities($comment['subject'])], - 'link' => [false, [], Cfg::get('HOST_URL').'/?go-to-comment&id='.$comment['id']], + 'link' => [false, [], $url], 'description' => [true, [], htmlentities($comment['preview'])."

".Lang::main('byUser', [$comment['user'], '']) . Util::formatTimeDiff($comment['date'])], 'pubDate' => [false, [], date(DATE_RSS, $comment['date'])], - 'guid' => [false, [], Cfg::get('HOST_URL').'/?go-to-comment&id='.$comment['id']] + 'guid' => [false, [], $url] // 'domain' => [false, [], null] ); } From 9b0aa5c885c10e9b66be5601cc184ba8f82a7204 Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Sat, 11 Oct 2025 22:25:01 +0200 Subject: [PATCH 040/260] Achievements/Fixup * fix fetching achievements from child catgs if selected catg is empty --- endpoints/achievements/achievements.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/endpoints/achievements/achievements.php b/endpoints/achievements/achievements.php index 98c2676a..fc82692c 100644 --- a/endpoints/achievements/achievements.php +++ b/endpoints/achievements/achievements.php @@ -119,7 +119,7 @@ class AchievementsBaseResponse extends TemplateResponse implements ICache $conditions = []; if ($fiCnd) $conditions[] = $fiCnd; - if ($catList = DB::Aowow()->SelectCol('SELECT `id` FROM ?_achievementcategory WHERE `parentCat` IN (?a) OR `parentCat2` IN (?a) ', end($this->category), end($this->category))) + if ($catList = DB::Aowow()->SelectCol('SELECT `id` FROM ?_achievementcategory WHERE `parentCat` IN (?a) OR `parentCat2` IN (?a) ', $this->category, $this->category)) $conditions[] = ['category', $catList]; $acvList = new AchievementList($conditions, ['calcTotal' => true]); From 494061de82442d4c95c35b424301be69ab1dfe76 Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Sat, 11 Oct 2025 22:33:50 +0200 Subject: [PATCH 041/260] Cache/Fixup * correct miscData offset for tooltips introduced in 3edac3c77a10990465713629d4372e0624cb1800 * fix generating cache key for item upgrade searches --- includes/components/response/baseresponse.class.php | 8 +++++++- includes/components/response/textresponse.class.php | 2 +- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/includes/components/response/baseresponse.class.php b/includes/components/response/baseresponse.class.php index 137741d8..37761981 100644 --- a/includes/components/response/baseresponse.class.php +++ b/includes/components/response/baseresponse.class.php @@ -323,11 +323,17 @@ trait TrSearch public function getCacheKeyComponents() : array { + $misc = $this->query . // can be empty for upgrade search + serialize($this->_get['wt'] ?? null) . // extra &_GET not expected for normal and opensearch + serialize($this->_get['wtv'] ?? null) . + serialize($this->_get['type'] ?? null) . + serialize($this->_get['slots'] ?? null); + return array( -1, // DBType $this->searchMask, // DBTypeId/category User::$groups, // staff mask - md5($this->query) // misc (here search query) + md5($misc) // misc ); } } diff --git a/includes/components/response/textresponse.class.php b/includes/components/response/textresponse.class.php index 7885af27..ef35a2fd 100644 --- a/includes/components/response/textresponse.class.php +++ b/includes/components/response/textresponse.class.php @@ -20,7 +20,7 @@ trait TrTooltip ); if ($this->enhancedTT) - $key[4] = md5(serialize($this->enhancedTT)); + $key[3] = md5(serialize($this->enhancedTT)); return $key; } From d32074fdcdb59e0e1ef6c2caf072b18ccf13e761 Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Sat, 11 Oct 2025 23:11:31 +0200 Subject: [PATCH 042/260] ZoneDetailPage/Tabs * only offer 'filter result' prompt on tabs for zones that are filtrable --- endpoints/zone/zone.php | 27 +++++++++++++++++---------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/endpoints/zone/zone.php b/endpoints/zone/zone.php index a26ddbab..65fb9c36 100644 --- a/endpoints/zone/zone.php +++ b/endpoints/zone/zone.php @@ -568,10 +568,10 @@ class ZoneBaseResponse extends TemplateResponse implements ICache // tab: NPCs if ($cSpawns && !$creatureSpawns->error) { - $tabData = array( - 'data' => $creatureSpawns->getListviewData(), - 'note' => sprintf(Util::$filterResultString, '?npcs&filter=cr=6;crs='.$this->typeId.';crv=0') - ); + $tabData = ['data' => $creatureSpawns->getListviewData()]; + + if (!is_null(CreatureListFilter::getCriteriaIndex(6, $this->typeId))) + $tabData['note'] = sprintf(Util::$filterResultString, '?npcs&filter=cr=6;crs='.$this->typeId.';crv=0'); if ($creatureSpawns->getMatches() > Cfg::get('SQL_LIMIT_DEFAULT')) $tabData['_truncated'] = 1; @@ -584,10 +584,10 @@ class ZoneBaseResponse extends TemplateResponse implements ICache // tab: Objects if ($oSpawns && !$objectSpawns->error) { - $tabData = array( - 'data' => $objectSpawns->getListviewData(), - 'note' => sprintf(Util::$filterResultString, '?objects&filter=cr=1;crs='.$this->typeId.';crv=0') - ); + $tabData = ['data' => $objectSpawns->getListviewData()]; + + if (!is_null(GameObjectListFilter::getCriteriaIndex(1, $this->typeId))) + $tabData['note'] = sprintf(Util::$filterResultString, '?objects&filter=cr=1;crs='.$this->typeId.';crv=0'); if ($objectSpawns->getMatches() > Cfg::get('SQL_LIMIT_DEFAULT')) $tabData['_truncated'] = 1; @@ -623,7 +623,10 @@ class ZoneBaseResponse extends TemplateResponse implements ICache if (!in_array($this->typeId, $children)) continue; - $tabData['note'] = '$$WH.sprintf(LANG.lvnote_zonequests, '.$parent.', '.$this->typeId.',"'.$this->subject->getField('name', true).'", '.$this->typeId.')'; + if (!is_null(ItemListFilter::getCriteriaIndex(126, $this->typeId))) + $tabData['note'] = '$$WH.sprintf(LANG.lvnote_zonequests, '.$parent.', '.$this->typeId.',"'.$this->subject->getField('name', true).'", '.$this->typeId.')'; + else + $tabData['note'] = '$$WH.sprintf(LANG.lvnote_questsind, '.$parent.', '.$this->typeId.',"'.$this->subject->getField('name', true).'")'; break; } @@ -662,11 +665,15 @@ class ZoneBaseResponse extends TemplateResponse implements ICache $rewards = new ItemList(array(['id', array_unique($rewardsLV)])); if (!$rewards->error) { + $note = null; + if (!is_null(ItemListFilter::getCriteriaIndex(126, $this->typeId))) + $note = sprintf(Util::$filterResultString, '?items&filter=cr=126;crs='.$this->typeId.';crv=0'); + $this->lvTabs->addListviewTab(new Listview(array( 'data' => $rewards->getListviewData(), 'name' => '$LANG.tab_questrewards', 'id' => 'quest-rewards', - 'note' => sprintf(Util::$filterResultString, '?items&filter=cr=126;crs='.$this->typeId.';crv=0') + 'note' => $note ), ItemList::$brickFile)); $this->extendGlobalData($rewards->getJSGlobals(GLOBALINFO_SELF)); From dd838fa994dbe6d558ad37c0eefad116613e9321 Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Sun, 12 Oct 2025 00:52:51 +0200 Subject: [PATCH 043/260] Misc/Doc * add several ItemMods unusd by client but still found in item_template as comment --- includes/dbtypes/item.class.php | 7 +++++++ includes/defines.php | 9 +++++++++ includes/game/chrstatistics.php | 16 ++++++++-------- 3 files changed, 24 insertions(+), 8 deletions(-) diff --git a/includes/dbtypes/item.class.php b/includes/dbtypes/item.class.php index 881bd40f..3bd322da 100644 --- a/includes/dbtypes/item.class.php +++ b/includes/dbtypes/item.class.php @@ -753,6 +753,13 @@ class ItemList extends DBTypeList case Stat::INTELLECT: case Stat::SPIRIT: case Stat::STAMINA: + // case Stat::ARMOR: // unused by 335a client, still set in item_template + // case Stat::FIRE_RESISTANCE: + // case Stat::FROST_RESISTANCE: + // case Stat::HOLY_RESISTANCE: + // case Stat::SHADOW_RESISTANCE: + // case Stat::NATURE_RESISTANCE: + // case Stat::ARCANE_RESISTANCE: $x .= ''.Lang::item('statType', $type, [ord($qty > 0 ? '+' : '-'), abs($qty)]).'
'; break; default: // rating with % for reqLevel diff --git a/includes/defines.php b/includes/defines.php index 5084912c..4894f295 100644 --- a/includes/defines.php +++ b/includes/defines.php @@ -494,6 +494,15 @@ define('ITEM_MOD_SPELL_POWER', 45); define('ITEM_MOD_HEALTH_REGEN', 46); define('ITEM_MOD_SPELL_PENETRATION', 47); define('ITEM_MOD_BLOCK_VALUE', 48); +// unknown by 335a client but still used by several item_templates +// define('ITEM_MOD_MASTERY_RATING', 49); +// define('ITEM_MOD_EXTRA_ARMOR', 50); +// define('ITEM_MOD_FIRE_RESISTANCE', 51); +// define('ITEM_MOD_FROST_RESISTANCE', 52); +// define('ITEM_MOD_HOLY_RESISTANCE', 53); +// define('ITEM_MOD_SHADOW_RESISTANCE', 54); +// define('ITEM_MOD_NATURE_RESISTANCE', 55); +// define('ITEM_MOD_ARCANE_RESISTANCE', 56); // Combat Ratings define('CR_WEAPON_SKILL', 0); diff --git a/includes/game/chrstatistics.php b/includes/game/chrstatistics.php index 136eeb2d..82d6d453 100644 --- a/includes/game/chrstatistics.php +++ b/includes/game/chrstatistics.php @@ -172,14 +172,14 @@ abstract class Stat // based on g_statTo self::HEALTH_REGENERATION => ['healthrgn', ITEM_MOD_HEALTH_REGEN, null, 60, self::FLAG_ITEM], self::SPELL_PENETRATION => ['splpen', ITEM_MOD_SPELL_PENETRATION, null, 94, self::FLAG_ITEM], self::BLOCK => ['block', ITEM_MOD_BLOCK_VALUE, null, 43, self::FLAG_ITEM], - // self::MASTERY_RTG => ['mastrtng', null, CR_MASTERY, null, self::FLAG_NONE], - self::ARMOR => ['armor', null, null, 41, self::FLAG_ITEM], - self::FIRE_RESISTANCE => ['firres', null, null, 26, self::FLAG_ITEM], - self::FROST_RESISTANCE => ['frores', null, null, 28, self::FLAG_ITEM], - self::HOLY_RESISTANCE => ['holres', null, null, 30, self::FLAG_ITEM], - self::SHADOW_RESISTANCE => ['shares', null, null, 29, self::FLAG_ITEM], - self::NATURE_RESISTANCE => ['natres', null, null, 27, self::FLAG_ITEM], - self::ARCANE_RESISTANCE => ['arcres', null, null, 25, self::FLAG_ITEM], + // self::MASTERY_RTG => ['mastrtng', ITEM_MOD_MASTERY_RATING, CR_MASTERY, null, self::FLAG_NONE], + self::ARMOR => ['armor', null,/*ITEM_MOD_EXTRA_ARMOR */null, 41, self::FLAG_ITEM], + self::FIRE_RESISTANCE => ['firres', null,/*ITEM_MOD_FIRE_RESISTANCE */null, 26, self::FLAG_ITEM], + self::FROST_RESISTANCE => ['frores', null,/*ITEM_MOD_FROST_RESISTANCE */null, 28, self::FLAG_ITEM], + self::HOLY_RESISTANCE => ['holres', null,/*ITEM_MOD_HOLY_RESISTANCE */null, 30, self::FLAG_ITEM], + self::SHADOW_RESISTANCE => ['shares', null,/*ITEM_MOD_SHADOW_RESISTANCE*/null, 29, self::FLAG_ITEM], + self::NATURE_RESISTANCE => ['natres', null,/*ITEM_MOD_NATURE_RESISTANCE*/null, 27, self::FLAG_ITEM], + self::ARCANE_RESISTANCE => ['arcres', null,/*ITEM_MOD_ARCANE_RESISTANCE*/null, 25, self::FLAG_ITEM], self::FIRE_SPELL_POWER => ['firsplpwr', null, null, 53, self::FLAG_ITEM], self::FROST_SPELL_POWER => ['frosplpwr', null, null, 54, self::FLAG_ITEM], self::HOLY_SPELL_POWER => ['holsplpwr', null, null, 55, self::FLAG_ITEM], From fb7b22db3691e07587a34920fbac24c94df3c0dc Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Sun, 12 Oct 2025 04:42:34 +0200 Subject: [PATCH 044/260] Account/Passwords * use buildin php functions to handle passwords * increase cost of BCRYPT * make use of the SensitiveParameter attribute --- config/extAuth.php.in | 2 +- includes/user.class.php | 27 +++++++++------------------ 2 files changed, 10 insertions(+), 19 deletions(-) diff --git a/config/extAuth.php.in b/config/extAuth.php.in index f05e4b83..6158987f 100644 --- a/config/extAuth.php.in +++ b/config/extAuth.php.in @@ -4,7 +4,7 @@ if (!defined('AOWOW_REVISION')) die('illegal access'); - function extAuth(string &$usernameOrEmail, string $password, int &$userId = 0, int &$userGroup = -1) : int + function extAuth(string &$usernameOrEmail, #[\SensitiveParameter] string $password, int &$userId = 0, int &$userGroup = -1) : int { /* insert some auth mechanism here diff --git a/includes/user.class.php b/includes/user.class.php index cfa8673a..12928616 100644 --- a/includes/user.class.php +++ b/includes/user.class.php @@ -236,7 +236,7 @@ class User /* auth mechanisms */ /*******************/ - public static function authenticate(string $login, string $password) : int + public static function authenticate(string $login, #[\SensitiveParameter] string $password) : int { $userId = 0; @@ -259,7 +259,7 @@ class User return $result; } - private static function authSelf(string $nameOrEmail, string $password, int &$userId) : int + private static function authSelf(string $nameOrEmail, #[\SensitiveParameter] string $password, int &$userId) : int { if (!self::$ip) return AUTH_INTERNAL_ERR; @@ -304,7 +304,7 @@ class User return AUTH_OK; } - private static function authRealm(string $name, string $password, int &$userId) : int + private static function authRealm(string $name, #[\SensitiveParameter] string $password, int &$userId) : int { if (!DB::isConnectable(DB_AUTH)) return AUTH_INTERNAL_ERR; @@ -327,7 +327,7 @@ class User return AUTH_OK; } - private static function authExtern(string $nameOrEmail, string $password, int &$userId) : int + private static function authExtern(string $nameOrEmail, #[\SensitiveParameter] string $password, int &$userId) : int { if (!file_exists('config/extAuth.php')) { @@ -387,24 +387,15 @@ class User return $newId ?: 0; } - private static function createSalt() : string + // crypt used by us + public static function hashCrypt(#[\SensitiveParameter] string $pass) : string { - $algo = '$2a'; - $strength = '$09'; - $salt = '$'.Util::createHash(22); - - return $algo.$strength.$salt; + return password_hash($pass, PASSWORD_BCRYPT, ['cost' => 15]); } - // crypt used by aowow - public static function hashCrypt(string $pass) : string + public static function verifyCrypt(#[\SensitiveParameter] string $pass, string $hash) : bool { - return crypt($pass, self::createSalt()); - } - - public static function verifyCrypt(string $pass, string $hash) : bool - { - return $hash === crypt($pass, $hash); + return password_verify($pass, $hash); } // SRP6 used by TC From a33abb84fea11ed02e1ab986c8da3b2b5969bb7c Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Sun, 12 Oct 2025 05:01:52 +0200 Subject: [PATCH 045/260] Setup/Account * don't overwrite existing account in case of email conflict --- setup/tools/clisetup/account.us.php | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/setup/tools/clisetup/account.us.php b/setup/tools/clisetup/account.us.php index 9cc03d9d..f0844cfd 100644 --- a/setup/tools/clisetup/account.us.php +++ b/setup/tools/clisetup/account.us.php @@ -80,10 +80,13 @@ CLISetup::registerUtility(new class extends UtilityScript else if (!$passw) $passw = $uiAccount['pass1']; - if (!$email && Util::validateEmail($uiAccount['email'])) + if (!$email && !empty($uiAccount['email']) && Util::validateEmail($uiAccount['email'])) $email = $uiAccount['email']; - else if (!$email && $uiAccount && $uiAccount['email']) - CLI::write('[account] email invalid ... using default: ' . Cfg::get('CONTACT_EMAIL'), CLI::LOG_INFO); + else if (!$email && empty($uiAccount['email'])) + { + $email = Cfg::get('CONTACT_EMAIL'); + CLI::write('[account] no email given, using default: ' . Cfg::get('CONTACT_EMAIL'), CLI::LOG_INFO); + } } else if ($this->fields) { @@ -93,18 +96,18 @@ CLISetup::registerUtility(new class extends UtilityScript return true; } - if (DB::Aowow()->SelectCell('SELECT 1 FROM ?_account WHERE LOWER(`username`) = LOWER(?) AND (`status` <> ?d OR (`status` = ?d AND `statusTimer` > UNIX_TIMESTAMP()))', $name, ACC_STATUS_NEW, ACC_STATUS_NEW)) + if (!$name || !$passw || !$email) + return false; + + if ($username = DB::Aowow()->selectCell('SELECT `username` FROM ?_account WHERE (LOWER(`username`) = LOWER(?) OR LOWER(`email`) = LOWER(?)) AND (`status` <> ?d OR (`status` = ?d AND `statusTimer` > UNIX_TIMESTAMP()))', $name, $email, ACC_STATUS_NEW, ACC_STATUS_NEW)) { - CLI::write('[account] ' . Lang::account('nameInUse'), CLI::LOG_ERROR); + CLI::write('[account] ' . (Util::lower($name) == Util::lower($username) ? Lang::account('nameInUse') : Lang::account('mailInUse')), CLI::LOG_ERROR); CLI::write(); return false; } - if (!$name || !$passw) - return false; - if (DB::Aowow()->query('REPLACE INTO ?_account (`login`, `passHash`, `username`, `joindate`, `email`, `userGroups`, `userPerms`) VALUES (?, ?, ?, UNIX_TIMESTAMP(), ?, ?d, 1)', - $name, User::hashCrypt($passw), $name, $email ?: Cfg::get('CONTACT_EMAIL'), U_GROUP_ADMIN)) + $name, User::hashCrypt($passw), $name, $email, U_GROUP_ADMIN)) { $newId = DB::Aowow()->selectCell('SELECT `id` FROM ?_account WHERE LOWER(`username`) = LOWER(?)', $name); Util::gainSiteReputation($newId, SITEREP_ACTION_REGISTER); From 034eca1f58cbdcfac027c7f78a61106c6ccb9f5c Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Sun, 12 Oct 2025 17:26:49 +0200 Subject: [PATCH 046/260] Items/RandEnchants * fix amount calculation for scaling enchantments * cache RandomPropPoints lookups --- includes/dbtypes/item.class.php | 118 +++++++++++++------------------- 1 file changed, 49 insertions(+), 69 deletions(-) diff --git a/includes/dbtypes/item.class.php b/includes/dbtypes/item.class.php index 3bd322da..4bec8f3e 100644 --- a/includes/dbtypes/item.class.php +++ b/includes/dbtypes/item.class.php @@ -18,11 +18,12 @@ class ItemList extends DBTypeList public array $rndEnchIds = []; public array $subItems = []; - private array $ssd = []; - private array $vendors = []; - private array $jsGlobals = []; // getExtendedCost creates some and has no access to template - private array $enhanceR = []; - private array $relEnchant = []; + private array $randPropPoints = []; + private array $ssd = []; + private array $vendors = []; + private array $jsGlobals = []; // getExtendedCost creates some and has no access to template + private array $enhanceR = []; + private array $relEnchant = []; protected string $queryBase = 'SELECT i.*, i.`block` AS "tplBlock", i.`armor` AS tplArmor, i.`dmgMin1` AS "tplDmgMin1", i.`dmgMax1` AS "tplDmgMax1", i.`id` AS ARRAY_KEY, i.`id` AS "id" FROM ?_items i'; protected array $queryOpts = array( // 3 => Type::ITEM @@ -1228,76 +1229,55 @@ class ItemList extends DBTypeList } // from Trinity - public function generateEnchSuffixFactor() : int + public function generateEnchSuffixFactor() : float { - $rpp = DB::Aowow()->selectRow('SELECT * FROM ?_itemrandomproppoints WHERE `id` = ?', $this->curTpl['itemLevel']); - if (!$rpp) - return 0; + if (empty($this->randPropPoints[$this->curTpl['itemLevel']])) + $this->randPropPoints[$this->curTpl['itemLevel']] = DB::Aowow()->selectRow('SELECT * FROM ?_itemrandomproppoints WHERE `id` = ?', $this->curTpl['itemLevel']); - switch ($this->curTpl['slot']) + $rpp = &$this->randPropPoints[$this->curTpl['itemLevel']]; + + if (!$rpp) + return 0.0; + + $fieldIdx = match((int)$this->curTpl['slot']) { - // Items of that type don`t have points - case INVTYPE_NON_EQUIP: - case INVTYPE_BAG: - case INVTYPE_TABARD: - case INVTYPE_AMMO: - case INVTYPE_QUIVER: - case INVTYPE_RELIC: - return 0; - // Select point coefficient - case INVTYPE_HEAD: - case INVTYPE_BODY: - case INVTYPE_CHEST: - case INVTYPE_LEGS: - case INVTYPE_2HWEAPON: - case INVTYPE_ROBE: - $suffixFactor = 1; - break; - case INVTYPE_SHOULDERS: - case INVTYPE_WAIST: - case INVTYPE_FEET: - case INVTYPE_HANDS: - case INVTYPE_TRINKET: - $suffixFactor = 2; - break; - case INVTYPE_NECK: - case INVTYPE_WRISTS: - case INVTYPE_FINGER: - case INVTYPE_SHIELD: - case INVTYPE_CLOAK: - case INVTYPE_HOLDABLE: - $suffixFactor = 3; - break; - case INVTYPE_WEAPON: - case INVTYPE_WEAPONMAINHAND: - case INVTYPE_WEAPONOFFHAND: - $suffixFactor = 4; - break; - case INVTYPE_RANGED: - case INVTYPE_THROWN: - case INVTYPE_RANGEDRIGHT: - $suffixFactor = 5; - break; - default: - return 0; - } + INVTYPE_HEAD, + INVTYPE_BODY, + INVTYPE_CHEST, + INVTYPE_LEGS, + INVTYPE_2HWEAPON, + INVTYPE_ROBE => 1, + INVTYPE_SHOULDERS, + INVTYPE_WAIST, + INVTYPE_FEET, + INVTYPE_HANDS, + INVTYPE_TRINKET => 2, + INVTYPE_NECK, + INVTYPE_WRISTS, + INVTYPE_FINGER, + INVTYPE_SHIELD, + INVTYPE_CLOAK, + INVTYPE_HOLDABLE => 3, + INVTYPE_WEAPON, + INVTYPE_WEAPONMAINHAND, + INVTYPE_WEAPONOFFHAND => 4, + INVTYPE_RANGED, + INVTYPE_THROWN, + INVTYPE_RANGEDRIGHT => 5, + default => 0 // inv types that don`t have points + }; + + if (!$fieldIdx) + return 0.0; // Select rare/epic modifier - switch ($this->curTpl['quality']) + return match((int)$this->curTpl['quality']) { - case ITEM_QUALITY_UNCOMMON: - return $rpp['uncommon'.$suffixFactor] / 10000; - case ITEM_QUALITY_RARE: - return $rpp['rare'.$suffixFactor] / 10000; - case ITEM_QUALITY_EPIC: - return $rpp['epic'.$suffixFactor] / 10000; - case ITEM_QUALITY_LEGENDARY: - case ITEM_QUALITY_ARTIFACT: - return 0; // not have random properties - default: - break; - } - return 0; + ITEM_QUALITY_UNCOMMON => $rpp['uncommon'.$fieldIdx] / 10000, + ITEM_QUALITY_RARE => $rpp['rare'.$fieldIdx] / 10000, + ITEM_QUALITY_EPIC => $rpp['epic'.$fieldIdx] / 10000, + default => 0.0 // qualities that don't have random properties + }; } public function extendJsonStats() : void From 816eacaf73fdc919333fd105d16a92e8d832aa01 Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Sun, 12 Oct 2025 18:07:01 +0200 Subject: [PATCH 047/260] Summary/Fixup * allow signed integers (random enchantments) in summary definition --- endpoints/compare/compare.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/endpoints/compare/compare.php b/endpoints/compare/compare.php index 5268795d..04a16a4e 100644 --- a/endpoints/compare/compare.php +++ b/endpoints/compare/compare.php @@ -106,7 +106,7 @@ class CompareBaseResponse extends TemplateResponse protected static function checkCompareString(string $val) : string { $val = urldecode($val); - if (preg_match('/[^\d\.:;]/', $val)) + if (preg_match('/[^-?\d\.:;]/', $val)) return ''; return $val; From 65d490a8ae59c9dea965ffb36a708e5b0f7cc108 Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Sun, 12 Oct 2025 18:43:37 +0200 Subject: [PATCH 048/260] Enchantments/Stats * entirely forgo ?_item_stats table when calculating enchantment stats --- includes/dbtypes/enchantment.class.php | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/includes/dbtypes/enchantment.class.php b/includes/dbtypes/enchantment.class.php index fbe71b3a..4770910a 100644 --- a/includes/dbtypes/enchantment.class.php +++ b/includes/dbtypes/enchantment.class.php @@ -56,17 +56,23 @@ class EnchantmentList extends DBTypeList break; } } - - // issue with scaling stats enchantments - // stats are stored as NOT NULL to be usable by the search filters and such become indistinguishable from scaling enchantments that _actually_ use the value 0 - // so filter the stats container and if it is empty, rebuild from self. .. there are no mixed scaling/static enchantments, right!? - $this->jsonStats[$this->id] = (new StatsContainer)->fromJson($curTpl, true)->filter(); - if (!count($this->jsonStats[$this->id])) - $this->jsonStats[$this->id]->fromEnchantment($curTpl); } if ($relSpells) $this->relSpells = new SpellList(array(['id', $relSpells])); + + // issue with scaling stats enchantments + // stats are stored as NOT NULL to be usable by the search filters and such become indistinguishable from scaling enchantments that _actually_ use the value 0 + // so we can't rely on ?_item_stats and always have to calc stats + foreach ($this->iterate() as $ench) + { + $relSpells = []; + foreach ($ench['spells'] as $s) + if ($_ = $this->relSpells->getEntry($s[0])) + $relSpells[$s[0]] = $_; + + $this->jsonStats[$this->id] = (new StatsContainer($relSpells))->fromEnchantment($ench); + } } public function getListviewData(int $addInfoMask = 0x0) : array @@ -122,7 +128,7 @@ class EnchantmentList extends DBTypeList public function getStatGainForCurrent() : array { - return $this->jsonStats[$this->id]->toJson(includeEmpty: false); + return $this->jsonStats[$this->id]->toJson(includeEmpty: true); } public function getRelSpell(int $id) : ?array From 04f3aa7a8232dbd0ba640f159a3823b76db66a93 Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Sat, 11 Oct 2025 03:30:13 +0200 Subject: [PATCH 049/260] Setup/Source * respect disabled Quests and Spells when flagging Items as unavailable * reuse data from loot_link to set difficuly bits and zoneId for loot container GOs --- setup/sql/updates/1760300362_01.sql | 1 + setup/tools/sqlgen/source.ss.php | 147 +++++++++++++++++++--------- 2 files changed, 104 insertions(+), 44 deletions(-) create mode 100644 setup/sql/updates/1760300362_01.sql diff --git a/setup/sql/updates/1760300362_01.sql b/setup/sql/updates/1760300362_01.sql new file mode 100644 index 00000000..bef0e1a9 --- /dev/null +++ b/setup/sql/updates/1760300362_01.sql @@ -0,0 +1 @@ +UPDATE `aowow_dbversion` SET `sql` = CONCAT(IFNULL(`sql`, ''), ' source'); diff --git a/setup/tools/sqlgen/source.ss.php b/setup/tools/sqlgen/source.ss.php index 7aece181..ad7816eb 100644 --- a/setup/tools/sqlgen/source.ss.php +++ b/setup/tools/sqlgen/source.ss.php @@ -19,12 +19,14 @@ CLISetup::registerSetup("sql", new class extends SetupScript protected $worldDependency = ['playercreateinfo_skills', 'playercreateinfo_item', 'skill_discovery_template', 'achievement_reward', 'skill_perfect_item_template', 'item_template', 'gameobject_template', 'quest_template', 'quest_template_addon', 'creature_template', 'creature', 'creature_default_trainer', 'trainer_spell', 'npc_vendor', 'game_event_npc_vendor', 'reference_loot_template', 'item_loot_template', 'creature_loot_template', 'gameobject_loot_template', 'mail_loot_template', 'disenchant_loot_template', 'fishing_loot_template', 'skinning_loot_template', 'milling_loot_template', 'prospecting_loot_template', 'pickpocketing_loot_template']; protected $setupAfter = [['spell', 'achievement', 'items', 'itemset', 'spawns', 'creature', 'zones', 'titles'], []]; - private $srcBuffer = []; - private $refLoot = []; - private $dummyNPCs = []; + private array $srcBuffer = []; + private array $refLoot = []; + private array $dummyNPCs = []; + private array $dummyGOs = []; + private array $disables = []; - private const PVP_MONEY = [26045, 24581, 24579, 43589, 37836]; // Nagrand, Hellfire Pen. H, Hellfire Pen. A, Wintergrasp, Grizzly Hills - private const COMMON_THRESHOLD = 100; + private const /* array */ PVP_MONEY = [26045, 24581, 24579, 43589, 37836]; // Nagrand, Hellfire Pen. H, Hellfire Pen. A, Wintergrasp, Grizzly Hills + private const /* int */ COMMON_THRESHOLD = 100; public function generate(array $ids = []) : bool { @@ -33,17 +35,30 @@ CLISetup::registerSetup("sql", new class extends SetupScript /*********************************/ $this->dummyNPCs = DB::Aowow()->select( - 'SELECT difficultyEntry1 AS ARRAY_KEY, 2 AS "0", `id` AS "1" FROM ?_creature WHERE difficultyEntry1 > 0 UNION - SELECT difficultyEntry2 AS ARRAY_KEY, 4 AS "0", `id` AS "1" FROM ?_creature WHERE difficultyEntry2 > 0 UNION - SELECT difficultyEntry3 AS ARRAY_KEY, 8 AS "0", `id` AS "1" FROM ?_creature WHERE difficultyEntry3 > 0' + 'SELECT `difficultyEntry1` AS ARRAY_KEY, 2 AS "0", `id` AS "1" FROM ?_creature WHERE `difficultyEntry1` > 0 UNION + SELECT `difficultyEntry2` AS ARRAY_KEY, 4 AS "0", `id` AS "1" FROM ?_creature WHERE `difficultyEntry2` > 0 UNION + SELECT `difficultyEntry3` AS ARRAY_KEY, 8 AS "0", `id` AS "1" FROM ?_creature WHERE `difficultyEntry3` > 0' + ); + + $this->dummyGOs = DB::Aowow()->select( + 'SELECT l1.`objectId` AS ARRAY_KEY, BIT_OR(l1.`difficulty`) AS "0", IFNULL(l2.`npcId`, l1.`npcId`) AS "1" + FROM aowow_loot_link l1 + LEFT JOIN aowow_loot_link l2 ON l1.`objectId` = l2.`objectId` AND l2.`priority` = 1 + GROUP BY l1.`objectid`' + ); + + $this->disables = DB::World()->selectCol( + 'SELECT IF(`sourceType`, ?d, ?d) AS ARRAY_KEY, `entry` AS ARRAY_KEY2, `entry` + FROM disables + WHERE (`sourceType` = 0 AND (`flags` & 0xF)) OR `sourceType` = 1', + Type::QUEST, Type::SPELL ); - // todo: do the same for GOs CLI::write('[source] - resolving ref-loot tree', CLI::LOG_BLANK, true, true); $this->refLoot = DB::World()->select( - 'SELECT rlt.Entry AS ARRAY_KEY, IF(Reference, -Reference, Item) AS ARRAY_KEY2, it.entry, it.class, it.subclass, it.spellid_1, it.spelltrigger_1, it.spellid_2, it.spelltrigger_2 + 'SELECT rlt.`Entry` AS ARRAY_KEY, IF(`Reference`, -`Reference`, `Item`) AS ARRAY_KEY2, it.`entry`, it.`class`, it.`subclass`, it.`spellid_1`, it.`spelltrigger_1`, it.`spellid_2`, it.`spelltrigger_2` FROM reference_loot_template rlt - LEFT JOIN item_template it ON rlt.Reference = 0 AND rlt.Item = it.entry + LEFT JOIN item_template it ON rlt.`Reference` = 0 AND rlt.`Item` = it.`entry` GROUP BY ARRAY_KEY, ARRAY_KEY2' ); @@ -155,9 +170,22 @@ CLISetup::registerSetup("sql", new class extends SetupScript CLI::write('[source] - Inserting... (done)'); + // generally treat all items generated by spell as being available. The spells may be triggered in convoluted ways but usually _are_ in use. + $itemSpellSource = DB::Aowow()->selectCol( + 'SELECT x.`itemId` FROM ( + SELECT `effect1CreateItemId` AS "itemId", s.`id` FROM dbc_spell s WHERE `effect1CreateItemId` > 0 AND (`effect1Id` IN (?a) OR `effect1AuraId` IN (?a)) UNION ALL + SELECT `effect2CreateItemId` AS "itemId", s.`id` FROM dbc_spell s WHERE `effect2CreateItemId` > 0 AND (`effect2Id` IN (?a) OR `effect2AuraId` IN (?a)) UNION ALL + SELECT `effect3CreateItemId` AS "itemId", s.`id` FROM dbc_spell s WHERE `effect3CreateItemId` > 0 AND (`effect3Id` IN (?a) OR `effect3AuraId` IN (?a)) ) AS x + { WHERE x.`id` NOT IN (?a) }', + SpellList::EFFECTS_ITEM_CREATE, SpellList::AURAS_ITEM_CREATE, + SpellList::EFFECTS_ITEM_CREATE, SpellList::AURAS_ITEM_CREATE, + SpellList::EFFECTS_ITEM_CREATE, SpellList::AURAS_ITEM_CREATE, + !empty($this->disables[Type::SPELL]) ? array_values($this->disables[Type::SPELL]) : DBSIMPLE_SKIP + ); + // flagging aowow_items for source (note: this is not exact! creatures dropping items may not be spawnd, quests granting items may be disabled) DB::Aowow()->query('UPDATE ?_items SET `cuFlags` = `cuFlags` & ?d', ~CUSTOM_UNAVAILABLE); - DB::Aowow()->query('UPDATE ?_items i LEFT JOIN ?_source s ON s.`typeId` = i.`id` AND s.`type` = ?d SET i.`cuFlags` = i.`cuFlags` | ?d WHERE s.`typeId` IS NULL', Type::ITEM, CUSTOM_UNAVAILABLE); + DB::Aowow()->query('UPDATE ?_items i LEFT JOIN ?_source s ON s.`typeId` = i.`id` AND s.`type` = ?d SET i.`cuFlags` = i.`cuFlags` | ?d WHERE s.`typeId` IS NULL AND i.`id` NOT IN (?a)', Type::ITEM, CUSTOM_UNAVAILABLE, $itemSpellSource); return true; } @@ -206,8 +234,6 @@ CLISetup::registerSetup("sql", new class extends SetupScript private function taughtSpell(array $item) : int { - # spelltrigger_X (0: onUse; 6: onLearnSpell) - // should not be able to teach spells (recipe || mount || vanityPet) if ($item['class'] != ITEM_CLASS_RECIPE && ($item['class'] != ITEM_CLASS_MISC || ($item['subclass'] != 2 && $item['subclass'] != 5))) return 0; @@ -235,11 +261,14 @@ CLISetup::registerSetup("sql", new class extends SetupScript GROUP BY ARRAY_KEY', array_merge(SKILLS_TRADE_PRIMARY, [SKILL_FIRST_AID, SKILL_COOKING, SKILL_FISHING]) ); + // assume unique craft spells per item $perfectItems = DB::World()->selectCol('SELECT `perfectItemType` AS ARRAY_KEY, `spellId` AS "spell" FROM skill_perfect_item_template'); foreach ($perfectItems AS $item => $spell) $itemSpells[$item] = $spell; + $itemSpells = array_filter($itemSpells, fn($x) => empty($this->disables[Type::SPELL][$x])); + $spellItems = DB::World()->select('SELECT `entry` AS ARRAY_KEY, `class`, `subclass`, `spellid_1`, `spelltrigger_1`, `spellid_2`, `spelltrigger_2` FROM item_template WHERE `entry` IN (?a)', array_keys($itemSpells)); foreach ($spellItems as $iId => $si) { @@ -266,8 +295,8 @@ CLISetup::registerSetup("sql", new class extends SetupScript GROUP BY `refOrItem`, ct.`entry`' ); - $spawns = DB::Aowow()->select('SELECT `typeId` AS ARRAY_KEY, IF(COUNT(DISTINCT s.`areaId`) > 1, 0, s.`areaId`) AS "areaId", z.`type` FROM ?_spawns s JOIN ?_zones z ON z.`id` = s.`areaId` WHERE s.`type` = ?d AND `typeId`IN (?a) GROUP BY `typeId`', Type::NPC, array_filter(array_column($creatureLoot, 'entry'))); - $bosses = DB::Aowow()->selectCol('SELECT `id` AS ARRAY_KEY, IF(`cuFlags` & ?d, 1, IF(`typeFlags` & 0x4 AND `rank` > 0, 1, 0)) FROM ?_creature WHERE `id` IN (?a)', NPC_CU_INSTANCE_BOSS, array_filter(array_column($creatureLoot, 'entry'))); + $npcSpawns = DB::Aowow()->select('SELECT `typeId` AS ARRAY_KEY, IF(COUNT(DISTINCT s.`areaId`) > 1, 0, s.`areaId`) AS "areaId", z.`type` FROM ?_spawns s JOIN ?_zones z ON z.`id` = s.`areaId` WHERE s.`type` = ?d AND `typeId`IN (?a) GROUP BY `typeId`', Type::NPC, array_merge(array_column($this->dummyGOs, 1), array_filter(array_column($creatureLoot, 'entry')))); + $bosses = DB::Aowow()->selectCol('SELECT `id` AS ARRAY_KEY, IF(`cuFlags` & ?d, 1, IF(`typeFlags` & 0x4 AND `rank` > 0, 1, 0)) FROM ?_creature WHERE `id` IN (?a)', NPC_CU_INSTANCE_BOSS, array_filter(array_column($creatureLoot, 'entry'))); foreach ($creatureLoot as $l) { @@ -282,9 +311,9 @@ CLISetup::registerSetup("sql", new class extends SetupScript if (isset($bosses[$entry]) && $bosses[$entry]) // can be empty...? $mMask |= SRC_FLAG_BOSSDROP; - if (isset($spawns[$entry])) + if (isset($npcSpawns[$entry])) { - switch ($spawns[$entry]['type']) + switch ($npcSpawns[$entry]['type']) { case MAP_TYPE_DUNGEON_HC: $mMask |= SRC_FLAG_DUNGEON_DROP; break; @@ -293,7 +322,7 @@ CLISetup::registerSetup("sql", new class extends SetupScript $mMask |= SRC_FLAG_RAID_DROP; break; } - $zoneId = $spawns[$entry]['areaId']; + $zoneId = $npcSpawns[$entry]['areaId']; } if ($roi < 0 && !empty($this->refLoot[-$roi])) @@ -333,34 +362,58 @@ CLISetup::registerSetup("sql", new class extends SetupScript DB::Aowow()->selectCol('SELECT `id` FROM dbc_lock WHERE `properties1` IN (?a)', [LOCK_PROPERTY_HERBALISM, LOCK_PROPERTY_MINING]) ); - $spawns = DB::Aowow()->selectCol('SELECT `typeId` AS ARRAY_KEY, IF(COUNT(DISTINCT `areaId`) > 1, 0, `areaId`) FROM ?_spawns WHERE `type` = ?d AND `typeId`IN (?a) GROUP BY `typeId`', Type::OBJECT, array_column($objectLoot, 'entry')); + $goSpawns = DB::Aowow()->select('SELECT `typeId` AS ARRAY_KEY, IF(COUNT(DISTINCT s.`areaId`) > 1, 0, s.`areaId`) AS "areaId", z.`type` FROM ?_spawns s JOIN ?_zones z ON z.`id` = s.`areaId` WHERE s.`type` = ?d AND `typeId`IN (?a) GROUP BY `typeId`', Type::OBJECT, array_filter(array_column($objectLoot, 'entry'))); - // todo: difficulty entrys for boss chests foreach ($objectLoot as $l) { $roi = $l['refOrItem']; - $zoneId = $spawns[$l['entry']] ?? 0; + $entry = $l['entry']; + $mode = 1 | ($this->dummyGOs[$entry][0] ?? 0); + $zoneId = 0; $mMask = 0x0; + $spawn = []; + + if (isset($this->dummyGOs[$entry])) // we know these are all boss drops + $mMask |= SRC_FLAG_BOSSDROP; + + if (isset($goSpawns[$entry])) + $spawn = $goSpawns[$entry]; + else if (isset($this->dummyGOs[$entry]) && isset($npcSpawns[$this->dummyGOs[$entry][1]])) + $spawn = $npcSpawns[$this->dummyGOs[$entry][1]]; + + if ($spawn) + { + switch ($spawn['type']) + { + case MAP_TYPE_DUNGEON_HC: + $mMask |= SRC_FLAG_DUNGEON_DROP; break; + case MAP_TYPE_MMODE_RAID: + case MAP_TYPE_MMODE_RAID_HC: + $mMask |= SRC_FLAG_RAID_DROP; break; + } + + $zoneId = $spawn['areaId']; + } if ($roi < 0 && !empty($this->refLoot[-$roi])) { foreach ($this->refLoot[-$roi] as $iId => $r) { if ($_ = $this->taughtSpell($r)) - $this->pushBuffer(Type::SPELL, $_, SRC_DROP, 1, $l['qty'] > 1 ? 0 : Type::OBJECT, $l['entry'], $zoneId, $mMask, $l['qty']); + $this->pushBuffer(Type::SPELL, $_, SRC_DROP, $mode, $l['qty'] > 1 ? 0 : Type::OBJECT, $l['entry'], $zoneId, $mMask, $l['qty']); $objectOT[] = $iId; - $this->pushBuffer(Type::ITEM, $iId, SRC_DROP, 1, $l['qty'] > 1 ? 0 : Type::OBJECT, $l['entry'], $zoneId, $mMask, $l['qty']); + $this->pushBuffer(Type::ITEM, $iId, SRC_DROP, $mode, $l['qty'] > 1 ? 0 : Type::OBJECT, $l['entry'], $zoneId, $mMask, $l['qty']); } continue; } if ($_ = $this->taughtSpell($l)) - $this->pushBuffer(Type::SPELL, $_, SRC_DROP, 1, $l['qty'] > 1 ? 0 : Type::OBJECT, $l['entry'], $zoneId, $mMask, $l['qty']); + $this->pushBuffer(Type::SPELL, $_, SRC_DROP, $mode, $l['qty'] > 1 ? 0 : Type::OBJECT, $l['entry'], $zoneId, $mMask, $l['qty']); $objectOT[] = $roi; - $this->pushBuffer(Type::ITEM, $roi, SRC_DROP, 1, $l['qty'] > 1 ? 0 : Type::OBJECT, $l['entry'], $zoneId, $mMask, $l['qty']); + $this->pushBuffer(Type::ITEM, $roi, SRC_DROP, $mode, $l['qty'] > 1 ? 0 : Type::OBJECT, $l['entry'], $zoneId, $mMask, $l['qty']); } CLI::write('[source] * #2 Drop [Item]', CLI::LOG_BLANK, true, true); @@ -436,19 +489,20 @@ CLISetup::registerSetup("sql", new class extends SetupScript CLI::write('[source] * #4 Quest', CLI::LOG_BLANK, true, true); $quests = DB::World()->select( - 'SELECT n.`item` AS ARRAY_KEY, n.`Id` AS "quest", SUM(n.`qty`) AS "qty", BIT_OR(n.`side`) AS "side", IF(COUNT(DISTINCT `zone`) > 1, 0, `zone`) AS "zone", it.`class`, it.`subclass`, it.`spellid_1`, it.`spelltrigger_1`, it.`spellid_2`, it.`spelltrigger_2` - FROM (SELECT `RewardChoiceItemID1` AS "item", `ID`, COUNT(1) AS "qty", IF(`AllowableRaces` & ?d AND NOT (`AllowableRaces` & ?d), ?d, IF(`AllowableRaces` & ?d AND NOT (`AllowableRaces` & ?d), ?d, ?d)) AS "side", IF(COUNT(DISTINCT `QuestSortID`) > 1, 0, GREATEST(`QuestSortID`, 0)) AS "zone" FROM quest_template WHERE `RewardChoiceItemID1` > 0 GROUP BY `item` UNION - SELECT `RewardChoiceItemID2` AS "item", `ID`, COUNT(1) AS "qty", IF(`AllowableRaces` & ?d AND NOT (`AllowableRaces` & ?d), ?d, IF(`AllowableRaces` & ?d AND NOT (`AllowableRaces` & ?d), ?d, ?d)) AS "side", IF(COUNT(DISTINCT `QuestSortID`) > 1, 0, GREATEST(`QuestSortID`, 0)) AS "zone" FROM quest_template WHERE `RewardChoiceItemID2` > 0 GROUP BY `item` UNION - SELECT `RewardChoiceItemID3` AS "item", `ID`, COUNT(1) AS "qty", IF(`AllowableRaces` & ?d AND NOT (`AllowableRaces` & ?d), ?d, IF(`AllowableRaces` & ?d AND NOT (`AllowableRaces` & ?d), ?d, ?d)) AS "side", IF(COUNT(DISTINCT `QuestSortID`) > 1, 0, GREATEST(`QuestSortID`, 0)) AS "zone" FROM quest_template WHERE `RewardChoiceItemID3` > 0 GROUP BY `item` UNION - SELECT `RewardChoiceItemID4` AS "item", `ID`, COUNT(1) AS "qty", IF(`AllowableRaces` & ?d AND NOT (`AllowableRaces` & ?d), ?d, IF(`AllowableRaces` & ?d AND NOT (`AllowableRaces` & ?d), ?d, ?d)) AS "side", IF(COUNT(DISTINCT `QuestSortID`) > 1, 0, GREATEST(`QuestSortID`, 0)) AS "zone" FROM quest_template WHERE `RewardChoiceItemID4` > 0 GROUP BY `item` UNION - SELECT `RewardChoiceItemID5` AS "item", `ID`, COUNT(1) AS "qty", IF(`AllowableRaces` & ?d AND NOT (`AllowableRaces` & ?d), ?d, IF(`AllowableRaces` & ?d AND NOT (`AllowableRaces` & ?d), ?d, ?d)) AS "side", IF(COUNT(DISTINCT `QuestSortID`) > 1, 0, GREATEST(`QuestSortID`, 0)) AS "zone" FROM quest_template WHERE `RewardChoiceItemID5` > 0 GROUP BY `item` UNION - SELECT `RewardChoiceItemID6` AS "item", `ID`, COUNT(1) AS "qty", IF(`AllowableRaces` & ?d AND NOT (`AllowableRaces` & ?d), ?d, IF(`AllowableRaces` & ?d AND NOT (`AllowableRaces` & ?d), ?d, ?d)) AS "side", IF(COUNT(DISTINCT `QuestSortID`) > 1, 0, GREATEST(`QuestSortID`, 0)) AS "zone" FROM quest_template WHERE `RewardChoiceItemID6` > 0 GROUP BY `item` UNION - SELECT `RewardItem1` AS "item", `ID`, COUNT(1) AS "qty", IF(`AllowableRaces` & ?d AND NOT (`AllowableRaces` & ?d), ?d, IF(`AllowableRaces` & ?d AND NOT (`AllowableRaces` & ?d), ?d, ?d)) AS "side", IF(COUNT(DISTINCT `QuestSortID`) > 1, 0, GREATEST(`QuestSortID`, 0)) AS "zone" FROM quest_template WHERE `RewardItem1` > 0 GROUP BY `item` UNION - SELECT `RewardItem2` AS "item", `ID`, COUNT(1) AS "qty", IF(`AllowableRaces` & ?d AND NOT (`AllowableRaces` & ?d), ?d, IF(`AllowableRaces` & ?d AND NOT (`AllowableRaces` & ?d), ?d, ?d)) AS "side", IF(COUNT(DISTINCT `QuestSortID`) > 1, 0, GREATEST(`QuestSortID`, 0)) AS "zone" FROM quest_template WHERE `RewardItem2` > 0 GROUP BY `item` UNION - SELECT `RewardItem3` AS "item", `ID`, COUNT(1) AS "qty", IF(`AllowableRaces` & ?d AND NOT (`AllowableRaces` & ?d), ?d, IF(`AllowableRaces` & ?d AND NOT (`AllowableRaces` & ?d), ?d, ?d)) AS "side", IF(COUNT(DISTINCT `QuestSortID`) > 1, 0, GREATEST(`QuestSortID`, 0)) AS "zone" FROM quest_template WHERE `RewardItem3` > 0 GROUP BY `item` UNION - SELECT `RewardItem4` AS "item", `ID`, COUNT(1) AS "qty", IF(`AllowableRaces` & ?d AND NOT (`AllowableRaces` & ?d), ?d, IF(`AllowableRaces` & ?d AND NOT (`AllowableRaces` & ?d), ?d, ?d)) AS "side", IF(COUNT(DISTINCT `QuestSortID`) > 1, 0, GREATEST(`QuestSortID`, 0)) AS "zone" FROM quest_template WHERE `RewardItem4` > 0 GROUP BY `item` UNION - SELECT `StartItem` AS "item", `ID`, COUNT(1) AS "qty", IF(`AllowableRaces` & ?d AND NOT (`AllowableRaces` & ?d), ?d, IF(`AllowableRaces` & ?d AND NOT (`AllowableRaces` & ?d), ?d, ?d)) AS "side", IF(COUNT(DISTINCT `QuestSortID`) > 1, 0, GREATEST(`QuestSortID`, 0)) AS "zone" FROM quest_template WHERE `StartItem` > 0 GROUP BY `item`) n + 'SELECT n.`item` AS ARRAY_KEY, n.`ID` AS "quest", SUM(n.`qty`) AS "qty", BIT_OR(n.`side`) AS "side", IF(COUNT(DISTINCT `zone`) > 1, 0, `zone`) AS "zone", it.`class`, it.`subclass`, it.`spellid_1`, it.`spelltrigger_1`, it.`spellid_2`, it.`spelltrigger_2` + FROM (SELECT `RewardChoiceItemID1` AS "item", `ID`, 1 AS "qty", IF(`AllowableRaces` & ?d AND NOT (`AllowableRaces` & ?d), ?d, IF(`AllowableRaces` & ?d AND NOT (`AllowableRaces` & ?d), ?d, ?d)) AS "side", GREATEST(`QuestSortID`, 0) AS "zone" FROM quest_template WHERE `RewardChoiceItemID1` > 0 UNION ALL + SELECT `RewardChoiceItemID2` AS "item", `ID`, 1 AS "qty", IF(`AllowableRaces` & ?d AND NOT (`AllowableRaces` & ?d), ?d, IF(`AllowableRaces` & ?d AND NOT (`AllowableRaces` & ?d), ?d, ?d)) AS "side", GREATEST(`QuestSortID`, 0) AS "zone" FROM quest_template WHERE `RewardChoiceItemID2` > 0 UNION ALL + SELECT `RewardChoiceItemID3` AS "item", `ID`, 1 AS "qty", IF(`AllowableRaces` & ?d AND NOT (`AllowableRaces` & ?d), ?d, IF(`AllowableRaces` & ?d AND NOT (`AllowableRaces` & ?d), ?d, ?d)) AS "side", GREATEST(`QuestSortID`, 0) AS "zone" FROM quest_template WHERE `RewardChoiceItemID3` > 0 UNION ALL + SELECT `RewardChoiceItemID4` AS "item", `ID`, 1 AS "qty", IF(`AllowableRaces` & ?d AND NOT (`AllowableRaces` & ?d), ?d, IF(`AllowableRaces` & ?d AND NOT (`AllowableRaces` & ?d), ?d, ?d)) AS "side", GREATEST(`QuestSortID`, 0) AS "zone" FROM quest_template WHERE `RewardChoiceItemID4` > 0 UNION ALL + SELECT `RewardChoiceItemID5` AS "item", `ID`, 1 AS "qty", IF(`AllowableRaces` & ?d AND NOT (`AllowableRaces` & ?d), ?d, IF(`AllowableRaces` & ?d AND NOT (`AllowableRaces` & ?d), ?d, ?d)) AS "side", GREATEST(`QuestSortID`, 0) AS "zone" FROM quest_template WHERE `RewardChoiceItemID5` > 0 UNION ALL + SELECT `RewardChoiceItemID6` AS "item", `ID`, 1 AS "qty", IF(`AllowableRaces` & ?d AND NOT (`AllowableRaces` & ?d), ?d, IF(`AllowableRaces` & ?d AND NOT (`AllowableRaces` & ?d), ?d, ?d)) AS "side", GREATEST(`QuestSortID`, 0) AS "zone" FROM quest_template WHERE `RewardChoiceItemID6` > 0 UNION ALL + SELECT `RewardItem1` AS "item", `ID`, 1 AS "qty", IF(`AllowableRaces` & ?d AND NOT (`AllowableRaces` & ?d), ?d, IF(`AllowableRaces` & ?d AND NOT (`AllowableRaces` & ?d), ?d, ?d)) AS "side", GREATEST(`QuestSortID`, 0) AS "zone" FROM quest_template WHERE `RewardItem1` > 0 UNION ALL + SELECT `RewardItem2` AS "item", `ID`, 1 AS "qty", IF(`AllowableRaces` & ?d AND NOT (`AllowableRaces` & ?d), ?d, IF(`AllowableRaces` & ?d AND NOT (`AllowableRaces` & ?d), ?d, ?d)) AS "side", GREATEST(`QuestSortID`, 0) AS "zone" FROM quest_template WHERE `RewardItem2` > 0 UNION ALL + SELECT `RewardItem3` AS "item", `ID`, 1 AS "qty", IF(`AllowableRaces` & ?d AND NOT (`AllowableRaces` & ?d), ?d, IF(`AllowableRaces` & ?d AND NOT (`AllowableRaces` & ?d), ?d, ?d)) AS "side", GREATEST(`QuestSortID`, 0) AS "zone" FROM quest_template WHERE `RewardItem3` > 0 UNION ALL + SELECT `RewardItem4` AS "item", `ID`, 1 AS "qty", IF(`AllowableRaces` & ?d AND NOT (`AllowableRaces` & ?d), ?d, IF(`AllowableRaces` & ?d AND NOT (`AllowableRaces` & ?d), ?d, ?d)) AS "side", GREATEST(`QuestSortID`, 0) AS "zone" FROM quest_template WHERE `RewardItem4` > 0 UNION ALL + SELECT `StartItem` AS "item", `ID`, 1 AS "qty", IF(`AllowableRaces` & ?d AND NOT (`AllowableRaces` & ?d), ?d, IF(`AllowableRaces` & ?d AND NOT (`AllowableRaces` & ?d), ?d, ?d)) AS "side", GREATEST(`QuestSortID`, 0) AS "zone" FROM quest_template WHERE `StartItem` > 0) n JOIN item_template it ON it.`entry` = n.`item` + { WHERE n.`ID` NOT IN (?a) } GROUP BY `item`', ChrRace::MASK_HORDE, ChrRace::MASK_ALLIANCE, SIDE_HORDE, ChrRace::MASK_ALLIANCE, ChrRace::MASK_HORDE, SIDE_ALLIANCE, SIDE_BOTH, ChrRace::MASK_HORDE, ChrRace::MASK_ALLIANCE, SIDE_HORDE, ChrRace::MASK_ALLIANCE, ChrRace::MASK_HORDE, SIDE_ALLIANCE, SIDE_BOTH, @@ -460,7 +514,8 @@ CLISetup::registerSetup("sql", new class extends SetupScript ChrRace::MASK_HORDE, ChrRace::MASK_ALLIANCE, SIDE_HORDE, ChrRace::MASK_ALLIANCE, ChrRace::MASK_HORDE, SIDE_ALLIANCE, SIDE_BOTH, ChrRace::MASK_HORDE, ChrRace::MASK_ALLIANCE, SIDE_HORDE, ChrRace::MASK_ALLIANCE, ChrRace::MASK_HORDE, SIDE_ALLIANCE, SIDE_BOTH, ChrRace::MASK_HORDE, ChrRace::MASK_ALLIANCE, SIDE_HORDE, ChrRace::MASK_ALLIANCE, ChrRace::MASK_HORDE, SIDE_ALLIANCE, SIDE_BOTH, - ChrRace::MASK_HORDE, ChrRace::MASK_ALLIANCE, SIDE_HORDE, ChrRace::MASK_ALLIANCE, ChrRace::MASK_HORDE, SIDE_ALLIANCE, SIDE_BOTH + ChrRace::MASK_HORDE, ChrRace::MASK_ALLIANCE, SIDE_HORDE, ChrRace::MASK_ALLIANCE, ChrRace::MASK_HORDE, SIDE_ALLIANCE, SIDE_BOTH, + !empty($this->disables[Type::QUEST]) ? array_values($this->disables[Type::QUEST]) : DBSIMPLE_SKIP ); $areaParent = DB::Aowow()->selectCol('SELECT `id` AS ARRAY_KEY, `parentArea` FROM ?_zones WHERE `id` IN (?a) AND `parentArea` > 0', array_filter(array_column($quests, 'zone'))); @@ -1012,12 +1067,14 @@ CLISetup::registerSetup("sql", new class extends SetupScript CLI::write('[source] * #4 Quest', CLI::LOG_BLANK, true, true); $quests = DB::World()->select( - 'SELECT `spell` AS ARRAY_KEY, `id`, SUM(`qty`) AS "qty", BIT_OR(`side`) AS "side", IF(COUNT(DISTINCT `zone`) > 1, 0, `zone`) AS "zone" - FROM (SELECT IF(`RewardSpell` = 0, `RewardDisplaySpell`, `RewardSpell`) AS "spell", `Id`, COUNT(1) AS "qty", IF(`AllowableRaces` & ?d AND NOT (`AllowableRaces` & ?d), ?d, IF(`AllowableRaces` & ?d AND NOT (`AllowableRaces` & ?d), ?d, ?d)) AS "side", GREATEST(`QuestSortID`, 0) AS "zone" FROM quest_template WHERE IF(`RewardSpell` = 0, `RewardDisplaySpell`, `RewardSpell`) > 0 GROUP BY `spell` UNION - SELECT qta.`SourceSpellId` AS "spell", qt.`Id`, COUNT(1) AS "qty", IF(`AllowableRaces` & ?d AND NOT (`AllowableRaces` & ?d), ?d, IF(`AllowableRaces` & ?d AND NOT (`AllowableRaces` & ?d), ?d, ?d)) AS "side", GREATEST(`QuestSortID`, 0) AS "zone" FROM quest_template qt JOIN quest_template_addon qta ON qta.ID = qt.ID WHERE qta.`SourceSpellId` > 0 GROUP BY `spell`) t + 'SELECT `spell` AS ARRAY_KEY, `ID` AS "id", SUM(`qty`) AS "qty", BIT_OR(`side`) AS "side", IF(COUNT(DISTINCT `zone`) > 1, 0, `zone`) AS "zone" + FROM (SELECT IF(`RewardSpell` = 0, `RewardDisplaySpell`, `RewardSpell`) AS "spell", `ID`, 1 AS "qty", IF(`AllowableRaces` & ?d AND NOT (`AllowableRaces` & ?d), ?d, IF(`AllowableRaces` & ?d AND NOT (`AllowableRaces` & ?d), ?d, ?d)) AS "side", GREATEST(`QuestSortID`, 0) AS "zone" FROM quest_template WHERE IF(`RewardSpell` = 0, `RewardDisplaySpell`, `RewardSpell`) > 0 UNION ALL + SELECT qta.`SourceSpellId` AS "spell", qt.`ID`, 1 AS "qty", IF(`AllowableRaces` & ?d AND NOT (`AllowableRaces` & ?d), ?d, IF(`AllowableRaces` & ?d AND NOT (`AllowableRaces` & ?d), ?d, ?d)) AS "side", GREATEST(`QuestSortID`, 0) AS "zone" FROM quest_template qt JOIN quest_template_addon qta ON qta.ID = qt.ID WHERE qta.`SourceSpellId` > 0 ) t + { WHERE t.`ID` NOT IN (?a) } GROUP BY `spell`', ChrRace::MASK_HORDE, ChrRace::MASK_ALLIANCE, SIDE_HORDE, ChrRace::MASK_ALLIANCE, ChrRace::MASK_HORDE, SIDE_ALLIANCE, SIDE_BOTH, - ChrRace::MASK_HORDE, ChrRace::MASK_ALLIANCE, SIDE_HORDE, ChrRace::MASK_ALLIANCE, ChrRace::MASK_HORDE, SIDE_ALLIANCE, SIDE_BOTH + ChrRace::MASK_HORDE, ChrRace::MASK_ALLIANCE, SIDE_HORDE, ChrRace::MASK_ALLIANCE, ChrRace::MASK_HORDE, SIDE_ALLIANCE, SIDE_BOTH, + !empty($this->disables[Type::QUEST]) ? array_values($this->disables[Type::QUEST]) : DBSIMPLE_SKIP ); if (!$quests) @@ -1145,8 +1202,10 @@ CLISetup::registerSetup("sql", new class extends SetupScript $quests = DB::World()->select( 'SELECT `RewardTitle` AS ARRAY_KEY, `ID` AS "id", SUM(`qty`) AS "qty", BIT_OR(`side`) AS "side", IF(COUNT(DISTINCT `zone`) > 1, 0, `zone`) AS "zone" FROM (SELECT `RewardTitle`, `ID`, 1 AS "qty", IF(`AllowableRaces` & ?d AND NOT (`AllowableRaces` & ?d), ?d, IF(`AllowableRaces` & ?d AND NOT (`AllowableRaces` & ?d), ?d, ?d)) AS "side", GREATEST(`QuestSortID`, 0) AS "zone" FROM quest_template WHERE `RewardTitle` > 0) q + { WHERE q.`Id` NOT IN (?a) } GROUP BY `RewardTitle`', - ChrRace::MASK_HORDE, ChrRace::MASK_ALLIANCE, SIDE_HORDE, ChrRace::MASK_ALLIANCE, ChrRace::MASK_HORDE, SIDE_ALLIANCE, SIDE_BOTH + ChrRace::MASK_HORDE, ChrRace::MASK_ALLIANCE, SIDE_HORDE, ChrRace::MASK_ALLIANCE, ChrRace::MASK_HORDE, SIDE_ALLIANCE, SIDE_BOTH, + !empty($this->disables[Type::QUEST]) ? array_values($this->disables[Type::QUEST]) : DBSIMPLE_SKIP ); if (!$quests) From 92c58cc5d1baa234623bc96ee018985c08ac1d10 Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Mon, 13 Oct 2025 20:32:19 +0200 Subject: [PATCH 050/260] Localization/UIEscapes * fixed expanding |2 placeholder in general and when the referenced word itself was a placeholder ($N => ) * fixed expanding |3 placeholder for caseIds > 9 --- localization/lang.class.php | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/localization/lang.class.php b/localization/lang.class.php index 8e7cfa79..c0aa3080 100644 --- a/localization/lang.class.php +++ b/localization/lang.class.php @@ -746,28 +746,28 @@ class Lang }, $var); // |2 - frFR preposition: de |2 - $var = preg_replace_callback('/\|2\s?(\w)/i', function ($m) + $var = preg_replace_callback('/\|2\s?(.)/i', function ($m) { - [$_, $word] = $m; + [$_, $char] = $m; - switch (strtolower($word[1])) + switch (strtolower($char)) { case 'h': if (self::$locale != Locale::FR) - return 'de ' . $word; + return 'de ' . $char; case 'a': case 'e': case 'i': case 'o': case 'u': - return "d'" . $word; + return "d'" . $char; default: - return 'de ' . $word; + return 'de ' . $char; } }, $var); // |3 - ruRU declinations |3-() - $var = preg_replace_callback('/\|3-(\d)\(([^\)]+)\)/iu', function ($m) + $var = preg_replace_callback('/\|3-(\d+)\(([^\)]+)\)/iu', function ($m) { [$_, $caseIdx, $word] = $m; @@ -777,7 +777,7 @@ class Lang if (preg_match('/\P{Cyrillic}/iu', $word)) // not in cyrillic script return $word; - if ($declWord = DB::Aowow()->selectCell('SELECT dwc.word FROM ?_declinedwordcases dwc JOIN ?_declinedword dc ON dwc.wordId = dc.id WHERE dwc.caseIdx = ?d AND dc.word = ?', $caseIdx, $word)) + if ($declWord = DB::Aowow()->selectCell('SELECT dwc.`word` FROM ?_declinedwordcases dwc JOIN ?_declinedword dc ON dwc.`wordId` = dc.`id` WHERE dwc.`caseIdx` = ?d AND dc.`word` = ?', $caseIdx, $word)) return $declWord; return $word; From c0097f3987d9b2d22cf4c37dfc982ec4c0a55ead Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Tue, 14 Oct 2025 16:07:10 +0200 Subject: [PATCH 051/260] Mapper/Objectives * fix display of item objectives by making LocString JsonSerializable --- includes/components/locstring.class.php | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/includes/components/locstring.class.php b/includes/components/locstring.class.php index 53da1fc9..2bfec006 100644 --- a/includes/components/locstring.class.php +++ b/includes/components/locstring.class.php @@ -6,7 +6,7 @@ if (!defined('AOWOW_REVISION')) die('illegal access'); -class LocString +class LocString implements \JsonSerializable { private \WeakMap $store; @@ -24,6 +24,11 @@ class LocString $this->store[$l] = (string)$callback($data[$key.'_loc'.$l->value] ?? ''); } + public function jsonSerialize() : string + { + return $this->__toString(); + } + public function __toString() : string { if ($str = $this->store[Lang::getLocale()]) From 37beaa2db5d92381afc3ca7243b316bbdb51e57c Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Tue, 14 Oct 2025 19:36:47 +0200 Subject: [PATCH 052/260] Filter/Errors * move checks to __construct so they can be run on $_POST data and don't create malformed filter urls * if we received malformed $_GET params, build new params and reload * do not store error state in cache * cleanup --- endpoints/achievements/achievements.php | 10 +- endpoints/areatriggers/areatriggers.php | 10 +- endpoints/arena-teams/arena-teams.php | 6 + endpoints/enchantments/enchantments.php | 11 +- endpoints/guilds/guilds.php | 6 + endpoints/icons/icons.php | 10 +- endpoints/items/items.php | 11 +- endpoints/itemsets/itemsets.php | 10 +- endpoints/npcs/npcs.php | 10 +- endpoints/objects/objects.php | 10 +- endpoints/profiles/profiles.php | 10 +- endpoints/quests/quests.php | 10 +- endpoints/sounds/sounds.php | 11 +- endpoints/spells/spells.php | 10 +- includes/components/filter.class.php | 330 ++++++++++-------- .../components/frontend/listview.class.php | 4 +- includes/components/pagetemplate.class.php | 9 +- includes/dbtypes/profile.class.php | 4 +- 18 files changed, 280 insertions(+), 202 deletions(-) diff --git a/endpoints/achievements/achievements.php b/endpoints/achievements/achievements.php index fc82692c..58830919 100644 --- a/endpoints/achievements/achievements.php +++ b/endpoints/achievements/achievements.php @@ -54,6 +54,12 @@ class AchievementsBaseResponse extends TemplateResponse implements ICache $this->subCat = $pageParam !== '' ? '='.$pageParam : ''; $this->filter = new AchievementListFilter($this->_get['filter'] ?? '', ['parentCats' => $this->category]); + if ($this->filter->shouldReload) + { + $_SESSION['error']['fi'] = $this->filter::class; + $get = $this->filter->buildGETParam(); + $this->forward('?' . $this->pageName . $this->subCat . ($get ? '&filter=' . $get : '')); + } $this->filterError = $this->filter->error; } @@ -69,13 +75,9 @@ class AchievementsBaseResponse extends TemplateResponse implements ICache if ($this->category) $conditions[] = ['category', end($this->category)]; - $this->filter->evalCriteria(); - if ($fiCnd = $this->filter->getConditions()) $conditions[] = $fiCnd; - $this->filterError = $this->filter->error; // maybe the evalX() caused something - /*************/ /* Menu Path */ diff --git a/endpoints/areatriggers/areatriggers.php b/endpoints/areatriggers/areatriggers.php index 55ffa720..a2e03b2d 100644 --- a/endpoints/areatriggers/areatriggers.php +++ b/endpoints/areatriggers/areatriggers.php @@ -33,6 +33,12 @@ class AreatriggersBaseResponse extends TemplateResponse implements ICache parent::__construct($pageParam); $this->filter = new AreaTriggerListFilter($this->_get['filter'] ?? ''); + if ($this->filter->shouldReload) + { + $_SESSION['error']['fi'] = $this->filter::class; + $get = $this->filter->buildGETParam(); + $this->forward('?' . $this->pageName . ($get ? '&filter=' . $get : '')); + } $this->filterError = $this->filter->error; } @@ -40,8 +46,6 @@ class AreatriggersBaseResponse extends TemplateResponse implements ICache { $this->h1 = Util::ucFirst(Lang::game('areatriggers')); - $this->filter->evalCriteria(); - $fiForm = $this->filter->values; @@ -73,8 +77,6 @@ class AreatriggersBaseResponse extends TemplateResponse implements ICache if ($_ = $this->filter->getConditions()) $conditions[] = $_; - $this->filterError = $this->filter->error; // maybe the evalX() caused something - $tabData = []; $trigger = new AreaTriggerList($conditions, ['calcTotal' => true]); if (!$trigger->error) diff --git a/endpoints/arena-teams/arena-teams.php b/endpoints/arena-teams/arena-teams.php index 2b8dbf73..5160833e 100644 --- a/endpoints/arena-teams/arena-teams.php +++ b/endpoints/arena-teams/arena-teams.php @@ -53,6 +53,12 @@ class ArenateamsBaseResponse extends TemplateResponse implements IProfilerList $this->subCat = $pageParam !== '' ? '='.$pageParam : ''; $this->filter = new ArenaTeamListFilter($this->_get['filter'] ?? '', ['realms' => $realms]); + if ($this->filter->shouldReload) + { + $_SESSION['error']['fi'] = $this->filter::class; + $get = $this->filter->buildGETParam(); + $this->forward('?' . $this->pageName . $this->subCat . ($get ? '&filter=' . $get : '')); + } $this->filterError = $this->filter->error; } diff --git a/endpoints/enchantments/enchantments.php b/endpoints/enchantments/enchantments.php index 2ca90fdb..2a2e80f0 100644 --- a/endpoints/enchantments/enchantments.php +++ b/endpoints/enchantments/enchantments.php @@ -35,6 +35,12 @@ class EnchantmentsBaseResponse extends TemplateResponse implements ICache $this->subCat = $pageParam !== '' ? '='.$pageParam : ''; $this->filter = new EnchantmentListFilter($this->_get['filter'] ?? '', ['parentCats' => $this->category]); + if ($this->filter->shouldReload) + { + $_SESSION['error']['fi'] = $this->filter::class; + $get = $this->filter->buildGETParam(); + $this->forward('?' . $this->pageName . $this->subCat . ($get ? '&filter=' . $get : '')); + } $this->filterError = $this->filter->error; } @@ -42,18 +48,13 @@ class EnchantmentsBaseResponse extends TemplateResponse implements ICache { $this->h1 = Util::ucFirst(Lang::game('enchantments')); - $this->filter->evalCriteria(); - $conditions = []; - if (!User::isInGroup(U_GROUP_EMPLOYEE)) $conditions[] = [['cuFlags', CUSTOM_EXCLUDE_FOR_LISTVIEW, '&'], 0]; if ($_ = $this->filter->getConditions()) $conditions[] = $_; - $this->filterError = $this->filter->error; // maybe the evalX() caused something - /**************/ /* Page Title */ diff --git a/endpoints/guilds/guilds.php b/endpoints/guilds/guilds.php index 4142461b..99108173 100644 --- a/endpoints/guilds/guilds.php +++ b/endpoints/guilds/guilds.php @@ -53,6 +53,12 @@ class GuildsBaseResponse extends TemplateResponse implements IProfilerList $this->subCat = $pageParam !== '' ? '='.$pageParam : ''; $this->filter = new GuildListFilter($this->_get['filter'] ?? '', ['realms' => $realms]); + if ($this->filter->shouldReload) + { + $_SESSION['error']['fi'] = $this->filter::class; + $get = $this->filter->buildGETParam(); + $this->forward('?' . $this->pageName . $this->subCat . ($get ? '&filter=' . $get : '')); + } $this->filterError = $this->filter->error; } diff --git a/endpoints/icons/icons.php b/endpoints/icons/icons.php index 7b2cf323..3ceab77a 100644 --- a/endpoints/icons/icons.php +++ b/endpoints/icons/icons.php @@ -32,6 +32,12 @@ class IconsBaseResponse extends TemplateResponse implements ICache $this->subCat = $pageParam !== '' ? '='.$pageParam : ''; $this->filter = new IconListFilter($this->_get['filter'] ?? '', ['parentCats' => $this->category]); + if ($this->filter->shouldReload) + { + $_SESSION['error']['fi'] = $this->filter::class; + $get = $this->filter->buildGETParam(); + $this->forward('?' . $this->pageName . $this->subCat . ($get ? '&filter=' . $get : '')); + } $this->filterError = $this->filter->error; } @@ -43,13 +49,9 @@ class IconsBaseResponse extends TemplateResponse implements ICache if (!User::isInGroup(U_GROUP_EMPLOYEE)) $conditions[] = [['cuFlags', CUSTOM_EXCLUDE_FOR_LISTVIEW, '&'], 0]; - $this->filter->evalCriteria(); - if ($_ = $this->filter->getConditions()) $conditions[] = $_; - $this->filterError = $this->filter->error; // maybe the evalX() caused something - /**************/ /* Page Title */ diff --git a/endpoints/items/items.php b/endpoints/items/items.php index 442f0c08..5c9f59be 100644 --- a/endpoints/items/items.php +++ b/endpoints/items/items.php @@ -99,6 +99,12 @@ class ItemsBaseResponse extends TemplateResponse implements ICache $this->subCat = $pageParam !== '' ? '='.$pageParam : ''; $this->filter = new ItemListFilter($this->_get['filter'] ?? '', ['parentCats' => $this->category]); + if ($this->filter->shouldReload) + { + $_SESSION['error']['fi'] = $this->filter::class; + $get = $this->filter->buildGETParam(); + $this->forward('?' . $this->pageName . $this->subCat . ($get ? '&filter=' . $get : '')); + } $this->filterError = $this->filter->error; } @@ -110,14 +116,9 @@ class ItemsBaseResponse extends TemplateResponse implements ICache if (!User::isInGroup(U_GROUP_EMPLOYEE)) $conditions[] = [['cuFlags', CUSTOM_EXCLUDE_FOR_LISTVIEW, '&'], 0]; - $this->filter->evalCriteria(); - $this->filter->evalWeights(); - if ($_ = $this->filter->getConditions()) $conditions[] = $_; - $this->filterError = $this->filter->error; // maybe the evalX() caused something - /*******************/ /* evaluate filter */ diff --git a/endpoints/itemsets/itemsets.php b/endpoints/itemsets/itemsets.php index d1905239..eecae8a4 100644 --- a/endpoints/itemsets/itemsets.php +++ b/endpoints/itemsets/itemsets.php @@ -32,6 +32,12 @@ class ItemsetsBaseResponse extends TemplateResponse implements ICache $this->subCat = $pageParam !== '' ? '='.$pageParam : ''; $this->filter = new ItemsetListFilter($this->_get['filter'] ?? '', ['parentCats' => $this->category]); + if ($this->filter->shouldReload) + { + $_SESSION['error']['fi'] = $this->filter::class; + $get = $this->filter->buildGETParam(); + $this->forward('?' . $this->pageName . $this->subCat . ($get ? '&filter=' . $get : '')); + } $this->filterError = $this->filter->error; } @@ -43,13 +49,9 @@ class ItemsetsBaseResponse extends TemplateResponse implements ICache if (!User::isInGroup(U_GROUP_EMPLOYEE)) $conditions[] = [['cuFlags', CUSTOM_EXCLUDE_FOR_LISTVIEW, '&'], 0]; - $this->filter->evalCriteria(); - if ($_ = $this->filter->getConditions()) $conditions[] = $_; - $this->filterError = $this->filter->error; // maybe the evalX() caused something - /*************/ /* Menu Path */ diff --git a/endpoints/npcs/npcs.php b/endpoints/npcs/npcs.php index 5a325144..6984cb45 100644 --- a/endpoints/npcs/npcs.php +++ b/endpoints/npcs/npcs.php @@ -35,6 +35,12 @@ class NpcsBaseResponse extends TemplateResponse implements ICache $this->subCat = $pageParam !== '' ? '='.$pageParam : ''; $this->filter = new CreatureListFilter($this->_get['filter'] ?? '', ['parentCats' => $this->category]); + if ($this->filter->shouldReload) + { + $_SESSION['error']['fi'] = $this->filter::class; + $get = $this->filter->buildGETParam(); + $this->forward('?' . $this->pageName . $this->subCat . ($get ? '&filter=' . $get : '')); + } $this->filterError = $this->filter->error; } @@ -46,13 +52,9 @@ class NpcsBaseResponse extends TemplateResponse implements ICache if (!User::isInGroup(U_GROUP_EMPLOYEE)) $conditions[] = [['cuFlags', CUSTOM_EXCLUDE_FOR_LISTVIEW, '&'], 0]; - $this->filter->evalCriteria(); - if ($_ = $this->filter->getConditions()) $conditions[] = $_; - $this->filterError = $this->filter->error; // maybe the evalX() caused something - if ($this->category) { $conditions[] = ['type', $this->category[0]]; diff --git a/endpoints/objects/objects.php b/endpoints/objects/objects.php index 00b3d463..0c726548 100644 --- a/endpoints/objects/objects.php +++ b/endpoints/objects/objects.php @@ -35,6 +35,12 @@ class ObjectsBaseResponse extends TemplateResponse implements ICache $this->subCat = $pageParam !== '' ? '='.$pageParam : ''; $this->filter = new GameObjectListFilter($this->_get['filter'] ?? '', ['parentCats' => $this->category]); + if ($this->filter->shouldReload) + { + $_SESSION['error']['fi'] = $this->filter::class; + $get = $this->filter->buildGETParam(); + $this->forward('?' . $this->pageName . $this->subCat . ($get ? '&filter=' . $get : '')); + } $this->filterError = $this->filter->error; } @@ -46,16 +52,12 @@ class ObjectsBaseResponse extends TemplateResponse implements ICache if (!User::isInGroup(U_GROUP_EMPLOYEE)) $conditions[] = [['cuFlags', CUSTOM_EXCLUDE_FOR_LISTVIEW, '&'], 0]; - $this->filter->evalCriteria(); - if ($_ = $this->filter->getConditions()) $conditions[] = $_; if ($this->category) $conditions[] = ['typeCat', (int)$this->category[0]]; - $this->filterError = $this->filter->error; // maybe the evalX() caused something - /*************/ /* Menu Path */ diff --git a/endpoints/profiles/profiles.php b/endpoints/profiles/profiles.php index e3d4dd78..53d3c37b 100644 --- a/endpoints/profiles/profiles.php +++ b/endpoints/profiles/profiles.php @@ -57,6 +57,12 @@ class ProfilesBaseResponse extends TemplateResponse implements IProfilerList $this->subCat = $pageParam !== '' ? '='.$pageParam : ''; $this->filter = new ProfileListFilter($this->_get['filter'] ?? '', ['realms' => $realms]); + if ($this->filter->shouldReload) + { + $_SESSION['error']['fi'] = $this->filter::class; + $get = $this->filter->buildGETParam(); + $this->forward('?' . $this->pageName . $this->subCat . ($get ? '&filter=' . $get : '')); + } $this->filterError = $this->filter->error; } @@ -64,8 +70,6 @@ class ProfilesBaseResponse extends TemplateResponse implements IProfilerList { $this->h1 = Util::ucFirst(Lang::game('profiles')); - $this->filter->evalCriteria(); - /*************/ /* Menu Path */ @@ -94,8 +98,6 @@ class ProfilesBaseResponse extends TemplateResponse implements IProfilerList if ($_ = $this->filter->getConditions()) $conditions[] = $_; - $this->filterError = $this->filter->error; // maybe the evalX() caused something - $fiExtraCols = $this->filter->fiExtraCols; $lvData = []; diff --git a/endpoints/quests/quests.php b/endpoints/quests/quests.php index 78fac93e..562132a2 100644 --- a/endpoints/quests/quests.php +++ b/endpoints/quests/quests.php @@ -53,6 +53,12 @@ class QuestsBaseResponse extends TemplateResponse implements ICache $this->subCat = $pageParam !== '' ? '='.$pageParam : ''; $this->filter = new QuestListFilter($this->_get['filter'] ?? '', ['parentCats' => $this->category]); + if ($this->filter->shouldReload) + { + $_SESSION['error']['fi'] = $this->filter::class; + $get = $this->filter->buildGETParam(); + $this->forward('?' . $this->pageName . $this->subCat . ($get ? '&filter=' . $get : '')); + } $this->filterError = $this->filter->error; } @@ -64,13 +70,9 @@ class QuestsBaseResponse extends TemplateResponse implements ICache if (!User::isInGroup(U_GROUP_EMPLOYEE)) $conditions[] = [['cuFlags', CUSTOM_EXCLUDE_FOR_LISTVIEW, '&'], 0]; - $this->filter->evalCriteria(); - if ($_ = $this->filter->getConditions()) $conditions[] = $_; - $this->filterError = $this->filter->error; // maybe the evalX() caused something - if (isset($this->category[1])) $conditions[] = ['zoneOrSort', $this->category[1]]; else if (isset($this->category[0])) diff --git a/endpoints/sounds/sounds.php b/endpoints/sounds/sounds.php index 9132c40f..d9f3e7f5 100644 --- a/endpoints/sounds/sounds.php +++ b/endpoints/sounds/sounds.php @@ -34,6 +34,12 @@ class SoundsBaseResponse extends TemplateResponse implements ICache $this->subCat = $pageParam !== '' ? '='.$pageParam : ''; $this->filter = new SoundListFilter($this->_get['filter'] ?? '', ['parentCats' => $this->category]); + if ($this->filter->shouldReload) + { + $_SESSION['error']['fi'] = $this->filter::class; + $get = $this->filter->buildGETParam(); + $this->forward('?' . $this->pageName . $this->subCat . ($get ? '&filter=' . $get : '')); + } $this->filterError = $this->filter->error; } @@ -41,18 +47,13 @@ class SoundsBaseResponse extends TemplateResponse implements ICache { $this->h1 = Util::ucFirst(Lang::game('sounds')); - $this->filter->evalCriteria(); - $conditions = []; - if (!User::isInGroup(U_GROUP_EMPLOYEE)) $conditions[] = [['cuFlags', CUSTOM_EXCLUDE_FOR_LISTVIEW, '&'], 0]; if ($_ = $this->filter->getConditions()) $conditions[] = $_; - $this->filterError = $this->filter->error; // maybe the evalX() caused something - /**************/ /* Page Title */ diff --git a/endpoints/spells/spells.php b/endpoints/spells/spells.php index fa6c4533..968e9897 100644 --- a/endpoints/spells/spells.php +++ b/endpoints/spells/spells.php @@ -99,6 +99,12 @@ class SpellsBaseResponse extends TemplateResponse implements ICache $this->subCat = $pageParam !== '' ? '='.$pageParam : ''; $this->filter = new SpellListFilter($this->_get['filter'] ?? '', ['parentCats' => $this->category]); + if ($this->filter->shouldReload) + { + $_SESSION['error']['fi'] = $this->filter::class; + $get = $this->filter->buildGETParam(); + $this->forward('?' . $this->pageName . $this->subCat . ($get ? '&filter=' . $get : '')); + } $this->filterError = $this->filter->error; } @@ -110,13 +116,9 @@ class SpellsBaseResponse extends TemplateResponse implements ICache if (!User::isInGroup(U_GROUP_EMPLOYEE)) $conditions[] = [['cuFlags', CUSTOM_EXCLUDE_FOR_LISTVIEW, '&'], 0]; - $this->filter->evalCriteria(); - if ($_ = $this->filter->getConditions()) $conditions[] = $_; - $this->filterError = $this->filter->error; // maybe the evalX() caused something - /*************/ /* Menu Path */ diff --git a/includes/components/filter.class.php b/includes/components/filter.class.php index c9927cd5..c58117d0 100644 --- a/includes/components/filter.class.php +++ b/includes/components/filter.class.php @@ -91,12 +91,13 @@ abstract class Filter protected const ENUM_RACE = array( null, 1, 2, 3, 4, 5, 6, 7, 8, null, 10, 11, true, false); protected const ENUM_PROFESSION = array( null, 171, 164, 185, 333, 202, 129, 755, 165, 186, 197, true, false, 356, 182, 773); - public bool $error = false; // erroneous search fields + public bool $error = false; + public bool $shouldReload = false; // erroneous params have been corrected. Build GET string and reload // item related - public array $upgrades = []; // [itemId => slotId] - public array $extraOpts = []; // score for statWeights - public array $wtCnd = []; // DBType condition for statWeights + public array $upgrades = []; // [itemId => slotId] + public array $extraOpts = []; // score for statWeights + public array $wtCnd = []; // DBType condition for statWeights private array $cndSet = []; // db type query storage private array $rawData = []; @@ -106,7 +107,7 @@ abstract class Filter [self::CR_FLAG, , , ] # default param2: matchExact [self::CR_NUMERIC, , , ] [self::CR_STRING, , , null] - [self::CR_ENUM, , , ] # param3 ? crv is val in enum : key in enum + [self::CR_ENUM, , , ] # param3 ? crv is val in enum : key in enum [self::CR_STAFFFLAG, , null, null] [self::CR_CALLBACK, , , ] [self::CR_NYI_PH, null, , param2] # mostly 1: to ignore this criterium; 0: to fail the whole query @@ -126,8 +127,7 @@ abstract class Filter public array $fiReputationCols = []; // fn params ([[factionId, factionName], ...]) public array $fiExtraCols = []; // public string $query = ''; // as in url query params - public array $values = []; // old fiData['v'] - public array $criteria = []; // old fiData['c'] + public array $values = []; // prefiltered rawData // parse the provided request into a usable format public function __construct(string|array $data, array $opts = []) @@ -157,6 +157,8 @@ abstract class Filter } $this->initFields(); + $this->evalCriteria(); + $this->evalWeights(); } public function mergeCat(array &$cats) : void @@ -167,13 +169,13 @@ abstract class Filter private function &criteriaIterator() : \Generator { - if (!$this->criteria) + if (empty($this->values['cr'])) return; - for ($i = 0; $i < count($this->criteria['cr']); $i++) + for ($i = 0; $i < count($this->values['cr']); $i++) { // throws a notice if yielded directly "Only variable references should be yielded by reference" - $v = [&$this->criteria['cr'][$i], &$this->criteria['crs'][$i], &$this->criteria['crv'][$i]]; + $v = [&$this->values['cr'][$i], &$this->values['crs'][$i], &$this->values['crv'][$i]]; yield $i => $v; } } @@ -195,7 +197,7 @@ abstract class Filter public function buildGETParam(array $override = [], array $addCr = []) : string { $get = []; - foreach (array_merge($this->criteria, $this->values, $override) as $k => $v) + foreach (array_merge($this->values, $override) as $k => $v) { if (isset($addCr[$k])) { @@ -228,7 +230,7 @@ abstract class Filter $this->cndSet = $this->createSQLForValues(); // criteria - foreach ($this->criteriaIterator() as &$_cr) + foreach ($this->criteriaIterator() as $_cr) if ($cnd = $this->createSQLForCriterium(...$_cr)) $this->cndSet[] = $cnd; @@ -241,10 +243,10 @@ abstract class Filter public function getSetCriteria(int ...$cr) : array { - if (!$cr || !$this->fiSetCriteria) - return $this->fiSetCriteria; + if (!$cr || empty($this->values['cr'])) + return []; - return array_values(array_intersect($this->fiSetCriteria['cr'], $cr)); + return array_values(array_intersect($this->values['cr'], $cr)); } @@ -258,12 +260,17 @@ abstract class Filter return []; $data = []; + + // someone copy/pasted a WH filter + $get = preg_replace('/^(\d+(:\d+)*);(\d+(:\d+)*);(\P{C}+(:\P{C}+)*)$/', 'cr=$1;crs=$3;crv=$5', $get); + foreach (explode(';', $get) as $field) { if (!strstr($field, '=')) { trigger_error('Filter::transformGET - malformed GET string', E_USER_NOTICE); - $this->error = true; + $this->error = + $this->shouldReload = true; continue; } @@ -272,7 +279,8 @@ abstract class Filter if (!isset(static::$inputFields[$k])) { trigger_error('Filter::transformGET - GET param not in filter: '.$k, E_USER_NOTICE); - $this->error = true; + $this->error = + $this->shouldReload = true; continue; } @@ -286,13 +294,17 @@ abstract class Filter private function initFields() : void { + // quirk: in the POST step criteria will be [[''], null, null] if no criteria are selected, + // due to the first criteria selector always being visible + if (($this->rawData['cr'] ?? null) === [''] && !isset($this->rawData['crs']) && !isset($this->rawData['crv'])) + unset($this->rawData['cr']); // unset or Filter::checkInput() screams bloody error + + $cleanupCr = []; foreach (static::$inputFields as $inp => [$type, $valid, $asArray]) { - $var = in_array($inp, ['cr', 'crs', 'crv']) ? 'criteria' : 'values'; - if (!isset($this->rawData[$inp]) || $this->rawData[$inp] === '') { - $this->$var[$inp] = $asArray ? [] : null; + $this->values[$inp] = $asArray ? [] : null; continue; } @@ -300,37 +312,57 @@ abstract class Filter if ($asArray) { - // quirk: in the POST step criteria can be [[''], null, null] if not selected. $buff = []; - foreach ((array)$val as $v) // can be string|int in POST step if only one value present - if ($v !== '' && $this->checkInput($type, $valid, $v)) + foreach ((array)$val as $i => $v) // can be string|int in POST step if only one value present + { + if (in_array($inp, ['cr', 'crs', 'crv'])) + { + if (!$this->checkInput($type, $valid, $v)) + $cleanupCr[] = $i; + $buff[] = $v; // always assign, gets removed later as tuple + } + else if ($this->checkInput($type, $valid, $v)) $buff[] = $v; + } - $this->$var[$inp] = $buff; + $this->values[$inp] = $buff; } else - $this->$var[$inp] = $this->checkInput($type, $valid, $val) ? $val : null; + $this->values[$inp] = $this->checkInput($type, $valid, $val) ? $val : null; + } + + if ($cleanupCr) + { + $this->error = + $this->shouldReload = true; + + foreach (array_unique($cleanupCr) as $i) + unset($this->values['cr'][$i], $this->values['crs'][$i], $this->values['crv'][$i]); + + $this->values['cr'] = array_values($this->values['cr']); + $this->values['crs'] = array_values($this->values['crs']); + $this->values['crv'] = array_values($this->values['crv']); } } - public function evalCriteria() : void // [cr]iterium, [cr].[s]ign, [cr].[v]alue + private function evalCriteria() : void // [cr]iterium, [cr].[s]ign, [cr].[v]alue { - if (empty($this->criteria['cr']) && empty($this->criteria['crs']) && empty($this->criteria['crv'])) + if (empty($this->values['cr']) && empty($this->values['crs']) && empty($this->values['crv'])) return; - else if (empty($this->criteria['cr']) || empty($this->criteria['crs']) || empty($this->criteria['crv'])) - { - unset($this->criteria['cr']); - unset($this->criteria['crs']); - unset($this->criteria['crv']); - trigger_error('Filter::setCriteria - one of cr, crs, crv is missing', E_USER_NOTICE); - $this->error = true; + if (empty($this->values['cr']) || empty($this->values['crs']) || empty($this->values['crv'])) + { + trigger_error('Filter::evalCriteria - one of cr, crs, crv is missing', E_USER_NOTICE); + unset($this->values['cr'], $this->values['crs'], $this->values['crv']); + + $this->error = + $this->shouldReload = true; return; } - $_cr = &$this->criteria['cr']; - $_crs = &$this->criteria['crs']; - $_crv = &$this->criteria['crv']; + $_cr = &$this->values['cr']; + $_crs = &$this->values['crs']; + $_crv = &$this->values['crv']; if (count($_cr) != count($_crv) || count($_cr) != count($_crs) || count($_cr) > 5 || count($_crs) > 5 /*|| count($_crv) > 5*/) { @@ -345,70 +377,88 @@ abstract class Filter if (count($_crs) > $min) array_splice($_crs, $min); - trigger_error('Filter::setCriteria - cr, crs, crv are imbalanced', E_USER_NOTICE); - $this->error = true; + trigger_error('Filter::evalCriteria - cr, crs, crv are imbalanced', E_USER_NOTICE); + $this->error = + $this->shouldReload = true; } for ($i = 0; $i < count($_cr); $i++) { - // conduct filter specific checks & casts here - $unsetme = false; - if (isset(static::$genericFilter[$_cr[$i]])) + if (!isset(static::$genericFilter[$_cr[$i]]) || $_crs[$i] === '' || $_crv[$i] === '') { - $gf = static::$genericFilter[$_cr[$i]]; - switch ($gf[0]) - { - case self::CR_NUMERIC: - $_ = $_crs[$i]; - if (!Util::checkNumeric($_crv[$i], $gf[2]) || !$this->int2Op($_)) - $unsetme = true; - break; - case self::CR_BOOLEAN: - case self::CR_FLAG: - $_ = $_crs[$i]; - if (!$this->int2Bool($_)) - $unsetme = true; - break; - case self::CR_ENUM: - case self::CR_STAFFFLAG: - if (!Util::checkNumeric($_crs[$i], NUM_CAST_INT)) - $unsetme = true; - break; - } + if ($_crs[$i] === '' || $_crv[$i] === '') + trigger_error('Filter::evalCriteria - received malformed criterium ["'.$_cr[$i].'", "'.$_crs[$i].'", "'.$_crv[$i].'"]', E_USER_NOTICE); + else + trigger_error('Filter::evalCriteria - received unhandled criterium: '.$_cr[$i], E_USER_NOTICE); + + unset($_cr[$i], $_crs[$i], $_crv[$i]); + + $this->error = + $this->shouldReload = true; + continue; } - if (!$unsetme && intval($_cr[$i]) && $_crs[$i] !== '' && $_crv[$i] !== '') - continue; + [$crType, $colOrFn, $param1, $param2] = array_pad(static::$genericFilter[$_cr[$i]], 4, null); - unset($_cr[$i]); - unset($_crs[$i]); - unset($_crv[$i]); + // conduct filter specific checks & casts here + switch ($crType) + { + case self::CR_NUMERIC: + $_ = $_crs[$i]; + if (Util::checkNumeric($_crv[$i], $param1) && $this->int2Op($_)) + continue 2; + break; + case self::CR_BOOLEAN: + case self::CR_FLAG: + $_ = $_crs[$i]; + if ($this->int2Bool($_)) + continue 2; + break; + case self::CR_STAFFFLAG: + if (User::isInGroup(U_GROUP_EMPLOYEE) && Util::checkNumeric($_crs[$i], NUM_CAST_INT)) + continue 2; + break; + case self::CR_ENUM: + if (Util::checkNumeric($_crs[$i], NUM_CAST_INT) && ( + (!$param2 && isset(static::$enums[$_cr[$i]][$_crs[$i]])) || + ($param2 && in_array($_crs[$i], static::$enums[$_cr[$i]])) || + ($param1 && ($_crs[$i] == self::ENUM_ANY || $_crs[$i] == self::ENUM_NONE)) + )) + continue 2; + break; + case self::CR_STRING: + case self::CR_CALLBACK: + case self::CR_NYI_PH: + continue 2; + default: + trigger_error('Filter::evalCriteria - unknown criteria type: '.$crType, E_USER_WARNING); + break; + } - trigger_error('Filter::setCriteria - generic check failed ["'.$_cr[$i].'", "'.$_crs[$i].'", "'.$_crv[$i].'"]', E_USER_NOTICE); - $this->error = true; + trigger_error('Filter::evalCriteria - generic check failed ["'.$_cr[$i].'", "'.$_crs[$i].'", "'.$_crv[$i].'"]', E_USER_NOTICE); + unset($_cr[$i], $_crs[$i], $_crv[$i]); + + $this->error = + $this->shouldReload = true; } - $this->fiSetCriteria = array( - 'cr' => $_cr, - 'crs' => $_crs, - 'crv' => $_crv - ); + $this->fiSetCriteria = [$_cr, $_crs, $_crv]; } - public function evalWeights() : void + private function evalWeights() : void { // both empty: not in use; not an error - if (!$this->values['wt'] && !$this->values['wtv']) + if (empty($this->values['wt']) && empty($this->values['wtv'])) return; // one empty: erroneous manual input? if (!$this->values['wt'] || !$this->values['wtv']) { - unset($this->values['wt']); - unset($this->values['wtv']); - trigger_error('Filter::setWeights - one of wt, wtv is missing', E_USER_NOTICE); - $this->error = true; + unset($this->values['wt'], $this->values['wtv']); + + $this->error = + $this->shouldReload = true; return; } @@ -421,7 +471,8 @@ abstract class Filter if ($nwt != $nwtv) { trigger_error('Filter::setWeights - wt, wtv are imbalanced', E_USER_NOTICE); - $this->error = true; + $this->error = + $this->shouldReload = true; } if ($nwt > $nwtv) @@ -649,83 +700,72 @@ abstract class Filter return null; } - private function genericCriterion(int $cr, int $crs, string $crv) : ?array - { - [$crType, $colOrFn, $param1, $param2] = array_pad(static::$genericFilter[$cr], 4, null); - $result = null; - - switch ($crType) - { - case self::CR_NUMERIC: - $result = $this->genericNumeric($colOrFn, $crv, $crs, $param1); - break; - case self::CR_FLAG: - $result = $this->genericBooleanFlags($colOrFn, $param1, $crs, $param2); - break; - case self::CR_STAFFFLAG: - if (User::isInGroup(U_GROUP_EMPLOYEE) && $crs > 0) - $result = $this->genericBooleanFlags($colOrFn, (1 << ($crs - 1)), true); - break; - case self::CR_BOOLEAN: - $result = $this->genericBoolean($colOrFn, $crs, !empty($param1)); - break; - case self::CR_STRING: - $result = $this->genericString($colOrFn, $crv, $param1); - break; - case self::CR_ENUM: - if (!$param2 && isset(static::$enums[$cr][$crs])) - $result = $this->genericEnum($colOrFn, static::$enums[$cr][$crs]); - if ($param2 && in_array($crs, static::$enums[$cr])) - $result = $this->genericEnum($colOrFn, $crs); - else if ($param1 && ($crs == self::ENUM_ANY || $crs == self::ENUM_NONE)) - $result = $this->genericEnum($colOrFn, $crs); - break; - case self::CR_CALLBACK: - $result = $this->{$colOrFn}($cr, $crs, $crv, $param1, $param2); - break; - case self::CR_NYI_PH: // do not limit with not implemented filters - if (is_int($param1)) - return [$param1]; - - // for nonsensical values; compare against 0 - if ($this->int2Op($crs) && Util::checkNumeric($crv)) - { - if ($crs == '=') - $crs = '=='; - - return eval('return ('.$crv.' '.$crs.' 0);') ? [1] : [0]; - } - else - return [0]; - } - - if ($result && $crType == self::CR_NUMERIC && !empty($param2)) - $this->fiExtraCols[] = $cr; - - return $result; - } - /***********************************/ /* create conditions from */ /* non-generic values and criteria */ /***********************************/ - protected function createSQLForCriterium(int &$cr, int &$crs, string &$crv) : array + protected function createSQLForCriterium(int $cr, int $crs, string $crv) : array { if (!static::$genericFilter) // criteria not in use - no error return []; - if (isset(static::$genericFilter[$cr])) - if ($genCr = $this->genericCriterion($cr, $crs, $crv)) - return $genCr; + [$crType, $colOrFn, $param1, $param2] = array_pad(static::$genericFilter[$cr], 4, null); - trigger_error('Filter::createSQLForCriterium - received unhandled criterium: ["'.$cr.'", "'.$crs.'", "'.$crv.'"]', E_USER_NOTICE); - $this->error = true; + $handleEnum = function(int $cr, int $crs, string $col, bool $hasAnyNone, bool $crsAsVal) : ?array + { + if ($hasAnyNone && ($crs == self::ENUM_ANY || $crs == self::ENUM_NONE)) + return $this->genericEnum($col, $crs); + else if (!$crsAsVal && isset(static::$enums[$cr][$crs])) + return $this->genericEnum($col, static::$enums[$cr][$crs]); + else if ($crsAsVal && in_array($crs, static::$enums[$cr])) + return $this->genericEnum($col, $crs); - unset($cr, $crs, $crv); + return null; + }; - return []; + $handleNYIPH = function(int $crs, string $crv, ?int $forceResult) : ?array + { + if (is_int($forceResult)) + return [$forceResult]; + + // for nonsensical values; compare against 0 + if ($this->int2Op($crs) && Util::checkNumeric($crv)) + { + if ($crs == '=') + $crs = '=='; + + return eval('return ('.$crv.' '.$crs.' 0);') ? [1] : [0]; + } + else + return [0]; + }; + + $result = match ($crType) + { + self::CR_NUMERIC => $this->genericNumeric($colOrFn, $crv, $crs, $param1), + self::CR_FLAG => $this->genericBooleanFlags($colOrFn, $param1, $crs, $param2), + self::CR_STAFFFLAG => $this->genericBooleanFlags($colOrFn, (1 << ($crs - 1)), true), + self::CR_BOOLEAN => $this->genericBoolean($colOrFn, $crs, !empty($param1)), + self::CR_STRING => $this->genericString($colOrFn, $crv, $param1), + self::CR_CALLBACK => $this->{$colOrFn}($cr, $crs, $crv, $param1, $param2), + self::CR_ENUM => $handleEnum($cr, $crs, $colOrFn, $param1, $param2), + self::CR_NYI_PH => $handleNYIPH($crs, $crv, $param1), + default => null + }; + + if (!$result) + { + // this really should not have happened. The relevant checks are run on __construct() + trigger_error('Filter::createSQLForCriterium - failed to resolve criterium: ["'.$cr.'", "'.$crs.'", "'.$crv.'"]', E_USER_WARNING); + return []; + } + + if ($crType == self::CR_NUMERIC && !empty($param2)) + $this->fiExtraCols[] = $cr; + + return $result; } abstract protected function createSQLForValues() : array; diff --git a/includes/components/frontend/listview.class.php b/includes/components/frontend/listview.class.php index 7180d6ba..9520d306 100644 --- a/includes/components/frontend/listview.class.php +++ b/includes/components/frontend/listview.class.php @@ -146,9 +146,9 @@ class Listview implements \JsonSerializable $this->tabs = $tabVar; } - public function setError() : void + public function setError(bool $enable) : void { - $this->_errors = 1; + $this->_errors = $enable ? 1 : null; } public function jsonSerialize() : array diff --git a/includes/components/pagetemplate.class.php b/includes/components/pagetemplate.class.php index 7eb11c8c..26822561 100644 --- a/includes/components/pagetemplate.class.php +++ b/includes/components/pagetemplate.class.php @@ -336,7 +336,7 @@ class PageTemplate $result[] = "var fi_type = '".$this->filter->fiType."'"; if ($this->filter->fiSetCriteria) // arr:criteria, arr:signs, arr:values - $result[] = 'fi_setCriteria('.mb_substr(Util::toJSON(array_values($this->filter->fiSetCriteria)), 1, -1).");"; + $result[] = 'fi_setCriteria('.mb_substr(Util::toJSON($this->filter->fiSetCriteria), 1, -1).");"; /* nt: don't try to match provided weights on predefined weight sets (preselects preset from opt list and ..?) @@ -496,7 +496,7 @@ class PageTemplate foreach ($this->lvTabs->iterate() as $lv) if ($lv instanceof \Aowow\Listview) - $lv->setError(); + $lv->setError(true); } // pre-serialization: if a var is relevant it was stored in $rawData @@ -505,6 +505,11 @@ class PageTemplate $this->context = null; // unlink from TemplateResponse $this->pageData = []; // clear modified data + if ($this->lvTabs) // do not store lvErrors in cache + foreach ($this->lvTabs->iterate() as $lv) + if ($lv instanceof \Aowow\Listview) + $lv->setError(false); + $vars = []; foreach ($this as $k => $_) $vars[] = $k; diff --git a/includes/dbtypes/profile.class.php b/includes/dbtypes/profile.class.php index 8aa712b1..cf75fcb1 100644 --- a/includes/dbtypes/profile.class.php +++ b/includes/dbtypes/profile.class.php @@ -269,8 +269,8 @@ class ProfileListFilter extends Filter { parent::__construct($data, $opts); - if (!empty($this->criteria['cr'])) - if (array_intersect($this->criteria['cr'], [2, 5, 6, 7, 21])) + if (!empty($this->values['cr'])) + if (array_intersect($this->values['cr'], [2, 5, 6, 7, 21])) $this->useLocalList = true; } From a275955ee357d0236a6ca0ff9ea3d579d38e6f27 Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Wed, 15 Oct 2025 00:24:40 +0200 Subject: [PATCH 053/260] Admin/Config * handle Ajax errors --- template/pages/admin/siteconfig.tpl.php | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/template/pages/admin/siteconfig.tpl.php b/template/pages/admin/siteconfig.tpl.php index 70324333..048355ba 100644 --- a/template/pages/admin/siteconfig.tpl.php +++ b/template/pages/admin/siteconfig.tpl.php @@ -143,7 +143,10 @@ else { $WH.ae(_status, createStatusIcon(xhr.responseText)); } - + }, + onFailure: function(xhr) { + $WH.ee(_status); + $WH.ae(_status, createStatusIcon(xhr.status == 403 ? 'Forbidden' : 'Failed to add Cfg')); } }); } @@ -204,6 +207,10 @@ onSuccess: function(xhr) { $WH.ee(_status); $WH.ae(_status, createStatusIcon(xhr.responseText)); + }, + onFailure: function(xhr) { + $WH.ee(_status); + $WH.ae(_status, createStatusIcon(xhr.status == 403 ? 'Forbidden' : 'Failed to update Cfg')); } }); } @@ -254,7 +261,10 @@ $WH.ee(_status); $WH.ae(_status, createStatusIcon(xhr.responseText)); } - + }, + onFailure: function(xhr) { + $WH.ee(_status); + $WH.ae(_status, createStatusIcon(xhr.status == 403 ? 'Forbidden' : 'Failed to remove Cfg')); } }); } From 95918c04102238e6c184e07f526e5ff48854bd53 Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Wed, 15 Oct 2025 01:06:00 +0200 Subject: [PATCH 054/260] SoundDetailPage/Fixup * fix exception when assigning WorldState conditions to the zones tab --- endpoints/sound/sound.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/endpoints/sound/sound.php b/endpoints/sound/sound.php index e2512285..b88da8ef 100644 --- a/endpoints/sound/sound.php +++ b/endpoints/sound/sound.php @@ -212,7 +212,7 @@ class SoundBaseResponse extends TemplateResponse implements ICache } } - if ($worldStates = array_filter($zoneIds, function ($x) { return $x['worldStateId'] > 0; })) + if ($worldStates = array_filter($zoneIds, fn($x) => $x['worldStateId'] > 0)) { $tabData['extraCols'] = ['$Listview.extraCols.condition']; @@ -222,7 +222,7 @@ class SoundBaseResponse extends TemplateResponse implements ICache Conditions::extendListviewRow($zoneData[$state['id']], Conditions::SRC_NONE, $this->typeId, [Conditions::WORLD_STATE, $state['worldStateId'], $state['worldStateValue']]); else foreach ($zoneData as &$d) - if (in_array($state['id'], $d['subzones'])) + if (in_array($state['id'], $d['subzones'] ?? [])) Conditions::extendListviewRow($d, Conditions::SRC_NONE, $this->typeId, [Conditions::WORLD_STATE, $state['worldStateId'], $state['worldStateValue']]); } } From 7d8b524478b7ab4fbb10dc7b8fc7be6d433282c7 Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Wed, 15 Oct 2025 21:51:04 +0200 Subject: [PATCH 055/260] Listview/Cost * do not append 0 achievementpoints to cost builder. It will be displayed --- setup/sql/updates/1760557948_01.sql | 1 + setup/tools/filegen/templates/global.js/listview.js | 3 +-- 2 files changed, 2 insertions(+), 2 deletions(-) create mode 100644 setup/sql/updates/1760557948_01.sql diff --git a/setup/sql/updates/1760557948_01.sql b/setup/sql/updates/1760557948_01.sql new file mode 100644 index 00000000..03adf418 --- /dev/null +++ b/setup/sql/updates/1760557948_01.sql @@ -0,0 +1 @@ +UPDATE `aowow_dbversion` SET `build` = CONCAT(IFNULL(`build`, ''), ' globaljs'); diff --git a/setup/tools/filegen/templates/global.js/listview.js b/setup/tools/filegen/templates/global.js/listview.js index 6720feaf..25038cc9 100644 --- a/setup/tools/filegen/templates/global.js/listview.js +++ b/setup/tools/filegen/templates/global.js/listview.js @@ -2572,7 +2572,6 @@ Listview.extraCols = { var side = null; var items = row.cost[2]; var currency = row.cost[1]; - var achievementPoints = 0; if (row.side != null) side = row.side; @@ -2584,7 +2583,7 @@ Listview.extraCols = { side = 2; } - Listview.funcBox.appendMoney(td, money, side, items, currency, achievementPoints); + Listview.funcBox.appendMoney(td, money, side, items, currency); } }, sortFunc: function(a, b, col) From 830edb8265a9f43775b99b84d15f3d25243e9d18 Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Thu, 16 Oct 2025 02:04:25 +0200 Subject: [PATCH 056/260] PageTemplate/Fixup * use get_object_vars() instead of property_exists() to test if we can load a variable from provided context. The former only returns accessible vars while the latter returns true for all properties. --- includes/components/pagetemplate.class.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/components/pagetemplate.class.php b/includes/components/pagetemplate.class.php index 26822561..ae216407 100644 --- a/includes/components/pagetemplate.class.php +++ b/includes/components/pagetemplate.class.php @@ -541,7 +541,7 @@ class PageTemplate if (!$this->context) return null; - if (!property_exists($this->context, $var)) + if (!isset(get_object_vars($this->context)[$var])) return null; $this->rawData[$var] = $this->context->$var; From 176cf137fbaaee2f13adc6998dff9249caf06d7d Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Fri, 17 Oct 2025 14:32:58 +0200 Subject: [PATCH 057/260] Filter/Fixup * criteria parameters can be placeholder/null --- includes/components/filter.class.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/components/filter.class.php b/includes/components/filter.class.php index c58117d0..eb79af3d 100644 --- a/includes/components/filter.class.php +++ b/includes/components/filter.class.php @@ -713,7 +713,7 @@ abstract class Filter [$crType, $colOrFn, $param1, $param2] = array_pad(static::$genericFilter[$cr], 4, null); - $handleEnum = function(int $cr, int $crs, string $col, bool $hasAnyNone, bool $crsAsVal) : ?array + $handleEnum = function(int $cr, int $crs, string $col, ?bool $hasAnyNone, ?bool $crsAsVal) : ?array { if ($hasAnyNone && ($crs == self::ENUM_ANY || $crs == self::ENUM_NONE)) return $this->genericEnum($col, $crs); From 4d421d2bbbcf259b001302427cd68942bca0eaeb Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Sat, 18 Oct 2025 16:46:25 +0200 Subject: [PATCH 058/260] Filter/Errors * handle stat weights quirk, analogous to the criteria quirk --- includes/components/filter.class.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/includes/components/filter.class.php b/includes/components/filter.class.php index eb79af3d..27746d05 100644 --- a/includes/components/filter.class.php +++ b/includes/components/filter.class.php @@ -299,6 +299,10 @@ abstract class Filter if (($this->rawData['cr'] ?? null) === [''] && !isset($this->rawData['crs']) && !isset($this->rawData['crv'])) unset($this->rawData['cr']); // unset or Filter::checkInput() screams bloody error + // same for stat weights on ItemListFilter + if ($this instanceof ItemListFilter && ($this->rawData['wt'] ?? null) === [''] && !isset($this->rawData['wtv'])) + unset($this->rawData['wt']); + $cleanupCr = []; foreach (static::$inputFields as $inp => [$type, $valid, $asArray]) { From 6a32c770cd279e462cfa6c22bde41322bad35c59 Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Sun, 19 Oct 2025 22:42:50 +0200 Subject: [PATCH 059/260] Profiler/Pets * catch error case where a player owns a pet that is no longer tameable/has no pet family --- includes/components/profiler.class.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/includes/components/profiler.class.php b/includes/components/profiler.class.php index 943a986d..37a30e24 100644 --- a/includes/components/profiler.class.php +++ b/includes/components/profiler.class.php @@ -619,6 +619,12 @@ class Profiler $petData['entry'] ); + if (!$morePet) + { + trigger_error('char #'.$charGuid.' on realm #'.$realmId.' owns pet #'.$petGuid.' (creature entry: #'.$petData['entry'].') without pet family. skipping...', E_USER_WARNING); + continue; + } + $_ = DB::Aowow()->selectCol('SELECT `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, From 2e029f3d966de2bfece178892798c5c821c34452 Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Sun, 19 Oct 2025 23:19:51 +0200 Subject: [PATCH 060/260] Profiler/Talents * fix building talent string for hunter pets. the alternate spells (e.g. Dash & Swoop) must both be included * align talent order in build scripts talenticons, talentcalc with Profiler talent string builder * fix Shamans gaining 5% Parry by talent Spirit Weapons * cleanup --- includes/components/profiler.class.php | 11 ++- includes/dbtypes/spell.class.php | 95 ++++++++++++-------------- setup/sql/01-db_structure.sql | 2 +- setup/sql/updates/1760911493_01.sql | 7 ++ setup/tools/filegen/talentcalc.ss.php | 2 +- setup/tools/filegen/talenticons.ss.php | 2 +- 6 files changed, 65 insertions(+), 54 deletions(-) create mode 100644 setup/sql/updates/1760911493_01.sql diff --git a/includes/components/profiler.class.php b/includes/components/profiler.class.php index 37a30e24..91240710 100644 --- a/includes/components/profiler.class.php +++ b/includes/components/profiler.class.php @@ -625,7 +625,16 @@ class Profiler continue; } - $_ = 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']); + $_ = DB::Aowow()->selectCol( + 'SELECT IFNULL(t2.`rank`, 0) + FROM ?_talents t1 + LEFT JOIN (SELECT `id`, `rank` FROM ?_talents WHERE `spell` IN (?a)) t2 ON t2.`id` = t1.`id` + WHERE `class` = 0 AND `petTypeMask` = ?d + GROUP BY t1.`id` + ORDER BY t1.`row`, t1.`col`, t1.`id` ASC', + $petSpells ?: [0], 1 << $morePet['type'] + ); + $pet = array( 'id' => $petGuid, 'owner' => $profileId, diff --git a/includes/dbtypes/spell.class.php b/includes/dbtypes/spell.class.php index 935f6fd4..924c1386 100644 --- a/includes/dbtypes/spell.class.php +++ b/includes/dbtypes/spell.class.php @@ -230,56 +230,48 @@ class SpellList extends DBTypeList // => [123, 'add'] // => ... as from getStatGain() - $modXByStat = function (&$arr, $stat, $pts) use (&$mv) + $modXByStat = function (array &$arr, int $srcStat, ?string $destStat, int $pts) : void { - if ($mv == STAT_STRENGTH) - $arr[$stat ?: 'str'] = [$pts / 100, 'percentOf', 'str']; - else if ($mv == STAT_AGILITY) - $arr[$stat ?: 'agi'] = [$pts / 100, 'percentOf', 'agi']; - else if ($mv == STAT_STAMINA) - $arr[$stat ?: 'sta'] = [$pts / 100, 'percentOf', 'sta']; - else if ($mv == STAT_INTELLECT) - $arr[$stat ?: 'int'] = [$pts / 100, 'percentOf', 'int']; - else if ($mv == STAT_SPIRIT) - $arr[$stat ?: 'spi'] = [$pts / 100, 'percentOf', 'spi']; + match ($srcStat) + { + STAT_STRENGTH => $arr[$destStat ?: 'str'] = [$pts / 100, 'percentOf', 'str'], + STAT_AGILITY => $arr[$destStat ?: 'agi'] = [$pts / 100, 'percentOf', 'agi'], + STAT_STAMINA => $arr[$destStat ?: 'sta'] = [$pts / 100, 'percentOf', 'sta'], + STAT_INTELLECT => $arr[$destStat ?: 'int'] = [$pts / 100, 'percentOf', 'int'], + STAT_SPIRIT => $arr[$destStat ?: 'spi'] = [$pts / 100, 'percentOf', 'spi'] + }; }; - $modXBySchool = function (&$arr, $stat, $val, $mask = null) use (&$mv) + $modXBySchool = function (array &$arr, int $srcStat, string $destStat, array|int $val) : void { - if (($mask ?: $mv) & (1 << SPELL_SCHOOL_HOLY)) - $arr['hol'.$stat] = is_array($val) ? $val : [$val / 100, 'percentOf', 'hol'.$stat]; - if (($mask ?: $mv) & (1 << SPELL_SCHOOL_FIRE)) - $arr['fir'.$stat] = is_array($val) ? $val : [$val / 100, 'percentOf', 'fir'.$stat]; - if (($mask ?: $mv) & (1 << SPELL_SCHOOL_NATURE)) - $arr['nat'.$stat] = is_array($val) ? $val : [$val / 100, 'percentOf', 'nat'.$stat]; - if (($mask ?: $mv) & (1 << SPELL_SCHOOL_FROST)) - $arr['fro'.$stat] = is_array($val) ? $val : [$val / 100, 'percentOf', 'fro'.$stat]; - if (($mask ?: $mv) & (1 << SPELL_SCHOOL_SHADOW)) - $arr['sha'.$stat] = is_array($val) ? $val : [$val / 100, 'percentOf', 'sha'.$stat]; - if (($mask ?: $mv) & (1 << SPELL_SCHOOL_ARCANE)) - $arr['arc'.$stat] = is_array($val) ? $val : [$val / 100, 'percentOf', 'arc'.$stat]; + if ($srcStat & (1 << SPELL_SCHOOL_HOLY)) + $arr['hol'.$destStat] = is_array($val) ? $val : [$val / 100, 'percentOf', 'hol'.$destStat]; + if ($srcStat & (1 << SPELL_SCHOOL_FIRE)) + $arr['fir'.$destStat] = is_array($val) ? $val : [$val / 100, 'percentOf', 'fir'.$destStat]; + if ($srcStat & (1 << SPELL_SCHOOL_NATURE)) + $arr['nat'.$destStat] = is_array($val) ? $val : [$val / 100, 'percentOf', 'nat'.$destStat]; + if ($srcStat & (1 << SPELL_SCHOOL_FROST)) + $arr['fro'.$destStat] = is_array($val) ? $val : [$val / 100, 'percentOf', 'fro'.$destStat]; + if ($srcStat & (1 << SPELL_SCHOOL_SHADOW)) + $arr['sha'.$destStat] = is_array($val) ? $val : [$val / 100, 'percentOf', 'sha'.$destStat]; + if ($srcStat & (1 << SPELL_SCHOOL_ARCANE)) + $arr['arc'.$destStat] = is_array($val) ? $val : [$val / 100, 'percentOf', 'arc'.$destStat]; }; - $jsonStat = function ($stat) + $jsonStat = function (int $statId) : string { - if ($stat == STAT_STRENGTH) - return 'str'; - if ($stat == STAT_AGILITY) - return 'agi'; - if ($stat == STAT_STAMINA) - return 'sta'; - if ($stat == STAT_INTELLECT) - return 'int'; - if ($stat == STAT_SPIRIT) - return 'spi'; + return match ($statId) + { + STAT_STRENGTH => Stat::getJsonString(Stat::STRENGTH), + STAT_AGILITY => Stat::getJsonString(Stat::AGILITY), + STAT_STAMINA => Stat::getJsonString(Stat::STAMINA), + STAT_INTELLECT => Stat::getJsonString(Stat::INTELLECT), + STAT_SPIRIT => Stat::getJsonString(Stat::SPIRIT) + }; }; foreach ($this->iterate() as $id => $__) { - // kept for reference - if (($this->getField('cuFlags') & SPELL_CU_TALENTSPELL) && $id != 20711) - if (!($this->getField('attributes0') & SPELL_ATTR0_PASSIVE)) - continue; - // Shaman - Spirit Weapons (16268) (parry is normaly stored in g_statistics) // i should recurse into SPELL_EFFECT_LEARN_SPELL and apply SPELL_EFFECT_PARRY from there if ($id == 16268) @@ -288,6 +280,9 @@ class SpellList extends DBTypeList continue; } + if (!($this->getField('attributes0') & SPELL_ATTR0_PASSIVE)) + continue; + for ($i = 1; $i < 4; $i++) { $pts = $this->calculateAmountForCurrent($i)[1]; @@ -311,18 +306,18 @@ class SpellList extends DBTypeList if ($mv == (1 << SPELL_SCHOOL_NORMAL)) $data[$id]['armor'] = [$pts / 100, 'percentOf', ['armor', 0]]; else if ($mv) - $modXBySchool($data[$id], 'res', $pts); + $modXBySchool($data[$id], $mv, 'res', $pts); break; case SPELL_AURA_MOD_RESISTANCE_OF_STAT_PERCENT: // Armor only if explicitly specified if ($mv == (1 << SPELL_SCHOOL_NORMAL)) $data[$id]['armor'] = [$pts / 100, 'percentOf', $jsonStat($mvB)]; else if ($mv) - $modXBySchool($data[$id], 'res', [$pts / 100, 'percentOf', $jsonStat($mvB)]); + $modXBySchool($data[$id], $mv, 'res', [$pts / 100, 'percentOf', $jsonStat($mvB)]); break; case SPELL_AURA_MOD_TOTAL_STAT_PERCENTAGE: if ($mv > -1) // one stat - $modXByStat($data[$id], null, $pts); + $modXByStat($data[$id], $mv, null, $pts); else if ($mv < 0) // all stats for ($iMod = ITEM_MOD_AGILITY; $iMod <= ITEM_MOD_STAMINA; $iMod++) if ($idx = Stat::getIndexFrom(Stat::IDX_ITEM_MOD, $iMod)) @@ -331,19 +326,19 @@ class SpellList extends DBTypeList break; case SPELL_AURA_MOD_SPELL_DAMAGE_OF_STAT_PERCENT: $mv = $mv ?: SPELL_MAGIC_SCHOOLS; - $modXBySchool($data[$id], 'spldmg', [$pts / 100, 'percentOf', $jsonStat($mvB)]); + $modXBySchool($data[$id], $mv, 'spldmg', [$pts / 100, 'percentOf', $jsonStat($mvB)]); break; case SPELL_AURA_MOD_RANGED_ATTACK_POWER_OF_STAT_PERCENT: - $modXByStat($data[$id], 'rgdatkpwr', $pts); + $modXByStat($data[$id], $mv, 'rgdatkpwr', $pts); break; case SPELL_AURA_MOD_ATTACK_POWER_OF_STAT_PERCENT: - $modXByStat($data[$id], 'mleatkpwr', $pts); + $modXByStat($data[$id], $mv, 'mleatkpwr', $pts); break; case SPELL_AURA_MOD_SPELL_HEALING_OF_STAT_PERCENT: - $modXByStat($data[$id], 'splheal', $pts); + $modXByStat($data[$id], $mv, 'splheal', $pts); break; case SPELL_AURA_MOD_MANA_REGEN_FROM_STAT: - $modXByStat($data[$id], 'manargn', $pts); + $modXByStat($data[$id], $mv, 'manargn', $pts); break; case SPELL_AURA_MOD_MANA_REGEN_INTERRUPT: $data[$id]['icmanargn'] = [$pts / 100, 'percentOf', 'oocmanargn']; @@ -351,7 +346,7 @@ class SpellList extends DBTypeList case SPELL_AURA_MOD_SPELL_CRIT_CHANCE: case SPELL_AURA_MOD_SPELL_CRIT_CHANCE_SCHOOL: $mv = $mv ?: SPELL_MAGIC_SCHOOLS; - $modXBySchool($data[$id], 'splcritstrkpct', [$pts, 'add']); + $modXBySchool($data[$id], $mv, 'splcritstrkpct', [$pts, 'add']); if (($mv & SPELL_MAGIC_SCHOOLS) == SPELL_MAGIC_SCHOOLS) $data[$id]['splcritstrkpct'] = [$pts, 'add']; break; @@ -392,7 +387,7 @@ class SpellList extends DBTypeList $data[$id]['health'] = [$pts / 100, 'percentOf', 'health']; break; case SPELL_AURA_MOD_BASE_HEALTH_PCT: // only Tauren - Endurance (20550) ... if you are looking for something elegant, look away! - $data[$id]['health'] = [$pts / 100, 'functionOf', '$function(p) { return g_statistics.combo[p.classs][p.level][5]; }']; + $data[$id]['health'] = [$pts / 100, 'functionOf', '$(x) => g_statistics.combo[x.classs][x.level][5]']; break; case SPELL_AURA_MOD_SHIELD_BLOCKVALUE_PCT: $data[$id]['block'] = [$pts / 100, 'percentOf', 'block']; @@ -404,7 +399,7 @@ class SpellList extends DBTypeList break; case SPELL_AURA_MOD_SPELL_DAMAGE_OF_ATTACK_POWER: $mv = $mv ?: SPELL_MAGIC_SCHOOLS; - $modXBySchool($data[$id], 'spldmg', [$pts / 100, 'percentOf', 'mleatkpwr']); + $modXBySchool($data[$id], $mv, 'spldmg', [$pts / 100, 'percentOf', 'mleatkpwr']); break; case SPELL_AURA_MOD_SPELL_HEALING_OF_ATTACK_POWER: $data[$id]['splheal'] = [$pts / 100, 'percentOf', 'mleatkpwr']; diff --git a/setup/sql/01-db_structure.sql b/setup/sql/01-db_structure.sql index 6063d5fb..04e310c6 100644 --- a/setup/sql/01-db_structure.sql +++ b/setup/sql/01-db_structure.sql @@ -1919,7 +1919,7 @@ CREATE TABLE `aowow_profiler_pets` ( `family` tinyint(3) unsigned DEFAULT NULL, `npc` smallint(5) unsigned DEFAULT NULL, `displayId` smallint(5) unsigned DEFAULT NULL, - `talents` varchar(20) DEFAULT NULL, + `talents` varchar(22) DEFAULT NULL, PRIMARY KEY (`id`), KEY `owner` (`owner`), CONSTRAINT `FK_pr_pets` FOREIGN KEY (`owner`) REFERENCES `aowow_profiler_profiles` (`id`) ON DELETE CASCADE ON UPDATE CASCADE diff --git a/setup/sql/updates/1760911493_01.sql b/setup/sql/updates/1760911493_01.sql new file mode 100644 index 00000000..b4f362f0 --- /dev/null +++ b/setup/sql/updates/1760911493_01.sql @@ -0,0 +1,7 @@ +ALTER TABLE `aowow_profiler_pets` + MODIFY COLUMN `talents` varchar(22) DEFAULT NULL; + +UPDATE `aowow_dbversion` SET `build` = CONCAT(IFNULL(`build`, ''), ' talenticons talentcalc'); + +-- flag all hunters as requiring update +UPDATE `aowow_profiler_profiles` SET `flags` = `flags` | 16, `lastupdated` = 0 WHERE `class` = 3 AND `realmGUID` IS NOT NULL; diff --git a/setup/tools/filegen/talentcalc.ss.php b/setup/tools/filegen/talentcalc.ss.php index 6443f871..aac14bc7 100644 --- a/setup/tools/filegen/talentcalc.ss.php +++ b/setup/tools/filegen/talentcalc.ss.php @@ -100,7 +100,7 @@ CLISetup::registerSetup("build", new class extends SetupScript LOWER(SUBSTRING_INDEX(si.`iconPath`, "\\\\", -1)) AS "iconString" FROM dbc_talent t, dbc_spell s, dbc_spellicon si WHERE si.`id` = s.`iconId` AND t.`tabId`= ?d AND s.`id` = t.`rank1` - ORDER BY t.`row`, t.`column`', + ORDER BY t.`row`, t.`column`, t.`id` ASC', $tabs[$tabIdx]['id'] ); diff --git a/setup/tools/filegen/talenticons.ss.php b/setup/tools/filegen/talenticons.ss.php index d7187ddc..cde3143f 100644 --- a/setup/tools/filegen/talenticons.ss.php +++ b/setup/tools/filegen/talenticons.ss.php @@ -85,7 +85,7 @@ CLISetup::registerSetup("build", new class extends SetupScript JOIN dbc_talent t ON t.`rank1` = s.`id` JOIN dbc_talenttab tt ON tt.`id` = t.`tabId` WHERE tt.?# = ?d AND tt.`tabNumber` = ?d - ORDER BY t.`row`, t.`column` ASC, s.`id` DESC', + ORDER BY t.`row`, t.`column`, t.`id` ASC', $ttField, $searchMask, $tabIdx); if (empty($icons)) From 33cd290dc3553764fd955fb30c087316630a4f6c Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Mon, 20 Oct 2025 17:57:41 +0200 Subject: [PATCH 061/260] Setup/Spawns * fix coords for cases with coords in both WorldMapArea.dbc and DungeonMap.dbc without using WorldMapArea.dbc as base floor --- includes/game/worldposition.class.php | 6 +++--- includes/utilities.php | 2 +- setup/sql/updates/1760979519_01.sql | 1 + 3 files changed, 5 insertions(+), 4 deletions(-) create mode 100644 setup/sql/updates/1760979519_01.sql diff --git a/includes/game/worldposition.class.php b/includes/game/worldposition.class.php index 9f8d4ff3..fbcd16e2 100644 --- a/includes/game/worldposition.class.php +++ b/includes/game/worldposition.class.php @@ -123,7 +123,7 @@ abstract class WorldPosition 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`, + IF(dm.`id` IS NOT NULL OR x.`defaultDungeonMapId` < 0, 1, 0) AS `srcPrio`, 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) + @@ -133,7 +133,7 @@ abstract class WorldPosition 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 + ?_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 @@ -141,7 +141,7 @@ abstract class WorldPosition HAVING (`posX` BETWEEN 0.1 AND 99.9 AND `posY` BETWEEN 0.1 AND 99.9) ORDER BY - `multifloor` DESC, `dist` ASC'; + `srcPrio` 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); diff --git a/includes/utilities.php b/includes/utilities.php index 2f3db816..e746e2fe 100644 --- a/includes/utilities.php +++ b/includes/utilities.php @@ -1173,7 +1173,7 @@ abstract class Util $menu = [[null, "Move Location to..."]]; foreach ($points as $p) { - if ($p['multifloor']) + if ($p['srcPrio']) $floors[$p['areaId']][] = $p['floor']; if (isset($menu[$p['areaId']])) diff --git a/setup/sql/updates/1760979519_01.sql b/setup/sql/updates/1760979519_01.sql new file mode 100644 index 00000000..c0c38b25 --- /dev/null +++ b/setup/sql/updates/1760979519_01.sql @@ -0,0 +1 @@ +UPDATE `aowow_dbversion` SET `sql` = CONCAT(IFNULL(`sql`, ''), ' spawns'); From 14c159c1640f1faa3c9ddb5972f40922bea9dcf3 Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Mon, 20 Oct 2025 19:08:55 +0200 Subject: [PATCH 062/260] Setup/Factions * fix switched base rep field indizes, causing Profiler to miscalculate character standing * replace hardcoded sql table prefixes --- includes/components/profiler.class.php | 8 ++-- setup/sql/01-db_structure.sql | 2 +- setup/sql/updates/1760979719_01.sql | 8 ++++ setup/tools/clisetup/dbconfig.us.php | 4 +- setup/tools/sqlgen/factions.ss.php | 52 +++++++++++++------------- setup/tools/sqlgen/source.ss.php | 4 +- 6 files changed, 43 insertions(+), 35 deletions(-) create mode 100644 setup/sql/updates/1760979719_01.sql diff --git a/includes/components/profiler.class.php b/includes/components/profiler.class.php index 91240710..13e432e8 100644 --- a/includes/components/profiler.class.php +++ b/includes/components/profiler.class.php @@ -703,10 +703,10 @@ class Profiler // get base values for this race/class $reputation = []; $baseRep = DB::Aowow()->selectCol( - 'SELECT `id` AS ARRAY_KEY, `baseRepValue1` FROM aowow_factions WHERE `baseRepValue1` AND (`baseRepRaceMask1` & ?d OR (`baseRepClassMask1` AND NOT `baseRepRaceMask1`)) AND ((`baseRepClassMask1` & ?d) OR NOT `baseRepClassMask1`) UNION - SELECT `id` AS ARRAY_KEY, `baseRepValue2` FROM aowow_factions WHERE `baseRepValue2` AND (`baseRepRaceMask2` & ?d OR (`baseRepClassMask2` AND NOT `baseRepRaceMask2`)) AND ((`baseRepClassMask2` & ?d) OR NOT `baseRepClassMask2`) UNION - SELECT `id` AS ARRAY_KEY, `baseRepValue3` FROM aowow_factions WHERE `baseRepValue3` AND (`baseRepRaceMask3` & ?d OR (`baseRepClassMask3` AND NOT `baseRepRaceMask3`)) AND ((`baseRepClassMask3` & ?d) OR NOT `baseRepClassMask3`) UNION - SELECT `id` AS ARRAY_KEY, `baseRepValue4` FROM aowow_factions WHERE `baseRepValue4` AND (`baseRepRaceMask4` & ?d OR (`baseRepClassMask4` AND NOT `baseRepRaceMask4`)) AND ((`baseRepClassMask4` & ?d) OR NOT `baseRepClassMask4`)', + 'SELECT `id` AS ARRAY_KEY, `baseRepValue1` FROM ?_factions WHERE `baseRepValue1` AND (`baseRepRaceMask1` & ?d OR (`baseRepClassMask1` AND NOT `baseRepRaceMask1`)) AND ((`baseRepClassMask1` & ?d) OR NOT `baseRepClassMask1`) UNION + SELECT `id` AS ARRAY_KEY, `baseRepValue2` FROM ?_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 ?_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 ?_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() ); diff --git a/setup/sql/01-db_structure.sql b/setup/sql/01-db_structure.sql index 04e310c6..dcd90ea8 100644 --- a/setup/sql/01-db_structure.sql +++ b/setup/sql/01-db_structure.sql @@ -863,8 +863,8 @@ CREATE TABLE `aowow_factions` ( `baseRepClassMask4` mediumint(8) unsigned NOT NULL, `baseRepValue1` mediumint(9) NOT NULL, `baseRepValue2` mediumint(9) NOT NULL, - `baseRepValue4` mediumint(9) NOT NULL, `baseRepValue3` mediumint(9) NOT NULL, + `baseRepValue4` mediumint(9) NOT NULL, `side` tinyint(3) unsigned NOT NULL, `expansion` tinyint(3) unsigned NOT NULL, `qmNpcIds` varchar(12) NOT NULL COMMENT 'space separated', diff --git a/setup/sql/updates/1760979719_01.sql b/setup/sql/updates/1760979719_01.sql new file mode 100644 index 00000000..c2bbcf7a --- /dev/null +++ b/setup/sql/updates/1760979719_01.sql @@ -0,0 +1,8 @@ +ALTER TABLE `aowow_factions` + DROP COLUMN `baseRepValue3`, + DROP COLUMN `baseRepValue4`, + ADD COLUMN `baseRepValue3` mediumint(9) NOT NULL AFTER `baseRepValue2`, + ADD COLUMN `baseRepValue4` mediumint(9) NOT NULL AFTER `baseRepValue3` +; + +UPDATE `aowow_dbversion` SET `sql` = CONCAT(IFNULL(`sql`, ''), ' factions'); diff --git a/setup/tools/clisetup/dbconfig.us.php b/setup/tools/clisetup/dbconfig.us.php index 3bc433df..c69a8a88 100644 --- a/setup/tools/clisetup/dbconfig.us.php +++ b/setup/tools/clisetup/dbconfig.us.php @@ -216,7 +216,7 @@ CLISetup::registerUtility(new class extends UtilityScript switch ($idx) { case DB_AOWOW: - if (DB::Aowow()->selectCell('SHOW TABLES LIKE ?', 'aowow_dbversion')) + if (DB::Aowow()->selectCell('SHOW TABLES LIKE ?', '?_dbversion')) Cfg::load(); // first time load after successful db setup else $error[] = ' * '.$what.': doesn\'t seem to contain aowow tables!'; @@ -255,7 +255,7 @@ CLISetup::registerUtility(new class extends UtilityScript switch ($idx) { case DB_AOWOW: - if (DB::Aowow()->selectCell('SHOW TABLES LIKE ?', 'aowow_dbversion')) + if (DB::Aowow()->selectCell('SHOW TABLES LIKE ?', '?_dbversion')) { if ($date = DB::Aowow()->selectCell('SELECT `date` FROM ?_dbversion')) { diff --git a/setup/tools/sqlgen/factions.ss.php b/setup/tools/sqlgen/factions.ss.php index 7fb7e001..2b0025a4 100644 --- a/setup/tools/sqlgen/factions.ss.php +++ b/setup/tools/sqlgen/factions.ss.php @@ -26,47 +26,47 @@ CLISetup::registerSetup("sql", new class extends SetupScript DB::Aowow()->query( 'INSERT INTO ?_factions - SELECT f.id, - f.repIdx, - baseRepRaceMask1, baseRepRaceMask2, baseRepRaceMask3, baseRepRaceMask4, - baseRepClassMask1, baseRepClassMask2, baseRepClassMask3, baseRepClassMask4, - baseRepValue1, baseRepValue2, baseRepValue3, baseRepValue4, - IF(SUM(ft.ourMask & 0x6) / COUNT(1) = 0x4, 2, IF(SUM(ft.ourMask & 0x6) / COUNT(1) = 0x2, 1, 0)) as side, + SELECT f.`id`, + f.`repIdx`, + `baseRepRaceMask1`, `baseRepRaceMask2`, `baseRepRaceMask3`, `baseRepRaceMask4`, + `baseRepClassMask1`, `baseRepClassMask2`, `baseRepClassMask3`, `baseRepClassMask4`, + `baseRepValue1`, `baseRepValue2`, `baseRepValue3`, `baseRepValue4`, + IF(SUM(ft.`ourMask` & 0x6) / COUNT(1) = 0x4, 2, IF(SUM(ft.`ourMask` & 0x6) / COUNT(1) = 0x2, 1, 0)) AS "side", 0, -- expansion "", -- quartermasterNpcIds "", -- factionTemplateIds 0, -- cuFlags - parentFaction, - spilloverRateIn, spilloverRateOut, spilloverMaxRank, - name_loc0, name_loc2, name_loc3, name_loc4, name_loc6, name_loc8 + `parentFaction`, + `spilloverRateIn`, `spilloverRateOut`, `spilloverMaxRank`, + `name_loc0`, `name_loc2`, `name_loc3`, `name_loc4`, `name_loc6`, `name_loc8` FROM dbc_faction f - LEFT JOIN dbc_factiontemplate ft ON ft.factionid = f.id - GROUP BY f.id' + LEFT JOIN dbc_factiontemplate ft ON ft.`factionid` = f.`id` + GROUP BY f.`id`' ); DB::Aowow()->query( 'INSERT INTO ?_factiontemplate - SELECT id, - factionId, - IF(friendFactionId1 = 1 OR friendFactionId2 = 1 OR friendFactionId3 = 1 OR friendFactionId4 = 1 OR friendlyMask & 0x3, 1, - IF(enemyFactionId1 = 1 OR enemyFactionId2 = 1 OR enemyFactionId3 = 1 OR enemyFactionId4 = 1 OR hostileMask & 0x3, -1, 0)), - IF(friendFactionId1 = 2 OR friendFactionId2 = 2 OR friendFactionId3 = 2 OR friendFactionId4 = 2 OR friendlyMask & 0x5, 1, - IF(enemyFactionId1 = 2 OR enemyFactionId2 = 2 OR enemyFactionId3 = 2 OR enemyFactionId4 = 2 OR hostileMask & 0x5, -1, 0)) + SELECT `id`, + `factionId`, + IF(`friendFactionId1` = 1 OR `friendFactionId2` = 1 OR `friendFactionId3` = 1 OR `friendFactionId4` = 1 OR `friendlyMask` & 0x3, 1, + IF(`enemyFactionId1` = 1 OR `enemyFactionId2` = 1 OR `enemyFactionId3` = 1 OR `enemyFactionId4` = 1 OR `hostileMask` & 0x3, -1, 0)), + IF(`friendFactionId1` = 2 OR `friendFactionId2` = 2 OR `friendFactionId3` = 2 OR `friendFactionId4` = 2 OR `friendlyMask` & 0x5, 1, + IF(`enemyFactionId1` = 2 OR `enemyFactionId2` = 2 OR `enemyFactionId3` = 2 OR `enemyFactionId4` = 2 OR `hostileMask` & 0x5, -1, 0)) FROM dbc_factiontemplate' ); DB::Aowow()->query( 'UPDATE ?_factions f - JOIN (SELECT ft.factionId, GROUP_CONCAT(ft.id SEPARATOR " ") AS tplIds FROM dbc_factiontemplate ft GROUP BY ft.factionId) temp ON f.id = temp.factionId - SET f.templateIds = temp.tplIds' + JOIN (SELECT ft.`factionId`, GROUP_CONCAT(ft.`id` SEPARATOR " ") AS "tplIds" FROM dbc_factiontemplate ft GROUP BY ft.`factionId`) temp ON f.`id` = temp.`factionId` + SET f.`templateIds` = temp.`tplIds`' ); DB::Aowow()->query( 'UPDATE ?_factions x - JOIN dbc_faction f ON f.id = x.id - LEFT JOIN dbc_factiontemplate ft ON f.id = ft.factionId - SET cuFlags = cuFlags | ?d - WHERE f.repIdx < 0 OR ( f.repIdx > 0 AND (f.repFlags1 & 0x8 OR ft.id IS NULL) AND (f.repFlags1 & 0x80) = 0 )', + JOIN dbc_faction f ON f.`id` = x.`id` + LEFT JOIN dbc_factiontemplate ft ON f.`id` = ft.`factionId` + SET `cuFlags` = `cuFlags` | ?d + WHERE f.`repIdx` < 0 OR ( f.`repIdx` > 0 AND (f.`repFlags1` & 0x8 OR ft.`id` IS NULL) AND (f.`repFlags1` & 0x80) = 0 )', CUSTOM_EXCLUDE_FOR_LISTVIEW ); @@ -80,10 +80,10 @@ CLISetup::registerSetup("sql", new class extends SetupScript foreach ($pairs as $p) DB::Aowow()->query( 'UPDATE ?_factions top - JOIN (SELECT id, parentFactionId FROM ?_factions) mid ON mid.parentFactionId IN (?a) - LEFT JOIN (SELECT id, parentFactionId FROM ?_factions) low ON low.parentFactionId = mid.id + JOIN (SELECT `id`, `parentFactionId` FROM ?_factions) mid ON mid.`parentFactionId` IN (?a) + LEFT JOIN (SELECT `id`, `parentFactionId` FROM ?_factions) low ON low.`parentFactionId` = mid.`id` SET ?a - WHERE repIdx > 0 AND (top.id IN (?a) OR top.id = mid.id OR top.id = low.id)', + WHERE `repIdx` > 0 AND (top.`id` IN (?a) OR top.`id` = mid.`id` OR top.`id` = low.`id`)', $p[0], $p[1], $p[0] ); diff --git a/setup/tools/sqlgen/source.ss.php b/setup/tools/sqlgen/source.ss.php index ad7816eb..627afed5 100644 --- a/setup/tools/sqlgen/source.ss.php +++ b/setup/tools/sqlgen/source.ss.php @@ -42,8 +42,8 @@ CLISetup::registerSetup("sql", new class extends SetupScript $this->dummyGOs = DB::Aowow()->select( 'SELECT l1.`objectId` AS ARRAY_KEY, BIT_OR(l1.`difficulty`) AS "0", IFNULL(l2.`npcId`, l1.`npcId`) AS "1" - FROM aowow_loot_link l1 - LEFT JOIN aowow_loot_link l2 ON l1.`objectId` = l2.`objectId` AND l2.`priority` = 1 + FROM ?_loot_link l1 + LEFT JOIN ?_loot_link l2 ON l1.`objectId` = l2.`objectId` AND l2.`priority` = 1 GROUP BY l1.`objectid`' ); From 51b6e293168f8d3d7f295fe5c38677d301166893 Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Mon, 20 Oct 2025 20:02:47 +0200 Subject: [PATCH 063/260] Profiler/Queue * send ready status for characters/guilds/arena-teams whose resync cooldown hasn't expired yet --- includes/components/profiler.class.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/includes/components/profiler.class.php b/includes/components/profiler.class.php index 13e432e8..bfaa1128 100644 --- a/includes/components/profiler.class.php +++ b/includes/components/profiler.class.php @@ -335,7 +335,7 @@ class Profiler // error out all profiles with status WORKING, that are older than 60sec DB::Aowow()->query('UPDATE ?_profiler_sync SET `status` = ?d, `errorCode` = ?d WHERE `status` = ?d AND `requestTime` < ?d', PR_QUEUE_STATUS_ERROR, PR_QUEUE_ERROR_UNK, PR_QUEUE_STATUS_WORKING, time() - MINUTE); - $subjectStatus = DB::Aowow()->select('SELECT `typeId` AS ARRAY_KEY, `status`, `realm`, `errorCode` FROM ?_profiler_sync WHERE `type` = ?d AND `typeId` IN (?a)', $type, $subjectGUIDs); + $subjectStatus = DB::Aowow()->select('SELECT `typeId` AS ARRAY_KEY, `status`, `realm`, `errorCode`, `requestTime` 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) { @@ -343,6 +343,8 @@ class Profiler $response[] = [PR_QUEUE_STATUS_ERROR, 0, 0, PR_QUEUE_ERROR_UNK]; else if ($subjectStatus[$guid]['status'] == PR_QUEUE_STATUS_ERROR) $response[] = [PR_QUEUE_STATUS_ERROR, 0, 0, $subjectStatus[$guid]['errorCode']]; + else if ($subjectStatus[$guid]['requestTime'] > time()) + $response[] = [PR_QUEUE_STATUS_READY, 0, 0, 0]; else $response[] = array( $subjectStatus[$guid]['status'], From 1dcc9363da85fc5f426f2377cd0397bc64f852ee Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Tue, 21 Oct 2025 15:33:50 +0200 Subject: [PATCH 064/260] Profiler/Filter * fix filter params being propagated between different profiler types (e.g. arena team size filter being appended to guilds menu) --- template/pages/arena-teams.tpl.php | 2 +- template/pages/guilds.tpl.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/template/pages/arena-teams.tpl.php b/template/pages/arena-teams.tpl.php index 144069a9..c318999e 100644 --- a/template/pages/arena-teams.tpl.php +++ b/template/pages/arena-teams.tpl.php @@ -13,7 +13,7 @@ $f = $this->filter->values; // shorthand brick('announcement'); -$this->brick('pageTemplate', ['fiQuery' => $this->filter->query, 'fiMenuItem' => array_slice($this->pageTemplate['breadcrumb'], 0, -1)]); +$this->brick('pageTemplate', ['fiQuery' => $this->filter->query, 'fiMenuItem' => array_slice($this->pageTemplate['breadcrumb'], 0, 3)]); # pr_setRegionRealm($WH.ge('fi').firstChild, realm, region) - never have \n\s before
, it will become firstChild (a text node) ?> diff --git a/template/pages/guilds.tpl.php b/template/pages/guilds.tpl.php index fe889a9c..925f1547 100644 --- a/template/pages/guilds.tpl.php +++ b/template/pages/guilds.tpl.php @@ -13,7 +13,7 @@ $f = $this->filter->values; // shorthand brick('announcement'); -$this->brick('pageTemplate', ['fiQuery' => $this->filter->query, 'fiMenuItem' => array_slice($this->pageTemplate['breadcrumb'], 0, -1)]); +$this->brick('pageTemplate', ['fiQuery' => $this->filter->query, 'fiMenuItem' => array_slice($this->pageTemplate['breadcrumb'], 0, 3)]); # pr_setRegionRealm($WH.ge('fi').firstChild, realm, region) - never have \n\s before , it will become firstChild (a text node) ?> From 033a9181ae71493c8e04e0636a14e7bb20954fdd Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Tue, 21 Oct 2025 17:28:15 +0200 Subject: [PATCH 065/260] =?UTF-8?q?Profiler/Localization=20=20*=20TCs=20gu?= =?UTF-8?q?ild=20and=20arena=5Fteam=20tables=20as=20encoded=20as=20utf8mb4?= =?UTF-8?q?=5Fgeneral=5Fci,=20=20=20=20which=20is=20not=20accent-aware.=20?= =?UTF-8?q?So=20we=20have=20to=20get=20all=20results=20and=20filter=20=20?= =?UTF-8?q?=20=20for=20the=20correct=20one=20in=20php.=20=20*=20fixes=20an?= =?UTF-8?q?=20issue=20where=20direcly=20accessing=20a=20guild/arena-team?= =?UTF-8?q?=20whith=20a=20name=20=20=20=20simiar=20to=20an=20already=20kno?= =?UTF-8?q?wn=20guild/team=20would=20lookup=20the=20wrong=20subject=20=20?= =?UTF-8?q?=20=20on=20the=20server=20and=20then=20fail=20to=20create=20a?= =?UTF-8?q?=20local=20stub=20with=20already=20existing=20key.=20=20=20=20(?= =?UTF-8?q?Sh=C3=A2d=C3=B3w=20vs=20Shadow)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- endpoints/arena-team/arena-team.php | 4 +++- endpoints/guild/guild.php | 4 +++- endpoints/profile/profile.php | 6 ++++-- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/endpoints/arena-team/arena-team.php b/endpoints/arena-team/arena-team.php index 2ade7851..2d50aa08 100644 --- a/endpoints/arena-team/arena-team.php +++ b/endpoints/arena-team/arena-team.php @@ -57,8 +57,10 @@ class ArenateamBaseResponse extends TemplateResponse } // 2) not yet synced but exists on realm (wont work if we get passed an urlized name, but there is nothing we can do about it) - else if ($subject = DB::Characters($this->realmId)->selectRow('SELECT at.`arenaTeamId` AS "realmGUID", at.`name`, at.`type` FROM arena_team at WHERE at.`name` = ?', Util::ucFirst($this->subjectName))) + $subjects = DB::Characters($this->realmId)->select('SELECT at.`arenaTeamId` AS "realmGUID", at.`name`, at.`type` FROM arena_team at WHERE at.`name` = ?', $this->subjectName); + if ($subject = array_filter($subjects, fn($x) => Util::lower($x['name']) == Util::lower($this->subjectName))) { + $subject = $subject[0]; $subject['realm'] = $this->realmId; $subject['cuFlags'] = PROFILER_CU_NEEDS_RESYNC; $subject['nameUrl'] = Profiler::urlize($subject['name']); diff --git a/endpoints/guild/guild.php b/endpoints/guild/guild.php index f4ec2335..fa247e40 100644 --- a/endpoints/guild/guild.php +++ b/endpoints/guild/guild.php @@ -57,8 +57,10 @@ class GuildBaseResponse extends TemplateResponse } // 2) not yet synced but exists on realm (wont work if we get passed an urlized name, but there is nothing we can do about it) - else if ($subject = DB::Characters($this->realmId)->selectRow('SELECT `guildid` AS "realmGUID", `name` FROM guild WHERE `name` = ?', Util::ucFirst($this->subjectName))) + $subjects = DB::Characters($this->realmId)->select('SELECT `guildid` AS "realmGUID", `name` FROM guild WHERE `name` = ?', $this->subjectName); + if ($subject = array_filter($subjects, fn($x) => Util::lower($x['name']) == Util::lower($this->subjectName))) { + $subject = $subject[0]; $subject['realm'] = $this->realmId; $subject['cuFlags'] = PROFILER_CU_NEEDS_RESYNC; $subject['nameUrl'] = Profiler::urlize($subject['name']); diff --git a/endpoints/profile/profile.php b/endpoints/profile/profile.php index 958ffaae..39484228 100644 --- a/endpoints/profile/profile.php +++ b/endpoints/profile/profile.php @@ -82,15 +82,17 @@ class ProfileBaseResponse extends TemplateResponse $this->notFound(); // 2) not yet synced but exists on realm (and not a gm character) - if ($subject = DB::Characters($this->realmId)->selectRow( + $subjects = DB::Characters($this->realmId)->select( 'SELECT c.`guid` AS "realmGUID", c.`name`, c.`race`, c.`class`, c.`level`, c.`gender`, c.`at_login`, g.`guildid` AS "guildGUID", IFNULL(g.`name`, "") AS "guildName", IFNULL(gm.`rank`, 0) AS "guildRank" FROM characters c LEFT JOIN guild_member gm ON gm.`guid` = c.`guid` LEFT JOIN guild g ON g.`guildid` = gm.`guildid` WHERE c.`name` = ? AND `level` <= ?d AND (`extra_flags` & ?d) = 0', Util::ucFirst($this->subjectName), MAX_LEVEL, Profiler::CHAR_GMFLAGS - )) + ); + if ($subject = array_filter($subjects, fn($x) => Util::lower($x['name']) == Util::lower($this->subjectName))) { + $subject = $subject[0]; $subject['realm'] = $this->realmId; $subject['cuFlags'] = PROFILER_CU_NEEDS_RESYNC; From 6da71afc683cd2290b20d804821544517c29f8dd Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Tue, 21 Oct 2025 20:48:39 +0200 Subject: [PATCH 066/260] Filter/Fixup * allow unicode chars when checking GET param --- includes/components/filter.class.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/components/filter.class.php b/includes/components/filter.class.php index 27746d05..13a45cdd 100644 --- a/includes/components/filter.class.php +++ b/includes/components/filter.class.php @@ -63,7 +63,7 @@ abstract class Filter protected const PATTERN_NAME = '/[\p{C};%\\\\]/ui'; protected const PATTERN_CRV = '/[\p{C};:%\\\\]/ui'; protected const PATTERN_INT = '/\D/'; - public const PATTERN_PARAM = '/^[\p{L}\p{Sm} \d\p{P}]+$/i'; + public const PATTERN_PARAM = '/^[\p{L}\p{Sm} \d\p{P}]+$/ui'; 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, From 9b591e7a3a167cf253ec9f542045a201b56f9ae5 Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Tue, 21 Oct 2025 23:31:53 +0200 Subject: [PATCH 067/260] Items/Tooltips * fix itemId to scientific notation conversion, when itemId was joined by an enchantmentId (1234e56 => 1.234e53), breaking tooltip display. --- includes/components/frontend/tooltip.class.php | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/includes/components/frontend/tooltip.class.php b/includes/components/frontend/tooltip.class.php index 78118900..0042c417 100644 --- a/includes/components/frontend/tooltip.class.php +++ b/includes/components/frontend/tooltip.class.php @@ -19,8 +19,11 @@ class Tooltip implements \JsonSerializable private ?string $buff = null; private ?array $buffspells = null; - public function __construct(private string $__powerTpl, private string $__subject, array $opts = []) + public function __construct(private string $__powerTpl, private int|string $__subject, array $opts = []) { + if (!is_int($this->__subject)) + $this->__subject = Util::toJSON($this->__subject, JSON_UNESCAPED_UNICODE); + foreach ($opts as $k => $v) { if (property_exists($this, $k)) @@ -54,7 +57,7 @@ class Tooltip implements \JsonSerializable public function __toString() : string { - return sprintf($this->__powerTpl, Util::toJSON($this->__subject, JSON_AOWOW_POWER), Lang::getLocale()->value, Util::toJSON($this, JSON_AOWOW_POWER))."\n"; + return sprintf($this->__powerTpl, $this->__subject, Lang::getLocale()->value, Util::toJSON($this, JSON_AOWOW_POWER))."\n"; } } From 1365cdb261a875552b5fce4588a1308bc2bf5d1f Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Wed, 22 Oct 2025 01:57:58 +0200 Subject: [PATCH 068/260] Spell/Effects * show spells affected by SPELL_AURA_IGNORE_COMBAT_RESULT --- endpoints/spell/spell.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/endpoints/spell/spell.php b/endpoints/spell/spell.php index abb5a783..c86dbf9c 100644 --- a/endpoints/spell/spell.php +++ b/endpoints/spell/spell.php @@ -14,7 +14,7 @@ class SpellBaseResponse extends TemplateResponse implements ICache SPELL_AURA_ABILITY_PERIODIC_CRIT, SPELL_AURA_MOD_TARGET_ABILITY_ABSORB_SCHOOL, SPELL_AURA_ABILITY_IGNORE_AURASTATE, SPELL_AURA_ALLOW_ONLY_ABILITY, SPELL_AURA_IGNORE_MELEE_RESET, SPELL_AURA_ABILITY_CONSUME_NO_AMMO, SPELL_AURA_MOD_IGNORE_SHAPESHIFT, SPELL_AURA_PERIODIC_HASTE, SPELL_AURA_OVERRIDE_CLASS_SCRIPTS, - SPELL_AURA_MOD_DAMAGE_FROM_CASTER, SPELL_AURA_ADD_TARGET_TRIGGER, /* SPELL_AURA_DUMMY ? */]; + SPELL_AURA_MOD_DAMAGE_FROM_CASTER, SPELL_AURA_ADD_TARGET_TRIGGER, SPELL_AURA_IGNORE_COMBAT_RESULT, /* SPELL_AURA_DUMMY ? */]; protected int $cacheType = CACHE_TYPE_DETAIL_PAGE; From f522c960d97318be6786873c8b58dff5d6bb2b27 Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Wed, 22 Oct 2025 17:07:21 +0200 Subject: [PATCH 069/260] Modelviewer/Fixup * fix buttons of lightbox --- setup/sql/updates/1761145594_01.sql | 1 + setup/tools/filegen/templates/global.js/modelviewer.js | 7 +++++-- 2 files changed, 6 insertions(+), 2 deletions(-) create mode 100644 setup/sql/updates/1761145594_01.sql diff --git a/setup/sql/updates/1761145594_01.sql b/setup/sql/updates/1761145594_01.sql new file mode 100644 index 00000000..03adf418 --- /dev/null +++ b/setup/sql/updates/1761145594_01.sql @@ -0,0 +1 @@ +UPDATE `aowow_dbversion` SET `build` = CONCAT(IFNULL(`build`, ''), ' globaljs'); diff --git a/setup/tools/filegen/templates/global.js/modelviewer.js b/setup/tools/filegen/templates/global.js/modelviewer.js index a1ffe708..182dffa6 100644 --- a/setup/tools/filegen/templates/global.js/modelviewer.js +++ b/setup/tools/filegen/templates/global.js/modelviewer.js @@ -444,8 +444,11 @@ var ModelViewer = new function() rightDiv.append(animDiv); - var a1 = $('', { 'class': 'modelviewer-help', href: '?help=modelviewer', target: '_blank', text: LANG.help }), - a2 = $('', { 'class': 'modelviewer-close', href: 'javascript:;', click: Lightbox.hide, text: LANG.close }); + var a1 = $('', { 'class': 'modelviewer-help', href: '?help=modelviewer', target: '_blank'/* , text: LANG.help */ }), + a2 = $('', { 'class': 'modelviewer-close', href: 'javascript:;', click: Lightbox.hide/* , text: LANG.close */ }); + + a1.append($('')); + a2.append($('')); rightDiv.append(a2); rightDiv.append(a1); From 6ea1457c4f086a55a5e2eed2ab89f874370827e0 Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Wed, 22 Oct 2025 18:03:11 +0200 Subject: [PATCH 070/260] Guides/Fixup * make preview area of class: text so styles are applied as expected * fix evaluation of Markup h2/h3 attribute toc=false, so headings are excluded from TOC as expected --- setup/sql/updates/1761148832_01.sql | 1 + setup/tools/filegen/templates/global.js/markup.js | 4 ++-- template/pages/guide-edit.tpl.php | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) create mode 100644 setup/sql/updates/1761148832_01.sql diff --git a/setup/sql/updates/1761148832_01.sql b/setup/sql/updates/1761148832_01.sql new file mode 100644 index 00000000..03adf418 --- /dev/null +++ b/setup/sql/updates/1761148832_01.sql @@ -0,0 +1 @@ +UPDATE `aowow_dbversion` SET `build` = CONCAT(IFNULL(`build`, ''), ' globaljs'); diff --git a/setup/tools/filegen/templates/global.js/markup.js b/setup/tools/filegen/templates/global.js/markup.js index 97f2e128..285fbc9a 100644 --- a/setup/tools/filegen/templates/global.js/markup.js +++ b/setup/tools/filegen/templates/global.js/markup.js @@ -3126,7 +3126,7 @@ var Markup = { str += "
  • Changelog
  • "; lastNode = "h2"; } - if (node.name == 'h2' && node.attr.toc !== false) + if (node.name == 'h2' && node.attr.toc !== 'false') { if (lastNode == 'h3') { @@ -3136,7 +3136,7 @@ var Markup = { str += '
  • ' + node.attr._textContents + '
  • '; lastNode = 'h2'; } - if (node.name == 'h3' && allowH3 && node.attr.toc !== false && (lastNode != '' || nodes.h2.length == 0)) + if (node.name == 'h3' && allowH3 && node.attr.toc !== 'false' && (lastNode != '' || nodes.h2.length == 0)) { if (lastNode == 'h2') { diff --git a/template/pages/guide-edit.tpl.php b/template/pages/guide-edit.tpl.php index 9f6f2eec..b497929c 100644 --- a/template/pages/guide-edit.tpl.php +++ b/template/pages/guide-edit.tpl.php @@ -295,7 +295,7 @@ endif; } updateQfPreview(); }).bind(this), 50)"> -
    +
    From f9ace6a6719779e38d72df7f10875b154c695544 Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Wed, 22 Oct 2025 18:57:33 +0200 Subject: [PATCH 071/260] Spell/Sources * always display quest source from RewardSpellCast * fix inherited quest sources via learn spells * closes #353 --- endpoints/spell/spell.php | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/endpoints/spell/spell.php b/endpoints/spell/spell.php index c86dbf9c..0ab1ee92 100644 --- a/endpoints/spell/spell.php +++ b/endpoints/spell/spell.php @@ -1066,6 +1066,7 @@ class SpellBaseResponse extends TemplateResponse implements ICache $tbsData = []; if (!$tbSpell->error) { + $tbsData = $tbSpell->getFoundIDs(); $this->lvTabs->addListviewTab(new Listview(array( 'data' => $tbSpell->getListviewData(), 'id' => 'taught-by-spell', @@ -1076,15 +1077,14 @@ class SpellBaseResponse extends TemplateResponse implements ICache } // tab: taught by quest - $conditions = ['OR', ['sourceSpellId', $this->typeId], ['rewardSpell', $this->typeId]]; + $conditions = array( + 'OR', + ['sourceSpellId', $this->typeId], + ['rewardSpell', $this->typeId], + ['rewardSpellCast', $this->typeId] + ); if ($tbsData) - { - $conditions[] = ['rewardSpell', array_keys($tbsData)]; - if (User::isInGroup(U_GROUP_EMPLOYEE)) - $conditions[] = ['rewardSpellCast', array_keys($tbsData)]; - } - if (User::isInGroup(U_GROUP_EMPLOYEE)) - $conditions[] = ['rewardSpellCast', $this->typeId]; + array_push($conditions, ['rewardSpell', $tbsData], ['rewardSpellCast', $tbsData]); $tbQuest = new QuestList($conditions); if (!$tbQuest->error) From 1d922c114740290ff403d4aa0b86a74de503fe4d Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Wed, 22 Oct 2025 22:07:10 +0200 Subject: [PATCH 072/260] Locks * implemented display of LOCK_TYPE_SPELL (3 cases) * show "unlocks" tab on spell detail page * closes #288 --- endpoints/item/item.php | 12 +-- endpoints/spell/spell.php | 49 ++++++++++ includes/dbtypes/gameobject.class.php | 2 +- includes/dbtypes/item.class.php | 2 +- includes/defines.php | 1 + localization/lang.class.php | 124 ++++++++++++++------------ 6 files changed, 127 insertions(+), 63 deletions(-) diff --git a/endpoints/item/item.php b/endpoints/item/item.php index 03f5da42..8f90208d 100644 --- a/endpoints/item/item.php +++ b/endpoints/item/item.php @@ -592,12 +592,14 @@ class ItemBaseResponse extends TemplateResponse implements ICache ), SpellList::$brickFile)); } - // tab: unlocks (object or item) - LOCK_TYPE_ITEM: 1 + // tab: unlocks (object or item) $lockIds = DB::Aowow()->selectCol( - 'SELECT `id` FROM ?_lock WHERE (`type1` = 1 AND `properties1` = ?d) OR - (`type2` = 1 AND `properties2` = ?d) OR (`type3` = 1 AND `properties3` = ?d) OR - (`type4` = 1 AND `properties4` = ?d) OR (`type5` = 1 AND `properties5` = ?d)', - $this->typeId, $this->typeId, $this->typeId, $this->typeId, $this->typeId + 'SELECT `id` FROM ?_lock WHERE (`type1` = ?d AND `properties1` = ?d) OR + (`type2` = ?d AND `properties2` = ?d) OR (`type3` = ?d AND `properties3` = ?d) OR + (`type4` = ?d AND `properties4` = ?d) OR (`type5` = ?d AND `properties5` = ?d)', + LOCK_TYPE_ITEM, $this->typeId, LOCK_TYPE_ITEM, $this->typeId, + LOCK_TYPE_ITEM, $this->typeId, LOCK_TYPE_ITEM, $this->typeId, + LOCK_TYPE_ITEM, $this->typeId ); if ($lockIds) diff --git a/endpoints/spell/spell.php b/endpoints/spell/spell.php index 0ab1ee92..9f651ada 100644 --- a/endpoints/spell/spell.php +++ b/endpoints/spell/spell.php @@ -1166,6 +1166,55 @@ class SpellBaseResponse extends TemplateResponse implements ICache } } + // tab: unlocks (object or item) + $lockIds = DB::Aowow()->selectCol( + 'SELECT `id` FROM ?_lock WHERE (`type1` = ?d AND `properties1` = ?d) OR + (`type2` = ?d AND `properties2` = ?d) OR (`type3` = ?d AND `properties3` = ?d) OR + (`type4` = ?d AND `properties4` = ?d) OR (`type5` = ?d AND `properties5` = ?d)', + LOCK_TYPE_SPELL, $this->typeId, LOCK_TYPE_SPELL, $this->typeId, + LOCK_TYPE_SPELL, $this->typeId, LOCK_TYPE_SPELL, $this->typeId, + LOCK_TYPE_SPELL, $this->typeId + ); + + // we know this spell effect is only in use on index 1 + if ($this->subject->getField('effect1Id') == SPELL_EFFECT_OPEN_LOCK && ($lockId = $this->subject->getField('effect1MiscValue'))) + $lockIds += DB::Aowow()->selectCol( + 'SELECT `id` FROM ?_lock WHERE (`type1` = ?d AND `properties1` = ?d) OR + (`type2` = ?d AND `properties2` = ?d) OR (`type3` = ?d AND `properties3` = ?d) OR + (`type4` = ?d AND `properties4` = ?d) OR (`type5` = ?d AND `properties5` = ?d)', + LOCK_TYPE_SKILL, $lockId, LOCK_TYPE_SKILL, $lockId, + LOCK_TYPE_SKILL, $lockId, LOCK_TYPE_SKILL, $lockId, + LOCK_TYPE_SKILL, $lockId + ); + + if ($lockIds) + { + // objects + $lockedObj = new GameObjectList(array(Cfg::get('SQL_LIMIT_NONE'), ['lockId', $lockIds])); + if (!$lockedObj->error) + { + $this->addDataLoader('zones'); + $this->lvTabs->addListviewTab(new Listview(array( + 'data' => $lockedObj->getListviewData(), + 'name' => '$LANG.tab_unlocks', + 'id' => 'unlocks-object', + 'visibleCols' => $lockedObj->hasSetFields('reqSkill') ? ['skill'] : null + ), GameObjectList::$brickFile)); + } + + $lockedItm = new ItemList(array(Cfg::get('SQL_LIMIT_NONE'), ['lockId', $lockIds])); + if (!$lockedItm->error) + { + $this->extendGlobalData($lockedItm->getJSGlobals(GLOBALINFO_SELF)); + + $this->lvTabs->addListviewTab(new Listview(array( + 'data' => $lockedItm->getListviewData(), + 'name' => '$LANG.tab_unlocks', + 'id' => 'unlocks-item' + ), ItemList::$brickFile)); + } + } + // find associated NPC, Item and merge results // taughtbypets (unused..?) // taughtbyquest (usually the spell casted as quest reward teaches something; exclude those seplls from taughtBySpell) diff --git a/includes/dbtypes/gameobject.class.php b/includes/dbtypes/gameobject.class.php index bcdc0c84..f79db7e6 100644 --- a/includes/dbtypes/gameobject.class.php +++ b/includes/dbtypes/gameobject.class.php @@ -99,7 +99,7 @@ class GameObjectList extends DBTypeList if (isset($this->curTpl['lockId'])) if ($locks = Lang::getLocks($this->curTpl['lockId'])) foreach ($locks as $l) - $x .= ''.sprintf(Lang::game('requires'), $l).''; + $x .= ''.Lang::game('requires', [$l]).''; $x .= ''; diff --git a/includes/dbtypes/item.class.php b/includes/dbtypes/item.class.php index 4bec8f3e..10c4fc1d 100644 --- a/includes/dbtypes/item.class.php +++ b/includes/dbtypes/item.class.php @@ -937,7 +937,7 @@ class ItemList extends DBTypeList // locked or openable if ($locks = Lang::getLocks($this->curTpl['lockId'], $arr, true)) - $x .= ''.Lang::item('locked').'
    '.implode('
    ', array_map(function($x) { return sprintf(Lang::game('requires'), $x); }, $locks)).'

    '; + $x .= ''.Lang::item('locked').'
    '.implode('
    ', array_map(fn($x) => Lang::game('requires', [$x]), $locks)).'

    '; else if ($this->curTpl['flags'] & ITEM_FLAG_OPENABLE) $x .= ''.Lang::item('openClick').'
    '; diff --git a/includes/defines.php b/includes/defines.php index 4894f295..03cf0fce 100644 --- a/includes/defines.php +++ b/includes/defines.php @@ -609,6 +609,7 @@ define('TEAM_NEUTRAL', 2); // Lock Types define('LOCK_TYPE_ITEM', 1); define('LOCK_TYPE_SKILL', 2); +define('LOCK_TYPE_SPELL', 3); // Lock-Properties (also categorizes GOs) define('LOCK_PROPERTY_FOOTLOCKER', 1); diff --git a/localization/lang.class.php b/localization/lang.class.php index c0aa3080..bdedb1c1 100644 --- a/localization/lang.class.php +++ b/localization/lang.class.php @@ -267,75 +267,87 @@ class Lang $rank = $lock['reqSkill'.$i]; $name = ''; - if ($lock['type'.$i] == LOCK_TYPE_ITEM) + switch ($lock['type'.$i]) { - $name = ItemList::getName($prop); - if (!$name) - continue; - - if ($fmt == self::FMT_HTML) - $name = $interactive ? ''.$name.'' : ''.$name.''; - else if ($interactive && $fmt == self::FMT_MARKUP) - { - $name = '[item='.$prop.']'; - $ids[Type::ITEM][] = $prop; - } - else - $name = $prop; - - } - else if ($lock['type'.$i] == LOCK_TYPE_SKILL) - { - $name = self::spell('lockType', $prop); - if (!$name) - continue; - - // skills - if (in_array($prop, [1, 2, 3, 20])) - { - $skills = array( - 1 => SKILL_LOCKPICKING, - 2 => SKILL_HERBALISM, - 3 => SKILL_MINING, - 20 => SKILL_INSCRIPTION - ); + case LOCK_TYPE_ITEM: + $name = ItemList::getName($prop); + if (!$name) + continue 2; if ($fmt == self::FMT_HTML) - $name = $interactive ? ''.$name.'' : ''.$name.''; + $name = $interactive ? ''.$name.'' : ''.$name.''; else if ($interactive && $fmt == self::FMT_MARKUP) { - $name = '[skill='.$skills[$prop].']'; - $ids[Type::SKILL][] = $skills[$prop]; + $name = '[item='.$prop.']'; + $ids[Type::ITEM][] = $prop; + } + + break; + case LOCK_TYPE_SKILL: + $name = self::spell('lockType', $prop); + if (!$name) + continue 2; + + // skills + if (in_array($prop, [1, 2, 3, 20])) + { + $skills = array( + 1 => SKILL_LOCKPICKING, + 2 => SKILL_HERBALISM, + 3 => SKILL_MINING, + 20 => SKILL_INSCRIPTION + ); + + if ($fmt == self::FMT_HTML) + $name = $interactive ? ''.$name.'' : ''.$name.''; + else if ($interactive && $fmt == self::FMT_MARKUP) + { + $name = '[skill='.$skills[$prop].']'; + $ids[Type::SKILL][] = $skills[$prop]; + } + else + $name = SkillList::getName($prop); + + if ($rank > 0) + $name .= ' ('.$rank.')'; + } + // Lockpicking + else if ($prop == 4) + { + if ($fmt == self::FMT_HTML) + $name = $interactive ? ''.$name.'' : ''.$name.''; + else if ($interactive && $fmt == self::FMT_MARKUP) + { + $name = '[spell=1842]'; + $ids[Type::SPELL][] = 1842; + } + } + // exclude unusual stuff + else if (User::isInGroup(U_GROUP_STAFF)) + { + if ($rank > 0) + $name .= ' ('.$rank.')'; } else - $name = $skills[$prop]; + continue 2; + break; + case LOCK_TYPE_SPELL: + $name = SpellList::getName($prop); + if (!$name) + continue 2; - if ($rank > 0) - $name .= ' ('.$rank.')'; - } - // Lockpicking - else if ($prop == 4) - { if ($fmt == self::FMT_HTML) - $name = $interactive ? ''.$name.'' : ''.$name.''; + $name = $interactive ? ''.$name.'' : ''.$name.''; else if ($interactive && $fmt == self::FMT_MARKUP) { - $name = '[spell=1842]'; - $ids[Type::SPELL][] = 1842; + $name = '[spell='.$prop.']'; + $ids[Type::SPELL][] = $prop; } - // else $name = $name - } - // exclude unusual stuff - else if (User::isInGroup(U_GROUP_STAFF)) - { - if ($rank > 0) - $name .= ' ('.$rank.')'; - } - else - continue; + + break; + default: + continue 2; } - else - continue; $locks[$lock['type'.$i] == LOCK_TYPE_ITEM ? $prop : -$prop] = $name; } From b1f22f7e68bcefccea1b3181ba9a241885671329 Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Wed, 22 Oct 2025 23:34:53 +0200 Subject: [PATCH 073/260] Spells/ExtraLoot * display extra loot from skill_extra_item_template like perfect gems * for specializations, display affected spells in Bonus Loot tab * cleanup subject->id to typeId * closes #286 --- endpoints/item/item.php | 14 ++-- endpoints/item/item_xml.php | 2 +- endpoints/spell/spell.php | 144 +++++++++++++++++++++--------------- 3 files changed, 94 insertions(+), 66 deletions(-) diff --git a/endpoints/item/item.php b/endpoints/item/item.php index 8f90208d..d6582962 100644 --- a/endpoints/item/item.php +++ b/endpoints/item/item.php @@ -148,9 +148,9 @@ class ItemBaseResponse extends TemplateResponse implements ICache $infobox[] = Lang::item('tool').'[url=?items&filter=cr=91;crs='.$tId.';crv=0]'.Util::localizedString($tName, 'name').'[/url]'; // extendedCost - if (!empty($this->subject->getExtendedCost([], $_reqRating)[$this->subject->id])) + if (!empty($this->subject->getExtendedCost([], $_reqRating)[$this->typeId])) { - $vendors = $this->subject->getExtendedCost()[$this->subject->id]; + $vendors = $this->subject->getExtendedCost()[$this->typeId]; $stack = $this->subject->getField('buyCount'); $divisor = $stack; $each = ''; @@ -480,9 +480,9 @@ class ItemBaseResponse extends TemplateResponse implements ICache // tabs: this item contains.. $sourceFor = array( - [LOOT_ITEM, $this->subject->id, '$LANG.tab_contains', 'contains', ['$Listview.extraCols.percent'], [] ], - [LOOT_PROSPECTING, $this->subject->id, '$LANG.tab_prospecting', 'prospecting', ['$Listview.extraCols.percent'], ['side', 'slot', 'reqlevel']], - [LOOT_MILLING, $this->subject->id, '$LANG.tab_milling', 'milling', ['$Listview.extraCols.percent'], ['side', 'slot', 'reqlevel']], + [LOOT_ITEM, $this->typeId, '$LANG.tab_contains', 'contains', ['$Listview.extraCols.percent'], [] ], + [LOOT_PROSPECTING, $this->typeId, '$LANG.tab_prospecting', 'prospecting', ['$Listview.extraCols.percent'], ['side', 'slot', 'reqlevel']], + [LOOT_MILLING, $this->typeId, '$LANG.tab_milling', 'milling', ['$Listview.extraCols.percent'], ['side', 'slot', 'reqlevel']], [LOOT_DISENCHANT, $this->subject->getField('disenchantId'), '$LANG.tab_disenchanting', 'disenchanting', ['$Listview.extraCols.percent'], ['side', 'slot', 'reqlevel']] ); @@ -735,9 +735,9 @@ class ItemBaseResponse extends TemplateResponse implements ICache } // tab: sold by - if (!empty($this->subject->getExtendedCost()[$this->subject->id])) + if (!empty($this->subject->getExtendedCost()[$this->typeId])) { - $vendors = $this->subject->getExtendedCost()[$this->subject->id]; + $vendors = $this->subject->getExtendedCost()[$this->typeId]; $soldBy = new CreatureList(array(['id', array_keys($vendors)])); if (!$soldBy->error) { diff --git a/endpoints/item/item_xml.php b/endpoints/item/item_xml.php index ee71200b..6a06851b 100644 --- a/endpoints/item/item_xml.php +++ b/endpoints/item/item_xml.php @@ -192,7 +192,7 @@ class ItemXmlResponse extends TextResponse implements ICache } // link - $xml->addChild('link', Cfg::get('HOST_URL').'?item='.$this->subject->id); + $xml->addChild('link', Cfg::get('HOST_URL').'?item='.$this->typeId); $this->result = $root->asXML(); } diff --git a/endpoints/spell/spell.php b/endpoints/spell/spell.php index 9f651ada..e4f1a206 100644 --- a/endpoints/spell/spell.php +++ b/endpoints/spell/spell.php @@ -449,7 +449,7 @@ class SpellBaseResponse extends TemplateResponse implements ICache ['s.effect1Id', $this->subject->getField('effect1Id')], ['s.effect2Id', $this->subject->getField('effect2Id')], ['s.effect3Id', $this->subject->getField('effect3Id')], - ['s.id', $this->subject->id, '!'], + ['s.id', $this->typeId, '!'], ['s.name_loc'.Lang::getLocale()->value, $this->subject->getField('name', true)] ); @@ -534,7 +534,7 @@ class SpellBaseResponse extends TemplateResponse implements ICache } // tab: used by - spell - if ($so = DB::Aowow()->selectCell('SELECT `id` FROM ?_spelloverride WHERE `spellId1` = ?d OR `spellId2` = ?d OR `spellId3` = ?d OR `spellId4` = ?d OR `spellId5` = ?d', $this->subject->id, $this->subject->id, $this->subject->id, $this->subject->id, $this->subject->id)) + if ($so = DB::Aowow()->selectCell('SELECT `id` FROM ?_spelloverride WHERE `spellId1` = ?d OR `spellId2` = ?d OR `spellId3` = ?d OR `spellId4` = ?d OR `spellId5` = ?d', $this->typeId, $this->typeId, $this->typeId, $this->typeId, $this->typeId)) { $conditions = array( 'OR', @@ -559,8 +559,8 @@ class SpellBaseResponse extends TemplateResponse implements ICache // tab: used by - itemset $conditions = array( 'OR', - ['spell1', $this->subject->id], ['spell2', $this->subject->id], ['spell3', $this->subject->id], ['spell4', $this->subject->id], - ['spell5', $this->subject->id], ['spell6', $this->subject->id], ['spell7', $this->subject->id], ['spell8', $this->subject->id] + ['spell1', $this->typeId], ['spell2', $this->typeId], ['spell3', $this->typeId], ['spell4', $this->typeId], + ['spell5', $this->typeId], ['spell6', $this->typeId], ['spell7', $this->typeId], ['spell8', $this->typeId] ); $ubSets = new ItemsetList($conditions); @@ -578,11 +578,11 @@ class SpellBaseResponse extends TemplateResponse implements ICache // tab: used by - item $conditions = array( 'OR', - ['AND', ['spellTrigger1', SPELL_TRIGGER_LEARN, '!'], ['spellId1', $this->subject->id]], - ['AND', ['spellTrigger2', SPELL_TRIGGER_LEARN, '!'], ['spellId2', $this->subject->id]], - ['AND', ['spellTrigger3', SPELL_TRIGGER_LEARN, '!'], ['spellId3', $this->subject->id]], - ['AND', ['spellTrigger4', SPELL_TRIGGER_LEARN, '!'], ['spellId4', $this->subject->id]], - ['AND', ['spellTrigger5', SPELL_TRIGGER_LEARN, '!'], ['spellId5', $this->subject->id]] + ['AND', ['spellTrigger1', SPELL_TRIGGER_LEARN, '!'], ['spellId1', $this->typeId]], + ['AND', ['spellTrigger2', SPELL_TRIGGER_LEARN, '!'], ['spellId2', $this->typeId]], + ['AND', ['spellTrigger3', SPELL_TRIGGER_LEARN, '!'], ['spellId3', $this->typeId]], + ['AND', ['spellTrigger4', SPELL_TRIGGER_LEARN, '!'], ['spellId4', $this->typeId]], + ['AND', ['spellTrigger5', SPELL_TRIGGER_LEARN, '!'], ['spellId5', $this->typeId]] ); $ubItems = new ItemList($conditions); @@ -600,8 +600,8 @@ class SpellBaseResponse extends TemplateResponse implements ICache // tab: used by - object $conditions = array( 'OR', - ['onUseSpell', $this->subject->id], ['onSuccessSpell', $this->subject->id], - ['auraSpell', $this->subject->id], ['triggeredSpell', $this->subject->id] + ['onUseSpell', $this->typeId], ['onSuccessSpell', $this->typeId], + ['auraSpell', $this->typeId], ['triggeredSpell', $this->typeId] ); if (!empty($ubSAI[Type::OBJECT])) $conditions[] = ['id', $ubSAI[Type::OBJECT]]; @@ -656,54 +656,53 @@ class SpellBaseResponse extends TemplateResponse implements ICache } // tab: contains - // spell_loot_template & skill_extra_item_template - $extraItem = DB::World()->selectRow('SELECT * FROM skill_extra_item_template WHERE `spellid` = ?d', $this->subject->id); + // spell_loot_template $spellLoot = new Loot(); - - if ($spellLoot->getByContainer(LOOT_SPELL, $this->subject->id) || $extraItem) + if ($spellLoot->getByContainer(LOOT_SPELL, $this->typeId)) { $this->extendGlobalData($spellLoot->jsGlobals); - $lv = $spellLoot->getResult(); $extraCols = $spellLoot->extraCols; $extraCols[] = '$Listview.extraCols.percent'; - $lvName = '$LANG.tab_contains'; - - if ($extraItem && $this->subject->canCreateItem()) - { - $foo = $this->subject->relItems->getListviewData(); - - for ($i = 1; $i < 4; $i++) - { - if (($bar = $this->subject->getField('effect'.$i.'CreateItemId')) && isset($foo[$bar])) - { - $lvName = '$LANG.tab_bonusloot'; - $lv[$bar] = $foo[$bar]; - $lv[$bar]['percent'] = $extraItem['additionalCreateChance']; - $lv[$bar]['pctstack'] = $this->buildPctStack($extraItem['additionalCreateChance'] / 100, $extraItem['additionalMaxNum']); - if ($max = ($extraItem['additionalMaxNum'] - 1)) - $lv[$bar]['stack'] = [1, $max]; - - if (Conditions::extendListviewRow($lv[$bar], Conditions::SRC_NONE, $this->typeId, [Conditions::SPELL, $extraItem['requiredSpecialization']])) - { - $this->extendGlobalIds(Type::SPELL, $extraItem['requiredSpecialization']); - $extraCols[] = '$Listview.extraCols.condition'; - } - - break; // skill_extra_item_template can only contain 1 item - } - } - } $this->lvTabs->addListviewTab(new Listview(array( - 'data' => $lv, - 'name' => $lvName, + 'data' => $spellLoot->getResult(), + 'name' => '$LANG.tab_contains', 'id' => 'contains', 'hiddenCols' => ['side', 'slot', 'source', 'reqlevel'], 'extraCols' => array_unique($extraCols) ), ItemList::$brickFile)); } + // tab: bonus loot + if ($extraItemData = DB::World()->select('SELECT `spellId` AS ARRAY_KEY, `additionalCreateChance` AS "0", `additionalMaxNum` AS "1" FROM skill_extra_item_template WHERE `requiredSpecialization` = ?d', $this->typeId)) + { + $extraSpells = new SpellList(array(['id', array_keys($extraItemData)])); + if (!$extraSpells->error) + { + $this->extendGlobalData($extraSpells->getJSGlobals(GLOBALINFO_RELATED)); + $lvItems = $extraSpells->getListviewData(); + + foreach ($lvItems as $iId => $data) + { + [$chance, $maxItr] = $extraItemData[$iId]; + + $lvItems[$iId]['count'] = 1; // expected by js or the pct-col becomes unsortable + $lvItems[$iId]['percent'] = $chance; + $lvItems[$iId]['pctstack'] = $this->buildPctStack($chance / 100, $maxItr, $data['creates'][1]); + $lvItems[$iId]['creates'][2] *= $maxItr; + } + + $this->lvTabs->addListviewTab(new Listview(array( + 'data' => $lvItems, + 'name' => '$LANG.tab_bonusloot', + 'id' => 'bonusloot', + 'hiddenCols' => ['side', 'reqlevel'], + 'extraCols' => ['$Listview.extraCols.percent'] + ), SpellList::$brickFile)); + } + } + // tab: exclusive with if ($this->firstRank && DB::World()->selectCell('SELECT 1 FROM spell_group WHERE `spell_id` = ?d', $this->firstRank)) { @@ -819,9 +818,9 @@ class SpellBaseResponse extends TemplateResponse implements ICache // tab: triggered by $conditions = array( 'OR', - ['AND', ['OR', ['effect1Id', SpellList::EFFECTS_TRIGGER], ['effect1AuraId', SpellList::AURAS_TRIGGER]], ['effect1TriggerSpell', $this->subject->id]], - ['AND', ['OR', ['effect2Id', SpellList::EFFECTS_TRIGGER], ['effect2AuraId', SpellList::AURAS_TRIGGER]], ['effect2TriggerSpell', $this->subject->id]], - ['AND', ['OR', ['effect3Id', SpellList::EFFECTS_TRIGGER], ['effect3AuraId', SpellList::AURAS_TRIGGER]], ['effect3TriggerSpell', $this->subject->id]], + ['AND', ['OR', ['effect1Id', SpellList::EFFECTS_TRIGGER], ['effect1AuraId', SpellList::AURAS_TRIGGER]], ['effect1TriggerSpell', $this->typeId]], + ['AND', ['OR', ['effect2Id', SpellList::EFFECTS_TRIGGER], ['effect2AuraId', SpellList::AURAS_TRIGGER]], ['effect2TriggerSpell', $this->typeId]], + ['AND', ['OR', ['effect3Id', SpellList::EFFECTS_TRIGGER], ['effect3AuraId', SpellList::AURAS_TRIGGER]], ['effect3TriggerSpell', $this->typeId]], ); $trigger = new SpellList($conditions); @@ -1057,9 +1056,9 @@ class SpellBaseResponse extends TemplateResponse implements ICache // tab: taught by spell $conditions = array( 'OR', - ['AND', ['effect1Id', SpellList::EFFECTS_TEACH], ['effect1TriggerSpell', $this->subject->id]], - ['AND', ['effect2Id', SpellList::EFFECTS_TEACH], ['effect2TriggerSpell', $this->subject->id]], - ['AND', ['effect3Id', SpellList::EFFECTS_TEACH], ['effect3TriggerSpell', $this->subject->id]], + ['AND', ['effect1Id', SpellList::EFFECTS_TEACH], ['effect1TriggerSpell', $this->typeId]], + ['AND', ['effect2Id', SpellList::EFFECTS_TEACH], ['effect2TriggerSpell', $this->typeId]], + ['AND', ['effect3Id', SpellList::EFFECTS_TEACH], ['effect3TriggerSpell', $this->typeId]], ); $tbSpell = new SpellList($conditions); @@ -1101,11 +1100,11 @@ class SpellBaseResponse extends TemplateResponse implements ICache // tab: taught by item (i'd like to precheck $this->subject->sources, but there is no source:item only complicated crap like "drop" and "vendor") $conditions = array( 'OR', - ['AND', ['spellTrigger1', SPELL_TRIGGER_LEARN], ['spellId1', $this->subject->id]], - ['AND', ['spellTrigger2', SPELL_TRIGGER_LEARN], ['spellId2', $this->subject->id]], - ['AND', ['spellTrigger3', SPELL_TRIGGER_LEARN], ['spellId3', $this->subject->id]], - ['AND', ['spellTrigger4', SPELL_TRIGGER_LEARN], ['spellId4', $this->subject->id]], - ['AND', ['spellTrigger5', SPELL_TRIGGER_LEARN], ['spellId5', $this->subject->id]], + ['AND', ['spellTrigger1', SPELL_TRIGGER_LEARN], ['spellId1', $this->typeId]], + ['AND', ['spellTrigger2', SPELL_TRIGGER_LEARN], ['spellId2', $this->typeId]], + ['AND', ['spellTrigger3', SPELL_TRIGGER_LEARN], ['spellId3', $this->typeId]], + ['AND', ['spellTrigger4', SPELL_TRIGGER_LEARN], ['spellId4', $this->typeId]], + ['AND', ['spellTrigger5', SPELL_TRIGGER_LEARN], ['spellId5', $this->typeId]], ); $tbItem = new ItemList($conditions); @@ -1240,7 +1239,7 @@ class SpellBaseResponse extends TemplateResponse implements ICache /* SpellLoot recursive dropchance builder */ /******************************************/ - private function buildPctStack(float $baseChance, int $maxStack) : string + private function buildPctStack(float $baseChance, int $maxStack, int $baseCount = 1) : string { // note: pctStack does not contain absolute values but chances relative to the overall drop chance // e.g.: dropChance is 17% then [1 => 50, 2 => 25, 3 => 25] displays > Stack of 1: 8%; Stack of 2: 4%; Stack of 3: 4% @@ -1261,6 +1260,9 @@ class SpellBaseResponse extends TemplateResponse implements ICache // cleanup tiny fractions $pctStack = array_filter($pctStack, fn($x) => ($x * $baseChance) >= 0.01); + if ($baseCount > 1) + $pctStack = array_combine(array_map(fn($x) => $x * $baseCount, array_keys($pctStack)), $pctStack); + return json_encode($pctStack, JSON_NUMERIC_CHECK); // do not replace with Util::toJSON ! } @@ -1640,7 +1642,7 @@ class SpellBaseResponse extends TemplateResponse implements ICache if (!$cndSpell->error) { $_perfItem = array( - 'spellId' => $perfItem['reqSpellId'], + 'spellId' => $cndSpell->id, 'spellName' => $cndSpell->getField('name', true), 'icon' => $cndSpell->getField('iconString'), 'chance' => $perfItem['chance'], @@ -1653,6 +1655,32 @@ class SpellBaseResponse extends TemplateResponse implements ICache ); } } + else if ($extraItem = DB::World()->selectRow('SELECT * FROM skill_extra_item_template WHERE `spellid` = ?d', $this->typeId)) + { + $cndSpell = new SpellList(array(['id', $extraItem['requiredSpecialization']])); + if (!$cndSpell->error) + { + $num = '+'.($effBP + 1); + if ($extraItem['additionalMaxNum'] > 1) + $num .= '-'.($extraItem['additionalMaxNum'] * ($effBP + $effDS)); + else if ($effDS > 1) + $num .= '-'.($effBP + $effDS); + + $_perfItem = array( + 'spellId' => $cndSpell->id, + 'spellName' => $cndSpell->getField('name', true), + 'icon' => $cndSpell->getField('iconString'), + 'chance' => $extraItem['additionalCreateChance'], + 'item' => new IconElement( + Type::ITEM, + $this->subject->relItems->id, + $this->subject->relItems->getField('name', true), + num: $num, + quality: $this->subject->relItems->getField('quality') + ) + ); + } + } } // .. from spell else if (in_array($i, $spellIdx) || $effId == SPELL_EFFECT_UNLEARN_SPECIALIZATION) From 862b3dff7322b365d8914f562f6b0bc9af029899 Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Thu, 23 Oct 2025 00:48:04 +0200 Subject: [PATCH 074/260] IconElement/Fixup * do not change type of num / qty params ('+1' is not numeric) --- includes/components/frontend/iconelement.class.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/includes/components/frontend/iconelement.class.php b/includes/components/frontend/iconelement.class.php index d5ab685e..435e1a28 100644 --- a/includes/components/frontend/iconelement.class.php +++ b/includes/components/frontend/iconelement.class.php @@ -148,9 +148,9 @@ class IconElement $params = [$this->typeId, $this->size]; if ($this->num || $this->qty) - $params[] = is_numeric($this->num) ? $this->num : "'".$this->num."'"; + $params[] = is_int($this->num) ? $this->num : "'".$this->num."'"; if ($this->qty) - $params[] = is_numeric($this->qty) ? $this->qty : "'".$this->qty."'"; + $params[] = is_int($this->qty) ? $this->qty : "'".$this->qty."'"; return str_repeat(' ', $lpad) . sprintf(self::CREATE_ICON_TPL, $this->element, $this->idx, Type::getJSGlobalString($this->type), implode(', ', $params)); } From 6d7f9c0f005efc7d7f2e0230c2f92750f462d48c Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Thu, 23 Oct 2025 16:26:59 +0200 Subject: [PATCH 075/260] Quest/Fixup * don't try to create an objective for empty SourceItem * fixed size of source spell icon --- endpoints/quest/quest.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/endpoints/quest/quest.php b/endpoints/quest/quest.php index a18c5d7e..4c01b65a 100644 --- a/endpoints/quest/quest.php +++ b/endpoints/quest/quest.php @@ -320,7 +320,7 @@ class QuestBaseResponse extends TemplateResponse implements ICache $olItems[$i] = [$id, $qty, $id == $olItems[0][0]]; } - if ($ids = array_column($olItems, 0)) + if ($ids = array_filter(array_column($olItems, 0))) { $olItemData = new ItemList(array(['id', $ids])); $this->extendGlobalData($olItemData->getJSGlobals(GLOBALINFO_SELF)); @@ -498,7 +498,7 @@ class QuestBaseResponse extends TemplateResponse implements ICache if ($_ = $this->subject->getField('sourceSpellId')) { $this->extendGlobalIds(Type::SPELL, $_); - $this->objectiveList[] = [0, new IconElement(Type::SPELL, $_, SpellList::getName($_), extraText: Lang::quest('provided'), element: 'iconlist-icon')]; + $this->objectiveList[] = [0, new IconElement(Type::SPELL, $_, SpellList::getName($_), extraText: Lang::quest('provided'), element: 'iconlist-icon', size: IconElement::SIZE_SMALL)]; } // required money From 1a55b307665b543fe850762b31481e7a235184f2 Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Thu, 23 Oct 2025 17:10:32 +0200 Subject: [PATCH 076/260] UserPage/Fixup * move description inside text container * add missing 'related' heading --- template/pages/user.tpl.php | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/template/pages/user.tpl.php b/template/pages/user.tpl.php index a18a3a48..ae6995df 100644 --- a/template/pages/user.tpl.php +++ b/template/pages/user.tpl.php @@ -13,12 +13,11 @@ $this->brick('announcement'); $this->brick('pageTemplate'); - +?> + +brick('infobox'); ?> - - -
    userIcon): @@ -31,19 +30,20 @@

    h1; ?>

    -
    -

    -
    +
    description): ?> -
    - +
    +
    - + ?>
    + + + From 2f8e035783ff2b18eab0f1b37d6dd4e8f41b0651 Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Fri, 24 Oct 2025 18:28:25 +0200 Subject: [PATCH 077/260] Search/Fixup * fix redirecting to result on exact hit --- endpoints/search/search.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/endpoints/search/search.php b/endpoints/search/search.php index edf8cb73..5a134613 100644 --- a/endpoints/search/search.php +++ b/endpoints/search/search.php @@ -70,7 +70,7 @@ class SearchBaseResponse extends TemplateResponse implements ICache $this->lvTabs->addListviewTab(new Listview(...$lvData)); // we already have a target > can't have more targets > no redirects - if ($canRedirect && $redirectTo) + if (($canRedirect && $redirectTo) || count($lvData[0]['data']) > 1) $canRedirect = false; if ($canRedirect) // note - we are very lucky that in case of searches $template is identical to the typeString From cf2ace805b23d3c15bf368ba4844eb2899ee438d Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Fri, 24 Oct 2025 18:29:41 +0200 Subject: [PATCH 078/260] Spawns/Fixup * fix maps for single-floor dungeons borked in 33cd290dc3553764fd955fb30c087316630a4f6c --- includes/game/worldposition.class.php | 7 +++++-- includes/utilities.php | 2 +- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/includes/game/worldposition.class.php b/includes/game/worldposition.class.php index fbcd16e2..ae7d457e 100644 --- a/includes/game/worldposition.class.php +++ b/includes/game/worldposition.class.php @@ -123,7 +123,8 @@ abstract class WorldPosition 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 `srcPrio`, + IF(useDM.`id` IS NOT NULL OR x.`defaultDungeonMapId` < 0, 1, 0) AS `srcPrio`, + IF(multiDM.`id` IS NOT NULL OR x.`defaultDungeonMapId` < 0, 1, 0) AS `multifloor`, 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) + @@ -133,7 +134,9 @@ abstract class WorldPosition 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 + ?_dungeonmap useDM ON useDM.`mapId` = x.`mapId` AND useDM.`worldMapAreaId` = x.`worldMapAreaId` AND useDM.`floor` = x.`floor` AND useDM.`worldMapAreaId` > 0 + LEFT JOIN + ?_dungeonmap multiDM ON multiDM.`mapId` = x.`mapId` AND multiDM.`worldMapAreaId` = x.`worldMapAreaId` AND multiDM.`floor` <> x.`floor` AND multiDM.`worldMapAreaId` > 0 WHERE x.`mapId` = ?d AND IF(?d, x.`areaId` = ?d, x.`areaId` <> 0){ AND x.`floor` = ?d - IF(x.`defaultDungeonMapId` < 0, 1, 0)} GROUP BY diff --git a/includes/utilities.php b/includes/utilities.php index e746e2fe..2f3db816 100644 --- a/includes/utilities.php +++ b/includes/utilities.php @@ -1173,7 +1173,7 @@ abstract class Util $menu = [[null, "Move Location to..."]]; foreach ($points as $p) { - if ($p['srcPrio']) + if ($p['multifloor']) $floors[$p['areaId']][] = $p['floor']; if (isset($menu[$p['areaId']])) From 3a25c2390f8c93b15a05515b92f00e75553e1f22 Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Sun, 26 Oct 2025 17:24:27 +0100 Subject: [PATCH 079/260] Listviews/AddIns * AddIns must be output directly before the listview it is used by --- .../components/frontend/listview.class.php | 5 +-- includes/game/loot.class.php | 35 ++++++++++++++++++ template/bricks/lvTabs.tpl.php | 18 +++++----- template/listviews/itemStandingCol.tpl | 36 ------------------- 4 files changed, 47 insertions(+), 47 deletions(-) diff --git a/includes/components/frontend/listview.class.php b/includes/components/frontend/listview.class.php index 9520d306..f9913354 100644 --- a/includes/components/frontend/listview.class.php +++ b/includes/components/frontend/listview.class.php @@ -164,10 +164,11 @@ class Listview implements \JsonSerializable public function __toString() : string { + $addIn = ''; if ($this->__addIn) - include($this->__addIn); + $addIn = file_get_contents($this->__addIn).PHP_EOL; - return "new Listview(".Util::toJSON($this).");\n"; + return $addIn.'new Listview('.Util::toJSON($this).');'.PHP_EOL; } } diff --git a/includes/game/loot.class.php b/includes/game/loot.class.php index 1102fd24..56acb2e0 100644 --- a/includes/game/loot.class.php +++ b/includes/game/loot.class.php @@ -19,6 +19,41 @@ if (!defined('AOWOW_REVISION')) quest_mail_loot_template entry quest_template RewMailTemplateId reference_loot_template entry many <- many *_loot_template reference */ +/* 4.3 loot-example + + template: 'item', + id: 'drops', + name: LANG.tab_drops, + tabs: tabsRelated, + parent: 'lkljbjkb574', + extraCols: [Listview.extraCols.count, Listview.extraCols.percent], + sort:['-percent', 'name'], + _totalCount: 448092, // total # creature killed/looted + computeDataFunc: Listview.funcBox.initLootTable, + onAfterCreate: Listview.funcBox.addModeIndicator, + data: [ + { + "classs":15, // Tab Type + "commondrop":true, // loot filtered as "not noteworthy" + "id":25445, + "level":1, + "name":"7Wretched Ichor", + "slot":0, + "source":[2], + "sourcemore":[{"z":3520}], + "subclass":0, // Tab:Type + modes:{ + "mode":4, // &1: heroic; &4: noteworthy(?); &8: reg10; &16: reg25; &32: hc10; &64: hc25; &128: RaidFinder + "4":{"count":363318,"outof":448092} // calculate pct chance + }, + count:363318, + stack:[1,1], + pctstack:'{1: 50.0123, 2: 49.9877}' + } + ] + }); +*/ + class Loot { diff --git a/template/bricks/lvTabs.tpl.php b/template/bricks/lvTabs.tpl.php index f336fe6c..8ed8e9bd 100644 --- a/template/bricks/lvTabs.tpl.php +++ b/template/bricks/lvTabs.tpl.php @@ -14,7 +14,7 @@ if (($this->lvTabs && count($this->lvTabs)) || $this->charactersLvData || $this-
    lvTabs?->getDataContainer() ?? [] as $container): - echo ' '.$container."\n"; + echo ' '.$container.PHP_EOL; endforeach; ?>
    @@ -23,31 +23,31 @@ if (($this->lvTabs && count($this->lvTabs)) || $this->charactersLvData || $this- // seems like WH keeps their modules separated, as fi_gemScores should be with the other fi_ items but are here instead and originally the dbtype globals used by the listviews were also here) // May 2025: WH no longer calculates gems into item scores. Dude .. why? if ($this->gemScores) // set by ItemsBaseResponse - echo ' var fi_gemScores = '.$this->json($this->gemScores).";\n"; + echo ' var fi_gemScores = '.$this->json($this->gemScores).';'.PHP_EOL; // g_items, g_spells, etc required by the listviews used to be here echo $this->lvTabs; if ($this->charactersLvData): - echo ' us_addCharactersTab('.$this->json('charactersLvData').");\n"; + echo ' us_addCharactersTab('.$this->json('charactersLvData').');'.PHP_EOL; endif; if ($this->profilesLvData): - echo ' us_addProfilesTab('.$this->json('profilesLvData').");\n"; + echo ' us_addProfilesTab('.$this->json('profilesLvData').');'.PHP_EOL; endif; if ($this->contribute & CONTRIBUTE_CO): - echo " new Listview({template: 'comment', id: 'comments', name: LANG.tab_comments".($this->lvTabs ? ", tabs: ".$this->lvTabs->__tabVar : '').", parent: 'lv-generic', data: lv_comments});\n"; + echo " new Listview({template: 'comment', id: 'comments', name: LANG.tab_comments".($this->lvTabs ? ", tabs: ".$this->lvTabs->__tabVar : '').", parent: 'lv-generic', data: lv_comments});".PHP_EOL; endif; if ($this->contribute & CONTRIBUTE_SS): - echo " new Listview({template: 'screenshot', id: 'screenshots', name: LANG.tab_screenshots".($this->lvTabs ? ", tabs: ".$this->lvTabs->__tabVar : '').", parent: 'lv-generic', data: lv_screenshots});\n"; + echo " new Listview({template: 'screenshot', id: 'screenshots', name: LANG.tab_screenshots".($this->lvTabs ? ", tabs: ".$this->lvTabs->__tabVar : '').", parent: 'lv-generic', data: lv_screenshots});".PHP_EOL; endif; if ($this->contribute & CONTRIBUTE_VI): - echo " if (lv_videos.length || (g_user && g_user.roles & (U_GROUP_ADMIN | U_GROUP_BUREAU | U_GROUP_VIDEO)))\n"; - echo " new Listview({template: 'video', id: 'videos', name: LANG.tab_videos".($this->lvTabs ? ", tabs: ".$this->lvTabs->__tabVar : '').", parent: 'lv-generic', data: lv_videos});\n"; + echo " if (lv_videos.length || (g_user && g_user.roles & (U_GROUP_ADMIN | U_GROUP_BUREAU | U_GROUP_VIDEO)))".PHP_EOL; + echo " new Listview({template: 'video', id: 'videos', name: LANG.tab_videos".($this->lvTabs ? ", tabs: ".$this->lvTabs->__tabVar : '').", parent: 'lv-generic', data: lv_videos});".PHP_EOL; endif; if ($flushTabs = $this->lvTabs?->getFlush()): - echo " ".$flushTabs."\n"; + echo " ".$flushTabs.PHP_EOL; endif; ?> //]]> diff --git a/template/listviews/itemStandingCol.tpl b/template/listviews/itemStandingCol.tpl index 2b6440f6..86114660 100644 --- a/template/listviews/itemStandingCol.tpl +++ b/template/listviews/itemStandingCol.tpl @@ -16,39 +16,3 @@ var _ = [ } } ]; - From 7cbe1f60073417ef9344174383821cd1dc00d17d Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Sun, 26 Oct 2025 18:49:17 +0100 Subject: [PATCH 080/260] Reports/Fixup * also include source url when checking target context * cleanup source url to be usable as key --- includes/components/report.class.php | 27 ++++++++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/includes/components/report.class.php b/includes/components/report.class.php index f3b766c3..a4ca9457 100644 --- a/includes/components/report.class.php +++ b/includes/components/report.class.php @@ -144,11 +144,11 @@ class Report $this->subject ??= 0; // 0 for utility, tools and misc pages? } - private function checkTargetContext() : int + private function checkTargetContext(?string $url) : int { // check already reported $field = User::isLoggedIn() ? 'userId' : 'ip'; - if (DB::Aowow()->selectCell('SELECT 1 FROM ?_reports WHERE `mode` = ?d AND `reason`= ?d AND `subject` = ?d AND ?# = ?', $this->mode, $this->reason, $this->subject, $field, User::$id ?: User::$ip)) + if (DB::Aowow()->selectCell('SELECT 1 FROM ?_reports WHERE `mode` = ?d AND `reason`= ?d AND `subject` = ?d{ AND `url` = ?} AND ?# = ?', $this->mode, $this->reason, $this->subject, $url ?: DBSIMPLE_SKIP, $field, User::$id ?: User::$ip)) return self::ERR_ALREADY_REPORTED; // check targeted post/postOwner staff status @@ -190,7 +190,28 @@ class Report return false; } - if($err = $this->checkTargetContext()) + // clean up src url: dont use anchors, clean up query + if ($pageUrl) + { + $urlParts = parse_url($pageUrl); + if (!empty($urlParts['query'])) + { + parse_str($urlParts['query'], $query); // kills redundant param declarations + unset($query['locale']); // locale param shouldn't be needed. more..? + $urlParts['query'] = http_build_query($query); + } + + $pageUrl = ''; + if (isset($urlParts['scheme'])) + $pageUrl .= $urlParts['scheme'].':'; + + $pageUrl .= '//'.($urlParts['host'] ?? '').($urlParts['path'] ?? ''); + + if (isset($urlParts['query'])) + $pageUrl .= '?'.$urlParts['query']; + } + + if ($err = $this->checkTargetContext($pageUrl)) { $this->errorCode = $err; return false; From e8bc37f82f419187c816eff8b5b24176f1fdd1dd Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Mon, 27 Oct 2025 01:09:21 +0100 Subject: [PATCH 081/260] Filter/NPC * fix react filter * remove excess colons --- includes/dbtypes/creature.class.php | 4 ++-- template/pages/areatriggers.tpl.php | 2 +- template/pages/npcs.tpl.php | 16 ++++++++-------- template/pages/quests.tpl.php | 6 +++--- template/pages/sounds.tpl.php | 2 +- 5 files changed, 15 insertions(+), 15 deletions(-) diff --git a/includes/dbtypes/creature.class.php b/includes/dbtypes/creature.class.php index 5eb4edd8..bd92c9ec 100644 --- a/includes/dbtypes/creature.class.php +++ b/includes/dbtypes/creature.class.php @@ -376,11 +376,11 @@ class CreatureListFilter extends Filter $parts[] = ['rank', $_v['cl']]; // react Alliance [int] - if ($_v['ra']) + if (!is_null($_v['ra'])) $parts[] = ['ft.A', $_v['ra']]; // react Horde [int] - if ($_v['rh']) + if (!is_null($_v['rh'])) $parts[] = ['ft.H', $_v['rh']]; return $parts; diff --git a/template/pages/areatriggers.tpl.php b/template/pages/areatriggers.tpl.php index 3f187918..e9d574a3 100644 --- a/template/pages/areatriggers.tpl.php +++ b/template/pages/areatriggers.tpl.php @@ -26,7 +26,7 @@ $this->brick('redButtons');

    h1; ?>

    -
    +
    > +   - > - - - + + + diff --git a/template/pages/quests.tpl.php b/template/pages/quests.tpl.php index e7de3d19..427e5aaa 100644 --- a/template/pages/quests.tpl.php +++ b/template/pages/quests.tpl.php @@ -26,7 +26,7 @@ $this->brick('redButtons');

    h1; ?>

    -
    +
    /> - /> - +
            /> - />
    - +   From 9741774683bcb0066460327f88b21dc41aedd573 Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Mon, 27 Oct 2025 17:02:30 +0100 Subject: [PATCH 082/260] Spells/Reagents * fix reagents listing and spell tooltips for nonexistent reagents --- endpoints/spell/spell.php | 14 +++++++------- .../components/frontend/iconelement.class.php | 6 ++++-- includes/dbtypes/spell.class.php | 17 +++++++++++------ template/bricks/reagentList.tpl.php | 10 +++++++--- 4 files changed, 29 insertions(+), 18 deletions(-) diff --git a/endpoints/spell/spell.php b/endpoints/spell/spell.php index e4f1a206..67a7e2d0 100644 --- a/endpoints/spell/spell.php +++ b/endpoints/spell/spell.php @@ -1384,10 +1384,9 @@ class SpellBaseResponse extends TemplateResponse implements ICache if (!$reagents) return; - foreach ($this->subject->relItems->iterate() as $iId => $__) + foreach ($reagents as [$iId, $num]) { - if (!in_array($iId, array_keys($reagents))) - continue; + $relItem = $this->subject->relItems->getEntry($iId); $data = array( 'path' => Type::ITEM.'-'.$iId, // id of the html-element @@ -1396,11 +1395,12 @@ class SpellBaseResponse extends TemplateResponse implements ICache 'typeStr' => Type::getFileString(Type::ITEM), 'icon' => new IconElement( Type::ITEM, - $iId, - $this->subject->relItems->getField('name', true), - $reagents[$iId][1], - quality: $this->subject->relItems->getField('quality'), + is_null($relItem) ? 0 : $iId, + is_null($relItem) ? 'Item #'.$iId : $this->subject->relItems->getField('name', true), + $num, + quality: $relItem['quality'] ?? 'q', size: IconElement::SIZE_SMALL, + link: !is_null($relItem), align: 'right', element: 'iconlist-icon' ) diff --git a/includes/components/frontend/iconelement.class.php b/includes/components/frontend/iconelement.class.php index 435e1a28..6014bd31 100644 --- a/includes/components/frontend/iconelement.class.php +++ b/includes/components/frontend/iconelement.class.php @@ -15,12 +15,12 @@ class IconElement private const CREATE_ICON_TPL = "\$WH.ge('%s%d').appendChild(%s.createIcon(%s));\n"; private int $idx = 0; - private string $href = ''; - private bool $noIcon = false; public readonly string $quality; public readonly ?string $align; + public readonly ?string $href; public readonly int $size; + public readonly bool $noIcon; public function __construct( public readonly int $type, @@ -70,6 +70,8 @@ class IconElement if ($link || $url) $this->href = $url ?: '?'.Type::getFileString($this->type).'='.$this->typeId; + else + $this->href = null; // see Spell/Tools having icon container but no actual icon and having to be inline with other IconElements $this->noIcon = !$typeId || !Type::hasIcon($type); diff --git a/includes/dbtypes/spell.class.php b/includes/dbtypes/spell.class.php index 924c1386..d7e44cd0 100644 --- a/includes/dbtypes/spell.class.php +++ b/includes/dbtypes/spell.class.php @@ -1839,8 +1839,13 @@ class SpellList extends DBTypeList // get reagents $reagents = $this->getReagentsForCurrent(); - foreach ($reagents as &$r) - $r[2] = ItemList::getName($r[0]); + foreach ($reagents as $k => $r) + { + if ($item = $this->relItems->getEntry($r[0])) + $reagents[$k] += [2 => new LocString($item), 3 => true]; + else + $reagents[$k] += [2 => 'Item #'.$r[0], 3 => false]; + } $reagents = array_reverse($reagents); @@ -1940,11 +1945,11 @@ class SpellList extends DBTypeList if ($reagents) { $_ = Lang::spell('reagents').':
    '; - while ($reagent = array_pop($reagents)) + while ([$iId, $qty, $text, $exists] = array_pop($reagents)) { - $_ .= ''.$reagent[2].''; - if ($reagent[1] > 1) - $_ .= ' ('.$reagent[1].')'; + $_ .= $exists ? ''.$text.'' : $text; + if ($qty > 1) + $_ .= ' ('.$qty.')'; $_ .= empty($reagents) ? '
    ' : ', '; } diff --git a/template/bricks/reagentList.tpl.php b/template/bricks/reagentList.tpl.php index b512ee44..0bdd4a13 100644 --- a/template/bricks/reagentList.tpl.php +++ b/template/bricks/reagentList.tpl.php @@ -185,8 +185,12 @@ endif; foreach ($reagents as $k => ['path' => $path, 'level' => $level, 'final' => $final, 'typeStr' => $typeStr, 'icon' => $icon]): $icon->renderContainer(0, $k); // just to set offset - echo '' . - ''; + if ($icon->noIcon): + echo '
    •  
    '; + else: + echo '' . + ''; + endif; if ($final && $enhanced): echo '
     
    '; @@ -194,7 +198,7 @@ foreach ($reagents as $k => ['path' => $path, 'level' => $level, 'final' => $fin echo '
     
    '; endif; - echo ''.$icon->text.''.($icon->num > 1 ? ' ('.$icon->num.')' : '')."\n"; + echo ''.($icon->href ? ''.$icon->text.'' : $icon->text).''.($icon->num > 1 ? ' ('.$icon->num.')' : '')."\n"; endforeach; ?> From 40b2830cad4e637d2f55b2a2153f253c6ea9d239 Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Mon, 27 Oct 2025 17:52:22 +0100 Subject: [PATCH 083/260] Spells/Scaling * hopefully fix a lot of nonsensical spell scaling infos * note: an aweful lot of physical spells are hardcoded or have spell scripts and won't display any info --- endpoints/spell/spell.php | 12 +++++++---- includes/dbtypes/spell.class.php | 35 +++++++++++++++----------------- 2 files changed, 24 insertions(+), 23 deletions(-) diff --git a/endpoints/spell/spell.php b/endpoints/spell/spell.php index 67a7e2d0..30cd67fa 100644 --- a/endpoints/spell/spell.php +++ b/endpoints/spell/spell.php @@ -1425,6 +1425,12 @@ class SpellBaseResponse extends TemplateResponse implements ICache private function createScalingData() : void // calculation mostly like seen in TC { + if ($this->subject->getField('attributes3') & SPELL_ATTR3_NO_DONE_BONUS) + return; + + if (!$this->subject->isScalableDamagingSpell() && !$this->subject->isScalableHealingSpell()) + return; + $scaling = ['directSP' => 0, 'dotSP' => 0, 'directAP' => 0, 'dotAP' => 0]; $pMask = $this->subject->periodicEffectsMask(); $allDoTs = true; @@ -1448,9 +1454,7 @@ class SpellBaseResponse extends TemplateResponse implements ICache if ($s = DB::World()->selectRow('SELECT `direct_bonus` AS "directSP", `dot_bonus` AS "dotSP", `ap_bonus` AS "directAP", `ap_dot_bonus` AS "dotAP" FROM spell_bonus_data WHERE `entry` = ?d', $this->firstRank)) $scaling = $s; - if ((!$this->subject->isDamagingSpell() && !$this->subject->isHealingSpell()) || - !in_array($this->subject->getField('typeCat'), [-2, -3, -7, 7]) || - $this->subject->getField('damageClass') == SPELL_DAMAGE_CLASS_NONE) + if (!in_array($this->subject->getField('typeCat'), [-2, -3, -7, 7]) || $this->subject->getField('damageClass') == SPELL_DAMAGE_CLASS_NONE) { $this->scaling = array_filter($scaling, fn($x) => $x > 0); return; @@ -1509,7 +1513,7 @@ class SpellBaseResponse extends TemplateResponse implements ICache } } - if ($this->subject->isHealingSpell()) + if ($this->subject->isScalableHealingSpell()) $castingTime *= 1.88; // SPELL_SCHOOL_MASK_NORMAL diff --git a/includes/dbtypes/spell.class.php b/includes/dbtypes/spell.class.php index d7e44cd0..e44be63e 100644 --- a/includes/dbtypes/spell.class.php +++ b/includes/dbtypes/spell.class.php @@ -23,12 +23,11 @@ class SpellList extends DBTypeList 11 => SKILLS_TRADE_PRIMARY // prim. Professions ); - public const EFFECTS_HEAL = array( - SPELL_EFFECT_NONE, /*SPELL_EFFECT_DUMMY*/ SPELL_EFFECT_HEAL, SPELL_EFFECT_HEAL_MAX_HEALTH, SPELL_EFFECT_HEAL_MECHANICAL, - SPELL_EFFECT_HEAL_PCT + public const EFFECTS_SCALING_HEAL = array( // as per Unit::SpellHealingBonusDone() calls in TC + SPELL_EFFECT_HEAL, SPELL_EFFECT_HEAL_PCT, SPELL_EFFECT_HEAL_MECHANICAL, SPELL_EFFECT_HEALTH_LEECH ); - public const EFFECTS_DAMAGE = array( - SPELL_EFFECT_NONE, SPELL_EFFECT_DUMMY, SPELL_EFFECT_SCHOOL_DAMAGE, SPELL_EFFECT_HEALTH_LEECH, SPELL_EFFECT_POWER_BURN + public const EFFECTS_SCALING_DAMAGE = array( // as per Unit::SpellDamageBonusDone() calls in TC + SPELL_EFFECT_SCHOOL_DAMAGE, SPELL_EFFECT_HEALTH_LEECH, SPELL_EFFECT_POWER_BURN ); public const EFFECTS_ITEM_CREATE = array( SPELL_EFFECT_CREATE_ITEM, SPELL_EFFECT_SUMMON_CHANGE_ITEM, SPELL_EFFECT_CREATE_RANDOM_ITEM, SPELL_EFFECT_CREATE_MANA_GEM, SPELL_EFFECT_CREATE_ITEM_2 @@ -56,13 +55,11 @@ class SpellList extends DBTypeList SPELL_EFFECT_ENCHANT_ITEM, SPELL_EFFECT_ENCHANT_ITEM_TEMPORARY, SPELL_EFFECT_ENCHANT_HELD_ITEM, SPELL_EFFECT_ENCHANT_ITEM_PRISMATIC ); - public const AURAS_HEAL = array( - SPELL_AURA_DUMMY, SPELL_AURA_PERIODIC_HEAL, SPELL_AURA_PERIODIC_HEALTH_FUNNEL, SPELL_AURA_SCHOOL_ABSORB, SPELL_AURA_MANA_SHIELD, - SPELL_AURA_PERIODIC_DUMMY + public const AURAS_SCALING_HEAL = array( // as per Unit::SpellHealingBonusDone() calls in TC (SPELL_AURA_SCHOOL_ABSORB + SPELL_AURA_MANA_SHIELD priest/mage cases are scripted) + SPELL_AURA_PERIODIC_HEAL, SPELL_AURA_PERIODIC_LEECH, SPELL_AURA_OBS_MOD_HEALTH ); - public const AURAS_DAMAGE = array( - SPELL_AURA_PERIODIC_DAMAGE, SPELL_AURA_DUMMY, SPELL_AURA_DAMAGE_SHIELD, SPELL_AURA_PERIODIC_LEECH, SPELL_AURA_PERIODIC_DAMAGE_PERCENT, - SPELL_AURA_POWER_BURN, SPELL_AURA_PERIODIC_DUMMY + public const AURAS_SCALING_DAMAGE = array( // as per Unit::SpellDamageBonusDone() calls in TC + SPELL_AURA_PERIODIC_DAMAGE, SPELL_AURA_PERIODIC_LEECH, SPELL_AURA_DAMAGE_SHIELD, SPELL_AURA_PROC_TRIGGER_DAMAGE ); public const AURAS_ITEM_CREATE = array( SPELL_AURA_CHANNEL_DEATH_ITEM @@ -770,22 +767,22 @@ class SpellList extends DBTypeList return $this->curTpl['attributes1'] & (SPELL_ATTR1_CHANNELED_1 | SPELL_ATTR1_CHANNELED_2); } - public function isHealingSpell() : bool + public function isScalableHealingSpell() : bool { for ($i = 1; $i < 4; $i++) - if (!in_array($this->curTpl['effect'.$i.'Id'], SpellList::EFFECTS_HEAL) && !in_array($this->curTpl['effect'.$i.'AuraId'], SpellList::AURAS_HEAL)) - return false; + if (in_array($this->curTpl['effect'.$i.'Id'], SpellList::EFFECTS_SCALING_HEAL) || in_array($this->curTpl['effect'.$i.'AuraId'], SpellList::AURAS_SCALING_HEAL)) + return true; - return true; + return false; } - public function isDamagingSpell() : bool + public function isScalableDamagingSpell() : bool { for ($i = 1; $i < 4; $i++) - if (!in_array($this->curTpl['effect'.$i.'Id'], SpellList::EFFECTS_DAMAGE) && !in_array($this->curTpl['effect'.$i.'AuraId'], SpellList::AURAS_DAMAGE)) - return false; + if (in_array($this->curTpl['effect'.$i.'Id'], SpellList::EFFECTS_SCALING_DAMAGE) || in_array($this->curTpl['effect'.$i.'AuraId'], SpellList::AURAS_SCALING_DAMAGE)) + return true; - return true; + return false; } public function periodicEffectsMask() : int From 96c777191d245031c9b55181826d6977faad4f7e Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Mon, 27 Oct 2025 21:19:00 +0100 Subject: [PATCH 084/260] Spells/Scaling * move scaling data to the appropriate spell effects (like WH) --- endpoints/spell/spell.php | 51 ++++++++++++++++++-------------- includes/dbtypes/spell.class.php | 6 ++-- localization/locale_dede.php | 7 ++--- localization/locale_enus.php | 7 ++--- localization/locale_eses.php | 7 ++--- localization/locale_frfr.php | 7 ++--- localization/locale_ruru.php | 7 ++--- localization/locale_zhcn.php | 7 ++--- template/pages/spell.tpl.php | 17 ----------- 9 files changed, 43 insertions(+), 73 deletions(-) diff --git a/endpoints/spell/spell.php b/endpoints/spell/spell.php index 30cd67fa..8196d1f3 100644 --- a/endpoints/spell/spell.php +++ b/endpoints/spell/spell.php @@ -26,7 +26,6 @@ class SpellBaseResponse extends TemplateResponse implements ICache public int $type = Type::SPELL; public int $typeId = 0; public array $reagents = [false, null]; - public array $scaling = []; public string $items = ''; public array $tools = []; public array $effects = []; @@ -161,13 +160,6 @@ class SpellBaseResponse extends TemplateResponse implements ICache $this->createReagentList(); - /**********************/ - /* Spell Scaling Info */ - /**********************/ - - $this->createScalingData(); - - /******************/ /* Required Items */ /******************/ @@ -1423,15 +1415,15 @@ class SpellBaseResponse extends TemplateResponse implements ICache $this->reagents = [$enhanced, $reagentResult]; } - private function createScalingData() : void // calculation mostly like seen in TC + private function calculateEffectScaling() : array // calculation mostly like seen in TC { if ($this->subject->getField('attributes3') & SPELL_ATTR3_NO_DONE_BONUS) - return; + return [0, 0, 0, 0]; if (!$this->subject->isScalableDamagingSpell() && !$this->subject->isScalableHealingSpell()) - return; + return [0, 0, 0, 0]; - $scaling = ['directSP' => 0, 'dotSP' => 0, 'directAP' => 0, 'dotAP' => 0]; + $scaling = [0, 0, 0, 0]; $pMask = $this->subject->periodicEffectsMask(); $allDoTs = true; @@ -1442,23 +1434,20 @@ class SpellBaseResponse extends TemplateResponse implements ICache if ($pMask & 1 << ($i - 1)) { - $scaling['dotSP'] = $this->subject->getField('effect'.$i.'BonusMultiplier'); + $scaling[1] = $this->subject->getField('effect'.$i.'BonusMultiplier'); continue; } else - $scaling['directSP'] = $this->subject->getField('effect'.$i.'BonusMultiplier'); + $scaling[0] = $this->subject->getField('effect'.$i.'BonusMultiplier'); $allDoTs = false; } - if ($s = DB::World()->selectRow('SELECT `direct_bonus` AS "directSP", `dot_bonus` AS "dotSP", `ap_bonus` AS "directAP", `ap_dot_bonus` AS "dotAP" FROM spell_bonus_data WHERE `entry` = ?d', $this->firstRank)) + if ($s = DB::World()->selectRow('SELECT `direct_bonus` AS "0", `dot_bonus` AS "1", `ap_bonus` AS "2", `ap_dot_bonus` AS "3" FROM spell_bonus_data WHERE `entry` = ?d', $this->firstRank)) $scaling = $s; if (!in_array($this->subject->getField('typeCat'), [-2, -3, -7, 7]) || $this->subject->getField('damageClass') == SPELL_DAMAGE_CLASS_NONE) - { - $this->scaling = array_filter($scaling, fn($x) => $x > 0); - return; - } + return array_map(fn($x) => $x < 0 ? 0 : $x, $scaling); foreach ($scaling as $k => $v) { @@ -1467,15 +1456,15 @@ class SpellBaseResponse extends TemplateResponse implements ICache continue; // no known calculation for physical abilities - if ($k == 'directAP' || $k == 'dotAP') + if (in_array($k, [2, 3])) // [direct AP, DoT AP] continue; // dont use spellPower to scale physical Abilities - if ($this->subject->getField('schoolMask') == (1 << SPELL_SCHOOL_NORMAL) && ($k == 'directSP' || $k == 'dotSP')) + if ($this->subject->getField('schoolMask') == (1 << SPELL_SCHOOL_NORMAL) && in_array($k, [0, 1])) continue; $isDOT = false; - if ($k == 'dotSP' || $k == 'dotAP') + if (in_array($k, [1, 3])) // [DoT SP, DoT AP] { if ($pMask) $isDOT = true; @@ -1523,7 +1512,7 @@ class SpellBaseResponse extends TemplateResponse implements ICache $scaling[$k] = 0; // would be 1 ($dotFactor), but we dont want it to be displayed } - $this->scaling = array_filter($scaling, fn($x) => $x > 0); + return array_map(fn($x) => $x < 0 ? 0 : $x, $scaling); } private function createRequiredItems() : void @@ -1585,6 +1574,7 @@ class SpellBaseResponse extends TemplateResponse implements ICache $spellIdx = array_unique(array_merge($this->subject->canTriggerSpell(), $this->subject->canTeachSpell())); $itemIdx = $this->subject->canCreateItem(); $perfItem = DB::World()->selectRow('SELECT `perfectItemType` AS "itemId", `requiredSpecialization` AS "reqSpellId", `perfectCreateChance` AS "chance" FROM skill_perfect_item_template WHERE `spellId` = ?d', $this->typeId); + $scaling = $this->calculateEffectScaling(); // Iterate through all effects: for ($i = 1; $i < 4; $i++) @@ -2220,6 +2210,21 @@ class SpellBaseResponse extends TemplateResponse implements ICache if (isset($_footer['value'][2])) $buffer .= $_footer['value'][2]; + if (in_array($effId, SpellList::EFFECTS_SCALING_DAMAGE)) + { + if ($scaling[2]) + $buffer .= Lang::spell('apMod', [$scaling[2]]); + if ($scaling[0]) + $buffer .= Lang::spell('spMod', [$scaling[0]]); + } + if (in_array($effAura, SpellList::AURAS_SCALING_DAMAGE)) + { + if ($scaling[3]) + $buffer .= Lang::spell('apMod', [$scaling[3]]); + if ($scaling[1]) + $buffer .= Lang::spell('spMod', [$scaling[1]]); + } + $_footer['value'] = $buffer; } else diff --git a/includes/dbtypes/spell.class.php b/includes/dbtypes/spell.class.php index e44be63e..70d6e46f 100644 --- a/includes/dbtypes/spell.class.php +++ b/includes/dbtypes/spell.class.php @@ -47,9 +47,9 @@ class SpellList extends DBTypeList public const EFFECTS_MODEL_NPC = array( SPELL_EFFECT_SUMMON, SPELL_EFFECT_SUMMON_PET, SPELL_EFFECT_SUMMON_DEMON, SPELL_EFFECT_KILL_CREDIT, SPELL_EFFECT_KILL_CREDIT2 ); - public const EFFECTS_DIRECT_SCALING = array( + public const EFFECTS_DIRECT_SCALING = array( // as per Unit::GetCastingTimeForBonus() SPELL_EFFECT_SCHOOL_DAMAGE, SPELL_EFFECT_ENVIRONMENTAL_DAMAGE, SPELL_EFFECT_POWER_DRAIN, SPELL_EFFECT_HEALTH_LEECH, SPELL_EFFECT_POWER_BURN, - SPELL_EFFECT_HEAL_MAX_HEALTH + SPELL_EFFECT_HEAL ); public const EFFECTS_ENCHANTMENT = array( SPELL_EFFECT_ENCHANT_ITEM, SPELL_EFFECT_ENCHANT_ITEM_TEMPORARY, SPELL_EFFECT_ENCHANT_HELD_ITEM, SPELL_EFFECT_ENCHANT_ITEM_PRISMATIC @@ -72,7 +72,7 @@ class SpellList extends DBTypeList SPELL_AURA_TRANSFORM, SPELL_AURA_MOUNTED, SPELL_AURA_CHANGE_MODEL_FOR_ALL_HUMANOIDS, SPELL_AURA_X_RAY, SPELL_AURA_MOD_FAKE_INEBRIATE ); - public const AURAS_PERIODIC_SCALING = array( + public const AURAS_PERIODIC_SCALING = array( // as per Unit::GetCastingTimeForBonus() SPELL_AURA_PERIODIC_DAMAGE, SPELL_AURA_PERIODIC_HEAL, SPELL_AURA_PERIODIC_LEECH ); diff --git a/localization/locale_dede.php b/localization/locale_dede.php index 2a47572c..98b8523a 100644 --- a/localization/locale_dede.php +++ b/localization/locale_dede.php @@ -1678,7 +1678,8 @@ $lang = array( 'pointsPerCP' => ", plus %s pro Combopunkt", 'stackGroup' => "Stack Gruppierung", 'linkedWith' => "Verknüpft mit", - '_scaling' => "Skalierung", + 'apMod' => " (AP mod: %.3g)", + 'spMod' => " (ZM mod: %.3g)", 'instantPhys' => "Sofort", 'castTime' => array( "Spontanzauber", @@ -1725,10 +1726,6 @@ $lang = array( -1 => "Munition", -41 => "Pyrit", -61 => "Dampfdruck", -101 => "Hitze", -121 => "Schlamm", -141 => "Blutmacht", -142 => "Wrath" ), - 'scaling' => array( - 'directSP' => "+%.2f%% der Zaubermacht zum direkten Effekt", 'directAP' => "+%.2f%% der Angriffskraft zum direkten Effekt", - 'dotSP' => "+%.2f%% der Zaubermacht pro Tick", 'dotAP' => "+%.2f%% der Angriffskraft pro Tick" - ), 'relItems' => array( 'base' => "%s im Zusammenhang mit %s anzeigen", 'link' => " oder ", diff --git a/localization/locale_enus.php b/localization/locale_enus.php index 99d09f1d..d26924c9 100644 --- a/localization/locale_enus.php +++ b/localization/locale_enus.php @@ -1678,7 +1678,8 @@ $lang = array( 'pointsPerCP' => ", plus %s per combo point", 'stackGroup' => "Stack Group", 'linkedWith' => "Linked with", - '_scaling' => "Scaling", + 'apMod' => " (AP mod: %.3g)", + 'spMod' => " (SP mod: %.3g)", 'instantPhys' => "Instant", // SPELL_CAST_TIME_INSTANT_NO_MANA 'castTime' => array( "Instant cast", // SPELL_CAST_TIME_INSTANT @@ -1725,10 +1726,6 @@ $lang = array( -1 => "Ammo", -41 => "Pyrite", -61 => "Steam Pressure", -101 => "Heat", -121 => "Ooze", -141 => "Blood Power", -142 => "Wrath" ), - 'scaling' => array( - 'directSP' => "+%.2f%% of spell power to direct component", 'directAP' => "+%.2f%% of attack power to direct component", - 'dotSP' => "+%.2f%% of spell power per tick", 'dotAP' => "+%.2f%% of attack power per tick" - ), 'relItems' => array( 'base' => "Show %s related to %s", 'link' => " or ", diff --git a/localization/locale_eses.php b/localization/locale_eses.php index e88d151e..d775d8da 100644 --- a/localization/locale_eses.php +++ b/localization/locale_eses.php @@ -1678,7 +1678,8 @@ $lang = array( 'pointsPerCP' => ", mas %s por punto de combo", 'stackGroup' => "Grupo de aplilamiento", 'linkedWith' => "Asociado con", - '_scaling' => "Escala", + 'apMod' => " (Mod AP: %.3g)", + 'spMod' => " (Mod SP: %.3g)", 'instantPhys' => "Instantáneo", 'castTime' => array( "Hechizo instantáneo", @@ -1725,10 +1726,6 @@ $lang = array( -1 => "Munición", -41 => "Pirita", -61 => "Presión de vapor", -101 => "Calor", -121 => "Moco", -141 => "Poder de sangre", -142 => "Cólera" ), - 'scaling' => array( - 'directSP' => "+%.2f%% del poder de hechizo al componente directo", 'directAP' => "+%.2f%% del poder de ataque al componente directo", - 'dotSP' => "+%.2f%% del poder de hechizo por tick", 'dotAP' => "+%.2f%% del poder de ataque por tick" - ), 'relItems' => array( 'base' => "Muestra %s relacionados con %s", 'link' => " u ", diff --git a/localization/locale_frfr.php b/localization/locale_frfr.php index 58ac1bbf..15fb5a83 100644 --- a/localization/locale_frfr.php +++ b/localization/locale_frfr.php @@ -1678,7 +1678,8 @@ $lang = array( 'pointsPerCP' => ", plus %s par point de combo", 'stackGroup' => "[Stack Group]", 'linkedWith' => "[Linked with]", - '_scaling' => "[Scaling]", + 'apMod' => " (Mod. AP : %.2f)", + 'spMod' => " (Mod. SP : %.2f)", 'instantPhys' => "Instantané", 'castTime' => array( "Incantation immédiate", @@ -1725,10 +1726,6 @@ $lang = array( -1 => "Munitions", -41 => "Pyrite", -61 => "Pression vapeur", -101 => "Chaleur", -121 => "Limon", -141 => "Puissance de sang", -142 => "Courroux" ), - 'scaling' => array( - 'directSP' => "+%.2f%% de la puissance des sorts directe", 'directAP' => "+%.2f%% de la puissance d'attaque directe", - 'dotSP' => "+%.2f%% de la puissance des sorts par tick", 'dotAP' => "+%.2f%% de la puissance d'attaque par tick" - ), 'relItems' => array( 'base' => "Montre %s reliés à %s", 'link' => " ou ", diff --git a/localization/locale_ruru.php b/localization/locale_ruru.php index 57620310..3058c9f4 100644 --- a/localization/locale_ruru.php +++ b/localization/locale_ruru.php @@ -1678,7 +1678,8 @@ $lang = array( 'pointsPerCP' => ", плюс %s в прием в серии", 'stackGroup' => "[Stack Group]", 'linkedWith' => "[Linked with]", - '_scaling' => "[Scaling]", + 'apMod' => " (Мод.-р АП:%.3g)", + 'spMod' => " (Мод.-р СП:%.3g)", 'instantPhys' => "Мгновенное действие", 'castTime' => array( "Мгновенное действие", @@ -1725,10 +1726,6 @@ $lang = array( -1 => "Боеприпасы", -41 => "Колчедан", -61 => "Давление пара", -101 => "Жар", -121 => "Слизнюк", -141 => "Сила крови", -142 => "Гнев" ), - 'scaling' => array( - 'directSP' => "[+%.2f%% of spell power to direct component]", 'directAP' => "[+%.2f%% of attack power to direct component]", - 'dotSP' => "[+%.2f%% of spell power per tick]", 'dotAP' => "[+%.2f%% of attack power per tick]" - ), 'relItems' => array( 'base' => "Показать %s, относящиеся к профессии %s", 'link' => " или ", diff --git a/localization/locale_zhcn.php b/localization/locale_zhcn.php index 87276104..6d606432 100644 --- a/localization/locale_zhcn.php +++ b/localization/locale_zhcn.php @@ -1678,7 +1678,8 @@ $lang = array( 'pointsPerCP' => ",加%s每连击", 'stackGroup' => "Stack Group", 'linkedWith' => "Linked with", - '_scaling' => "缩放比例", + 'apMod' => "(攻强 mod:%.3g)", + 'spMod' => "(法力 mod:%.3g)", 'instantPhys' => "瞬发", 'castTime' => array( "瞬发法术", @@ -1725,10 +1726,6 @@ $lang = array( -1 => "弹药", -41 => "蓝铁", -61 => "蒸汽动力", -101 => "热能", -121 => "软泥", -141 => "鲜血能量", -142 => "愤怒" ), - 'scaling' => array( - 'directSP' => "直接效果的攻击强度 +%.2f%%", 'directAP' => "直接效果的攻击强度 +%.2f%%", - 'dotSP' => "每个周期的法术强度 +%.2f%%", 'dotAP' => "每个周期的攻击强度 +%.2f%%" - ), 'relItems' => array( 'base' => "显示与%s相关的 %s", 'link' => "或", diff --git a/template/pages/spell.tpl.php b/template/pages/spell.tpl.php index 6995c737..476e3f05 100644 --- a/template/pages/spell.tpl.php +++ b/template/pages/spell.tpl.php @@ -134,23 +134,6 @@ endif; gcd;?> scaling): -?> - - - - -scaling as $k => $v): - echo ' '.Lang::spell('scaling', $k, [$v * 100])."
    \n"; - endforeach; -?> - - -stances): ?> From 88cc76feae8129e510325ca4c6a6a5dba504e593 Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Tue, 28 Oct 2025 19:18:54 +0100 Subject: [PATCH 085/260] Localization/Filters * add missing spell effects & aura names * don't return null for unused effects/auras (triggers an error) --- localization/locale_dede.php | 32 ++++++++++++++++---------------- localization/locale_enus.php | 32 ++++++++++++++++---------------- localization/locale_eses.php | 32 ++++++++++++++++---------------- localization/locale_frfr.php | 32 ++++++++++++++++---------------- localization/locale_ruru.php | 32 ++++++++++++++++---------------- localization/locale_zhcn.php | 32 ++++++++++++++++---------------- static/js/locale_dede.js | 9 ++++----- static/js/locale_enus.js | 9 ++++----- static/js/locale_eses.js | 9 ++++----- static/js/locale_frfr.js | 9 ++++----- static/js/locale_ruru.js | 9 ++++----- static/js/locale_zhcn.js | 9 ++++----- 12 files changed, 120 insertions(+), 126 deletions(-) diff --git a/localization/locale_dede.php b/localization/locale_dede.php index 98b8523a..f62d0677 100644 --- a/localization/locale_dede.php +++ b/localization/locale_dede.php @@ -1839,14 +1839,14 @@ $lang = array( /*102+ */ 'Dismiss Pet', 'Give Reputation', 'Summon Object (Trap)', 'Summon Object (Battle S.)','Summon Object (#3)', 'Summon Object (#4)', /*108+ */ 'Dispel Mechanic', 'Summon Dead Pet', 'Destroy All Totems', 'Durability Damage - Flat', 'Summon Demon', 'Resurrect with Flat Health', /*114+ */ 'Taunt', 'Durability Damage - %', 'Skin Player Corpse (PvP)', 'AoE Resurrect with % Health','Learn Skill', 'Apply Area Aura - Pet', -/*120+ */ 'Teleport to Graveyard', 'Normalized Weapon Damage', null, 'Take Flight Path', 'Pull Towards', 'Modify Threat - %', +/*120+ */ 'Teleport to Graveyard', 'Normalized Weapon Damage', '', 'Take Flight Path', 'Pull Towards', 'Modify Threat - %', /*126+ */ 'Spell Steal ', 'Prospect', 'Apply Area Aura - Friend', 'Apply Area Aura - Enemy', 'Redirect Done Threat %', 'Play Sound', /*132+ */ 'Play Music', 'Unlearn Specialization', 'Kill Credit 2', 'Call Pet', 'Heal for % of Total Health','Give % of Total Power', /*138+ */ 'Leap Back', 'Abandon Quest', 'Force Cast', 'Force Spell Cast with Value','Trigger Spell with Value','Apply Area Aura - Pet Owner', -/*144+ */ 'Knockback to Dest.', 'Pull Towards Dest.', 'Activate Rune', 'Fail Quest', null, 'Charge to Dest', +/*144+ */ 'Knockback to Dest.', 'Pull Towards Dest.', 'Activate Rune', 'Fail Quest', 'Trigger Missile with Value','Charge to Dest', /*150+ */ 'Start Quest', 'Trigger Spell 2', 'Summon - Refer-A-Friend', 'Create Tamed Pet', 'Discover Flight Path', 'Dual Wield 2H Weapons', /*156+ */ 'Add Socket to Item', 'Create Tradeskill Item', 'Milling', 'Rename Pet', 'Force Cast 2', 'Change Talent Spec. Count', -/*162-167*/ 'Activate Talent Spec.', null, 'Remove Aura', null, null, 'Update Player Phase' +/*162-167*/ 'Activate Talent Spec.', '', 'Remove Aura' ), 'unkAura' => 'Unknown Aura (%1$d)', 'auras' => array( @@ -1859,7 +1859,7 @@ $lang = array( 'Mod Skill - Temporary', 'Increase Run Speed %', 'Mod Mounted Speed %', 'Decrease Run Speed %', 'Mod Maximum Health - Flat', 'Mod Maximum Power - Flat', 'Shapeshift', 'Spell Effect Immunity', 'Spell Aura Immunity', 'Spell School Immunity', 'Damage Immunity', 'Dispel Type Immunity', 'Proc Trigger Spell', 'Proc Trigger Damage', 'Track Creatures', - 'Track Resources', 'Ignore All Gear', 'Mod Parry %', null, 'Mod Dodge %', + 'Track Resources', 'Ignore All Gear', 'Mod Parry %', 'Periodic Trigger Spell from Client', 'Mod Dodge %', /*50+ */ 'Mod Critical Healing Amount %', 'Mod Block %', 'Mod Physical Crit Chance', 'Periodically Drain Health', 'Mod Physical Hit Chance', 'Mod Spell Hit Chance', 'Transform', 'Mod Spell Crit Chance', 'Increase Swim Speed %', 'Mod Damage Done Versus Creature', 'Pacify & Silence', 'Mod Size %', 'Periodically Transfer Health', 'Periodic Transfer Power', 'Periodic Drain Power', @@ -1882,19 +1882,19 @@ $lang = array( 'Increase Pet Talent Points', 'Allow Exotic Pets Taming', 'Mechanic Immunity Mask', 'Retain Combo Points', 'Reduce Pushback Time %', /*150+ */ 'Mod Shield Block Value - %', 'Track Stealthed', 'Mod Player Aggro Range', 'Split Damage - Flat', 'Mod Stealth Level', 'Mod Underwater Breathing %', 'Mod All Reputation Gained by %', 'Done Pet Damage Multiplier', 'Mod Shield Block Value - Flat', 'No PvP Credit', - 'Mod AoE Avoidance', 'Mod Health Regen During Combat', 'Mana Burn', 'Mod Melee Critical Damage %', null, + 'Mod AoE Avoidance', 'Mod Health Regen During Combat', 'Mana Burn', 'Mod Melee Critical Damage %', '', 'Mod Attacker Melee Attack Power', 'Mod Melee Attack Power - %', 'Mod Ranged Attack Power - %', 'Mod Damage Done vs Creature', 'Mod Crit Chance vs Creature', - 'Change Object Visibility for Player', 'Mod Run Speed (not stacking)', 'Mod Mounted Speed (not stacking)', null, 'Mod Spell Power by % of Stat', + 'Change Object Visibility for Player', 'Mod Run Speed (not stacking)', 'Mod Mounted Speed (not stacking)', '', 'Mod Spell Power by % of Stat', /*175+ */ 'Mod Healing Power by % of Stat', 'Spirit of Redemption', 'AoE Charm', 'Mod Debuff Resistance - %', 'Mod Attacker Spell Crit Chance', - 'Mod Spell Power vs Creature', null, 'Mod Resistance by % of Stat', 'Mod Threat % of Critical Hits', 'Mod Attacker Melee Hit Chance', + 'Mod Spell Power vs Creature', '', 'Mod Resistance by % of Stat', 'Mod Threat % of Critical Hits', 'Mod Attacker Melee Hit Chance', 'Mod Attacker Ranged Hit Chance', 'Mod Attacker Spell Hit Chance', 'Mod Attacker Melee Crit Chance', 'Mod Attacker Ranged Crit Chance', 'Mod Rating', 'Mod Reputation Gained %', 'Limit Movement Speed', 'Mod Attack Speed %', 'Mod Haste % (gain)', 'Mod Target School Absorb %', - 'Mod Target School Absorb for Ability', 'Mod Cooldowns', 'Mod Attacker Crit Chance', null, 'Mod Spell Hit Chance', + 'Mod Target School Absorb for Ability', 'Mod Cooldowns', 'Mod Attacker Crit Chance', '', 'Mod Spell Hit Chance', /*200+ */ 'Mod Kill Experience Gained %', 'Can Fly', 'Ignore Combat Result', 'Mod Attacker Melee Crit Damage %', 'Mod Attacker Ranged Crit Damage %', 'Mod Attacker Spell Crit Damage %', 'Mod Vehicle Flight Speed %', 'Mod Mounted Flight Speed %', 'Mod Flight Speed %', 'Mod Mounted Flight Speed % (always)', 'Mod Vehicle Speed % (always)', 'Mod Flight Speed % (not stacking)', 'Mod Ranged Attack Power by % of Stat', 'Mod Rage Generated from Damage Dealt', 'Tamed Pet Passive', 'Arena Preparation', 'Mod Spell Haste %', 'Killing Spree', 'Mod Ranged Haste %', 'Mod Mana Regeneration by % of Stat', - 'Mod Combat Rating by % of Stat', 'Ignore Threat', null, 'Raid Proc from Charge', null, + 'Mod Combat Rating by % of Stat', 'Ignore Threat', '', 'Raid Proc from Charge', '', /*225+ */ 'Raid Proc from Charge with Value', 'Periodic Dummy', 'Periodically Trigger Spell with Value','Detect Stealth', 'Mod AoE Damage Taken %', 'Mod Maximum Health - Flat (no stacking)','Proc Trigger Spell with Value', 'Mod Mechanic Duration %', 'Change other Humanoid Display', 'Mod Mechanic Duration % (not stacking)', 'Mod Dispel Resistance %', 'Control Vehicle', 'Mod Spell Power by % of Attack Power', 'Mod Healing Power by % of Attack Power','Mod Size % (not stacking)', @@ -1902,17 +1902,17 @@ $lang = array( 'Mod Aura Duration by Dispel Type', 'Mod Aura Duration by Dispel Type (not stacking)', 'Clone Caster', 'Mod Combat Result Chance', 'Convert Rune', /*250+ */ 'Mod Maximum Health - Flat (stacking)', 'Mod Enemy Dodge Chance', 'Mod Haste % (loss)', 'Mod Critical Block Chance', 'Disarm Offhand', 'Mod Mechanic Damage Taken %', 'No Reagent Cost', 'Mod Target Resistance by Spell Class', 'Mod Spell Visual', 'Mod Periodic Healing Taken %', - 'Screen Effect', 'Phase', 'Ability Ignore Aurastate', 'Allow Only Ability', null, - null, null, 'Cancel Aura Buffer at % of Caster Health','Mod Attack Power by % of Stat', 'Ignore Target Resistance', + 'Screen Effect', 'Phase', 'Ability Ignore Aurastate', 'Allow Only Ability', '', + '', '', 'Cancel Aura Buffer at % of Caster Health','Mod Attack Power by % of Stat', 'Ignore Target Resistance', 'Ignore Target Resistance for Ability', 'Mod Damage Taken % from Caster', 'Ignore Swing Timer Reset', 'X-Ray', 'Ability Consume No Ammo', /*275+ */ 'Mod Ability Ignore Shapeshift', 'Mod Mechanic Damage Done %', 'Mod Max Affected Targets', 'Disarm Ranged Weapon', 'Spawn Effect', 'Mod Armor Penetration %', 'Mod Honor Gain %', 'Mod Base Health %', 'Mod Healing Taken % from Caster', 'Linked Aura', - 'Mod Attack Power by School Resistance','Allow Periodic Ability to Crit', 'Mod Spell Deflect Chance', 'Ignore Hit Direction', null, + 'Mod Attack Power by School Resistance','Allow Periodic Ability to Crit', 'Mod Spell Deflect Chance', 'Ignore Hit Direction', '', 'Mod Crit Chance', 'Mod Quest Experience Gained %', 'Open Stable', 'Override Spells', 'Prevent Power Regeneration', - null, 'Set Vehicle Id', 'Spirit Burst', 'Strangulate', null, -/*300+ */ 'Share Damage %', 'Mod Absorb School Healing', null, 'Mod Damage Done vs Aurastate - %', 'Fake Inebriate', - 'Mod Minimum Speed %', null, 'Heal Absorb Test', 'Mod Critical Strike Chance for Caster',null, - 'Mod Pet AoE Damage Avoidance', null, null, null, 'Prevent Ressurection', + '', 'Set Vehicle Id', 'Spirit Burst', 'Strangulate', '', +/*300+ */ 'Share Damage %', 'Mod Absorb School Healing', '', 'Mod Damage Done vs Aurastate - %', 'Fake Inebriate', + 'Mod Minimum Speed %', '', 'Heal Absorb Test', 'Mod Critical Strike Chance for Caster','', + 'Mod Pet AoE Damage Avoidance', '', '', '', 'Prevent Ressurection', /* -316*/ 'Underwater Walking', 'Periodic Haste' ), 'attributes0' => array( diff --git a/localization/locale_enus.php b/localization/locale_enus.php index d26924c9..bc658ca6 100644 --- a/localization/locale_enus.php +++ b/localization/locale_enus.php @@ -1839,14 +1839,14 @@ $lang = array( /*102+ */ "Dismiss Pet", "Give Reputation", "Summon Object (Trap)", "Summon Object (Battle S.)","Summon Object (#3)", "Summon Object (#4)", /*108+ */ "Dispel Mechanic", "Summon Dead Pet", "Destroy All Totems", "Durability Damage - Flat", "Summon Demon", "Resurrect with Flat Health", /*114+ */ "Taunt", "Durability Damage - %", "Skin Player Corpse (PvP)", "AoE Resurrect with % Health","Learn Skill", "Apply Area Aura - Pet", -/*120+ */ "Teleport to Graveyard", "Normalized Weapon Damage", null, "Take Flight Path", "Pull Towards", "Modify Threat - %", +/*120+ */ "Teleport to Graveyard", "Normalized Weapon Damage", "", "Take Flight Path", "Pull Towards", "Modify Threat - %", /*126+ */ "Spell Steal ", "Prospect", "Apply Area Aura - Friend", "Apply Area Aura - Enemy", "Redirect Done Threat %", "Play Sound", /*132+ */ "Play Music", "Unlearn Specialization", "Kill Credit 2", "Call Pet", "Heal for % of Total Health","Give % of Total Power", /*138+ */ "Leap Back", "Abandon Quest", "Force Cast", "Force Spell Cast with Value","Trigger Spell with Value","Apply Area Aura - Pet Owner", -/*144+ */ "Knockback to Dest.", "Pull Towards Dest.", "Activate Rune", "Fail Quest", null, "Charge to Dest", +/*144+ */ "Knockback to Dest.", "Pull Towards Dest.", "Activate Rune", "Fail Quest", "Trigger Missile with Value","Charge to Dest", /*150+ */ "Start Quest", "Trigger Spell 2", "Summon - Refer-A-Friend", "Create Tamed Pet", "Discover Flight Path", "Dual Wield 2H Weapons", /*156+ */ "Add Socket to Item", "Create Tradeskill Item", "Milling", "Rename Pet", "Force Cast 2", "Change Talent Spec. Count", -/*162-167*/ "Activate Talent Spec.", null, "Remove Aura", null, null, "Update Player Phase" +/*162-167*/ "Activate Talent Spec.", "", "Remove Aura" ), 'unkAura' => 'Unknown Aura (%1$d)', 'auras' => array( @@ -1859,7 +1859,7 @@ $lang = array( "Mod Skill - Temporary", "Increase Run Speed %", "Mod Mounted Speed %", "Decrease Run Speed %", "Mod Maximum Health - Flat", "Mod Maximum Power - Flat", "Shapeshift", "Spell Effect Immunity", "Spell Aura Immunity", "Spell School Immunity", "Damage Immunity", "Dispel Type Immunity", "Proc Trigger Spell", "Proc Trigger Damage", "Track Creatures", - "Track Resources", "Ignore All Gear", "Mod Parry %", null, "Mod Dodge %", + "Track Resources", "Ignore All Gear", "Mod Parry %", "Periodic Trigger Spell from Client", "Mod Dodge %", /*50+ */ "Mod Critical Healing Amount %", "Mod Block %", "Mod Physical Crit Chance", "Periodically Drain Health", "Mod Physical Hit Chance", "Mod Spell Hit Chance", "Transform", "Mod Spell Crit Chance", "Increase Swim Speed %", "Mod Damage Done Versus Creature", "Pacify & Silence", "Mod Size %", "Periodically Transfer Health", "Periodic Transfer Power", "Periodic Drain Power", @@ -1882,19 +1882,19 @@ $lang = array( "Increase Pet Talent Points", "Allow Exotic Pets Taming", "Mechanic Immunity Mask", "Retain Combo Points", "Reduce Pushback Time %", /*150+ */ "Mod Shield Block Value - %", "Track Stealthed", "Mod Player Aggro Range", "Split Damage - Flat", "Mod Stealth Level", "Mod Underwater Breathing %", "Mod All Reputation Gained by %", "Done Pet Damage Multiplier", "Mod Shield Block Value - Flat", "No PvP Credit", - "Mod AoE Avoidance", "Mod Health Regen During Combat", "Mana Burn", "Mod Melee Critical Damage %", null, + "Mod AoE Avoidance", "Mod Health Regen During Combat", "Mana Burn", "Mod Melee Critical Damage %", "", "Mod Attacker Melee Attack Power", "Mod Melee Attack Power - %", "Mod Ranged Attack Power - %", "Mod Damage Done vs Creature", "Mod Crit Chance vs Creature", - "Change Object Visibility for Player", "Mod Run Speed (not stacking)", "Mod Mounted Speed (not stacking)", null, "Mod Spell Power by % of Stat", + "Change Object Visibility for Player", "Mod Run Speed (not stacking)", "Mod Mounted Speed (not stacking)", "", "Mod Spell Power by % of Stat", /*175+ */ "Mod Healing Power by % of Stat", "Spirit of Redemption", "AoE Charm", "Mod Debuff Resistance - %", "Mod Attacker Spell Crit Chance", - "Mod Spell Power vs Creature", null, "Mod Resistance by % of Stat", "Mod Threat % of Critical Hits", "Mod Attacker Melee Hit Chance", + "Mod Spell Power vs Creature", "", "Mod Resistance by % of Stat", "Mod Threat % of Critical Hits", "Mod Attacker Melee Hit Chance", "Mod Attacker Ranged Hit Chance", "Mod Attacker Spell Hit Chance", "Mod Attacker Melee Crit Chance", "Mod Attacker Ranged Crit Chance", "Mod Rating", "Mod Reputation Gained %", "Limit Movement Speed", "Mod Attack Speed %", "Mod Haste % (gain)", "Mod Target School Absorb %", - "Mod Target School Absorb for Ability", "Mod Cooldowns", "Mod Attacker Crit Chance", null, "Mod Spell Hit Chance", + "Mod Target School Absorb for Ability", "Mod Cooldowns", "Mod Attacker Crit Chance", "", "Mod Spell Hit Chance", /*200+ */ "Mod Kill Experience Gained %", "Can Fly", "Ignore Combat Result", "Mod Attacker Melee Crit Damage %", "Mod Attacker Ranged Crit Damage %", "Mod Attacker Spell Crit Damage %", "Mod Vehicle Flight Speed %", "Mod Mounted Flight Speed %", "Mod Flight Speed %", "Mod Mounted Flight Speed % (always)", "Mod Vehicle Speed % (always)", "Mod Flight Speed % (not stacking)", "Mod Ranged Attack Power by % of Stat", "Mod Rage Generated from Damage Dealt", "Tamed Pet Passive", "Arena Preparation", "Mod Spell Haste %", "Killing Spree", "Mod Ranged Haste %", "Mod Mana Regeneration by % of Stat", - "Mod Combat Rating by % of Stat", "Ignore Threat", null, "Raid Proc from Charge", null, + "Mod Combat Rating by % of Stat", "Ignore Threat", "", "Raid Proc from Charge", "", /*225+ */ "Raid Proc from Charge with Value", "Periodic Dummy", "Periodically Trigger Spell with Value","Detect Stealth", "Mod AoE Damage Taken %", "Mod Maximum Health - Flat (no stacking)","Proc Trigger Spell with Value", "Mod Mechanic Duration %", "Change other Humanoid Display", "Mod Mechanic Duration % (not stacking)", "Mod Dispel Resistance %", "Control Vehicle", "Mod Spell Power by % of Attack Power", "Mod Healing Power by % of Attack Power","Mod Size % (not stacking)", @@ -1902,17 +1902,17 @@ $lang = array( "Mod Aura Duration by Dispel Type", "Mod Aura Duration by Dispel Type (not stacking)", "Clone Caster", "Mod Combat Result Chance", "Convert Rune", /*250+ */ "Mod Maximum Health - Flat (stacking)", "Mod Enemy Dodge Chance", "Mod Haste % (loss)", "Mod Critical Block Chance", "Disarm Offhand", "Mod Mechanic Damage Taken %", "No Reagent Cost", "Mod Target Resistance by Spell Class", "Mod Spell Visual", "Mod Periodic Healing Taken %", - "Screen Effect", "Phase", "Ability Ignore Aurastate", "Allow Only Ability", null, - null, null, "Cancel Aura Buffer at % of Caster Health","Mod Attack Power by % of Stat", "Ignore Target Resistance", + "Screen Effect", "Phase", "Ability Ignore Aurastate", "Allow Only Ability", "", + "", "", "Cancel Aura Buffer at % of Caster Health","Mod Attack Power by % of Stat", "Ignore Target Resistance", "Ignore Target Resistance for Ability", "Mod Damage Taken % from Caster", "Ignore Swing Timer Reset", "X-Ray", "Ability Consume No Ammo", /*275+ */ "Mod Ability Ignore Shapeshift", "Mod Mechanic Damage Done %", "Mod Max Affected Targets", "Disarm Ranged Weapon", "Spawn Effect", "Mod Armor Penetration %", "Mod Honor Gain %", "Mod Base Health %", "Mod Healing Taken % from Caster", "Linked Aura", - "Mod Attack Power by School Resistance","Allow Periodic Ability to Crit", "Mod Spell Deflect Chance", "Ignore Hit Direction", null, + "Mod Attack Power by School Resistance","Allow Periodic Ability to Crit", "Mod Spell Deflect Chance", "Ignore Hit Direction", "", "Mod Crit Chance", "Mod Quest Experience Gained %", "Open Stable", "Override Spells", "Prevent Power Regeneration", - null, "Set Vehicle Id", "Spirit Burst", "Strangulate", null, -/*300+ */ "Share Damage %", "Mod Absorb School Healing", null, "Mod Damage Done vs Aurastate - %", "Fake Inebriate", - "Mod Minimum Speed %", null, "Heal Absorb Test", "Mod Critical Strike Chance for Caster",null, - "Mod Pet AoE Damage Avoidance", null, null, null, "Prevent Ressurection", + "", "Set Vehicle Id", "Spirit Burst", "Strangulate", "", +/*300+ */ "Share Damage %", "Mod Absorb School Healing", "", "Mod Damage Done vs Aurastate - %", "Fake Inebriate", + "Mod Minimum Speed %", "", "Heal Absorb Test", "Mod Critical Strike Chance for Caster","", + "Mod Pet AoE Damage Avoidance", "", "", "", "Prevent Ressurection", /* -316*/ "Underwater Walking", "Periodic Haste" ), 'attributes0' => array( diff --git a/localization/locale_eses.php b/localization/locale_eses.php index d775d8da..80f646a2 100644 --- a/localization/locale_eses.php +++ b/localization/locale_eses.php @@ -1839,14 +1839,14 @@ $lang = array( /*102+ */ "Despedir mascota", "Dar reputación", "Invocar objeto (trampa)", "Invocar objeto (batalla)", "Invocar objeto (#3)", "Invocar objeto (#4)", /*108+ */ "Disipar mecánica", "Invocar mascota muerta", "Destruir todos los tótems", "Daño de durabilidad - Fijo", "Invocar demonio", "Resucitar con salud fija", /*114+ */ "Provocar", "Daño de durabilidad - %", "Desollar cadáver de jugador (JcJ)", "Resucitar en área con % salud", "Aprender habilidad", "Aplicar aura de área - Mascota", -/*120+ */ "Teletransportar al cementerio", "Daño de arma normalizado", null, "Tomar ruta de vuelo", "Atraer hacia", "Modificar amenaza - %", +/*120+ */ "Teletransportar al cementerio", "Daño de arma normalizado", "", "Tomar ruta de vuelo", "Atraer hacia", "Modificar amenaza - %", /*126+ */ "Robar hechizo", "Prospectar", "Aplicar aura de área - Amigo", "Aplicar aura de área - Enemigo", "Redirigir amenaza hecha %", "Reproducir sonido", /*132+ */ "Reproducir música", "Olvidar especialización", "Crédito de muerte 2", "Llamar mascota", "Sanar % de salud total", "Dar % de poder total", /*138+ */ "Saltar hacia atrás", "Abandonar misión", "Forzar lanzamiento", "Forzar lanzamiento con valor", "Activar hechizo con valor", "Aplicar aura de área - Dueño de mascota", -/*144+ */ "Empujar a destino", "Atraer a destino", "Activar runa", "Fallar misión", null, "Cargar a destino", +/*144+ */ "Empujar a destino", "Atraer a destino", "Activar runa", "Fallar misión", "Disparar misil con valor", "Cargar a destino", /*150+ */ "Iniciar misión", "Activar hechizo 2", "Invocar - Recluta a un amigo", "Crear mascota domesticada", "Descubrir ruta de vuelo", "Doble empuñadura de armas 2M", /*156+ */ "Añadir hueco a objeto", "Crear objeto de profesión", "Moler", "Renombrar mascota", "Forzar lanzamiento 2", "Cambiar número de espec. de talentos", -/*162-167*/ "Activar espec. de talentos", null, "Eliminar aura", null, null, "Actualizar fase del jugador" +/*162-167*/ "Activar espec. de talentos", "", "Eliminar aura" ), 'unkAura' => 'Aura desconocida (%1$d)', 'auras' => array( @@ -1859,7 +1859,7 @@ $lang = array( "Modificar habilidad - Temporal", "Aumentar velocidad de carrera %", "Modificar velocidad montado %", "Reducir velocidad de carrera %", "Modificar salud máxima - Fijo", "Modificar poder máximo - Fijo", "Cambio de forma", "Inmunidad a efectos de hechizo", "Inmunidad a auras de hechizo", "Inmunidad a escuela de hechizo", "Inmunidad a daño", "Inmunidad a tipo de disipación", "Activar hechizo al recibir efecto", "Activar daño al recibir efecto", "Rastrear criaturas", - "Rastrear recursos", "Ignorar todo el equipo", "Modificar parada %", null, "Modificar esquiva %", + "Rastrear recursos", "Ignorar todo el equipo", "Modificar parada %", "Activar hechizo periódicamente desde el cliente", "Modificar esquiva %", /*50+ */ "Modificar sanación crítica %", "Modificar bloqueo %", "Modificar probabilidad de crítico físico", "Drenar salud periódicamente", "Modificar probabilidad de golpe físico", "Modificar probabilidad de golpe de hechizo", "Transformar", "Modificar probabilidad de crítico de hechizo", "Aumentar velocidad de nado %", "Modificar daño hecho contra criatura", "Pacificar y silenciar", "Modificar tamaño %", "Transferir salud periódicamente", "Transferir poder periódicamente", "Drenar poder periódicamente", @@ -1882,19 +1882,19 @@ $lang = array( "Aumentar puntos de talento de mascota", "Permitir domar mascotas exóticas", "Máscara de inmunidad a mecánicas", "Retener puntos de combo", "Reducir tiempo de retroceso %", /*150+ */ "Modificar valor de bloqueo de escudo - %", "Rastrear en sigilo", "Modificar rango de agresión del jugador", "Dividir daño - Fijo", "Modificar nivel de sigilo", "Modificar respiración bajo el agua %", "Modificar toda la reputación ganada %", "Multiplicador de daño de mascota", "Modificar valor de bloqueo de escudo - Fijo", "Sin crédito JcJ", - "Modificar evasión de área de efecto", "Modificar regeneración de salud en combate", "Quemar maná", "Modificar daño crítico cuerpo a cuerpo %", null, + "Modificar evasión de área de efecto", "Modificar regeneración de salud en combate", "Quemar maná", "Modificar daño crítico cuerpo a cuerpo %", "", "Modificar poder de ataque cuerpo a cuerpo del atacante", "Modificar poder de ataque cuerpo a cuerpo - %", "Modificar poder de ataque a distancia - %", "Modificar daño hecho vs criatura", "Modificar probabilidad de crítico vs criatura", - "Cambiar visibilidad de objeto para jugador", "Modificar velocidad de carrera (no acumulable)", "Modificar velocidad montado (no acumulable)", null, "Modificar poder con % de estadística", + "Cambiar visibilidad de objeto para jugador", "Modificar velocidad de carrera (no acumulable)", "Modificar velocidad montado (no acumulable)", "", "Modificar poder con % de estadística", /*175+ */ "Modificar poder de sanación con % de estadística", "Espíritu de redención", "Encantamiento de área de efecto", "Modificar resistencia a perjuicios - %", "Modificar probabilidad de crítico de hechizo del atacante", - "Modificar poder de hechizo vs criatura", null, "Modificar resistencia con % de estadística", "Modificar amenaza % de golpes críticos", "Modificar probabilidad de golpe cuerpo a cuerpo del atacante", + "Modificar poder de hechizo vs criatura", "", "Modificar resistencia con % de estadística", "Modificar amenaza % de golpes críticos", "Modificar probabilidad de golpe cuerpo a cuerpo del atacante", "Modificar probabilidad de golpe a distancia del atacante", "Modificar probabilidad de golpe de hechizo del atacante", "Modificar probabilidad de crítico cuerpo a cuerpo del atacante", "Modificar probabilidad de crítico a distancia del atacante", "Modificar índice", "Modificar reputación ganada %", "Limitar velocidad de movimiento", "Modificar velocidad de ataque %", "Modificar celeridad % (ganancia)", "Modificar absorción de escuela objetivo %", - "Modificar absorción de escuela objetivo para habilidad", "Modificar tiempos de reutilización", "Modificar probabilidad de crítico del atacante", null, "Modificar probabilidad de golpe de hechizo", + "Modificar absorción de escuela objetivo para habilidad", "Modificar tiempos de reutilización", "Modificar probabilidad de crítico del atacante", "", "Modificar probabilidad de golpe de hechizo", /*200+ */ "Modificar experiencia de muerte ganada %", "Puede volar", "Ignorar resultado de combate", "Modificar daño crítico cuerpo a cuerpo del atacante %", "Modificar daño crítico a distancia del atacante %", "Modificar daño crítico de hechizo del atacante %", "Modificar velocidad de vuelo de vehículo %", "Modificar velocidad de vuelo montado %", "Modificar velocidad de vuelo %", "Modificar velocidad de vuelo montado % (siempre)", "Modificar velocidad de vehículo % (siempre)", "Modificar velocidad de vuelo % (no acumulable)", "Modificar poder de ataque a distancia con % de estadística", "Modificar ira generada por daño infligido", "Mascota domesticada pasiva", "Preparación de arena", "Modificar celeridad de hechizo %", "Racha de muertes", "Modificar celeridad a distancia %", "Modificar regeneración de maná con % de estadística", - "Modificar índice de combate con % de estadística", "Ignorar amenaza", null, "Proc de banda por carga", null, + "Modificar índice de combate con % de estadística", "Ignorar amenaza", "", "Proc de banda por carga", "", /*225+ */ "Proc de banda por carga con valor", "Ficticio periódico", "Activar hechizo periódicamente con valor", "Detectar sigilo", "Modificar daño de área de efecto recibido %", "Modificar salud máxima - Fijo (sin acumulación)", "Activar hechizo al recibir efecto con valor", "Modificar duración de mecánica %", "Cambiar apariencia de humanoide", "Modificar duración de mecánica % (no acumulable)", "Modificar resistencia a disipación %", "Controlar vehículo", "Modificar poder de hechizo con poder de ataque", "Modificar poder de sanación con poder de ataque", "Modificar tamaño % (no acumulable)", @@ -1902,17 +1902,17 @@ $lang = array( "Modificar duración de aura por tipo de disipación", "Modificar duración de aura por tipo de disipación (no acumulable)", "Clonar lanzador", "Modificar probabilidad de resultado de combate", "Convertir runa", /*250+ */ "Modificar salud máxima - Fijo (acumulable)", "Modificar probabilidad de esquiva enemiga", "Modificar celeridad % (pérdida)", "Modificar probabilidad de bloqueo crítico", "Desarmar mano secundaria", "Modificar daño de mecánica %", "Sin coste de reagente", "Modificar resistencia objetivo por clase de hechizo", "Modificar visual de hechizo", "Modificar sanación periódica recibida %", - "Efecto de pantalla", "Fase", "Ignorar estado de aura de habilidad", "Permitir solo habilidad", null, - null, null, "Cancelar buffer de aura al % de salud del lanzador", "Modificar poder de ataque con % de estadística", "Ignorar resistencia del objetivo", + "Efecto de pantalla", "Fase", "Ignorar estado de aura de habilidad", "Permitir solo habilidad", "", + "", "", "Cancelar buffer de aura al % de salud del lanzador", "Modificar poder de ataque con % de estadística", "Ignorar resistencia del objetivo", "Ignorar resistencia del objetivo para habilidad", "Modificar daño recibido % del lanzador", "Ignorar reinicio de temporizador de golpe", "Rayos X", "Habilidad no consume munición", /*275+ */ "Ignorar cambio de forma para habilidad", "Modificar daño de mecánica hecho %", "Modificar máximo de objetivos afectados", "Desarmar arma a distancia", "Efecto de aparición", "Modificar penetración de armadura %", "Modificar ganancia de honor %", "Modificar salud base %", "Modificar sanación recibida % del lanzador", "Aura vinculada", - "Modificar poder de ataque con resistencia de escuela", "Permitir crítico periódico de habilidad", "Modificar probabilidad de desvío de hechizo", "Ignorar dirección de golpe", null, + "Modificar poder de ataque con resistencia de escuela", "Permitir crítico periódico de habilidad", "Modificar probabilidad de desvío de hechizo", "Ignorar dirección de golpe", "", "Modificar probabilidad de crítico", "Modificar experiencia de misión ganada %", "Abrir establo", "Sobrescribir hechizos", "Prevenir regeneración de poder", - null, "Establecer ID de vehículo", "Explosión espiritual", "Estrangular", null, -/*300+ */ "Compartir daño %", "Modificar absorción de sanación de escuela", null, "Modificar daño hecho vs aura de estado - %", "Fingir embriaguez", - "Modificar velocidad mínima %", null, "Prueba de absorción de sanación", "Modificar probabilidad de golpe crítico para lanzador", null, - "Evitar daño de área de efecto en mascota", null, null, null, "Prevenir resurrección", + "", "Establecer ID de vehículo", "Explosión espiritual", "Estrangular", "", +/*300+ */ "Compartir daño %", "Modificar absorción de sanación de escuela", "", "Modificar daño hecho vs aura de estado - %", "Fingir embriaguez", + "Modificar velocidad mínima %", "", "Prueba de absorción de sanación", "Modificar probabilidad de golpe crítico para lanzador", "", + "Evitar daño de área de efecto en mascota", "", "", "", "Prevenir resurrección", /* -316*/ "Caminar bajo el agua", "Celeridad periódica" ), 'attributes0' => array( diff --git a/localization/locale_frfr.php b/localization/locale_frfr.php index 15fb5a83..8224a965 100644 --- a/localization/locale_frfr.php +++ b/localization/locale_frfr.php @@ -1839,14 +1839,14 @@ $lang = array( /*102+ */ 'Dismiss Pet', 'Give Reputation', 'Summon Object (Trap)', 'Summon Object (Battle S.)','Summon Object (#3)', 'Summon Object (#4)', /*108+ */ 'Dispel Mechanic', 'Summon Dead Pet', 'Destroy All Totems', 'Durability Damage - Flat', 'Summon Demon', 'Resurrect with Flat Health', /*114+ */ 'Taunt', 'Durability Damage - %', 'Skin Player Corpse (PvP)', 'AoE Resurrect with % Health','Learn Skill', 'Apply Area Aura - Pet', -/*120+ */ 'Teleport to Graveyard', 'Normalized Weapon Damage', null, 'Take Flight Path', 'Pull Towards', 'Modify Threat - %', +/*120+ */ 'Teleport to Graveyard', 'Normalized Weapon Damage', '', 'Take Flight Path', 'Pull Towards', 'Modify Threat - %', /*126+ */ 'Spell Steal ', 'Prospect', 'Apply Area Aura - Friend', 'Apply Area Aura - Enemy', 'Redirect Done Threat %', 'Play Sound', /*132+ */ 'Play Music', 'Unlearn Specialization', 'Kill Credit 2', 'Call Pet', 'Heal for % of Total Health','Give % of Total Power', /*138+ */ 'Leap Back', 'Abandon Quest', 'Force Cast', 'Force Spell Cast with Value','Trigger Spell with Value','Apply Area Aura - Pet Owner', -/*144+ */ 'Knockback to Dest.', 'Pull Towards Dest.', 'Activate Rune', 'Fail Quest', null, 'Charge to Dest', +/*144+ */ 'Knockback to Dest.', 'Pull Towards Dest.', 'Activate Rune', 'Fail Quest', 'Trigger Missile with Value','Charge to Dest', /*150+ */ 'Start Quest', 'Trigger Spell 2', 'Summon - Refer-A-Friend', 'Create Tamed Pet', 'Discover Flight Path', 'Dual Wield 2H Weapons', /*156+ */ 'Add Socket to Item', 'Create Tradeskill Item', 'Milling', 'Rename Pet', 'Force Cast 2', 'Change Talent Spec. Count', -/*162-167*/ 'Activate Talent Spec.', null, 'Remove Aura', null, null, 'Update Player Phase' +/*162-167*/ 'Activate Talent Spec.', '', 'Remove Aura' ), 'unkAura' => 'Unknown Aura (%1$d)', 'auras' => array( @@ -1859,7 +1859,7 @@ $lang = array( 'Mod Skill - Temporary', 'Increase Run Speed %', 'Mod Mounted Speed %', 'Decrease Run Speed %', 'Mod Maximum Health - Flat', 'Mod Maximum Power - Flat', 'Shapeshift', 'Spell Effect Immunity', 'Spell Aura Immunity', 'Spell School Immunity', 'Damage Immunity', 'Dispel Type Immunity', 'Proc Trigger Spell', 'Proc Trigger Damage', 'Track Creatures', - 'Track Resources', 'Ignore All Gear', 'Mod Parry %', null, 'Mod Dodge %', + 'Track Resources', 'Ignore All Gear', 'Mod Parry %', 'Periodic Trigger Spell from Client', 'Mod Dodge %', /*50+ */ 'Mod Critical Healing Amount %', 'Mod Block %', 'Mod Physical Crit Chance', 'Periodically Drain Health', 'Mod Physical Hit Chance', 'Mod Spell Hit Chance', 'Transform', 'Mod Spell Crit Chance', 'Increase Swim Speed %', 'Mod Damage Done Versus Creature', 'Pacify & Silence', 'Mod Size %', 'Periodically Transfer Health', 'Periodic Transfer Power', 'Periodic Drain Power', @@ -1882,19 +1882,19 @@ $lang = array( 'Increase Pet Talent Points', 'Allow Exotic Pets Taming', 'Mechanic Immunity Mask', 'Retain Combo Points', 'Reduce Pushback Time %', /*150+ */ 'Mod Shield Block Value - %', 'Track Stealthed', 'Mod Player Aggro Range', 'Split Damage - Flat', 'Mod Stealth Level', 'Mod Underwater Breathing %', 'Mod All Reputation Gained by %', 'Done Pet Damage Multiplier', 'Mod Shield Block Value - Flat', 'No PvP Credit', - 'Mod AoE Avoidance', 'Mod Health Regen During Combat', 'Mana Burn', 'Mod Melee Critical Damage %', null, + 'Mod AoE Avoidance', 'Mod Health Regen During Combat', 'Mana Burn', 'Mod Melee Critical Damage %', '', 'Mod Attacker Melee Attack Power', 'Mod Melee Attack Power - %', 'Mod Ranged Attack Power - %', 'Mod Damage Done vs Creature', 'Mod Crit Chance vs Creature', - 'Change Object Visibility for Player', 'Mod Run Speed (not stacking)', 'Mod Mounted Speed (not stacking)', null, 'Mod Spell Power by % of Stat', + 'Change Object Visibility for Player', 'Mod Run Speed (not stacking)', 'Mod Mounted Speed (not stacking)', '', 'Mod Spell Power by % of Stat', /*175+ */ 'Mod Healing Power by % of Stat', 'Spirit of Redemption', 'AoE Charm', 'Mod Debuff Resistance - %', 'Mod Attacker Spell Crit Chance', - 'Mod Spell Power vs Creature', null, 'Mod Resistance by % of Stat', 'Mod Threat % of Critical Hits', 'Mod Attacker Melee Hit Chance', + 'Mod Spell Power vs Creature', '', 'Mod Resistance by % of Stat', 'Mod Threat % of Critical Hits', 'Mod Attacker Melee Hit Chance', 'Mod Attacker Ranged Hit Chance', 'Mod Attacker Spell Hit Chance', 'Mod Attacker Melee Crit Chance', 'Mod Attacker Ranged Crit Chance', 'Mod Rating', 'Mod Reputation Gained %', 'Limit Movement Speed', 'Mod Attack Speed %', 'Mod Haste % (gain)', 'Mod Target School Absorb %', - 'Mod Target School Absorb for Ability', 'Mod Cooldowns', 'Mod Attacker Crit Chance', null, 'Mod Spell Hit Chance', + 'Mod Target School Absorb for Ability', 'Mod Cooldowns', 'Mod Attacker Crit Chance', '', 'Mod Spell Hit Chance', /*200+ */ 'Mod Kill Experience Gained %', 'Can Fly', 'Ignore Combat Result', 'Mod Attacker Melee Crit Damage %', 'Mod Attacker Ranged Crit Damage %', 'Mod Attacker Spell Crit Damage %', 'Mod Vehicle Flight Speed %', 'Mod Mounted Flight Speed %', 'Mod Flight Speed %', 'Mod Mounted Flight Speed % (always)', 'Mod Vehicle Speed % (always)', 'Mod Flight Speed % (not stacking)', 'Mod Ranged Attack Power by % of Stat', 'Mod Rage Generated from Damage Dealt', 'Tamed Pet Passive', 'Arena Preparation', 'Mod Spell Haste %', 'Killing Spree', 'Mod Ranged Haste %', 'Mod Mana Regeneration by % of Stat', - 'Mod Combat Rating by % of Stat', 'Ignore Threat', null, 'Raid Proc from Charge', null, + 'Mod Combat Rating by % of Stat', 'Ignore Threat', '', 'Raid Proc from Charge', '', /*225+ */ 'Raid Proc from Charge with Value', 'Periodic Dummy', 'Periodically Trigger Spell with Value','Detect Stealth', 'Mod AoE Damage Taken %', 'Mod Maximum Health - Flat (no stacking)','Proc Trigger Spell with Value', 'Mod Mechanic Duration %', 'Change other Humanoid Display', 'Mod Mechanic Duration % (not stacking)', 'Mod Dispel Resistance %', 'Control Vehicle', 'Mod Spell Power by % of Attack Power', 'Mod Healing Power by % of Attack Power','Mod Size % (not stacking)', @@ -1902,17 +1902,17 @@ $lang = array( 'Mod Aura Duration by Dispel Type', 'Mod Aura Duration by Dispel Type (not stacking)', 'Clone Caster', 'Mod Combat Result Chance', 'Convert Rune', /*250+ */ 'Mod Maximum Health - Flat (stacking)', 'Mod Enemy Dodge Chance', 'Mod Haste % (loss)', 'Mod Critical Block Chance', 'Disarm Offhand', 'Mod Mechanic Damage Taken %', 'No Reagent Cost', 'Mod Target Resistance by Spell Class', 'Mod Spell Visual', 'Mod Periodic Healing Taken %', - 'Screen Effect', 'Phase', 'Ability Ignore Aurastate', 'Allow Only Ability', null, - null, null, 'Cancel Aura Buffer at % of Caster Health','Mod Attack Power by % of Stat', 'Ignore Target Resistance', + 'Screen Effect', 'Phase', 'Ability Ignore Aurastate', 'Allow Only Ability', '', + '', '', 'Cancel Aura Buffer at % of Caster Health','Mod Attack Power by % of Stat', 'Ignore Target Resistance', 'Ignore Target Resistance for Ability', 'Mod Damage Taken % from Caster', 'Ignore Swing Timer Reset', 'X-Ray', 'Ability Consume No Ammo', /*275+ */ 'Mod Ability Ignore Shapeshift', 'Mod Mechanic Damage Done %', 'Mod Max Affected Targets', 'Disarm Ranged Weapon', 'Spawn Effect', 'Mod Armor Penetration %', 'Mod Honor Gain %', 'Mod Base Health %', 'Mod Healing Taken % from Caster', 'Linked Aura', - 'Mod Attack Power by School Resistance','Allow Periodic Ability to Crit', 'Mod Spell Deflect Chance', 'Ignore Hit Direction', null, + 'Mod Attack Power by School Resistance','Allow Periodic Ability to Crit', 'Mod Spell Deflect Chance', 'Ignore Hit Direction', '', 'Mod Crit Chance', 'Mod Quest Experience Gained %', 'Open Stable', 'Override Spells', 'Prevent Power Regeneration', - null, 'Set Vehicle Id', 'Spirit Burst', 'Strangulate', null, -/*300+ */ 'Share Damage %', 'Mod Absorb School Healing', null, 'Mod Damage Done vs Aurastate - %', 'Fake Inebriate', - 'Mod Minimum Speed %', null, 'Heal Absorb Test', 'Mod Critical Strike Chance for Caster',null, - 'Mod Pet AoE Damage Avoidance', null, null, null, 'Prevent Ressurection', + '', 'Set Vehicle Id', 'Spirit Burst', 'Strangulate', '', +/*300+ */ 'Share Damage %', 'Mod Absorb School Healing', '', 'Mod Damage Done vs Aurastate - %', 'Fake Inebriate', + 'Mod Minimum Speed %', '', 'Heal Absorb Test', 'Mod Critical Strike Chance for Caster','', + 'Mod Pet AoE Damage Avoidance', '', '', '', 'Prevent Ressurection', /* -316*/ 'Underwater Walking', 'Periodic Haste' ), 'attributes0' => array( diff --git a/localization/locale_ruru.php b/localization/locale_ruru.php index 3058c9f4..8555d957 100644 --- a/localization/locale_ruru.php +++ b/localization/locale_ruru.php @@ -1839,14 +1839,14 @@ $lang = array( /*102+ */ 'Dismiss Pet', 'Give Reputation', 'Summon Object (Trap)', 'Summon Object (Battle S.)','Summon Object (#3)', 'Summon Object (#4)', /*108+ */ 'Dispel Mechanic', 'Summon Dead Pet', 'Destroy All Totems', 'Durability Damage - Flat', 'Summon Demon', 'Resurrect with Flat Health', /*114+ */ 'Taunt', 'Durability Damage - %', 'Skin Player Corpse (PvP)', 'AoE Resurrect with % Health','Learn Skill', 'Apply Area Aura - Pet', -/*120+ */ 'Teleport to Graveyard', 'Normalized Weapon Damage', null, 'Take Flight Path', 'Pull Towards', 'Modify Threat - %', +/*120+ */ 'Teleport to Graveyard', 'Normalized Weapon Damage', '', 'Take Flight Path', 'Pull Towards', 'Modify Threat - %', /*126+ */ 'Spell Steal ', 'Prospect', 'Apply Area Aura - Friend', 'Apply Area Aura - Enemy', 'Redirect Done Threat %', 'Play Sound', /*132+ */ 'Play Music', 'Unlearn Specialization', 'Kill Credit 2', 'Call Pet', 'Heal for % of Total Health','Give % of Total Power', /*138+ */ 'Leap Back', 'Abandon Quest', 'Force Cast', 'Force Spell Cast with Value','Trigger Spell with Value','Apply Area Aura - Pet Owner', -/*144+ */ 'Knockback to Dest.', 'Pull Towards Dest.', 'Activate Rune', 'Fail Quest', null, 'Charge to Dest', +/*144+ */ 'Knockback to Dest.', 'Pull Towards Dest.', 'Activate Rune', 'Fail Quest', 'Trigger Missile with Value','Charge to Dest', /*150+ */ 'Start Quest', 'Trigger Spell 2', 'Summon - Refer-A-Friend', 'Create Tamed Pet', 'Discover Flight Path', 'Dual Wield 2H Weapons', /*156+ */ 'Add Socket to Item', 'Create Tradeskill Item', 'Milling', 'Rename Pet', 'Force Cast 2', 'Change Talent Spec. Count', -/*162-167*/ 'Activate Talent Spec.', null, 'Remove Aura', null, null, 'Update Player Phase' +/*162-167*/ 'Activate Talent Spec.', '', 'Remove Aura' ), 'unkAura' => 'Unknown Aura (%1$d)', 'auras' => array( @@ -1859,7 +1859,7 @@ $lang = array( 'Mod Skill - Temporary', 'Increase Run Speed %', 'Mod Mounted Speed %', 'Decrease Run Speed %', 'Mod Maximum Health - Flat', 'Mod Maximum Power - Flat', 'Shapeshift', 'Spell Effect Immunity', 'Spell Aura Immunity', 'Spell School Immunity', 'Damage Immunity', 'Dispel Type Immunity', 'Proc Trigger Spell', 'Proc Trigger Damage', 'Track Creatures', - 'Track Resources', 'Ignore All Gear', 'Mod Parry %', null, 'Mod Dodge %', + 'Track Resources', 'Ignore All Gear', 'Mod Parry %', 'Periodic Trigger Spell from Client', 'Mod Dodge %', /*50+ */ 'Mod Critical Healing Amount %', 'Mod Block %', 'Mod Physical Crit Chance', 'Periodically Drain Health', 'Mod Physical Hit Chance', 'Mod Spell Hit Chance', 'Transform', 'Mod Spell Crit Chance', 'Increase Swim Speed %', 'Mod Damage Done Versus Creature', 'Pacify & Silence', 'Mod Size %', 'Periodically Transfer Health', 'Periodic Transfer Power', 'Periodic Drain Power', @@ -1882,19 +1882,19 @@ $lang = array( 'Increase Pet Talent Points', 'Allow Exotic Pets Taming', 'Mechanic Immunity Mask', 'Retain Combo Points', 'Reduce Pushback Time %', /*150+ */ 'Mod Shield Block Value - %', 'Track Stealthed', 'Mod Player Aggro Range', 'Split Damage - Flat', 'Mod Stealth Level', 'Mod Underwater Breathing %', 'Mod All Reputation Gained by %', 'Done Pet Damage Multiplier', 'Mod Shield Block Value - Flat', 'No PvP Credit', - 'Mod AoE Avoidance', 'Mod Health Regen During Combat', 'Mana Burn', 'Mod Melee Critical Damage %', null, + 'Mod AoE Avoidance', 'Mod Health Regen During Combat', 'Mana Burn', 'Mod Melee Critical Damage %', '', 'Mod Attacker Melee Attack Power', 'Mod Melee Attack Power - %', 'Mod Ranged Attack Power - %', 'Mod Damage Done vs Creature', 'Mod Crit Chance vs Creature', - 'Change Object Visibility for Player', 'Mod Run Speed (not stacking)', 'Mod Mounted Speed (not stacking)', null, 'Mod Spell Power by % of Stat', + 'Change Object Visibility for Player', 'Mod Run Speed (not stacking)', 'Mod Mounted Speed (not stacking)', '', 'Mod Spell Power by % of Stat', /*175+ */ 'Mod Healing Power by % of Stat', 'Spirit of Redemption', 'AoE Charm', 'Mod Debuff Resistance - %', 'Mod Attacker Spell Crit Chance', - 'Mod Spell Power vs Creature', null, 'Mod Resistance by % of Stat', 'Mod Threat % of Critical Hits', 'Mod Attacker Melee Hit Chance', + 'Mod Spell Power vs Creature', '', 'Mod Resistance by % of Stat', 'Mod Threat % of Critical Hits', 'Mod Attacker Melee Hit Chance', 'Mod Attacker Ranged Hit Chance', 'Mod Attacker Spell Hit Chance', 'Mod Attacker Melee Crit Chance', 'Mod Attacker Ranged Crit Chance', 'Mod Rating', 'Mod Reputation Gained %', 'Limit Movement Speed', 'Mod Attack Speed %', 'Mod Haste % (gain)', 'Mod Target School Absorb %', - 'Mod Target School Absorb for Ability', 'Mod Cooldowns', 'Mod Attacker Crit Chance', null, 'Mod Spell Hit Chance', + 'Mod Target School Absorb for Ability', 'Mod Cooldowns', 'Mod Attacker Crit Chance', '', 'Mod Spell Hit Chance', /*200+ */ 'Mod Kill Experience Gained %', 'Can Fly', 'Ignore Combat Result', 'Mod Attacker Melee Crit Damage %', 'Mod Attacker Ranged Crit Damage %', 'Mod Attacker Spell Crit Damage %', 'Mod Vehicle Flight Speed %', 'Mod Mounted Flight Speed %', 'Mod Flight Speed %', 'Mod Mounted Flight Speed % (always)', 'Mod Vehicle Speed % (always)', 'Mod Flight Speed % (not stacking)', 'Mod Ranged Attack Power by % of Stat', 'Mod Rage Generated from Damage Dealt', 'Tamed Pet Passive', 'Arena Preparation', 'Mod Spell Haste %', 'Killing Spree', 'Mod Ranged Haste %', 'Mod Mana Regeneration by % of Stat', - 'Mod Combat Rating by % of Stat', 'Ignore Threat', null, 'Raid Proc from Charge', null, + 'Mod Combat Rating by % of Stat', 'Ignore Threat', '', 'Raid Proc from Charge', '', /*225+ */ 'Raid Proc from Charge with Value', 'Periodic Dummy', 'Periodically Trigger Spell with Value','Detect Stealth', 'Mod AoE Damage Taken %', 'Mod Maximum Health - Flat (no stacking)','Proc Trigger Spell with Value', 'Mod Mechanic Duration %', 'Change other Humanoid Display', 'Mod Mechanic Duration % (not stacking)', 'Mod Dispel Resistance %', 'Control Vehicle', 'Mod Spell Power by % of Attack Power', 'Mod Healing Power by % of Attack Power','Mod Size % (not stacking)', @@ -1902,17 +1902,17 @@ $lang = array( 'Mod Aura Duration by Dispel Type', 'Mod Aura Duration by Dispel Type (not stacking)', 'Clone Caster', 'Mod Combat Result Chance', 'Convert Rune', /*250+ */ 'Mod Maximum Health - Flat (stacking)', 'Mod Enemy Dodge Chance', 'Mod Haste % (loss)', 'Mod Critical Block Chance', 'Disarm Offhand', 'Mod Mechanic Damage Taken %', 'No Reagent Cost', 'Mod Target Resistance by Spell Class', 'Mod Spell Visual', 'Mod Periodic Healing Taken %', - 'Screen Effect', 'Phase', 'Ability Ignore Aurastate', 'Allow Only Ability', null, - null, null, 'Cancel Aura Buffer at % of Caster Health','Mod Attack Power by % of Stat', 'Ignore Target Resistance', + 'Screen Effect', 'Phase', 'Ability Ignore Aurastate', 'Allow Only Ability', '', + '', '', 'Cancel Aura Buffer at % of Caster Health','Mod Attack Power by % of Stat', 'Ignore Target Resistance', 'Ignore Target Resistance for Ability', 'Mod Damage Taken % from Caster', 'Ignore Swing Timer Reset', 'X-Ray', 'Ability Consume No Ammo', /*275+ */ 'Mod Ability Ignore Shapeshift', 'Mod Mechanic Damage Done %', 'Mod Max Affected Targets', 'Disarm Ranged Weapon', 'Spawn Effect', 'Mod Armor Penetration %', 'Mod Honor Gain %', 'Mod Base Health %', 'Mod Healing Taken % from Caster', 'Linked Aura', - 'Mod Attack Power by School Resistance','Allow Periodic Ability to Crit', 'Mod Spell Deflect Chance', 'Ignore Hit Direction', null, + 'Mod Attack Power by School Resistance','Allow Periodic Ability to Crit', 'Mod Spell Deflect Chance', 'Ignore Hit Direction', '', 'Mod Crit Chance', 'Mod Quest Experience Gained %', 'Open Stable', 'Override Spells', 'Prevent Power Regeneration', - null, 'Set Vehicle Id', 'Spirit Burst', 'Strangulate', null, -/*300+ */ 'Share Damage %', 'Mod Absorb School Healing', null, 'Mod Damage Done vs Aurastate - %', 'Fake Inebriate', - 'Mod Minimum Speed %', null, 'Heal Absorb Test', 'Mod Critical Strike Chance for Caster',null, - 'Mod Pet AoE Damage Avoidance', null, null, null, 'Prevent Ressurection', + '', 'Set Vehicle Id', 'Spirit Burst', 'Strangulate', '', +/*300+ */ 'Share Damage %', 'Mod Absorb School Healing', '', 'Mod Damage Done vs Aurastate - %', 'Fake Inebriate', + 'Mod Minimum Speed %', '', 'Heal Absorb Test', 'Mod Critical Strike Chance for Caster','', + 'Mod Pet AoE Damage Avoidance', '', '', '', 'Prevent Ressurection', /* -316*/ 'Underwater Walking', 'Periodic Haste' ), 'attributes0' => array( diff --git a/localization/locale_zhcn.php b/localization/locale_zhcn.php index 6d606432..fe4b5ce3 100644 --- a/localization/locale_zhcn.php +++ b/localization/locale_zhcn.php @@ -1839,14 +1839,14 @@ $lang = array( /*102+ */ 'Dismiss Pet', 'Give Reputation', 'Summon Object (Trap)', 'Summon Object (Battle S.)','Summon Object (#3)', 'Summon Object (#4)', /*108+ */ 'Dispel Mechanic', 'Summon Dead Pet', 'Destroy All Totems', 'Durability Damage - Flat', 'Summon Demon', 'Resurrect with Flat Health', /*114+ */ 'Taunt', 'Durability Damage - %', 'Skin Player Corpse (PvP)', 'AoE Resurrect with % Health','Learn Skill', 'Apply Area Aura - Pet', -/*120+ */ 'Teleport to Graveyard', 'Normalized Weapon Damage', null, 'Take Flight Path', 'Pull Towards', 'Modify Threat - %', +/*120+ */ 'Teleport to Graveyard', 'Normalized Weapon Damage', '', 'Take Flight Path', 'Pull Towards', 'Modify Threat - %', /*126+ */ 'Spell Steal ', 'Prospect', 'Apply Area Aura - Friend', 'Apply Area Aura - Enemy', 'Redirect Done Threat %', 'Play Sound', /*132+ */ 'Play Music', 'Unlearn Specialization', 'Kill Credit 2', 'Call Pet', 'Heal for % of Total Health','Give % of Total Power', /*138+ */ 'Leap Back', 'Abandon Quest', 'Force Cast', 'Force Spell Cast with Value','Trigger Spell with Value','Apply Area Aura - Pet Owner', -/*144+ */ 'Knockback to Dest.', 'Pull Towards Dest.', 'Activate Rune', 'Fail Quest', null, 'Charge to Dest', +/*144+ */ 'Knockback to Dest.', 'Pull Towards Dest.', 'Activate Rune', 'Fail Quest', 'Trigger Missile with Value','Charge to Dest', /*150+ */ 'Start Quest', 'Trigger Spell 2', 'Summon - Refer-A-Friend', 'Create Tamed Pet', 'Discover Flight Path', 'Dual Wield 2H Weapons', /*156+ */ 'Add Socket to Item', 'Create Tradeskill Item', 'Milling', 'Rename Pet', 'Force Cast 2', 'Change Talent Spec. Count', -/*162-167*/ 'Activate Talent Spec.', null, 'Remove Aura', null, null, 'Update Player Phase' +/*162-167*/ 'Activate Talent Spec.', '', 'Remove Aura' ), 'unkAura' => '未知光环 (%1$d)', 'auras' => array( @@ -1859,7 +1859,7 @@ $lang = array( 'Mod Skill - Temporary', 'Increase Run Speed %', 'Mod Mounted Speed %', 'Decrease Run Speed %', 'Mod Maximum Health - Flat', 'Mod Maximum Power - Flat', 'Shapeshift', 'Spell Effect Immunity', 'Spell Aura Immunity', 'Spell School Immunity', 'Damage Immunity', 'Dispel Type Immunity', 'Proc Trigger Spell', 'Proc Trigger Damage', 'Track Creatures', - 'Track Resources', 'Ignore All Gear', 'Mod Parry %', null, 'Mod Dodge %', + 'Track Resources', 'Ignore All Gear', 'Mod Parry %', 'Periodic Trigger Spell from Client', 'Mod Dodge %', /*50+ */ 'Mod Critical Healing Amount %', 'Mod Block %', 'Mod Physical Crit Chance', 'Periodically Drain Health', 'Mod Physical Hit Chance', 'Mod Spell Hit Chance', 'Transform', 'Mod Spell Crit Chance', 'Increase Swim Speed %', 'Mod Damage Done Versus Creature', 'Pacify & Silence', 'Mod Size %', 'Periodically Transfer Health', 'Periodic Transfer Power', 'Periodic Drain Power', @@ -1882,19 +1882,19 @@ $lang = array( 'Increase Pet Talent Points', 'Allow Exotic Pets Taming', 'Mechanic Immunity Mask', 'Retain Combo Points', 'Reduce Pushback Time %', /*150+ */ 'Mod Shield Block Value - %', 'Track Stealthed', 'Mod Player Aggro Range', 'Split Damage - Flat', 'Mod Stealth Level', 'Mod Underwater Breathing %', 'Mod All Reputation Gained by %', 'Done Pet Damage Multiplier', 'Mod Shield Block Value - Flat', 'No PvP Credit', - 'Mod AoE Avoidance', 'Mod Health Regen During Combat', 'Mana Burn', 'Mod Melee Critical Damage %', null, + 'Mod AoE Avoidance', 'Mod Health Regen During Combat', 'Mana Burn', 'Mod Melee Critical Damage %', '', 'Mod Attacker Melee Attack Power', 'Mod Melee Attack Power - %', 'Mod Ranged Attack Power - %', 'Mod Damage Done vs Creature', 'Mod Crit Chance vs Creature', - 'Change Object Visibility for Player', 'Mod Run Speed (not stacking)', 'Mod Mounted Speed (not stacking)', null, 'Mod Spell Power by % of Stat', + 'Change Object Visibility for Player', 'Mod Run Speed (not stacking)', 'Mod Mounted Speed (not stacking)', '', 'Mod Spell Power by % of Stat', /*175+ */ 'Mod Healing Power by % of Stat', 'Spirit of Redemption', 'AoE Charm', 'Mod Debuff Resistance - %', 'Mod Attacker Spell Crit Chance', - 'Mod Spell Power vs Creature', null, 'Mod Resistance by % of Stat', 'Mod Threat % of Critical Hits', 'Mod Attacker Melee Hit Chance', + 'Mod Spell Power vs Creature', '', 'Mod Resistance by % of Stat', 'Mod Threat % of Critical Hits', 'Mod Attacker Melee Hit Chance', 'Mod Attacker Ranged Hit Chance', 'Mod Attacker Spell Hit Chance', 'Mod Attacker Melee Crit Chance', 'Mod Attacker Ranged Crit Chance', 'Mod Rating', 'Mod Reputation Gained %', 'Limit Movement Speed', 'Mod Attack Speed %', 'Mod Haste % (gain)', 'Mod Target School Absorb %', - 'Mod Target School Absorb for Ability', 'Mod Cooldowns', 'Mod Attacker Crit Chance', null, 'Mod Spell Hit Chance', + 'Mod Target School Absorb for Ability', 'Mod Cooldowns', 'Mod Attacker Crit Chance', '', 'Mod Spell Hit Chance', /*200+ */ 'Mod Kill Experience Gained %', 'Can Fly', 'Ignore Combat Result', 'Mod Attacker Melee Crit Damage %', 'Mod Attacker Ranged Crit Damage %', 'Mod Attacker Spell Crit Damage %', 'Mod Vehicle Flight Speed %', 'Mod Mounted Flight Speed %', 'Mod Flight Speed %', 'Mod Mounted Flight Speed % (always)', 'Mod Vehicle Speed % (always)', 'Mod Flight Speed % (not stacking)', 'Mod Ranged Attack Power by % of Stat', 'Mod Rage Generated from Damage Dealt', 'Tamed Pet Passive', 'Arena Preparation', 'Mod Spell Haste %', 'Killing Spree', 'Mod Ranged Haste %', 'Mod Mana Regeneration by % of Stat', - 'Mod Combat Rating by % of Stat', 'Ignore Threat', null, 'Raid Proc from Charge', null, + 'Mod Combat Rating by % of Stat', 'Ignore Threat', '', 'Raid Proc from Charge', '', /*225+ */ 'Raid Proc from Charge with Value', 'Periodic Dummy', 'Periodically Trigger Spell with Value','Detect Stealth', 'Mod AoE Damage Taken %', 'Mod Maximum Health - Flat (no stacking)','Proc Trigger Spell with Value', 'Mod Mechanic Duration %', 'Change other Humanoid Display', 'Mod Mechanic Duration % (not stacking)', 'Mod Dispel Resistance %', 'Control Vehicle', 'Mod Spell Power by % of Attack Power', 'Mod Healing Power by % of Attack Power','Mod Size % (not stacking)', @@ -1902,17 +1902,17 @@ $lang = array( 'Mod Aura Duration by Dispel Type', 'Mod Aura Duration by Dispel Type (not stacking)', 'Clone Caster', 'Mod Combat Result Chance', 'Convert Rune', /*250+ */ 'Mod Maximum Health - Flat (stacking)', 'Mod Enemy Dodge Chance', 'Mod Haste % (loss)', 'Mod Critical Block Chance', 'Disarm Offhand', 'Mod Mechanic Damage Taken %', 'No Reagent Cost', 'Mod Target Resistance by Spell Class', 'Mod Spell Visual', 'Mod Periodic Healing Taken %', - 'Screen Effect', 'Phase', 'Ability Ignore Aurastate', 'Allow Only Ability', null, - null, null, 'Cancel Aura Buffer at % of Caster Health','Mod Attack Power by % of Stat', 'Ignore Target Resistance', + 'Screen Effect', 'Phase', 'Ability Ignore Aurastate', 'Allow Only Ability', '', + '', '', 'Cancel Aura Buffer at % of Caster Health','Mod Attack Power by % of Stat', 'Ignore Target Resistance', 'Ignore Target Resistance for Ability', 'Mod Damage Taken % from Caster', 'Ignore Swing Timer Reset', 'X-Ray', 'Ability Consume No Ammo', /*275+ */ 'Mod Ability Ignore Shapeshift', 'Mod Mechanic Damage Done %', 'Mod Max Affected Targets', 'Disarm Ranged Weapon', 'Spawn Effect', 'Mod Armor Penetration %', 'Mod Honor Gain %', 'Mod Base Health %', 'Mod Healing Taken % from Caster', 'Linked Aura', - 'Mod Attack Power by School Resistance','Allow Periodic Ability to Crit', 'Mod Spell Deflect Chance', 'Ignore Hit Direction', null, + 'Mod Attack Power by School Resistance','Allow Periodic Ability to Crit', 'Mod Spell Deflect Chance', 'Ignore Hit Direction', '', 'Mod Crit Chance', 'Mod Quest Experience Gained %', 'Open Stable', 'Override Spells', 'Prevent Power Regeneration', - null, 'Set Vehicle Id', 'Spirit Burst', 'Strangulate', null, -/*300+ */ 'Share Damage %', 'Mod Absorb School Healing', null, 'Mod Damage Done vs Aurastate - %', 'Fake Inebriate', - 'Mod Minimum Speed %', null, 'Heal Absorb Test', 'Mod Critical Strike Chance for Caster',null, - 'Mod Pet AoE Damage Avoidance', null, null, null, 'Prevent Ressurection', + '', 'Set Vehicle Id', 'Spirit Burst', 'Strangulate', '', +/*300+ */ 'Share Damage %', 'Mod Absorb School Healing', '', 'Mod Damage Done vs Aurastate - %', 'Fake Inebriate', + 'Mod Minimum Speed %', '', 'Heal Absorb Test', 'Mod Critical Strike Chance for Caster','', + 'Mod Pet AoE Damage Avoidance', '', '', '', 'Prevent Ressurection', /* -316*/ 'Underwater Walking', 'Periodic Haste' ), 'attributes0' => array( diff --git a/static/js/locale_dede.js b/static/js/locale_dede.js index 1e46333b..12724635 100644 --- a/static/js/locale_dede.js +++ b/static/js/locale_dede.js @@ -3818,7 +3818,7 @@ var LANG = { [31, "Increase Run Speed %"], [32, "Mod Mounted Speed %"], [33, "Decrease Run Speed %"], [34, "Mod Maximum Health - Flat"], [35, "Mod Maximum Power - Flat"], [36, "Shapeshift"], [37, "Spell Effect Immunity"], [38, "Spell Aura Immunity"], [39, "Spell School Immunity"], [40, "Damage Immunity"], [41, "Dispel Type Immunity"], [42, "Proc Trigger Spell"], [43, "Proc Trigger Damage"], [44, "Track Creatures"], [45, "Track Resources"], - [46, "Ignore All Gear"], [47, "Mod Parry %"], /* [48, null] */ [49, "Mod Dodge %"], [50, "Mod Critical Healing Amount %"], + [46, "Ignore All Gear"], [47, "Mod Parry %"], [48, "Periodic Trigger Spell from Client"],[49, "Mod Dodge %"], [50, "Mod Critical Healing Amount %"], [51, "Mod Block %"], [52, "Mod Physical Crit Chance"], [53, "Periodically Drain Health"], [54, "Mod Physical Hit Chance"], [55, "Mod Spell Hit Chance"], [56, "Transform"], [57, "Mod Spell Crit Chance"], [58, "Increase Swim Speed %"], [59, "Mod Damage Done Versus Creature"],[60, "Pacify & Silence"], [61, "Mod Size %"], [62, "Periodically Transfer Health"], [63, "Periodically Transfer Power"], [64, "Periodically Drain Power"], [65, "Mod Spell Haste % (not stacking)"], @@ -3900,11 +3900,10 @@ var LANG = { [131, "Play Sound"], [132, "Play Music"], [133, "Unlearn Specialization"], [134, "Kill Credit2"], [135, "Call Pet"], [136, "Heal for % of Total Health"], [137, "Give % of Total Power"], [138, "Leap Back"], [139, "Abandon Quest"], [140, "Force Spell Cast"], [141, "Force Spell Cast with Value"], [142, "Trigger Spell with Value"], [143, "Apply Area Aura - Pet Owner"], [144, "Knockback to Dest."], [145, "Pull Towards Dest."], - [146, "Activate Rune"], [147, "Fail Quest"], [149, "Charge to Dest."], [150, "Start Quest"], + [146, "Activate Rune"], [147, "Fail Quest"], [148, "Trigger Missile with Value"], [149, "Charge to Dest."], [150, "Start Quest"], [151, "Trigger Spell 2"], [152, "Summon - Refer-A-Friend"], [153, "Create Tamed Pet"], [154, "Discover Flight Path"], [155, "Dual Wield 2H Weapons"], - [156, "Add Socket to Item"], [157, "Create Tradeskill Item"], [158, "Milling"], [159, "Rename Pet"], - [161, "Change Talent Spec. Count"], [162, "Activate Talent Spec."], [164, "Remove Aura"], - [167, "Update Player Phase"] + [156, "Add Socket to Item"], [157, "Create Tradeskill Item"], [158, "Milling"], [159, "Rename Pet"], [160, "Force Spell Cast 2"], + [161, "Change Talent Spec. Count"], [162, "Activate Talent Spec."], [164, "Remove Aura"] ], damagetype: [ [1, "Keins"], [2, "Magie"], [3, "Nahkampf"], [4, "Distanz"] diff --git a/static/js/locale_enus.js b/static/js/locale_enus.js index 3f0292ec..65eac3b5 100644 --- a/static/js/locale_enus.js +++ b/static/js/locale_enus.js @@ -3866,7 +3866,7 @@ var LANG = { [31, "Increase Run Speed %"], [32, "Mod Mounted Speed %"], [33, "Decrease Run Speed %"], [34, "Mod Maximum Health - Flat"], [35, "Mod Maximum Power - Flat"], [36, "Shapeshift"], [37, "Spell Effect Immunity"], [38, "Spell Aura Immunity"], [39, "Spell School Immunity"], [40, "Damage Immunity"], [41, "Dispel Type Immunity"], [42, "Proc Trigger Spell"], [43, "Proc Trigger Damage"], [44, "Track Creatures"], [45, "Track Resources"], - [46, "Ignore All Gear"], [47, "Mod Parry %"], /* [48, null] */ [49, "Mod Dodge %"], [50, "Mod Critical Healing Amount %"], + [46, "Ignore All Gear"], [47, "Mod Parry %"], [48, "Periodic Trigger Spell from Client"],[49, "Mod Dodge %"], [50, "Mod Critical Healing Amount %"], [51, "Mod Block %"], [52, "Mod Physical Crit Chance"], [53, "Periodically Drain Health"], [54, "Mod Physical Hit Chance"], [55, "Mod Spell Hit Chance"], [56, "Transform"], [57, "Mod Spell Crit Chance"], [58, "Increase Swim Speed %"], [59, "Mod Damage Done Versus Creature"],[60, "Pacify & Silence"], [61, "Mod Size %"], [62, "Periodically Transfer Health"], [63, "Periodically Transfer Power"], [64, "Periodically Drain Power"], [65, "Mod Spell Haste % (not stacking)"], @@ -3948,11 +3948,10 @@ var LANG = { [131, "Play Sound"], [132, "Play Music"], [133, "Unlearn Specialization"], [134, "Kill Credit2"], [135, "Call Pet"], [136, "Heal for % of Total Health"], [137, "Give % of Total Power"], [138, "Leap Back"], [139, "Abandon Quest"], [140, "Force Spell Cast"], [141, "Force Spell Cast with Value"], [142, "Trigger Spell with Value"], [143, "Apply Area Aura - Pet Owner"], [144, "Knockback to Dest."], [145, "Pull Towards Dest."], - [146, "Activate Rune"], [147, "Fail Quest"], [149, "Charge to Dest."], [150, "Start Quest"], + [146, "Activate Rune"], [147, "Fail Quest"], [148, "Trigger Missile with Value"], [149, "Charge to Dest."], [150, "Start Quest"], [151, "Trigger Spell 2"], [152, "Summon - Refer-A-Friend"], [153, "Create Tamed Pet"], [154, "Discover Flight Path"], [155, "Dual Wield 2H Weapons"], - [156, "Add Socket to Item"], [157, "Create Tradeskill Item"], [158, "Milling"], [159, "Rename Pet"], - [161, "Change Talent Spec. Count"], [162, "Activate Talent Spec."], [164, "Remove Aura"], - [167, "Update Player Phase"] + [156, "Add Socket to Item"], [157, "Create Tradeskill Item"], [158, "Milling"], [159, "Rename Pet"], [160, "Force Spell Cast 2"], + [161, "Change Talent Spec. Count"], [162, "Activate Talent Spec."], [164, "Remove Aura"] ], damagetype: [ [1, "None"], [2, "Magic"], [3, "Melee"], [4, "Ranged"] diff --git a/static/js/locale_eses.js b/static/js/locale_eses.js index d5e3722a..5ddeb9ce 100644 --- a/static/js/locale_eses.js +++ b/static/js/locale_eses.js @@ -3820,7 +3820,7 @@ var LANG = { [31, "Aumentar velocidad de carrera %"], [32, "Modificar velocidad montado %"], [33, "Reducir velocidad de carrera %"], [34, "Modificar salud máxima - Fijo"], [35, "Modificar poder máximo - Fijo"], [36, "Cambio de forma"], [37, "Inmunidad a efecto de hechizo"], [38, "Inmunidad a aura de hechizo"], [39, "Inmunidad a escuela de hechizo"], [40, "Inmunidad a daño"], [41, "Inmunidad a tipo de disipación"], [42, "Activar hechizo al recibir golpe"], [43, "Activar daño al recibir golpe"], [44, "Rastrear criaturas"], [45, "Rastrear recursos"], - [46, "Ignorar todo el equipo"], [47, "Modificar parada %"], [49, "Modificar esquiva %"], [50, "Modificar sanación crítica %"], + [46, "Ignorar todo el equipo"], [47, "Modificar parada %"], [48, "Activar hechizo periódicamente desde el cliente"] [49, "Modificar esquiva %"], [50, "Modificar sanación crítica %"], [51, "Modificar bloqueo %"], [52, "Modificar probabilidad de crítico físico"], [53, "Drenar salud periódicamente"], [54, "Modificar probabilidad de golpe físico"], [55, "Modificar probabilidad de golpe de hechizo"], [56, "Transformar"], [57, "Modificar probabilidad de crítico de hechizo"], [58, "Aumentar velocidad de nado %"], [59, "Modificar daño hecho vs criatura"], [60, "Pacificar y silenciar"], [61, "Modificar tamaño %"], [62, "Transferir salud periódicamente"], [63, "Transferir poder periódicamente"], [64, "Drenar poder periódicamente"], [65, "Modificar celeridad de hechizo % (no acumulable)"], @@ -3902,11 +3902,10 @@ var LANG = { [131, "Reproducir sonido"], [132, "Reproducir música"], [133, "Olvidar especialización"], [134, "Crédito de muerte2"], [135, "Llamar mascota"], [136, "Sanar por % de salud total"], [137, "Dar % de poder total"], [138, "Saltar hacia atrás"], [139, "Abandonar misión"], [140, "Forzar lanzamiento de hechizo"], [141, "Forzar lanzamiento de hechizo con valor"], [142, "Activar hechizo con valor"], [143, "Aplicar aura de área - Dueño de mascota"], [144, "Empujar hacia destino."], [145, "Atraer hacia destino."], - [146, "Activar runa"], [147, "Fallar misión"], [149, "Cargar hacia destino."], [150, "Iniciar misión"], + [146, "Activar runa"], [147, "Fallar misión"], [148, "Disparar misil con valor"], [149, "Cargar hacia destino."], [150, "Iniciar misión"], [151, "Activar hechizo 2"], [152, "Invocar - Referido por un amigo"], [153, "Crear mascota domesticada"], [154, "Descubrir ruta de vuelo"], [155, "Doble empuñadura de armas de 2 manos"], - [156, "Añadir socket a objeto"], [157, "Crear objeto de oficio"], [158, "Molienda"], [159, "Renombrar mascota"], - [161, "Cambiar conteo de espe. de talento"], [162, "Activar espec. de talento."], [164, "Eliminar aura"], - [167, "Actualizar fase de jugador"] + [156, "Añadir socket a objeto"], [157, "Crear objeto de oficio"], [158, "Molienda"], [159, "Renombrar mascota"], [160, "Forzar lanzamiento de hechizo 2"], + [161, "Cambiar conteo de espe. de talento"], [162, "Activar espec. de talento."], [164, "Eliminar aura"] ], damagetype: [ [1, "Ninguno"], [2, "Magia"], [3, "Cuerpo a cuerpo"], [4, "A distancia"] diff --git a/static/js/locale_frfr.js b/static/js/locale_frfr.js index bc23f10c..234acbc0 100644 --- a/static/js/locale_frfr.js +++ b/static/js/locale_frfr.js @@ -3820,7 +3820,7 @@ var LANG = { [31, "Increase Run Speed %"], [32, "Mod Mounted Speed %"], [33, "Decrease Run Speed %"], [34, "Mod Maximum Health - Flat"], [35, "Mod Maximum Power - Flat"], [36, "Shapeshift"], [37, "Spell Effect Immunity"], [38, "Spell Aura Immunity"], [39, "Spell School Immunity"], [40, "Damage Immunity"], [41, "Dispel Type Immunity"], [42, "Proc Trigger Spell"], [43, "Proc Trigger Damage"], [44, "Track Creatures"], [45, "Track Resources"], - [46, "Ignore All Gear"], [47, "Mod Parry %"], /* [48, null] */ [49, "Mod Dodge %"], [50, "Mod Critical Healing Amount %"], + [46, "Ignore All Gear"], [47, "Mod Parry %"], [48, "Periodic Trigger Spell from Client"],[49, "Mod Dodge %"], [50, "Mod Critical Healing Amount %"], [51, "Mod Block %"], [52, "Mod Physical Crit Chance"], [53, "Periodically Drain Health"], [54, "Mod Physical Hit Chance"], [55, "Mod Spell Hit Chance"], [56, "Transform"], [57, "Mod Spell Crit Chance"], [58, "Increase Swim Speed %"], [59, "Mod Damage Done Versus Creature"],[60, "Pacify & Silence"], [61, "Mod Size %"], [62, "Periodically Transfer Health"], [63, "Periodically Transfer Power"], [64, "Periodically Drain Power"], [65, "Mod Spell Haste % (not stacking)"], @@ -3902,11 +3902,10 @@ var LANG = { [131, "Play Sound"], [132, "Play Music"], [133, "Unlearn Specialization"], [134, "Kill Credit2"], [135, "Call Pet"], [136, "Heal for % of Total Health"], [137, "Give % of Total Power"], [138, "Leap Back"], [139, "Abandon Quest"], [140, "Force Spell Cast"], [141, "Force Spell Cast with Value"], [142, "Trigger Spell with Value"], [143, "Apply Area Aura - Pet Owner"], [144, "Knockback to Dest."], [145, "Pull Towards Dest."], - [146, "Activate Rune"], [147, "Fail Quest"], [149, "Charge to Dest."], [150, "Start Quest"], + [146, "Activate Rune"], [147, "Fail Quest"], [148, "Trigger Missile with Value"], [149, "Charge to Dest."], [150, "Start Quest"], [151, "Trigger Spell 2"], [152, "Summon - Refer-A-Friend"], [153, "Create Tamed Pet"], [154, "Discover Flight Path"], [155, "Dual Wield 2H Weapons"], - [156, "Add Socket to Item"], [157, "Create Tradeskill Item"], [158, "Milling"], [159, "Rename Pet"], - [161, "Change Talent Spec. Count"], [162, "Activate Talent Spec."], [164, "Remove Aura"], - [167, "Update Player Phase"] + [156, "Add Socket to Item"], [157, "Create Tradeskill Item"], [158, "Milling"], [159, "Rename Pet"], [160, "Force Cast 2"], + [161, "Change Talent Spec. Count"], [162, "Activate Talent Spec."], [164, "Remove Aura"] ], damagetype: [ [1, "Aucun"], [2, "Magie"], [3, "En mêlée"], [4, "À distance"] diff --git a/static/js/locale_ruru.js b/static/js/locale_ruru.js index b5487d57..a88b356f 100644 --- a/static/js/locale_ruru.js +++ b/static/js/locale_ruru.js @@ -3820,7 +3820,7 @@ var LANG = { [31, "Increase Run Speed %"], [32, "Mod Mounted Speed %"], [33, "Decrease Run Speed %"], [34, "Mod Maximum Health - Flat"], [35, "Mod Maximum Power - Flat"], [36, "Shapeshift"], [37, "Spell Effect Immunity"], [38, "Spell Aura Immunity"], [39, "Spell School Immunity"], [40, "Damage Immunity"], [41, "Dispel Type Immunity"], [42, "Proc Trigger Spell"], [43, "Proc Trigger Damage"], [44, "Track Creatures"], [45, "Track Resources"], - [46, "Ignore All Gear"], [47, "Mod Parry %"], /* [48, null] */ [49, "Mod Dodge %"], [50, "Mod Critical Healing Amount %"], + [46, "Ignore All Gear"], [47, "Mod Parry %"], [48, "Periodic Trigger Spell from Client"],[49, "Mod Dodge %"], [50, "Mod Critical Healing Amount %"], [51, "Mod Block %"], [52, "Mod Physical Crit Chance"], [53, "Periodically Drain Health"], [54, "Mod Physical Hit Chance"], [55, "Mod Spell Hit Chance"], [56, "Transform"], [57, "Mod Spell Crit Chance"], [58, "Increase Swim Speed %"], [59, "Mod Damage Done Versus Creature"],[60, "Pacify & Silence"], [61, "Mod Size %"], [62, "Periodically Transfer Health"], [63, "Periodically Transfer Power"], [64, "Periodically Drain Power"], [65, "Mod Spell Haste % (not stacking)"], @@ -3902,11 +3902,10 @@ var LANG = { [131, "Play Sound"], [132, "Play Music"], [133, "Unlearn Specialization"], [134, "Kill Credit2"], [135, "Call Pet"], [136, "Heal for % of Total Health"], [137, "Give % of Total Power"], [138, "Leap Back"], [139, "Abandon Quest"], [140, "Force Spell Cast"], [141, "Force Spell Cast with Value"], [142, "Trigger Spell with Value"], [143, "Apply Area Aura - Pet Owner"], [144, "Knockback to Dest."], [145, "Pull Towards Dest."], - [146, "Activate Rune"], [147, "Fail Quest"], [149, "Charge to Dest."], [150, "Start Quest"], + [146, "Activate Rune"], [147, "Fail Quest"], [148, "Trigger Missile with Value"], [149, "Charge to Dest."], [150, "Start Quest"], [151, "Trigger Spell 2"], [152, "Summon - Refer-A-Friend"], [153, "Create Tamed Pet"], [154, "Discover Flight Path"], [155, "Dual Wield 2H Weapons"], - [156, "Add Socket to Item"], [157, "Create Tradeskill Item"], [158, "Milling"], [159, "Rename Pet"], - [161, "Change Talent Spec. Count"], [162, "Activate Talent Spec."], [164, "Remove Aura"], - [167, "Update Player Phase"] + [156, "Add Socket to Item"], [157, "Create Tradeskill Item"], [158, "Milling"], [159, "Rename Pet"], [160, "Force Cast 2"], + [161, "Change Talent Spec. Count"], [162, "Activate Talent Spec."], [164, "Remove Aura"] ], damagetype: [ [1, "Нет"], [2, "Магический"], [3, "Ближний бой"], [4, "Дальний бой"] diff --git a/static/js/locale_zhcn.js b/static/js/locale_zhcn.js index a0b0c801..a82b4e53 100644 --- a/static/js/locale_zhcn.js +++ b/static/js/locale_zhcn.js @@ -3846,7 +3846,7 @@ var LANG = { [31, "Increase Run Speed %"], [32, "Mod Mounted Speed %"], [33, "Decrease Run Speed %"], [34, "Mod Maximum Health - Flat"], [35, "Mod Maximum Power - Flat"], [36, "Shapeshift"], [37, "Spell Effect Immunity"], [38, "Spell Aura Immunity"], [39, "Spell School Immunity"], [40, "Damage Immunity"], [41, "Dispel Type Immunity"], [42, "Proc Trigger Spell"], [43, "Proc Trigger Damage"], [44, "Track Creatures"], [45, "Track Resources"], - [46, "Ignore All Gear"], [47, "Mod Parry %"], /* [48, null] */ [49, "Mod Dodge %"], [50, "Mod Critical Healing Amount %"], + [46, "Ignore All Gear"], [47, "Mod Parry %"], [48, "Periodic Trigger Spell from Client"],[49, "Mod Dodge %"], [50, "Mod Critical Healing Amount %"], [51, "Mod Block %"], [52, "Mod Physical Crit Chance"], [53, "Periodically Drain Health"], [54, "Mod Physical Hit Chance"], [55, "Mod Spell Hit Chance"], [56, "Transform"], [57, "Mod Spell Crit Chance"], [58, "Increase Swim Speed %"], [59, "Mod Damage Done Versus Creature"],[60, "Pacify & Silence"], [61, "Mod Size %"], [62, "Periodically Transfer Health"], [63, "Periodically Transfer Power"], [64, "Periodically Drain Power"], [65, "Mod Spell Haste % (not stacking)"], @@ -3928,11 +3928,10 @@ var LANG = { [131, "Play Sound"], [132, "Play Music"], [133, "Unlearn Specialization"], [134, "Kill Credit2"], [135, "Call Pet"], [136, "Heal for % of Total Health"], [137, "Give % of Total Power"], [138, "Leap Back"], [139, "Abandon Quest"], [140, "Force Spell Cast"], [141, "Force Spell Cast with Value"], [142, "Trigger Spell with Value"], [143, "Apply Area Aura - Pet Owner"], [144, "Knockback to Dest."], [145, "Pull Towards Dest."], - [146, "Activate Rune"], [147, "Fail Quest"], [149, "Charge to Dest."], [150, "Start Quest"], + [146, "Activate Rune"], [147, "Fail Quest"], [148, "Trigger Missile with Value"], [149, "Charge to Dest."], [150, "Start Quest"], [151, "Trigger Spell 2"], [152, "Summon - Refer-A-Friend"], [153, "Create Tamed Pet"], [154, "Discover Flight Path"], [155, "Dual Wield 2H Weapons"], - [156, "Add Socket to Item"], [157, "Create Tradeskill Item"], [158, "Milling"], [159, "Rename Pet"], - [161, "Change Talent Spec. Count"], [162, "Activate Talent Spec."], [164, "Remove Aura"], - [167, "Update Player Phase"] + [156, "Add Socket to Item"], [157, "Create Tradeskill Item"], [158, "Milling"], [159, "Rename Pet"], [160, "Force Cast 2"], + [161, "Change Talent Spec. Count"], [162, "Activate Talent Spec."], [164, "Remove Aura"] ], damagetype: [ [1, "无"], [2, "魔法"], [3, "近战"], [4, "远程"] From 441ad3854391ac7496355c424280d8f7e94bba07 Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Tue, 28 Oct 2025 19:54:47 +0100 Subject: [PATCH 086/260] Filter/Locales * fix embedding and triggering fi_toggle on filtrable listviews for locale zhCN --- static/js/locale_zhcn.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/js/locale_zhcn.js b/static/js/locale_zhcn.js index a82b4e53..54f20940 100644 --- a/static/js/locale_zhcn.js +++ b/static/js/locale_zhcn.js @@ -2863,7 +2863,7 @@ var LANG = { lvnote_sort: "分类:", - lvnote_tryfiltering: "试试筛选您的结果", + lvnote_tryfiltering: "试试筛选您的结果", lvnote_trynarrowing: "请尝试缩小搜索范围", lvnote_upgradesfor: '寻找升级 $3.', lvnote_witherrors: "您搜索中的部分筛选词无效,已被忽略。", From 3984bd0ae25789359f8f9afbff98f5810baf603a Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Wed, 29 Oct 2025 15:45:46 +0100 Subject: [PATCH 087/260] Spells/Misc * limit chance/ppm precision on spell procs chances * do not apply a spells EffectXBonusMultiplier for physical spells --- endpoints/spell/spell.php | 4 ++-- localization/locale_dede.php | 4 ++-- localization/locale_enus.php | 4 ++-- localization/locale_eses.php | 4 ++-- localization/locale_frfr.php | 4 ++-- localization/locale_ruru.php | 4 ++-- localization/locale_zhcn.php | 4 ++-- template/pages/enchantment.tpl.php | 4 ++-- 8 files changed, 16 insertions(+), 16 deletions(-) diff --git a/endpoints/spell/spell.php b/endpoints/spell/spell.php index 8196d1f3..d3945836 100644 --- a/endpoints/spell/spell.php +++ b/endpoints/spell/spell.php @@ -1437,7 +1437,7 @@ class SpellBaseResponse extends TemplateResponse implements ICache $scaling[1] = $this->subject->getField('effect'.$i.'BonusMultiplier'); continue; } - else + else if ($this->subject->getField('damageClass') == SPELL_DAMAGE_CLASS_MAGIC) $scaling[0] = $this->subject->getField('effect'.$i.'BonusMultiplier'); $allDoTs = false; @@ -1716,7 +1716,7 @@ class SpellBaseResponse extends TemplateResponse implements ICache if (in_array($i, $this->subject->canTriggerSpell()) && $procData['chance'] && $procData['chance'] < 100) { - $_footer['proc'] = $procData['chance'] < 0 ? Lang::spell('ppm', [Lang::nf(-$procData['chance'], 1)]) : Lang::spell('procChance') . $procData['chance'] . '%'; + $_footer['proc'] = $procData['chance'] < 0 ? Lang::spell('ppm', [-$procData['chance']]) : Lang::spell('procChance', [$procData['chance']]); if ($procData['cooldown']) $_footer['procCD'] = Lang::game('cooldown', [Util::formatTime($procData['cooldown'], true)]); } diff --git a/localization/locale_dede.php b/localization/locale_dede.php index f62d0677..2cc5dcb6 100644 --- a/localization/locale_dede.php +++ b/localization/locale_dede.php @@ -1661,8 +1661,8 @@ $lang = array( 'currentArea' => '<Momentanes Gebiet>', 'discovered' => "Durch Geistesblitz erlernt", - 'ppm' => "(%s Auslösungen pro Minute)", - 'procChance' => "Procchance: ", + 'ppm' => "(%.1f Auslösungen pro Minute)", + 'procChance' => "Procchance: %.4g%%", 'starter' => "Basiszauber", 'trainingCost' => "Trainingskosten: ", 'channeled' => "Kanalisiert", diff --git a/localization/locale_enus.php b/localization/locale_enus.php index bc658ca6..e15fae40 100644 --- a/localization/locale_enus.php +++ b/localization/locale_enus.php @@ -1661,8 +1661,8 @@ $lang = array( 'currentArea' => '<current area>', 'discovered' => "Learned via discovery", - 'ppm' => "(%s procs per minute)", - 'procChance' => "Proc chance: ", + 'ppm' => "(%.1f procs per minute)", + 'procChance' => "Proc chance: %.4g%%", 'starter' => "Starter spell", 'trainingCost' => "Training cost: ", 'channeled' => "Channeled", // SPELL_CAST_CHANNELED diff --git a/localization/locale_eses.php b/localization/locale_eses.php index 80f646a2..a8b20675 100644 --- a/localization/locale_eses.php +++ b/localization/locale_eses.php @@ -1661,8 +1661,8 @@ $lang = array( 'currentArea' => '<current area>', 'discovered' => "Aprendido via descubrimiento", - 'ppm' => "(%s procs por minuto)", - 'procChance' => "Probabilidad de que accione: ", + 'ppm' => "(%.1f procs por minuto)", + 'procChance' => "Probabilidad de que accione: %.4g%%", 'starter' => "Hechizo inicial", 'trainingCost' => "Costo de enseñanza: ", 'channeled' => "Canalizado", diff --git a/localization/locale_frfr.php b/localization/locale_frfr.php index 8224a965..26b363f9 100644 --- a/localization/locale_frfr.php +++ b/localization/locale_frfr.php @@ -1661,8 +1661,8 @@ $lang = array( 'currentArea' => '<current area>', 'discovered' => "Appris via une découverte", - 'ppm' => "(%s déclenchements par minute)", - 'procChance' => "Chance : ", + 'ppm' => "(%.1f déclenchements par minute)", + 'procChance' => "Chance : %.4g%%", 'starter' => "Sortilège initiaux", 'trainingCost' => "Coût d'entraînement : ", 'channeled' => "Canalisée", diff --git a/localization/locale_ruru.php b/localization/locale_ruru.php index 8555d957..c9bf049f 100644 --- a/localization/locale_ruru.php +++ b/localization/locale_ruru.php @@ -1661,8 +1661,8 @@ $lang = array( 'currentArea' => '<current area>', 'discovered' => "Изучается путём освоения местности", - 'ppm' => "(Срабатывает %s раз в минуту)", - 'procChance' => "Шанс срабатывания: ", + 'ppm' => "(Срабатывает %.1f раз в минуту)", + 'procChance' => "Шанс срабатывания: %.4g%%", 'starter' => "Начальное заклинание", 'trainingCost' => "Цена обучения: ", 'channeled' => "Направляемое", diff --git a/localization/locale_zhcn.php b/localization/locale_zhcn.php index fe4b5ce3..43b73336 100644 --- a/localization/locale_zhcn.php +++ b/localization/locale_zhcn.php @@ -1661,8 +1661,8 @@ $lang = array( 'currentArea' => '<当前区域>', 'discovered' => "通过发现学习", - 'ppm' => "%s每分钟触发几率", - 'procChance' => "触发几率:", + 'ppm' => "%.1f每分钟触发几率", + 'procChance' => "触发几率:%.4g%%", 'starter' => "初始法术", 'trainingCost' => "训练成本:", 'channeled' => "需引导", diff --git a/template/pages/enchantment.tpl.php b/template/pages/enchantment.tpl.php index 83f1350f..e759cff4 100644 --- a/template/pages/enchantment.tpl.php +++ b/template/pages/enchantment.tpl.php @@ -62,9 +62,9 @@ foreach ($this->effects as $i => $e): echo '
    '; if ($e['proc'] < 0): - echo Lang::spell('ppm', [Lang::nf(-$e['proc'], 1)]); + echo Lang::spell('ppm', [-$e['proc']]); elseif ($e['proc'] < 100.0): - echo Lang::spell('procChance').Lang::main('colon').$e['proc'].'%'; + echo Lang::spell('procChance', [$e['proc']]); endif; endif; From 1e9e406ff0864b661cf0b70a296d3d601884a4e9 Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Fri, 31 Oct 2025 16:44:37 +0100 Subject: [PATCH 088/260] TextResponse/Fixup * make class non-abstract so we can generate a 403/404 message on base --- includes/components/response/textresponse.class.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/includes/components/response/textresponse.class.php b/includes/components/response/textresponse.class.php index ef35a2fd..57e5faf0 100644 --- a/includes/components/response/textresponse.class.php +++ b/includes/components/response/textresponse.class.php @@ -89,7 +89,7 @@ trait TrCommunityHelper } } -abstract class TextResponse extends BaseResponse +class TextResponse extends BaseResponse { protected string $contentType = MIME_TYPE_JAVASCRIPT; protected ?string $redirectTo = null; @@ -163,6 +163,8 @@ abstract class TextResponse extends BaseResponse echo $out; } + + protected function generate() : void {} } ?> From 8212811970e9b1c686797f297e929ed838c2b573 Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Sat, 1 Nov 2025 20:12:41 +0100 Subject: [PATCH 089/260] Misc/Cleanup * create function for num range .. creation --- endpoints/itemset/itemset.php | 10 +--------- endpoints/object/object.php | 15 +++++---------- endpoints/spell/spell.php | 17 ++++++++--------- includes/components/SmartAI/SmartAI.class.php | 10 +++------- includes/utilities.php | 12 ++++++++++++ 5 files changed, 29 insertions(+), 35 deletions(-) diff --git a/endpoints/itemset/itemset.php b/endpoints/itemset/itemset.php index 317637b3..a975d734 100644 --- a/endpoints/itemset/itemset.php +++ b/endpoints/itemset/itemset.php @@ -95,15 +95,7 @@ class ItemsetBaseResponse extends TemplateResponse implements ICache // itemLevel if ($min = $this->subject->getField('minLevel')) - { - $foo = Lang::game('level').Lang::main('colon').$min; - $max = $this->subject->getField('maxLevel'); - - if ($min < $max) - $foo .= ' - '.$max; - - $infobox[] = $foo; - } + $infobox[] = Lang::game('level').Lang::main('colon').Util::createNumRange($min, $this->subject->getField('maxLevel'), ' - '); // class if ($cl = Lang::getClassString($this->subject->getField('classMask'), $jsg, Lang::FMT_MARKUP)) diff --git a/endpoints/object/object.php b/endpoints/object/object.php index de643a4d..6b5150ef 100644 --- a/endpoints/object/object.php +++ b/endpoints/object/object.php @@ -148,9 +148,7 @@ class ObjectBaseResponse extends TemplateResponse implements ICache if ($this->subject->getField('lootStack')) { [$min, $max, $restock] = $this->subject->getField('lootStack'); - $buff = Lang::spell('spellModOp', 4).Lang::main('colon').$min; - if ($min < $max) - $buff .= Lang::game('valueDelim').$max; + $buff = Lang::spell('spellModOp', 4).Lang::main('colon').Util::createNumRange($min, $max); // since Veins don't have charges anymore, the timer is questionable $infobox[] = $restock > 1 ? '[tooltip name=restock]'.Lang::gameObject('restock', [Util::formatTime($restock * 1000)]).'[/tooltip][span class=tip tooltip=restock]'.$buff.'[/span]' : $buff; @@ -163,10 +161,7 @@ class ObjectBaseResponse extends TemplateResponse implements ICache $this->extendGlobalIds(Type::ZONE, $zone); $m = Lang::game('meetingStone').'[zone='.$zone.']'; - - $l = $minLevel; - if ($minLevel > 1 && $maxLevel > $minLevel) - $l .= Lang::game('valueDelim').min($maxLevel, MAX_LEVEL); + $l = Util::createNumRange($minLevel, min($maxLevel, MAX_LEVEL)); $infobox[] = $l ? '[tooltip name=meetingstone]'.Lang::game('reqLevel', [$l]).'[/tooltip][span class=tip tooltip=meetingstone]'.$m.'[/span]' : $m; } @@ -181,11 +176,11 @@ class ObjectBaseResponse extends TemplateResponse implements ICache if ($minTime > 1 || $minPlayer || $radius) $buff .= Lang::main('colon').'[ul]'; - if ($minTime > 1) - $buff .= '[li]'.Lang::game('duration').Lang::main('colon').($maxTime > $minTime ? Util::FormatTime($maxTime * 1000, true).' - ' : '').Util::FormatTime($minTime * 1000, true).'[/li]'; + if ($minTime > 1) // sign shenannigans reverse the display order + $buff .= '[li]'.Lang::game('duration').Lang::main('colon').Util::createNumRange(-$maxTime, -$minTime, fn: fn($x) => Util::FormatTime(-$x * 1000, true)).'[/li]'; if ($minPlayer) - $buff .= '[li]'.Lang::main('players').Lang::main('colon').$minPlayer.($maxPlayer > $minPlayer ? ' - '.$maxPlayer : '').'[/li]'; + $buff .= '[li]'.Lang::main('players').Lang::main('colon').Util::createNumRange($minPlayer, $maxPlayer).'[/li]'; if ($radius) $buff .= '[li]'.Lang::spell('range', [$radius]).'[/li]'; diff --git a/endpoints/spell/spell.php b/endpoints/spell/spell.php index d3945836..28bafde2 100644 --- a/endpoints/spell/spell.php +++ b/endpoints/spell/spell.php @@ -1606,7 +1606,7 @@ class SpellBaseResponse extends TemplateResponse implements ICache */ $_nameEffect = $_nameAura = $_nameMV = $_nameMVB = $_markup = ''; - $_icon = $_perfItem = $_footer = []; + $_icon = $_perfItem = $_footer = []; $_footer['value'] = [0, 0]; $valueFmt = '%s'; @@ -1623,7 +1623,7 @@ class SpellBaseResponse extends TemplateResponse implements ICache Type::ITEM, $itemId, $itemEntry ? $this->subject->relItems->getField('name', true) : Util::ucFirst(Lang::game('item')).' #'.$itemId, - ($effBP + 1) . ($effDS > 1 ? '-' . ($effBP + $effDS) : ''), + $this->createNumRange($effBP, $effDS), quality: $itemEntry ? $this->subject->relItems->getField('quality') : '', link: !empty($itemEntry) ); @@ -1654,12 +1654,6 @@ class SpellBaseResponse extends TemplateResponse implements ICache $cndSpell = new SpellList(array(['id', $extraItem['requiredSpecialization']])); if (!$cndSpell->error) { - $num = '+'.($effBP + 1); - if ($extraItem['additionalMaxNum'] > 1) - $num .= '-'.($extraItem['additionalMaxNum'] * ($effBP + $effDS)); - else if ($effDS > 1) - $num .= '-'.($effBP + $effDS); - $_perfItem = array( 'spellId' => $cndSpell->id, 'spellName' => $cndSpell->getField('name', true), @@ -1669,7 +1663,7 @@ class SpellBaseResponse extends TemplateResponse implements ICache Type::ITEM, $this->subject->relItems->id, $this->subject->relItems->getField('name', true), - num: $num, + num: '+'.$this->createNumRange($effBP, $effDS, $extraItem['additionalMaxNum']), quality: $this->subject->relItems->getField('quality') ) ); @@ -2285,6 +2279,11 @@ class SpellBaseResponse extends TemplateResponse implements ICache $this->attributes = $list; } + private function createNumRange(int $bp, int $ds, int $mult = 1) : string + { + return Util::createNumRange($bp + 1, ($bp + $ds) * $mult, '-'); + } + private function generatePath() { $cat = $this->subject->getField('typeCat'); diff --git a/includes/components/SmartAI/SmartAI.class.php b/includes/components/SmartAI/SmartAI.class.php index 1518e426..a9307160 100644 --- a/includes/components/SmartAI/SmartAI.class.php +++ b/includes/components/SmartAI/SmartAI.class.php @@ -21,14 +21,10 @@ trait SmartHelper private function numRange(int $min, int $max, bool $isTime) : string { - if (!$min && !$max) - return ''; + if ($isTime) + return Util::createNumRange($min, $max, ' – ', fn($x) => Util::formatTime($x, true)); - $str = $isTime ? Util::formatTime($min, true) : $min; - if ($max > $min) - $str .= ' – '.($isTime ? Util::formatTime($max, true) : $max); - - return $str; + return Util::createNumRange($min, $max, ' – '); } private function formatTime(int $time, int $_, bool $isMilliSec) : string diff --git a/includes/utilities.php b/includes/utilities.php index 2f3db816..5c1193c4 100644 --- a/includes/utilities.php +++ b/includes/utilities.php @@ -554,6 +554,18 @@ abstract class Util } } + public static function createNumRange(int $min, int $max, string $delim = '', ?callable $fn = null) : string + { + if (!$min && !$max) + return ''; + + $fn ??= fn($x) => $x; + $_min = $fn($min); + $_max = $fn($max); + + return $max > $min ? $_min . ($delim ?: Lang::main('valueDelim')) . $_max : $_min; + } + public static function validateLogin(?string $val) : string { if ($_ = self::validateEmail($val)) From 37380ff5150a3a106a5c62f1f35976a88d99469f Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Mon, 3 Nov 2025 18:00:24 +0100 Subject: [PATCH 090/260] Frontend/InfoboxMarkup * you can now pass attributes to the [li] element --- .../frontend/infoboxmarkup.class.php | 29 +++++++++++++++---- includes/utilities.php | 18 +++++++++++- 2 files changed, 40 insertions(+), 7 deletions(-) diff --git a/includes/components/frontend/infoboxmarkup.class.php b/includes/components/frontend/infoboxmarkup.class.php index 7e6b5d74..bf6d5d18 100644 --- a/includes/components/frontend/infoboxmarkup.class.php +++ b/includes/components/frontend/infoboxmarkup.class.php @@ -23,27 +23,44 @@ class InfoboxMarkup extends Markup public function append(string $text) : self { - if ($this->items && !$this->__text) - $this->replace('[ul][li]' . implode('[/li][li]', $this->items) . '[/li][/ul]'); + if ($_ = $this->prepare()) + $this->replace($_); return parent::append($text); } public function __toString() : string { - if ($this->items && !$this->__text) - $this->replace('[ul][li]' . implode('[/li][li]', $this->items) . '[/li][/ul]'); + if ($_ = $this->prepare()) + $this->replace($_); return parent::__toString(); } public function getJsGlobals() : array { - if ($this->items && !$this->__text) - $this->replace('[ul][li]' . implode('[/li][li]', $this->items) . '[/li][/ul]'); + if ($_ = $this->prepare()) + $this->replace($_); return parent::getJsGlobals(); } + + private function prepare() : string + { + if (!$this->items || $this->__text) + return ''; + + $buff = ''; + foreach ($this->items as $row) + { + if (is_array($row)) + $buff .= '[li'.Util::nodeAttributes($row[1]).']' . $row[0] . '[/li]'; + else if (is_string($row)) + $buff .= '[li]' . $row . '[/li]'; + } + + return $buff ? '[ul]'.$buff.'[/ul]' : ''; + } } ?> diff --git a/includes/utilities.php b/includes/utilities.php index 5c1193c4..e6e62dd5 100644 --- a/includes/utilities.php +++ b/includes/utilities.php @@ -35,7 +35,7 @@ abstract class Util private static $perfectGems = null; - public static $regions = array( + public static $regions = array( 'us', 'eu', 'kr', 'tw', 'cn', 'dev' ); @@ -1175,6 +1175,22 @@ abstract class Util return (string)$var; } + public static function nodeAttributes(?array $attribs) : string + { + if (!$attribs) + return ''; + + return array_reduce(array_keys($attribs), fn($carry, $name) => $carry . match(gettype($attribs[$name])) + { + 'boolean' => ' ' . $attribs[$name] ? $name : '', + 'integer', + 'double' => ' ' . $name . '="' . $attribs[$name] . '"', + 'string' => ' ' . $name . '="' . self::htmlEscape($attribs[$name]) . '"', + 'array' => ' ' . $name . '="' . implode(' ', self::htmlEscape($attribs[$name])) . '"', + default => '' + }, ''); + } + public static function buildPosFixMenu(int $mapId, float $posX, float $posY, int $type, int $guid, int $parentArea = 0, int $parentFloor = 0) : array { $points = WorldPosition::toZonePos($mapId, $posX, $posY); From e3d6f7b3a77bacbbcb26dd99fe23793205a754aa Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Mon, 3 Nov 2025 18:00:50 +0100 Subject: [PATCH 091/260] Profiler/Completions * show completion info for claimed characters in infobox on appropriate db pages --- endpoints/achievement/achievement.php | 7 +- endpoints/faction/faction.php | 9 +- endpoints/item/item.php | 5 +- endpoints/quest/quest.php | 10 +- endpoints/spell/spell.php | 14 +- endpoints/title/title.php | 7 +- .../frontend/infoboxmarkup.class.php | 6 +- includes/dbtypes/item.class.php | 5 + includes/dbtypes/quest.class.php | 8 + includes/dbtypes/spell.class.php | 5 +- includes/user.class.php | 53 ++++ setup/sql/updates/1762199391_01.sql | 1 + .../templates/global.js/listview_templates.js | 286 +++++++++++++++++- .../filegen/templates/global.js/profiler.js | 134 ++++++++ .../tools/filegen/templates/global.js/wow.js | 6 + setup/tools/filegen/templates/power.js.in | 43 +++ static/css/aowow.css | 30 ++ static/js/Profiler.js | 13 +- static/js/locale_dede.js | 6 + static/js/locale_enus.js | 6 + static/js/locale_eses.js | 6 + static/js/locale_frfr.js | 6 + static/js/locale_ruru.js | 6 + static/js/locale_zhcn.js | 6 + template/bricks/infobox.tpl.php | 1 + 25 files changed, 640 insertions(+), 39 deletions(-) create mode 100644 setup/sql/updates/1762199391_01.sql diff --git a/endpoints/achievement/achievement.php b/endpoints/achievement/achievement.php index 7ab7fb86..1068fe42 100644 --- a/endpoints/achievement/achievement.php +++ b/endpoints/achievement/achievement.php @@ -124,10 +124,7 @@ class AchievementBaseResponse extends TemplateResponse implements ICache $y = DB::Aowow()->selectCell('SELECT COUNT(1) FROM ?_profiler_profiles WHERE `realm` IS NOT NULL AND `realmGUID` IS NOT NULL AND `cuFlags` & ?d = 0', PROFILER_CU_NEEDS_RESYNC); $infobox[] = Lang::profiler('attainedBy', [round(($x ?: 0) * 100 / ($y ?: 1))]); - // - js component missing; - // - can't yet assign styles to li element - // if (User::getPinnedCharacter()) - // $infobox[] = Lang::profiler('completion') . '[span class="compact-completion-display"][/span]'; // [li style="display:none"]...[/li] + // completion row added by InfoboxMarkup } // original name @@ -135,7 +132,7 @@ class AchievementBaseResponse extends TemplateResponse implements ICache $infobox[] = Util::ucFirst(Lang::lang(Locale::EN->value) . Lang::main('colon')) . '[copy button=false]'.$this->subject->getField('name_loc0').'[/copy][/li]'; if ($infobox) - $this->infobox = new InfoboxMarkup($infobox, ['allow' => Markup::CLASS_STAFF, 'dbpage' => true], 'infobox-contents0'); + $this->infobox = new InfoboxMarkup($infobox, ['allow' => Markup::CLASS_STAFF, 'dbpage' => true], 'infobox-contents0', !($this->subject->getField('flags') & ACHIEVEMENT_FLAG_COUNTER)); /**********/ diff --git a/endpoints/faction/faction.php b/endpoints/faction/faction.php index 84ba340f..2ca7ca92 100644 --- a/endpoints/faction/faction.php +++ b/endpoints/faction/faction.php @@ -106,18 +106,15 @@ class FactionBaseResponse extends TemplateResponse implements ICache $y = DB::Aowow()->selectCell('SELECT COUNT(1) FROM ?_profiler_profiles WHERE `realm` IS NOT NULL AND `realmGUID` IS NOT NULL AND `cuFlags` & ?d = 0', PROFILER_CU_NEEDS_RESYNC); $infobox[] = Lang::profiler('attainedBy', [round(($x ?: 0) * 100 / ($y ?: 1))]); - // - js component missing; - // - can't yet assign styles to li element - // if (User::getPinnedCharacter()) - // $infobox[] = Lang::profiler('completion') . '[span class="compact-completion-display"][/span]'; // [li style="display:none"]...[/li] + // completion row added by InfoboxMarkup } // original name if (Lang::getLocale() != Locale::EN) $infobox[] = Util::ucFirst(Lang::lang(Locale::EN->value) . Lang::main('colon')) . '[copy button=false]'.$this->subject->getField('name_loc0').'[/copy][/li]'; - if ($infobox) - $this->infobox = new InfoboxMarkup($infobox, ['allow' => Markup::CLASS_STAFF, 'dbpage' => true], 'infobox-contents0'); + if ($infobox) // unsure if this should be tracked (needs data dump in User::getCompletion()) + $this->infobox = new InfoboxMarkup($infobox, ['allow' => Markup::CLASS_STAFF, 'dbpage' => true], 'infobox-contents0', 0); /****************/ diff --git a/endpoints/item/item.php b/endpoints/item/item.php index d6582962..34d94c1b 100644 --- a/endpoints/item/item.php +++ b/endpoints/item/item.php @@ -319,12 +319,15 @@ class ItemBaseResponse extends TemplateResponse implements ICache if ($_bagFamily & 0x0100) $infobox[] = Lang::item('atKeyring'); + // completion row added by InfoboxMarkup + // original name if (Lang::getLocale() != Locale::EN) $infobox[] = Util::ucFirst(Lang::lang(Locale::EN->value) . Lang::main('colon')) . '[copy button=false]'.$this->subject->getField('name_loc0').'[/copy][/li]'; + $hasCompletion = !($this->subject->getField('cuFlags') & CUSTOM_EXCLUDE_FOR_LISTVIEW) && ($_class == ITEM_CLASS_RECIPE || ($_class == ITEM_CLASS_MISC && in_array($_subClass, [2, 5, -7]))); if ($infobox) - $this->infobox = new InfoboxMarkup($infobox, ['allow' => Markup::CLASS_STAFF, 'dbpage' => true], 'infobox-contents0'); + $this->infobox = new InfoboxMarkup($infobox, ['allow' => Markup::CLASS_STAFF, 'dbpage' => true], 'infobox-contents0', $hasCompletion); /****************/ diff --git a/endpoints/quest/quest.php b/endpoints/quest/quest.php index 4c01b65a..6830704d 100644 --- a/endpoints/quest/quest.php +++ b/endpoints/quest/quest.php @@ -64,6 +64,7 @@ class QuestBaseResponse extends TemplateResponse implements ICache $_flags = $this->subject->getField('flags'); $_specialFlags = $this->subject->getField('specialFlags'); $_side = ChrRace::sideFromMask($this->subject->getField('reqRaceMask')); + $hasCompletion = !($_flags & QUEST_FLAG_UNAVAILABLE || $this->subject->getField('cuFlags') & CUSTOM_EXCLUDE_FOR_LISTVIEW); /*************/ @@ -275,16 +276,13 @@ class QuestBaseResponse extends TemplateResponse implements ICache $infobox[] = Lang::quest('id') . $this->typeId; // profiler relateed (note that this is part of the cache. I don't think this is important enough to calc for every view) - if (Cfg::get('PROFILER_ENABLE') && !($_flags & QUEST_FLAG_UNAVAILABLE || $this->subject->getField('cuFlags') & CUSTOM_EXCLUDE_FOR_LISTVIEW)) + if (Cfg::get('PROFILER_ENABLE') && $hasCompletion) { $x = DB::Aowow()->selectCell('SELECT COUNT(1) FROM ?_profiler_completion_quests WHERE `questId` = ?d', $this->typeId); $y = DB::Aowow()->selectCell('SELECT COUNT(1) FROM ?_profiler_profiles WHERE `realm` IS NOT NULL AND `realmGUID` IS NOT NULL AND `cuFlags` & ?d = 0', PROFILER_CU_NEEDS_RESYNC); $infobox[] = Lang::profiler('attainedBy', [round(($x ?: 0) * 100 / ($y ?: 1))]); - // - js component missing; - // - can't yet assign styles to li element - // if (User::getPinnedCharacter()) - // $infobox[] = Lang::profiler('completion') . '[span class="compact-completion-display"][/span]'; // [li style="display:none"]...[/li] + // completion row added by InfoboxMarkup } // original name @@ -292,7 +290,7 @@ class QuestBaseResponse extends TemplateResponse implements ICache $infobox[] = Util::ucFirst(Lang::lang(Locale::EN->value) . Lang::main('colon')) . '[copy button=false]'.$this->subject->getField('name_loc0').'[/copy][/li]'; if ($infobox) - $this->infobox = new InfoboxMarkup($infobox, ['allow' => Markup::CLASS_STAFF, 'dbpage' => true], 'infobox-contents0'); + $this->infobox = new InfoboxMarkup($infobox, ['allow' => Markup::CLASS_STAFF, 'dbpage' => true], 'infobox-contents0', $hasCompletion); /*******************/ diff --git a/endpoints/spell/spell.php b/endpoints/spell/spell.php index 28bafde2..5508beff 100644 --- a/endpoints/spell/spell.php +++ b/endpoints/spell/spell.php @@ -2352,8 +2352,9 @@ class SpellBaseResponse extends TemplateResponse implements ICache private function createInfobox() : void { - $infobox = Lang::getInfoBoxForFlags($this->subject->getField('cuFlags')); - $typeCat = $this->subject->getField('typeCat'); + $infobox = Lang::getInfoBoxForFlags($this->subject->getField('cuFlags')); + $typeCat = $this->subject->getField('typeCat'); + $hasCompletion = in_array($typeCat, [-5, -6]) && !($this->subject->getField('cuFlags') & CUSTOM_EXCLUDE_FOR_LISTVIEW); // level if (!in_array($typeCat, [-5, -6])) // not mount or vanity pet @@ -2445,16 +2446,13 @@ class SpellBaseResponse extends TemplateResponse implements ICache } // profiler relateed (note that this is part of the cache. I don't think this is important enough to calc for every view) - if (Cfg::get('PROFILER_ENABLE') && in_array($this->subject->getField('typeCat'), [-5, -6]) && !($this->subject->getField('cuFlags') & CUSTOM_EXCLUDE_FOR_LISTVIEW)) + if (Cfg::get('PROFILER_ENABLE') && $hasCompletion) { $x = DB::Aowow()->selectCell('SELECT COUNT(1) FROM ?_profiler_completion_spells WHERE `spellId` = ?d', $this->typeId); $y = DB::Aowow()->selectCell('SELECT COUNT(1) FROM ?_profiler_profiles WHERE `realm` IS NOT NULL AND `realmGUID` IS NOT NULL AND `cuFlags` & ?d = 0', PROFILER_CU_NEEDS_RESYNC); $infobox[] = Lang::profiler('attainedBy', [round(($x ?: 0) * 100 / ($y ?: 1))]); - // - js component missing; - // - can't yet assign styles to li element - // if (User::getPinnedCharacter()) - // $infobox[] = Lang::profiler('completion') . '[span class="compact-completion-display"][/span]'; // [li style="display:none"]...[/li] + // completion row added by InfoboxMarkup } // original name @@ -2484,7 +2482,7 @@ class SpellBaseResponse extends TemplateResponse implements ICache $infobox[] = 'Script'.Lang::main('colon').$_; - $this->infobox = new InfoboxMarkup($infobox, ['allow' => Markup::CLASS_STAFF, 'dbpage' => true], 'infobox-contents0'); + $this->infobox = new InfoboxMarkup($infobox, ['allow' => Markup::CLASS_STAFF, 'dbpage' => true], 'infobox-contents0', $hasCompletion); // append glyph symbol if available $glyphId = 0; diff --git a/endpoints/title/title.php b/endpoints/title/title.php index 7bc4ae01..1c935196 100644 --- a/endpoints/title/title.php +++ b/endpoints/title/title.php @@ -94,10 +94,7 @@ class TitleBaseResponse extends TemplateResponse implements ICache $y = DB::Aowow()->selectCell('SELECT COUNT(1) FROM ?_profiler_profiles WHERE `realm` IS NOT NULL AND `realmGUID` IS NOT NULL AND `cuFlags` & ?d = 0', PROFILER_CU_NEEDS_RESYNC); $infobox[] = Lang::profiler('attainedBy', [round(($x ?: 0) * 100 / ($y ?: 1))]); - // - js component missing; - // - can't yet assign styles to li element - // if (User::getPinnedCharacter()) - // $infobox[] = Lang::profiler('completion') . '[span class="compact-completion-display"][/span]'; // [li style="display:none"]...[/li] + // completion row added by InfoboxMarkup } // original name @@ -105,7 +102,7 @@ class TitleBaseResponse extends TemplateResponse implements ICache $infobox[] = Util::ucFirst(Lang::lang(Locale::EN->value) . Lang::main('colon')) . '[copy button=false]'.$this->subject->getField('name_loc0').'[/copy][/li]'; if ($infobox) - $this->infobox = new InfoboxMarkup($infobox, ['allow' => Markup::CLASS_STAFF, 'dbpage' => true], 'infobox-contents0'); + $this->infobox = new InfoboxMarkup($infobox, ['allow' => Markup::CLASS_STAFF, 'dbpage' => true], 'infobox-contents0', 1); /****************/ diff --git a/includes/components/frontend/infoboxmarkup.class.php b/includes/components/frontend/infoboxmarkup.class.php index bf6d5d18..dff22075 100644 --- a/includes/components/frontend/infoboxmarkup.class.php +++ b/includes/components/frontend/infoboxmarkup.class.php @@ -8,7 +8,7 @@ if (!defined('AOWOW_REVISION')) class InfoboxMarkup extends Markup { - public function __construct(private array $items, array $opts, string $parent = '') + public function __construct(private array $items, array $opts, string $parent = '', private int $completionRowType = 0) { parent::__construct('', $opts, $parent); } @@ -31,6 +31,10 @@ class InfoboxMarkup extends Markup public function __toString() : string { + // inject before output to avoid adding it to cache + if ($this->completionRowType && User::getCharacters()) + $this->items[] = [Lang::profiler('completion') . '[span class="compact-completion-display"][/span]', ['style' => 'display:none']]; + if ($_ = $this->prepare()) $this->replace($_); diff --git a/includes/dbtypes/item.class.php b/includes/dbtypes/item.class.php index 10c4fc1d..ceca727c 100644 --- a/includes/dbtypes/item.class.php +++ b/includes/dbtypes/item.class.php @@ -487,6 +487,11 @@ class ItemList extends DBTypeList 'quality' => $this->curTpl['quality'], 'icon' => $this->curTpl['iconString'] ); + + if ($this->curTpl['class'] == ITEM_CLASS_RECIPE) + $data[Type::ITEM][$id]['completion_category'] = $this->curTpl['class']; + else if ($this->curTpl['class'] == ITEM_CLASS_MISC && in_array($this->curTpl['subClass'], [2, 5, -7])) + $data[Type::ITEM][$id]['completion_category'] = $this->curTpl['class'].'-'.$this->curTpl['subClass']; } if ($addMask & GLOBALINFO_EXTRA) diff --git a/includes/dbtypes/quest.class.php b/includes/dbtypes/quest.class.php index f7798935..7754a173 100644 --- a/includes/dbtypes/quest.class.php +++ b/includes/dbtypes/quest.class.php @@ -415,7 +415,15 @@ class QuestList extends DBTypeList } if ($addMask & GLOBALINFO_SELF) + { $data[Type::QUEST][$this->id] = ['name' => $this->getField('name', true)]; + + if ($this->curTpl['flags'] & QUEST_FLAG_DAILY) + $data[Type::QUEST][$this->id]['daily'] = true; + + if ($this->curTpl['flags'] & QUEST_FLAG_WEEKLY) + $data[Type::QUEST][$this->id]['weekly'] = true; + } } return $data; diff --git a/includes/dbtypes/spell.class.php b/includes/dbtypes/spell.class.php index 70d6e46f..aabf72e0 100644 --- a/includes/dbtypes/spell.class.php +++ b/includes/dbtypes/spell.class.php @@ -2150,8 +2150,11 @@ class SpellList extends DBTypeList { $data[Type::SPELL][$id] = array( 'icon' => $this->curTpl['iconStringAlt'] ?: $this->curTpl['iconString'], - 'name' => $this->getField('name', true), + 'name' => $this->getField('name', true) ); + + if (($_ = $this->curTpl['typeCat']) && in_array($_, [-5, -6, 9, 11])) + $data[Type::SPELL][$id]['completion_category'] = $_; } if ($addMask & GLOBALINFO_EXTRA) diff --git a/includes/user.class.php b/includes/user.class.php index 12928616..8ebd6261 100644 --- a/includes/user.class.php +++ b/includes/user.class.php @@ -569,6 +569,7 @@ class User $gUser['downvoteRep'] = Cfg::get('REP_REQ_DOWNVOTE'); $gUser['upvoteRep'] = Cfg::get('REP_REQ_UPVOTE'); $gUser['characters'] = self::getCharacters(); + $gUser['completion'] = self::getCompletion(); $gUser['excludegroups'] = self::$excludeGroups; if (self::$debug) @@ -723,6 +724,58 @@ class User return $data; } + + public static function getCompletion() : array + { + $ids = []; + foreach (self::$profiles->iterate() as $_) + if (!self::$profiles->isCustom()) + $ids[] = self::$profiles->id; + + if (!$ids) + return []; + + $completion = []; + + $x = DB::Aowow()->selectCol('SELECT `id` AS ARRAY_KEY, `questId` AS ARRAY_KEY2, `questId` FROM ?_profiler_completion_quests WHERE `id` IN (?a)', $ids); + $completion[Type::QUEST] = $x ? array_map(array_values(...), $x) : []; + + $x = DB::Aowow()->selectCol('SELECT `id` AS ARRAY_KEY, `achievementId` AS ARRAY_KEY2, `achievementId` FROM ?_profiler_completion_achievements WHERE `id` IN (?a)', $ids); + $completion[Type::ACHIEVEMENT] = $x ? array_map(array_values(...), $x) : []; + + $x = DB::Aowow()->selectCol('SELECT `id` AS ARRAY_KEY, `titleId` AS ARRAY_KEY2, `titleId` FROM ?_profiler_completion_titles WHERE `id` IN (?a)', $ids); + $completion[Type::TITLE] = $x ? array_map(array_values(...), $x) : []; + + $completion[Type::ITEM] = []; + + $spells = DB::Aowow()->select( + 'SELECT pcs.`id` AS ARRAY_KEY, pcs.`spellId` AS ARRAY_KEY2, pcs.`spellId`, i.`id` AS "itemId" + FROM ?_spell s + JOIN ?_profiler_completion_spells pcs ON s.`id` = pcs.`spellId` + LEFT JOIN ?_items i ON i.spellId1 IN (?a) AND i.spellId2 = pcs.spellId + WHERE s.`typeCat` IN (?a) AND pcs.`id` IN (?a)', + LEARN_SPELLS, [-5, -6, 9, 11], $ids + ); + + if ($spells) + { + $completion[Type::SPELL] = array_map(fn($x) => array_column($x, 'spellId'), $spells); + + if ($recipes = array_map(fn($x) => array_filter(array_column($x, 'itemId')), $spells)) + foreach ($ids as $id) // array_merge_recursive does not respect numeric keys + $completion[Type::ITEM][$id] = array_merge($completion[Type::ITEM][$id] ?? [], $recipes[$id] ?? []); + } + else + $completion[Type::SPELL] = []; + + // init empty result sets + foreach ($completion as &$c) + foreach ($ids as $id) + if (!isset($c[$id])) + $c[$id] = []; + + return $completion; + } } ?> diff --git a/setup/sql/updates/1762199391_01.sql b/setup/sql/updates/1762199391_01.sql new file mode 100644 index 00000000..168a5f1b --- /dev/null +++ b/setup/sql/updates/1762199391_01.sql @@ -0,0 +1 @@ +UPDATE `aowow_dbversion` SET `build` = CONCAT(IFNULL(`build`, ''), ' globaljs tooltips'); diff --git a/setup/tools/filegen/templates/global.js/listview_templates.js b/setup/tools/filegen/templates/global.js/listview_templates.js index 2dc8aca2..4f3b1d92 100644 --- a/setup/tools/filegen/templates/global.js/listview_templates.js +++ b/setup/tools/filegen/templates/global.js/listview_templates.js @@ -905,6 +905,43 @@ Listview.templates = { var _ = Listview.funcBox.getItemType; return $WH.strcmp(_(a.classs, a.subclass, a.subsubclass).text, _(b.classs, b.subclass, b.subsubclass).text); } + }, + { + id: 'completed', // Listview.COLUMN_ID_COMPLETION + name: LANG.completion, // WH.TERMS.completion + hidden: true, + compute: function (item, td) + { + var skip = !item.hasOwnProperty('classs') || !Listview.templates.item._validCompletionCategory(item.classs, item.subclass, item.quality); + $WH.addCompletionIcons(td, 3, item.id, skip); + }, + sortFunc: function (a, b) + { + // return $WH.stringCompare( + return $WH.strcmp( + $WH.getCompletionFlags(3, a.id), + $WH.getCompletionFlags(3, b.id) + ); + }, + getValue: function (item) { + var value = 0; + var completionData = g_user.completion?.hasOwnProperty(3) ? g_user.completion[3] : {}; + + if (!item.hasOwnProperty('classs') || !Listview.templates.item._validCompletionCategory(item.classs, item.subclass, item.quality)) + return -1; + + for (var i in g_user.characters) + { + var profile = g_user.characters[i]; + if (!(profile.id in completionData)) + continue; + + if ($WH.in_array(completionData[profile.id], item.id) != -1) + value++; + } + + return value; + } } ], @@ -912,6 +949,12 @@ Listview.templates = { return item.name.charAt(0) == '@' ? 'javascript:;' : '?item=' + item.id; }, + _validCompletionCategory: function (classs, subclass, quality) { + return $WH.in_array(g_completion_categories[3], classs) != -1 || + $WH.in_array(g_completion_categories[3], '' + classs + '-' + subclass) != -1 || + $WH.in_array(g_completion_categories[3], '' + classs + 'q' + quality) != -1 + }, + onBeforeCreate: function() { var nComparable = false; @@ -930,6 +973,23 @@ Listview.templates = { this.mode = Listview.MODE_CHECKBOX; this._nComparable = nComparable; } + + for (var i in this.columns) + { + if (this.columns[i].id == 'completed' && this.columns[i].hidden) + { + if ($WH.isset('g_user') && 'characters' in g_user && $WH.in_array(this.hiddenCols, this.columns[i].id) == -1) + { + var n = 0; + for (var j in this.data) + if (this.data[j].hasOwnProperty('classs') && Listview.templates.item._validCompletionCategory(this.data[j].classs, this.data[j].subclass, this.data[j].quality)) + n++; + + if (n > this.data.length * 0.1) + this.visibility.push(parseInt(i)); + } + } + } }, createCbControls: function(div, topBar) { @@ -1148,7 +1208,7 @@ Listview.templates = { } } else { - return - 1; + return -1; } }, sortFunc: function(a, b, col) { @@ -1902,11 +1962,69 @@ Listview.templates = { var _ = Listview.funcBox.getQuestCategory; return $WH.strcmp(_(a.category), _(b.category)); } + }, + { + id: 'completed', // Listview.COLUMN_ID_COMPLETION + name: LANG.completion, // WH.TERMS.completion + hidden: true, + compute: function (quest, td) + { + if (quest.daily || quest.weekly) + return; + + $WH.addCompletionIcons(td, 5, quest.id) + }, + sortFunc: function (a, b) + { + // return $WH.stringCompare( + return $WH.strcmp( + $WH.getCompletionFlags(5, a.id), + $WH.getCompletionFlags(5, b.id) + ); + }, + getValue: function (quest) { + var value = 0; + var completionData = g_user.completion?.hasOwnProperty(5) ? g_user.completion[5] : {}; + + if (quest.daily || quest.weekly) + return -1; + + for (var i in g_user.characters) + { + var profile = g_user.characters[i]; + if (!(profile.id in completionData)) + continue; + + if ($WH.in_array(completionData[profile.id], quest.id) != -1) + value++; + } + + return value; + } } ], getItemLink: function(quest) { return '?quest=' + quest.id; + }, + + onBeforeCreate: function () { + for (var i in this.columns) + { + if (this.columns[i].id == 'completed' && this.columns[i].hidden) + { + if ($WH.isset('g_user') && 'characters' in g_user && $WH.in_array(this.hiddenCols, this.columns[i].id) == -1) + { + var n = 0; + for (var j in this.data) + if (!this.data[j].daily && !this.data[j].weekly) + n++; + + if (n > this.data.length * 0.1) + this.visibility.push(parseInt(i)); + } + } + } } }, @@ -3432,12 +3550,74 @@ Listview.templates = { return 0; } - } + }, /* AoWoW: custom end */ + { + id: 'completed', // Listview.COLUMN_ID_COMPLETION + name: LANG.completion, // WH.TERMS.completion + hidden: true, + compute: function (spell, td) + { + if (!spell.hasOwnProperty('cat') || !Listview.templates.spell._validCompletionCategory(spell.cat)) + return; + + $WH.addCompletionIcons(td, 6, spell.id) + }, + sortFunc: function (a, b) + { + // return $WH.stringCompare( + return $WH.strcmp( + $WH.getCompletionFlags(6, a.id), + $WH.getCompletionFlags(6, b.id) + ); + }, + getValue: function (spell) { + var value = 0; + var completionData = g_user.completion?.hasOwnProperty(6) ? g_user.completion[6] : {}; + + if (!spell.hasOwnProperty('cat') || !Listview.templates.spell._validCompletionCategory(spell.cat)) + return -1; + + for (var i in g_user.characters) + { + var profile = g_user.characters[i]; + if (!(profile.id in completionData)) + continue; + + if ($WH.in_array(completionData[profile.id], spell.id) != -1) + value++; + } + + return value; + } + } ], getItemLink: function(spell) { return '?spell=' + spell.id; + }, + + _validCompletionCategory: function (category) { + return $WH.in_array(g_completion_categories[6], category) != -1; + }, + + onBeforeCreate: function () { + for (var i in this.columns) + { + if (this.columns[i].id == 'completed' && this.columns[i].hidden) + { + if ($WH.isset('g_user') && 'characters' in g_user && $WH.in_array(this.hiddenCols, this.columns[i].id) == -1) + { + var n = 0; + for (var j in this.data) + if (this.data[j].hasOwnProperty('cat') && Listview.templates.spell._validCompletionCategory(this.data[j].cat)) + n++; + + if (n > this.data.length * 0.1) + this.visibility.push(parseInt(i)); + } + } + } } }, @@ -5834,11 +6014,69 @@ Listview.templates = { return $WH.strcmp(g_achievement_categories[a.category], g_achievement_categories[b.category]); }, hidden: true + }, + { + id: 'completed', // Listview.COLUMN_ID_COMPLETION + name: LANG.completion, // WH.TERMS.completion + hidden: true, + compute: function (achievement, td) + { + if (achievement.type) + return; + + $WH.addCompletionIcons(td, 10, achievement.id) + }, + sortFunc: function (a, b) + { + // return $WH.stringCompare( + return $WH.strcmp( + $WH.getCompletionFlags(10, a.id), + $WH.getCompletionFlags(10, b.id) + ); + }, + getValue: function (achievement) { + var value = 0; + var completionData = g_user.completion?.hasOwnProperty(10) ? g_user.completion[10] : {}; + + if (achievement.type) + return -1; + + for (var i in g_user.characters) + { + var profile = g_user.characters[i]; + if (!(profile.id in completionData)) + continue; + + if ($WH.in_array(completionData[profile.id], achievement.id) != -1) + value++; + } + + return value; + } } ], getItemLink: function(achievement) { return '?achievement=' + achievement.id; + }, + + onBeforeCreate: function () { + for (var i in this.columns) + { + if (this.columns[i].id == 'completed' && this.columns[i].hidden) + { + if ($WH.isset('g_user') && 'characters' in g_user && $WH.in_array(this.hiddenCols, this.columns[i].id) == -1) + { + var n = 0; + for (var j in this.data) + if (!this.data[j].type) + n++; + + if (n > this.data.length * 0.1) + this.visibility.push(parseInt(i)); + } + } + } } }, @@ -6052,11 +6290,55 @@ Listview.templates = { return $WH.strcmp(g_title_categories[a.category], g_title_categories[b.category]); }, hidden: true + }, + { + id: 'completed', // Listview.COLUMN_ID_COMPLETION + name: LANG.completion, // WH.TERMS.completion + hidden: true, + compute: function (title, td) + { + $WH.addCompletionIcons(td, 11, title.id) + }, + sortFunc: function (a, b) + { + // return $WH.stringCompare( + return $WH.strcmp( + $WH.getCompletionFlags(11, a.id), + $WH.getCompletionFlags(11, b.id) + ); + }, + getValue: function (title) { + var value = 0; + var completionData = g_user.completion?.hasOwnProperty(11) ? g_user.completion[11] : {}; + + for (var i in g_user.characters) + { + var profile = g_user.characters[i]; + if (!(profile.id in completionData)) + continue; + + if ($WH.in_array(completionData[profile.id], title.id) != -1) + value++; + } + + return value; + } } ], getItemLink: function(title) { return '?title=' + title.id; + }, + + onBeforeCreate: function () { + for (var i in this.columns) + { + if (this.columns[i].id == 'completed' && this.columns[i].hidden) + { + if ($WH.isset('g_user') && 'characters' in g_user && $WH.in_array(this.hiddenCols, this.columns[i].id) == -1) + this.visibility.push(parseInt(i)); + } + } } }, diff --git a/setup/tools/filegen/templates/global.js/profiler.js b/setup/tools/filegen/templates/global.js/profiler.js index 52a23ecc..273ea12f 100644 --- a/setup/tools/filegen/templates/global.js/profiler.js +++ b/setup/tools/filegen/templates/global.js/profiler.js @@ -17,3 +17,137 @@ function g_getProfileUrl(profile) { function g_getProfileRealmUrl(profile) { return '?profiles=' + profile.region + '.' + profile.realm; } + +$WH.prepInfobox = function () { + $('.infobox').each(function () + { + var row = $(this); + if (row.data('infobox-completion-info-added') !== true && g_user.characters && + typeof g_pageInfo == 'object' && typeof g_pageInfo.type == 'number' && typeof g_pageInfo.typeId == 'number') + { + var wardrobe = $('.compact-completion-display', row); + if (wardrobe.length) + { + var completionIcon = $('span', wardrobe); + if (completionIcon.length) + { + var i = 0; + completionIcon.each(function () + { + var e = $(this); + var t = e.html(); + + try { t = JSON.parse(t) } + catch (e) { return } + + var a; + for (var n = 0, s; s = g_user.characters[n]; n++) + { + if (s.id == t.id) + { + a = s; + break + } + } + + if (a) + { + e.parent().append($WH.createCompletionIcon(a, t.completed ? 1 : 0, t.rel)); + e.remove(); + i++ + } + }); + + if (i) + wardrobe.parent().parent().show() + } + else if (wardrobe.is(':empty') && $WH.addCompletionIcons(wardrobe.get(0), g_pageInfo.type, g_pageInfo.typeId)) + wardrobe.parent().parent().show() + } + + row.data('infobox-completion-info-added', true) + } + }); +}; + +$WH.addCompletionIcons = function (parent, type, typeIdOrData, skipIncomplete) { + var nComplete = 0; + + for (var i in g_user.characters) + { + if (!g_user.characters.hasOwnProperty(i)) + continue; + + let profile = g_user.characters[i]; + + let completion = 0; + let completionData = g_user.completion?.hasOwnProperty(type) ? g_user.completion[type] : {}; + if (!completionData.hasOwnProperty(profile.id)) + continue; + + completion = completionData[profile.id].includes(typeIdOrData) ? 1 : 0; + + if (skipIncomplete && !completion) + continue; + + $WH.ae(parent, $WH.createCompletionIcon(profile, completion)); + nComplete++; + } + + return nComplete; +}; + +$WH.createCompletionIcon = function (profile, completePct, rel) { + var icon = $WH.ce('a'); + icon.href = '?profile=' + profile.region + '.' + profile.realm + '.' + profile.name; + + // aowow - so the generic tooltips dont override our completion tooltip + icon.setAttribute('data-disable-wowhead-tooltip', true); + + icon.className = 'progress-icon progress-' + (completePct ? Math.max(1, Math.floor(completePct * 8)) : 0); + $WH.Tooltip.simple(icon, $WH.getCompletionTooltip(profile, completePct), null, true); + + if (rel) + icon.rel = rel; + + return icon; +}; + +$WH.getCompletionTooltip = function (profile, completePct) { + let tooltip = $WH.ce('div'); + $WH.ae(tooltip, $WH.ce('span', { className: 'q' }, $WH.ct((completePct >= 1 ? LANG.complete : LANG.incomplete) + LANG.colon))); + $WH.ae(tooltip, $WH.ce('br')); + + let charRow = $WH.ce('span', { style: { whiteSpace: 'nowrap' } }); + $WH.ae(tooltip, charRow); + $WH.ae(charRow, $WH.ce('b', { className: 'c' + profile.classs }, $WH.ct(profile.name))); + + let server = [' ', profile.realmname]; + + if (profile.hasOwnProperty('region')) + server.push(profile.region.toUpperCase()); + + $WH.ae(charRow, $WH.ce('span', { className: 'q0' }, $WH.ct(server.join(' ')))); + + if (completePct > 0 && completePct < 1) + $WH.ae(charRow, $WH.ct( $WH.sprintf(LANG.parens_format, '', Math.round(completePct * 100) + '%') )); + + return tooltip.innerHTML; +}; + +$WH.getCompletionFlags = function (type, typeId) { + var flags = 0; + var profiles = g_user.characters || []; + let completionData = g_user.completion?.hasOwnProperty(type) ? g_user.completion[type] : {}; + + for (var i = profiles.length - 1; i >= 0; i--) + { + var profile = profiles[i]; + if (!(profile.id in completionData)) + continue + + flags = flags << 1 | (completionData[profile.id].includes(typeId) ? 1 : 0) + } + + return flags; +}; diff --git a/setup/tools/filegen/templates/global.js/wow.js b/setup/tools/filegen/templates/global.js/wow.js index d482e5a7..99323c8b 100644 --- a/setup/tools/filegen/templates/global.js/wow.js +++ b/setup/tools/filegen/templates/global.js/wow.js @@ -269,6 +269,12 @@ var g_types = { 504: 'mail' }; +var g_completion_categories = { + // 1: [12], // NPCs: Battle Pets + 3: [9, "15-2", "15-5", "15--7"], // Items: Recipes, Minipets, Mounts (Ground), Mounts (Flying) + 6: [-5, -6, 9, 11] // Spells: Mounts, Minipets, Sec. Skills, Prim. Skills +}; + // Items $WH.cO(g_items, { add: function(id, json) diff --git a/setup/tools/filegen/templates/power.js.in b/setup/tools/filegen/templates/power.js.in index 717345d7..f6e916f2 100644 --- a/setup/tools/filegen/templates/power.js.in +++ b/setup/tools/filegen/templates/power.js.in @@ -644,6 +644,49 @@ if (typeof $WowheadPower == "undefined") { } } + if (!isRemote && window.g_user && g_user.characters) + { + var completion = ''; + let completionData = g_user.completion.hasOwnProperty(currentType) ? g_user.completion[currentType] : false; + + let entity = {}; + if (currentType == TYPE_QUEST && $WH.isset('g_quests')) + entity = g_quests[currentId] || {}; + if (currentType == TYPE_ACHIEVEMENT && $WH.isset('g_achievements')) + entity = g_achievements[currentId] || {}; + if (currentType == TYPE_ITEM && $WH.isset('g_items')) + entity = g_items[currentId] || {}; + if (currentType == TYPE_SPELL && $WH.isset('g_spells')) + entity = g_spells[currentId] || {}; + + if ((!LOOKUPS[currentType][0] || LOOKUPS[currentType][0][currentId].status[currentLocale] !== STATUS_OK) || + (currentType === TYPE_QUEST && (entity.daily || entity.weekly)) || + (currentType === TYPE_ACHIEVEMENT && entity.type)) + completionData = false; + + let CompetionWithoutCatg = !(completionData && currentType in g_completion_categories && $WH.in_array(g_completion_categories[currentType], entity.completion_category) === -1); + + if (completionData) + { + for (var i in g_user.characters) + { + var profile = g_user.characters[i]; + if (!(profile.id in completionData)) + continue; + + let isComplete = $WH.in_array(completionData[profile.id], currentId) !== - 1; + if (!isComplete && !CompetionWithoutCatg) + continue; + + completion += '
    '; + completion += profile.name + ' - ' + profile.realmname + ' ' + profile.region.toUpperCase(); + } + } + + if (completion !== '') + html += '
    ' + LANG.completion + ':' + completion; + } + if (currentParams.map && map && map.getMap) { html2 = map.getMap(); } diff --git a/static/css/aowow.css b/static/css/aowow.css index 64c3fa45..4befa58b 100644 --- a/static/css/aowow.css +++ b/static/css/aowow.css @@ -4260,3 +4260,33 @@ a.button-red.fa-clipboard > em > span { .fav-star-1 { background-position: -65px center; } + +/* imported from lists */ +.compact-completion-display a { + margin-left: 2px; + vertical-align: text-top; /* middle; */ +} + +.progress-icon { + /* background: url(/images/ListManager/completion.png?4) no-repeat; */ + background: url(../images/ui/check.png) no-repeat -1px -15px; + border-radius: 99px; + display: inline-block; + height: 13px; /* 16px; */ + line-height: 16px; + vertical-align: sub; /* bottom; */ + width: 13px; /* 16px; */ +} + +.progress-icon:focus { + box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.5); +} + +/* newer atlas has 9 icons */ +.progress-icon.progress-8 { + background-position: -1px 0px; +} + +.progress-icon.with-text { + padding-left: 16px; +} diff --git a/static/js/Profiler.js b/static/js/Profiler.js index ca60a8bf..2856b7e0 100644 --- a/static/js/Profiler.js +++ b/static/js/Profiler.js @@ -8538,10 +8538,15 @@ function ProfilerCompletion(_parent) { _tabsListview.show((_subtotal[_category].complete[_subcategory] ? 2 : 3)); if (_opt.subname) { - _listview.createIndicator($WH.sprintf(LANG['lvnote_' + _mode + 'ind'], _category, _subcategory, _opt.subname(_subcategory)), Listview.headerFilter.bind(_listview, _listview.columns[_listview.columns.length - 2], '')); - _excluded.createIndicator($WH.sprintf(LANG['lvnote_' + _mode + 'ind'], _category, _subcategory, _opt.subname(_subcategory)), Listview.headerFilter.bind(_excluded, _listview.columns[_listview.columns.length - 2], '')); - setTimeout(Listview.headerFilter.bind(_listview, _listview.columns[_listview.columns.length - 2], _opt.subname(_subcategory)), 1); - setTimeout(Listview.headerFilter.bind(_excluded, _excluded.columns[_excluded.columns.length - 2], _opt.subname(_subcategory)), 1); + // aowow - adressing col by offset breaks everytime we add/remove cols in a listview template + // _listview.createIndicator($WH.sprintf(LANG['lvnote_' + _mode + 'ind'], _category, _subcategory, _opt.subname(_subcategory)), Listview.headerFilter.bind(_listview, _listview.columns[_listview.columns.length - 2], '')); + // _excluded.createIndicator($WH.sprintf(LANG['lvnote_' + _mode + 'ind'], _category, _subcategory, _opt.subname(_subcategory)), Listview.headerFilter.bind(_excluded, _listview.columns[_listview.columns.length - 2], '')); + // setTimeout(Listview.headerFilter.bind(_listview, _listview.columns[_listview.columns.length - 2], _opt.subname(_subcategory)), 1); + // setTimeout(Listview.headerFilter.bind(_excluded, _excluded.columns[_excluded.columns.length - 2], _opt.subname(_subcategory)), 1); + _listview.createIndicator($WH.sprintf(LANG['lvnote_' + _mode + 'ind'], _category, _subcategory, _opt.subname(_subcategory)), Listview.headerFilter.bind(_listview, _listview.columns.find((x) => x.id == (_opt.catgcol || 'category')), '')); + _excluded.createIndicator($WH.sprintf(LANG['lvnote_' + _mode + 'ind'], _category, _subcategory, _opt.subname(_subcategory)), Listview.headerFilter.bind(_excluded, _listview.columns.find((x) => x.id == (_opt.catgcol || 'category')), '')); + setTimeout(Listview.headerFilter.bind(_listview, _listview.columns.find((x) => x.id == (_opt.catgcol || 'category')), _opt.subname(_subcategory)), 1); + setTimeout(Listview.headerFilter.bind(_excluded, _excluded.columns.find((x) => x.id == (_opt.catgcol || 'category')), _opt.subname(_subcategory)), 1); } } } diff --git a/static/js/locale_dede.js b/static/js/locale_dede.js index 12724635..67e6393b 100644 --- a/static/js/locale_dede.js +++ b/static/js/locale_dede.js @@ -4899,6 +4899,12 @@ var LANG = { /* AoWoW: start custom */ + // Profiler completions import + completion: 'Vervollständigung', // WH.TERMS.completion + complete: 'Vollständig', // WH.TERMS.complete + incomplete: 'Unvollständig', // WH.TERMS.incomplete + parens_format: '$1 ($2)', // WH.TERMS.parens_format + // click to copy fn copied: 'Kopiert', clickToCopy: 'Klicke zum Kopieren', diff --git a/static/js/locale_enus.js b/static/js/locale_enus.js index 65eac3b5..1255bfce 100644 --- a/static/js/locale_enus.js +++ b/static/js/locale_enus.js @@ -4947,6 +4947,12 @@ var LANG = { /* AoWoW: start custom */ + // Profiler completions import + completion: 'Completion', // WH.TERMS.completion + complete: 'Complete', // WH.TERMS.complete + incomplete: 'Incomplete', // WH.TERMS.incomplete + parens_format: '$1 ($2)', // WH.TERMS.parens_format + // click to copy fn copied: 'Copied', clickToCopy: 'Click to Copy', diff --git a/static/js/locale_eses.js b/static/js/locale_eses.js index 5ddeb9ce..a2ea7de4 100644 --- a/static/js/locale_eses.js +++ b/static/js/locale_eses.js @@ -4901,6 +4901,12 @@ var LANG = { /* AoWoW: start custom */ + // Profiler completions import + completion: 'Terminación', // WH.TERMS.completion + complete: 'Completo', // WH.TERMS.complete + incomplete: 'Incompleto', // WH.TERMS.incomplete + parens_format: '$1 ($2)', // WH.TERMS.parens_format + // click to copy fn copied: 'Copiado', clickToCopy: 'Click para copiar', diff --git a/static/js/locale_frfr.js b/static/js/locale_frfr.js index 234acbc0..37edaeb0 100644 --- a/static/js/locale_frfr.js +++ b/static/js/locale_frfr.js @@ -4901,6 +4901,12 @@ var LANG = { /* AoWoW: start custom */ + // Profiler completions import + completion: 'Achèvement', // WH.TERMS.completion + complete: 'Complète', // WH.TERMS.complete + incomplete: 'Incomplet', // WH.TERMS.incomplete + parens_format: '$1 ($2)', // WH.TERMS.parens_format + // click to copy fn copied: 'Copié', clickToCopy: 'Cliquer pour Copier', diff --git a/static/js/locale_ruru.js b/static/js/locale_ruru.js index a88b356f..a3f5c265 100644 --- a/static/js/locale_ruru.js +++ b/static/js/locale_ruru.js @@ -4903,6 +4903,12 @@ var LANG = { /* AoWoW: start custom */ + // Profiler completions import + completion: 'Завершено', // WH.TERMS.completion + complete: 'Завершено', // WH.TERMS.complete + incomplete: 'Не завершено', // WH.TERMS.incomplete + parens_format: '$1 ($2)', // WH.TERMS.parens_format + // click to copy fn copied: 'Скопировано', clickToCopy: 'Нажмите, чтобы скопировать', diff --git a/static/js/locale_zhcn.js b/static/js/locale_zhcn.js index 54f20940..065eb7e4 100644 --- a/static/js/locale_zhcn.js +++ b/static/js/locale_zhcn.js @@ -4927,6 +4927,12 @@ var LANG = { /* AoWoW: start custom */ + // Profiler completions import + completion: '达成', // WH.TERMS.completion + complete: '完成', // WH.TERMS.complete + incomplete: '未完成', // WH.TERMS.incomplete + parens_format: '$1($2)', // WH.TERMS.parens_format + // click to copy fn copied: '已复制', clickToCopy: '点击复制', diff --git a/template/bricks/infobox.tpl.php b/template/bricks/infobox.tpl.php index d9ca62ed..60252209 100644 --- a/template/bricks/infobox.tpl.php +++ b/template/bricks/infobox.tpl.php @@ -51,6 +51,7 @@ echo " \n"; ?> + Date: Mon, 3 Nov 2025 23:51:49 +0100 Subject: [PATCH 092/260] Filter/Fixup * try to prune deselected criteria/weight selectors from filter input --- includes/components/filter.class.php | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/includes/components/filter.class.php b/includes/components/filter.class.php index 13a45cdd..d6fc0162 100644 --- a/includes/components/filter.class.php +++ b/includes/components/filter.class.php @@ -294,14 +294,17 @@ abstract class Filter private function initFields() : void { - // quirk: in the POST step criteria will be [[''], null, null] if no criteria are selected, - // due to the first criteria selector always being visible - if (($this->rawData['cr'] ?? null) === [''] && !isset($this->rawData['crs']) && !isset($this->rawData['crv'])) - unset($this->rawData['cr']); // unset or Filter::checkInput() screams bloody error + /* quirks: + * - in the POST step there may be excess criteria selectors with a value of '', as unselecting a criteria that is not the last will not remove the row from the UI + * - if there are no criteria selected, the placeholder selection will always be sent as ['', null, null], similar to the previous quirk + * + * same for stat weights on ItemListFilter + */ + if (!empty($this->rawData['cr'])) + $this->rawData['cr'] = array_filter($this->rawData['cr'], fn($x) => $x !== '') ?: null; - // same for stat weights on ItemListFilter - if ($this instanceof ItemListFilter && ($this->rawData['wt'] ?? null) === [''] && !isset($this->rawData['wtv'])) - unset($this->rawData['wt']); + if (!empty($this->rawData['wt'])) + $this->rawData['wt'] = array_filter($this->rawData['wt'], fn($x) => $x !== '') ?: null; $cleanupCr = []; foreach (static::$inputFields as $inp => [$type, $valid, $asArray]) From 597898450d2a1a638eb64f990f9bc7743324bc69 Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Tue, 4 Nov 2025 02:01:01 +0100 Subject: [PATCH 093/260] WorldEvent/Misc * fix excess colons in tooltip * fix advancing date window while event is still active --- includes/dbtypes/worldevent.class.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/includes/dbtypes/worldevent.class.php b/includes/dbtypes/worldevent.class.php index 831b57c3..c1a930b3 100644 --- a/includes/dbtypes/worldevent.class.php +++ b/includes/dbtypes/worldevent.class.php @@ -91,7 +91,7 @@ class WorldEventList extends DBTypeList if ($rec < 0 || $date['lastDate'] < time()) return true; - $nIntervals = ceil((time() - $start) / $rec); + $nIntervals = (int)ceil((time() - $end) / $rec); $start += $nIntervals * $rec; $end += $nIntervals * $rec; @@ -157,9 +157,9 @@ class WorldEventList extends DBTypeList // use string-placeholder for dates // start - $x .= Lang::event('start').Lang::main('colon').'%s
    '; + $x .= Lang::event('start').'%s
    '; // end - $x .= Lang::event('end').Lang::main('colon').'%s'; + $x .= Lang::event('end').'%s'; $x .= '
    '; From 9020e36db65e168d61b711078e58645247d04129 Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Tue, 4 Nov 2025 19:36:49 +0100 Subject: [PATCH 094/260] SmartAI/Misc * make errors more verbose if SAI tries to set unexpected flags * do not escape strings. By now thats handled by Frontend/Markup --- includes/components/SmartAI/SmartAI.class.php | 44 +++++++++++++++++++ .../components/SmartAI/SmartAction.class.php | 4 +- .../components/SmartAI/SmartEvent.class.php | 9 +++- includes/defines.php | 8 +++- localization/locale_dede.php | 17 +++---- localization/locale_enus.php | 17 +++---- localization/locale_eses.php | 17 +++---- localization/locale_frfr.php | 17 +++---- localization/locale_ruru.php | 17 +++---- localization/locale_zhcn.php | 17 +++---- 10 files changed, 115 insertions(+), 52 deletions(-) diff --git a/includes/components/SmartAI/SmartAI.class.php b/includes/components/SmartAI/SmartAI.class.php index a9307160..07f25a11 100644 --- a/includes/components/SmartAI/SmartAI.class.php +++ b/includes/components/SmartAI/SmartAI.class.php @@ -37,6 +37,12 @@ trait SmartHelper private function castFlags(int $flags) : string { + if ($x = ($flags & ~SmartAI::CAST_FLAG_VALIDATE)) + { + trigger_error('SmartAI::castFlags - unknown SmartCastFlags '.Util::asBin($x).' set on id #'.$this->id, E_USER_NOTICE); + $flags &= SmartAI::CAST_FLAG_VALIDATE; + } + $cf = []; for ($i = 1; $i <= SmartAI::CAST_FLAG_COMBAT_MOVE; $i <<= 1) if (($flags & $i) && ($x = Lang::smartAI('castFlags', $i))) @@ -47,6 +53,12 @@ trait SmartHelper private function npcFlags(int $flags) : string { + if ($x = ($flags & ~NPC_FLAG_VALIDATE)) + { + trigger_error('SmartAI::npcFlags - unknown NpcFlags '.Util::asBin($x).' set on id #'.$this->id, E_USER_NOTICE); + $flags &= NPC_FLAG_VALIDATE; + } + $nf = []; for ($i = 1; $i <= NPC_FLAG_MAILBOX; $i <<= 1) if (($flags & $i) && ($x = Lang::npc('npcFlags', $i))) @@ -57,6 +69,12 @@ trait SmartHelper private function dynFlags(int $flags) : string { + if ($x = ($flags & ~UNIT_DYNFLAG_VALIDATE)) + { + trigger_error('SmartAI::dynFlags - unknown unit dynFlags '.Util::asBin($x).' set on id #'.$this->id, E_USER_NOTICE); + $flags &= UNIT_DYNFLAG_VALIDATE; + } + $df = []; for ($i = 1; $i <= UNIT_DYNFLAG_TAPPED_BY_ALL_THREAT_LIST; $i <<= 1) if (($flags & $i) && ($x = Lang::unit('dynFlags', $i))) @@ -67,6 +85,12 @@ trait SmartHelper private function goFlags(int $flags) : string { + if ($x = ($flags & ~GO_FLAG_VALIDATE)) + { + trigger_error('SmartAI::goFlags - unknown GameobjectFlags '.Util::asBin($x).' set on id #'.$this->id, E_USER_NOTICE); + $flags &= GO_FLAG_VALIDATE; + } + $gf = []; for ($i = 1; $i <= GO_FLAG_DESTROYED; $i <<= 1) if (($flags & $i) && ($x = Lang::gameObject('goFlags', $i))) @@ -77,6 +101,12 @@ trait SmartHelper private function spawnFlags(int $flags) : string { + if ($x = ($flags & ~SmartAI::SPAWN_FLAG_VALIDATE)) + { + trigger_error('SmartAI::spawnFlags - unknown SmartSpawnFlags '.Util::asBin($x).' set on id #'.$this->id, E_USER_NOTICE); + $flags &= SmartAI::SPAWN_FLAG_VALIDATE; + } + $sf = []; for ($i = 1; $i <= SmartAI::SPAWN_FLAG_NOSAVE_RESPAWN; $i <<= 1) if (($flags & $i) && ($x = Lang::smartAI('spawnFlags', $i))) @@ -87,6 +117,18 @@ trait SmartHelper private function unitFlags(int $flags, int $flags2) : string { + if ($x = ($flags & ~UNIT_FLAG_VALIDATE)) + { + trigger_error('SmartAI::unitFlags - unknown UnitFlags '.Util::asBin($x).' set on id #'.$this->id, E_USER_NOTICE); + $flags &= UNIT_FLAG_VALIDATE; + } + + if ($x = ($flags2 & ~UNIT_FLAG2_VALIDATE)) + { + trigger_error('SmartAI::unitFlags - unknown UnitFlags2 '.Util::asBin($x).' set on id #'.$this->id, E_USER_NOTICE); + $flags2 &= UNIT_FLAG2_VALIDATE; + } + $field = $flags2 ? 'flags2' : 'flags'; $max = $flags2 ? UNIT_FLAG2_ALLOW_CHEAT_SPELLS : UNIT_FLAG_UNK_31; $uf = []; @@ -181,6 +223,7 @@ class SmartAI // public const CAST_FORCE_TARGET_SELF = 0x10; // the target to cast this spell on itself public const CAST_FLAG_AURA_MISSING = 0x20; // Only casts the spell if the target does not have an aura from the spell public const CAST_FLAG_COMBAT_MOVE = 0x40; // Prevents combat movement if cast successful. Allows movement on range, OOM, LOS + public const CAST_FLAG_VALIDATE = self::CAST_FLAG_INTERRUPT_PREV | self::CAST_FLAG_TRIGGERED | self::CAST_FLAG_AURA_MISSING | self::CAST_FLAG_COMBAT_MOVE; public const REACT_PASSIVE = 0; public const REACT_DEFENSIVE = 1; @@ -207,6 +250,7 @@ class SmartAI public const SPAWN_FLAG_IGNORE_RESPAWN = 0x01; // onSpawnIn - ignore & reset respawn timer public const SPAWN_FLAG_FORCE_SPAWN = 0x02; // onSpawnIn - force additional spawn if already in world public const SPAWN_FLAG_NOSAVE_RESPAWN = 0x04; // onDespawn - remove respawn time + public const SPAWN_FLAG_VALIDATE = self::SPAWN_FLAG_IGNORE_RESPAWN | self::SPAWN_FLAG_FORCE_SPAWN | self::SPAWN_FLAG_NOSAVE_RESPAWN; private array $jsGlobals = []; private array $rawData = []; diff --git a/includes/components/SmartAI/SmartAction.class.php b/includes/components/SmartAI/SmartAction.class.php index c80c8b7f..924c8358 100644 --- a/includes/components/SmartAI/SmartAction.class.php +++ b/includes/components/SmartAI/SmartAction.class.php @@ -467,8 +467,8 @@ class SmartAction 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')); + $this->param[10] = Util::localizedString($nodes, 'start'); + $this->param[11] = Util::localizedString($nodes, 'end'); break; case self::ACTION_SET_INGAME_PHASE_MASK: // 44 -> any target if ($this->param[0]) diff --git a/includes/components/SmartAI/SmartEvent.class.php b/includes/components/SmartAI/SmartEvent.class.php index a9f1f3f8..726ac3a6 100644 --- a/includes/components/SmartAI/SmartEvent.class.php +++ b/includes/components/SmartAI/SmartEvent.class.php @@ -112,6 +112,7 @@ class SmartEvent public const FLAG_NO_RESET = 0x0100; public const FLAG_WHILE_CHARMED = 0x0200; public const FLAG_ALL_DIFFICULTIES = self::FLAG_DIFFICULTY_0 | self::FLAG_DIFFICULTY_1 | self::FLAG_DIFFICULTY_2 | self::FLAG_DIFFICULTY_3; + public const FLAG_VALIDATE = self::FLAG_NO_REPEAT | self::FLAG_DEBUG_ONLY | self::FLAG_NO_RESET | self::FLAG_WHILE_CHARMED | self::FLAG_ALL_DIFFICULTIES; private const EVENT_CELL_TPL = '[tooltip name=e-#rowIdx#]%1$s[/tooltip][span tooltip=e-#rowIdx#]%2$s[/span]'; @@ -304,7 +305,7 @@ class SmartEvent ); if ($gmo) - $this->param[10] = Util::jsEscape(Util::localizedString($gmo, 'text')); + $this->param[10] = Util::localizedString($gmo, 'text'); else trigger_error('SmartAI::event - could not find gossip menu option for event #'.$this->type); break; @@ -374,6 +375,12 @@ class SmartEvent { $flags = $this->flags; + if ($x = ($flags & ~self::FLAG_VALIDATE)) + { + trigger_error('SmartEvent::formatFlags - unused SmartEventFlags '.Util::asBin($x).' set on id #'.$this->id, E_USER_NOTICE); + $flags &= self::FLAG_VALIDATE; + } + if (($flags & self::FLAG_ALL_DIFFICULTIES) == self::FLAG_ALL_DIFFICULTIES) $flags &= ~self::FLAG_ALL_DIFFICULTIES; diff --git a/includes/defines.php b/includes/defines.php index 03cf0fce..b6eda11c 100644 --- a/includes/defines.php +++ b/includes/defines.php @@ -681,6 +681,7 @@ define('NPC_FLAG_STABLE_MASTER', 0x00400000); define('NPC_FLAG_GUILD_BANK', 0x00800000); define('NPC_FLAG_SPELLCLICK', 0x01000000); define('NPC_FLAG_MAILBOX', 0x04000000); +define('NPC_FLAG_VALIDATE', 0x05FFFFF3); define('CREATURE_FLAG_EXTRA_INSTANCE_BIND', 0x00000001); // creature kill binds instance to killer and killer's group define('CREATURE_FLAG_EXTRA_CIVILIAN', 0x00000002); // creature does not aggro (ignore faction/reputation hostility) @@ -740,6 +741,7 @@ define('UNIT_FLAG_UNK_28', 0x10000000); // (PreventKneelingW define('UNIT_FLAG_UNK_29', 0x20000000); // Used in Feign Death spell or NPC will play dead. (PreventEmotes) define('UNIT_FLAG_SHEATHE', 0x40000000); // define('UNIT_FLAG_UNK_31', 0x80000000); // +define('UNIT_FLAG_VALIDATE', 0x7FFFFFFF); // define('UNIT_FLAG2_FEIGN_DEATH', 0x00000001); // define('UNIT_FLAG2_UNK1', 0x00000002); // Hide unit model (show only player equip) @@ -759,6 +761,7 @@ define('UNIT_FLAG2_DISABLE_TURN', 0x00008000); // define('UNIT_FLAG2_UNK2', 0x00010000); // define('UNIT_FLAG2_PLAY_DEATH_ANIM', 0x00020000); // Plays special death animation upon death define('UNIT_FLAG2_ALLOW_CHEAT_SPELLS', 0x00040000); // allows casting spells with AttributesEx7 & SPELL_ATTR7_IS_CHEAT_SPELL +define('UNIT_FLAG2_VALIDATE', 0x0006FDFF); // // UNIT_FIELD_BYTES_1 - idx 0 (UnitStandStateType) define('UNIT_STAND_STATE_STAND', 0); @@ -794,6 +797,7 @@ define('UNIT_DYNFLAG_SPECIALINFO', 0x10); // define('UNIT_DYNFLAG_DEAD', 0x20); // Makes the creature appear dead (this DOES NOT make the creature's name grey or not attack players). define('UNIT_DYNFLAG_REFER_A_FRIEND', 0x40); // define('UNIT_DYNFLAG_TAPPED_BY_ALL_THREAT_LIST', 0x80); // Lua_UnitIsTappedByAllThreatList +define('UNIT_DYNFLAG_VALIDATE', 0xFF); // define('PET_TALENT_TYPE_FEROCITY', 0); define('PET_TALENT_TYPE_TENACITY', 1); @@ -863,9 +867,11 @@ define('GO_FLAG_INTERACT_COND', 0x0004); // Untargetable, can define('GO_FLAG_TRANSPORT', 0x0008); // Gameobject can transport (boat, elevator, car) define('GO_FLAG_NOT_SELECTABLE', 0x0010); // Not selectable (Not even in GM-mode) define('GO_FLAG_NODESPAWN', 0x0020); // Never despawns. Typical for gameobjects with on/off state (doors for example) -define('GO_FLAG_TRIGGERED', 0x0040); // typically, summoned objects. Triggered by spell or other events +define('GO_FLAG_AI_OBSTACLE', 0x0040); // makes the client register the object in something called AIObstacleMgr, unknown what it does +define('GO_FLAG_FREEZE_ANIMATION',0x0080); // define('GO_FLAG_DAMAGED', 0x0200); // Gameobject has been siege damaged define('GO_FLAG_DESTROYED', 0x0400); // Gameobject has been destroyed +define('GO_FLAG_VALIDATE', 0x06FF); // define('GO_STATE_ACTIVE', 0); // show in world as used and not reset (closed door open) define('GO_STATE_READY', 1); // show in world as ready (closed door close) diff --git a/localization/locale_dede.php b/localization/locale_dede.php index 2cc5dcb6..d55c252a 100644 --- a/localization/locale_dede.php +++ b/localization/locale_dede.php @@ -1139,14 +1139,15 @@ $lang = array( 'foundIn' => "Dieses Objekt befindet sich in", 'restock' => "Wird alle %s wieder aufgefüllt.", 'goFlags' => array( - GO_FLAG_IN_USE => 'In Benutzung', - GO_FLAG_LOCKED => 'Verschlossen', - GO_FLAG_INTERACT_COND => 'Nicht interagierbar', - GO_FLAG_TRANSPORT => 'Transporter', - GO_FLAG_NOT_SELECTABLE => 'Nicht anwählbar', - GO_FLAG_TRIGGERED => 'Ausgelöst', - GO_FLAG_DAMAGED => 'Belagerung beschädigt', - GO_FLAG_DESTROYED => 'Belagerung zerstört' + GO_FLAG_IN_USE => 'In Benutzung', + GO_FLAG_LOCKED => 'Verschlossen', + GO_FLAG_INTERACT_COND => 'Nicht interagierbar', + GO_FLAG_TRANSPORT => 'Transporter', + GO_FLAG_NOT_SELECTABLE => 'Nicht anwählbar', + GO_FLAG_AI_OBSTACLE => 'Ausgelöst', + GO_FLAG_FREEZE_ANIMATION => 'Pausiert Animation', + GO_FLAG_DAMAGED => 'Belagerung beschädigt', + GO_FLAG_DESTROYED => 'Belagerung zerstört' ), 'actions' => array( "None", "Animate Custom 0", "Animate Custom 1", "Animate Custom 2", "Animate Custom 3", diff --git a/localization/locale_enus.php b/localization/locale_enus.php index e15fae40..d306d84a 100644 --- a/localization/locale_enus.php +++ b/localization/locale_enus.php @@ -1139,14 +1139,15 @@ $lang = array( 'foundIn' => "This object can be found in", 'restock' => "Restocks every %s.", 'goFlags' => array( - GO_FLAG_IN_USE => 'In use', - GO_FLAG_LOCKED => 'Locked', - GO_FLAG_INTERACT_COND => 'Cannot interact', - GO_FLAG_TRANSPORT => 'Transport', - GO_FLAG_NOT_SELECTABLE => 'Not selectable', - GO_FLAG_TRIGGERED => 'Triggered', - GO_FLAG_DAMAGED => 'Siege damaged', - GO_FLAG_DESTROYED => 'Siege destroyed' + GO_FLAG_IN_USE => 'In use', + GO_FLAG_LOCKED => 'Locked', + GO_FLAG_INTERACT_COND => 'Cannot interact', + GO_FLAG_TRANSPORT => 'Transport', + GO_FLAG_NOT_SELECTABLE => 'Not selectable', + GO_FLAG_AI_OBSTACLE => 'Triggered', + GO_FLAG_FREEZE_ANIMATION => 'Freeze Animation', + GO_FLAG_DAMAGED => 'Siege damaged', + GO_FLAG_DESTROYED => 'Siege destroyed' ), 'actions' => array( "None", "Animate Custom 0", "Animate Custom 1", "Animate Custom 2", "Animate Custom 3", diff --git a/localization/locale_eses.php b/localization/locale_eses.php index a8b20675..e78c918e 100644 --- a/localization/locale_eses.php +++ b/localization/locale_eses.php @@ -1139,14 +1139,15 @@ $lang = array( 'foundIn' => "Esta entidad se puede encontrar en", 'restock' => "Se renueva cada %s.", 'goFlags' => array( - GO_FLAG_IN_USE => 'En uso', - GO_FLAG_LOCKED => 'Bloqueado', - GO_FLAG_INTERACT_COND => 'No se puede interactuar', - GO_FLAG_TRANSPORT => 'Transporte', - GO_FLAG_NOT_SELECTABLE => 'No seleccionable', - GO_FLAG_TRIGGERED => 'Activado', - GO_FLAG_DAMAGED => 'Dañado por asedio', - GO_FLAG_DESTROYED => 'Destruido por asedio' + GO_FLAG_IN_USE => 'En uso', + GO_FLAG_LOCKED => 'Bloqueado', + GO_FLAG_INTERACT_COND => 'No se puede interactuar', + GO_FLAG_TRANSPORT => 'Transporte', + GO_FLAG_NOT_SELECTABLE => 'No seleccionable', + GO_FLAG_AI_OBSTACLE => 'Activado', + GO_FLAG_FREEZE_ANIMATION => 'Congelar animación', + GO_FLAG_DAMAGED => 'Dañado por asedio', + GO_FLAG_DESTROYED => 'Destruido por asedio' ), 'actions' => array( "Ninguno", "Animar personalizado 0", "Animar personalizado 1", "Animar personalizado 2", "Animar personalizado 3", diff --git a/localization/locale_frfr.php b/localization/locale_frfr.php index 26b363f9..52b16a3b 100644 --- a/localization/locale_frfr.php +++ b/localization/locale_frfr.php @@ -1139,14 +1139,15 @@ $lang = array( 'foundIn' => "Cette entité se trouve dans", 'restock' => "Se remplit toutes les %s.", 'goFlags' => array( - GO_FLAG_IN_USE => 'In use', - GO_FLAG_LOCKED => 'Locked', - GO_FLAG_INTERACT_COND => 'Cannot interact', - GO_FLAG_TRANSPORT => 'Transport', - GO_FLAG_NOT_SELECTABLE => 'Not selectable', - GO_FLAG_TRIGGERED => 'Triggered', - GO_FLAG_DAMAGED => 'Siege damaged', - GO_FLAG_DESTROYED => 'Siege destroyed' + GO_FLAG_IN_USE => 'In use', + GO_FLAG_LOCKED => 'Locked', + GO_FLAG_INTERACT_COND => 'Cannot interact', + GO_FLAG_TRANSPORT => 'Transport', + GO_FLAG_NOT_SELECTABLE => 'Not selectable', + GO_FLAG_AI_OBSTACLE => 'Triggered', + GO_FLAG_FREEZE_ANIMATION => 'Freeze Animation', + GO_FLAG_DAMAGED => 'Siege damaged', + GO_FLAG_DESTROYED => 'Siege destroyed' ), 'actions' => array( "None", "Animate Custom 0", "Animate Custom 1", "Animate Custom 2", "Animate Custom 3", diff --git a/localization/locale_ruru.php b/localization/locale_ruru.php index c9bf049f..51b53869 100644 --- a/localization/locale_ruru.php +++ b/localization/locale_ruru.php @@ -1139,14 +1139,15 @@ $lang = array( 'foundIn' => "Этот НИП может быть найден в следующих зонах:", 'restock' => "[Restocks every %s.]", 'goFlags' => array( - GO_FLAG_IN_USE => 'In use', - GO_FLAG_LOCKED => 'Locked', - GO_FLAG_INTERACT_COND => 'Cannot interact', - GO_FLAG_TRANSPORT => 'Transport', - GO_FLAG_NOT_SELECTABLE => 'Not selectable', - GO_FLAG_TRIGGERED => 'Triggered', - GO_FLAG_DAMAGED => 'Siege damaged', - GO_FLAG_DESTROYED => 'Siege destroyed' + GO_FLAG_IN_USE => 'In use', + GO_FLAG_LOCKED => 'Locked', + GO_FLAG_INTERACT_COND => 'Cannot interact', + GO_FLAG_TRANSPORT => 'Transport', + GO_FLAG_NOT_SELECTABLE => 'Not selectable', + GO_FLAG_AI_OBSTACLE => 'Triggered', + GO_FLAG_FREEZE_ANIMATION => 'Freeze Animation', + GO_FLAG_DAMAGED => 'Siege damaged', + GO_FLAG_DESTROYED => 'Siege destroyed' ), 'actions' => array( "None", "Animate Custom 0", "Animate Custom 1", "Animate Custom 2", "Animate Custom 3", diff --git a/localization/locale_zhcn.php b/localization/locale_zhcn.php index 43b73336..e3eb62f3 100644 --- a/localization/locale_zhcn.php +++ b/localization/locale_zhcn.php @@ -1139,14 +1139,15 @@ $lang = array( 'foundIn' => "这个游戏对象可以在以下地区找到:", 'restock' => "每%s重新补给一次。", 'goFlags' => array( - GO_FLAG_IN_USE => '正在使用', - GO_FLAG_LOCKED => '已锁定', - GO_FLAG_INTERACT_COND => '无法交互', - GO_FLAG_TRANSPORT => '传送', - GO_FLAG_NOT_SELECTABLE => '不可选择', - GO_FLAG_TRIGGERED => '已触发', - GO_FLAG_DAMAGED => 'Siege damaged', - GO_FLAG_DESTROYED => 'Siege destroyed' + GO_FLAG_IN_USE => '正在使用', + GO_FLAG_LOCKED => '已锁定', + GO_FLAG_INTERACT_COND => '无法交互', + GO_FLAG_TRANSPORT => '传送', + GO_FLAG_NOT_SELECTABLE => '不可选择', + GO_FLAG_AI_OBSTACLE => '已触发', + GO_FLAG_FREEZE_ANIMATION => 'Freeze Animation', + GO_FLAG_DAMAGED => 'Siege damaged', + GO_FLAG_DESTROYED => 'Siege destroyed' ), 'actions' => array( "None", "Animate Custom 0", "Animate Custom 1", "Animate Custom 2", "Animate Custom 3", From 16c5b73cd3a6573171bc10bde8f83f43909f3f3b Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Wed, 5 Nov 2025 15:28:57 +0100 Subject: [PATCH 095/260] User/Misc * don't run querys if not strictly required (e.g. query for chars from ajax context) * prepare user globals and favorites so errors can be handled and don't destroy the template this also allows for profiling of the affected queries * add keys to items table to speed up querying for recipes in general and user completions in particular --- includes/components/pagetemplate.class.php | 5 + includes/user.class.php | 113 ++++++++++++--------- setup/sql/01-db_structure.sql | 4 +- setup/sql/02-db_initial_data.sql | 2 +- setup/sql/updates/1762352733_01.sql | 3 + template/bricks/head.tpl.php | 6 +- 6 files changed, 78 insertions(+), 55 deletions(-) create mode 100644 setup/sql/updates/1762352733_01.sql diff --git a/includes/components/pagetemplate.class.php b/includes/components/pagetemplate.class.php index ae216407..5bc172a8 100644 --- a/includes/components/pagetemplate.class.php +++ b/includes/components/pagetemplate.class.php @@ -30,6 +30,8 @@ class PageTemplate private string $gStaticUrl; private string $gHost; private string $gServerTime; + private string $gUser; + private string $gFavorites; private ?string $analyticsTag = null; private bool $consentFooter = false; private string $dbProfiles = ''; @@ -484,6 +486,9 @@ class PageTemplate // js + css $this->prepareScripts(); + $this->gUser = Util::toJSON(User::getUserGlobal()); + $this->gFavorites = Util::toJSON(User::getFavorites()); + // db profiling if (Cfg::get('DEBUG') >= LOG_LEVEL_INFO && User::isInGroup(U_GROUP_DEV | U_GROUP_ADMIN)) $this->dbProfiles = \Aowow\DB::getProfiles(); diff --git a/includes/user.class.php b/includes/user.class.php index 8ebd6261..e8028229 100644 --- a/includes/user.class.php +++ b/includes/user.class.php @@ -29,7 +29,28 @@ class User public static function init() { - self::setIP(); + # set ip # + + $ipAddr = ''; + foreach (['HTTP_CLIENT_IP', 'HTTP_X_FORWARDED_FOR', 'HTTP_X_FORWARDED', 'HTTP_FORWARDED_FOR', 'HTTP_FORWARDED', 'REMOTE_ADDR'] as $env) + { + if ($rawIp = getenv($env)) + { + if ($env == 'HTTP_X_FORWARDED') + $rawIp = explode(',', $rawIp)[0]; // [ip, proxy1, proxy2] + + if ($ipAddr = filter_var($rawIp, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) + break; + + if ($ipAddr = filter_var($rawIp, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) + break; + } + } + + self::$ip = $ipAddr ?: null; + + + # set locale # if (isset($_SESSION['locale']) && $_SESSION['locale'] instanceof Locale) self::$preferedLoc = $_SESSION['locale']->validate() ?? Locale::getFallback(); @@ -38,8 +59,10 @@ class User else self::$preferedLoc = Locale::getFallback(); - // session have a dataKey to access the JScripts (yes, also the anons) - if (empty($_SESSION['dataKey'])) + + # set basic data # + + if (empty($_SESSION['dataKey'])) // session have a dataKey to access the JScripts (yes, also the anons) $_SESSION['dataKey'] = Util::createHash(); // just some random numbers for identification purpose self::$dataKey = $_SESSION['dataKey']; @@ -48,7 +71,9 @@ class User if (!self::$ip) return false; - // check IP bans + + # check IP bans # + if ($ipBan = DB::Aowow()->selectRow('SELECT `count`, IF(`unbanDate` > UNIX_TIMESTAMP(), 1, 0) AS "active" FROM ?_account_bannedips WHERE `ip` = ? AND `type` = ?d', self::$ip, IP_BAN_TYPE_LOGIN_ATTEMPT)) { if ($ipBan['count'] > Cfg::get('ACC_FAILED_AUTH_COUNT') && $ipBan['active']) @@ -57,7 +82,9 @@ class User DB::Aowow()->query('DELETE FROM ?_account_bannedips WHERE `ip` = ?', self::$ip); } - // try to restore session + + # try to restore session # + if (empty($_SESSION['user'])) return false; @@ -122,16 +149,9 @@ class User self::$email = $userData['email']; self::$avatarborder = $userData['avatarborder']; - if (Cfg::get('PROFILER_ENABLE')) - { - $conditions = [['OR', ['user', self::$id], ['ap.accountId', self::$id]]]; - if (!self::isInGroup(U_GROUP_ADMIN | U_GROUP_BUREAU)) - $conditions[] = [['cuFlags', PROFILER_CU_DELETED, '&'], 0]; - self::$profiles = (new LocalProfileList($conditions)); - } + # reset premium options # - // reset premium options if (!self::isPremium()) { if ($userData['avatar'] == 2) @@ -144,17 +164,16 @@ class User // do not reset, it's just not sent to the browser } - // stuff, that updates on a daily basis goes here (if you keep you session alive indefinitly, the signin-handler doesn't do very much) - // - consecutive visits - // - votes per day - // - reputation for daily visit + + # update daily limits # + if (!self::isBanned()) { $lastLogin = DB::Aowow()->selectCell('SELECT `curLogin` FROM ?_account WHERE `id` = ?d', self::$id); // either the day changed or the last visit was >24h ago if (date('j', $lastLogin) != date('j') || (time() - $lastLogin) > 1 * DAY) { - // daily votes (we need to reset this one) + // - daily votes (we need to reset this one) self::$dailyVotes = self::getMaxDailyVotes(); DB::Aowow()->query( @@ -166,12 +185,11 @@ class User self::$id ); - // gain rep for daily visit + // - gain reputation for daily visit if (!(self::isBanned()) && !self::isInGroup(U_GROUP_PENDING)) Util::gainSiteReputation(self::$id, SITEREP_ACTION_DAILYVISIT); - // increment consecutive visits (next day or first of new month and not more than 48h) - // i bet my ass i forgot a corner case + // - increment consecutive visits (next day or first of new month and not more than 48h) if ((date('j', $lastLogin) + 1 == date('j') || (date('j') == 1 && date('n', $lastLogin) != date('n'))) && (time() - $lastLogin) < 2 * DAY) DB::Aowow()->query('UPDATE ?_account SET `consecutiveVisits` = `consecutiveVisits` + 1 WHERE `id` = ?d', self::$id); else @@ -182,31 +200,6 @@ class User return true; } - private static function setIP() : void - { - $ipAddr = ''; - $method = ['HTTP_CLIENT_IP', 'HTTP_X_FORWARDED_FOR', 'HTTP_X_FORWARDED', 'HTTP_FORWARDED_FOR', 'HTTP_FORWARDED', 'REMOTE_ADDR']; - - foreach ($method as $m) - { - if ($rawIp = getenv($m)) - { - if ($m == 'HTTP_X_FORWARDED') - $rawIp = explode(',', $rawIp)[0]; // [ip, proxy1, proxy2] - - // check IPv4 - if ($ipAddr = filter_var($rawIp, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) - break; - - // check IPv6 - if ($ipAddr = filter_var($rawIp, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) - break; - } - } - - self::$ip = $ipAddr ?: null; - } - public static function save(bool $toDB = false) { $_SESSION['user'] = self::$id; @@ -640,7 +633,7 @@ class User public static function getCharacters() : array { - if (!self::$profiles) + if (!self::loadProfiles()) return []; return self::$profiles->getJSGlobals(PROFILEINFO_CHARACTER); @@ -648,7 +641,7 @@ class User public static function getProfiles() : array { - if (!self::$profiles) + if (!self::loadProfiles()) return []; return self::$profiles->getJSGlobals(PROFILEINFO_PROFILE); @@ -656,7 +649,7 @@ class User public static function getPinnedCharacter() : array { - if (!self::$profiles) + if (!self::loadProfiles()) return []; $realms = Profiler::getRealms(); @@ -727,6 +720,9 @@ class User public static function getCompletion() : array { + if (!self::loadProfiles()) + return []; + $ids = []; foreach (self::$profiles->iterate() as $_) if (!self::$profiles->isCustom()) @@ -752,7 +748,7 @@ class User 'SELECT pcs.`id` AS ARRAY_KEY, pcs.`spellId` AS ARRAY_KEY2, pcs.`spellId`, i.`id` AS "itemId" FROM ?_spell s JOIN ?_profiler_completion_spells pcs ON s.`id` = pcs.`spellId` - LEFT JOIN ?_items i ON i.spellId1 IN (?a) AND i.spellId2 = pcs.spellId + LEFT JOIN ?_items i ON i.`spellId1` IN (?a) AND i.`spellId2` = pcs.`spellId` WHERE s.`typeCat` IN (?a) AND pcs.`id` IN (?a)', LEARN_SPELLS, [-5, -6, 9, 11], $ids ); @@ -776,6 +772,23 @@ class User return $completion; } + + private static function loadProfiles() : bool + { + if (!Cfg::get('PROFILER_ENABLE')) + return false; + + if (self::$profiles === null) + { + $conditions = [['OR', ['user', self::$id], ['ap.accountId', self::$id]]]; + if (!self::isInGroup(U_GROUP_ADMIN | U_GROUP_BUREAU)) + $conditions[] = [['cuFlags', PROFILER_CU_DELETED, '&'], 0]; + + self::$profiles = (new LocalProfileList($conditions)); + } + + return !!self::$profiles->getFoundIDs(); + } } ?> diff --git a/setup/sql/01-db_structure.sql b/setup/sql/01-db_structure.sql index dcd90ea8..d276bdfd 100644 --- a/setup/sql/01-db_structure.sql +++ b/setup/sql/01-db_structure.sql @@ -1441,7 +1441,9 @@ CREATE TABLE `aowow_items` ( KEY `items_index` (`class`), KEY `idx_model` (`displayId`), KEY `idx_faction` (`requiredFaction`), - KEY `iconId` (`iconId`) + KEY `iconId` (`iconId`), + KEY `spellId1` (`spellId1`), + KEY `spellId2` (`spellId2`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; diff --git a/setup/sql/02-db_initial_data.sql b/setup/sql/02-db_initial_data.sql index fb3e1d8a..f895ef4c 100644 --- a/setup/sql/02-db_initial_data.sql +++ b/setup/sql/02-db_initial_data.sql @@ -71,7 +71,7 @@ UNLOCK TABLES; LOCK TABLES `aowow_dbversion` WRITE; /*!40000 ALTER TABLE `aowow_dbversion` DISABLE KEYS */; -INSERT INTO `aowow_dbversion` VALUES (1758578400,17,NULL,NULL); +INSERT INTO `aowow_dbversion` VALUES (1762352734,0,NULL,NULL); /*!40000 ALTER TABLE `aowow_dbversion` ENABLE KEYS */; UNLOCK TABLES; diff --git a/setup/sql/updates/1762352733_01.sql b/setup/sql/updates/1762352733_01.sql new file mode 100644 index 00000000..203b6646 --- /dev/null +++ b/setup/sql/updates/1762352733_01.sql @@ -0,0 +1,3 @@ +ALTER TABLE `aowow_items` + ADD KEY spellId1 (`spellId1`), + ADD KEY spellId2 (`spellId2`); diff --git a/template/bricks/head.tpl.php b/template/bricks/head.tpl.php index 0306305b..1f20cec2 100644 --- a/template/bricks/head.tpl.php +++ b/template/bricks/head.tpl.php @@ -22,10 +22,10 @@ endif; renderArray('js', 4); ?> From f44de66de701b1775e0f8b017f2ff2c1603cd5f8 Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Wed, 5 Nov 2025 18:16:30 +0100 Subject: [PATCH 096/260] User/Profiles * speed up load of user profiles --- includes/dbtypes/profile.class.php | 2 +- includes/user.class.php | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/includes/dbtypes/profile.class.php b/includes/dbtypes/profile.class.php index cf75fcb1..fe109400 100644 --- a/includes/dbtypes/profile.class.php +++ b/includes/dbtypes/profile.class.php @@ -662,7 +662,7 @@ class LocalProfileList extends ProfileList protected string $queryBase = 'SELECT p.*, p.`id` AS ARRAY_KEY FROM ?_profiler_profiles p'; protected array $queryOpts = array( 'p' => [['g'], 'g' => 'p.`id`'], - 'ap' => ['j' => ['?_account_profiles ap ON ap.`profileId` = p.`id`', true], 's' => ', (IFNULL(ap.`ExtraFlags`, 0) | p.`cuFlags`) AS "cuFlags"'], + 'ap' => ['j' => ['?_account_profiles ap ON ap.`profileId` = p.`id`', true], 's' => ', (IFNULL(ap.`extraFlags`, 0) | p.`cuFlags`) AS "cuFlags"'], 'atm' => ['j' => ['?_profiler_arena_team_member atm ON atm.`profileId` = p.`id`', true], 's' => ', atm.`captain`, atm.`personalRating` AS "rating", atm.`seasonGames`, atm.`seasonWins`'], 'at' => [['atm'], 'j' => ['?_profiler_arena_team at ON at.`id` = atm.`arenaTeamId`', true], 's' => ', at.`type`'], 'g' => ['j' => ['?_profiler_guild g ON g.`id` = p.`guild`', true], 's' => ', g.`name` AS "guildname"'] diff --git a/includes/user.class.php b/includes/user.class.php index e8028229..a854b225 100644 --- a/includes/user.class.php +++ b/includes/user.class.php @@ -780,7 +780,10 @@ class User if (self::$profiles === null) { - $conditions = [['OR', ['user', self::$id], ['ap.accountId', self::$id]]]; + $ap = DB::Aowow()->selectCol('SELECT `profileId` FROM ?_account_profiles WHERE `accountId` = ?d', self::$id); + + // the old approach ['OR', ['user', self::$id], ['ap.accountId', self::$id]] caused keys to not get used + $conditions = $ap ? [['OR', ['user', self::$id], ['id', array_keys($ap)]]] : ['user', self::$id]; if (!self::isInGroup(U_GROUP_ADMIN | U_GROUP_BUREAU)) $conditions[] = [['cuFlags', PROFILER_CU_DELETED, '&'], 0]; From 5d02a20719eaf046927aaf2b1fc61a377a77c652 Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Fri, 7 Nov 2025 20:30:59 +0100 Subject: [PATCH 097/260] SQL/Misc * add keys to spells table to speed up related spells queries --- setup/sql/01-db_structure.sql | 19 ++++++++++++++++++- setup/sql/02-db_initial_data.sql | 2 +- setup/sql/updates/1762543652_01.sql | 18 ++++++++++++++++++ 3 files changed, 37 insertions(+), 2 deletions(-) create mode 100644 setup/sql/updates/1762543652_01.sql diff --git a/setup/sql/01-db_structure.sql b/setup/sql/01-db_structure.sql index d276bdfd..3434c71c 100644 --- a/setup/sql/01-db_structure.sql +++ b/setup/sql/01-db_structure.sql @@ -2737,7 +2737,24 @@ CREATE TABLE `aowow_spell` ( KEY `spell` (`id`) USING BTREE, KEY `effects` (`effect1Id`,`effect2Id`,`effect3Id`), KEY `items` (`effect1CreateItemId`,`effect2CreateItemId`,`effect3CreateItemId`), - KEY `iconId` (`iconId`) + KEY `iconId` (`iconId`), + KEY `reagent1` (`reagent1`), + KEY `reagent2` (`reagent2`), + KEY `reagent3` (`reagent3`), + KEY `reagent4` (`reagent4`), + KEY `reagent5` (`reagent5`), + KEY `reagent6` (`reagent6`), + KEY `reagent7` (`reagent7`), + KEY `reagent8` (`reagent8`), + KEY `effect1CreateItemId` (`effect1CreateItemId`), + KEY `effect2CreateItemId` (`effect2CreateItemId`), + KEY `effect3CreateItemId` (`effect3CreateItemId`), + KEY `effect1Id` (`effect1Id`), + KEY `effect2Id` (`effect2Id`), + KEY `effect3Id` (`effect3Id`), + KEY `effect1AuraId` (`effect1AuraId`), + KEY `effect2AuraId` (`effect2AuraId`), + KEY `effect3AuraId` (`effect3AuraId`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; diff --git a/setup/sql/02-db_initial_data.sql b/setup/sql/02-db_initial_data.sql index f895ef4c..df0fbe49 100644 --- a/setup/sql/02-db_initial_data.sql +++ b/setup/sql/02-db_initial_data.sql @@ -71,7 +71,7 @@ UNLOCK TABLES; LOCK TABLES `aowow_dbversion` WRITE; /*!40000 ALTER TABLE `aowow_dbversion` DISABLE KEYS */; -INSERT INTO `aowow_dbversion` VALUES (1762352734,0,NULL,NULL); +INSERT INTO `aowow_dbversion` VALUES (1762543653,0,NULL,NULL); /*!40000 ALTER TABLE `aowow_dbversion` ENABLE KEYS */; UNLOCK TABLES; diff --git a/setup/sql/updates/1762543652_01.sql b/setup/sql/updates/1762543652_01.sql new file mode 100644 index 00000000..2e3027b3 --- /dev/null +++ b/setup/sql/updates/1762543652_01.sql @@ -0,0 +1,18 @@ +ALTER TABLE `aowow_spell` + ADD KEY reagent1 (`reagent1`), + ADD KEY reagent2 (`reagent2`), + ADD KEY reagent3 (`reagent3`), + ADD KEY reagent4 (`reagent4`), + ADD KEY reagent5 (`reagent5`), + ADD KEY reagent6 (`reagent6`), + ADD KEY reagent7 (`reagent7`), + ADD KEY reagent8 (`reagent8`), + ADD KEY effect1CreateItemId (`effect1CreateItemId`), + ADD KEY effect2CreateItemId (`effect2CreateItemId`), + ADD KEY effect3CreateItemId (`effect3CreateItemId`), + ADD KEY effect1Id (`effect1Id`), + ADD KEY effect2Id (`effect2Id`), + ADD KEY effect3Id (`effect3Id`), + ADD KEY effect1AuraId (`effect1AuraId`), + ADD KEY effect2AuraId (`effect2AuraId`), + ADD KEY effect3AuraId (`effect3AuraId`); From edc297f97aa2c933c22b641773484b15a4e63748 Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Fri, 7 Nov 2025 20:46:09 +0100 Subject: [PATCH 098/260] NPCs/Fixup * fix parent npcs name for locales CN and ES --- includes/dbtypes/creature.class.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/dbtypes/creature.class.php b/includes/dbtypes/creature.class.php index bd92c9ec..5ee7a39b 100644 --- a/includes/dbtypes/creature.class.php +++ b/includes/dbtypes/creature.class.php @@ -16,7 +16,7 @@ class CreatureList extends DBTypeList protected string $queryBase = 'SELECT ct.*, ct.`id` AS ARRAY_KEY FROM ?_creature ct'; public array $queryOpts = array( - 'ct' => [['ft', 'qse', 'dct1', 'dct2', 'dct3'], 's' => ', IFNULL(dct1.`id`, IFNULL(dct2.`id`, IFNULL(dct3.`id`, 0))) AS "parentId", IFNULL(dct1.`name_loc0`, IFNULL(dct2.`name_loc0`, IFNULL(dct3.`name_loc0`, ""))) AS "parent_loc0", IFNULL(dct1.`name_loc2`, IFNULL(dct2.`name_loc2`, IFNULL(dct3.`name_loc2`, ""))) AS "parent_loc2", IFNULL(dct1.`name_loc3`, IFNULL(dct2.`name_loc3`, IFNULL(dct3.`name_loc3`, ""))) AS "parent_loc3", IFNULL(dct1.`name_loc4`, IFNULL(dct2.`name_loc4`, IFNULL(dct3.`name_loc4`, ""))) AS "`parent_loc4`", IFNULL(dct1.`name_loc6`, IFNULL(dct2.`name_loc6`, IFNULL(dct3.`name_loc6`, ""))) AS "`parent_loc6`", IFNULL(dct1.name_loc8, IFNULL(dct2.`name_loc8`, IFNULL(dct3.`name_loc8`, ""))) AS "parent_loc8", IF(dct1.`difficultyEntry1` = ct.`id`, 1, IF(dct2.`difficultyEntry2` = ct.`id`, 2, IF(dct3.`difficultyEntry3` = ct.`id`, 3, 0))) AS "difficultyMode"'], + 'ct' => [['ft', 'qse', 'dct1', 'dct2', 'dct3'], 's' => ', IFNULL(dct1.`id`, IFNULL(dct2.`id`, IFNULL(dct3.`id`, 0))) AS "parentId", IFNULL(dct1.`name_loc0`, IFNULL(dct2.`name_loc0`, IFNULL(dct3.`name_loc0`, ""))) AS "parent_loc0", IFNULL(dct1.`name_loc2`, IFNULL(dct2.`name_loc2`, IFNULL(dct3.`name_loc2`, ""))) AS "parent_loc2", IFNULL(dct1.`name_loc3`, IFNULL(dct2.`name_loc3`, IFNULL(dct3.`name_loc3`, ""))) AS "parent_loc3", IFNULL(dct1.`name_loc4`, IFNULL(dct2.`name_loc4`, IFNULL(dct3.`name_loc4`, ""))) AS "parent_loc4", IFNULL(dct1.`name_loc6`, IFNULL(dct2.`name_loc6`, IFNULL(dct3.`name_loc6`, ""))) AS "parent_loc6", IFNULL(dct1.name_loc8, IFNULL(dct2.`name_loc8`, IFNULL(dct3.`name_loc8`, ""))) AS "parent_loc8", IF(dct1.`difficultyEntry1` = ct.`id`, 1, IF(dct2.`difficultyEntry2` = ct.`id`, 2, IF(dct3.`difficultyEntry3` = ct.`id`, 3, 0))) AS "difficultyMode"'], 'dct1' => ['j' => ['?_creature dct1 ON ct.`cuFlags` & 0x02 AND dct1.`difficultyEntry1` = ct.`id`', true]], 'dct2' => ['j' => ['?_creature dct2 ON ct.`cuFlags` & 0x02 AND dct2.`difficultyEntry2` = ct.`id`', true]], 'dct3' => ['j' => ['?_creature dct3 ON ct.`cuFlags` & 0x02 AND dct3.`difficultyEntry3` = ct.`id`', true]], From 48564ab8b50ce844850c03a126cfad2ccfb1d2b6 Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Fri, 7 Nov 2025 21:19:43 +0100 Subject: [PATCH 099/260] Misc/Fixup * fix building num ranges, added in 8212811970e9b1c686797f297e929ed838c2b573 --- includes/utilities.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/utilities.php b/includes/utilities.php index e6e62dd5..97de57e8 100644 --- a/includes/utilities.php +++ b/includes/utilities.php @@ -563,7 +563,7 @@ abstract class Util $_min = $fn($min); $_max = $fn($max); - return $max > $min ? $_min . ($delim ?: Lang::main('valueDelim')) . $_max : $_min; + return $max > $min ? $_min . ($delim ?: Lang::game('valueDelim')) . $_max : $_min; } public static function validateLogin(?string $val) : string From cf4e8a527c4a6b6bd15a517531f7bfdeeaee51d6 Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Sat, 8 Nov 2025 18:07:08 +0100 Subject: [PATCH 100/260] User/Fixup * fix fetching user characters, borked in 474b5b5aec062b61e8d707c91739b50ad77e81ef --- includes/user.class.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/user.class.php b/includes/user.class.php index a854b225..7233b016 100644 --- a/includes/user.class.php +++ b/includes/user.class.php @@ -783,7 +783,7 @@ class User $ap = DB::Aowow()->selectCol('SELECT `profileId` FROM ?_account_profiles WHERE `accountId` = ?d', self::$id); // the old approach ['OR', ['user', self::$id], ['ap.accountId', self::$id]] caused keys to not get used - $conditions = $ap ? [['OR', ['user', self::$id], ['id', array_keys($ap)]]] : ['user', self::$id]; + $conditions = $ap ? [['OR', ['user', self::$id], ['id', $ap]]] : ['user', self::$id]; if (!self::isInGroup(U_GROUP_ADMIN | U_GROUP_BUREAU)) $conditions[] = [['cuFlags', PROFILER_CU_DELETED, '&'], 0]; From 8a169eb4000e483739176118012227092a828dd8 Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Sat, 8 Nov 2025 16:45:54 +0100 Subject: [PATCH 101/260] Setup/Fixup * catch error if url from self test is unreachable --- setup/tools/clisetup/siteconfig.us.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup/tools/clisetup/siteconfig.us.php b/setup/tools/clisetup/siteconfig.us.php index 405061b7..80090245 100644 --- a/setup/tools/clisetup/siteconfig.us.php +++ b/setup/tools/clisetup/siteconfig.us.php @@ -451,7 +451,7 @@ CLISetup::registerUtility(new class extends UtilityScript $res = get_headers($protocol.$host.$testFile, true, $ctx); - if (!preg_match('/HTTP\/[0-9\.]+\s+([0-9]+)/', $res[0], $m)) + if (!$res || !preg_match('/HTTP\/[0-9\.]+\s+([0-9]+)/', $res[0], $m)) return false; $status = $m[1]; From 6eb5a67add9891eda11802b9a1a37746c7943b5e Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Sat, 8 Nov 2025 21:06:03 +0100 Subject: [PATCH 102/260] Profiler/Optimization * move searchable flags to their own db cols to speed up lookups * don't cast profile name to LOWER in SQL when displaying tooltips. --- endpoints/achievement/achievement.php | 2 +- endpoints/arena-team/arena-team.php | 6 +++--- endpoints/faction/faction.php | 2 +- endpoints/guild/guild.php | 6 +++--- endpoints/profile/delete.php | 4 +--- endpoints/profile/link.php | 4 ++-- endpoints/profile/load.php | 8 ++++---- endpoints/profile/profile.php | 12 ++++++------ endpoints/profile/profile_power.php | 2 +- endpoints/profile/save.php | 7 ++++--- endpoints/quest/quest.php | 2 +- endpoints/spell/spell.php | 2 +- endpoints/title/title.php | 2 +- endpoints/user/user.php | 2 +- includes/components/profiler.class.php | 12 ++++++------ includes/dbtypes/arenateam.class.php | 2 +- includes/dbtypes/guild.class.php | 10 ++++------ includes/dbtypes/profile.class.php | 15 +++++++-------- includes/defines.php | 6 +++--- includes/user.class.php | 2 +- setup/sql/01-db_structure.sql | 14 ++++++++++++-- setup/sql/updates/1762629696_01.sql | 18 ++++++++++++++++++ setup/sql/updates/1762629696_02.sql | 10 ++++++++++ 23 files changed, 92 insertions(+), 58 deletions(-) create mode 100644 setup/sql/updates/1762629696_01.sql create mode 100644 setup/sql/updates/1762629696_02.sql diff --git a/endpoints/achievement/achievement.php b/endpoints/achievement/achievement.php index 1068fe42..07e5532e 100644 --- a/endpoints/achievement/achievement.php +++ b/endpoints/achievement/achievement.php @@ -121,7 +121,7 @@ class AchievementBaseResponse extends TemplateResponse implements ICache if (Cfg::get('PROFILER_ENABLE') && !($this->subject->getField('flags') & ACHIEVEMENT_FLAG_COUNTER)) { $x = DB::Aowow()->selectCell('SELECT COUNT(1) FROM ?_profiler_completion_achievements WHERE `achievementId` = ?d', $this->typeId); - $y = DB::Aowow()->selectCell('SELECT COUNT(1) FROM ?_profiler_profiles WHERE `realm` IS NOT NULL AND `realmGUID` IS NOT NULL AND `cuFlags` & ?d = 0', PROFILER_CU_NEEDS_RESYNC); + $y = DB::Aowow()->selectCell('SELECT COUNT(1) FROM ?_profiler_profiles WHERE `custom` = 0 AND `stub` = 0'); $infobox[] = Lang::profiler('attainedBy', [round(($x ?: 0) * 100 / ($y ?: 1))]); // completion row added by InfoboxMarkup diff --git a/endpoints/arena-team/arena-team.php b/endpoints/arena-team/arena-team.php index 2d50aa08..7e30a1c2 100644 --- a/endpoints/arena-team/arena-team.php +++ b/endpoints/arena-team/arena-team.php @@ -46,11 +46,11 @@ class ArenateamBaseResponse extends TemplateResponse // 3 possibilities // 1) already synced to aowow - if ($subject = DB::Aowow()->selectRow('SELECT `id`, `realmGUID`, `cuFlags` FROM ?_profiler_arena_team WHERE `realm` = ?d AND `nameUrl` = ?', $this->realmId, Profiler::urlize($this->subjectName))) + if ($subject = DB::Aowow()->selectRow('SELECT `id`, `realmGUID`, `stub` FROM ?_profiler_arena_team WHERE `realm` = ?d AND `nameUrl` = ?', $this->realmId, Profiler::urlize($this->subjectName))) { $this->typeId = $subject['id']; - if ($subject['cuFlags'] & PROFILER_CU_NEEDS_RESYNC) + if ($subject['stub']) $this->handleIncompleteData(Type::ARENA_TEAM, $subject['realmGUID']); return; @@ -62,7 +62,7 @@ class ArenateamBaseResponse extends TemplateResponse { $subject = $subject[0]; $subject['realm'] = $this->realmId; - $subject['cuFlags'] = PROFILER_CU_NEEDS_RESYNC; + $subject['stub'] = 1; $subject['nameUrl'] = Profiler::urlize($subject['name']); // create entry from realm with basic info diff --git a/endpoints/faction/faction.php b/endpoints/faction/faction.php index 2ca7ca92..a5bd4867 100644 --- a/endpoints/faction/faction.php +++ b/endpoints/faction/faction.php @@ -103,7 +103,7 @@ class FactionBaseResponse extends TemplateResponse implements ICache if (Cfg::get('PROFILER_ENABLE') && !($this->subject->getField('cuFlags') & CUSTOM_EXCLUDE_FOR_LISTVIEW)) { $x = DB::Aowow()->selectCell('SELECT COUNT(1) FROM ?_profiler_completion_reputation WHERE `standing` >= ?d AND `factionId` = ?d', 42000, $this->typeId); - $y = DB::Aowow()->selectCell('SELECT COUNT(1) FROM ?_profiler_profiles WHERE `realm` IS NOT NULL AND `realmGUID` IS NOT NULL AND `cuFlags` & ?d = 0', PROFILER_CU_NEEDS_RESYNC); + $y = DB::Aowow()->selectCell('SELECT COUNT(1) FROM ?_profiler_profiles WHERE `custom` = 0 AND `stub` = 0'); $infobox[] = Lang::profiler('attainedBy', [round(($x ?: 0) * 100 / ($y ?: 1))]); // completion row added by InfoboxMarkup diff --git a/endpoints/guild/guild.php b/endpoints/guild/guild.php index fa247e40..2cd0f3f6 100644 --- a/endpoints/guild/guild.php +++ b/endpoints/guild/guild.php @@ -46,11 +46,11 @@ class GuildBaseResponse extends TemplateResponse // 3 possibilities // 1) already synced to aowow - if ($subject = DB::Aowow()->selectRow('SELECT `id`, `realmGUID`, `cuFlags` FROM ?_profiler_guild WHERE `realm` = ?d AND `nameUrl` = ?', $this->realmId, Profiler::urlize($this->subjectName))) + if ($subject = DB::Aowow()->selectRow('SELECT `id`, `realmGUID`, `stub` FROM ?_profiler_guild WHERE `realm` = ?d AND `nameUrl` = ?', $this->realmId, Profiler::urlize($this->subjectName))) { $this->typeId = $subject['id']; - if ($subject['cuFlags'] & PROFILER_CU_NEEDS_RESYNC) + if ($subject['stub']) $this->handleIncompleteData(Type::GUILD, $subject['realmGUID']); return; @@ -62,7 +62,7 @@ class GuildBaseResponse extends TemplateResponse { $subject = $subject[0]; $subject['realm'] = $this->realmId; - $subject['cuFlags'] = PROFILER_CU_NEEDS_RESYNC; + $subject['stub'] = 1; $subject['nameUrl'] = Profiler::urlize($subject['name']); // create entry from realm with basic info diff --git a/endpoints/profile/delete.php b/endpoints/profile/delete.php index 0bf905cd..15a8e961 100644 --- a/endpoints/profile/delete.php +++ b/endpoints/profile/delete.php @@ -37,10 +37,8 @@ class ProfileDeleteResponse extends TextResponse // only flag as deleted; only custom profiles DB::Aowow()->query( - 'UPDATE ?_profiler_profiles SET `cuFlags` = `cuFlags` | ?d WHERE `id` IN (?a) AND `cuFlags` & ?d {AND `user` = ?d}', - PROFILER_CU_DELETED, + 'UPDATE ?_profiler_profiles SET `deleted` = 1 WHERE `id` IN (?a) AND `custom` = 1 {AND `user` = ?d}', $this->_get['id'], - PROFILER_CU_PROFILE, User::isInGroup(U_GROUP_ADMIN | U_GROUP_BUREAU) ? DBSIMPLE_SKIP : User::$id ); } diff --git a/endpoints/profile/link.php b/endpoints/profile/link.php index bcd32ab0..63a7434f 100644 --- a/endpoints/profile/link.php +++ b/endpoints/profile/link.php @@ -38,8 +38,8 @@ class ProfileLinkResponse extends TextResponse // only link characters, not custom profiles $newId = DB::Aowow()->query( 'REPLACE INTO ?_account_profiles (`accountId`, `profileId`, `extraFlags`) - SELECT ?d, p.`id`, 0 FROM ?_profiler_profiles p WHERE p.`id` = ?d AND (`cuFlags` & ?d) = 0', - User::$id, $this->_get['id'], PROFILER_CU_PROFILE + SELECT ?d, p.`id`, 0 FROM ?_profiler_profiles p WHERE p.`id` = ?d AND `custom` = 0', + User::$id, $this->_get['id'] ); if (!is_int($newId)) diff --git a/endpoints/profile/load.php b/endpoints/profile/load.php index 8be7a2da..32370ee1 100644 --- a/endpoints/profile/load.php +++ b/endpoints/profile/load.php @@ -47,7 +47,7 @@ class ProfileLoadResponse extends TextResponse return; } - if (($pBase['cuFlags'] & PROFILER_CU_DELETED) && !User::isInGroup(U_GROUP_ADMIN | U_GROUP_BUREAU)) + if ($pBase['deleted'] && !User::isInGroup(U_GROUP_ADMIN | U_GROUP_BUREAU)) return; @@ -56,7 +56,7 @@ class ProfileLoadResponse extends TextResponse if ($rId == $pBase['realm']) break; - if (!$rData) // realm doesn't exist or access is restricted + if ($pBase['realm'] && !$rData) // realm doesn't exist or access is restricted return; $profile = array( @@ -103,7 +103,7 @@ class ProfileLoadResponse extends TextResponse 'activity' => [], // recent raid activity [achievementId => 1] (is a subset of statistics) ); - if ($pBase['cuFlags'] & PROFILER_CU_PROFILE) + if ($pBase['custom']) { // this parameter is _really_ strange .. probably still not doing this right $profile['source'] = $pBase['realm'] ? $pBase['sourceId'] : 0; @@ -135,7 +135,7 @@ class ProfileLoadResponse extends TextResponse $profile['pets'] = $pets; // source for custom profiles; profileId => [name, ownerId, iconString(optional)] - if ($customs = DB::Aowow()->select('SELECT `id` AS ARRAY_KEY, `name`, `user`, `icon` FROM ?_profiler_profiles WHERE `sourceId` = ?d AND `sourceId` <> `id` {AND (`cuFlags` & ?d) = 0}', $pBase['id'], User::isInGroup(U_GROUP_STAFF) ? DBSIMPLE_SKIP : PROFILER_CU_DELETED)) + if ($customs = DB::Aowow()->select('SELECT `id` AS ARRAY_KEY, `name`, `user`, `icon` FROM ?_profiler_profiles WHERE `sourceId` = ?d AND `sourceId` <> `id` {AND `deleted` = ?d}', $pBase['id'], User::isInGroup(U_GROUP_STAFF) ? DBSIMPLE_SKIP : 0)) { foreach ($customs as $id => $cu) { diff --git a/endpoints/profile/profile.php b/endpoints/profile/profile.php index 39484228..e3db2365 100644 --- a/endpoints/profile/profile.php +++ b/endpoints/profile/profile.php @@ -67,11 +67,11 @@ class ProfileBaseResponse extends TemplateResponse // 3 possibilities // 1) already synced to aowow - if ($subject = DB::Aowow()->selectRow('SELECT `id`, `realmGUID`, `cuFlags` FROM ?_profiler_profiles WHERE `realm` = ?d AND `realmGUID` IS NOT NULL AND `name` = ? AND `renameItr` = ?d', $this->realmId, Util::ucFirst($this->subjectName), $rnItr)) + if ($subject = DB::Aowow()->selectRow('SELECT `id`, `realmGUID`, `stub` FROM ?_profiler_profiles WHERE `realm` = ?d AND `custom` = 0 AND `name` = ? AND `renameItr` = ?d', $this->realmId, Util::ucFirst($this->subjectName), $rnItr)) { $this->typeId = $subject['id']; - if ($subject['cuFlags'] & PROFILER_CU_NEEDS_RESYNC) + if ($subject['stub']) $this->handleIncompleteData(Type::PROFILE, $subject['realmGUID']); return; @@ -93,18 +93,18 @@ class ProfileBaseResponse extends TemplateResponse if ($subject = array_filter($subjects, fn($x) => Util::lower($x['name']) == Util::lower($this->subjectName))) { $subject = $subject[0]; - $subject['realm'] = $this->realmId; - $subject['cuFlags'] = PROFILER_CU_NEEDS_RESYNC; + $subject['realm'] = $this->realmId; + $subject['stub'] = 1; if ($subject['at_login'] & 0x1) - $subject['renameItr'] = DB::Aowow()->selectCell('SELECT MAX(`renameItr`) FROM ?_profiler_profiles WHERE `realm` = ?d AND `realmGUID` IS NOT NULL AND `name` = ?', $this->realmId, $subject['name']); + $subject['renameItr'] = DB::Aowow()->selectCell('SELECT MAX(`renameItr`) FROM ?_profiler_profiles WHERE `realm` = ?d AND `custom` = 0 AND `name` = ?', $this->realmId, $subject['name']); if ($subject['guildGUID']) { // create empty guild if necessary to satisfy foreign keys $subject['guild'] = DB::Aowow()->selectCell('SELECT `id` FROM ?_profiler_guild WHERE `realm` = ?d AND `realmGUID` = ?d', $this->realmId, $subject['guildGUID']); if (!$subject['guild']) - $subject['guild'] = DB::Aowow()->query('INSERT INTO ?_profiler_guild (`realm`, `realmGUID`, `cuFlags`, `name`, `nameUrl`) VALUES (?d, ?d, ?d, ?, ?)', $this->realmId, $subject['guildGUID'], PROFILER_CU_NEEDS_RESYNC, $subject['guildName'], Profiler::urlize($subject['guildName'])); + $subject['guild'] = DB::Aowow()->query('INSERT INTO ?_profiler_guild (`realm`, `realmGUID`, `stub`, `name`, `nameUrl`) VALUES (?d, ?d, 1, ?, ?)', $this->realmId, $subject['guildGUID'], $subject['guildName'], Profiler::urlize($subject['guildName'])); } unset($subject['guildGUID'], $subject['guildName'], $subject['at_login']); diff --git a/endpoints/profile/profile_power.php b/endpoints/profile/profile_power.php index ea15084a..a1a23844 100644 --- a/endpoints/profile/profile_power.php +++ b/endpoints/profile/profile_power.php @@ -38,7 +38,7 @@ class ProfilePowerResponse extends TextResponse implements ICache if (preg_match('/([^\-]+)-(\d+)/i', $this->subjectName, $m)) [, $this->subjectName, $renameItr] = $m; - if ($x = DB::Aowow()->selectCell('SELECT `id` FROM ?_profiler_profiles WHERE `realm` = ?d AND `realmGUID` IS NOT NULL AND LOWER(`name`) = ? AND `renameItr` = ?d', $this->realmId, $this->subjectName, $renameItr ?? 0)) + if ($x = DB::Aowow()->selectCell('SELECT `id` FROM ?_profiler_profiles WHERE `realm` = ?d AND `custom` = 0 AND `name` = ? AND `renameItr` = ?d', $this->realmId, Util::ucWords($this->subjectName), $renameItr ?? 0)) $this->typeId = $x; } diff --git a/endpoints/profile/save.php b/endpoints/profile/save.php index 0dd3eabc..51caca81 100644 --- a/endpoints/profile/save.php +++ b/endpoints/profile/save.php @@ -72,7 +72,8 @@ class ProfileSaveResponse extends TextResponse 'glyphs2' => $this->_post['glyphs2'], 'gearscore' => $this->_post['gearscore'], 'icon' => $this->_post['icon'], - 'cuFlags' => PROFILER_CU_PROFILE | ($this->_post['public'] ? PROFILER_CU_PUBLISHED : 0) + 'custom' => 1, + 'cuFlags' => $this->_post['public'] ? PROFILER_CU_PUBLISHED : 0 ); // remnant of a conflict between wotlk generic icons and cata+ auto-generated, char-based icons (see profile=avatar) @@ -88,7 +89,7 @@ class ProfileSaveResponse extends TextResponse if ($_ = $this->_post['copy']) // gets set to source profileId when "save as" is clicked. Whats the difference to 'source' though? { // get character origin info if possible - if ($r = DB::Aowow()->selectCell('SELECT `realm` FROM ?_profiler_profiles WHERE `id` = ?d AND `realm` IS NOT NULL', $_)) + if ($r = DB::Aowow()->selectCell('SELECT `realm` FROM ?_profiler_profiles WHERE `id` = ?d AND `custom` = 0', $_)) $cuProfile['realm'] = $r; $cuProfile['sourceId'] = $_; @@ -105,7 +106,7 @@ class ProfileSaveResponse extends TextResponse } else // new { - $nProfiles = DB::Aowow()->selectCell('SELECT COUNT(*) FROM ?_profiler_profiles WHERE `user` = ?d AND (`cuFlags` & ?d) = 0 AND `realmGUID` IS NULL', User::$id, PROFILER_CU_DELETED); + $nProfiles = DB::Aowow()->selectCell('SELECT COUNT(*) FROM ?_profiler_profiles WHERE `user` = ?d AND `deleted` = 0 AND `custom` = 1', User::$id); if ($nProfiles < 10 || User::isPremium()) if ($newId = DB::Aowow()->query('INSERT INTO ?_profiler_profiles (?#) VALUES (?a)', array_keys($cuProfile), array_values($cuProfile))) $charId = $newId; diff --git a/endpoints/quest/quest.php b/endpoints/quest/quest.php index 6830704d..10c56534 100644 --- a/endpoints/quest/quest.php +++ b/endpoints/quest/quest.php @@ -279,7 +279,7 @@ class QuestBaseResponse extends TemplateResponse implements ICache if (Cfg::get('PROFILER_ENABLE') && $hasCompletion) { $x = DB::Aowow()->selectCell('SELECT COUNT(1) FROM ?_profiler_completion_quests WHERE `questId` = ?d', $this->typeId); - $y = DB::Aowow()->selectCell('SELECT COUNT(1) FROM ?_profiler_profiles WHERE `realm` IS NOT NULL AND `realmGUID` IS NOT NULL AND `cuFlags` & ?d = 0', PROFILER_CU_NEEDS_RESYNC); + $y = DB::Aowow()->selectCell('SELECT COUNT(1) FROM ?_profiler_profiles WHERE `custom` = 0 AND `stub` = 0'); $infobox[] = Lang::profiler('attainedBy', [round(($x ?: 0) * 100 / ($y ?: 1))]); // completion row added by InfoboxMarkup diff --git a/endpoints/spell/spell.php b/endpoints/spell/spell.php index 5508beff..f473a698 100644 --- a/endpoints/spell/spell.php +++ b/endpoints/spell/spell.php @@ -2449,7 +2449,7 @@ class SpellBaseResponse extends TemplateResponse implements ICache if (Cfg::get('PROFILER_ENABLE') && $hasCompletion) { $x = DB::Aowow()->selectCell('SELECT COUNT(1) FROM ?_profiler_completion_spells WHERE `spellId` = ?d', $this->typeId); - $y = DB::Aowow()->selectCell('SELECT COUNT(1) FROM ?_profiler_profiles WHERE `realm` IS NOT NULL AND `realmGUID` IS NOT NULL AND `cuFlags` & ?d = 0', PROFILER_CU_NEEDS_RESYNC); + $y = DB::Aowow()->selectCell('SELECT COUNT(1) FROM ?_profiler_profiles WHERE `custom` = 0 AND `stub` = 0'); $infobox[] = Lang::profiler('attainedBy', [round(($x ?: 0) * 100 / ($y ?: 1))]); // completion row added by InfoboxMarkup diff --git a/endpoints/title/title.php b/endpoints/title/title.php index 1c935196..726c9dfe 100644 --- a/endpoints/title/title.php +++ b/endpoints/title/title.php @@ -91,7 +91,7 @@ class TitleBaseResponse extends TemplateResponse implements ICache if (Cfg::get('PROFILER_ENABLE')) { $x = DB::Aowow()->selectCell('SELECT COUNT(1) FROM ?_profiler_completion_titles WHERE `titleId` = ?d', $this->typeId); - $y = DB::Aowow()->selectCell('SELECT COUNT(1) FROM ?_profiler_profiles WHERE `realm` IS NOT NULL AND `realmGUID` IS NOT NULL AND `cuFlags` & ?d = 0', PROFILER_CU_NEEDS_RESYNC); + $y = DB::Aowow()->selectCell('SELECT COUNT(1) FROM ?_profiler_profiles WHERE `custom` = 0 AND `stub` = 0'); $infobox[] = Lang::profiler('attainedBy', [round(($x ?: 0) * 100 / ($y ?: 1))]); // completion row added by InfoboxMarkup diff --git a/endpoints/user/user.php b/endpoints/user/user.php index 72b87671..93b8c2af 100644 --- a/endpoints/user/user.php +++ b/endpoints/user/user.php @@ -235,7 +235,7 @@ class UserBaseResponse extends TemplateResponse { $conditions = array( ['OR', ['cuFlags', PROFILER_CU_PUBLISHED, '&'], ['ap.extraFlags', PROFILER_CU_PUBLISHED, '&']], - [['cuFlags', PROFILER_CU_DELETED, '&'], 0], + ['deleted', 0], ['OR', ['user', $this->user['id']], ['ap.accountId', $this->user['id']]] ); diff --git a/includes/components/profiler.class.php b/includes/components/profiler.class.php index bfaa1128..d7b1a621 100644 --- a/includes/components/profiler.class.php +++ b/includes/components/profiler.class.php @@ -503,7 +503,7 @@ class Profiler // char is flagged for rename if ($char['at_login'] & 0x1) { - $ri = DB::Aowow()->selectCell('SELECT MAX(`renameItr`) FROM ?_profiler_profiles WHERE `realm` = ?d AND `realmGUID` IS NOT NULL AND `name` = ?', $realmId, $char['name']); + $ri = DB::Aowow()->selectCell('SELECT MAX(`renameItr`) FROM ?_profiler_profiles WHERE `realm` = ?d AND `custom` = 0 AND `name` = ?', $realmId, $char['name']); $data['renameItr'] = $ri ? ++$ri : 1; } @@ -827,7 +827,7 @@ class Profiler 'realmGUID' => $guild['id'], 'name' => $guild['name'], 'nameUrl' => self::urlize($guild['name']), - 'cuFlags' => PROFILER_CU_NEEDS_RESYNC + 'stub' => 1 ); $guildId = DB::Aowow()->query('INSERT IGNORE INTO ?_profiler_guild (?#) VALUES (?a)', array_keys($gData), array_values($gData)); @@ -853,7 +853,7 @@ class Profiler 'name' => $t['name'], 'nameUrl' => self::urlize($t['name']), 'type' => $t['type'], - 'cuFlags' => PROFILER_CU_NEEDS_RESYNC + 'stub' => 1 ); $teamId = DB::Aowow()->query('INSERT IGNORE INTO ?_profiler_arena_team (?#) VALUES (?a)', array_keys($team), array_values($team)); @@ -889,7 +889,7 @@ class Profiler /*********************/ 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); + DB::Aowow()->query('UPDATE ?_profiler_profiles SET `stub` = 0 WHERE `id` = ?d', $profileId); return true; } @@ -956,7 +956,7 @@ 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 `stub` = 0 WHERE `id` = ?d', $guildId); return true; } @@ -1056,7 +1056,7 @@ class Profiler /* mark team as done */ /*********************/ - DB::Aowow()->query('UPDATE ?_profiler_arena_team SET `cuFlags` = `cuFlags` & ?d WHERE `id` = ?d', ~PROFILER_CU_NEEDS_RESYNC, $teamId); + DB::Aowow()->query('UPDATE ?_profiler_arena_team SET `stub` = 0 WHERE `id` = ?d', $teamId); return true; } diff --git a/includes/dbtypes/arenateam.class.php b/includes/dbtypes/arenateam.class.php index 8bb26a37..a5947644 100644 --- a/includes/dbtypes/arenateam.class.php +++ b/includes/dbtypes/arenateam.class.php @@ -235,7 +235,7 @@ class RemoteArenaTeamList extends ArenaTeamList 'nameUrl' => Profiler::urlize($this->getField('name')), 'type' => $this->getField('type'), 'rating' => $this->getField('rating'), - 'cuFlags' => PROFILER_CU_NEEDS_RESYNC + 'stub' => 1 ); } diff --git a/includes/dbtypes/guild.class.php b/includes/dbtypes/guild.class.php index 93f1c9d1..b58e288c 100644 --- a/includes/dbtypes/guild.class.php +++ b/includes/dbtypes/guild.class.php @@ -48,18 +48,16 @@ class GuildList extends DBTypeList if (!$guilds) return; - $stats = DB::Aowow()->select('SELECT `guild` AS ARRAY_KEY, `id` AS ARRAY_KEY2, `level`, `gearscore`, `achievementpoints`, IF(`cuFlags` & ?d, 0, 1) AS "synced" FROM ?_profiler_profiles WHERE `guild` IN (?a) ORDER BY `gearscore` DESC', PROFILER_CU_NEEDS_RESYNC, $guilds); + $stats = DB::Aowow()->select('SELECT `guild` AS ARRAY_KEY, `id` AS ARRAY_KEY2, `level`, `gearscore`, `achievementpoints` FROM ?_profiler_profiles WHERE `guild` IN (?a) AND `stub` = 0 ORDER BY `gearscore` DESC', $guilds); foreach ($this->iterate() as &$_curTpl) { $id = $_curTpl['id']; if (empty($stats[$id])) continue; - $guildStats = array_filter($stats[$id], function ($x) { return $x['synced']; } ); - if (!$guildStats) - continue; + $guildStats = $stats[$id]; - $nMaxLevel = count(array_filter($stats[$id], function ($x) { return $x['level'] >= MAX_LEVEL; } )); + $nMaxLevel = count(array_filter($stats[$id], fn($x) => $x['level'] >= MAX_LEVEL)); $levelMod = 1.0; if ($nMaxLevel < 25) @@ -227,7 +225,7 @@ class RemoteGuildList extends GuildList 'realmGUID' => $this->getField('guildid'), 'name' => $this->getField('name'), 'nameUrl' => Profiler::urlize($this->getField('name')), - 'cuFlags' => PROFILER_CU_NEEDS_RESYNC + 'stub' => 1 ); } diff --git a/includes/dbtypes/profile.class.php b/includes/dbtypes/profile.class.php index fe109400..98819401 100644 --- a/includes/dbtypes/profile.class.php +++ b/includes/dbtypes/profile.class.php @@ -83,7 +83,7 @@ class ProfileList extends DBTypeList if ($this->getField('cuFlags') & PROFILER_CU_PINNED) $data[$this->id]['pinned'] = 1; - if ($this->getField('cuFlags') & PROFILER_CU_DELETED) + if ($this->getField('deleted')) $data[$this->id]['deleted'] = 1; } @@ -168,7 +168,7 @@ class ProfileList extends DBTypeList public function isCustom() : bool { - return $this->getField('cuFlags') & PROFILER_CU_PROFILE; + return $this->getField('custom'); } public function isVisibleToUser() : bool @@ -176,7 +176,7 @@ class ProfileList extends DBTypeList if (!$this->isCustom() || User::isInGroup(U_GROUP_ADMIN | U_GROUP_BUREAU)) return true; - if ($this->getField('cuFlags') & PROFILER_CU_DELETED) + if ($this->getField('deleted')) return false; if (User::$id == $this->getField('user')) @@ -515,7 +515,7 @@ class RemoteProfileList extends ProfileList if ($curTpl['at_login'] & 0x1) { if (!isset($this->rnItr[$curTpl['name']])) - $this->rnItr[$curTpl['name']] = DB::Aowow()->selectCell('SELECT MAX(`renameItr`) FROM ?_profiler_profiles WHERE `realm` = ?d AND `realmGUID` IS NOT NULL AND `name` = ?', $r, $curTpl['name']) ?: 0; + $this->rnItr[$curTpl['name']] = DB::Aowow()->selectCell('SELECT MAX(`renameItr`) FROM ?_profiler_profiles WHERE `realm` = ?d AND `custom` = 0 AND `name` = ?', $r, $curTpl['name']) ?: 0; // already saved as "pending rename" if ($rnItr = DB::Aowow()->selectCell('SELECT `renameItr` FROM ?_profiler_profiles WHERE `realm` = ?d AND `realmGUID` = ?d', $r, $g)) @@ -606,7 +606,7 @@ class RemoteProfileList extends ProfileList 'gender' => $this->getField('gender'), 'guild' => $guildGUID ?: null, 'guildrank' => $guildGUID ? $this->getField('guildrank') : null, - 'cuFlags' => PROFILER_CU_NEEDS_RESYNC + 'stub' => 1 ); if ($guildGUID && empty($guildData[$realmId.'-'.$guildGUID])) @@ -615,7 +615,7 @@ class RemoteProfileList extends ProfileList 'realmGUID' => $guildGUID, 'name' => $this->getField('guildname'), 'nameUrl' => Profiler::urlize($this->getField('guildname')), - 'cuFlags' => PROFILER_CU_NEEDS_RESYNC + 'stub' => 1 ); } @@ -643,8 +643,7 @@ class RemoteProfileList extends ProfileList // merge back local ids $localIds = DB::Aowow()->select( - 'SELECT CONCAT(`realm`, ":", `realmGUID`) AS ARRAY_KEY, `id`, `gearscore` FROM ?_profiler_profiles WHERE (`cuFlags` & ?d) = 0 AND `realm` IN (?a) AND `realmGUID` IN (?a)', - PROFILER_CU_PROFILE, + 'SELECT CONCAT(`realm`, ":", `realmGUID`) AS ARRAY_KEY, `id`, `gearscore` FROM ?_profiler_profiles WHERE `custom` = 0 AND `realm` IN (?a) AND `realmGUID` IN (?a)', array_column($baseData, 'realm'), array_column($baseData, 'realmGUID') ); diff --git a/includes/defines.php b/includes/defines.php index b6eda11c..d2c944d8 100644 --- a/includes/defines.php +++ b/includes/defines.php @@ -374,9 +374,9 @@ define('QUEST_CU_PART_OF_SERIES', 0x0200); define('PROFILER_CU_PUBLISHED', 0x01); define('PROFILER_CU_PINNED', 0x02); -define('PROFILER_CU_DELETED', 0x04); -define('PROFILER_CU_PROFILE', 0x08); -define('PROFILER_CU_NEEDS_RESYNC', 0x10); +// define('PROFILER_CU_DELETED', 0x04); // migrated to separate db cols +// define('PROFILER_CU_PROFILE', 0x08); +// define('PROFILER_CU_NEEDS_RESYNC', 0x10); define('GUIDE_CU_NO_QUICKFACTS', 0x100); // merge with CC_FLAG_* define('GUIDE_CU_NO_RATING', 0x200); diff --git a/includes/user.class.php b/includes/user.class.php index 7233b016..c16c0c7e 100644 --- a/includes/user.class.php +++ b/includes/user.class.php @@ -785,7 +785,7 @@ class User // the old approach ['OR', ['user', self::$id], ['ap.accountId', self::$id]] caused keys to not get used $conditions = $ap ? [['OR', ['user', self::$id], ['id', $ap]]] : ['user', self::$id]; if (!self::isInGroup(U_GROUP_ADMIN | U_GROUP_BUREAU)) - $conditions[] = [['cuFlags', PROFILER_CU_DELETED, '&'], 0]; + $conditions[] = ['deleted', 0]; self::$profiles = (new LocalProfileList($conditions)); } diff --git a/setup/sql/01-db_structure.sql b/setup/sql/01-db_structure.sql index 3434c71c..04d45dc6 100644 --- a/setup/sql/01-db_structure.sql +++ b/setup/sql/01-db_structure.sql @@ -1663,6 +1663,7 @@ CREATE TABLE `aowow_profiler_arena_team` ( `nameUrl` varchar(24) NOT NULL, `type` tinyint(3) unsigned NOT NULL DEFAULT 0, `cuFlags` int(10) unsigned NOT NULL DEFAULT 0 COMMENT 'see defines.php for flags', + `stub` tinyint(1) DEFAULT 0 COMMENT 'arena team stub needs resync', `rating` smallint(5) unsigned NOT NULL DEFAULT 0, `seasonGames` smallint(5) unsigned NOT NULL DEFAULT 0, `seasonWins` smallint(5) unsigned NOT NULL DEFAULT 0, @@ -1676,7 +1677,8 @@ CREATE TABLE `aowow_profiler_arena_team` ( `borderColor` int(10) unsigned NOT NULL DEFAULT 0, PRIMARY KEY (`id`), UNIQUE KEY `realm_realmGUID` (`realm`,`realmGUID`), - KEY `name` (`name`) + KEY `name` (`name`), + KEY `idx_stub` (`stub`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; @@ -1849,6 +1851,7 @@ CREATE TABLE `aowow_profiler_guild` ( `realm` int(10) unsigned NOT NULL, `realmGUID` int(10) unsigned NOT NULL, `cuFlags` int(10) unsigned NOT NULL DEFAULT 0 COMMENT 'see defines.php for flags', + `stub` tinyint(1) DEFAULT 0 COMMENT 'guild stub needs resync', `name` varchar(26) NOT NULL, `nameUrl` varchar(26) NOT NULL, `emblemStyle` tinyint(3) unsigned NOT NULL DEFAULT 0, @@ -1860,7 +1863,8 @@ CREATE TABLE `aowow_profiler_guild` ( `createDate` int(10) unsigned NOT NULL DEFAULT 0, PRIMARY KEY (`id`), UNIQUE KEY `realm_realmGUID` (`realm`,`realmGUID`), - KEY `name` (`name`) + KEY `name` (`name`), + KEY `idx_stub` (`stub`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; @@ -1940,6 +1944,9 @@ CREATE TABLE `aowow_profiler_profiles` ( `realm` tinyint(3) unsigned DEFAULT NULL, `realmGUID` int(10) unsigned DEFAULT NULL, `cuFlags` int(10) unsigned NOT NULL DEFAULT 0 COMMENT 'see defines.php for flags', + `custom` tinyint(1) DEFAULT 0 COMMENT 'custom profile', + `stub` tinyint(1) DEFAULT 0 COMMENT 'profile stub needs resync', + `deleted` tinyint(1) DEFAULT 0 COMMENT 'only on custom profiles', `sourceId` int(10) unsigned DEFAULT NULL, `sourceName` varchar(50) DEFAULT NULL, `copy` int(10) unsigned DEFAULT NULL, @@ -1978,6 +1985,9 @@ CREATE TABLE `aowow_profiler_profiles` ( KEY `user` (`user`), KEY `guild` (`guild`), KEY `name` (`name`), + KEY `idx_custom` (`custom`), + KEY `idx_stub` (`stub`), + KEY `idx_deleted` (`deleted`), CONSTRAINT `FK_aowow_profiler_profiles_aowow_profiler_guild` FOREIGN KEY (`guild`) REFERENCES `aowow_profiler_guild` (`id`) ON DELETE SET NULL ON UPDATE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; diff --git a/setup/sql/updates/1762629696_01.sql b/setup/sql/updates/1762629696_01.sql new file mode 100644 index 00000000..40beca4d --- /dev/null +++ b/setup/sql/updates/1762629696_01.sql @@ -0,0 +1,18 @@ +ALTER TABLE aowow_profiler_profiles + ADD COLUMN `custom` tinyint(1) DEFAULT 0 COMMENT 'custom profile' AFTER `cuFlags`, + ADD COLUMN `stub` tinyint(1) DEFAULT 0 COMMENT 'character stub needs resync' AFTER `custom`, + ADD COLUMN `deleted` tinyint(1) DEFAULT 0 COMMENT 'only on custom profiles' AFTER `stub`, + ADD KEY `idx_custom` (`custom`), + ADD KEY `idx_stub` (`stub`), + ADD KEY `idx_deleted` (`deleted`) +; + +ALTER TABLE aowow_profiler_arena_team + ADD COLUMN `stub` tinyint(1) DEFAULT 0 COMMENT 'arena team stub needs resync' AFTER `cuFlags`, + ADD KEY `idx_stub` (`stub`) +; + +ALTER TABLE aowow_profiler_guild + ADD COLUMN `stub` tinyint(1) DEFAULT 0 COMMENT 'guild stub needs resync' AFTER `cuFlags`, + ADD KEY `idx_stub` (`stub`) +; diff --git a/setup/sql/updates/1762629696_02.sql b/setup/sql/updates/1762629696_02.sql new file mode 100644 index 00000000..c9f18965 --- /dev/null +++ b/setup/sql/updates/1762629696_02.sql @@ -0,0 +1,10 @@ +UPDATE aowow_profiler_profiles SET `deleted` = 1 WHERE `cuFlags` & 4; +UPDATE aowow_profiler_profiles SET `custom` = 1 WHERE `cuFlags` & 8; +UPDATE aowow_profiler_profiles SET `stub` = 1 WHERE `cuFlags` & 16; +UPDATE aowow_profiler_profiles SET `cuFlags` = `cuFlags` & ~(4 | 8 | 16); + +UPDATE aowow_profiler_arena_team SET `stub` = 1 WHERE `cuFlags` & 16; +UPDATE aowow_profiler_arena_team SET `cuFlags` = `cuFlags` & ~16; + +UPDATE aowow_profiler_guild SET `stub` = 1 WHERE `cuFlags` & 16; +UPDATE aowow_profiler_guild SET `cuFlags` = `cuFlags` & ~16; From fa89a5ad1e3975a9ff8b7e9f4adaf82ec51c101d Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Sat, 8 Nov 2025 22:07:07 +0100 Subject: [PATCH 103/260] User/Fixup * fix fetching user characters, borked in 474b5b5aec062b61e8d707c91739b50ad77e81ef * take #2 --- includes/user.class.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/user.class.php b/includes/user.class.php index c16c0c7e..d0faba8b 100644 --- a/includes/user.class.php +++ b/includes/user.class.php @@ -783,7 +783,7 @@ class User $ap = DB::Aowow()->selectCol('SELECT `profileId` FROM ?_account_profiles WHERE `accountId` = ?d', self::$id); // the old approach ['OR', ['user', self::$id], ['ap.accountId', self::$id]] caused keys to not get used - $conditions = $ap ? [['OR', ['user', self::$id], ['id', $ap]]] : ['user', self::$id]; + $conditions = $ap ? [['OR', ['user', self::$id], ['id', $ap]]] : [['user', self::$id]]; if (!self::isInGroup(U_GROUP_ADMIN | U_GROUP_BUREAU)) $conditions[] = ['deleted', 0]; From 0d42d2a2c4e6850006e7f94026ac778176b764e7 Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Sat, 8 Nov 2025 22:48:18 +0100 Subject: [PATCH 104/260] UserPage/Optimization * split up fetching of custom profiles and characters to make use of existing keys --- endpoints/user/user.php | 33 ++++++++++++++++++--------------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/endpoints/user/user.php b/endpoints/user/user.php index 93b8c2af..20af4077 100644 --- a/endpoints/user/user.php +++ b/endpoints/user/user.php @@ -233,30 +233,33 @@ class UserBaseResponse extends TemplateResponse if (Cfg::get('PROFILER_ENABLE')) { - $conditions = array( - ['OR', ['cuFlags', PROFILER_CU_PUBLISHED, '&'], ['ap.extraFlags', PROFILER_CU_PUBLISHED, '&']], - ['deleted', 0], - ['OR', ['user', $this->user['id']], ['ap.accountId', $this->user['id']]] - ); - - if (User::isInGroup(U_GROUP_ADMIN | U_GROUP_BUREAU)) - $conditions = array_slice($conditions, 2); - else if (User::$id == $this->user['id']) - array_shift($conditions); + $conditions = [['user', $this->user['id']]]; + if (User::$id != $this->user['id'] && !User::isInGroup(U_GROUP_ADMIN | U_GROUP_BUREAU)) + $conditions[] = ['cuFlags', PROFILER_CU_PUBLISHED, '&']; + if (!User::isInGroup(U_GROUP_ADMIN | U_GROUP_BUREAU)) + $conditions[] = ['deleted', 0]; $profiles = new LocalProfileList($conditions); if (!$profiles->error) { $this->addDataLoader('weight-presets'); - // Characters - if ($chars = $profiles->getListviewData(PROFILEINFO_CHARACTER | PROFILEINFO_USER)) - $this->charactersLvData = array_values($chars); - - // Profiles if ($prof = $profiles->getListviewData(PROFILEINFO_PROFILE | PROFILEINFO_USER)) $this->profilesLvData = array_values($prof); } + + $conditions = [['ap.accountId', $this->user['id']]]; + if (User::$id != $this->user['id'] && !User::isInGroup(U_GROUP_ADMIN | U_GROUP_BUREAU)) + $conditions[] = ['ap.extraFlags', PROFILER_CU_PUBLISHED, '&']; + + $characters = new LocalProfileList($conditions); + if (!$characters->error) + { + $this->addDataLoader('weight-presets'); + + if ($chars = $characters->getListviewData(PROFILEINFO_CHARACTER | PROFILEINFO_USER)) + $this->charactersLvData = array_values($chars); + } } // My Guides From a135dfce9099a5978ade7d92d2ac7cabd65cb23b Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Sun, 9 Nov 2025 15:59:06 +0100 Subject: [PATCH 105/260] Profiler/Completions * add keyed col `exalted` to reputation completions table to speed up lookups --- endpoints/faction/faction.php | 2 +- setup/sql/01-db_structure.sql | 2 ++ setup/sql/02-db_initial_data.sql | 2 +- setup/sql/updates/1762700147_01.sql | 4 ++++ 4 files changed, 8 insertions(+), 2 deletions(-) create mode 100644 setup/sql/updates/1762700147_01.sql diff --git a/endpoints/faction/faction.php b/endpoints/faction/faction.php index a5bd4867..cee5a79c 100644 --- a/endpoints/faction/faction.php +++ b/endpoints/faction/faction.php @@ -102,7 +102,7 @@ class FactionBaseResponse extends TemplateResponse implements ICache // profiler relateed (note that this is part of the cache. I don't think this is important enough to calc for every view) if (Cfg::get('PROFILER_ENABLE') && !($this->subject->getField('cuFlags') & CUSTOM_EXCLUDE_FOR_LISTVIEW)) { - $x = DB::Aowow()->selectCell('SELECT COUNT(1) FROM ?_profiler_completion_reputation WHERE `standing` >= ?d AND `factionId` = ?d', 42000, $this->typeId); + $x = DB::Aowow()->selectCell('SELECT COUNT(1) FROM ?_profiler_completion_reputation WHERE `exalted` = 1 AND `factionId` = ?d', $this->typeId); $y = DB::Aowow()->selectCell('SELECT COUNT(1) FROM ?_profiler_profiles WHERE `custom` = 0 AND `stub` = 0'); $infobox[] = Lang::profiler('attainedBy', [round(($x ?: 0) * 100 / ($y ?: 1))]); diff --git a/setup/sql/01-db_structure.sql b/setup/sql/01-db_structure.sql index 04d45dc6..76fcdf5a 100644 --- a/setup/sql/01-db_structure.sql +++ b/setup/sql/01-db_structure.sql @@ -1749,8 +1749,10 @@ CREATE TABLE `aowow_profiler_completion_reputation` ( `id` int(10) unsigned NOT NULL, `factionId` smallint(5) unsigned NOT NULL, `standing` mediumint(9) DEFAULT NULL, + `exalted` tinyint(1) GENERATED ALWAYS AS (`standing` >= 42000) STORED, KEY `id` (`id`), KEY `typeId` (`factionId`), + KEY `idx_exalted` (`exalted`), CONSTRAINT `FK_pr_completion_reputation` FOREIGN KEY (`id`) REFERENCES `aowow_profiler_profiles` (`id`) ON DELETE CASCADE ON UPDATE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; diff --git a/setup/sql/02-db_initial_data.sql b/setup/sql/02-db_initial_data.sql index df0fbe49..0f64d138 100644 --- a/setup/sql/02-db_initial_data.sql +++ b/setup/sql/02-db_initial_data.sql @@ -71,7 +71,7 @@ UNLOCK TABLES; LOCK TABLES `aowow_dbversion` WRITE; /*!40000 ALTER TABLE `aowow_dbversion` DISABLE KEYS */; -INSERT INTO `aowow_dbversion` VALUES (1762543653,0,NULL,NULL); +INSERT INTO `aowow_dbversion` VALUES (1762700148,0,NULL,NULL); /*!40000 ALTER TABLE `aowow_dbversion` ENABLE KEYS */; UNLOCK TABLES; diff --git a/setup/sql/updates/1762700147_01.sql b/setup/sql/updates/1762700147_01.sql new file mode 100644 index 00000000..dd011d10 --- /dev/null +++ b/setup/sql/updates/1762700147_01.sql @@ -0,0 +1,4 @@ +ALTER TABLE aowow_profiler_completion_reputation + ADD COLUMN `exalted` tinyint(1) GENERATED ALWAYS AS (`standing` >= 42000) STORED AFTER `standing`, + ADD KEY idx_exalted (`exalted`) +; From 643c3c2a83bb2ca25d2be7ec3a8b8f3e9f869a4f Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Mon, 10 Nov 2025 18:44:44 +0100 Subject: [PATCH 106/260] Comments/Goto * fix comment links in reputation history on user page --- endpoints/go-to-comment/go-to-comment.php | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/endpoints/go-to-comment/go-to-comment.php b/endpoints/go-to-comment/go-to-comment.php index 16112048..6c558d65 100644 --- a/endpoints/go-to-comment/go-to-comment.php +++ b/endpoints/go-to-comment/go-to-comment.php @@ -21,8 +21,9 @@ class GotocommentBaseResponse extends TextResponse return; } - // type <> 0 AND typeId <> 0 AND replyTo = 0 for comments - $comment = DB::Aowow()->selectRow('SELECT `id`, `type`, `typeId` FROM ?_comments WHERE `replyTo` = 0 AND `id` = ?d', $this->_get['id']); + // the reputation-history listview only creates go-to-comment links. So either upvoting replies does not grant reputation, or.... bug.? + + $comment = DB::Aowow()->selectRow('SELECT IFNULL(c2.`id`, c1.`id`) AS "id", IFNULL(c2.`type`, c1.`type`) AS "type", IFNULL(c2.`typeId`, c1.`typeId`) AS "typeId" FROM ?_comments c1 LEFT JOIN ?_comments c2 ON c1.`replyTo` = c2.`id` WHERE c1.`id` = ?d', $this->_get['id']); if (!$comment) { trigger_error('GotocommentBaseResponse - comment #'.$this->_get['id'].' not found', E_USER_ERROR); @@ -36,6 +37,8 @@ class GotocommentBaseResponse extends TextResponse } $this->redirectTo = sprintf('?%s=%d#comments:id=%d', Type::getFileString($comment['type']), $comment['typeId'], $comment['id']); + if ($comment['id'] != $this->_get['id']) // i am reply + $this->redirectTo .= ':reply='.$this->_get['id']; } } From 31f51276b2c78f0c5f0770398648abb9f6431b03 Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Mon, 10 Nov 2025 20:30:41 +0100 Subject: [PATCH 107/260] =?UTF-8?q?Profiler/Fixup=20=20*=20fix=20exception?= =?UTF-8?q?=20when=20manually=20querying=20for=20unsynced=20guild/arena-te?= =?UTF-8?q?am=20=20=20=20that=20shares=20it's=20name=20with=20another=20gu?= =?UTF-8?q?ild/team=20(e.g.=20=C3=AC=C3=AD=C3=AEi=20is=20the=20same=20to?= =?UTF-8?q?=20SQL)=20=20=20=20and=20the=20target=20guild/team=20not=20bein?= =?UTF-8?q?g=20the=20first=20result?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- endpoints/arena-team/arena-team.php | 4 ++-- endpoints/guild/guild.php | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/endpoints/arena-team/arena-team.php b/endpoints/arena-team/arena-team.php index 7e30a1c2..509fbdf3 100644 --- a/endpoints/arena-team/arena-team.php +++ b/endpoints/arena-team/arena-team.php @@ -58,9 +58,9 @@ class ArenateamBaseResponse extends TemplateResponse // 2) not yet synced but exists on realm (wont work if we get passed an urlized name, but there is nothing we can do about it) $subjects = DB::Characters($this->realmId)->select('SELECT at.`arenaTeamId` AS "realmGUID", at.`name`, at.`type` FROM arena_team at WHERE at.`name` = ?', $this->subjectName); - if ($subject = array_filter($subjects, fn($x) => Util::lower($x['name']) == Util::lower($this->subjectName))) + if ($subject = array_filter($subjects, fn($x) => Util::lower($x['name']) === Util::lower($this->subjectName))) { - $subject = $subject[0]; + $subject = array_pop($subject); $subject['realm'] = $this->realmId; $subject['stub'] = 1; $subject['nameUrl'] = Profiler::urlize($subject['name']); diff --git a/endpoints/guild/guild.php b/endpoints/guild/guild.php index 2cd0f3f6..dff1e831 100644 --- a/endpoints/guild/guild.php +++ b/endpoints/guild/guild.php @@ -58,9 +58,9 @@ class GuildBaseResponse extends TemplateResponse // 2) not yet synced but exists on realm (wont work if we get passed an urlized name, but there is nothing we can do about it) $subjects = DB::Characters($this->realmId)->select('SELECT `guildid` AS "realmGUID", `name` FROM guild WHERE `name` = ?', $this->subjectName); - if ($subject = array_filter($subjects, fn($x) => Util::lower($x['name']) == Util::lower($this->subjectName))) + if ($subject = array_filter($subjects, fn($x) => Util::lower($x['name']) === Util::lower($this->subjectName))) { - $subject = $subject[0]; + $subject = array_pop($subject); $subject['realm'] = $this->realmId; $subject['stub'] = 1; $subject['nameUrl'] = Profiler::urlize($subject['name']); From 7cf5dded98acdee7f892f1eccba51398176c7008 Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Tue, 11 Nov 2025 20:15:15 +0100 Subject: [PATCH 108/260] Signatures * add non-functional endpoint stubs with info --- endpoints/signature/delete.php | 30 ++++++++++++++++ endpoints/signature/generate.php | 60 +++++++++++++++++++++++++++++++ endpoints/signature/signature.php | 55 ++++++++++++++++++++++++++++ endpoints/user/user.php | 11 ++++++ 4 files changed, 156 insertions(+) create mode 100644 endpoints/signature/delete.php create mode 100644 endpoints/signature/generate.php create mode 100644 endpoints/signature/signature.php diff --git a/endpoints/signature/delete.php b/endpoints/signature/delete.php new file mode 100644 index 00000000..3266220c --- /dev/null +++ b/endpoints/signature/delete.php @@ -0,0 +1,30 @@ + ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkIdListUnsigned']] + ); + + public function __construct(string $pageParam) + { + parent::__construct($pageParam); + } + + protected function generate() : void + { + if (!$this->assertGET('id')) + $this->generate404(); + + // DB::Aowow()->query(DELETE FROM ?_account_signatures WHERE `id` IN (?a) { AND `accountId` = ?d }', $this->_get['id], User::isInGroup(U_GROUP_MODERATOR) ? DBSIMPLE_SKIP : User::$id); + } +} + +?> diff --git a/endpoints/signature/generate.php b/endpoints/signature/generate.php new file mode 100644 index 00000000..af88f9d7 --- /dev/null +++ b/endpoints/signature/generate.php @@ -0,0 +1,60 @@ + ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkProfileId']] + ); + + public function __construct(string $pageParam) + { + parent::__construct($pageParam); + } + + protected function generate() : void + { + parent::generate(); + + if (!$this->assertGET('id')) + $this->generate404(); + + // find file in storage + + // build image + } + + public function generate404(?string $out = null) : never + { + // "Signature Unavailable" + $out = /*data:image/png;base64,*/'iVBORw0KGgoAAAANSUhEUgAAAdQAAAA8CAIAAABQJdxgAAALbElEQVR4nO3bXWxT5R8H8OectnvtDtB2fVnXN6Cr29zW0SCSjMgSh0YSifPtjgsu1EuvjXFojPfKDRLitYQYxEggEwzhQiJb1+GG4MrWjrbr1petW6FubXee/8UTT05aKBv85az4/VyY9uyc5y32y9PfOeXMZjMBAIBni1d6AAAA/0UIXwAABSB8AQAUgPAFAFAAwhcAQAEIXwAABSB8AQAUgPAFAFAAwhcAQAEIXwAABSB8AQAUgPAFAFAAwhcAQAEIXwAABSB8AQAUgPAFAFAAwhcAQAEIXwAABSB8AQAUgPAFAFAAwhcAQAEIXwAABaiVHgDA8+bzzz+XXouiKH+72TM33hRUHYRv1di7d++BAwcEQZCOcBzHXgwNDR07dsxkMhFCFhYWvvvuO2WG+K8pD52hoaHy4+zgs1e++D6fjxBCKR0bG6t87WPP3HhTUF0QvtVhYGCgr6/P5XJt376dZS6ldH19XaPR+P1+QojJZNq3bx8h5Pfff/+3B/Ppp5+q1WpCiCiKX3zxxb/dHcMyiGFTLjkuP/iMlS/+7Oysw+GQ/nWsIBQKuVyuCmduvCmoLqj5VoeXX37Z4/HodLpCoXDz5s2JiYlMJsMSUMJx3LP5iKrVap/P5/P5eP7Z/f8TCoXYi2AwKD+eSqUIIfPz889sJA8lX/zTp0+zUT3WqVOnFhcXK5yw8aag6mDnWx1UKlVjYyMhZGpqamlpaXx8vK+vz263s2+77Nv3+Pg4IaS2tpb999ChQ93d3RqNpkIiU0ql1ysrK2fPno1EIoSQvr6+ffv2NTU1SX9NpVLnzp2LxWLyb/ocx1WuQrIypfwcSimllOM4Sumff/45MjJy+PBhg8HABlksFkdHR4eHh0VRlLfz7bfffvjhhy6Xi43zxIkT7PhPP/301ltvGQyGeDx+5syZko42NbUPPvigwkQIIb/88kuFC+WLzzoq8ah+5eccP36cvchms8PDwxMTEw9tyul0bmTRYItD+FYZt9vN8/yBAwey2Ww0Go1Go+x4SWXw/fff93g8bre7vr6eyKrDhBC/3y+dHAwGd+7cqVKpKKWRSGRwcPDrr78mhOzfv7+np6epqYntbUVRvHfv3ttvv/3NN98Q2Td99kJqkPzz3b+8TCkdmZ6edrlcPM/n8/l8Pt/e3m42m61WK+sol8tpNJpCoXDlyhX5rOfm5uRvpc3gH3/88eabb7IR/vXXX/KONjs1UlbZsFqtZrNZevvYCyuXZStczlgsFrPZzP5ZSqVSgiAUCoU7d+6UtGO3248ePbqRRYMtDmWH6rC2tra0tEQIqaur6+jo8Hq9XV1dPM9ns1m2DZydnSWykHW5XB0dHQ0NDYlEYnx8nH23LRQKLBzZV3iO4/R6/cTERCKR4HneZrPt2LFD6lEQhJs3b/r9/mg0qlKpHA6HTqdjf5Kqq36/X96ghFUG5Ikv9dja2nr37l1KqVqt1mg0ZrPZZrMVCoVAIJBOpxsbG9va2l566aUNLkuhUJBer6+vP+XUQqEQ2y+zIkYsFkun05RSaXaPurBk8R+lwpISQkRRHBsbm5qaopQajUar1frqq6+WN/L6668/5aLBFoHwrQ6XL18OhUKRSCSfzxNC1Gq1wWDwer1Wq3VwcLC8MshxHKsIz83NpdNplg5qtZpSKq8zhsNhlgVEVrXkeZ7juNHRUVEUa2trGxoa5H8teaJgaGjo5MmT8sLliRMnVlZW5OfIe7x161Y6nb5x48bIyAjP8y0tLRzHsdSLxWKEEK1Wy768P4GnmdrJkyeTyWQulyOEbN++XRTFtbU1jUaTy+VSqdTp06cfdeFGyrKVl5RZWFg4f/78vXv3WKmhublZr9eXN2WxWP6/iwZKQdmhOoyMjKRSqf7+fpvNVlNTY7Va9Xo9x3Eul2t5ebm8MkgpLRaLGo3G4XAQQux2OyFkfX1d+rhKzp8/39vbS2QbtzfeeINtpurr61Uq1YMHD0jFbV08Hpe/LU+ikh6/+uor9uL48eMqlYoQ0t7evsF1YFQqFdvnajSaCh1tamrxeDwQCAiC4PF4amtrWblGq9Xevn07EAj09PQ86sKHlmVLbHBJA4FAJpM5duyY0+lkeV3eFMdxT7ZosNUgfKuDdCvp3LlzGo1mYGDAaDTu3r2bfQ7LhcPhuro6t9ut0+l0Oh2ldHV1NRgMsi2wnPzGFLNnz56Ojo76+vr79+/fuXOH4zh5MfSxnuARiEAgIL9ZVD4k6TjHcTzP2+12Vgpoa2tjVd1HnV9ypPLUrl275vP58vl8bW2tw+G4f/9+oVDIZrNXr179+OOPn2ZNNr6klW+Qym1w0WDLQtmhavh8vtbW1iNHjgiCkEgkWP2B2bZtm/xMs9l85syZtbU1tVodDAb9fv/Y2Njk5OTq6qooijabTX4ye4RAYjAYeJ6vq6sjhNy9ezeTycjDVKvVloyK7ayJ7MP/0UcflTRYMjx5+ZXFhyAI8Xjc7/enUqlkMnnr1q3y6VNK2cl6vf7dd9/1eDzd3d1HjhwxmUyiKLLeSzra7NSWl5dnZmbYzT2tVms0GqPR6Ozs7MrKSoULLRaLvBez2VwyDKPRWOFyuffee+/w4cOs2sC29uVNbWrRYCtTlX+cYAvq7+9vaWlpbGzUarU6nW7btm0ej0elUi0uLs7Nzb3yyiuNjY0Gg4EQ8vfff3d1dV2+fPngwYOsOmGxWFihsLm5uaampqurSzo5l8u1t7c3NDQ0NzcTQh48eOD1egkhrKpICBFF0ePxqNVqjuOWlpYcDsfo6Gh/f7/FYuE4rlgsut1uu92+Y8cOdkSlUjU3NzudTlZxZg3u379f6jGbze7Zs+fq1auEkM7OzsbGxqamJkEQisWixWLp7e3duXPn7du3y6PkxRdfrKurEwRBEASVSrVr167Ozk6bzWY2m1lE3rhx45NPPnnKqaVSqY6ODpPJxPO8KIrT09M//PBDNps9ePDgoy70er0liy+fby6X6+7urtBvsVhsaWmhlOp0Oo7jDAaD0+kkhMzNzc3MzAwMDJQ0lUwmN75osJVh51tlBEHo6urq6elRq9WJRCIcDrMHjF544QV2wq5duwghR48eZeFI/rmxw3FcTU2N2+2Wn7x7924iqx6yv+ZyuWg0Sik1mUzd3d2ZTGZ1dZVSKl21urrKHgOw2Wws0TKZDLvEaDRaLJZYLMa2oqxBeY8ej0eay/DwcDwen5+f53m+s7Ozp6eH5/nJycmHTvznn3+en5+fmZkpFAqtra29vb1er9dgMLD7kBcuXCjp6Mmmxp5wYGXrpaWlTCbDNsKVLyxZ/PJhVL6cUppOp/P5fG9vb1tbGyEkHo8vLCxcunSpvKlNLRpsZaj5Vg32UJe8tLe8vHzlyhX2rGvJj2udTicL6Onp6UwmQwjR6XTsAVtS9kvckrdnz54dHBxcWFhgb/P5fCwWk3ZthJCLFy/yPB8Oh6Xx/Pjjj++8804ikZAaKXni9aG//Q0Gg99///1rr70m3SgTRXFycvLixYvlJ4fD4VOnTg0MDCSTSemnfZTSaDR66dIlqYWnnBoh5Ndff9Xr9ewxiWvXrj32Qo7jyme3qX6vX7/OKtfSvFZWVi5cuMDq2iVNbWrRYCvjpMfI4Xny2Wef7d27l+O4ZDIZiUQ4jrPZbAaDYXFxcWpq6ssvv1R6gAD/ddj5Pp8KhUIkEmlpaTEYDKxiSCnNZDKhUGh4eFjp0QEAdr7PKbvdzn4KJb+xns1mf/vtt+vXrys4MABgEL4AAArA0w4AAApA+AIAKADhCwCgAIQvAIACEL4AAApA+AIAKADhCwCgAIQvAIACEL4AAApA+AIAKADhCwCgAIQvAIACEL4AAApA+AIAKADhCwCgAIQvAIACEL4AAApA+AIAKADhCwCggP8BQFB72fG1SEAAAAAASUVORK5CYII='; + parent::generate404(base64_decode($out)); + } + + protected static function checkProfileId(string $sigId) : ?int + { + if (preg_match('/^(\d+)\.png$/i', $sigId, $m)) + return $m[0]; + + return null; + } +} + +?> diff --git a/endpoints/signature/signature.php b/endpoints/signature/signature.php new file mode 100644 index 00000000..dfe34e9e --- /dev/null +++ b/endpoints/signature/signature.php @@ -0,0 +1,55 @@ + ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkProfile']] // optional - full profile string to build sig from + ); + + private int $id = 0; + + public function __construct(string $pageParam) + { + parent::__construct($pageParam); + + if ($pageParam) + $this->id = intVal($pageParam); + else if ($this->assertGET('profile')) + $this->id = $this->_get['profile']; + else + $this->generateError(); + } + + protected function generate() : void + { + // show editor + + parent::generate(); + } + + protected static function checkProfile(string $profile) : ?int + { + if (!preg_match('/^([a-z]+)\.([a-z_]+)\.(.+)$/i', $profile, $m)) + return null; + + [, $region, $realm, $char] = $m; + + $realms = Profiler::getRealms(); + if ($rId = array_find_key($realms, fn($x) => $x['region'] == $region && $x['name'] == $realm)) + return DB::Aowow()->selectCell('SELECT `id` FROM ?_profiler_profiles WHERE `realm` = ?d AND `custom` = 0 AND `name` = ?', $rId, urldecode($char)) ?: null; + + return null; + } +} + +?> diff --git a/endpoints/user/user.php b/endpoints/user/user.php index 20af4077..d6ad92a1 100644 --- a/endpoints/user/user.php +++ b/endpoints/user/user.php @@ -260,6 +260,17 @@ class UserBaseResponse extends TemplateResponse if ($chars = $characters->getListviewData(PROFILEINFO_CHARACTER | PROFILEINFO_USER)) $this->charactersLvData = array_values($chars); } + + // signatures + /* $this->lvTabs->addListviewTab(new Listview(array( + * 'id' => 'signatures', + * 'name' => '$LANG.tab_signatures', + * 'hiddenCols' => ['name','faction','location','guild'], + * 'extraCols' => ['$Listview.extraCols.signature'], + * 'onBeforeCreate' => '$Listview.funcBox.beforeUserSignatures', + * 'data' => [ ProfileList->getListviewData() ] // no extra signature related data observed + * ), 'profile')); + */ } // My Guides From 45417122c24756b288a67c8531d659b45fee02b4 Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Tue, 11 Nov 2025 20:35:17 +0100 Subject: [PATCH 109/260] UserPage/Fixup * only show related heading if we have tabs to display --- template/pages/user.tpl.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/template/pages/user.tpl.php b/template/pages/user.tpl.php index ae6995df..d1605f84 100644 --- a/template/pages/user.tpl.php +++ b/template/pages/user.tpl.php @@ -42,7 +42,9 @@ if ($this->description): endif; ?>
    +lvTabs)): ?> +
    From 1fe3690244e86440b38423271d5ad4b13fe163f9 Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Thu, 13 Nov 2025 20:55:41 +0100 Subject: [PATCH 110/260] Cache/Fixup * fix cache collision on list pages caused by improper encoding of category * fix cache key not encoding category values of int: 0 * version bump to flush caches --- .../components/response/templateresponse.class.php | 12 +++++++++--- includes/kernel.php | 2 +- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/includes/components/response/templateresponse.class.php b/includes/components/response/templateresponse.class.php index 4b165c82..6353e87d 100644 --- a/includes/components/response/templateresponse.class.php +++ b/includes/components/response/templateresponse.class.php @@ -37,14 +37,20 @@ trait TrListPage public function getCacheKeyComponents() : array { // max. 3 catgs - // catg max 65535 + // catg max 32767 - largest in use should be 11.197.26801 (Spells: Professions > Tailoring > Spellfire Tailoring) if ($this->category) { $catg = 0x0; for ($i = 0; $i < 3; $i++) { - $catg <<= 4; - $catg |= ($this->category[$i] ?? 0) & 0xFFFF; + $catg <<= 4 * 4; + if (!isset($this->category[$i])) + continue; + + if ($this->category[$i]) + $catg |= ($this->category[$i] << 1) & 0xFFFF; + else + $catg |= 1; } } diff --git a/includes/kernel.php b/includes/kernel.php index 00a4414d..b2f43938 100644 --- a/includes/kernel.php +++ b/includes/kernel.php @@ -6,7 +6,7 @@ mb_internal_encoding('UTF-8'); error_reporting(E_ALL); mysqli_report(MYSQLI_REPORT_ERROR); -define('AOWOW_REVISION', 42); +define('AOWOW_REVISION', 43); 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 From f5654ae21f8470067b6c5586fc82b477b0aeafb9 Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Fri, 14 Nov 2025 17:00:18 +0100 Subject: [PATCH 111/260] DateTime * recreate date functions from javascript in new class DateTime * move date and time functions from Util to new class * fixes various cooldown messages for account recovery --- endpoints/account/account.php | 2 +- endpoints/account/forgot-password.php | 2 +- endpoints/account/forgot-username.php | 2 +- endpoints/account/resend.php | 2 +- endpoints/account/signin.php | 2 +- endpoints/account/signup.php | 2 +- endpoints/account/update-email.php | 2 +- endpoints/account/update-password.php | 2 +- endpoints/event/event.php | 2 +- endpoints/guide/changelog.php | 9 +- .../latest-comments/latest-comments_rss.php | 4 +- .../latest-screenshots_rss.php | 4 +- endpoints/latest-videos/latest-videos_rss.php | 4 +- endpoints/mail/mail.php | 2 +- endpoints/object/object.php | 4 +- endpoints/quest/quest.php | 4 +- endpoints/spell/spell.php | 12 +- endpoints/user/user.php | 22 +- includes/components/SmartAI/SmartAI.class.php | 4 +- includes/components/dbtypelist.class.php | 2 +- .../response/baseresponse.class.php | 2 +- .../response/templateresponse.class.php | 6 +- .../response/textresponse.class.php | 8 +- includes/dbtypes/item.class.php | 2 +- includes/dbtypes/spell.class.php | 2 +- includes/kernel.php | 1 + includes/utilities.php | 106 +-------- localization/datetime.class.php | 218 ++++++++++++++++++ localization/lang.class.php | 31 +-- localization/locale_dede.php | 20 +- localization/locale_enus.php | 20 +- localization/locale_eses.php | 20 +- localization/locale_frfr.php | 20 +- localization/locale_ruru.php | 20 +- localization/locale_zhcn.php | 20 +- template/pages/spell.tpl.php | 12 +- 36 files changed, 409 insertions(+), 188 deletions(-) create mode 100644 localization/datetime.class.php diff --git a/endpoints/account/account.php b/endpoints/account/account.php index 2512d508..f095ea81 100644 --- a/endpoints/account/account.php +++ b/endpoints/account/account.php @@ -112,7 +112,7 @@ class AccountBaseResponse extends TemplateResponse // Username $this->curName = User::$username; - $this->renameCD = Util::formatTime(Cfg::get('ACC_RENAME_DECAY') * 1000); + $this->renameCD = DateTime::formatTimeElapsedFloat(Cfg::get('ACC_RENAME_DECAY') * 1000); if ($user['renameCooldown'] > time()) { $locCode = implode('_', str_split(Lang::getLocale()->json(), 2)); // ._. diff --git a/endpoints/account/forgot-password.php b/endpoints/account/forgot-password.php index 4121f47f..1f340cad 100644 --- a/endpoints/account/forgot-password.php +++ b/endpoints/account/forgot-password.php @@ -76,7 +76,7 @@ class AccountforgotpasswordResponse extends TemplateResponse // on cooldown pretend we dont know the email address if ($timeout && $timeout > time()) - return Cfg::get('DEBUG') ? 'resend on cooldown: '.Util::formatTimeDiff($timeout).' remaining' : Lang::account('inputbox', 'error', 'emailNotFound'); + return Cfg::get('DEBUG') ? 'resend on cooldown: '.DateTime::formatTimeElapsed($timeout * 1000).' remaining' : Lang::account('inputbox', 'error', 'emailNotFound'); // pretend recovery started if (!DB::Aowow()->selectCell('SELECT 1 FROM ?_account WHERE `email` = ?', $this->_post['email'])) diff --git a/endpoints/account/forgot-username.php b/endpoints/account/forgot-username.php index 4a6245d4..0b8345fd 100644 --- a/endpoints/account/forgot-username.php +++ b/endpoints/account/forgot-username.php @@ -75,7 +75,7 @@ class AccountforgotusernameResponse extends TemplateResponse // on cooldown pretend we dont know the email address if ($timeout && $timeout > time()) - return Cfg::get('DEBUG') ? 'resend on cooldown: '.Util::formatTimeDiff($timeout).' remaining' : Lang::account('inputbox', 'error', 'emailNotFound'); + return Cfg::get('DEBUG') ? 'resend on cooldown: '.DateTime::formatTimeElapsed($timeout * 1000).' remaining' : Lang::account('inputbox', 'error', 'emailNotFound'); // pretend recovery started if (!DB::Aowow()->selectCell('SELECT 1 FROM ?_account WHERE `email` = ?', $this->_post['email'])) diff --git a/endpoints/account/resend.php b/endpoints/account/resend.php index 234aea17..7c02075e 100644 --- a/endpoints/account/resend.php +++ b/endpoints/account/resend.php @@ -73,7 +73,7 @@ class AccountResendResponse extends TemplateResponse // on cooldown pretend we dont know the email address if ($timeout && $timeout > time()) - return Cfg::get('DEBUG') ? 'resend on cooldown: '.Util::formatTimeDiff($timeout).' remaining' : Lang::account('inputbox', 'error', 'emailNotFound'); + return Cfg::get('DEBUG') ? 'resend on cooldown: '.DateTime::formatTimeElapsed($timeout * 1000).' remaining' : Lang::account('inputbox', 'error', 'emailNotFound'); // check email and account status if ($token = DB::Aowow()->selectCell('SELECT `token` FROM ?_account WHERE `email` = ? AND `status` = ?d', $this->_post['email'], ACC_STATUS_NEW)) diff --git a/endpoints/account/signin.php b/endpoints/account/signin.php index c4a0329e..ffcd15b7 100644 --- a/endpoints/account/signin.php +++ b/endpoints/account/signin.php @@ -99,7 +99,7 @@ class AccountSigninResponse extends TemplateResponse // AUTH_BANNED => Lang::account('accBanned'); // ToDo: should this return an error? the actual account functionality should be blocked elsewhere AUTH_WRONGUSER => Lang::account('userNotFound'), AUTH_WRONGPASS => Lang::account('wrongPass'), - AUTH_IPBANNED => Lang::account('inputbox', 'error', 'loginExceeded', [Util::formatTime(Cfg::get('ACC_FAILED_AUTH_BLOCK') * 1000)]), + AUTH_IPBANNED => Lang::account('inputbox', 'error', 'loginExceeded', [DateTime::formatTimeElapsedFloat(Cfg::get('ACC_FAILED_AUTH_BLOCK') * 1000)]), AUTH_INTERNAL_ERR => Lang::main('intError'), default => Lang::main('intError') }; diff --git a/endpoints/account/signup.php b/endpoints/account/signup.php index f8a819e1..734832b1 100644 --- a/endpoints/account/signup.php +++ b/endpoints/account/signup.php @@ -112,7 +112,7 @@ class AccountSignupResponse extends TemplateResponse if (DB::Aowow()->selectRow('SELECT 1 FROM ?_account_bannedips WHERE `type` = ?d AND `ip` = ? AND `count` >= ?d AND `unbanDate` >= UNIX_TIMESTAMP()', IP_BAN_TYPE_REGISTRATION_ATTEMPT, User::$ip, Cfg::get('ACC_FAILED_AUTH_COUNT'))) { DB::Aowow()->query('UPDATE ?_account_bannedips SET `count` = `count` + 1, `unbanDate` = UNIX_TIMESTAMP() + ?d WHERE `ip` = ? AND `type` = ?d', Cfg::get('ACC_FAILED_AUTH_BLOCK'), User::$ip, IP_BAN_TYPE_REGISTRATION_ATTEMPT); - return Lang::account('inputbox', 'error', 'signupExceeded', [Util::formatTime(Cfg::get('ACC_FAILED_AUTH_BLOCK') * 1000)]); + return Lang::account('inputbox', 'error', 'signupExceeded', [DateTime::formatTimeElapsedFloat(Cfg::get('ACC_FAILED_AUTH_BLOCK') * 1000)]); } // username / email taken diff --git a/endpoints/account/update-email.php b/endpoints/account/update-email.php index 104e18a5..6806ad3b 100644 --- a/endpoints/account/update-email.php +++ b/endpoints/account/update-email.php @@ -53,7 +53,7 @@ class AccountUpdateemailResponse extends TextResponse $status = DB::Aowow()->selectCell('SELECT `status` FROM ?_account WHERE `statusTimer` > UNIX_TIMESTAMP() AND `id` = ?d', User::$id); if ($status != ACC_STATUS_NONE && $status != ACC_STATUS_CHANGE_EMAIL) - return Lang::account('isRecovering', [Util::formatTime(Cfg::get('ACC_RECOVERY_DECAY') * 1000)]); + return Lang::account('inputbox', 'error', 'isRecovering', [DateTime::formatTimeElapsedFloat(Cfg::get('ACC_RECOVERY_DECAY') * 1000)]); $oldEmail = DB::Aowow()->selectCell('SELECT `email` FROM ?_account WHERE `id` = ?d', User::$id); if ($this->_post['newemail'] == $oldEmail) diff --git a/endpoints/account/update-password.php b/endpoints/account/update-password.php index cc4151d6..35543fe6 100644 --- a/endpoints/account/update-password.php +++ b/endpoints/account/update-password.php @@ -55,7 +55,7 @@ class AccountUpdatepasswordResponse extends TextResponse $userData = DB::Aowow()->selectRow('SELECT `status`, `passHash`, `statusTimer` FROM ?_account WHERE `id` = ?d', User::$id); if ($userData['status'] != ACC_STATUS_NONE && $userData['status'] != ACC_STATUS_CHANGE_PASS && $userData['statusTimer'] > time()) - return Lang::account('isRecovering', [Util::formatTime(Cfg::get('ACC_RECOVERY_DECAY') * 1000)]); + return Lang::account('inputbox', 'error', 'isRecovering', [DateTime::formatTimeElapsedFloat(Cfg::get('ACC_RECOVERY_DECAY') * 1000)]); if (!User::verifyCrypt($this->_post['currentPassword'], $userData['passHash'])) return Lang::account('wrongPass'); diff --git a/endpoints/event/event.php b/endpoints/event/event.php index ac3b5f31..2218d781 100644 --- a/endpoints/event/event.php +++ b/endpoints/event/event.php @@ -327,7 +327,7 @@ class EventBaseResponse extends TemplateResponse implements ICache // interval if ($rec > 0) - $infobox[] = Lang::event('interval').Util::formatTime($rec * 1000); + $infobox[] = Lang::event('interval').DateTime::formatTimeElapsed($rec * 1000); // in progress if ($start < time() && $end > time()) diff --git a/endpoints/guide/changelog.php b/endpoints/guide/changelog.php index 614af6d6..aa41d802 100644 --- a/endpoints/guide/changelog.php +++ b/endpoints/guide/changelog.php @@ -79,20 +79,21 @@ class GuideChangelogResponse extends TemplateResponse $buff = '
      '; $inp = fn($rev) => User::isInGroup(U_GROUP_STAFF) && false ? ($rev !== null ? '' : '') : ''; + $now = new DateTime(); $logEntries = DB::Aowow()->select('SELECT a.`username` AS `name`, gcl.`date`, gcl.`status`, gcl.`msg`, gcl.`rev` FROM ?_guides_changelog gcl JOIN ?_account a ON a.`id` = gcl.`userId` WHERE gcl.`id` = ?d ORDER BY gcl.`date` DESC', $this->_get['id']); foreach ($logEntries as $log) { if ($log['status'] != GuideMgr::STATUS_NONE) - $buff .= '
    • '.$inp($log['rev']).''.Lang::guide('clStatusSet', [Lang::guide('status', $log['status'])]).''.Util::formatTimeDiff($log['date'])."
    • \n"; + $buff .= '
    • '.$inp($log['rev']).''.Lang::guide('clStatusSet', [Lang::guide('status', $log['status'])]).''.$now->formatDate($log['date'], true)."
    • \n"; else if ($log['msg']) - $buff .= '
    • '.$inp($log['rev']).''.Util::formatTimeDiff($log['date']).Lang::main('colon').''.$log['msg'].' '.Lang::main('byUser', [$log['name'], 'style="text-decoration:underline"'])."
    • \n"; + $buff .= '
    • '.$inp($log['rev']).''.$now->formatDate($log['date'], true).Lang::main('colon').''.$log['msg'].' '.Lang::main('byUser', [$log['name'], 'style="text-decoration:underline"'])."
    • \n"; else - $buff .= '
    • '.$inp($log['rev']).''.Util::formatTimeDiff($log['date']).Lang::main('colon').''.Lang::guide('clMinorEdit').' '.Lang::main('byUser', [$log['name'], 'style="text-decoration:underline"'])."
    • \n"; + $buff .= '
    • '.$inp($log['rev']).''.$now->formatDate($log['date'], true).Lang::main('colon').''.Lang::guide('clMinorEdit').' '.Lang::main('byUser', [$log['name'], 'style="text-decoration:underline"'])."
    • \n"; } // append creation - $buff .= '
    • '.$inp(0).''.Lang::guide('clCreated').''.Util::formatTimeDiff($guide->getField('date'))."
    • \n
    \n"; + $buff .= '
  • '.$inp(0).''.Lang::guide('clCreated').''.$now->formatDate($guide->getField('date'), true)."
  • \n\n"; if (User::isInGroup(U_GROUP_STAFF) && false) $buff .= ''; diff --git a/endpoints/latest-comments/latest-comments_rss.php b/endpoints/latest-comments/latest-comments_rss.php index 22389005..0176ec41 100644 --- a/endpoints/latest-comments/latest-comments_rss.php +++ b/endpoints/latest-comments/latest-comments_rss.php @@ -14,6 +14,8 @@ class LatestcommentsRssResponse extends TextResponse protected function generate() : void { + $now = new DateTime(); + foreach (CommunityContent::getCommentPreviews(['comments' => 1, 'replies' => 1], dateFmt: false) as $comment) { if (empty($comment['commentid'])) @@ -25,7 +27,7 @@ class LatestcommentsRssResponse extends TextResponse $this->feedData[] = array( 'title' => [true, [], Lang::typeName($comment['type']).Lang::main('colon').htmlentities($comment['subject'])], 'link' => [false, [], $url], - 'description' => [true, [], htmlentities($comment['preview'])."

    ".Lang::main('byUser', [$comment['user'], '']) . Util::formatTimeDiff($comment['date'])], + 'description' => [true, [], htmlentities($comment['preview'])."

    ".Lang::main('byUser', [$comment['user'], '']) . $now->formatDate($comment['date'], true)], 'pubDate' => [false, [], date(DATE_RSS, $comment['date'])], 'guid' => [false, [], $url] // 'domain' => [false, [], null] diff --git a/endpoints/latest-screenshots/latest-screenshots_rss.php b/endpoints/latest-screenshots/latest-screenshots_rss.php index a91c0127..78520b50 100644 --- a/endpoints/latest-screenshots/latest-screenshots_rss.php +++ b/endpoints/latest-screenshots/latest-screenshots_rss.php @@ -14,12 +14,14 @@ class LatestscreenshotsRssResponse extends TextResponse protected function generate() : void { + $now = new DateTime(); + foreach (CommunityContent::getScreenshots(dateFmt: false) as $screenshot) { $desc = ''; if ($screenshot['caption']) $desc .= '
    '.$screenshot['caption']; - $desc .= "

    ".Lang::main('byUser', [$screenshot['user'], '']) . Util::formatTimeDiff($screenshot['date'], true); + $desc .= "

    ".Lang::main('byUser', [$screenshot['user'], '']) . $now->formatDate($screenshot['date'], true); // enclosure/length => filesize('static/uploads/screenshots/thumb/'.$screenshot['id'].'.jpg') .. always set to this placeholder value though $this->feedData[] = array( diff --git a/endpoints/latest-videos/latest-videos_rss.php b/endpoints/latest-videos/latest-videos_rss.php index 4fd63165..e35a5f94 100644 --- a/endpoints/latest-videos/latest-videos_rss.php +++ b/endpoints/latest-videos/latest-videos_rss.php @@ -14,12 +14,14 @@ class LatestvideosRssResponse extends TextResponse protected function generate() : void { + $now = new DateTime(); + foreach (CommunityContent::getvideos(dateFmt: false) as $video) { $desc = ''; if ($video['caption']) $desc .= '
    '.$video['caption']; - $desc .= "

    ".Lang::main('byUser', [$video['user'], '']) . Util::formatTimeDiff($video['date'], true); + $desc .= "

    ".Lang::main('byUser', [$video['user'], '']) . $now->formatDate($video['date'], true); // is enclosure/length .. is this even relevant..? $this->feedData[] = array( diff --git a/endpoints/mail/mail.php b/endpoints/mail/mail.php index 73bc132e..ffad7f29 100644 --- a/endpoints/mail/mail.php +++ b/endpoints/mail/mail.php @@ -99,7 +99,7 @@ class MailBaseResponse extends TemplateResponse implements ICache } if ($q['rewardMailDelay'] > 0) - $infobox[] = Lang::mail('delay', [Util::formatTime($q['rewardMailDelay'] * 1000)]); + $infobox[] = Lang::mail('delay', [DateTime::formatTimeElapsed($q['rewardMailDelay'] * 1000)]); } else if ($npcId = DB::World()->selectCell('SELECT `Sender` FROM achievement_reward WHERE `MailTemplateId` = ?d', $this->typeId)) { diff --git a/endpoints/object/object.php b/endpoints/object/object.php index 6b5150ef..d41d478b 100644 --- a/endpoints/object/object.php +++ b/endpoints/object/object.php @@ -151,7 +151,7 @@ class ObjectBaseResponse extends TemplateResponse implements ICache $buff = Lang::spell('spellModOp', 4).Lang::main('colon').Util::createNumRange($min, $max); // since Veins don't have charges anymore, the timer is questionable - $infobox[] = $restock > 1 ? '[tooltip name=restock]'.Lang::gameObject('restock', [Util::formatTime($restock * 1000)]).'[/tooltip][span class=tip tooltip=restock]'.$buff.'[/span]' : $buff; + $infobox[] = $restock > 1 ? '[tooltip name=restock]'.Lang::gameObject('restock', [DateTime::formatTimeElapsed($restock * 1000)]).'[/tooltip][span class=tip tooltip=restock]'.$buff.'[/span]' : $buff; } // meeting stone [minLevel, maxLevel, zone] @@ -177,7 +177,7 @@ class ObjectBaseResponse extends TemplateResponse implements ICache $buff .= Lang::main('colon').'[ul]'; if ($minTime > 1) // sign shenannigans reverse the display order - $buff .= '[li]'.Lang::game('duration').Lang::main('colon').Util::createNumRange(-$maxTime, -$minTime, fn: fn($x) => Util::FormatTime(-$x * 1000, true)).'[/li]'; + $buff .= '[li]'.Lang::game('duration').Lang::main('colon').Util::createNumRange(-$maxTime, -$minTime, fn: fn($x) => DateTime::formatTimeElapsed(-$x * 1000)).'[/li]'; if ($minPlayer) $buff .= '[li]'.Lang::main('players').Lang::main('colon').Util::createNumRange($minPlayer, $maxPlayer).'[/li]'; diff --git a/endpoints/quest/quest.php b/endpoints/quest/quest.php index 10c56534..1c21e771 100644 --- a/endpoints/quest/quest.php +++ b/endpoints/quest/quest.php @@ -197,7 +197,7 @@ class QuestBaseResponse extends TemplateResponse implements ICache // timer if ($_ = $this->subject->getField('timeLimit')) - $infobox[] = Lang::quest('timer').Util::formatTime($_ * 1000); + $infobox[] = Lang::quest('timer').DateTime::formatTimeElapsedFloat($_ * 1000); $startEnd = DB::Aowow()->select('SELECT * FROM ?_quests_startend WHERE `questId` = ?d', $this->typeId); @@ -1159,7 +1159,7 @@ class QuestBaseResponse extends TemplateResponse implements ICache 'header' => array( $rmtId, null, - $delay ? Lang::mail('mailIn', [Util::formatTime($delay * 1000)]) : null, + $delay ? Lang::mail('mailIn', [DateTime::formatTimeElapsed($delay * 1000)]) : null, ) ); diff --git a/endpoints/spell/spell.php b/endpoints/spell/spell.php index f473a698..f2369f30 100644 --- a/endpoints/spell/spell.php +++ b/endpoints/spell/spell.php @@ -228,7 +228,7 @@ class SpellBaseResponse extends TemplateResponse implements ICache $this->castTime = $this->subject->createCastTimeForCurrent(false, false); $this->level = $this->subject->getField('spellLevel'); $this->rangeName = $this->subject->getField('rangeText', true); - $this->gcd = Util::formatTime($this->subject->getField('startRecoveryTime')); + $this->gcd = DateTime::formatTimeElapsedFloat($this->subject->getField('startRecoveryTime')); $this->school = $this->fmtStaffTip(Lang::getMagicSchools($this->subject->getField('schoolMask')), Util::asHex($this->subject->getField('schoolMask'))); $this->dispel = $this->subject->getField('dispelType') ? Lang::game('dt', $this->subject->getField('dispelType')) : null; $this->mechanic = $this->subject->getField('mechanic') ? Lang::game('me', $this->subject->getField('mechanic')) : null; @@ -258,12 +258,12 @@ class SpellBaseResponse extends TemplateResponse implements ICache $this->stances = Lang::getStances($this->subject->getField('stanceMask')); if (($_ = $this->subject->getField('recoveryTime')) && $_ > 0) - $this->cooldown = Util::formatTime($_); + $this->cooldown = DateTime::formatTimeElapsedFloat($_); else if (($_ = $this->subject->getField('recoveryCategory')) && $_ > 0) - $this->cooldown = Util::formatTime($_); + $this->cooldown = DateTime::formatTimeElapsedFloat($_); if (($_ = $this->subject->getField('duration')) && $_ > 0) - $this->duration = Util::formatTime($_); + $this->duration = DateTime::formatTimeElapsedFloat($_); /**************/ @@ -1703,7 +1703,7 @@ class SpellBaseResponse extends TemplateResponse implements ICache $_footer['radius'] = Lang::spell('_radius').$this->subject->getField('effect'.$i.'RadiusMax').' '.Lang::spell('_distUnit'); if ($this->subject->getField('effect'.$i.'Periode') > 0) - $_footer['interval'] = Lang::spell('_interval').Util::formatTime($this->subject->getField('effect'.$i.'Periode')); + $_footer['interval'] = Lang::spell('_interval').DateTime::formatTimeElapsedFloat($this->subject->getField('effect'.$i.'Periode')); if ($_ = $this->subject->getField('effect'.$i.'Mechanic')) $_footer['mechanic'] = Lang::game('mechanic').Lang::main('colon').Lang::game('me', $_); @@ -1712,7 +1712,7 @@ class SpellBaseResponse extends TemplateResponse implements ICache { $_footer['proc'] = $procData['chance'] < 0 ? Lang::spell('ppm', [-$procData['chance']]) : Lang::spell('procChance', [$procData['chance']]); if ($procData['cooldown']) - $_footer['procCD'] = Lang::game('cooldown', [Util::formatTime($procData['cooldown'], true)]); + $_footer['procCD'] = Lang::game('cooldown', [DateTime::formatTimeElapsed($procData['cooldown'] * 1000)]); } // Effect Name diff --git a/endpoints/user/user.php b/endpoints/user/user.php index d6ad92a1..d18a968f 100644 --- a/endpoints/user/user.php +++ b/endpoints/user/user.php @@ -46,7 +46,7 @@ class UserBaseResponse extends TemplateResponse // do not display system account if (!$this->user['id']) $this->generateNotFound(Lang::user('notFound', [$pageParam])); - } + } protected function generate() : void { @@ -74,14 +74,16 @@ class UserBaseResponse extends TemplateResponse } if ($this->user['joinDate']) - $infobox[] = Lang::user('joinDate') . '[tooltip name=joinDate]'. date('l, G:i:s', $this->user['joinDate']). '[/tooltip][span class=tip tooltip=joinDate]'. date(Lang::main('dateFmtShort'), $this->user['joinDate']). '[/span]'; + $infobox[] = Lang::user('joinDate') . '[tooltip name=joinDate]'. date('l, G:i:s', $this->user['joinDate']). '[/tooltip][span class=tip tooltip=joinDate]'.(new DateTime())->formatDate($this->user['joinDate']). '[/span]'; if ($this->user['prevLogin']) - $infobox[] = Lang::user('lastLogin') . '[tooltip name=lastLogin]'.date('l, G:i:s', $this->user['prevLogin']).'[/tooltip][span class=tip tooltip=lastLogin]'.date(Lang::main('dateFmtShort'), $this->user['prevLogin']).'[/span]'; + $infobox[] = Lang::user('lastLogin') . '[tooltip name=lastLogin]'.date('l, G:i:s', $this->user['prevLogin']).'[/tooltip][span class=tip tooltip=lastLogin]'.(new DateTime())->formatDate($this->user['prevLogin']).'[/span]'; if ($groups) $infobox[] = Lang::user('userGroups') . implode(', ', $groups); $infobox[] = Lang::user('consecVisits'). $this->user['consecutiveVisits']; - $infobox[] = Lang::main('siteRep') . Lang::nf($this->user['sumRep']); + + if ($this->user['sumRep']) + $infobox[] = Lang::main('siteRep') . Lang::nf($this->user['sumRep']); if ($infobox) $this->infobox = new InfoboxMarkup($infobox, ['allow' => Markup::CLASS_STAFF], 'infobox-contents0'); @@ -298,6 +300,9 @@ class UserBaseResponse extends TemplateResponse [$sum, $nRatings] = $co; + if (!$sum) + return null; + return Lang::user('comments').$sum.($nRatings ? ' [small]([tooltip=tooltip_totalratings]'.$nRatings.'[/tooltip])[/small]' : ''); } @@ -313,6 +318,9 @@ class UserBaseResponse extends TemplateResponse [$sum, $nSticky, $nPending] = $ss; + if (!$sum) + return null; + $buff = []; if ($nSticky || $nPending) { @@ -341,6 +349,9 @@ class UserBaseResponse extends TemplateResponse [$sum, $nSticky, $nPending] = $vi; + if (!$sum) + return null; + $buff = []; if ($nSticky || $nPending) { @@ -373,6 +384,9 @@ class UserBaseResponse extends TemplateResponse if ($nReplies) $buff[] = '[tooltip=replies]'.$nReplies.'[/tooltip]'; + if (!$buff) + return null; + return Lang::user('posts').($nTopics + $nReplies).($buff ? ' [small]('.implode(' + ', $buff).')[/small]' : ''); } } diff --git a/includes/components/SmartAI/SmartAI.class.php b/includes/components/SmartAI/SmartAI.class.php index 07f25a11..f5e0fcfc 100644 --- a/includes/components/SmartAI/SmartAI.class.php +++ b/includes/components/SmartAI/SmartAI.class.php @@ -22,7 +22,7 @@ trait SmartHelper private function numRange(int $min, int $max, bool $isTime) : string { if ($isTime) - return Util::createNumRange($min, $max, ' – ', fn($x) => Util::formatTime($x, true)); + return Util::createNumRange($min, $max, ' – ', fn($x) => DateTime::formatTimeElapsed($x)); return Util::createNumRange($min, $max, ' – '); } @@ -32,7 +32,7 @@ trait SmartHelper if (!$time) return ''; - return Util::formatTime($time * ($isMilliSec ? 1 : 1000), false); + return DateTime::formatTimeElapsedFloat($time * ($isMilliSec ? 1 : 1000)); } private function castFlags(int $flags) : string diff --git a/includes/components/dbtypelist.class.php b/includes/components/dbtypelist.class.php index 204a8319..95542533 100644 --- a/includes/components/dbtypelist.class.php +++ b/includes/components/dbtypelist.class.php @@ -653,7 +653,7 @@ trait spawnHelper $label = [Lang::npc('waypoint').Lang::main('colon').$p['point']]; if ($p['wait']) - $label[] = Lang::npc('wait').Lang::main('colon').Util::formatTime($p['wait'], false); + $label[] = Lang::npc('wait').Lang::main('colon').DateTime::formatTimeElapsedFloat($p['wait']); $opts = array( // \0 doesn't get printed and tricks Util::toJSON() into handling this as a string .. i feel slightly dirty now 'label' => "\0$
    ".implode('
    ', $label).'
    ', diff --git a/includes/components/response/baseresponse.class.php b/includes/components/response/baseresponse.class.php index 37761981..7e32969d 100644 --- a/includes/components/response/baseresponse.class.php +++ b/includes/components/response/baseresponse.class.php @@ -19,7 +19,7 @@ trait TrRecoveryHelper // check if already processing if ($_ = DB::Aowow()->selectCell('SELECT `statusTimer` - UNIX_TIMESTAMP() FROM ?_account WHERE `email` = ? AND `status` > ?d AND `statusTimer` > UNIX_TIMESTAMP()', $email, ACC_STATUS_NEW)) - return Lang::account('inputbox', 'error', 'isRecovering', [Util::formatTime($_ * 1000)]); + return Lang::account('inputbox', 'error', 'isRecovering', [DateTime::formatTimeElapsed($_ * 1000)]); // create new token and write to db $token = Util::createHash(); diff --git a/includes/components/response/templateresponse.class.php b/includes/components/response/templateresponse.class.php index 6353e87d..ce53910f 100644 --- a/includes/components/response/templateresponse.class.php +++ b/includes/components/response/templateresponse.class.php @@ -228,9 +228,9 @@ class TemplateResponse extends BaseResponse { if (User::isInGroup(U_GROUP_EMPLOYEE)) { - $stats['time'] = Util::formatTime((microtime(true) - self::$time) * 1000, true); - $stats['sql'] = ['count' => parent::$sql['count'], 'time' => Util::formatTime(parent::$sql['time'] * 1000, true)]; - $stats['cache'] = !empty(static::$cacheStats) ? [static::$cacheStats[0], Util::formatTimeDiff(static::$cacheStats[1])] : null; + $stats['time'] = DateTime::formatTimeElapsed((microtime(true) - self::$time) * 1000); + $stats['sql'] = ['count' => parent::$sql['count'], 'time' => DateTime::formatTimeElapsed(parent::$sql['time'] * 1000)]; + $stats['cache'] = !empty(static::$cacheStats) ? [static::$cacheStats[0], (new DateTime())->formatDate(static::$cacheStats[1])] : null; } else $stats = []; diff --git a/includes/components/response/textresponse.class.php b/includes/components/response/textresponse.class.php index 57e5faf0..cfcc3167 100644 --- a/includes/components/response/textresponse.class.php +++ b/includes/components/response/textresponse.class.php @@ -149,14 +149,14 @@ class TextResponse extends BaseResponse $this->sumSQLStats(); echo "/*\n"; - echo " * generated in ".Util::formatTime((microtime(true) - self::$time) * 1000)."\n"; - echo " * " . parent::$sql['count'] . " SQL queries in " . Util::formatTime(parent::$sql['time'] * 1000) . "\n"; + echo " * generated in ".DateTime::formatTimeElapsedFloat((microtime(true) - self::$time) * 1000)."\n"; + echo " * " . parent::$sql['count'] . " SQL queries in " . DateTime::formatTimeElapsedFloat(parent::$sql['time'] * 1000) . "\n"; if ($this instanceof ICache && static::$cacheStats) { [$mode, $set, $lifetime] = static::$cacheStats; echo " * stored in " . ($mode == CACHE_MODE_MEMCACHED ? 'Memcached' : 'filecache') . ":\n"; - echo " * + ".date('c', $set) . ' - ' . Util::formatTimeDiff($set) . "\n"; - echo " * - ".date('c', $set + $lifetime) . ' - in '.Util::formatTime(($set + $lifetime - time()) * 1000) . "\n"; + echo " * + ".date('c', $set) . ' - ' . DateTime::formatTimeElapsedFloat((time() - $set) * 1000) . " ago\n"; + echo " * - ".date('c', $set + $lifetime) . ' - in '.DateTime::formatTimeElapsedFloat(($set + $lifetime - time()) * 1000) . "\n"; } echo " */\n\n"; } diff --git a/includes/dbtypes/item.class.php b/includes/dbtypes/item.class.php index ceca727c..901cf289 100644 --- a/includes/dbtypes/item.class.php +++ b/includes/dbtypes/item.class.php @@ -965,7 +965,7 @@ class ItemList extends DBTypeList $extra = []; if ($cd >= 5000 && $this->curTpl['spellTrigger'.$j] != SPELL_TRIGGER_EQUIP) { - $pt = Util::parseTime($cd); + $pt = DateTime::parse($cd); if (count(array_filter($pt)) == 1) // simple time: use simple method $extra[] = Lang::formatTime($cd, 'item', 'cooldown'); else // build block with generic time diff --git a/includes/dbtypes/spell.class.php b/includes/dbtypes/spell.class.php index aabf72e0..032f0e6b 100644 --- a/includes/dbtypes/spell.class.php +++ b/includes/dbtypes/spell.class.php @@ -668,7 +668,7 @@ class SpellList extends DBTypeList else if ($noInstant && !in_array($this->curTpl['typeCat'], [11, 7, -3, -6, -8, 0]) && !($this->curTpl['cuFlags'] & SPELL_CU_TALENTSPELL)) return ''; else - return $short ? Lang::formatTime($this->curTpl['castTime'] * 1000, 'spell', 'castTime') : Util::formatTime($this->curTpl['castTime'] * 1000); + return $short ? Lang::formatTime($this->curTpl['castTime'] * 1000, 'spell', 'castTime') : DateTime::formatTimeElapsedFloat($this->curTpl['castTime'] * 1000); } private function createCooldownForCurrent() : string diff --git a/includes/kernel.php b/includes/kernel.php index b2f43938..8f1c9780 100644 --- a/includes/kernel.php +++ b/includes/kernel.php @@ -32,6 +32,7 @@ if ($error) require_once 'includes/defines.php'; require_once 'includes/locale.class.php'; require_once 'localization/lang.class.php'; +require_once 'localization/datetime.class.php'; require_once 'includes/libs/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 diff --git a/includes/utilities.php b/includes/utilities.php index 97de57e8..e4e8aff6 100644 --- a/includes/utilities.php +++ b/includes/utilities.php @@ -148,110 +148,6 @@ abstract class Util return $money; } - public static function parseTime(int $msec) : array - { - $time = [0, 0, 0, 0, 0]; - - if ($_ = ($msec % 1000)) - $time[0] = $_; - - $sec = $msec / 1000; - - if ($sec >= 3600 * 24) - { - $time[4] = floor($sec / 3600 / 24); - $sec -= $time[4] * 3600 * 24; - } - - if ($sec >= 3600) - { - $time[3] = floor($sec / 3600); - $sec -= $time[3] * 3600; - } - - if ($sec >= 60) - { - $time[2] = floor($sec / 60); - $sec -= $time[2] * 60; - } - - if ($sec > 0) - { - $time[1] = (int)$sec; - $sec -= $time[1]; - } - - return $time; - } - - public static function formatTime(int $msec, bool $short = false) : string - { - [$ms, $s, $m, $h, $d] = self::parseTime(abs($msec)); - // \u00A0 is  , but also usable by js - - if ($short) - { - if ($_ = round($d / 365)) - return $_."\u{00A0}".Lang::timeUnits('ab', 0); - if ($_ = round($d / 30)) - return $_."\u{00A0}".Lang::timeUnits('ab', 1); - if ($_ = round($d / 7)) - return $_."\u{00A0}".Lang::timeUnits('ab', 2); - if ($_ = round($d)) - return $_."\u{00A0}".Lang::timeUnits('ab', 3); - if ($_ = round($h)) - return $_."\u{00A0}".Lang::timeUnits('ab', 4); - if ($_ = round($m)) - return $_."\u{00A0}".Lang::timeUnits('ab', 5); - if ($_ = round($s + $ms / 1000, 2)) - return $_."\u{00A0}".Lang::timeUnits('ab', 6); - if ($ms) - return $ms."\u{00A0}".Lang::timeUnits('ab', 7); - - return "0\u{00A0}".Lang::timeUnits('ab', 6); - } - else - { - $_ = $d + $h / 24; - if ($_ > 1 && !($_ % 365)) // whole years - return round(($d + $h / 24) / 365, 2)."\u{00A0}".Lang::timeUnits($d / 365 == 1 && !$h ? 'sg' : 'pl', 0); - if ($_ > 1 && !($_ % 30)) // whole months - return round(($d + $h / 24) / 30, 2)."\u{00A0}".Lang::timeUnits($d / 30 == 1 && !$h ? 'sg' : 'pl', 1); - if ($_ > 1 && !($_ % 7)) // whole weeks - return round(($d + $h / 24) / 7, 2)."\u{00A0}".Lang::timeUnits($d / 7 == 1 && !$h ? 'sg' : 'pl', 2); - if ($d) - return round($d + $h / 24, 2)."\u{00A0}".Lang::timeUnits($d == 1 && !$h ? 'sg' : 'pl', 3); - if ($h) - return round($h + $m / 60, 2)."\u{00A0}".Lang::timeUnits($h == 1 && !$m ? 'sg' : 'pl', 4); - if ($m) - return round($m + $s / 60, 2)."\u{00A0}".Lang::timeUnits($m == 1 && !$s ? 'sg' : 'pl', 5); - if ($s) - return round($s + $ms / 1000, 2)."\u{00A0}".Lang::timeUnits($s == 1 && !$ms ? 'sg' : 'pl', 6); - if ($ms) - return $ms."\u{00A0}".Lang::timeUnits($ms == 1 ? 'sg' : 'pl', 7); - - return "0\u{00A0}".Lang::timeUnits('pl', 6); - } - } - - public static function formatTimeDiff(int $sec) : string - { - $delta = abs(time() - $sec); - - [, $s, $m, $h, $d] = self::parseTime($delta * 1000); - - if ($delta > (1 * MONTH)) // use absolute - return date(Lang::main('dateFmtLong'), $sec); - else if ($delta > (2 * DAY)) // days ago - return Lang::main('timeAgo', [$d . ' ' . Lang::timeUnits('pl', 3)]); - else if ($h) // hours, minutes ago - return Lang::main('timeAgo', [$h . ' ' . Lang::timeUnits('ab', 4) . ' ' . $m . ' ' . Lang::timeUnits('ab', 5)]); - else if ($m) // minutes, seconds ago - return Lang::main('timeAgo', [$m . ' ' . Lang::timeUnits('ab', 5) . ' ' . $s . ' ' . Lang::timeUnits('ab', 6)]); - else // seconds ago - return Lang::main('timeAgo', [$s . ' ' . Lang::timeUnits($s == 1 ? 'sg' : 'pl', 6)]); - } - // pageTexts, questTexts and mails public static function parseHtmlText(string|array $text, bool $markdown = false) : string|array { @@ -1264,7 +1160,7 @@ abstract class Util if ($expiration) { $vars += array_fill(0, 9, null); // vsprintf requires all unused indizes to also be set... - $vars[9] = Util::formatTime($expiration * 1000); + $vars[9] = DateTime::formatTimeElapsed($expiration * 1000, 0); } if ($vars) diff --git a/localization/datetime.class.php b/localization/datetime.class.php new file mode 100644 index 00000000..f6789390 --- /dev/null +++ b/localization/datetime.class.php @@ -0,0 +1,218 @@ +getTimestamp() - $timestamp); + + $today = new DateTime(); + $eventDay = new DateTime(time() - $elapsed); + + $todayMidnight = $today->setTime(0, 0); + $eventDayMidnight = $eventDay->setTime(0, 0); + + $delta = $todayMidnight->diff($eventDayMidnight, true)->days; + + if ($elapsed >= 2592000) /* More than a month ago */ + $txt = Lang::main('date_on') . $eventDay->formatDateSimple($withTime); + else if ($delta > 1) + $txt = Lang::main('ddaysago', [$delta]); + else if ($elapsed >= 43200) + { + if ($today->format('j') == $eventDay->format('j')) + $txt = Lang::main('today'); + else + $txt = Lang::main('yesterday'); + + $txt = $eventDay->formatTimeSimple($txt); + } + else /* Less than 12 hours ago */ + $txt = Lang::main('date_ago', [self::formatTimeElapsed($elapsed * 1000)]); + + return $txt; + } + + /** + * Human-readable format of a date. Optionally append time of day. + * + * @param bool $withTime [optional] affixes day time + * @return string a formatted date string. + */ + public function formatDateSimple(bool $withTime = false) : string + { + $txt = ''; + $day = $this->format('d'); + $month = $this->format('m'); + $year = $this->format('Y'); + + if ($year <= 1970) + $txt .= Lang::main('unknowndate_stc'); + else + $txt .= Lang::main('date_simple', [$day, $month, $year]); + + if ($withTime) + $txt = $this->formatTimeSimple($txt); + + return $txt; + } + + /** + * Human-readable format of the time of day. + * + * @param string $txt [optional] text to affeix the day time to + * @param bool $noPrefix [optional] don't use " at " to affix time of day to $txt + * @return string a formatted time of day string. + */ + public function formatTimeSimple(string $txt = '', bool $noPrefix = false) : string + { + $hours = $this->format('G'); + $minutes = $this->format('i'); + + $txt .= ($noPrefix ? ' ' : Lang::main('date_at')); + + if ($hours == 12) + $txt .= Lang::main('noon'); + else if ($hours == 0) + $txt .= Lang::main('midnight'); + else if ($hours > 12) + $txt .= ($hours - 12) . ':' . $minutes . ' ' . Lang::main('pm'); + else + $txt .= $hours . ':' . $minutes . ' ' . Lang::main('am'); + + return $txt; + } + + /** + * Calculate component values from timestamp + * + * @param int $msec time in milliseconds to parse + * @return int[] [msec, sec, min, hr, day] + */ + public static function parse(int $msec) : array + { + $time = [0, 0, 0, 0, 0]; + $msec = abs($msec); + + for ($i = 3; $i < count(self::RANGE); ++$i) + { + if ($msec < self::RANGE[$i]) + continue; + + $time[7 - $i] = intVal($msec / self::RANGE[$i]); + $msec %= self::RANGE[$i]; + } + + return $time; + } + + /** + * Human-readable longform format of a timespan. + * + * @param int $delay time in milliseconds to format + * @return string a formatted time string. If an error occured "n/a" (localized) is returned + */ + public static function formatTimeElapsedFloat(int $delay) : string + { + $delay = max($delay, 1); + + for ($i = 0; $i < count(self::RANGE); ++$i) + { + if ($delay < self::RANGE[$i]) + continue; + + $v = round($delay / self::RANGE[$i], 2); + return $v . self::NBSP . Lang::timeUnits($v === 1.0 ? 'sg' : 'pl', $i); + } + + return Lang::main('n_a'); + } + + /** + * Human-readable format of a timespan. + * + * @param int $delay time in milliseconds to format + * @param int $maxRange [optional] time unit index - 0 (year) ... 7 (milliseconds) + * @return string a formatted time string. If an error occured "n/a" (localized) is returned + */ + public static function formatTimeElapsed(int $delay, int $maxRange = 3) : string + { + if ($maxRange > 7 || $maxRange < 0) + $maxRange = 3; // default: days + + $subunit = [1, 3, 3, -1, 5, -1, 7, -1]; + $delay = max($delay, 1); + + for ($i = $maxRange; $i < count(self::RANGE); ++$i) + { + if ($delay >= self::RANGE[$i]) + { + $i1 = $i; + $v1 = floor($delay / self::RANGE[$i1]); + + if ($subunit[$i1] != -1) + { + $i2 = $subunit[$i1]; + $delay %= self::RANGE[$i1]; + $v2 = floor($delay / self::RANGE[$i2]); + + if ($v2 > 0) + return self::OMG($v1, $i1, true) . self::NBSP . self::OMG($v2, $i2, true); + } + + return self::OMG($v1, $i1, false); + } + } + + return Lang::main('n_a'); + } + + /** + * internal number formatter + * + * @param int $value unit value + * @param int $unit time unit index 0 (year) ... 7 (milliseconds) + * @param bool $abbrv use abbreviation + * @return string value + unit + */ + private static function OMG(int $value, int $unit, bool $abbrv) : string + { + if ($abbrv && !Lang::timeUnits('ab', $unit)) + $abbrv = false; + + return $value .= self::NBSP . match(true) + { + $abbrv => Lang::timeUnits('ab', $unit), + $value == 1 => Lang::timeUnits('sg', $unit), + default => Lang::timeUnits('pl', $unit) + }; + } +} + +?> diff --git a/localization/lang.class.php b/localization/lang.class.php index bdedb1c1..48e4dc1b 100644 --- a/localization/lang.class.php +++ b/localization/lang.class.php @@ -535,7 +535,7 @@ class Lang if ($msec < 0) $msec = 0; - $time = Util::parseTime($msec); // [$ms, $s, $m, $h, $d] + $time = DateTime::parse($msec); // [$ms, $s, $m, $h, $d] $mult = [0, 1000, 60, 60, 24]; $total = 0; $ref = []; @@ -552,33 +552,22 @@ class Lang if (!$msec) return self::vspf($ref[0], [0]); - if ($concat) - { - for ($i = 4; $i > 0; $i--) - { - $total += $time[$i]; - if (isset($ref[$i]) && ($total || ($i == 1 && !$result))) - { - $result[] = self::vspf($ref[$i], [$total]); - $total = 0; - } - else - $total *= $mult[$i]; - } - - return implode(', ', $result); - } - for ($i = 4; $i > 0; $i--) { $total += $time[$i]; - if (isset($ref[$i]) && ($total || $i == 1)) - return self::vspf($ref[$i], [$total + ($time[$i-1] ?? 0) / $mult[$i]]); + if (isset($ref[$i]) && ($total || ($i == 1 && !$result))) + { + if (!$concat) + return self::vspf($ref[$i], [$total + ($time[$i-1] ?? 0) / $mult[$i]]); + + $result[] = self::vspf($ref[$i], [$total]); + $total = 0; + } else $total *= $mult[$i]; } - return ''; + return implode(', ', $result); } private static function vspf(null|array|string $var, array $args = []) : null|array|string diff --git a/localization/locale_dede.php b/localization/locale_dede.php index d55c252a..956604e2 100644 --- a/localization/locale_dede.php +++ b/localization/locale_dede.php @@ -134,8 +134,25 @@ $lang = array( 'dateFmtShort' => "d.m.Y", 'dateFmtLong' => "d.m.Y \u\m H:i", 'dateFmtIntl' => "d. MMMM y", - 'timeAgo' => 'vor %s', 'nfSeparators' => ['.', ','], + 'n_a' => "n. v.", + + // date time + 'date' => "Datum", + 'date_colon' => "Datum: ", + 'date_on' => "am ", + 'date_ago' => "vor %s", + 'date_at' => " um ", + 'date_to' => " bis ", + 'date_simple' => '%1$d.%2$d.%3$d', + 'unknowndate' => "Unbekanntes Datum", + 'ddaysago' => "vor %d Tagen", + 'today' => "heute", + 'yesterday' => "gestern", + 'noon' => "Mittag", + 'midnight' => "Mitternacht", + 'am' => "vormittags", + 'pm' => "nachmittags", // error 'intError' => "Ein interner Fehler ist aufgetreten.", @@ -1656,7 +1673,6 @@ $lang = array( '_rankRange' => "Rang: %d - %d", '_showXmore' => "Zeige %d weitere", - 'n_a' => "n. v.", 'normal' => "Normal", 'special' => "Besonders", diff --git a/localization/locale_enus.php b/localization/locale_enus.php index d306d84a..8361d2b4 100644 --- a/localization/locale_enus.php +++ b/localization/locale_enus.php @@ -134,8 +134,25 @@ $lang = array( 'dateFmtShort' => "Y/m/d", 'dateFmtLong' => "Y/m/d \a\\t g:i A", 'dateFmtIntl' => "MMMM d, y", - 'timeAgo' => "%s ago", 'nfSeparators' => [',', '.'], + 'n_a' => "n/a", + + // date time + 'date' => "Date", + 'date_colon' => "Date: ", + 'date_on' => "on ", + 'date_ago' => "%s ago", + 'date_at' => " at ", + 'date_to' => " to ", + 'date_simple' => '%2$d/%1$d/%3$d', + 'unknowndate' => "Unknown date", + 'ddaysago' => "%d days ago", + 'today' => "today", + 'yesterday' => "yesterday", + 'noon' => "noon", + 'midnight' => "midnight", + 'am' => "AM", + 'pm' => "PM", // error 'intError' => "An internal error has occurred.", @@ -1656,7 +1673,6 @@ $lang = array( '_rankRange' => "Rank: %d - %d", '_showXmore' => "Show %d More", - 'n_a' => "n/a", 'normal' => "Normal", 'special' => "Special", diff --git a/localization/locale_eses.php b/localization/locale_eses.php index e78c918e..d1687ea0 100644 --- a/localization/locale_eses.php +++ b/localization/locale_eses.php @@ -134,8 +134,25 @@ $lang = array( 'dateFmtShort' => "d/m/Y", 'dateFmtLong' => "d/m/Y \a \l\a\s g:i A", 'dateFmtIntl' => "d 'de' MMMM 'de' y", - 'timeAgo' => 'hace %s', 'nfSeparators' => ['.', ','], + 'n_a' => "n/d", + + // date time + 'date' => "Fecha", + 'date_colon' => "Fecha: ", + 'date_on' => "el ", + 'date_ago' => "hace %s", + 'date_at' => " a las ", + 'date_to' => " al ", + 'date_simple' => '%1$d/%2$d/%3$d', + 'unknowndate' => "Fecha desconocida", + 'ddaysago' => "Hace %d días", + 'today' => "hoy", + 'yesterday' => "ayer", + 'noon' => "medio día", + 'midnight' => "medianoche", + 'am' => "a.m.", + 'pm' => "p.m.", // error 'intError' => "Un error interno ha ocurrido.", @@ -1656,7 +1673,6 @@ $lang = array( '_rankRange' => "Rango: %d - %d", '_showXmore' => "Mostrar %d más", - 'n_a' => "n/d", 'normal' => "Normal", 'special' => "Especial", diff --git a/localization/locale_frfr.php b/localization/locale_frfr.php index 52b16a3b..3da25911 100644 --- a/localization/locale_frfr.php +++ b/localization/locale_frfr.php @@ -134,8 +134,25 @@ $lang = array( 'dateFmtShort' => "Y-m-d", 'dateFmtLong' => "Y-m-d à g:i A", 'dateFmtIntl' => "d MMMM y", - 'timeAgo' => 'il y a %s', 'nfSeparators' => [' ', ','], + 'n_a' => "n/d", + + // date time + 'date' => "Date", + 'date_colon' => "Date : ", + 'date_on' => "le ", + 'date_ago' => "il y a %s", + 'date_at' => " à ", + 'date_to' => " à ", + 'date_simple' => '%1$d-%2$d-%3$d', + 'unknowndate' => "Date inconnue", + 'ddaysago' => "%d jours avant", + 'today' => "aujourd'hui", + 'yesterday' => "hier", + 'noon' => "midi", + 'midnight' => "minuit", + 'am' => "AM", + 'pm' => "PM", // error 'intError' => "[An internal error occured.]", @@ -1656,7 +1673,6 @@ $lang = array( '_rankRange' => "Rang : %d - %d", '_showXmore' => "En afficher %d de plus", - 'n_a' => "n/d", 'normal' => "Standard", 'special' => "Spécial", diff --git a/localization/locale_ruru.php b/localization/locale_ruru.php index 51b53869..e2466a5a 100644 --- a/localization/locale_ruru.php +++ b/localization/locale_ruru.php @@ -134,8 +134,25 @@ $lang = array( 'dateFmtShort' => "Y-m-d", 'dateFmtLong' => "Y-m-d в g:i A", 'dateFmtIntl' => "d MMMM y г.", - 'timeAgo' => '%s назад', 'nfSeparators' => [' ', ','], + 'n_a' => "нет", + + // date time + 'date' => "По дате", + 'date_colon' => "Дата: ", + 'date_on' => "на ", + 'date_ago' => "%s назад", + 'date_at' => " в ", + 'date_to' => " в ", + 'date_simple' => '%1$d.%2$d.%3$d', + 'unknowndate' => "Неизвестная дата", + 'ddaysago' => "%d дней назад", + 'today' => "сегодня", + 'yesterday' => "вчера", + 'noon' => "полдень", + 'midnight' => "полночь", + 'am' => "a.m.", + 'pm' => "p.m.", // error 'intError' => "[An internal error occured.]", @@ -1656,7 +1673,6 @@ $lang = array( '_rankRange' => "Ранг: %d - %d", '_showXmore' => "Показать на %d больше", - 'n_a' => "нет", 'normal' => "Обычный", 'special' => "Особый", diff --git a/localization/locale_zhcn.php b/localization/locale_zhcn.php index e3eb62f3..fbc1908e 100644 --- a/localization/locale_zhcn.php +++ b/localization/locale_zhcn.php @@ -134,8 +134,25 @@ $lang = array( 'dateFmtShort' => "Y/m/d", 'dateFmtLong' => "Y/m/d \a\\t g:i A", 'dateFmtIntl' => "y年M月d日", - 'timeAgo' => '%s之前', 'nfSeparators' => [',', '.'], + 'n_a' => "n/a", + + // date time + 'date' => "日期", + 'date_colon' => "日期:", + 'date_on' => "在 ", + 'date_ago' => "%s前", + 'date_at' => "于", + 'date_to' => "至", + 'date_simple' => '%3$d/%2$d/%1$d', + 'unknowndate' => "未知日期", + 'ddaysago' => "%d天前", + 'today' => "今日", + 'yesterday' => "昨天", + 'noon' => "正午", + 'midnight' => "午夜", + 'am' => "AM", + 'pm' => "PM", // error 'intError' => "发生内部错误。", @@ -1656,7 +1673,6 @@ $lang = array( '_rankRange' => "排名: %d - %d", '_showXmore' => "[Show %d More]", - 'n_a' => "n/a", 'normal' => "普通", 'special' => "特殊", diff --git a/template/pages/spell.tpl.php b/template/pages/spell.tpl.php index 476e3f05..c03f6ad1 100644 --- a/template/pages/spell.tpl.php +++ b/template/pages/spell.tpl.php @@ -92,23 +92,23 @@ endif; - duration ?: ''.Lang::spell('n_a').'');?> + duration ?: ''.Lang::main('n_a').'');?> - school ?: ''.Lang::spell('n_a').'');?> + school ?: ''.Lang::main('n_a').'');?> - mechanic ?:''.Lang::spell('n_a').'');?> + mechanic ?:''.Lang::main('n_a').'');?> - dispel ?: ''.Lang::spell('n_a').'');?> + dispel ?: ''.Lang::main('n_a').'');?> - gcdCat ?: ''.Lang::spell('n_a').'');?> + gcdCat ?: ''.Lang::main('n_a').'');?> @@ -127,7 +127,7 @@ endif; - cooldown ?: ''.Lang::spell('n_a').'');?> + cooldown ?: ''.Lang::main('n_a').'');?> '.Lang::spell('_gcd');?> From 82f36fd34270459363f57d5afa24768ac6787f37 Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Sat, 15 Nov 2025 19:56:11 +0100 Subject: [PATCH 112/260] Setup/Source * generally flag items of quality artifact as unavailable * 04f3aa7a8232dbd0ba640f159a3823b76db66a93 caused some items transformed by spell to be 'available' --- setup/sql/updates/1763168697_01.sql | 1 + setup/tools/sqlgen/source.ss.php | 10 ++++++++-- 2 files changed, 9 insertions(+), 2 deletions(-) create mode 100644 setup/sql/updates/1763168697_01.sql diff --git a/setup/sql/updates/1763168697_01.sql b/setup/sql/updates/1763168697_01.sql new file mode 100644 index 00000000..bef0e1a9 --- /dev/null +++ b/setup/sql/updates/1763168697_01.sql @@ -0,0 +1 @@ +UPDATE `aowow_dbversion` SET `sql` = CONCAT(IFNULL(`sql`, ''), ' source'); diff --git a/setup/tools/sqlgen/source.ss.php b/setup/tools/sqlgen/source.ss.php index 627afed5..a80c982a 100644 --- a/setup/tools/sqlgen/source.ss.php +++ b/setup/tools/sqlgen/source.ss.php @@ -183,9 +183,15 @@ CLISetup::registerSetup("sql", new class extends SetupScript !empty($this->disables[Type::SPELL]) ? array_values($this->disables[Type::SPELL]) : DBSIMPLE_SKIP ); - // flagging aowow_items for source (note: this is not exact! creatures dropping items may not be spawnd, quests granting items may be disabled) + // flagging aowow_items for source (note: this is not exact! creatures dropping items may not be spawned, etc.) DB::Aowow()->query('UPDATE ?_items SET `cuFlags` = `cuFlags` & ?d', ~CUSTOM_UNAVAILABLE); - DB::Aowow()->query('UPDATE ?_items i LEFT JOIN ?_source s ON s.`typeId` = i.`id` AND s.`type` = ?d SET i.`cuFlags` = i.`cuFlags` | ?d WHERE s.`typeId` IS NULL AND i.`id` NOT IN (?a)', Type::ITEM, CUSTOM_UNAVAILABLE, $itemSpellSource); + DB::Aowow()->query( + 'UPDATE ?_items i + LEFT JOIN ?_source s ON s.`typeId` = i.`id` AND s.`type` = ?d + SET i.`cuFlags` = i.`cuFlags` | ?d + WHERE (s.`typeId` IS NULL AND i.`id` NOT IN (?a)) OR i.`quality` = ?d', + Type::ITEM, CUSTOM_UNAVAILABLE, $itemSpellSource, ITEM_QUALITY_ARTIFACT + ); return true; } From 103287f91b163b87c45c92db0ceb23d08bf93412 Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Sat, 15 Nov 2025 19:57:21 +0100 Subject: [PATCH 113/260] Setup/Pets * move custom data from pet script to db --- setup/sql/02-db_initial_data.sql | 4 +- setup/sql/updates/1763200071_01.sql | 22 +++++++ setup/tools/sqlgen/pet.ss.php | 95 ++++++++++++++--------------- 3 files changed, 70 insertions(+), 51 deletions(-) create mode 100644 setup/sql/updates/1763200071_01.sql diff --git a/setup/sql/02-db_initial_data.sql b/setup/sql/02-db_initial_data.sql index 0f64d138..5c5bdab3 100644 --- a/setup/sql/02-db_initial_data.sql +++ b/setup/sql/02-db_initial_data.sql @@ -71,7 +71,7 @@ UNLOCK TABLES; LOCK TABLES `aowow_dbversion` WRITE; /*!40000 ALTER TABLE `aowow_dbversion` DISABLE KEYS */; -INSERT INTO `aowow_dbversion` VALUES (1762700148,0,NULL,NULL); +INSERT INTO `aowow_dbversion` VALUES (1763200072,0,NULL,NULL); /*!40000 ALTER TABLE `aowow_dbversion` ENABLE KEYS */; UNLOCK TABLES; @@ -142,7 +142,7 @@ UNLOCK TABLES; LOCK TABLES `aowow_setup_custom_data` WRITE; /*!40000 ALTER TABLE `aowow_setup_custom_data` DISABLE KEYS */; -INSERT INTO `aowow_setup_custom_data` VALUES ('zones',2257,'cuFlags','0','Deeprun Tram - make visible'),('zones',2257,'category','0','Deeprun Tram - Category: Eastern Kingdoms'),('zones',2257,'type','1','Deeprun Tram - Type: Transit'),('zones',3698,'expansion','1','Nagrand Arena - Addon: BC'),('zones',3702,'expansion','1','Blades Edge Arena - Addon: BC'),('zones',3968,'expansion','1','Ruins of Lordaeron Arena - Addon: BC'),('zones',4378,'expansion','1','Dalaran Arena - Addon: WotLK'),('zones',4406,'expansion','1','Ring of Valor Arena - Addon: WotLK'),('zones',2597,'maxPlayer','40','Alterac Valey - Players: 40 [battlemasterlist.dbc: 5]'),('zones',4710,'maxPlayer','40','Isle of Conquest - Players: 40 [battlemasterlist.dbc: 5]'),('zones',4893,'cuFlags','1073741824','The Frost Queen\'s Lair - set: CUSTOM_EXCLUDE_FOR_LISTVIEW'),('zones',4894,'cuFlags','1073741824','Putricide\'s Laboratory [..] - set: CUSTOM_EXCLUDE_FOR_LISTVIEW'),('achievement',1956,'itemExtra','44738','Higher Learning - item rewarded through gossip'),('zones',4895,'cuFlags','1073741824','The Crimson Hall - set: CUSTOM_EXCLUDE_FOR_LISTVIEW'),('titles',137,'gender','2','Matron - female'),('zones',4896,'cuFlags','1073741824','The Frozen Throne - set: CUSTOM_EXCLUDE_FOR_LISTVIEW'),('zones',4897,'cuFlags','1073741824','The Sanctum of Blood - set: CUSTOM_EXCLUDE_FOR_LISTVIEW'),('zones',4076,'cuFlags','1073741824','Reuse Me 7 - set: CUSTOM_EXCLUDE_FOR_LISTVIEW'),('zones',207,'cuFlags','1073741824','The Great Sea - set: CUSTOM_EXCLUDE_FOR_LISTVIEW'),('zones',208,'cuFlags','1073741824','Unused Ironcladcove - set: CUSTOM_EXCLUDE_FOR_LISTVIEW'),('zones',2817,'levelMin','74','Crystalsong Forest - missing lfgDungeons entry'),('zones',1417,'cuFlags','1073741824','Sunken Temple [extra area on map 109] - set: CUSTOM_EXCLUDE_FOR_LISTVIEW'),('zones',22,'cuFlags','1073741824','Programmer Isle - set: CUSTOM_EXCLUDE_FOR_LISTVIEW'),('zones',151,'cuFlags','1073741824','Designer Island - set: CUSTOM_EXCLUDE_FOR_LISTVIEW'),('zones',3948,'cuFlags','1073741824','Brian and Pat Test - set: CUSTOM_EXCLUDE_FOR_LISTVIEW'),('zones',4019,'cuFlags','1073741824','Development Land - set: CUSTOM_EXCLUDE_FOR_LISTVIEW'),('zones',3605,'cuFlags','1073741824','Hyjal Past [extra area on map 560] - set: CUSTOM_EXCLUDE_FOR_LISTVIEW'),('zones',3535,'cuFlags','1073741824','Hellfire Citadel [extra area on map 540] - set: CUSTOM_EXCLUDE_FOR_LISTVIEW'),('zones',41,'levelMin','50','Deadwind Pass - missing lfgDungeons entry'),('zones',41,'levelMax','60','Deadwind Pass - missing lfgDungeons entry'),('zones',2257,'levelMin','1','Deeprun Tram - missing lfgDungeons entry'),('zones',2257,'levelMax','80','Deeprun Tram - missing lfgDungeons entry'),('zones',4298,'category','0','Plaguelands: The Scarlet Enclave - Parent: Eastern Kingdoms'),('zones',4298,'levelMin','55','Plaguelands: The Scarlet Enclave - missing lfgDungeons entry'),('zones',4298,'levelMax','58','Plaguelands: The Scarlet Enclave - missing lfgDungeons entry'),('zones',493,'levelMin','15','Moonglade - missing lfgDungeons entry'),('zones',493,'levelMax','60','Moonglade - missing lfgDungeons entry'),('zones',2817,'levelMax','76','Crystalsong Forest - missing lfgDungeons entry'),('zones',4742,'levelMin','77','Hrothgar\'s Landing - missing lfgDungeons entry'),('zones',4742,'levelMax','80','Hrothgar\'s Landing - missing lfgDungeons entry'),('classes',1,'roles','10','Warrior - rngDPS'),('classes',2,'roles','11','Paladin - mleDPS + Tank + Heal'),('classes',3,'roles','4','Hunter - rngDPS'),('classes',4,'roles','2','Rogue - mleDPS'),('classes',5,'roles','5','Priest - rngDPS + Heal'),('classes',6,'roles','10','Death Knight - mleDPS + Tank'),('classes',7,'roles','7','Shaman - mleDPS + rngDPS + Heal'),('classes',8,'roles','4','Mage - rngDPS'),('classes',9,'roles','4','Warlock - rngDPS'),('classes',11,'roles','15','Druid - mleDPS + Tank + Heal + rngDPS'),('currencies',103,'cap','10000','Arena Points - cap'),('currencies',104,'cap','75000','Honor Points - cap'),('currencies',1,'cuFlags','1073741824','Currency Token Test Token 1 - set: CUSTOM_EXCLUDE_FOR_LISTVIEW'),('currencies',2,'cuFlags','1073741824','Currency Token Test Token 2 - set: CUSTOM_EXCLUDE_FOR_LISTVIEW'),('currencies',4,'cuFlags','1073741824','Currency Token Test Token 3 - set: CUSTOM_EXCLUDE_FOR_LISTVIEW'),('currencies',22,'cuFlags','1073741824','Birmingham Test Item 3 - set: CUSTOM_EXCLUDE_FOR_LISTVIEW'),('currencies',141,'cuFlags','1073741824','zzzOLDDaily Quest Faction Token - set: CUSTOM_EXCLUDE_FOR_LISTVIEW'),('currencies',1,'category','3','Currency Token Test Token 1 - category: unused'),('currencies',2,'category','3','Currency Token Test Token 2 - category: unused'),('currencies',4,'category','3','Currency Token Test Token 3 - category: unused'),('currencies',22,'category','3','Birmingham Test Item 3 - category: unused'),('currencies',141,'category','3','zzzOLDDaily Quest Faction Token - category: unused'),('factions',68,'qmNpcIds','33555','Undercity - set Quartermaster'),('factions',47,'qmNpcIds','33310','Ironforge - set Quartermaster'),('factions',69,'qmNpcIds','33653','Darnassus - set Quartermaster'),('factions',72,'qmNpcIds','33307','Stormwind - set Quartermaster'),('factions',76,'qmNpcIds','33553','Orgrimmar - set Quartermaster'),('factions',81,'qmNpcIds','33556','Thunder Bluff - set Quartermaster'),('factions',922,'qmNpcIds','16528','Tranquillien - set Quartermaster'),('factions',930,'qmNpcIds','33657','Exodar - set Quartermaster'),('factions',932,'qmNpcIds','19321','The Aldor - set Quartermaster'),('factions',933,'qmNpcIds','20242 23007','The Consortium - set Quartermaster'),('factions',935,'qmNpcIds','21432','The Sha\'tar - set Quartermaster'),('factions',941,'qmNpcIds','20241','The Mag\'har - set Quartermaster'),('factions',942,'qmNpcIds','17904','Cenarion Expedition - set Quartermaster'),('factions',946,'qmNpcIds','17657','Honor Hold - set Quartermaster'),('factions',947,'qmNpcIds','17585','Thrallmar - set Quartermaster'),('factions',970,'qmNpcIds','18382','Sporeggar - set Quartermaster'),('factions',978,'qmNpcIds','20240','Kurenai - set Quartermaster'),('factions',989,'qmNpcIds','21643','Keepers of Time - set Quartermaster'),('factions',1011,'qmNpcIds','21655','Lower City - set Quartermaster'),('factions',1012,'qmNpcIds','23159','Ashtongue Deathsworn - set Quartermaster'),('factions',1037,'qmNpcIds','32773 32564','Alliance Vanguard - set Quartermaster'),('factions',1038,'qmNpcIds','23428','Ogri\'la - set Quartermaster'),('factions',1052,'qmNpcIds','32774 32565','Horde Expedition - set Quartermaster'),('factions',1073,'qmNpcIds','31916 32763','The Kalu\'ak - set Quartermaster'),('factions',1090,'qmNpcIds','32287','Kirin Tor - set Quartermaster'),('factions',1091,'qmNpcIds','32533','The Wyrmrest Accord - set Quartermaster'),('factions',1094,'qmNpcIds','34881','The Silver Covenant - set Quartermaster'),('factions',1105,'qmNpcIds','31910','The Oracles - set Quartermaster'),('factions',1106,'qmNpcIds','30431','Argent Crusade - set Quartermaster'),('factions',1119,'qmNpcIds','32540','The Sons of Hodir - set Quartermaster'),('factions',1124,'qmNpcIds','34772','The Sunreavers - set Quartermaster'),('factions',1156,'qmNpcIds','37687','The Ashen Verdict - set Quartermaster'),('factions',1082,'cuFlags','1073741824','REUSE - set: CUSTOM_EXCLUDE_FOR_LISTVIEW'),('factions',952,'cuFlags','1073741824','Test Faction 3 - set: CUSTOM_EXCLUDE_FOR_LISTVIEW'),('titles',138,'gender','1','Patron - male'),('sounds',15407,'cat','10','UR_Algalon_Summon03 - is not an item pickup'),('shapeshiftforms',1,'displayIdH','8571','Cat Form - spellshapeshiftform.dbc missing displayId'),('shapeshiftforms',15,'displayIdH','8571','Creature - Cat - spellshapeshiftform.dbc missing displayId'),('shapeshiftforms',5,'displayIdH','2289','Bear Form - spellshapeshiftform.dbc missing displayId'),('shapeshiftforms',8,'displayIdH','2289','Dire Bear Form - spellshapeshiftform.dbc missing displayId'),('shapeshiftforms',14,'displayIdH','2289','Creature - Bear - spellshapeshiftform.dbc missing displayId'),('shapeshiftforms',27,'displayIdH','21244','Flight Form, Epic - spellshapeshiftform.dbc missing displayId'),('shapeshiftforms',29,'displayIdH','20872','Flight Form - spellshapeshiftform.dbc missing displayId'),('races',1,'leader','29611','Human - King Varian Wrynn'),('races',1,'factionId','72','Human - Stormwind'),('races',1,'startAreaId','12','Human - Elwynn Forest'),('races',2,'leader','4949','Orc - Thrall'),('races',2,'factionId','76','Orc - Orgrimmar'),('races',2,'startAreaId','14','Orc - Durotar'),('races',3,'leader','2784','Dwarf - King Magni Bronzebeard'),('races',3,'factionId','47','Dwarf - Ironforge'),('races',3,'startAreaId','1','Dwarf - Dun Morogh'),('races',4,'leader','7999','Night Elf - Tyrande Whisperwind'),('races',4,'factionId','69','Night Elf - Darnassus'),('races',4,'startAreaId','141','Night Elf - Teldrassil'),('races',5,'leader','10181','Undead - Lady Sylvanas Windrunner'),('races',5,'factionId','68','Undead - Undercity'),('races',5,'startAreaId','85','Undead - Tirisfal Glades'),('races',6,'leader','3057','Tauren - Cairne Bloodhoof'),('races',6,'factionId','81','Tauren - Thunder Bluff'),('races',6,'startAreaId','215','Tauren - Mulgore'),('races',7,'leader','7937','Gnome - High Tinker Mekkatorque'),('races',7,'factionId','54','Gnome - Gnomeregan Exiles'),('races',7,'startAreaId','1','Gnome - Dun Morogh'),('races',8,'leader','10540','Troll - Vol\'jin'),('races',8,'factionId','530','Troll - Darkspear Trolls'),('races',8,'startAreaId','14','Troll - Durotar'),('races',10,'leader','16802','Blood Elf - Lor\'themar Theron'),('races',10,'factionId','911','Blood Elf - Silvermoon City'),('races',10,'startAreaId','3430','Blood Elf - Eversong Woods'),('races',11,'leader','17468','Draenei - Prophet Velen'),('races',11,'factionId','930','Draenei - Exodar'),('races',11,'startAreaId','3524','Draenei - Azuremyst Isle'),('holidays',62,'iconString','inv_misc_missilelarge_red','Fireworks Spectacular'),('holidays',141,'iconString','calendar_winterveilstart','Feast of Winter Veil'),('holidays',181,'iconString','calendar_noblegardenstart','Noblegarden'),('holidays',201,'iconString','calendar_childrensweekstart','Children\'s Week'),('holidays',283,'iconString','inv_jewelry_necklace_21','Call to Arms: Alterac Valley'),('holidays',284,'iconString','inv_misc_rune_07','Call to Arms: Warsong Gulch'),('holidays',285,'iconString','inv_jewelry_amulet_07','Call to Arms: Arathi Basin'),('holidays',301,'iconString','calendar_fishingextravaganzastart','Stranglethorn Fishing Extravaganza'),('holidays',321,'iconString','calendar_harvestfestivalstart','Harvest Festival'),('holidays',324,'iconString','calendar_hallowsendstart','Hallow\'s End'),('holidays',327,'iconString','calendar_lunarfestivalstart','Lunar Festival'),('holidays',335,'iconString','calendar_loveintheairstart','Love is in the Air'),('holidays',341,'iconString','calendar_midsummerstart','Midsummer Fire Festival'),('holidays',353,'iconString','spell_nature_eyeofthestorm','Call to Arms: Eye of the Storm'),('holidays',372,'iconString','calendar_brewfeststart','Brewfest'),('holidays',374,'iconString','calendar_darkmoonfaireelwynnstart','Darkmoon Faire'),('holidays',375,'iconString','calendar_darkmoonfairemulgorestart','Darkmoon Faire'),('holidays',376,'iconString','calendar_darkmoonfaireterokkarstart','Darkmoon Faire'),('holidays',398,'iconString','calendar_piratesdaystart','Pirates\' Day'),('holidays',400,'iconString','achievement_bg_winsoa','Call to Arms: Strand of the Ancients'),('holidays',404,'iconString','calendar_harvestfestivalstart','Pilgrim\'s Bounty'),('holidays',406,'iconString','achievement_boss_lichking','Wrath of the Lich King Launch'),('holidays',409,'iconString','calendar_dayofthedeadstart','Day of the Dead'),('holidays',420,'iconString','achievement_bg_winwsg','Call to Arms: Isle of Conquest'),('holidays',423,'iconString','calendar_loveintheairstart','Love is in the Air'),('holidays',424,'iconString','calendar_fishingextravaganzastart','Kalu\'ak Fishing Derby'),('holidays',141,'achievementCatOrId','156','Feast of Winter Veil - Category: Feast of Winter Veil'),('holidays',181,'achievementCatOrId','159','Noblegarden - Category: Noblegarden'),('holidays',201,'achievementCatOrId','163','Children\'s Week - Category: Children\'s Week'),('holidays',324,'achievementCatOrId','158','Hallow\'s End - Category: Hallow\'s End'),('holidays',327,'achievementCatOrId','160','Lunar Festival - Category: Lunar Festival'),('holidays',341,'achievementCatOrId','161','Midsummer Fire Festival - Category: Midsummer Fire Festival'),('holidays',372,'achievementCatOrId','162','Brewfest - Category: Brewfest'),('holidays',398,'achievementCatOrId','-3457','Pirates\' Day - Achievement: The Captain\'s Booty'),('holidays',404,'achievementCatOrId','14981','Pilgrim\'s Bounty - Category: Pilgrim\'s Bounty'),('holidays',409,'achievementCatOrId','-3456','Day of the Dead - Achievement: Dead Man\'s Party'),('holidays',423,'achievementCatOrId','187','Love is in the Air - Category: Love is in the Air'),('holidays',324,'bossCreature','23682','Hallow\'s End - Headless Horseman'),('holidays',327,'bossCreature','15467','Lunar Festival - Omen'),('holidays',341,'bossCreature','25740','Midsummer Fire Festival - Ahune'),('holidays',372,'bossCreature','23872','Brewfest - Coren Direbrew'),('holidays',423,'bossCreature','36296','Love is in the Air - Apothecary Hummel'),('skillline',197,'professionMask','512','Tailoring'),('skillline',186,'professionMask','256','Mining'),('skillline',165,'specializations','10656 10658 10660','Leatherworking'),('skillline',165,'recipeSubClass','1','Leatherworking'),('skillline',165,'professionMask','128','Leatherworking'),('skillline',755,'recipeSubClass','10','Jewelcrafting'),('skillline',755,'professionMask','64','Jewelcrafting'),('skillline',129,'recipeSubClass','7','First Aid'),('skillline',129,'professionMask','32','First Aid'),('skillline',202,'specializations','20219 20222','Engineering'),('skillline',202,'recipeSubClass','3','Engineering'),('skillline',202,'professionMask','16','Engineering'),('skillline',333,'recipeSubClass','8','Enchanting'),('skillline',333,'professionMask','8','Enchanting'),('skillline',185,'recipeSubClass','5','Cooking'),('skillline',185,'professionMask','4','Cooking'),('skillline',164,'specializations','9788 9787 17041 17040 17039','Blacksmithing'),('skillline',164,'recipeSubClass','4','Blacksmithing'),('skillline',164,'professionMask','2','Blacksmithing'),('skillline',171,'specializations','28677 28675 28672','Alchemy'),('skillline',171,'recipeSubClass','6','Alchemy'),('skillline',171,'professionMask','1','Alchemy'),('skillline',393,'professionMask','0','Skinning'),('skillline',197,'recipeSubClass','2','Tailoring'),('skillline',197,'specializations','26798 26801 26797','Tailoring'),('skillline',356,'professionMask','1024','Fishing'),('skillline',356,'recipeSubClass','9','Fishing'),('skillline',182,'professionMask','2048','Herbalism'),('skillline',773,'professionMask','4096','Inscription'),('skillline',773,'recipeSubClass','11','Inscription'),('skillline',785,'name_loc0','Pet - Wasp','Pet - Wasp'),('skillline',781,'name_loc2','Familier - diablosaure exotique','Pet - Exotic Devlisaur'),('skillline',758,'name_loc6','Mascota: Evento - Control remoto','Pet - Event - Remote Control'),('skillline',758,'name_loc3','Tier - Ereignis Ferngesteuert','Pet - Event - Remote Control'),('skillline',758,'categoryId','7','Pet - Event - Remote Control - bring in line with other pets'),('skillline',788,'categoryId','7','Pet - Exotic Spirit Beast - bring in line with other pets'),('items',33147,'class','9','Formula: Enchant Cloak - Subtlety - Class: Recipes'),('items',33147,'subClass','8','Formula: Enchant Cloak - Subtlety - Subclass: Enchanting'),('currencies',1,'description_loc0','Text that describes this item can be found here.',''),('currencies',1,'description_loc2','Un texte qui décrit l\'objet figure ici.',''),('currencies',1,'description_loc3','Text, der den Gegenstand beschreibt, wird hier angezeigt.',''),('currencies',1,'description_loc6','Aquí puede encontrarse el texto que describe a este objeto.',''),('currencies',1,'description_loc8','Здесь находится описание предмета.',''),('currencies',61,'description_loc0','Tiffany Cartier\'s shop in Dalaran will gladly accept these tokens for unique jewelcrafting recipes.',''),('currencies',61,'description_loc2','La boutique de Tiffany Kartier, à Dalaran, accepte avec joie ces marques contre des dessins de joaillerie uniques.',''),('currencies',61,'description_loc3','Tiffany Cartiers Geschäft in Dalaran wird diese Symbole im Tausch gegen einzigartige Juweliersrezepte dankend annehmen.',''),('currencies',61,'description_loc4','达拉然的蒂凡妮·卡蒂亚会欣然接受这些代币,并用稀有的珠宝加工图鉴来交换。',''),('currencies',61,'description_loc6','La tienda de Tiffany Cartier en Dalaran cambiará gustosamente estos talismanes por recetas de joyería.',''),('currencies',61,'description_loc8','В магазине Тиффани Картье, что в Даларане, вам с радостью обменяют эти знаки на уникальные ювелирные эскизы.',''),('currencies',81,'description_loc0','Visit special cooking vendors in Dalaran and the capital cities to to purchase unusual cooking recipes, spices, and even a fine hat!',''),('currencies',81,'description_loc2','Rendez visite aux marchands de fournitures de cuisine à Dalaran et dans les autres capitales pour acheter des recettes de cuisine spéciales, des épices, et même une superbe toque !',''),('currencies',81,'description_loc3','Besucht besondere Kochhändler in Dalaran und den Hauptstädten, um ungewöhnliche Kochrezepte, Gewürze und sogar eine großartige Mütze zu kaufen!',''),('currencies',81,'description_loc4','造访达拉然以及各个主城的特殊烹饪供应商,购买罕见的烹饪配方、香料以及大厨的帽子!',''),('currencies',81,'description_loc6','Visita a los vendedores de cocina especiales de Dalaran y de las capitales para comprar recetas de cocina poco frecuentes, especias, ¡e incluso un bonito gorro!',''),('currencies',81,'description_loc8','Посетите торговцев кулинарными товарами в Даларане и других столицах, чтобы приобрести особые кулинарные рецепты, специи и даже головной убор!',''),('currencies',241,'description_loc0','Awarded for valiant acts in the Crusader\'s Coliseum.',''),('currencies',241,'description_loc2','Obtenu en récompense d’actes de bravoure au colisée des Croisés.',''),('currencies',241,'description_loc3','Werden für hehre Taten im Kolosseum der Kreuzfahrer verliehen.',''),('currencies',241,'description_loc4','表彰你在十字军演武场中展示的武勇。',''),('currencies',241,'description_loc6','Otorgado por las hazañas en el Coliseo de los Cruzados.',''),('currencies',241,'description_loc8','За храбрость, проявленную на турнирах Колизея Авангарда.',''),('currencies',181,'description_loc0','If you can read this, you\'ve found a bug. REPORT IT!',''),('currencies',181,'description_loc2','Si vous lisez ceci, c\'est un bug. SIGNALEZ-LE !',''),('currencies',181,'description_loc3','Wenn Ihr das hier lesen könnt, habt Ihr einen Bug gefunden. MELDET IHN!',''),('currencies',181,'description_loc6','Si puedes leer esto, has encontrado un error. ¡Informa!',''),('currencies',181,'description_loc8','Если вы видите это сообщение, это значит, что вы обнаружили ошибку. Сообщите о ней!',''),('currencies',103,'description_loc0','Used to purchase powerful PvP armor and weapons.',''),('currencies',103,'description_loc2','Utilisés pour acheter des armures et armes de JcJ puissantes.',''),('currencies',103,'description_loc3','Können für den Erwerb von mächtigen PVP-Waffen und -Rüstungen verwendet werden.',''),('currencies',103,'description_loc4','竞技场点数是通过在竞技场战斗中获胜而赢得的。你可以消费这些点数来购买强大的奖励品!',''),('currencies',103,'description_loc6','Se utilizan para comprar armas y armaduras de JcJ poderosas.',''),('currencies',103,'description_loc8','За эти очки можно покупать мощное оружие и доспехи для PvP-сражений.',''),('currencies',104,'description_loc0','Used to purchase less-powerful PvP armor and weapons.',''),('currencies',104,'description_loc2','Utilisés pour acheter des armures et armes de JcJ moyennement puissantes.',''),('currencies',104,'description_loc3','Können für den Erwerb von weniger mächtigen PVP-Waffen und -Rüstungen verwendet werden.',''),('currencies',104,'description_loc4','荣誉是通过在PvP战斗中 杀死敌对阵营的成员获得的。你可以使用荣誉点数购买特殊的物品。',''),('currencies',104,'description_loc6','Se utilizan para comprar armas y armaduras de JcJ menos poderosas.',''),('currencies',104,'description_loc8','За эти очки можно покупать не очень мощное оружие и доспехи для PvP-сражений.',''),('currencies',221,'description_loc0','Used to purchase less-powerful armor and weapons.',''),('currencies',221,'description_loc2','Utilisés pour acheter des armures et armes de JcJ moyennement puissantes.',''),('currencies',221,'description_loc3','Können für den Erwerb von weniger mächtigen Waffen und Rüstungen verwendet werden.',''),('currencies',221,'description_loc6','Se utilizan para comprar armas y armaduras menos poderosas.',''),('currencies',221,'description_loc8','За эти очки можно покупать не очень мощное оружие и доспехи.',''),('currencies',341,'description_loc0','Used to purchase powerful PvE armor and weapons.',''),('currencies',341,'description_loc2','Utilisés pour acheter des armures et armes de JcE puissantes.',''),('currencies',341,'description_loc3','Können für den Erwerb von mächtigen PVE-Waffen und -Rüstungen verwendet werden.',''),('currencies',341,'description_loc6','Se utilizan para comprar armas y armaduras de JcE poderosas.',''),('currencies',341,'description_loc8','За эти очки можно покупать мощное оружие и доспехи для PvE-сражений.',''),('spell',9787,'reqSpellId',9787,'Weaponsmith - requires itself'),('spell',9788,'reqSpellId',9788,'Armorsmith - requires itself'),('spell',10656,'reqSpellId',10656,'Dragonscale Leatherworking - requires itself'),('spell',10658,'reqSpellId',10658,'Elemental Leatherworking - requires itself'),('spell',10660,'reqSpellId',10660,'Tribal Leatherworking - requires itself'),('spell',17039,'reqSpellId',17039,'Master Swordsmith - requires itself'),('spell',17040,'reqSpellId',17040,'Master Hammersmith - requires itself'),('spell',17041,'reqSpellId',17041,'Master Axesmith - requires itself'),('spell',20219,'reqSpellId',20219,'Gnomish Engineer - requires itself'),('spell',20222,'reqSpellId',20222,'Goblin Engineer - requires itself'),('spell',26797,'reqSpellId',26797,'Spellfire Tailoring - requires itself'),('spell',26798,'reqSpellId',26798,'Mooncloth Tailoring - requires itself'),('spell',26801,'reqSpellId',26801,'Shadoweave Tailoring - requires itself'),('spell',379,'cuFLags',1073741824,'Earth Shield - hide'),('spell',17567,'cuFLags',1073741824,'Summon Blood Parrot - hide'),('spell',19483,'cuFLags',1073741824,'Immolation - hide'),('spell',20154,'cuFLags',1073741824,'Seal of Righteousness - hide'),('spell',21169,'cuFLags',1073741824,'Reincarnation - hide'),('spell',22845,'cuFLags',1073741824,'Frenzied Regeneration - hide'),('spell',23885,'cuFLags',1073741824,'Bloodthirst - hide'),('spell',27813,'cuFLags',1073741824,'Blessed Recovery - hide'),('spell',27817,'cuFLags',1073741824,'Blessed Recovery - hide'),('spell',27818,'cuFLags',1073741824,'Blessed Recovery - hide'),('spell',29442,'cuFLags',1073741824,'Magic Absorption - hide'),('spell',29841,'cuFLags',1073741824,'Second Wind - hide'),('spell',29842,'cuFLags',1073741824,'Second Wind - hide'),('spell',29886,'cuFLags',1073741824,'Create Soulwell - hide'),('spell',30708,'cuFLags',1073741824,'Totem of Wrath - hide'),('spell',30874,'cuFLags',1073741824,'Gift of the Water Spirit - hide'),('spell',31643,'cuFLags',1073741824,'Blazing Speed - hide'),('spell',32841,'cuFLags',1073741824,'Mass Resurrection - hide'),('spell',34919,'cuFLags',1073741824,'Vampiric Touch - hide'),('spell',44450,'cuFLags',1073741824,'Burnout - hide'),('spell',47633,'cuFLags',1073741824,'Death Coil - hide'),('spell',48954,'cuFLags',1073741824,'Swift Zhevra - hide'),('spell',49575,'cuFLags',1073741824,'Death Grip - hide'),('spell',50536,'cuFLags',1073741824,'Unholy Blight - hide'),('spell',52374,'cuFLags',1073741824,'Blood Strike - hide'),('spell',56816,'cuFLags',1073741824,'Rune Strike - hide'),('spell',58427,'cuFLags',1073741824,'Overkill - hide'),('spell',58889,'cuFLags',1073741824,'Create Soulwell - hide'),('spell',64380,'cuFLags',1073741824,'Shattering Throw - hide'),('spell',66122,'cuFLags',1073741824,'Magic Rooster - hide'),('spell',66123,'cuFLags',1073741824,'Magic Rooster - hide'),('spell',66124,'cuFLags',1073741824,'Magic Rooster - hide'),('spell',66175,'cuFLags',1073741824,'Macabre Marionette - hide'),('spell',54910,'cuFLags',1073741824,'Glyph of the Red Lynx - hide unused glyph'),('spell',57231,'cuFLags',1073741824,'Death Knight Glyph 25 - hide unused glyph'),('spell',58166,'cuFLags',1073741824,'Glyph of the Forest Lynx - hide unused glyph'),('spell',58239,'cuFLags',1073741824,'Glyph of the Penguin - hide unused glyph'),('spell',58240,'cuFLags',1073741824,'Glyph of the Bear Cub - hide unused glyph'),('spell',58261,'cuFLags',1073741824,'Glyph of the Arctic Wolf - hide unused glyph'),('spell',58262,'cuFLags',1073741824,'Glyph of the Black Wolf - hide unused glyph'),('spell',60460,'cuFLags',1073741824,'Glyph of Raise Dead - hide unused glyph'),('spell',54910,'skillLine1',0,'Glyph of the Red Lynx - hide unused glyph'),('spell',57231,'skillLine1',0,'Death Knight Glyph 25 - hide unused glyph'),('spell',58166,'skillLine1',0,'Glyph of the Forest Lynx - hide unused glyph'),('spell',58239,'skillLine1',0,'Glyph of the Penguin - hide unused glyph'),('spell',58240,'skillLine1',0,'Glyph of the Bear Cub - hide unused glyph'),('spell',58261,'skillLine1',0,'Glyph of the Arctic Wolf - hide unused glyph'),('spell',58262,'skillLine1',0,'Glyph of the Black Wolf - hide unused glyph'),('spell',60460,'skillLine1',0,'Glyph of Raise Dead - hide unused glyph'),('spell',54910,'iconIdAlt',0,'Glyph of the Red Lynx - hide unused glyph'),('spell',57231,'iconIdAlt',0,'Death Knight Glyph 25 - hide unused glyph'),('spell',58166,'iconIdAlt',0,'Glyph of the Forest Lynx - hide unused glyph'),('spell',58239,'iconIdAlt',0,'Glyph of the Penguin - hide unused glyph'),('spell',58240,'iconIdAlt',0,'Glyph of the Bear Cub - hide unused glyph'),('spell',58261,'iconIdAlt',0,'Glyph of the Arctic Wolf - hide unused glyph'),('spell',58262,'iconIdAlt',0,'Glyph of the Black Wolf - hide unused glyph'),('spell',60460,'iconIdAlt',0,'Glyph of Raise Dead - hide unused glyph'),('quests',9572,'zoneOrSort','3562','Weaken the Ramparts - category Hellfire Citadel -> Hellfire Ramparts'),('quests',9575,'zoneOrSort','3562','Weaken the Ramparts - category Hellfire Citadel -> Hellfire Ramparts'),('quests',11354,'zoneOrSort','3562','Wanted: Nazan\'s Riding Crop - category Hellfire Citadel -> Hellfire Ramparts'),('quests',9589,'zoneOrSort','3713','The Blood is Life - category Hellfire Citadel -> Blood Furnace'),('quests',9590,'zoneOrSort','3713','The Blood is Life - category Hellfire Citadel -> Blood Furnace'),('quests',9607,'zoneOrSort','3713','Heart of Rage - category Hellfire Citadel -> Blood Furnace'),('quests',9608,'zoneOrSort','3713','Heart of Rage - category Hellfire Citadel -> Blood Furnace'),('quests',11362,'zoneOrSort','3713','Wanted: Keli\'dan\'s Feathered Stave - category Hellfire Citadel -> Blood Furnace'),('quests',9492,'zoneOrSort','3714','Turning the Tide - category Hellfire Citadel -> Shattered Halls'),('quests',9493,'zoneOrSort','3714','Pride of the Fel Horde - category Hellfire Citadel -> Shattered Halls'),('quests',9494,'zoneOrSort','3714','Fel Embers - category Hellfire Citadel -> Shattered Halls'),('quests',9495,'zoneOrSort','3714','The Will of the Warchief - category Hellfire Citadel -> Shattered Halls'),('quests',9496,'zoneOrSort','3714','Pride of the Fel Horde - category Hellfire Citadel -> Shattered Halls'),('quests',9497,'zoneOrSort','3714','Emblem of the Fel Horde - category Hellfire Citadel -> Shattered Halls'),('quests',9524,'zoneOrSort','3714','Imprisoned in the Citadel - category Hellfire Citadel -> Shattered Halls'),('quests',9525,'zoneOrSort','3714','Imprisoned in the Citadel - category Hellfire Citadel -> Shattered Halls'),('quests',11363,'zoneOrSort','3714','Wanted: Bladefist\'s Seal - category Hellfire Citadel -> Shattered Halls'),('quests',11364,'zoneOrSort','3714','Wanted: Shattered Hand Centurions - category Hellfire Citadel -> Shattered Halls'); +INSERT INTO `aowow_setup_custom_data` VALUES ('zones',2257,'cuFlags','0','Deeprun Tram - make visible'),('zones',2257,'category','0','Deeprun Tram - Category: Eastern Kingdoms'),('zones',2257,'type','1','Deeprun Tram - Type: Transit'),('zones',3698,'expansion','1','Nagrand Arena - Addon: BC'),('zones',3702,'expansion','1','Blades Edge Arena - Addon: BC'),('zones',3968,'expansion','1','Ruins of Lordaeron Arena - Addon: BC'),('zones',4378,'expansion','1','Dalaran Arena - Addon: WotLK'),('zones',4406,'expansion','1','Ring of Valor Arena - Addon: WotLK'),('zones',2597,'maxPlayer','40','Alterac Valey - Players: 40 [battlemasterlist.dbc: 5]'),('zones',4710,'maxPlayer','40','Isle of Conquest - Players: 40 [battlemasterlist.dbc: 5]'),('zones',4893,'cuFlags','1073741824','The Frost Queen\'s Lair - set: CUSTOM_EXCLUDE_FOR_LISTVIEW'),('zones',4894,'cuFlags','1073741824','Putricide\'s Laboratory [..] - set: CUSTOM_EXCLUDE_FOR_LISTVIEW'),('achievement',1956,'itemExtra','44738','Higher Learning - item rewarded through gossip'),('zones',4895,'cuFlags','1073741824','The Crimson Hall - set: CUSTOM_EXCLUDE_FOR_LISTVIEW'),('titles',137,'gender','2','Matron - female'),('zones',4896,'cuFlags','1073741824','The Frozen Throne - set: CUSTOM_EXCLUDE_FOR_LISTVIEW'),('zones',4897,'cuFlags','1073741824','The Sanctum of Blood - set: CUSTOM_EXCLUDE_FOR_LISTVIEW'),('zones',4076,'cuFlags','1073741824','Reuse Me 7 - set: CUSTOM_EXCLUDE_FOR_LISTVIEW'),('zones',207,'cuFlags','1073741824','The Great Sea - set: CUSTOM_EXCLUDE_FOR_LISTVIEW'),('zones',208,'cuFlags','1073741824','Unused Ironcladcove - set: CUSTOM_EXCLUDE_FOR_LISTVIEW'),('zones',2817,'levelMin','74','Crystalsong Forest - missing lfgDungeons entry'),('zones',1417,'cuFlags','1073741824','Sunken Temple [extra area on map 109] - set: CUSTOM_EXCLUDE_FOR_LISTVIEW'),('zones',22,'cuFlags','1073741824','Programmer Isle - set: CUSTOM_EXCLUDE_FOR_LISTVIEW'),('zones',151,'cuFlags','1073741824','Designer Island - set: CUSTOM_EXCLUDE_FOR_LISTVIEW'),('zones',3948,'cuFlags','1073741824','Brian and Pat Test - set: CUSTOM_EXCLUDE_FOR_LISTVIEW'),('zones',4019,'cuFlags','1073741824','Development Land - set: CUSTOM_EXCLUDE_FOR_LISTVIEW'),('zones',3605,'cuFlags','1073741824','Hyjal Past [extra area on map 560] - set: CUSTOM_EXCLUDE_FOR_LISTVIEW'),('zones',3535,'cuFlags','1073741824','Hellfire Citadel [extra area on map 540] - set: CUSTOM_EXCLUDE_FOR_LISTVIEW'),('zones',41,'levelMin','50','Deadwind Pass - missing lfgDungeons entry'),('zones',41,'levelMax','60','Deadwind Pass - missing lfgDungeons entry'),('zones',2257,'levelMin','1','Deeprun Tram - missing lfgDungeons entry'),('zones',2257,'levelMax','80','Deeprun Tram - missing lfgDungeons entry'),('zones',4298,'category','0','Plaguelands: The Scarlet Enclave - Parent: Eastern Kingdoms'),('zones',4298,'levelMin','55','Plaguelands: The Scarlet Enclave - missing lfgDungeons entry'),('zones',4298,'levelMax','58','Plaguelands: The Scarlet Enclave - missing lfgDungeons entry'),('zones',493,'levelMin','15','Moonglade - missing lfgDungeons entry'),('zones',493,'levelMax','60','Moonglade - missing lfgDungeons entry'),('zones',2817,'levelMax','76','Crystalsong Forest - missing lfgDungeons entry'),('zones',4742,'levelMin','77','Hrothgar\'s Landing - missing lfgDungeons entry'),('zones',4742,'levelMax','80','Hrothgar\'s Landing - missing lfgDungeons entry'),('classes',1,'roles','10','Warrior - rngDPS'),('classes',2,'roles','11','Paladin - mleDPS + Tank + Heal'),('classes',3,'roles','4','Hunter - rngDPS'),('classes',4,'roles','2','Rogue - mleDPS'),('classes',5,'roles','5','Priest - rngDPS + Heal'),('classes',6,'roles','10','Death Knight - mleDPS + Tank'),('classes',7,'roles','7','Shaman - mleDPS + rngDPS + Heal'),('classes',8,'roles','4','Mage - rngDPS'),('classes',9,'roles','4','Warlock - rngDPS'),('classes',11,'roles','15','Druid - mleDPS + Tank + Heal + rngDPS'),('currencies',103,'cap','10000','Arena Points - cap'),('currencies',104,'cap','75000','Honor Points - cap'),('currencies',1,'cuFlags','1073741824','Currency Token Test Token 1 - set: CUSTOM_EXCLUDE_FOR_LISTVIEW'),('currencies',2,'cuFlags','1073741824','Currency Token Test Token 2 - set: CUSTOM_EXCLUDE_FOR_LISTVIEW'),('currencies',4,'cuFlags','1073741824','Currency Token Test Token 3 - set: CUSTOM_EXCLUDE_FOR_LISTVIEW'),('currencies',22,'cuFlags','1073741824','Birmingham Test Item 3 - set: CUSTOM_EXCLUDE_FOR_LISTVIEW'),('currencies',141,'cuFlags','1073741824','zzzOLDDaily Quest Faction Token - set: CUSTOM_EXCLUDE_FOR_LISTVIEW'),('currencies',1,'category','3','Currency Token Test Token 1 - category: unused'),('currencies',2,'category','3','Currency Token Test Token 2 - category: unused'),('currencies',4,'category','3','Currency Token Test Token 3 - category: unused'),('currencies',22,'category','3','Birmingham Test Item 3 - category: unused'),('currencies',141,'category','3','zzzOLDDaily Quest Faction Token - category: unused'),('factions',68,'qmNpcIds','33555','Undercity - set Quartermaster'),('factions',47,'qmNpcIds','33310','Ironforge - set Quartermaster'),('factions',69,'qmNpcIds','33653','Darnassus - set Quartermaster'),('factions',72,'qmNpcIds','33307','Stormwind - set Quartermaster'),('factions',76,'qmNpcIds','33553','Orgrimmar - set Quartermaster'),('factions',81,'qmNpcIds','33556','Thunder Bluff - set Quartermaster'),('factions',922,'qmNpcIds','16528','Tranquillien - set Quartermaster'),('factions',930,'qmNpcIds','33657','Exodar - set Quartermaster'),('factions',932,'qmNpcIds','19321','The Aldor - set Quartermaster'),('factions',933,'qmNpcIds','20242 23007','The Consortium - set Quartermaster'),('factions',935,'qmNpcIds','21432','The Sha\'tar - set Quartermaster'),('factions',941,'qmNpcIds','20241','The Mag\'har - set Quartermaster'),('factions',942,'qmNpcIds','17904','Cenarion Expedition - set Quartermaster'),('factions',946,'qmNpcIds','17657','Honor Hold - set Quartermaster'),('factions',947,'qmNpcIds','17585','Thrallmar - set Quartermaster'),('factions',970,'qmNpcIds','18382','Sporeggar - set Quartermaster'),('factions',978,'qmNpcIds','20240','Kurenai - set Quartermaster'),('factions',989,'qmNpcIds','21643','Keepers of Time - set Quartermaster'),('factions',1011,'qmNpcIds','21655','Lower City - set Quartermaster'),('factions',1012,'qmNpcIds','23159','Ashtongue Deathsworn - set Quartermaster'),('factions',1037,'qmNpcIds','32773 32564','Alliance Vanguard - set Quartermaster'),('factions',1038,'qmNpcIds','23428','Ogri\'la - set Quartermaster'),('factions',1052,'qmNpcIds','32774 32565','Horde Expedition - set Quartermaster'),('factions',1073,'qmNpcIds','31916 32763','The Kalu\'ak - set Quartermaster'),('factions',1090,'qmNpcIds','32287','Kirin Tor - set Quartermaster'),('factions',1091,'qmNpcIds','32533','The Wyrmrest Accord - set Quartermaster'),('factions',1094,'qmNpcIds','34881','The Silver Covenant - set Quartermaster'),('factions',1105,'qmNpcIds','31910','The Oracles - set Quartermaster'),('factions',1106,'qmNpcIds','30431','Argent Crusade - set Quartermaster'),('factions',1119,'qmNpcIds','32540','The Sons of Hodir - set Quartermaster'),('factions',1124,'qmNpcIds','34772','The Sunreavers - set Quartermaster'),('factions',1156,'qmNpcIds','37687','The Ashen Verdict - set Quartermaster'),('factions',1082,'cuFlags','1073741824','REUSE - set: CUSTOM_EXCLUDE_FOR_LISTVIEW'),('factions',952,'cuFlags','1073741824','Test Faction 3 - set: CUSTOM_EXCLUDE_FOR_LISTVIEW'),('titles',138,'gender','1','Patron - male'),('sounds',15407,'cat','10','UR_Algalon_Summon03 - is not an item pickup'),('shapeshiftforms',1,'displayIdH','8571','Cat Form - spellshapeshiftform.dbc missing displayId'),('shapeshiftforms',15,'displayIdH','8571','Creature - Cat - spellshapeshiftform.dbc missing displayId'),('shapeshiftforms',5,'displayIdH','2289','Bear Form - spellshapeshiftform.dbc missing displayId'),('shapeshiftforms',8,'displayIdH','2289','Dire Bear Form - spellshapeshiftform.dbc missing displayId'),('shapeshiftforms',14,'displayIdH','2289','Creature - Bear - spellshapeshiftform.dbc missing displayId'),('shapeshiftforms',27,'displayIdH','21244','Flight Form, Epic - spellshapeshiftform.dbc missing displayId'),('shapeshiftforms',29,'displayIdH','20872','Flight Form - spellshapeshiftform.dbc missing displayId'),('races',1,'leader','29611','Human - King Varian Wrynn'),('races',1,'factionId','72','Human - Stormwind'),('races',1,'startAreaId','12','Human - Elwynn Forest'),('races',2,'leader','4949','Orc - Thrall'),('races',2,'factionId','76','Orc - Orgrimmar'),('races',2,'startAreaId','14','Orc - Durotar'),('races',3,'leader','2784','Dwarf - King Magni Bronzebeard'),('races',3,'factionId','47','Dwarf - Ironforge'),('races',3,'startAreaId','1','Dwarf - Dun Morogh'),('races',4,'leader','7999','Night Elf - Tyrande Whisperwind'),('races',4,'factionId','69','Night Elf - Darnassus'),('races',4,'startAreaId','141','Night Elf - Teldrassil'),('races',5,'leader','10181','Undead - Lady Sylvanas Windrunner'),('races',5,'factionId','68','Undead - Undercity'),('races',5,'startAreaId','85','Undead - Tirisfal Glades'),('races',6,'leader','3057','Tauren - Cairne Bloodhoof'),('races',6,'factionId','81','Tauren - Thunder Bluff'),('races',6,'startAreaId','215','Tauren - Mulgore'),('races',7,'leader','7937','Gnome - High Tinker Mekkatorque'),('races',7,'factionId','54','Gnome - Gnomeregan Exiles'),('races',7,'startAreaId','1','Gnome - Dun Morogh'),('races',8,'leader','10540','Troll - Vol\'jin'),('races',8,'factionId','530','Troll - Darkspear Trolls'),('races',8,'startAreaId','14','Troll - Durotar'),('races',10,'leader','16802','Blood Elf - Lor\'themar Theron'),('races',10,'factionId','911','Blood Elf - Silvermoon City'),('races',10,'startAreaId','3430','Blood Elf - Eversong Woods'),('races',11,'leader','17468','Draenei - Prophet Velen'),('races',11,'factionId','930','Draenei - Exodar'),('races',11,'startAreaId','3524','Draenei - Azuremyst Isle'),('holidays',62,'iconString','inv_misc_missilelarge_red','Fireworks Spectacular'),('holidays',141,'iconString','calendar_winterveilstart','Feast of Winter Veil'),('holidays',181,'iconString','calendar_noblegardenstart','Noblegarden'),('holidays',201,'iconString','calendar_childrensweekstart','Children\'s Week'),('holidays',283,'iconString','inv_jewelry_necklace_21','Call to Arms: Alterac Valley'),('holidays',284,'iconString','inv_misc_rune_07','Call to Arms: Warsong Gulch'),('holidays',285,'iconString','inv_jewelry_amulet_07','Call to Arms: Arathi Basin'),('holidays',301,'iconString','calendar_fishingextravaganzastart','Stranglethorn Fishing Extravaganza'),('holidays',321,'iconString','calendar_harvestfestivalstart','Harvest Festival'),('holidays',324,'iconString','calendar_hallowsendstart','Hallow\'s End'),('holidays',327,'iconString','calendar_lunarfestivalstart','Lunar Festival'),('holidays',335,'iconString','calendar_loveintheairstart','Love is in the Air'),('holidays',341,'iconString','calendar_midsummerstart','Midsummer Fire Festival'),('holidays',353,'iconString','spell_nature_eyeofthestorm','Call to Arms: Eye of the Storm'),('holidays',372,'iconString','calendar_brewfeststart','Brewfest'),('holidays',374,'iconString','calendar_darkmoonfaireelwynnstart','Darkmoon Faire'),('holidays',375,'iconString','calendar_darkmoonfairemulgorestart','Darkmoon Faire'),('holidays',376,'iconString','calendar_darkmoonfaireterokkarstart','Darkmoon Faire'),('holidays',398,'iconString','calendar_piratesdaystart','Pirates\' Day'),('holidays',400,'iconString','achievement_bg_winsoa','Call to Arms: Strand of the Ancients'),('holidays',404,'iconString','calendar_harvestfestivalstart','Pilgrim\'s Bounty'),('holidays',406,'iconString','achievement_boss_lichking','Wrath of the Lich King Launch'),('holidays',409,'iconString','calendar_dayofthedeadstart','Day of the Dead'),('holidays',420,'iconString','achievement_bg_winwsg','Call to Arms: Isle of Conquest'),('holidays',423,'iconString','calendar_loveintheairstart','Love is in the Air'),('holidays',424,'iconString','calendar_fishingextravaganzastart','Kalu\'ak Fishing Derby'),('holidays',141,'achievementCatOrId','156','Feast of Winter Veil - Category: Feast of Winter Veil'),('holidays',181,'achievementCatOrId','159','Noblegarden - Category: Noblegarden'),('holidays',201,'achievementCatOrId','163','Children\'s Week - Category: Children\'s Week'),('holidays',324,'achievementCatOrId','158','Hallow\'s End - Category: Hallow\'s End'),('holidays',327,'achievementCatOrId','160','Lunar Festival - Category: Lunar Festival'),('holidays',341,'achievementCatOrId','161','Midsummer Fire Festival - Category: Midsummer Fire Festival'),('holidays',372,'achievementCatOrId','162','Brewfest - Category: Brewfest'),('holidays',398,'achievementCatOrId','-3457','Pirates\' Day - Achievement: The Captain\'s Booty'),('holidays',404,'achievementCatOrId','14981','Pilgrim\'s Bounty - Category: Pilgrim\'s Bounty'),('holidays',409,'achievementCatOrId','-3456','Day of the Dead - Achievement: Dead Man\'s Party'),('holidays',423,'achievementCatOrId','187','Love is in the Air - Category: Love is in the Air'),('holidays',324,'bossCreature','23682','Hallow\'s End - Headless Horseman'),('holidays',327,'bossCreature','15467','Lunar Festival - Omen'),('holidays',341,'bossCreature','25740','Midsummer Fire Festival - Ahune'),('holidays',372,'bossCreature','23872','Brewfest - Coren Direbrew'),('holidays',423,'bossCreature','36296','Love is in the Air - Apothecary Hummel'),('skillline',197,'professionMask','512','Tailoring'),('skillline',186,'professionMask','256','Mining'),('skillline',165,'specializations','10656 10658 10660','Leatherworking'),('skillline',165,'recipeSubClass','1','Leatherworking'),('skillline',165,'professionMask','128','Leatherworking'),('skillline',755,'recipeSubClass','10','Jewelcrafting'),('skillline',755,'professionMask','64','Jewelcrafting'),('skillline',129,'recipeSubClass','7','First Aid'),('skillline',129,'professionMask','32','First Aid'),('skillline',202,'specializations','20219 20222','Engineering'),('skillline',202,'recipeSubClass','3','Engineering'),('skillline',202,'professionMask','16','Engineering'),('skillline',333,'recipeSubClass','8','Enchanting'),('skillline',333,'professionMask','8','Enchanting'),('skillline',185,'recipeSubClass','5','Cooking'),('skillline',185,'professionMask','4','Cooking'),('skillline',164,'specializations','9788 9787 17041 17040 17039','Blacksmithing'),('skillline',164,'recipeSubClass','4','Blacksmithing'),('skillline',164,'professionMask','2','Blacksmithing'),('skillline',171,'specializations','28677 28675 28672','Alchemy'),('skillline',171,'recipeSubClass','6','Alchemy'),('skillline',171,'professionMask','1','Alchemy'),('skillline',393,'professionMask','0','Skinning'),('skillline',197,'recipeSubClass','2','Tailoring'),('skillline',197,'specializations','26798 26801 26797','Tailoring'),('skillline',356,'professionMask','1024','Fishing'),('skillline',356,'recipeSubClass','9','Fishing'),('skillline',182,'professionMask','2048','Herbalism'),('skillline',773,'professionMask','4096','Inscription'),('skillline',773,'recipeSubClass','11','Inscription'),('skillline',785,'name_loc0','Pet - Wasp','Pet - Wasp'),('skillline',781,'name_loc2','Familier - diablosaure exotique','Pet - Exotic Devlisaur'),('skillline',758,'name_loc6','Mascota: Evento - Control remoto','Pet - Event - Remote Control'),('skillline',758,'name_loc3','Tier - Ereignis Ferngesteuert','Pet - Event - Remote Control'),('skillline',758,'categoryId','7','Pet - Event - Remote Control - bring in line with other pets'),('skillline',788,'categoryId','7','Pet - Exotic Spirit Beast - bring in line with other pets'),('items',33147,'class','9','Formula: Enchant Cloak - Subtlety - Class: Recipes'),('items',33147,'subClass','8','Formula: Enchant Cloak - Subtlety - Subclass: Enchanting'),('currencies',1,'description_loc0','Text that describes this item can be found here.',''),('currencies',1,'description_loc2','Un texte qui décrit l\'objet figure ici.',''),('currencies',1,'description_loc3','Text, der den Gegenstand beschreibt, wird hier angezeigt.',''),('currencies',1,'description_loc6','Aquí puede encontrarse el texto que describe a este objeto.',''),('currencies',1,'description_loc8','Здесь находится описание предмета.',''),('currencies',61,'description_loc0','Tiffany Cartier\'s shop in Dalaran will gladly accept these tokens for unique jewelcrafting recipes.',''),('currencies',61,'description_loc2','La boutique de Tiffany Kartier, à Dalaran, accepte avec joie ces marques contre des dessins de joaillerie uniques.',''),('currencies',61,'description_loc3','Tiffany Cartiers Geschäft in Dalaran wird diese Symbole im Tausch gegen einzigartige Juweliersrezepte dankend annehmen.',''),('currencies',61,'description_loc4','达拉然的蒂凡妮·卡蒂亚会欣然接受这些代币,并用稀有的珠宝加工图鉴来交换。',''),('currencies',61,'description_loc6','La tienda de Tiffany Cartier en Dalaran cambiará gustosamente estos talismanes por recetas de joyería.',''),('currencies',61,'description_loc8','В магазине Тиффани Картье, что в Даларане, вам с радостью обменяют эти знаки на уникальные ювелирные эскизы.',''),('currencies',81,'description_loc0','Visit special cooking vendors in Dalaran and the capital cities to to purchase unusual cooking recipes, spices, and even a fine hat!',''),('currencies',81,'description_loc2','Rendez visite aux marchands de fournitures de cuisine à Dalaran et dans les autres capitales pour acheter des recettes de cuisine spéciales, des épices, et même une superbe toque !',''),('currencies',81,'description_loc3','Besucht besondere Kochhändler in Dalaran und den Hauptstädten, um ungewöhnliche Kochrezepte, Gewürze und sogar eine großartige Mütze zu kaufen!',''),('currencies',81,'description_loc4','造访达拉然以及各个主城的特殊烹饪供应商,购买罕见的烹饪配方、香料以及大厨的帽子!',''),('currencies',81,'description_loc6','Visita a los vendedores de cocina especiales de Dalaran y de las capitales para comprar recetas de cocina poco frecuentes, especias, ¡e incluso un bonito gorro!',''),('currencies',81,'description_loc8','Посетите торговцев кулинарными товарами в Даларане и других столицах, чтобы приобрести особые кулинарные рецепты, специи и даже головной убор!',''),('currencies',241,'description_loc0','Awarded for valiant acts in the Crusader\'s Coliseum.',''),('currencies',241,'description_loc2','Obtenu en récompense d’actes de bravoure au colisée des Croisés.',''),('currencies',241,'description_loc3','Werden für hehre Taten im Kolosseum der Kreuzfahrer verliehen.',''),('currencies',241,'description_loc4','表彰你在十字军演武场中展示的武勇。',''),('currencies',241,'description_loc6','Otorgado por las hazañas en el Coliseo de los Cruzados.',''),('currencies',241,'description_loc8','За храбрость, проявленную на турнирах Колизея Авангарда.',''),('currencies',181,'description_loc0','If you can read this, you\'ve found a bug. REPORT IT!',''),('currencies',181,'description_loc2','Si vous lisez ceci, c\'est un bug. SIGNALEZ-LE !',''),('currencies',181,'description_loc3','Wenn Ihr das hier lesen könnt, habt Ihr einen Bug gefunden. MELDET IHN!',''),('currencies',181,'description_loc6','Si puedes leer esto, has encontrado un error. ¡Informa!',''),('currencies',181,'description_loc8','Если вы видите это сообщение, это значит, что вы обнаружили ошибку. Сообщите о ней!',''),('currencies',103,'description_loc0','Used to purchase powerful PvP armor and weapons.',''),('currencies',103,'description_loc2','Utilisés pour acheter des armures et armes de JcJ puissantes.',''),('currencies',103,'description_loc3','Können für den Erwerb von mächtigen PVP-Waffen und -Rüstungen verwendet werden.',''),('currencies',103,'description_loc4','竞技场点数是通过在竞技场战斗中获胜而赢得的。你可以消费这些点数来购买强大的奖励品!',''),('currencies',103,'description_loc6','Se utilizan para comprar armas y armaduras de JcJ poderosas.',''),('currencies',103,'description_loc8','За эти очки можно покупать мощное оружие и доспехи для PvP-сражений.',''),('currencies',104,'description_loc0','Used to purchase less-powerful PvP armor and weapons.',''),('currencies',104,'description_loc2','Utilisés pour acheter des armures et armes de JcJ moyennement puissantes.',''),('currencies',104,'description_loc3','Können für den Erwerb von weniger mächtigen PVP-Waffen und -Rüstungen verwendet werden.',''),('currencies',104,'description_loc4','荣誉是通过在PvP战斗中 杀死敌对阵营的成员获得的。你可以使用荣誉点数购买特殊的物品。',''),('currencies',104,'description_loc6','Se utilizan para comprar armas y armaduras de JcJ menos poderosas.',''),('currencies',104,'description_loc8','За эти очки можно покупать не очень мощное оружие и доспехи для PvP-сражений.',''),('currencies',221,'description_loc0','Used to purchase less-powerful armor and weapons.',''),('currencies',221,'description_loc2','Utilisés pour acheter des armures et armes de JcJ moyennement puissantes.',''),('currencies',221,'description_loc3','Können für den Erwerb von weniger mächtigen Waffen und Rüstungen verwendet werden.',''),('currencies',221,'description_loc6','Se utilizan para comprar armas y armaduras menos poderosas.',''),('currencies',221,'description_loc8','За эти очки можно покупать не очень мощное оружие и доспехи.',''),('currencies',341,'description_loc0','Used to purchase powerful PvE armor and weapons.',''),('currencies',341,'description_loc2','Utilisés pour acheter des armures et armes de JcE puissantes.',''),('currencies',341,'description_loc3','Können für den Erwerb von mächtigen PVE-Waffen und -Rüstungen verwendet werden.',''),('currencies',341,'description_loc6','Se utilizan para comprar armas y armaduras de JcE poderosas.',''),('currencies',341,'description_loc8','За эти очки можно покупать мощное оружие и доспехи для PvE-сражений.',''),('spell',9787,'reqSpellId','9787','Weaponsmith - requires itself'),('spell',9788,'reqSpellId','9788','Armorsmith - requires itself'),('spell',10656,'reqSpellId','10656','Dragonscale Leatherworking - requires itself'),('spell',10658,'reqSpellId','10658','Elemental Leatherworking - requires itself'),('spell',10660,'reqSpellId','10660','Tribal Leatherworking - requires itself'),('spell',17039,'reqSpellId','17039','Master Swordsmith - requires itself'),('spell',17040,'reqSpellId','17040','Master Hammersmith - requires itself'),('spell',17041,'reqSpellId','17041','Master Axesmith - requires itself'),('spell',20219,'reqSpellId','20219','Gnomish Engineer - requires itself'),('spell',20222,'reqSpellId','20222','Goblin Engineer - requires itself'),('spell',26797,'reqSpellId','26797','Spellfire Tailoring - requires itself'),('spell',26798,'reqSpellId','26798','Mooncloth Tailoring - requires itself'),('spell',26801,'reqSpellId','26801','Shadoweave Tailoring - requires itself'),('spell',379,'cuFLags','1073741824','Earth Shield - hide'),('spell',17567,'cuFLags','1073741824','Summon Blood Parrot - hide'),('spell',19483,'cuFLags','1073741824','Immolation - hide'),('spell',20154,'cuFLags','1073741824','Seal of Righteousness - hide'),('spell',21169,'cuFLags','1073741824','Reincarnation - hide'),('spell',22845,'cuFLags','1073741824','Frenzied Regeneration - hide'),('spell',23885,'cuFLags','1073741824','Bloodthirst - hide'),('spell',27813,'cuFLags','1073741824','Blessed Recovery - hide'),('spell',27817,'cuFLags','1073741824','Blessed Recovery - hide'),('spell',27818,'cuFLags','1073741824','Blessed Recovery - hide'),('spell',29442,'cuFLags','1073741824','Magic Absorption - hide'),('spell',29841,'cuFLags','1073741824','Second Wind - hide'),('spell',29842,'cuFLags','1073741824','Second Wind - hide'),('spell',29886,'cuFLags','1073741824','Create Soulwell - hide'),('spell',30708,'cuFLags','1073741824','Totem of Wrath - hide'),('spell',30874,'cuFLags','1073741824','Gift of the Water Spirit - hide'),('spell',31643,'cuFLags','1073741824','Blazing Speed - hide'),('spell',32841,'cuFLags','1073741824','Mass Resurrection - hide'),('spell',34919,'cuFLags','1073741824','Vampiric Touch - hide'),('spell',44450,'cuFLags','1073741824','Burnout - hide'),('spell',47633,'cuFLags','1073741824','Death Coil - hide'),('spell',48954,'cuFLags','1073741824','Swift Zhevra - hide'),('spell',49575,'cuFLags','1073741824','Death Grip - hide'),('spell',50536,'cuFLags','1073741824','Unholy Blight - hide'),('spell',52374,'cuFLags','1073741824','Blood Strike - hide'),('spell',56816,'cuFLags','1073741824','Rune Strike - hide'),('spell',58427,'cuFLags','1073741824','Overkill - hide'),('spell',58889,'cuFLags','1073741824','Create Soulwell - hide'),('spell',64380,'cuFLags','1073741824','Shattering Throw - hide'),('spell',66122,'cuFLags','1073741824','Magic Rooster - hide'),('spell',66123,'cuFLags','1073741824','Magic Rooster - hide'),('spell',66124,'cuFLags','1073741824','Magic Rooster - hide'),('spell',66175,'cuFLags','1073741824','Macabre Marionette - hide'),('spell',54910,'cuFLags','1073741824','Glyph of the Red Lynx - hide unused glyph'),('spell',57231,'cuFLags','1073741824','Death Knight Glyph 25 - hide unused glyph'),('spell',58166,'cuFLags','1073741824','Glyph of the Forest Lynx - hide unused glyph'),('spell',58239,'cuFLags','1073741824','Glyph of the Penguin - hide unused glyph'),('spell',58240,'cuFLags','1073741824','Glyph of the Bear Cub - hide unused glyph'),('spell',58261,'cuFLags','1073741824','Glyph of the Arctic Wolf - hide unused glyph'),('spell',58262,'cuFLags','1073741824','Glyph of the Black Wolf - hide unused glyph'),('spell',60460,'cuFLags','1073741824','Glyph of Raise Dead - hide unused glyph'),('spell',54910,'skillLine1','0','Glyph of the Red Lynx - hide unused glyph'),('spell',57231,'skillLine1','0','Death Knight Glyph 25 - hide unused glyph'),('spell',58166,'skillLine1','0','Glyph of the Forest Lynx - hide unused glyph'),('spell',58239,'skillLine1','0','Glyph of the Penguin - hide unused glyph'),('spell',58240,'skillLine1','0','Glyph of the Bear Cub - hide unused glyph'),('spell',58261,'skillLine1','0','Glyph of the Arctic Wolf - hide unused glyph'),('spell',58262,'skillLine1','0','Glyph of the Black Wolf - hide unused glyph'),('spell',60460,'skillLine1','0','Glyph of Raise Dead - hide unused glyph'),('spell',54910,'iconIdAlt','0','Glyph of the Red Lynx - hide unused glyph'),('spell',57231,'iconIdAlt','0','Death Knight Glyph 25 - hide unused glyph'),('spell',58166,'iconIdAlt','0','Glyph of the Forest Lynx - hide unused glyph'),('spell',58239,'iconIdAlt','0','Glyph of the Penguin - hide unused glyph'),('spell',58240,'iconIdAlt','0','Glyph of the Bear Cub - hide unused glyph'),('spell',58261,'iconIdAlt','0','Glyph of the Arctic Wolf - hide unused glyph'),('spell',58262,'iconIdAlt','0','Glyph of the Black Wolf - hide unused glyph'),('spell',60460,'iconIdAlt','0','Glyph of Raise Dead - hide unused glyph'),('quests',9572,'zoneOrSort','3562','Weaken the Ramparts - category Hellfire Citadel -> Hellfire Ramparts'),('quests',9575,'zoneOrSort','3562','Weaken the Ramparts - category Hellfire Citadel -> Hellfire Ramparts'),('quests',11354,'zoneOrSort','3562','Wanted: Nazan\'s Riding Crop - category Hellfire Citadel -> Hellfire Ramparts'),('quests',9589,'zoneOrSort','3713','The Blood is Life - category Hellfire Citadel -> Blood Furnace'),('quests',9590,'zoneOrSort','3713','The Blood is Life - category Hellfire Citadel -> Blood Furnace'),('quests',9607,'zoneOrSort','3713','Heart of Rage - category Hellfire Citadel -> Blood Furnace'),('quests',9608,'zoneOrSort','3713','Heart of Rage - category Hellfire Citadel -> Blood Furnace'),('quests',11362,'zoneOrSort','3713','Wanted: Keli\'dan\'s Feathered Stave - category Hellfire Citadel -> Blood Furnace'),('quests',9492,'zoneOrSort','3714','Turning the Tide - category Hellfire Citadel -> Shattered Halls'),('quests',9493,'zoneOrSort','3714','Pride of the Fel Horde - category Hellfire Citadel -> Shattered Halls'),('quests',9494,'zoneOrSort','3714','Fel Embers - category Hellfire Citadel -> Shattered Halls'),('quests',9495,'zoneOrSort','3714','The Will of the Warchief - category Hellfire Citadel -> Shattered Halls'),('quests',9496,'zoneOrSort','3714','Pride of the Fel Horde - category Hellfire Citadel -> Shattered Halls'),('quests',9497,'zoneOrSort','3714','Emblem of the Fel Horde - category Hellfire Citadel -> Shattered Halls'),('quests',9524,'zoneOrSort','3714','Imprisoned in the Citadel - category Hellfire Citadel -> Shattered Halls'),('quests',9525,'zoneOrSort','3714','Imprisoned in the Citadel - category Hellfire Citadel -> Shattered Halls'),('quests',11363,'zoneOrSort','3714','Wanted: Bladefist\'s Seal - category Hellfire Citadel -> Shattered Halls'),('quests',11364,'zoneOrSort','3714','Wanted: Shattered Hand Centurions - category Hellfire Citadel -> Shattered Halls'),('pet',30,'expansion','1','Pet - Dragonhawk: BC'),('pet',31,'expansion','1','Pet - Ravager: BC'),('pet',32,'expansion','1','Pet - Warp Stalker: BC'),('pet',33,'expansion','1','Pet - Sporebat: BC'),('pet',34,'expansion','1','Pet - Nether Ray: BC'),('pet',37,'expansion','2','Pet - Moth: WotLK'),('pet',38,'expansion','2','Pet - Chimaera: WotLK'),('pet',39,'expansion','2','Pet - Devilsaur: WotLK'),('pet',41,'expansion','2','Pet - Silithid: WotLK'),('pet',42,'expansion','2','Pet - Worm: WotLK'),('pet',43,'expansion','2','Pet - Rhino: WotLK'),('pet',44,'expansion','2','Pet - Wasp: WotLK'),('pet',45,'expansion','2','Pet - Core Hound: WotLK'),('pet',46,'expansion','2','Pet - Spirit Beast: WotLK'); /*!40000 ALTER TABLE `aowow_setup_custom_data` ENABLE KEYS */; UNLOCK TABLES; /*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */; diff --git a/setup/sql/updates/1763200071_01.sql b/setup/sql/updates/1763200071_01.sql new file mode 100644 index 00000000..27b16cdf --- /dev/null +++ b/setup/sql/updates/1763200071_01.sql @@ -0,0 +1,22 @@ +UPDATE `aowow_pet` SET `expansion` = 1 WHERE `id` IN (30, 31, 32, 33, 34); +UPDATE `aowow_pet` SET `expansion` = 2 WHERE `id` IN (37, 38, 39, 41, 42, 43, 44, 45, 46); + +DELETE FROM `aowow_setup_custom_data` WHERE `command` = 'pet' AND `field` = 'expansion'; +INSERT INTO `aowow_setup_custom_data` VALUES + ('pet', 30, 'expansion', 1, 'Pet - Dragonhawk: BC'), + ('pet', 31, 'expansion', 1, 'Pet - Ravager: BC'), + ('pet', 32, 'expansion', 1, 'Pet - Warp Stalker: BC'), + ('pet', 33, 'expansion', 1, 'Pet - Sporebat: BC'), + ('pet', 34, 'expansion', 1, 'Pet - Nether Ray: BC'), + ('pet', 37, 'expansion', 2, 'Pet - Moth: WotLK'), + ('pet', 38, 'expansion', 2, 'Pet - Chimaera: WotLK'), + ('pet', 39, 'expansion', 2, 'Pet - Devilsaur: WotLK'), + ('pet', 41, 'expansion', 2, 'Pet - Silithid: WotLK'), + ('pet', 42, 'expansion', 2, 'Pet - Worm: WotLK'), + ('pet', 43, 'expansion', 2, 'Pet - Rhino: WotLK'), + ('pet', 44, 'expansion', 2, 'Pet - Wasp: WotLK'), + ('pet', 45, 'expansion', 2, 'Pet - Core Hound: WotLK'), + ('pet', 46, 'expansion', 2, 'Pet - Spirit Beast: WotLK') +; + +UPDATE `aowow_dbversion` SET `sql` = CONCAT(IFNULL(`sql`, ''), ' pet'); diff --git a/setup/tools/sqlgen/pet.ss.php b/setup/tools/sqlgen/pet.ss.php index 20b08a99..6095aced 100644 --- a/setup/tools/sqlgen/pet.ss.php +++ b/setup/tools/sqlgen/pet.ss.php @@ -11,6 +11,8 @@ if (!CLI) CLISetup::registerSetup("sql", new class extends SetupScript { + use TrCustomData; + protected $info = array( 'pet' => [[], CLISetup::ARGV_PARAM, 'Compiles data for type: Pet from dbc and world db.'] ); @@ -25,69 +27,64 @@ CLISetup::registerSetup("sql", new class extends SetupScript // basic copy from creaturefamily.dbc DB::Aowow()->query( - 'INSERT INTO ?_pet - SELECT f.id, - categoryEnumId, - 0, -- cuFlags - 0, -- minLevel - 0, -- maxLevel - petFoodMask, - petTalentType, - 0, -- exotic - 0, -- expansion - name_loc0, name_loc2, name_loc3, name_loc4, name_loc6, name_loc8, - ic.id, - skillLine1, - 0, 0, 0, 0, -- spell[1-4] - 0, 0, 0 -- armor, damage, health - FROM dbc_creaturefamily f - LEFT JOIN ?_icons ic ON ic.name = LOWER(SUBSTRING_INDEX(f.iconString, "\\\\", -1)) - WHERE petTalentType <> -1' + 'INSERT INTO ?_pet + SELECT f.`id`, + `categoryEnumId`, + 0, -- cuFlags + 0, -- minLevel + 0, -- maxLevel + `petFoodMask`, + `petTalentType`, + 0, -- exotic + 0, -- expansion + `name_loc0`, `name_loc2`, `name_loc3`, `name_loc4`, `name_loc6`, `name_loc8`, + ic.`id`, + `skillLine1`, + 0, 0, 0, 0, -- spell[1-4] + 0, 0, 0 -- armor, damage, health + FROM dbc_creaturefamily f + LEFT JOIN ?_icons ic ON ic.`name` = LOWER(SUBSTRING_INDEX(f.`iconString`, "\\\\", -1)) + WHERE `petTalentType` <> -1' ); // stats from craeture_template $spawnInfo = DB::World()->query( - 'SELECT ct.family AS ARRAY_KEY, - MIN(ct.minlevel) AS minLevel, - MAX(ct.maxlevel) AS maxLevel, - IF(ct.type_flags & 0x10000, 1, 0) AS exotic + 'SELECT ct.`family` AS ARRAY_KEY, + MIN(ct.`minlevel`) AS "minLevel", + MAX(ct.`maxlevel`) AS "maxLevel", + IF(ct.`type_flags` & ?d, 1, 0) AS "exotic" FROM creature_template ct - JOIN creature c ON ct.entry = c.id - WHERE ct.type_flags & 0x1 - GROUP BY ct.family' + JOIN creature c ON ct.`entry` = c.`id` + WHERE ct.`type_flags` & ?d + GROUP BY ct.`family`', + NPC_TYPEFLAG_EXOTIC_PET, NPC_TYPEFLAG_TAMEABLE ); foreach ($spawnInfo as $id => $info) DB::Aowow()->query('UPDATE ?_pet SET ?a WHERE id = ?d', $info, $id); // add petFamilyModifier to health, mana, dmg DB::Aowow()->query( - 'UPDATE ?_pet p, - dbc_skilllineability sla, - dbc_spell s - SET armor = s.effect2BasePoints + s.effect2DieSides, - damage = s.effect1BasePoints + s.effect1DieSides, - health = s.effect3BasePoints + s.effect3DieSides - WHERE p.skillLineId = sla.skillLineId AND - sla.spellId = s.id AND - s.name_loc0 = "Tamed Pet Passive (DND)"' + 'UPDATE ?_pet p, + dbc_skilllineability sla, + dbc_spell s + SET `armor` = s.`effect2BasePoints` + s.`effect2DieSides`, + `damage` = s.`effect1BasePoints` + s.`effect1DieSides`, + `health` = s.`effect3BasePoints` + s.`effect3DieSides` + WHERE p.`skillLineId` = sla.`skillLineId` AND + sla.`spellId` = s.`id` AND + s.`name_loc0` = "Tamed Pet Passive (DND)"' ); - // add expansion manually - /********************/ - /* TODO: MOVE TO DB */ - /********************/ - DB::Aowow()->query('UPDATE ?_pet SET expansion = 1 WHERE id IN (30, 31, 32, 33, 34)'); - DB::Aowow()->query('UPDATE ?_pet SET expansion = 2 WHERE id IN (37, 38, 39, 41, 42, 43, 44, 45, 46)'); - // assign pet spells $pets = DB::Aowow()->select( - 'SELECT p.id, MAX(s.id) AS spell - FROM dbc_skilllineability sla - JOIN ?_pet p ON p.skillLineId = sla.skillLineId - JOIN dbc_spell s ON sla.spellId = s.id - LEFT OUTER JOIN dbc_talent t ON s.id = t.rank1 - WHERE (s.attributes0 & 0x40) = 0 AND t.id IS NULL - GROUP BY s.name_loc0, p.id' + 'SELECT p.`id`, MAX(s.`id`) AS "spell" + FROM dbc_skilllineability sla + JOIN ?_pet p ON p.`skillLineId` = sla.`skillLineId` + JOIN dbc_spell s ON sla.`spellId` = s.`id` + LEFT OUTER JOIN dbc_talent t ON s.`id` = t.`rank1` + WHERE (s.`attributes0` & ?d) = 0 AND t.`id` IS NULL + GROUP BY s.`name_loc0`, p.`id`', + SPELL_ATTR0_PASSIVE ); $petSpells = []; @@ -100,7 +97,7 @@ CLISetup::registerSetup("sql", new class extends SetupScript } foreach ($petSpells as $petId => $row) - DB::Aowow()->query('UPDATE ?_pet SET ?a WHERE id = ?d', $row, $petId); + DB::Aowow()->query('UPDATE ?_pet SET ?a WHERE `id` = ?d', $row, $petId); $this->reapplyCCFlags('pet', Type::PET); From 03bab92cb8c02743428b118f11ad909504aba2c2 Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Sat, 15 Nov 2025 20:00:50 +0100 Subject: [PATCH 114/260] DateTime/Fixup * fix displaying expected '0 seconds' instead of 'n/a' or '1 ms' --- localization/datetime.class.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/localization/datetime.class.php b/localization/datetime.class.php index f6789390..8e4685d4 100644 --- a/localization/datetime.class.php +++ b/localization/datetime.class.php @@ -140,7 +140,7 @@ class DateTime extends \DateTimeImmutable */ public static function formatTimeElapsedFloat(int $delay) : string { - $delay = max($delay, 1); + $delay = abs($delay); for ($i = 0; $i < count(self::RANGE); ++$i) { @@ -151,7 +151,7 @@ class DateTime extends \DateTimeImmutable return $v . self::NBSP . Lang::timeUnits($v === 1.0 ? 'sg' : 'pl', $i); } - return Lang::main('n_a'); + return '0' . self::NBSP . Lang::timeUnits('pl', 6); // 0 seconds } /** From a2b87da285da3b69f252ba9794a81a9c3aac910f Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Sat, 15 Nov 2025 20:34:14 +0100 Subject: [PATCH 115/260] Localization/CN * fix excess whitespaces betweeen number and unit --- localization/datetime.class.php | 12 ++++++++---- localization/locale_dede.php | 2 +- localization/locale_enus.php | 2 +- localization/locale_eses.php | 2 +- localization/locale_frfr.php | 2 +- localization/locale_ruru.php | 2 +- template/pages/spell.tpl.php | 2 +- 7 files changed, 14 insertions(+), 10 deletions(-) diff --git a/localization/datetime.class.php b/localization/datetime.class.php index 8e4685d4..98d60c6e 100644 --- a/localization/datetime.class.php +++ b/localization/datetime.class.php @@ -141,6 +141,7 @@ class DateTime extends \DateTimeImmutable public static function formatTimeElapsedFloat(int $delay) : string { $delay = abs($delay); + $nbsp = Lang::getLocale()->isLogographic() ? '' : self::NBSP; for ($i = 0; $i < count(self::RANGE); ++$i) { @@ -148,10 +149,10 @@ class DateTime extends \DateTimeImmutable continue; $v = round($delay / self::RANGE[$i], 2); - return $v . self::NBSP . Lang::timeUnits($v === 1.0 ? 'sg' : 'pl', $i); + return $v . $nbsp . Lang::timeUnits($v === 1.0 ? 'sg' : 'pl', $i); } - return '0' . self::NBSP . Lang::timeUnits('pl', 6); // 0 seconds + return '0' . $nbsp . Lang::timeUnits('pl', 6); // 0 seconds } /** @@ -181,9 +182,10 @@ class DateTime extends \DateTimeImmutable $i2 = $subunit[$i1]; $delay %= self::RANGE[$i1]; $v2 = floor($delay / self::RANGE[$i2]); + $nbsp = Lang::getLocale()->isLogographic() ? '' : self::NBSP; if ($v2 > 0) - return self::OMG($v1, $i1, true) . self::NBSP . self::OMG($v2, $i2, true); + return self::OMG($v1, $i1, true) . $nbsp . self::OMG($v2, $i2, true); } return self::OMG($v1, $i1, false); @@ -206,7 +208,9 @@ class DateTime extends \DateTimeImmutable if ($abbrv && !Lang::timeUnits('ab', $unit)) $abbrv = false; - return $value .= self::NBSP . match(true) + $nbsp = Lang::getLocale()->isLogographic() ? '' : self::NBSP; + + return $value .= $nbsp . match(true) { $abbrv => Lang::timeUnits('ab', $unit), $value == 1 => Lang::timeUnits('sg', $unit), diff --git a/localization/locale_dede.php b/localization/locale_dede.php index 956604e2..b7c75473 100644 --- a/localization/locale_dede.php +++ b/localization/locale_dede.php @@ -1653,7 +1653,7 @@ $lang = array( '_range' => "Reichweite", '_castTime' => "Zauberzeit", '_cooldown' => "Abklingzeit", - '_distUnit' => "Meter", + '_distUnit' => " Meter", '_forms' => "Gestalten", '_aura' => "Aura", '_effect' => "Effekt", diff --git a/localization/locale_enus.php b/localization/locale_enus.php index 8361d2b4..ae57cd91 100644 --- a/localization/locale_enus.php +++ b/localization/locale_enus.php @@ -1653,7 +1653,7 @@ $lang = array( '_range' => "Range", '_castTime' => "Cast time", '_cooldown' => "Cooldown", - '_distUnit' => "yards", + '_distUnit' => " yards", '_forms' => "Forms", '_aura' => "Aura", '_effect' => "Effect", diff --git a/localization/locale_eses.php b/localization/locale_eses.php index d1687ea0..67d5486d 100644 --- a/localization/locale_eses.php +++ b/localization/locale_eses.php @@ -1653,7 +1653,7 @@ $lang = array( '_range' => "Rango", '_castTime' => "Tiempo de lanzamiento", '_cooldown' => "Reutilización", - '_distUnit' => "metros", + '_distUnit' => " metros", '_forms' => "Formas", '_aura' => "Aura", '_effect' => "Efecto", diff --git a/localization/locale_frfr.php b/localization/locale_frfr.php index 3da25911..9c98f2ed 100644 --- a/localization/locale_frfr.php +++ b/localization/locale_frfr.php @@ -1653,7 +1653,7 @@ $lang = array( '_range' => "Portée", '_castTime' => "Incantation", '_cooldown' => "Recharge", - '_distUnit' => "mètres", + '_distUnit' => " mètres", '_forms' => "Formes", '_aura' => "Aura", '_effect' => "Effet", diff --git a/localization/locale_ruru.php b/localization/locale_ruru.php index e2466a5a..383dd2ee 100644 --- a/localization/locale_ruru.php +++ b/localization/locale_ruru.php @@ -1653,7 +1653,7 @@ $lang = array( '_range' => "Радиус действия", '_castTime' => "Применение", '_cooldown' => "Восстановление", - '_distUnit' => "метров", + '_distUnit' => " метров", '_forms' => "Форма", '_aura' => "аура", '_effect' => "Эффект", diff --git a/template/pages/spell.tpl.php b/template/pages/spell.tpl.php index c03f6ad1..a2f8adcd 100644 --- a/template/pages/spell.tpl.php +++ b/template/pages/spell.tpl.php @@ -119,7 +119,7 @@ endif; - range.' '.Lang::spell('_distUnit').' ('.$this->rangeName.')';?> + range.Lang::spell('_distUnit').' ('.$this->rangeName.')';?> From 4cb544182d0cbe9dc6f756092a72c9d942f4dd90 Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Sat, 15 Nov 2025 21:02:50 +0100 Subject: [PATCH 116/260] Setup/Spawns * evaluate waypoint paths linked via creature_template_addon --- setup/sql/updates/1763240934_01.sql | 1 + setup/tools/sqlgen/spawns.ss.php | 17 +++++++++++++---- 2 files changed, 14 insertions(+), 4 deletions(-) create mode 100644 setup/sql/updates/1763240934_01.sql diff --git a/setup/sql/updates/1763240934_01.sql b/setup/sql/updates/1763240934_01.sql new file mode 100644 index 00000000..c0c38b25 --- /dev/null +++ b/setup/sql/updates/1763240934_01.sql @@ -0,0 +1 @@ +UPDATE `aowow_dbversion` SET `sql` = CONCAT(IFNULL(`sql`, ''), ' spawns'); diff --git a/setup/tools/sqlgen/spawns.ss.php b/setup/tools/sqlgen/spawns.ss.php index f139a424..522fead5 100644 --- a/setup/tools/sqlgen/spawns.ss.php +++ b/setup/tools/sqlgen/spawns.ss.php @@ -24,7 +24,7 @@ CLISetup::registerSetup("sql", new class extends SetupScript ); protected $dbcSourceFiles = ['worldmaparea', 'map', 'taxipathnode', 'soundemitters', 'areatrigger', 'areatable']; - protected $worldDependency = ['creature', 'creature_addon', 'gameobject', 'gameobject_template', 'vehicle_accessory', 'vehicle_accessory_template', 'script_waypoint', 'waypoints', 'waypoint_data', 'smart_scripts', 'areatrigger_teleport']; + protected $worldDependency = ['creature', 'creature_addon', 'creature_template_addon', 'gameobject', 'gameobject_template', 'vehicle_accessory', 'vehicle_accessory_template', 'script_waypoint', 'waypoints', 'waypoint_data', 'smart_scripts', 'areatrigger_teleport']; protected $setupAfter = [['dungeonmap', 'worldmaparea', 'zones'], ['img-maps']]; private $transports = []; @@ -214,9 +214,11 @@ CLISetup::registerSetup("sql", new class extends SetupScript { // [guid, type, typeId, map, posX, posY [, respawn, spawnMask, phaseMask, areaId, floor, pathId]] return DB::World()->select( - 'SELECT c.`guid`, ?d AS `type`, c.`id` AS `typeId`, c.`map`, c.`position_x` AS `posX`, c.`position_y` AS `posY`, c.`spawntimesecs` AS `respawn`, c.`spawnMask`, c.`phaseMask`, c.`zoneId` AS `areaId`, IFNULL(ca.`path_id`, 0) AS `pathId` + 'SELECT c.`guid`, ?d AS `type`, c.`id` AS `typeId`, c.`map`, c.`position_x` AS `posX`, c.`position_y` AS `posY`, c.`spawntimesecs` AS `respawn`, c.`spawnMask`, c.`phaseMask`, c.`zoneId` AS `areaId`, IFNULL(ca.`path_id`, IFNULL(cta.`path_id`, 0)) AS `pathId` FROM creature c - LEFT JOIN creature_addon ca ON ca.guid = c.guid', + LEFT JOIN creature_addon ca ON ca.guid = c.guid + LEFT JOIN creature_template_addon cta ON cta.entry = c.id + GROUP BY c.id', Type::NPC ); } @@ -277,6 +279,8 @@ CLISetup::registerSetup("sql", new class extends SetupScript { // todo (med): at least `waypoints` can contain paths that do not belong to a creature but get assigned by SmartAI (or script) during runtime // in the future guid should be optional and additional parameters substituting guid should be passed down from NpcPage after SmartAI has been evaluated + + // assume that creature_template_addon data isn't stupid and only creatures with a single spawn are referenced here return DB::World()->select( 'SELECT c.`guid`, w.`entry` AS `creatureOrPath`, w.`pointId` AS `point`, c.`zoneId` AS `areaId`, c.`map`, w.`waittime` AS `wait`, w.`location_x` AS `posX`, w.`location_y` AS `posY` FROM creature c @@ -288,7 +292,12 @@ CLISetup::registerSetup("sql", new class extends SetupScript FROM creature c JOIN creature_addon ca ON ca.`guid` = c.`guid` JOIN waypoint_data w ON w.`id` = ca.`path_id` - WHERE ca.`path_id` <> 0' + WHERE ca.`path_id` <> 0 UNION + SELECT c.`guid`, -w.`id` AS `creatureOrPath`, w.`point`, c.`zoneId` AS `areaId`, c.`map`, w.`delay` AS `wait`, w.`position_x` AS `posX`, w.`position_y` AS `posY` + FROM creature c + JOIN creature_template_addon cta ON cta.`entry` = c.`id` + JOIN waypoint_data w ON w.`id` = cta.`path_id` + WHERE cta.`path_id` <> 0' ); } From 57665aaa9e0b64a010a6215c426ef9d4b231da30 Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Wed, 19 Nov 2025 14:31:31 +0100 Subject: [PATCH 117/260] ItemDetailPage/Misc * fix "vendor in" mapper * add "fished in" mapper * move 'see-also' and 'same-model-as' tabs to the back of the tabs list --- endpoints/item/item.php | 137 ++++++++++++++++++++--------------- localization/locale_dede.php | 4 +- localization/locale_enus.php | 4 +- localization/locale_eses.php | 6 +- localization/locale_frfr.php | 4 +- localization/locale_ruru.php | 4 +- localization/locale_zhcn.php | 4 +- template/pages/item.tpl.php | 4 +- 8 files changed, 101 insertions(+), 66 deletions(-) diff --git a/endpoints/item/item.php b/endpoints/item/item.php index 34d94c1b..4a158b7b 100644 --- a/endpoints/item/item.php +++ b/endpoints/item/item.php @@ -474,6 +474,28 @@ class ItemBaseResponse extends TemplateResponse implements ICache } } + if ($idx == 15 && !$this->map) + { + $nodeIds = array_map(fn($x) => $x['id'], $tabData['data']); + $fishedIn = new GameObjectList(array(['id', $nodeIds])); + if (!$fishedIn->error) + { + // show mapper for fishing locations + if ($nodeSpawns = $fishedIn->getSpawns(SPAWNINFO_FULL, true, true, true, true)) + { + $this->map = array( + ['parent' => 'mapper-generic'], // Mapper + $nodeSpawns, // mapperData + null, // ShowOnMap + [Lang::item('fishedIn')], // foundIn + Lang::item('fishingLoc') // title + ); + foreach ($nodeSpawns as $areaId => $_) + $this->map[3][$areaId] = ZoneList::getName($areaId); + } + } + } + if ($template == 'npc' || $template == 'object') $this->addDataLoader('zones'); @@ -633,40 +655,6 @@ class ItemBaseResponse extends TemplateResponse implements ICache } } - // tab: see also - $conditions = array( - ['id', $this->typeId, '!'], - [ - 'OR', - ['name_loc'.Lang::getLocale()->value, $this->subject->getField('name', true)], - [ - 'AND', - ['class', $_class], - ['subClass', $_subClass], - ['slot', $_slot], - ['itemLevel', $_ilvl - 15, '>'], - ['itemLevel', $_ilvl + 15, '<'], - ['quality', $this->subject->getField('quality')], - ['requiredClass', $this->subject->getField('requiredClass') ?: -1] // todo: fix db data in setup and not on fetch - ] - ] - ); - - if ($_ = $this->subject->getField('itemset')) - $conditions[1][] = ['AND', ['slot', $_slot], ['itemset', $_]]; - - $saItems = new ItemList($conditions); - if (!$saItems->error) - { - $this->extendGlobalData($saItems->getJSGlobals(GLOBALINFO_SELF)); - - $this->lvTabs->addListviewTab(new Listview(array( - 'data' => $saItems->getListviewData(), - 'name' => '$LANG.tab_seealso', - 'id' => 'see-also' - ), ItemList::$brickFile)); - } - // tab: starts (quest) if ($qId = $this->subject->getField('startQuest')) { @@ -719,24 +707,6 @@ class ItemBaseResponse extends TemplateResponse implements ICache ), QuestList::$brickFile)); } - // tab: same model as - // todo (low): should also work for creatures summoned by item - if (($model = $this->subject->getField('model')) && $_slot) - { - $sameModel = new ItemList(array(['model', $model], ['id', $this->typeId, '!'], ['slot', $_slot])); - if (!$sameModel->error) - { - $this->extendGlobalData($sameModel->getJSGlobals(GLOBALINFO_SELF)); - - $this->lvTabs->addListviewTab(new Listview(array( - 'data' => $sameModel->getListviewData(ITEMINFO_MODEL), - 'name' => '$LANG.tab_samemodelas', - 'id' => 'same-model-as', - 'genericlinktype' => 'item' - ), 'genericmodel')); - } - } - // tab: sold by if (!empty($this->subject->getExtendedCost()[$this->typeId])) { @@ -748,13 +718,14 @@ class ItemBaseResponse extends TemplateResponse implements ICache if ($vendorSpawns = $soldBy->getSpawns(SPAWNINFO_FULL, true, true, true, true)) { $this->map = array( - ['parent' => 'mapper-generic'], // Mapper - $vendorSpawns, // mapperData - null, // ShowOnMap - [Lang::item('purchasedIn')] // foundIn + ['parent' => 'mapper-generic'], // Mapper + $vendorSpawns, // mapperData + null, // ShowOnMap + [Lang::item('purchasedIn')], // foundIn + Lang::item('vendorLoc') // title ); foreach ($vendorSpawns as $areaId => $_) - $this->map['extra'][$areaId] = ZoneList::getName($areaId); + $this->map[3][$areaId] = ZoneList::getName($areaId); } $sbData = $soldBy->getListviewData(); @@ -904,6 +875,58 @@ class ItemBaseResponse extends TemplateResponse implements ICache } } + // tab: see also + $conditions = array( + ['id', $this->typeId, '!'], + [ + 'OR', + ['name_loc'.Lang::getLocale()->value, $this->subject->getField('name', true)], + [ + 'AND', + ['class', $_class], + ['subClass', $_subClass], + ['slot', $_slot], + ['itemLevel', $_ilvl - 15, '>'], + ['itemLevel', $_ilvl + 15, '<'], + ['quality', $this->subject->getField('quality')], + ['requiredClass', $this->subject->getField('requiredClass') ?: -1] // todo: fix db data in setup and not on fetch + ] + ] + ); + + if ($_ = $this->subject->getField('itemset')) + $conditions[1][] = ['AND', ['slot', $_slot], ['itemset', $_]]; + + $saItems = new ItemList($conditions); + if (!$saItems->error) + { + $this->extendGlobalData($saItems->getJSGlobals(GLOBALINFO_SELF)); + + $this->lvTabs->addListviewTab(new Listview(array( + 'data' => $saItems->getListviewData(), + 'name' => '$LANG.tab_seealso', + 'id' => 'see-also' + ), ItemList::$brickFile)); + } + + // tab: same model as + // todo (low): should also work for creatures summoned by item + if (($model = $this->subject->getField('model')) && $_slot) + { + $sameModel = new ItemList(array(['model', $model], ['id', $this->typeId, '!'], ['slot', $_slot])); + if (!$sameModel->error) + { + $this->extendGlobalData($sameModel->getJSGlobals(GLOBALINFO_SELF)); + + $this->lvTabs->addListviewTab(new Listview(array( + 'data' => $sameModel->getListviewData(ITEMINFO_MODEL), + 'name' => '$LANG.tab_samemodelas', + 'id' => 'same-model-as', + 'genericlinktype' => 'item' + ), 'genericmodel')); + } + } + // tab: Shared cooldown $cdCats = []; $useSpells = []; diff --git a/localization/locale_dede.php b/localization/locale_dede.php index b7c75473..a094562b 100644 --- a/localization/locale_dede.php +++ b/localization/locale_dede.php @@ -2265,8 +2265,10 @@ $lang = array( 'uniqueEquipped'=> ["Einzigartig anlegbar", null, "Einzigartig angelegt: %s (%d)"], 'speed' => "Tempo", 'dps' => "(%.1f Schaden pro Sekunde)", - 'vendorIn' => "Händlerstandpunkte", + 'vendorLoc' => "Händlerstandpunkte", 'purchasedIn' => "Dieser Gegenstand kann gekauft werden in", + 'fishingLoc' => "Angelplätze", + 'fishedIn' => "Dieser Gegenstand kann geangelt werden in", 'duration' => array( // ITEM_DURATION_* '', "Dauer: %d Sek.", diff --git a/localization/locale_enus.php b/localization/locale_enus.php index ae57cd91..42d18fd6 100644 --- a/localization/locale_enus.php +++ b/localization/locale_enus.php @@ -2265,8 +2265,10 @@ $lang = array( 'uniqueEquipped'=> ["Unique-Equipped", null, "Unique-Equipped: %s (%d)"], // ITEM_UNIQUE_EQUIPPABLE, null, ITEM_LIMIT_CATEGORY_MULTIPLE 'speed' => "Speed", // SPEED 'dps' => "(%.1f damage per second)", // DPS_TEMPLATE - 'vendorIn' => "Vendor Locations", + 'vendorLoc' => "Vendor Locations", 'purchasedIn' => "This item can be purchased in", + 'fishingLoc' => "Fishing Locations", + 'fishedIn' => "This item can be fished in", 'duration' => array( // ITEM_DURATION_* '', "Duration: %d sec", diff --git a/localization/locale_eses.php b/localization/locale_eses.php index 67d5486d..d3951955 100644 --- a/localization/locale_eses.php +++ b/localization/locale_eses.php @@ -2265,8 +2265,10 @@ $lang = array( 'uniqueEquipped'=> ["Único-Equipado", null, "Único-Equipado: %s (%d)"], 'speed' => "Veloc.", 'dps' => "(%.1f daño por segundo)", - 'vendorIn' => "Ubicación de Vendedores", - 'purchasedIn' => "[This item can be purchased in]", + 'vendorLoc' => "Ubicación de Vendedores", + 'purchasedIn' => "Este objeto se puede comprar en", + 'fishingLoc' => "Lugares de pesca", + 'fishedIn' => "Este objeto se puede pescar en", 'duration' => array( '', "Duración: %d s", diff --git a/localization/locale_frfr.php b/localization/locale_frfr.php index 9c98f2ed..d0247e71 100644 --- a/localization/locale_frfr.php +++ b/localization/locale_frfr.php @@ -2265,8 +2265,10 @@ $lang = array( 'uniqueEquipped'=> ["Unique - Equipé", null, "Unique - Equipé: %s (%d)"], // ITEM_UNIQUE_EQUIPPABLE, null, ITEM_LIMIT_CATEGORY_MULTIPLE 'speed' => "Vitesse", 'dps' => "(%.1f dégâts par seconde)", - 'vendorIn' => "Lieux de vente", + 'vendorLoc' => "Lieux de vente", 'purchasedIn' => "Cet objet peut être acheté dans", + 'fishingLoc' => "Lieux de pêche", + 'fishedIn' => "Cet objet peut être pêché dans", 'duration' => array( '', "Durée : %d sec", diff --git a/localization/locale_ruru.php b/localization/locale_ruru.php index 383dd2ee..00622880 100644 --- a/localization/locale_ruru.php +++ b/localization/locale_ruru.php @@ -2265,8 +2265,10 @@ $lang = array( 'uniqueEquipped'=> ["Уникальный использующийся", null, "Уникальный использующийся предмет: %s (%d)"], 'speed' => "Скорость", 'dps' => "(%.1f ед. урона в секунду)", - 'vendorIn' => "Расположение торговца", + 'vendorLoc' => "Расположение торговца", 'purchasedIn' => "Этот предмет приобретается в", + 'fishingLoc' => "Места рыбной ловли", + 'fishedIn' => "Этот предмет вылавливается в", 'duration' => array( '', "Срок действия: %d |4секунда:секунды:секунд;", diff --git a/localization/locale_zhcn.php b/localization/locale_zhcn.php index fbc1908e..f3e5ac65 100644 --- a/localization/locale_zhcn.php +++ b/localization/locale_zhcn.php @@ -2265,8 +2265,10 @@ $lang = array( 'uniqueEquipped'=> ["装备唯一", null, "装备唯一:%s (%d)"], 'speed' => "速度", 'dps' => "(每秒伤害%.1f)", - 'vendorIn' => "[Vendor Locations]", + 'vendorLoc' => "[Vendor Locations]", 'purchasedIn' => "[This item can be purchased in]", + 'fishingLoc' => "钓鱼地点", + 'fishedIn' => "此物品位于", 'duration' => array( '', "持续时间:%d秒", diff --git a/template/pages/item.tpl.php b/template/pages/item.tpl.php index f519205a..e4314d4d 100644 --- a/template/pages/item.tpl.php +++ b/template/pages/item.tpl.php @@ -34,8 +34,8 @@ endif; $this->brick('markup', ['markup' => $this->article]); -if (isset($this->map)): - echo "

    ".Lang::item('vendorIn')."

    \n"; +if ($this->map): + echo "

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

    \n"; $this->brick('mapper'); endif; From 7f29c1d4b7cb439c77705aea246f1af7b78ff1bd Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Wed, 19 Nov 2025 14:41:08 +0100 Subject: [PATCH 118/260] CurrencyDetailPage/Tabs * add currency column to display gains --- endpoints/currency/currency.php | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/endpoints/currency/currency.php b/endpoints/currency/currency.php index 346a71bd..81a0dada 100644 --- a/endpoints/currency/currency.php +++ b/endpoints/currency/currency.php @@ -128,6 +128,15 @@ class CurrencyBaseResponse extends TemplateResponse implements ICache if ($template == 'npc' || $template == 'object') $this->addDataLoader('zones'); + if ($template != 'quest') + { + foreach ($tabData['data'] as &$row) + if (!empty($row['stack'])) + $row['currency'] = [[$this->typeId, $row['stack'][0]]]; + + $tabData['extraCols'][] = '$Listview.extraCols.currency'; + } + $this->lvTabs->addListviewTab(new Listview($tabData, $template)); } } From 9db3e766daf9c999f06754cf55be77d48f8f9280 Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Wed, 19 Nov 2025 14:45:05 +0100 Subject: [PATCH 119/260] QuestDetailPage/ShowOnMap * increase strictness for sources of required items shown on mapper from 1% to 5% --- endpoints/quest/quest.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/endpoints/quest/quest.php b/endpoints/quest/quest.php index 1c21e771..7a0d3249 100644 --- a/endpoints/quest/quest.php +++ b/endpoints/quest/quest.php @@ -531,22 +531,22 @@ class QuestBaseResponse extends TemplateResponse implements ICache for the moment: if an item has >10 sources, only display sources with >80% chance - always filter sources with <1% chance + always filter sources with <5% chance */ $nSources = 0; foreach ($lootTabs->iterate() as [$type, $data]) if ($type == 'creature' || $type == 'object') - $nSources += count(array_filter($data['data'], function($val) { return $val['percent'] >= 1.0; })); + $nSources += count(array_filter($data['data'], fn($x) => $x['percent'] >= 5.0)); - foreach ($lootTabs->iterate() as $idx => [$file, $tabData]) + foreach ($lootTabs->iterate() as [$file, $tabData]) { if (!$tabData['data']) continue; foreach ($tabData['data'] as $data) { - if ($data['percent'] < 1.0) + if ($data['percent'] < 5.0) continue; if ($nSources > 10 && $data['percent'] < 80.0) From 9b905883dfa08169ee532c7bb2a0f540264e8830 Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Wed, 19 Nov 2025 15:34:02 +0100 Subject: [PATCH 120/260] Listview/Conditions * make column width flex --- setup/sql/updates/1763555620_01.sql | 1 + setup/tools/filegen/templates/global.js/listview.js | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 setup/sql/updates/1763555620_01.sql diff --git a/setup/sql/updates/1763555620_01.sql b/setup/sql/updates/1763555620_01.sql new file mode 100644 index 00000000..03adf418 --- /dev/null +++ b/setup/sql/updates/1763555620_01.sql @@ -0,0 +1 @@ +UPDATE `aowow_dbversion` SET `build` = CONCAT(IFNULL(`build`, ''), ' globaljs'); diff --git a/setup/tools/filegen/templates/global.js/listview.js b/setup/tools/filegen/templates/global.js/listview.js index 25038cc9..7942ba64 100644 --- a/setup/tools/filegen/templates/global.js/listview.js +++ b/setup/tools/filegen/templates/global.js/listview.js @@ -2945,7 +2945,6 @@ Listview.extraCols = { id: 'condition', name: LANG.tab_conditions, type: 'text', - width: '25%', compute: function(row, td) { if (!row.condition) @@ -2954,6 +2953,7 @@ Listview.extraCols = { td.className = 'small'; td.style.lineHeight = '18px'; td.style.textAlign = 'left'; + td.style.whiteSpace = 'nowrap'; // tiny links are hard to hit, hmkey? td.onclick = (e) => $WH.sp(e); From be3701df911ebd842ce56060759efa8a5fc47c61 Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Wed, 19 Nov 2025 17:32:26 +0100 Subject: [PATCH 121/260] Params/Fixup * FILTER_SANITIZE_URL is absurdly strict and will not tolerate umlauts or spaces replaced with printable chars regex --- endpoints/account/reset-password.php | 2 +- endpoints/account/signin.php | 2 +- endpoints/account/signout.php | 4 ++-- endpoints/account/signup.php | 2 +- endpoints/contactus/contactus.php | 18 +++++++++--------- 5 files changed, 14 insertions(+), 14 deletions(-) diff --git a/endpoints/account/reset-password.php b/endpoints/account/reset-password.php index 44c39b0b..c9abf6a8 100644 --- a/endpoints/account/reset-password.php +++ b/endpoints/account/reset-password.php @@ -25,7 +25,7 @@ class AccountresetpasswordResponse extends TemplateResponse protected array $expectedGET = array( 'key' => ['filter' => FILTER_VALIDATE_REGEXP, 'options' => ['regexp' => '/^[a-zA-Z0-9]{40}$/']], - 'next' => ['filter' => FILTER_SANITIZE_URL, 'flags' => FILTER_FLAG_STRIP_AOWOW ] + 'next' => ['filter' => FILTER_VALIDATE_REGEXP, 'options' => ['regexp' => '/^[[:print:]]+$/' ]] ); protected array $expectedPOST = array( 'key' => ['filter' => FILTER_VALIDATE_REGEXP, 'options' => ['regexp' => '/^[a-zA-Z0-9]{40}$/']], diff --git a/endpoints/account/signin.php b/endpoints/account/signin.php index ffcd15b7..9beea099 100644 --- a/endpoints/account/signin.php +++ b/endpoints/account/signin.php @@ -28,7 +28,7 @@ class AccountSigninResponse extends TemplateResponse ); protected array $expectedGET = array( 'key' => ['filter' => FILTER_VALIDATE_REGEXP, 'options' => ['regexp' => '/^[a-zA-Z0-9]{40}$/']], - 'next' => ['filter' => FILTER_SANITIZE_URL, 'flags' => FILTER_FLAG_STRIP_AOWOW ] + 'next' => ['filter' => FILTER_VALIDATE_REGEXP, 'options' => ['regexp' => '/^[[:print:]]+$/'] ] ); private bool $success = false; diff --git a/endpoints/account/signout.php b/endpoints/account/signout.php index 7447f92e..67eaeacf 100644 --- a/endpoints/account/signout.php +++ b/endpoints/account/signout.php @@ -11,8 +11,8 @@ class AccountSignoutResponse extends TextResponse use TrGetNext; protected array $expectedGET = array( - 'next' => ['filter' => FILTER_SANITIZE_URL, 'flags' => FILTER_FLAG_STRIP_LOW | FILTER_FLAG_STRIP_HIGH], - 'global' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkEmptySet'] ] + 'next' => ['filter' => FILTER_VALIDATE_REGEXP, 'options' => ['regexp' => '/^[[:print:]]+$/']], + 'global' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkEmptySet'] ] ); public function __construct(string $pageParam) diff --git a/endpoints/account/signup.php b/endpoints/account/signup.php index 734832b1..5fd827b5 100644 --- a/endpoints/account/signup.php +++ b/endpoints/account/signup.php @@ -26,7 +26,7 @@ class AccountSignupResponse extends TemplateResponse ); protected array $expectedGET = array( - 'next' => ['filter' => FILTER_SANITIZE_URL, 'flags' => FILTER_FLAG_STRIP_AOWOW] + 'next' => ['filter' => FILTER_VALIDATE_REGEXP, 'options' => ['regexp' => '/^[[:print:]]+$/']] ); private bool $success = false; diff --git a/endpoints/contactus/contactus.php b/endpoints/contactus/contactus.php index 196ad9c6..ace3fa02 100644 --- a/endpoints/contactus/contactus.php +++ b/endpoints/contactus/contactus.php @@ -9,15 +9,15 @@ if (!defined('AOWOW_REVISION')) class ContactusBaseResponse extends TextResponse { protected array $expectedPOST = array( - 'mode' => ['filter' => FILTER_VALIDATE_INT ], - 'reason' => ['filter' => FILTER_VALIDATE_INT ], - 'ua' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkTextLine']], - 'appname' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkTextLine']], - 'page' => ['filter' => FILTER_SANITIZE_URL ], - 'desc' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkTextBlob']], - 'id' => ['filter' => FILTER_VALIDATE_INT ], - 'relatedurl' => ['filter' => FILTER_SANITIZE_URL ], - 'email' => ['filter' => FILTER_SANITIZE_EMAIL ] + 'mode' => ['filter' => FILTER_VALIDATE_INT ], + 'reason' => ['filter' => FILTER_VALIDATE_INT ], + 'ua' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkTextLine'] ], + 'appname' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkTextLine'] ], + 'page' => ['filter' => FILTER_VALIDATE_REGEXP, 'options' => ['regexp' => '/^[[:print:]]+$/']], + 'desc' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkTextBlob'] ], + 'id' => ['filter' => FILTER_VALIDATE_INT ], + 'relatedurl' => ['filter' => FILTER_VALIDATE_REGEXP, 'options' => ['regexp' => '/^[[:print:]]+$/']], + 'email' => ['filter' => FILTER_SANITIZE_EMAIL ] ); /* responses From a5051c9bf59d59d9f994da037c915f2b2ccb1160 Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Wed, 19 Nov 2025 17:01:16 +0100 Subject: [PATCH 122/260] Loot/Modes * work against more correctly assigning instance mode to entities and loot - added manually collected data for difficulty versions of gameobjects, just boss chests for now. update setup/source to default object source to base difficulty version if able - update spelldifficulty table to contain the (likely) mapmode it will be used in * refactored class loot - implement loot mode indicators on listview for creature and gameobject loot - show 'drops' listview tab on instance zone page - fixes against tribute chest systems (toc / ulduar) - fix icc gunship battle chest ownership --- endpoints/currency/currency.php | 6 +- endpoints/item/item.php | 52 +- endpoints/npc/npc.php | 156 ++-- endpoints/object/object.php | 135 +++- endpoints/quest/quest.php | 8 +- endpoints/spell/spell.php | 75 +- endpoints/zone/zone.php | 59 +- .../Conditions/Conditions.class.php | 30 +- includes/components/dbtypelist.class.php | 37 +- includes/dbtypes/achievement.class.php | 4 +- includes/dbtypes/creature.class.php | 17 +- includes/dbtypes/gameobject.class.php | 2 +- includes/dbtypes/item.class.php | 7 +- includes/defines.php | 14 - includes/game/loot.class.php | 728 ------------------ includes/game/loot/loot.class.php | 71 ++ includes/game/loot/lootbycontainer.class.php | 329 ++++++++ includes/game/loot/lootbyitem.class.php | 439 +++++++++++ includes/kernel.php | 2 + includes/utilities.php | 21 + localization/locale_dede.php | 10 +- localization/locale_enus.php | 10 +- localization/locale_eses.php | 10 +- localization/locale_frfr.php | 10 +- localization/locale_ruru.php | 10 +- localization/locale_zhcn.php | 10 +- setup/sql/01-db_structure.sql | 21 + setup/sql/02-db_initial_data.sql | 11 + setup/sql/updates/1763557620_01.sql | 42 + setup/sql/updates/1763557620_02.sql | 18 + setup/tools/sqlgen/source.ss.php | 191 +++-- setup/tools/sqlgen/spelldifficulty.ss.php | 51 +- static/css/aowow.css | 2 +- 33 files changed, 1550 insertions(+), 1038 deletions(-) delete mode 100644 includes/game/loot.class.php create mode 100644 includes/game/loot/loot.class.php create mode 100644 includes/game/loot/lootbycontainer.class.php create mode 100644 includes/game/loot/lootbyitem.class.php create mode 100644 setup/sql/updates/1763557620_01.sql create mode 100644 setup/sql/updates/1763557620_02.sql diff --git a/endpoints/currency/currency.php b/endpoints/currency/currency.php index 81a0dada..8bf76fd1 100644 --- a/endpoints/currency/currency.php +++ b/endpoints/currency/currency.php @@ -117,9 +117,9 @@ class CurrencyBaseResponse extends TemplateResponse implements ICache if ($this->typeId != CURRENCY_HONOR_POINTS && $this->typeId != CURRENCY_ARENA_POINTS) { // tabs: this currency is contained in.. - $lootTabs = new Loot(); + $lootTabs = new LootByItem($_relItemId); - if ($lootTabs->getByItem($_relItemId)) + if ($lootTabs->getByItem()) { $this->extendGlobalData($lootTabs->jsGlobals); @@ -198,7 +198,7 @@ class CurrencyBaseResponse extends TemplateResponse implements ICache } } - // tab: created by (spell) [for items its handled in Loot::getByContainer()] + // tab: created by (spell) [for items its handled in LootByItem] if ($this->typeId == CURRENCY_HONOR_POINTS) { $createdBy = new SpellList(array(['effect1Id', SPELL_EFFECT_ADD_HONOR], ['effect2Id', SPELL_EFFECT_ADD_HONOR], ['effect3Id', SPELL_EFFECT_ADD_HONOR], 'OR')); diff --git a/endpoints/item/item.php b/endpoints/item/item.php index 4a158b7b..7a734022 100644 --- a/endpoints/item/item.php +++ b/endpoints/item/item.php @@ -443,9 +443,9 @@ class ItemBaseResponse extends TemplateResponse implements ICache } // tabs: this item is contained in.. - $lootTabs = new Loot(); + $lootTabs = new LootByItem($this->typeId); $createdBy = []; - if ($lootTabs->getByItem($this->typeId)) + if ($lootTabs->getByItem()) { $this->extendGlobalData($lootTabs->jsGlobals); @@ -454,27 +454,26 @@ class ItemBaseResponse extends TemplateResponse implements ICache if (!$tabData['data']) continue; - if ($idx == 16) + if ($idx == LootByItem::SPELL_CREATED) $createdBy = array_column($tabData['data'], 'id'); - if ($idx == 1) + if ($idx == LootByItem::ITEM_DISENCHANTED) $tabData['note'] = sprintf(Util::$filterResultString, '?items&filter=cr=163;crs='.$this->typeId.';crv=0'); - if ($idx == 4 && $this->subject->getSources($s, $sm) && $s[0] == SRC_DROP && isset($sm[0]['dd'])) - { - switch ($sm[0]['dd']) + if ($idx == LootByItem::NPC_DROPPED && $this->subject->getSources($s, $sm) && $s[0] == SRC_DROP && isset($sm[0]['dd'])) + $tabData['note'] = match($sm[0]['dd']) { - case -1: $tabData['note'] = '$LANG.lvnote_itemdropsinnormalonly'; break; - case -2: $tabData['note'] = '$LANG.lvnote_itemdropsinheroiconly'; break; - case -3: $tabData['note'] = '$LANG.lvnote_itemdropsinnormalheroic'; break; - case 1: $tabData['note'] = '$LANG.lvnote_itemdropsinnormal10only'; break; - case 2: $tabData['note'] = '$LANG.lvnote_itemdropsinnormal25only'; break; - case 3: $tabData['note'] = '$LANG.lvnote_itemdropsinheroic10only'; break; - case 4: $tabData['note'] = '$LANG.lvnote_itemdropsinheroic25only'; break; - } - } + -1 => '$LANG.lvnote_itemdropsinnormalonly', + -2 => '$LANG.lvnote_itemdropsinheroiconly', + -3 => '$LANG.lvnote_itemdropsinnormalheroic', + 1 => '$LANG.lvnote_itemdropsinnormal10only', + 2 => '$LANG.lvnote_itemdropsinnormal25only', + 3 => '$LANG.lvnote_itemdropsinheroic10only', + 4 => '$LANG.lvnote_itemdropsinheroic25only', + default => null + }; - if ($idx == 15 && !$this->map) + if ($idx == LootByItem::OBJECT_FISHED && !$this->map) { $nodeIds = array_map(fn($x) => $x['id'], $tabData['data']); $fishedIn = new GameObjectList(array(['id', $nodeIds])); @@ -505,24 +504,25 @@ class ItemBaseResponse extends TemplateResponse implements ICache // tabs: this item contains.. $sourceFor = array( - [LOOT_ITEM, $this->typeId, '$LANG.tab_contains', 'contains', ['$Listview.extraCols.percent'], [] ], - [LOOT_PROSPECTING, $this->typeId, '$LANG.tab_prospecting', 'prospecting', ['$Listview.extraCols.percent'], ['side', 'slot', 'reqlevel']], - [LOOT_MILLING, $this->typeId, '$LANG.tab_milling', 'milling', ['$Listview.extraCols.percent'], ['side', 'slot', 'reqlevel']], - [LOOT_DISENCHANT, $this->subject->getField('disenchantId'), '$LANG.tab_disenchanting', 'disenchanting', ['$Listview.extraCols.percent'], ['side', 'slot', 'reqlevel']] + [Loot::ITEM, $this->typeId, '$LANG.tab_contains', 'contains', ['$Listview.extraCols.percent'], [] ], + [Loot::PROSPECTING, $this->typeId, '$LANG.tab_prospecting', 'prospecting', ['$Listview.extraCols.percent'], ['side', 'slot', 'reqlevel']], + [Loot::MILLING, $this->typeId, '$LANG.tab_milling', 'milling', ['$Listview.extraCols.percent'], ['side', 'slot', 'reqlevel']], + [Loot::DISENCHANT, $this->subject->getField('disenchantId'), '$LANG.tab_disenchanting', 'disenchanting', ['$Listview.extraCols.percent'], ['side', 'slot', 'reqlevel']] ); foreach ($sourceFor as [$lootTemplate, $lootId, $tabName, $tabId, $extraCols, $hiddenCols]) { - $lootTab = new Loot(); - if ($lootTab->getByContainer($lootTemplate, $lootId)) + $lootTab = new LootByContainer(); + if ($lootTab->getByContainer($lootTemplate, [$lootId])) { $this->extendGlobalData($lootTab->jsGlobals); $extraCols = array_merge($extraCols, $lootTab->extraCols); $tabData = array( - 'data' => $lootTab->getResult(), - 'name' => $tabName, - 'id' => $tabId, + 'data' => $lootTab->getResult(), + 'name' => $tabName, + 'id' => $tabId, + 'computeDataFunc' => '$Listview.funcBox.initLootTable' ); if ($extraCols) diff --git a/endpoints/npc/npc.php b/endpoints/npc/npc.php index b7faf170..8d126feb 100644 --- a/endpoints/npc/npc.php +++ b/endpoints/npc/npc.php @@ -103,18 +103,17 @@ class NpcBaseResponse extends TemplateResponse implements ICache /**********************/ $mapType = 0; - if ($maps = DB::Aowow()->selectCol('SELECT DISTINCT `areaId` FROM ?_spawns WHERE `type` = ?d AND `typeId` = ?d', Type::NPC, $this->typeId)) + if ($maps = DB::Aowow()->selectCell('SELECT IF(COUNT(DISTINCT `areaId`) > 1, 0, `areaId`) FROM ?_spawns WHERE `type` = ?d AND `typeId` = ?d', Type::NPC, $this->typeId)) { - if (count($maps) == 1) // should only exist in one instance - $mapType = match (DB::Aowow()->selectCell('SELECT `type` FROM ?_zones WHERE `id` = ?d', $maps[0])) - { - // MAP_TYPE_DUNGEON, - MAP_TYPE_DUNGEON_HC => 1, - // MAP_TYPE_RAID, - MAP_TYPE_MMODE_RAID, - MAP_TYPE_MMODE_RAID_HC => 2, - default => 0 - }; + $mapType = match ((int)DB::Aowow()->selectCell('SELECT `type` FROM ?_zones WHERE `id` = ?d', $maps[0])) + { + // MAP_TYPE_DUNGEON, + MAP_TYPE_DUNGEON_HC => 1, + // MAP_TYPE_RAID, + MAP_TYPE_MMODE_RAID, + MAP_TYPE_MMODE_RAID_HC => 2, + default => 0 + }; } // npc is difficulty dummy: get max difficulty from parent npc if ($this->placeholder && ($mt = DB::Aowow()->selectCell('SELECT IF(`difficultyEntry1` = ?d, 1, 2) FROM ?_creature WHERE `difficultyEntry1` = ?d OR `difficultyEntry2` = ?d OR `difficultyEntry3` = ?d', $this->typeId, $this->typeId, $this->typeId, $this->typeId))) @@ -244,7 +243,7 @@ class NpcBaseResponse extends TemplateResponse implements ICache } if ($stats = $this->getCreatureStats($mapType, $_altIds)) - $infobox[] = Lang::npc('stats').($_altIds ? ' ('.Lang::npc('modes', $mapType, 0).')' : '').Lang::main('colon').'[ul][li]'.implode('[/li][li]', $stats).'[/li][/ul]'; + $infobox[] = Lang::npc('stats').($_altIds ? ' ('.Lang::game('modes', $mapType, 0).')' : '').Lang::main('colon').'[ul][li]'.implode('[/li][li]', $stats).'[/li][/ul]'; if ($infobox) { @@ -566,79 +565,108 @@ class NpcBaseResponse extends TemplateResponse implements ICache } // tabs: this creature contains.. - $skinTab = ['tab_skinning', 'skinning', SKILL_SKINNING]; - if ($_typeFlags & NPC_TYPEFLAG_SKIN_WITH_HERBALISM) - $skinTab = ['tab_herbalism', 'herbalism', SKILL_HERBALISM]; - else if ($_typeFlags & NPC_TYPEFLAG_SKIN_WITH_MINING) - $skinTab = ['tab_mining', 'mining', SKILL_MINING]; - else if ($_typeFlags & NPC_TYPEFLAG_SKIN_WITH_ENGINEERING) - $skinTab = ['tab_engineering', 'engineering', SKILL_ENGINEERING]; - - /* - extraCols: [Listview.extraCols.count, Listview.extraCols.percent, Listview.extraCols.mode], - _totalCount: 22531, - computeDataFunc: Listview.funcBox.initLootTable, - onAfterCreate: Listview.funcBox.addModeIndicator, - - modes:{"mode":1,"1":{"count":4408,"outof":16013},"4":{"count":4408,"outof":22531}} - */ + if ($this->subject->isGatherable()) + $skinTab = ['$LANG.tab_herbalism', 'herbalism', SKILL_HERBALISM]; + else if ($this->subject->isMineable()) + $skinTab = ['$LANG.tab_mining', 'mining', SKILL_MINING]; + else if ($this->subject->isSalvageable()) + $skinTab = ['$LANG.tab_engineering', 'engineering', SKILL_ENGINEERING]; + else + $skinTab = ['$LANG.tab_skinning', 'skinning', SKILL_SKINNING]; $sourceFor = array( - 0 => [LOOT_CREATURE, $this->subject->getField('lootId'), '$LANG.tab_drops', 'drops', [ ], ''], - 8 => [LOOT_PICKPOCKET, $this->subject->getField('pickpocketLootId'), '$LANG.tab_pickpocketing', 'pickpocketing', ['side', 'slot', 'reqlevel'], ''], - 9 => [LOOT_SKINNING, $this->subject->getField('skinLootId'), '$LANG.'.$skinTab[0], $skinTab[1], ['side', 'slot', 'reqlevel'], ''] + 0 => [Loot::CREATURE, [4 => $this->subject->getField('lootId')], '$LANG.tab_drops', 'drops', [ ], ''], + 1 => [Loot::GAMEOBJECT, [], '$LANG.tab_drops', 'drops-object', [ ], ''], + 2 => [Loot::PICKPOCKET, [4 => $this->subject->getField('pickpocketLootId')], '$LANG.tab_pickpocketing', 'pickpocketing', ['side', 'slot', 'reqlevel'], ''], + 3 => [Loot::SKINNING, [4 => $this->subject->getField('skinLootId')], $skinTab[0], $skinTab[1], ['side', 'slot', 'reqlevel'], ''] ); - // temp: manually add loot for difficulty-versions - $langref = array( - "-2" => '$LANG.tab_heroic', - "-1" => '$LANG.tab_normal', - 1 => '$$WH.sprintf(LANG.tab_normalX, 10)', - 2 => '$$WH.sprintf(LANG.tab_normalX, 25)', - 3 => '$$WH.sprintf(LANG.tab_heroicX, 10)', - 4 => '$$WH.sprintf(LANG.tab_heroicX, 25)' - ); + /* loot tabs to sub tabs + * (1 << 0) => '$LANG.tab_heroic', + * (1 << 1) => '$LANG.tab_normal', + * (1 << 2) => '$LANG.tab_drops', + * (1 << 3) => '$$WH.sprintf(LANG.tab_normalX, 10)', + * (1 << 4) => '$$WH.sprintf(LANG.tab_normalX, 25)', + * (1 << 5) => '$$WH.sprintf(LANG.tab_heroicX, 10)', + * (1 << 6) => '$$WH.sprintf(LANG.tab_heroicX, 25)' + */ + + $getBit = function(int $type, int $difficulty) : int + { + if ($type == 1) // dungeon + return 1 << (2 - $difficulty); + if ($type == 2) // raid + return 1 << (2 + $difficulty); + return 4; // generic case + }; + + foreach (DB::Aowow()->select('SELECT l.`difficulty` AS ARRAY_KEY, o.`id`, o.`lootId`, o.`name_loc0`, o.`name_loc2`, o.`name_loc3`, o.`name_loc4`, o.`name_loc6`, o.`name_loc8` FROM ?_loot_link l JOIN ?_objects o ON o.`id` = l.`objectId` WHERE l.`npcId` = ?d ORDER BY `difficulty` ASC', $this->typeId) as $difficulty => $lgo) + { + $sourceFor[1][1][$getBit($mapType, $difficulty)] = $lgo['lootId']; + $sourceFor[1][5] = $sourceFor[1][5] ?: '$$WH.sprintf(LANG.lvnote_npcobjectsource, '.$lgo['id'].', "'.Util::localizedString($lgo, 'name').'")'; + } if ($_altIds) { - $sourceFor[0][2] = $mapType == 1 ? $langref[-1] : $langref[1]; + if ($mapType == 1) // map generic loot to dungeon NH + { + $sourceFor[0][1] = [2 => $sourceFor[0][1][4]]; + $sourceFor[2][1] = [2 => $sourceFor[2][1][4]]; + $sourceFor[3][1] = [2 => $sourceFor[3][1][4]]; + } + if ($mapType == 2) // map generic loot to raid 10NH + { + $sourceFor[0][1] = [8 => $sourceFor[0][1][4]]; + $sourceFor[2][1] = [8 => $sourceFor[2][1][4]]; + $sourceFor[3][1] = [8 => $sourceFor[3][1][4]]; + } + foreach ($this->altNPCs->iterate() as $id => $__) { - $mode = ($_altIds[$id] + 1) * ($mapType == 1 ? -1 : 1); - foreach (DB::Aowow()->select('SELECT o.`id`, o.`lootId`, o.`name_loc0`, o.`name_loc2`, o.`name_loc3`, o.`name_loc4`, o.`name_loc6`, o.`name_loc8`, l.`difficulty` FROM ?_loot_link l JOIN ?_objects o ON o.`id` = l.`objectId` WHERE l.`npcId` = ?d', $id) as $l) - $sourceFor[(($l['difficulty'] - 1) * 2) + 1] = [LOOT_GAMEOBJECT, $l['lootId'], $langref[$l['difficulty'] * ($mapType == 1 ? -1 : 1)], 'drops-object-'.$l['difficulty'], [], '$$WH.sprintf(LANG.lvnote_npcobjectsource, '.$l['id'].', "'.Util::localizedString($l, 'name').'")']; + foreach (DB::Aowow()->select('SELECT l.`difficulty` AS ARRAY_KEY, o.`id`, o.`lootId`, o.`name_loc0`, o.`name_loc2`, o.`name_loc3`, o.`name_loc4`, o.`name_loc6`, o.`name_loc8` FROM ?_loot_link l JOIN ?_objects o ON o.`id` = l.`objectId` WHERE l.`npcId` = ?d ORDER BY `difficulty` ASC', $id) as $difficulty => $lgo) + { + $sourceFor[1][1][$getBit($mapType, $difficulty)] = $lgo['lootId']; + $sourceFor[1][5] = $sourceFor[1][5] ?: '$$WH.sprintf(LANG.lvnote_npcobjectsource, '.$lgo['id'].', "'.Util::localizedString($lgo, 'name').'")'; + } + if ($lootId = $this->altNPCs->getField('lootId')) - $sourceFor[($mode - 1) * 2] = [LOOT_CREATURE, $lootId, $langref[$mode], 'drops-'.abs($mode), [], '']; + $sourceFor[0][1][$getBit($mapType, $_altIds[$id] + 1)] = $lootId; + if ($lootId = $this->altNPCs->getField('pickpocketLootId')) + $sourceFor[2][1][$getBit($mapType, $_altIds[$id] + 1)] = $lootId; + if ($lootId = $this->altNPCs->getField('skinLootId')) + $sourceFor[3][1][$getBit($mapType, $_altIds[$id] + 1)] = $lootId; } } - foreach (DB::Aowow()->select('SELECT l.`difficulty` AS ARRAY_KEY, o.`id`, o.`lootId`, o.`name_loc0`, o.`name_loc2`, o.`name_loc3`, o.`name_loc4`, o.`name_loc6`, o.`name_loc8` FROM ?_loot_link l JOIN ?_objects o ON o.`id` = l.`objectId` WHERE l.`npcId` = ?d', $this->typeId) as $difficulty => $lgo) - $sourceFor[(($difficulty - 1) * 2) + 1] = [LOOT_GAMEOBJECT, $lgo['lootId'], $mapType ? $langref[$difficulty * ($mapType == 1 ? -1 : 1)] : '$LANG.tab_drops', 'drops-object-'.$difficulty, [], '$$WH.sprintf(LANG.lvnote_npcobjectsource, '.$lgo['id'].', "'.Util::localizedString($lgo, 'name').'")']; - - ksort($sourceFor); - - foreach ($sourceFor as [$lootTpl, $lootId, $tabName, $tabId, $hiddenCols, $note]) + foreach ($sourceFor as [$lootTpl, $lootEntries, $tabName, $tabId, $hiddenCols, $note]) { - $creatureLoot = new Loot(); - if ($creatureLoot->getByContainer($lootTpl, $lootId)) + $creatureLoot = new LootByContainer(); + if ($creatureLoot->getByContainer($lootTpl, $lootEntries)) { $extraCols = $creatureLoot->extraCols; - $extraCols[] = '$Listview.extraCols.percent'; + array_push($extraCols, '$Listview.extraCols.count', '$Listview.extraCols.percent'); + if (count($lootEntries) > 1) + $extraCols[] = '$Listview.extraCols.mode'; + + $hiddenCols[] = 'count'; $this->extendGlobalData($creatureLoot->jsGlobals); $tabData = array( - 'data' => $creatureLoot->getResult(), - 'name' => $tabName, - 'id' => $tabId, - 'extraCols' => array_unique($extraCols), - 'hiddenCols' => $hiddenCols ?: null, - 'sort' => ['-percent', 'name'] + 'data' => $creatureLoot->getResult(), + 'id' => $tabId, + 'name' => $tabName, + 'extraCols' => array_unique($extraCols), + 'hiddenCols' => $hiddenCols ?: null, + 'sort' => ['-percent', 'name'], + '_totalCount' => 10000, + 'computeDataFunc' => '$Listview.funcBox.initLootTable', + 'onAfterCreate' => '$Listview.funcBox.addModeIndicator', ); if ($note) $tabData['note'] = $note; - else if ($lootTpl == LOOT_SKINNING) + else if ($lootTpl == Loot::SKINNING) $tabData['note'] = ''.Lang::formatSkillBreakpoints(Game::getBreakpointsForSkill($skinTab[2], $this->subject->getField('maxLevel') * 5), Lang::FMT_HTML).''; $this->lvTabs->addListviewTab(new Listview($tabData, ItemList::$brickFile)); @@ -848,7 +876,7 @@ class NpcBaseResponse extends TemplateResponse implements ICache // base NPC if ($base = $this->getRepForId([$this->typeId], $spilledParents)) - $reputation[] = [Lang::npc('modes', 1, 0), $base]; + $reputation[] = [Lang::game('modes', 1, 0), $base]; // difficulty dummys if ($dummyIds && ($mapType == 1 || $mapType == 2)) @@ -862,7 +890,7 @@ class NpcBaseResponse extends TemplateResponse implements ICache // apply by difficulty foreach ($alt as $mode => $dat) - $reputation[] = [Lang::npc('modes', $mapType, $mode), $dat]; + $reputation[] = [Lang::game('modes', $mapType, $mode), $dat]; } // get spillover factions and apply @@ -964,7 +992,7 @@ class NpcBaseResponse extends TemplateResponse implements ICache if (!$this->altNPCs->getEntry($id)) continue; - $m = Lang::npc('modes', $mapType, $mode); + $m = Lang::game('modes', $mapType, $mode); // Health $health = $this->altNPCs->getBaseStats('health'); diff --git a/endpoints/object/object.php b/endpoints/object/object.php index d41d478b..8fc3f2dd 100644 --- a/endpoints/object/object.php +++ b/endpoints/object/object.php @@ -22,6 +22,9 @@ class ObjectBaseResponse extends TemplateResponse implements ICache public ?Book $book = null; public ?array $relBoss = null; + private array $difficulties = []; + private int $mapType = 0; + private GameObjectList $subject; public function __construct(string $id) @@ -61,6 +64,37 @@ class ObjectBaseResponse extends TemplateResponse implements ICache array_unshift($this->title, Lang::unescapeUISequences($this->subject->getField('name', true), Lang::FMT_RAW), Util::ucFirst(Lang::game('object'))); + /**********************/ + /* Determine Map Type */ + /**********************/ + + if ($objectdifficulty = DB::Aowow()->select( // has difficulty versions of itself + 'SELECT `normal10` AS "0", `normal25` AS "1", + `heroic10` AS "2", `heroic25` AS "3", + `mapType` AS ARRAY_KEY + FROM ?_objectdifficulty + WHERE `normal10` = ?d OR `normal25` = ?d OR + `heroic10` = ?d OR `heroic25` = ?d', + $this->typeId, $this->typeId, $this->typeId, $this->typeId + )) + { + $this->mapType = key($objectdifficulty); + $this->difficulties = array_pop($objectdifficulty); + } + else if ($maps = DB::Aowow()->selectCell('SELECT IF(COUNT(DISTINCT `areaId`) > 1, 0, `areaId`) FROM ?_spawns WHERE `type` = ?d AND `typeId` = ?d', Type::OBJECT, $this->typeId)) + { + $this->mapType = match ((int)DB::Aowow()->selectCell('SELECT `type` FROM ?_zones WHERE `id` = ?d', $maps)) + { + // MAP_TYPE_DUNGEON, + MAP_TYPE_DUNGEON_HC => 1, + // MAP_TYPE_RAID, + MAP_TYPE_MMODE_RAID, + MAP_TYPE_MMODE_RAID_HC => 2, + default => 0 + }; + } + + /***********/ /* Infobox */ /***********/ @@ -198,6 +232,11 @@ class ObjectBaseResponse extends TemplateResponse implements ICache if (Lang::getLocale() != Locale::EN) $infobox[] = Util::ucFirst(Lang::lang(Locale::EN->value) . Lang::main('colon')) . '[copy button=false]'.$this->subject->getField('name_loc0').'[/copy][/li]'; + // used in mode + foreach ($this->difficulties as $n => $id) + if ($id == $this->typeId) + $infobox[] = Lang::game('mode').Lang::game('modes', $this->mapType, $n); + // AI if (User::isInGroup(U_GROUP_EMPLOYEE)) if ($_ = $this->subject->getField('ScriptOrAI')) @@ -402,12 +441,37 @@ class ObjectBaseResponse extends TemplateResponse implements ICache // tab: contains if ($_ = $this->subject->getField('lootId')) { - $goLoot = new Loot(); - if ($goLoot->getByContainer(LOOT_GAMEOBJECT, $_)) + // check if loot_link entry exists (only difficulty: 1) + if ($npcId = DB::Aowow()->selectCell('SELECT `npcId` FROM ?_loot_link WHERE `objectId` = ?d AND `difficulty` = 1', $this->typeId)) { - $extraCols = $goLoot->extraCols; - $extraCols[] = '$Listview.extraCols.percent'; - $hiddenCols = ['source', 'side', 'slot', 'reqlevel']; + // get id set of npc + $lootEntries = DB::Aowow()->selectCol( + 'SELECT ll.`difficulty` AS ARRAY_KEY, o.`lootId` + FROM ?_creature c + LEFT JOIN ?_loot_link ll ON ll.`npcId` IN (c.`id`, c.`difficultyEntry1`, c.`difficultyEntry2`, c.`difficultyEntry3`) + LEFT JOIN ?_objects o ON o.`id` = ll.`objectId` + WHERE c.`id` = ?d + ORDER BY ll.`difficulty` ASC', + $npcId + ); + + if ($this->mapType == 2 || count($lootEntries) > 2) // always raid + $lootEntries = array_combine(array_map(fn($x) => 1 << (2 + $x), array_keys($lootEntries)), array_values($lootEntries)); + else if ($this->mapType == 1 || count($lootEntries) == 2) // dungeon or raid, assume dungeon + $lootEntries = array_combine(array_map(fn($x) => 1 << (2 - $x), array_keys($lootEntries)), array_values($lootEntries)); + } + else + $lootEntries = [4 => $_]; + + $goLoot = new LootByContainer(); + if ($goLoot->getByContainer(Loot::GAMEOBJECT, $lootEntries)) + { + $extraCols = $goLoot->extraCols; + array_push($extraCols, '$Listview.extraCols.count', '$Listview.extraCols.percent'); + if (count($lootEntries) > 1) + $extraCols[] = '$Listview.extraCols.mode'; + + $hiddenCols = ['source', 'side', 'slot', 'reqlevel', 'count']; $this->extendGlobalData($goLoot->jsGlobals); $lootResult = $goLoot->getResult(); @@ -421,12 +485,16 @@ class ObjectBaseResponse extends TemplateResponse implements ICache } $this->lvTabs->addListviewTab(new Listview(array( - 'data' => $lootResult, - 'id' => 'contains', - 'name' => '$LANG.tab_contains', - 'sort' => ['-percent', 'name'], - 'extraCols' => array_unique($extraCols), - 'hiddenCols' => $hiddenCols ?: null + 'data' => $lootResult, + 'id' => 'contains', + 'name' => '$LANG.tab_contains', + 'sort' => ['-percent', 'name'], + 'extraCols' => array_unique($extraCols), + 'hiddenCols' => $hiddenCols ?: null, + 'sort' => ['-percent', 'name'], + '_totalCount' => 10000, + 'computeDataFunc' => '$Listview.funcBox.initLootTable', + 'onAfterCreate' => '$Listview.funcBox.addModeIndicator', ), ItemList::$brickFile)); } } @@ -471,6 +539,51 @@ class ObjectBaseResponse extends TemplateResponse implements ICache ), GameObjectList::$brickFile)); } + // tab: see also + if ($this->difficulties) + { + $conditions = array( + 'AND', + ['id', $this->difficulties], + ['id', $this->typeId, '!'] + ); + + $saObjects = new GameObjectList($conditions); + if (!$saObjects->error) + { + $data = $saObjects->getListviewData(); + if ($this->difficulties) + { + $saE = ['$Listview.extraCols.mode']; + + foreach ($data as $id => &$d) + { + if (($modeBit = array_search($id, $this->difficulties)) !== false) + { + if ($this->mapType) + $d['modes'] = ['mode' => 1 << ($modeBit + 3)]; + else + $d['modes'] = ['mode' => 2 - $modeBit]; + } + else + $d['modes'] = ['mode' => 0]; + } + } + + $tabData = array( + 'data' => $data, + 'id' => 'see-also', + 'name' => '$LANG.tab_seealso', + 'visibleCols' => ['level'], + ); + + if (isset($saE)) + $tabData['extraCols'] = $saE; + + $this->lvTabs->addListviewTab(new Listview($tabData, GameObjectList::$brickFile)); + } + } + // tab: Same model as $sameModel = new GameObjectList(array(['displayId', $this->subject->getField('displayId')], ['id', $this->typeId, '!'])); if (!$sameModel->error) diff --git a/endpoints/quest/quest.php b/endpoints/quest/quest.php index 7a0d3249..190770b5 100644 --- a/endpoints/quest/quest.php +++ b/endpoints/quest/quest.php @@ -518,8 +518,8 @@ class QuestBaseResponse extends TemplateResponse implements ICache // todo (med): this double list creation very much sucks ... $getItemSource = function ($itemId, $method = 0) use (&$mapNPCs, &$mapGOs) { - $lootTabs = new Loot(); - if ($lootTabs->getByItem($itemId)) + $lootTabs = new LootByItem($itemId); + if ($lootTabs->getByItem()) { /* todo (med): sanity check: @@ -1175,8 +1175,8 @@ class QuestBaseResponse extends TemplateResponse implements ICache $this->mail['header'][1] = Lang::mail('mailBy', [$senderTypeId, $ti]); // while mail attachemnts are handled as loot, it has no variance. Always 100% chance, always one item. - $mailLoot = new Loot(); - if ($mailLoot->getByContainer(LOOT_MAIL, $rmtId)) + $mailLoot = new LootByContainer(); + if ($mailLoot->getByContainer(Loot::MAIL, [$rmtId])) { $this->extendGlobalData($mailLoot->jsGlobals); foreach ($mailLoot->getResult() as $loot) diff --git a/endpoints/spell/spell.php b/endpoints/spell/spell.php index f2369f30..dae8f96b 100644 --- a/endpoints/spell/spell.php +++ b/endpoints/spell/spell.php @@ -49,6 +49,7 @@ class SpellBaseResponse extends TemplateResponse implements ICache private int $firstRank = 0; private array $modelInfo = []; private array $difficulties = []; + private int $mapType = 0; public function __construct(string $id) { @@ -67,15 +68,20 @@ class SpellBaseResponse extends TemplateResponse implements ICache if ($jsg = $this->subject->getJSGlobals(GLOBALINFO_ANY, $extra)) $this->extendGlobalData($jsg, $extra); - $this->modelInfo = $this->subject->getModelInfo($this->typeId); - $this->difficulties = DB::Aowow()->selectRow( // has difficulty versions of itself + $this->modelInfo = $this->subject->getModelInfo($this->typeId); + if ($spelldifficulty = DB::Aowow()->select( // has difficulty versions of itself 'SELECT `normal10` AS "0", `normal25` AS "1", - `heroic10` AS "2", `heroic25` AS "3" + `heroic10` AS "2", `heroic25` AS "3", + `mapType` AS ARRAY_KEY FROM ?_spelldifficulty WHERE `normal10` = ?d OR `normal25` = ?d OR `heroic10` = ?d OR `heroic25` = ?d', $this->typeId, $this->typeId, $this->typeId, $this->typeId - ); + )) + { + $this->mapType = key($spelldifficulty); + $this->difficulties = array_pop($spelldifficulty); + } // returns self or firstRank if ($fr = DB::World()->selectCell('SELECT `first_spell_id` FROM spell_ranks WHERE `spell_id` = ?d', $this->typeId)) @@ -445,39 +451,28 @@ class SpellBaseResponse extends TemplateResponse implements ICache ['s.name_loc'.Lang::getLocale()->value, $this->subject->getField('name', true)] ); + if ($this->difficulties) + $conditions = ['OR', ['AND', ...$conditions], ['AND', ['s.id', $this->difficulties], ['s.id', $this->typeId, '!']]]; + $saSpells = new SpellList($conditions); if (!$saSpells->error) { $data = $saSpells->getListviewData(); - if ($this->difficulties) // needs a way to distinguish between dungeon and raid :x; creature using this -> map -> areaType? + if ($this->difficulties) { $saE = ['$Listview.extraCols.mode']; foreach ($data as $id => &$d) { - $d['modes'] = ['mode' => 0]; - - if ($this->difficulties[0] == $id) // b0001000 + if (($modeBit = array_search($id, $this->difficulties)) !== false) { - if (!$this->difficulties[2] && !$this->difficulties[3]) - $d['modes']['mode'] |= 0x2; + if ($this->mapType) + $d['modes'] = ['mode' => 1 << ($modeBit + 3)]; else - $d['modes']['mode'] |= 0x8; + $d['modes'] = ['mode' => 2 - $modeBit]; } - - if ($this->difficulties[1] == $id) // b0010000 - { - if (!$this->difficulties[2] && !$this->difficulties[3]) - $d['modes']['mode'] |= 0x1; - else - $d['modes']['mode'] |= 0x10; - } - - if ($this->difficulties[2] == $id) // b0100000 - $d['modes']['mode'] |= 0x20; - - if ($this->difficulties[3] == $id) // b1000000 - $d['modes']['mode'] |= 0x40; + else + $d['modes'] = ['mode' => 0]; } } @@ -649,8 +644,8 @@ class SpellBaseResponse extends TemplateResponse implements ICache // tab: contains // spell_loot_template - $spellLoot = new Loot(); - if ($spellLoot->getByContainer(LOOT_SPELL, $this->typeId)) + $spellLoot = new LootByContainer(); + if ($spellLoot->getByContainer(Loot::SPELL, [$this->typeId])) { $this->extendGlobalData($spellLoot->jsGlobals); @@ -658,11 +653,12 @@ class SpellBaseResponse extends TemplateResponse implements ICache $extraCols[] = '$Listview.extraCols.percent'; $this->lvTabs->addListviewTab(new Listview(array( - 'data' => $spellLoot->getResult(), - 'name' => '$LANG.tab_contains', - 'id' => 'contains', - 'hiddenCols' => ['side', 'slot', 'source', 'reqlevel'], - 'extraCols' => array_unique($extraCols) + 'data' => $spellLoot->getResult(), + 'name' => '$LANG.tab_contains', + 'id' => 'contains', + 'hiddenCols' => ['side', 'slot', 'source', 'reqlevel'], + 'extraCols' => array_unique($extraCols), + 'computeDataFunc' => '$Listview.funcBox.initLootTable' ), ItemList::$brickFile)); } @@ -991,7 +987,7 @@ class SpellBaseResponse extends TemplateResponse implements ICache } // tab: taught by npc - if ($this->subject->getSources($s) && in_array(SRC_TRAINER, $s)) + if ($this->subject->getRawSource(SRC_TRAINER)) { $trainers = DB::World()->select( 'SELECT cdt.`CreatureId` AS ARRAY_KEY, ts.`ReqSkillLine` AS "reqSkillId", ts.`ReqSkillRank` AS "reqSkillValue", ts.`ReqLevel` AS "reqLevel", ts.`ReqAbility1` AS "reqSpellId1", ts.`reqAbility2` AS "reqSpellId2" @@ -2423,13 +2419,10 @@ class SpellBaseResponse extends TemplateResponse implements ICache } // accquisition.. 10: starter spell; 7: discovery - if ($this->subject->getSources($s)) - { - if (in_array(SRC_STARTER, $s)) - $infobox[] = Lang::spell('starter'); - else if (in_array(SRC_DISCOVERY, $s)) - $infobox[] = Lang::spell('discovered'); - } + if ($this->subject->getRawSource(SRC_STARTER)) + $infobox[] = Lang::spell('starter'); + else if ($this->subject->getRawSource(SRC_DISCOVERY)) + $infobox[] = Lang::spell('discovered'); // training cost if ($cost = $this->subject->getField('trainingCost')) @@ -2462,7 +2455,7 @@ class SpellBaseResponse extends TemplateResponse implements ICache // used in mode foreach ($this->difficulties as $n => $id) if ($id == $this->typeId) - $infobox[] = Lang::game('mode').Lang::game('modes', $n); + $infobox[] = Lang::game('mode').Lang::game('modes', $this->mapType, $n); // Creature Type from Aura: Shapeshift foreach ($this->modelInfo as $mI) diff --git a/endpoints/zone/zone.php b/endpoints/zone/zone.php index 65fb9c36..83f9780c 100644 --- a/endpoints/zone/zone.php +++ b/endpoints/zone/zone.php @@ -565,6 +565,48 @@ class ZoneBaseResponse extends TemplateResponse implements ICache $this->lvTabs = new Tabs(['parent' => "\$\$WH.ge('tabs-generic')"], 'tabsRelated', true); + // tab: Drops + if (in_array($this->subject->getField('category'), [MAP_TYPE_DUNGEON, MAP_TYPE_RAID])) + { + // Issue 1 - if the bosses drop items that are also sold by vendors moreZoneId will be 0 as vendor location and boss location are likely in conflict with each other + // Issue 2 - if the boss/chest isn't spawned the loot will not show up + $items = new ItemList(array(Cfg::get('SQL_LIMIT_NONE'), ['src.moreZoneId', $this->typeId], ['src.src2', 0, '>'], ['quality', ITEM_QUALITY_UNCOMMON, '>=']), ['calcTotal' => true]); + $data = $items->getListviewData(); + $subTabs = false; + foreach ($items->iterate() as $id => $__) + { + $src = $items->getRawSource(SRC_DROP); + $map = ($items->getField('moreMask') ?: 0) & (SRC_FLAG_DUNGEON_DROP | SRC_FLAG_RAID_DROP); + if (!$src || !$map) + continue; + + $subTabs = true; + + if ($map & SRC_FLAG_RAID_DROP) + $mode = ($src[0] << 3); + else + $mode = ($src[0] & 0x1 ? 0x2 : 0) | ($src[0] & 0x2 ? 0x1 : 0); + + $data[$id] += ['modes' => ['mode' => $mode]]; + } + + $tabData = array( + 'data' => $data, + 'id' => 'drops', + 'name' => '$LANG.tab_drops', + 'extraCols' => $subTabs ? ['$Listview.extraCols.mode'] : null, + 'computeDataFunc' => '$Listview.funcBox.initLootTable', + 'onAfterCreate' => $subTabs ? '$Listview.funcBox.addModeIndicator' : null + ); + + if (!is_null(ItemListFilter::getCriteriaIndex(16, $this->typeId))) + $tabData['note'] = sprintf(Util::$filterResultString, '?items&filter=cr=16;crs='.$this->typeId.';crv=0'); + + $this->extendGlobalData($items->getJSGlobals(GLOBALINFO_SELF)); + + $this->lvTabs->addListviewTab(new Listview($tabData, ItemList::$brickFile)); + } + // tab: NPCs if ($cSpawns && !$creatureSpawns->error) { @@ -683,8 +725,8 @@ class ZoneBaseResponse extends TemplateResponse implements ICache // tab: achievements // tab: fished in zone - $fish = new Loot(); - if ($fish->getByContainer(LOOT_FISHING, $this->typeId)) + $fish = new LootByContainer(); + if ($fish->getByContainer(Loot::FISHING, [$this->typeId])) { $this->extendGlobalData($fish->jsGlobals); $xCols = array_merge(['$Listview.extraCols.percent'], $fish->extraCols); @@ -696,12 +738,13 @@ class ZoneBaseResponse extends TemplateResponse implements ICache $note = sprintf(Util::$lvTabNoteString, Lang::zone('fishingSkill'), Lang::formatSkillBreakpoints(Game::getBreakpointsForSkill(SKILL_FISHING, $skill), Lang::FMT_HTML)); $this->lvTabs->addListviewTab(new Listview(array( - 'data' => $fish->getResult(), - 'name' => '$LANG.tab_fishing', - 'id' => 'fishing', - 'extraCols' => array_unique($xCols), - 'hiddenCols' => ['side'], - 'note' => $note + 'data' => $fish->getResult(), + 'name' => '$LANG.tab_fishing', + 'id' => 'fishing', + 'extraCols' => array_unique($xCols), + 'hiddenCols' => ['side'], + 'note' => $note, + 'computeDataFunc' => '$Listview.funcBox.initLootTable' ), ItemList::$brickFile)); } diff --git a/includes/components/Conditions/Conditions.class.php b/includes/components/Conditions/Conditions.class.php index 6168ca1f..96b3ea34 100644 --- a/includes/components/Conditions/Conditions.class.php +++ b/includes/components/Conditions/Conditions.class.php @@ -398,22 +398,22 @@ class Conditions public static function lootTableToConditionSource(string $lootTable) : int { - switch ($lootTable) + return match ($lootTable) { - case LOOT_FISHING: return self::SRC_FISHING_LOOT_TEMPLATE; - case LOOT_CREATURE: return self::SRC_CREATURE_LOOT_TEMPLATE; - case LOOT_GAMEOBJECT: return self::SRC_GAMEOBJECT_LOOT_TEMPLATE; - case LOOT_ITEM: return self::SRC_ITEM_LOOT_TEMPLATE; - case LOOT_DISENCHANT: return self::SRC_DISENCHANT_LOOT_TEMPLATE; - case LOOT_PROSPECTING: return self::SRC_PROSPECTING_LOOT_TEMPLATE; - case LOOT_MILLING: return self::SRC_MILLING_LOOT_TEMPLATE; - case LOOT_PICKPOCKET: return self::SRC_PICKPOCKETING_LOOT_TEMPLATE; - case LOOT_SKINNING: return self::SRC_SKINNING_LOOT_TEMPLATE; - case LOOT_MAIL: return self::SRC_MAIL_LOOT_TEMPLATE; - case LOOT_SPELL: return self::SRC_SPELL_LOOT_TEMPLATE; - case LOOT_REFERENCE: return self::SRC_REFERENCE_LOOT_TEMPLATE; - default: return self::SRC_NONE; - } + Loot::FISHING => self::SRC_FISHING_LOOT_TEMPLATE, + Loot::CREATURE => self::SRC_CREATURE_LOOT_TEMPLATE, + Loot::GAMEOBJECT => self::SRC_GAMEOBJECT_LOOT_TEMPLATE, + Loot::ITEM => self::SRC_ITEM_LOOT_TEMPLATE, + Loot::DISENCHANT => self::SRC_DISENCHANT_LOOT_TEMPLATE, + Loot::PROSPECTING => self::SRC_PROSPECTING_LOOT_TEMPLATE, + Loot::MILLING => self::SRC_MILLING_LOOT_TEMPLATE, + Loot::PICKPOCKET => self::SRC_PICKPOCKETING_LOOT_TEMPLATE, + Loot::SKINNING => self::SRC_SKINNING_LOOT_TEMPLATE, + Loot::MAIL => self::SRC_MAIL_LOOT_TEMPLATE, + Loot::SPELL => self::SRC_SPELL_LOOT_TEMPLATE, + Loot::REFERENCE => self::SRC_REFERENCE_LOOT_TEMPLATE, + default => self::SRC_NONE + }; } public static function extendListviewRow(array &$lvRow, int $srcType, int $groupKey, array $condition) : bool diff --git a/includes/components/dbtypelist.class.php b/includes/components/dbtypelist.class.php index 95542533..bac9bd9c 100644 --- a/includes/components/dbtypelist.class.php +++ b/includes/components/dbtypelist.class.php @@ -306,6 +306,11 @@ abstract class DBTypeList $this->error = false; } + /** + * iterate over fetched templates + * + * @return array the current template + */ public function &iterate() : \Generator { if (!$this->templates) @@ -483,7 +488,7 @@ abstract class DBTypeList 't': type [always set] 'ti': typeId [always set] 'bd': BossDrop [0; 1] [Creature / GO] - 'dd': DungeonDifficulty [-2: DungeonHC; -1: DungeonNM; 1: Raid10NM; 2:Raid25NM; 3:Raid10HM; 4: Raid25HM] [Creature / GO] + 'dd': DungeonDifficulty [-2: DungeonHC; -1: DungeonNM; 1: Raid10NM; 2:Raid25NM; 3:Raid10HM; 4: Raid25HM; 99: filler trash] [Creature / GO] 'q': cssQuality [Items] 'z': zone [set when all happens in here] 'p': PvP [pvpSourceId] @@ -632,7 +637,18 @@ trait spawnHelper $wpSum = []; $wpIdx = 0; $worldPos = []; - $spawns = DB::Aowow()->select("SELECT * FROM ?_spawns WHERE `type` = ?d AND `typeId` IN (?a) AND `posX` > 0 AND `posY` > 0", self::$type, $this->getFoundIDs()) ?: []; + $spawns = DB::Aowow()->select( + 'SELECT CASE WHEN z.`type` = ?d THEN 1 + WHEN z.`type` = ?d THEN 2 + WHEN z.`type` = ?d THEN 2 + ELSE 0 + END AS "mapType", s.* + FROM ?_spawns s + JOIN ?_zones z ON s.areaId = z.id + WHERE s.`type` = ?d AND s.`typeId` IN (?a) AND s.`posX` > 0 AND s.`posY` > 0', + MAP_TYPE_DUNGEON_HC, MAP_TYPE_MMODE_RAID, MAP_TYPE_MMODE_RAID_HC, + self::$type, $this->getFoundIDs() + ) ?: []; if (!$skipAdmin && User::isInGroup(U_GROUP_MODERATOR)) if ($guids = array_column(array_filter($spawns, fn($x) => $x['guid'] > 0 || $x['type'] != Type::NPC), 'guid')) @@ -696,13 +712,13 @@ trait spawnHelper $info[2] = Lang::game('phases').Lang::main('colon').Util::asHex($s['phaseMask']); if ($s['spawnMask'] == 15) - $info[3] = Lang::game('mode').Lang::game('modes', -1); + $info[3] = Lang::game('mode').Lang::game('modes', 0, -1); else if ($s['spawnMask']) { $_ = []; for ($i = 0; $i < 4; $i++) if ($s['spawnMask'] & 1 << $i) - $_[] = Lang::game('modes', $i); + $_[] = Lang::game('modes', $s['mapType'], $i); $info[4] = Lang::game('mode').implode(', ', $_); } @@ -880,8 +896,13 @@ trait profilerHelper trait sourceHelper { - protected $sources = []; - protected $sourceMore = null; + protected array $sources = []; + protected ?array $sourceMore = null; + + public function getRawSource(int $src) : array + { + return $this->sources[$this->id][$src] ?? []; + } public function getSources(?array &$s = [], ?array &$sm = []) : bool { @@ -923,7 +944,9 @@ trait sourceHelper 10H 0b0100 2 0b011 25H 0b1000 3 0b100 */ - if ($this->curTpl['moreMask'] & SRC_FLAG_DUNGEON_DROP) + if ($this->curTpl['moreMask'] & SRC_FLAG_COMMON) + $sm['dd'] = 99; + else if ($this->curTpl['moreMask'] & SRC_FLAG_DUNGEON_DROP) $sm['dd'] = $this->sources[$this->id][SRC_DROP][0] * -1; else if ($this->curTpl['moreMask'] & SRC_FLAG_RAID_DROP) { diff --git a/includes/dbtypes/achievement.class.php b/includes/dbtypes/achievement.class.php index d0b12a37..89c19b1a 100644 --- a/includes/dbtypes/achievement.class.php +++ b/includes/dbtypes/achievement.class.php @@ -57,8 +57,8 @@ class AchievementList extends DBTypeList if ($rewards[$_id]['MailTemplateID']) { // using class Loot creates an inifinite loop cirling between Loot, ItemList and SpellList or something - // $mailSrc = new Loot(); - // $mailSrc->getByContainer(LOOT_MAIL, $rewards[$_id]['MailTemplateID']); + // $mailSrc = new LootByContainer(); + // $mailSrc->getByContainer(Loot::MAIL, $rewards[$_id]['MailTemplateID']); // foreach ($mailSrc->iterate() as $loot) // $_curTpl['rewards'][] = [Type::ITEM, $loot['id']]; diff --git a/includes/dbtypes/creature.class.php b/includes/dbtypes/creature.class.php index 5ee7a39b..f7267887 100644 --- a/includes/dbtypes/creature.class.php +++ b/includes/dbtypes/creature.class.php @@ -59,7 +59,7 @@ class CreatureList extends DBTypeList $row3 = [Lang::game('level')]; $fam = $this->curTpl['family']; - if (!($this->curTpl['typeFlags'] & 0x4)) + if (!($this->curTpl['typeFlags'] & NPC_TYPEFLAG_BOSS_MOB)) { $level = $this->curTpl['minLevel']; if ($level != $this->curTpl['maxLevel']) @@ -154,6 +154,21 @@ class CreatureList extends DBTypeList return ($this->curTpl['cuFlags'] & NPC_CU_INSTANCE_BOSS) || ($this->curTpl['typeFlags'] & NPC_TYPEFLAG_BOSS_MOB && $this->curTpl['rank']); } + public function isMineable() : bool + { + return $this->curTpl['skinLootId'] && ($this->curTpl['typeFlags'] & NPC_TYPEFLAG_SKIN_WITH_MINING); + } + + public function isGatherable() : bool + { + return $this->curTpl['skinLootId'] && ($this->curTpl['typeFlags'] & NPC_TYPEFLAG_SKIN_WITH_HERBALISM); + } + + public function isSalvageable() : bool + { + return $this->curTpl['skinLootId'] && ($this->curTpl['typeFlags'] & NPC_TYPEFLAG_SKIN_WITH_ENGINEERING); + } + public function getListviewData(int $addInfoMask = 0x0) : array { /* looks like this data differs per occasion diff --git a/includes/dbtypes/gameobject.class.php b/includes/dbtypes/gameobject.class.php index f79db7e6..1fdba3d0 100644 --- a/includes/dbtypes/gameobject.class.php +++ b/includes/dbtypes/gameobject.class.php @@ -70,7 +70,7 @@ class GameObjectList extends DBTypeList $data[$this->id] = array( 'id' => $this->id, 'name' => Lang::unescapeUISequences($this->getField('name', true), Lang::FMT_RAW), - 'type' => $this->curTpl['typeCat'], + 'type' => $this->getField('typeCat'), 'location' => $this->getSpawns(SPAWNINFO_ZONES) ); diff --git a/includes/dbtypes/item.class.php b/includes/dbtypes/item.class.php index 901cf289..cd98d37b 100644 --- a/includes/dbtypes/item.class.php +++ b/includes/dbtypes/item.class.php @@ -1688,7 +1688,6 @@ class ItemList extends DBTypeList 'level' => $this->curTpl['itemLevel'], 'reqlevel' => $this->curTpl['requiredLevel'], 'displayid' => $this->curTpl['displayId'], - // 'commondrop' => 'true' / null // set if the item is a loot-filler-item .. check common ref-templates..? 'holres' => $this->curTpl['resHoly'], 'firres' => $this->curTpl['resFire'], 'natres' => $this->curTpl['resNature'], @@ -2486,14 +2485,14 @@ class ItemListFilter extends Filter return null; $refResults = []; - $newRefs = DB::World()->selectCol('SELECT `entry` FROM ?# WHERE `item` = ?d AND `reference` = 0', LOOT_REFERENCE, $crs); + $newRefs = DB::World()->selectCol('SELECT `entry` FROM ?# WHERE `item` = ?d AND `reference` = 0', Loot::REFERENCE, $crs); while ($newRefs) { $refResults += $newRefs; - $newRefs = DB::World()->selectCol('SELECT `entry` FROM ?# WHERE `reference` IN (?a)', LOOT_REFERENCE, $newRefs); + $newRefs = DB::World()->selectCol('SELECT `entry` FROM ?# WHERE `reference` IN (?a)', Loot::REFERENCE, $newRefs); } - $lootIds = DB::World()->selectCol('SELECT `entry` FROM ?# WHERE {`reference` IN (?a) OR }(`reference` = 0 AND `item` = ?d)', LOOT_DISENCHANT, $refResults ?: DBSIMPLE_SKIP, $crs); + $lootIds = DB::World()->selectCol('SELECT `entry` FROM ?# WHERE {`reference` IN (?a) OR }(`reference` = 0 AND `item` = ?d)', Loot::DISENCHANT, $refResults ?: DBSIMPLE_SKIP, $crs); return $lootIds ? ['disenchantId', $lootIds] : [0]; } diff --git a/includes/defines.php b/includes/defines.php index d2c944d8..6f9e37c9 100644 --- a/includes/defines.php +++ b/includes/defines.php @@ -385,20 +385,6 @@ define('MAX_LEVEL', 80); define('MAX_SKILL', 450); define('WOW_BUILD', 12340); -// Loot handles -define('LOOT_FISHING', 'fishing_loot_template'); -define('LOOT_CREATURE', 'creature_loot_template'); -define('LOOT_GAMEOBJECT', 'gameobject_loot_template'); -define('LOOT_ITEM', 'item_loot_template'); -define('LOOT_DISENCHANT', 'disenchant_loot_template'); -define('LOOT_PROSPECTING', 'prospecting_loot_template'); -define('LOOT_MILLING', 'milling_loot_template'); -define('LOOT_PICKPOCKET', 'pickpocketing_loot_template'); -define('LOOT_SKINNING', 'skinning_loot_template'); -define('LOOT_MAIL', 'mail_loot_template'); // used by achievements and quests -define('LOOT_SPELL', 'spell_loot_template'); -define('LOOT_REFERENCE', 'reference_loot_template'); - // Sides define('SIDE_NONE', 0); define('SIDE_ALLIANCE', 1); diff --git a/includes/game/loot.class.php b/includes/game/loot.class.php deleted file mode 100644 index 56acb2e0..00000000 --- a/includes/game/loot.class.php +++ /dev/null @@ -1,728 +0,0 @@ -results); - - foreach ($this->results as $k => [, $tabData]) - if ($tabData['data']) // only yield tabs with content - yield $k => $this->results[$k]; - } - - public function getResult() : array - { - return $this->results; - } - - private function createStack(array $l) : string // issue: TC always has an equal distribution between min/max - { - if (empty($l['min']) || empty($l['max']) || $l['max'] <= $l['min']) - return ''; - - $stack = []; - for ($i = $l['min']; $i <= $l['max']; $i++) - $stack[$i] = round(100 / (1 + $l['max'] - $l['min']), 3); - - // yes, it wants a string .. how weired is that.. - return json_encode($stack, JSON_NUMERIC_CHECK); // do not replace with Util::toJSON ! - } - - private function storeJSGlobals(array $data) : void - { - foreach ($data as $type => $jsData) - { - foreach ($jsData as $k => $v) - { - // was already set at some point with full data - if (isset($this->jsGlobals[$type][$k]) && is_array($this->jsGlobals[$type][$k])) - continue; - - $this->jsGlobals[$type][$k] = $v; - } - } - } - - private function calcChance(array $refs, array $parents = []) : array - { - $retData = []; - $retKeys = []; - - foreach ($refs as $rId => $ref) - { - // check for possible database inconsistencies - if (!$ref['chance'] && !$ref['isGrouped']) - trigger_error('Loot by Item: Ungrouped Item/Ref '.$ref['item'].' has 0% chance assigned!', E_USER_WARNING); - - if ($ref['isGrouped'] && $ref['sumChance'] > 100) - trigger_error('Loot by Item: Group with Item/Ref '.$ref['item'].' has '.number_format($ref['sumChance'], 2).'% total chance! Some items cannot drop!', E_USER_WARNING); - - if ($ref['isGrouped'] && $ref['sumChance'] >= 100 && !$ref['chance']) - trigger_error('Loot by Item: Item/Ref '.$ref['item'].' with adaptive chance cannot drop. Group already at 100%!', E_USER_WARNING); - - $chance = abs($ref['chance'] ?: (100 - $ref['sumChance']) / $ref['nZeroItems']) / 100; - - // apply inherited chanceMods - if (isset($this->chanceMods[$ref['item']])) - { - $chance *= $this->chanceMods[$ref['item']][0]; - $chance = 1 - pow(1 - $chance, $this->chanceMods[$ref['item']][1]); - } - - // save chance for parent-ref - $this->chanceMods[$rId] = [$chance, $ref['multiplier']]; - - // refTemplate doesn't point to a new ref -> we are done - if (!in_array($rId, $parents)) - { - $data = array( - 'percent' => $chance, - 'stack' => [$ref['min'], $ref['max']], - 'count' => 1 // ..and one for the sort script - ); - - if ($_ = self::createStack($ref)) - $data['pctstack'] = $_; - - // sort highest chances first - $i = 0; - for (; $i < count($retData); $i++) - if ($retData[$i]['percent'] < $data['percent']) - break; - - array_splice($retData, $i, 0, [$data]); - array_splice($retKeys, $i, 0, [$rId]); - } - } - - return array_combine($retKeys, $retData); - } - - private function getByContainerRecursive(string $tableName, int $lootId, array &$handledRefs, int $groupId = 0, float $baseChance = 1.0) : array - { - $loot = []; - $rawItems = []; - - if (!$tableName || !$lootId) - return [null, null]; - - $rows = DB::World()->select('SELECT * FROM ?# WHERE entry = ?d{ AND groupid = ?d}', $tableName, $lootId, $groupId ?: DBSIMPLE_SKIP); - if (!$rows) - return [null, null]; - - $groupChances = []; - $nGroupEquals = []; - $cnd = new Conditions(); - foreach ($rows as $entry) - { - $set = array( - 'quest' => $entry['QuestRequired'], - 'group' => $entry['GroupId'], - 'parentRef' => $tableName == LOOT_REFERENCE ? $lootId : 0, - 'realChanceMod' => $baseChance, - 'groupChance' => 0 - ); - - if ($entry['QuestRequired']) - foreach (DB::Aowow()->selectCol('SELECT id FROM ?_quests WHERE (`reqSourceItemId1` = ?d OR `reqSourceItemId2` = ?d OR `reqSourceItemId3` = ?d OR `reqSourceItemId4` = ?d OR `reqItemId1` = ?d OR `reqItemId2` = ?d OR `reqItemId3` = ?d OR `reqItemId4` = ?d OR `reqItemId5` = ?d OR `reqItemId6` = ?d) AND (`cuFlags` & ?d) = 0', - $entry['Item'], $entry['Item'], $entry['Item'], $entry['Item'], $entry['Item'], $entry['Item'], $entry['Item'], $entry['Item'], $entry['Item'], $entry['Item'], CUSTOM_EXCLUDE_FOR_LISTVIEW | CUSTOM_UNAVAILABLE) as $questId) - $cnd->addExternalCondition(Conditions::lootTableToConditionSource($tableName), $lootId . ':' . $entry['Item'], [Conditions::QUESTTAKEN, $questId], true); - - // if ($entry['LootMode'] > 1) - // { - $buff = []; - for ($i = 0; $i < 8; $i++) - if ($entry['LootMode'] & (1 << $i)) - $buff[] = $i + 1; - - $set['mode'] = implode(', ', $buff); - // } - // else - // $set['mode'] = 0; - - /* - modes:{"mode":8,"4":{"count":7173,"outof":17619},"8":{"count":7173,"outof":10684}} - ignore lootmodes from sharedDefines.h use different creatures/GOs from each template - modes.mode = b6543210 - ||||||'dungeon heroic - |||||'dungeon normal - ||||' - |||'10man normal - ||'25man normal - |'10man heroic - '25man heroic - */ - - if ($entry['Reference']) - { - // bandaid.. remove when propperly handling lootmodes - if (!in_array($entry['Reference'], $handledRefs)) - { // todo (high): find out, why i used this in the first place. (don't do drugs, kids) - [$data, $raw] = self::getByContainerRecursive(LOOT_REFERENCE, $entry['Reference'], $handledRefs, /*$entry['GroupId'],*/ 0, $entry['Chance'] / 100); - - $handledRefs[] = $entry['Reference']; - - $loot = array_merge($loot, $data); - $rawItems = array_merge($rawItems, $raw); - } - $set['reference'] = $entry['Reference']; - $set['multiplier'] = $entry['MaxCount']; - } - else - { - $rawItems[] = $entry['Item']; - $set['content'] = $entry['Item']; - $set['min'] = $entry['MinCount']; - $set['max'] = $entry['MaxCount']; - } - - if (!isset($groupChances[$entry['GroupId']])) - { - $groupChances[$entry['GroupId']] = 0; - $nGroupEquals[$entry['GroupId']] = 0; - } - - if ($set['quest'] || !$set['group']) - $set['groupChance'] = $entry['Chance']; - else if ($entry['GroupId'] && !$entry['Chance']) - { - $nGroupEquals[$entry['GroupId']]++; - $set['groupChance'] = &$groupChances[$entry['GroupId']]; - } - else if ($entry['GroupId'] && $entry['Chance']) - { - $set['groupChance'] = $entry['Chance']; - - if (!$entry['Reference']) - { - if (empty($groupChances[$entry['GroupId']])) - $groupChances[$entry['GroupId']] = 0; - - $groupChances[$entry['GroupId']] += $entry['Chance']; - } - } - else // shouldn't have happened - { - trigger_error('Unhandled case in calculating chance for item '.$entry['Item'].'!', E_USER_WARNING); - continue; - } - - $loot[] = $set; - } - - foreach (array_keys($nGroupEquals) as $k) - { - $sum = $groupChances[$k]; - if (!$sum) - $sum = 0; - else if ($sum >= 100.01) - { - trigger_error('Loot entry '.$lootId.' / group '.$k.' has a total chance of '.number_format($sum, 2).'%. Some items cannot drop!', E_USER_WARNING); - $sum = 100; - } - // is applied as backReference to items with 0-chance - $groupChances[$k] = (100 - $sum) / ($nGroupEquals[$k] ?: 1); - } - - if ($cnd->getBySourceGroup($lootId, Conditions::lootTableToConditionSource($tableName))->prepare()) - { - self::storeJSGlobals($cnd->getJsGlobals()); - $cnd->toListviewColumn($loot, $this->extraCols, $lootId, 'content'); - } - - return [$loot, array_unique($rawItems)]; - } - - public function getByContainer(string $table, int $entry): bool - { - $this->entry = intVal($entry); - - if (!in_array($table, $this->lootTemplates) || !$this->entry) - return false; - - /* - // if (is_array($this->entry) && in_array($table, [LOOT_CREATURE, LOOT_GAMEOBJECT]) - // iterate over the 4 available difficulties and assign modes - - - modes:{"mode":1,"1":{"count":4408,"outof":16013},"4":{"count":4408,"outof":22531}} - */ - $handledRefs = []; - [$lootRows, $itemIds] = self::getByContainerRecursive($table, $this->entry, $handledRefs); - if (!$lootRows) - return false; - - $items = new ItemList(array(['i.id', $itemIds], Cfg::get('SQL_LIMIT_NONE'))); - self::storeJSGlobals($items->getJSGlobals(GLOBALINFO_SELF | GLOBALINFO_RELATED)); - $foo = $items->getListviewData(); - - // assign listview LV rows to loot rows, not the other way round! The same item may be contained multiple times - foreach ($lootRows as $loot) - { - $base = array( - 'percent' => round($loot['groupChance'] * $loot['realChanceMod'], 3), - 'group' => $loot['group'], - 'quest' => $loot['quest'], - 'count' => 1 // satisfies the sort-script - ); - - if ($_ = $loot['mode']) - $base['mode'] = $_; - - if ($_ = $loot['parentRef']) - $base['reference'] = $_; - - if (isset($loot['condition'])) - $base['condition'] = $loot['condition']; - - if ($_ = self::createStack($loot)) - $base['pctstack'] = $_; - - if (empty($loot['reference'])) // regular drop - { - if (!isset($foo[$loot['content']])) - { - trigger_error('Item #'.$loot['content'].' referenced by loot does not exist!', E_USER_WARNING); - continue; - } - - if (!User::isInGroup(U_GROUP_EMPLOYEE)) - { - if (!isset($this->results[$loot['content']])) - $this->results[$loot['content']] = array_merge($foo[$loot['content']], $base, ['stack' => [$loot['min'], $loot['max']]]); - else - $this->results[$loot['content']]['percent'] += $base['percent']; - } - else // in case of limited trash loot, check if $foo[] exists - $this->results[] = array_merge($foo[$loot['content']], $base, ['stack' => [$loot['min'], $loot['max']]]); - } - else if (User::isInGroup(U_GROUP_EMPLOYEE)) // create dummy for ref-drop - { - $data = array( - 'id' => $loot['reference'], - 'name' => '@REFERENCE: '.$loot['reference'], - 'icon' => 'trade_engineering', - 'stack' => [$loot['multiplier'], $loot['multiplier']] - ); - $this->results[] = array_merge($base, $data); - - $this->jsGlobals[Type::ITEM][$loot['reference']] = $data; - } - } - - // move excessive % to extra loot - if (!User::isInGroup(U_GROUP_EMPLOYEE)) - { - foreach ($this->results as &$_) - { - if ($_['percent'] <= 100) - continue; - - while ($_['percent'] > 200) - { - $_['stack'][0]++; - $_['stack'][1]++; - $_['percent'] -= 100; - } - - $_['stack'][1]++; - $_['percent'] = 100; - } - } - else - { - $fields = ['mode', 'reference']; - $base = []; - $set = 0; - foreach ($this->results as $foo) - { - foreach ($fields as $idx => $field) - { - $val = isset($foo[$field]) ? $foo[$field] : 0; - if (!isset($base[$idx])) - $base[$idx] = $val; - else if ($base[$idx] != $val) - $set |= 1 << $idx; - } - - if ($set == (pow(2, count($fields)) - 1)) - break; - } - - $this->extraCols[] = "\$Listview.funcBox.createSimpleCol('group', 'Group', '7%', 'group')"; - foreach ($fields as $idx => $field) - if ($set & (1 << $idx)) - $this->extraCols[] = "\$Listview.funcBox.createSimpleCol('".$field."', '".Util::ucFirst($field)."', '7%', '".$field."')"; - } - - return true; - } - - public function getByItem(int $entry, int $maxResults = -1, array $lootTableList = []) : bool - { - $this->entry = $entry; - - if (!$this->entry) - return false; - - if ($maxResults < 0) - $maxResults = Cfg::get('SQL_LIMIT_DEFAULT'); - - // [fileName, tabData, tabName, tabId, extraCols, hiddenCols, visibleCols] - $tabsFinal = array( - [Type::ITEM, [], '$LANG.tab_containedin', 'contained-in-item', [], [], []], - [Type::ITEM, [], '$LANG.tab_disenchantedfrom', 'disenchanted-from', [], [], []], - [Type::ITEM, [], '$LANG.tab_prospectedfrom', 'prospected-from', [], [], []], - [Type::ITEM, [], '$LANG.tab_milledfrom', 'milled-from', [], [], []], - [Type::NPC, [], '$LANG.tab_droppedby', 'dropped-by', [], [], []], - [Type::NPC, [], '$LANG.tab_pickpocketedfrom', 'pickpocketed-from', [], [], []], - [Type::NPC, [], '$LANG.tab_skinnedfrom', 'skinned-from', [], [], []], - [Type::NPC, [], '$LANG.tab_minedfromnpc', 'mined-from-npc', [], [], []], - [Type::NPC, [], '$LANG.tab_salvagedfrom', 'salvaged-from', [], [], []], - [Type::NPC, [], '$LANG.tab_gatheredfromnpc', 'gathered-from-npc', [], [], []], - [Type::QUEST, [], '$LANG.tab_rewardfrom', 'reward-from-quest', [], [], []], - [Type::ZONE, [], '$LANG.tab_fishedin', 'fished-in-zone', [], [], []], - [Type::OBJECT, [], '$LANG.tab_containedin', 'contained-in-object', [], [], []], - [Type::OBJECT, [], '$LANG.tab_minedfrom', 'mined-from-object', [], [], []], - [Type::OBJECT, [], '$LANG.tab_gatheredfrom', 'gathered-from-object', [], [], []], - [Type::OBJECT, [], '$LANG.tab_fishedin', 'fished-in-object', [], [], []], - [Type::SPELL, [], '$LANG.tab_createdby', 'created-by', [], [], []], - [Type::ACHIEVEMENT, [], '$LANG.tab_rewardfrom', 'reward-from-achievement', [], [], []] - ); - $refResults = []; - $query = 'SELECT - lt1.entry AS ARRAY_KEY, - IF(lt1.reference = 0, lt1.item, lt1.reference) AS item, - lt1.chance, - SUM(IF(lt2.chance = 0, 1, 0)) AS nZeroItems, - SUM(IF(lt2.reference = 0, lt2.chance, 0)) AS sumChance, - IF(lt1.groupid > 0, 1, 0) AS isGrouped, - IF(lt1.reference = 0, lt1.mincount, 1) AS min, - IF(lt1.reference = 0, lt1.maxcount, 1) AS max, - IF(lt1.reference > 0, lt1.maxcount, 1) AS multiplier - FROM - ?# lt1 - LEFT JOIN - ?# lt2 ON lt1.entry = lt2.entry AND lt1.groupid = lt2.groupid - WHERE - %s - GROUP BY lt2.entry, lt2.groupid'; - - /* - get references containing the item - */ - $newRefs = DB::World()->select( - sprintf($query, 'lt1.item = ?d AND lt1.reference = 0'), - LOOT_REFERENCE, LOOT_REFERENCE, - $this->entry - ); - - /* i'm currently not seeing a reasonable way to blend this into creature/gobject/etc tabs as one entity may drop the same item multiple times, with and without conditions. - if ($newRefs) - { - $cnd = new Conditions(); - if ($cnd->getBySourceEntry($this->entry, Conditions::SRC_REFERENCE_LOOT_TEMPLATE)) - if ($cnd->toListviewColumn($newRefs, $x, $this->entry)) - self::storejsGlobals($cnd->getJsGlobals()); - } - */ - - while ($newRefs) - { - $curRefs = $newRefs; - $newRefs = DB::World()->select( - sprintf($query, 'lt1.reference IN (?a)'), - LOOT_REFERENCE, LOOT_REFERENCE, - array_keys($curRefs) - ); - - $refResults += $this->calcChance($curRefs, array_column($newRefs, 'item')); - } - - /* - search the real loot-templates for the itemId and gathered refds - */ - foreach ($this->lootTemplates as $lootTemplate) - { - if ($lootTableList && !in_array($lootTemplate, $lootTableList)) - continue; - - if ($lootTemplate == LOOT_REFERENCE) - continue; - - $result = $this->calcChance(DB::World()->select( - sprintf($query, '{lt1.reference IN (?a) OR }(lt1.reference = 0 AND lt1.item = ?d)'), - $lootTemplate, $lootTemplate, - $refResults ? array_keys($refResults) : DBSIMPLE_SKIP, - $this->entry - )); - - // do not skip here if $result is empty. Additional loot for spells and quest is added separately - - // format for actual use - foreach ($result as $k => $v) - { - unset($result[$k]); - $v['percent'] = round($v['percent'] * 100, 3); - $result[abs($k)] = $v; - } - - // cap fetched entries to the sql-limit to guarantee, that the highest chance items get selected first - // screws with GO-loot and skinning-loot as these templates are shared for several tabs (fish, herb, ore) (herb, ore, leather) - $ids = array_slice(array_keys($result), 0, $maxResults); - - switch ($lootTemplate) - { - case LOOT_CREATURE: $field = 'lootId'; $tabId = 4; break; - case LOOT_PICKPOCKET: $field = 'pickpocketLootId'; $tabId = 5; break; - case LOOT_SKINNING: $field = 'skinLootId'; $tabId = -6; break; // assigned later - case LOOT_PROSPECTING: $field = 'id'; $tabId = 2; break; - case LOOT_MILLING: $field = 'id'; $tabId = 3; break; - case LOOT_ITEM: $field = 'id'; $tabId = 0; break; - case LOOT_DISENCHANT: $field = 'disenchantId'; $tabId = 1; break; - case LOOT_FISHING: $field = 'id'; $tabId = 11; break; // subAreas are currently ignored - case LOOT_GAMEOBJECT: - if (!$ids) - continue 2; - - $srcObj = new GameObjectList(array(['lootId', $ids])); - if ($srcObj->error) - continue 2; - - $srcData = $srcObj->getListviewData(); - - foreach ($srcObj->iterate() as $curTpl) - { - switch ($curTpl['typeCat']) - { - case 25: $tabId = 15; break; // fishing node - case -3: $tabId = 14; break; // herb - case -4: $tabId = 13; break; // vein - default: $tabId = 12; break; // general chest loot - } - - $tabsFinal[$tabId][1][] = array_merge($srcData[$srcObj->id], $result[$srcObj->getField('lootId')]); - $tabsFinal[$tabId][4][] = '$Listview.extraCols.percent'; - if ($tabId != 15) - $tabsFinal[$tabId][6][] = 'skill'; - } - continue 2; - case LOOT_MAIL: - // quest part - $conditions = array(['rewardChoiceItemId1', $this->entry], ['rewardChoiceItemId2', $this->entry], ['rewardChoiceItemId3', $this->entry], ['rewardChoiceItemId4', $this->entry], ['rewardChoiceItemId5', $this->entry], - ['rewardChoiceItemId6', $this->entry], ['rewardItemId1', $this->entry], ['rewardItemId2', $this->entry], ['rewardItemId3', $this->entry], ['rewardItemId4', $this->entry], - 'OR'); - if ($ids) - $conditions[] = ['rewardMailTemplateId', $ids]; - - $srcObj = new QuestList($conditions); - if (!$srcObj->error) - { - self::storeJSGlobals($srcObj->getJSGlobals(GLOBALINFO_SELF | GLOBALINFO_REWARDS)); - $srcData = $srcObj->getListviewData(); - - foreach ($srcObj->iterate() as $_) - $tabsFinal[10][1][] = array_merge($srcData[$srcObj->id], empty($result[$srcObj->id]) ? ['percent' => -1] : $result[$srcObj->id]); - } - - // achievement part - $conditions = array(['itemExtra', $this->entry]); - if ($ar = DB::World()->selectCol('SELECT ID FROM achievement_reward WHERE ItemID = ?d{ OR MailTemplateID IN (?a)}', $this->entry, $ids ?: DBSIMPLE_SKIP)) - array_push($conditions, ['id', $ar], 'OR'); - - $srcObj = new AchievementList($conditions); - if (!$srcObj->error) - { - self::storeJSGlobals($srcObj->getJSGlobals(GLOBALINFO_SELF | GLOBALINFO_REWARDS)); - $srcData = $srcObj->getListviewData(); - - foreach ($srcObj->iterate() as $_) - $tabsFinal[17][1][] = array_merge($srcData[$srcObj->id], empty($result[$srcObj->id]) ? ['percent' => -1] : $result[$srcObj->id]); - - $tabsFinal[17][5][] = 'rewards'; - $tabsFinal[17][6][] = 'category'; - } - continue 2; - case LOOT_SPELL: - $conditions = array( - 'OR', - ['AND', ['effect1CreateItemId', $this->entry], ['OR', ['effect1Id', SpellList::EFFECTS_ITEM_CREATE], ['effect1AuraId', SpellList::AURAS_ITEM_CREATE]]], - ['AND', ['effect2CreateItemId', $this->entry], ['OR', ['effect2Id', SpellList::EFFECTS_ITEM_CREATE], ['effect2AuraId', SpellList::AURAS_ITEM_CREATE]]], - ['AND', ['effect3CreateItemId', $this->entry], ['OR', ['effect3Id', SpellList::EFFECTS_ITEM_CREATE], ['effect3AuraId', SpellList::AURAS_ITEM_CREATE]]], - ); - if ($ids) - $conditions[] = ['id', $ids]; - - $srcObj = new SpellList($conditions); - if (!$srcObj->error) - { - self::storeJSGlobals($srcObj->getJSGlobals(GLOBALINFO_SELF | GLOBALINFO_RELATED)); - $srcData = $srcObj->getListviewData(); - - if (!empty($result)) - $tabsFinal[16][4][] = '$Listview.extraCols.percent'; - - if ($srcObj->hasSetFields('reagent1', 'reagent2', 'reagent3', 'reagent4', 'reagent5', 'reagent6', 'reagent7', 'reagent8')) - $tabsFinal[16][6][] = 'reagents'; - - foreach ($srcObj->iterate() as $_) - $tabsFinal[16][1][] = array_merge($srcData[$srcObj->id], empty($result[$srcObj->id]) ? ['percent' => -1] : $result[$srcObj->id]); - } - continue 2; - } - - if (!$ids) - continue; - - $parentData = []; - switch ($tabsFinal[abs($tabId)][0]) - { - case TYPE::NPC: // new CreatureList - if ($baseIds = DB::Aowow()->selectCol( - 'SELECT `difficultyEntry1` AS ARRAY_KEY, `id` FROM ?_creature WHERE difficultyEntry1 IN (?a) UNION - SELECT `difficultyEntry2` AS ARRAY_KEY, `id` FROM ?_creature WHERE difficultyEntry2 IN (?a) UNION - SELECT `difficultyEntry3` AS ARRAY_KEY, `id` FROM ?_creature WHERE difficultyEntry3 IN (?a)', - $ids, $ids, $ids)) - { - $parentObj = new CreatureList(array(['id', $baseIds])); - if (!$parentObj->error) - { - self::storeJSGlobals($parentObj->getJSGlobals()); - $parentData = $parentObj->getListviewData(); - $ids = array_diff($ids, $baseIds); - } - } - - case Type::ITEM: // new ItemList - case Type::ZONE: // new ZoneList - $srcObj = Type::newList($tabsFinal[abs($tabId)][0], array([$field, $ids])); - if (!$srcObj->error) - { - $srcData = $srcObj->getListviewData(); - self::storeJSGlobals($srcObj->getJSGlobals(GLOBALINFO_SELF | GLOBALINFO_RELATED)); - - foreach ($srcObj->iterate() as $curTpl) - { - if ($tabId < 0 && $curTpl['typeFlags'] & NPC_TYPEFLAG_SKIN_WITH_HERBALISM) - $tabId = 9; - else if ($tabId < 0 && $curTpl['typeFlags'] & NPC_TYPEFLAG_SKIN_WITH_ENGINEERING) - $tabId = 8; - else if ($tabId < 0 && $curTpl['typeFlags'] & NPC_TYPEFLAG_SKIN_WITH_MINING) - $tabId = 7; - else if ($tabId < 0) - $tabId = abs($tabId); // general case (skinning) - - if (($p = $srcObj->getField('parentId')) && ($d = $parentData[$p] ?? null)) - $tabsFinal[$tabId][1][] = array_merge($d, $result[$srcObj->getField($field)]); - else - $tabsFinal[$tabId][1][] = array_merge($srcData[$srcObj->id], $result[$srcObj->getField($field)]); - - $tabsFinal[$tabId][4][] = '$Listview.extraCols.percent'; - } - } - break; - } - } - - foreach ($tabsFinal as $tabId => $data) - { - $tabData = array( - 'data' => $data[1], - 'name' => $data[2], - 'id' => $data[3] - ); - - if ($data[4]) - $tabData['extraCols'] = array_unique($data[4]); - - if ($data[5]) - $tabData['hiddenCols'] = array_unique($data[5]); - - if ($data[6]) - $tabData['visibleCols'] = array_unique($data[6]); - - $this->results[$tabId] = [Type::getFileString($data[0]), $tabData]; - } - - return true; - } -} - -?> diff --git a/includes/game/loot/loot.class.php b/includes/game/loot/loot.class.php new file mode 100644 index 00000000..55c6f513 --- /dev/null +++ b/includes/game/loot/loot.class.php @@ -0,0 +1,71 @@ +jsGlobals, $data); + } +} + +?> diff --git a/includes/game/loot/lootbycontainer.class.php b/includes/game/loot/lootbycontainer.class.php new file mode 100644 index 00000000..3ede055d --- /dev/null +++ b/includes/game/loot/lootbycontainer.class.php @@ -0,0 +1,329 @@ +results; + } + + /** + * recurse through reference loot while applying modifiers from parent container + * + * @param string $tableName a known loot template table name + * @param int $lootId a loot template entry + * @param int $groupId [optional] limit result to provided loot group + * @param float $baseChance [optional] chance multiplier passed down from parent container + * @return array [[lootRows], [itemIds]] + */ + private function getByContainerRecursive(string $tableName, int $lootId, int $groupId = 0, float $baseChance = 1.0) : array + { + $loot = []; + $rawItems = []; + + if (!$tableName || !$lootId) + return [null, null]; + + $rows = DB::World()->select('SELECT * FROM ?# WHERE entry = ?d{ AND groupid = ?d}', $tableName, $lootId, $groupId ?: DBSIMPLE_SKIP); + if (!$rows) + return [null, null]; + + $groupChances = []; + $nGroupEquals = []; + $cnd = new Conditions(); + foreach ($rows as $entry) + { + $set = array( + 'quest' => $entry['QuestRequired'], + 'group' => $entry['GroupId'], + 'parentRef' => $tableName == self::REFERENCE ? $lootId : 0, + 'realChanceMod' => $baseChance, + 'groupChance' => 0 + ); + + if ($entry['QuestRequired']) + foreach (DB::Aowow()->selectCol('SELECT id FROM ?_quests WHERE (`reqSourceItemId1` = ?d OR `reqSourceItemId2` = ?d OR `reqSourceItemId3` = ?d OR `reqSourceItemId4` = ?d OR `reqItemId1` = ?d OR `reqItemId2` = ?d OR `reqItemId3` = ?d OR `reqItemId4` = ?d OR `reqItemId5` = ?d OR `reqItemId6` = ?d) AND (`cuFlags` & ?d) = 0', + $entry['Item'], $entry['Item'], $entry['Item'], $entry['Item'], $entry['Item'], $entry['Item'], $entry['Item'], $entry['Item'], $entry['Item'], $entry['Item'], CUSTOM_EXCLUDE_FOR_LISTVIEW | CUSTOM_UNAVAILABLE) as $questId) + $cnd->addExternalCondition(Conditions::lootTableToConditionSource($tableName), $lootId . ':' . $entry['Item'], [Conditions::QUESTTAKEN, $questId], true); + + // TC 'mode' (dynamic loot modifier) + $buff = []; + for ($i = 0; $i < 8; $i++) + if ($entry['LootMode'] & (1 << $i)) + $buff[] = $i + 1; + + $set['mode'] = implode(', ', $buff); + + if ($entry['Reference']) + { + if (!in_array($entry['Reference'], $this->knownRefs)) + $this->knownRefs[$entry['Reference']] = $this->getByContainerRecursive(self::REFERENCE, $entry['Reference'], 0, $entry['Chance'] / 100); + + [$data, $raw] = $this->knownRefs[$entry['Reference']]; + + $loot = array_merge($loot, $data); + $rawItems = array_merge($rawItems, $raw); + + $set['reference'] = $entry['Reference']; + $set['multiplier'] = $entry['MaxCount']; + } + else + { + $rawItems[] = $entry['Item']; + $set['content'] = $entry['Item']; + $set['min'] = $entry['MinCount']; + $set['max'] = $entry['MaxCount']; + } + + if (!isset($groupChances[$entry['GroupId']])) + { + $groupChances[$entry['GroupId']] = 0; + $nGroupEquals[$entry['GroupId']] = 0; + } + + if ($set['quest'] || !$set['group']) + $set['groupChance'] = $entry['Chance']; + else if ($entry['GroupId'] && !$entry['Chance']) + { + $nGroupEquals[$entry['GroupId']]++; + $set['groupChance'] = &$groupChances[$entry['GroupId']]; + } + else if ($entry['GroupId'] && $entry['Chance']) + { + $set['groupChance'] = $entry['Chance']; + + if (!$entry['Reference']) + { + if (empty($groupChances[$entry['GroupId']])) + $groupChances[$entry['GroupId']] = 0; + + $groupChances[$entry['GroupId']] += $entry['Chance']; + } + } + else // shouldn't have happened + { + trigger_error('Unhandled case in calculating chance for item '.$entry['Item'].'!', E_USER_WARNING); + continue; + } + + $loot[] = $set; + } + + foreach (array_keys($nGroupEquals) as $k) + { + $sum = $groupChances[$k]; + if (!$sum) + $sum = 0; + else if ($sum >= 100.01) + { + trigger_error('Loot entry '.$lootId.' / group '.$k.' has a total chance of '.number_format($sum, 2).'%. Some items cannot drop!', E_USER_WARNING); + $sum = 100; + } + // is applied as backReference to items with 0-chance + $groupChances[$k] = (100 - $sum) / ($nGroupEquals[$k] ?: 1); + } + + if ($cnd->getBySourceGroup($lootId, Conditions::lootTableToConditionSource($tableName))->prepare()) + { + $this->storeJSGlobals($cnd->getJsGlobals()); + $cnd->toListviewColumn($loot, $this->extraCols, $lootId, 'content'); + } + + return [$loot, array_unique($rawItems)]; + } + + /** + * fetch loot for given loot container and optionally merge multiple container while adding mode info. + * If difficultyBit is 0, no merge will occur + * + * @param string $table a known loote template table name + * @param array $lootEntries array of [difficultyBit => entry]. + * @return bool success and found loot + */ + public function getByContainer(string $table, array $lootEntries): bool + { + if (!in_array($table, self::TEMPLATES)) + return false; + + foreach ($lootEntries as $modeBit => $entry) + { + if (!$entry) + continue; + + [$lootRows, $itemIds] = $this->getByContainerRecursive($table, $entry); + if (!$lootRows) + continue; + + $items = new ItemList(array(['i.id', $itemIds], Cfg::get('SQL_LIMIT_NONE'))); + $this->storeJSGlobals($items->getJSGlobals(GLOBALINFO_SELF | GLOBALINFO_RELATED)); + $itemRows = $items->getListviewData(); + + // assign listview LV rows to loot rows, not the other way round! The same item may be contained multiple times + foreach ($lootRows as $loot) + { + $count = ceil($loot['groupChance'] * $loot['realChanceMod'] * 100); + + /* on modes... + * modes.mode is the (masked) sum of all modes where this item has been seen + * modes.mode & 1 dungeon normal + * modes.mode & 2 dungeon heroic + * modes.mode & 4 generic case (never included in mask for instanced creatures/gos or always === 4 for non-instanced creatures/gos) + * modes.mode & 8 raid 10 nh + * modes.mode & 16 raid 25 nh + * modes.mode & 32 raid 10 hc + * modes.mode & 64 raid 25 hc + * + * modes[4] is _always_ included and is the sum total over all modes: + * ex: modes:{"mode":1,"1":{"count":4408,"outof":16013},"4":{"count":4408,"outof":22531}} + */ + if ($modeBit) + { + $modes = array( // emulate 'percent' with precision: 2 + 'mode' => $modeBit, + $modeBit => ['count' => $count, 'outof' => 10000] + ); + if ($modeBit != 4) + $modes[4] = $modes[$modeBit]; + + + // unsure: force display as noteworthy + // if (!empty($loot['content']) && !empty($itemRows[$loot['content']]) && $itemRows[$loot['content']]['name'][0] == 7 - ITEM_QUALITY_POOR) + // $modes['mode'] = 4; + // else if ($count < 100) // chance < 1% + // $modes['mode'] = 4; + + + // existing result row; merge modes and move on + if (!is_null($k = array_find_key($this->results, function($x) use ($loot) { + if (!empty($loot['reference'])) + return $x['id'] == $loot['reference'] && $x['mode'] == $loot['mode'] && $x['group'] == $loot['group'] && $x['stack'] == [$loot['multiplier'], $loot['multiplier']]; + else + return $x['id'] == $loot['content'] && $x['mode'] == $loot['mode'] && $x['group'] == $loot['group']; + }))) + { + $this->results[$k]['modes']['mode'] |= $modes['mode']; + $this->results[$k]['modes'][$modeBit] = $modes[$modeBit]; + $this->results[$k]['modes'][4]['count'] = max($modes[4]['count'], $this->results[$k]['modes'][4]['count']); + + continue; + } + } + + $base = array( + 'count' => $count, + 'outof' => 10000, + 'group' => $loot['group'], + 'quest' => $loot['quest'], + 'mode' => $loot['mode'] ?: null, // dyn loot mode + 'modes' => $modes ?? null, // difficulties + 'reference' => $loot['parentRef'] ?: null, + 'condition' => $loot['condition'] ?? null, + 'pctstack' => self::buildStack($loot['min'] ?? 0, $loot['max'] ?? 0) + ); + + $base = array_filter($base, fn($x) => $x !== null); + + if (empty($loot['reference'])) // regular drop + { + if ($itemRow = $itemRows[$loot['content']] ?? null) + { + $extra = ['stack' => [$loot['min'], $loot['max']]]; + + // unsure if correct - tag item as trash if chance < 1% and tagged as having many sources + if ($base['count'] < 100 && $items->getEntry($loot['content'])['moreMask'] & SRC_FLAG_COMMON) + $extra['commondrop'] = 1; + + if (!User::isInGroup(U_GROUP_EMPLOYEE)) + { + if (!isset($this->results[$loot['content']])) + $this->results[$loot['content']] = array_merge($itemRow, $base, $extra); + else + $this->results[$loot['content']]['count'] += $base['count']; + } + else + $this->results[] = array_merge($itemRow, $base, $extra); + } + else + trigger_error('Item #'.$loot['content'].' referenced by loot does not exist!', E_USER_WARNING); + } + else if (User::isInGroup(U_GROUP_EMPLOYEE)) // create dummy for ref-drop + { + $data = array( + 'id' => $loot['reference'], + 'name' => '@REFERENCE: '.$loot['reference'], + 'icon' => 'trade_engineering', + 'stack' => [$loot['multiplier'], $loot['multiplier']] + ); + $this->results[] = array_merge($base, $data); + + $this->jsGlobals[Type::ITEM][$loot['reference']] = $data; + } + } + } + + // move excessive % to extra loot + if (!User::isInGroup(U_GROUP_EMPLOYEE)) + { + foreach ($this->results as &$_) + { + // remember 'count' is always relative to a base of 10000 + if ($_['count'] <= 10000) + continue; + + while ($_['count'] > 20000) + { + $_['stack'][0]++; + $_['stack'][1]++; + $_['count'] -= 10000; + } + + $_['stack'][1]++; + $_['count'] = 10000; + } + } + else + { + $fields = [['mode', 'Dyn. Mode'], ['reference', 'Reference']]; + $base = []; + $set = 0; + foreach ($this->results as $foo) + { + foreach ($fields as $idx => [$field, $title]) + { + $val = $foo[$field] ?? 0; + if (!isset($base[$idx])) + $base[$idx] = $val; + else if ($base[$idx] != $val) + $set |= 1 << $idx; + } + + if ($set == (pow(2, count($fields)) - 1)) + break; + } + + $this->extraCols[] = "\$Listview.funcBox.createSimpleCol('group', 'Group', '7%', 'group')"; + foreach ($fields as $idx => [$field, $title]) + if ($set & (1 << $idx)) + $this->extraCols[] = "\$Listview.funcBox.createSimpleCol('".$field."', '".$title."', '7%', '".$field."')"; + } + + return !empty($this->results); + } +} + +?> diff --git a/includes/game/loot/lootbyitem.class.php b/includes/game/loot/lootbyitem.class.php new file mode 100644 index 00000000..ea75e947 --- /dev/null +++ b/includes/game/loot/lootbyitem.class.php @@ -0,0 +1,439 @@ + [Type::NPC, [], '$LANG.tab_droppedby', 'dropped-by', [], [], []], + self::QUEST_REWARD => [Type::QUEST, [], '$LANG.tab_rewardfrom', 'reward-from-quest', [], [], []], + self::ITEM_CONTAINED => [Type::ITEM, [], '$LANG.tab_containedin', 'contained-in-item', [], [], []], + self::OBJECT_CONTAINED => [Type::OBJECT, [], '$LANG.tab_containedin', 'contained-in-object', [], [], []], + self::NPC_PICKPOCKETED => [Type::NPC, [], '$LANG.tab_pickpocketedfrom', 'pickpocketed-from', [], [], []], + self::NPC_SKINNED => [Type::NPC, [], '$LANG.tab_skinnedfrom', 'skinned-from', [], [], []], + self::ITEM_DISENCHANTED => [Type::ITEM, [], '$LANG.tab_disenchantedfrom', 'disenchanted-from', [], [], []], + self::ITEM_PROSPECTED => [Type::ITEM, [], '$LANG.tab_prospectedfrom', 'prospected-from', [], [], []], + self::ITEM_MILLED => [Type::ITEM, [], '$LANG.tab_milledfrom', 'milled-from', [], [], []], + self::NPC_MINED => [Type::NPC, [], '$LANG.tab_minedfromnpc', 'mined-from-npc', [], [], []], + self::NPC_SALVAGED => [Type::NPC, [], '$LANG.tab_salvagedfrom', 'salvaged-from', [], [], []], + self::NPC_GATHERED => [Type::NPC, [], '$LANG.tab_gatheredfromnpc', 'gathered-from-npc', [], [], []], + self::OBJECT_MINED => [Type::OBJECT, [], '$LANG.tab_minedfrom', 'mined-from-object', [], [], []], + self::OBJECT_GATHERED => [Type::OBJECT, [], '$LANG.tab_gatheredfrom', 'gathered-from-object', [], [], []], + self::ZONE_FISHED => [Type::ZONE, [], '$LANG.tab_fishedin', 'fished-in-zone', [], [], []], + self::OBJECT_FISHED => [Type::OBJECT, [], '$LANG.tab_fishedin', 'fished-in-object', [], [], []], + self::SPELL_CREATED => [Type::SPELL, [], '$LANG.tab_createdby', 'created-by', [], [], []], + self::ACHIEVEMENT_REWARD => [Type::ACHIEVEMENT, [], '$LANG.tab_rewardfrom', 'reward-from-achievement', [], [], []] + ); + private string $queryTemplate = + 'SELECT lt1.`entry` AS ARRAY_KEY, + IF(lt1.`reference` = 0, lt1.`item`, lt1.`reference`) AS "item", + lt1.`chance` AS "chance", + SUM(IF(lt2.`chance` = 0, 1, 0)) AS "nZeroItems", + SUM(IF(lt2.`reference` = 0, lt2.`chance`, 0)) AS "sumChance", + IF(lt1.`groupid` > 0, 1, 0) AS "isGrouped", + IF(lt1.`reference` = 0, lt1.`mincount`, 1) AS "min", + IF(lt1.`reference` = 0, lt1.`maxcount`, 1) AS "max", + IF(lt1.`reference` > 0, lt1.`maxcount`, 1) AS "multiplier" + FROM ?# lt1 + LEFT JOIN ?# lt2 ON lt1.`entry` = lt2.`entry` AND lt1.`groupid` = lt2.`groupid` + WHERE %s + GROUP BY lt2.`entry`, lt2.`groupid`'; + + /** + * @param int $entry item id to find loot container for + * @return void + */ + public function __construct(private int $entry) + { + + } + + /** + * iterate over result set + * + * @return iterable [tabIdx => [lvTemplate, lvData]] + */ + public function &iterate() : \Generator + { + reset($this->results); + + foreach ($this->results as $k => [, $tabData]) + if ($tabData['data']) // only yield tabs with content + yield $k => $this->results[$k]; + } + + /** + * calculate chance and stack info and apply to loot rows + * + * @param array $refs loot rows to apply chance + stack info to + * @param array $parents [optional] ref loot ids this call is derived from + * @return array [entry => stack+chance-info] + */ + private function calcChance(array $refs, array $parents = []) : array + { + $result = []; + + foreach ($refs as $rId => $ref) + { + // check for possible database inconsistencies + if (!$ref['chance'] && !$ref['isGrouped']) + trigger_error('Loot by Item: Ungrouped Item/Ref '.$ref['item'].' has 0% chance assigned!', E_USER_WARNING); + + if ($ref['isGrouped'] && $ref['sumChance'] > 100) + trigger_error('Loot by Item: Group with Item/Ref '.$ref['item'].' has '.number_format($ref['sumChance'], 2).'% total chance! Some items cannot drop!', E_USER_WARNING); + + if ($ref['isGrouped'] && $ref['sumChance'] >= 100 && !$ref['chance']) + trigger_error('Loot by Item: Item/Ref '.$ref['item'].' with adaptive chance cannot drop. Group already at 100%!', E_USER_WARNING); + + $chance = abs($ref['chance'] ?: (100 - $ref['sumChance']) / $ref['nZeroItems']) / 100; + + // apply inherited chanceMods + if (isset($this->chanceMods[$ref['item']])) + { + $chance *= $this->chanceMods[$ref['item']][0]; + $chance = 1 - pow(1 - $chance, $this->chanceMods[$ref['item']][1]); + } + + // save chance for parent-ref + $this->chanceMods[$rId] = [$chance, $ref['multiplier']]; + + // refTemplate doesn't point to a new ref -> we are done + if (in_array($rId, $parents)) + continue; + + $result[$rId] = array( + 'percent' => $chance, + 'stack' => [$ref['min'], $ref['max']], + 'count' => 1, // ..and one for the sort script + 'pctstack' => self::buildStack($ref['min'], $ref['max']) + ); + } + + // sort by % DESC + uasort($result, fn($a, $b) => $b['percent'] <=> $a['percent']); + + return $result; + } + + /** + * fetch loot container for item provided to __construct + * + * @param int $maxResults [optional] SQL_LIMIT override + * @param array $lootTableList [optional] limit lookup to provided loot template table names + * @return bool success + */ + public function getByItem(int $maxResults = -1, array $lootTableList = []) : bool + { + if (!$this->entry) + return false; + + if ($maxResults < 0) + $maxResults = Cfg::get('SQL_LIMIT_DEFAULT'); + + $refResults = []; + + /* + get references containing the item + */ + $newRefs = DB::World()->select( + sprintf($this->queryTemplate, 'lt1.`item` = ?d AND lt1.`reference` = 0'), + Loot::REFERENCE, Loot::REFERENCE, + $this->entry + ); + + /* + i'm currently not seeing a reasonable way to blend this into creature/gobject/etc tabs as one entity may drop the same item multiple times, with and without conditions. + if ($newRefs) + { + $cnd = new Conditions(); + if ($cnd->getBySourceEntry($this->entry, Conditions::SRC_REFERENCE_LOOT_TEMPLATE)) + if ($cnd->toListviewColumn($newRefs, $x, $this->entry)) + $this->storejsGlobals($cnd->getJsGlobals()); + } + */ + + while ($newRefs) + { + $curRefs = $newRefs; + $newRefs = DB::World()->select( + sprintf($this->queryTemplate, 'lt1.`reference` IN (?a)'), + Loot::REFERENCE, Loot::REFERENCE, + array_keys($curRefs) + ); + + $refResults += $this->calcChance($curRefs, array_column($newRefs, 'item')); + } + + /* + search the real loot-templates for the itemId and gathered refs + */ + foreach (self::TEMPLATES as $lootTemplate) + { + if ($lootTableList && !in_array($lootTemplate, $lootTableList)) + continue; + + if ($lootTemplate == Loot::REFERENCE) + continue; + + $result = $this->calcChance(DB::World()->select( + sprintf($this->queryTemplate, '{lt1.`reference` IN (?a) OR }(lt1.`reference` = 0 AND lt1.`item` = ?d)'), + $lootTemplate, $lootTemplate, + $refResults ? array_keys($refResults) : DBSIMPLE_SKIP, + $this->entry + )); + + // do not skip here if $result is empty. Additional loot for spells and quest is added separately + + // format for actual use + foreach ($result as $k => $v) + { + unset($result[$k]); + $v['percent'] = round($v['percent'] * 100, 3); + $result[abs($k)] = $v; + } + + // cap fetched entries to the sql-limit to guarantee that the highest chance items get selected first + // screws with GO-loot and skinning-loot as these templates are shared for several tabs (fish, herb, ore) and (herb, ore, leather) + $ids = array_slice(array_keys($result), 0, $maxResults); + + // fill ListviewTabs + match ($lootTemplate) + { + Loot::GAMEOBJECT => $this->handleObjectLoot( $ids, $result), + Loot::MAIL => $this->handleMailLoot( $ids, $result), + Loot::SPELL => $this->handleSpellLoot( $ids, $result), + Loot::CREATURE => $this->handleNpcLoot( $ids, $result, self::NPC_DROPPED, 'lootId'), + Loot::PICKPOCKET => $this->handleNpcLoot( $ids, $result, self::NPC_PICKPOCKETED, 'pickpocketLootId'), + Loot::SKINNING => $this->handleNpcLoot( $ids, $result, self::NPC_SKINNED, 'skinLootId'), // tabId < 0: assigned real id later + Loot::PROSPECTING => $this->handleGenericLoot($ids, $result, self::ITEM_PROSPECTED, 'id'), + Loot::MILLING => $this->handleGenericLoot($ids, $result, self::ITEM_MILLED, 'id'), + Loot::ITEM => $this->handleGenericLoot($ids, $result, self::ITEM_CONTAINED, 'id'), + Loot::DISENCHANT => $this->handleGenericLoot($ids, $result, self::ITEM_DISENCHANTED, 'disenchantId'), + Loot::FISHING => $this->handleGenericLoot($ids, $result, self::ZONE_FISHED, 'id') // subAreas are currently ignored + }; + } + + // finalize tabs + foreach ($this->listviewTabs as $idx => [$type, $data, $name, $id, $extraCols, $hiddenCols, $visibleCols]) + { + $tabData = array( + 'data' => $data, + 'name' => $name, + 'id' => $id + ); + + if ($extraCols) + $tabData['extraCols'] = array_unique($extraCols); + + if ($hiddenCols) + $tabData['hiddenCols'] = array_unique($hiddenCols); + + if ($visibleCols) + $tabData['visibleCols'] = array_unique($visibleCols); + + $this->results[$idx] = [Type::getFileString($type), $tabData]; + } + + return true; + } + + private function handleGenericLoot(array $ids, array $result, int $tabId, string $dbField) : bool + { + if (!$ids) + return false; + + [$type, &$data, , , &$extraCols, ,] = $this->listviewTabs[$tabId]; + + $srcObj = Type::newList($type, array([$dbField, $ids])); + if (!$srcObj || $srcObj->error) + return false; + + $srcData = $srcObj->getListviewData(); + $this->storeJSGlobals($srcObj->getJSGlobals(GLOBALINFO_SELF | GLOBALINFO_RELATED)); + + $extraCols[] = '$Listview.extraCols.percent'; + + foreach ($srcObj->iterate() as $__) + $data[] = array_merge($srcData[$srcObj->id], $result[$srcObj->getField($dbField)]); + + return true; + } + + private function handleNpcLoot(array $ids, array $result, int $tabId, string $dbField) : bool + { + if (!$ids) + return false; + + if ($baseIds = DB::Aowow()->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) + { + $this->storeJSGlobals($parentObj->getJSGlobals()); + $parentData = $parentObj->getListviewData(); + $ids = array_diff($ids, $baseIds); + } + } + + $npc = new CreatureList(array([$dbField, $ids])); + if ($npc->error) + return false; + + $srcData = $npc->getListviewData(); + $this->storeJSGlobals($npc->getJSGlobals(GLOBALINFO_SELF | GLOBALINFO_RELATED)); + [, &$data, , , &$extraCols, ,] = $this->listviewTabs[$tabId]; + + foreach ($npc->iterate() as $__) + { + if ($tabId == self::NPC_SKINNED) + { + if ($npc->isMineable()) + $tabId = self::NPC_MINED; + else if ($npc->isGatherable()) + $tabId = self::NPC_GATHERED; + else if ($npc->isSalvageable()) + $tabId = self::NPC_SALVAGED; + } + + $p = $npc->getField('parentId'); + + $data[] = array_merge($parentData[$p] ?? $srcData[$npc->id], $result[$npc->getField($dbField)]); + $extraCols[] = '$Listview.extraCols.percent'; + } + + return true; + } + + private function handleSpellLoot(array $ids, array $result) : bool + { + $conditions = array( + 'OR', + ['AND', ['effect1CreateItemId', $this->entry], ['OR', ['effect1Id', SpellList::EFFECTS_ITEM_CREATE], ['effect1AuraId', SpellList::AURAS_ITEM_CREATE]]], + ['AND', ['effect2CreateItemId', $this->entry], ['OR', ['effect2Id', SpellList::EFFECTS_ITEM_CREATE], ['effect2AuraId', SpellList::AURAS_ITEM_CREATE]]], + ['AND', ['effect3CreateItemId', $this->entry], ['OR', ['effect3Id', SpellList::EFFECTS_ITEM_CREATE], ['effect3AuraId', SpellList::AURAS_ITEM_CREATE]]], + ); + if ($ids) + $conditions[] = ['id', $ids]; + + $srcObj = new SpellList($conditions); + if ($srcObj->error) + return false; + + $this->storeJSGlobals($srcObj->getJSGlobals(GLOBALINFO_SELF | GLOBALINFO_RELATED)); + [, &$data, , , &$extraCols, , &$visibleCols] = $this->listviewTabs[self::SPELL_CREATED]; + + if (!empty($result)) + $extraCols[] = '$Listview.extraCols.percent'; + + if ($srcObj->hasSetFields('reagent1', 'reagent2', 'reagent3', 'reagent4', 'reagent5', 'reagent6', 'reagent7', 'reagent8')) + $visibleCols[] = 'reagents'; + + foreach ($srcObj->getListviewData() as $id => $row) + $data[] = array_merge($row, $result[$id] ?? ['percent' => -1]); + + return true; + } + + private function handleMailLoot(array $ids, array $result) : bool + { + // quest part + $conditions = array('OR', + ['rewardChoiceItemId1', $this->entry], ['rewardChoiceItemId2', $this->entry], ['rewardChoiceItemId3', $this->entry], ['rewardChoiceItemId4', $this->entry], ['rewardChoiceItemId5', $this->entry], + ['rewardChoiceItemId6', $this->entry], ['rewardItemId1', $this->entry], ['rewardItemId2', $this->entry], ['rewardItemId3', $this->entry], ['rewardItemId4', $this->entry] + ); + if ($ids) + $conditions[] = ['rewardMailTemplateId', $ids]; + + $quests = new QuestList($conditions); + if (!$quests->error) + { + $this->storeJSGlobals($quests->getJSGlobals(GLOBALINFO_SELF | GLOBALINFO_REWARDS)); + [, &$qData, , , , , ] = $this->listviewTabs[self::QUEST_REWARD]; + + foreach ($quests->getListviewData() as $id => $row) + $qData[] = array_merge($row, $result[$id] ?? ['percent' => -1]); + } + + // achievement part + $conditions = array(['itemExtra', $this->entry]); + if ($ar = DB::World()->selectCol('SELECT `ID` FROM achievement_reward WHERE `ItemID` = ?d{ OR `MailTemplateID` IN (?a)}', $this->entry, $ids ?: DBSIMPLE_SKIP)) + array_push($conditions, ['id', $ar], 'OR'); + + $achievements = new AchievementList($conditions); + if (!$achievements->error) + { + $this->storeJSGlobals($achievements->getJSGlobals(GLOBALINFO_SELF | GLOBALINFO_REWARDS)); + [, &$aData, , , , &$hiddenCols, &$visibleCols] = $this->listviewTabs[self::ACHIEVEMENT_REWARD]; + + foreach ($achievements->getListviewData() as $id => $row) + $aData[] = array_merge($row, $result[$id] ?? ['percent' => -1]); + + $hiddenCols[] = 'rewards'; + $visibleCols[] = 'category'; + } + + return !$quests->error || !$achievements->error; + } + + private function handleObjectLoot(array $ids, array $result) : bool + { + if (!$ids) + return false; + + $srcObj = new GameObjectList(array(['lootId', $ids])); + if ($srcObj->error) + return false; + + foreach ($srcObj->getListviewData() as $id => $row) + { + $tabId = match($row['type']) + { + 25 => self::OBJECT_FISHED, // fishing node + -3 => self::OBJECT_GATHERED, // herb + -4 => self::OBJECT_MINED, // vein + default => self::OBJECT_CONTAINED // general chest loot + }; + + [, &$tabData, , , &$extraCols, , &$visibleCols] = $this->listviewTabs[$tabId]; + + $tabData[] = array_merge($row, $result[$srcObj->getEntry($id)['lootId']]); + $extraCols[] = '$Listview.extraCols.percent'; + if ($tabId != 15) + $visibleCols[] = 'skill'; + } + + return true; + } +} + +?> diff --git a/includes/kernel.php b/includes/kernel.php index 8f1c9780..47f2f407 100644 --- a/includes/kernel.php +++ b/includes/kernel.php @@ -54,6 +54,8 @@ spl_autoload_register(function (string $class) : void require_once 'includes/game/chrstatistics.php'; else if (file_exists('includes/game/'.strtolower($class).'.class.php')) require_once 'includes/game/'.strtolower($class).'.class.php'; + else if (file_exists('includes/game/loot/'.strtolower($class).'.class.php')) + require_once 'includes/game/loot/'.strtolower($class).'.class.php'; }); // our site components diff --git a/includes/utilities.php b/includes/utilities.php index e4e8aff6..8822745e 100644 --- a/includes/utilities.php +++ b/includes/utilities.php @@ -6,6 +6,27 @@ if (!defined('AOWOW_REVISION')) die('illegal access'); + +// PHP 8.4 polyfill +if (version_compare(PHP_VERSION, '8.4.0') < 0) +{ + function array_find(array $array, callable $callback) : mixed + { + foreach ($array as $k => $v) + if ($callback($v, $k)) + return $array[$k]; + return null; + } + + function array_find_key(array $array, callable $callback) : mixed + { + foreach ($array as $k => $v) + if ($callback($v, $k)) + return $k; + return null; + } +} + class SimpleXML extends \SimpleXMLElement { public function addCData(string $cData) : \SimpleXMLElement diff --git a/localization/locale_dede.php b/localization/locale_dede.php index a094562b..94cec81b 100644 --- a/localization/locale_dede.php +++ b/localization/locale_dede.php @@ -397,7 +397,11 @@ $lang = array( 'phases' => "Phasen", 'mode' => "Modus: ", - 'modes' => [-1 => "Beliebig", "Normal / Normal 10", "Heroisch / Normal 25", "Heroisch 10", "Heroisch 25"], + 'modes' => array( + [-1 => "Beliebig", "Normal / Normal 10", "Heroisch / Normal 25", "Heroisch 10", "Heroisch 25"], + ["Normal", "Heroisch"], + ["Normal 10", "Normal 25", "Heroisch 10", "Heroisch 25"] + ), 'expansions' => ["Classic", "The Burning Crusade", "Wrath of the Lich King"], 'stats' => ["Stärke", "Beweglichkeit", "Ausdauer", "Intelligenz", "Willenskraft"], 'timeAbbrev' => array( @@ -1208,10 +1212,6 @@ $lang = array( 'mechanicimmune'=> 'Nicht anfällig für Mechanik: %s', '_extraFlags' => 'Extra Flags: ', 'versions' => 'Schwierigkeitsgrade: ', - 'modes' => array( - 1 => ["Normal", "Heroisch"], - 2 => ["10-Spieler Normal", "25-Spieler Normal", "10-Spieler Heroisch", "25-Spieler Heroisch"] - ), 'cat' => array( "Nicht kategorisiert", "Wildtiere", "Drachkin", "Dämonen", "Elementare", "Riesen", "Untote", "Humanoide", "Tiere", "Mechanisch", "Nicht spezifiziert", "Totems", "Haustiere", "Gaswolken" diff --git a/localization/locale_enus.php b/localization/locale_enus.php index 42d18fd6..9a7a1310 100644 --- a/localization/locale_enus.php +++ b/localization/locale_enus.php @@ -397,7 +397,11 @@ $lang = array( 'phases' => "Phases", 'mode' => "Mode: ", - 'modes' => [-1 => "Any", "Normal / Normal 10", "Heroic / Normal 25", "Heroic 10", "Heroic 25"], + 'modes' => array( + [-1 => "Any", "Normal / Normal 10", "Heroic / Normal 25", "Heroic 10", "Heroic 25"], + ["Normal", "Heroic"], + ["Normal 10", "Normal 25", "Heroic 10", "Heroic 25"] + ), 'expansions' => ["Classic", "The Burning Crusade", "Wrath of the Lich King"], 'stats' => ["Strength", "Agility", "Stamina", "Intellect", "Spirit"], 'timeAbbrev' => array( //