';
if ($description)
- $x .= ' '.$description.' ';
+ $x .= ' '.Util::jsEscape($description).' ';
if ($criteria)
{
@@ -242,19 +253,16 @@ class AchievementList extends DBTypeList
return $x;
}
- public function getSourceData(int $id = 0) : array
+ public function getSourceData()
{
$data = [];
foreach ($this->iterate() as $__)
{
- if ($id && $id != $this->id)
- continue;
-
$data[$this->id] = array(
"n" => $this->getField('name', true),
"s" => $this->curTpl['faction'],
- "t" => Type::ACHIEVEMENT,
+ "t" => TYPE_ACHIEVEMENT,
"ti" => $this->id
);
}
@@ -266,12 +274,11 @@ class AchievementList extends DBTypeList
class AchievementListFilter extends Filter
{
- protected string $type = 'achievements';
- protected static array $enums = array(
- 4 => parent::ENUM_ZONE, // location
+
+ protected $enums = array(
11 => array(
327 => 160, // Lunar Festival
- 423 => 187, // Love is in the Air
+ 335 => 187, // Love is in the Air
181 => 159, // Noblegarden
201 => 163, // Children's Week
341 => 161, // Midsummer Fire Festival
@@ -281,8 +288,8 @@ class AchievementListFilter extends Filter
141 => 156, // Feast of Winter Veil
409 => -3456, // Day of the Dead
398 => -3457, // Pirates' Day
- parent::ENUM_ANY => true,
- parent::ENUM_NONE => false,
+ FILTER_ENUM_ANY => true,
+ FILTER_ENUM_NONE => false,
283 => -1, // valid events without achievements
285 => -1, 353 => -1, 420 => -1,
400 => -1, 284 => -1, 374 => -1,
@@ -290,100 +297,116 @@ class AchievementListFilter extends Filter
)
);
- protected static array $genericFilter = array(
- 2 => [parent::CR_BOOLEAN, 'reward_loc0', true ], // givesreward
- 3 => [parent::CR_STRING, 'reward', STR_LOCALIZED ], // rewardtext
- 4 => [parent::CR_NYI_PH, null, 1, ], // location [enum]
- 5 => [parent::CR_CALLBACK, 'cbSeries', ACHIEVEMENT_CU_FIRST_SERIES, null], // first in series [yn]
- 6 => [parent::CR_CALLBACK, 'cbSeries', ACHIEVEMENT_CU_LAST_SERIES, null], // last in series [yn]
- 7 => [parent::CR_BOOLEAN, 'chainId', ], // partseries
- 9 => [parent::CR_NUMERIC, 'id', NUM_CAST_INT, true], // id
- 10 => [parent::CR_STRING, 'ic.name', ], // icon
- 11 => [parent::CR_CALLBACK, 'cbRelEvent', null, null], // related event [enum]
- 14 => [parent::CR_FLAG, 'cuFlags', CUSTOM_HAS_COMMENT ], // hascomments
- 15 => [parent::CR_FLAG, 'cuFlags', CUSTOM_HAS_SCREENSHOT ], // hasscreenshots
- 16 => [parent::CR_FLAG, 'cuFlags', CUSTOM_HAS_VIDEO ], // hasvideos
- 18 => [parent::CR_STAFFFLAG, 'flags', ] // flags
+ protected $genericFilter = array( // misc (bool): _NUMERIC => useFloat; _STRING => localized; _FLAG => match Value; _BOOLEAN => stringSet
+ 2 => [FILTER_CR_BOOLEAN, 'reward_loc0', true ], // givesreward
+ 3 => [FILTER_CR_STRING, 'reward', STR_LOCALIZED ], // rewardtext
+ 4 => [FILTER_CR_NYI_PH, null, 1, ], // location [enum]
+ 5 => [FILTER_CR_CALLBACK, 'cbSeries', ACHIEVEMENT_CU_FIRST_SERIES, null], // first in series [yn]
+ 6 => [FILTER_CR_CALLBACK, 'cbSeries', ACHIEVEMENT_CU_LAST_SERIES, null], // last in series [yn]
+ 7 => [FILTER_CR_BOOLEAN, 'chainId', ], // partseries
+ 9 => [FILTER_CR_NUMERIC, 'id', NUM_CAST_INT, true], // id
+ 10 => [FILTER_CR_STRING, 'ic.name', ], // icon
+ 11 => [FILTER_CR_CALLBACK, 'cbRelEvent', null, null], // related event [enum]
+ 14 => [FILTER_CR_FLAG, 'cuFlags', CUSTOM_HAS_COMMENT ], // hascomments
+ 15 => [FILTER_CR_FLAG, 'cuFlags', CUSTOM_HAS_SCREENSHOT ], // hasscreenshots
+ 16 => [FILTER_CR_FLAG, 'cuFlags', CUSTOM_HAS_VIDEO ], // hasvideos
+ 18 => [FILTER_CR_STAFFFLAG, 'flags', ] // flags
);
- protected static array $inputFields = array(
- 'cr' => [parent::V_RANGE, [2, 18], true ], // criteria ids
- 'crs' => [parent::V_LIST, [parent::ENUM_NONE, parent::ENUM_ANY, [0, 99999]], true ], // criteria operators
- 'crv' => [parent::V_REGEX, parent::PATTERN_CRV, true ], // criteria values - only printable chars, no delimiters
- 'na' => [parent::V_NAME, false, false], // name / description - only printable chars, no delimiter
- 'ex' => [parent::V_EQUAL, 'on', false], // extended name search
- 'ma' => [parent::V_EQUAL, 1, false], // match any / all filter
- 'si' => [parent::V_LIST, [SIDE_ALLIANCE, SIDE_HORDE, SIDE_BOTH, -SIDE_ALLIANCE, -SIDE_HORDE], false], // side
- 'minpt' => [parent::V_RANGE, [1, 99], false], // required level min
- 'maxpt' => [parent::V_RANGE, [1, 99], false] // required level max
+ // fieldId => [checkType, checkValue[, fieldIsArray]]
+ protected $inputFields = array(
+ 'cr' => [FILTER_V_RANGE, [2, 18], true ], // criteria ids
+ 'crs' => [FILTER_V_LIST, [FILTER_ENUM_NONE, FILTER_ENUM_ANY, [0, 99999]], true ], // criteria operators
+ 'crv' => [FILTER_V_REGEX, '/[\p{C};:]/ui', true ], // criteria values - only printable chars, no delimiters
+ 'na' => [FILTER_V_REGEX, '/[\p{C};]/ui', false], // name / description - only printable chars, no delimiter
+ 'ex' => [FILTER_V_EQUAL, 'on', false], // extended name search
+ 'ma' => [FILTER_V_EQUAL, 1, false], // match any / all filter
+ 'si' => [FILTER_V_LIST, [1, 2, 3, -1, -2], false], // side
+ 'minpt' => [FILTER_V_RANGE, [1, 99], false], // required level min
+ 'maxpt' => [FILTER_V_RANGE, [1, 99], false] // required level max
);
- protected function createSQLForValues() : array
+ protected function createSQLForCriterium(&$cr)
+ {
+ if (in_array($cr[0], array_keys($this->genericFilter)))
+ if ($genCr = $this->genericCriterion($cr))
+ return $genCr;
+
+ unset($cr);
+ $this->error = true;
+ return [1];
+ }
+
+ protected function createSQLForValues()
{
$parts = [];
- $_v = &$this->values;
+ $_v = &$this->fiData['v'];
// name ex: +description, +rewards
- if ($_v['na'])
+ if (isset($_v['na']))
{
$_ = [];
- if ($_v['ex'] == 'on')
- $_ = $this->buildLikeLookup([['na', 'name_loc'.Lang::getLocale()->value], ['na', 'reward_loc'.Lang::getLocale()->value], ['na', 'description_loc'.Lang::getLocale()->value]]);
+ if (isset($_v['ex']) && $_v['ex'] == 'on')
+ $_ = $this->modularizeString(['name_loc'.User::$localeId, 'reward_loc'.User::$localeId, 'description_loc'.User::$localeId]);
else
- $_ = $this->buildLikeLookup([['na', 'name_loc'.Lang::getLocale()->value]]);
+ $_ = $this->modularizeString(['name_loc'.User::$localeId]);
if ($_)
$parts[] = $_;
}
// points min
- if ($_v['minpt'])
+ if (isset($_v['minpt']))
$parts[] = ['points', $_v['minpt'], '>='];
// points max
- if ($_v['maxpt'])
+ if (isset($_v['maxpt']))
$parts[] = ['points', $_v['maxpt'], '<='];
// faction (side)
- if ($_v['si'])
+ if (isset($_v['si']))
{
- $parts[] = match ($_v['si'])
+ switch ($_v['si'])
{
- -SIDE_ALLIANCE, // equals faction
- -SIDE_HORDE => ['faction', -$_v['si']],
- SIDE_ALLIANCE, // includes faction
- SIDE_HORDE,
- SIDE_BOTH => ['faction', $_v['si'], '&']
- };
+ case -1: // faction, exclusive both
+ case -2:
+ $parts[] = ['faction', -$_v['si']];
+ break;
+ case 1: // faction, inclusive both
+ case 2:
+ case 3: // both
+ $parts[] = ['faction', $_v['si'], '&'];
+ break;
+ }
}
return $parts;
}
- protected function cbRelEvent(int $cr, int $crs, string $crv) : ?array
+ protected function cbRelEvent($cr, $value)
{
- if (!isset(self::$enums[$cr][$crs]))
- return null;
+ if (!isset($this->enums[$cr[0]][$cr[1]]))
+ return false;
- $_ = self::$enums[$cr][$crs];
+ $_ = $this->enums[$cr[0]][$cr[1]];
if (is_int($_))
return ($_ > 0) ? ['category', $_] : ['id', abs($_)];
else
{
- $ids = array_filter(self::$enums[$cr], fn($x) => is_int($x) && $x > 0);
+ $ids = array_filter($this->enums[$cr[0]], function($x) { return is_int($x) && $x > 0; });
return ['category', $ids, $_ ? null : '!'];
}
- return null;
+ return false;
}
- protected function cbSeries(int $cr, int $crs, string $crv, int $seriesFlag) : ?array
+ protected function cbSeries($cr, $value)
{
- if ($this->int2Bool($crs))
- return $crs ? [DB::AND, ['chainId', 0, '!'], ['cuFlags', $seriesFlag, '&']] : [DB::AND, ['chainId', 0, '!'], [['cuFlags', $seriesFlag, '&'], 0]];
+ if ($this->int2Bool($cr[1]))
+ return $cr[1] ? ['AND', ['chainId', 0, '!'], ['cuFlags', $value, '&']] : ['AND', ['chainId', 0, '!'], [['cuFlags', $value, '&'], 0]];
- return null;
+ return false;
}
}
diff --git a/includes/types/arenateam.class.php b/includes/types/arenateam.class.php
new file mode 100644
index 00000000..7bb5ebc8
--- /dev/null
+++ b/includes/types/arenateam.class.php
@@ -0,0 +1,346 @@
+iterate() as $__)
+ {
+ $data[$this->id] = array(
+ 'name' => $this->curTpl['name'],
+ 'realm' => Profiler::urlize($this->curTpl['realmName']),
+ 'realmname' => $this->curTpl['realmName'],
+ // 'battlegroup' => Profiler::urlize($this->curTpl['battlegroup']), // was renamed to subregion somewhere around cata release
+ // 'battlegroupname' => $this->curTpl['battlegroup'],
+ 'region' => Profiler::urlize($this->curTpl['region']),
+ 'faction' => $this->curTpl['faction'],
+ 'size' => $this->curTpl['type'],
+ 'rank' => $this->curTpl['rank'],
+ 'wins' => $this->curTpl['seasonWins'],
+ 'games' => $this->curTpl['seasonGames'],
+ 'rating' => $this->curTpl['rating'],
+ 'members' => $this->curTpl['members']
+ );
+ }
+
+ return array_values($data);
+ }
+
+ public function renderTooltip() {}
+ public function getJSGlobals($addMask = 0) {}
+}
+
+
+class ArenaTeamListFilter extends Filter
+{
+ public $extraOpts = [];
+ protected $genericFilter = [];
+
+ // fieldId => [checkType, checkValue[, fieldIsArray]]
+ protected $inputFields = array(
+ 'na' => [FILTER_V_REGEX, '/[\p{C};]/ui', false], // name - only printable chars, no delimiter
+ 'ma' => [FILTER_V_EQUAL, 1, false], // match any / all filter
+ 'ex' => [FILTER_V_EQUAL, 'on', false], // only match exact
+ 'si' => [FILTER_V_LIST, [1, 2], false], // side
+ 'sz' => [FILTER_V_LIST, [2, 3, 5], false], // tema size
+ 'rg' => [FILTER_V_CALLBACK, 'cbRegionCheck', false], // region
+ 'sv' => [FILTER_V_CALLBACK, 'cbServerCheck', false], // server
+ );
+
+ protected function createSQLForCriterium(&$cr) { }
+
+ protected function createSQLForValues()
+ {
+ $parts = [];
+ $_v = $this->fiData['v'];
+
+ // region (rg), battlegroup (bg) and server (sv) are passed to ArenaTeamList as miscData and handled there
+
+ // name [str]
+ if (!empty($_v['na']))
+ if ($_ = $this->modularizeString(['at.name'], $_v['na'], !empty($_v['ex']) && $_v['ex'] == 'on'))
+ $parts[] = $_;
+
+ // side [list]
+ if (!empty($_v['si']))
+ {
+ if ($_v['si'] == 1)
+ $parts[] = ['c.race', [1, 3, 4, 7, 11]];
+ else if ($_v['si'] == 2)
+ $parts[] = ['c.race', [2, 5, 6, 8, 10]];
+ }
+
+ // size [int]
+ if (!empty($_v['sz']))
+ $parts[] = ['at.type', $_v['sz']];
+
+ return $parts;
+ }
+
+ protected function cbRegionCheck(&$v)
+ {
+ if ($v == 'eu' || $v == 'us')
+ {
+ $this->parentCats[0] = $v; // directly redirect onto this region
+ $v = ''; // remove from filter
+
+ return true;
+ }
+
+ return false;
+ }
+
+ protected function cbServerCheck(&$v)
+ {
+ foreach (Profiler::getRealms() as $realm)
+ if ($realm['name'] == $v)
+ {
+ $this->parentCats[1] = Profiler::urlize($v);// directly redirect onto this server
+ $v = ''; // remove from filter
+
+ return true;
+ }
+
+ return false;
+ }
+}
+
+
+class RemoteArenaTeamList extends ArenaTeamList
+{
+ protected $queryBase = 'SELECT `at`.*, `at`.`arenaTeamId` AS ARRAY_KEY FROM arena_team at';
+ protected $queryOpts = array(
+ 'at' => [['atm', 'c'], 'g' => 'ARRAY_KEY', 'o' => 'rating DESC'],
+ 'atm' => ['j' => 'arena_team_member atm ON atm.arenaTeamId = at.arenaTeamId'],
+ 'c' => ['j' => 'characters c ON c.guid = atm.guid AND c.deleteInfos_Account IS NULL AND c.level <= 80 AND (c.extra_flags & '.Profiler::CHAR_GMFLAGS.') = 0', 's' => ', BIT_OR(IF(c.race IN (1, 3, 4, 7, 11), 1, 2)) - 1 AS faction']
+ );
+
+ private $members = [];
+
+ public function __construct($conditions = [], $miscData = null)
+ {
+ // select DB by realm
+ if (!$this->selectRealms($miscData))
+ {
+ trigger_error('no access to auth-db or table realmlist is empty', E_USER_WARNING);
+ return;
+ }
+
+ parent::__construct($conditions, $miscData);
+
+ if ($this->error)
+ return;
+
+ // ranks in DB are inaccurate. recalculate from rating (fetched as DESC from DB)
+ foreach ($this->dbNames as $rId => $__)
+ foreach ([2, 3, 5] as $type)
+ $this->rankOrder[$rId][$type] = DB::Characters($rId)->selectCol('SELECT arenaTeamId FROM arena_team WHERE `type` = ?d ORDER BY rating DESC', $type);
+
+ reset($this->dbNames); // only use when querying single realm
+ $realmId = key($this->dbNames);
+ $realms = Profiler::getRealms();
+ $distrib = [];
+
+ // post processing
+ foreach ($this->iterate() as $guid => &$curTpl)
+ {
+ // battlegroup
+ $curTpl['battlegroup'] = CFG_BATTLEGROUP;
+
+ // realm, rank
+ $r = explode(':', $guid);
+ if (!empty($realms[$r[0]]))
+ {
+ $curTpl['realm'] = $r[0];
+ $curTpl['realmName'] = $realms[$r[0]]['name'];
+ $curTpl['region'] = $realms[$r[0]]['region'];
+ $curTpl['rank'] = array_search($curTpl['arenaTeamId'], $this->rankOrder[$r[0]][$curTpl['type']]) + 1;
+ }
+ else
+ {
+ trigger_error('arena team "'.$curTpl['name'].'" belongs to nonexistant realm #'.$r, E_USER_WARNING);
+ unset($this->templates[$guid]);
+ continue;
+ }
+
+ // team members
+ $this->members[$r[0]][$r[1]] = $r[1];
+
+ // equalize distribution
+ if (empty($distrib[$curTpl['realm']]))
+ $distrib[$curTpl['realm']] = 1;
+ else
+ $distrib[$curTpl['realm']]++;
+ }
+
+ // get team members
+ foreach ($this->members as $realmId => &$teams)
+ $teams = DB::Characters($realmId)->select('
+ SELECT
+ at.arenaTeamId AS ARRAY_KEY, c.guid AS ARRAY_KEY2, c.name AS "0", c.class AS "1", IF(at.captainguid = c.guid, 1, 0) AS "2"
+ FROM
+ arena_team at
+ JOIN
+ arena_team_member atm ON atm.arenaTeamId = at.arenaTeamId JOIN characters c ON c.guid = atm.guid
+ WHERE
+ at.arenaTeamId IN (?a) AND
+ c.deleteInfos_Account IS NULL AND
+ c.level <= ?d AND
+ (c.extra_flags & ?d) = 0',
+ $teams,
+ MAX_LEVEL,
+ Profiler::CHAR_GMFLAGS
+ );
+
+ // equalize subject distribution across realms
+ $limit = CFG_SQL_LIMIT_DEFAULT;
+ foreach ($conditions as $c)
+ if (is_int($c))
+ $limit = $c;
+
+ $total = array_sum($distrib);
+ foreach ($distrib as &$d)
+ $d = ceil($limit * $d / $total);
+
+ foreach ($this->iterate() as $guid => &$curTpl)
+ {
+ if ($limit <= 0 || $distrib[$curTpl['realm']] <= 0)
+ {
+ unset($this->templates[$guid]);
+ continue;
+ }
+
+ $r = explode(':', $guid);
+ if (isset($this->members[$r[0]][$r[1]]))
+ $curTpl['members'] = array_values($this->members[$r[0]][$r[1]]); // [name, classId, isCaptain]
+
+ $distrib[$curTpl['realm']]--;
+ $limit--;
+ }
+ }
+
+ public function initializeLocalEntries()
+ {
+ $profiles = [];
+ // init members for tooltips
+ foreach ($this->members as $realmId => $teams)
+ {
+ $gladiators = [];
+ foreach ($teams as $team)
+ $gladiators = array_merge($gladiators, array_keys($team));
+
+ $profiles[$realmId] = new RemoteProfileList(array(['c.guid', $gladiators], CFG_SQL_LIMIT_NONE), ['sv' => $realmId]);
+
+ if (!$profiles[$realmId]->error)
+ $profiles[$realmId]->initializeLocalEntries();
+ }
+
+ $data = [];
+ foreach ($this->iterate() as $guid => $__)
+ {
+ $data[$guid] = array(
+ 'realm' => $this->getField('realm'),
+ 'realmGUID' => $this->getField('arenaTeamId'),
+ 'name' => $this->getField('name'),
+ 'nameUrl' => Profiler::urlize($this->getField('name')),
+ 'type' => $this->getField('type'),
+ 'rating' => $this->getField('rating'),
+ 'cuFlags' => PROFILER_CU_NEEDS_RESYNC
+ );
+ }
+
+ // basic arena team data
+ foreach (Util::createSqlBatchInsert($data) as $ins)
+ DB::Aowow()->query('INSERT IGNORE INTO ?_profiler_arena_team (?#) VALUES '.$ins, array_keys(reset($data)));
+
+ // merge back local ids
+ $localIds = DB::Aowow()->selectCol(
+ 'SELECT CONCAT(realm, ":", realmGUID) AS ARRAY_KEY, id FROM ?_profiler_arena_team WHERE realm IN (?a) AND realmGUID IN (?a)',
+ array_column($data, 'realm'),
+ array_column($data, 'realmGUID')
+ );
+
+ foreach ($this->iterate() as $guid => &$_curTpl)
+ if (isset($localIds[$guid]))
+ $_curTpl['id'] = $localIds[$guid];
+
+
+ // profiler_arena_team_member requires profiles and arena teams to be filled
+ foreach ($this->members as $realmId => $teams)
+ {
+ if (empty($profiles[$realmId]))
+ continue;
+
+ $memberData = [];
+ foreach ($teams as $teamId => $team)
+ foreach ($team as $memberId => $member)
+ $memberData[] = array(
+ 'arenaTeamId' => $localIds[$realmId.':'.$teamId],
+ 'profileId' => $profiles[$realmId]->getEntry($realmId.':'.$memberId)['id'],
+ 'captain' => $member[2]
+ );
+
+ foreach (Util::createSqlBatchInsert($memberData) as $ins)
+ DB::Aowow()->query('INSERT IGNORE INTO ?_profiler_arena_team_member (?#) VALUES '.$ins, array_keys(reset($memberData)));
+ }
+ }
+}
+
+
+class LocalArenaTeamList extends ArenaTeamList
+{
+ protected $queryBase = 'SELECT at.*, at.id AS ARRAY_KEY FROM ?_profiler_arena_team at';
+
+ public function __construct($conditions = [], $miscData = null)
+ {
+ parent::__construct($conditions, $miscData);
+
+ if ($this->error)
+ return;
+
+ $realms = Profiler::getRealms();
+
+ // post processing
+ $members = DB::Aowow()->selectCol('SELECT *, arenaTeamId AS ARRAY_KEY, profileId AS ARRAY_KEY2 FROM ?_profiler_arena_team_member WHERE arenaTeamId IN (?a)', $this->getFoundIDs());
+
+ foreach ($this->iterate() as $id => &$curTpl)
+ {
+ if ($curTpl['realm'] && !isset($realms[$curTpl['realm']]))
+ continue;
+
+ if (isset($realms[$curTpl['realm']]))
+ {
+ $curTpl['realmName'] = $realms[$curTpl['realm']]['name'];
+ $curTpl['region'] = $realms[$curTpl['realm']]['region'];
+ }
+
+ // battlegroup
+ $curTpl['battlegroup'] = CFG_BATTLEGROUP;
+
+ $curTpl['members'] = $members[$id];
+ }
+ }
+
+ public function getProfileUrl()
+ {
+ $url = '?arena-team=';
+
+ return $url.implode('.', array(
+ Profiler::urlize($this->getField('region')),
+ Profiler::urlize($this->getField('realmName')),
+ Profiler::urlize($this->getField('name'))
+ ));
+ }
+}
+
+
+?>
diff --git a/includes/dbtypes/charclass.class.php b/includes/types/charclass.class.php
similarity index 58%
rename from includes/dbtypes/charclass.class.php
rename to includes/types/charclass.class.php
index 5c6db072..a3d574f0 100644
--- a/includes/dbtypes/charclass.class.php
+++ b/includes/types/charclass.class.php
@@ -1,32 +1,26 @@
[['ic']],
- 'ic' => ['j' => ['::icons ic ON ic.`id` = c.`iconId`', true], 's' => ', ic.`name` AS "iconString"']
- );
+ protected $queryBase = 'SELECT *, id AS ARRAY_KEY FROM ?_classes c';
- public function __construct($conditions = [], array $miscData = [])
+ public function __construct($conditions = [])
{
- parent::__construct($conditions, $miscData);
+ parent::__construct($conditions);
foreach ($this->iterate() as $k => &$_curTpl)
$_curTpl['skills'] = explode(' ', $_curTpl['skills']);
}
- public function getListviewData() : array
+ public function getListviewData()
{
$data = [];
@@ -52,7 +46,7 @@ class CharClassList extends DBTypeList
return $data;
}
- public function getJSGlobals(int $addMask = 0) : array
+ public function getJSGlobals($addMask = 0)
{
$data = [];
@@ -62,7 +56,8 @@ class CharClassList extends DBTypeList
return $data;
}
- public function renderTooltip() : ?string { return null; }
+ public function addRewardsToJScript(&$ref) { }
+ public function renderTooltip() { }
}
?>
diff --git a/includes/types/charrace.class.php b/includes/types/charrace.class.php
new file mode 100644
index 00000000..9fcfd4c5
--- /dev/null
+++ b/includes/types/charrace.class.php
@@ -0,0 +1,52 @@
+iterate() as $__)
+ {
+ $data[$this->id] = array(
+ 'id' => $this->id,
+ 'name' => $this->getField('name', true),
+ 'classes' => $this->curTpl['classMask'],
+ 'faction' => $this->curTpl['factionId'],
+ 'leader' => $this->curTpl['leader'],
+ 'zone' => $this->curTpl['startAreaId'],
+ 'side' => $this->curTpl['side']
+ );
+
+ if ($this->curTpl['expansion'])
+ $data[$this->id]['expansion'] = $this->curTpl['expansion'];
+ }
+
+ return $data;
+ }
+
+ public function getJSGlobals($addMask = 0)
+ {
+ $data = [];
+
+ foreach ($this->iterate() as $__)
+ $data[TYPE_RACE][$this->id] = ['name' => $this->getField('name', true)];
+
+ return $data;
+ }
+
+ public function addRewardsToJScript(&$ref) { }
+ public function renderTooltip() { }
+}
+
+?>
diff --git a/includes/types/creature.class.php b/includes/types/creature.class.php
new file mode 100644
index 00000000..191f14ec
--- /dev/null
+++ b/includes/types/creature.class.php
@@ -0,0 +1,553 @@
+ [['ft', 'qse', 'dct1', 'dct2', 'dct3'], 's' => ', IFNULL(dct1.id, IFNULL(dct2.id, IFNULL(dct3.id, 0))) AS parentId, IFNULL(dct1.name_loc0, IFNULL(dct2.name_loc0, IFNULL(dct3.name_loc0, ""))) AS parent_loc0, IFNULL(dct1.name_loc2, IFNULL(dct2.name_loc2, IFNULL(dct3.name_loc2, ""))) AS parent_loc2, IFNULL(dct1.name_loc3, IFNULL(dct2.name_loc3, IFNULL(dct3.name_loc3, ""))) AS parent_loc3, IFNULL(dct1.name_loc6, IFNULL(dct2.name_loc6, IFNULL(dct3.name_loc6, ""))) AS parent_loc6, IFNULL(dct1.name_loc8, IFNULL(dct2.name_loc8, IFNULL(dct3.name_loc8, ""))) AS parent_loc8, IF(dct1.difficultyEntry1 = ct.id, 1, IF(dct2.difficultyEntry2 = ct.id, 2, IF(dct3.difficultyEntry3 = ct.id, 3, 0))) AS difficultyMode'],
+ 'dct1' => ['j' => ['?_creature dct1 ON ct.cuFlags & 0x02 AND dct1.difficultyEntry1 = ct.id', true]],
+ 'dct2' => ['j' => ['?_creature dct2 ON ct.cuFlags & 0x02 AND dct2.difficultyEntry2 = ct.id', true]],
+ 'dct3' => ['j' => ['?_creature dct3 ON ct.cuFlags & 0x02 AND dct3.difficultyEntry3 = ct.id', true]],
+ 'ft' => ['j' => '?_factiontemplate ft ON ft.id = ct.faction', 's' => ', ft.A, ft.H, ft.factionId'],
+ 'qse' => ['j' => ['?_quests_startend qse ON qse.type = 1 AND qse.typeId = ct.id', true], 's' => ', IF(min(qse.method) = 1 OR max(qse.method) = 3, 1, 0) AS startsQuests, IF(min(qse.method) = 2 OR max(qse.method) = 3, 1, 0) AS endsQuests', 'g' => 'ct.id'],
+ 'qt' => ['j' => '?_quests qt ON qse.questId = qt.id'],
+ 's' => ['j' => ['?_spawns s ON s.type = 1 AND s.typeId = ct.id', true]]
+ );
+
+ public function __construct($conditions = [], $miscData = null)
+ {
+ parent::__construct($conditions, $miscData);
+
+ if ($this->error)
+ return;
+
+ // post processing
+ foreach ($this->iterate() as $_id => &$curTpl)
+ {
+ // check for attackspeeds
+ if (!$curTpl['atkSpeed'])
+ $curTpl['atkSpeed'] = 2.0;
+ else
+ $curTpl['atkSpeed'] /= 1000;
+
+ if (!$curTpl['rngAtkSpeed'])
+ $curTpl['rngAtkSpeed'] = 2.0;
+ else
+ $curTpl['rngAtkSpeed'] /= 1000;
+ }
+ }
+
+ public static function getName($id)
+ {
+ $n = DB::Aowow()->SelectRow('SELECT name_loc0, name_loc2, name_loc3, name_loc6, name_loc8 FROM ?_creature WHERE id = ?d', $id);
+ return Util::localizedString($n, 'name');
+ }
+
+ public function renderTooltip()
+ {
+ if (!$this->curTpl)
+ return null;
+
+ $level = '??';
+ $type = $this->curTpl['type'];
+ $row3 = [Lang::game('level')];
+ $fam = $this->curTpl['family'];
+
+ if (!($this->curTpl['typeFlags'] & 0x4))
+ {
+ $level = $this->curTpl['minLevel'];
+ if ($level != $this->curTpl['maxLevel'])
+ $level .= ' - '.$this->curTpl['maxLevel'];
+ }
+ else
+ $level = '??';
+
+ $row3[] = $level;
+
+ if ($type)
+ $row3[] = Lang::game('ct', $type);
+
+ if ($_ = Lang::npc('rank', $this->curTpl['rank']))
+ $row3[] = '('.$_.')';
+
+ $x = '';
+ $x .= '| '.$this->getField('name', true).' | ';
+
+ if ($sn = $this->getField('subname', true))
+ $x .= '| '.$sn.' | ';
+
+ $x .= '| '.implode(' ', $row3).' | ';
+
+ if ($type == 1 && $fam) // 1: Beast
+ $x .= '| '.Lang::game('fa', $fam).' | ';
+
+ $fac = new FactionList(array([['cuFlags', CUSTOM_EXCLUDE_FOR_LISTVIEW, '&'], 0], ['id', (int)$this->getField('factionId')]));
+ if (!$fac->error)
+ $x .= '| '.$fac->getField('name', true).' | ';
+
+ $x .= ' ';
+
+ return $x;
+ }
+
+ public function getRandomModelId()
+ {
+ // totems use hardcoded models, tauren model is base
+ $totems = array( // tauren => [orc, dwarf(?!), troll, tauren, draenei]
+ 4589 => [30758, 30754, 30762, 4589, 19074], // fire
+ 4588 => [30757, 30753, 30761, 4588, 19073], // earth
+ 4587 => [30759, 30755, 30763, 4587, 19075], // water
+ 4590 => [30756, 30736, 30760, 4590, 19071], // air
+ );
+
+ $data = [];
+
+ for ($i = 1; $i < 5; $i++)
+ if ($_ = $this->curTpl['displayId'.$i])
+ $data[] = $_;
+
+ if (count($data) == 1 && in_array($data[0], array_keys($totems)))
+ $data = $totems[$data[0]];
+
+ return !$data ? 0 : $data[array_rand($data)];
+ }
+
+ public function getBaseStats($type)
+ {
+ // i'm aware of the BaseVariance/RangedVariance fields ... i'm just totaly unsure about the whole damage calculation
+ switch ($type)
+ {
+ case 'health':
+ $hMin = $this->getField('healthMin');
+ $hMax = $this->getField('healthMax');
+ return [$hMin, $hMax];
+ case 'power':
+ $mMin = $this->getField('manaMin');
+ $mMax = $this->getField('manaMax');
+ return [$mMin, $mMax];
+ case 'armor':
+ $aMin = $this->getField('armorMin');
+ $aMax = $this->getField('armorMax');
+ return [$aMin, $aMax];
+ case 'melee':
+ $mleMin = ($this->getField('dmgMin') + ($this->getField('mleAtkPwrMin') / 14)) * $this->getField('dmgMultiplier') * $this->getField('atkSpeed');
+ $mleMax = ($this->getField('dmgMax') * 1.5 + ($this->getField('mleAtkPwrMax') / 14)) * $this->getField('dmgMultiplier') * $this->getField('atkSpeed');
+ return [$mleMin, $mleMax];
+ case 'ranged':
+ $rngMin = ($this->getField('dmgMin') + ($this->getField('rngAtkPwrMin') / 14)) * $this->getField('dmgMultiplier') * $this->getField('rngAtkSpeed');
+ $rngMax = ($this->getField('dmgMax') * 1.5 + ($this->getField('rngAtkPwrMax') / 14)) * $this->getField('dmgMultiplier') * $this->getField('rngAtkSpeed');
+ return [$rngMin, $rngMax];
+ default:
+ return [0, 0];
+ }
+ }
+
+ public function isBoss()
+ {
+ return ($this->curTpl['cuFlags'] & NPC_CU_INSTANCE_BOSS) || ($this->curTpl['typeFlags'] & 0x4 && $this->curTpl['rank']);
+ }
+
+ public function getListviewData($addInfoMask = 0x0)
+ {
+ /* looks like this data differs per occasion
+ *
+ * NPCINFO_TAMEABLE (0x1): include texture & react
+ * NPCINFO_MODEL (0x2):
+ * NPCINFO_REP (0x4): include repreward
+ */
+
+ $data = [];
+ $rewRep = [];
+
+ if ($addInfoMask & NPCINFO_REP && $this->getFoundIDs())
+ {
+ $rewRep = DB::World()->selectCol('
+ SELECT creature_id AS ARRAY_KEY, RewOnKillRepFaction1 AS ARRAY_KEY2, RewOnKillRepValue1 FROM creature_onkill_reputation WHERE creature_id IN (?a) AND RewOnKillRepFaction1 > 0 UNION
+ SELECT creature_id AS ARRAY_KEY, RewOnKillRepFaction2 AS ARRAY_KEY2, RewOnKillRepValue2 FROM creature_onkill_reputation WHERE creature_id IN (?a) AND RewOnKillRepFaction2 > 0',
+ $this->getFoundIDs(),
+ $this->getFoundIDs()
+ );
+ }
+
+
+ foreach ($this->iterate() as $__)
+ {
+ if ($addInfoMask & NPCINFO_MODEL)
+ {
+ $texStr = strtolower($this->curTpl['textureString']);
+
+ if (isset($data[$texStr]))
+ {
+ if ($data[$texStr]['minLevel'] > $this->curTpl['minLevel'])
+ $data[$texStr]['minLevel'] = $this->curTpl['minLevel'];
+
+ if ($data[$texStr]['maxLevel'] < $this->curTpl['maxLevel'])
+ $data[$texStr]['maxLevel'] = $this->curTpl['maxLevel'];
+
+ $data[$texStr]['count']++;
+ }
+ else
+ $data[$texStr] = array(
+ 'family' => $this->curTpl['family'],
+ 'minLevel' => $this->curTpl['minLevel'],
+ 'maxLevel' => $this->curTpl['maxLevel'],
+ 'modelId' => $this->curTpl['modelId'],
+ 'displayId' => $this->curTpl['displayId1'],
+ 'skin' => $texStr,
+ 'count' => 1
+ );
+ }
+ else
+ {
+ $data[$this->id] = array(
+ 'family' => $this->curTpl['family'],
+ 'minlevel' => $this->curTpl['minLevel'],
+ 'maxlevel' => $this->curTpl['maxLevel'],
+ 'id' => $this->id,
+ 'boss' => $this->isBoss() ? 1 : 0,
+ 'classification' => $this->curTpl['rank'],
+ 'location' => $this->getSpawns(SPAWNINFO_ZONES),
+ 'name' => $this->getField('name', true),
+ 'type' => $this->curTpl['type'],
+ 'react' => [$this->curTpl['A'], $this->curTpl['H']],
+ );
+
+
+ if ($this->getField('startsQuests'))
+ $data[$this->id]['hasQuests'] = 1;
+
+ if ($_ = $this->getField('subname', true))
+ $data[$this->id]['tag'] = $_;
+
+ if ($addInfoMask & NPCINFO_TAMEABLE) // only first skin of first model ... we're omitting potentially 11 skins here .. but the lv accepts only one .. w/e
+ $data[$this->id]['skin'] = $this->curTpl['textureString'];
+
+ if ($addInfoMask & NPCINFO_REP)
+ {
+ $data[$this->id]['reprewards'] = [];
+ if ($rewRep[$this->id])
+ foreach ($rewRep[$this->id] as $fac => $val)
+ $data[$this->id]['reprewards'][] = [$fac, $val];
+ }
+ }
+ }
+
+ ksort($data);
+ return $data;
+ }
+
+ public function getJSGlobals($addMask = 0)
+ {
+ $data = [];
+
+ foreach ($this->iterate() as $__)
+ $data[TYPE_NPC][$this->id] = ['name' => $this->getField('name', true)];
+
+ return $data;
+ }
+
+ public function getSourceData()
+ {
+ $data = [];
+
+ foreach ($this->iterate() as $__)
+ {
+ $data[$this->id] = array(
+ 'n' => $this->getField('parentId') ? $this->getField('parent', true) : $this->getField('name', true),
+ 't' => TYPE_NPC,
+ 'ti' => $this->getField('parentId') ?: $this->id,
+ // 'bd' => (int)($this->curTpl['cuFlags'] & NPC_CU_INSTANCE_BOSS || ($this->curTpl['typeFlags'] & 0x4 && $this->curTpl['rank']))
+ // 'z' where am i spawned
+ // 'dd' DungeonDifficulty requires 'z'
+ );
+ }
+
+ return $data;
+ }
+
+ public function addRewardsToJScript(&$refs) { }
+
+
+}
+
+
+class CreatureListFilter extends Filter
+{
+ public $extraOpts = null;
+ protected $enums = array(
+ 3 => array( 469, 1037, 1106, 529, 1012, 87, 21, 910, 609, 942, 909, 530, 69, 577, 930, 1068, 1104, 729, 369, 92, 54, 946, 67, 1052, 749,
+ 47, 989, 1090, 1098, 978, 1011, 93, 1015, 1038, 76, 470, 349, 1031, 1077, 809, 911, 890, 970, 169, 730, 72, 70, 932, 1156, 933,
+ 510, 1126, 1067, 1073, 509, 941, 1105, 990, 934, 935, 1094, 1119, 1124, 1064, 967, 1091, 59, 947, 81, 576, 922, 68, 1050, 1085, 889,
+ 589, 270)
+ );
+
+ // cr => [type, field, misc, extraCol]
+ protected $genericFilter = array( // misc (bool): _NUMERIC => useFloat; _STRING => localized; _FLAG => match Value; _BOOLEAN => stringSet
+ 1 => [FILTER_CR_CALLBACK, 'cbHealthMana', 'healthMax', 'healthMin'], // health [num]
+ 2 => [FILTER_CR_CALLBACK, 'cbHealthMana', 'manaMin', 'manaMax' ], // mana [num]
+ 3 => [FILTER_CR_CALLBACK, 'cbFaction', null, null ], // faction [enum]
+ 5 => [FILTER_CR_FLAG, 'npcflag', NPC_FLAG_REPAIRER ], // canrepair
+ 6 => [FILTER_CR_ENUM, 's.areaId', null ], // foundin
+ 7 => [FILTER_CR_CALLBACK, 'cbQuestRelation', 'startsQuests', 0x1 ], // startsquest [enum]
+ 8 => [FILTER_CR_CALLBACK, 'cbQuestRelation', 'endsQuests', 0x2 ], // endsquest [enum]
+ 9 => [FILTER_CR_BOOLEAN, 'lootId', ], // lootable
+ 10 => [FILTER_CR_BOOLEAN, 'cbRegularSkinLoot', NPC_TYPEFLAG_SPECIALLOOT ], // skinnable [yn]
+ 11 => [FILTER_CR_BOOLEAN, 'pickpocketLootId', ], // pickpocketable
+ 12 => [FILTER_CR_CALLBACK, 'cbMoneyDrop', null, null ], // averagemoneydropped [op] [int]
+ 15 => [FILTER_CR_CALLBACK, 'cbSpecialSkinLoot', NPC_TYPEFLAG_HERBLOOT, null ], // gatherable [yn]
+ 16 => [FILTER_CR_CALLBACK, 'cbSpecialSkinLoot', NPC_TYPEFLAG_ENGINEERLOOT, null ], // minable [yn]
+ 18 => [FILTER_CR_FLAG, 'npcflag', NPC_FLAG_AUCTIONEER ], // auctioneer
+ 19 => [FILTER_CR_FLAG, 'npcflag', NPC_FLAG_BANKER ], // banker
+ 20 => [FILTER_CR_FLAG, 'npcflag', NPC_FLAG_BATTLEMASTER ], // battlemaster
+ 21 => [FILTER_CR_FLAG, 'npcflag', NPC_FLAG_FLIGHT_MASTER ], // flightmaster
+ 22 => [FILTER_CR_FLAG, 'npcflag', NPC_FLAG_GUILD_MASTER ], // guildmaster
+ 23 => [FILTER_CR_FLAG, 'npcflag', NPC_FLAG_INNKEEPER ], // innkeeper
+ 24 => [FILTER_CR_FLAG, 'npcflag', NPC_FLAG_CLASS_TRAINER ], // talentunlearner
+ 25 => [FILTER_CR_FLAG, 'npcflag', NPC_FLAG_GUILD_MASTER ], // tabardvendor
+ 27 => [FILTER_CR_FLAG, 'npcflag', NPC_FLAG_STABLE_MASTER ], // stablemaster
+ 28 => [FILTER_CR_FLAG, 'npcflag', NPC_FLAG_TRAINER ], // trainer
+ 29 => [FILTER_CR_FLAG, 'npcflag', NPC_FLAG_VENDOR ], // vendor
+ 31 => [FILTER_CR_FLAG, 'cuFlags', CUSTOM_HAS_SCREENSHOT ], // hasscreenshots
+ 32 => [FILTER_CR_FLAG, 'cuFlags', NPC_CU_INSTANCE_BOSS ], // instanceboss
+ 33 => [FILTER_CR_FLAG, 'cuFlags', CUSTOM_HAS_COMMENT ], // hascomments
+ 34 => [FILTER_CR_NYI_PH, 1, null ], // usemodel [str] - displayId -> id:creatureDisplayInfo.dbc/model -> id:cratureModelData.dbc/modelPath
+ 35 => [FILTER_CR_STRING, 'textureString' ], // useskin
+ 37 => [FILTER_CR_NUMERIC, 'id', NUM_CAST_INT, true ], // id
+ 38 => [FILTER_CR_CALLBACK, 'cbRelEvent', null, null ], // relatedevent [enum]
+ 40 => [FILTER_CR_FLAG, 'cuFlags', CUSTOM_HAS_VIDEO ], // hasvideos
+ 41 => [FILTER_CR_NYI_PH, 1, null ], // haslocation [yn] [staff]
+ 42 => [FILTER_CR_CALLBACK, 'cbReputation', '>', null ], // increasesrepwith [enum]
+ 43 => [FILTER_CR_CALLBACK, 'cbReputation', '<', null ], // decreasesrepwith [enum]
+ 44 => [FILTER_CR_CALLBACK, 'cbSpecialSkinLoot', NPC_TYPEFLAG_MININGLOOT, null ] // salvageable [yn]
+ );
+
+ // fieldId => [checkType, checkValue[, fieldIsArray]]
+ protected $inputFields = array(
+ 'cr' => [FILTER_V_LIST, [[1, 3],[5, 12], 15, 16, [18, 25], [27, 29], [31, 35], 37, 38, [40, 44]], true ], // criteria ids
+ 'crs' => [FILTER_V_LIST, [FILTER_ENUM_NONE, FILTER_ENUM_ANY, [0, 9999]], true ], // criteria operators
+ 'crv' => [FILTER_V_REGEX, '/[\p{C}:;]/ui', true ], // criteria values - only printable chars, no delimiter
+ 'na' => [FILTER_V_REGEX, '/[\p{C};]/ui', false], // name / subname - only printable chars, no delimiter
+ 'ex' => [FILTER_V_EQUAL, 'on', false], // also match subname
+ 'ma' => [FILTER_V_EQUAL, 1, false], // match any / all filter
+ 'fa' => [FILTER_V_CALLBACK, 'cbPetFamily', true ], // pet family [list] - cat[0] == 1
+ 'minle' => [FILTER_V_RANGE, [1, 99], false], // min level [int]
+ 'maxle' => [FILTER_V_RANGE, [1, 99], false], // max level [int]
+ 'cl' => [FILTER_V_RANGE, [0, 4], true ], // classification [list]
+ 'ra' => [FILTER_V_LIST, [-1, 0, 1], false], // react alliance [int]
+ 'rh' => [FILTER_V_LIST, [-1, 0, 1], false] // react horde [int]
+ );
+
+ protected function createSQLForCriterium(&$cr)
+ {
+ if (in_array($cr[0], array_keys($this->genericFilter)))
+ if ($genCr = $this->genericCriterion($cr))
+ return $genCr;
+
+ unset($cr);
+ $this->error = true;
+ return [1];
+ }
+
+ protected function createSQLForValues()
+ {
+ $parts = [];
+ $_v = &$this->fiData['v'];
+
+ // name [str]
+ if (isset($_v['na']))
+ {
+ $_ = [];
+ if (isset($_v['ex']) && $_v['ex'] == 'on')
+ $_ = $this->modularizeString(['name_loc'.User::$localeId, 'subname_loc'.User::$localeId]);
+ else
+ $_ = $this->modularizeString(['name_loc'.User::$localeId]);
+
+ if ($_)
+ $parts[] = $_;
+ }
+
+ // pet family [list]
+ if (isset($_v['fa']))
+ $parts[] = ['family', $_v['fa']];
+
+ // creatureLevel min [int]
+ if (isset($_v['minle']))
+ $parts[] = ['minLevel', $_v['minle'], '>='];
+
+ // creatureLevel max [int]
+ if (isset($_v['maxle']))
+ $parts[] = ['maxLevel', $_v['maxle'], '<='];
+
+ // classification [list]
+ if (isset($_v['cl']))
+ $parts[] = ['rank', $_v['cl']];
+
+ // react Alliance [int]
+ if (isset($_v['ra']))
+ $parts[] = ['ft.A', $_v['ra']];
+
+ // react Horde [int]
+ if (isset($_v['rh']))
+ $parts[] = ['ft.H', $_v['rh']];
+
+ return $parts;
+ }
+
+ protected function cbPetFamily(&$val)
+ {
+ if (!$this->parentCats || $this->parentCats[0] != 1)
+ return false;
+
+ if (!Util::checkNumeric($val, NUM_REQ_INT))
+ return false;
+
+ $type = FILTER_V_LIST;
+ $valid = [[1, 9], 11, 12, 20, 21, [24, 27], [30, 35], [37, 39], [41, 46]];
+
+ return $this->checkInput($type, $valid, $val);
+ }
+
+ protected function cbRelEvent($cr)
+ {
+ if (!Util::checkNumeric($cr[1], NUM_REQ_INT))
+ return false;
+
+ if ($cr[1] == FILTER_ENUM_ANY)
+ {
+ $eventIds = DB::Aowow()->selectCol('SELECT id FROM ?_events WHERE holidayId <> 0');
+ $cGuids = DB::World()->selectCol('SELECT DISTINCT guid FROM game_event_creature WHERE eventEntry IN (?a)', $eventIds);
+ return ['s.guid', $cGuids];
+ }
+ else if ($cr[1] == FILTER_ENUM_NONE)
+ {
+ $eventIds = DB::Aowow()->selectCol('SELECT id FROM ?_events WHERE holidayId <> 0');
+ $cGuids = DB::World()->selectCol('SELECT DISTINCT guid FROM game_event_creature WHERE eventEntry IN (?a)', $eventIds);
+ return ['s.guid', $cGuids, '!'];
+ }
+ else if ($cr[1])
+ {
+ $eventIds = DB::Aowow()->selectCol('SELECT id FROM ?_events WHERE holidayId = ?d', $cr[1]);
+ $cGuids = DB::World()->selectCol('SELECT DISTINCT guid FROM game_event_creature WHERE eventEntry IN (?a)', $eventIds);
+ return ['s.guid', $cGuids];
+ }
+
+ return false;
+ }
+
+ protected function cbMoneyDrop($cr)
+ {
+ if (!Util::checkNumeric($cr[2], NUM_CAST_INT) || !$this->int2Op($cr[1]))
+ return false;
+
+ return ['AND', ['((minGold + maxGold) / 2)', $cr[2], $cr[1]]];
+ }
+
+ protected function cbQuestRelation($cr, $field, $val)
+ {
+ switch ($cr[1])
+ {
+ case 1: // any
+ return ['AND', ['qse.method', $val, '&'], ['qse.questId', null, '!']];
+ case 2: // alliance
+ return ['AND', ['qse.method', $val, '&'], ['qse.questId', null, '!'], [['qt.reqRaceMask', RACE_MASK_HORDE, '&'], 0], ['qt.reqRaceMask', RACE_MASK_ALLIANCE, '&']];
+ case 3: // horde
+ return ['AND', ['qse.method', $val, '&'], ['qse.questId', null, '!'], [['qt.reqRaceMask', RACE_MASK_ALLIANCE, '&'], 0], ['qt.reqRaceMask', RACE_MASK_HORDE, '&']];
+ case 4: // both
+ return ['AND', ['qse.method', $val, '&'], ['qse.questId', null, '!'], ['OR', ['AND', ['qt.reqRaceMask', RACE_MASK_ALLIANCE, '&'], ['qt.reqRaceMask', RACE_MASK_HORDE, '&']], ['qt.reqRaceMask', 0]]];
+ case 5: // none
+ $this->extraOpts['ct']['h'][] = $field.' = 0';
+ return [1];
+ }
+
+ return false;
+ }
+
+ protected function cbHealthMana($cr, $minField, $maxField)
+ {
+ if (!Util::checkNumeric($cr[2], NUM_CAST_INT) || !$this->int2Op($cr[1]))
+ return false;
+
+ // remap OP for this special case
+ switch ($cr[1])
+ {
+ case '=': // min > max is totally possible
+ $this->extraOpts['ct']['h'][] = $minField.' = '.$maxField.' AND '.$minField.' = '.$cr[2];
+ break;
+ case '>':
+ case '>=':
+ case '<':
+ case '<=':
+ $this->extraOpts['ct']['h'][] = 'IF('.$minField.' > '.$maxField.', '.$maxField.', '.$minField.') '.$cr[1].' '.$cr[2];
+ break;
+ }
+
+
+ return [1]; // always true, use post-filter
+ }
+
+ protected function cbSpecialSkinLoot($cr, $typeFlag)
+ {
+ if (!$this->int2Bool($cr[1]))
+ return false;
+
+
+ if ($cr[1])
+ return ['AND', ['skinLootId', 0, '>'], ['typeFlags', $typeFlag, '&']];
+ else
+ return ['OR', ['skinLootId', 0], [['typeFlags', $typeFlag, '&'], 0]];
+ }
+
+ protected function cbRegularSkinLoot($cr, $typeFlag)
+ {
+ if (!$this->int2Bool($cr[1]))
+ return false;
+
+ if ($cr[1])
+ return ['AND', ['skinLootId', 0, '>'], [['typeFlags', $typeFlag, '&'], 0]];
+ else
+ return ['OR', ['skinLootId', 0], ['typeFlags', $typeFlag, '&']];
+ }
+
+ protected function cbReputation($cr, $op)
+ {
+ if (!in_array($cr[1], $this->enums[3])) // reuse
+ return false;
+
+ if ($_ = DB::Aowow()->selectRow('SELECT * FROM ?_factions WHERE id = ?d', $cr[1]))
+ $this->formData['reputationCols'][] = [$cr[1], Util::localizedString($_, 'name')];
+
+ if ($cIds = DB::World()->selectCol('SELECT creature_id FROM creature_onkill_reputation WHERE (RewOnKillRepFaction1 = ?d AND RewOnKillRepValue1 '.$op.' 0) OR (RewOnKillRepFaction2 = ?d AND RewOnKillRepValue2 '.$op.' 0)', $cr[1], $cr[1]))
+ return ['id', $cIds];
+ else
+ return [0];
+ }
+
+ protected function cbFaction($cr)
+ {
+ if (!Util::checkNumeric($cr[1], NUM_REQ_INT))
+ return false;
+
+ if (!in_array($cr[1], $this->enums[$cr[0]]))
+ return false;
+
+
+ $facTpls = [];
+ $facs = new FactionList(array('OR', ['parentFactionId', $cr[1]], ['id', $cr[1]]));
+ foreach ($facs->iterate() as $__)
+ $facTpls = array_merge($facTpls, $facs->getField('templateIds'));
+
+ return $facTpls ? ['faction', $facTpls] : [0];
+ }
+}
+
+?>
diff --git a/includes/types/currency.class.php b/includes/types/currency.class.php
new file mode 100644
index 00000000..f0437552
--- /dev/null
+++ b/includes/types/currency.class.php
@@ -0,0 +1,87 @@
+ [['ic']],
+ 'ic' => ['j' => ['?_icons ic ON ic.id = c.iconId', true], 's' => ', ic.name AS iconString']
+ );
+
+ public function __construct($conditions = [])
+ {
+ parent::__construct($conditions);
+
+ foreach ($this->iterate() as &$_curTpl)
+ if (!$_curTpl['iconString'])
+ $_curTpl['iconString'] = 'inv_misc_questionmark';
+ }
+
+
+ public function getListviewData()
+ {
+ $data = [];
+
+ foreach ($this->iterate() as $__)
+ {
+ $data[$this->id] = array(
+ 'id' => $this->id,
+ 'category' => $this->curTpl['category'],
+ 'name' => $this->getField('name', true),
+ 'icon' => $this->curTpl['iconString']
+ );
+ }
+
+ return $data;
+ }
+
+ public function getJSGlobals($addMask = 0)
+ {
+ $data = [];
+
+ foreach ($this->iterate() as $__)
+ {
+ // todo (low): find out, why i did this in the first place
+ if ($this->id == 104) // in case of honor commit sebbuku
+ $icon = ['inv_bannerpvp_02', 'inv_bannerpvp_01']; // ['alliance', 'horde'];
+ else if ($this->id == 103) // also arena-icon diffs from item-icon
+ $icon = ['money_arena', 'money_arena'];
+ else
+ $icon = [$this->curTpl['iconString'], $this->curTpl['iconString']];
+
+ $data[TYPE_CURRENCY][$this->id] = ['name' => $this->getField('name', true), 'icon' => $icon];
+ }
+
+ return $data;
+ }
+
+ public function renderTooltip()
+ {
+ if (!$this->curTpl)
+ return array();
+
+ $x = '';
+ $x .= ''.Util::jsEscape($this->getField('name', true)).' ';
+
+ // cata+ (or go fill it by hand)
+ if ($_ = $this->getField('description', true))
+ $x .= ''.Util::jsEscape($_).' ';
+
+ if ($_ = $this->getField('cap'))
+ $x .= ' '.Lang::currency('cap').Lang::main('colon').''.Lang::nf($_).' ';
+
+ $x .= ' | ';
+
+ return $x;
+ }
+}
+
+?>
diff --git a/includes/types/emote.class.php b/includes/types/emote.class.php
new file mode 100644
index 00000000..523777d9
--- /dev/null
+++ b/includes/types/emote.class.php
@@ -0,0 +1,58 @@
+iterate() as &$curTpl)
+ {
+ // remap for generic access
+ $curTpl['name'] = $curTpl['cmd'];
+ }
+ }
+
+ public function getListviewData()
+ {
+ $data = [];
+
+ foreach ($this->iterate() as $__)
+ {
+ $data[$this->id] = array(
+ 'id' => $this->curTpl['id'],
+ 'name' => $this->curTpl['cmd'],
+ 'preview' => $this->getField('self', true) ?: ($this->getField('noTarget', true) ?: $this->getField('target', true))
+ );
+
+ // [nyi] sounds
+ }
+
+ return $data;
+ }
+
+ public function getJSGlobals($addMask = GLOBALINFO_ANY)
+ {
+ $data = [];
+
+ foreach ($this->iterate() as $__)
+ $data[TYPE_EMOTE][$this->id] = ['name' => $this->getField('cmd')];
+
+ return $data;
+ }
+
+ public function renderTooltip() { }
+}
+
+?>
diff --git a/includes/types/enchantment.class.php b/includes/types/enchantment.class.php
new file mode 100644
index 00000000..cb245818
--- /dev/null
+++ b/includes/types/enchantment.class.php
@@ -0,0 +1,353 @@
+ TYPE_ENCHANTMENT
+ 'ie' => [['is']],
+ 'is' => ['j' => ['?_item_stats `is` ON `is`.`type` = 502 AND `is`.`typeId` = `ie`.`id`', true], 's' => ', `is`.*'],
+ );
+
+ public function __construct($conditions = [])
+ {
+ parent::__construct($conditions);
+
+ // post processing
+ foreach ($this->iterate() as &$curTpl)
+ {
+ $curTpl['spells'] = []; // [spellId, triggerType, charges, chanceOrPpm]
+ for ($i = 1; $i <=3; $i++)
+ {
+ if ($curTpl['object'.$i] <= 0)
+ continue;
+
+ switch ($curTpl['type'.$i])
+ {
+ case 1:
+ $proc = -$this->getField('ppmRate') ?: ($this->getField('procChance') ?: $this->getField('amount'.$i));
+ $curTpl['spells'][$i] = [$curTpl['object'.$i], 2, $curTpl['charges'], $proc];
+ $this->relSpells[] = $curTpl['object'.$i];
+ break;
+ case 3:
+ $curTpl['spells'][$i] = [$curTpl['object'.$i], 1, $curTpl['charges'], 0];
+ $this->relSpells[] = $curTpl['object'.$i];
+ break;
+ case 7:
+ $curTpl['spells'][$i] = [$curTpl['object'.$i], 0, $curTpl['charges'], 0];
+ $this->relSpells[] = $curTpl['object'.$i];
+ break;
+ }
+ }
+
+ // floats are fetched as string from db :<
+ $curTpl['dmg'] = floatVal($curTpl['dmg']);
+ $curTpl['dps'] = floatVal($curTpl['dps']);
+
+ // remove zero-stats
+ foreach (Game::$itemMods as $str)
+ if ($curTpl[$str] == 0) // empty(0.0f) => true .. yeah, sure
+ unset($curTpl[$str]);
+
+ if ($curTpl['dps'] == 0)
+ unset($curTpl['dps']);
+ }
+
+ if ($this->relSpells)
+ $this->relSpells = new SpellList(array(['id', $this->relSpells]));
+ }
+
+ // use if you JUST need the name
+ public static function getName($id)
+ {
+ $n = DB::Aowow()->SelectRow('SELECT name_loc0, name_loc2, name_loc3, name_loc6, name_loc8 FROM ?_itemenchantment WHERE id = ?d', $id );
+ return Util::localizedString($n, 'name');
+ }
+ // end static use
+
+ public function getListviewData($addInfoMask = 0x0)
+ {
+ $data = [];
+
+ foreach ($this->iterate() as $__)
+ {
+ $data[$this->id] = array(
+ 'id' => $this->id,
+ 'name' => $this->getField('name', true),
+ 'spells' => []
+ );
+
+ if ($this->curTpl['skillLine'] > 0)
+ $data[$this->id]['reqskill'] = $this->curTpl['skillLine'];
+
+ if ($this->curTpl['skillLevel'] > 0)
+ $data[$this->id]['reqskillrank'] = $this->curTpl['skillLevel'];
+
+ if ($this->curTpl['requiredLevel'] > 0)
+ $data[$this->id]['reqlevel'] = $this->curTpl['requiredLevel'];
+
+ foreach ($this->curTpl['spells'] as $s)
+ {
+ // enchant is procing or onUse
+ if ($s[1] == 2 || $s[1] == 0)
+ $data[$this->id]['spells'][$s[0]] = $s[2];
+ // spell is procing
+ else if ($this->relSpells && $this->relSpells->getEntry($s[0]) && ($_ = $this->relSpells->canTriggerSpell()))
+ {
+ foreach ($_ as $idx)
+ {
+ $this->triggerIds[] = $this->relSpells->getField('effect'.$idx.'TriggerSpell');
+ $data[$this->id]['spells'][$this->relSpells->getField('effect'.$idx.'TriggerSpell')] = $s[2];
+ }
+ }
+ }
+
+ if (!$data[$this->id]['spells'])
+ unset($data[$this->id]['spells']);
+
+ Util::arraySumByKey($data[$this->id], $this->getStatGain());
+ }
+
+ return $data;
+ }
+
+ public function getStatGain($addScalingKeys = false)
+ {
+ $data = [];
+
+ foreach (Game::$itemMods as $str)
+ if (isset($this->curTpl[$str]))
+ $data[$str] = $this->curTpl[$str];
+
+ if (isset($this->curTpl['dps']))
+ $data['dps'] = $this->curTpl['dps'];
+
+ // scaling enchantments are saved as 0 to item_stats, thus return empty
+ if ($addScalingKeys)
+ {
+ $spellStats = [];
+ if ($this->relSpells)
+ $spellStats = $this->relSpells->getStatGain();
+
+ for ($h = 1; $h <= 3; $h++)
+ {
+ $obj = (int)$this->curTpl['object'.$h];
+
+ switch ($this->curTpl['type'.$h])
+ {
+ case 3: // TYPE_EQUIP_SPELL Spells from ObjectX (use of amountX?)
+ if (!empty($spellStats[$obj]))
+ foreach ($spellStats[$obj] as $mod => $_)
+ if ($str = Game::$itemMods[$mod])
+ Util::arraySumByKey($data, [$str => 0]);
+
+ $obj = null;
+ break;
+ case 4: // TYPE_RESISTANCE +AmountX resistance for ObjectX School
+ switch ($obj)
+ {
+ case 0: // Physical
+ $obj = ITEM_MOD_ARMOR;
+ break;
+ case 1: // Holy
+ $obj = ITEM_MOD_HOLY_RESISTANCE;
+ break;
+ case 2: // Fire
+ $obj = ITEM_MOD_FIRE_RESISTANCE;
+ break;
+ case 3: // Nature
+ $obj = ITEM_MOD_NATURE_RESISTANCE;
+ break;
+ case 4: // Frost
+ $obj = ITEM_MOD_FROST_RESISTANCE;
+ break;
+ case 5: // Shadow
+ $obj = ITEM_MOD_SHADOW_RESISTANCE;
+ break;
+ case 6: // Arcane
+ $obj = ITEM_MOD_ARCANE_RESISTANCE;
+ break;
+ default:
+ $obj = null;
+ }
+ break;
+ case 5: // TYPE_STAT +AmountX for Statistic by type of ObjectX
+ if ($obj < 2) // [mana, health] are on [0, 1] respectively and are expected on [1, 2] ..
+ $obj++; // 0 is weaponDmg .. ehh .. i messed up somewhere
+
+ break; // stats are directly assigned below
+ default: // TYPE_NONE dnd stuff; skip assignment below
+ $obj = null;
+ }
+
+ if ($obj !== null)
+ if ($str = Game::$itemMods[$obj]) // check if we use these mods
+ Util::arraySumByKey($data, [$str => 0]);
+ }
+ }
+
+ return $data;
+ }
+
+ public function getRelSpell($id)
+ {
+ if ($this->relSpells)
+ return $this->relSpells->getEntry($id);
+
+ return null;
+ }
+
+ public function getJSGlobals($addMask = GLOBALINFO_ANY)
+ {
+ $data = [];
+
+ if ($addMask & GLOBALINFO_SELF)
+ foreach ($this->iterate() as $__)
+ $data[TYPE_ENCHANTMENT][$this->id] = ['name' => $this->getField('name', true)];
+
+ if ($addMask & GLOBALINFO_RELATED)
+ {
+ if ($this->relSpells)
+ $data = $this->relSpells->getJSGlobals(GLOBALINFO_SELF);
+
+ foreach ($this->triggerIds as $tId)
+ if (empty($data[TYPE_SPELL][$tId]))
+ $data[TYPE_SPELL][$tId] = $tId;
+ }
+
+ return $data;
+ }
+
+ public function renderTooltip() { }
+}
+
+
+class EnchantmentListFilter extends Filter
+{
+ protected $enums = array(
+ 3 => array( // requiresprof
+ null, 171, 164, 185, 333, 202, 129, 755, 165, 186, 197, true, false, 356, 182, 773
+ )
+ );
+
+ protected $genericFilter = array( // misc (bool): _NUMERIC => useFloat; _STRING => localized; _FLAG => match Value; _BOOLEAN => stringSet
+ 2 => [FILTER_CR_NUMERIC, 'id', NUM_CAST_INT, true], // id
+ 3 => [FILTER_CR_ENUM, 'skillLine' ], // requiresprof
+ 4 => [FILTER_CR_NUMERIC, 'skillLevel', NUM_CAST_INT ], // reqskillrank
+ 5 => [FILTER_CR_BOOLEAN, 'conditionId' ], // hascondition
+ 10 => [FILTER_CR_FLAG, 'cuFlags', CUSTOM_HAS_COMMENT ], // hascomments
+ 11 => [FILTER_CR_FLAG, 'cuFlags', CUSTOM_HAS_SCREENSHOT ], // hasscreenshots
+ 12 => [FILTER_CR_FLAG, 'cuFlags', CUSTOM_HAS_VIDEO ], // hasvideos
+ 20 => [FILTER_CR_NUMERIC, 'is.str', NUM_CAST_INT, true], // str
+ 21 => [FILTER_CR_NUMERIC, 'is.agi', NUM_CAST_INT, true], // agi
+ 22 => [FILTER_CR_NUMERIC, 'is.sta', NUM_CAST_INT, true], // sta
+ 23 => [FILTER_CR_NUMERIC, 'is.int', NUM_CAST_INT, true], // int
+ 24 => [FILTER_CR_NUMERIC, 'is.spi', NUM_CAST_INT, true], // spi
+ 25 => [FILTER_CR_NUMERIC, 'is.arcres', NUM_CAST_INT, true], // arcres
+ 26 => [FILTER_CR_NUMERIC, 'is.firres', NUM_CAST_INT, true], // firres
+ 27 => [FILTER_CR_NUMERIC, 'is.natres', NUM_CAST_INT, true], // natres
+ 28 => [FILTER_CR_NUMERIC, 'is.frores', NUM_CAST_INT, true], // frores
+ 29 => [FILTER_CR_NUMERIC, 'is.shares', NUM_CAST_INT, true], // shares
+ 30 => [FILTER_CR_NUMERIC, 'is.holres', NUM_CAST_INT, true], // holres
+ 32 => [FILTER_CR_NUMERIC, 'is.dps', NUM_CAST_FLOAT, true], // dps
+ 34 => [FILTER_CR_NUMERIC, 'is.dmg', NUM_CAST_FLOAT, true], // dmg
+ 37 => [FILTER_CR_NUMERIC, 'is.mleatkpwr', NUM_CAST_INT, true], // mleatkpwr
+ 38 => [FILTER_CR_NUMERIC, 'is.rgdatkpwr', NUM_CAST_INT, true], // rgdatkpwr
+ 39 => [FILTER_CR_NUMERIC, 'is.rgdhitrtng', NUM_CAST_INT, true], // rgdhitrtng
+ 40 => [FILTER_CR_NUMERIC, 'is.rgdcritstrkrtng', NUM_CAST_INT, true], // rgdcritstrkrtng
+ 41 => [FILTER_CR_NUMERIC, 'is.armor' , NUM_CAST_INT, true], // armor
+ 42 => [FILTER_CR_NUMERIC, 'is.defrtng', NUM_CAST_INT, true], // defrtng
+ 43 => [FILTER_CR_NUMERIC, 'is.block', NUM_CAST_INT, true], // block
+ 44 => [FILTER_CR_NUMERIC, 'is.blockrtng', NUM_CAST_INT, true], // blockrtng
+ 45 => [FILTER_CR_NUMERIC, 'is.dodgertng', NUM_CAST_INT, true], // dodgertng
+ 46 => [FILTER_CR_NUMERIC, 'is.parryrtng', NUM_CAST_INT, true], // parryrtng
+ 48 => [FILTER_CR_NUMERIC, 'is.splhitrtng', NUM_CAST_INT, true], // splhitrtng
+ 49 => [FILTER_CR_NUMERIC, 'is.splcritstrkrtng', NUM_CAST_INT, true], // splcritstrkrtng
+ 50 => [FILTER_CR_NUMERIC, 'is.splheal', NUM_CAST_INT, true], // splheal
+ 51 => [FILTER_CR_NUMERIC, 'is.spldmg', NUM_CAST_INT, true], // spldmg
+ 52 => [FILTER_CR_NUMERIC, 'is.arcsplpwr', NUM_CAST_INT, true], // arcsplpwr
+ 53 => [FILTER_CR_NUMERIC, 'is.firsplpwr', NUM_CAST_INT, true], // firsplpwr
+ 54 => [FILTER_CR_NUMERIC, 'is.frosplpwr', NUM_CAST_INT, true], // frosplpwr
+ 55 => [FILTER_CR_NUMERIC, 'is.holsplpwr', NUM_CAST_INT, true], // holsplpwr
+ 56 => [FILTER_CR_NUMERIC, 'is.natsplpwr', NUM_CAST_INT, true], // natsplpwr
+ 57 => [FILTER_CR_NUMERIC, 'is.shasplpwr', NUM_CAST_INT, true], // shasplpwr
+ 60 => [FILTER_CR_NUMERIC, 'is.healthrgn', NUM_CAST_INT, true], // healthrgn
+ 61 => [FILTER_CR_NUMERIC, 'is.manargn', NUM_CAST_INT, true], // manargn
+ 77 => [FILTER_CR_NUMERIC, 'is.atkpwr', NUM_CAST_INT, true], // atkpwr
+ 78 => [FILTER_CR_NUMERIC, 'is.mlehastertng', NUM_CAST_INT, true], // mlehastertng
+ 79 => [FILTER_CR_NUMERIC, 'is.resirtng', NUM_CAST_INT, true], // resirtng
+ 84 => [FILTER_CR_NUMERIC, 'is.mlecritstrkrtng', NUM_CAST_INT, true], // mlecritstrkrtng
+ 94 => [FILTER_CR_NUMERIC, 'is.splpen', NUM_CAST_INT, true], // splpen
+ 95 => [FILTER_CR_NUMERIC, 'is.mlehitrtng', NUM_CAST_INT, true], // mlehitrtng
+ 96 => [FILTER_CR_NUMERIC, 'is.critstrkrtng', NUM_CAST_INT, true], // critstrkrtng
+ 97 => [FILTER_CR_NUMERIC, 'is.feratkpwr', NUM_CAST_INT, true], // feratkpwr
+ 101 => [FILTER_CR_NUMERIC, 'is.rgdhastertng', NUM_CAST_INT, true], // rgdhastertng
+ 102 => [FILTER_CR_NUMERIC, 'is.splhastertng', NUM_CAST_INT, true], // splhastertng
+ 103 => [FILTER_CR_NUMERIC, 'is.hastertng', NUM_CAST_INT, true], // hastertng
+ 114 => [FILTER_CR_NUMERIC, 'is.armorpenrtng', NUM_CAST_INT, true], // armorpenrtng
+ 115 => [FILTER_CR_NUMERIC, 'is.health', NUM_CAST_INT, true], // health
+ 116 => [FILTER_CR_NUMERIC, 'is.mana', NUM_CAST_INT, true], // mana
+ 117 => [FILTER_CR_NUMERIC, 'is.exprtng', NUM_CAST_INT, true], // exprtng
+ 119 => [FILTER_CR_NUMERIC, 'is.hitrtng', NUM_CAST_INT, true], // hitrtng
+ 123 => [FILTER_CR_NUMERIC, 'is.splpwr', NUM_CAST_INT, true] // splpwr
+ );
+
+ // fieldId => [checkType, checkValue[, fieldIsArray]]
+ protected $inputFields = array(
+ 'cr' => [FILTER_V_RANGE, [2, 123], true ], // criteria ids
+ 'crs' => [FILTER_V_RANGE, [1, 15], true ], // criteria operators
+ 'crv' => [FILTER_V_RANGE, [0, 99999], true ], // criteria values - only numerals
+ 'na' => [FILTER_V_REGEX, '/[\p{C};]/ui', false], // name - only printable chars, no delimiter
+ 'ma' => [FILTER_V_EQUAL, 1, false], // match any / all filter
+ 'ty' => [FILTER_V_RANGE, [1, 8], true ] // types
+ );
+
+ protected function createSQLForCriterium(&$cr)
+ {
+ if (in_array($cr[0], array_keys($this->genericFilter)))
+ if ($genCr = $this->genericCriterion($cr))
+ return $genCr;
+
+ unset($cr);
+ $this->error = true;
+ return [1];
+ }
+
+ protected function createSQLForValues()
+ {
+ $parts = [];
+ $_v = &$this->fiData['v'];
+
+ //string
+ if (isset($_v['na']))
+ if ($_ = $this->modularizeString(['name_loc'.User::$localeId]))
+ $parts[] = $_;
+
+ // type
+ if (isset($_v['ty']))
+ {
+ $_ = (array)$_v['ty'];
+ if (!array_diff($_, [1, 2, 3, 4, 5, 6, 7, 8]))
+ $parts[] = ['OR', ['type1', $_], ['type2', $_], ['type3', $_]];
+ else
+ unset($_v['ty']);
+ }
+
+ return $parts;
+ }
+}
+
+?>
diff --git a/includes/dbtypes/faction.class.php b/includes/types/faction.class.php
similarity index 57%
rename from includes/dbtypes/faction.class.php
rename to includes/types/faction.class.php
index cf76280d..f327e95c 100644
--- a/includes/dbtypes/faction.class.php
+++ b/includes/types/faction.class.php
@@ -1,27 +1,25 @@
[['f2']],
- 'f2' => ['j' => ['::factions f2 ON f.`parentFactionId` = f2.`id`', true], 's' => ', IFNULL(f2.`parentFactionId`, 0) AS "cat2"'],
- 'ft' => ['j' => '::factiontemplate ft ON ft.`factionId` = f.`id`']
+ 'f2' => ['j' => ['?_factions f2 ON f.parentFactionId = f2.id', true], 's' => ', IFNULL(f2.parentFactionId, 0) AS cat2'],
+ 'ft' => ['j' => '?_factiontemplate ft ON ft.factionId = f.id']
);
- public function __construct(array $conditions = [], array $miscData = [])
+ public function __construct($conditions = [])
{
- parent::__construct($conditions, $miscData);
+ parent::__construct($conditions);
if ($this->error)
return;
@@ -37,7 +35,13 @@ class FactionList extends DBTypeList
}
}
- public function getListviewData() : array
+ public static function getName($id)
+ {
+ $n = DB::Aowow()->SelectRow('SELECT name_loc0, name_loc2, name_loc3, name_loc6, name_loc8 FROM ?_factions WHERE id = ?d', $id);
+ return Util::localizedString($n, 'name');
+ }
+
+ public function getListviewData()
{
$data = [];
@@ -66,17 +70,17 @@ class FactionList extends DBTypeList
return $data;
}
- public function getJSGlobals(int $addMask = 0) : array
+ public function getJSGlobals($addMask = 0)
{
$data = [];
foreach ($this->iterate() as $__)
- $data[Type::FACTION][$this->id] = ['name' => $this->getField('name', true)];
+ $data[TYPE_FACTION][$this->id] = ['name' => $this->getField('name', true)];
return $data;
}
- public function renderTooltip() : ?string { return null; }
+ public function renderTooltip() { }
}
diff --git a/includes/types/gameobject.class.php b/includes/types/gameobject.class.php
new file mode 100644
index 00000000..7838ac21
--- /dev/null
+++ b/includes/types/gameobject.class.php
@@ -0,0 +1,247 @@
+ [['ft', 'qse']],
+ 'ft' => ['j' => ['?_factiontemplate ft ON ft.id = o.faction', true], 's' => ', ft.factionId, ft.A, ft.H'],
+ 'qse' => ['j' => ['?_quests_startend qse ON qse.type = 2 AND qse.typeId = o.id', true], 's' => ', IF(min(qse.method) = 1 OR max(qse.method) = 3, 1, 0) AS startsQuests, IF(min(qse.method) = 2 OR max(qse.method) = 3, 1, 0) AS endsQuests', 'g' => 'o.id'],
+ 'qt' => ['j' => '?_quests qt ON qse.questId = qt.id'],
+ 's' => ['j' => '?_spawns s ON s.type = 2 AND s.typeId = o.id']
+ );
+
+ public function __construct($conditions = [], $miscData = null)
+ {
+ parent::__construct($conditions, $miscData);
+
+ if ($this->error)
+ return;
+
+ // post processing
+ foreach ($this->iterate() as $_id => &$curTpl)
+ {
+ // unpack miscInfo
+ $curTpl['lootStack'] = [];
+ $curTpl['spells'] = [];
+
+ if (in_array($curTpl['type'], [OBJECT_GOOBER, OBJECT_RITUAL, OBJECT_SPELLCASTER, OBJECT_FLAGSTAND, OBJECT_FLAGDROP, OBJECT_AURA_GENERATOR, OBJECT_TRAP]))
+ $curTpl['spells'] = array_combine(['onUse', 'onSuccess', 'aura', 'triggered'], [$curTpl['onUseSpell'], $curTpl['onSuccessSpell'], $curTpl['auraSpell'], $curTpl['triggeredSpell']]);
+
+ if (!$curTpl['miscInfo'])
+ continue;
+
+ switch ($curTpl['type'])
+ {
+ case OBJECT_CHEST:
+ case OBJECT_FISHINGHOLE:
+ $curTpl['lootStack'] = explode(' ', $curTpl['miscInfo']);
+ break;
+ case OBJECT_CAPTURE_POINT:
+ $curTpl['capture'] = explode(' ', $curTpl['miscInfo']);
+ break;
+ case OBJECT_MEETINGSTONE:
+ $curTpl['mStone'] = explode(' ', $curTpl['miscInfo']);
+ break;
+ }
+ }
+ }
+
+ public static function getName($id)
+ {
+ $n = DB::Aowow()->SelectRow('SELECT name_loc0, name_loc2, name_loc3, name_loc6, name_loc8 FROM ?_objects WHERE id = ?d', $id);
+ return Util::localizedString($n, 'name');
+ }
+
+ public function getListviewData()
+ {
+ $data = [];
+ foreach ($this->iterate() as $__)
+ {
+ $data[$this->id] = array(
+ 'id' => $this->id,
+ 'name' => $this->getField('name', true),
+ 'type' => $this->curTpl['typeCat'],
+ 'location' => $this->getSpawns(SPAWNINFO_ZONES)
+ );
+
+ if (!empty($this->curTpl['reqSkill']))
+ $data[$this->id]['skill'] = $this->curTpl['reqSkill'];
+
+ if ($this->curTpl['startsQuests'])
+ $data[$this->id]['hasQuests'] = 1;
+
+ }
+
+ return $data;
+ }
+
+ public function renderTooltip($interactive = false)
+ {
+ if (!$this->curTpl)
+ return array();
+
+ $x = '';
+ $x .= '| '.$this->getField('name', true).' | ';
+ if ($this->curTpl['typeCat'])
+ if ($_ = Lang::gameObject('type', $this->curTpl['typeCat']))
+ $x .= '| '.$_.' | ';
+
+ if (isset($this->curTpl['lockId']))
+ if ($locks = Lang::getLocks($this->curTpl['lockId']))
+ foreach ($locks as $l)
+ $x .= '| '.$l.' | ';
+
+ $x .= ' ';
+
+ return $x;
+ }
+
+ public function getJSGlobals($addMask = 0)
+ {
+ $data = [];
+
+ foreach ($this->iterate() as $__)
+ $data[TYPE_OBJECT][$this->id] = ['name' => $this->getField('name', true)];
+
+ return $data;
+ }
+
+ public function getSourceData()
+ {
+ $data = [];
+
+ foreach ($this->iterate() as $__)
+ {
+ $data[$this->id] = array(
+ 'n' => $this->getField('name', true),
+ 't' => TYPE_OBJECT,
+ 'ti' => $this->id
+ // 'bd' => bossdrop
+ // 'dd' => dungeondifficulty
+ );
+ }
+
+ return $data;
+ }
+}
+
+
+class GameObjectListFilter extends Filter
+{
+ public $extraOpts = [];
+
+ protected $genericFilter = array(
+ 1 => [FILTER_CR_ENUM, 's.areaId', null ], // foundin
+ 2 => [FILTER_CR_CALLBACK, 'cbQuestRelation', 'startsQuests', 0x1 ], // startsquest [side]
+ 3 => [FILTER_CR_CALLBACK, 'cbQuestRelation', 'endsQuests', 0x2 ], // endsquest [side]
+ 4 => [FILTER_CR_CALLBACK, 'cbOpenable', null, null], // openable [yn]
+ 5 => [FILTER_CR_NYI_PH, null, null ], // averagemoneycontained [op] [int] - GOs don't contain money, match against 0
+ 7 => [FILTER_CR_NUMERIC, 'reqSkill', NUM_CAST_INT ], // requiredskilllevel
+ 11 => [FILTER_CR_FLAG, 'cuFlags', CUSTOM_HAS_SCREENSHOT ], // hasscreenshots
+ 13 => [FILTER_CR_FLAG, 'cuFlags', CUSTOM_HAS_COMMENT ], // hascomments
+ 15 => [FILTER_CR_NUMERIC, 'id', NUM_CAST_INT ], // id
+ 16 => [FILTER_CR_CALLBACK, 'cbRelEvent', null, null], // relatedevent (ignore removed by event)
+ 18 => [FILTER_CR_FLAG, 'cuFlags', CUSTOM_HAS_VIDEO ] // hasvideos
+ );
+
+ // fieldId => [checkType, checkValue[, fieldIsArray]]
+ protected $inputFields = array(
+ 'cr' => [FILTER_V_LIST, [[1, 5], 7, 11, 13, 15, 16, 18], true ], // criteria ids
+ 'crs' => [FILTER_V_LIST, [FILTER_ENUM_NONE, FILTER_ENUM_ANY, [0, 5000]], true ], // criteria operators
+ 'crv' => [FILTER_V_RANGE, [0, 99999], true ], // criteria values - only numeric input values expected
+ 'na' => [FILTER_V_REGEX, '/[\p{C};]/ui', false], // name - only printable chars, no delimiter
+ 'ma' => [FILTER_V_EQUAL, 1, false] // match any / all filter
+ );
+
+ protected function createSQLForCriterium(&$cr)
+ {
+ if (in_array($cr[0], array_keys($this->genericFilter)))
+ if ($genCR = $this->genericCriterion($cr))
+ return $genCR;
+
+ unset($cr);
+ $this->error = true;
+ return [1];
+ }
+
+ protected function createSQLForValues()
+ {
+ $parts = [];
+ $_v = $this->fiData['v'];
+
+ // name
+ if (isset($_v['na']))
+ if ($_ = $this->modularizeString(['name_loc'.User::$localeId]))
+ $parts[] = $_;
+
+ return $parts;
+ }
+
+ protected function cbOpenable($cr)
+ {
+ if ($this->int2Bool($cr[1]))
+ return $cr[1] ? ['OR', ['flags', 0x2, '&'], ['type', 3]] : ['AND', [['flags', 0x2, '&'], 0], ['type', 3, '!']];
+
+ return false;
+ }
+
+ protected function cbQuestRelation($cr, $field, $value)
+ {
+ switch ($cr[1])
+ {
+ case 1: // any
+ return ['AND', ['qse.method', $value, '&'], ['qse.questId', null, '!']];
+ case 2: // alliance only
+ return ['AND', ['qse.method', $value, '&'], ['qse.questId', null, '!'], [['qt.reqRaceMask', RACE_MASK_HORDE, '&'], 0], ['qt.reqRaceMask', RACE_MASK_ALLIANCE, '&']];
+ case 3: // horde only
+ return ['AND', ['qse.method', $value, '&'], ['qse.questId', null, '!'], [['qt.reqRaceMask', RACE_MASK_ALLIANCE, '&'], 0], ['qt.reqRaceMask', RACE_MASK_HORDE, '&']];
+ case 4: // both
+ return ['AND', ['qse.method', $value, '&'], ['qse.questId', null, '!'], ['OR', ['AND', ['qt.reqRaceMask', RACE_MASK_ALLIANCE, '&'], ['qt.reqRaceMask', RACE_MASK_HORDE, '&']], ['qt.reqRaceMask', 0]]];
+ case 5: // none todo (low): broken, if entry starts and ends quests...
+ $this->extraOpts['o']['h'][] = $field.' = 0';
+ return [1];
+ }
+
+ return false;
+ }
+
+ protected function cbRelEvent($cr)
+ {
+ if (!Util::checkNumeric($cr[1], NUM_REQ_INT))
+ return false;;
+
+ if ($cr[1] == FILTER_ENUM_ANY)
+ {
+ $eventIds = DB::Aowow()->selectCol('SELECT id FROM ?_events WHERE holidayId <> 0');
+ $goGuids = DB::World()->selectCol('SELECT DISTINCT guid FROM game_event_gameobject WHERE eventEntry IN (?a)', $eventIds);
+ return ['s.guid', $goGuids];
+ }
+ else if ($cr[1] == FILTER_ENUM_NONE)
+ {
+ $eventIds = DB::Aowow()->selectCol('SELECT id FROM ?_events WHERE holidayId <> 0');
+ $goGuids = DB::World()->selectCol('SELECT DISTINCT guid FROM game_event_gameobject WHERE eventEntry IN (?a)', $eventIds);
+ return ['s.guid', $goGuids, '!'];
+ }
+ else if ($cr[1])
+ {
+ $eventIds = DB::Aowow()->selectCol('SELECT id FROM ?_events WHERE holidayId = ?d', $cr[1]);
+ $goGuids = DB::World()->selectCol('SELECT DISTINCT guid FROM game_event_gameobject WHERE eventEntry IN (?a)', $eventIds);
+ return ['s.guid', $goGuids];
+ }
+
+ return false;
+ }
+}
+
+?>
diff --git a/includes/dbtypes/guild.class.php b/includes/types/guild.class.php
similarity index 50%
rename from includes/dbtypes/guild.class.php
rename to includes/types/guild.class.php
index e31ea4cc..c6136261 100644
--- a/includes/dbtypes/guild.class.php
+++ b/includes/types/guild.class.php
@@ -1,18 +1,14 @@
getGuildScores();
@@ -20,23 +16,23 @@ class GuildList extends DBTypeList
foreach ($this->iterate() as $__)
{
$data[$this->id] = array(
- 'name' => '$"'.str_replace ('"', '', $this->curTpl['name']).'"', // MUST be a string, omit any quotes in name
+ 'name' => "$'".$this->curTpl['name']."'", // MUST be a string
'members' => $this->curTpl['members'],
'faction' => $this->curTpl['faction'],
'achievementpoints' => $this->getField('achievementpoints'),
'gearscore' => $this->getField('gearscore'),
- 'realm' => Profiler::urlize($this->curTpl['realmName'], true),
+ 'realm' => Profiler::urlize($this->curTpl['realmName']),
'realmname' => $this->curTpl['realmName'],
- // 'battlegroup' => Profiler::urlize($this->curTpl['battlegroup']), // was renamed to subregion somewhere around cata release
+ // 'battlegroup' => Profiler::urlize($this->curTpl['battlegroup']), // was renamed to subregion somewhere around cata release
// 'battlegroupname' => $this->curTpl['battlegroup'],
'region' => Profiler::urlize($this->curTpl['region'])
);
}
- return $data;
+ return array_values($data);
}
- private function getGuildScores() : void
+ private function getGuildScores()
{
/*
Guild gear scores and achievement points are derived using a weighted average of all of the known characters in that guild.
@@ -48,16 +44,18 @@ class GuildList extends DBTypeList
if (!$guilds)
return;
- $stats = DB::Aowow()->selectAssoc('SELECT `guild` AS ARRAY_KEY, `id` AS ARRAY_KEY2, `level`, `gearscore`, `achievementpoints` FROM ::profiler_profiles WHERE `guild` IN %in AND `stub` = 0 ORDER BY `gearscore` DESC', $guilds);
+ $stats = DB::Aowow()->select('SELECT guild AS ARRAY_KEY, id AS ARRAY_KEY2, level, gearscore, achievementpoints, IF(cuFlags & ?d, 0, 1) AS synced FROM ?_profiler_profiles WHERE guild IN (?a) ORDER BY gearscore DESC', PROFILER_CU_NEEDS_RESYNC, $guilds);
foreach ($this->iterate() as &$_curTpl)
{
$id = $_curTpl['id'];
if (empty($stats[$id]))
continue;
- $guildStats = $stats[$id];
+ $guildStats = array_filter($stats[$id], function ($x) { return $x['synced']; } );
+ if (!$guildStats)
+ continue;
- $nMaxLevel = count(array_filter($stats[$id], fn($x) => $x['level'] >= MAX_LEVEL));
+ $nMaxLevel = count(array_filter($stats[$id], function ($x) { return $x['level'] >= MAX_LEVEL; } ));
$levelMod = 1.0;
if ($nMaxLevel < 25)
@@ -80,69 +78,96 @@ class GuildList extends DBTypeList
}
}
- public static function getName(int $id) : ?LocString { return null; }
-
- public function renderTooltip() : ?string { return null; }
- public function getJSGlobals(int $addMask = 0) : array { return []; }
+ public function renderTooltip() {}
+ public function getJSGlobals($addMask = 0) {}
}
class GuildListFilter extends Filter
{
- use TrProfilerFilter;
+ public $extraOpts = [];
+ protected $genericFilter = [];
- protected string $type = 'guilds';
- protected static array $genericFilter = [];
- protected static array $inputFields = array(
- 'ex' => [parent::V_EQUAL, 'on', false], // only match exact - must be defined before 'na' as it's test relies on 'ex's value
- 'na' => [parent::V_NAME, true, false], // name - only printable chars, no delimiter
- 'ma' => [parent::V_EQUAL, 1, false], // match any / all filter
- 'si' => [parent::V_LIST, [SIDE_ALLIANCE, SIDE_HORDE], false], // side
- 'rg' => [parent::V_CALLBACK, 'cbRegionCheck', false], // region
- 'bg' => [parent::V_EQUAL, null, false], // battlegroup - unsued here, but var expected by template
- 'sv' => [parent::V_CALLBACK, 'cbServerCheck', false] // server
+ // fieldId => [checkType, checkValue[, fieldIsArray]]
+ protected $inputFields = array(
+ 'na' => [FILTER_V_REGEX, '/[\p{C};]/ui', false], // name - only printable chars, no delimiter
+ 'ma' => [FILTER_V_EQUAL, 1, false], // match any / all filter
+ 'ex' => [FILTER_V_EQUAL, 'on', false], // only match exact
+ 'si' => [FILTER_V_LIST, [1, 2], false], // side
+ 'rg' => [FILTER_V_CALLBACK, 'cbRegionCheck', false], // region
+ 'sv' => [FILTER_V_CALLBACK, 'cbServerCheck', false], // server
);
- public array $extraOpts = [];
+ protected function createSQLForCriterium(&$cr) { }
- protected function createSQLForValues() : array
+ protected function createSQLForValues()
{
$parts = [];
- $_v = $this->values;
+ $_v = $this->fiData['v'];
// region (rg), battlegroup (bg) and server (sv) are passed to GuildList as miscData and handled there
// name [str]
- if ($_v['na'])
- if ($_ = $this->buildLikeLookup([['na', 'g.name']], $_v['ex'] == 'on'))
+ if (!empty($_v['na']))
+ if ($_ = $this->modularizeString(['g.name'], $_v['na'], !empty($_v['ex']) && $_v['ex'] == 'on'))
$parts[] = $_;
// side [list]
- if ($_v['si'] == SIDE_ALLIANCE)
- $parts[] = ['c.race', ChrRace::fromMask(ChrRace::MASK_ALLIANCE)];
- else if ($_v['si'] == SIDE_HORDE)
- $parts[] = ['c.race', ChrRace::fromMask(ChrRace::MASK_HORDE)];
+ if (!empty($_v['si']))
+ {
+ if ($_v['si'] == 1)
+ $parts[] = ['c.race', [1, 3, 4, 7, 11]];
+ else if ($_v['si'] == 2)
+ $parts[] = ['c.race', [2, 5, 6, 8, 10]];
+ }
return $parts;
}
+
+ protected function cbRegionCheck(&$v)
+ {
+ if ($v == 'eu' || $v == 'us')
+ {
+ $this->parentCats[0] = $v; // directly redirect onto this region
+ $v = ''; // remove from filter
+
+ return true;
+ }
+
+ return false;
+ }
+
+ protected function cbServerCheck(&$v)
+ {
+ foreach (Profiler::getRealms() as $realm)
+ if ($realm['name'] == $v)
+ {
+ $this->parentCats[1] = Profiler::urlize($v);// directly redirect onto this server
+ $v = ''; // remove from filter
+
+ return true;
+ }
+
+ return false;
+ }
}
class RemoteGuildList extends GuildList
{
- protected string $queryBase = 'SELECT `g`.*, `g`.`guildid` AS ARRAY_KEY FROM guild g';
- protected array $queryOpts = array(
+ protected $queryBase = 'SELECT `g`.*, `g`.`guildid` AS ARRAY_KEY FROM guild g';
+ protected $queryOpts = array(
'g' => [['gm', 'c'], 'g' => 'ARRAY_KEY'],
- 'gm' => ['j' => 'guild_member gm ON gm.`guildid` = g.`guildid`', 's' => ', COUNT(1) AS "members"'],
- 'c' => ['j' => 'characters c ON c.`guid` = gm.`guid`', 's' => ', BIT_OR(IF(c.`race` IN (1, 3, 4, 7, 11), 1, 2)) - 1 AS "faction"']
+ 'gm' => ['j' => 'guild_member gm ON gm.guildid = g.guildid', 's' => ', COUNT(1) AS members'],
+ 'c' => ['j' => 'characters c ON c.guid = gm.guid', 's' => ', BIT_OR(IF(c.race IN (1, 3, 4, 7, 11), 1, 2)) - 1 AS faction']
);
- public function __construct(array $conditions = [], array $miscData = [])
+ public function __construct($conditions = [], $miscData = null)
{
// select DB by realm
if (!$this->selectRealms($miscData))
{
- trigger_error('RemoteGuildList::__construct - cannot access any realm.', E_USER_WARNING);
+ trigger_error('no access to auth-db or table realmlist is empty', E_USER_WARNING);
return;
}
@@ -152,14 +177,15 @@ class RemoteGuildList extends GuildList
return;
reset($this->dbNames); // only use when querying single realm
- $realms = Profiler::getRealms();
- $distrib = [];
+ $realmId = key($this->dbNames);
+ $realms = Profiler::getRealms();
+ $distrib = [];
// post processing
foreach ($this->iterate() as $guid => &$curTpl)
{
// battlegroup
- $curTpl['battlegroup'] = Cfg::get('BATTLEGROUP');
+ $curTpl['battlegroup'] = CFG_BATTLEGROUP;
$r = explode(':', $guid)[0];
if (!empty($realms[$r]))
@@ -170,15 +196,7 @@ class RemoteGuildList extends GuildList
}
else
{
- trigger_error('guild #'.$guid.' belongs to nonexistent realm #'.$r, E_USER_WARNING);
- unset($this->templates[$guid]);
- continue;
- }
-
- // empty name
- if (!$curTpl['name'])
- {
- trigger_error('guild #'.$guid.' on realm #'.$r.' has empty name.', E_USER_WARNING);
+ trigger_error('character "'.$curTpl['name'].'" belongs to nonexistant realm #'.$r, E_USER_WARNING);
unset($this->templates[$guid]);
continue;
}
@@ -190,14 +208,10 @@ class RemoteGuildList extends GuildList
$distrib[$curTpl['realm']]++;
}
- // equalize subject distribution across realms
- $limit = 0;
+ $limit = CFG_SQL_LIMIT_DEFAULT;
foreach ($conditions as $c)
- if (is_numeric($c))
- $limit = max(0, (int)$c);
-
- if (!$limit) // int:0 means unlimited, so skip early
- return;
+ if (is_int($c))
+ $limit = $c;
$total = array_sum($distrib);
foreach ($distrib as &$d)
@@ -216,27 +230,29 @@ class RemoteGuildList extends GuildList
}
}
- public function initializeLocalEntries() : void
+ public function initializeLocalEntries()
{
- if (!$this->templates)
- return;
-
$data = [];
foreach ($this->iterate() as $guid => $__)
{
- $data['realm'][$guid] = $this->getField('realm');
- $data['realmGUID'][$guid] = $this->getField('guildid');
- $data['name'][$guid] = $this->getField('name');
- $data['nameUrl'][$guid] = Profiler::urlize($this->getField('name'));
- $data['stub'][$guid] = 1;
+ $data[$guid] = array(
+ 'realm' => $this->getField('realm'),
+ 'realmGUID' => $this->getField('guildid'),
+ 'name' => $this->getField('name'),
+ 'nameUrl' => Profiler::urlize($this->getField('name')),
+ 'cuFlags' => PROFILER_CU_NEEDS_RESYNC
+ );
}
// basic guild data
- DB::Aowow()->qry('INSERT INTO ::profiler_guild %m ON DUPLICATE KEY UPDATE `id` = `id`', $data);
+ foreach (Util::createSqlBatchInsert($data) as $ins)
+ DB::Aowow()->query('INSERT IGNORE INTO ?_profiler_guild (?#) VALUES '.$ins, array_keys(reset($data)));
// merge back local ids
- $localIds = DB::Aowow()->selectCol('SELECT CONCAT(`realm`, ":", `realmGUID`) AS ARRAY_KEY, `id` FROM ::profiler_guild WHERE `realm` IN %in AND `realmGUID` IN %in',
- $data['realm'], $data['realmGUID']
+ $localIds = DB::Aowow()->selectCol(
+ 'SELECT CONCAT(realm, ":", realmGUID) AS ARRAY_KEY, id FROM ?_profiler_guild WHERE realm IN (?a) AND realmGUID IN (?a)',
+ array_column($data, 'realm'),
+ array_column($data, 'realmGUID')
);
foreach ($this->iterate() as $guid => &$_curTpl)
@@ -248,38 +264,17 @@ class RemoteGuildList extends GuildList
class LocalGuildList extends GuildList
{
- protected string $queryBase = 'SELECT g.*, g.`id` AS ARRAY_KEY FROM ::profiler_guild g';
+ protected $queryBase = 'SELECT g.*, g.id AS ARRAY_KEY FROM ?_profiler_guild g';
- public function __construct(array $conditions = [], array $miscData = [])
+ public function __construct($conditions = [], $miscData = null)
{
- $realms = Profiler::getRealms();
-
- // graft realm selection from miscData onto conditions
- if (isset($miscData['sv']))
- $realms = array_filter($realms, fn($x) => Profiler::urlize($x['name']) == Profiler::urlize($miscData['sv']));
-
- if (isset($miscData['rg']))
- $realms = array_filter($realms, fn($x) => $x['region'] == $miscData['rg']);
-
- if (!$realms)
- {
- trigger_error('LocalGuildList::__construct - cannot access any realm.', E_USER_WARNING);
- return;
- }
-
- if ($conditions)
- {
- array_unshift($conditions, DB::AND);
- $conditions = [DB::AND, ['realm', array_keys($realms)], $conditions];
- }
- else
- $conditions = [['realm', array_keys($realms)]];
-
parent::__construct($conditions, $miscData);
if ($this->error)
return;
+ $realms = Profiler::getRealms();
+
foreach ($this->iterate() as $id => &$curTpl)
{
if ($curTpl['realm'] && !isset($realms[$curTpl['realm']]))
@@ -292,17 +287,17 @@ class LocalGuildList extends GuildList
}
// battlegroup
- $curTpl['battlegroup'] = Cfg::get('BATTLEGROUP');
+ $curTpl['battlegroup'] = CFG_BATTLEGROUP;
}
}
- public function getProfileUrl() : string
+ public function getProfileUrl()
{
$url = '?guild=';
return $url.implode('.', array(
- $this->getField('region'),
- Profiler::urlize($this->getField('realmName'), true),
+ Profiler::urlize($this->getField('region')),
+ Profiler::urlize($this->getField('realmName')),
Profiler::urlize($this->getField('name'))
));
}
diff --git a/includes/types/icon.class.php b/includes/types/icon.class.php
new file mode 100644
index 00000000..94ce25c0
--- /dev/null
+++ b/includes/types/icon.class.php
@@ -0,0 +1,237 @@
+ '?_items',
+ 'nSpells' => '?_spell',
+ 'nAchievements' => '?_achievement',
+ 'nCurrencies' => '?_currencies',
+ 'nPets' => '?_pet'
+ );
+
+ protected $queryBase = 'SELECT ic.*, ic.id AS ARRAY_KEY FROM ?_icons ic';
+ /* this works, but takes ~100x more time than i'm comfortable with .. kept as reference
+ protected $queryOpts = array( // 29 => TYPE_ICON
+ 'ic' => [['s', 'i', 'a', 'c', 'p'], 'g' => 'ic.id'],
+ 'i' => ['j' => ['?_items `i` ON `i`.`iconId` = `ic`.`id`', true], 's' => ', COUNT(DISTINCT `i`.`id`) AS nItems'],
+ 's' => ['j' => ['?_spell `s` ON `s`.`iconId` = `ic`.`id`', true], 's' => ', COUNT(DISTINCT `s`.`id`) AS nSpells'],
+ 'a' => ['j' => ['?_achievement `a` ON `a`.`iconId` = `ic`.`id`', true], 's' => ', COUNT(DISTINCT `a`.`id`) AS nAchievements'],
+ 'c' => ['j' => ['?_currencies `c` ON `c`.`iconId` = `ic`.`id`', true], 's' => ', COUNT(DISTINCT `c`.`id`) AS nCurrencies'],
+ 'p' => ['j' => ['?_pet `p` ON `p`.`iconId` = `ic`.`id`', true], 's' => ', COUNT(DISTINCT `p`.`id`) AS nPets']
+ );
+ */
+
+ public function __construct($conditions)
+ {
+ parent::__construct($conditions);
+
+ if (!$this->getFoundIDs())
+ return;
+
+ foreach ($this->pseudoJoin as $var => $tbl)
+ {
+ $res = DB::Aowow()->selectCol($this->pseudoQry, $tbl, $this->getFoundIDs());
+ foreach ($res as $icon => $qty)
+ $this->templates[$icon][$var] = $qty;
+ }
+ }
+
+
+ // use if you JUST need the name
+ public static function getName($id)
+ {
+ $n = DB::Aowow()->SelectRow('SELECT name FROM ?_icons WHERE id = ?d', $id );
+ return Util::localizedString($n, 'name');
+ }
+ // end static use
+
+ public function getListviewData($addInfoMask = 0x0)
+ {
+ $data = [];
+
+ foreach ($this->iterate() as $__)
+ {
+ $data[$this->id] = array(
+ 'id' => $this->id,
+ 'name' => $this->getField('name', true, true),
+ 'icon' => $this->getField('name', true, true),
+ 'itemcount' => (int)$this->getField('nItems'),
+ 'spellcount' => (int)$this->getField('nSpells'),
+ 'achievementcount' => (int)$this->getField('nAchievements'),
+ 'npccount' => 0, // UNUSED
+ 'petabilitycount' => 0, // UNUSED
+ 'currencycount' => (int)$this->getField('nCurrencies'),
+ 'missionabilitycount' => 0, // UNUSED
+ 'buildingcount' => 0, // UNUSED
+ 'petcount' => (int)$this->getField('nPets'),
+ 'threatcount' => 0, // UNUSED
+ 'classcount' => 0 // class icons are hardcoded and not referenced in dbc
+ );
+ }
+
+ return $data;
+ }
+
+ public function getJSGlobals($addMask = GLOBALINFO_ANY)
+ {
+ $data = [];
+
+ foreach ($this->iterate() as $__)
+ $data[TYPE_ICON][$this->id] = ['name' => $this->getField('name', true, true), 'icon' => $this->getField('name', true, true)];
+
+ return $data;
+ }
+
+ public function renderTooltip() { }
+}
+
+
+class IconListFilter extends Filter
+{
+ public $extraOpts = null;
+
+ // cr => [type, field, misc, extraCol]
+ private $criterion2field = array(
+ 1 => '?_items', // items [num]
+ 2 => '?_spell', // spells [num]
+ 3 => '?_achievement', // achievements [num]
+ // 4 => '', // battlepets [num]
+ // 5 => '', // battlepetabilities [num]
+ 6 => '?_currencies', // currencies [num]
+ // 7 => '', // garrisonabilities [num]
+ // 8 => '', // garrisonbuildings [num]
+ 9 => '?_pet', // hunterpets [num]
+ // 10 => '', // garrisonmissionthreats [num]
+ 11 => '', // classes [num]
+ 13 => '' // used [num]
+ );
+ private $totalUses = [];
+
+ protected $genericFilter = array(
+ 1 => [FILTER_CR_CALLBACK, 'cbUseAny' ], // items [num]
+ 2 => [FILTER_CR_CALLBACK, 'cbUseAny' ], // spells [num]
+ 3 => [FILTER_CR_CALLBACK, 'cbUseAny' ], // achievements [num]
+ 6 => [FILTER_CR_CALLBACK, 'cbUseAny' ], // currencies [num]
+ 9 => [FILTER_CR_CALLBACK, 'cbUseAny' ], // hunterpets [num]
+ 11 => [FILTER_CR_NYI_PH, null, null], // classes [num]
+ 13 => [FILTER_CR_CALLBACK, 'cbUseAll' ] // used [num]
+ );
+
+ // fieldId => [checkType, checkValue[, fieldIsArray]]
+ protected $inputFields = array(
+ 'cr' => [FILTER_V_LIST, [1, 2, 3, 6, 9, 11, 13], true ], // criteria ids
+ 'crs' => [FILTER_V_RANGE, [1, 6], true ], // criteria operators
+ 'crv' => [FILTER_V_RANGE, [0, 99999], true ], // criteria values - all criteria are numeric here
+ 'na' => [FILTER_V_REGEX, '/[\p{C};]/ui', false], // name - only printable chars, no delimiter
+ 'ma' => [FILTER_V_EQUAL, 1, false] // match any / all filter
+ );
+
+ private function _getCnd($op, $val, $tbl)
+ {
+ switch ($op)
+ {
+ case '>':
+ case '>=':
+ case '=':
+ $ids = DB::Aowow()->selectCol('SELECT iconId AS ARRAY_KEY, COUNT(*) AS n FROM ?# GROUP BY iconId HAVING n '.$op.' '.$val, $tbl);
+ return $ids ? ['id', array_keys($ids)] : [1];
+ case '<=':
+ if ($val)
+ $op = '>';
+ break;
+ case '<':
+ if ($val)
+ $op = '>=';
+ break;
+ case '!=':
+ if ($val)
+ $op = '=';
+ break;
+ }
+
+ $ids = DB::Aowow()->selectCol('SELECT iconId AS ARRAY_KEY, COUNT(*) AS n FROM ?# GROUP BY iconId HAVING n '.$op.' '.$val, $tbl);
+ return $ids ? ['id', array_keys($ids), '!'] : [1];
+ }
+
+ protected function createSQLForCriterium(&$cr)
+ {
+ if (in_array($cr[0], array_keys($this->genericFilter)))
+ if ($genCr = $this->genericCriterion($cr))
+ return $genCr;
+
+ unset($cr);
+ $this->error = true;
+ return [1];
+ }
+
+ protected function createSQLForValues()
+ {
+ $parts = [];
+ $_v = &$this->fiData['v'];
+
+ //string
+ if (isset($_v['na']))
+ if ($_ = $this->modularizeString(['name']))
+ $parts[] = $_;
+
+ return $parts;
+ }
+
+ protected function cbUseAny($cr, $value)
+ {
+ if (Util::checkNumeric($cr[2], NUM_CAST_INT) && $this->int2Op($cr[1]))
+ return $this->_getCnd($cr[1], $cr[2], $this->criterion2field[$cr[0]]);
+
+ return false;
+ }
+
+ protected function cbUseAll($cr)
+ {
+ if (!Util::checkNumeric($cr[2], NUM_CAST_INT) || !$this->int2Op($cr[1]))
+ return false;
+
+ if (!$this->totalUses)
+ {
+ foreach ($this->criterion2field as $tbl)
+ {
+ if (!$tbl)
+ continue;
+
+ $res = DB::Aowow()->selectCol('SELECT iconId AS ARRAY_KEY, COUNT(*) AS n FROM ?# GROUP BY iconId', $tbl);
+ Util::arraySumByKey($this->totalUses, $res);
+ }
+ }
+
+ if ($cr[1] == '=')
+ $cr[1] = '==';
+
+ $op = $cr[1];
+ if ($cr[1] == '<=' && $cr[2])
+ $op = '>';
+ else if ($cr[1] == '<' && $cr[2])
+ $op = '>=';
+ else if ($cr[1] == '!=' && $cr[2])
+ $op = '==';
+ $ids = array_filter($this->totalUses, function ($x) use ($op, $cr) { return eval('return '.$x.' '.$op.' '.$cr[2].';'); });
+
+ if ($cr[1] != $op)
+ return $ids ? ['id', array_keys($ids), '!'] : [1];
+ else
+ return $ids ? ['id', array_keys($ids)] : ['id', array_keys($this->totalUses), '!'];
+ }
+}
+
+?>
diff --git a/includes/types/item.class.php b/includes/types/item.class.php
new file mode 100644
index 00000000..c1d21903
--- /dev/null
+++ b/includes/types/item.class.php
@@ -0,0 +1,2588 @@
+ TYPE_ITEM
+ 'i' => [['is', 'src', 'ic'], 'o' => 'i.quality DESC, i.itemLevel DESC'],
+ 'ic' => ['j' => ['?_icons `ic` ON `ic`.`id` = `i`.`iconId`', true], 's' => ', ic.name AS iconString'],
+ 'is' => ['j' => ['?_item_stats `is` ON `is`.`type` = 3 AND `is`.`typeId` = `i`.`id`', true], 's' => ', `is`.*'],
+ 's' => ['j' => ['?_spell `s` ON `s`.`effect1CreateItemId` = `i`.`id`', true], 'g' => 'i.id'],
+ 'e' => ['j' => ['?_events `e` ON `e`.`id` = `i`.`eventId`', true], 's' => ', e.holidayId'],
+ 'src' => ['j' => ['?_source `src` ON `src`.`type` = 3 AND `src`.`typeId` = `i`.`id`', true], 's' => ', moreType, moreTypeId, src1, src2, src3, src4, src5, src6, src7, src8, src9, src10, src11, src12, src13, src14, src15, src16, src17, src18, src19, src20, src21, src22, src23, src24']
+ );
+
+ public function __construct($conditions = [], $miscData = null)
+ {
+ parent::__construct($conditions, $miscData);
+
+ foreach ($this->iterate() as &$_curTpl)
+ {
+ // item is scaling; overwrite other values
+ if ($_curTpl['scalingStatDistribution'] > 0 && $_curTpl['scalingStatValue'] > 0)
+ $this->initScalingStats();
+
+ $this->initJsonStats();
+
+ // readdress itemset .. is wrong for virtual sets
+ if ($miscData && isset($miscData['pcsToSet']) && isset($miscData['pcsToSet'][$this->id]))
+ $this->json[$this->id]['itemset'] = $miscData['pcsToSet'][$this->id];
+
+ // unify those pesky masks
+ $_ = &$_curTpl['requiredClass'];
+ $_ &= CLASS_MASK_ALL;
+ if ($_ < 0 || $_ == CLASS_MASK_ALL)
+ $_ = 0;
+ unset($_);
+
+ $_ = &$_curTpl['requiredRace'];
+ $_ &= RACE_MASK_ALL;
+ if ($_ < 0 || $_ == RACE_MASK_ALL)
+ $_ = 0;
+ unset($_);
+
+ // sources
+ for ($i = 1; $i < 25; $i++)
+ {
+ if ($_ = $_curTpl['src'.$i])
+ $this->sources[$this->id][$i][] = $_;
+
+ unset($_curTpl['src'.$i]);
+ }
+ }
+ }
+
+ // use if you JUST need the name
+ public static function getName($id)
+ {
+ $n = DB::Aowow()->selectRow('SELECT name_loc0, name_loc2, name_loc3, name_loc6, name_loc8 FROM ?_items WHERE id = ?d', $id);
+ return Util::localizedString($n, 'name');
+ }
+
+ // todo (med): information will get lost if one vendor sells one item multiple times with different costs (e.g. for item 54637)
+ // wowhead seems to have had the same issues
+ public function getExtendedCost($filter = [], &$reqRating = [])
+ {
+ if ($this->error)
+ return [];
+
+ if (empty($this->vendors))
+ {
+ $itemz = DB::World()->select('
+ SELECT nv.item AS ARRAY_KEY1, nv.entry AS ARRAY_KEY2, 0 AS eventId, nv.maxcount, nv.extendedCost FROM npc_vendor nv WHERE {nv.entry IN (?a) AND} nv.item IN (?a)
+ UNION
+ SELECT genv.item AS ARRAY_KEY1, c.id AS ARRAY_KEY2, ge.eventEntry AS eventId, genv.maxcount, genv.extendedCost FROM game_event_npc_vendor genv LEFT JOIN game_event ge ON genv.eventEntry = ge.eventEntry JOIN creature c ON c.guid = genv.guid WHERE {c.id IN (?a) AND} genv.item IN (?a)',
+ empty($filter[TYPE_NPC]) || !is_array($filter[TYPE_NPC]) ? DBSIMPLE_SKIP : $filter[TYPE_NPC],
+ array_keys($this->templates),
+ empty($filter[TYPE_NPC]) || !is_array($filter[TYPE_NPC]) ? DBSIMPLE_SKIP : $filter[TYPE_NPC],
+ array_keys($this->templates)
+ );
+
+ $xCosts = [];
+ foreach ($itemz as $i => $vendors)
+ $xCosts = array_merge($xCosts, array_column($vendors, 'extendedCost'));
+
+ if ($xCosts)
+ $xCosts = DB::Aowow()->select('SELECT *, id AS ARRAY_KEY FROM ?_itemextendedcost WHERE id IN (?a)', $xCosts);
+
+ $cItems = [];
+ foreach ($itemz as $k => $vendors)
+ {
+ foreach ($vendors as $l => $vInfo)
+ {
+ $costs = [];
+ if (!empty($xCosts[$vInfo['extendedCost']]))
+ $costs = $xCosts[$vInfo['extendedCost']];
+
+ $data = array(
+ 'stock' => $vInfo['maxcount'] ?: -1,
+ 'event' => $vInfo['eventId'],
+ 'reqRating' => $costs ? $costs['reqPersonalRating'] : 0,
+ 'reqBracket' => $costs ? $costs['reqArenaSlot'] : 0
+ );
+
+ // hardcode arena(103) & honor(104)
+ if (!empty($costs['reqArenaPoints']))
+ {
+ $data[-103] = $costs['reqArenaPoints'];
+ $this->jsGlobals[TYPE_CURRENCY][103] = 103;
+ }
+
+ if (!empty($costs['reqHonorPoints']))
+ {
+ $data[-104] = $costs['reqHonorPoints'];
+ $this->jsGlobals[TYPE_CURRENCY][104] = 104;
+ }
+
+ for ($i = 1; $i < 6; $i++)
+ {
+ if (!empty($costs['reqItemId'.$i]) && $costs['itemCount'.$i] > 0)
+ {
+ $data[$costs['reqItemId'.$i]] = $costs['itemCount'.$i];
+ $cItems[] = $costs['reqItemId'.$i];
+ }
+ }
+
+ // no extended cost or additional gold required
+ if (!$costs || $this->getField('flagsExtra') & 0x04)
+ if ($_ = $this->getField('buyPrice'))
+ $data[0] = $_;
+
+ $vendors[$l] = $data;
+ }
+
+ $itemz[$k] = $vendors;
+ }
+
+ // convert items to currency if possible
+ if ($cItems)
+ {
+ $moneyItems = new CurrencyList(array(['itemId', $cItems]));
+ foreach ($moneyItems->getJSGlobals() as $type => $jsData)
+ foreach ($jsData as $k => $v)
+ $this->jsGlobals[$type][$k] = $v;
+
+ foreach ($itemz as $id => $vendors)
+ {
+ foreach ($vendors as $l => $costs)
+ {
+ foreach ($costs as $k => $v)
+ {
+ if (in_array($k, $cItems))
+ {
+ $found = false;
+ foreach ($moneyItems->iterate() as $__)
+ {
+ if ($moneyItems->getField('itemId') == $k)
+ {
+ unset($costs[$k]);
+ $costs[-$moneyItems->id] = $v;
+ $found = true;
+ break;
+ }
+ }
+
+ if (!$found)
+ $this->jsGlobals[TYPE_ITEM][$k] = $k;
+ }
+ }
+ $vendors[$l] = $costs;
+ }
+ $itemz[$id] = $vendors;
+ }
+ }
+
+ $this->vendors = $itemz;
+ }
+
+ $result = $this->vendors;
+
+ // apply filter if given
+ $tok = !empty($filter[TYPE_ITEM]) ? $filter[TYPE_ITEM] : null;
+ $cur = !empty($filter[TYPE_CURRENCY]) ? $filter[TYPE_CURRENCY] : null;
+
+ foreach ($result as $itemId => &$data)
+ {
+ $reqRating = [];
+ foreach ($data as $npcId => $costs)
+ {
+ if ($tok || $cur) // bought with specific token or currency
+ {
+ $valid = false;
+ foreach ($costs as $k => $qty)
+ {
+ if ((!$tok || $k == $tok) && (!$cur || $k == -$cur))
+ {
+ $valid = true;
+ break;
+ }
+ }
+
+ if (!$valid)
+ unset($data[$npcId]);
+ }
+
+ // reqRating ins't really a cost .. so pass it by ref instead of return
+ // use highest total value
+ if (isset($data[$npcId]) && $costs['reqRating'] && (!$reqRating || $reqRating[0] < $costs['reqRating']))
+ $reqRating = [$costs['reqRating'], $costs['reqBracket']];
+ }
+
+ if ($reqRating)
+ $data['reqRating'] = $reqRating[0];
+
+ if (empty($data))
+ unset($result[$itemId]);
+ }
+
+ return $result;
+ }
+
+ public function getListviewData($addInfoMask = 0x0, $miscData = null)
+ {
+ /*
+ * ITEMINFO_JSON (0x01): itemMods (including spells) and subitems parsed
+ * ITEMINFO_SUBITEMS (0x02): searched by comparison
+ * ITEMINFO_VENDOR (0x04): costs-obj, when displayed as vendor
+ * ITEMINFO_GEM (0x10): gem infos and score
+ * ITEMINFO_MODEL (0x20): sameModelAs-Tab
+ */
+
+ $data = [];
+
+ // random item is random
+ if ($addInfoMask & ITEMINFO_SUBITEMS)
+ $this->initSubItems();
+
+ if ($addInfoMask & ITEMINFO_JSON)
+ $this->extendJsonStats();
+
+ foreach ($this->iterate() as $__)
+ {
+ foreach ($this->json[$this->id] as $k => $v)
+ $data[$this->id][$k] = $v;
+
+ // json vs listview quirk
+ $data[$this->id]['name'] = $data[$this->id]['quality'].$data[$this->id]['name'];
+ unset($data[$this->id]['quality']);
+
+ if ($addInfoMask & ITEMINFO_JSON)
+ {
+ foreach ($this->itemMods[$this->id] as $k => $v)
+ $data[$this->id][$k] = $v;
+
+ if ($_ = intVal(($this->curTpl['minMoneyLoot'] + $this->curTpl['maxMoneyLoot']) / 2))
+ $data[$this->id]['avgmoney'] = $_;
+
+ if ($_ = $this->curTpl['repairPrice'])
+ $data[$this->id]['repaircost'] = $_;
+ }
+
+ if ($addInfoMask & (ITEMINFO_JSON | ITEMINFO_GEM))
+ if (isset($this->curTpl['score']))
+ $data[$this->id]['score'] = $this->curTpl['score'];
+
+ if ($addInfoMask & ITEMINFO_GEM)
+ {
+ $data[$this->id]['uniqEquip'] = ($this->curTpl['flags'] & ITEM_FLAG_UNIQUEEQUIPPED) ? 1 : 0;
+ $data[$this->id]['socketLevel'] = 0; // not used with wotlk
+ }
+
+ if ($addInfoMask & ITEMINFO_VENDOR)
+ {
+ // just use the first results
+ // todo (med): dont use first result; search for the right one
+ if (!empty($this->getExtendedCost($miscData)[$this->id]))
+ {
+ $cost = reset($this->getExtendedCost($miscData)[$this->id]);
+ $currency = [];
+ $tokens = [];
+
+ foreach ($cost as $k => $qty)
+ {
+ if (is_string($k))
+ continue;
+
+ if ($k > 0)
+ $tokens[] = [$k, $qty];
+ else if ($k < 0)
+ $currency[] = [-$k, $qty];
+ }
+
+ $data[$this->id]['stock'] = $cost['stock']; // display as column in lv
+ $data[$this->id]['avail'] = $cost['stock']; // display as number on icon
+ $data[$this->id]['cost'] = [empty($cost[0]) ? 0 : $cost[0]];
+
+ if ($cost['event'])
+ {
+ $this->jsGlobals[TYPE_WORLDEVENT][$cost['event']] = $cost['event'];
+ $row['condition'][0][$this->id][] = [[CND_ACTIVE_EVENT, $cost['event']]];
+ }
+
+ if ($currency || $tokens) // fill idx:3 if required
+ $data[$this->id]['cost'][] = $currency;
+
+ if ($tokens)
+ $data[$this->id]['cost'][] = $tokens;
+
+ if (!empty($cost['reqRating']))
+ $data[$this->id]['reqarenartng'] = $cost['reqRating'];
+ }
+
+ if ($x = $this->curTpl['buyPrice'])
+ $data[$this->id]['buyprice'] = $x;
+
+ if ($x = $this->curTpl['sellPrice'])
+ $data[$this->id]['sellprice'] = $x;
+
+ if ($x = $this->curTpl['buyCount'])
+ $data[$this->id]['stack'] = $x;
+ }
+
+ if ($this->curTpl['class'] == ITEM_CLASS_GLYPH)
+ $data[$this->id]['glyph'] = $this->curTpl['subSubClass'];
+
+ if ($x = $this->curTpl['requiredSkill'])
+ $data[$this->id]['reqskill'] = $x;
+
+ if ($x = $this->curTpl['requiredSkillRank'])
+ $data[$this->id]['reqskillrank'] = $x;
+
+ if ($x = $this->curTpl['requiredSpell'])
+ $data[$this->id]['reqspell'] = $x;
+
+ if ($x = $this->curTpl['requiredFaction'])
+ $data[$this->id]['reqfaction'] = $x;
+
+ if ($x = $this->curTpl['requiredFactionRank'])
+ {
+ $data[$this->id]['reqrep'] = $x;
+ $data[$this->id]['standing'] = $x; // used in /faction item-listing
+ }
+
+ if ($x = $this->curTpl['slots'])
+ $data[$this->id]['nslots'] = $x;
+
+ $_ = $this->curTpl['requiredRace'];
+ if ($_ && $_ & RACE_MASK_ALLIANCE != RACE_MASK_ALLIANCE && $_ & RACE_MASK_HORDE != RACE_MASK_HORDE)
+ $data[$this->id]['reqrace'] = $_;
+
+ if ($_ = $this->curTpl['requiredClass'])
+ $data[$this->id]['reqclass'] = $_; // $data[$this->id]['classes'] ??
+
+ if ($this->curTpl['flags'] & ITEM_FLAG_HEROIC)
+ $data[$this->id]['heroic'] = true;
+
+ if ($addInfoMask & ITEMINFO_MODEL)
+ if ($_ = $this->getField('displayId'))
+ $data[$this->id]['displayid'] = $_;
+
+ if ($this->getSources($s, $sm) && !($addInfoMask & ITEMINFO_MODEL))
+ {
+ $data[$this->id]['source'] = $s;
+ if ($sm)
+ $data[$this->id]['sourcemore'] = $sm;
+ }
+
+ if (!empty($this->curTpl['cooldown']))
+ $data[$this->id]['cooldown'] = $this->curTpl['cooldown'] / 1000;
+ }
+
+ /* even more complicated crap
+ modelviewer {type:X, displayid:Y, slot:z} .. not sure, when to set
+ */
+
+ return $data;
+ }
+
+ public function getJSGlobals($addMask = GLOBALINFO_SELF, &$extra = [])
+ {
+ $data = $addMask & GLOBALINFO_RELATED ? $this->jsGlobals : [];
+
+ foreach ($this->iterate() as $id => $__)
+ {
+ if ($addMask & GLOBALINFO_SELF)
+ {
+ $data[TYPE_ITEM][$id] = array(
+ 'name' => $this->getField('name', true),
+ 'quality' => $this->curTpl['quality'],
+ 'icon' => $this->curTpl['iconString']
+ );
+ }
+
+ if ($addMask & GLOBALINFO_EXTRA)
+ {
+ $extra[$id] = array(
+ 'id' => $id,
+ 'tooltip' => $this->renderTooltip(true),
+ 'spells' => new StdClass // placeholder for knownSpells
+ );
+ }
+ }
+
+ return $data;
+ }
+
+ /*
+ enhance (set by comparison tool or formated external links)
+ ench: enchantmentId
+ sock: bool (extraScoket (gloves, belt))
+ gems: array (:-separated itemIds)
+ rand: >0: randomPropId; <0: randomSuffixId
+ interactive (set to place javascript/anchors to manipulate level and ratings or link to filters (static tooltips vs popup tooltip))
+ subOf (tabled layout doesn't work if used as sub-tooltip in other item or spell tooltips; use line-break instead)
+ */
+ public function getField($field, $localized = false, $silent = false, $enhance = [])
+ {
+ $res = parent::getField($field, $localized, $silent);
+
+ if ($field == 'name' && !empty($enhance['r']))
+ if ($this->getRandEnchantForItem($enhance['r']))
+ $res .= ' '.Util::localizedString($this->enhanceR, 'name');
+
+ return $res;
+ }
+
+ public function renderTooltip($interactive = false, $subOf = 0, $enhance = [])
+ {
+ if ($this->error)
+ return;
+
+ $_name = $this->getField('name', true);
+ $_reqLvl = $this->curTpl['requiredLevel'];
+ $_quality = $this->curTpl['quality'];
+ $_flags = $this->curTpl['flags'];
+ $_class = $this->curTpl['class'];
+ $_subClass = $this->curTpl['subClass'];
+ $_slot = $this->curTpl['slot'];
+ $causesScaling = false;
+
+ if (!empty($enhance['r']))
+ {
+ if ($this->getRandEnchantForItem($enhance['r']))
+ {
+ $_name .= ' '.Util::localizedString($this->enhanceR, 'name');
+ $randEnchant = '';
+
+ for ($i = 1; $i < 6; $i++)
+ {
+ if ($this->enhanceR['enchantId'.$i] <= 0)
+ continue;
+
+ $enchant = DB::Aowow()->selectRow('SELECT * FROM ?_itemenchantment WHERE Id = ?d', $this->enhanceR['enchantId'.$i]);
+ if ($this->enhanceR['allocationPct'.$i] > 0)
+ {
+ $amount = intVal($this->enhanceR['allocationPct'.$i] * $this->generateEnchSuffixFactor());
+ $randEnchant .= ''.str_replace('$i', $amount, Util::localizedString($enchant, 'name')).' ';
+ }
+ else
+ $randEnchant .= ''.Util::localizedString($enchant, 'name').' ';
+ }
+ }
+ else
+ unset($enhance['r']);
+ }
+
+ if (isset($enhance['s']) && !in_array($_slot, [INVTYPE_WRISTS, INVTYPE_WAIST, INVTYPE_HANDS]))
+ unset($enhance['s']);
+
+ // IMPORTAT: DO NOT REMOVE THE HTML-COMMENTS! THEY ARE REQUIRED TO UPDATE THE TOOLTIP CLIENTSIDE
+ $x = '';
+
+ // upper table: stats
+ if (!$subOf)
+ $x .= '';
+
+ // name; quality
+ if ($subOf)
+ $x .= ''.$_name.'';
+ else
+ $x .= ''.$_name.'';
+
+ // heroic tag
+ if (($_flags & ITEM_FLAG_HEROIC) && $_quality == ITEM_QUALITY_EPIC)
+ $x .= ' '.Lang::item('heroic').'';
+
+ // requires map (todo: reparse ?_zones for non-conflicting data; generate Link to zone)
+ if ($_ = $this->curTpl['map'])
+ {
+ $map = DB::Aowow()->selectRow('SELECT * FROM ?_zones WHERE mapId = ?d LIMIT 1', $_);
+ $x .= ' '.Util::localizedString($map, 'name').'';
+ }
+
+ // requires area
+ if ($this->curTpl['area'])
+ {
+ $area = DB::Aowow()->selectRow('SELECT * FROM ?_zones WHERE Id=?d LIMIT 1', $this->curTpl['area']);
+ $x .= ' '.Util::localizedString($area, 'name');
+ }
+
+ // conjured
+ if ($_flags & ITEM_FLAG_CONJURED)
+ $x .= ' '.Lang::item('conjured');
+
+ // bonding
+ if ($_flags & ITEM_FLAG_ACCOUNTBOUND)
+ $x .= ' '.Lang::item('bonding', 0);
+ else if ($this->curTpl['bonding'])
+ $x .= ' '.Lang::item('bonding', $this->curTpl['bonding']);
+
+ // unique || unique-equipped || unique-limited
+ if ($this->curTpl['maxCount'] == 1)
+ $x .= ' '.Lang::item('unique', 0);
+ // not for currency tokens
+ else if ($this->curTpl['maxCount'] && $this->curTpl['bagFamily'] != 8192)
+ $x .= ' '.sprintf(Lang::item('unique', 1), $this->curTpl['maxCount']);
+ else if ($_flags & ITEM_FLAG_UNIQUEEQUIPPED)
+ $x .= ' '.Lang::item('uniqueEquipped', 0);
+ else if ($this->curTpl['itemLimitCategory'])
+ {
+ $limit = DB::Aowow()->selectRow("SELECT * FROM ?_itemlimitcategory WHERE id = ?", $this->curTpl['itemLimitCategory']);
+ $x .= ' '.sprintf(Lang::item($limit['isGem'] ? 'uniqueEquipped' : 'unique', 2), Util::localizedString($limit, 'name'), $limit['count']);
+ }
+
+ // max duration
+ if ($dur = $this->curTpl['duration'])
+ {
+ $rt = '';
+ if ($this->curTpl['flagsCustom'] & 0x1)
+ $rt = $interactive ? ' ('.sprintf(Util::$dfnString, 'LANG.tooltip_realduration', Lang::item('realTime')).')' : ' ('.Lang::item('realTime').')';
+
+ $x .= " ".Lang::game('duration').Lang::main('colon').Util::formatTime(abs($dur) * 1000).$rt;
+ }
+
+ // required holiday
+ if ($eId = $this->curTpl['eventId'])
+ if ($hName = DB::Aowow()->selectRow('SELECT h.* FROM ?_holidays h JOIN ?_events e ON e.holidayId = h.id WHERE e.id = ?d', $eId))
+ $x .= ' '.sprintf(Lang::game('requires'), ''.Util::localizedString($hName, 'name').'');
+
+ // item begins a quest
+ if ($this->curTpl['startQuest'])
+ $x .= ' '.Lang::item('startQuest').'';
+
+ // containerType (slotCount)
+ if ($this->curTpl['slots'] > 0)
+ {
+ $fam = $this->curTpl['bagFamily'] ? log($this->curTpl['bagFamily'], 2) + 1 : 0;
+ $x .= ' '.Lang::item('bagSlotString', [$this->curTpl['slots'], Lang::item('bagFamily', $fam)]);
+ }
+
+ if (in_array($_class, [ITEM_CLASS_ARMOR, ITEM_CLASS_WEAPON, ITEM_CLASS_AMMUNITION]))
+ {
+ $x .= '';
+
+ // Class
+ if ($_slot)
+ $x .= '| '.Lang::item('inventoryType', $_slot).' | ';
+
+ // Subclass
+ if ($_class == ITEM_CLASS_ARMOR && $_subClass > 0)
+ $x .= ''.Lang::item('armorSubClass', $_subClass).' | ';
+ else if ($_class == ITEM_CLASS_WEAPON)
+ $x .= ''.Lang::item('weaponSubClass', $_subClass).' | ';
+ else if ($_class == ITEM_CLASS_AMMUNITION)
+ $x .= ''.Lang::item('projectileSubClass', $_subClass).' | ';
+
+ $x .= ' ';
+ }
+ else if ($_slot && $_class != ITEM_CLASS_CONTAINER) // yes, slot can occur on random items and is then also displayed <_< .. excluding Bags >_>
+ $x .= ' '.Lang::item('inventoryType', $_slot).' ';
+ else
+ $x .= ' ';
+
+ // Weapon/Ammunition Stats (not limited to weapons (see item:1700))
+ $speed = $this->curTpl['delay'] / 1000;
+ $sc1 = $this->curTpl['dmgType1'];
+ $sc2 = $this->curTpl['dmgType2'];
+ $dmgmin = $this->curTpl['dmgMin1'] + $this->curTpl['dmgMin2'];
+ $dmgmax = $this->curTpl['dmgMax1'] + $this->curTpl['dmgMax2'];
+ $dps = $speed ? ($dmgmin + $dmgmax) / (2 * $speed) : 0;
+
+ if ($_class == ITEM_CLASS_AMMUNITION && $dmgmin && $dmgmax)
+ {
+ if ($sc1)
+ $x .= sprintf(Lang::item('damage', 'ammo', 1), ($dmgmin + $dmgmax) / 2, Lang::game('sc', $sc1)).' ';
+ else
+ $x .= sprintf(Lang::item('damage', 'ammo', 0), ($dmgmin + $dmgmax) / 2).' ';
+ }
+ else if ($dps)
+ {
+ if ($this->curTpl['dmgMin1'] == $this->curTpl['dmgMax1'])
+ $dmg = sprintf(Lang::item('damage', 'single', $sc1 ? 1 : 0), $this->curTpl['dmgMin1'], $sc1 ? Lang::game('sc', $sc1) : null);
+ else
+ $dmg = sprintf(Lang::item('damage', 'range', $sc1 ? 1 : 0), $this->curTpl['dmgMin1'], $this->curTpl['dmgMax1'], $sc1 ? Lang::game('sc', $sc1) : null);
+
+ if ($_class == ITEM_CLASS_WEAPON) // do not use localized format here!
+ $x .= '| '.$dmg.' | '.Lang::item('speed').' '.number_format($speed, 2).' |
|---|
';
+ else
+ $x .= ''.$dmg.' ';
+
+ // secondary damage is set
+ if (($this->curTpl['dmgMin2'] || $this->curTpl['dmgMax2']) && $this->curTpl['dmgMin2'] != $this->curTpl['dmgMax2'])
+ $x .= sprintf(Lang::item('damage', 'range', $sc2 ? 3 : 2), $this->curTpl['dmgMin2'], $this->curTpl['dmgMax2'], $sc2 ? Lang::game('sc', $sc2) : null).' ';
+ else if ($this->curTpl['dmgMin2'])
+ $x .= sprintf(Lang::item('damage', 'single', $sc2 ? 3 : 2), $this->curTpl['dmgMin2'], $sc2 ? Lang::game('sc', $sc2) : null).' ';
+
+ if ($_class == ITEM_CLASS_WEAPON)
+ $x .= ''.sprintf(Lang::item('dps'), $dps).' '; // do not use localized format here!
+
+ // display FeralAttackPower if set
+ if ($fap = $this->getFeralAP())
+ $x .= '('.$fap.' '.Lang::item('fap').') ';
+ }
+
+ // Armor
+ if ($_class == ITEM_CLASS_ARMOR && $this->curTpl['armorDamageModifier'] > 0)
+ {
+ $spanI = 'class="q2"';
+ if ($interactive)
+ $spanI = 'class="q2 tip" onmouseover="$WH.Tooltip.showAtCursor(event, $WH.sprintf(LANG.tooltip_armorbonus, '.$this->curTpl['armorDamageModifier'].'), 0, 0, \'q\')" onmousemove="$WH.Tooltip.cursorUpdate(event)" onmouseout="$WH.Tooltip.hide()"';
+
+ $x .= ''.Lang::item('armor', [intVal($this->curTpl['armor'] - $this->curTpl['armorDamageModifier'])]).' ';
+ }
+ else if (($this->curTpl['armor'] - $this->curTpl['armorDamageModifier']) > 0)
+ $x .= ''.Lang::item('armor', [intVal($this->curTpl['armor'] - $this->curTpl['armorDamageModifier'])]).' ';
+
+ // Block (note: block value from field block and from field stats or parsed from itemSpells are displayed independently)
+ if ($this->curTpl['tplBlock'])
+ $x .= ''.sprintf(Lang::item('block'), $this->curTpl['tplBlock']).' ';
+
+ // Item is a gem (don't mix with sockets)
+ if ($geId = $this->curTpl['gemEnchantmentId'])
+ {
+ $gemEnch = DB::Aowow()->selectRow('SELECT * FROM ?_itemenchantment WHERE id = ?d', $geId);
+ $x .= ''.Util::localizedString($gemEnch, 'name').' ';
+
+ // activation conditions for meta gems
+ if (!empty($gemEnch['conditionId']))
+ {
+ if ($gemCnd = DB::Aowow()->selectRow('SELECT * FROM ?_itemenchantmentcondition WHERE id = ?d', $gemEnch['conditionId']))
+ {
+ for ($i = 1; $i < 6; $i++)
+ {
+ if (!$gemCnd['color'.$i])
+ continue;
+
+ $vspfArgs = [];
+ switch ($gemCnd['comparator'.$i])
+ {
+ case 2: // requires less than ( || ) gems
+ case 5: // requires at least than ( || ) gems
+ $vspfArgs = [$gemCnd['value'.$i], Lang::item('gemColors', $gemCnd['color'.$i] - 1)];
+ break;
+ case 3: // requires more than ( || ) gems
+ $vspfArgs = [Lang::item('gemColors', $gemCnd['color'.$i] - 1), Lang::item('gemColors', $gemCnd['cmpColor'.$i] - 1)];
+ break;
+ default:
+ continue;
+ }
+
+ $x .= ''.Lang::achievement('reqNumCrt').' '.Lang::item('gemConditions', $gemCnd['comparator'.$i], $vspfArgs).' ';
+ }
+ }
+ }
+ }
+
+ // Random Enchantment - if random enchantment is set, prepend stats from it
+ if ($this->curTpl['randomEnchant'] && empty($enhance['r']))
+ $x .= ''.Lang::item('randEnchant').' ';
+ else if (!empty($enhance['r']))
+ $x .= $randEnchant;
+
+ // itemMods (display stats and save ratings for later use)
+ for ($j = 1; $j <= 10; $j++)
+ {
+ $type = $this->curTpl['statType'.$j];
+ $qty = $this->curTpl['statValue'.$j];
+
+ if (!$qty || $type <= 0)
+ continue;
+
+ // base stat
+ switch ($type)
+ {
+ case ITEM_MOD_MANA:
+ case ITEM_MOD_HEALTH:
+ // $type += 1; // i think i fucked up somewhere mapping item_mods: offsets may be required somewhere
+ case ITEM_MOD_AGILITY:
+ case ITEM_MOD_STRENGTH:
+ case ITEM_MOD_INTELLECT:
+ case ITEM_MOD_SPIRIT:
+ case ITEM_MOD_STAMINA:
+ $x .= ''.($qty > 0 ? '+' : '-').abs($qty).' '.Lang::item('statType', $type).' ';
+ break;
+ default: // rating with % for reqLevel
+ $green[] = $this->parseRating($type, $qty, $interactive, $causesScaling);
+
+ }
+ }
+
+ // magic resistances
+ foreach (Game::$resistanceFields as $j => $rowName)
+ if ($rowName && $this->curTpl[$rowName] != 0)
+ $x .= '+'.$this->curTpl[$rowName].' '.Lang::game('resistances', $j).' ';
+
+ // Enchantment
+ if (isset($enhance['e']))
+ {
+ if ($enchText = DB::Aowow()->selectRow('SELECT * FROM ?_itemenchantment WHERE Id = ?', $enhance['e']))
+ $x .= ''.Util::localizedString($enchText, 'name').' ';
+ else
+ {
+ unset($enhance['e']);
+ $x .= '';
+ }
+ }
+ else // enchantment placeholder
+ $x .= '';
+
+ // Sockets w/ Gems
+ if (!empty($enhance['g']))
+ {
+ $gems = DB::Aowow()->select('
+ SELECT it.id AS ARRAY_KEY, ic.name AS iconString, ae.*, it.gemColorMask AS colorMask
+ FROM ?_items it
+ JOIN ?_itemenchantment ae ON ae.id = it.gemEnchantmentId
+ JOIN ?_icons ic ON ic.id = it.iconId
+ WHERE it.id IN (?a)',
+ $enhance['g']);
+ foreach ($enhance['g'] as $k => $v)
+ if ($v && !in_array($v, array_keys($gems))) // 0 is valid
+ unset($enhance['g'][$k]);
+ }
+ else
+ $enhance['g'] = [];
+
+ // zero fill empty sockets
+ $sockCount = isset($enhance['s']) ? 1 : 0;
+ if (!empty($this->json[$this->id]['nsockets']))
+ $sockCount += $this->json[$this->id]['nsockets'];
+
+ while ($sockCount > count($enhance['g']))
+ $enhance['g'][] = 0;
+
+ $enhance['g'] = array_reverse($enhance['g']);
+
+ $hasMatch = 1;
+ // fill native sockets
+ for ($j = 1; $j <= 3; $j++)
+ {
+ if (!$this->curTpl['socketColor'.$j])
+ continue;
+
+ for ($i = 0; $i < 4; $i++)
+ if (($this->curTpl['socketColor'.$j] & (1 << $i)))
+ $colorId = $i;
+
+ $pop = array_pop($enhance['g']);
+ $col = $pop ? 1 : 0;
+ $hasMatch &= $pop ? (($gems[$pop]['colorMask'] & (1 << $colorId)) ? 1 : 0) : 0;
+ $icon = $pop ? sprintf(Util::$bgImagePath['tiny'], STATIC_URL, strtolower($gems[$pop]['iconString'])) : null;
+ $text = $pop ? Util::localizedString($gems[$pop], 'name') : Lang::item('socket', $colorId);
+
+ if ($interactive)
+ $x .= ''.$text.' ';
+ else
+ $x .= ''.$text.' ';
+ }
+
+ // fill extra socket
+ if (isset($enhance['s']))
+ {
+ $pop = array_pop($enhance['g']);
+ $col = $pop ? 1 : 0;
+ $icon = $pop ? sprintf(Util::$bgImagePath['tiny'], STATIC_URL, strtolower($gems[$pop]['iconString'])) : null;
+ $text = $pop ? Util::localizedString($gems[$pop], 'name') : Lang::item('socket', -1);
+
+ if ($interactive)
+ $x .= ''.$text.' ';
+ else
+ $x .= ''.$text.' ';
+ }
+ else // prismatic socket placeholder
+ $x .= '';
+
+ if ($_ = $this->curTpl['socketBonus'])
+ {
+ $sbonus = DB::Aowow()->selectRow('SELECT * FROM ?_itemenchantment WHERE Id = ?d', $_);
+ $x .= ''.Lang::item('socketBonus').Lang::main('colon').''.Util::localizedString($sbonus, 'name').' ';
+ }
+
+ // durability
+ if ($dur = $this->curTpl['durability'])
+ $x .= sprintf(Lang::item('durability'), $dur, $dur).' ';
+
+ // required classes
+ if ($classes = Lang::getClassString($this->curTpl['requiredClass'], $jsg, $__))
+ {
+ foreach ($jsg as $js)
+ if (empty($this->jsGlobals[TYPE_CLASS][$js]))
+ $this->jsGlobals[TYPE_CLASS][$js] = $js;
+
+ $x .= Lang::game('classes').Lang::main('colon').$classes.' ';
+ }
+
+ // required races
+ if ($races = Lang::getRaceString($this->curTpl['requiredRace'], $__, $jsg, $__))
+ {
+ foreach ($jsg as $js)
+ if (empty($this->jsGlobals[TYPE_RACE][$js]))
+ $this->jsGlobals[TYPE_RACE][$js] = $js;
+
+ if ($races != Lang::game('ra', 0)) // not "both", but display combinations like: troll, dwarf
+ $x .= Lang::game('races').Lang::main('colon').$races.' ';
+ }
+
+ // required honorRank (not used anymore)
+ if ($rhr = $this->curTpl['requiredHonorRank'])
+ $x .= sprintf(Lang::game('requires'), Lang::game('pvpRank', $rhr)).' ';
+
+ // required CityRank..?
+ // what the f..
+
+ // required level
+ if (($_flags & ITEM_FLAG_ACCOUNTBOUND) && $_quality == ITEM_QUALITY_HEIRLOOM)
+ $x .= sprintf(Lang::item('reqLevelRange'), 1, MAX_LEVEL, ($interactive ? sprintf(Util::$changeLevelString, MAX_LEVEL) : ''.MAX_LEVEL)).' ';
+ else if ($_reqLvl > 1)
+ $x .= sprintf(Lang::item('reqMinLevel'), $_reqLvl).' ';
+
+ // required arena team rating / personal rating / todo (low): sort out what kind of rating
+ if (!empty($this->getExtendedCost([], $reqRating)[$this->id]) && $reqRating)
+ $x .= sprintf(Lang::item('reqRating', $reqRating[1]), $reqRating[0]).' ';
+
+ // item level
+ if (in_array($_class, [ITEM_CLASS_ARMOR, ITEM_CLASS_WEAPON]))
+ $x .= sprintf(Lang::item('itemLevel'), $this->curTpl['itemLevel']).' ';
+
+ // required skill
+ if ($reqSkill = $this->curTpl['requiredSkill'])
+ {
+ $_ = ''.SkillList::getName($reqSkill).'';
+ if ($this->curTpl['requiredSkillRank'] > 0)
+ $_ .= ' ('.$this->curTpl['requiredSkillRank'].')';
+
+ $x .= sprintf(Lang::game('requires'), $_).' ';
+ }
+
+ // required spell
+ if ($reqSpell = $this->curTpl['requiredSpell'])
+ $x .= Lang::game('requires2').' '.SpellList::getName($reqSpell).' ';
+
+ // required reputation w/ faction
+ if ($reqFac = $this->curTpl['requiredFaction'])
+ $x .= sprintf(Lang::game('requires'), ''.FactionList::getName($reqFac).' - '.Lang::game('rep', $this->curTpl['requiredFactionRank'])).' ';
+
+ // locked or openable
+ if ($locks = Lang::getLocks($this->curTpl['lockId'], true))
+ $x .= ''.Lang::item('locked').' '.implode(' ', $locks).' ';
+ else if ($this->curTpl['flags'] & ITEM_FLAG_OPENABLE)
+ $x .= ''.Lang::item('openClick').' ';
+
+ // upper table: done
+ if (!$subOf)
+ $x .= ' | ';
+
+ // spells on item
+ if (!$this->canTeachSpell())
+ {
+ $itemSpellsAndTrigger = [];
+ for ($j = 1; $j <= 5; $j++)
+ {
+ if ($this->curTpl['spellId'.$j] > 0)
+ {
+ $cd = $this->curTpl['spellCooldown'.$j];
+ if ($cd < $this->curTpl['spellCategoryCooldown'.$j])
+ $cd = $this->curTpl['spellCategoryCooldown'.$j];
+
+ $cd = $cd < 5000 ? null : ' ('.sprintf(Lang::game('cooldown'), Util::formatTime($cd)).')';
+
+ $itemSpellsAndTrigger[$this->curTpl['spellId'.$j]] = [$this->curTpl['spellTrigger'.$j], $cd];
+ }
+ }
+
+ if ($itemSpellsAndTrigger)
+ {
+ $cooldown = '';
+
+ $itemSpells = new SpellList(array(['s.id', array_keys($itemSpellsAndTrigger)]));
+ foreach ($itemSpells->iterate() as $__)
+ if ($parsed = $itemSpells->parseText('description', $_reqLvl > 1 ? $_reqLvl : MAX_LEVEL, false, $causesScaling)[0])
+ {
+ if ($interactive)
+ {
+ $link = '%s';
+ $parsed = preg_replace_callback('/([^;]*)( .*?<\/small>)([^&]*)/i', function($m) use($link) {
+ $m[1] = $m[1] ? sprintf($link, $m[1]) : '';
+ $m[3] = $m[3] ? sprintf($link, $m[3]) : '';
+ return $m[1].$m[2].$m[3];
+ }, $parsed, -1, $nMatches
+ );
+
+ if (!$nMatches)
+ $parsed = sprintf($link, $parsed);
+ }
+
+ $green[] = Lang::item('trigger', $itemSpellsAndTrigger[$itemSpells->id][0]).$parsed.$itemSpellsAndTrigger[$itemSpells->id][1];
+ }
+ }
+ }
+
+ // lower table (ratings, spells, ect)
+ if (!$subOf)
+ $x .= '';
+
+ if (isset($green))
+ foreach ($green as $j => $bonus)
+ if ($bonus)
+ $x .= ''.$bonus.' ';
+
+ // Item Set
+ $pieces = [];
+ if ($setId = $this->getField('itemset'))
+ {
+ // while Ids can technically be used multiple times the only difference in data are the items used. So it doesn't matter what we get
+ $itemset = new ItemsetList(array(['id', $setId]));
+ if (!$itemset->error && $itemset->pieceToSet)
+ {
+ $pieces = DB::Aowow()->select('
+ SELECT b.id AS ARRAY_KEY, b.name_loc0, b.name_loc2, b.name_loc3, b.name_loc6, b.name_loc8, GROUP_CONCAT(a.id SEPARATOR \':\') AS equiv
+ FROM ?_items a, ?_items b
+ WHERE a.slotBak = b.slotBak AND a.itemset = b.itemset AND b.id IN (?a)
+ GROUP BY b.id;',
+ array_keys($itemset->pieceToSet)
+ );
+
+ foreach ($pieces as $k => &$p)
+ $p = ''.Util::localizedString($p, 'name').'';
+
+ $xSet = ' '.Lang::item('setName', [''.$itemset->getField('name', true).'', 0, count($pieces)]).'';
+
+ if ($skId = $itemset->getField('skillId')) // bonus requires skill to activate
+ {
+ $xSet .= ' '.sprintf(Lang::game('requires'), ''.SkillList::getName($skId).'');
+
+ if ($_ = $itemset->getField('skillLevel'))
+ $xSet .= ' ('.$_.')';
+
+ $xSet .= ' ';
+ }
+
+ // list pieces
+ $xSet .= ''.implode(' ', $pieces).' ';
+
+ // get bonuses
+ $setSpellsAndIdx = [];
+ for ($j = 1; $j <= 8; $j++)
+ if ($_ = $itemset->getField('spell'.$j))
+ $setSpellsAndIdx[$_] = $j;
+
+ $setSpells = [];
+ if ($setSpellsAndIdx)
+ {
+ $boni = new SpellList(array(['s.id', array_keys($setSpellsAndIdx)]));
+ foreach ($boni->iterate() as $__)
+ {
+ $setSpells[] = array(
+ 'tooltip' => $boni->parseText('description', $_reqLvl > 1 ? $_reqLvl : MAX_LEVEL, false, $causesScaling)[0],
+ 'entry' => $itemset->getField('spell'.$setSpellsAndIdx[$boni->id]),
+ 'bonus' => $itemset->getField('bonus'.$setSpellsAndIdx[$boni->id])
+ );
+ }
+ }
+
+ // sort and list bonuses
+ $xSet .= '';
+ for ($i = 0; $i < count($setSpells); $i++)
+ {
+ for ($j = $i; $j < count($setSpells); $j++)
+ {
+ if ($setSpells[$j]['bonus'] >= $setSpells[$i]['bonus'])
+ continue;
+
+ $tmp = $setSpells[$i];
+ $setSpells[$i] = $setSpells[$j];
+ $setSpells[$j] = $tmp;
+ }
+ $xSet .= ''.Lang::item('setBonus', [$setSpells[$i]['bonus'], ''.$setSpells[$i]['tooltip'].'']).'';
+ if ($i < count($setSpells) - 1)
+ $xSet .= ' ';
+ }
+ $xSet .= '';
+ }
+ }
+
+ // recipes, vanity pets, mounts
+ if ($this->canTeachSpell())
+ {
+ $craftSpell = new SpellList(array(['s.id', intVal($this->curTpl['spellId2'])]));
+ if (!$craftSpell->error)
+ {
+ $xCraft = '';
+ if ($desc = $this->getField('description', true))
+ $x .= ''.Lang::item('trigger', 0).' '.$desc.' ';
+
+ // recipe handling (some stray Techniques have subclass == 0), place at bottom of tooltipp
+ if ($_class == ITEM_CLASS_RECIPE || $this->curTpl['bagFamily'] == 16)
+ {
+ $craftItem = new ItemList(array(['i.id', (int)$craftSpell->curTpl['effect1CreateItemId']]));
+ if (!$craftItem->error)
+ {
+ if ($itemTT = $craftItem->renderTooltip($interactive, $this->id))
+ $xCraft .= ' '.$itemTT.' ';
+
+ $reagentItems = [];
+ for ($i = 1; $i <= 8; $i++)
+ if ($rId = $craftSpell->getField('reagent'.$i))
+ $reagentItems[$rId] = $craftSpell->getField('reagentCount'.$i);
+
+ if (isset($xCraft) && $reagentItems)
+ {
+ $reagents = new ItemList(array(['i.id', array_keys($reagentItems)]));
+ $reqReag = [];
+
+ foreach ($reagents->iterate() as $__)
+ $reqReag[] = ''.$reagents->getField('name', true).' ('.$reagentItems[$reagents->id].')';
+
+ $xCraft .= ' '.Lang::game('requires2').' '.implode(', ', $reqReag).' ';
+ }
+ }
+ }
+ }
+ }
+
+ // misc (no idea, how to organize the better)
+ $xMisc = [];
+
+ // itemset: pieces and boni
+ if (isset($xSet))
+ $xMisc[] = $xSet;
+
+ // funny, yellow text at the bottom, omit if we have a recipe
+ if ($this->curTpl['description_loc0'] && !$this->canTeachSpell())
+ $xMisc[] = '"'.$this->getField('description', true).'"';
+
+ // readable
+ if ($this->curTpl['pageTextId'])
+ $xMisc[] = ''.Lang::item('readClick').'';
+
+ // charges (i guess checking first spell is enough)
+ if ($this->curTpl['spellCharges1'])
+ $xMisc[] = ''.Lang::item('charges', [abs($this->curTpl['spellCharges1'])]).'';
+
+ // list required reagents
+ if (isset($xCraft))
+ $xMisc[] = $xCraft;
+
+ if ($xMisc)
+ $x .= implode(' ', $xMisc);
+
+ if ($sp = $this->curTpl['sellPrice'])
+ $x .= ''.Lang::item('sellPrice').Lang::main('colon').Util::formatMoney($sp).' ';
+
+ if (!$subOf)
+ $x .= ' | ';
+
+ // tooltip scaling
+ if (!isset($xCraft))
+ {
+ $link = [$subOf ? $subOf : $this->id, 1]; // itemId, scaleMinLevel
+ if (isset($this->ssd[$this->id])) // is heirloom
+ {
+ array_push($link,
+ $this->ssd[$this->id]['maxLevel'], // scaleMaxLevel
+ $this->ssd[$this->id]['maxLevel'], // scaleCurLevel
+ $this->curTpl['scalingStatDistribution'], // scaleDist
+ $this->curTpl['scalingStatValue'] // scaleFlags
+ );
+ }
+ else // may still use level dependant ratings
+ {
+ array_push($link,
+ $causesScaling ? MAX_LEVEL : 1, // scaleMaxLevel
+ $_reqLvl > 1 ? $_reqLvl : MAX_LEVEL // scaleCurLevel
+ );
+ }
+ $x .= '';
+ }
+
+ return $x;
+ }
+
+ public function getRandEnchantForItem($randId)
+ {
+ // is it available for this item? .. does it even exist?!
+ if (empty($this->enhanceR))
+ if (DB::World()->selectCell('SELECT 1 FROM item_enchantment_template WHERE entry = ?d AND ench = ?d', abs($this->getField('randomEnchant')), abs($randId)))
+ if ($_ = DB::Aowow()->selectRow('SELECT * FROM ?_itemrandomenchant WHERE Id = ?d', $randId))
+ $this->enhanceR = $_;
+
+ return !empty($this->enhanceR);
+ }
+
+ // from Trinity
+ public function generateEnchSuffixFactor()
+ {
+ $rpp = DB::Aowow()->selectRow('SELECT * FROM ?_itemrandomproppoints WHERE Id = ?', $this->curTpl['itemLevel']);
+ if (!$rpp)
+ return 0;
+
+ switch ($this->curTpl['slot'])
+ {
+ // Items of that type don`t have points
+ case INVTYPE_NON_EQUIP:
+ case INVTYPE_BAG:
+ case INVTYPE_TABARD:
+ case INVTYPE_AMMO:
+ case INVTYPE_QUIVER:
+ case INVTYPE_RELIC:
+ return 0;
+ // Select point coefficient
+ case INVTYPE_HEAD:
+ case INVTYPE_BODY:
+ case INVTYPE_CHEST:
+ case INVTYPE_LEGS:
+ case INVTYPE_2HWEAPON:
+ case INVTYPE_ROBE:
+ $suffixFactor = 1;
+ break;
+ case INVTYPE_SHOULDERS:
+ case INVTYPE_WAIST:
+ case INVTYPE_FEET:
+ case INVTYPE_HANDS:
+ case INVTYPE_TRINKET:
+ $suffixFactor = 2;
+ break;
+ case INVTYPE_NECK:
+ case INVTYPE_WRISTS:
+ case INVTYPE_FINGER:
+ case INVTYPE_SHIELD:
+ case INVTYPE_CLOAK:
+ case INVTYPE_HOLDABLE:
+ $suffixFactor = 3;
+ break;
+ case INVTYPE_WEAPON:
+ case INVTYPE_WEAPONMAINHAND:
+ case INVTYPE_WEAPONOFFHAND:
+ $suffixFactor = 4;
+ break;
+ case INVTYPE_RANGED:
+ case INVTYPE_THROWN:
+ case INVTYPE_RANGEDRIGHT:
+ $suffixFactor = 5;
+ break;
+ default:
+ return 0;
+ }
+
+ // Select rare/epic modifier
+ switch ($this->curTpl['quality'])
+ {
+ case ITEM_QUALITY_UNCOMMON:
+ return $rpp['uncommon'.$suffixFactor] / 10000;
+ case ITEM_QUALITY_RARE:
+ return $rpp['rare'.$suffixFactor] / 10000;
+ case ITEM_QUALITY_EPIC:
+ return $rpp['epic'.$suffixFactor] / 10000;
+ case ITEM_QUALITY_LEGENDARY:
+ case ITEM_QUALITY_ARTIFACT:
+ return 0; // not have random properties
+ default:
+ break;
+ }
+ return 0;
+ }
+
+ public function extendJsonStats()
+ {
+ $enchantments = []; // buffer Ids for lookup id => src; src>0: socketBonus; src<0: gemEnchant
+
+ foreach ($this->iterate() as $__)
+ {
+ $this->itemMods[$this->id] = [];
+
+ foreach (Game::$itemMods as $mod)
+ {
+ if (isset($this->curTpl[$mod]) && ($_ = floatVal($this->curTpl[$mod])))
+ {
+ if (!isset($this->itemMods[$this->id][$mod]))
+ $this->itemMods[$this->id][$mod] = 0;
+
+ $this->itemMods[$this->id][$mod] += $_;
+ }
+ }
+
+ // fetch and add socketbonusstats
+ if (!empty($this->json[$this->id]['socketbonus']))
+ $enchantments[$this->json[$this->id]['socketbonus']][] = $this->id;
+
+ // Item is a gem (don't mix with sockets)
+ if ($geId = $this->curTpl['gemEnchantmentId'])
+ $enchantments[$geId][] = -$this->id;
+ }
+
+ if ($enchantments)
+ {
+ $eStats = DB::Aowow()->select('SELECT *, typeId AS ARRAY_KEY FROM ?_item_stats WHERE `type` = ?d AND typeId IN (?a)', TYPE_ENCHANTMENT, array_keys($enchantments));
+ Util::checkNumeric($eStats);
+
+ // and merge enchantments back
+ foreach ($enchantments as $eId => $items)
+ {
+ if (empty($eStats[$eId]))
+ continue;
+
+ foreach ($items as $item)
+ {
+ if ($item > 0) // apply socketBonus
+ $this->json[$item]['socketbonusstat'] = array_filter($eStats[$eId]);
+ else /* if ($item < 0) */ // apply gemEnchantment
+ Util::arraySumByKey($this->json[-$item][$mod], array_filter($eStats[$eId]));
+ }
+ }
+ }
+
+ foreach ($this->json as $item => $json)
+ foreach ($json as $k => $v)
+ if (!$v && !in_array($k, ['classs', 'subclass', 'quality', 'side', 'gearscore']))
+ unset($this->json[$item][$k]);
+ }
+
+ public function getOnUseStats()
+ {
+ $onUseStats = [];
+
+ // convert Spells
+ $useSpells = [];
+ for ($h = 1; $h <= 5; $h++)
+ {
+ if ($this->curTpl['spellId'.$h] <= 0)
+ continue;
+
+ if ($this->curTpl['class'] != ITEM_CLASS_CONSUMABLE || $this->curTpl['spellTrigger'.$h])
+ continue;
+
+ $useSpells[] = $this->curTpl['spellId'.$h];
+ }
+
+ if ($useSpells)
+ {
+ $eqpSplList = new SpellList(array(['s.id', $useSpells]));
+ foreach ($eqpSplList->getStatGain() as $stat)
+ Util::arraySumByKey($onUseStats, $stat);
+ }
+
+ return $onUseStats;
+ }
+
+ public function getSourceData()
+ {
+ $data = [];
+
+ foreach ($this->iterate() as $__)
+ {
+ $data[$this->id] = array(
+ 'n' => $this->getField('name', true),
+ 't' => TYPE_ITEM,
+ 'ti' => $this->id,
+ 'q' => $this->curTpl['quality'],
+ // 'p' => PvP [NYI]
+ 'icon' => $this->curTpl['iconString']
+ );
+ }
+
+ return $data;
+ }
+
+ private function canTeachSpell()
+ {
+ // 483: learn recipe; 55884: learn mount/pet
+ if (!in_array($this->curTpl['spellId1'], [483, 55884]))
+ return false;
+
+ // needs learnable spell
+ if (!$this->curTpl['spellId2'])
+ return false;
+
+ return true;
+ }
+
+ private function getFeralAP()
+ {
+ // must be weapon
+ if ($this->curTpl['class'] != ITEM_CLASS_WEAPON)
+ return 0;
+
+ // must be 2H weapon (2H-Mace, Polearm, Staff, ..Fishing Pole)
+ if (!in_array($this->curTpl['subClass'], [5, 6, 10, 20]))
+ return 0;
+
+ // thats fucked up..
+ if (!$this->curTpl['delay'])
+ return 0;
+
+ // must have enough damage
+ $dps = ($this->curTpl['dmgMin1'] + $this->curTpl['dmgMin2'] + $this->curTpl['dmgMax1'] + $this->curTpl['dmgMax2']) / (2 * $this->curTpl['delay'] / 1000);
+ if ($dps < 54.8)
+ return 0;
+
+ return round(($dps - 54.8) * 14, 0);
+ }
+
+ public function getSources(&$s, &$sm)
+ {
+ $s = $sm = null;
+ if (empty($this->sources[$this->id]))
+ return false;
+
+ if ($this->sourceMore === null)
+ {
+ $buff = [];
+ $this->sourceMore = [];
+
+ foreach ($this->iterate() as $_curTpl)
+ if ($_curTpl['moreType'] && $_curTpl['moreTypeId'])
+ $buff[$_curTpl['moreType']][] = $_curTpl['moreTypeId'];
+
+ foreach ($buff as $type => $ids)
+ $this->sourceMore[$type] = (new Util::$typeClasses[$type](array(['id', $ids])))->getSourceData();
+ }
+
+ $s = array_keys($this->sources[$this->id]);
+ if ($this->curTpl['moreType'] && $this->curTpl['moreTypeId'] && !empty($this->sourceMore[$this->curTpl['moreType']][$this->curTpl['moreTypeId']]))
+ $sm = [$this->sourceMore[$this->curTpl['moreType']][$this->curTpl['moreTypeId']]];
+ else if (!empty($this->sources[$this->id][3]))
+ $sm = [['p' => $this->sources[$this->id][3][0]]];
+
+ return true;
+ }
+
+ private function parseRating($type, $value, $interactive = false, &$scaling = false)
+ {
+ // clamp level range
+ $ssdLvl = isset($this->ssd[$this->id]) ? $this->ssd[$this->id]['maxLevel'] : 1;
+ $reqLvl = $this->curTpl['requiredLevel'] > 1 ? $this->curTpl['requiredLevel'] : MAX_LEVEL;
+ $level = min(max($reqLvl, $ssdLvl), MAX_LEVEL);
+
+ // unknown rating
+ if (in_array($type, [2, 8, 9, 10, 11]) || $type > ITEM_MOD_BLOCK_VALUE || $type < 0)
+ {
+ if (User::isInGroup(U_GROUP_EMPLOYEE))
+ return sprintf(Lang::item('statType', count(Lang::item('statType')) - 1), $type, $value);
+ else
+ return null;
+ }
+ // level independant Bonus
+ else if (in_array($type, Game::$lvlIndepRating))
+ return Lang::item('trigger', 1).str_replace('%d', ''.$value, Lang::item('statType', $type));
+ // rating-Bonuses
+ else
+ {
+ $scaling = true;
+
+ if ($interactive)
+ $js = ' ('.sprintf(Util::$changeLevelString, Util::setRatingLevel($level, $type, $value)).')';
+ else
+ $js = ' ('.Util::setRatingLevel($level, $type, $value).')';
+
+ return Lang::item('trigger', 1).str_replace('%d', ''.$value.$js, Lang::item('statType', $type));
+ }
+ }
+
+ private function getSSDMod($type)
+ {
+ $mask = $this->curTpl['scalingStatValue'];
+
+ switch ($type)
+ {
+ case 'stats': $mask &= 0x04001F; break;
+ case 'armor': $mask &= 0xF001E0; break;
+ case 'dps' : $mask &= 0x007E00; break;
+ case 'spell': $mask &= 0x008000; break;
+ case 'fap' : $mask &= 0x010000; break; // unused
+ default: $mask &= 0x0;
+ }
+
+ $field = null;
+ for ($i = 0; $i < count(Util::$ssdMaskFields); $i++)
+ if ($mask & (1 << $i))
+ $field = Util::$ssdMaskFields[$i];
+
+ return $field ? DB::Aowow()->selectCell('SELECT ?# FROM ?_scalingstatvalues WHERE id = ?d', $field, $this->ssd[$this->id]['maxLevel']) : 0;
+ }
+
+ private function initScalingStats()
+ {
+ $this->ssd[$this->id] = DB::Aowow()->selectRow('SELECT * FROM ?_scalingstatdistribution WHERE id = ?d', $this->curTpl['scalingStatDistribution']);
+
+ if (!$this->ssd[$this->id])
+ return;
+
+ // stats and ratings
+ for ($i = 1; $i <= 10; $i++)
+ {
+ if ($this->ssd[$this->id]['statMod'.$i] <= 0)
+ {
+ $this->templates[$this->id]['statType'.$i] = 0;
+ $this->templates[$this->id]['statValue'.$i] = 0;
+ }
+ else
+ {
+ $this->templates[$this->id]['statType'.$i] = $this->ssd[$this->id]['statMod'.$i];
+ $this->templates[$this->id]['statValue'.$i] = intVal(($this->getSSDMod('stats') * $this->ssd[$this->id]['modifier'.$i]) / 10000);
+ }
+ }
+
+ // armor: only replace if set
+ if ($ssvArmor = $this->getSSDMod('armor'))
+ $this->templates[$this->id]['armor'] = $ssvArmor;
+
+ // if set dpsMod in ScalingStatValue use it for min/max damage
+ // mle: 20% range / rgd: 30% range
+ if ($extraDPS = $this->getSSDMod('dps')) // dmg_x2 not used for heirlooms
+ {
+ $range = isset($this->json[$this->id]['rgddps']) ? 0.3 : 0.2;
+ $average = $extraDPS * $this->curTpl['delay'] / 1000;
+
+ $this->templates[$this->id]['dmgMin1'] = floor((1 - $range) * $average);
+ $this->templates[$this->id]['dmgMax1'] = floor((1 + $range) * $average);
+ }
+
+ // apply Spell Power from ScalingStatValue if set
+ if ($spellBonus = $this->getSSDMod('spell'))
+ {
+ $this->templates[$this->id]['statType10'] = ITEM_MOD_SPELL_POWER;
+ $this->templates[$this->id]['statValue10'] = $spellBonus;
+ }
+ }
+
+ public function initSubItems()
+ {
+ if (!array_keys($this->templates))
+ return;
+
+ $subItemIds = [];
+ foreach ($this->iterate() as $__)
+ if ($_ = $this->getField('randomEnchant'))
+ $subItemIds[abs($_)] = $_;
+
+ if (!$subItemIds)
+ return;
+
+ // remember: id < 0: randomSuffix; id > 0: randomProperty
+ $subItemTpls = DB::World()->select('
+ SELECT CAST( entry as SIGNED) AS ARRAY_KEY, CAST( ench as SIGNED) AS ARRAY_KEY2, chance FROM item_enchantment_template WHERE entry IN (?a) UNION
+ SELECT CAST(-entry as SIGNED) AS ARRAY_KEY, CAST(-ench as SIGNED) AS ARRAY_KEY2, chance FROM item_enchantment_template WHERE entry IN (?a)',
+ array_keys(array_filter($subItemIds, function ($v) { return $v > 0; })) ?: [0],
+ array_keys(array_filter($subItemIds, function ($v) { return $v < 0; })) ?: [0]
+ );
+
+ $randIds = [];
+ foreach ($subItemTpls as $tpl)
+ $randIds = array_merge($randIds, array_keys($tpl));
+
+ if (!$randIds)
+ return;
+
+ $randEnchants = DB::Aowow()->select('SELECT *, id AS ARRAY_KEY FROM ?_itemrandomenchant WHERE id IN (?a)', $randIds);
+ $enchIds = array_unique(array_merge(
+ array_column($randEnchants, 'enchantId1'),
+ array_column($randEnchants, 'enchantId2'),
+ array_column($randEnchants, 'enchantId3'),
+ array_column($randEnchants, 'enchantId4'),
+ array_column($randEnchants, 'enchantId5')
+ ));
+
+ $enchants = new EnchantmentList(array(['id', $enchIds], CFG_SQL_LIMIT_NONE));
+ foreach ($enchants->iterate() as $eId => $_)
+ {
+ $this->rndEnchIds[$eId] = array(
+ 'text' => $enchants->getField('name', true),
+ 'stats' => $enchants->getStatGain(true)
+ );
+ }
+
+ foreach ($this->iterate() as $mstItem => $__)
+ {
+ if (!$this->getField('randomEnchant'))
+ continue;
+
+ if (empty($subItemTpls[$this->getField('randomEnchant')]))
+ continue;
+
+ foreach ($subItemTpls[$this->getField('randomEnchant')] as $subId => $data)
+ {
+ if (empty($randEnchants[$subId]))
+ continue;
+
+ $data = array_merge($randEnchants[$subId], $data);
+ $jsonEquip = [];
+ $jsonText = [];
+
+ for ($i = 1; $i < 6; $i++)
+ {
+ $enchId = $data['enchantId'.$i];
+ if ($enchId <= 0 || empty($this->rndEnchIds[$enchId]))
+ continue;
+
+ if ($data['allocationPct'.$i] > 0) // RandomSuffix: scaling Enchantment; enchId < 0
+ {
+ $qty = intVal($data['allocationPct'.$i] * $this->generateEnchSuffixFactor());
+ $stats = array_fill_keys(array_keys($this->rndEnchIds[$enchId]['stats']), $qty);
+
+ $jsonText[$enchId] = str_replace('$i', $qty, $this->rndEnchIds[$enchId]['text']);
+ Util::arraySumByKey($jsonEquip, $stats);
+ }
+ else // RandomProperty: static Enchantment; enchId > 0
+ {
+ $jsonText[$enchId] = $this->rndEnchIds[$enchId]['text'];
+ Util::arraySumByKey($jsonEquip, $this->rndEnchIds[$enchId]['stats']);
+ }
+ }
+
+ $this->subItems[$mstItem][$subId] = array(
+ 'name' => Util::localizedString($data, 'name'),
+ 'enchantment' => $jsonText,
+ 'jsonequip' => $jsonEquip,
+ 'chance' => $data['chance'] // hmm, only needed for item detail page...
+ );
+ }
+
+ if (!empty($this->subItems[$mstItem]))
+ $this->json[$mstItem]['subitems'] = $this->subItems[$mstItem];
+ }
+ }
+
+ public function getScoreTotal($class = 0, $spec = [], $mhItem = 0, $ohItem = 0)
+ {
+ if (!$class || !$spec)
+ return array_sum(array_column($this->json, 'gearscore'));
+
+ $score = 0.0;
+ $mh = $oh = [];
+
+ foreach ($this->json as $j)
+ {
+ if ($j['id'] == $mhItem)
+ $mh = $j;
+ else if ($j['id'] == $ohItem)
+ $oh = $j;
+ else if ($j['gearscore'])
+ {
+ if ($j['slot'] == INVTYPE_RELIC)
+ $score += 20;
+
+ $score += round($j['gearscore']);
+ }
+ }
+
+ $score += array_sum(Util::fixWeaponScores($class, $spec, $mh, $oh));
+
+ return $score;
+ }
+
+ private function initJsonStats()
+ {
+ $json = array(
+ 'id' => $this->id,
+ 'name' => $this->getField('name', true),
+ 'quality' => ITEM_QUALITY_HEIRLOOM - $this->curTpl['quality'],
+ 'icon' => $this->curTpl['iconString'],
+ 'classs' => $this->curTpl['class'],
+ 'subclass' => $this->curTpl['subClass'],
+ 'subsubclass' => $this->curTpl['subSubClass'],
+ 'heroic' => ($this->curTpl['flags'] & 0x8) >> 3,
+ 'side' => $this->curTpl['flagsExtra'] & 0x3 ? 3 - ($this->curTpl['flagsExtra'] & 0x3) : Game::sideByRaceMask($this->curTpl['requiredRace']),
+ 'slot' => $this->curTpl['slot'],
+ 'slotbak' => $this->curTpl['slotBak'],
+ 'level' => $this->curTpl['itemLevel'],
+ 'reqlevel' => $this->curTpl['requiredLevel'],
+ 'displayid' => $this->curTpl['displayId'],
+ // 'commondrop' => 'true' / null // set if the item is a loot-filler-item .. check common ref-templates..?
+ 'holres' => $this->curTpl['resHoly'],
+ 'firres' => $this->curTpl['resFire'],
+ 'natres' => $this->curTpl['resNature'],
+ 'frores' => $this->curTpl['resFrost'],
+ 'shares' => $this->curTpl['resShadow'],
+ 'arcres' => $this->curTpl['resArcane'],
+ 'armorbonus' => $this->curTpl['armorDamageModifier'],
+ 'armor' => $this->curTpl['armor'],
+ 'dura' => $this->curTpl['durability'],
+ 'itemset' => $this->curTpl['itemset'],
+ 'socket1' => $this->curTpl['socketColor1'],
+ 'socket2' => $this->curTpl['socketColor2'],
+ 'socket3' => $this->curTpl['socketColor3'],
+ 'nsockets' => ($this->curTpl['socketColor1'] > 0 ? 1 : 0) + ($this->curTpl['socketColor2'] > 0 ? 1 : 0) + ($this->curTpl['socketColor3'] > 0 ? 1 : 0),
+ 'socketbonus' => $this->curTpl['socketBonus'],
+ 'scadist' => $this->curTpl['scalingStatDistribution'],
+ 'scaflags' => $this->curTpl['scalingStatValue']
+ );
+
+ if ($this->curTpl['class'] == ITEM_CLASS_WEAPON || $this->curTpl['class'] == ITEM_CLASS_AMMUNITION)
+ {
+
+ $json['dmgtype1'] = $this->curTpl['dmgType1'];
+ $json['dmgmin1'] = $this->curTpl['dmgMin1'] + $this->curTpl['dmgMin2'];
+ $json['dmgmax1'] = $this->curTpl['dmgMax1'] + $this->curTpl['dmgMax2'];
+ $json['speed'] = number_format($this->curTpl['delay'] / 1000, 2);
+ $json['dps'] = !floatVal($json['speed']) ? 0 : number_format(($json['dmgmin1'] + $json['dmgmax1']) / (2 * $json['speed']), 1);
+
+ if (in_array($json['subclass'], [2, 3, 18, 19]))
+ {
+ $json['rgddmgmin'] = $json['dmgmin1'];
+ $json['rgddmgmax'] = $json['dmgmax1'];
+ $json['rgdspeed'] = $json['speed'];
+ $json['rgddps'] = $json['dps'];
+ }
+ else if ($json['classs'] != ITEM_CLASS_AMMUNITION)
+ {
+ $json['mledmgmin'] = $json['dmgmin1'];
+ $json['mledmgmax'] = $json['dmgmax1'];
+ $json['mlespeed'] = $json['speed'];
+ $json['mledps'] = $json['dps'];
+ }
+
+ if ($fap = $this->getFeralAP())
+ $json['feratkpwr'] = $fap;
+ }
+
+ if ($this->curTpl['armorDamageModifier'] > 0)
+ $json['armor'] -= $this->curTpl['armorDamageModifier'];
+
+ if ($this->curTpl['class'] == ITEM_CLASS_ARMOR || $this->curTpl['class'] == ITEM_CLASS_WEAPON)
+ $json['gearscore'] = Util::getEquipmentScore($json['level'], $this->getField('quality'), $json['slot'], $json['nsockets']);
+ else if ($this->curTpl['class'] == ITEM_CLASS_GEM)
+ $json['gearscore'] = Util::getGemScore($json['level'], $this->getField('quality'), $this->getField('requiredSkill') == 755, $this->id);
+
+ // clear zero-values afterwards
+ foreach ($json as $k => $v)
+ if (!$v && !in_array($k, ['classs', 'subclass', 'quality', 'side', 'gearscore']))
+ unset($json[$k]);
+
+ Util::checkNumeric($json);
+
+ $this->json[$json['id']] = $json;
+ }
+
+ public function addRewardsToJScript(&$ref) { }
+}
+
+
+class ItemListFilter extends Filter
+{
+ private $ubFilter = []; // usable-by - limit weapon/armor selection per CharClass - itemClass => available itemsubclasses
+ private $extCostQuery = 'SELECT item FROM npc_vendor WHERE extendedCost IN (?a) UNION
+ SELECT item FROM game_event_npc_vendor WHERE extendedCost IN (?a)';
+ private $otFields = [18 => 4, 68 => 15, 69 => 16, 70 => 17, 72 => 2, 73 => 19, 75 => 21, 76 => 23, 88 => 20, 92 => 5, 93 => 3, 143 => 18, 171 => 8, 172 => 12];
+
+ public $extraOpts = []; // score for statWeights
+ public $wtCnd = [];
+ protected $enums = array(
+ 99 => array( // profession | recycled for 86, 87
+ null, 171, 164, 185, 333, 202, 129, 755, 165, 186, 197, true, false, 356, 182, 773
+ ),
+ 66 => array( // profession specialization
+ 1 => -1,
+ 2 => [ 9788, 9787, 17041, 17040, 17039 ],
+ 3 => -1,
+ 4 => -1,
+ 5 => [20219, 20222 ],
+ 6 => -1,
+ 7 => -1,
+ 8 => [10656, 10658, 10660 ],
+ 9 => -1,
+ 10 => [26798, 26801, 26797 ],
+ 11 => [ 9788, 9787, 17041, 17040, 17039, 20219, 20222, 10656, 10658, 10660, 26798, 26801, 26797], // i know, i know .. lazy as fuck
+ 12 => false,
+ 13 => -1,
+ 14 => -1,
+ 15 => -1
+ ),
+ 152 => array( // class-specific
+ null, 1, 2, 3, 4, 5, 6, 7, 8, 9, null, 11, true, false
+ ),
+ 153 => array( // race-specific
+ null, 1, 2, 3, 4, 5, 6, 7, 8, null, 10, 11, true, false
+ ),
+ 158 => array( // currency
+ 32572, 32569, 29736, 44128, 20560, 20559, 29434, 37829, 23247, 44990, 24368, 52027, 52030, 43016, 41596, 34052, 45624, 49426, 40752, 47241, 40753, 29024,
+ 24245, 26045, 26044, 38425, 29735, 24579, 24581, 32897, 22484, 52026, 52029, 4291, 28558, 43228, 34664, 47242, 52025, 52028, 37836, 20558, 34597, 43589
+ ),
+ 118 => array( // tokens
+ 34853, 34854, 34855, 34856, 34857, 34858, 34848, 34851, 34852, 40625, 40626, 40627, 45632, 45633, 45634, 34169, 34186, 29754, 29753, 29755, 31089, 31091, 31090,
+ 40610, 40611, 40612, 30236, 30237, 30238, 45635, 45636, 45637, 34245, 34332, 34339, 34345, 40631, 40632, 40633, 45638, 45639, 45640, 34244, 34208, 34180, 34229,
+ 34350, 40628, 40629, 40630, 45641, 45642, 45643, 29757, 29758, 29756, 31092, 31094, 31093, 40613, 40614, 40615, 30239, 30240, 30241, 45644, 45645, 45646, 34342,
+ 34211, 34243, 29760, 29761, 29759, 31097, 31095, 31096, 40616, 40617, 40618, 30242, 30243, 30244, 45647, 45648, 45649, 34216, 29766, 29767, 29765, 31098, 31100,
+ 31099, 40619, 40620, 40621, 30245, 30246, 30247, 45650, 45651, 45652, 34167, 40634, 40635, 40636, 45653, 45654, 45655, 40637, 40638, 40639, 45656, 45657, 45658,
+ 34170, 34192, 29763, 29764, 29762, 31101, 31103, 31102, 30248, 30249, 30250, 47557, 47558, 47559, 34233, 34234, 34202, 34195, 34209, 40622, 40623, 40624, 34193,
+ 45659, 45660, 45661, 34212, 34351, 34215
+ ),
+ 128 => array( // source
+ 1 => true, // Any
+ 2 => false, // None
+ 3 => 1, // Crafted
+ 4 => 2, // Drop
+ 5 => 3, // PvP
+ 6 => 4, // Quest
+ 7 => 5, // Vendor
+ 9 => 10, // Starter
+ 10 => 11, // Event
+ 11 => 12 // Achievement
+ ),
+ 126 => array( // Zones
+ 4494, 36, 2597, 3358, 45, 331, 3790, 4277, 16, 3524, 3, 3959, 719, 1584, 25, 1583, 2677, 3702, 3522, 4, 3525, 3537, 46, 1941,
+ 2918, 3905, 4024, 2817, 4395, 4378, 148, 393, 1657, 41, 2257, 405, 2557, 65, 4196, 1, 14, 10, 15, 139, 12, 3430, 3820, 361,
+ 357, 3433, 721, 394, 3923, 4416, 2917, 4272, 4820, 4264, 3483, 3562, 267, 495, 4742, 3606, 210, 4812, 1537, 4710, 4080, 3457, 38, 4131,
+ 3836, 3792, 2100, 2717, 493, 215, 3518, 3698, 3456, 3523, 2367, 2159, 1637, 4813, 4298, 2437, 722, 491, 44, 3429, 3968, 796, 2057, 51,
+ 3607, 3791, 3789, 209, 3520, 3703, 3711, 1377, 3487, 130, 3679, 406, 1519, 4384, 33, 2017, 1477, 4075, 8, 440, 141, 3428, 3519, 3848,
+ 17, 2366, 3840, 3713, 3847, 3775, 4100, 1581, 3557, 3845, 4500, 4809, 47, 3849, 4265, 4493, 4228, 3698, 4406, 3714, 3717, 3715, 717, 67,
+ 3716, 457, 4415, 400, 1638, 1216, 85, 4723, 4722, 1337, 4273, 490, 1497, 206, 1196, 4603, 718, 3277, 28, 40, 11, 4197, 618, 3521,
+ 3805, 66, 1176, 1977
+ ),
+ 163 => array( // enchantment mats
+ 34057, 22445, 11176, 34052, 11082, 34055, 16203, 10939, 11135, 11175, 22446, 16204, 34054, 14344, 11084, 11139, 22449, 11178,
+ 10998, 34056, 16202, 10938, 11134, 11174, 22447, 20725, 14343, 34053, 10978, 11138, 22448, 11177, 11083, 10940, 11137, 22450
+ )
+ );
+
+ // cr => [type, field, misc, extraCol]
+ protected $genericFilter = array( // misc (bool): _NUMERIC => useFloat; _STRING => localized; _FLAG => match Value; _BOOLEAN => stringSet
+ 2 => [FILTER_CR_CALLBACK, 'cbFieldHasVal', 'bonding', 1 ], // bindonpickup [yn]
+ 3 => [FILTER_CR_CALLBACK, 'cbFieldHasVal', 'bonding', 2 ], // bindonequip [yn]
+ 4 => [FILTER_CR_CALLBACK, 'cbFieldHasVal', 'bonding', 3 ], // bindonuse [yn]
+ 5 => [FILTER_CR_CALLBACK, 'cbFieldHasVal', 'bonding', [4, 5] ], // questitem [yn]
+ 6 => [FILTER_CR_CALLBACK, 'cbQuestRelation', null, null ], // startsquest [side]
+ 7 => [FILTER_CR_BOOLEAN, 'description_loc0', true ], // hasflavortext
+ 8 => [FILTER_CR_BOOLEAN, 'requiredDisenchantSkill' ], // disenchantable
+ 9 => [FILTER_CR_FLAG, 'flags', ITEM_FLAG_CONJURED ], // conjureditem
+ 10 => [FILTER_CR_BOOLEAN, 'lockId' ], // locked
+ 11 => [FILTER_CR_FLAG, 'flags', ITEM_FLAG_OPENABLE ], // openable
+ 12 => [FILTER_CR_BOOLEAN, 'itemset' ], // partofset
+ 13 => [FILTER_CR_BOOLEAN, 'randomEnchant' ], // randomlyenchanted
+ 14 => [FILTER_CR_BOOLEAN, 'pageTextId' ], // readable
+ 15 => [FILTER_CR_CALLBACK, 'cbFieldHasVal', 'maxCount', 1 ], // unique [yn]
+ 16 => [FILTER_CR_NYI_PH, null, 1, ], // dropsin [zone]
+ 17 => [FILTER_CR_ENUM, 'requiredFaction' ], // requiresrepwith
+ 18 => [FILTER_CR_CALLBACK, 'cbFactionQuestReward', null, null ], // rewardedbyfactionquest [side]
+ 20 => [FILTER_CR_NUMERIC, 'is.str', NUM_CAST_INT, true ], // str
+ 21 => [FILTER_CR_NUMERIC, 'is.agi', NUM_CAST_INT, true ], // agi
+ 22 => [FILTER_CR_NUMERIC, 'is.sta', NUM_CAST_INT, true ], // sta
+ 23 => [FILTER_CR_NUMERIC, 'is.int', NUM_CAST_INT, true ], // int
+ 24 => [FILTER_CR_NUMERIC, 'is.spi', NUM_CAST_INT, true ], // spi
+ 25 => [FILTER_CR_NUMERIC, 'is.arcres', NUM_CAST_INT, true ], // arcres
+ 26 => [FILTER_CR_NUMERIC, 'is.firres', NUM_CAST_INT, true ], // firres
+ 27 => [FILTER_CR_NUMERIC, 'is.natres', NUM_CAST_INT, true ], // natres
+ 28 => [FILTER_CR_NUMERIC, 'is.frores', NUM_CAST_INT, true ], // frores
+ 29 => [FILTER_CR_NUMERIC, 'is.shares', NUM_CAST_INT, true ], // shares
+ 30 => [FILTER_CR_NUMERIC, 'is.holres', NUM_CAST_INT, true ], // holres
+ 32 => [FILTER_CR_NUMERIC, 'is.dps', NUM_CAST_FLOAT, true ], // dps
+ 33 => [FILTER_CR_NUMERIC, 'is.dmgmin1', NUM_CAST_INT, true ], // dmgmin1
+ 34 => [FILTER_CR_NUMERIC, 'is.dmgmax1', NUM_CAST_INT, true ], // dmgmax1
+ 35 => [FILTER_CR_CALLBACK, 'cbDamageType', null, null ], // damagetype [enum]
+ 36 => [FILTER_CR_NUMERIC, 'is.speed', NUM_CAST_FLOAT, true ], // speed
+ 37 => [FILTER_CR_NUMERIC, 'is.mleatkpwr', NUM_CAST_INT, true ], // mleatkpwr
+ 38 => [FILTER_CR_NUMERIC, 'is.rgdatkpwr', NUM_CAST_INT, true ], // rgdatkpwr
+ 39 => [FILTER_CR_NUMERIC, 'is.rgdhitrtng', NUM_CAST_INT, true ], // rgdhitrtng
+ 40 => [FILTER_CR_NUMERIC, 'is.rgdcritstrkrtng', NUM_CAST_INT, true ], // rgdcritstrkrtng
+ 41 => [FILTER_CR_NUMERIC, 'is.armor' , NUM_CAST_INT, true ], // armor
+ 42 => [FILTER_CR_NUMERIC, 'is.defrtng', NUM_CAST_INT, true ], // defrtng
+ 43 => [FILTER_CR_NUMERIC, 'is.block', NUM_CAST_INT, true ], // block
+ 44 => [FILTER_CR_NUMERIC, 'is.blockrtng', NUM_CAST_INT, true ], // blockrtng
+ 45 => [FILTER_CR_NUMERIC, 'is.dodgertng', NUM_CAST_INT, true ], // dodgertng
+ 46 => [FILTER_CR_NUMERIC, 'is.parryrtng', NUM_CAST_INT, true ], // parryrtng
+ 48 => [FILTER_CR_NUMERIC, 'is.splhitrtng', NUM_CAST_INT, true ], // splhitrtng
+ 49 => [FILTER_CR_NUMERIC, 'is.splcritstrkrtng', NUM_CAST_INT, true ], // splcritstrkrtng
+ 50 => [FILTER_CR_NUMERIC, 'is.splheal', NUM_CAST_INT, true ], // splheal
+ 51 => [FILTER_CR_NUMERIC, 'is.spldmg', NUM_CAST_INT, true ], // spldmg
+ 52 => [FILTER_CR_NUMERIC, 'is.arcsplpwr', NUM_CAST_INT, true ], // arcsplpwr
+ 53 => [FILTER_CR_NUMERIC, 'is.firsplpwr', NUM_CAST_INT, true ], // firsplpwr
+ 54 => [FILTER_CR_NUMERIC, 'is.frosplpwr', NUM_CAST_INT, true ], // frosplpwr
+ 55 => [FILTER_CR_NUMERIC, 'is.holsplpwr', NUM_CAST_INT, true ], // holsplpwr
+ 56 => [FILTER_CR_NUMERIC, 'is.natsplpwr', NUM_CAST_INT, true ], // natsplpwr
+ 57 => [FILTER_CR_NUMERIC, 'is.shasplpwr', NUM_CAST_INT, true ], // shasplpwr
+ 59 => [FILTER_CR_NUMERIC, 'durability', NUM_CAST_INT, true ], // dura
+ 60 => [FILTER_CR_NUMERIC, 'is.healthrgn', NUM_CAST_INT, true ], // healthrgn
+ 61 => [FILTER_CR_NUMERIC, 'is.manargn', NUM_CAST_INT, true ], // manargn
+ 62 => [FILTER_CR_CALLBACK, 'cbCooldown', null, null ], // cooldown [op] [int]
+ 63 => [FILTER_CR_NUMERIC, 'buyPrice', NUM_CAST_INT, true ], // buyprice
+ 64 => [FILTER_CR_NUMERIC, 'sellPrice', NUM_CAST_INT, true ], // sellprice
+ 65 => [FILTER_CR_CALLBACK, 'cbAvgMoneyContent', null, null ], // avgmoney [op] [int]
+ 66 => [FILTER_CR_ENUM, 'requiredSpell' ], // requiresprofspec
+ 68 => [FILTER_CR_CALLBACK, 'cbObtainedBy', null, null ], // otdisenchanting [yn]
+ 69 => [FILTER_CR_CALLBACK, 'cbObtainedBy', null, null ], // otfishing [yn]
+ 70 => [FILTER_CR_CALLBACK, 'cbObtainedBy', null, null ], // otherbgathering [yn]
+ 71 => [FILTER_CR_FLAG, 'cuFlags', ITEM_CU_OT_ITEMLOOT ], // otitemopening [yn]
+ 72 => [FILTER_CR_CALLBACK, 'cbObtainedBy', null, null ], // otlooting [yn]
+ 73 => [FILTER_CR_CALLBACK, 'cbObtainedBy', null, null ], // otmining [yn]
+ 74 => [FILTER_CR_FLAG, 'cuFlags', ITEM_CU_OT_OBJECTLOOT ], // otobjectopening [yn]
+ 75 => [FILTER_CR_CALLBACK, 'cbObtainedBy', null, null ], // otpickpocketing [yn]
+ 76 => [FILTER_CR_CALLBACK, 'cbObtainedBy', null, null ], // otskinning [yn]
+ 77 => [FILTER_CR_NUMERIC, 'is.atkpwr', NUM_CAST_INT, true ], // atkpwr
+ 78 => [FILTER_CR_NUMERIC, 'is.mlehastertng', NUM_CAST_INT, true ], // mlehastertng
+ 79 => [FILTER_CR_NUMERIC, 'is.resirtng', NUM_CAST_INT, true ], // resirtng
+ 80 => [FILTER_CR_CALLBACK, 'cbHasSockets', null, null ], // has sockets [enum]
+ 81 => [FILTER_CR_CALLBACK, 'cbFitsGemSlot', null, null ], // fits gem slot [enum]
+ 83 => [FILTER_CR_FLAG, 'flags', ITEM_FLAG_UNIQUEEQUIPPED ], // uniqueequipped
+ 84 => [FILTER_CR_NUMERIC, 'is.mlecritstrkrtng', NUM_CAST_INT, true ], // mlecritstrkrtng
+ 85 => [FILTER_CR_CALLBACK, 'cbObjectiveOfQuest', null, null ], // objectivequest [side]
+ 86 => [FILTER_CR_CALLBACK, 'cbCraftedByProf', null, null ], // craftedprof [enum]
+ 87 => [FILTER_CR_CALLBACK, 'cbReagentForAbility', null, null ], // reagentforability [enum]
+ 88 => [FILTER_CR_CALLBACK, 'cbObtainedBy', null, null ], // otprospecting [yn]
+ 89 => [FILTER_CR_FLAG, 'flags', ITEM_FLAG_PROSPECTABLE ], // prospectable
+ 90 => [FILTER_CR_CALLBACK, 'cbAvgBuyout', null, null ], // avgbuyout [op] [int]
+ 91 => [FILTER_CR_ENUM, 'totemCategory' ], // tool
+ 92 => [FILTER_CR_CALLBACK, 'cbObtainedBy', null, null ], // soldbyvendor [yn]
+ 93 => [FILTER_CR_CALLBACK, 'cbObtainedBy', null, null ], // otpvp [pvp]
+ 94 => [FILTER_CR_NUMERIC, 'is.splpen', NUM_CAST_INT, true ], // splpen
+ 95 => [FILTER_CR_NUMERIC, 'is.mlehitrtng', NUM_CAST_INT, true ], // mlehitrtng
+ 96 => [FILTER_CR_NUMERIC, 'is.critstrkrtng', NUM_CAST_INT, true ], // critstrkrtng
+ 97 => [FILTER_CR_NUMERIC, 'is.feratkpwr', NUM_CAST_INT, true ], // feratkpwr
+ 98 => [FILTER_CR_FLAG, 'flags', ITEM_FLAG_PARTYLOOT ], // partyloot
+ 99 => [FILTER_CR_ENUM, 'requiredSkill' ], // requiresprof
+ 100 => [FILTER_CR_NUMERIC, 'is.nsockets', NUM_CAST_INT ], // nsockets
+ 101 => [FILTER_CR_NUMERIC, 'is.rgdhastertng', NUM_CAST_INT, true ], // rgdhastertng
+ 102 => [FILTER_CR_NUMERIC, 'is.splhastertng', NUM_CAST_INT, true ], // splhastertng
+ 103 => [FILTER_CR_NUMERIC, 'is.hastertng', NUM_CAST_INT, true ], // hastertng
+ 104 => [FILTER_CR_STRING, 'description', STR_LOCALIZED ], // flavortext
+ 105 => [FILTER_CR_NYI_PH, null, 1, ], // dropsinnormal [heroicdungeon-any]
+ 106 => [FILTER_CR_NYI_PH, null, 1, ], // dropsinheroic [heroicdungeon-any]
+ 107 => [FILTER_CR_NYI_PH, null, 1, ], // effecttext [str] not yet parsed ['effectsParsed_loc'.User::$localeId, $cr[2]]
+ 109 => [FILTER_CR_CALLBACK, 'cbArmorBonus', null, null ], // armorbonus [op] [int]
+ 111 => [FILTER_CR_NUMERIC, 'requiredSkillRank', NUM_CAST_INT, true ], // reqskillrank
+ 113 => [FILTER_CR_FLAG, 'cuFlags', CUSTOM_HAS_SCREENSHOT ], // hasscreenshots
+ 114 => [FILTER_CR_NUMERIC, 'is.armorpenrtng', NUM_CAST_INT, true ], // armorpenrtng
+ 115 => [FILTER_CR_NUMERIC, 'is.health', NUM_CAST_INT, true ], // health
+ 116 => [FILTER_CR_NUMERIC, 'is.mana', NUM_CAST_INT, true ], // mana
+ 117 => [FILTER_CR_NUMERIC, 'is.exprtng', NUM_CAST_INT, true ], // exprtng
+ 118 => [FILTER_CR_CALLBACK, 'cbPurchasableWith', null, null ], // purchasablewithitem [enum]
+ 119 => [FILTER_CR_NUMERIC, 'is.hitrtng', NUM_CAST_INT, true ], // hitrtng
+ 123 => [FILTER_CR_NUMERIC, 'is.splpwr', NUM_CAST_INT, true ], // splpwr
+ 124 => [FILTER_CR_CALLBACK, 'cbHasRandEnchant', null, null ], // randomenchants [str]
+ 125 => [FILTER_CR_CALLBACK, 'cbReqArenaRating', null, null ], // reqarenartng [op] [int] todo (low): 'find out, why "IN (W, X, Y) AND IN (X, Y, Z)" doesn't result in "(X, Y)"
+ 126 => [FILTER_CR_CALLBACK, 'cbQuestRewardIn', null, null ], // rewardedbyquestin [zone-any]
+ 128 => [FILTER_CR_CALLBACK, 'cbSource', null, null ], // source [enum]
+ 129 => [FILTER_CR_CALLBACK, 'cbSoldByNPC', null, null ], // soldbynpc [str-small]
+ 130 => [FILTER_CR_FLAG, 'cuFlags', CUSTOM_HAS_COMMENT ], // hascomments
+ 132 => [FILTER_CR_CALLBACK, 'cbGlyphType', null, null ], // glyphtype [enum]
+ 133 => [FILTER_CR_FLAG, 'flags', ITEM_FLAG_ACCOUNTBOUND ], // accountbound
+ 134 => [FILTER_CR_NUMERIC, 'is.mledps', NUM_CAST_FLOAT, true ], // mledps
+ 135 => [FILTER_CR_NUMERIC, 'is.mledmgmin', NUM_CAST_INT, true ], // mledmgmin
+ 136 => [FILTER_CR_NUMERIC, 'is.mledmgmax', NUM_CAST_INT, true ], // mledmgmax
+ 137 => [FILTER_CR_NUMERIC, 'is.mlespeed', NUM_CAST_FLOAT, true ], // mlespeed
+ 138 => [FILTER_CR_NUMERIC, 'is.rgddps', NUM_CAST_FLOAT, true ], // rgddps
+ 139 => [FILTER_CR_NUMERIC, 'is.rgddmgmin', NUM_CAST_INT, true ], // rgddmgmin
+ 140 => [FILTER_CR_NUMERIC, 'is.rgddmgmax', NUM_CAST_INT, true ], // rgddmgmax
+ 141 => [FILTER_CR_NUMERIC, 'is.rgdspeed', NUM_CAST_FLOAT, true ], // rgdspeed
+ 142 => [FILTER_CR_STRING, 'ic.name' ], // icon
+ 143 => [FILTER_CR_CALLBACK, 'cbObtainedBy', null, null ], // otmilling [yn]
+ 144 => [FILTER_CR_CALLBACK, 'cbPvpPurchasable', 'reqHonorPoints', null ], // purchasablewithhonor [yn]
+ 145 => [FILTER_CR_CALLBACK, 'cbPvpPurchasable', 'reqHonorPoints', null ], // purchasablewitharena [yn]
+ 146 => [FILTER_CR_FLAG, 'flags', ITEM_FLAG_HEROIC ], // heroic
+ 147 => [FILTER_CR_NYI_PH, null, 1, ], // dropsinnormal10 [multimoderaid-any]
+ 148 => [FILTER_CR_NYI_PH, null, 1, ], // dropsinnormal25 [multimoderaid-any]
+ 149 => [FILTER_CR_NYI_PH, null, 1, ], // dropsinheroic10 [heroicraid-any]
+ 150 => [FILTER_CR_NYI_PH, null, 1, ], // dropsinheroic25 [heroicraid-any]
+ 151 => [FILTER_CR_NUMERIC, 'id', NUM_CAST_INT, true ], // id
+ 152 => [FILTER_CR_CALLBACK, 'cbClassRaceSpec', 'requiredClass', CLASS_MASK_ALL], // classspecific [enum]
+ 153 => [FILTER_CR_CALLBACK, 'cbClassRaceSpec', 'requiredRace', RACE_MASK_ALL ], // racespecific [enum]
+ 154 => [FILTER_CR_FLAG, 'flags', ITEM_FLAG_REFUNDABLE ], // refundable
+ 155 => [FILTER_CR_FLAG, 'flags', ITEM_FLAG_USABLE_ARENA ], // usableinarenas
+ 156 => [FILTER_CR_FLAG, 'flags', ITEM_FLAG_USABLE_SHAPED ], // usablewhenshapeshifted
+ 157 => [FILTER_CR_FLAG, 'flags', ITEM_FLAG_SMARTLOOT ], // smartloot
+ 158 => [FILTER_CR_CALLBACK, 'cbPurchasableWith', null, null ], // purchasablewithcurrency [enum]
+ 159 => [FILTER_CR_FLAG, 'flags', ITEM_FLAG_MILLABLE ], // millable
+ 160 => [FILTER_CR_NYI_PH, null, 1, ], // relatedevent [enum] like 169 .. crawl though npc_vendor and loot_templates of event-related spawns
+ 161 => [FILTER_CR_CALLBACK, 'cbAvailable', null, null ], // availabletoplayers [yn]
+ 162 => [FILTER_CR_FLAG, 'flags', ITEM_FLAG_DEPRECATED ], // deprecated
+ 163 => [FILTER_CR_CALLBACK, 'cbDisenchantsInto', null, null ], // disenchantsinto [disenchanting]
+ 165 => [FILTER_CR_NUMERIC, 'repairPrice', NUM_CAST_INT, true ], // repaircost
+ 167 => [FILTER_CR_FLAG, 'cuFlags', CUSTOM_HAS_VIDEO ], // hasvideos
+ 168 => [FILTER_CR_CALLBACK, 'cbFieldHasVal', 'spellId1', [483, 55884] ], // teachesspell [yn] - 483: learn recipe; 55884: learn mount/pet
+ 169 => [FILTER_CR_ENUM, 'e.holidayId' ], // requiresevent
+ 171 => [FILTER_CR_CALLBACK, 'cbObtainedBy', null, null ], // otredemption [yn]
+ 172 => [FILTER_CR_CALLBACK, 'cbObtainedBy', null, null ], // rewardedbyachievement [yn]
+ 176 => [FILTER_CR_STAFFFLAG, 'flags' ], // flags
+ 177 => [FILTER_CR_STAFFFLAG, 'flagsExtra' ], // flags2
+ );
+
+ // fieldId => [checkType, checkValue[, fieldIsArray]]
+ protected $inputFields = array(
+ 'wt' => [FILTER_V_CALLBACK, 'cbWeightKeyCheck', true ], // weight keys
+ 'wtv' => [FILTER_V_RANGE, [1, 999], true ], // weight values
+ 'jc' => [FILTER_V_LIST, [1], false], // use jewelcrafter gems for weight calculation
+ 'gm' => [FILTER_V_LIST, [2, 3, 4], false], // gem rarity for weight calculation
+ 'cr' => [FILTER_V_RANGE, [1, 177], true ], // criteria ids
+ 'crs' => [FILTER_V_LIST, [FILTER_ENUM_NONE, FILTER_ENUM_ANY, [0, 99999]], true ], // criteria operators
+ 'crv' => [FILTER_V_REGEX, '/[\p{C};:]/ui', true ], // criteria values - only printable chars, no delimiters
+ 'upg' => [FILTER_V_RANGE, [1, 999999], true ], // upgrade item ids
+ 'gb' => [FILTER_V_LIST, [0, 1, 2, 3], false], // search result grouping
+ 'na' => [FILTER_V_REGEX, '/[\p{C};]/ui', false], // name - only printable chars, no delimiter
+ 'ma' => [FILTER_V_EQUAL, 1, false], // match any / all filter
+ 'ub' => [FILTER_V_LIST, [[1, 9], 11], false], // usable by classId
+ 'qu' => [FILTER_V_RANGE, [0, 7], true ], // quality ids
+ 'ty' => [FILTER_V_CALLBACK, 'cbTypeCheck', true ], // item type - dynamic by current group
+ 'sl' => [FILTER_V_CALLBACK, 'cbSlotCheck', true ], // item slot - dynamic by current group
+ 'si' => [FILTER_V_LIST, [1, 2, 3, -1, -2], false], // side
+ 'minle' => [FILTER_V_RANGE, [1, 999], false], // item level min
+ 'maxle' => [FILTER_V_RANGE, [1, 999], false], // item level max
+ 'minrl' => [FILTER_V_RANGE, [1, MAX_LEVEL], false], // required level min
+ 'maxrl' => [FILTER_V_RANGE, [1, MAX_LEVEL], false] // required level max
+ );
+
+ public function __construct($fromPOST = false, $opts = [])
+ {
+ $classes = new CharClassList();
+ foreach ($classes->iterate() as $cId => $_tpl)
+ {
+ // preselect misc subclasses
+ $this->ubFilter[$cId] = [ITEM_CLASS_WEAPON => [14], ITEM_CLASS_ARMOR => [0]];
+
+ for ($i = 0; $i < 21; $i++)
+ if ($_tpl['weaponTypeMask'] & (1 << $i))
+ $this->ubFilter[$cId][ITEM_CLASS_WEAPON][] = $i;
+
+ for ($i = 0; $i < 11; $i++)
+ if ($_tpl['armorTypeMask'] & (1 << $i))
+ $this->ubFilter[$cId][ITEM_CLASS_ARMOR][] = $i;
+ }
+
+ parent::__construct($fromPOST, $opts);
+ }
+
+ public function createConditionsForWeights()
+ {
+ if (empty($this->fiData['v']['wt']))
+ return null;
+
+ $this->wtCnd = [];
+ $select = [];
+ $wtSum = 0;
+
+ foreach ($this->fiData['v']['wt'] as $k => $v)
+ {
+ $str = Util::$itemFilter[$v];
+ $qty = intVal($this->fiData['v']['wtv'][$k]);
+
+ if ($str == 'rgdspeed') // dont need no duplicate column
+ $str = 'speed';
+
+ if ($str == 'mledps') // todo (med): unify rngdps and mledps to dps
+ $str = 'dps';
+
+ $select[] = '(`is`.`'.$str.'` * '.$qty.')';
+ $this->wtCnd[] = ['is.'.$str, 0, '>'];
+ $wtSum += $qty;
+ }
+
+ if (count($this->wtCnd) > 1)
+ array_unshift($this->wtCnd, 'OR');
+ else if (count($this->wtCnd) == 1)
+ $this->wtCnd = $this->wtCnd[0];
+
+ if ($select)
+ {
+ $this->extraOpts['is']['s'][] = ', IF(is.typeId IS NULL, 0, ('.implode(' + ', $select).') / '.$wtSum.') AS score';
+ $this->extraOpts['is']['o'][] = 'score DESC';
+ $this->extraOpts['i']['o'][] = null; // remove default ordering
+ }
+ else
+ $this->extraOpts['is']['s'][] = ', 0 AS score'; // prevent errors
+
+ return $this->wtCnd;
+ }
+
+ protected function createSQLForCriterium(&$cr)
+ {
+ if (in_array($cr[0], array_keys($this->genericFilter)))
+ if ($genCr = $this->genericCriterion($cr))
+ return $genCr;
+
+ unset($cr);
+ $this->error = true;
+ return [1];
+ }
+
+ protected function createSQLForValues()
+ {
+ $parts = [];
+ $_v = $this->fiData['v'];
+
+ // weights
+ if (!empty($_v['wt']) && !empty($_v['wtv']))
+ {
+ // gm - gem quality (qualityId)
+ // jc - jc-gems included (bool)
+
+ $parts[] = $this->createConditionsForWeights();
+
+ foreach ($_v['wt'] as $_)
+ $this->formData['extraCols'][] = $_;
+ }
+
+ // upgrade for [form only]
+ if (isset($_v['upg']))
+ {
+ $_ = DB::Aowow()->selectCol('SELECT id as ARRAY_KEY, slot FROM ?_items WHERE class IN (2, 3, 4) AND id IN (?a)', (array)$_v['upg']);
+ if ($_ === null)
+ {
+ unset($_v['upg']);
+ unset($this->formData['form']['upg']);
+ }
+ else
+ {
+ $this->formData['form']['upg'] = $_;
+ if ($_)
+ $parts[] = ['slot', $_];
+ }
+ }
+
+ // group by [form only]
+ if (isset($_v['gb']))
+ $this->formData['form']['gb'] = $_v['gb'];
+
+ // name
+ if (isset($_v['na']))
+ if ($_ = $this->modularizeString(['name_loc'.User::$localeId]))
+ $parts[] = $_;
+
+ // usable-by (not excluded by requiredClass && armor or weapons match mask from ?_classes)
+ if (isset($_v['ub']))
+ {
+ $parts[] = array(
+ 'AND',
+ ['OR', ['requiredClass', 0], ['requiredClass', $this->list2Mask((array)$_v['ub']), '&']],
+ [
+ 'OR',
+ ['class', [2, 4], '!'],
+ ['AND', ['class', 2], ['subclassbak', $this->ubFilter[$_v['ub']][ITEM_CLASS_WEAPON]]],
+ ['AND', ['class', 4], ['subclassbak', $this->ubFilter[$_v['ub']][ITEM_CLASS_ARMOR]]]
+ ]
+ );
+ }
+
+ // quality [list]
+ if (isset($_v['qu']))
+ $parts[] = ['quality', $_v['qu']];
+
+ // type
+ if (isset($_v['ty']))
+ $parts[] = ['subclass', $_v['ty']];
+
+ // slot
+ if (isset($_v['sl']))
+ $parts[] = ['slot', $_v['sl']];
+
+ // side
+ if (isset($_v['si']))
+ {
+ $ex = [['requiredRace', RACE_MASK_ALL, '&'], RACE_MASK_ALL, '!'];
+ $notEx = ['OR', ['requiredRace', 0], [['requiredRace', RACE_MASK_ALL, '&'], RACE_MASK_ALL]];
+
+ switch ($_v['si'])
+ {
+ case 3:
+ $parts[] = $notEx;
+ break;
+ case 2:
+ $parts[] = ['AND', [['flagsExtra', 0x3, '&'], [0, 1]], ['OR', $notEx, ['requiredRace', RACE_MASK_HORDE, '&']]];
+ break;
+ case -2:
+ $parts[] = ['OR', [['flagsExtra', 0x3, '&'], 1], ['AND', $ex, ['requiredRace', RACE_MASK_HORDE, '&']]];
+ break;
+ case 1:
+ $parts[] = ['AND', [['flagsExtra', 0x3, '&'], [0, 2]], ['OR', $notEx, ['requiredRace', RACE_MASK_ALLIANCE, '&']]];
+ break;
+ case -1:
+ $parts[] = ['OR', [['flagsExtra', 0x3, '&'], 2], ['AND', $ex, ['requiredRace', RACE_MASK_ALLIANCE, '&']]];
+ break;
+ }
+ }
+
+ // itemLevel min
+ if (isset($_v['minle']))
+ $parts[] = ['itemLevel', $_v['minle'], '>='];
+
+ // itemLevel max
+ if (isset($_v['maxle']))
+ $parts[] = ['itemLevel', $_v['maxle'], '<='];
+
+ // reqLevel min
+ if (isset($_v['minrl']))
+ $parts[] = ['requiredLevel', $_v['minrl'], '>='];
+
+ // reqLevel max
+ if (isset($_v['maxrl']))
+ $parts[] = ['requiredLevel', $_v['maxrl'], '<='];
+
+ return $parts;
+ }
+
+ protected function cbFactionQuestReward($cr)
+ {
+ if (!isset($this->otFields[$cr[0]]))
+ return false;
+
+ $field = 'src.src'.$this->otFields[$cr[0]];
+ switch ($cr[1])
+ {
+ case 1: // Yes
+ return [$field, null, '!'];
+ case 2: // Alliance
+ return [$field, 1];
+ case 3: // Horde
+ return [$field, 2];
+ case 4: // Both
+ return [$field, 3];
+ case 5: // No
+ return [$field, null];
+ }
+
+ return false;
+ }
+
+ protected function cbAvailable($cr)
+ {
+ if ($this->int2Bool($cr[1]))
+ return [['cuFlags', CUSTOM_UNAVAILABLE, '&'], 0, $cr[1] ? null : '!'];
+
+ return false;
+ }
+
+ protected function cbHasSockets($cr)
+ {
+ switch ($cr[1])
+ {
+ case 5: // Yes
+ return ['is.nsockets', 0, '!'];
+ case 6: // No
+ return ['is.nsockets', 0];
+ case 1: // Meta
+ case 2: // Red
+ case 3: // Yellow
+ case 4: // Blue
+ $mask = 1 << ($cr[1] - 1);
+ return ['OR', ['socketColor1', $mask], ['socketColor2', $mask], ['socketColor3', $mask]];
+ }
+
+ return false;
+ }
+
+ protected function cbFitsGemSlot($cr)
+ {
+ switch ($cr[1])
+ {
+ case 5: // Yes
+ return ['gemEnchantmentId', 0, '!'];
+ case 6: // No
+ return ['gemEnchantmentId', 0];
+ case 1: // Meta
+ case 2: // Red
+ case 3: // Yellow
+ case 4: // Blue
+ $mask = 1 << ($cr[1] - 1);
+ return ['AND', ['gemEnchantmentId', 0, '!'], ['gemColorMask', $mask, '&']];
+ }
+ }
+
+ protected function cbGlyphType($cr)
+ {
+ switch ($cr[1])
+ {
+ case 1: // Major
+ case 2: // Minor
+ return ['AND', ['class', 16], ['subSubClass', $cr[1]]];
+ }
+
+ return false;
+ }
+
+ protected function cbHasRandEnchant($cr)
+ {
+ $randIds = DB::Aowow()->selectCol('SELECT id AS ARRAY_KEY, ABS(id) FROM ?_itemrandomenchant WHERE name_loc?d LIKE ?', User::$localeId, '%'.$cr[2].'%');
+ $tplIds = $randIds ? DB::World()->select('SELECT entry, ench FROM item_enchantment_template WHERE ench IN (?a)', $randIds) : [];
+ foreach ($tplIds as $k => &$set)
+ if (array_search($set['ench'], $randIds) < 0)
+ $set['entry'] *= -1;
+
+ if ($tplIds)
+ return ['randomEnchant', array_column($tplIds, 'entry')];
+ else
+ return [0]; // no results aren't really input errors
+ }
+
+ protected function cbReqArenaRating($cr)
+ {
+ if (!Util::checkNumeric($cr[2], NUM_CAST_INT) || !$this->int2Op($cr[1]))
+ return false;
+
+ $this->formData['extraCols'][] = $cr[0];
+
+ $costs = DB::Aowow()->selectCol('SELECT id FROM ?_itemextendedcost WHERE reqPersonalrating '.$cr[1].' '.$cr[2]);
+ $items = DB::World()->selectCol($this->extCostQuery, $costs, $costs);
+ return ['id', $items];
+ }
+
+ protected function cbClassRaceSpec($cr, $field, $mask)
+ {
+ if (!isset($this->enums[$cr[0]][$cr[1]]))
+ return false;
+
+ $_ = $this->enums[$cr[0]][$cr[1]];
+ if (is_bool($_))
+ return $_ ? ['AND', [[$field, $mask, '&'], $mask, '!'], [$field, 0, '>']] : ['OR', [[$field, $mask, '&'], $mask], [$field, 0]];
+ else if (is_int($_))
+ return ['AND', [[$field, $mask, '&'], $mask, '!'], [$field, 1 << ($_ - 1), '&']];
+
+ return false;
+ }
+
+ protected function cbDamageType($cr)
+ {
+ if (!$this->checkInput(FILTER_V_RANGE, [0, 6], $cr[1]))
+ return false;
+
+ return ['OR', ['dmgType1', $cr[1]], ['dmgType2', $cr[1]]];
+ }
+
+ protected function cbArmorBonus($cr)
+ {
+ if (!Util::checkNumeric($cr[2], NUM_CAST_FLOAT) || !$this->int2Op($cr[1]))
+ return false;
+
+ $this->formData['extraCols'][] = $cr[0];
+ return ['AND', ['armordamagemodifier', $cr[2], $cr[1]], ['class', ITEM_CLASS_ARMOR]];
+ }
+
+ protected function cbCraftedByProf($cr)
+ {
+ if (!isset($this->enums[99][$cr[1]]))
+ return false;
+
+ $_ = $this->enums[99][$cr[1]];
+ if (is_bool($_))
+ return ['src.src1', null, $_ ? '!' : null];
+ else if (is_int($_))
+ return ['s.skillLine1', $_];
+
+ return false;
+ }
+
+ protected function cbQuestRewardIn($cr)
+ {
+ if (in_array($cr[1], $this->enums[$cr[0]]))
+ return ['AND', ['src.src4', null, '!'], ['src.moreZoneId', $cr[1]]];
+ else if ($cr[1] == FILTER_ENUM_ANY)
+ return ['src.src4', null, '!']; // well, this seems a bit redundant..
+
+ return false;
+ }
+
+ protected function cbPurchasableWith($cr)
+ {
+ if (in_array($cr[1], $this->enums[$cr[0]]))
+ $_ = (array)$cr[1];
+ else if ($cr[1] == FILTER_ENUM_ANY)
+ $_ = $this->enums[$cr[0]];
+ else
+ return false;
+
+ $costs = DB::Aowow()->selectCol(
+ 'SELECT id FROM ?_itemextendedcost WHERE reqItemId1 IN (?a) OR reqItemId2 IN (?a) OR reqItemId3 IN (?a) OR reqItemId4 IN (?a) OR reqItemId5 IN (?a)',
+ $_, $_, $_, $_, $_
+ );
+ if ($items = DB::World()->selectCol($this->extCostQuery, $costs, $costs))
+ return ['id', $items];
+ }
+
+ protected function cbSoldByNPC($cr)
+ {
+ if (!Util::checkNumeric($cr[2], NUM_CAST_INT))
+ return false;
+
+ if ($iIds = DB::World()->selectCol('SELECT item FROM npc_vendor WHERE entry = ?d UNION SELECT item FROM game_event_npc_vendor v JOIN creature c ON c.guid = v.guid WHERE c.id = ?d', $cr[2], $cr[2]))
+ return ['i.id', $iIds];
+ else
+ return [0];
+ }
+
+ protected function cbAvgBuyout($cr)
+ {
+ if (!Util::checkNumeric($cr[2], NUM_CAST_INT) || !$this->int2Op($cr[1]))
+ return false;
+
+ foreach (Profiler::getRealms() as $rId => $__)
+ {
+ // todo: do something sensible..
+ // // todo (med): get the avgbuyout into the listview
+ // if ($_ = DB::Characters()->select('SELECT ii.itemEntry AS ARRAY_KEY, AVG(ah.buyoutprice / ii.count) AS buyout FROM auctionhouse ah JOIN item_instance ii ON ah.itemguid = ii.guid GROUP BY ii.itemEntry HAVING avgbuyout '.$cr[1].' ?f', $c[1]))
+ // return ['i.id', array_keys($_)];
+ // else
+ // return [0];
+ return [1];
+ }
+
+ return [0];
+ }
+
+ protected function cbAvgMoneyContent($cr)
+ {
+ if (!Util::checkNumeric($cr[2], NUM_CAST_INT) || !$this->int2Op($cr[1]))
+ return false;
+
+ $this->formData['extraCols'][] = $cr[0];
+ return ['AND', ['flags', ITEM_FLAG_OPENABLE, '&'], ['((minMoneyLoot + maxMoneyLoot) / 2)', $cr[2], $cr[1]]];
+ }
+
+ protected function cbCooldown($cr)
+ {
+ if (!Util::checkNumeric($cr[2], NUM_CAST_INT) || !$this->int2Op($cr[1]))
+ return false;
+
+ $cr[2] *= 1000; // field supplied in milliseconds
+
+ $this->formData['extraCols'][] = $cr[0];
+ $this->extraOpts['is']['s'][] = ', IF(spellCooldown1 > 1, spellCooldown1, IF(spellCooldown2 > 1, spellCooldown2, IF(spellCooldown3 > 1, spellCooldown3, IF(spellCooldown4 > 1, spellCooldown4, IF(spellCooldown5 > 1, spellCooldown5,))))) AS cooldown';
+
+ return [
+ 'OR',
+ ['AND', ['spellTrigger1', 0], ['spellId1', 0, '!'], ['spellCooldown1', 0, '>'], ['spellCooldown1', $cr[2], $cr[1]]],
+ ['AND', ['spellTrigger2', 0], ['spellId2', 0, '!'], ['spellCooldown2', 0, '>'], ['spellCooldown2', $cr[2], $cr[1]]],
+ ['AND', ['spellTrigger3', 0], ['spellId3', 0, '!'], ['spellCooldown3', 0, '>'], ['spellCooldown3', $cr[2], $cr[1]]],
+ ['AND', ['spellTrigger4', 0], ['spellId4', 0, '!'], ['spellCooldown4', 0, '>'], ['spellCooldown4', $cr[2], $cr[1]]],
+ ['AND', ['spellTrigger5', 0], ['spellId5', 0, '!'], ['spellCooldown5', 0, '>'], ['spellCooldown5', $cr[2], $cr[1]]],
+ ];
+ }
+
+ protected function cbQuestRelation($cr)
+ {
+ switch ($cr[1])
+ {
+ case 1: // any
+ return ['startQuest', 0, '>'];
+ case 2: // exclude horde only
+ return ['AND', ['startQuest', 0, '>'], [['flagsExtra', 0x3, '&'], 2]];
+ case 3: // exclude alliance only
+ return ['AND', ['startQuest', 0, '>'], [['flagsExtra', 0x3, '&'], 1]];
+ case 4: // both
+ return ['AND', ['startQuest', 0, '>'], [['flagsExtra', 0x3, '&'], 0]];
+ case 5: // none
+ return ['startQuest', 0];
+ }
+
+ return false;
+ }
+
+ protected function cbFieldHasVal($cr, $field, $val)
+ {
+ if ($this->int2Bool($cr[1]))
+ return [$field, $val, $cr[1] ? null : '!'];
+
+ return false;
+ }
+
+ protected function cbObtainedBy($cr, $field)
+ {
+ if ($this->int2Bool($cr[1]))
+ return ['src.src'.$this->otFields[$cr[0]], null, $cr[1] ? '!' : null];
+
+ return false;
+ }
+
+ protected function cbPvpPurchasable($cr, $field)
+ {
+ if (!$this->int2Bool($cr[1]))
+ return false;
+
+ $costs = DB::Aowow()->selectCol('SELECT id FROM ?_itemextendedcost WHERE ?# > 0', $field);
+ if ($items = DB::World()->selectCol($this->extCostQuery, $costs, $costs))
+ return ['id', $items, $cr[1] ? null : '!'];
+
+ return false;
+ }
+
+ protected function cbDisenchantsInto($cr)
+ {
+ if (!Util::checkNumeric($cr[1], NUM_REQ_INT))
+ return false;
+
+ if (!in_array($cr[1], $this->enums[$cr[0]]))
+ return false;
+
+ $refResults = [];
+ $newRefs = DB::World()->selectCol('SELECT entry FROM ?# WHERE item = ?d AND reference = 0', LOOT_REFERENCE, $cr[1]);
+ while ($newRefs)
+ {
+ $refResults += $newRefs;
+ $newRefs = DB::World()->selectCol('SELECT entry FROM ?# WHERE reference IN (?a)', LOOT_REFERENCE, $newRefs);
+ }
+
+ $lootIds = DB::World()->selectCol('SELECT entry FROM ?# WHERE {reference IN (?a) OR }(reference = 0 AND item = ?d)', LOOT_DISENCHANT, $refResults ?: DBSIMPLE_SKIP, $cr[1]);
+
+ return $lootIds ? ['disenchantId', $lootIds] : [0];
+ }
+
+ protected function cbObjectiveOfQuest($cr)
+ {
+ $w = '';
+ switch ($cr[1])
+ {
+ case 1: // Yes
+ case 5: // No
+ $w = 1;
+ return;
+ case 2: // Alliance
+ $w = 'reqRaceMask & '.RACE_MASK_ALLIANCE.' AND (reqRaceMask & '.RACE_MASK_HORDE.') = 0';
+ break;
+ case 3: // Horde
+ $w = 'reqRaceMask & '.RACE_MASK_HORDE.' AND (reqRaceMask & '.RACE_MASK_ALLIANCE.') = 0';
+ break;
+ case 4: // Both
+ $w = '(reqRaceMask & '.RACE_MASK_ALLIANCE.' AND reqRaceMask & '.RACE_MASK_HORDE.') OR reqRaceMask = 0';
+ break;
+ default:
+ return false;
+ }
+
+ $itemIds = DB::Aowow()->selectCol(sprintf('
+ SELECT reqItemId1 FROM ?_quests WHERE %1$s UNION SELECT reqItemId2 FROM ?_quests WHERE %1$s UNION
+ SELECT reqItemId3 FROM ?_quests WHERE %1$s UNION SELECT reqItemId4 FROM ?_quests WHERE %1$s UNION
+ SELECT reqItemId5 FROM ?_quests WHERE %1$s UNION SELECT reqItemId6 FROM ?_quests WHERE %1$s',
+ $w
+ ));
+
+ if ($itemIds)
+ return ['id', $itemIds, $cr[1] == 5 ? '!' : null];
+
+ return [0];
+ }
+
+ protected function cbReagentForAbility($cr)
+ {
+ if (!isset($this->enums[99][$cr[1]]))
+ return false;
+
+ $_ = $this->enums[99][$cr[1]];
+ if ($_ === null)
+ return false;
+
+ $ids = [];
+ $spells = DB::Aowow()->select( // todo (med): hmm, selecting all using SpellList would exhaust 128MB of memory :x .. see, that we only select the fields that are really needed
+ 'SELECT reagent1, reagent2, reagent3, reagent4, reagent5, reagent6, reagent7, reagent8,
+ reagentCount1, reagentCount2, reagentCount3, reagentCount4, reagentCount5, reagentCount6, reagentCount7, reagentCount8
+ FROM ?_spell
+ WHERE skillLine1 IN (?a)',
+ is_bool($_) ? array_filter($this->enums[99], "is_numeric") : $_
+ );
+ foreach ($spells as $spell)
+ for ($i = 1; $i < 9; $i++)
+ if ($spell['reagent'.$i] > 0 && $spell['reagentCount'.$i] > 0)
+ $ids[] = $spell['reagent'.$i];
+
+ if (empty($ids))
+ return [0];
+ else if ($_)
+ return ['id', $ids];
+ else
+ return ['id', $ids, '!'];
+ }
+
+ protected function cbSource($cr)
+ {
+ if (!isset($this->enums[$cr[0]][$cr[1]]))
+ return false;
+
+ $_ = $this->enums[$cr[0]][$cr[1]];
+ if (is_int($_)) // specific
+ return ['src.src'.$_, null, '!'];
+ else if ($_) // any
+ {
+ $foo = ['OR'];
+ foreach ($this->enums[$cr[0]] as $bar)
+ if (is_int($bar))
+ $foo[] = ['src.src'.$bar, null, '!'];
+
+ return $foo;
+ }
+ else // none
+ {
+ $foo = ['AND'];
+ foreach ($this->enums[$cr[0]] as $bar)
+ if (is_int($bar))
+ $foo[] = ['src.src'.$bar, null];
+
+ return $foo;
+ }
+ }
+
+ protected function cbTypeCheck(&$v)
+ {
+ if (!$this->parentCats)
+ return false;
+
+ if (!Util::checkNumeric($v, NUM_REQ_INT))
+ return false;
+
+ $c = $this->parentCats;
+
+ if (isset($c[2]) && is_array(Lang::item('cat', $c[0], 1, $c[1])))
+ $catList = Lang::item('cat', $c[0], 1, $c[1], 1, $c[2]);
+ else if (isset($c[1]) && is_array(Lang::item('cat', $c[0])))
+ $catList = Lang::item('cat', $c[0], 1, $c[1]);
+ else
+ $catList = Lang::item('cat', $c[0]);
+
+ // consumables - always
+ if ($c[0] == 0)
+ return in_array($v, array_keys(Lang::item('cat', 0, 1)));
+ // weapons - only if parent
+ else if ($c[0] == 2 && !isset($c[1]))
+ return in_array($v, array_keys(Lang::spell('weaponSubClass')));
+ // armor - only if parent
+ else if ($c[0] == 4 && !isset($c[1]))
+ return in_array($v, array_keys(Lang::item('cat', 4, 1)));
+ // uh ... other stuff...
+ else if (in_array($c[0], [1, 3, 7, 9, 15]) && !isset($c[1]))
+ return in_array($v, array_keys($catList[1]));
+
+ return false;
+ }
+
+ protected function cbSlotCheck(&$v)
+ {
+ if (!Util::checkNumeric($v, NUM_REQ_INT))
+ return false;
+
+ // todo (low): limit to concrete slots
+ $sl = array_keys(Lang::item('inventoryType'));
+ $c = $this->parentCats;
+
+ // no selection
+ if (!isset($c[0]))
+ return in_array($v, $sl);
+
+ // consumables - any; perm / temp item enhancements
+ else if ($c[0] == 0 && (!isset($c[1]) || in_array($c[1], [-3, 6])))
+ return in_array($v, $sl);
+
+ // weapons - always
+ else if ($c[0] == 2)
+ return in_array($v, $sl);
+
+ // armor - any; any armor
+ else if ($c[0] == 4 && (!isset($c[1]) || in_array($c[1], [1, 2, 3, 4])))
+ return in_array($v, $sl);
+
+ return false;
+ }
+
+ protected function cbWeightKeyCheck(&$v)
+ {
+ if (preg_match('/\W/i', $v))
+ return false;
+
+ return isset(Util::$itemFilter[$v]);
+ }
+}
+
+?>
diff --git a/includes/types/itemset.class.php b/includes/types/itemset.class.php
new file mode 100644
index 00000000..87fd3acb
--- /dev/null
+++ b/includes/types/itemset.class.php
@@ -0,0 +1,259 @@
+ ['o' => 'maxlevel DESC'],
+ 'e' => ['j' => ['?_events e ON e.id = `set`.eventId', true], 's' => ', e.holidayId']
+ );
+
+ public function __construct($conditions = [])
+ {
+ parent::__construct($conditions);
+
+ // post processing
+ foreach ($this->iterate() as &$_curTpl)
+ {
+ $_curTpl['classes'] = [];
+ $_curTpl['pieces'] = [];
+ for ($i = 1; $i < 12; $i++)
+ {
+ if ($_curTpl['classMask'] & (1 << ($i - 1)))
+ {
+ $this->classes[] = $i;
+ $_curTpl['classes'][] = $i;
+ }
+ }
+
+ for ($i = 1; $i < 10; $i++)
+ {
+ if ($piece = $_curTpl['item'.$i])
+ {
+ $_curTpl['pieces'][] = $piece;
+ $this->pieceToSet[$piece] = $this->id;
+ }
+ }
+ }
+ $this->classes = array_unique($this->classes);
+ }
+
+ public function getListviewData()
+ {
+ $data = [];
+
+ foreach ($this->iterate() as $__)
+ {
+ $data[$this->id] = array(
+ 'id' => $this->id,
+ 'idbak' => $this->curTpl['refSetId'],
+ 'name' => (7 - $this->curTpl['quality']).$this->getField('name', true),
+ 'minlevel' => $this->curTpl['minLevel'],
+ 'maxlevel' => $this->curTpl['maxLevel'],
+ 'note' => $this->curTpl['contentGroup'],
+ 'type' => $this->curTpl['type'],
+ 'reqclass' => $this->curTpl['classMask'],
+ 'classes' => $this->curTpl['classes'],
+ 'pieces' => $this->curTpl['pieces'],
+ 'heroic' => $this->curTpl['heroic']
+ );
+ }
+
+ return $data;
+ }
+
+ public function getJSGlobals($addMask = GLOBALINFO_ANY)
+ {
+ $data = [];
+
+ if ($this->classes && ($addMask & GLOBALINFO_RELATED))
+ $data[TYPE_CLASS] = array_combine($this->classes, $this->classes);
+
+ if ($this->pieceToSet && ($addMask & GLOBALINFO_SELF))
+ $data[TYPE_ITEM] = array_combine(array_keys($this->pieceToSet), array_keys($this->pieceToSet));
+
+ if ($addMask & GLOBALINFO_SELF)
+ foreach ($this->iterate() as $id => $__)
+ $data[TYPE_ITEMSET][$id] = ['name' => $this->getField('name', true)];
+
+ return $data;
+ }
+
+ public function renderTooltip()
+ {
+ if (!$this->curTpl)
+ return array();
+
+ $x = '';
+ $x .= ''.Util::jsEscape($this->getField('name', true)).' ';
+
+ $nClasses = 0;
+ if ($_ = $this->getField('classMask'))
+ {
+ $cl = Lang::getClassString($_, $__, $nClasses);
+ $x .= Util::ucFirst($nClasses > 1 ? Lang::game('classes') : Lang::game('class')).Lang::main('colon').$cl.' ';
+ }
+
+ if ($_ = $this->getField('contentGroup'))
+ $x .= Util::jsEscape(Lang::itemset('notes', $_)).($this->getField('heroic') ? ' ('.Lang::item('heroic').')' : '').' ';
+
+ if (!$nClasses || !$this->getField('contentGroup'))
+ $x.= Lang::itemset('types', $this->getField('type')).' ';
+
+ if ($bonuses = $this->getBonuses())
+ {
+ $x .= '';
+
+ foreach ($bonuses as $b)
+ $x .= ' '.$b['bonus'].' '.Lang::itemset('_pieces').Lang::main('colon').''.Util::jsEscape($b['desc']);
+
+ $x .= '';
+ }
+
+ $x .= ' | ';
+
+ return $x;
+ }
+
+ public function getBonuses()
+ {
+ $spells = [];
+ for ($i = 1; $i < 9; $i++)
+ {
+ $spl = $this->getField('spell'.$i);
+ $qty = $this->getField('bonus'.$i);
+
+ // cant use spell as index, would change order
+ if ($spl && $qty)
+ $spells[] = ['id' => $spl, 'bonus' => $qty];
+ }
+
+ // sort by required pieces ASC
+ usort($spells, function($a, $b) {
+ if ($a['bonus'] == $b['bonus'])
+ return 0;
+
+ return ($a['bonus'] > $b['bonus']) ? 1 : -1;
+ });
+
+ $setSpells = new SpellList(array(['s.id', array_column($spells, 'id')]));
+ foreach ($setSpells->iterate() as $spellId => $__)
+ {
+ foreach ($spells as &$s)
+ {
+ if ($spellId != $s['id'])
+ continue;
+
+ $s['desc'] = $setSpells->parseText('description', $this->getField('reqLevel') ?: MAX_LEVEL)[0];
+ }
+ }
+
+ return $spells;
+ }
+}
+
+
+// missing filter: "Available to Players"
+class ItemsetListFilter extends Filter
+{
+ // cr => [type, field, misc, extraCol]
+ protected $genericFilter = array( // misc (bool): _NUMERIC => useFloat; _STRING => localized; _FLAG => match Value; _BOOLEAN => stringSet
+ 2 => [FILTER_CR_NUMERIC, 'id', NUM_CAST_INT, true], // id
+ 3 => [FILTER_CR_NUMERIC, 'npieces', NUM_CAST_INT ], // pieces
+ 4 => [FILTER_CR_STRING, 'bonusText', STR_LOCALIZED ], // bonustext
+ 5 => [FILTER_CR_BOOLEAN, 'heroic', ], // heroic
+ 6 => [FILTER_CR_ENUM, 'e.holidayId', ], // relatedevent
+ 8 => [FILTER_CR_FLAG, 'cuFlags', CUSTOM_HAS_COMMENT ], // hascomments
+ 9 => [FILTER_CR_FLAG, 'cuFlags', CUSTOM_HAS_SCREENSHOT ], // hasscreenshots
+ 10 => [FILTER_CR_FLAG, 'cuFlags', CUSTOM_HAS_VIDEO ], // hasvideos
+ 12 => [FILTER_CR_NYI_PH, null, 1 ] // available to players [yn] - ugh .. scan loot, quest and vendor templates and write to ?_itemset
+ );
+
+ // fieldId => [checkType, checkValue[, fieldIsArray]]
+ protected $inputFields = array(
+ 'cr' => [FILTER_V_RANGE, [2, 12], true ], // criteria ids
+ 'crs' => [FILTER_V_LIST, [FILTER_ENUM_NONE, FILTER_ENUM_ANY, [0, 424]], true ], // criteria operators
+ 'crv' => [FILTER_V_REGEX, '/[\p{C};:]/ui', true ], // criteria values - only printable chars, no delimiters
+ 'na' => [FILTER_V_REGEX, '/[\p{C};]/ui', false], // name / description - only printable chars, no delimiter
+ 'ma' => [FILTER_V_EQUAL, 1, false], // match any / all filter
+ 'qu' => [FILTER_V_RANGE, [0, 7], true ], // quality
+ 'ty' => [FILTER_V_RANGE, [1, 12], true ], // set type
+ 'minle' => [FILTER_V_RANGE, [1, 999], false], // min item level
+ 'maxle' => [FILTER_V_RANGE, [1, 999], false], // max itemlevel
+ 'minrl' => [FILTER_V_RANGE, [1, MAX_LEVEL], false], // min required level
+ 'maxrl' => [FILTER_V_RANGE, [1, MAX_LEVEL], false], // max required level
+ 'cl' => [FILTER_V_LIST, [[1, 9], 11], false], // class
+ 'ta' => [FILTER_V_RANGE, [1, 30], false] // tag / content group
+ );
+
+ protected function createSQLForCriterium(&$cr)
+ {
+ if (in_array($cr[0], array_keys($this->genericFilter)))
+ if ($genCR = $this->genericCriterion($cr))
+ return $genCR;
+
+ unset($cr);
+ $this->error = true;
+ return [1];
+ }
+
+ protected function createSQLForValues()
+ {
+ $parts = [];
+ $_v = &$this->fiData['v'];
+
+ // name [str]
+ if (isset($_v['na']))
+ if ($_ = $this->modularizeString(['name_loc'.User::$localeId]))
+ $parts[] = $_;
+
+ // quality [enum]
+ if (isset($_v['qu']))
+ $parts[] = ['quality', $_v['qu']];
+
+ // type [enum]
+ if (isset($_v['ty']))
+ $parts[] = ['type', $_v['ty']];
+
+ // itemLevel min [int]
+ if (isset($_v['minle']))
+ $parts[] = ['minLevel', $_v['minle'], '>='];
+
+ // itemLevel max [int]
+ if (isset($_v['maxle']))
+ $parts[] = ['maxLevel', $_v['maxle'], '<='];
+
+ // reqLevel min [int]
+ if (isset($_v['minrl']))
+ $parts[] = ['reqLevel', $_v['minrl'], '>='];
+
+ // reqLevel max [int]
+ if (isset($_v['maxrl']))
+ $parts[] = ['reqLevel', $_v['maxrl'], '<='];
+
+ // class [enum]
+ if (isset($_v['cl']))
+ $parts[] = ['classMask', $this->list2Mask([$_v['cl']]), '&'];
+
+ // tag [enum]
+ if (isset($_v['ta']))
+ $parts[] = ['contentGroup', intVal($_v['ta'])];
+
+ return $parts;
+ }
+}
+
+?>
diff --git a/includes/dbtypes/pet.class.php b/includes/types/pet.class.php
similarity index 66%
rename from includes/dbtypes/pet.class.php
rename to includes/types/pet.class.php
index 86834fa5..0b27cd2e 100644
--- a/includes/dbtypes/pet.class.php
+++ b/includes/types/pet.class.php
@@ -1,26 +1,24 @@
[['ic']],
- 'ic' => ['j' => ['::icons ic ON p.`iconId` = ic.`id`', true], 's' => ', ic.`name` AS "iconString"'],
+ 'ic' => ['j' => ['?_icons ic ON p.iconId = ic.id', true], 's' => ', ic.name AS iconString'],
);
- public function getListviewData() : array
+ public function getListviewData()
{
$data = [];
@@ -52,7 +50,7 @@ class PetList extends DBTypeList
return $data;
}
- public function getJSGlobals(int $addMask = GLOBALINFO_ANY) : array
+ public function getJSGlobals($addMask = GLOBALINFO_ANY)
{
$data = [];
@@ -61,16 +59,16 @@ class PetList extends DBTypeList
if ($addMask & GLOBALINFO_RELATED)
for ($i = 1; $i <= 4; $i++)
if ($this->curTpl['spellId'.$i] > 0)
- $data[Type::SPELL][$this->curTpl['spellId'.$i]] = $this->curTpl['spellId'.$i];
+ $data[TYPE_SPELL][$this->curTpl['spellId'.$i]] = $this->curTpl['spellId'.$i];
if ($addMask & GLOBALINFO_SELF)
- $data[Type::PET][$this->id] = ['icon' => $this->curTpl['iconString']];
+ $data[TYPE_PET][$this->id] = ['icon' => $this->curTpl['iconString']];
}
return $data;
}
- public function renderTooltip() : ?string { return null; }
+ public function renderTooltip() { }
}
?>
diff --git a/includes/types/profile.class.php b/includes/types/profile.class.php
new file mode 100644
index 00000000..02561231
--- /dev/null
+++ b/includes/types/profile.class.php
@@ -0,0 +1,711 @@
+iterate() as $__)
+ {
+ if ($this->getField('user') && User::$id != $this->getField('user') && !($this->getField('cuFlags') & PROFILER_CU_PUBLISHED))
+ continue;
+
+ if (($addInfo & PROFILEINFO_PROFILE) && !$this->isCustom())
+ continue;
+
+ if (($addInfo & PROFILEINFO_CHARACTER) && $this->isCustom())
+ continue;
+
+ $data[$this->id] = array(
+ 'id' => $this->getField('id'),
+ 'name' => $this->getField('name'),
+ 'race' => $this->getField('race'),
+ 'classs' => $this->getField('class'),
+ 'gender' => $this->getField('gender'),
+ 'level' => $this->getField('level'),
+ 'faction' => (1 << ($this->getField('race') - 1)) & RACE_MASK_ALLIANCE ? 0 : 1,
+ 'talenttree1' => $this->getField('talenttree1'),
+ 'talenttree2' => $this->getField('talenttree2'),
+ 'talenttree3' => $this->getField('talenttree3'),
+ 'talentspec' => $this->getField('activespec') + 1, // 0 => 1; 1 => 2
+ 'achievementpoints' => $this->getField('achievementpoints'),
+ 'guild' => '$"'.$this->getField('guildname').'"', // force this to be a string
+ 'guildrank' => $this->getField('guildrank'),
+ 'realm' => Profiler::urlize($this->getField('realmName')),
+ 'realmname' => $this->getField('realmName'),
+ // 'battlegroup' => Profiler::urlize($this->getField('battlegroup')), // was renamed to subregion somewhere around cata release
+ // 'battlegroupname' => $this->getField('battlegroup'),
+ 'gearscore' => $this->getField('gearscore')
+ );
+
+
+ // for the lv this determins if the link is profile= or profile=..
+ if ($this->isCustom())
+ $data[$this->id]['published'] = (int)!!($this->getField('cuFlags') & PROFILER_CU_PUBLISHED);
+ else
+ $data[$this->id]['region'] = Profiler::urlize($this->getField('region'));
+
+ if ($addInfo & PROFILEINFO_ARENA)
+ {
+ $data[$this->id]['rating'] = $this->getField('rating');
+ $data[$this->id]['captain'] = $this->getField('captain');
+ $data[$this->id]['games'] = $this->getField('seasonGames');
+ $data[$this->id]['wins'] = $this->getField('seasonWins');
+ }
+
+ // Filter asked for skills - add them
+ foreach ($reqCols as $col)
+ $data[$this->id][$col] = $this->getField($col);
+
+ if ($addInfo & PROFILEINFO_PROFILE)
+ if ($_ = $this->getField('description'))
+ $data[$this->id]['description'] = $_;
+
+ if ($addInfo & PROFILEINFO_PROFILE)
+ if ($_ = $this->getField('icon'))
+ $data[$this->id]['icon'] = $_;
+
+ if ($this->getField('cuFlags') & PROFILER_CU_PINNED)
+ $data[$this->id]['pinned'] = 1;
+
+ if ($this->getField('cuFlags') & PROFILER_CU_DELETED)
+ $data[$this->id]['deleted'] = 1;
+ }
+
+ return array_values($data);
+ }
+
+ public function renderTooltip($interactive = false)
+ {
+ if (!$this->curTpl)
+ return [];
+
+ $title = '';
+ $name = $this->getField('name');
+ if ($_ = $this->getField('chosenTitle'))
+ $title = (new TitleList(array(['bitIdx', $_])))->getField($this->getField('gender') ? 'female' : 'male', true);
+
+ if ($this->isCustom())
+ $name .= ' (Custom Profile)';
+ else if ($title)
+ $name = sprintf($title, $name);
+
+ $x = '';
+ $x .= '| '.$name.' | ';
+ if ($g = $this->getField('guildname'))
+ $x .= '| <'.$g.'> | ';
+ else if ($d = $this->getField('description'))
+ $x .= '| '.$d.' | ';
+ $x .= '| '.Lang::game('level').' '.$this->getField('level').' '.Lang::game('ra', $this->getField('race')).' '.Lang::game('cl', $this->getField('class')).' | ';
+ $x .= ' ';
+
+ return $x;
+ }
+
+ public function getJSGlobals($addMask = 0)
+ {
+ $data = [];
+ $realms = Profiler::getRealms();
+
+ foreach ($this->iterate() as $id => $__)
+ {
+ if (($addMask & PROFILEINFO_PROFILE) && ($this->getField('cuFlags') & PROFILER_CU_PROFILE))
+ {
+ $profile = array(
+ 'id' => $this->getField('id'),
+ 'name' => $this->getField('name'),
+ 'race' => $this->getField('race'),
+ 'classs' => $this->getField('class'),
+ 'level' => $this->getField('level'),
+ 'gender' => $this->getField('gender')
+ );
+
+ if ($_ = $this->getField('icon'))
+ $profile['icon'] = $_;
+
+ $data[] = $profile;
+
+ continue;
+ }
+
+ if ($addMask & PROFILEINFO_CHARACTER && !($this->getField('cuFlags') & PROFILER_CU_PROFILE))
+ {
+ if (!isset($realms[$this->getField('realm')]))
+ continue;
+
+ $data[] = array(
+ 'id' => $this->getField('id'),
+ 'name' => $this->getField('name'),
+ 'realmname' => $realms[$this->getField('realm')]['name'],
+ 'region' => $realms[$this->getField('realm')]['region'],
+ 'realm' => Profiler::urlize($realms[$this->getField('realm')]['name']),
+ 'race' => $this->getField('race'),
+ 'classs' => $this->getField('class'),
+ 'level' => $this->getField('level'),
+ 'gender' => $this->getField('gender'),
+ 'pinned' => $this->getField('cuFlags') & PROFILER_CU_PINNED ? 1 : 0
+ );
+ }
+ }
+
+ return $data;
+ }
+
+ public function isCustom()
+ {
+ return $this->getField('cuFlags') & PROFILER_CU_PROFILE;
+ }
+}
+
+
+class ProfileListFilter extends Filter
+{
+ public $useLocalList = false;
+ public $extraOpts = [];
+
+ private $realms = [];
+
+ protected $enums = array(
+ -1 => array( // arena team sizes
+ // by name by rating by contrib
+ 12 => 2, 13 => 2, 14 => 2,
+ 15 => 3, 16 => 3, 17 => 3,
+ 18 => 5, 19 => 5, 20 => 5
+ )
+ );
+
+ protected $genericFilter = array( // misc (bool): _NUMERIC => useFloat; _STRING => localized; _FLAG => match Value; _BOOLEAN => stringSet
+ 2 => [FILTER_CR_NUMERIC, 'gearscore', NUM_CAST_INT ], // gearscore [num]
+ 3 => [FILTER_CR_NUMERIC, 'achievementpoints', NUM_CAST_INT ], // achievementpoints [num]
+ 5 => [FILTER_CR_NUMERIC, 'talenttree1', NUM_CAST_INT ], // talenttree1 [num]
+ 6 => [FILTER_CR_NUMERIC, 'talenttree2', NUM_CAST_INT ], // talenttree2 [num]
+ 7 => [FILTER_CR_NUMERIC, 'talenttree3', NUM_CAST_INT ], // talenttree3 [num]
+ 9 => [FILTER_CR_STRING, 'g.name', ], // guildname
+ 10 => [FILTER_CR_CALLBACK, 'cbHasGuildRank', null, null], // guildrank
+ 12 => [FILTER_CR_CALLBACK, 'cbTeamName', null, null], // teamname2v2
+ 15 => [FILTER_CR_CALLBACK, 'cbTeamName', null, null], // teamname3v3
+ 18 => [FILTER_CR_CALLBACK, 'cbTeamName', null, null], // teamname5v5
+ 13 => [FILTER_CR_CALLBACK, 'cbTeamRating', null, null], // teamrtng2v2
+ 16 => [FILTER_CR_CALLBACK, 'cbTeamRating', null, null], // teamrtng3v3
+ 19 => [FILTER_CR_CALLBACK, 'cbTeamRating', null, null], // teamrtng5v5
+ 14 => [FILTER_CR_NYI_PH, 0 ], // teamcontrib2v2 [num]
+ 17 => [FILTER_CR_NYI_PH, 0 ], // teamcontrib3v3 [num]
+ 20 => [FILTER_CR_NYI_PH, 0 ], // teamcontrib5v5 [num]
+ 21 => [FILTER_CR_CALLBACK, 'cbWearsItems', null, null], // wearingitem [str]
+ 23 => [FILTER_CR_CALLBACK, 'cbCompletedAcv', null, null], // completedachievement
+ 25 => [FILTER_CR_CALLBACK, 'cbProfession', 171, null], // alchemy [num]
+ 26 => [FILTER_CR_CALLBACK, 'cbProfession', 164, null], // blacksmithing [num]
+ 27 => [FILTER_CR_CALLBACK, 'cbProfession', 333, null], // enchanting [num]
+ 28 => [FILTER_CR_CALLBACK, 'cbProfession', 202, null], // engineering [num]
+ 29 => [FILTER_CR_CALLBACK, 'cbProfession', 182, null], // herbalism [num]
+ 30 => [FILTER_CR_CALLBACK, 'cbProfession', 773, null], // inscription [num]
+ 31 => [FILTER_CR_CALLBACK, 'cbProfession', 755, null], // jewelcrafting [num]
+ 32 => [FILTER_CR_CALLBACK, 'cbProfession', 165, null], // leatherworking [num]
+ 33 => [FILTER_CR_CALLBACK, 'cbProfession', 186, null], // mining [num]
+ 34 => [FILTER_CR_CALLBACK, 'cbProfession', 393, null], // skinning [num]
+ 35 => [FILTER_CR_CALLBACK, 'cbProfession', 197, null], // tailoring [num]
+ 36 => [FILTER_CR_CALLBACK, 'cbHasGuild', null, null] // hasguild [yn]
+ );
+
+
+ // fieldId => [checkType, checkValue[, fieldIsArray]]
+ protected $inputFields = array(
+ 'cr' => [FILTER_V_RANGE, [1, 36], true ], // criteria ids
+ 'crs' => [FILTER_V_LIST, [FILTER_ENUM_NONE, FILTER_ENUM_ANY, [0, 5000]], true ], // criteria operators
+ 'crv' => [FILTER_V_REGEX, '/[\p{C};]/ui', true ], // criteria values
+ 'na' => [FILTER_V_REGEX, '/[\p{C};]/ui', false], // name - only printable chars, no delimiter
+ 'ma' => [FILTER_V_EQUAL, 1, false], // match any / all filter
+ 'ex' => [FILTER_V_EQUAL, 'on', false], // only match exact
+ 'si' => [FILTER_V_LIST, [1, 2], false], // side
+ 'ra' => [FILTER_V_LIST, [[1, 8], 10, 11], true ], // race
+ 'cl' => [FILTER_V_LIST, [[1, 9], 11], true ], // class
+ 'minle' => [FILTER_V_RANGE, [1, MAX_LEVEL], false], // min level
+ 'maxle' => [FILTER_V_RANGE, [1, MAX_LEVEL], false], // max level
+ 'rg' => [FILTER_V_CALLBACK, 'cbRegionCheck', false], // region
+ 'sv' => [FILTER_V_CALLBACK, 'cbServerCheck', false], // server
+ );
+
+ /* heads up!
+ a couple of filters are too complex to be run against the characters database
+ if they are selected, force useage of LocalProfileList
+ */
+
+ public function __construct($fromPOST = false, $opts = [])
+ {
+ if (!empty($opts['realms']))
+ $this->realms = $opts['realms'];
+ else
+ $this->realms = array_keys(Profiler::getRealms());
+
+ parent::__construct($fromPOST, $opts);
+
+ if (!empty($this->fiData['c']['cr']))
+ if (array_intersect($this->fiData['c']['cr'], [2, 3, 5, 6, 7, 21]))
+ $this->useLocalList = true;
+ }
+
+ protected function createSQLForCriterium(&$cr)
+ {
+ if (in_array($cr[0], array_keys($this->genericFilter)))
+ if ($genCR = $this->genericCriterion($cr))
+ return $genCR;
+
+ unset($cr);
+ $this->error = true;
+ return [1];
+ }
+
+ protected function createSQLForValues()
+ {
+ $parts = [];
+ $_v = $this->fiData['v'];
+
+ // region (rg), battlegroup (bg) and server (sv) are passed to ProflieList as miscData and handled there
+
+ // table key differs between remote and local :<
+ $k = $this->useLocalList ? 'p' : 'c';
+
+ // name [str] - the table is case sensitive. Since i down't want to destroy indizes, lets alter the search terms
+ if (!empty($_v['na']))
+ {
+ $lower = $this->modularizeString([$k.'.name'], Util::lower($_v['na']), !empty($_v['ex']) && $_v['ex'] == 'on');
+ $proper = $this->modularizeString([$k.'.name'], Util::ucWords($_v['na']), !empty($_v['ex']) && $_v['ex'] == 'on');
+
+ $parts[] = ['OR', $lower, $proper];
+ }
+
+ // side [list]
+ if (!empty($_v['si']))
+ {
+ if ($_v['si'] == 1)
+ $parts[] = [$k.'.race', [1, 3, 4, 7, 11]];
+ else if ($_v['si'] == 2)
+ $parts[] = [$k.'.race', [2, 5, 6, 8, 10]];
+ }
+
+ // race [list]
+ if (!empty($_v['ra']))
+ $parts[] = [$k.'.race', $_v['ra']];
+
+ // class [list]
+ if (!empty($_v['cl']))
+ $parts[] = [$k.'.class', $_v['cl']];
+
+ // min level [int]
+ if (isset($_v['minle']))
+ $parts[] = [$k.'.level', $_v['minle'], '>='];
+
+ // max level [int]
+ if (isset($_v['maxle']))
+ $parts[] = [$k.'.level', $_v['maxle'], '<='];
+
+ return $parts;
+ }
+
+ protected function cbRegionCheck(&$v)
+ {
+ if ($v == 'eu' || $v == 'us')
+ {
+ $this->parentCats[0] = $v; // directly redirect onto this region
+ $v = ''; // remove from filter
+
+ return true;
+ }
+
+ return false;
+ }
+
+ protected function cbServerCheck(&$v)
+ {
+ foreach (Profiler::getRealms() as $realm)
+ if ($realm['name'] == $v)
+ {
+ $this->parentCats[1] = Profiler::urlize($v);// directly redirect onto this server
+ $v = ''; // remove from filter
+
+ return true;
+ }
+
+ return false;
+ }
+
+ protected function cbProfession($cr, $skillId)
+ {
+ if (!Util::checkNumeric($cr[2], NUM_CAST_INT) || !$this->int2Op($cr[1]))
+ return;
+
+ $k = 'sk_'.Util::createHash(12);
+ $col = 'skill'.$skillId;
+
+ $this->formData['extraCols'][$skillId] = $col;
+
+ if ($this->useLocalList)
+ {
+ $this->extraOpts[$k] = array(
+ 'j' => ['?_profiler_completion '.$k.' ON '.$k.'.id = p.id AND '.$k.'.`type` = '.TYPE_SKILL.' AND '.$k.'.typeId = '.$skillId.' AND '.$k.'.cur '.$cr[1].' '.$cr[2], true],
+ 's' => [', '.$k.'.cur AS '.$col]
+ );
+ return [$k.'.typeId', null, '!'];
+ }
+ else
+ {
+ $this->extraOpts[$k] = array(
+ 'j' => ['character_skills '.$k.' ON '.$k.'.guid = c.guid AND '.$k.'.skill = '.$skillId.' AND '.$k.'.value '.$cr[1].' '.$cr[2], true],
+ 's' => [', '.$k.'.value AS '.$col]
+ );
+ return [$k.'.skill', null, '!'];
+ }
+ }
+
+ protected function cbCompletedAcv($cr)
+ {
+ if (!Util::checkNumeric($cr[2], NUM_CAST_INT))
+ return false;
+
+ if (!DB::Aowow()->selectCell('SELECT 1 FROM ?_achievement WHERE id = ?d', $cr[2]))
+ return false;
+
+ $k = 'acv_'.Util::createHash(12);
+
+ if ($this->useLocalList)
+ {
+ $this->extraOpts[$k] = ['j' => ['?_profiler_completion '.$k.' ON '.$k.'.id = p.id AND '.$k.'.`type` = '.TYPE_ACHIEVEMENT.' AND '.$k.'.typeId = '.$cr[2], true]];
+ return [$k.'.typeId', null, '!'];
+ }
+ else
+ {
+ $this->extraOpts[$k] = ['j' => ['character_achievement '.$k.' ON '.$k.'.guid = c.guid AND '.$k.'.achievement = '.$cr[2], true]];
+ return [$k.'.achievement', null, '!'];
+ }
+ }
+
+ protected function cbWearsItems($cr)
+ {
+ if (!Util::checkNumeric($cr[2], NUM_CAST_INT))
+ return false;
+
+ if (!DB::Aowow()->selectCell('SELECT 1 FROM ?_items WHERE id = ?d', $cr[2]))
+ return false;
+
+ $k = 'i_'.Util::createHash(12);
+
+ $this->extraOpts[$k] = ['j' => ['?_profiler_items '.$k.' ON '.$k.'.id = p.id AND '.$k.'.item = '.$cr[2], true]];
+ return [$k.'.item', null, '!'];
+ }
+
+ protected function cbHasGuild($cr)
+ {
+ if (!$this->int2Bool($cr[1]))
+ return false;
+
+ if ($this->useLocalList)
+ return ['p.guild', null, $cr[1] ? '!' : null];
+ else
+ return ['gm.guildId', null, $cr[1] ? '!' : null];
+ }
+
+ protected function cbHasGuildRank($cr)
+ {
+ if (!Util::checkNumeric($cr[2], NUM_CAST_INT) || !$this->int2Op($cr[1]))
+ return false;
+
+ if ($this->useLocalList)
+ return ['p.guildrank', $cr[2], $cr[1]];
+ else
+ return ['gm.rank', $cr[2], $cr[1]];
+ }
+
+ protected function cbTeamName($cr)
+ {
+ if ($_ = $this->modularizeString(['at.name'], $cr[2]))
+ return ['AND', ['at.type', $this->enums[-1][$cr[0]]], $_];
+
+ return false;
+ }
+
+ protected function cbTeamRating($cr)
+ {
+ if (!Util::checkNumeric($cr[2], NUM_CAST_INT) || !$this->int2Op($cr[1]))
+ return false;
+
+ return ['AND', ['at.type', $this->enums[-1][$cr[0]]], ['at.rating', $cr[2], $cr[1]]];
+ }
+}
+
+
+class RemoteProfileList extends ProfileList
+{
+ protected $queryBase = 'SELECT `c`.*, `c`.`guid` AS ARRAY_KEY FROM characters c';
+ protected $queryOpts = array(
+ 'c' => [['gm', 'g', 'ca', 'ct'], 'g' => 'ARRAY_KEY', 'o' => 'level DESC, name ASC'],
+ 'ca' => ['j' => ['character_achievement ca ON ca.guid = c.guid', true], 's' => ', GROUP_CONCAT(DISTINCT ca.achievement SEPARATOR " ") AS _acvs'],
+ 'ct' => ['j' => ['character_talent ct ON ct.guid = c.guid AND ct.spec = c.activespec', true], 's' => ', GROUP_CONCAT(DISTINCT ct.spell SEPARATOR " ") AS _talents'],
+ 'gm' => ['j' => ['guild_member gm ON gm.guid = c.guid', true], 's' => ', gm.rank AS guildrank'],
+ 'g' => ['j' => ['guild g ON g.guildid = gm.guildid', true], 's' => ', g.guildid AS guild, g.name AS guildname'],
+ 'atm' => ['j' => ['arena_team_member atm ON atm.guid = c.guid', true], 's' => ', atm.personalRating AS rating'],
+ 'at' => [['atm'], 'j' => 'arena_team at ON atm.arenaTeamId = at.arenaTeamId', 's' => ', at.name AS arenateam, IF(at.captainGuid = c.guid, 1, 0) AS captain']
+ );
+
+ public function __construct($conditions = [], $miscData = null)
+ {
+ // select DB by realm
+ if (!$this->selectRealms($miscData))
+ {
+ trigger_error('no access to auth-db or table realmlist is empty', E_USER_WARNING);
+ return;
+ }
+
+ parent::__construct($conditions, $miscData);
+
+ if ($this->error)
+ return;
+
+ reset($this->dbNames); // only use when querying single realm
+ $realmId = key($this->dbNames);
+ $realms = Profiler::getRealms();
+ $acvCache = [];
+ $talentCache = [];
+ $atCache = [];
+ $distrib = null;
+ $talentData = [];
+ $limit = CFG_SQL_LIMIT_DEFAULT;
+
+ foreach ($conditions as $c)
+ if (is_int($c))
+ $limit = $c;
+
+ // post processing
+ foreach ($this->iterate() as $guid => &$curTpl)
+ {
+ // battlegroup
+ $curTpl['battlegroup'] = CFG_BATTLEGROUP;
+
+ // realm
+ $r = explode(':', $guid)[0];
+ if (!empty($realms[$r]))
+ {
+ $curTpl['realm'] = $r;
+ $curTpl['realmName'] = $realms[$r]['name'];
+ $curTpl['region'] = $realms[$r]['region'];
+ }
+ else
+ {
+ trigger_error('character "'.$curTpl['name'].'" belongs to nonexistant realm #'.$r, E_USER_WARNING);
+ unset($this->templates[$guid]);
+ continue;
+ }
+
+ // temp id
+ $curTpl['id'] = 0;
+
+ // achievement points pre
+ if ($acvs = explode(' ', $curTpl['_acvs']))
+ foreach ($acvs as $a)
+ if ($a && !isset($acvCache[$a]))
+ $acvCache[$a] = $a;
+
+ // talent points pre
+ if ($talents = explode(' ', $curTpl['_talents']))
+ foreach ($talents as $t)
+ if ($t && !isset($talentCache[$t]))
+ $talentCache[$t] = $t;
+
+ // equalize distribution
+ if ($limit != CFG_SQL_LIMIT_NONE)
+ {
+ if (empty($distrib[$curTpl['realm']]))
+ $distrib[$curTpl['realm']] = 1;
+ else
+ $distrib[$curTpl['realm']]++;
+ }
+
+ $curTpl['cuFlags'] = 0;
+ }
+
+ if ($talentCache)
+ $talentData = DB::Aowow()->select('SELECT spell AS ARRAY_KEY, tab, rank FROM ?_talents WHERE spell IN (?a)', $talentCache);
+
+ if ($distrib !== null)
+ {
+ $total = array_sum($distrib);
+ foreach ($distrib as &$d)
+ $d = ceil($limit * $d / $total);
+ }
+
+ if ($acvCache)
+ $acvCache = DB::Aowow()->selectCol('SELECT id AS ARRAY_KEY, points FROM ?_achievement WHERE id IN (?a)', $acvCache);
+
+ foreach ($this->iterate() as $guid => &$curTpl)
+ {
+ if ($distrib !== null)
+ {
+ if ($limit <= 0 || $distrib[$curTpl['realm']] <= 0)
+ {
+ unset($this->templates[$guid]);
+ continue;
+ }
+
+ $distrib[$curTpl['realm']]--;
+ $limit--;
+ }
+
+
+ $a = explode(' ', $curTpl['_acvs']);
+ $t = explode(' ', $curTpl['_talents']);
+ unset($curTpl['_acvs']);
+ unset($curTpl['_talents']);
+
+ // achievement points post
+ $curTpl['achievementpoints'] = array_sum(array_intersect_key($acvCache, array_combine($a, $a)));
+
+ // talent points post
+ $curTpl['talenttree1'] = 0;
+ $curTpl['talenttree2'] = 0;
+ $curTpl['talenttree3'] = 0;
+ foreach ($talentData as $spell => $data)
+ if (in_array($spell, $t))
+ $curTpl['talenttree'.($data['tab'] + 1)] += $data['rank'];
+ }
+ }
+
+ public function getListviewData($addInfoMask = 0, array $reqCols = [])
+ {
+ $data = parent::getListviewData($addInfoMask, $reqCols);
+
+ // not wanted on server list
+ foreach ($data as &$d)
+ unset($d['published']);
+
+ return $data;
+ }
+
+ public function initializeLocalEntries()
+ {
+ $baseData = $guildData = [];
+ foreach ($this->iterate() as $guid => $__)
+ {
+ $baseData[$guid] = array(
+ 'realm' => $this->getField('realm'),
+ 'realmGUID' => $this->getField('guid'),
+ 'name' => $this->getField('name'),
+ 'race' => $this->getField('race'),
+ 'class' => $this->getField('class'),
+ 'level' => $this->getField('level'),
+ 'gender' => $this->getField('gender'),
+ 'guild' => $this->getField('guild') ?: null,
+ 'guildrank' => $this->getField('guild') ? $this->getField('guildrank') : null,
+ 'cuFlags' => PROFILER_CU_NEEDS_RESYNC
+ );
+
+ if ($this->getField('guild'))
+ $guildData[] = array(
+ 'realm' => $this->getField('realm'),
+ 'realmGUID' => $this->getField('guild'),
+ 'name' => $this->getField('guildname'),
+ 'nameUrl' => Profiler::urlize($this->getField('guildname')),
+ 'cuFlags' => PROFILER_CU_NEEDS_RESYNC
+ );
+ }
+
+ // basic guild data (satisfying table constraints)
+ if ($guildData)
+ {
+ foreach (Util::createSqlBatchInsert($guildData) as $ins)
+ DB::Aowow()->query('INSERT IGNORE INTO ?_profiler_guild (?#) VALUES '.$ins, array_keys(reset($guildData)));
+
+ // merge back local ids
+ $localGuilds = DB::Aowow()->selectCol('SELECT realm AS ARRAY_KEY, realmGUID AS ARRAY_KEY2, id FROM ?_profiler_guild WHERE realm IN (?a) AND realmGUID IN (?a)',
+ array_column($guildData, 'realm'), array_column($guildData, 'realmGUID')
+ );
+
+ foreach ($baseData as &$bd)
+ if ($bd['guild'])
+ $bd['guild'] = $localGuilds[$bd['realm']][$bd['guild']];
+ }
+
+ // basic char data (enough for tooltips)
+ if ($baseData)
+ {
+ foreach (Util::createSqlBatchInsert($baseData) as $ins)
+ DB::Aowow()->query('INSERT IGNORE INTO ?_profiler_profiles (?#) VALUES '.$ins, array_keys(reset($baseData)));
+
+ // merge back local ids
+ $localIds = DB::Aowow()->select(
+ 'SELECT CONCAT(realm, ":", realmGUID) AS ARRAY_KEY, id, gearscore FROM ?_profiler_profiles WHERE (cuFlags & ?d) = 0 AND realm IN (?a) AND realmGUID IN (?a)',
+ PROFILER_CU_PROFILE,
+ array_column($baseData, 'realm'),
+ array_column($baseData, 'realmGUID')
+ );
+
+ foreach ($this->iterate() as $guid => &$_curTpl)
+ if (isset($localIds[$guid]))
+ $_curTpl = array_merge($_curTpl, $localIds[$guid]);
+ }
+ }
+}
+
+
+class LocalProfileList extends ProfileList
+{
+ protected $queryBase = 'SELECT p.*, p.id AS ARRAY_KEY FROM ?_profiler_profiles p';
+ protected $queryOpts = array(
+ 'p' => [['g'], 'g' => 'p.id'],
+ 'ap' => ['j' => ['?_account_profiles ap ON ap.profileId = p.id', true], 's' => ', (IFNULL(ap.ExtraFlags, 0) | p.cuFlags) AS cuFlags'],
+ 'atm' => ['j' => ['?_profiler_arena_team_member atm ON atm.profileId = p.id', true], 's' => ', atm.captain, atm.personalRating AS rating, atm.seasonGames, atm.seasonWins'],
+ 'at' => [['atm'], 'j' => ['?_profiler_arena_team at ON at.id = atm.arenaTeamId', true], 's' => ', at.type'],
+ 'g' => ['j' => ['?_profiler_guild g ON g.id = p.guild', true], 's' => ', g.name AS guildname']
+ );
+
+ public function __construct($conditions = [], $miscData = null)
+ {
+ parent::__construct($conditions, $miscData);
+
+ if ($this->error)
+ return;
+
+ $realms = Profiler::getRealms();
+
+ // post processing
+ $acvPoints = DB::Aowow()->selectCol('SELECT pc.id AS ARRAY_KEY, SUM(a.points) FROM ?_profiler_completion pc LEFT JOIN ?_achievement a ON a.id = pc.typeId WHERE pc.`type` = ?d AND pc.id IN (?a) GROUP BY pc.id', TYPE_ACHIEVEMENT, $this->getFoundIDs());
+
+ foreach ($this->iterate() as $id => &$curTpl)
+ {
+ if ($curTpl['realm'] && !isset($realms[$curTpl['realm']]))
+ continue;
+
+ if (isset($realms[$curTpl['realm']]))
+ {
+ $curTpl['realmName'] = $realms[$curTpl['realm']]['name'];
+ $curTpl['region'] = $realms[$curTpl['realm']]['region'];
+ }
+
+ // battlegroup
+ $curTpl['battlegroup'] = CFG_BATTLEGROUP;
+
+ $curTpl['achievementpoints'] = isset($acvPoints[$id]) ? $acvPoints[$id] : 0;
+ }
+ }
+
+ public function getProfileUrl()
+ {
+ $url = '?profile=';
+
+ if ($this->isCustom())
+ return $url.$this->getField('id');
+
+ return $url.implode('.', array(
+ Profiler::urlize($this->getField('region')),
+ Profiler::urlize($this->getField('realmName')),
+ urlencode($this->getField('name'))
+ ));
+ }
+}
+
+
+?>
diff --git a/includes/types/quest.class.php b/includes/types/quest.class.php
new file mode 100644
index 00000000..417d8797
--- /dev/null
+++ b/includes/types/quest.class.php
@@ -0,0 +1,720 @@
+ [],
+ 'rsc' => ['j' => '?_spell rsc ON q.rewardSpellCast = rsc.id'], // limit rewardSpellCasts
+ 'qse' => ['j' => '?_quests_startend qse ON q.id = qse.questId', 's' => ', qse.method'], // groupConcat..?
+ 'e' => ['j' => ['?_events e ON e.id = `q`.eventId', true], 's' => ', e.holidayId']
+ );
+
+ public function __construct($conditions = [], $miscData = null)
+ {
+ parent::__construct($conditions, $miscData);
+
+ // i don't like this very much
+ $currencies = DB::Aowow()->selectCol('SELECT id AS ARRAY_KEY, itemId FROM ?_currencies');
+
+ // post processing
+ foreach ($this->iterate() as $id => &$_curTpl)
+ {
+ $_curTpl['cat1'] = $_curTpl['zoneOrSort']; // should probably be in a method...
+ $_curTpl['cat2'] = 0;
+
+ foreach (Game::$questClasses as $k => $arr)
+ {
+ if (in_array($_curTpl['cat1'], $arr))
+ {
+ $_curTpl['cat2'] = $k;
+ break;
+ }
+ }
+
+ // store requirements
+ $requires = [];
+ for ($i = 1; $i < 7; $i++)
+ {
+ if ($_ = $_curTpl['reqItemId'.$i])
+ $requires[TYPE_ITEM][] = $_;
+
+ if ($i > 4)
+ continue;
+
+ if ($_curTpl['reqNpcOrGo'.$i] > 0)
+ $requires[TYPE_NPC][] = $_curTpl['reqNpcOrGo'.$i];
+ else if ($_curTpl['reqNpcOrGo'.$i] < 0)
+ $requires[TYPE_OBJECT][] = -$_curTpl['reqNpcOrGo'.$i];
+
+ if ($_ = $_curTpl['reqSourceItemId'.$i])
+ $requires[TYPE_ITEM][] = $_;
+ }
+ if ($requires)
+ $this->requires[$id] = $requires;
+
+ // store rewards
+ $rewards = [];
+ $choices = [];
+
+ if ($_ = $_curTpl['rewardTitleId'])
+ $rewards[TYPE_TITLE][] = $_;
+
+ if ($_ = $_curTpl['rewardHonorPoints'])
+ $rewards[TYPE_CURRENCY][104] = $_;
+
+ if ($_ = $_curTpl['rewardArenaPoints'])
+ $rewards[TYPE_CURRENCY][103] = $_;
+
+ for ($i = 1; $i < 7; $i++)
+ {
+ if ($_ = $_curTpl['rewardChoiceItemId'.$i])
+ $choices[TYPE_ITEM][$_] = $_curTpl['rewardChoiceItemCount'.$i];
+
+ if ($i > 5)
+ continue;
+
+ if ($_ = $_curTpl['rewardFactionId'.$i])
+ $rewards[TYPE_FACTION][$_] = $_curTpl['rewardFactionValue'.$i];
+
+ if ($i > 4)
+ continue;
+
+ if ($_ = $_curTpl['rewardItemId'.$i])
+ {
+ $qty = $_curTpl['rewardItemCount'.$i];
+ if (in_array($_, $currencies))
+ $rewards[TYPE_CURRENCY][array_search($_, $currencies)] = $qty;
+ else
+ $rewards[TYPE_ITEM][$_] = $qty;
+ }
+ }
+ if ($rewards)
+ $this->rewards[$id] = $rewards;
+
+ if ($choices)
+ $this->choices[$id] = $choices;
+ }
+ }
+
+ // static use START
+ public static function getName($id)
+ {
+ $n = DB::Aowow()->SelectRow('SELECT name_loc0, name_loc2, name_loc3, name_loc6, name_loc8 FROM ?_quests WHERE id = ?d', $id);
+ return Util::localizedString($n, 'name');
+ }
+ // static use END
+
+ public function isRepeatable()
+ {
+ return $this->curTpl['flags'] & QUEST_FLAG_REPEATABLE || $this->curTpl['specialFlags'] & QUEST_FLAG_SPECIAL_REPEATABLE;
+ }
+
+ public function isDaily()
+ {
+ if ($this->curTpl['flags'] & QUEST_FLAG_DAILY)
+ return 1;
+
+ if ($this->curTpl['flags'] & QUEST_FLAG_WEEKLY)
+ return 2;
+
+ if ($this->curTpl['specialFlags'] & QUEST_FLAG_SPECIAL_MONTHLY)
+ return 3;
+
+ return 0;
+ }
+
+ // using reqPlayerKills and rewardHonor as a crutch .. has TC this even implemented..?
+ public function isPvPEnabled()
+ {
+ return $this->curTpl['reqPlayerKills'] || $this->curTpl['rewardHonorPoints'] || $this->curTpl['rewardArenaPoints'];
+ }
+
+ // by TC definition
+ public function isSeasonal()
+ {
+ return in_array($this->getField('zoneOrSortBak'), [-22, -284, -366, -369, -370, -376, -374]) && !$this->isRepeatable();
+ }
+
+ public function getSourceData()
+ {
+ $data = [];
+
+ foreach ($this->iterate() as $__)
+ {
+ $data[$this->id] = array(
+ "n" => $this->getField('name', true),
+ "t" => TYPE_QUEST,
+ "ti" => $this->id,
+ "c" => $this->curTpl['cat1'],
+ "c2" => $this->curTpl['cat2']
+ );
+ }
+
+ return $data;
+ }
+
+ public function getSOMData($side = SIDE_BOTH)
+ {
+ $data = [];
+
+ foreach ($this->iterate() as $__)
+ {
+ if (!(Game::sideByRaceMask($this->curTpl['reqRaceMask']) & $side))
+ continue;
+
+ list($series, $first) = DB::Aowow()->SelectRow(
+ 'SELECT IF(prev.id OR cur.nextQuestIdChain, 1, 0) AS "0", IF(prev.id IS NULL AND cur.nextQuestIdChain, 1, 0) AS "1" FROM ?_quests cur LEFT JOIN ?_quests prev ON prev.nextQuestIdChain = cur.id WHERE cur.id = ?d',
+ $this->id
+ );
+
+ $data[$this->id] = array(
+ 'level' => $this->curTpl['level'] < 0 ? MAX_LEVEL : $this->curTpl['level'],
+ 'name' => $this->getField('name', true),
+ 'category' => $this->curTpl['cat1'],
+ 'category2' => $this->curTpl['cat2'],
+ 'series' => $series,
+ 'first' => $first
+ );
+
+ if ($this->isDaily())
+ $data[$this->id]['daily'] = 1;
+ }
+
+ return $data;
+ }
+
+ public function getListviewData($extraFactionId = 0) // i should formulate a propper parameter..
+ {
+ $data = [];
+
+ foreach ($this->iterate() as $__)
+ {
+ $data[$this->id] = array(
+ 'category' => $this->curTpl['cat1'],
+ 'category2' => $this->curTpl['cat2'],
+ 'id' => $this->id,
+ 'level' => $this->curTpl['level'],
+ 'reqlevel' => $this->curTpl['minLevel'],
+ 'name' => $this->getField('name', true),
+ 'side' => Game::sideByRaceMask($this->curTpl['reqRaceMask']),
+ 'wflags' => 0x0,
+ 'xp' => $this->curTpl['rewardXP']
+ );
+
+ if (!empty($this->rewards[$this->id][TYPE_CURRENCY]))
+ foreach ($this->rewards[$this->id][TYPE_CURRENCY] as $iId => $qty)
+ $data[$this->id]['currencyrewards'][] = [$iId, $qty];
+
+ if (!empty($this->rewards[$this->id][TYPE_ITEM]))
+ foreach ($this->rewards[$this->id][TYPE_ITEM] as $iId => $qty)
+ $data[$this->id]['itemrewards'][] = [$iId, $qty];
+
+ if (!empty($this->choices[$this->id][TYPE_ITEM]))
+ foreach ($this->choices[$this->id][TYPE_ITEM] as $iId => $qty)
+ $data[$this->id]['itemchoices'][] = [$iId, $qty];
+
+ if ($_ = $this->curTpl['rewardTitleId'])
+ $data[$this->id]['titlereward'] = $_;
+
+ if ($_ = $this->curTpl['type'])
+ $data[$this->id]['type'] = $_;
+
+ if ($_ = $this->curTpl['reqClassMask'])
+ $data[$this->id]['reqclass'] = $_;
+
+ if ($_ = ($this->curTpl['reqRaceMask'] & RACE_MASK_ALL))
+ if ((($_ & RACE_MASK_ALLIANCE) != RACE_MASK_ALLIANCE) && (($_ & RACE_MASK_HORDE) != RACE_MASK_HORDE))
+ $data[$this->id]['reqrace'] = $_;
+
+ if ($_ = $this->curTpl['rewardOrReqMoney'])
+ if ($_ > 0)
+ $data[$this->id]['money'] = $_;
+
+ // todo (med): also get disables
+ if ($this->curTpl['flags'] & QUEST_FLAG_UNAVAILABLE)
+ $data[$this->id]['historical'] = true;
+
+ // if ($this->isRepeatable()) // dafuque..? says repeatable and is used as 'disabled'..?
+ // $data[$this->id]['wflags'] |= QUEST_CU_REPEATABLE;
+ if ($this->curTpl['cuFlags'] & (CUSTOM_UNAVAILABLE | CUSTOM_DISABLED))
+ $data[$this->id]['wflags'] |= QUEST_CU_REPEATABLE;
+
+ if ($this->curTpl['flags'] & QUEST_FLAG_DAILY)
+ {
+ $data[$this->id]['wflags'] |= QUEST_CU_DAILY;
+ $data[$this->id]['daily'] = true;
+ }
+
+ if ($this->curTpl['flags'] & QUEST_FLAG_WEEKLY)
+ {
+ $data[$this->id]['wflags'] |= QUEST_CU_WEEKLY;
+ $data[$this->id]['weekly'] = true;
+ }
+
+ if ($this->isSeasonal())
+ $data[$this->id]['wflags'] |= QUEST_CU_SEASONAL;
+
+ if ($this->curTpl['flags'] & QUEST_FLAG_AUTO_REWARDED) // not shown in log
+ $data[$this->id]['wflags'] |= QUEST_CU_SKIP_LOG;
+
+ if ($this->curTpl['flags'] & QUEST_FLAG_AUTO_ACCEPT) // self-explanatory
+ $data[$this->id]['wflags'] |= QUEST_CU_AUTO_ACCEPT;
+
+ if ($this->isPvPEnabled()) // not sure why this flag also requires auto-accept to be set
+ $data[$this->id]['wflags'] |= (QUEST_CU_AUTO_ACCEPT | QUEST_CU_PVP_ENABLED);
+
+ $data[$this->id]['reprewards'] = [];
+ for ($i = 1; $i < 6; $i++)
+ {
+ $foo = $this->curTpl['rewardFactionId'.$i];
+ $bar = $this->curTpl['rewardFactionValue'.$i];
+ if ($foo && $bar)
+ {
+ $data[$this->id]['reprewards'][] = [$foo, $bar];
+
+ if ($extraFactionId == $foo)
+ $data[$this->id]['reputation'] = $bar;
+ }
+ }
+ }
+
+ return $data;
+ }
+
+ public function parseText($type = 'objectives', $jsEscaped = true)
+ {
+ $text = $this->getField($type, true);
+ if (!$text)
+ return '';
+
+ $text = Util::parseHtmlText($text);
+
+ if ($jsEscaped)
+ $text = Util::jsEscape($text);
+
+ return $text;
+ }
+
+ public function renderTooltip()
+ {
+ if (!$this->curTpl)
+ return null;
+
+ $title = Util::jsEscape($this->getField('name', true));
+ $level = $this->curTpl['level'];
+ if ($level < 0)
+ $level = 0;
+
+ $x = '';
+ if ($level)
+ {
+ $level = sprintf(Lang::quest('questLevel'), $level);
+
+ if ($this->curTpl['flags'] & QUEST_FLAG_DAILY) // daily
+ $level .= ' '.Lang::quest('daily');
+
+ $x .= '';
+ }
+ else
+ $x .= '';
+
+
+ $x .= ' '.$this->parseText('objectives');
+
+
+ $xReq = '';
+ for ($i = 1; $i < 5; $i++)
+ {
+ $ot = $this->getField('objectiveText'.$i, true);
+ $rng = $this->curTpl['reqNpcOrGo'.$i];
+ $rngQty = $this->curTpl['reqNpcOrGoCount'.$i];
+
+ if ($rngQty < 1 && (!$rng || $ot))
+ continue;
+
+ if ($ot)
+ $name = $ot;
+ else
+ $name = $rng > 0 ? CreatureList::getName($rng) : GameObjectList::getName(-$rng);
+
+ $xReq .= ' - '.Util::jsEscape($name).($rngQty > 1 ? ' x '.$rngQty : null);
+ }
+
+ for ($i = 1; $i < 7; $i++)
+ {
+ $ri = $this->curTpl['reqItemId'.$i];
+ $riQty = $this->curTpl['reqItemCount'.$i];
+
+ if (!$ri || $riQty < 1)
+ continue;
+
+ $xReq .= ' - '.Util::jsEscape(ItemList::getName($ri)).($riQty > 1 ? ' x '.$riQty : null);
+ }
+
+ if ($et = $this->getField('end', true))
+ $xReq .= ' - '.Util::jsEscape($et);
+
+ if ($_ = $this->getField('rewardOrReqMoney'))
+ if ($_ < 0)
+ $xReq .= ' - '.Lang::quest('money').Lang::main('colon').Util::formatMoney(abs($_));
+
+ if ($xReq)
+ $x .= '
'.Lang::quest('requirements').Lang::main('colon').''.$xReq;
+
+ $x .= ' | ';
+
+ return $x;
+ }
+
+ public function getJSGlobals($addMask = GLOBALINFO_ANY)
+ {
+ $data = [];
+
+ foreach ($this->iterate() as $__)
+ {
+ if ($addMask & GLOBALINFO_REWARDS)
+ {
+ // items
+ for ($i = 1; $i < 5; $i++)
+ if ($this->curTpl['rewardItemId'.$i] > 0)
+ $data[TYPE_ITEM][$this->curTpl['rewardItemId'.$i]] = $this->curTpl['rewardItemId'.$i];
+
+ for ($i = 1; $i < 7; $i++)
+ if ($this->curTpl['rewardChoiceItemId'.$i] > 0)
+ $data[TYPE_ITEM][$this->curTpl['rewardChoiceItemId'.$i]] = $this->curTpl['rewardChoiceItemId'.$i];
+
+ // spells
+ if ($this->curTpl['rewardSpell'] > 0)
+ $data[TYPE_SPELL][$this->curTpl['rewardSpell']] = $this->curTpl['rewardSpell'];
+
+ if ($this->curTpl['rewardSpellCast'] > 0)
+ $data[TYPE_SPELL][$this->curTpl['rewardSpellCast']] = $this->curTpl['rewardSpellCast'];
+
+ // titles
+ if ($this->curTpl['rewardTitleId'] > 0)
+ $data[TYPE_TITLE][$this->curTpl['rewardTitleId']] = $this->curTpl['rewardTitleId'];
+
+ // currencies
+ if (!empty($this->rewards[$this->id][TYPE_CURRENCY]))
+ foreach ($this->rewards[$this->id][TYPE_CURRENCY] as $id => $__)
+ $data[TYPE_CURRENCY][$id] = $id;
+ }
+
+ if ($addMask & GLOBALINFO_SELF)
+ $data[TYPE_QUEST][$this->id] = ['name' => $this->getField('name', true)];
+ }
+
+ return $data;
+ }
+}
+
+
+class QuestListFilter extends Filter
+{
+ public $extraOpts = [];
+ protected $enums = array( // massive enums could be put here, if you want to restrict inputs further to be valid IDs instead of just integers
+ 37 => [null, 1, 2, 3, 4, 5, 6, 7, 8, 9, null, 11, true, false],
+ 38 => [null, 1, 2, 3, 4, 5, 6, 7, 8, null, 10, 11, true, false],
+ );
+ protected $genericFilter = array(
+ 1 => [FILTER_CR_CALLBACK, 'cbReputation', '>', null], // increasesrepwith
+ 2 => [FILTER_CR_NUMERIC, 'rewardXP', NUM_CAST_INT ], // experiencegained
+ 3 => [FILTER_CR_NUMERIC, 'rewardOrReqMoney', NUM_CAST_INT ], // moneyrewarded
+ 4 => [FILTER_CR_CALLBACK, 'cbSpellRewards', null, null], // spellrewarded [yn]
+ 5 => [FILTER_CR_FLAG, 'flags', QUEST_FLAG_SHARABLE ], // sharable
+ 6 => [FILTER_CR_NUMERIC, 'timeLimit', NUM_CAST_INT ], // timer
+ 7 => [FILTER_CR_NYI_PH, null, 1 ], // firstquestseries
+ 9 => [FILTER_CR_CALLBACK, 'cbEarnReputation', null, null], // objectiveearnrepwith [enum]
+ 10 => [FILTER_CR_CALLBACK, 'cbReputation', '<', null], // decreasesrepwith
+ 11 => [FILTER_CR_NUMERIC, 'suggestedPlayers', NUM_CAST_INT ], // suggestedplayers
+ 15 => [FILTER_CR_NYI_PH, null, 1 ], // lastquestseries
+ 16 => [FILTER_CR_NYI_PH, null, 1 ], // partseries
+ 18 => [FILTER_CR_FLAG, 'cuFlags', CUSTOM_HAS_SCREENSHOT ], // hasscreenshots
+ 19 => [FILTER_CR_CALLBACK, 'cbQuestRelation', 0x1, null], // startsfrom [enum]
+ 21 => [FILTER_CR_CALLBACK, 'cbQuestRelation', 0x2, null], // endsat [enum]
+ 22 => [FILTER_CR_CALLBACK, 'cbItemRewards', null, null], // itemrewards [op] [int]
+ 23 => [FILTER_CR_CALLBACK, 'cbItemChoices', null, null], // itemchoices [op] [int]
+ 24 => [FILTER_CR_CALLBACK, 'cbLacksStartEnd', null, null], // lacksstartend [yn]
+ 25 => [FILTER_CR_FLAG, 'cuFlags', CUSTOM_HAS_COMMENT ], // hascomments
+ 27 => [FILTER_CR_FLAG, 'flags', QUEST_FLAG_DAILY ], // daily
+ 28 => [FILTER_CR_FLAG, 'flags', QUEST_FLAG_WEEKLY ], // weekly
+ 29 => [FILTER_CR_FLAG, 'flags', QUEST_FLAG_REPEATABLE ], // repeatable
+ 30 => [FILTER_CR_NUMERIC, 'id', NUM_CAST_INT, true], // id
+ 33 => [FILTER_CR_ENUM, 'e.holidayId' ], // relatedevent
+ 34 => [FILTER_CR_CALLBACK, 'cbAvailable', null, null], // availabletoplayers [yn]
+ 36 => [FILTER_CR_FLAG, 'cuFlags', CUSTOM_HAS_VIDEO ], // hasvideos
+ 37 => [FILTER_CR_CALLBACK, 'cbClassSpec', null, null], // classspecific [enum]
+ 37 => [FILTER_CR_CALLBACK, 'cbRaceSpec', null, null], // racespecific [enum]
+ 42 => [FILTER_CR_STAFFFLAG, 'flags' ], // flags
+ 43 => [FILTER_CR_CALLBACK, 'cbCurrencyReward', null, null], // currencyrewarded [enum]
+ 44 => [FILTER_CR_CALLBACK, 'cbLoremaster', null, null], // countsforloremaster_stc [yn]
+ 45 => [FILTER_CR_BOOLEAN, 'rewardTitleId' ] // titlerewarded
+ );
+
+ // fieldId => [checkType, checkValue[, fieldIsArray]]
+ protected $inputFields = array(
+ 'cr' => [FILTER_V_RANGE, [1, 45], true ], // criteria ids
+ 'crs' => [FILTER_V_LIST, [FILTER_ENUM_NONE, FILTER_ENUM_ANY, [0, 99999]], true ], // criteria operators
+ 'crv' => [FILTER_V_REGEX, '/\D/', true ], // criteria values - only numerals
+ 'na' => [FILTER_V_REGEX, '/[\p{C};]/ui', false], // name / text - only printable chars, no delimiter
+ 'ex' => [FILTER_V_EQUAL, 'on', false], // also match subname
+ 'ma' => [FILTER_V_EQUAL, 1, false], // match any / all filter
+ 'minle' => [FILTER_V_RANGE, [1, 99], false], // min quest level
+ 'maxle' => [FILTER_V_RANGE, [1, 99], false], // max quest level
+ 'minrl' => [FILTER_V_RANGE, [1, 99], false], // min required level
+ 'maxrl' => [FILTER_V_RANGE, [1, 99], false], // max required level
+ 'si' => [FILTER_V_LIST, [-2, -1, 1, 2, 3], false], // siede
+ 'ty' => [FILTER_V_LIST, [0, 1, 21, 41, 62, [81, 85], 88, 89], true ] // type
+ );
+
+ protected function createSQLForCriterium(&$cr)
+ {
+ if (in_array($cr[0], array_keys($this->genericFilter)))
+ if ($genCr = $this->genericCriterion($cr))
+ return $genCr;
+
+ unset($cr);
+ $this->error = true;
+ return [1];
+ }
+
+ protected function createSQLForValues()
+ {
+ $parts = [];
+ $_v = $this->fiData['v'];
+
+ // name
+ if (isset($_v['na']))
+ {
+ $_ = [];
+ if (isset($_v['ex']) && $_v['ex'] == 'on')
+ $_ = $this->modularizeString(['name_loc'.User::$localeId, 'objectives_loc'.User::$localeId, 'details_loc'.User::$localeId]);
+ else
+ $_ = $this->modularizeString(['name_loc'.User::$localeId]);
+
+ if ($_)
+ $parts[] = $_;
+ }
+
+ // level min
+ if (isset($_v['minle']))
+ $parts[] = ['level', $_v['minle'], '>=']; // not considering quests that are always at player level (-1)
+
+ // level max
+ if (isset($_v['maxle']))
+ $parts[] = ['level', $_v['maxle'], '<='];
+
+ // reqLevel min
+ if (isset($_v['minrl']))
+ $parts[] = ['minLevel', $_v['minrl'], '>=']; // ignoring maxLevel
+
+ // reqLevel max
+ if (isset($_v['maxrl']))
+ $parts[] = ['minLevel', $_v['maxrl'], '<=']; // ignoring maxLevel
+
+ // side
+ if (isset($_v['si']))
+ {
+ $ex = [['reqRaceMask', RACE_MASK_ALL, '&'], RACE_MASK_ALL, '!'];
+ $notEx = ['OR', ['reqRaceMask', 0], [['reqRaceMask', RACE_MASK_ALL, '&'], RACE_MASK_ALL]];
+
+ switch ($_v['si'])
+ {
+ case 3:
+ $parts[] = $notEx;
+ break;
+ case 2:
+ $parts[] = ['OR', $notEx, ['reqRaceMask', RACE_MASK_HORDE, '&']];
+ break;
+ case -2:
+ $parts[] = ['AND', $ex, ['reqRaceMask', RACE_MASK_HORDE, '&']];
+ break;
+ case 1:
+ $parts[] = ['OR', $notEx, ['reqRaceMask', RACE_MASK_ALLIANCE, '&']];
+ break;
+ case -1:
+ $parts[] = ['AND', $ex, ['reqRaceMask', RACE_MASK_ALLIANCE, '&']];
+ break;
+ }
+ }
+
+ // type [list]
+ if (isset($_v['ty']))
+ $parts[] = ['type', $_v['ty']];
+
+ return $parts;
+ }
+
+ protected function cbReputation($cr, $sign)
+ {
+ if (!Util::checkNumeric($cr[1], NUM_REQ_INT) || $cr[1] <= 0)
+ return false;
+
+ if ($_ = DB::Aowow()->selectRow('SELECT * FROM ?_factions WHERE id = ?d', $cr[1]))
+ $this->formData['reputationCols'][] = [$cr[1], Util::localizedString($_, 'name')];
+
+ return [
+ 'OR',
+ ['AND', ['rewardFactionId1', $cr[1]], ['rewardFactionValue1', 0, $sign]],
+ ['AND', ['rewardFactionId2', $cr[1]], ['rewardFactionValue2', 0, $sign]],
+ ['AND', ['rewardFactionId3', $cr[1]], ['rewardFactionValue3', 0, $sign]],
+ ['AND', ['rewardFactionId4', $cr[1]], ['rewardFactionValue4', 0, $sign]],
+ ['AND', ['rewardFactionId5', $cr[1]], ['rewardFactionValue5', 0, $sign]]
+ ];
+ }
+
+ protected function cbQuestRelation($cr, $flags)
+ {
+ switch ($cr[1])
+ {
+ case 1: // npc
+ return ['AND', ['qse.type', TYPE_NPC], ['qse.method', $flags, '&']];
+ case 2: // object
+ return ['AND', ['qse.type', TYPE_OBJECT], ['qse.method', $flags, '&']];
+ case 3: // item
+ return ['AND', ['qse.type', TYPE_ITEM], ['qse.method', $flags, '&']];
+ }
+
+ return false;
+ }
+
+ protected function cbCurrencyReward($cr)
+ {
+ if (!Util::checkNumeric($cr[1], NUM_REQ_INT) || $cr[1] <= 0)
+ return false;
+
+ return [
+ 'OR',
+ ['rewardItemId1', $cr[1]], ['rewardItemId2', $cr[1]], ['rewardItemId3', $cr[1]], ['rewardItemId4', $cr[1]],
+ ['rewardChoiceItemId1', $cr[1]], ['rewardChoiceItemId2', $cr[1]], ['rewardChoiceItemId3', $cr[1]], ['rewardChoiceItemId4', $cr[1]], ['rewardChoiceItemId5', $cr[1]], ['rewardChoiceItemId6', $cr[1]]
+ ];
+ }
+
+ protected function cbAvailable($cr)
+ {
+ if (!$this->int2Bool($cr[1]))
+ return false;
+
+ if ($cr[1])
+ return [['cuFlags', CUSTOM_UNAVAILABLE | CUSTOM_DISABLED, '&'], 0];
+ else
+ return ['cuFlags', CUSTOM_UNAVAILABLE | CUSTOM_DISABLED, '&'];
+ }
+
+ protected function cbItemChoices($cr)
+ {
+ if (!Util::checkNumeric($cr[2], NUM_CAST_INT) || !$this->int2Op($cr[1]))
+ return false;
+
+ $this->extraOpts['q']['s'][] = ', (IF(rewardChoiceItemId1, 1, 0) + IF(rewardChoiceItemId2, 1, 0) + IF(rewardChoiceItemId3, 1, 0) + IF(rewardChoiceItemId4, 1, 0) + IF(rewardChoiceItemId5, 1, 0) + IF(rewardChoiceItemId6, 1, 0)) as numChoices';
+ $this->extraOpts['q']['h'][] = 'numChoices '.$cr[1].' '.$cr[2];
+ return [1];
+ }
+
+ protected function cbItemRewards($cr)
+ {
+ if (!Util::checkNumeric($cr[2], NUM_CAST_INT) || !$this->int2Op($cr[1]))
+ return false;
+
+ $this->extraOpts['q']['s'][] = ', (IF(rewardItemId1, 1, 0) + IF(rewardItemId2, 1, 0) + IF(rewardItemId3, 1, 0) + IF(rewardItemId4, 1, 0)) as numRewards';
+ $this->extraOpts['q']['h'][] = 'numRewards '.$cr[1].' '.$cr[2];
+ return [1];
+ }
+
+ protected function cbLoremaster($cr)
+ {
+ if (!$this->int2Bool($cr[1]))
+ return false;
+
+ if ($cr[1])
+ return ['AND', ['zoneOrSort', 0, '>'], [['flags', QUEST_FLAG_DAILY | QUEST_FLAG_WEEKLY | QUEST_FLAG_REPEATABLE , '&'], 0], [['specialFlags', QUEST_FLAG_SPECIAL_REPEATABLE | QUEST_FLAG_SPECIAL_MONTHLY , '&'], 0]];
+ else
+ return ['OR', ['zoneOrSort', 0, '<'], ['flags', QUEST_FLAG_DAILY | QUEST_FLAG_WEEKLY | QUEST_FLAG_REPEATABLE , '&'], ['specialFlags', QUEST_FLAG_SPECIAL_REPEATABLE | QUEST_FLAG_SPECIAL_MONTHLY , '&']];;
+ }
+
+ protected function cbSpellRewards($cr)
+ {
+ if (!$this->int2Bool($cr[1]))
+ return false;
+
+ if ($cr[1])
+ return ['OR', ['sourceSpellId', 0, '>'], ['rewardSpell', 0, '>'], ['rsc.effect1Id', SpellList::$effects['teach']], ['rsc.effect2Id', SpellList::$effects['teach']], ['rsc.effect3Id', SpellList::$effects['teach']]];
+ else
+ return ['AND', ['sourceSpellId', 0], ['rewardSpell', 0], ['rewardSpellCast', 0]];
+ }
+
+ protected function cbEarnReputation($cr)
+ {
+ if (!Util::checkNumeric($cr[1], NUM_REQ_INT))
+ return false;
+
+ if ($cr[1] > 0)
+ return ['OR', ['reqFactionId1', $cr[1]], ['reqFactionId2', $cr[1]]];
+ else if ($cr[1] == FILTER_ENUM_ANY) // any
+ return ['OR', ['reqFactionId1', 0, '>'], ['reqFactionId2', 0, '>']];
+ else if ($cr[1] == FILTER_ENUM_NONE) // none
+ return ['AND', ['reqFactionId1', 0], ['reqFactionId2', 0]];
+
+ return false;
+ }
+
+ protected function cbClassSpec($cr)
+ {
+ if (!isset($this->enums[$cr[0]][$cr[1]]))
+ return false;
+
+ $_ = $this->enums[$cr[0]][$cr[1]];
+ if ($_ === true)
+ return ['AND', ['reqClassMask', 0, '!'], [['reqClassMask', CLASS_MASK_ALL, '&'], CLASS_MASK_ALL, '!']];
+ else if ($_ === false)
+ return ['OR', ['reqClassMask', 0], [['reqClassMask', CLASS_MASK_ALL, '&'], CLASS_MASK_ALL]];
+ else if (is_int($_))
+ return ['AND', ['reqClassMask', (1 << ($_ - 1)), '&'], [['reqClassMask', CLASS_MASK_ALL, '&'], CLASS_MASK_ALL, '!']];
+
+ return false;
+ }
+
+ protected function cbRaceSpec($cr)
+ {
+ if (!isset($this->enums[$cr[0]][$cr[1]]))
+ return false;
+
+ $_ = $this->enums[$cr[0]][$cr[1]];
+ if ($_ === true)
+ return ['AND', ['reqRaceMask', 0, '!'], [['reqRaceMask', RACE_MASK_ALL, '&'], RACE_MASK_ALL, '!'], [['reqRaceMask', RACE_MASK_ALLIANCE, '&'], RACE_MASK_ALLIANCE, '!'], [['reqRaceMask', RACE_MASK_HORDE, '&'], RACE_MASK_HORDE, '!']];
+ else if ($_ === false)
+ return ['OR', ['reqRaceMask', 0], ['reqRaceMask', RACE_MASK_ALL], ['reqRaceMask', RACE_MASK_ALLIANCE], ['reqRaceMask', RACE_MASK_HORDE]];
+ else if (is_int($_))
+ return ['AND', ['reqRaceMask', (1 << ($_ - 1)), '&'], [['reqRaceMask', RACE_MASK_ALLIANCE, '&'], RACE_MASK_ALLIANCE, '!'], [['reqRaceMask', RACE_MASK_HORDE, '&'], RACE_MASK_HORDE, '!']];
+
+ return false;
+ }
+
+ protected function cbLacksStartEnd($cr)
+ {
+ if (!$this->int2Bool($cr[1]))
+ return false;
+
+ $missing = DB::Aowow()->selectCol('SELECT questId, max(method) a, min(method) b FROM ?_quests_startend GROUP BY questId HAVING (a | b) <> 3');
+ if ($cr[1])
+ return ['id', $missing];
+ else
+ return ['id', $missing, '!'];
+ }
+}
+
+
+?>
diff --git a/includes/types/skill.class.php b/includes/types/skill.class.php
new file mode 100644
index 00000000..2aa2dd13
--- /dev/null
+++ b/includes/types/skill.class.php
@@ -0,0 +1,81 @@
+ [['ic']],
+ 'ic' => ['j' => ['?_icons ic ON ic.id = sl.iconId', true], 's' => ', ic.name AS iconString'],
+ );
+
+ public function __construct($conditions = [])
+ {
+ parent::__construct($conditions);
+
+ // post processing
+ foreach ($this->iterate() as &$_curTpl)
+ {
+ $_ = &$_curTpl['specializations']; // shorthand
+ if (!$_)
+ $_ = [0, 0, 0, 0, 0];
+ else
+ {
+ $_ = explode(' ', $_);
+ while (count($_) < 5)
+ $_[] = 0;
+ }
+
+ if (!$_curTpl['iconId'])
+ $_curTpl['iconString'] = 'inv_misc_questionmark';
+ }
+ }
+
+ public static function getName($id)
+ {
+ $n = DB::Aowow()->SelectRow('SELECT name_loc0, name_loc2, name_loc3, name_loc6, name_loc8 FROM ?_skillline WHERE id = ?d', $id);
+ return Util::localizedString($n, 'name');
+ }
+
+ public function getListviewData()
+ {
+ $data = [];
+
+ foreach ($this->iterate() as $__)
+ {
+ $data[$this->id] = array(
+ 'category' => $this->curTpl['typeCat'],
+ 'categorybak' => $this->curTpl['categoryId'],
+ 'id' => $this->id,
+ 'name' => Util::jsEscape($this->getField('name', true)),
+ 'profession' => $this->curTpl['professionMask'],
+ 'recipeSubclass' => $this->curTpl['recipeSubClass'],
+ 'specializations' => Util::toJSON($this->curTpl['specializations'], JSON_NUMERIC_CHECK),
+ 'icon' => Util::jsEscape($this->curTpl['iconString'])
+ );
+ }
+
+ return $data;
+ }
+
+ public function getJSGlobals($addMask = 0)
+ {
+ $data = [];
+
+ foreach ($this->iterate() as $__)
+ $data[self::$type][$this->id] = ['name' => Util::jsEscape($this->getField('name', true)), 'icon' => Util::jsEscape($this->curTpl['iconString'])];
+
+ return $data;
+ }
+
+ public function renderTooltip() { }
+}
+
+?>
diff --git a/includes/types/sound.class.php b/includes/types/sound.class.php
new file mode 100644
index 00000000..86001e76
--- /dev/null
+++ b/includes/types/sound.class.php
@@ -0,0 +1,136 @@
+ 'audio/ogg; codecs="vorbis"',
+ SOUND_TYPE_MP3 => 'audio/mpeg'
+ );
+
+ public function __construct($conditions = [])
+ {
+ parent::__construct($conditions);
+
+ // post processing
+ foreach ($this->iterate() as $id => &$_curTpl)
+ {
+ $_curTpl['files'] = [];
+ for ($i = 1; $i < 11; $i++)
+ {
+ if ($_curTpl['soundFile'.$i])
+ {
+ $this->fileBuffer[$_curTpl['soundFile'.$i]] = null;
+ $_curTpl['files'][] = &$this->fileBuffer[$_curTpl['soundFile'.$i]];
+ }
+
+ unset($_curTpl['soundFile'.$i]);
+ }
+ }
+
+ if ($this->fileBuffer)
+ {
+ $files = DB::Aowow()->select('SELECT id AS ARRAY_KEY, `id`, `file` AS title, `type` FROM ?_sounds_files sf WHERE id IN (?a)', array_keys($this->fileBuffer));
+ foreach ($files as $id => $data)
+ {
+ // skipp file extension
+ $data['title'] = substr($data['title'], 0, -4);
+ // enum to string
+ $data['type'] = self::$fileTypes[$data['type']];
+ // get real url
+ $data['url'] = STATIC_URL . '/wowsounds/' . $data['id'];
+ // v push v
+ $this->fileBuffer[$id] = $data;
+ }
+ }
+ }
+
+ public static function getName($id)
+ {
+ $this->getEntry($id);
+
+ return $this->getField('name');
+ }
+
+ public function getListviewData()
+ {
+ $data = [];
+
+ foreach ($this->iterate() as $__)
+ {
+ $data[$this->id] = array(
+ 'id' => $this->id,
+ 'type' => $this->getField('cat'),
+ 'name' => $this->getField('name'),
+ 'files' => array_values(array_filter($this->getField('files')))
+ );
+ }
+
+ return $data;
+ }
+
+ public function getJSGlobals($addMask = 0)
+ {
+ $data = [];
+
+ foreach ($this->iterate() as $__)
+ $data[self::$type][$this->id] = array(
+ 'name' => Util::jsEscape($this->getField('name', true)),
+ 'type' => $this->getField('cat'),
+ 'files' => array_values(array_filter($this->getField('files')))
+ );
+
+ return $data;
+ }
+
+ public function renderTooltip() { }
+}
+
+class SoundListFilter extends Filter
+{
+ // we have no criteria for this one...
+ protected function createSQLForCriterium(&$cr)
+ {
+ unset($cr);
+ $this->error = true;
+ return [1];
+ }
+
+ // fieldId => [checkType, checkValue[, fieldIsArray]]
+ protected $inputFields = array(
+ 'na' => [FILTER_V_REGEX, '/[\p{C};]/ui', false], // name - only printable chars, no delimiter
+ 'ty' => [FILTER_V_LIST, [[1, 4], 6, 9, 10, 12, 13, 14, 16, 17, [19, 23], [25, 31], 50, 52, 53], true ] // type
+ );
+
+ protected function createSQLForValues()
+ {
+ $parts = [];
+ $_v = &$this->fiData['v'];
+
+ // name [str]
+ if (isset($_v['na']))
+ if ($_ = $this->modularizeString(['name']))
+ $parts[] = $_;
+
+ // type [list]
+ if (isset($_v['ty']))
+ $parts[] = ['cat', $_v['ty']];
+
+ return $parts;
+ }
+}
+
+?>
diff --git a/includes/types/spell.class.php b/includes/types/spell.class.php
new file mode 100644
index 00000000..9bc9cb4a
--- /dev/null
+++ b/includes/types/spell.class.php
@@ -0,0 +1,2542 @@
+ [ 43, 44, 45, 46, 54, 55, 95, 118, 136, 160, 162, 172, 173, 176, 226, 228, 229, 473], // Weapons
+ 8 => [293, 413, 414, 415, 433], // Armor
+ 9 => [129, 185, 356, 762], // sec. Professions
+ 10 => [ 98, 109, 111, 113, 115, 137, 138, 139, 140, 141, 313, 315, 673, 759], // Languages
+ 11 => [164, 165, 171, 182, 186, 197, 202, 333, 393, 755, 773] // prim. Professions
+ );
+
+ public static $spellTypes = array(
+ 6 => 1,
+ 8 => 2,
+ 10 => 4
+ );
+
+ public static $effects = array(
+ 'heal' => [ 0,/*3,*/10, 67, 75, 136 ], // , Dummy, Heal, Heal Max Health, Heal Mechanical, Heal Percent
+ 'damage' => [ 0, 2, 3, 9, 62 ], // , Dummy, School Damage, Health Leech, Power Burn
+ 'itemCreate' => [24, 34, 59, 66, 157 ], // createItem, changeItem, randomItem, createManaGem, createItem2
+ 'trigger' => [ 3, 32, 64, 101, 142, 148, 151, 152, 155, 160, 164], // dummy, trigger missile, trigger spell, feed pet, force cast, force cast with value, unk, trigger spell 2, unk, dualwield 2H, unk, remove aura
+ 'teach' => [36, 57, /*133*/ ] // learn spell, learn pet spell, /*unlearn specialization*/
+ );
+
+ public static $auras = array(
+ 'heal' => [ 4, 8, 62, 69, 97, 226 ], // Dummy, Periodic Heal, Periodic Health Funnel, School Absorb, Mana Shield, Periodic Dummy
+ 'damage' => [ 3, 4, 15, 53, 89, 162, 226 ], // Periodic Damage, Dummy, Damage Shield, Periodic Health Leech, Periodic Damage Percent, Power Burn Mana, Periodic Dummy
+ 'itemCreate' => [86 ], // Channel Death Item
+ 'trigger' => [ 4, 23, 42, 48, 109, 226, 227, 231, 236, 284 ], // dummy; 23/227: periodic trigger spell (with value); 42/231: proc trigger spell (with value); 48: unk; 109: add target trigger; 226: periodic dummy; 236: control vehicle; 284: linked
+ 'teach' => [ ]
+ );
+
+ private $spellVars = [];
+ private $refSpells = [];
+ private $tools = [];
+ private $interactive = false;
+ private $charLevel = MAX_LEVEL;
+
+ protected $queryBase = 'SELECT s.*, s.id AS ARRAY_KEY FROM ?_spell s';
+ protected $queryOpts = array(
+ 's' => [['src', 'sr', 'ic', 'ica']], // 6: TYPE_SPELL
+ '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_loc6 AS rangeText_loc6, sr.name_loc8 AS rangeText_loc8'],
+ 'src' => ['j' => ['?_source src ON type = 6 AND typeId = s.id', true], 's' => ', src1, src2, src3, src4, src5, src6, src7, src8, src9, src10, src11, src12, src13, src14, src15, src16, src17, src18, src19, src20, src21, src22, src23, src24']
+ );
+
+ public function __construct($conditions = [])
+ {
+ parent::__construct($conditions);
+
+ if ($this->error)
+ return;
+
+ // post processing
+ $foo = DB::World()->selectCol('SELECT perfectItemType FROM skill_perfect_item_template WHERE spellId IN (?a)', $this->getFoundIDs());
+ foreach ($this->iterate() as &$_curTpl)
+ {
+ // required for globals
+ if ($idx = $this->canCreateItem())
+ foreach ($idx as $i)
+ $foo[] = (int)$_curTpl['effect'.$i.'CreateItemId'];
+
+ for ($i = 1; $i <= 8; $i++)
+ if ($_curTpl['reagent'.$i] > 0)
+ $foo[] = (int)$_curTpl['reagent'.$i];
+
+ for ($i = 1; $i <= 2; $i++)
+ if ($_curTpl['tool'.$i] > 0)
+ $foo[] = (int)$_curTpl['tool'.$i];
+
+ // ranks
+ $this->ranks[$this->id] = $this->getField('rank', true);
+
+ // sources
+ for ($i = 1; $i < 25; $i++)
+ {
+ if ($_ = $_curTpl['src'.$i])
+ $this->sources[$this->id][$i][] = $_;
+
+ unset($_curTpl['src'.$i]);
+ }
+
+ // set full masks to 0
+ $_curTpl['reqClassMask'] &= CLASS_MASK_ALL;
+ if ($_curTpl['reqClassMask'] == CLASS_MASK_ALL)
+ $_curTpl['reqClassMask'] = 0;
+
+ $_curTpl['reqRaceMask'] &= RACE_MASK_ALL;
+ if ($_curTpl['reqRaceMask'] == RACE_MASK_ALL)
+ $_curTpl['reqRaceMask'] = 0;
+
+ // unpack skillLines
+ $_curTpl['skillLines'] = [];
+ if ($_curTpl['skillLine1'] < 0)
+ {
+ foreach (Game::$skillLineMask[$_curTpl['skillLine1']] as $idx => $pair)
+ if ($_curTpl['skillLine2OrMask'] & (1 << $idx))
+ $_curTpl['skillLines'][] = $pair[1];
+ }
+ else if ($sec = $_curTpl['skillLine2OrMask'])
+ {
+ if ($this->id == 818) // and another hack .. basic Campfire (818) has deprecated skill Survival (142) as first skillLine
+ $_curTpl['skillLines'] = [$sec, $_curTpl['skillLine1']];
+ else
+ $_curTpl['skillLines'] = [$_curTpl['skillLine1'], $sec];
+ }
+ else if ($prim = $_curTpl['skillLine1'])
+ $_curTpl['skillLines'] = [$prim];
+
+ unset($_curTpl['skillLine1']);
+ unset($_curTpl['skillLine2OrMask']);
+
+ if (!$_curTpl['iconString'])
+ $_curTpl['iconString'] = 'inv_misc_questionmark';
+ }
+
+ if ($foo)
+ $this->relItems = new ItemList(array(['i.id', array_unique($foo)], CFG_SQL_LIMIT_NONE));
+ }
+
+ // use if you JUST need the name
+ public static function getName($id)
+ {
+ $n = DB::Aowow()->SelectRow('SELECT name_loc0, name_loc2, name_loc3, name_loc6, name_loc8 FROM ?_spell WHERE id = ?d', $id );
+ return Util::localizedString($n, 'name');
+ }
+ // end static use
+
+ // required for item-comparison
+ public function getStatGain()
+ {
+ $data = [];
+
+ foreach ($this->iterate() as $__)
+ {
+ $stats = [];
+
+ for ($i = 1; $i <= 3; $i++)
+ {
+ $pts = $this->calculateAmountForCurrent($i)[1];
+ $mv = $this->curTpl['effect'.$i.'MiscValue'];
+ $au = $this->curTpl['effect'.$i.'AuraId'];
+
+ // Enchant Item Permanent (53) / Temporary (54)
+ if (in_array($this->curTpl['effect'.$i.'Id'], [53, 54]))
+ {
+ if ($mv && ($json = DB::Aowow()->selectRow('SELECT * FROM ?_item_stats WHERE `type` = ?d AND `typeId` = ?d', TYPE_ENCHANTMENT, $mv)))
+ {
+ $mods = [];
+ foreach ($json as $str => $val)
+ if ($val && ($idx = array_search($str, Game::$itemMods)))
+ $mods[$idx] = $val;
+
+ if ($mods)
+ Util::arraySumByKey($stats, $mods);
+ }
+
+ continue;
+ }
+
+ switch ($au)
+ {
+ case 29: // ModStat MiscVal:type
+ if ($mv < 0) // all stats
+ {
+ for ($iMod = ITEM_MOD_AGILITY; $iMod <= ITEM_MOD_STAMINA; $iMod++)
+ Util::arraySumByKey($stats, [$iMod => $pts]);
+ }
+ else if ($mv == STAT_STRENGTH) // one stat
+ Util::arraySumByKey($stats, [ITEM_MOD_STRENGTH => $pts]);
+ else if ($mv == STAT_AGILITY)
+ Util::arraySumByKey($stats, [ITEM_MOD_AGILITY => $pts]);
+ else if ($mv == STAT_STAMINA)
+ Util::arraySumByKey($stats, [ITEM_MOD_STAMINA => $pts]);
+ else if ($mv == STAT_INTELLECT)
+ Util::arraySumByKey($stats, [ITEM_MOD_INTELLECT => $pts]);
+ else if ($mv == STAT_SPIRIT)
+ Util::arraySumByKey($stats, [ITEM_MOD_SPIRIT => $pts]);
+ else // one bullshit
+ trigger_error('AuraId 29 of spell #'.$this->id.' has wrong statId #'.$mv, E_USER_WARNING);
+
+ break;
+ case 34: // Increase Health
+ case 230:
+ case 250:
+ Util::arraySumByKey($stats, [ITEM_MOD_HEALTH => $pts]);
+ break;
+ case 13: // damage splpwr + physical (dmg & any)
+ // + weapon damage
+ if ($mv == (1 << SPELL_SCHOOL_NORMAL))
+ {
+ Util::arraySumByKey($stats, [ITEM_MOD_WEAPON_DMG => $pts]);
+ break;
+ }
+
+ // full magic mask, also counts towards healing
+ if ($mv == SPELL_MAGIC_SCHOOLS)
+ {
+ Util::arraySumByKey($stats, [ITEM_MOD_SPELL_POWER => $pts]);
+ Util::arraySumByKey($stats, [ITEM_MOD_SPELL_DAMAGE_DONE => $pts]);
+ }
+ else
+ {
+ // HolySpellpower (deprecated; still used in randomproperties)
+ if ($mv & (1 << SPELL_SCHOOL_HOLY))
+ Util::arraySumByKey($stats, [ITEM_MOD_HOLY_POWER => $pts]);
+
+ // FireSpellpower (deprecated; still used in randomproperties)
+ if ($mv & (1 << SPELL_SCHOOL_FIRE))
+ Util::arraySumByKey($stats, [ITEM_MOD_FIRE_POWER => $pts]);
+
+ // NatureSpellpower (deprecated; still used in randomproperties)
+ if ($mv & (1 << SPELL_SCHOOL_NATURE))
+ Util::arraySumByKey($stats, [ITEM_MOD_NATURE_POWER => $pts]);
+
+ // FrostSpellpower (deprecated; still used in randomproperties)
+ if ($mv & (1 << SPELL_SCHOOL_FROST))
+ Util::arraySumByKey($stats, [ITEM_MOD_FROST_POWER => $pts]);
+
+ // ShadowSpellpower (deprecated; still used in randomproperties)
+ if ($mv & (1 << SPELL_SCHOOL_SHADOW))
+ Util::arraySumByKey($stats, [ITEM_MOD_SHADOW_POWER => $pts]);
+
+ // ArcaneSpellpower (deprecated; still used in randomproperties)
+ if ($mv & (1 << SPELL_SCHOOL_ARCANE))
+ Util::arraySumByKey($stats, [ITEM_MOD_ARCANE_POWER => $pts]);
+ }
+
+ break;
+ case 135: // healing splpwr (healing & any) .. not as a mask..
+ Util::arraySumByKey($stats, [ITEM_MOD_SPELL_HEALING_DONE => $pts]);
+ break;
+ case 35: // ModPower - MiscVal:type see defined Powers only energy/mana in use
+ if ($mv == POWER_HEALTH)
+ Util::arraySumByKey($stats, [ITEM_MOD_HEALTH => $pts]);
+ else if ($mv == POWER_ENERGY)
+ Util::arraySumByKey($stats, [ITEM_MOD_ENERGY => $pts]);
+ else if ($mv == POWER_RAGE)
+ Util::arraySumByKey($stats, [ITEM_MOD_RAGE => $pts]);
+ else if ($mv == POWER_MANA)
+ Util::arraySumByKey($stats, [ITEM_MOD_MANA => $pts]);
+ else if ($mv == POWER_RUNIC_POWER)
+ Util::arraySumByKey($stats, [ITEM_MOD_RUNIC_POWER => $pts]);
+
+ break;
+ case 189: // CombatRating MiscVal:ratingMask
+ case 220:
+ if ($mod = Game::itemModByRatingMask($mv))
+ Util::arraySumByKey($stats, [$mod => $pts]);
+ break;
+ case 143: // Resistance MiscVal:school
+ case 83:
+ case 22:
+ if ($mv == 1) // Armor only if explicitly specified
+ {
+ Util::arraySumByKey($stats, [ITEM_MOD_ARMOR => $pts]);
+ break;
+ }
+
+ if ($mv == 2) // holy-resistance ONLY if explicitly specified (shouldn't even exist...)
+ {
+ Util::arraySumByKey($stats, [ITEM_MOD_HOLY_RESISTANCE => $pts]);
+ break;
+ }
+
+ for ($j = 0; $j < 7; $j++)
+ {
+ if (($mv & (1 << $j)) == 0)
+ continue;
+
+ switch ($j)
+ {
+ case 2:
+ Util::arraySumByKey($stats, [ITEM_MOD_FIRE_RESISTANCE => $pts]);
+ break;
+ case 3:
+ Util::arraySumByKey($stats, [ITEM_MOD_NATURE_RESISTANCE => $pts]);
+ break;
+ case 4:
+ Util::arraySumByKey($stats, [ITEM_MOD_FROST_RESISTANCE => $pts]);
+ break;
+ case 5:
+ Util::arraySumByKey($stats, [ITEM_MOD_SHADOW_RESISTANCE => $pts]);
+ break;
+ case 6:
+ Util::arraySumByKey($stats, [ITEM_MOD_ARCANE_RESISTANCE => $pts]);
+ break;
+ }
+ }
+ break;
+ case 8: // hp5
+ case 84:
+ case 161:
+ Util::arraySumByKey($stats, [ITEM_MOD_HEALTH_REGEN => $pts]);
+ break;
+ case 85: // mp5
+ Util::arraySumByKey($stats, [ITEM_MOD_MANA_REGENERATION => $pts]);
+ break;
+ case 99: // atkpwr
+ Util::arraySumByKey($stats, [ITEM_MOD_ATTACK_POWER => $pts]);
+ break; // ?carries over to rngatkpwr?
+ case 124: // rngatkpwr
+ Util::arraySumByKey($stats, [ITEM_MOD_RANGED_ATTACK_POWER => $pts]);
+ break;
+ case 158: // blockvalue
+ Util::arraySumByKey($stats, [ITEM_MOD_BLOCK_VALUE => $pts]);
+ break;
+ case 240: // ModExpertise
+ Util::arraySumByKey($stats, [ITEM_MOD_EXPERTISE_RATING => $pts]);
+ break;
+ case 123: // Mod Target Resistance
+ if ($mv == 0x7C && $pts < 0)
+ Util::arraySumByKey($stats, [ITEM_MOD_SPELL_PENETRATION => -$pts]);
+ break;
+ }
+ }
+
+ $data[$this->id] = $stats;
+ }
+
+ return $data;
+ }
+
+ public function getProfilerMods()
+ {
+ // weapon hand check: param: slot, class, subclass, value
+ $whCheck = '$function() { var j, w = _inventory.getInventory()[%d]; if (!w[0] || !g_items[w[0]]) { return 0; } j = g_items[w[0]].jsonequip; return (j.classs == %d && (%d & (1 << (j.subclass)))) ? %d : 0; }';
+
+ $data = $this->getStatGain(); // flat gains
+ foreach ($data as $id => &$spellData)
+ {
+ foreach ($spellData as $modId => $val)
+ {
+ if (!isset(Game::$itemMods[$modId]))
+ continue;
+
+ if ($modId == ITEM_MOD_EXPERTISE_RATING) // not a rating .. pure expertise
+ $spellData['exp'] = $val;
+ else
+ $spellData[Game::$itemMods[$modId]] = $val;
+
+ unset($spellData[$modId]);
+ }
+
+ // apply weapon restrictions
+ $this->getEntry($id);
+ $class = $this->getField('equippedItemClass');
+ $subClass = $this->getField('equippedItemSubClassMask');
+ $slot = $subClass & 0x5000C ? 18 : 16;
+ if ($class != ITEM_CLASS_WEAPON || !$subClass)
+ continue;
+
+ foreach ($spellData as $json => $pts)
+ $spellData[$json] = [1, 'functionOf', sprintf($whCheck, $slot, $class, $subClass, $pts)];
+ }
+
+ // 4 possible modifiers found
+ // => [0.15, 'functionOf', ]
+ // => [0.33, 'percentOf', ]
+ // => [123, 'add']
+ // => ... as from getStatGain()
+
+ $modXByStat = function (&$arr, $stat, $pts) use (&$mv)
+ {
+ 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'];
+ };
+
+ $modXBySchool = function (&$arr, $stat, $val, $mask = null) use (&$mv)
+ {
+ 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];
+ };
+
+ $jsonStat = function ($stat)
+ {
+ 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';
+ };
+
+ foreach ($this->iterate() as $id => $__)
+ {
+ // Priest: Spirit of Redemption is a spell but also a passive. *yaaayyyy*
+ if (($this->getField('cuFlags') & SPELL_CU_TALENTSPELL) && $id != 20711)
+ continue;
+
+ // curious cases of OH MY FUCKING GOD WHY?!
+ if ($id == 16268) // Shaman - Spirit Weapons (parry is normaly stored in g_statistics)
+ {
+ $data[$id]['parrypct'] = [5, 'add'];
+ continue;
+ }
+
+ if ($id == 20550) // Tauren - Endurance (dependant on base health) ... if you are looking for something elegant, look away!
+ {
+ $data[$id]['health'] = [0.05, 'functionOf', '$function(p) { return g_statistics.combo[p.classs][p.level][5]; }'];
+ continue;
+ }
+
+ for ($i = 1; $i < 4; $i++)
+ {
+ $pts = $this->calculateAmountForCurrent($i)[1];
+ $mv = $this->getField('effect'.$i.'MiscValue');
+ $mvB = $this->getField('effect'.$i.'MiscValueB');
+ $au = $this->getField('effect'.$i.'AuraId');
+ $class = $this->getField('equippedItemClass');
+ $subClass = $this->getField('equippedItemSubClassMask');
+
+
+ /* ISSUE!
+ mods formated like ['' => [, 'percentOf', '']] are applied as multiplier and not
+ as a flat value (that is equal to the percentage, like they should be). So the stats-table won't show the actual deficit
+ */
+
+ switch ($au)
+ {
+ case 101: // Mod Resistance Percent
+ case 142: // Mod Base Resistance Percent
+ if ($mv == 1) // Armor only if explicitly specified only affects armor from equippment
+ $data[$id]['armor'] = [$pts / 100, 'percentOf', ['armor', 0]];
+ else if ($mv)
+ $modXBySchool($data[$id], 'res', $pts);
+ break;
+ case 182: // Mod Resistance Of Stat Percent
+ if ($mv == 1) // Armor only if explicitly specified
+ $data[$id]['armor'] = [$pts / 100, 'percentOf', $jsonStat($mvB)];
+ else if ($mv)
+ $modXBySchool($data[$id], 'res', [$pts / 100, 'percentOf', $jsonStat($mvB)]);
+ break;
+ case 137: // mod stat percent
+ if ($mv > -1) // one stat
+ $modXByStat($data[$id], null, $pts);
+ else if ($mv < 0) // all stats
+ for ($iMod = ITEM_MOD_AGILITY; $iMod <= ITEM_MOD_STAMINA; $iMod++)
+ $data[$id][Game::$itemMods[$iMod]] = [$pts / 100, 'percentOf', Game::$itemMods[$iMod]];
+ break;
+ case 174: // Mod Spell Damage Of Stat Percent
+ $mv = $mv ?: SPELL_MAGIC_SCHOOLS;
+ $modXBySchool($data[$id], 'spldmg', [$pts / 100, 'percentOf', $jsonStat($mvB)]);
+ break;
+ case 212: // Mod Ranged Attack Power Of Stat Percent
+ $modXByStat($data[$id], 'rgdatkpwr', $pts);
+ break;
+ case 268: // Mod Attack Power Of Stat Percent
+ $modXByStat($data[$id], 'mleatkpwr', $pts);
+ break;
+ case 175: // Mod Spell Healing Of Stat Percent
+ $modXByStat($data[$id], 'splheal', $pts);
+ break;
+ case 219: // Mod Mana Regeneration from Stat
+ $modXByStat($data[$id], 'manargn', $pts);
+ break;
+ case 134: // Mod Mana Regeneration Interrupt
+ $data[$id]['icmanargn'] = [$pts / 100, 'percentOf', 'oocmanargn'];
+ break;
+ case 57: // Mod Spell Crit Chance
+ case 71: // Mod Spell Crit Chance School
+ $mv = $mv ?: SPELL_MAGIC_SCHOOLS;
+ $modXBySchool($data[$id], 'splcritstrkpct', [$pts, 'add']);
+ if (($mv & SPELL_MAGIC_SCHOOLS) == SPELL_MAGIC_SCHOOLS)
+ $data[$id]['splcritstrkpct'] = [$pts, 'add'];
+ break;
+ case 285: // Mod Attack Power Of Armor
+ $data[$id]['mleatkpwr'] = [1 / $pts, 'percentOf', 'fullarmor'];
+ $data[$id]['rgdatkpwr'] = [1 / $pts, 'percentOf', 'fullarmor'];
+ break;
+ case 52: // Mod Physical Crit Percent
+ if ($class < 1 || ($class == ITEM_CLASS_WEAPON && ($subClass & 0x5000C)))
+ $data[$id]['rgdcritstrkpct'] = [1, 'functionOf', sprintf($whCheck, 18, $class, $subClass, $pts)];
+ // $data[$id]['rgdcritstrkpct'] = [$pts, 'add'];
+ if ($class < 1 || ($class == ITEM_CLASS_WEAPON && ($subClass & 0xA5F3)))
+ $data[$id]['mlecritstrkpct'] = [1, 'functionOf', sprintf($whCheck, 16, $class, $subClass, $pts)];
+ // $data[$id]['mlecritstrkpct'] = [$pts, 'add'];
+ break;
+ case 47: // Mod Parry Percent
+ $data[$id]['parrypct'] = [$pts, 'add'];
+ break;
+ case 49: // Mod Dodge Percent
+ $data[$id]['dodgepct'] = [$pts, 'add'];
+ break;
+ case 51: // Mod Block Percent
+ $data[$id]['blockpct'] = [$pts, 'add'];
+ break;
+ case 132: // Mod Increase Energy Percent
+ if ($mv == POWER_HEALTH)
+ $data[$id]['health'] = [$pts / 100, 'percentOf', 'health'];
+ else if ($mv == POWER_ENERGY)
+ $data[$id]['energy'] = [$pts / 100, 'percentOf', 'energy'];
+ else if ($mv == POWER_MANA)
+ $data[$id]['mana'] = [$pts / 100, 'percentOf', 'mana'];
+ else if ($mv == POWER_RAGE)
+ $data[$id]['rage'] = [$pts / 100, 'percentOf', 'rage'];
+ else if ($mv == POWER_RUNIC_POWER)
+ $data[$id]['runic'] = [$pts / 100, 'percentOf', 'runic'];
+ break;
+ case 133: // Mod Increase Health Percent
+ $data[$id]['health'] = [$pts / 100, 'percentOf', 'health'];
+ break;
+ case 150: // Mod Shield Blockvalue Percent
+ $data[$id]['block'] = [$pts / 100, 'percentOf', 'block'];
+ break;
+ case 290: // Mod Crit Percent
+ $data[$id]['mlecritstrkpct'] = [$pts, 'add'];
+ $data[$id]['rgdcritstrkpct'] = [$pts, 'add'];
+ $data[$id]['splcritstrkpct'] = [$pts, 'add'];
+ break;
+ case 237: // Mod Spell Damage Of Attack Power
+ $mv = $mv ?: SPELL_MAGIC_SCHOOLS;
+ $modXBySchool($data[$id], 'spldmg', [$pts / 100, 'percentOf', 'mleatkpwr']);
+ break;
+ case 238: // Mod Spell Healing Of Attack Power
+ $data[$id]['splheal'] = [$pts / 100, 'percentOf', 'mleatkpwr'];
+ break;
+ case 166: // Mod Attack Power Percent [ingmae only melee..?]
+ $data[$id]['mleatkpwr'] = [$pts / 100, 'percentOf', 'mleatkpwr'];
+ break;
+ case 88: // Mod Health Regeneration Percent
+ $data[$id]['healthrgn'] = [$pts / 100, 'percentOf', 'healthrgn'];
+ break;
+ }
+ }
+ }
+
+ return $data;
+ }
+
+ // halper
+ public function getReagentsForCurrent()
+ {
+ $data = [];
+
+ for ($i = 1; $i <= 8; $i++)
+ if ($this->curTpl['reagent'.$i] > 0 && $this->curTpl['reagentCount'.$i])
+ $data[$this->curTpl['reagent'.$i]] = [$this->curTpl['reagent'.$i], $this->curTpl['reagentCount'.$i]];
+
+ return $data;
+ }
+
+ public function getToolsForCurrent()
+ {
+ if ($this->tools)
+ return $this->tools;
+
+ $tools = [];
+ for ($i = 1; $i <= 2; $i++)
+ {
+ // TotemCategory
+ if ($_ = $this->curTpl['toolCategory'.$i])
+ {
+ $tc = DB::Aowow()->selectRow('SELECT * FROM ?_totemcategory WHERE id = ?d', $_);
+ $tools[$i + 1] = array(
+ 'id' => $_,
+ 'name' => Util::localizedString($tc, 'name'));
+ }
+
+ // Tools
+ if (!$this->curTpl['tool'.$i])
+ continue;
+
+ foreach ($this->relItems->iterate() as $relId => $__)
+ {
+ if ($relId != $this->curTpl['tool'.$i])
+ continue;
+
+ $tools[$i - 1] = array(
+ 'itemId' => $relId,
+ 'name' => $this->relItems->getField('name', true),
+ 'quality' => $this->relItems->getField('quality')
+ );
+
+ break;
+ }
+ }
+
+ $this->tools = array_reverse($tools);
+
+ return $this->tools;
+ }
+
+ public function getModelInfo($spellId = 0, $effIdx = 0)
+ {
+ $displays = [0 => []];
+ foreach ($this->iterate() as $id => $__)
+ {
+ if ($spellId && $spellId != $id)
+ continue;
+
+ for ($i = 1; $i < 4; $i++)
+ {
+ $effMV = $this->curTpl['effect'.$i.'MiscValue'];
+ if (!$effMV)
+ continue;
+
+ // GO Model from MiscVal
+ if (in_array($this->curTpl['effect'.$i.'Id'], [50, 76, 104, 105, 106, 107]))
+ {
+ if (isset($displays[TYPE_OBJECT][$id]))
+ $displays[TYPE_OBJECT][$id][0][] = $i;
+ else
+ $displays[TYPE_OBJECT][$id] = [[$i], $effMV];
+ }
+ // NPC Model from MiscVal
+ else if (in_array($this->curTpl['effect'.$i.'Id'], [28, 90, 134]) || in_array($this->curTpl['effect'.$i.'AuraId'], [56, 78]))
+ {
+ if (isset($displays[TYPE_NPC][$id]))
+ $displays[TYPE_NPC][$id][0][] = $i;
+ else
+ $displays[TYPE_NPC][$id] = [[$i], $effMV];
+ }
+ // Shapeshift
+ else if ($this->curTpl['effect'.$i.'AuraId'] == 36)
+ {
+ $subForms = array(
+ 892 => [892, 29407, 29406, 29408, 29405], // Cat - NE
+ 8571 => [8571, 29410, 29411, 29412], // Cat - Tauren
+ 2281 => [2281, 29413, 29414, 29416, 29417], // Bear - NE
+ 2289 => [2289, 29415, 29418, 29419, 29420, 29421] // Bear - Tauren
+ );
+
+ if ($st = DB::Aowow()->selectRow('SELECT *, displayIdA as model1, displayIdH as model2 FROM ?_shapeshiftforms WHERE id = ?d', $effMV))
+ {
+ foreach ([1, 2] as $j)
+ if (isset($subForms[$st['model'.$j]]))
+ $st['model'.$j] = $subForms[$st['model'.$j]][array_rand($subForms[$st['model'.$j]])];
+
+ $displays[0][$id][$i] = array(
+ 'typeId' => 0,
+ 'displayId' => $st['model2'] ? $st['model'.rand(1, 2)] : $st['model1'],
+ 'creatureType' => $st['creatureType'],
+ 'displayName' => Lang::game('st', $effMV)
+ );
+ }
+ }
+ }
+ }
+
+ $results = $displays[0];
+
+ if (!empty($displays[TYPE_NPC]))
+ {
+ $nModels = new CreatureList(array(['id', array_column($displays[TYPE_NPC], 1)]));
+ foreach ($nModels->iterate() as $nId => $__)
+ {
+ $srcId = 0;
+ foreach ($displays[TYPE_NPC] as $srcId => $set)
+ if ($set[1] == $nId)
+ break;
+
+ foreach ($set[0] as $idx)
+ {
+ $results[$srcId][$idx] = array(
+ 'typeId' => $nId,
+ 'displayId' => $nModels->getRandomModelId(),
+ 'displayName' => $nModels->getField('name', true)
+ );
+ }
+ }
+ }
+
+ if (!empty($displays[TYPE_OBJECT]))
+ {
+ $oModels = new GameObjectList(array(['id', array_column($displays[TYPE_OBJECT], 1)]));
+ foreach ($oModels->iterate() as $oId => $__)
+ {
+ $srcId = 0;
+ foreach ($displays[TYPE_OBJECT] as $srcId => $set)
+ if ($set[1] == $oId)
+ break;
+
+ foreach ($set[0] as $idx)
+ {
+ $results[$srcId][$idx] = array(
+ 'typeId' => $oId,
+ 'displayId' => $oModels->getField('displayId'),
+ 'displayName' => $oModels->getField('name', true)
+ );
+ }
+ }
+ }
+
+ if ($spellId && $effIdx)
+ return !empty($results[$spellId][$effIdx]) ? $results[$spellId][$effIdx] : 0;
+
+ return $results;
+ }
+
+ private function createRangesForCurrent()
+ {
+ if (!$this->curTpl['rangeMaxHostile'])
+ return '';
+
+ // minRange exists; show as range
+ if ($this->curTpl['rangeMinHostile'])
+ return sprintf(Lang::spell('range'), $this->curTpl['rangeMinHostile'].' - '.$this->curTpl['rangeMaxHostile']);
+ // friend and hostile differ; do color
+ else if ($this->curTpl['rangeMaxHostile'] != $this->curTpl['rangeMaxFriend'])
+ return sprintf(Lang::spell('range'), ''.$this->curTpl['rangeMaxHostile'].' - '.$this->curTpl['rangeMaxFriend']. '');
+ // hardcode: "melee range"
+ else if ($this->curTpl['rangeMaxHostile'] == 5)
+ return Lang::spell('meleeRange');
+ // hardcode "unlimited range"
+ else if ($this->curTpl['rangeMaxHostile'] == 50000)
+ return Lang::spell('unlimRange');
+ // regular case
+ else
+ return sprintf(Lang::spell('range'), $this->curTpl['rangeMaxHostile']);
+ }
+
+ public function createPowerCostForCurrent()
+ {
+ $str = '';
+
+ // check for custom PowerDisplay
+ $pt = $this->curTpl['powerType'];
+
+ if ($pt == POWER_RUNE && ($rCost = ($this->curTpl['powerCostRunes'] & 0x333)))
+ { // Blood 2|1 - Unholy 2|1 - Frost 2|1
+ $runes = [];
+ if ($_ = (($rCost & 0x300) >> 8))
+ $runes[] = $_.' '.Lang::spell('powerRunes', 2);
+ if ($_ = (($rCost & 0x030) >> 4))
+ $runes[] = $_.' '.Lang::spell('powerRunes', 1);
+ if ($_ = ($rCost & 0x003))
+ $runes[] = $_.' '.Lang::spell('powerRunes', 0);
+
+ $str .= implode(', ', $runes);
+ }
+ else if ($this->curTpl['powerCostPercent'] > 0) // power cost: pct over static
+ $str .= $this->curTpl['powerCostPercent']."% ".sprintf(Lang::spell('pctCostOf'), strtolower(Lang::spell('powerTypes', $pt)));
+ else if ($this->curTpl['powerCost'] > 0 || $this->curTpl['powerPerSecond'] > 0 || $this->curTpl['powerCostPerLevel'] > 0)
+ $str .= ($pt == POWER_RAGE || $pt == POWER_RUNIC_POWER ? $this->curTpl['powerCost'] / 10 : $this->curTpl['powerCost']).' '.Util::ucFirst(Lang::spell('powerTypes', $pt));
+
+ // append periodic cost
+ if ($this->curTpl['powerPerSecond'] > 0)
+ $str .= sprintf(Lang::spell('costPerSec'), $this->curTpl['powerPerSecond']);
+
+ // append level cost (todo (low): work in as scaling cost)
+ if ($this->curTpl['powerCostPerLevel'] > 0)
+ $str .= sprintf(Lang::spell('costPerLevel'), $this->curTpl['powerCostPerLevel']);
+
+ return $str;
+ }
+
+ public function createCastTimeForCurrent($short = true, $noInstant = true)
+ {
+ if ($this->isChanneledSpell())
+ return Lang::spell('channeled');
+ else if ($this->curTpl['castTime'] > 0)
+ return $short ? sprintf(Lang::spell('castIn'), $this->curTpl['castTime'] / 1000) : Util::formatTime($this->curTpl['castTime']);
+ // show instant only for player/pet/npc abilities (todo (low): unsure when really hidden (like talent-case))
+ else if ($noInstant && !in_array($this->curTpl['typeCat'], [11, 7, -3, -6, -8, 0]) && !($this->curTpl['cuFlags'] & SPELL_CU_TALENTSPELL))
+ return '';
+ // SPELL_ATTR0_ABILITY instant ability.. yeah, wording thing only (todo (low): rule is imperfect)
+ else if ($this->curTpl['damageClass'] != 1 || $this->curTpl['attributes0'] & 0x10)
+ return Lang::spell('instantPhys');
+ else // instant cast
+ return Lang::spell('instantMagic');
+ }
+
+ private function createCooldownForCurrent()
+ {
+ if ($this->curTpl['recoveryTime'])
+ return sprintf(Lang::game('cooldown'), Util::formatTime($this->curTpl['recoveryTime'], true));
+ else if ($this->curTpl['recoveryCategory'])
+ return sprintf(Lang::game('cooldown'), Util::formatTime($this->curTpl['recoveryCategory'], true));
+ else
+ return '';
+ }
+
+ // formulae base from TC
+ private function calculateAmountForCurrent($effIdx, $altTpl = null)
+ {
+ $ref = $altTpl ? $altTpl : $this;
+ $level = $this->charLevel;
+ $rppl = $ref->getField('effect'.$effIdx.'RealPointsPerLevel');
+ $base = $ref->getField('effect'.$effIdx.'BasePoints');
+ $add = $ref->getField('effect'.$effIdx.'DieSides');
+ $maxLvl = $ref->getField('maxLevel');
+ $baseLvl = $ref->getField('baseLevel');
+
+ if ($rppl)
+ {
+ if ($level > $maxLvl && $maxLvl > 0)
+ $level = $maxLvl;
+ else if ($level < $baseLvl)
+ $level = $baseLvl;
+
+ if (!$ref->getField('atributes0') & 0x40) // SPELL_ATTR0_PASSIVE
+ $level -= $ref->getField('spellLevel');
+
+ $base += (int)($level * $rppl);
+ }
+
+ return [
+ $add ? $base + 1 : $base,
+ $base + $add,
+ $rppl ? '' : null,
+ $rppl ? '' : null
+ ];
+ }
+
+ public function canCreateItem()
+ {
+ $idx = [];
+ for ($i = 1; $i < 4; $i++)
+ if (in_array($this->curTpl['effect'.$i.'Id'], SpellList::$effects['itemCreate']) || in_array($this->curTpl['effect'.$i.'AuraId'], SpellList::$auras['itemCreate']))
+ if ($this->curTpl['effect'.$i.'CreateItemId'] > 0)
+ $idx[] = $i;
+
+ return $idx;
+ }
+
+ public function canTriggerSpell()
+ {
+ $idx = [];
+ for ($i = 1; $i < 4; $i++)
+ if (in_array($this->curTpl['effect'.$i.'Id'], SpellList::$effects['trigger']) || in_array($this->curTpl['effect'.$i.'AuraId'], SpellList::$auras['trigger']))
+ if ($this->curTpl['effect'.$i.'TriggerSpell'] > 0 || ($this->curTpl['effect'.$i.'Id'] == 155 && $this->curTpl['effect'.$i.'MiscValue'] > 0))
+ $idx[] = $i;
+
+ return $idx;
+ }
+
+ public function canTeachSpell()
+ {
+ $idx = [];
+ for ($i = 1; $i < 4; $i++)
+ if (in_array($this->curTpl['effect'.$i.'Id'], SpellList::$effects['teach']) || in_array($this->curTpl['effect'.$i.'AuraId'], SpellList::$auras['teach']))
+ if ($this->curTpl['effect'.$i.'TriggerSpell'] > 0)
+ $idx[] = $i;
+
+ return $idx;
+ }
+
+ public function isChanneledSpell()
+ {
+ return $this->curTpl['attributes1'] & 0x44;
+ }
+
+ public function isHealingSpell()
+ {
+ for ($i = 1; $i < 4; $i++)
+ if (!in_array($this->curTpl['effect'.$i.'Id'], SpellList::$effects['heal']) && !in_array($this->curTpl['effect'.$i.'AuraId'], SpellList::$auras['heal']))
+ return false;
+
+ return true;
+ }
+
+ public function isDamagingSpell()
+ {
+ for ($i = 1; $i < 4; $i++)
+ if (!in_array($this->curTpl['effect'.$i.'Id'], SpellList::$effects['damage']) && !in_array($this->curTpl['effect'.$i.'AuraId'], SpellList::$auras['damage']))
+ return false;
+
+ return true;
+ }
+
+ public function periodicEffectsMask()
+ {
+ $effMask = 0x0;
+
+ for ($i = 1; $i < 4; $i++)
+ if ($this->curTpl['effect'.$i.'Periode'] > 0)
+ $effMask |= 1 << ($i - 1);
+
+ return $effMask;
+ }
+
+ // description-, buff-parsing component
+ private function resolveEvaluation($formula)
+ {
+ // see Traits in javascript locales
+
+ $PlayerName = Lang::main('name');
+ $pl = $PL = /* playerLevel set manually ? $this->charLevel : */ $this->interactive ? sprintf(Util::$dfnString, 'LANG.level', Lang::game('level')) : Lang::game('level');
+ $ap = $AP = $this->interactive ? sprintf(Util::$dfnString, 'LANG.traits.atkpwr[0]', Lang::spell('traitShort', 'atkpwr')) : Lang::spell('traitShort', 'atkpwr');
+ $rap = $RAP = $this->interactive ? sprintf(Util::$dfnString, 'LANG.traits.rgdatkpwr[0]', Lang::spell('traitShort', 'rgdatkpwr')) : Lang::spell('traitShort', 'rgdatkpwr');
+ $sp = $SP = $this->interactive ? sprintf(Util::$dfnString, 'LANG.traits.splpwr[0]', Lang::spell('traitShort', 'splpwr')) : Lang::spell('traitShort', 'splpwr');
+ $spa = $SPA = $this->interactive ? sprintf(Util::$dfnString, 'LANG.traits.arcsplpwr[0]', Lang::spell('traitShort', 'arcsplpwr')) : Lang::spell('traitShort', 'arcsplpwr');
+ $spfi = $SPFI = $this->interactive ? sprintf(Util::$dfnString, 'LANG.traits.firsplpwr[0]', Lang::spell('traitShort', 'firsplpwr')) : Lang::spell('traitShort', 'firsplpwr');
+ $spfr = $SPFR = $this->interactive ? sprintf(Util::$dfnString, 'LANG.traits.frosplpwr[0]', Lang::spell('traitShort', 'frosplpwr')) : Lang::spell('traitShort', 'frosplpwr');
+ $sph = $SPH = $this->interactive ? sprintf(Util::$dfnString, 'LANG.traits.holsplpwr[0]', Lang::spell('traitShort', 'holsplpwr')) : Lang::spell('traitShort', 'holsplpwr');
+ $spn = $SPN = $this->interactive ? sprintf(Util::$dfnString, 'LANG.traits.natsplpwr[0]', Lang::spell('traitShort', 'natsplpwr')) : Lang::spell('traitShort', 'natsplpwr');
+ $sps = $SPS = $this->interactive ? sprintf(Util::$dfnString, 'LANG.traits.shasplpwr[0]', Lang::spell('traitShort', 'shasplpwr')) : Lang::spell('traitShort', 'shasplpwr');
+ $bh = $BH = $this->interactive ? sprintf(Util::$dfnString, 'LANG.traits.splheal[0]', Lang::spell('traitShort', 'splheal')) : Lang::spell('traitShort', 'splheal');
+ $spi = $SPI = $this->interactive ? sprintf(Util::$dfnString, 'LANG.traits.spi[0]', Lang::spell('traitShort', 'spi')) : Lang::spell('traitShort', 'spi');
+ $sta = $STA = $this->interactive ? sprintf(Util::$dfnString, 'LANG.traits.sta[0]', Lang::spell('traitShort', 'sta')) : Lang::spell('traitShort', 'sta');
+ $str = $STR = $this->interactive ? sprintf(Util::$dfnString, 'LANG.traits.str[0]', Lang::spell('traitShort', 'str')) : Lang::spell('traitShort', 'str');
+ $agi = $AGI = $this->interactive ? sprintf(Util::$dfnString, 'LANG.traits.agi[0]', Lang::spell('traitShort', 'agi')) : Lang::spell('traitShort', 'agi');
+ $int = $INT = $this->interactive ? sprintf(Util::$dfnString, 'LANG.traits.int[0]', Lang::spell('traitShort', 'int')) : Lang::spell('traitShort', 'int');
+
+ // only 'ron test spell', guess its %-dmg mod; no idea what bc2 might be
+ $pa = '<$PctArcane>'; // %arcane
+ $pfi = '<$PctFire>'; // %fire
+ $pfr = '<$PctFrost>'; // %frost
+ $ph = '<$PctHoly>'; // %holy
+ $pn = '<$PctNature>'; // %nature
+ $ps = '<$PctShadow>'; // %shadow
+ $pbh = '<$PctHeal>'; // %heal
+ $pbhd = '<$PctHealDone>'; // %heal done
+ $bc2 = '<$bc2>'; // bc2
+
+ $HND = $hnd = $this->interactive ? sprintf(Util::$dfnString, '[Hands required by weapon]', 'HND') : 'HND'; // todo (med): localize this one
+ $MWS = $mws = $this->interactive ? sprintf(Util::$dfnString, 'LANG.traits.mlespeed[0]', 'MWS') : 'MWS';
+ $mw = $this->interactive ? sprintf(Util::$dfnString, 'LANG.traits.dmgmin1[0]', 'mw') : 'mw';
+ $MW = $this->interactive ? sprintf(Util::$dfnString, 'LANG.traits.dmgmax1[0]', 'MW') : 'MW';
+ $mwb = $this->interactive ? sprintf(Util::$dfnString, 'LANG.traits.mledmgmin[0]', 'mwb') : 'mwb';
+ $MWB = $this->interactive ? sprintf(Util::$dfnString, 'LANG.traits.mledmgmax[0]', 'MWB') : 'MWB';
+ $rwb = $this->interactive ? sprintf(Util::$dfnString, 'LANG.traits.rgddmgmin[0]', 'rwb') : 'rwb';
+ $RWB = $this->interactive ? sprintf(Util::$dfnString, 'LANG.traits.rgddmgmax[0]', 'RWB') : 'RWB';
+
+ $cond = $COND = function($a, $b, $c) { return $a ? $b : $c; };
+ $eq = $EQ = function($a, $b) { return $a == $b; };
+ $gt = $GT = function($a, $b) { return $a > $b; };
+ $gte = $GTE = function($a, $b) { return $a <= $b; };
+ $floor = $FLOOR = function($a) { return floor($a); };
+ $max = $MAX = function($a, $b) { return max($a, $b); };
+ $min = $MIN = function($a, $b) { return min($a, $b); };
+
+ if (preg_match_all('/\$\w+\b/i', $formula, $vars))
+ {
+
+ $evalable = true;
+
+ foreach ($vars[0] as $var) // oh lord, forgive me this sin .. but is_callable seems to bug out and function_exists doesn't find lambda-functions >.<
+ {
+ $var = substr($var, 1);
+
+ if (isset($$var))
+ {
+ $eval = eval('return @$'.$var.';'); // attention: error suppression active here (will be logged anyway)
+ if (getType($eval) == 'object')
+ continue;
+ else if (is_numeric($eval))
+ continue;
+ }
+ else
+ $$var = '';
+
+ $evalable = false;
+ break;
+ }
+
+ if (!$evalable)
+ {
+ // can't eval constructs because of strings present. replace constructs with strings
+ $cond = $COND = !$this->interactive ? 'COND' : sprintf(Util::$dfnString, 'COND(a, b, c) a ? b : c', 'COND');
+ $eq = $EQ = !$this->interactive ? 'EQ' : sprintf(Util::$dfnString, 'EQ(a, b) a == b', 'EQ');
+ $gt = $GT = !$this->interactive ? 'GT' : sprintf(Util::$dfnString, 'GT(a, b) a > b', 'GT');
+ $gte = $GTE = !$this->interactive ? 'GTE' : sprintf(Util::$dfnString, 'GTE(a, b) a <= b', 'GT');
+ $floor = $FLOOR = !$this->interactive ? 'FLOOR' : sprintf(Util::$dfnString, 'FLOOR(a)', 'FLOOR');
+ $min = $MIN = !$this->interactive ? 'MIN' : sprintf(Util::$dfnString, 'MIN(a, b)', 'MIN');
+ $max = $MAX = !$this->interactive ? 'MAX' : sprintf(Util::$dfnString, 'MAX(a, b)', 'MAX');
+ $pl = $PL = !$this->interactive ? 'PL' : sprintf(Util::$dfnString, 'LANG.level', 'PL');
+
+ // note the " !
+ return eval('return "'.$formula.'";');
+ }
+ else
+ return eval('return '.$formula.';');
+ }
+
+ // since this function may be called recursively, there are cases, where the already evaluated string is tried to be evaled again, throwing parse errors
+ // todo (med): also quit, if we replaced vars with non-interactive text
+ if (strstr($formula, '') || strstr($formula, '%s (%s)';
+ $result[4] = $rType;
+ }
+
+ $result[0] = $base;
+ break;
+ case 'n': // ProcCharges
+ case 'N':
+ $base = $srcSpell->getField('procCharges');
+
+ if (in_array($op, $signs) && is_numeric($oparg) && is_numeric($base))
+ eval("\$base = $base $op $oparg;");
+
+ $result[0] = $base;
+ break;
+ case 'o': // TotalAmount for periodic auras (with variance)
+ case 'O':
+ list($min, $max, $modStrMin, $modStrMax) = $this->calculateAmountForCurrent($effIdx, $srcSpell);
+ $periode = $srcSpell->getField('effect'.$effIdx.'Periode');
+ $duration = $srcSpell->getField('duration');
+
+ if (!$periode)
+ {
+ // Mod Power Regeneration & Mod Health Regeneration have an implicit periode of 5sec
+ $aura = $srcSpell->getField('effect'.$effIdx.'AuraId');
+ if ($aura == 84 || $aura == 85)
+ $periode = 5000;
+ else
+ $periode = 3000;
+ }
+
+ $min *= $duration / $periode;
+ $max *= $duration / $periode;
+
+ if (in_array($op, $signs) && is_numeric($oparg))
+ {
+ eval("\$min = $min $op $oparg;");
+ eval("\$max = $max $op $oparg;");
+ }
+
+ if ($this->interactive)
+ {
+ $result[2] = $modStrMin.'%s';
+ $result[3] = $modStrMax.'%s';
+ }
+
+ $result[0] = $min;
+ $result[1] = $max;
+ break;
+ case 'q': // EffectMiscValue
+ case 'Q':
+ $base = $srcSpell->getField('effect'.$effIdx.'MiscValue');
+
+ if (in_array($op, $signs) && is_numeric($oparg) && is_numeric($base))
+ eval("\$base = $base $op $oparg;");
+
+ $result[0] = $base;
+ break;
+ case 'r': // SpellRange
+ case 'R':
+ $base = $srcSpell->getField('rangeMaxHostile');
+
+ if (in_array($op, $signs) && is_numeric($oparg) && is_numeric($base))
+ eval("\$base = $base $op $oparg;");
+
+ $result[0] = $base;
+ break;
+ case 's': // BasePoints (with variance)
+ case 'S':
+ list($min, $max, $modStrMin, $modStrMax) = $this->calculateAmountForCurrent($effIdx, $srcSpell);
+ $mv = $srcSpell->getField('effect'.$effIdx.'MiscValue');
+ $aura = $srcSpell->getField('effect'.$effIdx.'AuraId');
+
+ if (in_array($op, $signs) && is_numeric($oparg))
+ {
+ eval("\$min = $min $op $oparg;");
+ eval("\$max = $max $op $oparg;");
+ }
+ // Aura giving combat ratings
+ $rType = 0;
+ if ($aura == 189)
+ if ($rType = Game::itemModByRatingMask($mv))
+ $usesScalingRating = true;
+ // Aura end
+
+ if ($rType)
+ {
+ $result[2] = '%s (%s)';
+ $result[4] = $rType;
+ }
+ else if ($aura == 189 && $this->interactive)
+ {
+ $result[2] = $modStrMin.'%s';
+ $result[3] = $modStrMax.'%s';
+ }
+
+ $result[0] = $min;
+ $result[1] = $max;
+ break;
+ case 't': // Periode
+ case 'T':
+ $base = $srcSpell->getField('effect'.$effIdx.'Periode') / 1000;
+
+ if (in_array($op, $signs) && is_numeric($oparg) && is_numeric($base))
+ eval("\$base = $base $op $oparg;");
+
+ $result[0] = $base;
+ break;
+ case 'u': // StackCount
+ case 'U':
+ $base = $srcSpell->getField('stackAmount');
+
+ if (in_array($op, $signs) && is_numeric($oparg) && is_numeric($base))
+ eval("\$base = $base $op $oparg;");
+
+ $result[0] = $base;
+ break;
+ case 'v': // MaxTargetLevel
+ case 'V':
+ $base = $srcSpell->getField('MaxTargetLevel');
+
+ if (in_array($op, $signs) && is_numeric($oparg) && is_numeric($base))
+ eval("\$base = $base $op $oparg;");
+
+ $result[0] = $base;
+ break;
+ case 'x': // ChainTargetCount
+ case 'X':
+ $base = $srcSpell->getField('effect'.$effIdx.'ChainTarget');
+
+ if (in_array($op, $signs) && is_numeric($oparg) && is_numeric($base))
+ eval("\$base = $base $op $oparg;");
+
+ $result[0] = $base;
+ break;
+ case 'z': // HomeZone
+ $result[2] = Lang::spell('home');
+ break;
+ }
+
+ // handle excessively precise floats
+ if (is_float($result[0]))
+ $result[0] = round($result[0], 2);
+ if (isset($result[1]) && is_float($result[1]))
+ $result[1] = round($result[1], 2);
+
+ return $result;
+ }
+
+ // description-, buff-parsing component
+ private function resolveFormulaString($formula, $precision = 0, &$scaling)
+ {
+ $fSuffix = '%s';
+ $fRating = 0;
+
+ // step 1: formula unpacking redux
+ while (($formStartPos = strpos($formula, '${')) !== false)
+ {
+ $formBrktCnt = 0;
+ $formPrecision = 0;
+ $formCurPos = $formStartPos;
+
+ $formOutStr = '';
+
+ while ($formCurPos <= strlen($formula))
+ {
+ $char = $formula[$formCurPos];
+
+ if ($char == '}')
+ $formBrktCnt--;
+
+ if ($formBrktCnt)
+ $formOutStr .= $char;
+
+ if ($char == '{')
+ $formBrktCnt++;
+
+ if (!$formBrktCnt && $formCurPos != $formStartPos)
+ break;
+
+ $formCurPos++;
+ }
+
+ if (isset($formula[++$formCurPos]) && $formula[$formCurPos] == '.')
+ {
+ $formPrecision = (int)$formula[++$formCurPos];
+ ++$formCurPos; // for some odd reason the precision decimal survives if we dont increment further..
+ }
+
+ list($formOutStr, $fSuffix, $fRating) = $this->resolveFormulaString($formOutStr, $formPrecision, $scaling);
+
+ $formula = substr_replace($formula, $formOutStr, $formStartPos, ($formCurPos - $formStartPos));
+ }
+
+ // note: broken tooltip on this one
+ // ${58644m1/-10} gets matched as a formula (ok), 58644m1 has no $ prefixed (not ok)
+ // the client scraps the m1 and prints -5864
+ if ($this->id == 58644)
+ $formula = '$'.$formula;
+
+ // step 2: resolve variables
+ $pos = 0; // continue strpos-search from this offset
+ $str = '';
+ while (($npos = strpos($formula, '$', $pos)) !== false)
+ {
+ if ($npos != $pos)
+ $str .= substr($formula, $pos, $npos - $pos);
+
+ $pos = $npos++;
+
+ if ($formula[$pos] == '$')
+ $pos++;
+
+ $varParts = $this->matchVariableString(substr($formula, $pos), $len);
+ if (!$varParts)
+ {
+ $str .= '#'; // mark as done, reset below
+ continue;
+ }
+
+ $pos += $len;
+
+ // we are resolving a formula -> omit ranges
+ $var = $this->resolveVariableString($varParts, $scaling);
+
+ // time within formula -> rebase to seconds and omit timeUnit
+ if (strtolower($varParts['var']) == 'd')
+ {
+ $var[0] /= 1000;
+ unset($var[2]);
+ }
+
+ $str .= $var[0];
+
+ // overwrite eventually inherited strings
+ if (isset($var[2]))
+ $fSuffix = $var[2];
+
+ // overwrite eventually inherited ratings
+ if (isset($var[4]))
+ $fRating = $var[4];
+ }
+ $str .= substr($formula, $pos);
+ $str = str_replace('#', '$', $str); // reset marks
+
+ // step 3: try to evaluate result
+ $evaled = $this->resolveEvaluation($str);
+
+ $return = is_numeric($evaled) ? Lang::nf($evaled, $precision, true) : $evaled;
+
+ return [$return, $fSuffix, $fRating];
+ }
+
+ // should probably used only once to create ?_spell. come to think of it, it yields the same results every time.. it absolutely has to!
+ // although it seems to be pretty fast, even on those pesky test-spells with extra complex tooltips (Ron Test Spell X))
+ public function parseText($type = 'description', $level = MAX_LEVEL, $interactive = false, &$scaling = false)
+ {
+ // oooo..kaaayy.. parsing text in 6 or 7 easy steps
+ // we don't use the internal iterator here. This func has to be called for the individual template.
+ // otherwise it will get a bit messy, when we iterate, while we iterate *yo dawg!*
+
+ /* documentation .. sort of
+ bracket use
+ ${}.x - formulas; .x is optional; x:[0-9] .. max-precision of a floatpoint-result; default: 0
+ $[] - conditionals ... like $?condition[true][false]; alternative $?!(cond1|cond2)[true]$?cond3[elseTrue][false]; ?a40120: has aura 40120; ?s40120: knows spell 40120(?)
+ $<> - variables
+ () - regular use for function-like calls
+
+ variables in use .. caseSensitive
+
+ game variables (optionally replace with textVars)
+ $PlayerName - Cpt. Obvious
+ $PL / $pl - PlayerLevel
+ $STR - Strength Attribute (not seen)
+ $AGI - Agility Attribute (not seen)
+ $STA - Stamina Attribute (not seen)
+ $INT - Intellect Attribute (not seen)
+ $SPI - Spirit Attribute
+ $AP - Atkpwr
+ $RAP - RngAtkPwr
+ $HND - hands used by weapon (1H, 2H) => (1, 2)
+ $MWS - MainhandWeaponSpeed
+ $mw / $MW - MainhandWeaponDamage Min/Max
+ $rwb / $RWB - RangedWeapon..Bonus? Min/Max
+ $sp - Spellpower
+ $spa - Spellpower Arcane
+ $spfi - Spellpower Fire
+ $spfr - Spellpower Frost
+ $sph - Spellpower Holy
+ $spn - Spellpower Nature
+ $sps - Spellpower Shadow
+ $bh - Bonus Healing
+ $pa - %-ArcaneDmg (as float) // V seems broken
+ $pfi - %-FireDmg (as float)
+ $pfr - %-FrostDmg (as float)
+ $ph - %-HolyDmg (as float)
+ $pn - %-NatureDmg (as float)
+ $ps - %-ShadowDmg (as float)
+ $pbh - %-HealingBonus (as float)
+ $pbhd - %-Healing Done (as float) // all above seem broken
+ $bc2 - baseCritChance? always 3.25 (unsure)
+
+ spell variables (the stuff we can actually parse) rounding... >5 up?
+ $a - SpellRadius; per EffectIdx
+ $b - PointsPerComboPoint; per EffectIdx
+ $d / $D - SpellDuration; appended timeShorthand; d/D maybe base/max duration?; interpret "0" as "until canceled"
+ $e - EffectValueMultiplier; per EffectIdx
+ $f / $F - EffectDamageMultiplier; per EffectIdx
+ $g / $G - Gender-Switch $Gmale:female;
+ $h / $H - ProcChance
+ $i - MaxAffectedTargets
+ $l - LastValue-Switch; last value as condition $Ltrue:false;
+ $m / $M - BasePoints; per EffectIdx; m/M +1/+effectDieSides
+ $n - ProcCharges
+ $o - TotalAmount (for periodic auras); per EffectIdx
+ $q - EffectMiscValue; per EffectIdx
+ $r - SpellRange (hostile)
+ $s / $S - BasePoints; per EffectIdx; as Range, if applicable
+ $t / $T - EffectPeriode; per EffectIdx
+ $u - StackAmount
+ $v - MaxTargetLevel
+ $x - MaxAffectedTargets
+ $z - no place like
+
+ deviations from standard procedures
+ division - example: $/10;2687s1 => $2687s1/10
+ - also: $61829/5;s1 => $61829s1/5
+
+ functions in use .. caseInsensitive
+ $cond(a, b, c) - like SQL, if A is met use B otherwise use C
+ $eq(a, b) - a == b
+ $floor(a) - floor()
+ $gt(a, b) - a > b
+ $gte(a, b) - a >= b
+ $min(a, b) - min()
+ $max(a, b) - max()
+ */
+
+ $this->interactive = $interactive;
+ $this->charLevel = $level;
+
+ // step 0: get text
+ $data = $this->getField($type, true);
+ if (empty($data) || $data == "[]") // empty tooltip shouldn't be displayed anyway
+ return ['', []];
+
+ // step 1: if the text is supplemented with text-variables, get and replace them
+ if ($this->curTpl['spellDescriptionVariableId'] > 0)
+ {
+ if (empty($this->spellVars[$this->id]))
+ {
+ $spellVars = DB::Aowow()->SelectCell('SELECT vars FROM ?_spellvariables WHERE id = ?d', $this->curTpl['spellDescriptionVariableId']);
+ $spellVars = explode("\n", $spellVars);
+ foreach ($spellVars as $sv)
+ if (preg_match('/\$(\w*\d*)=(.*)/i', trim($sv), $matches))
+ $this->spellVars[$this->id][$matches[1]] = $matches[2];
+ }
+
+ // replace self-references
+ $reset = true;
+ while ($reset)
+ {
+ $reset = false;
+ foreach ($this->spellVars[$this->id] as $k => $sv)
+ {
+ if (preg_match('/\$<(\w*\d*)>/i', $sv, $matches))
+ {
+ $this->spellVars[$this->id][$k] = str_replace('$<'.$matches[1].'>', '${'.$this->spellVars[$this->id][$matches[1]].'}', $sv);
+ $reset = true;
+ }
+ }
+ }
+
+ // finally, replace SpellDescVars
+ foreach ($this->spellVars[$this->id] as $k => $sv)
+ $data = str_replace('$<'.$k.'>', $sv, $data);
+ }
+
+ // step 2: resolving conditions
+ // aura- or spell-conditions cant be resolved for our purposes, so force them to false for now (todo (low): strg+f "know" in aowowPower.js ^.^)
+
+ /* sequences
+ a) simple - $?cond[A][B] // simple case of b)
+ b) elseif - $?cond[A]?cond[B]..[C] // can probably be repeated as often as you wanted
+ c) recursive - $?cond[A][$?cond[B][..]] // can probably be stacked as deep as you wanted
+
+ only case a) can be used for KNOW-parameter
+ */
+
+ $relSpells = [];
+ $data = $this->handleConditions($data, $scaling, $relSpells, true);
+
+ // step 3: unpack formulas ${ .. }.X
+ $data = $this->handleFormulas($data, $scaling, true);
+
+ // step 4: find and eliminate regular variables
+ $data = $this->handleVariables($data, $scaling, true);
+
+ // step 5: variable-dependant variable-text
+ // special case $lONE:ELSE[:ELSE2]; or $|ONE:ELSE[:ELSE2];
+ while (preg_match('/([\d\.]+)([^\d]*)(\$[l|]:*)([^:]*):([^;]*);/i', $data, $m))
+ {
+ $plurals = explode(':', $m[5]);
+ $replace = '';
+
+ if (count($plurals) == 2) // special case: ruRU
+ {
+ switch (substr($m[1], -1)) // check last digit of number
+ {
+ case 1:
+ // but not 11 (teen number)
+ if (!in_array($m[1], [11]))
+ {
+ $replace = $m[4];
+ break;
+ }
+ case 2:
+ case 3:
+ case 4:
+ // but not 12, 13, 14 (teen number) [11 is passthrough]
+ if (!in_array($m[1], [11, 12, 13, 14]))
+ {
+ $replace = $plurals[0];
+ break;
+ }
+ break;
+ default:
+ $replace = $plurals[1];
+ }
+
+ }
+ else
+ $replace = ($m[1] == 1 ? $m[4] : $plurals[0]);
+
+ $data = str_ireplace($m[1].$m[2].$m[3].$m[4].':'.$m[5].';', $m[1].$m[2].$replace, $data);
+ }
+
+ // step 6: HTMLize
+ // colors
+ $data = preg_replace('/\|cff([a-f0-9]{6})(.+?)\|r/i', '$2', $data);
+
+ // line endings
+ $data = strtr($data, ["\r" => '', "\n" => ' ']);
+
+ return [$data, $relSpells];
+ }
+
+ private function handleFormulas($data, &$scaling, $topLevel = false)
+ {
+ // they are stacked recursively but should be balanced .. hf
+ while (($formStartPos = strpos($data, '${')) !== false)
+ {
+ $formBrktCnt = 0;
+ $formPrecision = 0;
+ $formCurPos = $formStartPos;
+
+ $formOutStr = '';
+
+ while ($formCurPos <= strlen($data)) // only hard-exit condition, we'll hit those breaks eventually^^
+ {
+ $char = $data[$formCurPos];
+
+ if ($char == '}')
+ $formBrktCnt--;
+
+ if ($formBrktCnt)
+ $formOutStr .= $char;
+
+ if ($char == '{')
+ $formBrktCnt++;
+
+ if (!$formBrktCnt && $formCurPos != $formStartPos)
+ break;
+
+ // advance position
+ $formCurPos++;
+ }
+
+ $formCurPos++;
+
+ // check for precision-modifiers
+ if ($formCurPos + 1 < strlen($data) && $data[$formCurPos] == '.' && is_numeric($data[$formCurPos + 1]))
+ {
+ $formPrecision = $data[$formCurPos + 1];
+ $formCurPos += 2;
+ }
+ list($formOutVal, $formOutStr, $ratingId) = $this->resolveFormulaString($formOutStr, $formPrecision ?: ($topLevel ? 0 : 10), $scaling);
+
+ if ($ratingId && Util::checkNumeric($formOutVal) && $this->interactive)
+ $resolved = sprintf($formOutStr, $ratingId, abs($formOutVal), sprintf(Util::$setRatingLevelString, $this->charLevel, $ratingId, abs($formOutVal), Util::setRatingLevel($this->charLevel, $ratingId, abs($formOutVal))));
+ else if ($ratingId && Util::checkNumeric($formOutVal))
+ $resolved = sprintf($formOutStr, $ratingId, abs($formOutVal), Util::setRatingLevel($this->charLevel, $ratingId, abs($formOutVal)));
+ else
+ $resolved = sprintf($formOutStr, Util::checkNumeric($formOutVal) ? abs($formOutVal) : $formOutVal);
+
+ $data = substr_replace($data, $resolved, $formStartPos, ($formCurPos - $formStartPos));
+ }
+
+ return $data;
+ }
+
+ private function handleVariables($data, &$scaling, $topLevel = false)
+ {
+ $pos = 0; // continue strpos-search from this offset
+ $str = '';
+ while (($npos = strpos($data, '$', $pos)) !== false)
+ {
+ if ($npos != $pos)
+ $str .= substr($data, $pos, $npos - $pos);
+
+ $pos = $npos++;
+
+ if ($data[$pos] == '$')
+ $pos++;
+
+ $varParts = $this->matchVariableString(substr($data, $pos), $len);
+ if (!$varParts)
+ {
+ $str .= '#'; // mark as done, reset below
+ continue;
+ }
+
+ $pos += $len;
+
+ $var = $this->resolveVariableString($varParts, $scaling);
+ $resolved = is_numeric($var[0]) ? abs($var[0]) : $var[0];
+ if (isset($var[2]))
+ {
+ if (isset($var[4]) && $this->interactive)
+ $resolved = sprintf($var[2], $var[4], abs($var[0]), sprintf(Util::$setRatingLevelString, $this->charLevel, $var[4], abs($var[0]), Util::setRatingLevel($this->charLevel, $var[4], abs($var[0]))));
+ else if (isset($var[4]))
+ $resolved = sprintf($var[2], $var[4], abs($var[0]), Util::setRatingLevel($this->charLevel, $var[4], abs($var[0])));
+ else
+ $resolved = sprintf($var[2], $resolved);
+ }
+
+ if (isset($var[1]) && $var[0] != $var[1] && !isset($var[4]))
+ {
+ $_ = is_numeric($var[1]) ? abs($var[1]) : $var[1];
+ $resolved .= Lang::game('valueDelim');
+ $resolved .= isset($var[3]) ? sprintf($var[3], $_) : $_;
+ }
+
+ $str .= $resolved;
+ }
+ $str .= substr($data, $pos);
+ $str = str_replace('#', '$', $str); // reset marker
+
+ return $str;
+ }
+
+ private function handleConditions($data, &$scaling, &$relSpells, $topLevel = false)
+ {
+ while (($condStartPos = strpos($data, '$?')) !== false)
+ {
+ $condBrktCnt = 0;
+ $condCurPos = $condStartPos + 2; // after the '$?'
+ $targetPart = 3; // we usually want the second pair of brackets
+ $curPart = 0; // parts: $? 0 [ 1 ] 2 [ 3 ] 4 ...
+ $condParts = [];
+ $isLastElse = false;
+
+ while ($condCurPos <= strlen($data)) // only hard-exit condition, we'll hit those breaks eventually^^
+ {
+ $char = $data[$condCurPos];
+
+ // advance position
+ $condCurPos++;
+
+ if ($char == '[')
+ {
+ $condBrktCnt++;
+
+ if ($condBrktCnt == 1)
+ $curPart++;
+
+ // previously there was no condition -> last else
+ if ($condBrktCnt == 1)
+ if (($curPart && ($curPart % 2)) && (!isset($condParts[$curPart - 1]) || empty(trim($condParts[$curPart - 1]))))
+ $isLastElse = true;
+
+ if (empty($condParts[$curPart]))
+ continue;
+ }
+
+ if (empty($condParts[$curPart]))
+ $condParts[$curPart] = $char;
+ else
+ $condParts[$curPart] .= $char;
+
+ if ($char == ']')
+ {
+ $condBrktCnt--;
+
+ if (!$condBrktCnt)
+ {
+ $condParts[$curPart] = substr($condParts[$curPart], 0, -1);
+ $curPart++;
+ }
+
+ if ($condBrktCnt)
+ continue;
+
+ if ($isLastElse)
+ break;
+ }
+ }
+
+ // check if it is know-compatible
+ $know = 0;
+ if (preg_match('/\(?(\!?)[as](\d+)\)?$/i', $condParts[0], $m))
+ {
+ if (!strstr($condParts[1], '$?'))
+ if (!strstr($condParts[3], '$?'))
+ if (!isset($condParts[5]))
+ $know = $m[2];
+
+ // found a negation -> switching condition target
+ if ($m[1] == '!')
+ $targetPart = 1;
+ }
+ // if not, what part of the condition should be used?
+ else if (preg_match('/(([\W\D]*[as]\d+)|([^\[]*))/i', $condParts[0], $m) && !empty($m[3]))
+ {
+ $cnd = $this->resolveEvaluation($m[3]);
+ if ((is_numeric($cnd) || is_bool($cnd)) && $cnd) // only case, deviating from normal; positive result -> use [true]
+ $targetPart = 1;
+ }
+
+ // recursive conditions
+ if (strstr($condParts[$targetPart], '$?'))
+ $condParts[$targetPart] = $this->handleConditions($condParts[$targetPart], $scaling, $relSpells);
+
+ if ($know && $topLevel)
+ {
+ foreach ([1, 3] as $pos)
+ {
+ if (strstr($condParts[$pos], '${'))
+ $condParts[$pos] = $this->handleFormulas($condParts[$pos], $scaling);
+
+ if (strstr($condParts[$pos], '$'))
+ $condParts[$pos] = $this->handleVariables($condParts[$pos], $scaling);
+ }
+
+ // false condition first
+ if (!isset($relSpells[$know]))
+ $relSpells[$know] = [];
+
+ $relSpells[$know][] = [$condParts[3], $condParts[1]];
+
+ $data = substr_replace($data, ''.$condParts[$targetPart].'', $condStartPos, ($condCurPos - $condStartPos));
+ }
+ else
+ $data = substr_replace($data, $condParts[$targetPart], $condStartPos, ($condCurPos - $condStartPos));
+ }
+
+ return $data;
+ }
+
+ public function renderBuff($level = MAX_LEVEL, $interactive = false)
+ {
+ if (!$this->curTpl)
+ return ['', []];
+
+ // doesn't have a buff
+ if (!$this->getField('buff', true))
+ return ['', []];
+
+ $this->interactive = $interactive;
+
+ $x = '';
+
+ // spellName
+ $x .= '| '.$this->getField('name', true).' | ';
+
+ // dispelType (if applicable)
+ if ($this->curTpl['dispelType'])
+ if ($dispel = Lang::game('dt', $this->curTpl['dispelType']))
+ $x .= ''.$dispel.' | ';
+
+ $x .= ' ';
+
+ $x .= '';
+
+ // parse Buff-Text
+ $btt = $this->parseText('buff', $level, $this->interactive, $scaling);
+ $x .= $btt[0].' ';
+
+ // duration
+ if ($this->curTpl['duration'] > 0)
+ $x .= ''.sprintf(Lang::spell('remaining'), Util::formatTime($this->curTpl['duration'])).'';
+
+ $x .= ' | ';
+
+ // scaling information - spellId:min:max:curr
+ $x .= '';
+
+ return [$x, $btt[1]];
+ }
+
+ public function renderTooltip($level = MAX_LEVEL, $interactive = false)
+ {
+ if (!$this->curTpl)
+ return ['', []];
+
+ $this->interactive = $interactive;
+
+ // fetch needed texts
+ $name = $this->getField('name', true);
+ $rank = $this->getField('rank', true);
+ $desc = $this->parseText('description', $level, $this->interactive, $scaling);
+ $tools = $this->getToolsForCurrent();
+ $cool = $this->createCooldownForCurrent();
+ $cast = $this->createCastTimeForCurrent();
+ $cost = $this->createPowerCostForCurrent();
+ $range = $this->createRangesForCurrent();
+
+ // get reagents
+ $reagents = $this->getReagentsForCurrent();
+ foreach ($reagents as &$r)
+ $r[2] = ItemList::getName($r[0]);
+
+ $reagents = array_reverse($reagents);
+
+ // get stances (check: SPELL_ATTR2_NOT_NEED_SHAPESHIFT)
+ $stances = '';
+ if ($this->curTpl['stanceMask'] && !($this->curTpl['attributes2'] & 0x80000))
+ $stances = Lang::game('requires2').' '.Lang::getStances($this->curTpl['stanceMask']);
+
+ // get item requirement (skip for professions)
+ $reqItems = '';
+ if ($this->curTpl['typeCat'] != 11)
+ {
+ $class = $this->getField('equippedItemClass');
+ $mask = $this->getField('equippedItemSubClassMask');
+ $reqItems = Lang::getRequiredItems($class, $mask);
+ }
+
+ // get created items (may need improvement)
+ $createItem = '';
+ if (in_array($this->curTpl['typeCat'], [9, 11])) // only Professions
+ {
+ foreach ($this->canCreateItem() as $idx)
+ {
+ if ($this->curTpl['effect'.$idx.'Id'] == 53)// Enchantment (has createItem Scroll of Enchantment)
+ continue;
+
+ foreach ($this->relItems->iterate() as $cId => $__)
+ {
+ if ($cId != $this->curTpl['effect'.$idx.'CreateItemId'])
+ continue;
+
+ $createItem = $this->relItems->renderTooltip(true, $this->id);
+ break 2;
+ }
+ }
+ }
+
+ $x = '';
+ $x .= '';
+
+ // name & rank
+ if ($rank)
+ $x .= '';
+ else
+ $x .= ''.$name.' ';
+
+ // powerCost & ranges
+ if ($range && $cost)
+ $x .= '';
+ else if ($cost || $range)
+ $x .= $range.$cost.' ';
+
+ // castTime & cooldown
+ if ($cast && $cool) // tabled layout
+ {
+ $x .= '';
+ $x .= '| '.$cast.' | '.$cool.' | ';
+ if ($stances)
+ $x.= '| '.$stances.' | ';
+
+ $x .= ' ';
+ }
+ else if ($cast || $cool) // line-break layout
+ {
+ $x .= $cast.$cool;
+
+ if ($stances)
+ $x .= ' '.$stances;
+ }
+
+ $x .= ' | ';
+
+ $xTmp = [];
+
+ if ($tools)
+ {
+ $_ = Lang::spell('tools').':
';
+ while ($tool = array_pop($tools))
+ {
+ if (isset($tool['itemId']))
+ $_ .= ' '.$tool['name'].'';
+ else if (isset($tool['id']))
+ $_ .= ' '.$tool['name'].'';
+ else
+ $_ .= $tool['name'];
+
+ if (!empty($tools))
+ $_ .= ', ';
+ else
+ $_ .= ' ';
+ }
+
+ $xTmp[] = $_.' ';
+ }
+
+ if ($reagents)
+ {
+ $_ = Lang::spell('reagents').':
';
+ while ($reagent = array_pop($reagents))
+ {
+ $_ .= ' '.$reagent[2].'';
+ if ($reagent[1] > 1)
+ $_ .= ' ('.$reagent[1].')';
+
+ $_ .= empty($reagents) ? ' ' : ', ';
+ }
+
+ $xTmp[] = $_.' ';
+ }
+
+ if ($reqItems)
+ $xTmp[] = Lang::game('requires2').' '.$reqItems;
+
+ if ($desc[0])
+ $xTmp[] = ''.$desc[0].'';
+
+ if ($createItem)
+ $xTmp[] = $createItem;
+
+ if ($xTmp)
+ $x .= '';
+
+ // scaling information - spellId:min:max:curr
+ $x .= '';
+
+ return [$x, $desc[1]];
+ }
+
+ public function getTalentHeadForCurrent()
+ {
+ // power cost: pct over static
+ $cost = $this->createPowerCostForCurrent();
+
+ // ranges
+ $range = $this->createRangesForCurrent();
+
+ // cast times
+ $cast = $this->createCastTimeForCurrent();
+
+ // cooldown or categorycooldown
+ $cool = $this->createCooldownForCurrent();
+
+ // assemble parts
+ // upper: cost :: range
+ // lower: time :: cooldown
+ $x = '';
+
+ // upper
+ if ($cost && $range)
+ $x .= '';
+ else
+ $x .= $cost.$range;
+
+ if (($cost xor $range) && ($cast xor $cool))
+ $x .= ' ';
+
+ // lower
+ if ($cast && $cool)
+ $x .= '';
+ else
+ $x .= $cast.$cool;
+
+ return $x;
+ }
+
+ public function getColorsForCurrent()
+ {
+ $gry = $this->curTpl['skillLevelGrey'];
+ $ylw = $this->curTpl['skillLevelYellow'];
+ $grn = (int)(($ylw + $gry) / 2);
+ $org = $this->curTpl['learnedAt'];
+
+ if (($org && $ylw < $org) || $ylw >= $gry)
+ $ylw = 0;
+
+ if (($org && $grn < $org) || $grn >= $gry)
+ $grn = 0;
+
+ if (($grn && $org >= $grn) || $org >= $gry)
+ $org = 0;
+
+ return $gry > 1 ? [$org, $ylw, $grn, $gry] : null;
+ }
+
+ public function getListviewData($addInfoMask = 0x0)
+ {
+ $data = [];
+
+ if ($addInfoMask & ITEMINFO_MODEL)
+ $modelInfo = $this->getModelInfo();
+
+ foreach ($this->iterate() as $__)
+ {
+ $quality = ($this->curTpl['cuFlags'] & SPELL_CU_QUALITY_MASK) >> 8;
+ $talent = $this->curTpl['cuFlags'] & (SPELL_CU_TALENT | SPELL_CU_TALENTSPELL) && $this->curTpl['spellLevel'] <= 1;
+
+ $data[$this->id] = array(
+ 'id' => $this->id,
+ 'name' => ($quality ?: '@').$this->getField('name', true),
+ 'icon' => $this->curTpl['iconStringAlt'] ?: $this->curTpl['iconString'],
+ 'level' => $talent ? $this->curTpl['talentLevel'] : $this->curTpl['spellLevel'],
+ 'school' => $this->curTpl['schoolMask'],
+ 'cat' => $this->curTpl['typeCat'],
+ 'trainingcost' => $this->curTpl['trainingCost'],
+ 'skill' => count($this->curTpl['skillLines']) > 4 ? array_merge(array_splice($this->curTpl['skillLines'], 0, 4), [-1]): $this->curTpl['skillLines'], // display max 4 skillLines (fills max three lines in listview)
+ 'reagents' => array_values($this->getReagentsForCurrent()),
+ 'source' => []
+ // 'talentspec' => $this->curTpl['skillLines'][0] not used: g_chr_specs has the wrong structure for it; also setting .cat and .type does the same
+ );
+
+ // Sources
+ if (!empty($this->sources[$this->id]))
+ {
+ $data[$this->id]['source'] = array_keys($this->sources[$this->id]);
+ if (!empty($this->sources[$this->id][3]))
+ $data[$this->id]['sourcemore'] = [['p' => $this->sources[$this->id][3][0]]];
+ }
+
+ // Proficiencies
+ if ($this->curTpl['typeCat'] == -11)
+ foreach (self::$spellTypes as $cat => $type)
+ if (in_array($this->curTpl['skillLines'][0], self::$skillLines[$cat]))
+ $data[$this->id]['type'] = $type;
+
+ // creates item
+ foreach ($this->canCreateItem() as $idx)
+ {
+ $max = $this->curTpl['effect'.$idx.'DieSides'] + $this->curTpl['effect'.$idx.'BasePoints'];
+ $min = $this->curTpl['effect'.$idx.'DieSides'] > 1 ? 1 : $max;
+
+ $data[$this->id]['creates'] = [$this->curTpl['effect'.$idx.'CreateItemId'], $min, $max];
+ break;
+ }
+
+ // Profession
+ if (in_array($this->curTpl['typeCat'], [9, 11]))
+ {
+ if ($la = $this->curTpl['learnedAt'])
+ $data[$this->id]['learnedat'] = $la;
+ else if (($la = $this->curTpl['reqSkillLevel']) > 1)
+ $data[$this->id]['learnedat'] = $la;
+
+ $data[$this->id]['colors'] = $this->getColorsForCurrent();
+ }
+
+ // glyph
+ if ($this->curTpl['typeCat'] == -13)
+ $data[$this->id]['glyphtype'] = $this->curTpl['cuFlags'] & SPELL_CU_GLYPH_MAJOR ? 1 : 2;
+
+ if ($r = $this->getField('rank', true))
+ $data[$this->id]['rank'] = $r;
+
+ if ($mask = $this->curTpl['reqClassMask'])
+ $data[$this->id]['reqclass'] = $mask;
+
+ if ($mask = $this->curTpl['reqRaceMask'])
+ $data[$this->id]['reqrace'] = $mask;
+
+
+ if ($addInfoMask & ITEMINFO_MODEL)
+ {
+ // may have multiple models set, in this case i've no idea what should be picked
+ for ($i = 1; $i < 4; $i++)
+ {
+ if (!empty($modelInfo[$this->id][$i]))
+ {
+ $data[$this->id]['npcId'] = $modelInfo[$this->id][$i]['typeId'];
+ $data[$this->id]['displayId'] = $modelInfo[$this->id][$i]['displayId'];
+ $data[$this->id]['displayName'] = $modelInfo[$this->id][$i]['displayName'];
+ break;
+ }
+ }
+ }
+ }
+
+ return $data;
+ }
+
+ public function getJSGlobals($addMask = GLOBALINFO_SELF, &$extra = [])
+ {
+ $data = [];
+
+ if ($this->relItems && ($addMask & GLOBALINFO_RELATED))
+ $data = $this->relItems->getJSGlobals();
+
+ foreach ($this->iterate() as $id => $__)
+ {
+ if ($addMask & GLOBALINFO_RELATED)
+ {
+ if ($mask = $this->curTpl['reqClassMask'])
+ for ($i = 0; $i < 11; $i++)
+ if ($mask & (1 << $i))
+ $data[TYPE_CLASS][$i + 1] = $i + 1;
+
+ if ($mask = $this->curTpl['reqRaceMask'])
+ for ($i = 0; $i < 11; $i++)
+ if ($mask & (1 << $i))
+ $data[TYPE_RACE][$i + 1] = $i + 1;
+
+ // play sound effect
+ for ($i = 1; $i < 4; $i++)
+ if ($this->getField('effect'.$i.'Id') == 131 || $this->getField('effect'.$i.'Id') == 132)
+ $data[TYPE_SOUND][$this->getField('effect'.$i.'MiscValue')] = $this->getField('effect'.$i.'MiscValue');
+ }
+
+ if ($addMask & GLOBALINFO_SELF)
+ {
+ $iconString = $this->curTpl['iconStringAlt'] ? 'iconStringAlt' : 'iconString';
+
+ $data[TYPE_SPELL][$id] = array(
+ 'icon' => $this->curTpl[$iconString],
+ 'name' => $this->getField('name', true),
+ );
+ }
+
+ if ($addMask & GLOBALINFO_EXTRA)
+ {
+ $buff = $this->renderBuff(MAX_LEVEL, true);
+ $tTip = $this->renderTooltip(MAX_LEVEL, true);
+
+ foreach ($tTip[1] as $relId => $_)
+ if (empty($data[TYPE_SPELL][$relId]))
+ $data[TYPE_SPELL][$relId] = $relId;
+
+ foreach ($buff[1] as $relId => $_)
+ if (empty($data[TYPE_SPELL][$relId]))
+ $data[TYPE_SPELL][$relId] = $relId;
+
+ $extra[$id] = array(
+ 'id' => $id,
+ 'tooltip' => $tTip[0],
+ 'buff' => !empty($buff[0]) ? $buff[0] : null,
+ 'spells' => $tTip[1],
+ 'buffspells' => !empty($buff[1]) ? $buff[1] : null
+ );
+ }
+ }
+
+ return $data;
+ }
+
+ // mostly similar to TC
+ public function getCastingTimeForBonus($asDOT = false)
+ {
+ $areaTargets = [7, 8, 15, 16, 20, 24, 30, 31, 33, 34, 37, 54, 56, 59, 104, 108];
+ $castingTime = $this->IsChanneledSpell() ? $this->curTpl['duration'] : $this->curTpl['castTime'];
+
+ if (!$castingTime)
+ return 3500;
+
+ if ($castingTime > 7000)
+ $castingTime = 7000;
+
+ if ($castingTime < 1500)
+ $castingTime = 1500;
+
+ if ($asDOT && !$this->isChanneledSpell())
+ $castingTime = 3500;
+
+ $overTime = 0;
+ $nEffects = 0;
+ $isDirect = false;
+ $isArea = false;
+
+ for ($i = 1; $i <= 3; $i++)
+ {
+ if (in_array($this->curTpl['effect'.$i.'Id'], [2, 7, 8, 9, 62, 67]))
+ $isDirect = true;
+ else if (in_array($this->curTpl['effect'.$i.'AuraId'], [3, 8, 53]))
+ if ($_ = $this->curTpl['duration'])
+ $overTime = $_;
+ else if ($this->curTpl['effect'.$i.'AuraId'])
+ $nEffects++;
+
+ if (in_array($this->curTpl['effect'.$i.'ImplicitTargetA'], $areaTargets) || in_array($this->curTpl['effect'.$i.'ImplicitTargetB'], $areaTargets))
+ $isArea = true;
+ }
+
+ // Combined Spells with Both Over Time and Direct Damage
+ if ($overTime > 0 && $castingTime > 0 && $isDirect)
+ {
+ // mainly for DoTs which are 3500 here otherwise
+ $originalCastTime = $this->curTpl['castTime'];
+ if ($this->curTpl['attributes0'] & 0x2) // requires Ammo
+ $originalCastTime += 500;
+
+ if ($originalCastTime > 7000)
+ $originalCastTime = 7000;
+
+ if ($originalCastTime < 1500)
+ $originalCastTime = 1500;
+
+ // Portion to Over Time
+ $PtOT = ($overTime / 15000) / (($overTime / 15000) + (OriginalCastTime / 3500));
+
+ if ($asDOT)
+ $castingTime = $castingTime * $PtOT;
+ else if ($PtOT < 1)
+ $castingTime = $castingTime * (1 - $PtOT);
+ else
+ $castingTime = 0;
+ }
+
+ // Area Effect Spells receive only half of bonus
+ if ($isArea)
+ $castingTime /= 2;
+
+ // -5% of total per any additional effect
+ $castingTime -= ($nEffects * 175);
+ if ($castingTime < 0)
+ $castingTime = 0;
+
+ return $castingTime;
+ }
+
+ public function getSourceData()
+ {
+ $data = [];
+
+ foreach ($this->iterate() as $__)
+ {
+ $data[$this->id] = array(
+ 'n' => $this->getField('name', true),
+ 't' => TYPE_SPELL,
+ 'ti' => $this->id,
+ 's' => empty($this->curTpl['skillLines']) ? 0 : $this->curTpl['skillLines'][0],
+ 'c' => $this->curTpl['typeCat'],
+ 'icon' => $this->curTpl['iconStringAlt'] ? $this->curTpl['iconStringAlt'] : $this->curTpl['iconString'],
+ );
+ }
+
+ return $data;
+ }
+}
+
+
+class SpellListFilter extends Filter
+{
+ // sources in filter and general use different indizes
+ private $enums = array(
+ 9 => array(
+ 1 => true, // Any
+ 2 => false, // None
+ 3 => 1, // Crafted
+ 4 => 2, // Drop
+ 6 => 4, // Quest
+ 7 => 5, // Vendor
+ 8 => 6, // Trainer
+ 9 => 7, // Discovery
+ 10 => 9 // Talent
+ )
+ );
+
+ // cr => [type, field, misc, extraCol]
+ protected $genericFilter = array( // misc (bool): _NUMERIC => useFloat; _STRING => localized; _FLAG => match Value; _BOOLEAN => stringSet
+ 1 => [FILTER_CR_CALLBACK, 'cbCost', null, null], // costAbs [op] [int]
+ 2 => [FILTER_CR_NUMERIC, 'powerCostPercent', NUM_CAST_INT ], // prcntbasemanarequired
+ 3 => [FILTER_CR_BOOLEAN, 'spellFocusObject' ], // requiresnearbyobject
+ 4 => [FILTER_CR_NUMERIC, 'trainingcost', NUM_CAST_INT ], // trainingcost
+ 5 => [FILTER_CR_BOOLEAN, 'reqSpellId' ], // requiresprofspec
+ 8 => [FILTER_CR_FLAG, 'cuFlags', CUSTOM_HAS_SCREENSHOT ], // hasscreenshots
+ 9 => [FILTER_CR_CALLBACK, 'cbSource', null, null], // source [enum]
+ 10 => [FILTER_CR_FLAG, 'cuFlags', SPELL_CU_FIRST_RANK ], // firstrank
+ 11 => [FILTER_CR_FLAG, 'cuFlags', CUSTOM_HAS_COMMENT ], // hascomments
+ 12 => [FILTER_CR_FLAG, 'cuFlags', SPELL_CU_LAST_RANK ], // lastrank
+ 13 => [FILTER_CR_NUMERIC, 'rankNo', NUM_CAST_INT ], // rankno
+ 14 => [FILTER_CR_NUMERIC, 'id', NUM_CAST_INT, true], // id
+ 15 => [FILTER_CR_STRING, 'ic.name', ], // icon
+ 17 => [FILTER_CR_FLAG, 'cuFlags', CUSTOM_HAS_VIDEO ], // hasvideos
+ 19 => [FILTER_CR_FLAG, 'attributes0', 0x80000 ], // scaling
+ 20 => [FILTER_CR_CALLBACK, 'cbReagents', null, null], // has Reagents [yn]
+ 25 => [FILTER_CR_BOOLEAN, 'skillLevelYellow' ] // rewardsskillups
+ );
+
+ // fieldId => [checkType, checkValue[, fieldIsArray]]
+ protected $inputFields = array(
+ 'cr' => [FILTER_V_RANGE, [1, 25], true ], // criteria ids
+ 'crs' => [FILTER_V_LIST, [FILTER_ENUM_NONE, FILTER_ENUM_ANY, [0, 99999]], true ], // criteria operators
+ 'crv' => [FILTER_V_REGEX, '/[\p{C};:]/ui', true ], // criteria values - only printable chars, no delimiters
+ 'na' => [FILTER_V_REGEX, '/[\p{C};]/ui', false], // name / text - only printable chars, no delimiter
+ 'ex' => [FILTER_V_EQUAL, 'on', false], // extended name search
+ 'ma' => [FILTER_V_EQUAL, 1, false], // match any / all filter
+ 'minle' => [FILTER_V_RANGE, [1, 99], false], // spell level min
+ 'maxle' => [FILTER_V_RANGE, [1, 99], false], // spell level max
+ 'minrs' => [FILTER_V_RANGE, [1, 999], false], // required skill level min
+ 'maxrs' => [FILTER_V_RANGE, [1, 999], false], // required skill level max
+ 'ra' => [FILTER_V_LIST, [[1, 8], 10, 11], false], // races
+ 'cl' => [FILTER_V_CALLBACK, 'cbClasses', true ], // classes
+ 'gl' => [FILTER_V_CALLBACK, 'cbGlyphs', true ], // glyph type
+ 'sc' => [FILTER_V_RANGE, [0, 6], true ], // magic schools
+ 'dt' => [FILTER_V_LIST, [[1, 6], 9], false], // dispel types
+ 'me' => [FILTER_V_RANGE, [1, 31], false] // mechanics
+ );
+
+ protected function createSQLForCriterium(&$cr)
+ {
+ if (in_array($cr[0], array_keys($this->genericFilter)))
+ if ($genCr = $this->genericCriterion($cr))
+ return $genCr;
+
+ unset($cr);
+ $this->error = true;
+ return [1];
+ }
+
+ protected function createSQLForValues()
+ {
+ $parts = [];
+ $_v = &$this->fiData['v'];
+
+ //string (extended)
+ if (isset($_v['na']))
+ {
+ $_ = [];
+ if (isset($_v['ex']) && $_v['ex'] == 'on')
+ $_ = $this->modularizeString(['name_loc'.User::$localeId, 'buff_loc'.User::$localeId, 'description_loc'.User::$localeId]);
+ else
+ $_ = $this->modularizeString(['name_loc'.User::$localeId]);
+
+ if ($_)
+ $parts[] = $_;
+ }
+
+ // spellLevel min todo (low): talentSpells (typeCat -2) commonly have spellLevel 1 (and talentLevel >1) -> query is inaccurate
+ if (isset($_v['minle']))
+ $parts[] = ['spellLevel', $_v['minle'], '>='];
+
+ // spellLevel max
+ if (isset($_v['maxle']))
+ $parts[] = ['spellLevel', $_v['maxle'], '<='];
+
+ // skillLevel min
+ if (isset($_v['minrs']))
+ $parts[] = ['learnedAt', $_v['minrs'], '>='];
+
+ // skillLevel max
+ if (isset($_v['maxrs']))
+ $parts[] = ['learnedAt', $_v['maxrs'], '<='];
+
+ // race
+ if (isset($_v['ra']))
+ $parts[] = ['AND', [['reqRaceMask', RACE_MASK_ALL, '&'], RACE_MASK_ALL, '!'], ['reqRaceMask', $this->list2Mask([$_v['ra']]), '&']];
+
+ // class [list]
+ if (isset($_v['cl']))
+ $parts[] = ['reqClassMask', $this->list2Mask($_v['cl']), '&'];
+
+ // school [list]
+ if (isset($_v['sc']))
+ $parts[] = ['schoolMask', $this->list2Mask($_v['sc'], true), '&'];
+
+ // glyph type [list] wonky, admittedly, but consult SPELL_CU_* in defines and it makes sense
+ if (isset($_v['gl']))
+ $parts[] = ['cuFlags', ($this->list2Mask($_v['gl']) << 6), '&'];
+
+ // dispel type
+ if (isset($_v['dt']))
+ $parts[] = ['dispelType', $_v['dt']];
+
+ // mechanic
+ if (isset($_v['me']))
+ $parts[] = ['OR', ['mechanic', $_v['me']], ['effect1Mechanic', $_v['me']], ['effect2Mechanic', $_v['me']], ['effect3Mechanic', $_v['me']]];
+
+ return $parts;
+ }
+
+ protected function cbClasses(&$val)
+ {
+ if (!$this->parentCats || !in_array($this->parentCats[0], [-13, -2, 7]))
+ return false;
+
+ if (!Util::checkNumeric($val, NUM_REQ_INT))
+ return false;
+
+ $type = FILTER_V_LIST;
+ $valid = [[1, 9], 11];
+
+ return $this->checkInput($type, $valid, $val);
+ }
+
+ protected function cbGlyphs(&$val)
+ {
+ if (!$this->parentCats || $this->parentCats[0] != -13)
+ return false;
+
+ if (!Util::checkNumeric($val, NUM_REQ_INT))
+ return false;
+
+ $type = FILTER_V_LIST;
+ $valid = [1, 2];
+
+ return $this->checkInput($type, $valid, $val);
+ }
+
+ protected function cbCost($cr)
+ {
+ if (!Util::checkNumeric($cr[2], NUM_CAST_INT) || !$this->int2Op($cr[1]))
+ return false;
+
+ return ['OR', ['AND', ['powerType', [1, 6]], ['powerCost', (10 * $cr[2]), $cr[1]]], ['AND', ['powerType', [1, 6], '!'], ['powerCost', $cr[2], $cr[1]]]];
+ }
+
+ protected function cbSource($cr)
+ {
+ if (!isset($this->enums[$cr[0]][$cr[1]]))
+ return false;
+
+ $_ = $this->enums[$cr[0]][$cr[1]];
+ if (is_int($_)) // specific
+ return ['src.src'.$_, null, '!'];
+ else if ($_) // any
+ return ['OR', ['src.src1', null, '!'], ['src.src2', null, '!'], ['src.src4', null, '!'], ['src.src5', null, '!'], ['src.src6', null, '!'], ['src.src7', null, '!'], ['src.src9', null, '!']];
+ else if (!$_) // none
+ return ['AND', ['src.src1', null], ['src.src2', null], ['src.src4', null], ['src.src5', null], ['src.src6', null], ['src.src7', null], ['src.src9', null]];
+
+ return false;
+ }
+
+ protected function cbReagents($cr)
+ {
+ if (!$this->int2Bool($cr[1]))
+ return false;
+
+ if ($cr[1])
+ return ['OR', ['reagent1', 0, '>'], ['reagent2', 0, '>'], ['reagent3', 0, '>'], ['reagent4', 0, '>'], ['reagent5', 0, '>'], ['reagent6', 0, '>'], ['reagent7', 0, '>'], ['reagent8', 0, '>']];
+ else
+ return ['AND', ['reagent1', 0], ['reagent2', 0], ['reagent3', 0], ['reagent4', 0], ['reagent5', 0], ['reagent6', 0], ['reagent7', 0], ['reagent8', 0]];
+ }
+}
+
+?>
diff --git a/includes/types/title.class.php b/includes/types/title.class.php
new file mode 100644
index 00000000..00ac972a
--- /dev/null
+++ b/includes/types/title.class.php
@@ -0,0 +1,174 @@
+ [['src']], // 11: TYPE_TITLE
+ 'src' => ['j' => ['?_source src ON type = 11 AND typeId = t.id', true], 's' => ', src13, moreType, moreTypeId']
+ );
+
+ public function __construct($conditions = [])
+ {
+ parent::__construct($conditions);
+
+ // post processing
+ foreach ($this->iterate() as $id => &$_curTpl)
+ {
+ // preparse sources - notice: under this system titles can't have more than one source (or two for achivements), which is enough for standard TC cases but may break custom cases
+ if ($_curTpl['moreType'] == TYPE_ACHIEVEMENT)
+ $this->sources[$this->id][12][] = $_curTpl['moreTypeId'];
+ else if ($_curTpl['moreType'] == TYPE_QUEST)
+ $this->sources[$this->id][4][] = $_curTpl['moreTypeId'];
+ else if ($_curTpl['src13'])
+ $this->sources[$this->id][13][] = $_curTpl['src13'];
+
+ // titles display up to two achievements at once
+ if ($_curTpl['src12Ext'])
+ $this->sources[$this->id][12][] = $_curTpl['src12Ext'];
+
+ unset($_curTpl['src12Ext']);
+ unset($_curTpl['moreType']);
+ unset($_curTpl['moreTypeId']);
+ unset($_curTpl['src3']);
+
+ // shorthand for more generic access
+ foreach (Util::$localeStrings as $i => $str)
+ if ($str)
+ $_curTpl['name_loc'.$i] = trim(str_replace('%s', '', $_curTpl['male_loc'.$i]));
+ }
+ }
+
+ public function getListviewData()
+ {
+ $data = [];
+ $this->createSource();
+
+ foreach ($this->iterate() as $__)
+ {
+ $data[$this->id] = array(
+ 'id' => $this->id,
+ 'name' => $this->getField('male', true),
+ 'namefemale' => $this->getField('female', true),
+ 'side' => $this->curTpl['side'],
+ 'gender' => $this->curTpl['gender'],
+ 'expansion' => $this->curTpl['expansion'],
+ 'category' => $this->curTpl['category']
+ );
+
+ if (!empty($this->curTpl['source']))
+ $data[$this->id]['source'] = $this->curTpl['source'];
+ }
+
+ return $data;
+ }
+
+ public function getJSGlobals($addMask = 0)
+ {
+ $data = [];
+
+ foreach ($this->iterate() as $__)
+ {
+ $data[TYPE_TITLE][$this->id]['name'] = $this->getField('male', true);
+
+ if ($_ = $this->getField('female', true))
+ $data[TYPE_TITLE][$this->id]['namefemale'] = $_;
+ }
+
+ return $data;
+ }
+
+ private function createSource()
+ {
+ $sources = array(
+ 4 => [], // Quest
+ 12 => [], // Achievements
+ 13 => [] // simple text
+ );
+
+ foreach ($this->iterate() as $__)
+ {
+ if (empty($this->sources[$this->id]))
+ continue;
+
+ foreach (array_keys($sources) as $srcKey)
+ if (isset($this->sources[$this->id][$srcKey]))
+ $sources[$srcKey] = array_merge($sources[$srcKey], $this->sources[$this->id][$srcKey]);
+ }
+
+ // fill in the details
+ if (!empty($sources[4]))
+ $sources[4] = (new QuestList(array(['id', $sources[4]])))->getSourceData();
+
+ if (!empty($sources[12]))
+ $sources[12] = (new AchievementList(array(['id', $sources[12]])))->getSourceData();
+
+ if (!empty($sources[13]))
+ $sources[13] = DB::Aowow()->SELECT('SELECT *, Id AS ARRAY_KEY FROM ?_sourcestrings WHERE Id IN (?a)', $sources[13]);
+
+ foreach ($this->sources as $Id => $src)
+ {
+ $tmp = [];
+
+ // Quest-source
+ if (isset($src[4]))
+ {
+ foreach ($src[4] as $s)
+ {
+ if (isset($sources[4][$s]['s']))
+ $this->faction2Side($sources[4][$s]['s']);
+
+ $tmp[4][] = $sources[4][$s];
+ }
+ }
+
+ // Achievement-source
+ if (isset($src[12]))
+ {
+ foreach ($src[12] as $s)
+ {
+ if (isset($sources[12][$s]['s']))
+ $this->faction2Side($sources[12][$s]['s']);
+
+ $tmp[12][] = $sources[12][$s];
+ }
+ }
+
+ // other source (only one item possible, so no iteration needed)
+ if (isset($src[13]))
+ $tmp[13] = [Util::localizedString($sources[13][$this->sources[$Id][13][0]], 'source')];
+
+ $this->templates[$Id]['source'] = $tmp;
+ }
+ }
+
+ public function getHtmlizedName($gender = GENDER_MALE)
+ {
+ $field = $gender == GENDER_FEMALE ? 'female' : 'male';
+ return str_replace('%s', '<'.Util::ucFirst(Lang::main('name')).'>', $this->getField($field, true));
+ }
+
+ public function renderTooltip() { }
+
+ private function faction2Side(&$faction) // thats weird.. and hopefully unique to titles
+ {
+ if ($faction == 2) // Horde
+ $faction = 0;
+ else if ($faction != 1) // Alliance
+ $faction = -1; // Both
+ }
+}
+
+?>
diff --git a/includes/types/user.class.php b/includes/types/user.class.php
new file mode 100644
index 00000000..2a23413c
--- /dev/null
+++ b/includes/types/user.class.php
@@ -0,0 +1,61 @@
+ [['r']],
+ 'r' => ['j' => ['?_account_reputation r ON r.userId = a.id', true], 's' => ', IFNULL(SUM(r.amount), 0) AS reputation', 'g' => 'a.id']
+ );
+
+ public function getListviewData() { }
+
+ public function getJSGlobals($addMask = 0)
+ {
+ $data = [];
+
+ foreach ($this->iterate() as $__)
+ {
+ $data[$this->curTpl['displayName']] = array(
+ 'border' => 0, // border around avatar (rarityColors)
+ 'roles' => $this->curTpl['userGroups'],
+ 'joined' => date(Util::$dateFormatInternal, $this->curTpl['joinDate']),
+ 'posts' => 0, // forum posts
+ // 'gold' => 0, // achievement system
+ // 'silver' => 0, // achievement system
+ // 'copper' => 0, // achievement system
+ 'reputation' => $this->curTpl['reputation']
+ );
+
+ // custom titles (only ssen on user page..?)
+ if ($_ = $this->curTpl['title'])
+ $data[$this->curTpl['displayName']]['title'] = $_;
+
+ if ($_ = $this->curTpl['avatar'])
+ {
+ $data[$this->curTpl['displayName']]['avatar'] = is_numeric($_) ? 2 : 1;
+ $data[$this->curTpl['displayName']]['avatarmore'] = $_;
+ }
+
+ // more optional data
+ // sig: markdown formated string (only used in forum?)
+ // border: seen as null|1|3 .. changes the border around the avatar (i suspect its meaning changed and got decupled from premium-status with the introduction of patron-status)
+ }
+
+ return [TYPE_USER => $data];
+ }
+
+ public function renderTooltip() { }
+}
+
+?>
diff --git a/includes/types/worldevent.class.php b/includes/types/worldevent.class.php
new file mode 100644
index 00000000..b124771c
--- /dev/null
+++ b/includes/types/worldevent.class.php
@@ -0,0 +1,196 @@
+ [['h']],
+ 'h' => ['j' => ['?_holidays h ON e.holidayId = h.id', true], 'o' => '-e.id ASC']
+ );
+
+ public function __construct($conditions = [])
+ {
+ parent::__construct($conditions);
+
+ // unseting elements while we iterate over the array will cause the pointer to reset
+ $replace = [];
+
+ // post processing
+ foreach ($this->iterate() as $__)
+ {
+ // emulate category
+ $sT = $this->curTpl['scheduleType'];
+ if (!$this->curTpl['holidayId'])
+ $this->curTpl['category'] = 0;
+ else if ($sT == 2)
+ $this->curTpl['category'] = 3;
+ else if (in_array($sT, [0, 1]))
+ $this->curTpl['category'] = 2;
+ else if ($sT == -1)
+ $this->curTpl['category'] = 1;
+
+ // preparse requisites
+ if ($this->curTpl['requires'])
+ $this->curTpl['requires'] = explode(' ', $this->curTpl['requires']);
+
+ // change Ids if holiday is set
+ if ($this->curTpl['holidayId'] > 0)
+ {
+ $this->curTpl['name'] = $this->getField('name', true);
+ $replace[$this->id] = $this->curTpl;
+ }
+ else // set a name if holiday is missing
+ {
+ // template
+ $this->curTpl['name_loc0'] = $this->curTpl['nameINT'];
+ $this->curTpl['iconString'] = 'trade_engineering';
+ $this->curTpl['name'] = '(SERVERSIDE) '.$this->getField('nameINT', true);
+ $replace[$this->id] = $this->curTpl;
+ }
+ }
+
+ foreach ($replace as $old => $data)
+ {
+ unset($this->templates[$old]);
+ $this->templates[$data['id']] = $data;
+ }
+ }
+
+ public static function getName($id)
+ {
+ $row = DB::Aowow()->SelectRow('
+ SELECT
+ IFNULL(h.name_loc0, e.description) AS name_loc0,
+ h.name_loc2,
+ h.name_loc3,
+ h.name_loc6,
+ h.name_loc8
+ FROM
+ ?_events e
+ LEFT JOIN
+ ?_holidays h ON e.holidayId = h.id
+ WHERE
+ e.id = ?d',
+ $id
+ );
+
+ return Util::localizedString($row, 'name');
+ }
+
+ public static function updateDates($date = null)
+ {
+ if (!$date || empty($date['firstDate']) || empty($date['length']))
+ {
+ return array(
+ 'start' => 0,
+ 'end' => 0,
+ 'rec' => 0
+ );
+ }
+
+ // Convert everything to seconds
+ $firstDate = intVal($date['firstDate']);
+ $lastDate = !empty($date['lastDate']) ? intVal($date['lastDate']) : 5000000000; // in the far far FAR future..;
+ $interval = !empty($date['rec']) ? intVal($date['rec']) : -1;
+ $length = intVal($date['length']);
+
+ $curStart = $firstDate;
+ $curEnd = $firstDate + $length;
+ $nextStart = $curStart + $interval;
+ $nextEnd = $curEnd + $interval;
+
+ while ($interval > 0 && $nextEnd <= $lastDate && $curEnd < time())
+ {
+ $curStart = $nextStart;
+ $curEnd = $nextEnd;
+ $nextStart = $curStart + $interval;
+ $nextEnd = $curEnd + $interval;
+ }
+
+ return array(
+ 'start' => $curStart,
+ 'end' => $curEnd,
+ 'rec' => $interval
+ );
+ }
+
+ public function getListviewData($forNow = false)
+ {
+ $data = [];
+
+ foreach ($this->iterate() as $__)
+ {
+ $data[$this->id] = array(
+ 'category' => $this->curTpl['category'],
+ 'id' => $this->id,
+ 'name' => $this->getField('name', true),
+ '_date' => array(
+ 'rec' => $this->curTpl['occurence'],
+ 'length' => $this->curTpl['length'],
+ 'firstDate' => $this->curTpl['startTime'],
+ 'lastDate' => $this->curTpl['endTime']
+ )
+ );
+ }
+
+ if ($forNow)
+ {
+ foreach ($data as &$d)
+ {
+ $u = self::updateDates($d['_date']);
+ unset($d['_date']);
+ $d['startDate'] = $u['start'];
+ $d['endDate'] = $u['end'];
+ $d['rec'] = $u['rec'];
+ }
+ }
+
+ return $data;
+ }
+
+ public function getJSGlobals($addMask = 0)
+ {
+ $data = [];
+
+ foreach ($this->iterate() as $__)
+ $data[TYPE_WORLDEVENT][$this->id] = ['name' => $this->getField('name', true), 'icon' => $this->curTpl['iconString']];
+
+ return $data;
+ }
+
+ public function renderTooltip()
+ {
+ if (!$this->curTpl)
+ return null;
+
+ $x = '';
+
+ // head v that extra % is nesecary because we are using sprintf later on
+ $x .= '| '.Util::jsEscape($this->getField('name', true)).' | '.Lang::event('category', $this->getField('category')).' |
|---|
';
+
+ // use string-placeholder for dates
+ // start
+ $x .= Lang::event('start').Lang::main('colon').'%s ';
+ // end
+ $x .= Lang::event('end').Lang::main('colon').'%s';
+
+ $x .= ' | ';
+
+ // desc
+ if ($this->getField('holidayId'))
+ if ($_ = $this->getField('description', true))
+ $x .= '';
+
+ return $x;
+ }
+}
+
+?>
diff --git a/includes/dbtypes/zone.class.php b/includes/types/zone.class.php
similarity index 76%
rename from includes/dbtypes/zone.class.php
rename to includes/types/zone.class.php
index 668eac6e..e1e97d98 100644
--- a/includes/dbtypes/zone.class.php
+++ b/includes/types/zone.class.php
@@ -1,22 +1,20 @@
selectRow('SELECT name_loc0, name_loc2, name_loc3, name_loc6, name_loc8 FROM ?_zones WHERE id = ?d', $id );
+ return Util::localizedString($n, 'name');
+ }
+
+ public function getListviewData()
{
$data = [];
@@ -90,17 +95,17 @@ class ZoneList extends DBTypeList
return $data;
}
- public function getJSGlobals(int $addMask = 0) : array
+ public function getJSGlobals($addMask = 0)
{
$data = [];
foreach ($this->iterate() as $__)
- $data[Type::ZONE][$this->id] = ['name' => $this->getField('name', true)];
+ $data[TYPE_ZONE][$this->id] = ['name' => $this->getField('name', true)];
return $data;
}
- public function renderTooltip() : ?string { return null; }
+ public function renderTooltip() { }
}
?>
diff --git a/includes/user.class.php b/includes/user.class.php
index 9fa1bd59..9144eaed 100644
--- a/includes/user.class.php
+++ b/includes/user.class.php
@@ -1,213 +1,448 @@
validate() ?? Locale::getFallback();
- else if (!empty($_SERVER["HTTP_ACCEPT_LANGUAGE"]) && ($loc = Locale::tryFromHttpAcceptLanguage($_SERVER["HTTP_ACCEPT_LANGUAGE"])))
- self::$preferedLoc = $loc;
- else
- self::$preferedLoc = Locale::getFallback();
-
-
- # set basic data #
-
- if (empty($_SESSION['dataKey'])) // session have a dataKey to access the JScripts (yes, also the anons)
- $_SESSION['dataKey'] = Util::createHash(); // just some random numbers for identification purpose
+ // session have a dataKey to access the JScripts (yes, also the anons)
+ if (empty($_SESSION['dataKey']))
+ $_SESSION['dataKey'] = Util::createHash(); // just some random numbers for identifictaion purpose
self::$dataKey = $_SESSION['dataKey'];
- self::$agent = $_SERVER['HTTP_USER_AGENT'] ?? '';
if (!self::$ip)
return false;
-
- # check IP bans #
-
- if ($ipBan = DB::Aowow()->selectRow('SELECT `count`, IF(`unbanDate` > UNIX_TIMESTAMP(), 1, 0) AS "active" FROM ::account_bannedips WHERE `ip` = %s AND `type` = %i', self::$ip, IP_BAN_TYPE_LOGIN_ATTEMPT))
+ // check IP bans
+ if ($ipBan = DB::Aowow()->selectRow('SELECT count, unbanDate FROM ?_account_bannedips WHERE ip = ? AND type = 0', self::$ip))
{
- if ($ipBan['count'] > Cfg::get('ACC_FAILED_AUTH_COUNT') && $ipBan['active'])
+ if ($ipBan['count'] > CFG_ACC_FAILED_AUTH_COUNT && $ipBan['unbanDate'] > time())
return false;
- else if (!$ipBan['active'])
- DB::Aowow()->qry('DELETE FROM ::account_bannedips WHERE `ip` = %s', self::$ip);
+ else if ($ipBan['unbanDate'] <= time())
+ DB::Aowow()->query('DELETE FROM ?_account_bannedips WHERE ip = ?', self::$ip);
}
-
- # try to restore session #
-
+ // try to restore session
if (empty($_SESSION['user']))
return false;
- $session = DB::Aowow()->selectRow('SELECT `userId`, `expires` FROM ::account_sessions WHERE `status` = %i AND `sessionId` = %s', SESSION_ACTIVE, session_id());
- $userData = DB::Aowow()->selectRow(
- 'SELECT a.`id`, a.`passHash`, a.`username`, a.`locale`, a.`userGroups`, a.`userPerms`, BIT_OR(ab.`typeMask`) AS "bans", IFNULL(SUM(r.`amount`), 0) AS "reputation", a.`dailyVotes`, a.`excludeGroups`, a.`status`, a.`statusTimer`, a.`email`, a.`debug`, a.`avatar`, a.`avatarborder`
- FROM ::account a
- LEFT JOIN ::account_banned ab ON a.`id` = ab.`userId` AND ab.`end` > UNIX_TIMESTAMP()
- LEFT JOIN ::account_reputation r ON a.`id` = r.`userId`
- WHERE a.`id` = %i
- GROUP BY a.`id`',
+ // timed out...
+ if (!empty($_SESSION['timeout']) && $_SESSION['timeout'] <= time())
+ return false;
+
+ $query = DB::Aowow()->SelectRow('
+ SELECT a.id, a.passHash, a.displayName, a.locale, a.userGroups, a.userPerms, a.allowExpire, BIT_OR(ab.typeMask) AS bans, IFNULL(SUM(r.amount), 0) as reputation, a.avatar, a.dailyVotes, a.excludeGroups
+ FROM ?_account a
+ LEFT JOIN ?_account_banned ab ON a.id = ab.userId AND ab.end > UNIX_TIMESTAMP()
+ LEFT JOIN ?_account_reputation r ON a.id = r.userId
+ WHERE a.id = ?d
+ GROUP BY a.id',
$_SESSION['user']
);
- if (!$session || !$userData)
+ if (!$query)
+ return false;
+
+ // password changed, terminate session
+ if (AUTH_MODE_SELF && $query['passHash'] != $_SESSION['hash'])
{
self::destroy();
return false;
}
- else if ($session['expires'] && $session['expires'] < time())
+
+ self::$id = intval($query['id']);
+ self::$displayName = $query['displayName'];
+ self::$passHash = $query['passHash'];
+ self::$expires = (bool)$query['allowExpire'];
+ self::$reputation = $query['reputation'];
+ self::$banStatus = $query['bans'];
+ self::$groups = $query['bans'] & (ACC_BAN_TEMP | ACC_BAN_PERM) ? 0 : intval($query['userGroups']);
+ self::$perms = $query['bans'] & (ACC_BAN_TEMP | ACC_BAN_PERM) ? 0 : intval($query['userPerms']);
+ self::$dailyVotes = $query['dailyVotes'];
+ self::$excludeGroups = $query['excludeGroups'];
+
+ $conditions = array(
+ [['cuFlags', PROFILER_CU_DELETED, '&'], 0],
+ ['OR', ['user', self::$id], ['ap.accountId', self::$id]]
+ );
+
+ if (self::isInGroup(U_GROUP_ADMIN | U_GROUP_BUREAU))
+ array_shift($conditions);
+
+ self::$profiles = (new LocalProfileList($conditions));
+
+ if ($query['avatar'])
+ self::$avatar = $query['avatar'];
+
+ if (self::$localeId != $query['locale']) // reset, if changed
+ self::setLocale(intVal($query['locale']));
+
+ // stuff, that updates on a daily basis goes here (if you keep you session alive indefinitly, the signin-handler doesn't do very much)
+ // - conscutive visits
+ // - votes per day
+ // - reputation for daily visit
+ if (self::$id)
{
- DB::Aowow()->qry('UPDATE ::account_sessions SET `touched` = %i, `status` = %i WHERE `sessionId` = %s', time(), SESSION_EXPIRED, session_id());
- self::destroy();
- return false;
- }
- else if ($session['userId'] != $userData['id']) // what in the name of fuck..?
- {
- // Don't know why, don't know how .. doesn't matter, both parties are out.
- DB::Aowow()->qry('UPDATE ::account_sessions SET `touched` = %i, `status` = %i WHERE `userId` IN %in AND `status` = %i', time(), SESSION_FORCED_LOGOUT, [$userData['id'], $session['userId']], SESSION_ACTIVE);
- trigger_error('User::init - tried to resume session "'.session_id().'" of user #'.$_SESSION['user'].' linked to session data for user #'.$session['userId'].' Kicked both!', E_USER_ERROR);
- self::destroy();
- return false;
- }
-
- DB::Aowow()->qry('UPDATE ::account_sessions SET `touched` = %i, `expires` = IF(`expires`, %i, 0) WHERE `sessionId` = %s', time(), time() + Cfg::get('SESSION_TIMEOUT_DELAY'), session_id());
-
- if ($loc = Locale::tryFrom($userData['locale']))
- self::$preferedLoc = $loc;
-
- // reset expired account statuses
- if ($userData['statusTimer'] && $userData['statusTimer'] < time() && $userData['status'] != ACC_STATUS_NEW)
- {
- DB::Aowow()->qry('UPDATE ::account SET `status` = %i, `statusTimer` = 0, `token` = "", `updateValue` = "" WHERE `id` = %i', ACC_STATUS_NONE, User::$id);
- $userData['status'] = ACC_STATUS_NONE;
- }
-
-
- /*******************************/
- /* past here we are logged in */
- /*******************************/
-
- self::$id = intVal($userData['id']);
- self::$username = $userData['username'];
- self::$reputation = $userData['reputation'];
- self::$banStatus = $userData['bans'];
- self::$groups = self::isBanned() ? 0 : intval($userData['userGroups']);
- self::$perms = self::isBanned() ? 0 : intval($userData['userPerms']);
- self::$dailyVotes = $userData['dailyVotes'];
- self::$excludeGroups = $userData['excludeGroups'];
- self::$status = $userData['status'];
- self::$debug = $userData['debug'];
- self::$email = $userData['email'];
- self::$avatarborder = $userData['avatarborder'];
-
-
- # reset premium options #
-
- if (!self::isPremium())
- {
- if ($userData['avatar'] == 2)
- {
- DB::Aowow()->qry('UPDATE ::account SET `avatar` = 1 WHERE `id` = %i', self::$id);
- DB::Aowow()->qry('UPDATE ::account_avatars SET `current` = 0 WHERE `userId` = %i', self::$id);
- }
-
- // avatar borders
- // do not reset, it's just not sent to the browser
- }
-
-
- # update daily limits #
-
- if (!self::isBanned())
- {
- $lastLogin = DB::Aowow()->selectCell('SELECT `curLogin` FROM ::account WHERE `id` = %i', self::$id);
+ $lastLogin = DB::Aowow()->selectCell('SELECT curLogin FROM ?_account WHERE id = ?d', self::$id);
// either the day changed or the last visit was >24h ago
if (date('j', $lastLogin) != date('j') || (time() - $lastLogin) > 1 * DAY)
{
- // - daily votes (we need to reset this one)
+ // daily votes (we need to reset this one)
self::$dailyVotes = self::getMaxDailyVotes();
- DB::Aowow()->qry(
- 'UPDATE ::account
- SET `dailyVotes` = %i, `prevLogin` = `curLogin`, `curLogin` = UNIX_TIMESTAMP(), `prevIP` = `curIP`, `curIP` = ?
- WHERE `id` = %i',
+ DB::Aowow()->query('
+ UPDATE ?_account
+ SET dailyVotes = ?d, prevLogin = curLogin, curLogin = UNIX_TIMESTAMP(), prevIP = curIP, curIP = ?
+ WHERE id = ?d',
self::$dailyVotes,
self::$ip,
self::$id
);
- // - gain reputation for daily visit
- if (!(self::isBanned()) && !self::isInGroup(U_GROUP_PENDING))
+ // gain rep for daily visit
+ if (!(self::$banStatus & (ACC_BAN_TEMP | ACC_BAN_PERM)) && !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)
+ // increment consecutive visits (next day or first of new month and not more than 48h)
+ // i bet my ass i forgott a corner case
if ((date('j', $lastLogin) + 1 == date('j') || (date('j') == 1 && date('n', $lastLogin) != date('n'))) && (time() - $lastLogin) < 2 * DAY)
- DB::Aowow()->qry('UPDATE ::account SET `consecutiveVisits` = `consecutiveVisits` + 1 WHERE `id` = %i', self::$id);
+ DB::Aowow()->query('UPDATE ?_account SET consecutiveVisits = consecutiveVisits + 1 WHERE id = ?d', self::$id);
else
- DB::Aowow()->qry('UPDATE ::account SET `consecutiveVisits` = 0 WHERE `id` = %i', self::$id);
+ DB::Aowow()->query('UPDATE ?_account SET consecutiveVisits = 0 WHERE id = ?d', self::$id);
}
}
return true;
}
- public static function save(bool $toDB = false)
+ private static function setIP()
+ {
+ $ipAddr = '';
+ $method = ['HTTP_CLIENT_IP', 'HTTP_X_FORWARDED_FOR', 'HTTP_X_FORWARDED', 'HTTP_FORWARDED_FOR', 'HTTP_FORWARDED', 'REMOTE_ADDR'];
+
+ foreach ($method as $m)
+ {
+ if ($rawIp = getenv($m))
+ {
+ if ($m == 'HTTP_X_FORWARDED')
+ $rawIp = explode(',', $rawIp)[0]; // [ip, proxy1, proxy2]
+
+ // check IPv4
+ if ($ipAddr = filter_var($rawIp, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4))
+ break;
+
+ // check IPv6
+ if ($ipAddr = filter_var($rawIp, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6))
+ break;
+ }
+ }
+
+ self::$ip = $ipAddr ?: null;
+ }
+
+ /****************/
+ /* set language */
+ /****************/
+
+ // set and save
+ public static function setLocale($set = -1)
+ {
+ $loc = LOCALE_EN;
+
+ // get
+ if ($set != -1 && isset(Util::$localeStrings[$set]))
+ $loc = $set;
+ else if (isset($_SESSION['locale']) && isset(Util::$localeStrings[$_SESSION['locale']]))
+ $loc = $_SESSION['locale'];
+ else if (!empty($_SERVER["HTTP_ACCEPT_LANGUAGE"]))
+ {
+ $loc = strtolower(substr($_SERVER["HTTP_ACCEPT_LANGUAGE"], 0, 2));
+ switch ($loc) {
+ case 'ru': $loc = LOCALE_RU; break;
+ case 'es': $loc = LOCALE_ES; break;
+ case 'de': $loc = LOCALE_DE; break;
+ case 'fr': $loc = LOCALE_FR; break;
+ default: $loc = LOCALE_EN;
+ }
+ }
+
+ // check; pick first viable if failed
+ if (CFG_LOCALES && !(CFG_LOCALES & (1 << $loc)))
+ {
+ foreach (Util::$localeStrings as $idx => $__)
+ {
+ if (CFG_LOCALES & (1 << $idx))
+ {
+ $loc = $idx;
+ break;
+ }
+ }
+ }
+
+ // set
+ if (self::$id)
+ DB::Aowow()->query('UPDATE ?_account SET locale = ? WHERE id = ?', $loc, self::$id);
+
+ self::useLocale($loc);
+ }
+
+ // only use once
+ public static function useLocale($use)
+ {
+ self::$localeId = isset(Util::$localeStrings[$use]) ? $use : LOCALE_EN;
+ self::$localeString = self::localeString(self::$localeId);
+ }
+
+ private static function localeString($loc = -1)
+ {
+ if (!isset(Util::$localeStrings[$loc]))
+ $loc = 0;
+
+ return Util::$localeStrings[$loc];
+ }
+
+ /*******************/
+ /* auth mechanisms */
+ /*******************/
+
+ public static function Auth($name, $pass)
+ {
+ $user = 0;
+ $hash = '';
+
+ switch (CFG_ACC_AUTH_MODE)
+ {
+ case AUTH_MODE_SELF:
+ {
+ if (!self::$ip)
+ return AUTH_INTERNAL_ERR;
+
+ // handle login try limitation
+ $ip = DB::Aowow()->selectRow('SELECT ip, count, unbanDate FROM ?_account_bannedips WHERE type = 0 AND ip = ?', self::$ip);
+ if (!$ip || $ip['unbanDate'] < time()) // no entry exists or time expired; set count to 1
+ DB::Aowow()->query('REPLACE INTO ?_account_bannedips (ip, type, count, unbanDate) VALUES (?, 0, 1, UNIX_TIMESTAMP() + ?d)', self::$ip, CFG_ACC_FAILED_AUTH_BLOCK);
+ else // entry already exists; increment count
+ DB::Aowow()->query('UPDATE ?_account_bannedips SET count = count + 1, unbanDate = UNIX_TIMESTAMP() + ?d WHERE ip = ?', CFG_ACC_FAILED_AUTH_BLOCK, self::$ip);
+
+ if ($ip && $ip['count'] >= CFG_ACC_FAILED_AUTH_COUNT && $ip['unbanDate'] >= time())
+ return AUTH_IPBANNED;
+
+ $query = DB::Aowow()->SelectRow('
+ SELECT a.id, a.passHash, BIT_OR(ab.typeMask) AS bans, a.status
+ FROM ?_account a
+ LEFT JOIN ?_account_banned ab ON a.id = ab.userId AND ab.end > UNIX_TIMESTAMP()
+ WHERE a.user = ?
+ GROUP BY a.id',
+ $name
+ );
+ if (!$query)
+ return AUTH_WRONGUSER;
+
+ self::$passHash = $query['passHash'];
+ if (!self::verifyCrypt($pass))
+ return AUTH_WRONGPASS;
+
+ // successfull auth; clear bans for this IP
+ DB::Aowow()->query('DELETE FROM ?_account_bannedips WHERE type = 0 AND ip = ?', self::$ip);
+
+ if ($query['bans'] & (ACC_BAN_PERM | ACC_BAN_TEMP))
+ return AUTH_BANNED;
+
+ $user = $query['id'];
+ $hash = $query['passHash'];
+ break;
+ }
+ case AUTH_MODE_REALM:
+ {
+ if (!DB::isConnectable(DB_AUTH))
+ return AUTH_INTERNAL_ERR;
+
+ $wow = DB::Auth()->selectRow('SELECT a.id, a.sha_pass_hash, ab.active AS hasBan FROM account a LEFT JOIN account_banned ab ON ab.id = a.id AND active <> 0 WHERE username = ? LIMIT 1', $name);
+ if (!$wow)
+ return AUTH_WRONGUSER;
+
+ self::$passHash = $wow['sha_pass_hash'];
+ if (!self::verifySHA1($name, $pass))
+ return AUTH_WRONGPASS;
+
+ if ($wow['hasBan'])
+ return AUTH_BANNED;
+
+ if ($_ = self::checkOrCreateInDB($wow['id'], $name))
+ $user = $_;
+ else
+ return AUTH_INTERNAL_ERR;
+
+ break;
+ }
+ case AUTH_MODE_EXTERNAL:
+ {
+ if (!file_exists('config/extAuth.php'))
+ return AUTH_INTERNAL_ERR;
+
+ require 'config/extAuth.php';
+
+ $extGroup = -1;
+ $result = extAuth($name, $pass, $extId, $extGroup);
+
+ if ($result == AUTH_OK && $extId)
+ {
+ if ($_ = self::checkOrCreateInDB($extId, $name, $extGroup))
+ $user = $_;
+ else
+ return AUTH_INTERNAL_ERR;
+
+ break;
+ }
+
+ return $result;
+ }
+ default:
+ return AUTH_INTERNAL_ERR;
+ }
+
+ // kickstart session
+ session_unset();
+ $_SESSION['user'] = $user;
+ $_SESSION['hash'] = $hash;
+
+ return AUTH_OK;
+ }
+
+ // create a linked account for our settings if nessecary
+ private static function checkOrCreateInDB($extId, $name, $userGroup = -1)
+ {
+ if (!intVal($extId))
+ return 0;
+
+ $userGroup = intVal($userGroup);
+
+ if ($_ = DB::Aowow()->selectCell('SELECT id FROM ?_account WHERE extId = ?d', $extId))
+ {
+ if ($userGroup >= U_GROUP_NONE)
+ DB::Aowow()->query('UPDATE ?_account SET userGroups = ?d WHERE extId = ?d', $userGroup, $extId);
+ return $_;
+ }
+
+ $newId = DB::Aowow()->query('INSERT IGNORE INTO ?_account (extId, user, displayName, joinDate, prevIP, prevLogin, locale, status, userGroups) VALUES (?d, ?, ?, UNIX_TIMESTAMP(), ?, UNIX_TIMESTAMP(), ?d, ?d, ?d)',
+ $extId,
+ $name,
+ Util::ucFirst($name),
+ isset($_SERVER["REMOTE_ADDR"]) ? $_SERVER["REMOTE_ADDR"] : '',
+ User::$localeId,
+ ACC_STATUS_OK,
+ $userGroup >= U_GROUP_NONE ? $userGroup : U_GROUP_NONE
+ );
+
+ if ($newId)
+ Util::gainSiteReputation($newId, SITEREP_ACTION_REGISTER);
+
+ return $newId;
+ }
+
+ private static function createSalt()
+ {
+ $algo = '$2a';
+ $strength = '$09';
+ $salt = '$'.Util::createHash(22);
+
+ return $algo.$strength.$salt;
+ }
+
+ // crypt used by aowow
+ public static function hashCrypt($pass)
+ {
+ return crypt($pass, self::createSalt());
+ }
+
+ public static function verifyCrypt($pass, $hash = '')
+ {
+ $_ = $hash ?: self::$passHash;
+ return $_ === crypt($pass, $_);
+ }
+
+ // sha1 used by TC / MaNGOS
+ private static function hashSHA1($name, $pass)
+ {
+ return sha1(strtoupper($name).':'.strtoupper($pass));
+ }
+
+ private static function verifySHA1($name, $pass)
+ {
+ return strtoupper(self::$passHash) === strtoupper(self::hashSHA1($name, $pass));
+ }
+
+ public static function isValidName($name, &$errCode = 0)
+ {
+ $errCode = 0;
+
+ // different auth modes require different usernames
+ $min = 0; // external case
+ $max = 0;
+ if (CFG_ACC_AUTH_MODE == AUTH_MODE_SELF)
+ {
+ $min = 4;
+ $max = 16;
+ }
+ else if (CFG_ACC_AUTH_MODE == AUTH_MODE_REALM)
+ {
+ $min = 3;
+ $max = 32;
+ }
+
+ if (($min && mb_strlen($name) < $min) || ($max && mb_strlen($name) > $max))
+ $errCode = 1;
+ else if (preg_match('/[^\w\d\-]/i', $name))
+ $errCode = 2;
+
+ return $errCode == 0;
+ }
+
+ public static function isValidPass($pass, &$errCode = 0)
+ {
+ $errCode = 0;
+
+ // only enforce for own passwords
+ if (mb_strlen($pass) < 6 && CFG_ACC_AUTH_MODE == AUTH_MODE_SELF)
+ $errCode = 1;
+ // else if (preg_match('/[^\w\d!"#\$%]/', $pass)) // such things exist..? :o
+ // $errCode = 2;
+
+ return $errCode == 0;
+ }
+
+ public static function save()
{
$_SESSION['user'] = self::$id;
- $_SESSION['locale'] = self::$preferedLoc;
+ $_SESSION['hash'] = self::$passHash;
+ $_SESSION['locale'] = self::$localeId;
+ $_SESSION['timeout'] = self::$expires ? time() + CFG_SESSION_TIMEOUT_DELAY : 0;
// $_SESSION['dataKey'] does not depend on user login status and is set in User::init()
-
- if (self::isLoggedIn() && $toDB)
- DB::Aowow()->qry('UPDATE ::account SET `locale` = %s WHERE `id` = %s', self::$preferedLoc->value, self::$id);
}
public static function destroy()
@@ -215,342 +450,124 @@ class User
session_regenerate_id(true); // session itself is not destroyed; status changed => regenerate id
session_unset();
- $_SESSION['locale'] = self::$preferedLoc; // keep locale
+ $_SESSION['locale'] = self::$localeId; // keep locale
$_SESSION['dataKey'] = self::$dataKey; // keep dataKey
- self::$id = 0;
- self::$username = '';
- self::$perms = 0;
- self::$groups = U_GROUP_NONE;
+ self::$id = 0;
+ self::$displayName = '';
+ self::$perms = 0;
+ self::$groups = U_GROUP_NONE;
}
-
- /*******************/
- /* auth mechanisms */
- /*******************/
-
- public static function authenticate(string $login, #[\SensitiveParameter] string $password) : int
- {
- $userId = 0;
-
- $result = match (Cfg::get('ACC_AUTH_MODE'))
- {
- AUTH_MODE_SELF => self::authSelf($login, $password, $userId),
- AUTH_MODE_REALM => self::authRealm($login, $password, $userId),
- AUTH_MODE_EXTERNAL => self::authExtern($login, $password, $userId),
- default => AUTH_INTERNAL_ERR
- };
-
- // also banned? its a feature block, not login block..
- if ($result == AUTH_OK || $result == AUTH_BANNED)
- {
- session_unset();
- $_SESSION['user'] = $userId;
- self::$id = $userId;
- }
-
- return $result;
- }
-
- private static function authSelf(string $nameOrEmail, #[\SensitiveParameter] string $password, int &$userId) : int
- {
- if (!self::$ip)
- return AUTH_INTERNAL_ERR;
-
- // handle login try limitation
- $ipBan = DB::Aowow()->selectRow('SELECT `ip`, `count`, IF(`unbanDate` > UNIX_TIMESTAMP(), 1, 0) AS "active" FROM ::account_bannedips WHERE `type` = %i AND `ip` = %s', IP_BAN_TYPE_LOGIN_ATTEMPT, self::$ip);
- if (!$ipBan || !$ipBan['active']) // no entry exists or time expired; set count to 1
- DB::Aowow()->qry('REPLACE INTO ::account_bannedips (`ip`, `type`, `count`, `unbanDate`) VALUES (%s, %i, 1, UNIX_TIMESTAMP() + %i)', self::$ip, IP_BAN_TYPE_LOGIN_ATTEMPT, Cfg::get('ACC_FAILED_AUTH_BLOCK'));
- else // entry already exists; increment count
- DB::Aowow()->qry('UPDATE ::account_bannedips SET `count` = `count` + 1, `unbanDate` = UNIX_TIMESTAMP() + %i WHERE `ip` = %s', Cfg::get('ACC_FAILED_AUTH_BLOCK'), self::$ip);
-
- if ($ipBan && $ipBan['count'] >= Cfg::get('ACC_FAILED_AUTH_COUNT') && $ipBan['active'])
- return AUTH_IPBANNED;
-
- $email = filter_var($nameOrEmail, FILTER_VALIDATE_EMAIL);
-
- $query = DB::Aowow()->SelectRow(
- 'SELECT a.`id`, a.`passHash`, BIT_OR(ab.`typeMask`) AS "bans", a.`status`
- FROM ::account a
- LEFT JOIN ::account_banned ab ON a.`id` = ab.`userId` AND ab.`end` > UNIX_TIMESTAMP()
- WHERE %if', $email, 'a.`email` %else a.`login` %end = %s AND `status` <> %i
- GROUP BY a.`id`',
- $nameOrEmail,
- ACC_STATUS_DELETED
- );
-
- if (!$query)
- return AUTH_WRONGUSER;
-
- if (!self::verifyCrypt($password, $query['passHash']))
- return AUTH_WRONGPASS;
-
- // successfull auth; clear bans for this IP
- DB::Aowow()->qry('DELETE FROM ::account_bannedips WHERE `type` = %i AND `ip` = %s', IP_BAN_TYPE_LOGIN_ATTEMPT, self::$ip);
-
- if ($query['bans'] & (ACC_BAN_PERM | ACC_BAN_TEMP))
- return AUTH_BANNED;
-
- $userId = $query['id'];
-
- return AUTH_OK;
- }
-
- private static function authRealm(string $name, #[\SensitiveParameter] string $password, int &$userId) : int
- {
- if (!DB::isConnectable(DB_AUTH))
- return AUTH_INTERNAL_ERR;
-
- $wow = DB::Auth()->selectRow('SELECT a.id, a.salt, a.verifier, ab.active AS hasBan FROM account a LEFT JOIN account_banned ab ON ab.id = a.id AND active <> 0 WHERE username = %s LIMIT 1', $name);
- if (!$wow)
- return AUTH_WRONGUSER;
-
- if (!self::verifySRP6($name, $password, $wow['salt'], $wow['verifier']))
- return AUTH_WRONGPASS;
-
- if ($wow['hasBan'])
- return AUTH_BANNED;
-
- if ($_ = self::checkOrCreateInDB($wow['id'], $name))
- $userId = $_;
- else
- return AUTH_INTERNAL_ERR;
-
- return AUTH_OK;
- }
-
- private static function authExtern(string $nameOrEmail, #[\SensitiveParameter] string $password, int &$userId) : int
- {
- if (!file_exists('config/extAuth.php'))
- {
- trigger_error('User::authExtern - AUTH_MODE_EXTERNAL is selected but config/extAuth.php does not exist!', E_USER_ERROR);
- return AUTH_INTERNAL_ERR;
- }
-
- require 'config/extAuth.php';
-
- if (!function_exists('\extAuth'))
- {
- trigger_error('User::authExtern - AUTH_MODE_EXTERNAL is selected but function extAuth() is not defined!', E_USER_ERROR);
- return AUTH_INTERNAL_ERR;
- }
-
- $extGroup = -1;
- $extId = 0;
- $result = \extAuth($nameOrEmail, $password, $extId, $extGroup);
-
- // assert we don't have an email passed back from extAuth
- if (filter_var($nameOrEmail, FILTER_VALIDATE_EMAIL))
- return AUTH_WRONGUSER;
-
- if ($result == AUTH_OK && $extId)
- {
- if ($_ = self::checkOrCreateInDB($extId, $nameOrEmail, $extGroup))
- $userId = $_;
- else
- return AUTH_INTERNAL_ERR;
- }
-
- return $result;
- }
-
- // create a linked account for our settings if necessary
- private static function checkOrCreateInDB(int $extId, string $name, int $userGroup = -1) : int
- {
- if ($_ = DB::Aowow()->selectCell('SELECT `id` FROM ::account WHERE `extId` = %i', $extId))
- {
- if ($userGroup >= U_GROUP_NONE)
- DB::Aowow()->qry('UPDATE ::account SET `userGroups` = %i WHERE `extId` = %i', $userGroup, $extId);
- return $_;
- }
-
- $newId = DB::Aowow()->qry('INSERT IGNORE INTO ::account (`extId`, `passHash`, `username`, `joinDate`, `prevIP`, `prevLogin`, `locale`, `status`, `userGroups`) VALUES (%i, "", %s, UNIX_TIMESTAMP(), %s, UNIX_TIMESTAMP(), %i, %i, %i)',
- $extId,
- $name,
- $_SERVER["REMOTE_ADDR"] ?? '',
- self::$preferedLoc->value,
- ACC_STATUS_NONE,
- $userGroup >= U_GROUP_NONE ? $userGroup : U_GROUP_NONE
- );
-
- if ($newId)
- Util::gainSiteReputation($newId, SITEREP_ACTION_REGISTER);
-
- return $newId ?: 0;
- }
-
- // crypt used by us
- public static function hashCrypt(#[\SensitiveParameter] string $pass) : string
- {
- return password_hash($pass, PASSWORD_BCRYPT, ['cost' => 15]);
- }
-
- public static function verifyCrypt(#[\SensitiveParameter] string $pass, string $hash) : bool
- {
- return password_verify($pass, $hash);
- }
-
- // SRP6 used by TC
- private static function verifySRP6(string $user, string $pass, string $salt, string $verifier) : bool
- {
- $g = gmp_init(7);
- $N = gmp_init('894B645E89E1535BBDAD5B8B290650530801B18EBFBF5E8FAB3C82872A3E9BB7', 16);
- $x = gmp_import(
- sha1($salt . sha1(strtoupper($user . ':' . $pass), TRUE), TRUE),
- 1,
- GMP_LSW_FIRST
- );
- $v = gmp_powm($g, $x, $N);
- return ($verifier === str_pad(gmp_export($v, 1, GMP_LSW_FIRST), 32, chr(0), STR_PAD_RIGHT));
- }
-
-
/*********************/
/* access management */
/*********************/
- public static function isInGroup(int $group) : bool
+ public static function isInGroup($group)
{
- return $group == U_GROUP_NONE || (self::$groups & $group) != U_GROUP_NONE;
+ return (self::$groups & $group) != 0;
}
- public static function canComment() : bool
+ public static function canComment()
{
- if (!self::isLoggedIn() || self::isBanned(ACC_BAN_COMMENT))
+ if (!self::$id || self::$banStatus & (ACC_BAN_COMMENT | ACC_BAN_PERM | ACC_BAN_TEMP))
return false;
- return self::$perms || self::$reputation >= Cfg::get('REP_REQ_COMMENT');
+ return self::$perms || self::$reputation >= CFG_REP_REQ_COMMENT;
}
- public static function canReply() : bool
+ public static function canReply()
{
- if (!self::isLoggedIn() || self::isBanned(ACC_BAN_COMMENT))
+ if (!self::$id || self::$banStatus & (ACC_BAN_COMMENT | ACC_BAN_PERM | ACC_BAN_TEMP))
return false;
- return self::$perms || self::$reputation >= Cfg::get('REP_REQ_REPLY');
+ return self::$perms || self::$reputation >= CFG_REP_REQ_REPLY;
}
- public static function canUpvote() : bool
+ public static function canUpvote()
{
- if (!self::isLoggedIn() || self::isBanned(ACC_BAN_COMMENT))
+ if (!self::$id || self::$banStatus & (ACC_BAN_COMMENT | ACC_BAN_PERM | ACC_BAN_TEMP))
return false;
- return self::$perms || (self::$reputation >= Cfg::get('REP_REQ_UPVOTE') && self::$dailyVotes > 0);
+ return self::$perms || (self::$reputation >= CFG_REP_REQ_UPVOTE && self::$dailyVotes > 0);
}
- public static function canDownvote() : bool
+ public static function canDownvote()
{
- if (!self::isLoggedIn() || self::isBanned(ACC_BAN_RATE))
+ if (!self::$id || self::$banStatus & (ACC_BAN_RATE | ACC_BAN_PERM | ACC_BAN_TEMP))
return false;
- return self::$perms || (self::$reputation >= Cfg::get('REP_REQ_DOWNVOTE') && self::$dailyVotes > 0);
+ return self::$perms || (self::$reputation >= CFG_REP_REQ_DOWNVOTE && self::$dailyVotes > 0);
}
- public static function canSupervote() : bool
+ public static function canSupervote()
{
- if (!self::isLoggedIn() || self::isBanned(ACC_BAN_RATE) || self::isInGroup(U_GROUP_PENDING))
+ if (!self::$id || self::$banStatus & (ACC_BAN_RATE | ACC_BAN_PERM | ACC_BAN_TEMP))
return false;
- return self::$reputation >= Cfg::get('REP_REQ_SUPERVOTE');
+ return self::$reputation >= CFG_REP_REQ_SUPERVOTE;
}
- public static function canUploadScreenshot() : bool
+ public static function canUploadScreenshot()
{
- if (!self::isLoggedIn() || self::isBanned(ACC_BAN_SCREENSHOT) || self::isInGroup(U_GROUP_PENDING))
+ if (!self::$id || self::$banStatus & (ACC_BAN_SCREENSHOT | ACC_BAN_PERM | ACC_BAN_TEMP))
return false;
return true;
}
- public static function canWriteGuide() : bool
+ public static function canSuggestVideo()
{
- if (!self::isLoggedIn() || self::isBanned(ACC_BAN_GUIDE) || self::isInGroup(U_GROUP_PENDING))
+ if (!self::$id || self::$banStatus & (ACC_BAN_VIDEO | ACC_BAN_PERM | ACC_BAN_TEMP))
return false;
return true;
}
- public static function canSuggestVideo() : bool
+ public static function isPremium()
{
- if (!self::isLoggedIn() || self::isBanned(ACC_BAN_VIDEO) || self::isInGroup(U_GROUP_PENDING))
- return false;
-
- return true;
+ return self::isInGroup(U_GROUP_PREMIUM) || self::$reputation >= CFG_REP_REQ_PREMIUM;
}
- public static function isPremium() : bool
- {
- return !self::isBanned() && (self::isInGroup(U_GROUP_PREMIUM) || self::$reputation >= Cfg::get('REP_REQ_PREMIUM'));
- }
-
- public static function isLoggedIn() : bool
- {
- return self::$id > 0; // more checks? maybe check pending email verification here? (self::isInGroup(U_GROUP_PENDING))
- }
-
- public static function isBanned(int $addBanMask = 0x0) : bool
- {
- return self::$banStatus & (ACC_BAN_TEMP | ACC_BAN_PERM | $addBanMask);
- }
-
- public static function isRecovering() : bool
- {
- return self::$status != ACC_STATUS_NONE && self::$status != ACC_STATUS_NEW;
- }
-
-
/**************/
/* js-related */
/**************/
- public static function decrementDailyVotes() : void
+ public static function decrementDailyVotes()
{
- if (!self::isLoggedIn() || self::isBanned(ACC_BAN_RATE))
- return;
-
self::$dailyVotes--;
- DB::Aowow()->qry('UPDATE ::account SET `dailyVotes` = %i WHERE `id` = %i', self::$dailyVotes, self::$id);
+ DB::Aowow()->query('UPDATE ?_account SET dailyVotes = ?d WHERE id = ?d', self::$dailyVotes, self::$id);
}
- public static function getCurrentDailyVotes() : int
+ public static function getCurDailyVotes()
{
- if (!self::isLoggedIn() || self::isBanned(ACC_BAN_RATE) || self::$dailyVotes < 0)
- return 0;
-
return self::$dailyVotes;
}
- public static function getMaxDailyVotes() : int
+ public static function getMaxDailyVotes()
{
- if (!self::isLoggedIn() || self::isBanned(ACC_BAN_RATE))
+ if (!self::$id || self::$banStatus & (ACC_BAN_PERM | ACC_BAN_TEMP))
return 0;
- $threshold = Cfg::get('REP_REQ_VOTEMORE_BASE');
- $extra = Cfg::get('REP_REQ_VOTEMORE_ADD');
- $base = Cfg::get('USER_MAX_VOTES');
-
- return $base + max(0, intVal((self::$reputation - $threshold + $extra) / $extra));
+ return CFG_USER_MAX_VOTES + (self::$reputation >= CFG_REP_REQ_VOTEMORE_BASE ? 1 + intVal((self::$reputation - CFG_REP_REQ_VOTEMORE_BASE) / CFG_REP_REQ_VOTEMORE_ADD) : 0);
}
- public static function getReputation() : int
+ public static function getReputation()
{
- if (!self::isLoggedIn() || self::$reputation < 0)
- return 0;
-
return self::$reputation;
}
- public static function getUserGlobal() : array
+ public static function getUserGlobals()
{
$gUser = array(
'id' => self::$id,
- 'name' => self::$username,
+ 'name' => self::$displayName,
'roles' => self::$groups,
'permissions' => self::$perms,
'cookies' => []
);
- if (!self::isLoggedIn() || self::isBanned())
+ if (!self::$id || self::$banStatus & (ACC_BAN_TEMP | ACC_BAN_PERM))
return $gUser;
$gUser['commentban'] = !self::canComment();
@@ -558,22 +575,11 @@ class User
$gUser['canDownvote'] = self::canDownvote();
$gUser['canPostReplies'] = self::canReply();
$gUser['superCommentVotes'] = self::canSupervote();
- $gUser['downvoteRep'] = Cfg::get('REP_REQ_DOWNVOTE');
- $gUser['upvoteRep'] = Cfg::get('REP_REQ_UPVOTE');
+ $gUser['downvoteRep'] = CFG_REP_REQ_DOWNVOTE;
+ $gUser['upvoteRep'] = CFG_REP_REQ_UPVOTE;
$gUser['characters'] = self::getCharacters();
- $gUser['completion'] = self::getCompletion();
$gUser['excludegroups'] = self::$excludeGroups;
-
- if (self::$debug)
- $gUser['debug'] = true; // csv id-list output option on listviews
-
- if (self::isPremium())
- {
- $gUser['premium'] = 1;
- $gUser['settings'] = ['premiumborder' => self::$avatarborder];
- }
- else
- $gUser['settings'] = (new \StdClass); // existence is checked in Profiler.js before g_user.excludegroups is applied; should this contain - "defaultModel":{"gender":2,"race":6} ?
+ $gUser['settings'] = (new StdClass); // profiler requires this to be set; has property premiumborder (NYI)
if ($_ = self::getProfilerExclusions())
$gUser = array_merge($gUser, $_);
@@ -581,9 +587,6 @@ class User
if ($_ = self::getProfiles())
$gUser['profiles'] = $_;
- if ($_ = self::getGuides())
- $gUser['guides'] = $_;
-
if ($_ = self::getWeightScales())
$gUser['weightscales'] = $_;
@@ -593,203 +596,58 @@ class User
return $gUser;
}
- public static function getWeightScales() : array
+ public static function getWeightScales()
{
$result = [];
- if (!self::isLoggedIn() || self::isBanned())
- return $result;
-
- $res = DB::Aowow()->selectPairs('SELECT `id`, `name` FROM ::account_weightscales WHERE `userId` = %i', self::$id);
+ $res = DB::Aowow()->selectCol('SELECT id AS ARRAY_KEY, name FROM ?_account_weightscales WHERE userId = ?d', self::$id);
if (!$res)
return $result;
- $weights = DB::Aowow()->selectCol('SELECT `id` AS ARRAY_KEY, `field` AS ARRAY_KEY2, `val` FROM ::account_weightscale_data WHERE `id` IN %in', array_keys($res));
+ $weights = DB::Aowow()->selectCol('SELECT id AS ARRAY_KEY, `field` AS ARRAY_KEY2, val FROM ?_account_weightscale_data WHERE id IN (?a)', array_keys($res));
foreach ($weights as $id => $data)
$result[] = array_merge(['name' => $res[$id], 'id' => $id], $data);
return $result;
}
- public static function getProfilerExclusions() : array
+ public static function getProfilerExclusions()
{
$result = [];
-
- if (!self::isLoggedIn() || self::isBanned())
- return $result;
-
- if (!Cfg::get('PROFILER_ENABLE'))
- return $result;
-
- foreach ([Profiler::COMPLETION_EXCLUDE => 'excludes', Profiler::COMPLETION_INCLUDE => 'includes'] as $mode => $field)
- if ($ex = DB::Aowow()->selectCol('SELECT `type` AS ARRAY_KEY, `typeId` AS ARRAY_KEY2, `typeId` FROM ::account_excludes WHERE `mode` = %i AND `userId` = %i', $mode, self::$id))
+ $modes = [1 => 'excludes', 2 => 'includes'];
+ foreach ($modes as $mode => $field)
+ if ($ex = DB::Aowow()->selectCol('SELECT `type` AS ARRAY_KEY, typeId AS ARRAY_KEY2, typeId FROM ?_account_excludes WHERE mode = ?d AND userId = ?d', $mode, self::$id))
foreach ($ex as $type => $ids)
$result[$field][$type] = array_values($ids);
return $result;
}
- public static function getCharacters() : array
+ public static function getCharacters()
{
- if (!self::loadProfiles())
+ if (!self::$profiles)
return [];
return self::$profiles->getJSGlobals(PROFILEINFO_CHARACTER);
}
- public static function getProfiles() : array
+ public static function getProfiles()
{
- if (!self::loadProfiles())
+ if (!self::$profiles)
return [];
return self::$profiles->getJSGlobals(PROFILEINFO_PROFILE);
}
- public static function getPinnedCharacter() : array
+ public static function getCookies()
{
- if (!self::loadProfiles())
- return [];
-
- $realms = Profiler::getRealms();
-
- foreach (self::$profiles->iterate() as $id => $_)
- if (self::$profiles->getField('cuFlags') & PROFILER_CU_PINNED)
- if (isset($realms[self::$profiles->getField('realm')]))
- return [
- $id,
- self::$profiles->getField('name'),
- self::$profiles->getField('region') . '.' . Profiler::urlize($realms[self::$profiles->getField('realm')]['name'], true) . '.' . Profiler::urlize(self::$profiles->getField('name'), true, true)
- ];
-
- return [];
- }
-
- public static function getGuides() : array
- {
- $result = [];
-
- if (!self::isLoggedIn() || self::isBanned(ACC_BAN_GUIDE))
- return $result;
-
- if ($guides = DB::Aowow()->selectAssoc('SELECT `id`, `title`, `url` FROM ::guides WHERE `userId` = %i AND `status` <> %i', self::$id, GuideMgr::STATUS_ARCHIVED))
- {
- // fix url
- array_walk($guides, fn(&$x) => $x['url'] = '?guide='.($x['url'] ?: $x['id']));
- $result = $guides;
- }
-
- return $result;
- }
-
- public static function getCookies() : array
- {
- if (!self::isLoggedIn())
- return [];
-
- return DB::Aowow()->selectPairs('SELECT `name`, `data` FROM ::account_cookies WHERE `userId` = %i', self::$id);
- }
-
- public static function getFavorites() : array
- {
- if (!self::isLoggedIn() || self::isBanned())
- return [];
-
- $res = DB::Aowow()->selectCol('SELECT `type` AS ARRAY_KEY, `typeId` AS ARRAY_KEY2, `typeId` FROM ::account_favorites WHERE `userId` = %i', self::$id);
- if (!$res)
- return [];
-
$data = [];
- foreach ($res as $type => $ids)
- {
- $tc = Type::newList($type, [['id', $ids]]);
- if (!$tc || $tc->error)
- continue;
- $entities = [];
- foreach ($tc->iterate() as $id => $__)
- $entities[] = [$id, $tc->getField('name', true, true)];
-
- if ($entities)
- $data[] = ['id' => $type, 'entities' => $entities];
- }
+ if (self::$id)
+ $data = DB::Aowow()->selectCol('SELECT name AS ARRAY_KEY, data FROM ?_account_cookies WHERE userId = ?d', self::$id);
return $data;
}
-
- public static function getCompletion() : array
- {
- if (!self::loadProfiles())
- return [];
-
- $ids = [];
- foreach (self::$profiles->iterate() as $_)
- if (!self::$profiles->isCustom())
- $ids[] = self::$profiles->id;
-
- if (!$ids)
- return [];
-
- $completion = [];
-
- $x = DB::Aowow()->selectAssoc('SELECT `id` AS ARRAY_KEY, `questId` AS ARRAY_KEY2, `questId` FROM ::profiler_completion_quests WHERE `id` IN %in', $ids);
- $completion[Type::QUEST] = $x ? array_map(array_values(...), $x) : [];
-
- $x = DB::Aowow()->selectAssoc('SELECT `id` AS ARRAY_KEY, `achievementId` AS ARRAY_KEY2, `achievementId` FROM ::profiler_completion_achievements WHERE `id` IN %in', $ids);
- $completion[Type::ACHIEVEMENT] = $x ? array_map(array_values(...), $x) : [];
-
- $x = DB::Aowow()->selectAssoc('SELECT `id` AS ARRAY_KEY, `titleId` AS ARRAY_KEY2, `titleId` FROM ::profiler_completion_titles WHERE `id` IN %in', $ids);
- $completion[Type::TITLE] = $x ? array_map(array_values(...), $x) : [];
-
- $completion[Type::ITEM] = [];
-
- $spells = DB::Aowow()->selectAssoc(
- 'SELECT pcs.`id` AS ARRAY_KEY, pcs.`spellId` AS ARRAY_KEY2, pcs.`spellId`, i.`id` AS "itemId"
- FROM ::spell s
- JOIN ::profiler_completion_spells pcs ON s.`id` = pcs.`spellId`
- LEFT JOIN ::items i ON i.`spellId1` IN %in AND i.`spellId2` = pcs.`spellId`
- WHERE s.`typeCat` IN %in AND pcs.`id` IN %in',
- LEARN_SPELLS, [-5, -6, 9, 11], $ids
- );
-
- 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;
- }
-
- private static function loadProfiles() : bool
- {
- if (!Cfg::get('PROFILER_ENABLE'))
- return false;
-
- if (self::$profiles === null)
- {
- $ap = DB::Aowow()->selectCol('SELECT `profileId` FROM ::account_profiles WHERE `accountId` = %i', self::$id);
-
- // the old approach [DB::OR, ['user', self::$id], ['ap.accountId', self::$id]] caused keys to not get used
- $conditions = $ap ? [[DB::OR, ['user', self::$id], ['id', $ap]]] : [['user', self::$id]];
- if (!self::isInGroup(U_GROUP_ADMIN | U_GROUP_BUREAU))
- $conditions[] = ['deleted', 0];
-
- self::$profiles = (new LocalProfileList($conditions));
- }
-
- return !!self::$profiles->getFoundIDs();
- }
}
?>
diff --git a/includes/utilities.php b/includes/utilities.php
index 39e9f297..35591265 100644
--- a/includes/utilities.php
+++ b/includes/utilities.php
@@ -1,63 +1,325 @@
$v)
- if ($callback($v, $k))
- return $array[$k];
- return null;
- }
-
- function array_find_key(array $array, callable $callback) : mixed
- {
- foreach ($array as $k => $v)
- if ($callback($v, $k))
- return $k;
- return null;
- }
-}
-
-class SimpleXML extends \SimpleXMLElement
-{
- public function addCData(string $cData) : \SimpleXMLElement
+ public function addCData($str)
{
$node = dom_import_simplexml($this);
$no = $node->ownerDocument;
- $node->appendChild($no->createCDATASection($cData));
+ $node->appendChild($no->createCDATASection($str));
return $this;
}
}
-abstract class Util
+class CLI
{
- /* NOTE!
- * FILE_ACCESS should be 0755 or less, but CLI and web interface both access the same files. While in CLI php is executed with the current users perms,
- * while the web interface is always executed by www-data (or whoever runs the web server) who does not own the files previously created via CLI.
- * And thus web interface actions fail with permission denied, unless the files are flagged +wx for everyone.
- * This probably has to be solved on the system level by having www-data and the CLI user share a group or something.
- */
- public const FILE_ACCESS = 0777;
- public const DIR_ACCESS = 0777;
+ const CHR_BELL = 7;
+ const CHR_BACK = 8;
+ const CHR_TAB = 9;
+ const CHR_LF = 10;
+ const CHR_CR = 13;
+ const CHR_ESC = 27;
+ const CHR_BACKSPACE = 127;
- private const GEM_SCORE_BASE_WOTLK = 16; // rare quality wotlk gem score
- private const GEM_SCORE_BASE_BC = 8; // rare quality bc gem score
+ const LOG_OK = 0;
+ const LOG_WARN = 1;
+ const LOG_ERROR = 2;
+ const LOG_INFO = 3;
+
+ private static $logHandle = null;
+ private static $hasReadline = null;
+
+
+ /***********/
+ /* logging */
+ /***********/
+
+ public static function initLogFile($file = '')
+ {
+ if (!$file)
+ return;
+
+ $file = self::nicePath($file);
+ if (!file_exists($file))
+ self::$logHandle = fopen($file, 'w');
+ else
+ {
+ $logFileParts = pathinfo($file);
+
+ $i = 1;
+ while (file_exists($logFileParts['dirname'].'/'.$logFileParts['filename'].$i.(isset($logFileParts['extension']) ? '.'.$logFileParts['extension'] : '')))
+ $i++;
+
+ $file = $logFileParts['dirname'].'/'.$logFileParts['filename'].$i.(isset($logFileParts['extension']) ? '.'.$logFileParts['extension'] : '');
+ self::$logHandle = fopen($file, 'w');
+ }
+ }
+
+ public static function red($str)
+ {
+ return OS_WIN ? $str : "\e[31m".$str."\e[0m";
+ }
+
+ public static function green($str)
+ {
+ return OS_WIN ? $str : "\e[32m".$str."\e[0m";
+ }
+
+ public static function yellow($str)
+ {
+ return OS_WIN ? $str : "\e[33m".$str."\e[0m";
+ }
+
+ public static function blue($str)
+ {
+ return OS_WIN ? $str : "\e[36m".$str."\e[0m";
+ }
+
+ public static function bold($str)
+ {
+ return OS_WIN ? $str : "\e[1m".$str."\e[0m";
+ }
+
+ public static function write($txt = '', $lvl = -1)
+ {
+ $msg = "\n";
+ if ($txt)
+ {
+ $msg = str_pad(date('H:i:s'), 10);
+ switch ($lvl)
+ {
+ case self::LOG_ERROR: // red critical error
+ $msg .= '['.self::red('ERR').'] ';
+ break;
+ case self::LOG_WARN: // yellow notice
+ $msg .= '['.self::yellow('WARN').'] ';
+ break;
+ case self::LOG_OK: // green success
+ $msg .= '['.self::green('OK').'] ';
+ break;
+ case self::LOG_INFO: // blue info
+ $msg .= '['.self::blue('INFO').'] ';
+ break;
+ default:
+ $msg .= ' ';
+ }
+
+ $msg .= $txt."\n";
+ }
+
+ echo $msg;
+
+ if (self::$logHandle) // remove highlights for logging
+ fwrite(self::$logHandle, preg_replace(["/\e\[\d+m/", "/\e\[0m/"], '', $msg));
+
+ flush();
+ }
+
+ public static function nicePath(/* $file = '', ...$pathParts */)
+ {
+ $path = '';
+
+ switch (func_num_args())
+ {
+ case 0:
+ return '';
+ case 1:
+ $path = func_get_arg(0);
+ break;
+ default:
+ $args = func_get_args();
+ $file = array_shift($args);
+ $path = implode(DIRECTORY_SEPARATOR, $args).DIRECTORY_SEPARATOR.$file;
+ }
+
+ if (DIRECTORY_SEPARATOR == '/') // *nix
+ {
+ $path = str_replace('\\', '/', $path);
+ $path = preg_replace('/\/+/i', '/', $path);
+ }
+ else if (DIRECTORY_SEPARATOR == '\\') // win
+ {
+ $path = str_replace('/', '\\', $path);
+ $path = preg_replace('/\\\\+/i', '\\', $path);
+ }
+ else
+ CLI::write('Dafuq! Your directory separator is "'.DIRECTORY_SEPARATOR.'". Please report this!', CLI::LOG_ERROR);
+
+ $path = trim($path);
+
+ // resolve *nix home shorthand
+ if (!OS_WIN)
+ {
+ if (preg_match('/^~(\w+)\/.*/i', $path, $m))
+ $path = '/home/'.substr($path, 1);
+ else if (substr($path, 0, 2) == '~/')
+ $path = getenv('HOME').substr($path, 1);
+ else if ($path[0] == DIRECTORY_SEPARATOR && substr($path, 0, 6) != '/home/')
+ $path = substr($path, 1);
+ }
+
+ // remove quotes (from erronous user input)
+ $path = str_replace(['"', "'"], ['', ''], $path);
+
+ return $path;
+ }
+
+
+ /**************/
+ /* read input */
+ /**************/
+
+ /*
+ since the CLI on WIN ist not interactive, the following things have to be considered
+ you do not receive keystrokes but whole strings upon pressing (wich also appends a \r)
+ as such and probably other control chars can not be registered
+ this also means, you can't hide input at all, least process it
+ */
+
+ public static function readInput(&$fields, $singleChar = false)
+ {
+ // first time set
+ if (self::$hasReadline === null)
+ self::$hasReadline = function_exists('readline_callback_handler_install');
+
+ // prevent default output if able
+ if (self::$hasReadline)
+ readline_callback_handler_install('', function() { });
+
+ foreach ($fields as $name => $data)
+ {
+ $vars = ['desc', 'isHidden', 'validPattern'];
+ foreach ($vars as $idx => $v)
+ $$v = isset($data[$idx]) ? $data[$idx] : false;
+
+ $charBuff = '';
+
+ if ($desc)
+ echo "\n".$desc.": ";
+
+ while (true) {
+ $r = [STDIN];
+ $w = $e = null;
+ $n = stream_select($r, $w, $e, 200000);
+
+ if ($n && in_array(STDIN, $r)) {
+ $char = stream_get_contents(STDIN, 1);
+ $keyId = ord($char);
+
+ // ignore this one
+ if ($keyId == self::CHR_TAB)
+ continue;
+
+ // WIN sends \r\n as sequence, ignore one
+ if ($keyId == self::CHR_CR && OS_WIN)
+ continue;
+
+ // will not be send on WIN .. other ways of returning from setup? (besides ctrl + c)
+ if ($keyId == self::CHR_ESC)
+ {
+ echo chr(self::CHR_BELL);
+ return false;
+ }
+ else if ($keyId == self::CHR_BACKSPACE)
+ {
+ if (!$charBuff)
+ continue;
+
+ $charBuff = mb_substr($charBuff, 0, -1);
+ if (!$isHidden && self::$hasReadline)
+ echo chr(self::CHR_BACK)." ".chr(self::CHR_BACK);
+ }
+ else if ($keyId == self::CHR_LF)
+ {
+ $fields[$name] = $charBuff;
+ break;
+ }
+ else if (!$validPattern || preg_match($validPattern, $char))
+ {
+ $charBuff .= $char;
+ if (!$isHidden && self::$hasReadline)
+ echo $char;
+
+ if ($singleChar && self::$hasReadline)
+ {
+ $fields[$name] = $charBuff;
+ break;
+ }
+ }
+ }
+ }
+ }
+
+ echo chr(self::CHR_BELL);
+
+ foreach ($fields as $f)
+ if (strlen($f))
+ return true;
+
+ $fields = null;
+ return true;
+ }
+}
+
+
+class Util
+{
+ const FILE_ACCESS = 0777;
+
+ const GEM_SCORE_BASE_WOTLK = 16; // rare quality wotlk gem score
+ const GEM_SCORE_BASE_BC = 8; // rare quality bc gem score
private static $perfectGems = null;
- public static $regions = array(
- 'us', 'eu', 'kr', 'tw', 'cn', 'dev'
+ public static $localeStrings = array( // zero-indexed
+ 'enus', null, 'frfr', 'dede', null, null, 'eses', null, 'ruru'
+ );
+
+ public static $subDomains = array(
+ 'www', null, 'fr', 'de', null, null, 'es', null, 'ru'
+ );
+
+ public static $typeClasses = array(
+ null, 'CreatureList', 'GameObjectList', 'ItemList', 'ItemsetList', 'QuestList', 'SpellList',
+ 'ZoneList', 'FactionList', 'PetList', 'AchievementList', 'TitleList', 'WorldEventList', 'CharClassList',
+ 'CharRaceList', 'SkillList', null, 'CurrencyList', null, 'SoundList',
+ TYPE_ICON => 'IconList',
+ TYPE_EMOTE => 'EmoteList',
+ TYPE_ENCHANTMENT => 'EnchantmentList'
+ );
+
+ public static $typeStrings = array( // zero-indexed
+ null, 'npc', 'object', 'item', 'itemset', 'quest', 'spell', 'zone', 'faction',
+ 'pet', 'achievement', 'title', 'event', 'class', 'race', 'skill', null, 'currency',
+ null, 'sound',
+ TYPE_ICON => 'icon',
+ TYPE_USER => 'user',
+ TYPE_EMOTE => 'emote',
+ TYPE_ENCHANTMENT => 'enchantment'
+ );
+
+ # todo (high): find a sensible way to write data here on setup
+ private static $gtCombatRatings = array(
+ 12 => 1.5, 13 => 13.8, 14 => 13.8, 15 => 5, 16 => 10, 17 => 10, 18 => 8, 19 => 14, 20 => 14,
+ 21 => 14, 22 => 10, 23 => 10, 24 => 8, 25 => 0, 26 => 0, 27 => 0, 28 => 10, 29 => 10,
+ 30 => 10, 31 => 10, 32 => 14, 33 => 0, 34 => 0, 35 => 28.75, 36 => 10, 37 => 2.5, 44 => 4.268292513760655
+ );
+
+ public static $itemFilter = array(
+ 20 => 'str', 21 => 'agi', 23 => 'int', 22 => 'sta', 24 => 'spi', 25 => 'arcres', 26 => 'firres', 27 => 'natres',
+ 28 => 'frores', 29 => 'shares', 30 => 'holres', 37 => 'mleatkpwr', 32 => 'dps', 35 => 'damagetype', 33 => 'dmgmin1', 34 => 'dmgmax1',
+ 36 => 'speed', 38 => 'rgdatkpwr', 39 => 'rgdhitrtng', 40 => 'rgdcritstrkrtng', 41 => 'armor', 44 => 'blockrtng', 43 => 'block', 42 => 'defrtng',
+ 45 => 'dodgertng', 46 => 'parryrtng', 48 => 'splhitrtng', 49 => 'splcritstrkrtng', 50 => 'splheal', 51 => 'spldmg', 52 => 'arcsplpwr', 53 => 'firsplpwr',
+ 54 => 'frosplpwr', 55 => 'holsplpwr', 56 => 'natsplpwr', 60 => 'healthrgn', 61 => 'manargn', 57 => 'shasplpwr', 77 => 'atkpwr', 78 => 'mlehastertng',
+ 79 => 'resirtng', 84 => 'mlecritstrkrtng', 94 => 'splpen', 95 => 'mlehitrtng', 96 => 'critstrkrtng', 97 => 'feratkpwr', 100 => 'nsockets', 101 => 'rgdhastertng',
+ 102 => 'splhastertng', 103 => 'hastertng', 114 => 'armorpenrtng', 115 => 'health', 116 => 'mana', 117 => 'exprtng', 119 => 'hitrtng', 123 => 'splpwr',
+ 134 => 'mledps', 135 => 'mledmgmin', 136 => 'mledmgmax', 137 => 'mlespeed', 138 => 'rgddps', 139 => 'rgddmgmin', 140 => 'rgddmgmax', 141 => 'rgdspeed'
);
public static $ssdMaskFields = array(
@@ -69,11 +331,23 @@ abstract class Util
'clothChestArmor', 'leatherChestArmor', 'mailChestArmor', 'plateChestArmor'
);
+ public static $weightScales = array(
+ 'agi', 'int', 'sta', 'spi', 'str', 'health', 'mana', 'healthrgn', 'manargn',
+ 'armor', 'blockrtng', 'block', 'defrtng', 'dodgertng', 'parryrtng', 'resirtng',
+ 'atkpwr', 'feratkpwr', 'armorpenrtng', 'critstrkrtng', 'exprtng', 'hastertng', 'hitrtng', 'splpen',
+ 'splpwr', 'arcsplpwr', 'firsplpwr', 'frosplpwr', 'holsplpwr', 'natsplpwr', 'shasplpwr',
+ 'dmg', 'mledps', 'rgddps', 'mledmgmin', 'rgddmgmin', 'mledmgmax', 'rgddmgmax', 'mlespeed', 'rgdspeed',
+ 'arcres', 'firres', 'frores', 'holres', 'natres', 'shares',
+ 'mleatkpwr', 'mlecritstrkrtng', 'mlehastertng', 'mlehitrtng', 'rgdatkpwr', 'rgdcritstrkrtng', 'rgdhastertng', 'rgdhitrtng',
+ 'splcritstrkrtng', 'splhastertng', 'splhitrtng', 'spldmg', 'splheal',
+ 'nsockets'
+ );
+
public static $dateFormatInternal = "Y/m/d H:i:s";
public static $changeLevelString = '%s';
+
public static $setRatingLevelString = '%s';
- public static $lvTabNoteString = '%s';
public static $filterResultString = '$$WH.sprintf(LANG.lvnote_filterresults, \'%s\')';
public static $tryFilteringString = '$$WH.sprintf(%s, %s, %s) + LANG.dash + LANG.lvnote_tryfiltering.replace(\'\', \'\')';
@@ -84,65 +358,172 @@ abstract class Util
public static $mapSelectorString = '%s (%d)';
- public static $expansionString = [null, 'bc', 'wotlk'];
+ public static $expansionString = array( // 3 & 4 unused .. obviously
+ null, 'bc', 'wotlk', 'cata', 'mop'
+ );
+
+ public static $bgImagePath = array (
+ 'tiny' => 'style="background-image: url(%s/images/wow/icons/tiny/%s.gif)"',
+ 'small' => 'style="background-image: url(%s/images/wow/icons/small/%s.jpg)"',
+ 'medium' => 'style="background-image: url(%s/images/wow/icons/medium/%s.jpg)"',
+ 'large' => 'style="background-image: url(%s/images/wow/icons/large/%s.jpg)"',
+ );
+
+ public static $configCats = array(
+ 'Other', 'Site', 'Caching', 'Account', 'Session', 'Site Reputation', 'Google Analytics', 'Profiler'
+ );
public static $tcEncoding = '0zMcmVokRsaqbdrfwihuGINALpTjnyxtgevElBCDFHJKOPQSUWXYZ123456789';
+ public static $wowheadLink = '';
private static $notes = [];
- public static function addNote(string $note, int $uGroupMask = U_GROUP_EMPLOYEE, int $level = LOG_LEVEL_ERROR) : void
+ public static function addNote($uGroupMask, $str)
{
- self::$notes[] = [$note, $uGroupMask, $level];
+ self::$notes[] = [$uGroupMask, $str];
}
- public static function getNotes() : array
+ public static function getNotes()
{
$notes = [];
- $severity = LOG_LEVEL_INFO;
- foreach (self::$notes as $k => [$note, $uGroup, $level])
- {
- if ($uGroup && !User::isInGroup($uGroup))
- continue;
- if ($level < $severity)
- $severity = $level;
+ foreach (self::$notes as $data)
+ if (!$data[0] || User::isInGroup($data[0]))
+ $notes[] = $data[1];
- $notes[] = $note;
- unset(self::$notes[$k]);
- }
-
- return [$notes, $severity];
+ return $notes;
}
- public static function formatMoney(int $qty) : string
+ private static $execTime = 0.0;
+
+ public static function execTime($set = false)
{
- if ($qty <= 0)
- return '';
-
- $parts = [];
-
- if ($g = intdiv($qty, 10000))
- $parts[] = ''.$g.'';
-
- if ($s = intdiv($qty % 10000, 100))
- $parts[] = ''.$s.'';
-
- if ($c = ($qty % 100))
- $parts[] = ''.$c.'';
-
- return implode(' ', $parts);
- }
-
- // pageTexts, questTexts and mails
- public static function parseHtmlText(string|array $text, bool $markdown = false) : string|array
- {
- if (is_array($text))
+ if ($set)
{
- foreach ($text as &$t)
- $t = self::parseHtmlText($t, $markdown);
-
- return $text;
+ self::$execTime = microTime(true);
+ return;
}
+ if (!self::$execTime)
+ return;
+
+ $newTime = microTime(true);
+ $tDiff = $newTime - self::$execTime;
+ self::$execTime = $newTime;
+
+ return self::formatTime($tDiff * 1000, true);
+ }
+
+ public static function formatMoney($qty)
+ {
+ $money = '';
+
+ if ($qty >= 10000)
+ {
+ $g = floor($qty / 10000);
+ $money .= ''.$g.' ';
+ $qty -= $g * 10000;
+ }
+
+ if ($qty >= 100)
+ {
+ $s = floor($qty / 100);
+ $money .= ''.$s.' ';
+ $qty -= $s * 100;
+ }
+
+ if ($qty > 0)
+ $money .= ''.$qty.'';
+
+ return $money;
+ }
+
+ public static function parseTime($sec)
+ {
+ $time = ['d' => 0, 'h' => 0, 'm' => 0, 's' => 0, 'ms' => 0];
+
+ if ($sec >= 3600 * 24)
+ {
+ $time['d'] = floor($sec / 3600 / 24);
+ $sec -= $time['d'] * 3600 * 24;
+ }
+
+ if ($sec >= 3600)
+ {
+ $time['h'] = floor($sec / 3600);
+ $sec -= $time['h'] * 3600;
+ }
+
+ if ($sec >= 60)
+ {
+ $time['m'] = floor($sec / 60);
+ $sec -= $time['m'] * 60;
+ }
+
+ if ($sec > 0)
+ {
+ $time['s'] = (int)$sec;
+ $sec -= $time['s'];
+ }
+
+ if (($sec * 1000) % 1000)
+ $time['ms'] = (int)($sec * 1000);
+
+ return $time;
+ }
+
+ public static function formatTime($base, $short = false)
+ {
+ $s = self::parseTime($base / 1000);
+ $fmt = [];
+
+ if ($short)
+ {
+ if ($_ = round($s['d'] / 364))
+ return $_." ".Lang::timeUnits('ab', 0);
+ if ($_ = round($s['d'] / 30))
+ return $_." ".Lang::timeUnits('ab', 1);
+ if ($_ = round($s['d'] / 7))
+ return $_." ".Lang::timeUnits('ab', 2);
+ if ($_ = round($s['d']))
+ return $_." ".Lang::timeUnits('ab', 3);
+ if ($_ = round($s['h']))
+ return $_." ".Lang::timeUnits('ab', 4);
+ if ($_ = round($s['m']))
+ return $_." ".Lang::timeUnits('ab', 5);
+ if ($_ = round($s['s'] + $s['ms'] / 1000, 2))
+ return $_." ".Lang::timeUnits('ab', 6);
+ if ($s['ms'])
+ return $s['ms']." ".Lang::timeUnits('ab', 7);
+
+ return '0 '.Lang::timeUnits('ab', 6);
+ }
+ else
+ {
+ $_ = $s['d'] + $s['h'] / 24;
+ if ($_ > 1 && !($_ % 364)) // whole years
+ return round(($s['d'] + $s['h'] / 24) / 364, 2)." ".Lang::timeUnits($s['d'] / 364 == 1 && !$s['h'] ? 'sg' : 'pl', 0);
+ if ($_ > 1 && !($_ % 30)) // whole month
+ return round(($s['d'] + $s['h'] / 24) / 30, 2)." ".Lang::timeUnits($s['d'] / 30 == 1 && !$s['h'] ? 'sg' : 'pl', 1);
+ if ($_ > 1 && !($_ % 7)) // whole weeks
+ return round(($s['d'] + $s['h'] / 24) / 7, 2)." ".Lang::timeUnits($s['d'] / 7 == 1 && !$s['h'] ? 'sg' : 'pl', 2);
+ if ($s['d'])
+ return round($s['d'] + $s['h'] / 24, 2)." ".Lang::timeUnits($s['d'] == 1 && !$s['h'] ? 'sg' : 'pl', 3);
+ if ($s['h'])
+ return round($s['h'] + $s['m'] / 60, 2)." ".Lang::timeUnits($s['h'] == 1 && !$s['m'] ? 'sg' : 'pl', 4);
+ if ($s['m'])
+ return round($s['m'] + $s['s'] / 60, 2)." ".Lang::timeUnits($s['m'] == 1 && !$s['s'] ? 'sg' : 'pl', 5);
+ if ($s['s'])
+ return round($s['s'] + $s['ms'] / 1000, 2)." ".Lang::timeUnits($s['s'] == 1 && !$s['ms'] ? 'sg' : 'pl', 6);
+ if ($s['ms'])
+ return $s['ms']." ".Lang::timeUnits($s['ms'] == 1 ? 'sg' : 'pl', 7);
+
+ return '0 '.Lang::timeUnits('pl', 6);
+ }
+ }
+
+ // pageText for Books (Item or GO) and questText
+ public static function parseHtmlText($text)
+ {
if (stristr($text, '')) // text is basically a html-document with weird linebreak-syntax
{
$pairs = array(
@@ -150,61 +531,57 @@ abstract class Util
'' => '',
'' => '',
'' => '',
- ' ' => $markdown ? '[br]' : ' '
+ ' ' => ' '
);
// html may contain 'Pictures' and FlavorImages and "stuff"
$text = preg_replace_callback(
'/src="([^"]+)"/i',
- function ($m) { return sprintf('src="%s/images/wow/%s.png"', Cfg::get('STATIC_URL'), strtr($m[1], ['\\' => '/'])); },
+ function ($m) { return 'src="'.STATIC_URL.'/images/wow/'.strtr($m[1], ['\\' => '/']).'.png"'; },
strtr($text, $pairs)
);
}
else
- $text = strtr($text, ["\n" => $markdown ? '[br]' : ' ', "\r" => '']);
-
- // escape fake html-ish tags the browser skipsh dishplaying ...!
- $text = preg_replace('/<([^\s\/]+)>/iu', '<\1>', $text);
+ $text = strtr($text, ["\n" => ' ', "\r" => '']);
$from = array(
- '/\$g\s*([^:;]*)\s*:\s*([^:;]*)\s*(:?[^:;]*);/ui',// directed gender-reference $g::
- '/\$t([^;]+);/ui', // HK rank. $t:; (maybe male/female if pvp unranked? Gets replaced with current HK rank.)
+ '/\|T([\w]+\\\)*([^\.]+)\.blp:\d+\|t/ui', // images (force size to tiny) |T:|t
+ '/\|c(\w{6})\w{2}([^\|]+)\|r/ui', // color |c|r
+ '/\$g\s*([^:;]+)\s*:\s*([^:;]+)\s*(:?[^:;]*);/ui',// directed gender-reference $g:: |