diff --git a/includes/components/dbtypelist.class.php b/includes/components/dbtypelist.class.php index 43dc976b..c0d98f82 100644 --- a/includes/components/dbtypelist.class.php +++ b/includes/components/dbtypelist.class.php @@ -144,6 +144,9 @@ abstract class DBTypeList if ($h = array_filter(array_column($this->queryOpts, 'h'))) $this->queryBase .= ' HAVING '.implode(' AND ', $h); + // fill in locale + $this->queryBase = str_replace(['DB_LOC_I', 'DB_LOC_S'], [Lang::getLocale()->value, '"'.Lang::getLocale()->json().'"'], $this->queryBase); + // without applied LIMIT and ORDER if ($calcTotal) $totalQuery = $this->queryBase; @@ -227,12 +230,12 @@ abstract class DBTypeList $literal = false; - if (is_array($expOrField)) + if (is_array($expOrField) && $op != 'MATCH') $field = $this->resolveCondition($expOrField, $supLink); else { // basic formulas ex: [((minGold + maxGold) / 2), 0, '>'] - if (preg_match('/^\([\s\+\-\*\/\w\(\)\.]+\)$/i', strtr($expOrField, ['`' => '', '´' => '', '--' => '']))) + if (is_string($expOrField) && preg_match('/^\([\s\+\-\*\/\w\(\)\.]+\)$/i', strtr($expOrField, ['`' => '', '´' => '', '--' => '']))) { $field = preg_replace_callback('/[\w\]*\.?[\w]+/i', $this->setColPrefix(...), $expOrField); $literal = true; @@ -258,21 +261,20 @@ abstract class DBTypeList if (!$expr) return null; - // [[flags, 0x4, '&'], 0] -> (`flags` & 4) = 0 - if (is_array($field)) // $field is expression - return [...$field, $expr, $value]; if ($op == 'MATCH' && gettype($value) == 'array') return ['MATCH(%n)', $field, 'AGAINST(%s IN BOOLEAN MODE)', DB::Aowow()->translate($value)]; if (($op == 'LIKE' || $op == 'NOT LIKE') && gettype($value) == 'string') return ['%n', $field, $op, '%~like~', $value]; + if (is_array($field)) // $field is expression: [[flags, 0x4, '&'], 0] -> (`flags` & 4) = 0 + return [...$field, $expr, $value]; return [$literal ? '%SQL' : '%n', $field, $expr, $value]; } - private function setColPrefix(mixed $colName) : ?string + private function setColPrefix(mixed $colName) : null|string|array { if (is_array($colName)) - $colName = $colName[0]; + return array_filter(array_map([$this, 'setColPrefix'], $colName)) ?: null; // numeric allows for formulas e.g. (1 < 3) if (Util::checkNumeric($colName)) diff --git a/includes/components/filter.class.php b/includes/components/filter.class.php index 810e1a49..13b44d32 100644 --- a/includes/components/filter.class.php +++ b/includes/components/filter.class.php @@ -113,7 +113,7 @@ abstract class Filter [self::CR_BOOLEAN, , , null] [self::CR_FLAG, , , ] # default param2: matchExact [self::CR_NUMERIC, , , ] - [self::CR_STRING, , , null] + [self::CR_STRING, , , , , ] # param3 ? crv is val in enum : key in enum [self::CR_STAFFFLAG, , null, null] [self::CR_CALLBACK, , , ] @@ -600,7 +600,13 @@ abstract class Filter return null; // if the fulltext token contains invalid chars, should it be sub-tokenized (current behavior) or should the chars just be stripped - $ft = array_filter(explode(' ', preg_replace(self::PATTERN_FT, ' ', $str)), $lenTest); + if ($tok = explode(' ', preg_replace(self::PATTERN_FT, ' ', $str))) + { + $ft = array_filter($tok, $lenTest); + + if (count($tok) > 1) + $ft[] = implode('', $tok); + } // escape manually entered _; entering % should be prohibited // then replace search wildcards with sql wildcards @@ -646,7 +652,7 @@ abstract class Filter protected function buildLikeLookup(array $fields, bool $exact = false) : array { $qry = []; - foreach ($fields as $field => $col) + foreach ($fields as [$field, $col]) { $sub = []; if (!empty($this->inTokens[$field])) @@ -669,17 +675,25 @@ abstract class Filter protected function buildMatchLookup(array $fields, bool $exact = false) : array { if (Lang::getLocale()->isLogographic() && !Cfg::get('LOGOGRAPHIC_FT_SEARCH')) - return $this->buildLikeLookup($fields, $exact); + return []; $qry = []; - foreach ($fields as $field => $col) + foreach ($fields as [$field, $col]) { if (!empty($this->ftTokens[$field])) - $qry[] = [$col, $this->ftTokens[$field], 'MATCH']; - - $tok = $this->values[$field]; - if (self::transformToken($tok)) - $qry[] = [$col, $tok]; + $qry[] = [$col, array_unique($this->ftTokens[$field]), 'MATCH']; + else + { + $tok = $this->values[$field]; + if (self::transformToken($tok)) + { + if (!is_array($col)) + $qry[] = [$col, $tok]; + else + foreach ($col as $c) + $qry[] = [$c, $tok]; + } + } } return $qry ? [DB::OR, ...$qry] : []; @@ -754,14 +768,19 @@ abstract class Filter return [[$field, $value, '&'], $value]; } - private function genericString(string $field, ?int $strFlags) : ?array + private function genericString(string $field, ?int $strFlags, ?string $fulltextCol = null) : ?array { $strFlags ??= 0x0; + $lkCol = $field; if ($strFlags & STR_LOCALIZED) - $field .= '_loc'.Lang::getLocale()->value; + $lkCol .= '_loc'.Lang::getLocale()->value; - return $this->buildLikeLookup([$field => $field]); + $lookup = null; + if ($fulltextCol) + $lookup = $this->buildMatchLookup([[$lkCol, $fulltextCol]]); + + return $lookup ?: ($field ? $this->buildLikeLookup([[$lkCol, $lkCol]]) : null); } private function genericNumeric(string $field, int|float $value, int $op, int $typeCast) : ?array @@ -837,7 +856,7 @@ abstract class Filter self::CR_FLAG => $this->genericBooleanFlags($colOrFn, $param1, $crs, $param2), self::CR_STAFFFLAG => $this->genericBooleanFlags($colOrFn, (1 << ($crs - 1)), true), self::CR_BOOLEAN => $this->genericBoolean($colOrFn, $crs, !empty($param1)), - self::CR_STRING => $this->genericString($colOrFn, $param1), + self::CR_STRING => $this->genericString($colOrFn, $param1, $param2), self::CR_CALLBACK => $this->{$colOrFn}($cr, $crs, $crv, $param1, $param2), self::CR_ENUM => $handleEnum($cr, $crs, $colOrFn, $param1, $param2), self::CR_NYI_PH => $handleNYIPH($crs, $crv, $param1), diff --git a/includes/components/search.class.php b/includes/components/search.class.php index e8e35dda..78f52f21 100644 --- a/includes/components/search.class.php +++ b/includes/components/search.class.php @@ -162,32 +162,21 @@ class Search return $qry; } - private function createMatchLookup(array $fields = []) : array + private function createMatchLookup() : array { if ($this->idSearch && $this->included) return ['id', $this->included]; if (Lang::getLocale()->isLogographic() && !Cfg::get('LOGOGRAPHIC_FT_SEARCH')) - return $this->createLikeLookup($fields); + return $this->createLikeLookup(); - // default to name-field - if (!$fields) - $fields[] = 'name_loc'.Lang::getLocale()->value; - - $qry = []; if ($this->fulltext) - $qry = array_map(fn($x) => [$x, $this->fulltext, 'MATCH'], $fields); + return ['nml.nName', $this->fulltext, 'MATCH']; else if ($strBak = trim($this->query)) if (mb_strlen($strBak) > 2 || Lang::getLocale()->isLogographic()) - $qry = array_map(fn($x) => [$x, $strBak], $fields); + return ['name_loc'.Lang::getLocale()->value, $strBak]; - // single cnd? - if (count($qry) > 1) - array_unshift($qry, DB::OR); - else if (count($qry) == 1) - $qry = $qry[0]; - - return $qry; + return []; } public function canPerform() : bool diff --git a/includes/dbtypes/achievement.class.php b/includes/dbtypes/achievement.class.php index 72b45efc..5a665136 100644 --- a/includes/dbtypes/achievement.class.php +++ b/includes/dbtypes/achievement.class.php @@ -328,9 +328,9 @@ class AchievementListFilter extends Filter { $_ = []; if ($_v['ex'] == 'on') - $_ = $this->buildLikeLookup(['na' => 'name_loc'.Lang::getLocale()->value, 'na' => 'reward_loc'.Lang::getLocale()->value, 'na' => 'description_loc'.Lang::getLocale()->value]); + $_ = $this->buildLikeLookup([['na', 'name_loc'.Lang::getLocale()->value], ['na', 'reward_loc'.Lang::getLocale()->value], ['na', 'description_loc'.Lang::getLocale()->value]]); else - $_ = $this->buildLikeLookup(['na' => 'name_loc'.Lang::getLocale()->value]); + $_ = $this->buildLikeLookup([['na', 'name_loc'.Lang::getLocale()->value]]); if ($_) $parts[] = $_; diff --git a/includes/dbtypes/areatrigger.class.php b/includes/dbtypes/areatrigger.class.php index 0012d60e..aa0a543e 100644 --- a/includes/dbtypes/areatrigger.class.php +++ b/includes/dbtypes/areatrigger.class.php @@ -85,7 +85,7 @@ class AreaTriggerListFilter extends Filter // name [str] if ($_v['na']) - if ($_ = $this->buildLikeLookup(['na' => 'name'])) + if ($_ = $this->buildLikeLookup([['na', 'name']])) $parts[] = $_; // type [list] diff --git a/includes/dbtypes/arenateam.class.php b/includes/dbtypes/arenateam.class.php index b50b84fc..9103da0c 100644 --- a/includes/dbtypes/arenateam.class.php +++ b/includes/dbtypes/arenateam.class.php @@ -73,7 +73,7 @@ class ArenaTeamListFilter extends Filter // name [str] if ($_v['na']) - if ($_ = $this->buildLikeLookup(['na' => 'at.name'], $_v['ex'] == 'on')) + if ($_ = $this->buildLikeLookup([['na', 'at.name']], $_v['ex'] == 'on')) $parts[] = $_; // side [list] diff --git a/includes/dbtypes/creature.class.php b/includes/dbtypes/creature.class.php index c9e7947a..6c259153 100644 --- a/includes/dbtypes/creature.class.php +++ b/includes/dbtypes/creature.class.php @@ -17,6 +17,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"'], + 'nml' => ['j' => ['::creature_search nml ON nml.`id` = ct.`id` AND nml.`locale` = DB_LOC_I']], 'dct1' => ['j' => ['::creature dct1 ON ct.`cuFlags` & 0x02 AND dct1.`difficultyEntry1` = ct.`id`', true]], 'dct2' => ['j' => ['::creature dct2 ON ct.`cuFlags` & 0x02 AND dct2.`difficultyEntry2` = ct.`id`', true]], 'dct3' => ['j' => ['::creature dct3 ON ct.`cuFlags` & 0x02 AND dct3.`difficultyEntry3` = ct.`id`', true]], @@ -364,15 +365,19 @@ class CreatureListFilter extends Filter // name [str] if ($_v['na']) { - if ($_v['ex'] == 'on') - if ($_ = $this->buildLikeLookup(['na' => 'subname_loc'.Lang::getLocale()->value])) - $parts[] = $_; + $f = [['na', ['nml.nName', 'nml.nSubname']]]; + if ($_v['ex'] != 'on') + $f = [['na', 'nml.nName']]; - if ($_ = $this->buildMatchLookup(['na' => 'name_loc'.Lang::getLocale()->value])) + if ($_ = $this->buildMatchLookup($f)) + $parts[] = $_; + else { - if ($parts) - $parts = [DB::OR, $_, ...$parts]; - else + $f = [['na', 'name_loc'.Lang::getLocale()->value], ['na', 'subname_loc'.Lang::getLocale()->value]]; + if ($_v['ex'] != 'on') + $f = [$f[0]]; + + if ($_ = $this->buildLikeLookup($f)) $parts[] = $_; } } diff --git a/includes/dbtypes/enchantment.class.php b/includes/dbtypes/enchantment.class.php index e3966bc7..28c170e4 100644 --- a/includes/dbtypes/enchantment.class.php +++ b/includes/dbtypes/enchantment.class.php @@ -249,7 +249,7 @@ class EnchantmentListFilter extends Filter //string if ($_v['na']) - if ($_ = $this->buildLikeLookup(['na' => 'name_loc'.Lang::getLocale()->value])) + if ($_ = $this->buildLikeLookup([['na', 'name_loc'.Lang::getLocale()->value]])) $parts[] = $_; // type diff --git a/includes/dbtypes/gameobject.class.php b/includes/dbtypes/gameobject.class.php index 6ec73205..ad0fe167 100644 --- a/includes/dbtypes/gameobject.class.php +++ b/includes/dbtypes/gameobject.class.php @@ -17,6 +17,7 @@ class GameObjectList extends DBTypeList protected string $queryBase = 'SELECT o.*, o.`id` AS ARRAY_KEY FROM ::objects o'; protected array $queryOpts = array( 'o' => [['ft', 'qse']], + 'nml' => ['j' => ['::objects_search nml ON nml.`id` = o.`id` AND nml.`locale` = DB_LOC_I']], 'ft' => ['j' => ['::factiontemplate ft ON ft.`id` = o.`faction`', true], 's' => ', ft.`factionId`, IFNULL(ft.`A`, 0) AS "A", IFNULL(ft.`H`, 0) AS "H"'], 'qse' => ['j' => ['::quests_startend qse ON qse.`type` = 2 AND qse.`typeId` = o.id', true], 's' => ', IF(MIN(qse.`method`) = 1 OR MAX(qse.`method`) = 3, 1, 0) AS "startsQuests", IF(MIN(qse.`method`) = 2 OR MAX(qse.`method`) = 3, 1, 0) AS "endsQuests"', 'g' => 'o.`id`'], 'qt' => ['j' => '::quests qt ON qse.`questId` = qt.`id`'], @@ -180,8 +181,12 @@ class GameObjectListFilter extends Filter // name if ($_v['na']) - if ($_ = $this->buildMatchLookup(['na' => 'name_loc'.Lang::getLocale()->value])) + { + if ($_ = $this->buildMatchLookup([['na', 'nml.nName']])) $parts[] = $_; + else if ($_ = $this->buildLikeLookup([['na', 'name_loc'.Lang::getLocale()->value]])) + $parts[] = $_; + } return $parts; } diff --git a/includes/dbtypes/guild.class.php b/includes/dbtypes/guild.class.php index 93a59211..e31ea4cc 100644 --- a/includes/dbtypes/guild.class.php +++ b/includes/dbtypes/guild.class.php @@ -114,7 +114,7 @@ class GuildListFilter extends Filter // name [str] if ($_v['na']) - if ($_ = $this->buildLikeLookup(['na' => 'g.name'], $_v['ex'] == 'on')) + if ($_ = $this->buildLikeLookup([['na', 'g.name']], $_v['ex'] == 'on')) $parts[] = $_; // side [list] diff --git a/includes/dbtypes/icon.class.php b/includes/dbtypes/icon.class.php index a48764f3..c3971a97 100644 --- a/includes/dbtypes/icon.class.php +++ b/includes/dbtypes/icon.class.php @@ -145,7 +145,7 @@ class IconListFilter extends Filter //string if ($_v['na']) - if ($_ = $this->buildLikeLookup(['na' => 'name'])) + if ($_ = $this->buildLikeLookup([['na', 'name']])) $parts[] = $_; return $parts; diff --git a/includes/dbtypes/item.class.php b/includes/dbtypes/item.class.php index ba543d82..2932d2a9 100644 --- a/includes/dbtypes/item.class.php +++ b/includes/dbtypes/item.class.php @@ -28,6 +28,7 @@ class ItemList extends DBTypeList 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 'i' => [['is', 'src', 'ic'], 'o' => 'i.`quality` DESC, i.`itemLevel` DESC'], + 'nml' => ['j' => ['::items_search nml ON nml.`id` = i.`id` AND nml.`locale` = DB_LOC_I']], 'ic' => ['j' => ['::icons `ic` ON `ic`.`id` = `i`.`iconId`', true], 's' => ', ic.`name` AS "iconString"'], 'is' => ['j' => ['::item_stats `is` ON `is`.`type` = 3 AND `is`.`typeId` = `i`.`id`', true], 's' => ', `is`.*'], 's' => ['j' => ['::spell `s` ON `s`.`effect1CreateItemId` = `i`.`id`', true], 'g' => 'i.`id`'], @@ -1917,10 +1918,10 @@ class ItemListFilter extends Filter 101 => [parent::CR_NUMERIC, 'is.rgdhastertng', NUM_CAST_INT, true ], // rgdhastertng 102 => [parent::CR_NUMERIC, 'is.splhastertng', NUM_CAST_INT, true ], // splhastertng 103 => [parent::CR_NUMERIC, 'is.hastertng', NUM_CAST_INT, true ], // hastertng - 104 => [parent::CR_STRING, 'description', STR_LOCALIZED ], // flavortext + 104 => [parent::CR_STRING, 'description', STR_LOCALIZED, 'nml.nDescription'], // flavortext 105 => [parent::CR_CALLBACK, 'cbDropsInInstance', SRC_FLAG_DUNGEON_DROP, 1 ], // dropsinnormal [heroicdungeon-any] 106 => [parent::CR_CALLBACK, 'cbDropsInInstance', SRC_FLAG_DUNGEON_DROP, 2 ], // dropsinheroic [heroicdungeon-any] - 107 => [parent::CR_STRING, 'effects', STR_LOCALIZED ], // effecttext [str] + 107 => [parent::CR_STRING, '', STR_LOCALIZED, 'nml.nEffects' ], // effecttext [str] 109 => [parent::CR_CALLBACK, 'cbArmorBonus', null, null ], // armorbonus [op] [int] 111 => [parent::CR_NUMERIC, 'requiredSkillRank', NUM_CAST_INT, true ], // reqskillrank 113 => [parent::CR_FLAG, 'cuFlags', CUSTOM_HAS_SCREENSHOT ], // hasscreenshots @@ -2095,8 +2096,12 @@ class ItemListFilter extends Filter // name if ($_v['na']) - if ($_ = $this->buildMatchLookup(['na' => 'name_loc'.Lang::getLocale()->value])) + { + if ($_ = $this->buildMatchLookup([['na', 'nml.nName']])) $parts[] = $_; + else if ($_ = $this->buildLikeLookup([['na', 'name_loc'.Lang::getLocale()->value]])) + $parts[] = $_; + } // usable-by (not excluded by requiredClass && armor or weapons match mask from ::classes) if ($_v['ub']) diff --git a/includes/dbtypes/itemset.class.php b/includes/dbtypes/itemset.class.php index 064ac4a7..a80e5886 100644 --- a/includes/dbtypes/itemset.class.php +++ b/includes/dbtypes/itemset.class.php @@ -199,7 +199,7 @@ class ItemsetListFilter extends Filter // name [str] if ($_v['na']) - if ($_ = $this->buildLikeLookup(['na' => 'name_loc'.Lang::getLocale()->value])) + if ($_ = $this->buildLikeLookup([['na', 'name_loc'.Lang::getLocale()->value]])) $parts[] = $_; // quality [enum] diff --git a/includes/dbtypes/profile.class.php b/includes/dbtypes/profile.class.php index 6c6cc58b..541e4b1a 100644 --- a/includes/dbtypes/profile.class.php +++ b/includes/dbtypes/profile.class.php @@ -297,7 +297,7 @@ class ProfileListFilter extends Filter $this->{$prop}['_na'] = array_map(Util::ucWords(...), $this->{$prop}['na']); }; - $parts[] = $this->buildLikeLookup(['na' => $k.'.name', '_na' => $k.'.name'], $_v['ex'] == 'on'); + $parts[] = $this->buildLikeLookup([['na', $k.'.name'], ['_na', $k.'.name']], $_v['ex'] == 'on'); } // side [list] @@ -415,7 +415,7 @@ class ProfileListFilter extends Filter { $n = preg_replace(parent::PATTERN_NAME, '', $crv); if ($this->tokenizeString($cr, $n)) - if ($_ = $this->buildLikeLookup([$cr => 'at.name'])) + if ($_ = $this->buildLikeLookup([[$cr, 'at.name']])) return [DB::AND, ['at.type', $size], $_]; return null; diff --git a/includes/dbtypes/quest.class.php b/includes/dbtypes/quest.class.php index 64b74611..c24c2573 100644 --- a/includes/dbtypes/quest.class.php +++ b/includes/dbtypes/quest.class.php @@ -18,6 +18,7 @@ class QuestList extends DBTypeList protected string $queryBase = 'SELECT q.*, q.`id` AS ARRAY_KEY FROM ::quests q'; protected array $queryOpts = array( 'q' => [], + 'nml' => ['j' => '::quests_search nml ON nml.`id` = q.`id` AND nml.`locale` = DB_LOC_I'], 'rsc' => ['j' => '::spell rsc ON q.`rewardSpellCast` = rsc.`id`'], // limit rewardSpellCasts 'qse' => ['j' => '::quests_startend qse ON q.`id` = qse.`questId`', 's' => ', qse.`method`'], // groupConcat..? 'e' => ['j' => ['::events e ON e.`id` = q.`eventId`', true], 's' => ', e.`holidayId`'] @@ -506,15 +507,19 @@ class QuestListFilter extends Filter // name if ($_v['na']) { - if ($_v['ex'] == 'on') - if ($_ = $this->buildLikeLookup(['na' => 'objectives_loc'.Lang::getLocale()->value, 'na' => 'details_loc'.Lang::getLocale()->value])) - $parts[] = $_; + $f = [['na', ['nml.nName', 'nml.nObjectives', 'nml.nDetails']]]; + if ($_v['ex'] != 'on') + $f = [['na', 'nml.nName']]; - if ($_ = $this->buildMatchLookup(['na' => 'name_loc'.Lang::getLocale()->value])) + if ($_ = $this->buildMatchLookup($f)) + $parts[] = $_; + else { - if ($parts) - $parts[0][] = $_; - else + $f = [['na', 'name_loc'.Lang::getLocale()->value], ['na', 'objectives_loc'.Lang::getLocale()->value], ['na', 'details_loc'.Lang::getLocale()->value]]; + if ($_v['ex'] != 'on') + $f = [$f[0]]; + + if ($_ = $this->buildLikeLookup($f)) $parts[] = $_; } } diff --git a/includes/dbtypes/sound.class.php b/includes/dbtypes/sound.class.php index 85f73300..f43a8fb3 100644 --- a/includes/dbtypes/sound.class.php +++ b/includes/dbtypes/sound.class.php @@ -115,7 +115,7 @@ class SoundListFilter extends Filter // name [str] if ($_v['na']) - if ($_ = $this->buildLikeLookup(['na' => 'name'])) + if ($_ = $this->buildLikeLookup([['na', 'name']])) $parts[] = $_; // type [list] diff --git a/includes/dbtypes/spell.class.php b/includes/dbtypes/spell.class.php index 9ce5a82b..6aefde19 100644 --- a/includes/dbtypes/spell.class.php +++ b/includes/dbtypes/spell.class.php @@ -105,6 +105,7 @@ class SpellList extends DBTypeList protected string $queryBase = 'SELECT s.*, s.`id` AS ARRAY_KEY FROM ::spell s'; protected array $queryOpts = array( 's' => [['src', 'sr', 'ic', 'ica']], // 6: Type::SPELL + 'nml' => ['j' => ['::spell_search nml ON nml.`id` = s.`id` AND nml.`locale` = DB_LOC_I']], 'ic' => ['j' => ['::icons ic ON ic.`id` = s.`iconId`', true], 's' => ', ic.`name` AS "iconString"'], 'ica' => ['j' => ['::icons ica ON ica.`id` = s.`iconIdAlt`', true], 's' => ', ica.`name` AS "iconStringAlt"'], 'sr' => ['j' => ['::spellrange sr ON sr.`id` = s.`rangeId`'], 's' => ', sr.`rangeMinHostile`, sr.`rangeMinFriend`, sr.`rangeMaxHostile`, sr.`rangeMaxFriend`, sr.`name_loc0` AS "rangeText_loc0", sr.`name_loc2` AS "rangeText_loc2", sr.`name_loc3` AS "rangeText_loc3", sr.`name_loc4` AS "rangeText_loc4", sr.`name_loc6` AS "rangeText_loc6", sr.`name_loc8` AS "rangeText_loc8"'], @@ -2603,18 +2604,22 @@ class SpellListFilter extends Filter $parts = []; $_v = &$this->values; - //string (extended) + // string (extended) if ($_v['na']) { - if ($_v['ex'] == 'on') - if ($_ = $this->buildLikeLookup(['na' => 'buff_loc'.Lang::getLocale()->value, 'na' => 'description_loc'.Lang::getLocale()->value])) - $parts[] = $_; + $f = [['na', ['nml.nName', 'nml.nBuff', 'nml.nDescription']]]; + if ($_v['ex'] != 'on') + $f = [['na', 'nml.nName']]; - if ($_ = $this->buildMatchLookup(['na' => 'name_loc'.Lang::getLocale()->value])) + if ($_ = $this->buildMatchLookup($f)) + $parts[] = $_; + else { - if ($parts) - $parts[0][] = $_; - else + $f = [['na', 'name_loc'.Lang::getLocale()->value], ['na', 'buff_loc'.Lang::getLocale()->value], ['na', 'description_loc'.Lang::getLocale()->value]]; + if ($_v['ex'] != 'on') + $f = [$f[0]]; + + if ($_ = $this->buildLikeLookup($f)) $parts[] = $_; } } diff --git a/includes/kernel.php b/includes/kernel.php index 344ddfcd..6afe3dbb 100644 --- a/includes/kernel.php +++ b/includes/kernel.php @@ -7,7 +7,7 @@ mb_substitute_character('none'); // drop invalid char error_reporting(E_ALL); mysqli_report(MYSQLI_REPORT_ERROR); -define('AOWOW_REVISION', 46); +define('AOWOW_REVISION', 47); 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 diff --git a/setup/sql/01-db_structure.sql b/setup/sql/01-db_structure.sql index 99d8df6b..86c69286 100644 --- a/setup/sql/01-db_structure.sql +++ b/setup/sql/01-db_structure.sql @@ -578,11 +578,6 @@ CREATE TABLE `aowow_creature` ( KEY `idx_skinloot` (`skinLootId`), KEY `idx_trainer` (`trainerType`), KEY `idx_trainerrequirement` (`trainerRequirement`), - FULLTEXT `idx_ft_name0` (`name_loc0`), - FULLTEXT `idx_ft_name2` (`name_loc2`), - FULLTEXT `idx_ft_name3` (`name_loc3`), - FULLTEXT `idx_ft_name6` (`name_loc6`), - FULLTEXT `idx_ft_name8` (`name_loc8`), KEY `idx_name0` (`name_loc0`), KEY `idx_name2` (`name_loc2`), KEY `idx_name3` (`name_loc3`), @@ -600,6 +595,24 @@ CREATE TABLE `aowow_creature` ( ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; +-- +-- Table structure for table `aowow_creature_search` +-- + +DROP TABLE IF EXISTS `aowow_creature_search`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `aowow_creature_search` ( + `id` mediumint(8) unsigned NOT NULL, + `locale` tinyint(3) unsigned NOT NULL, + `nName` varchar(100) DEFAULT NULL, + `nSubname` varchar(100) DEFAULT NULL, + PRIMARY KEY (`id`,`locale`), + FULLTEXT KEY `idx_ft_na` (`nName`), + FULLTEXT KEY `idx_ft_na_ex` (`nName`,`nSubname`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + -- -- Table structure for table `aowow_creature_sounds` -- @@ -1460,12 +1473,6 @@ CREATE TABLE `aowow_items` ( `sheatheSoundId` smallint(5) unsigned NOT NULL DEFAULT 0, `unsheatheSoundId` smallint(5) unsigned NOT NULL DEFAULT 0, `flagsCustom` int(10) unsigned NOT NULL DEFAULT 0, - `effects_loc0` text DEFAULT NULL, - `effects_loc2` text DEFAULT NULL, - `effects_loc3` text DEFAULT NULL, - `effects_loc4` text DEFAULT NULL, - `effects_loc6` text DEFAULT NULL, - `effects_loc8` text DEFAULT NULL, PRIMARY KEY (`id`), KEY `items_index` (`class`), KEY `idx_model` (`displayId`), @@ -1482,11 +1489,6 @@ CREATE TABLE `aowow_items` ( KEY `idx_trigger4` (`spellTrigger4`), KEY `idx_trigger5` (`spellTrigger5`), KEY `idx_reqskill` (`requiredSkill`), - FULLTEXT `idx_ft_name0` (`name_loc0`), - FULLTEXT `idx_ft_name2` (`name_loc2`), - FULLTEXT `idx_ft_name3` (`name_loc3`), - FULLTEXT `idx_ft_name6` (`name_loc6`), - FULLTEXT `idx_ft_name8` (`name_loc8`), KEY `idx_name0` (`name_loc0`), KEY `idx_name2` (`name_loc2`), KEY `idx_name3` (`name_loc3`), @@ -1497,6 +1499,26 @@ CREATE TABLE `aowow_items` ( ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; +-- +-- Table structure for table `aowow_items_search` +-- + +DROP TABLE IF EXISTS `aowow_items_search`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `aowow_items_search` ( + `id` mediumint(8) unsigned NOT NULL, + `locale` tinyint(3) unsigned NOT NULL, + `nName` varchar(127) DEFAULT NULL, + `nDescription` varchar(255) DEFAULT NULL, + `nEffects` text DEFAULT NULL, + PRIMARY KEY (`id`,`locale`), + FULLTEXT KEY `idx_ft_na` (`nName`), + FULLTEXT KEY `idx_ft_description` (`nDescription`), + FULLTEXT KEY `idx_ft_effects` (`nEffects`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + -- -- Table structure for table `aowow_items_sounds` -- @@ -1682,11 +1704,6 @@ CREATE TABLE `aowow_objects` ( KEY `idx_onsuccessspell` (`onSuccessSpell`), KEY `idx_auraspell` (`auraSpell`), KEY `idx_triggeredspell` (`triggeredSpell`), - FULLTEXT `idx_ft_name0` (`name_loc0`), - FULLTEXT `idx_ft_name2` (`name_loc2`), - FULLTEXT `idx_ft_name3` (`name_loc3`), - FULLTEXT `idx_ft_name6` (`name_loc6`), - FULLTEXT `idx_ft_name8` (`name_loc8`), KEY `idx_name0` (`name_loc0`), KEY `idx_name2` (`name_loc2`), KEY `idx_name3` (`name_loc3`), @@ -1696,6 +1713,22 @@ CREATE TABLE `aowow_objects` ( ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; +-- +-- Table structure for table `aowow_objects_search` +-- + +DROP TABLE IF EXISTS `aowow_objects_search`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `aowow_objects_search` ( + `id` mediumint(8) unsigned NOT NULL, + `locale` tinyint(3) unsigned NOT NULL, + `nName` varchar(127) DEFAULT NULL, + PRIMARY KEY (`id`,`locale`), + FULLTEXT KEY `idx_ft_na` (`nName`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + -- -- Table structure for table `aowow_pet` -- @@ -2291,11 +2324,6 @@ CREATE TABLE `aowow_quests` ( `objectiveText4_loc8` text DEFAULT NULL, PRIMARY KEY (`id`), KEY `nextQuestIdChain` (`nextQuestIdChain`), - FULLTEXT `idx_ft_name0` (`name_loc0`), - FULLTEXT `idx_ft_name2` (`name_loc2`), - FULLTEXT `idx_ft_name3` (`name_loc3`), - FULLTEXT `idx_ft_name6` (`name_loc6`), - FULLTEXT `idx_ft_name8` (`name_loc8`), KEY `idx_name0` (`name_loc0`), KEY `idx_name2` (`name_loc2`), KEY `idx_name3` (`name_loc3`), @@ -2331,6 +2359,25 @@ CREATE TABLE `aowow_quests` ( ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; +-- +-- Table structure for table `aowow_quests_search` +-- + +DROP TABLE IF EXISTS `aowow_quests_search`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `aowow_quests_search` ( + `id` mediumint(8) unsigned NOT NULL, + `locale` tinyint(3) unsigned NOT NULL, + `nName` varchar(100) DEFAULT NULL, + `nObjectives` text DEFAULT NULL, + `nDetails` text DEFAULT NULL, + PRIMARY KEY (`id`,`locale`), + FULLTEXT KEY `idx_ft_na` (`nName`), + FULLTEXT KEY `idx_ft_na_ex` (`nName`,`nObjectives`,`nDetails`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + -- -- Table structure for table `aowow_quests_startend` -- @@ -2905,11 +2952,6 @@ CREATE TABLE `aowow_spell` ( KEY `effect3AuraId` (`effect3AuraId`), KEY `idx_skill1` (`skillLine1`), KEY `idx_skill2` (`skillLine2OrMask`), - FULLTEXT `idx_ft_name0` (`name_loc0`), - FULLTEXT `idx_ft_name2` (`name_loc2`), - FULLTEXT `idx_ft_name3` (`name_loc3`), - FULLTEXT `idx_ft_name6` (`name_loc6`), - FULLTEXT `idx_ft_name8` (`name_loc8`), KEY `idx_name0` (`name_loc0`), KEY `idx_name2` (`name_loc2`), KEY `idx_name3` (`name_loc3`), @@ -2926,6 +2968,25 @@ CREATE TABLE `aowow_spell` ( ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; +-- +-- Table structure for table `aowow_spell_search` +-- + +DROP TABLE IF EXISTS `aowow_spell_search`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `aowow_spell_search` ( + `id` mediumint(8) unsigned NOT NULL, + `locale` tinyint(3) unsigned NOT NULL, + `nName` varchar(185) DEFAULT NULL, + `nDescription` text DEFAULT NULL, + `nBuff` text DEFAULT NULL, + PRIMARY KEY (`id`,`locale`), + FULLTEXT KEY `idx_ft_na` (`nName`), + FULLTEXT KEY `idx_ft_na_ex` (`nName`,`nDescription`,`nBuff`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + -- -- Table structure for table `aowow_spell_sounds` -- diff --git a/setup/sql/02-db_initial_data.sql b/setup/sql/02-db_initial_data.sql index be9f609a..064dfdfa 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 (1770889049,0,NULL,NULL); +INSERT INTO `aowow_dbversion` VALUES (1772564119,0,NULL,NULL); /*!40000 ALTER TABLE `aowow_dbversion` ENABLE KEYS */; UNLOCK TABLES; diff --git a/setup/sql/updates/1772564118_01.sql b/setup/sql/updates/1772564118_01.sql new file mode 100644 index 00000000..9c80c4e1 --- /dev/null +++ b/setup/sql/updates/1772564118_01.sql @@ -0,0 +1,56 @@ +DROP TABLE IF EXISTS `aowow_quests_search`; +CREATE TABLE `aowow_quests_search` ( + `id` mediumint(8) unsigned NOT NULL, + `locale` tinyint(3) unsigned NOT NULL, + `nName` varchar(100) DEFAULT NULL, + `nObjectives` text DEFAULT NULL, + `nDetails` text DEFAULT NULL, + PRIMARY KEY (`id`, `locale`), + FULLTEXT `idx_ft_na` (`nName`), + FULLTEXT `idx_ft_na_ex` (`nName`, `nObjectives`, `nDetails`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +DROP TABLE IF EXISTS `aowow_objects_search`; +CREATE TABLE `aowow_objects_search` ( + `id` mediumint(8) unsigned NOT NULL, + `locale` tinyint(3) unsigned NOT NULL, + `nName` varchar(127) DEFAULT NULL, + PRIMARY KEY (`id`, `locale`), + FULLTEXT `idx_ft_na` (`nName`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +DROP TABLE IF EXISTS `aowow_items_search`; +CREATE TABLE `aowow_items_search` ( + `id` mediumint(8) unsigned NOT NULL, + `locale` tinyint(3) unsigned NOT NULL, + `nName` varchar(127) DEFAULT NULL, + `nDescription` varchar(255) DEFAULT NULL, + `nEffects` text DEFAULT NULL, + PRIMARY KEY (`id`, `locale`), + FULLTEXT `idx_ft_na` (`nName`), + FULLTEXT `idx_ft_description` (`nDescription`), + FULLTEXT `idx_ft_effects` (`nEffects`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +DROP TABLE IF EXISTS `aowow_creature_search`; +CREATE TABLE `aowow_creature_search` ( + `id` mediumint(8) unsigned NOT NULL, + `locale` tinyint(3) unsigned NOT NULL, + `nName` varchar(100) DEFAULT NULL, + `nSubname` varchar(100) DEFAULT NULL, + PRIMARY KEY (`id`, `locale`), + FULLTEXT `idx_ft_na` (`nName`), + FULLTEXT `idx_ft_na_ex` (`nName`, `nSubname`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +DROP TABLE IF EXISTS `aowow_spell_search`; +CREATE TABLE `aowow_spell_search` ( + `id` mediumint(8) unsigned NOT NULL, + `locale` tinyint(3) unsigned NOT NULL, + `nName` varchar(185) DEFAULT NULL, + `nDescription` text DEFAULT NULL, + `nBuff` text DEFAULT NULL, + PRIMARY KEY (`id`, `locale`), + FULLTEXT `idx_ft_na` (`nName`), + FULLTEXT `idx_ft_na_ex` (`nName`, `nDescription`, `nBuff`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; diff --git a/setup/sql/updates/1772564118_02.sql b/setup/sql/updates/1772564118_02.sql new file mode 100644 index 00000000..55f353a7 --- /dev/null +++ b/setup/sql/updates/1772564118_02.sql @@ -0,0 +1,40 @@ +ALTER TABLE `aowow_creature` + DROP INDEX `idx_ft_name0`, + DROP INDEX `idx_ft_name2`, + DROP INDEX `idx_ft_name3`, + DROP INDEX `idx_ft_name6`, + DROP INDEX `idx_ft_name8`; + +ALTER TABLE `aowow_objects` + DROP INDEX `idx_ft_name0`, + DROP INDEX `idx_ft_name2`, + DROP INDEX `idx_ft_name3`, + DROP INDEX `idx_ft_name6`, + DROP INDEX `idx_ft_name8`; + +ALTER TABLE `aowow_quests` + DROP INDEX `idx_ft_name0`, + DROP INDEX `idx_ft_name2`, + DROP INDEX `idx_ft_name3`, + DROP INDEX `idx_ft_name6`, + DROP INDEX `idx_ft_name8`; + +ALTER TABLE `aowow_spell` + DROP INDEX `idx_ft_name0`, + DROP INDEX `idx_ft_name2`, + DROP INDEX `idx_ft_name3`, + DROP INDEX `idx_ft_name6`, + DROP INDEX `idx_ft_name8`; + +ALTER TABLE `aowow_items` + DROP COLUMN `effects_loc0`, + DROP COLUMN `effects_loc2`, + DROP COLUMN `effects_loc3`, + DROP COLUMN `effects_loc4`, + DROP COLUMN `effects_loc6`, + DROP COLUMN `effects_loc8`, + DROP INDEX `idx_ft_name0`, + DROP INDEX `idx_ft_name2`, + DROP INDEX `idx_ft_name3`, + DROP INDEX `idx_ft_name6`, + DROP INDEX `idx_ft_name8`; diff --git a/setup/tools/sqlgen/creature.ss.php b/setup/tools/sqlgen/creature.ss.php index 0a90edd5..fea5bb27 100644 --- a/setup/tools/sqlgen/creature.ss.php +++ b/setup/tools/sqlgen/creature.ss.php @@ -102,7 +102,6 @@ CLISetup::registerSetup("sql", new class extends SetupScript LIMIT %i, %i'; DB::Aowow()->qry('TRUNCATE ::creature'); - DB::Aowow()->qry('SET SESSION innodb_ft_enable_stopword = OFF'); $i = 0; while ($npcs = DB::World()->selectAssoc($baseQuery, NPC_CU_INSTANCE_BOSS, CLISetup::SQL_BATCH * $i, CLISetup::SQL_BATCH)) diff --git a/setup/tools/sqlgen/items.ss.php b/setup/tools/sqlgen/items.ss.php index 4a1de925..fecd0ee5 100644 --- a/setup/tools/sqlgen/items.ss.php +++ b/setup/tools/sqlgen/items.ss.php @@ -118,8 +118,7 @@ CLISetup::registerSetup("sql", new class extends SetupScript 0 AS dropDownSoundId, 0 AS sheatheSoundId, 0 AS unsheatheSoundId, - flagsCustom, - null AS effects_loc0, null AS effects_loc2, null AS effects_loc3, null AS effects_loc4, null AS effects_loc6, null AS effects_loc8 + flagsCustom FROM item_template it LEFT JOIN item_template_locale itl2 ON it.entry = itl2.ID AND itl2.locale = "frFR" LEFT JOIN item_template_locale itl3 ON it.entry = itl3.ID AND itl3.locale = "deDE" @@ -131,7 +130,6 @@ CLISetup::registerSetup("sql", new class extends SetupScript LIMIT %i, %i'; DB::Aowow()->qry('TRUNCATE ::items'); - DB::Aowow()->qry('SET SESSION innodb_ft_enable_stopword = OFF'); $i = 0; while ($items = DB::World()->selectAssoc($baseQuery, CLISetup::SQL_BATCH * $i, CLISetup::SQL_BATCH)) @@ -265,57 +263,6 @@ CLISetup::registerSetup("sql", new class extends SetupScript DB::Aowow()->qry('UPDATE ::items SET `cuFlags` = `cuFlags` | %i WHERE `class`= %i AND `slotBak` IN %in AND `subClass` NOT IN %in', CUSTOM_EXCLUDE_FOR_LISTVIEW, ITEM_CLASS_WEAPON, $slots, $subclasses); - CLI::write('[items] - collecting spell descriptions'); - CLI::write(' * fetching', tmpRow: true); - - $itemSpellData = DB::Aowow()->selectAssoc( - 'SELECT `id` AS "0", `spellId1` AS "1" FROM ::items WHERE `spellId1` > 0 UNION - SELECT `id` AS "0", `spellId2` AS "1" FROM ::items WHERE `spellId2` > 0 UNION - SELECT `id` AS "0", `spellId3` AS "1" FROM ::items WHERE `spellId3` > 0 UNION - SELECT `id` AS "0", `spellId4` AS "1" FROM ::items WHERE `spellId4` > 0 UNION - SELECT `id` AS "0", `spellId5` AS "1" FROM ::items WHERE `spellId5` > 0' - ); - - $itemSpells = new SpellList(array(['id', array_column($itemSpellData, 1)]), ['interactive' => SpellList::INTERACTIVE_NONE]); - $items = DB::Aowow()->selectAssoc('SELECT `id` AS ARRAY_KEY, `spellId1`, `spellId2`, `spellId3`, `spellId4`, `spellId5` FROM ::items WHERE `id` IN %in', array_column($itemSpellData, 0)); - if (!$itemSpells->error) - { - $i = 0; - $total = count($items) * count(CLISetup::$locales); - $update = []; - foreach (CLISetup::$locales as $locId => $loc) - { - Lang::load($loc); - - foreach ($items as $itemId => $spells) - { - CLI::write(' * applying ' . str_pad(++$i, strlen($total), pad_type: STR_PAD_LEFT) . ' / ' . $total . str_pad('('.number_format(100 * $i / $total, 1).'%)', 8, pad_type: STR_PAD_LEFT), tmpRow: true); - - foreach ($spells as $spellId) - { - if (!$itemSpells->getEntry($spellId)) - continue; - - if ($_ = $itemSpells->parseText('description')) - { - if (!isset($update[$itemId]['effects_loc'.$locId])) - $update[$itemId]['effects_loc'.$locId] = ''; - - $update[$itemId]['effects_loc'.$locId] .= $_[0]."\n"; - } - } - } - } - - $i = 0; - $total = count($update); - foreach ($update as $itemId => $upd) - { - CLI::write(' * writing ' . str_pad(++$i, strlen($total), pad_type: STR_PAD_LEFT) . ' / ' . $total . str_pad('('.number_format(100 * $i / $total, 1).'%)', 8, pad_type: STR_PAD_LEFT), tmpRow: true); - DB::Aowow()->qry('UPDATE ::items SET %a WHERE `id` = %i', $upd, $itemId); - } - } - $this->reapplyCCFlags('items', Type::ITEM); return true; diff --git a/setup/tools/sqlgen/objects.ss.php b/setup/tools/sqlgen/objects.ss.php index 92ff9486..069df674 100644 --- a/setup/tools/sqlgen/objects.ss.php +++ b/setup/tools/sqlgen/objects.ss.php @@ -67,7 +67,6 @@ CLISetup::registerSetup("sql", new class extends SetupScript LIMIT %i, %i'; DB::Aowow()->qry('TRUNCATE ::objects'); - DB::Aowow()->qry('SET SESSION innodb_ft_enable_stopword = OFF'); $i = 0; while ($objects = DB::World()->selectAssoc($baseQuery, CLISetup::SQL_BATCH * $i, CLISetup::SQL_BATCH)) diff --git a/setup/tools/sqlgen/quests.ss.php b/setup/tools/sqlgen/quests.ss.php index 475f5d0a..12558f38 100644 --- a/setup/tools/sqlgen/quests.ss.php +++ b/setup/tools/sqlgen/quests.ss.php @@ -116,7 +116,6 @@ CLISetup::registerSetup("sql", new class extends SetupScript LIMIT %i, %i'; DB::Aowow()->qry('TRUNCATE ::quests'); - DB::Aowow()->qry('SET SESSION innodb_ft_enable_stopword = OFF'); $i = 0; while ($quests = DB::World()->selectAssoc($baseQuery, CLISetup::SQL_BATCH * $i, CLISetup::SQL_BATCH)) diff --git a/setup/tools/sqlgen/search.ss.php b/setup/tools/sqlgen/search.ss.php new file mode 100644 index 00000000..bcaebc09 --- /dev/null +++ b/setup/tools/sqlgen/search.ss.php @@ -0,0 +1,348 @@ + [[ ], CLISetup::ARGV_PARAM, 'Normalize strings from creatures, items, objects, quests & spells for fulltext search.'], +/* 1 */ 'creature' => [['1'], CLISetup::ARGV_OPTIONAL, '...only for creatures.'], +/* 2 */ 'item' => [['2'], CLISetup::ARGV_OPTIONAL, '...only for items.'], +/* 4 */ 'object' => [['3'], CLISetup::ARGV_OPTIONAL, '...only for objects.'], +/* 8 */ 'spell' => [['4'], CLISetup::ARGV_OPTIONAL, '...only for spells.'], +/*16 */ 'quest' => [['5'], CLISetup::ARGV_OPTIONAL, '...only for quests.'] + ); + + protected $setupAfter = [['creature', 'items', 'objects', 'spell', 'quests'], []]; + + private const /* int */ OPT_NPCS = (1 << 0); + private const /* int */ OPT_ITEMS = (1 << 1); + private const /* int */ OPT_OBJECTS = (1 << 2); + private const /* int */ OPT_SPELLS = (1 << 3); + private const /* int */ OPT_QUESTS = (1 << 4); + + private array $spells = []; + private array $locales = []; + + public function generate() : bool + { + // find out what to do + $opts = array_slice(array_keys($this->info), 1); + $getO = CLISetup::getOpt(...$opts); + $mask = null; + + // todo: have an extra search table with ngram fulltext indices + $this->locales = array_filter(CLISetup::$locales, fn($x) => !$x->isLogographic()); + + if ($getO['creature']) + $mask |= self::OPT_NPCS; + if ($getO['item']) + $mask |= self::OPT_ITEMS; + if ($getO['object']) + $mask |= self::OPT_OBJECTS; + if ($getO['spell']) + $mask |= self::OPT_SPELLS; + if ($getO['quest']) + $mask |= self::OPT_QUESTS; + + $mask ??= (self::OPT_NPCS | self::OPT_ITEMS | self::OPT_OBJECTS | self::OPT_SPELLS | self::OPT_QUESTS); + + // do what needs doing + DB::Aowow()->qry('SET SESSION innodb_ft_enable_stopword = OFF'); + + if ($mask & self::OPT_NPCS) + $this->normalizeCreatures(); + if ($mask & self::OPT_ITEMS) + $this->normalizeItems(); + if ($mask & self::OPT_OBJECTS) + $this->normalizeObjects(); + if ($mask & self::OPT_SPELLS) + $this->normalizeSpells(); + if ($mask & self::OPT_QUESTS) + $this->normalizeQuests(); + + return true; + } + + private function normalizeQuests() : void + { + CLI::write(' - creating indices for quest names, descriptions & details...'); + + DB::Aowow()->qry('TRUNCATE ::quests_search'); + + CLI::write(' * fetching', tmpRow: true); + + $rows = DB::Aowow()->selectAssoc( + 'SELECT `id`, 0 AS "locale", `name_loc0` AS "name", `objectives_loc0` AS "objectives", `details_loc0` AS "details" FROM ::quests UNION + SELECT `id`, 2 AS "locale", `name_loc2` AS "name", `objectives_loc2` AS "objectives", `details_loc2` AS "details" FROM ::quests UNION + SELECT `id`, 3 AS "locale", `name_loc3` AS "name", `objectives_loc3` AS "objectives", `details_loc3` AS "details" FROM ::quests UNION + SELECT `id`, 6 AS "locale", `name_loc6` AS "name", `objectives_loc6` AS "objectives", `details_loc6` AS "details" FROM ::quests UNION + SELECT `id`, 8 AS "locale", `name_loc8` AS "name", `objectives_loc8` AS "objectives", `details_loc8` AS "details" FROM ::quests' + ); + + CLI::write(' * normalizing', tmpRow: true); + + array_walk($rows, self::normalizeRow(...), ['name', 'objectives', 'details']); + + $rows = array_filter($rows, fn($x) => $x['name'] !== null); + + $n = ceil(count($rows) / CLISetup::SQL_BATCH); + for ($i = 0; $i < $n; $i++) + { + $sub = array_slice($rows, $i * CLISetup::SQL_BATCH, CLISetup::SQL_BATCH); + + CLI::write(' * inserting batch '.($i + 1).' / '.$n, tmpRow: true); + DB::Aowow()->qry('INSERT INTO ::quests_search %m', array( + 'id' => array_column($sub, 'id'), + 'locale' => array_column($sub, 'locale'), + 'nName' => array_column($sub, 'name'), + 'nObjectives' => array_column($sub, 'objectives'), + 'nDetails' => array_column($sub, 'details') + )); + } + } + + private function normalizeObjects() : void + { + CLI::write(' - creating indices for object names...'); + + DB::Aowow()->qry('TRUNCATE ::objects_search'); + + CLI::write(' * fetching', tmpRow: true); + + $rows = DB::Aowow()->selectAssoc( + 'SELECT `id`, 0 AS "locale", `name_loc0` AS "name" FROM ::objects UNION + SELECT `id`, 2 AS "locale", `name_loc2` AS "name" FROM ::objects UNION + SELECT `id`, 3 AS "locale", `name_loc3` AS "name" FROM ::objects UNION + SELECT `id`, 6 AS "locale", `name_loc6` AS "name" FROM ::objects UNION + SELECT `id`, 8 AS "locale", `name_loc8` AS "name" FROM ::objects' + ); + + CLI::write(' * normalizing', tmpRow: true); + + array_walk($rows, self::normalizeRow(...), ['name']); + + $rows = array_filter($rows, fn($x) => $x['name'] !== null); + + CLI::write(' * inserting', tmpRow: true); + + DB::Aowow()->qry('INSERT INTO ::objects_search %m', array( + 'id' => array_column($rows, 'id'), + 'locale' => array_column($rows, 'locale'), + 'nName' => array_column($rows, 'name') + )); + } + + private function normalizeCreatures() : void + { + CLI::write(' - creating indices for creature names & subnames...'); + + DB::Aowow()->qry('TRUNCATE ::creature_search'); + + CLI::write(' * fetching', tmpRow: true); + + $rows = DB::Aowow()->selectAssoc( + 'SELECT `id`, 0 AS "locale", `name_loc0` AS "name", `subname_loc0` AS "subname" FROM ::creature UNION + SELECT `id`, 2 AS "locale", `name_loc2` AS "name", `subname_loc2` AS "subname" FROM ::creature UNION + SELECT `id`, 3 AS "locale", `name_loc3` AS "name", `subname_loc3` AS "subname" FROM ::creature UNION + SELECT `id`, 6 AS "locale", `name_loc6` AS "name", `subname_loc6` AS "subname" FROM ::creature UNION + SELECT `id`, 8 AS "locale", `name_loc8` AS "name", `subname_loc8` AS "subname" FROM ::creature' + ); + + CLI::write(' * normalizing', tmpRow: true); + + array_walk($rows, self::normalizeRow(...), ['name', 'subname']); + + $rows = array_filter($rows, fn($x) => $x['name'] !== null && $x['subname'] !== null); + + CLI::write(' * inserting', tmpRow: true); + + DB::Aowow()->qry('INSERT INTO ::creature_search %m', array( + 'id' => array_column($rows, 'id'), + 'locale' => array_column($rows, 'locale'), + 'nName' => array_column($rows, 'name'), + 'nSubname' => array_column($rows, 'subname') + )); + } + + private function normalizeItems() : void + { + CLI::write(' - creating indices for item names, descriptions & spells...'); + + DB::Aowow()->qry('TRUNCATE ::items_search'); + + CLI::write(' * fetching', tmpRow: true); + + $result = []; + $items = DB::Aowow()->selectAssoc('SELECT `id` AS ARRAY_KEY, `name_loc0`, `name_loc2`, `name_loc3`, `name_loc6`, `name_loc8`, `description_loc0`, `description_loc2`, `description_loc3`, `description_loc6`, `description_loc8`, `spellId1`, `spellId2`, `spellId3`, `spellId4`, `spellId5` FROM ::items'); + $spells = new SpellList(array(['id', + array_filter(array_column($items, 'spellId1')), + array_filter(array_column($items, 'spellId2')), + array_filter(array_column($items, 'spellId3')), + array_filter(array_column($items, 'spellId4')), + array_filter(array_column($items, 'spellId5')) + ]), ['interactive' => SpellList::INTERACTIVE_NONE]); + + + $n = count($items) * count($this->locales); + $j = 0; + + foreach ($this->locales as $locId => $loc) + { + Lang::load($loc); + + foreach ($items as $id => $item) + { + CLI::write(' * normalizing '.++$j.' / '.$n.' ('.sprintf('%.2f%%', $j * 100 / $n).')', tmpRow: true); + + $name = $desc = $effects = null; + + // ui escape sequences not in default 335a, but undestood by client and may be custom + if ($_ = Util::localizedString($item, 'name', true)) + $name = self::normalize(Lang::unescapeUISequences($_, Lang::FMT_RAW)); + if ($_ = Util::localizedString($item, 'description', true)) + $desc = self::normalize($_); + + for ($i = 1; $i < 6; $i++) + { + $sId = $item['spellId'.$i]; + if (!$sId) + continue; + + if ($spells->getEntry($sId)) + if ($_ = $spells->parseText('description')[0]) + $effects .= str_replace('
', ' ', $_); + } + + if (($effects = self::normalize($effects)) || $name || $desc) + { + $result['id'][] = $id; + $result['locale'][] = $locId; + $result['nName'][] = $name; + $result['nDescription'][] = $desc; + $result['nEffects'][] = $effects; + } + } + } + + $n = ceil(count(current($result)) / CLISetup::SQL_BATCH); + for ($i = 0; $i < $n; $i++) + { + CLI::write(' * inserting batch '.($i + 1).' / '.$n, tmpRow: true); + DB::Aowow()->qry('INSERT INTO ::items_search %m', array_map(fn($x) => array_slice($x, $i * CLISetup::SQL_BATCH, CLISetup::SQL_BATCH), $result)); + } + } + + private function normalizeSpells() : void + { + CLI::write(' - creating indices for spell names, descriptions & buffs...'); + + DB::Aowow()->qry('TRUNCATE ::spell_search'); + + CLI::write(' * fetching', tmpRow: true); + + $splBuf = []; + $result = []; + $spells = DB::Aowow()->selectAssoc('SELECT `id` AS ARRAY_KEY, `name_loc0`, `name_loc2`, `name_loc3`, `name_loc6`, `name_loc8`, `buff_loc0`, `buff_loc2`, `buff_loc3`, `buff_loc6`, `buff_loc8`, `description_loc0`, `description_loc2`, `description_loc3`, `description_loc6`, `description_loc8` FROM ::spell'); + + $n = count($spells) * count($this->locales); + $j = 0; + + foreach ($this->locales as $locId => $loc) + { + Lang::load($loc); + + foreach ($spells as $id => $spell) + { + CLI::write(' * normalizing '.++$j.' / '.$n.' ('.sprintf('%.2f%%', $j * 100 / $n).')', tmpRow: true); + + $name = $desc = $buff = null; + + // initializing a Spell Object and parsing the tooltip is a lot of effort. + // so don't do that unless we really really have to + if (strpos($spell['description_loc'.$locId], '$') || strpos($spell['buff_loc'.$locId], '$')) + { + $splBuf[$id] ??= new SpellList(array(['id', $id]), ['interactive' => SpellList::INTERACTIVE_NONE]); + + if ($_ = $splBuf[$id]->parseText('description')[0]) + $desc = self::normalize(str_replace('
', ' ', $_)); + if ($_ = $splBuf[$id]->parseText('buff')[0]) + $buff = self::normalize(str_replace('
', ' ', $_)); + } + + if ($_ = $desc ?: Util::localizedString($spell, 'description', true)) + $desc = self::normalize($_); + if ($_ = $buff ?: Util::localizedString($spell, 'buff', true)) + $buff = self::normalize($_); + if ($_ = Util::localizedString($spell, 'name', true)) + $name = self::normalize($_); + + if ($buff || $name || $desc) + { + $result['id'][] = $id; + $result['locale'][] = $locId; + $result['nName'][] = $name; + $result['nDescription'][] = $desc; + $result['nBuff'][] = $buff; + } + } + } + + $n = ceil(count(current($result)) / CLISetup::SQL_BATCH); + for ($i = 0; $i < $n; $i++) + { + CLI::write(' * inserting batch '.($i + 1).' / '.$n, tmpRow: true); + DB::Aowow()->qry('INSERT INTO ::spell_search %m', array_map(fn($x) => array_slice($x, $i * CLISetup::SQL_BATCH, CLISetup::SQL_BATCH), $result)); + } + } + + private static function normalizeRow(array &$row, int $idx, array $keys) : void + { + foreach ($keys as $key) + $row[$key] = self::normalize($row[$key] ?? ''); + } + + // e.g. "Zul'Aman O'Reilly" => "Zul Aman ZulAman OReilly Reilly" + private static function normalize(?string $words) : ?string + { + if (!$words) + return null; + + $words = array_filter(explode(' ', $words), fn($x) => mb_strlen($x) > 2); + $result = []; + + foreach ($words as $word) + { + if (($new = trim(preg_replace(Filter::PATTERN_FT, ' ', $word, count: $n))) && $n) + { + if (!strpos($new, ' ')) // caught trailing dots or something + { + $result[] = $new; + continue; + } + + if ($splitWords = array_filter(explode(' ', $new), fn($x) => mb_strlen($x) > 2)) + $result = array_merge($result, $splitWords); + + $result[] = str_replace(' ', '', $new); + + continue; + } + + $result[] = $word; + } + + return $result ? implode(' ', array_unique($result)) : null; + } +}); + +?> diff --git a/setup/tools/sqlgen/spell.ss.php b/setup/tools/sqlgen/spell.ss.php index 14dcba03..7547c1ad 100644 --- a/setup/tools/sqlgen/spell.ss.php +++ b/setup/tools/sqlgen/spell.ss.php @@ -199,7 +199,6 @@ CLISetup::registerSetup("sql", new class extends SetupScript DB::Aowow()->qry('TRUNCATE ::spell'); - DB::Aowow()->qry('SET SESSION innodb_ft_enable_stopword = OFF'); // merge serverside spells into aowow_spell $lastMax = 0;