';
if ($description)
- $x .= ' '.Util::jsEscape($description).' ';
+ $x .= ' '.$description.' ';
if ($criteria)
{
@@ -253,16 +242,19 @@ class AchievementList extends BaseType
return $x;
}
- public function getSourceData()
+ public function getSourceData(int $id = 0) : array
{
$data = [];
foreach ($this->iterate() as $__)
{
+ if ($id && $id != $this->id)
+ continue;
+
$data[$this->id] = array(
"n" => $this->getField('name', true),
"s" => $this->curTpl['faction'],
- "t" => TYPE_ACHIEVEMENT,
+ "t" => Type::ACHIEVEMENT,
"ti" => $this->id
);
}
@@ -274,11 +266,12 @@ class AchievementList extends BaseType
class AchievementListFilter extends Filter
{
-
- protected $enums = array(
+ protected string $type = 'achievements';
+ protected static array $enums = array(
+ 4 => parent::ENUM_ZONE, // location
11 => array(
327 => 160, // Lunar Festival
- 335 => 187, // Love is in the Air
+ 423 => 187, // Love is in the Air
181 => 159, // Noblegarden
201 => 163, // Children's Week
341 => 161, // Midsummer Fire Festival
@@ -288,8 +281,8 @@ class AchievementListFilter extends Filter
141 => 156, // Feast of Winter Veil
409 => -3456, // Day of the Dead
398 => -3457, // Pirates' Day
- FILTER_ENUM_ANY => true,
- FILTER_ENUM_NONE => false,
+ parent::ENUM_ANY => true,
+ parent::ENUM_NONE => false,
283 => -1, // valid events without achievements
285 => -1, 353 => -1, 420 => -1,
400 => -1, 284 => -1, 374 => -1,
@@ -297,116 +290,100 @@ class AchievementListFilter extends Filter
)
);
- protected $genericFilter = array( // misc (bool): _NUMERIC => useFloat; _STRING => localized; _FLAG => match Value; _BOOLEAN => stringSet
- 2 => [FILTER_CR_BOOLEAN, 'reward_loc0', true ], // givesreward
- 3 => [FILTER_CR_STRING, 'reward', STR_LOCALIZED ], // rewardtext
- 4 => [FILTER_CR_NYI_PH, null, 1, ], // location [enum]
- 5 => [FILTER_CR_CALLBACK, 'cbSeries', ACHIEVEMENT_CU_FIRST_SERIES, null], // first in series [yn]
- 6 => [FILTER_CR_CALLBACK, 'cbSeries', ACHIEVEMENT_CU_LAST_SERIES, null], // last in series [yn]
- 7 => [FILTER_CR_BOOLEAN, 'chainId', ], // partseries
- 9 => [FILTER_CR_NUMERIC, 'id', NUM_CAST_INT, true], // id
- 10 => [FILTER_CR_STRING, 'ic.name', ], // icon
- 11 => [FILTER_CR_CALLBACK, 'cbRelEvent', null, null], // related event [enum]
- 14 => [FILTER_CR_FLAG, 'cuFlags', CUSTOM_HAS_COMMENT ], // hascomments
- 15 => [FILTER_CR_FLAG, 'cuFlags', CUSTOM_HAS_SCREENSHOT ], // hasscreenshots
- 16 => [FILTER_CR_FLAG, 'cuFlags', CUSTOM_HAS_VIDEO ], // hasvideos
- 18 => [FILTER_CR_STAFFFLAG, 'flags', ] // flags
+ protected 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
);
- // 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 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
);
- protected function createSQLForCriterium(&$cr)
- {
- if (in_array($cr[0], array_keys($this->genericFilter)))
- if ($genCr = $this->genericCriterion($cr))
- return $genCr;
-
- unset($cr);
- $this->error = true;
- return [1];
- }
-
- protected function createSQLForValues()
+ protected function createSQLForValues() : array
{
$parts = [];
- $_v = &$this->fiData['v'];
+ $_v = &$this->values;
// name ex: +description, +rewards
- if (isset($_v['na']))
+ if ($_v['na'])
{
$_ = [];
- if (isset($_v['ex']) && $_v['ex'] == 'on')
- $_ = $this->modularizeString(['name_loc'.User::$localeId, 'reward_loc'.User::$localeId, 'description_loc'.User::$localeId]);
+ if ($_v['ex'] == 'on')
+ $_ = $this->buildLikeLookup([['na', 'name_loc'.Lang::getLocale()->value], ['na', 'reward_loc'.Lang::getLocale()->value], ['na', 'description_loc'.Lang::getLocale()->value]]);
else
- $_ = $this->modularizeString(['name_loc'.User::$localeId]);
+ $_ = $this->buildLikeLookup([['na', 'name_loc'.Lang::getLocale()->value]]);
if ($_)
$parts[] = $_;
}
// points min
- if (isset($_v['minpt']))
+ if ($_v['minpt'])
$parts[] = ['points', $_v['minpt'], '>='];
// points max
- if (isset($_v['maxpt']))
+ if ($_v['maxpt'])
$parts[] = ['points', $_v['maxpt'], '<='];
// faction (side)
- if (isset($_v['si']))
+ if ($_v['si'])
{
- switch ($_v['si'])
+ $parts[] = match ($_v['si'])
{
- case -1: // faction, exclusive both
- case -2:
- $parts[] = ['faction', -$_v['si']];
- break;
- case 1: // faction, inclusive both
- case 2:
- case 3: // both
- $parts[] = ['faction', $_v['si'], '&'];
- break;
- }
+ -SIDE_ALLIANCE, // equals faction
+ -SIDE_HORDE => ['faction', -$_v['si']],
+ SIDE_ALLIANCE, // includes faction
+ SIDE_HORDE,
+ SIDE_BOTH => ['faction', $_v['si'], '&']
+ };
}
return $parts;
}
- protected function cbRelEvent($cr, $value)
+ protected function cbRelEvent(int $cr, int $crs, string $crv) : ?array
{
- if (!isset($this->enums[$cr[0]][$cr[1]]))
- return false;
+ if (!isset(self::$enums[$cr][$crs]))
+ return null;
- $_ = $this->enums[$cr[0]][$cr[1]];
+ $_ = self::$enums[$cr][$crs];
if (is_int($_))
return ($_ > 0) ? ['category', $_] : ['id', abs($_)];
else
{
- $ids = array_filter($this->enums[$cr[0]], function($x) { return is_int($x) && $x > 0; });
+ $ids = array_filter(self::$enums[$cr], fn($x) => is_int($x) && $x > 0);
return ['category', $ids, $_ ? null : '!'];
}
- return false;
+ return null;
}
- protected function cbSeries($cr, $value)
+ protected function cbSeries(int $cr, int $crs, string $crv, int $seriesFlag) : ?array
{
- if ($this->int2Bool($cr[1]))
- return $cr[1] ? ['AND', ['chainId', 0, '!'], ['cuFlags', $value, '&']] : ['AND', ['chainId', 0, '!'], [['cuFlags', $value, '&'], 0]];
+ if ($this->int2Bool($crs))
+ return $crs ? [DB::AND, ['chainId', 0, '!'], ['cuFlags', $seriesFlag, '&']] : [DB::AND, ['chainId', 0, '!'], [['cuFlags', $seriesFlag, '&'], 0]];
- return false;
+ return null;
}
}
diff --git a/includes/dbtypes/areatrigger.class.php b/includes/dbtypes/areatrigger.class.php
new file mode 100644
index 00000000..527afb25
--- /dev/null
+++ b/includes/dbtypes/areatrigger.class.php
@@ -0,0 +1,99 @@
+ [['s']], // guid < 0 are teleporter targets, so exclude them here
+ 's' => ['j' => ['::spawns s ON s.`type` = 503 AND s.`typeId` = a.`id` AND s.`guid` > 0', true], 's' => ', GROUP_CONCAT(s.`areaId`) AS "areaId"', 'g' => 'a.`id`']
+ );
+
+ public function __construct(array $conditions = [], array $miscData = [])
+ {
+ parent::__construct($conditions, $miscData);
+
+ foreach ($this->iterate() as $id => &$_curTpl)
+ if (!$_curTpl['name'])
+ $_curTpl['name'] = Lang::areatrigger('unnamed', [$id]);
+ }
+
+ public static function getName(int $id) : ?LocString
+ {
+ if ($n = DB::Aowow()->SelectRow('SELECT `name` AS "name_loc0" FROM %n WHERE `id` = %i', self::$dataTable, $id))
+ return new LocString($n, callback: fn($x) => $x ?: Lang::areatrigger('unnamed', [$id]));
+ return null;
+ }
+
+ public function getListviewData() : array
+ {
+ $data = [];
+
+ foreach ($this->iterate() as $__)
+ {
+ $data[$this->id] = array(
+ 'id' => $this->curTpl['id'],
+ 'type' => $this->curTpl['type'],
+ 'name' => $this->curTpl['name'],
+ );
+
+ if ($_ = $this->curTpl['areaId'])
+ $data[$this->id]['location'] = explode(',', $_);
+ }
+
+ return $data;
+ }
+
+ public function getJSGlobals(int $addMask = GLOBALINFO_ANY) : array { return []; }
+
+ public function renderTooltip() : ?string { return null; }
+}
+
+class AreaTriggerListFilter extends Filter
+{
+ protected string $type = 'areatrigger';
+ protected static array $genericFilter = array(
+ 2 => [parent::CR_NUMERIC, 'id', NUM_CAST_INT] // id
+ );
+
+ // fieldId => [checkType, checkValue[, fieldIsArray]]
+ protected static array $inputFields = array(
+ 'cr' => [parent::V_LIST, [2], true ], // criteria ids
+ 'crs' => [parent::V_RANGE, [1, 6], true ], // criteria operators
+ 'crv' => [parent::V_REGEX, parent::PATTERN_INT, true ], // criteria values - all criteria are numeric here
+ 'na' => [parent::V_NAME, false, false], // name - only printable chars, no delimiter
+ 'ma' => [parent::V_EQUAL, 1, false], // match any / all filter
+ 'ty' => [parent::V_RANGE, [0, 5], true ] // types
+ );
+
+ protected function createSQLForValues() : array
+ {
+ $parts = [];
+ $_v = &$this->values;
+
+ // name [str]
+ if ($_v['na'])
+ if ($_ = $this->buildLikeLookup([['na', 'name']]))
+ $parts[] = $_;
+
+ // type [list]
+ if ($_v['ty'])
+ $parts[] = ['type', $_v['ty']];
+
+ return $parts;
+ }
+}
+
+?>
diff --git a/includes/dbtypes/arenateam.class.php b/includes/dbtypes/arenateam.class.php
new file mode 100644
index 00000000..9103da0c
--- /dev/null
+++ b/includes/dbtypes/arenateam.class.php
@@ -0,0 +1,368 @@
+iterate() as $__)
+ {
+ $data[$this->id] = array(
+ 'name' => $this->curTpl['name'],
+ 'realm' => Profiler::urlize($this->curTpl['realmName'], true),
+ 'realmname' => $this->curTpl['realmName'],
+ // 'battlegroup' => Profiler::urlize($this->curTpl['battlegroup']), // was renamed to subregion somewhere around cata release
+ // 'battlegroupname' => $this->curTpl['battlegroup'],
+ 'region' => Profiler::urlize($this->curTpl['region']),
+ 'faction' => $this->curTpl['faction'],
+ 'size' => $this->curTpl['type'],
+ 'rank' => $this->curTpl['rank'],
+ 'wins' => $this->curTpl['seasonWins'],
+ 'games' => $this->curTpl['seasonGames'],
+ 'rating' => $this->curTpl['rating'],
+ 'members' => $this->curTpl['members']
+ );
+ }
+
+ return $data;
+ }
+
+ // plz dont..
+ public static function getName(int|string $id) : ?LocString { return null; }
+
+ public function renderTooltip() : ?string { return null; }
+ public function getJSGlobals(int $addMask = 0) : array { return []; }
+}
+
+
+class ArenaTeamListFilter extends Filter
+{
+ use TrProfilerFilter;
+
+ protected string $type = 'arenateams';
+ 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, [1, 2], false], // side
+ 'sz' => [parent::V_LIST, [2, 3, 5], false], // tema size
+ '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
+ );
+
+ public array $extraOpts = [];
+
+ protected function createSQLForValues() : array
+ {
+ $parts = [];
+ $_v = $this->values;
+
+ // region (rg), battlegroup (bg) and server (sv) are passed to ArenaTeamList as miscData and handled there
+
+ // name [str]
+ if ($_v['na'])
+ if ($_ = $this->buildLikeLookup([['na', 'at.name']], $_v['ex'] == 'on'))
+ $parts[] = $_;
+
+ // side [list]
+ if ($_v['si'] == SIDE_ALLIANCE)
+ $parts[] = ['c.race', ChrRace::fromMask(ChrRace::MASK_ALLIANCE)];
+ else if ($_v['si'] == SIDE_HORDE)
+ $parts[] = ['c.race', ChrRace::fromMask(ChrRace::MASK_HORDE)];
+
+ // size [int]
+ if ($_v['sz'])
+ $parts[] = ['at.type', $_v['sz']];
+
+ return $parts;
+ }
+}
+
+
+class RemoteArenaTeamList extends ArenaTeamList
+{
+ protected string $queryBase = 'SELECT `at`.*, `at`.`arenaTeamId` AS ARRAY_KEY FROM arena_team at';
+ protected array $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 array $members = [];
+ private array $rankOrder = [];
+
+ public function __construct(array $conditions = [], array $miscData = [])
+ {
+ // select DB by realm
+ if (!$this->selectRealms($miscData))
+ {
+ trigger_error('RemoteArenaTeamList::__construct - cannot access any realm.', E_USER_WARNING);
+ return;
+ }
+
+ parent::__construct($conditions, $miscData);
+
+ if ($this->error)
+ return;
+
+ // ranks in DB are inaccurate. recalculate from rating (fetched as DESC from DB)
+ foreach ($this->dbNames as $rId => $__)
+ foreach ([2, 3, 5] as $type)
+ $this->rankOrder[$rId][$type] = DB::Characters($rId)->selectCol('SELECT `arenaTeamId` FROM arena_team WHERE `type` = %i ORDER BY `rating` DESC', $type);
+
+ reset($this->dbNames); // only use when querying single realm
+ $realms = Profiler::getRealms();
+ $distrib = [];
+
+ // post processing
+ foreach ($this->iterate() as $guid => &$curTpl)
+ {
+ // battlegroup
+ $curTpl['battlegroup'] = Cfg::get('BATTLEGROUP');
+
+ // realm, rank
+ $r = explode(':', $guid);
+ if (!empty($realms[$r[0]]))
+ {
+ $curTpl['realm'] = $r[0];
+ $curTpl['realmName'] = $realms[$r[0]]['name'];
+ $curTpl['region'] = $realms[$r[0]]['region'];
+ $curTpl['rank'] = array_search($curTpl['arenaTeamId'], $this->rankOrder[$r[0]][$curTpl['type']]) + 1;
+ }
+ else
+ {
+ trigger_error('arena team #'.$guid.' belongs to nonexistent realm #'.$r, E_USER_WARNING);
+ unset($this->templates[$guid]);
+ continue;
+ }
+
+ // empty name
+ if (!$curTpl['name'])
+ {
+ trigger_error('arena team #'.$guid.' on realm #'.$r.' has empty name.', E_USER_WARNING);
+ unset($this->templates[$guid]);
+ continue;
+ }
+
+ // team members
+ $this->members[$r[0]][$r[1]] = $r[1];
+
+ // equalize distribution
+ if (empty($distrib[$curTpl['realm']]))
+ $distrib[$curTpl['realm']] = 1;
+ else
+ $distrib[$curTpl['realm']]++;
+ }
+
+ // get team members
+ foreach ($this->members as $realmId => &$teams)
+ $teams = DB::Characters($realmId)->selectAssoc(
+ '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 %in AND c.`deleteInfos_Account` IS NULL AND c.`level` <= %i AND (c.`extra_flags` & %i) = 0',
+ $teams, MAX_LEVEL, Profiler::CHAR_GMFLAGS
+ );
+
+ // equalize subject distribution across realms
+ $limit = 0;
+ foreach ($conditions as $c)
+ if (is_numeric($c))
+ $limit = max(0, (int)$c);
+
+ if (!$limit) // int:0 means unlimited, so skip early
+ return;
+
+ $total = array_sum($distrib);
+ foreach ($distrib as &$d)
+ $d = ceil($limit * $d / $total);
+
+ foreach ($this->iterate() as $guid => &$curTpl)
+ {
+ if ($limit <= 0 || $distrib[$curTpl['realm']] <= 0)
+ {
+ unset($this->templates[$guid]);
+ continue;
+ }
+
+ $r = explode(':', $guid);
+ if (isset($this->members[$r[0]][$r[1]]))
+ $curTpl['members'] = array_values($this->members[$r[0]][$r[1]]); // [name, classId, isCaptain]
+
+ $distrib[$curTpl['realm']]--;
+ $limit--;
+ }
+ }
+
+ public function initializeLocalEntries() : void
+ {
+ if (!$this->templates)
+ return;
+
+ $profiles = [];
+ // init members for tooltips
+ foreach ($this->members as $realmId => $teams)
+ {
+ $gladiators = [];
+ foreach ($teams as $team)
+ $gladiators = array_merge($gladiators, array_keys($team));
+
+ $profiles[$realmId] = new RemoteProfileList(array(['c.guid', $gladiators]), ['sv' => $realmId]);
+
+ if (!$profiles[$realmId]->error)
+ $profiles[$realmId]->initializeLocalEntries();
+ }
+
+ $data = [];
+ foreach ($this->iterate() as $guid => $__)
+ {
+ $data['realm'][$guid] = $this->getField('realm');
+ $data['realmGUID'][$guid] = $this->getField('arenaTeamId');
+ $data['name'][$guid] = $this->getField('name');
+ $data['nameUrl'][$guid] = Profiler::urlize($this->getField('name'));
+ $data['type'][$guid] = $this->getField('type');
+ $data['rating'][$guid] = $this->getField('rating');
+ $data['stub'][$guid] = 1;
+ }
+
+ // basic arena team data
+ DB::Aowow()->qry('INSERT INTO ::profiler_arena_team %m ON DUPLICATE KEY UPDATE `id` = `id`', $data);
+
+ // merge back local ids
+ $localIds = DB::Aowow()->selectCol('SELECT CONCAT(`realm`, ":", `realmGUID`) AS ARRAY_KEY, `id` FROM ::profiler_arena_team WHERE `realm` IN %in AND `realmGUID` IN %in',
+ $data['realm'], $data['realmGUID']
+ );
+
+ foreach ($this->iterate() as $guid => &$_curTpl)
+ if (isset($localIds[$guid]))
+ $_curTpl['id'] = $localIds[$guid];
+
+
+ // profiler_arena_team_member requires profiles and arena teams to be filled
+ foreach ($this->members as $realmId => $teams)
+ {
+ if (empty($profiles[$realmId]))
+ continue;
+
+ $memberData = [];
+ foreach ($teams as $teamId => $team)
+ {
+ $clearMembers = [];
+ foreach ($team as $memberId => $member)
+ {
+ $clearMembers[] = $profiles[$realmId]->getEntry($realmId.':'.$memberId)['id'];
+
+ $memberData['arenaTeamId'][] = $localIds[$realmId.':'.$teamId];
+ $memberData['profileId'][] = $profiles[$realmId]->getEntry($realmId.':'.$memberId)['id'];
+ $memberData['captain'][] = $member[2];
+ }
+
+ // Delete members from other teams of the same type
+ DB::Aowow()->qry(
+ 'DELETE atm
+ FROM ::profiler_arena_team_member atm
+ JOIN ::profiler_arena_team at ON atm.`arenaTeamId` = at.`id` AND at.`type` = %i
+ WHERE atm.`profileId` IN %in',
+ $data['type'][$realmId.':'.$teamId] ?? 0,
+ $clearMembers
+ );
+ }
+
+ DB::Aowow()->qry('INSERT INTO ::profiler_arena_team_member %m ON DUPLICATE KEY UPDATE `profileId` = `profileId`', $memberData);
+ }
+ }
+}
+
+
+class LocalArenaTeamList extends ArenaTeamList
+{
+ protected string $queryBase = 'SELECT at.*, at.id AS ARRAY_KEY FROM ::profiler_arena_team at';
+ protected array $queryOpts = array(
+ 'at' => [['atm', 'c'], 'g' => 'ARRAY_KEY', 'o' => 'rating DESC'],
+ 'atm' => ['j' => '::profiler_arena_team_member atm ON atm.`arenaTeamId` = at.`id`'],
+ 'c' => ['j' => '::profiler_profiles c ON c.`id` = atm.`profileId`', 's' => ', BIT_OR(IF(c.`race` IN (1, 3, 4, 7, 11), 1, 2)) - 1 AS "faction"']
+ );
+
+ public function __construct(array $conditions = [], array $miscData = [])
+ {
+ $realms = Profiler::getRealms();
+
+ // graft realm selection from miscData onto conditions
+ if (isset($miscData['sv']))
+ $realms = array_filter($realms, fn($x) => Profiler::urlize($x['name']) == Profiler::urlize($miscData['sv']));
+
+ if (isset($miscData['rg']))
+ $realms = array_filter($realms, fn($x) => $x['region'] == $miscData['rg']);
+
+ if (!$realms)
+ {
+ trigger_error('LocalArenaTeamList::__construct - cannot access any realm.', E_USER_WARNING);
+ return;
+ }
+
+ if ($conditions)
+ {
+ array_unshift($conditions, DB::AND);
+ $conditions = [DB::AND, ['realm', array_keys($realms)], $conditions];
+ }
+ else
+ $conditions = [['realm', array_keys($realms)]];
+
+ parent::__construct($conditions, $miscData);
+
+ if ($this->error)
+ return;
+
+ // post processing
+ $members = DB::Aowow()->selectAssoc(
+ 'SELECT `arenaTeamId` AS ARRAY_KEY, p.`id` AS ARRAY_KEY2, p.`name` AS "0", p.`class` AS "1", atm.`captain` AS "2"
+ FROM ::profiler_arena_team_member atm
+ JOIN ::profiler_profiles p ON p.`id` = atm.`profileId`
+ WHERE `arenaTeamId` IN %in',
+ $this->getFoundIDs()
+ );
+
+ foreach ($this->iterate() as $id => &$curTpl)
+ {
+ if ($curTpl['realm'] && !isset($realms[$curTpl['realm']]))
+ continue;
+
+ if (isset($realms[$curTpl['realm']]))
+ {
+ $curTpl['realmName'] = $realms[$curTpl['realm']]['name'];
+ $curTpl['region'] = $realms[$curTpl['realm']]['region'];
+ }
+
+ // battlegroup
+ $curTpl['battlegroup'] = Cfg::get('BATTLEGROUP');
+
+ $curTpl['members'] = array_values($members[$id]);
+ }
+ }
+
+ public function getProfileUrl() : string
+ {
+ $url = '?arena-team=';
+
+ return $url.implode('.', array(
+ $this->getField('region'),
+ Profiler::urlize($this->getField('realmName'), true),
+ Profiler::urlize($this->getField('name'))
+ ));
+ }
+}
+
+
+?>
diff --git a/includes/types/charclass.class.php b/includes/dbtypes/charclass.class.php
similarity index 58%
rename from includes/types/charclass.class.php
rename to includes/dbtypes/charclass.class.php
index a3d574f0..5c6db072 100644
--- a/includes/types/charclass.class.php
+++ b/includes/dbtypes/charclass.class.php
@@ -1,26 +1,32 @@
[['ic']],
+ 'ic' => ['j' => ['::icons ic ON ic.`id` = c.`iconId`', true], 's' => ', ic.`name` AS "iconString"']
+ );
- public function __construct($conditions = [])
+ public function __construct($conditions = [], array $miscData = [])
{
- parent::__construct($conditions);
+ parent::__construct($conditions, $miscData);
foreach ($this->iterate() as $k => &$_curTpl)
$_curTpl['skills'] = explode(' ', $_curTpl['skills']);
}
- public function getListviewData()
+ public function getListviewData() : array
{
$data = [];
@@ -46,7 +52,7 @@ class CharClassList extends BaseType
return $data;
}
- public function getJSGlobals($addMask = 0)
+ public function getJSGlobals(int $addMask = 0) : array
{
$data = [];
@@ -56,8 +62,7 @@ class CharClassList extends BaseType
return $data;
}
- public function addRewardsToJScript(&$ref) { }
- public function renderTooltip() { }
+ public function renderTooltip() : ?string { return null; }
}
?>
diff --git a/includes/dbtypes/charrace.class.php b/includes/dbtypes/charrace.class.php
new file mode 100644
index 00000000..c790aafe
--- /dev/null
+++ b/includes/dbtypes/charrace.class.php
@@ -0,0 +1,58 @@
+ [['ic0', 'ic1']],
+ 'ic0' => ['j' => ['::icons ic0 ON ic0.`id` = r.`iconId0`', true], 's' => ', ic0.`name` AS "iconStringMale"'],
+ 'ic1' => ['j' => ['::icons ic1 ON ic1.`id` = r.`iconId1`', true], 's' => ', ic1.`name` AS "iconStringFemale"']
+ );
+
+ public function getListviewData() : array
+ {
+ $data = [];
+
+ foreach ($this->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(int $addMask = 0) : array
+ {
+ $data = [];
+
+ foreach ($this->iterate() as $__)
+ $data[Type::CHR_RACE][$this->id] = ['name' => $this->getField('name', true)];
+
+ return $data;
+ }
+
+ public function renderTooltip() : ?string { return null; }
+}
+
+?>
diff --git a/includes/dbtypes/creature.class.php b/includes/dbtypes/creature.class.php
new file mode 100644
index 00000000..6c259153
--- /dev/null
+++ b/includes/dbtypes/creature.class.php
@@ -0,0 +1,578 @@
+ [['ft', 'qse', 'dct1', 'dct2', 'dct3'], 's' => ', IFNULL(dct1.`id`, IFNULL(dct2.`id`, IFNULL(dct3.`id`, 0))) AS "parentId", IFNULL(dct1.`name_loc0`, IFNULL(dct2.`name_loc0`, IFNULL(dct3.`name_loc0`, ""))) AS "parent_loc0", IFNULL(dct1.`name_loc2`, IFNULL(dct2.`name_loc2`, IFNULL(dct3.`name_loc2`, ""))) AS "parent_loc2", IFNULL(dct1.`name_loc3`, IFNULL(dct2.`name_loc3`, IFNULL(dct3.`name_loc3`, ""))) AS "parent_loc3", IFNULL(dct1.`name_loc4`, IFNULL(dct2.`name_loc4`, IFNULL(dct3.`name_loc4`, ""))) AS "parent_loc4", IFNULL(dct1.`name_loc6`, IFNULL(dct2.`name_loc6`, IFNULL(dct3.`name_loc6`, ""))) AS "parent_loc6", IFNULL(dct1.name_loc8, IFNULL(dct2.`name_loc8`, IFNULL(dct3.`name_loc8`, ""))) AS "parent_loc8", IF(dct1.`difficultyEntry1` = ct.`id`, 1, IF(dct2.`difficultyEntry2` = ct.`id`, 2, IF(dct3.`difficultyEntry3` = ct.`id`, 3, 0))) AS "difficultyMode"'],
+ 'nml' => ['j' => ['::creature_search nml ON nml.`id` = ct.`id` AND nml.`locale` = DB_LOC_I']],
+ 'dct1' => ['j' => ['::creature dct1 ON ct.`cuFlags` & 0x02 AND dct1.`difficultyEntry1` = ct.`id`', true]],
+ 'dct2' => ['j' => ['::creature dct2 ON ct.`cuFlags` & 0x02 AND dct2.`difficultyEntry2` = ct.`id`', true]],
+ 'dct3' => ['j' => ['::creature dct3 ON ct.`cuFlags` & 0x02 AND dct3.`difficultyEntry3` = ct.`id`', true]],
+ 'ft' => ['j' => '::factiontemplate ft ON ft.`id` = ct.`faction`', 's' => ', ft.`factionId`, IFNULL(ft.`A`, 0) AS "A", IFNULL(ft.`H`, 0) AS "H"'],
+ '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(array $conditions = [], array $miscData = [])
+ {
+ 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 function renderTooltip() : ?string
+ {
+ if (!$this->curTpl)
+ return null;
+
+ $level = '??';
+ $type = $this->curTpl['type'];
+ $row3 = [Lang::game('level')];
+ $fam = $this->curTpl['family'];
+
+ if (!($this->curTpl['typeFlags'] & NPC_TYPEFLAG_BOSS_MOB))
+ {
+ $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 .= '| '.Util::htmlEscape($this->getField('name', true)).' | ';
+
+ if ($sn = $this->getField('subname', true))
+ $x .= '| '.Util::htmlEscape($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() : int
+ {
+ // dwarf?? [null, 30754, 30753, 30755, 30736]
+ // totems use hardcoded models, tauren model is base
+ $totems = [null, 4589, 4588, 4587, 4590]; // slot => modelId
+ $data = [];
+
+ for ($i = 1; $i < 5; $i++)
+ if ($_ = $this->curTpl['displayId'.$i])
+ $data[] = $_;
+
+ if (count($data) == 1 && ($slotId = array_search($data[0], $totems)))
+ $data = DB::World()->selectCol('SELECT `DisplayId` FROM player_totem_model WHERE `TotemSlot` = %i', $slotId);
+
+ return !$data ? 0 : $data[array_rand($data)];
+ }
+
+ public function getBaseStats(string $type) : array
+ {
+ // 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];
+ case 'resistance':
+ $r = [];
+ for ($i = SPELL_SCHOOL_HOLY; $i < SPELL_SCHOOL_ARCANE+1; $i++)
+ $r[$i] = $this->getField('resistance'.$i);
+
+ return $r;
+ default:
+ return [];
+ }
+ }
+
+ public function isBoss() : bool
+ {
+ return ($this->curTpl['cuFlags'] & NPC_CU_INSTANCE_BOSS) || ($this->curTpl['typeFlags'] & NPC_TYPEFLAG_BOSS_MOB && $this->curTpl['rank']);
+ }
+
+ public function isMineable() : bool
+ {
+ return $this->curTpl['skinLootId'] && ($this->curTpl['typeFlags'] & NPC_TYPEFLAG_SKIN_WITH_MINING);
+ }
+
+ public function isGatherable() : bool
+ {
+ return $this->curTpl['skinLootId'] && ($this->curTpl['typeFlags'] & NPC_TYPEFLAG_SKIN_WITH_HERBALISM);
+ }
+
+ public function isSalvageable() : bool
+ {
+ return $this->curTpl['skinLootId'] && ($this->curTpl['typeFlags'] & NPC_TYPEFLAG_SKIN_WITH_ENGINEERING);
+ }
+
+ public function getListviewData(int $addInfoMask = 0x0) : array
+ {
+ /* looks like this data differs per occasion
+ *
+ * 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 %in AND `RewOnKillRepFaction1` > 0 UNION
+ SELECT `creature_id` AS ARRAY_KEY, `RewOnKillRepFaction2` AS ARRAY_KEY2, `RewOnKillRepValue2` FROM creature_onkill_reputation WHERE `creature_id` IN %in 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(int $addMask = 0) : array
+ {
+ $data = [];
+
+ foreach ($this->iterate() as $__)
+ $data[Type::NPC][$this->id] = ['name' => $this->getField('name', true)];
+
+ return $data;
+ }
+
+ public function getSourceData(int $id = 0) : array
+ {
+ $data = [];
+
+ foreach ($this->iterate() as $__)
+ {
+ if ($id && $id != $this->id)
+ continue;
+
+ $data[$this->id] = array(
+ 'n' => $this->getField('parentId') ? $this->getField('parent', true) : $this->getField('name', true),
+ 't' => Type::NPC,
+ 'ti' => $this->getField('parentId') ?: $this->id
+ );
+ }
+
+ return $data;
+ }
+}
+
+
+class CreatureListFilter extends Filter
+{
+ protected string $type = 'npcs';
+ protected static array $enums = array(
+ 3 => parent::ENUM_FACTION, // faction
+ 6 => parent::ENUM_ZONE, // foundin
+ 42 => parent::ENUM_FACTION, // increasesrepwith
+ 43 => parent::ENUM_FACTION, // decreasesrepwith
+ 38 => parent::ENUM_EVENT // relatedevent
+ );
+
+ protected static array $genericFilter = array(
+ 1 => [parent::CR_CALLBACK, 'cbHealthMana', 'healthMax', 'healthMin'], // health [num]
+ 2 => [parent::CR_CALLBACK, 'cbHealthMana', 'manaMin', 'manaMax' ], // mana [num]
+ 3 => [parent::CR_CALLBACK, 'cbFaction', null, null ], // faction [enum]
+ 5 => [parent::CR_FLAG, 'npcflag', NPC_FLAG_REPAIRER ], // canrepair
+ 6 => [parent::CR_ENUM, 's.areaId', false, true ], // foundin
+ 7 => [parent::CR_CALLBACK, 'cbQuestRelation', 'startsQuests', 0x1 ], // startsquest [enum]
+ 8 => [parent::CR_CALLBACK, 'cbQuestRelation', 'endsQuests', 0x2 ], // endsquest [enum]
+ 9 => [parent::CR_BOOLEAN, 'lootId', ], // lootable
+ 10 => [parent::CR_CALLBACK, 'cbRegularSkinLoot', NPC_TYPEFLAG_SPECIALLOOT ], // skinnable [yn]
+ 11 => [parent::CR_BOOLEAN, 'pickpocketLootId', ], // pickpocketable
+ 12 => [parent::CR_CALLBACK, 'cbMoneyDrop', null, null ], // averagemoneydropped [op] [int]
+ 15 => [parent::CR_CALLBACK, 'cbSpecialSkinLoot', NPC_TYPEFLAG_SKIN_WITH_HERBALISM, null ], // gatherable [yn]
+ 16 => [parent::CR_CALLBACK, 'cbSpecialSkinLoot', NPC_TYPEFLAG_SKIN_WITH_MINING, null ], // minable [yn]
+ 18 => [parent::CR_FLAG, 'npcflag', NPC_FLAG_AUCTIONEER ], // auctioneer
+ 19 => [parent::CR_FLAG, 'npcflag', NPC_FLAG_BANKER ], // banker
+ 20 => [parent::CR_FLAG, 'npcflag', NPC_FLAG_BATTLEMASTER ], // battlemaster
+ 21 => [parent::CR_FLAG, 'npcflag', NPC_FLAG_FLIGHT_MASTER ], // flightmaster
+ 22 => [parent::CR_FLAG, 'npcflag', NPC_FLAG_GUILD_MASTER ], // guildmaster
+ 23 => [parent::CR_FLAG, 'npcflag', NPC_FLAG_INNKEEPER ], // innkeeper
+ 24 => [parent::CR_FLAG, 'npcflag', NPC_FLAG_CLASS_TRAINER ], // talentunlearner
+ 25 => [parent::CR_FLAG, 'npcflag', NPC_FLAG_GUILD_MASTER ], // tabardvendor
+ 27 => [parent::CR_FLAG, 'npcflag', NPC_FLAG_STABLE_MASTER ], // stablemaster
+ 28 => [parent::CR_FLAG, 'npcflag', NPC_FLAG_TRAINER ], // trainer
+ 29 => [parent::CR_FLAG, 'npcflag', NPC_FLAG_VENDOR ], // vendor
+ 31 => [parent::CR_FLAG, 'cuFlags', CUSTOM_HAS_SCREENSHOT ], // hasscreenshots
+ 32 => [parent::CR_FLAG, 'cuFlags', NPC_CU_INSTANCE_BOSS ], // instanceboss
+ 33 => [parent::CR_FLAG, 'cuFlags', CUSTOM_HAS_COMMENT ], // hascomments
+ 34 => [parent::CR_CALLBACK, 'cbUseModel' ], // usemodel [str]
+ 35 => [parent::CR_STRING, 'textureString' ], // useskin [str]
+ 37 => [parent::CR_NUMERIC, 'id', NUM_CAST_INT, true ], // id
+ 38 => [parent::CR_CALLBACK, 'cbRelEvent', null, null ], // relatedevent [enum]
+ 40 => [parent::CR_FLAG, 'cuFlags', CUSTOM_HAS_VIDEO ], // hasvideos
+ 41 => [parent::CR_CALLBACK, 'cbHasLocation' ], // haslocation [yn] [staff]
+ 42 => [parent::CR_CALLBACK, 'cbReputation', '>', null ], // increasesrepwith [enum]
+ 43 => [parent::CR_CALLBACK, 'cbReputation', '<', null ], // decreasesrepwith [enum]
+ 44 => [parent::CR_CALLBACK, 'cbSpecialSkinLoot', NPC_TYPEFLAG_SKIN_WITH_ENGINEERING, null ] // salvageable [yn]
+ );
+
+ protected static array $inputFields = array(
+ 'cr' => [parent::V_LIST, [[1, 3],[5, 12], 15, 16, [18, 25], [27, 29], [31, 35], 37, 38, [40, 44]], true ], // criteria ids
+ 'crs' => [parent::V_LIST, [parent::ENUM_NONE, parent::ENUM_ANY, [0, 9999]], true ], // criteria operators
+ 'crv' => [parent::V_REGEX, parent::PATTERN_CRV, true ], // criteria values - only printable chars, no delimiter
+ 'na' => [parent::V_NAME, false, false], // name / subname - only printable chars, no delimiter
+ 'ex' => [parent::V_EQUAL, 'on', false], // also match subname
+ 'ma' => [parent::V_EQUAL, 1, false], // match any / all filter
+ 'fa' => [parent::V_CALLBACK, 'cbPetFamily', true ], // pet family [list] - cat[0] == 1
+ 'minle' => [parent::V_RANGE, [0, 99], false], // min level [int]
+ 'maxle' => [parent::V_RANGE, [0, 99], false], // max level [int]
+ 'cl' => [parent::V_RANGE, [0, 4], true ], // classification [list]
+ 'ra' => [parent::V_LIST, [-1, 0, 1], false], // react alliance [int]
+ 'rh' => [parent::V_LIST, [-1, 0, 1], false] // react horde [int]
+ );
+
+ public array $extraOpts = [];
+
+ protected function createSQLForValues() : array
+ {
+ $parts = [];
+ $_v = &$this->values;
+
+ // name [str]
+ if ($_v['na'])
+ {
+ $f = [['na', ['nml.nName', 'nml.nSubname']]];
+ if ($_v['ex'] != 'on')
+ $f = [['na', 'nml.nName']];
+
+ if ($_ = $this->buildMatchLookup($f))
+ $parts[] = $_;
+ else
+ {
+ $f = [['na', 'name_loc'.Lang::getLocale()->value], ['na', 'subname_loc'.Lang::getLocale()->value]];
+ if ($_v['ex'] != 'on')
+ $f = [$f[0]];
+
+ if ($_ = $this->buildLikeLookup($f))
+ $parts[] = $_;
+ }
+ }
+
+ // pet family [list]
+ if ($_v['fa'])
+ $parts[] = ['family', $_v['fa']];
+
+ // creatureLevel min [int]
+ if ($_v['minle'])
+ $parts[] = ['minLevel', $_v['minle'], '>='];
+
+ // creatureLevel max [int]
+ if ($_v['maxle'])
+ $parts[] = ['maxLevel', $_v['maxle'], '<='];
+
+ // classification [list]
+ if ($_v['cl'])
+ $parts[] = ['rank', $_v['cl']];
+
+ // react Alliance [int]
+ if (!is_null($_v['ra']))
+ $parts[] = ['ft.A', $_v['ra']];
+
+ // react Horde [int]
+ if (!is_null($_v['rh']))
+ $parts[] = ['ft.H', $_v['rh']];
+
+ return $parts;
+ }
+
+ protected function cbPetFamily(string &$val) : bool
+ {
+ if (!$this->parentCats || $this->parentCats[0] != 1)
+ return false;
+
+ if (!Util::checkNumeric($val, NUM_CAST_INT))
+ return false;
+
+ $type = parent::V_LIST;
+ $valid = [[1, 9], 11, 12, 20, 21, [24, 27], [30, 35], [37, 39], [41, 46]];
+
+ return $this->checkInput($type, $valid, $val);
+ }
+
+ protected function cbRelEvent(int $cr, int $crs, string $crv) : ?array
+ {
+ if ($crs == parent::ENUM_ANY)
+ {
+ if ($eventIds = DB::Aowow()->selectCol('SELECT `id` FROM ::events WHERE `holidayId` <> 0'))
+ if ($cGuids = DB::World()->selectCol('SELECT DISTINCT `guid` FROM game_event_creature WHERE `eventEntry` IN %in', $eventIds))
+ return ['s.guid', $cGuids];
+
+ return [0];
+ }
+ else if ($crs == parent::ENUM_NONE)
+ {
+ if ($eventIds = DB::Aowow()->selectCol('SELECT `id` FROM ::events WHERE `holidayId` <> 0'))
+ if ($cGuids = DB::World()->selectCol('SELECT DISTINCT `guid` FROM game_event_creature WHERE `eventEntry` IN %in', $eventIds))
+ return [DB::OR, ['s.guid', $cGuids, '!'], ['s.guid', null]];
+
+ return [0];
+ }
+ else if (in_array($crs, self::$enums[$cr]))
+ {
+ if ($eventIds = DB::Aowow()->selectCol('SELECT `id` FROM ::events WHERE `holidayId` = %i', $crs))
+ if ($cGuids = DB::World()->selectCol('SELECT DISTINCT `guid` FROM `game_event_creature` WHERE `eventEntry` IN %in', $eventIds))
+ return ['s.guid', $cGuids];
+
+ return [0];
+ }
+
+ return null;
+ }
+
+ protected function cbMoneyDrop(int $cr, int $crs, string $crv) : ?array
+ {
+ if (!Util::checkNumeric($crv, NUM_CAST_INT) || !$this->int2Op($crs))
+ return null;
+
+ return [DB::AND, ['((minGold + maxGold) / 2)', $crv, $crs]];
+ }
+
+ protected function cbQuestRelation(int $cr, int $crs, string $crv, $field, $val) : ?array
+ {
+ switch ($crs)
+ {
+ case 1: // any
+ return [DB::AND, ['qse.method', $val, '&'], ['qse.questId', null, '!']];
+ case 2: // alliance
+ return [DB::AND, ['qse.method', $val, '&'], ['qse.questId', null, '!'], [['qt.reqRaceMask', ChrRace::MASK_HORDE, '&'], 0], ['qt.reqRaceMask', ChrRace::MASK_ALLIANCE, '&']];
+ case 3: // horde
+ return [DB::AND, ['qse.method', $val, '&'], ['qse.questId', null, '!'], [['qt.reqRaceMask', ChrRace::MASK_ALLIANCE, '&'], 0], ['qt.reqRaceMask', ChrRace::MASK_HORDE, '&']];
+ case 4: // both
+ return [DB::AND, ['qse.method', $val, '&'], ['qse.questId', null, '!'], [DB::OR, [DB::AND, ['qt.reqRaceMask', ChrRace::MASK_ALLIANCE, '&'], ['qt.reqRaceMask', ChrRace::MASK_HORDE, '&']], ['qt.reqRaceMask', 0]]];
+ case 5: // none
+ $this->extraOpts['ct']['h'][] = $field.' = 0';
+ return [1];
+ }
+
+ return null;
+ }
+
+ protected function cbHealthMana(int $cr, int $crs, string $crv, $minField, $maxField) : ?array
+ {
+ if (!Util::checkNumeric($crv, NUM_CAST_INT) || !$this->int2Op($crs))
+ return null;
+
+ // remap OP for this special case
+ switch ($crs)
+ {
+ case '=': // min > max is totally possible
+ $this->extraOpts['ct']['h'][] = $minField.' = '.$maxField.' AND '.$minField.' = '.$crv;
+ break;
+ case '>':
+ case '>=':
+ case '<':
+ case '<=':
+ $this->extraOpts['ct']['h'][] = 'IF('.$minField.' > '.$maxField.', '.$maxField.', '.$minField.') '.$crs.' '.$crv;
+ break;
+ }
+
+
+ return [1]; // always true, use post-filter
+ }
+
+ protected function cbSpecialSkinLoot(int $cr, int $crs, string $crv, $typeFlag) : ?array
+ {
+ if (!$this->int2Bool($crs))
+ return null;
+
+
+ if ($crs)
+ return [DB::AND, ['skinLootId', 0, '>'], ['typeFlags', $typeFlag, '&']];
+ else
+ return [DB::OR, ['skinLootId', 0], [['typeFlags', $typeFlag, '&'], 0]];
+ }
+
+ protected function cbRegularSkinLoot(int $cr, int $crs, string $crv, $typeFlag) : ?array
+ {
+ if (!$this->int2Bool($crs))
+ return null;
+
+ if ($crs)
+ return [DB::AND, ['skinLootId', 0, '>'], [['typeFlags', $typeFlag, '&'], 0]];
+ else
+ return [DB::OR, ['skinLootId', 0], ['typeFlags', $typeFlag, '&']];
+ }
+
+ protected function cbReputation(int $cr, int $crs, string $crv, $op) : ?array
+ {
+ if (!in_array($crs, self::$enums[$cr]))
+ return null;
+
+ if ($_ = DB::Aowow()->selectRow('SELECT * FROM ::factions WHERE `id` = %i', $crs))
+ $this->fiReputationCols[] = [$crs, Util::localizedString($_, 'name')];
+
+ if ($cIds = DB::World()->selectCol('SELECT `creature_id` FROM creature_onkill_reputation WHERE (`RewOnKillRepFaction1` = %i AND `RewOnKillRepValue1` '.$op.' 0) OR (`RewOnKillRepFaction2` = %i AND `RewOnKillRepValue2` '.$op.' 0)', $crs, $crs))
+ return ['id', $cIds];
+ else
+ return [0];
+ }
+
+ protected function cbFaction(int $cr, int $crs, string $crv) : ?array
+ {
+ if (!Util::checkNumeric($crs, NUM_CAST_INT))
+ return null;
+
+ if (!in_array($crs, self::$enums[$cr]))
+ return null;
+
+ $facTpls = [];
+ $facs = new FactionList(array(DB::OR, ['parentFactionId', $crs], ['id', $crs]));
+ foreach ($facs->iterate() as $__)
+ $facTpls = array_merge($facTpls, $facs->getField('templateIds'));
+
+ return $facTpls ? ['faction', $facTpls] : [0];
+ }
+
+ // input is string, so there is no prompt for an operator. But a CR_NUMERIC expects crs to not be 0
+ protected function cbUseModel(int $cr, int $crs, string $crv) : ?array
+ {
+ if (!Util::checkNumeric($crv, NUM_CAST_INT))
+ return null;
+
+ return ['modelId', $crv];
+ }
+
+ protected function cbHasLocation(int $cr, int $crs, string $crv) : ?array
+ {
+ if (!$this->int2Bool($crs))
+ return null;
+
+ return ['s.typeId', null, $crs ? '!' : null];
+ }
+}
+
+?>
diff --git a/includes/dbtypes/currency.class.php b/includes/dbtypes/currency.class.php
new file mode 100644
index 00000000..79744097
--- /dev/null
+++ b/includes/dbtypes/currency.class.php
@@ -0,0 +1,88 @@
+ [['ic']],
+ 'ic' => ['j' => ['::icons ic ON ic.`id` = c.`iconId`', true], 's' => ', ic.`name` AS "iconString"']
+ );
+
+ public function __construct(array $conditions = [], array $miscData = [])
+ {
+ parent::__construct($conditions, $miscData);
+
+ foreach ($this->iterate() as &$_curTpl)
+ $_curTpl['iconString'] = $_curTpl['iconString'] ?: DEFAULT_ICON;
+ }
+
+
+ public function getListviewData() : array
+ {
+ $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(int $addMask = 0) : array
+ {
+ $data = [];
+
+ foreach ($this->iterate() as $__)
+ {
+ // todo (low): un-hardcode icon strings
+ $icon = match ($this->id)
+ {
+ CURRENCY_HONOR_POINTS => ['pvp-currency-alliance', 'pvp-currency-horde' ],
+ CURRENCY_ARENA_POINTS => ['pvp-arenapoints-icon', 'pvp-arenapoints-icon' ],
+ default => [$this->curTpl['iconString'], $this->curTpl['iconString']]
+ };
+
+ $data[Type::CURRENCY][$this->id] = ['name' => $this->getField('name', true), 'icon' => $icon];
+ }
+
+ return $data;
+ }
+
+ public function renderTooltip() : ?string
+ {
+ if (!$this->curTpl)
+ return null;
+
+ $x = '';
+ $x .= ''.$this->getField('name', true).' ';
+
+ // cata+ (or go fill it by hand)
+ if ($_ = $this->getField('description', true))
+ $x .= ''.$_.' ';
+
+ if ($_ = $this->getField('cap'))
+ $x .= ' '.Lang::currency('cap').''.Lang::nf($_).' ';
+
+ $x .= ' | ';
+
+ return $x;
+ }
+}
+
+?>
diff --git a/includes/dbtypes/emote.class.php b/includes/dbtypes/emote.class.php
new file mode 100644
index 00000000..3510d687
--- /dev/null
+++ b/includes/dbtypes/emote.class.php
@@ -0,0 +1,65 @@
+iterate() as &$curTpl)
+ {
+ // remap for generic access
+ $curTpl['name'] = $curTpl['cmd'];
+ }
+ }
+
+ public static function getName(int $id) : ?LocString
+ {
+ if ($n = DB::Aowow()->SelectRow('SELECT `cmd` AS "name_loc0" FROM %n WHERE `id` = %i', self::$dataTable, $id))
+ return new LocString($n);
+ return null;
+ }
+
+ public function getListviewData() : array
+ {
+ $data = [];
+
+ foreach ($this->iterate() as $__)
+ {
+ $data[$this->id] = array(
+ 'id' => $this->curTpl['id'],
+ 'name' => $this->curTpl['cmd'],
+ 'preview' => Util::parseHtmlText($this->getField('meToExt', true) ?: $this->getField('meToNone', true) ?: $this->getField('extToMe', true) ?: $this->getField('extToExt', true) ?: $this->getField('extToNone', true), true)
+ );
+ }
+
+ return $data;
+ }
+
+ public function getJSGlobals(int $addMask = GLOBALINFO_ANY) : array
+ {
+ $data = [];
+
+ foreach ($this->iterate() as $__)
+ $data[Type::EMOTE][$this->id] = ['name' => $this->getField('cmd')];
+
+ return $data;
+ }
+
+ public function renderTooltip() : ?string { return null; }
+}
+
+?>
diff --git a/includes/dbtypes/enchantment.class.php b/includes/dbtypes/enchantment.class.php
new file mode 100644
index 00000000..28c170e4
--- /dev/null
+++ b/includes/dbtypes/enchantment.class.php
@@ -0,0 +1,263 @@
+ Type::ENCHANTMENT
+ 'ie' => [['is']],
+ 'is' => ['j' => ['::item_stats `is` ON `is`.`type` = 502 AND `is`.`typeId` = `ie`.`id`', true], 's' => ', `is`.*'],
+ );
+
+ public function __construct(array $conditions = [], array $miscData = [])
+ {
+ parent::__construct($conditions, $miscData);
+
+ $relSpells = [];
+
+ // post processing
+ foreach ($this->iterate() as &$curTpl)
+ {
+ $curTpl['spells'] = []; // [spellId, triggerType, charges, chanceOrPpm]
+ for ($i = 1; $i <=3; $i++)
+ {
+ if ($curTpl['object'.$i] <= 0)
+ continue;
+
+ switch ($curTpl['type'.$i]) // SPELL_TRIGGER_* just reused for wording
+ {
+ case ENCHANTMENT_TYPE_COMBAT_SPELL:
+ $proc = -$this->getField('ppmRate') ?: ($this->getField('procChance') ?: $this->getField('amount'.$i));
+ $curTpl['spells'][$i] = [$curTpl['object'.$i], SPELL_TRIGGER_HIT, $curTpl['charges'], $proc];
+ $relSpells[] = $curTpl['object'.$i];
+ break;
+ case ENCHANTMENT_TYPE_EQUIP_SPELL:
+ $curTpl['spells'][$i] = [$curTpl['object'.$i], SPELL_TRIGGER_EQUIP, $curTpl['charges'], 0];
+ $relSpells[] = $curTpl['object'.$i];
+ break;
+ case ENCHANTMENT_TYPE_USE_SPELL:
+ $curTpl['spells'][$i] = [$curTpl['object'.$i], SPELL_TRIGGER_USE, $curTpl['charges'], 0];
+ $relSpells[] = $curTpl['object'.$i];
+ break;
+ }
+ }
+ }
+
+ if ($relSpells)
+ $this->relSpells = new SpellList(array(['id', $relSpells]));
+
+ // issue with scaling stats enchantments
+ // stats are stored as NOT NULL to be usable by the search filters and such become indistinguishable from scaling enchantments that _actually_ use the value 0
+ // so we can't rely on ::item_stats and always have to calc stats
+ foreach ($this->iterate() as $ench)
+ {
+ $relSpells = [];
+ foreach ($ench['spells'] as $s)
+ if ($_ = $this->relSpells->getEntry($s[0]))
+ $relSpells[$s[0]] = $_;
+
+ $this->jsonStats[$this->id] = (new StatsContainer($relSpells))->fromEnchantment($ench);
+ }
+ }
+
+ public function getListviewData(int $addInfoMask = 0x0) : array
+ {
+ $data = [];
+
+ foreach ($this->iterate() as $__)
+ {
+ $data[$this->id] = array(
+ 'id' => $this->id,
+ 'name' => $this->getField('name', true),
+ 'spells' => []
+ );
+
+ if ($this->curTpl['skillLine'] > 0)
+ $data[$this->id]['reqskill'] = $this->curTpl['skillLine'];
+
+ if ($this->curTpl['skillLevel'] > 0)
+ $data[$this->id]['reqskillrank'] = $this->curTpl['skillLevel'];
+
+ if ($this->curTpl['requiredLevel'] > 0)
+ $data[$this->id]['reqlevel'] = $this->curTpl['requiredLevel'];
+
+ foreach ($this->curTpl['spells'] as [$spellId, $trigger, $charges, $procChance])
+ {
+ // spell is procing
+ $trgSpell = 0;
+ if ($this->relSpells && $this->relSpells->getEntry($spellId) && ($_ = $this->relSpells->canTriggerSpell()))
+ {
+ foreach ($_ as $idx)
+ {
+ if ($trgSpell = $this->relSpells->getField('effect'.$idx.'TriggerSpell'))
+ {
+ $this->triggerIds[] = $trgSpell;
+ $data[$this->id]['spells'][$trgSpell] = $charges;
+ }
+ }
+ }
+
+ // spell was not proccing
+ if (!$trgSpell)
+ $data[$this->id]['spells'][$spellId] = $charges;
+ }
+
+ if (!$data[$this->id]['spells'])
+ unset($data[$this->id]['spells']);
+
+ Util::arraySumByKey($data[$this->id], $this->jsonStats[$this->id]->toJson(includeEmpty: false));
+ }
+
+ return $data;
+ }
+
+ public function getStatGainForCurrent() : array
+ {
+ return $this->jsonStats[$this->id]->toJson(includeEmpty: true);
+ }
+
+ public function getRelSpell(int $id) : ?array
+ {
+ if ($this->relSpells)
+ return $this->relSpells->getEntry($id);
+
+ return null;
+ }
+
+ public function getJSGlobals(int $addMask = GLOBALINFO_ANY) : array
+ {
+ $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() : ?string { return null; }
+}
+
+
+class EnchantmentListFilter extends Filter
+{
+ protected string $type = 'enchantments';
+ protected static array $enums = array(
+ 3 => parent::ENUM_PROFESSION // requiresprof
+ );
+
+ protected static array $genericFilter = array(
+ 2 => [parent::CR_NUMERIC, 'id', NUM_CAST_INT, true], // id
+ 3 => [parent::CR_ENUM, 'skillLine' ], // requiresprof
+ 4 => [parent::CR_NUMERIC, 'skillLevel', NUM_CAST_INT ], // reqskillrank
+ 5 => [parent::CR_BOOLEAN, 'conditionId' ], // hascondition
+ 10 => [parent::CR_FLAG, 'cuFlags', CUSTOM_HAS_COMMENT ], // hascomments
+ 11 => [parent::CR_FLAG, 'cuFlags', CUSTOM_HAS_SCREENSHOT ], // hasscreenshots
+ 12 => [parent::CR_FLAG, 'cuFlags', CUSTOM_HAS_VIDEO ], // hasvideos
+ 20 => [parent::CR_NUMERIC, 'is.str', NUM_CAST_INT, true], // str
+ 21 => [parent::CR_NUMERIC, 'is.agi', NUM_CAST_INT, true], // agi
+ 22 => [parent::CR_NUMERIC, 'is.sta', NUM_CAST_INT, true], // sta
+ 23 => [parent::CR_NUMERIC, 'is.int', NUM_CAST_INT, true], // int
+ 24 => [parent::CR_NUMERIC, 'is.spi', NUM_CAST_INT, true], // spi
+ 25 => [parent::CR_NUMERIC, 'is.arcres', NUM_CAST_INT, true], // arcres
+ 26 => [parent::CR_NUMERIC, 'is.firres', NUM_CAST_INT, true], // firres
+ 27 => [parent::CR_NUMERIC, 'is.natres', NUM_CAST_INT, true], // natres
+ 28 => [parent::CR_NUMERIC, 'is.frores', NUM_CAST_INT, true], // frores
+ 29 => [parent::CR_NUMERIC, 'is.shares', NUM_CAST_INT, true], // shares
+ 30 => [parent::CR_NUMERIC, 'is.holres', NUM_CAST_INT, true], // holres
+ 32 => [parent::CR_NUMERIC, 'is.dps', NUM_CAST_FLOAT, true], // dps
+ 34 => [parent::CR_NUMERIC, 'is.dmg', NUM_CAST_FLOAT, true], // dmg
+ 37 => [parent::CR_NUMERIC, 'is.mleatkpwr', NUM_CAST_INT, true], // mleatkpwr
+ 38 => [parent::CR_NUMERIC, 'is.rgdatkpwr', NUM_CAST_INT, true], // rgdatkpwr
+ 39 => [parent::CR_NUMERIC, 'is.rgdhitrtng', NUM_CAST_INT, true], // rgdhitrtng
+ 40 => [parent::CR_NUMERIC, 'is.rgdcritstrkrtng', NUM_CAST_INT, true], // rgdcritstrkrtng
+ 41 => [parent::CR_NUMERIC, 'is.armor', NUM_CAST_INT, true], // armor
+ 42 => [parent::CR_NUMERIC, 'is.defrtng', NUM_CAST_INT, true], // defrtng
+ 43 => [parent::CR_NUMERIC, 'is.block', NUM_CAST_INT, true], // block
+ 44 => [parent::CR_NUMERIC, 'is.blockrtng', NUM_CAST_INT, true], // blockrtng
+ 45 => [parent::CR_NUMERIC, 'is.dodgertng', NUM_CAST_INT, true], // dodgertng
+ 46 => [parent::CR_NUMERIC, 'is.parryrtng', NUM_CAST_INT, true], // parryrtng
+ 48 => [parent::CR_NUMERIC, 'is.splhitrtng', NUM_CAST_INT, true], // splhitrtng
+ 49 => [parent::CR_NUMERIC, 'is.splcritstrkrtng', NUM_CAST_INT, true], // splcritstrkrtng
+ 50 => [parent::CR_NUMERIC, 'is.splheal', NUM_CAST_INT, true], // splheal
+ 51 => [parent::CR_NUMERIC, 'is.spldmg', NUM_CAST_INT, true], // spldmg
+ 52 => [parent::CR_NUMERIC, 'is.arcsplpwr', NUM_CAST_INT, true], // arcsplpwr
+ 53 => [parent::CR_NUMERIC, 'is.firsplpwr', NUM_CAST_INT, true], // firsplpwr
+ 54 => [parent::CR_NUMERIC, 'is.frosplpwr', NUM_CAST_INT, true], // frosplpwr
+ 55 => [parent::CR_NUMERIC, 'is.holsplpwr', NUM_CAST_INT, true], // holsplpwr
+ 56 => [parent::CR_NUMERIC, 'is.natsplpwr', NUM_CAST_INT, true], // natsplpwr
+ 57 => [parent::CR_NUMERIC, 'is.shasplpwr', NUM_CAST_INT, true], // shasplpwr
+ 60 => [parent::CR_NUMERIC, 'is.healthrgn', NUM_CAST_INT, true], // healthrgn
+ 61 => [parent::CR_NUMERIC, 'is.manargn', NUM_CAST_INT, true], // manargn
+ 77 => [parent::CR_NUMERIC, 'is.atkpwr', NUM_CAST_INT, true], // atkpwr
+ 78 => [parent::CR_NUMERIC, 'is.mlehastertng', NUM_CAST_INT, true], // mlehastertng
+ 79 => [parent::CR_NUMERIC, 'is.resirtng', NUM_CAST_INT, true], // resirtng
+ 84 => [parent::CR_NUMERIC, 'is.mlecritstrkrtng', NUM_CAST_INT, true], // mlecritstrkrtng
+ 94 => [parent::CR_NUMERIC, 'is.splpen', NUM_CAST_INT, true], // splpen
+ 95 => [parent::CR_NUMERIC, 'is.mlehitrtng', NUM_CAST_INT, true], // mlehitrtng
+ 96 => [parent::CR_NUMERIC, 'is.critstrkrtng', NUM_CAST_INT, true], // critstrkrtng
+ 97 => [parent::CR_NUMERIC, 'is.feratkpwr', NUM_CAST_INT, true], // feratkpwr
+ 101 => [parent::CR_NUMERIC, 'is.rgdhastertng', NUM_CAST_INT, true], // rgdhastertng
+ 102 => [parent::CR_NUMERIC, 'is.splhastertng', NUM_CAST_INT, true], // splhastertng
+ 103 => [parent::CR_NUMERIC, 'is.hastertng', NUM_CAST_INT, true], // hastertng
+ 114 => [parent::CR_NUMERIC, 'is.armorpenrtng', NUM_CAST_INT, true], // armorpenrtng
+ 115 => [parent::CR_NUMERIC, 'is.health', NUM_CAST_INT, true], // health
+ 116 => [parent::CR_NUMERIC, 'is.mana', NUM_CAST_INT, true], // mana
+ 117 => [parent::CR_NUMERIC, 'is.exprtng', NUM_CAST_INT, true], // exprtng
+ 119 => [parent::CR_NUMERIC, 'is.hitrtng', NUM_CAST_INT, true], // hitrtng
+ 123 => [parent::CR_NUMERIC, 'is.splpwr', NUM_CAST_INT, true] // splpwr
+ );
+
+ protected static array $inputFields = array(
+ 'cr' => [parent::V_RANGE, [2, 123], true ], // criteria ids
+ 'crs' => [parent::V_RANGE, [1, 15], true ], // criteria operators
+ 'crv' => [parent::V_REGEX, parent::PATTERN_INT, true ], // criteria values - only numerals
+ 'na' => [parent::V_NAME, false, false], // name - only printable chars, no delimiter
+ 'ma' => [parent::V_EQUAL, 1, false], // match any / all filter
+ 'ty' => [parent::V_RANGE, [1, 8], true ] // types
+ );
+
+ protected function createSQLForValues() : array
+ {
+ $parts = [];
+ $_v = &$this->values;
+
+ //string
+ if ($_v['na'])
+ if ($_ = $this->buildLikeLookup([['na', 'name_loc'.Lang::getLocale()->value]]))
+ $parts[] = $_;
+
+ // type
+ if ($_v['ty'])
+ $parts[] = [DB::OR, ['type1', $_v['ty']], ['type2', $_v['ty']], ['type3', $_v['ty']]];
+
+ return $parts;
+ }
+}
+
+?>
diff --git a/includes/types/faction.class.php b/includes/dbtypes/faction.class.php
similarity index 57%
rename from includes/types/faction.class.php
rename to includes/dbtypes/faction.class.php
index f327e95c..cf76280d 100644
--- a/includes/types/faction.class.php
+++ b/includes/dbtypes/faction.class.php
@@ -1,25 +1,27 @@
[['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($conditions = [])
+ public function __construct(array $conditions = [], array $miscData = [])
{
- parent::__construct($conditions);
+ parent::__construct($conditions, $miscData);
if ($this->error)
return;
@@ -35,13 +37,7 @@ class FactionList extends BaseType
}
}
- 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()
+ public function getListviewData() : array
{
$data = [];
@@ -70,17 +66,17 @@ class FactionList extends BaseType
return $data;
}
- public function getJSGlobals($addMask = 0)
+ public function getJSGlobals(int $addMask = 0) : array
{
$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() { }
+ public function renderTooltip() : ?string { return null; }
}
diff --git a/includes/dbtypes/gameobject.class.php b/includes/dbtypes/gameobject.class.php
new file mode 100644
index 00000000..7dcc92c1
--- /dev/null
+++ b/includes/dbtypes/gameobject.class.php
@@ -0,0 +1,253 @@
+ [['ft', 'qse']],
+ 'nml' => ['j' => ['::objects_search nml ON nml.`id` = o.`id` AND nml.`locale` = DB_LOC_I']],
+ 'ft' => ['j' => ['::factiontemplate ft ON ft.`id` = o.`faction`', true], 's' => ', ft.`factionId`, IFNULL(ft.`A`, 0) AS "A", IFNULL(ft.`H`, 0) AS "H"'],
+ 'qse' => ['j' => ['::quests_startend qse ON qse.`type` = 2 AND qse.`typeId` = o.id', true], 's' => ', IF(MIN(qse.`method`) = 1 OR MAX(qse.`method`) = 3, 1, 0) AS "startsQuests", IF(MIN(qse.`method`) = 2 OR MAX(qse.`method`) = 3, 1, 0) AS "endsQuests"', 'g' => 'o.`id`'],
+ 'qt' => ['j' => '::quests qt ON qse.`questId` = qt.`id`'],
+ 's' => ['j' => '::spawns s ON s.`type` = 2 AND s.`typeId` = o.`id`']
+ );
+
+ public function __construct(array $conditions = [], array $miscData = [])
+ {
+ parent::__construct($conditions, $miscData);
+
+ if ($this->error)
+ return;
+
+ // post processing
+ foreach ($this->iterate() as $_id => &$curTpl)
+ {
+ if (!$curTpl['name_loc'.Lang::getLocale()->value])
+ $curTpl['name_loc'.Lang::getLocale()->value] = Lang::gameObject('unnamed', [$_id]);
+
+ // unpack miscInfo
+ $curTpl['mStone'] =
+ $curTpl['capture'] =
+ $curTpl['lootStack'] = null;
+ $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 function getListviewData() : array
+ {
+ $data = [];
+ foreach ($this->iterate() as $__)
+ {
+ $data[$this->id] = array(
+ 'id' => $this->id,
+ 'name' => Lang::unescapeUISequences($this->getField('name', true), Lang::FMT_RAW),
+ 'type' => $this->getField('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) : ?string
+ {
+ if (!$this->curTpl)
+ return null;
+
+ $x = '';
+ $x .= '| '.Lang::unescapeUISequences($this->getField('name', true), Lang::FMT_HTML).' | ';
+ 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 .= '| '.Lang::game('requires', [$l]).' | ';
+
+ $x .= ' ';
+
+ return $x;
+ }
+
+ public function getJSGlobals(int $addMask = 0) : array
+ {
+ $data = [];
+
+ foreach ($this->iterate() as $__)
+ $data[Type::OBJECT][$this->id] = ['name' => Lang::unescapeUISequences($this->getField('name', true), Lang::FMT_RAW)];
+
+ return $data;
+ }
+
+ public function getSourceData(int $id = 0) : array
+ {
+ $data = [];
+
+ foreach ($this->iterate() as $__)
+ {
+ if ($id && $id != $this->id)
+ continue;
+
+ $data[$this->id] = array(
+ 'n' => $this->getField('name', true),
+ 't' => Type::OBJECT,
+ 'ti' => $this->id
+ );
+ }
+
+ return $data;
+ }
+}
+
+
+class GameObjectListFilter extends Filter
+{
+ protected string $type = 'objects';
+ protected static array $enums = array(
+ 1 => parent::ENUM_ZONE,
+ 16 => parent::ENUM_EVENT,
+ 50 => [1, 2, 3, 4, 663, 883]
+ );
+
+ protected static array $genericFilter = array(
+ 1 => [parent::CR_ENUM, 's.areaId', false, true], // foundin
+ 2 => [parent::CR_CALLBACK, 'cbQuestRelation', 'startsQuests', 0x1 ], // startsquest [side]
+ 3 => [parent::CR_CALLBACK, 'cbQuestRelation', 'endsQuests', 0x2 ], // endsquest [side]
+ 4 => [parent::CR_CALLBACK, 'cbOpenable', null, null], // openable [yn]
+ 5 => [parent::CR_NYI_PH, null, 0 ], // averagemoneycontained [op] [int] - GOs don't contain money, match against 0
+ 7 => [parent::CR_NUMERIC, 'reqSkill', NUM_CAST_INT ], // requiredskilllevel
+ 11 => [parent::CR_FLAG, 'cuFlags', CUSTOM_HAS_SCREENSHOT ], // hasscreenshots
+ 13 => [parent::CR_FLAG, 'cuFlags', CUSTOM_HAS_COMMENT ], // hascomments
+ 15 => [parent::CR_NUMERIC, 'id', NUM_CAST_INT ], // id
+ 16 => [parent::CR_CALLBACK, 'cbRelEvent', null, null], // relatedevent (ignore removed by event)
+ 18 => [parent::CR_FLAG, 'cuFlags', CUSTOM_HAS_VIDEO ], // hasvideos
+ 50 => [parent::CR_ENUM, 'spellFocusId', true, true], // spellfocus
+ );
+
+ protected static array $inputFields = array(
+ 'cr' => [parent::V_LIST, [[1, 5], 7, 11, 13, 15, 16, 18, 50], true ], // criteria ids
+ 'crs' => [parent::V_LIST, [parent::ENUM_NONE, parent::ENUM_ANY, [0, 5000]], true ], // criteria operators
+ 'crv' => [parent::V_REGEX, parent::PATTERN_INT, true ], // criteria values - only numeric input values expected
+ 'na' => [parent::V_NAME, false, false], // name - only printable chars, no delimiter
+ 'ma' => [parent::V_EQUAL, 1, false] // match any / all filter
+ );
+
+ public array $extraOpts = [];
+
+ protected function createSQLForValues() : array
+ {
+ $parts = [];
+ $_v = $this->values;
+
+ // name
+ if ($_v['na'])
+ {
+ if ($_ = $this->buildMatchLookup([['na', 'nml.nName']]))
+ $parts[] = $_;
+ else if ($_ = $this->buildLikeLookup([['na', 'name_loc'.Lang::getLocale()->value]]))
+ $parts[] = $_;
+ }
+
+ return $parts;
+ }
+
+ protected function cbOpenable(int $cr, int $crs, string $crv) : ?array
+ {
+ if ($this->int2Bool($crs))
+ return $crs ? [DB::OR, ['flags', 0x2, '&'], ['type', 3]] : [DB::AND, [['flags', 0x2, '&'], 0], ['type', 3, '!']];
+
+ return null;
+ }
+
+ protected function cbQuestRelation(int $cr, int $crs, string $crv, $field, $value) : ?array
+ {
+ switch ($crs)
+ {
+ case 1: // any
+ return [DB::AND, ['qse.method', $value, '&'], ['qse.questId', null, '!']];
+ case 2: // alliance only
+ return [DB::AND, ['qse.method', $value, '&'], ['qse.questId', null, '!'], [['qt.reqRaceMask', ChrRace::MASK_HORDE, '&'], 0], ['qt.reqRaceMask', ChrRace::MASK_ALLIANCE, '&']];
+ case 3: // horde only
+ return [DB::AND, ['qse.method', $value, '&'], ['qse.questId', null, '!'], [['qt.reqRaceMask', ChrRace::MASK_ALLIANCE, '&'], 0], ['qt.reqRaceMask', ChrRace::MASK_HORDE, '&']];
+ case 4: // both
+ return [DB::AND, ['qse.method', $value, '&'], ['qse.questId', null, '!'], [DB::OR, [DB::AND, ['qt.reqRaceMask', ChrRace::MASK_ALLIANCE, '&'], ['qt.reqRaceMask', ChrRace::MASK_HORDE, '&']], ['qt.reqRaceMask', 0]]];
+ case 5: // none todo (low): broken, if entry starts and ends quests...
+ $this->extraOpts['o']['h'][] = $field.' = 0';
+ return [1];
+ }
+
+ return null;
+ }
+
+ protected function cbRelEvent(int $cr, int $crs, string $crv) : ?array
+ {
+ if ($crs == parent::ENUM_ANY)
+ {
+ if ($eventIds = DB::Aowow()->selectCol('SELECT `id` FROM ::events WHERE `holidayId` <> 0'))
+ if ($goGuids = DB::World()->selectCol('SELECT DISTINCT `guid` FROM game_event_gameobject WHERE `eventEntry` IN %in', $eventIds))
+ return ['s.guid', $goGuids];
+
+ return [0];
+ }
+ else if ($crs == parent::ENUM_NONE)
+ {
+ if ($eventIds = DB::Aowow()->selectCol('SELECT `id` FROM ::events WHERE `holidayId` <> 0'))
+ if ($goGuids = DB::World()->selectCol('SELECT DISTINCT `guid` FROM game_event_gameobject WHERE `eventEntry` IN %in', $eventIds))
+ return [DB::OR, ['s.guid', $goGuids, '!'], ['s.guid', null]];
+
+ return [0];
+ }
+ else if (in_array($crs, self::$enums[$cr]))
+ {
+ if ($eventIds = DB::Aowow()->selectCol('SELECT `id` FROM ::events WHERE `holidayId` = %i', $crs))
+ if ($goGuids = DB::World()->selectCol('SELECT DISTINCT `guid` FROM game_event_gameobject WHERE `eventEntry` IN %in', $eventIds))
+ return ['s.guid', $goGuids];
+
+ return [0];
+ }
+
+ return null;
+ }
+}
+
+?>
diff --git a/includes/dbtypes/guide.class.php b/includes/dbtypes/guide.class.php
new file mode 100644
index 00000000..d1cc5f5c
--- /dev/null
+++ b/includes/dbtypes/guide.class.php
@@ -0,0 +1,170 @@
+ [['a', 'c', 'ar'], 'g' => 'g.`id`'],
+ 'a' => ['j' => ['::account a ON a.`id` = g.`userId`', true], 's' => ', IFNULL(a.`username`, "") AS "author"'],
+ 'c' => ['j' => ['::comments c ON c.`type` = '.Type::GUIDE.' AND c.`typeId` = g.`id` AND (c.`flags` & '.CC_FLAG_DELETED.') = 0', true], 's' => ', COUNT(c.`id`) AS "comments"'],
+ 'ar' => ['j' => ['::articles ar ON ar.`type` = 300 AND ar.`typeId` = g.`id`'], 's' => ', MAX(ar.`rev`) AS "latest"']
+ );
+
+ public function __construct(array $conditions = [], array $miscData = [])
+ {
+ parent::__construct($conditions, $miscData);
+
+ if ($this->error)
+ return;
+
+ $ratings = GuideMgr::getRatings($this->getFoundIDs());
+
+ // post processing
+ foreach ($this->iterate() as $id => &$_curTpl)
+ $_curTpl = array_merge($_curTpl, $ratings[$id]);
+ }
+
+ public static function getName(int $id) : ?LocString
+ {
+ if ($n = DB::Aowow()->SelectRow('SELECT `title` AS "name_loc0" FROM %n WHERE `id` = %i', self::$dataTable, $id))
+ return new LocString($n);
+ return null;
+ }
+
+ public function getArticle(int $rev = -1) : string
+ {
+ if ($rev < -1)
+ $rev = -1;
+
+ if (empty($this->article[$rev]))
+ {
+ $where = array(
+ [DB::OR, [[DB::AND, [['`type` = %i', Type::GUIDE], ['`typeId` = %i', $this->id]]]]]
+ );
+ if ($url = $this->getField('url'))
+ $where[0][1][] = ['`url` = %s', $url];
+ if ($rev >= 0)
+ $where[] = ['`rev`= %i', $rev];
+
+ $a = DB::Aowow()->selectRow('SELECT `article`, `rev` FROM ::articles WHERE %and ORDER BY `rev` DESC LIMIT 1', $where);
+
+ $this->article[$a['rev']] = $a['article'];
+ if ($this->article[$a['rev']])
+ {
+ Markup::parseTags($this->article[$a['rev']], $this->jsGlobals);
+ return $this->article[$a['rev']];
+ }
+ else
+ trigger_error('GuideList::getArticle - linked article is missing');
+ }
+
+ return $this->article[$rev] ?? '';
+ }
+
+ public function getListviewData(bool $addDescription = false) : array
+ {
+ $data = [];
+
+ foreach ($this->iterate() as $__)
+ {
+ $data[$this->id] = array(
+ 'id' => $this->id,
+ 'category' => $this->getField('category'),
+ 'title' => $this->getField('title'),
+ 'description' => $this->getField('description'),
+ 'sticky' => !!($this->getField('cuFlags') & CC_FLAG_STICKY),
+ 'nvotes' => $this->getField('nvotes'),
+ 'url' => '?guide=' . ($this->getField('url') ?: $this->id),
+ 'status' => $this->getField('status'),
+ 'author' => $this->getField('author'),
+ 'authorroles' => $this->getField('roles'),
+ 'rating' => $this->getField('rating'),
+ 'views' => $this->getField('views'),
+ 'comments' => $this->getField('comments'),
+ // 'patch' => $this->getField(''), // 30305 - patch is pointless, use date instead
+ 'date' => $this->getField('date'), // ok
+ 'when' => date(Util::$dateFormatInternal, $this->getField('date'))
+ );
+
+ if ($this->getField('category') == 1)
+ {
+ $data[$this->id]['classs'] = $this->getField('classId');
+ $data[$this->id]['spec'] = $this->getField('specId');
+ }
+ }
+
+ return $data;
+ }
+
+ public function userCanView() : bool
+ {
+ // is owner || is staff
+ return $this->getField('userId') == User::$id || User::isInGroup(U_GROUP_STAFF);
+ }
+
+ public function canBeViewed() : bool
+ {
+ // currently approved || has prev. approved version
+ return $this->getField('status') == GuideMgr::STATUS_APPROVED || $this->getField('rev') > 0;
+ }
+
+ public function canBeReported() : bool
+ {
+ // not own guide && is not archived
+ return $this->getField('userId') != User::$id && $this->getField('status') != GuideMgr::STATUS_ARCHIVED;
+ }
+
+ public function getJSGlobals(int $addMask = GLOBALINFO_ANY) : array
+ {
+ return $this->jsGlobals;
+ }
+
+ public function renderTooltip() : ?string
+ {
+ $specStr = '';
+
+ if ($this->getField('classId') && $this->getField('category') == 1)
+ {
+ if ($c = $this->getField('classId'))
+ {
+ $n = Lang::game('cl', $c);
+ $specStr .= ' – %s';
+
+ if (($s = $this->getField('specId')) > -1)
+ {
+ $i = Game::$specIconStrings[$c][$s];
+ $n = '';
+ $specStr .= ''.Lang::game('classSpecs', $c, $s).'';
+ }
+
+ $specStr = sprintf($specStr, $n);
+ }
+ }
+
+ $tt = ''.$this->getField('title').'';
+ $tt .= ' | '.Lang::game('guide').' | '.Lang::guide('byAuthor', [$this->getField('author')]).' |
|---|
';
+ $tt .= ' | '.Lang::guide('category', $this->getField('category')).$specStr.' | '.Lang::guide('patch').' 3.3.5 |
|---|
';
+ $tt .= ' '.$this->getField('description').' ';
+ $tt .= ' | ';
+
+ return $tt;
+ }
+}
+
+?>
diff --git a/includes/types/guild.class.php b/includes/dbtypes/guild.class.php
similarity index 50%
rename from includes/types/guild.class.php
rename to includes/dbtypes/guild.class.php
index c6136261..e31ea4cc 100644
--- a/includes/types/guild.class.php
+++ b/includes/dbtypes/guild.class.php
@@ -1,14 +1,18 @@
getGuildScores();
@@ -16,23 +20,23 @@ class GuildList extends BaseType
foreach ($this->iterate() as $__)
{
$data[$this->id] = array(
- 'name' => "$'".$this->curTpl['name']."'", // MUST be a string
+ 'name' => '$"'.str_replace ('"', '', $this->curTpl['name']).'"', // MUST be a string, omit any quotes in name
'members' => $this->curTpl['members'],
'faction' => $this->curTpl['faction'],
'achievementpoints' => $this->getField('achievementpoints'),
'gearscore' => $this->getField('gearscore'),
- 'realm' => Profiler::urlize($this->curTpl['realmName']),
+ 'realm' => Profiler::urlize($this->curTpl['realmName'], true),
'realmname' => $this->curTpl['realmName'],
- // 'battlegroup' => Profiler::urlize($this->curTpl['battlegroup']), // was renamed to subregion somewhere around cata release
+ // 'battlegroup' => Profiler::urlize($this->curTpl['battlegroup']), // was renamed to subregion somewhere around cata release
// 'battlegroupname' => $this->curTpl['battlegroup'],
'region' => Profiler::urlize($this->curTpl['region'])
);
}
- return array_values($data);
+ return $data;
}
- private function getGuildScores()
+ private function getGuildScores() : void
{
/*
Guild gear scores and achievement points are derived using a weighted average of all of the known characters in that guild.
@@ -44,18 +48,16 @@ class GuildList extends BaseType
if (!$guilds)
return;
- $stats = DB::Aowow()->select('SELECT guild AS ARRAY_KEY, id AS ARRAY_KEY2, level, gearscore, achievementpoints, IF(cuFlags & ?d, 0, 1) AS synced FROM ?_profiler_profiles WHERE guild IN (?a) ORDER BY gearscore DESC', PROFILER_CU_NEEDS_RESYNC, $guilds);
+ $stats = DB::Aowow()->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);
foreach ($this->iterate() as &$_curTpl)
{
$id = $_curTpl['id'];
if (empty($stats[$id]))
continue;
- $guildStats = array_filter($stats[$id], function ($x) { return $x['synced']; } );
- if (!$guildStats)
- continue;
+ $guildStats = $stats[$id];
- $nMaxLevel = count(array_filter($stats[$id], function ($x) { return $x['level'] >= MAX_LEVEL; } ));
+ $nMaxLevel = count(array_filter($stats[$id], fn($x) => $x['level'] >= MAX_LEVEL));
$levelMod = 1.0;
if ($nMaxLevel < 25)
@@ -78,96 +80,69 @@ class GuildList extends BaseType
}
}
- public function renderTooltip() {}
- public function getJSGlobals($addMask = 0) {}
+ public static function getName(int $id) : ?LocString { return null; }
+
+ public function renderTooltip() : ?string { return null; }
+ public function getJSGlobals(int $addMask = 0) : array { return []; }
}
class GuildListFilter extends Filter
{
- public $extraOpts = [];
- protected $genericFilter = [];
+ use TrProfilerFilter;
- // fieldId => [checkType, checkValue[, fieldIsArray]]
- protected $inputFields = array(
- 'na' => [FILTER_V_REGEX, '/[\p{C};]/ui', false], // name - only printable chars, no delimiter
- 'ma' => [FILTER_V_EQUAL, 1, false], // match any / all filter
- 'ex' => [FILTER_V_EQUAL, 'on', false], // only match exact
- 'si' => [FILTER_V_LIST, [1, 2], false], // side
- 'rg' => [FILTER_V_CALLBACK, 'cbRegionCheck', false], // region
- 'sv' => [FILTER_V_CALLBACK, 'cbServerCheck', false], // server
+ protected string $type = 'guilds';
+ protected 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
);
- protected function createSQLForCriterium(&$cr) { }
+ public array $extraOpts = [];
- protected function createSQLForValues()
+ protected function createSQLForValues() : array
{
$parts = [];
- $_v = $this->fiData['v'];
+ $_v = $this->values;
// region (rg), battlegroup (bg) and server (sv) are passed to GuildList as miscData and handled there
// name [str]
- if (!empty($_v['na']))
- if ($_ = $this->modularizeString(['g.name'], $_v['na'], !empty($_v['ex']) && $_v['ex'] == 'on'))
+ if ($_v['na'])
+ if ($_ = $this->buildLikeLookup([['na', 'g.name']], $_v['ex'] == 'on'))
$parts[] = $_;
// side [list]
- if (!empty($_v['si']))
- {
- if ($_v['si'] == 1)
- $parts[] = ['c.race', [1, 3, 4, 7, 11]];
- else if ($_v['si'] == 2)
- $parts[] = ['c.race', [2, 5, 6, 8, 10]];
- }
+ if ($_v['si'] == SIDE_ALLIANCE)
+ $parts[] = ['c.race', ChrRace::fromMask(ChrRace::MASK_ALLIANCE)];
+ else if ($_v['si'] == SIDE_HORDE)
+ $parts[] = ['c.race', ChrRace::fromMask(ChrRace::MASK_HORDE)];
return $parts;
}
-
- protected function cbRegionCheck(&$v)
- {
- if ($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 $queryBase = 'SELECT `g`.*, `g`.`guildid` AS ARRAY_KEY FROM guild g';
- protected $queryOpts = array(
+ protected string $queryBase = 'SELECT `g`.*, `g`.`guildid` AS ARRAY_KEY FROM guild g';
+ protected array $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($conditions = [], $miscData = null)
+ public function __construct(array $conditions = [], array $miscData = [])
{
// select DB by realm
if (!$this->selectRealms($miscData))
{
- trigger_error('no access to auth-db or table realmlist is empty', E_USER_WARNING);
+ trigger_error('RemoteGuildList::__construct - cannot access any realm.', E_USER_WARNING);
return;
}
@@ -177,15 +152,14 @@ class RemoteGuildList extends GuildList
return;
reset($this->dbNames); // only use when querying single realm
- $realmId = key($this->dbNames);
- $realms = Profiler::getRealms();
- $distrib = [];
+ $realms = Profiler::getRealms();
+ $distrib = [];
// post processing
foreach ($this->iterate() as $guid => &$curTpl)
{
// battlegroup
- $curTpl['battlegroup'] = CFG_BATTLEGROUP;
+ $curTpl['battlegroup'] = Cfg::get('BATTLEGROUP');
$r = explode(':', $guid)[0];
if (!empty($realms[$r]))
@@ -196,7 +170,15 @@ class RemoteGuildList extends GuildList
}
else
{
- trigger_error('character "'.$curTpl['name'].'" belongs to nonexistant realm #'.$r, E_USER_WARNING);
+ trigger_error('guild #'.$guid.' belongs to 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);
unset($this->templates[$guid]);
continue;
}
@@ -208,10 +190,14 @@ class RemoteGuildList extends GuildList
$distrib[$curTpl['realm']]++;
}
- $limit = CFG_SQL_LIMIT_DEFAULT;
+ // equalize subject distribution across realms
+ $limit = 0;
foreach ($conditions as $c)
- if (is_int($c))
- $limit = $c;
+ if (is_numeric($c))
+ $limit = max(0, (int)$c);
+
+ if (!$limit) // int:0 means unlimited, so skip early
+ return;
$total = array_sum($distrib);
foreach ($distrib as &$d)
@@ -230,29 +216,27 @@ class RemoteGuildList extends GuildList
}
}
- public function initializeLocalEntries()
+ public function initializeLocalEntries() : void
{
+ if (!$this->templates)
+ return;
+
$data = [];
foreach ($this->iterate() as $guid => $__)
{
- $data[$guid] = array(
- 'realm' => $this->getField('realm'),
- 'realmGUID' => $this->getField('guildid'),
- 'name' => $this->getField('name'),
- 'nameUrl' => Profiler::urlize($this->getField('name')),
- 'cuFlags' => PROFILER_CU_NEEDS_RESYNC
- );
+ $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;
}
// basic guild data
- foreach (Util::createSqlBatchInsert($data) as $ins)
- DB::Aowow()->query('INSERT IGNORE INTO ?_profiler_guild (?#) VALUES '.$ins, array_keys(reset($data)));
+ DB::Aowow()->qry('INSERT INTO ::profiler_guild %m ON DUPLICATE KEY UPDATE `id` = `id`', $data);
// merge back local ids
- $localIds = DB::Aowow()->selectCol(
- 'SELECT CONCAT(realm, ":", realmGUID) AS ARRAY_KEY, id FROM ?_profiler_guild WHERE realm IN (?a) AND realmGUID IN (?a)',
- array_column($data, 'realm'),
- array_column($data, 'realmGUID')
+ $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']
);
foreach ($this->iterate() as $guid => &$_curTpl)
@@ -264,17 +248,38 @@ class RemoteGuildList extends GuildList
class LocalGuildList extends GuildList
{
- protected $queryBase = 'SELECT g.*, g.id AS ARRAY_KEY FROM ?_profiler_guild g';
+ protected string $queryBase = 'SELECT g.*, g.`id` AS ARRAY_KEY FROM ::profiler_guild g';
- public function __construct($conditions = [], $miscData = null)
+ public function __construct(array $conditions = [], array $miscData = [])
{
+ $realms = Profiler::getRealms();
+
+ // graft realm selection from miscData onto conditions
+ if (isset($miscData['sv']))
+ $realms = array_filter($realms, fn($x) => Profiler::urlize($x['name']) == Profiler::urlize($miscData['sv']));
+
+ if (isset($miscData['rg']))
+ $realms = array_filter($realms, fn($x) => $x['region'] == $miscData['rg']);
+
+ if (!$realms)
+ {
+ trigger_error('LocalGuildList::__construct - cannot access any realm.', E_USER_WARNING);
+ return;
+ }
+
+ if ($conditions)
+ {
+ array_unshift($conditions, 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']]))
@@ -287,17 +292,17 @@ class LocalGuildList extends GuildList
}
// battlegroup
- $curTpl['battlegroup'] = CFG_BATTLEGROUP;
+ $curTpl['battlegroup'] = Cfg::get('BATTLEGROUP');
}
}
- public function getProfileUrl()
+ public function getProfileUrl() : string
{
$url = '?guild=';
return $url.implode('.', array(
- Profiler::urlize($this->getField('region')),
- Profiler::urlize($this->getField('realmName')),
+ $this->getField('region'),
+ Profiler::urlize($this->getField('realmName'), true),
Profiler::urlize($this->getField('name'))
));
}
diff --git a/includes/dbtypes/icon.class.php b/includes/dbtypes/icon.class.php
new file mode 100644
index 00000000..c3971a97
--- /dev/null
+++ b/includes/dbtypes/icon.class.php
@@ -0,0 +1,205 @@
+ '::items',
+ 'nSpells' => '::spell',
+ 'nAchievements' => '::achievement',
+ 'nCurrencies' => '::currencies',
+ 'nPets' => '::pet'
+ );
+
+ protected string $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 array $queryOpts = array( // 29 => Type::ICON
+ 'ic' => [['s', 'i', 'a', 'c', 'p'], 'g' => 'ic.id'],
+ 'i' => ['j' => ['::items `i` ON `i`.`iconId` = `ic`.`id`', true], 's' => ', COUNT(DISTINCT `i`.`id`) AS "nItems"'],
+ 's' => ['j' => ['::spell `s` ON `s`.`iconId` = `ic`.`id`', true], 's' => ', COUNT(DISTINCT `s`.`id`) AS "nSpells"'],
+ 'a' => ['j' => ['::achievement `a` ON `a`.`iconId` = `ic`.`id`', true], 's' => ', COUNT(DISTINCT `a`.`id`) AS "nAchievements"'],
+ 'c' => ['j' => ['::currencies `c` ON `c`.`iconId` = `ic`.`id`', true], 's' => ', COUNT(DISTINCT `c`.`id`) AS "nCurrencies"'],
+ 'p' => ['j' => ['::pet `p` ON `p`.`iconId` = `ic`.`id`', true], 's' => ', COUNT(DISTINCT `p`.`id`) AS "nPets"']
+ );
+ */
+
+ public function __construct(array $conditions = [], array $miscData = [])
+ {
+ parent::__construct($conditions, $miscData);
+
+ if (!$this->getFoundIDs())
+ return;
+
+ foreach ($this->pseudoJoin as $var => $tbl)
+ {
+ $res = DB::Aowow()->selectCol($this->pseudoQry, $tbl, $this->getFoundIDs());
+ foreach ($res as $icon => $qty)
+ $this->templates[$icon][$var] = $qty;
+ }
+ }
+
+ public static function getName(int $id) : ?LocString
+ {
+ if ($n = DB::Aowow()->selectRow('SELECT `name` AS "name_loc0" FROM %n WHERE `id` = %i', self::$dataTable, $id))
+ return new LocString($n);
+ return null;
+ }
+
+ public function getListviewData(int $addInfoMask = 0x0) : array
+ {
+ $data = [];
+
+ foreach ($this->iterate() as $__)
+ {
+ $data[$this->id] = array(
+ 'id' => $this->id,
+ 'name' => $this->getField('name_source', 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(int $addMask = GLOBALINFO_ANY) : array
+ {
+ $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() : ?string { return null; }
+}
+
+
+class IconListFilter extends Filter
+{
+ private array $iconTotals = [];
+ private array $criterion2field = array(
+ 1 => '::items', // items [num]
+ 2 => '::spell', // spells [num]
+ 3 => '::achievement', // achievements [num]
+ // 4 => '', // battlepets [num]
+ // 5 => '', // battlepetabilities [num]
+ 6 => '::currencies', // currencies [num]
+ // 7 => '', // garrisonabilities [num]
+ // 8 => '', // garrisonbuildings [num]
+ 9 => '::pet', // hunterpets [num]
+ // 10 => '', // garrisonmissionthreats [num]
+ 11 => '::classes', // classes [num]
+ 13 => '' // used [num]
+ );
+
+ protected string $type = 'icons';
+ protected static array $genericFilter = array(
+ 1 => [parent::CR_CALLBACK, 'cbUsedBy' ], // items [num]
+ 2 => [parent::CR_CALLBACK, 'cbUsedBy' ], // spells [num]
+ 3 => [parent::CR_CALLBACK, 'cbUsedBy' ], // achievements [num]
+ 6 => [parent::CR_CALLBACK, 'cbUsedBy' ], // currencies [num]
+ 9 => [parent::CR_CALLBACK, 'cbUsedBy' ], // hunterpets [num]
+ 11 => [parent::CR_CALLBACK, 'cbUsedBy' ], // classes [num]
+ 13 => [parent::CR_CALLBACK, 'cbUsedBy', true] // used [num]
+ );
+
+ protected static array $inputFields = array(
+ 'cr' => [parent::V_LIST, [1, 2, 3, 6, 9, 11, 13], true ], // criteria ids
+ 'crs' => [parent::V_RANGE, [1, 6], true ], // criteria operators
+ 'crv' => [parent::V_REGEX, parent::PATTERN_INT, true ], // criteria values - all criteria are numeric here
+ 'na' => [parent::V_NAME, false, false], // name - only printable chars, no delimiter
+ 'ma' => [parent::V_EQUAL, 1, false] // match any / all filter
+ );
+
+ public array $extraOpts = [];
+
+ protected function createSQLForValues() : array
+ {
+ $parts = [];
+ $_v = &$this->values;
+
+ //string
+ if ($_v['na'])
+ if ($_ = $this->buildLikeLookup([['na', 'name']]))
+ $parts[] = $_;
+
+ return $parts;
+ }
+
+ protected function cbUsedBy(int $cr, int $crs, string $crv, ?bool $all = false) : ?array
+ {
+ if (!Util::checkNumeric($crv, NUM_CAST_INT) || ![$filter, $negate] = $this->int2Filter($crs, $crv))
+ return null;
+
+ $total = $this->prepareIconTotals($all ? 0 : $cr);
+
+ $ids = array_filter($total, $filter);
+
+ if ($negate)
+ return $ids ? ['id', array_keys($ids), '!'] : [1];
+ else
+ return $ids ? ['id', array_keys($ids)] : ['id', array_keys($total), '!'];
+ }
+
+ private function int2Filter(mixed $op, int $y) : ?array
+ {
+ return match ($op) {
+ 1 => [fn($x) => $x > $y, false],
+ 2 => [fn($x) => $x >= $y, false],
+ 3 => [fn($x) => $x == $y, false],
+ 4 => [fn($x) => $x > $y, true],
+ 5 => [fn($x) => $x >= $y, true],
+ 6 => [fn($x) => $x == $y, true],
+ default => null
+ };
+ }
+
+ private function prepareIconTotals(int $forCr = 0) : array
+ {
+ foreach ($this->criterion2field as $cr => $tbl)
+ {
+ if (!$tbl || isset($this->iconTotals[$cr]) || ($forCr && $forCr != $cr))
+ continue;
+
+ $this->iconTotals[$cr] = DB::Aowow()->selectCol('SELECT `iconId` AS ARRAY_KEY, COUNT(*) AS "n" FROM %n GROUP BY `iconId`', $tbl);
+ }
+
+ if ($forCr)
+ return $this->iconTotals[$forCr];
+
+ if (!isset($this->iconTotals['all']))
+ {
+ $this->iconTotals['all'] = [];
+ Util::arraySumByKey($this->iconTotals['all'], ...$this->iconTotals);
+ }
+
+ return $this->iconTotals['all'];
+ }
+}
+
+?>
diff --git a/includes/dbtypes/item.class.php b/includes/dbtypes/item.class.php
new file mode 100644
index 00000000..2932d2a9
--- /dev/null
+++ b/includes/dbtypes/item.class.php
@@ -0,0 +1,2643 @@
+ Type::ITEM
+ 'i' => [['is', 'src', 'ic'], 'o' => 'i.`quality` DESC, i.`itemLevel` DESC'],
+ 'nml' => ['j' => ['::items_search nml ON nml.`id` = i.`id` AND nml.`locale` = DB_LOC_I']],
+ 'ic' => ['j' => ['::icons `ic` ON `ic`.`id` = `i`.`iconId`', true], 's' => ', ic.`name` AS "iconString"'],
+ 'is' => ['j' => ['::item_stats `is` ON `is`.`type` = 3 AND `is`.`typeId` = `i`.`id`', true], 's' => ', `is`.*'],
+ 's' => ['j' => ['::spell `s` ON `s`.`effect1CreateItemId` = `i`.`id`', true], 'g' => 'i.`id`'],
+ '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`, `moreZoneId`, `moreMask`, `src1`, `src2`, `src3`, `src4`, `src5`, `src6`, `src7`, `src8`, `src9`, `src10`, `src11`, `src12`, `src13`, `src14`, `src15`, `src16`, `src17`, `src18`, `src19`, `src20`, `src21`, `src22`, `src23`, `src24`']
+ );
+
+ public function __construct(array $conditions = [], array $miscData = [])
+ {
+ parent::__construct($conditions, $miscData);
+
+ foreach ($this->iterate() as &$_curTpl)
+ {
+ // item is scaling; overwrite other values
+ if ($_curTpl['scalingStatDistribution'] > 0 && $_curTpl['scalingStatValue'] > 0)
+ $this->initScalingStats();
+
+ // fix missing icons
+ $_curTpl['iconString'] = $_curTpl['iconString'] ?: DEFAULT_ICON;
+
+ // from json to json .. the gentle fuckups of legacy code integration
+ $this->initJsonStats();
+ $this->jsonStats[$this->id] = (new StatsContainer())->fromJson($_curTpl, true)->toJson(Stat::FLAG_ITEM /* | Stat::FLAG_SERVERSIDE */, false);
+
+ if ($miscData)
+ {
+ // readdress itemset .. is wrong for virtual sets
+ if (isset($miscData['pcsToSet']) && isset($miscData['pcsToSet'][$this->id]))
+ $this->json[$this->id]['itemset'] = $miscData['pcsToSet'][$this->id];
+
+ // additional rel attribute for listview rows
+ if (isset($miscData['extraOpts']['relEnchant']))
+ $this->relEnchant = $miscData['extraOpts']['relEnchant'];
+ }
+
+ // sources
+ for ($i = 1; $i < 25; $i++)
+ {
+ if ($_ = $_curTpl['src'.$i])
+ $this->sources[$this->id][$i][] = $_;
+
+ unset($_curTpl['src'.$i]);
+ }
+ }
+ }
+
+ // 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(?array $filter = [], ?array &$reqRating = []) : array
+ {
+ if ($this->error)
+ return [];
+
+ $idx = $this->id;
+
+ if (empty($this->vendors))
+ {
+ $itemIds = array_keys($this->templates);
+ if (!empty($filter[Type::NPC]) && is_array($filter[Type::NPC]))
+ $itemIds = array_intersect($itemIds, $filter[Type::NPC]);
+
+ $itemz = [];
+ $xCostData = [];
+ $rawEntries = DB::World()->selectAssoc(
+ 'SELECT nv.`item`, nv.`entry`, 0 AS "eventId", nv.`maxcount`, nv.`extendedCost`, nv.`incrtime`
+ FROM npc_vendor nv
+ WHERE nv.`item` IN %in
+ UNION
+ SELECT nv2.`item`, nv1.`entry`, 0 AS "eventId", nv2.`maxcount`, nv2.`extendedCost`, nv2.`incrtime`
+ FROM npc_vendor nv1
+ JOIN npc_vendor nv2 ON -nv1.`item` = nv2.`entry`
+ WHERE nv2.`item` IN %in
+ UNION
+ SELECT genv.`item`, c.`id` AS "entry", ge.`eventEntry` AS "eventId", genv.`maxcount`, genv.`extendedCost`, genv.`incrtime`
+ 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 genv.`item` IN %in',
+ $itemIds, $itemIds, $itemIds
+ );
+
+ foreach ($rawEntries as $costEntry)
+ {
+ if ($costEntry['extendedCost'])
+ $xCostData[] = $costEntry['extendedCost'];
+
+ if (!isset($itemz[$costEntry['item']][$costEntry['entry']]))
+ $itemz[$costEntry['item']][$costEntry['entry']] = [$costEntry];
+ else
+ $itemz[$costEntry['item']][$costEntry['entry']][] = $costEntry;
+ }
+
+ if ($xCostData)
+ $xCostData = DB::Aowow()->selectAssoc('SELECT *, `id` AS ARRAY_KEY FROM ::itemextendedcost WHERE `id` IN %in', $xCostData);
+
+ $cItems = [];
+ foreach ($itemz as $k => $vendors)
+ {
+ foreach ($vendors as $l => $vendor)
+ {
+ foreach ($vendor as $m => $vInfo)
+ {
+ $costs = [];
+ if (!empty($xCostData[$vInfo['extendedCost']]))
+ $costs = $xCostData[$vInfo['extendedCost']];
+
+ $data = array(
+ 'stock' => $vInfo['maxcount'] ?: -1,
+ 'event' => $vInfo['eventId'],
+ 'restock' => $vInfo['incrtime'],
+ 'reqRating' => $costs ? $costs['reqPersonalRating'] : 0,
+ 'reqBracket' => $costs ? $costs['reqArenaSlot'] : 0
+ );
+
+ // hardcode arena) & honor
+ if (!empty($costs['reqArenaPoints']))
+ {
+ $data[-103] = $costs['reqArenaPoints'];
+ $this->jsGlobals[Type::CURRENCY][CURRENCY_ARENA_POINTS] = CURRENCY_ARENA_POINTS;
+ }
+
+ if (!empty($costs['reqHonorPoints']))
+ {
+ $data[-104] = $costs['reqHonorPoints'];
+ $this->jsGlobals[Type::CURRENCY][CURRENCY_HONOR_POINTS] = CURRENCY_HONOR_POINTS;
+ }
+
+ 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)
+ {
+ $this->getEntry($k);
+ if ($_ = $this->getField('buyPrice'))
+ $data[0] = $_;
+ }
+
+ $vendor[$m] = $data;
+ }
+ $vendors[$l] = $vendor;
+ }
+
+ $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 $itemId => $vendors)
+ {
+ foreach ($vendors as $npcId => $costData)
+ {
+ foreach ($costData as $itr => $cost)
+ {
+ foreach ($cost as $k => $v)
+ {
+ if (in_array($k, $cItems))
+ {
+ $found = false;
+ foreach ($moneyItems->iterate() as $__)
+ {
+ if ($moneyItems->getField('itemId') == $k)
+ {
+ unset($cost[$k]);
+ $cost[-$moneyItems->id] = $v;
+ $found = true;
+ break;
+ }
+ }
+
+ if (!$found)
+ $this->jsGlobals[Type::ITEM][$k] = $k;
+ }
+ }
+ $costData[$itr] = $cost;
+ }
+ $vendors[$npcId] = $costData;
+ }
+ $itemz[$itemId] = $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 => $entries)
+ {
+ foreach ($entries as $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
+ // data was invalid and deleted or some source doesn't require arena rating
+ if (!isset($data[$npcId]) || ($reqRating && !$reqRating[0]))
+ continue;
+
+ // use lowest total value
+ if (!$costs['reqRating'])
+ $reqRating = [0, 2];
+ else if ($costs['reqRating'] && (!$reqRating || $reqRating[0] > $costs['reqRating']))
+ $reqRating = [$costs['reqRating'], $costs['reqBracket']];
+ }
+ }
+
+ if (empty($data))
+ unset($result[$itemId]);
+ }
+
+ // restore internal index;
+ $this->getEntry($idx);
+
+ return $result;
+ }
+
+ public function getListviewData(int $addInfoMask = 0x0, ?array $miscData = null) : array
+ {
+ /*
+ * ITEMINFO_JSON (0x01): jsonStats (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();
+ Util::arraySumByKey($data, $this->jsonStats);
+ }
+
+ $extCosts = [];
+ if ($addInfoMask & ITEMINFO_VENDOR)
+ $extCosts = $this->getExtendedCost($miscData);
+
+ $extCostOther = [];
+ 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'].Lang::unescapeUISequences($this->getField('name', true), Lang::FMT_RAW);
+ unset($data[$this->id]['quality']);
+
+ if (!empty($this->relEnchant) && $this->curTpl['randomEnchant'])
+ {
+ if (($x = array_search($this->curTpl['randomEnchant'], array_column($this->relEnchant, 'entry'))) !== false)
+ {
+ $data[$this->id]['rel'] = 'rand='.$this->relEnchant[$x]['ench'];
+ $data[$this->id]['name'] .= ' '.$this->relEnchant[$x]['name'];
+ }
+ }
+
+ if ($addInfoMask & ITEMINFO_JSON)
+ {
+ 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 vendor; search for the right one
+ if (!empty($extCosts[$this->id]))
+ {
+ $cost = reset($extCosts[$this->id]);
+ foreach ($cost as $itr => $entries)
+ {
+ $currency = [];
+ $tokens = [];
+ $costArr = [];
+
+ foreach ($entries as $k => $qty)
+ {
+ if (is_string($k))
+ continue;
+
+ if ($k > 0)
+ $tokens[] = [$k, $qty];
+ else if ($k < 0)
+ $currency[] = [-$k, $qty];
+ }
+
+ $costArr['stock'] = $entries['stock'];// display as column in lv
+ $costArr['avail'] = $entries['stock'];// display as number on icon
+ $costArr['cost'] = [empty($entries[0]) ? 0 : $entries[0]];
+ $costArr['restock'] = $entries['restock'];
+
+ if ($entries['event'])
+ if (Conditions::extendListviewRow($costArr, Conditions::SRC_NONE, $this->id, [Conditions::ACTIVE_EVENT, $entries['event']]))
+ $this->jsGlobals[Type::WORLDEVENT][$entries['event']] = $entries['event'];
+
+ if ($currency || $tokens) // fill idx:3 if required
+ $costArr['cost'][] = $currency;
+
+ if ($tokens)
+ $costArr['cost'][] = $tokens;
+
+ if (!empty($entries['reqRating']))
+ $costArr['reqarenartng'] = $entries['reqRating'];
+
+ if ($itr > 0)
+ $extCostOther[$this->id][] = $costArr;
+ else
+ $data[$this->id] = array_merge($data[$this->id], $costArr);
+ }
+ }
+
+ 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 (ChrRace::sideFromMask($_) != SIDE_BOTH)
+ $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))
+ {
+ $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;
+ }
+
+ foreach ($extCostOther as $itemId => $duplicates)
+ foreach ($duplicates as $d)
+ $data[] = array_merge($data[$itemId], $d); // we dont really use keys on data, but this may cause errors in future
+
+ /* even more complicated crap
+ modelviewer {type:X, displayid:Y, slot:z} .. not sure, when to set
+ */
+
+ return $data;
+ }
+
+ public function getJSGlobals(int $addMask = GLOBALINFO_SELF, ?array &$extra = []) : array
+ {
+ $data = $addMask & GLOBALINFO_RELATED ? $this->jsGlobals : [];
+
+ foreach ($this->iterate() as $id => $__)
+ {
+ if ($addMask & GLOBALINFO_SELF)
+ {
+ $data[Type::ITEM][$id] = array(
+ 'name' => Lang::unescapeUISequences($this->getField('name', true), Lang::FMT_RAW),
+ 'quality' => $this->curTpl['quality'],
+ 'icon' => $this->curTpl['iconString']
+ );
+
+ if ($this->curTpl['class'] == ITEM_CLASS_RECIPE)
+ $data[Type::ITEM][$id]['completion_category'] = $this->curTpl['class'];
+ else if ($this->curTpl['class'] == ITEM_CLASS_MISC && in_array($this->curTpl['subClass'], [2, 5, -7]))
+ $data[Type::ITEM][$id]['completion_category'] = $this->curTpl['class'].'-'.$this->curTpl['subClass'];
+ }
+
+ if ($addMask & GLOBALINFO_EXTRA)
+ {
+ $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(string $field, bool $localized = false, bool $silent = false, ?array $enhance = []) : mixed
+ {
+ $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(bool $interactive = false, int $subOf = 0, ?array $enhance = []) : ?string
+ {
+ if ($this->error)
+ return null;
+
+ $_name = Lang::unescapeUISequences($this->getField('name', true), Lang::FMT_HTML);
+ $_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` = %i', $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` = %i LIMIT 1', $_);
+ $x .= ' '.Util::localizedString($map, 'name').'';
+ }
+
+ // requires area
+ if ($this->curTpl['area'])
+ {
+ $area = DB::Aowow()->selectRow('SELECT * FROM ::zones WHERE `id` = %i 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` = %i', $this->curTpl['itemLimitCategory']);
+ $x .= ' '.sprintf(Lang::item($limit['isGem'] ? 'uniqueEquipped' : 'unique', 2), Util::localizedString($limit, 'name'), $limit['count']);
+ }
+
+ // 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` = %i', $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['tplDmgMin1'] + $this->curTpl['dmgMin2'];
+ $dmgmax = $this->curTpl['tplDmgMax1'] + $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['tplDmgMin1'] == $this->curTpl['tplDmgMax1'])
+ $dmg = sprintf(Lang::item('damage', 'single', $sc1 ? 1 : 0), $this->curTpl['tplDmgMin1'], $sc1 ? Lang::game('sc', $sc1) : null);
+ else
+ $dmg = sprintf(Lang::item('damage', 'range', $sc1 ? 1 : 0), $this->curTpl['tplDmgMin1'], $this->curTpl['tplDmgMax1'], $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 .= ''.Lang::item('dps', [$dps]).' ';
+
+ // 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', [$this->curTpl['tplArmor']]).' ';
+ }
+ else if ($this->curTpl['tplArmor'])
+ $x .= ''.Lang::item('armor', [$this->curTpl['tplArmor']]).' ';
+
+ // 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` = %i', $geId);
+ $x .= ''.Util::localizedString($gemEnch, 'name').' ';
+
+ // activation conditions for meta gems
+ if (!empty($gemEnch['conditionId']))
+ $x .= Game::getEnchantmentCondition($gemEnch['conditionId'], $interactive);
+ }
+
+ // 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;
+
+ $statId = Stat::getIndexFrom(Stat::IDX_ITEM_MOD, $type);
+
+ // base stat
+ switch ($statId)
+ {
+ case Stat::MANA:
+ case Stat::HEALTH:
+ case Stat::AGILITY:
+ case Stat::STRENGTH:
+ case Stat::INTELLECT:
+ case Stat::SPIRIT:
+ case Stat::STAMINA:
+ // case Stat::ARMOR: // unused by 335a client, still set in item_template
+ // case Stat::FIRE_RESISTANCE:
+ // case Stat::FROST_RESISTANCE:
+ // case Stat::HOLY_RESISTANCE:
+ // case Stat::SHADOW_RESISTANCE:
+ // case Stat::NATURE_RESISTANCE:
+ // case Stat::ARCANE_RESISTANCE:
+ $x .= ''.Lang::item('statType', $type, [ord($qty > 0 ? '+' : '-'), abs($qty)]).' ';
+ break;
+ default: // rating with % for reqLevel
+ $green[] = $this->formatRating($statId, $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` = %s', $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()->selectAssoc(
+ '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 %in',
+ $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('style="background-image: url(%s/images/wow/icons/tiny/%s.gif)"', Cfg::get('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('style="background-image: url(%s/images/wow/icons/tiny/%s.gif)"', Cfg::get('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` = %i', $_);
+ $x .= ''.Lang::item('socketBonus', [''.Util::localizedString($sbonus, 'name').'']).' ';
+ }
+
+ // durability
+ if ($dur = $this->curTpl['durability'])
+ $x .= sprintf(Lang::item('durability'), $dur, $dur).' ';
+
+ // 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::formatTime(abs($dur) * 1000, 'item', 'duration').$rt." ";
+ }
+
+ // required classes
+ $jsg = [];
+ if ($classes = Lang::getClassString($this->curTpl['requiredClass'], $jsg))
+ {
+ foreach ($jsg as $js)
+ $this->jsGlobals[Type::CHR_CLASS][$js] ??= $js;
+
+ $x .= Lang::game('classes').Lang::main('colon').$classes.' ';
+ }
+
+ // required races
+ $jsg = [];
+ if ($races = Lang::getRaceString($this->curTpl['requiredRace'], $jsg))
+ {
+ foreach ($jsg as $js)
+ $this->jsGlobals[Type::CHR_RACE][$js] ??= $js;
+
+ $x .= Lang::game('races').Lang::main('colon').$races.' ';
+ }
+
+ // required honorRank (not used anymore)
+ if ($rhr = $this->curTpl['requiredHonorRank'])
+ $x .= Lang::game('requires', [implode(' / ', 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 && $reqRating[0])
+ $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'], $arr, true))
+ $x .= ''.Lang::item('locked').' '.implode(' ', array_map(fn($x) => Lang::game('requires', [$x]), $locks)).' ';
+ else if ($this->curTpl['flags'] & ITEM_FLAG_OPENABLE)
+ $x .= ''.Lang::item('openClick').' ';
+
+ // 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];
+
+ $extra = [];
+ if ($cd >= 5000 && $this->curTpl['spellTrigger'.$j] != SPELL_TRIGGER_EQUIP)
+ {
+ $pt = DateTime::parse($cd);
+ if (count(array_filter($pt)) == 1) // simple time: use simple method
+ $extra[] = Lang::formatTime($cd, 'item', 'cooldown');
+ else // build block with generic time
+ $extra[] = Lang::item('cooldown', 0, [Lang::formatTime($cd, 'game', 'timeAbbrev', true)]);
+ }
+ if ($this->curTpl['spellTrigger'.$j] == SPELL_TRIGGER_HIT)
+ if ($ppm = $this->curTpl['spellppmRate'.$j])
+ $extra[] = Lang::spell('ppm', [$ppm]);
+
+ $itemSpellsAndTrigger[$this->curTpl['spellId'.$j]] = [$this->curTpl['spellTrigger'.$j], $extra ? ' '.implode(', ', $extra) : ''];
+ }
+ }
+
+ if ($itemSpellsAndTrigger)
+ {
+ $itemSpells = new SpellList(array(['s.id', array_keys($itemSpellsAndTrigger)]));
+ foreach ($itemSpells->iterate() as $sId => $__)
+ {
+ [$parsed, $_, $scaling] = $itemSpells->parseText('description', $_reqLvl > 1 ? $_reqLvl : MAX_LEVEL);
+ if (!$parsed && User::isInGroup(U_GROUP_EMPLOYEE))
+ $parsed = '<'.$itemSpells->getField('name', true, true).'>';
+ else if (!$parsed)
+ continue;
+
+ if ($scaling)
+ $causesScaling = true;
+
+ 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'))
+ {
+ $condition = [
+ ['refSetId', $setId],
+ // ['quality', $this->curTpl['quality']],
+ ['minLevel', $this->curTpl['itemLevel'], '<='],
+ ['maxLevel', $this->curTpl['itemLevel'], '>=']
+ ];
+
+ $itemset = new ItemsetList($condition);
+ if (!$itemset->error && $itemset->pieceToSet)
+ {
+ // handle special cases where:
+ // > itemset has items of different qualities (handled by not limiting for this in the initial query)
+ // > itemset is virtual and multiple instances have the same itemLevel but not quality (filter below)
+ foreach ($itemset->iterate() as $id => $__)
+ {
+ if ($itemset->getField('quality') == $this->curTpl['quality'])
+ {
+ $itemset->pieceToSet = array_filter($itemset->pieceToSet, function($x) use ($id) { return $id == $x; });
+ break;
+ }
+ }
+
+ $pieces = DB::Aowow()->selectAssoc(
+ 'SELECT b.`id` AS ARRAY_KEY, b.`name_loc0`, b.`name_loc2`, b.`name_loc3`, b.`name_loc4`, 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 %in
+ 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 $__)
+ {
+ [$parsed, $_, $scaling] = $boni->parseText('description', $_reqLvl > 1 ? $_reqLvl : MAX_LEVEL);
+ if ($scaling && $interactive)
+ $causesScaling = true;
+
+ $setSpells[] = array(
+ 'tooltip' => $parsed,
+ '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', SPELL_TRIGGER_USE).' '.$desc.' ';
+
+ // recipe handling (some stray Techniques have subclass == 0), place at bottom of tooltipp
+ if ($_class == ITEM_CLASS_RECIPE || $this->curTpl['bagFamily'] == 16)
+ {
+ if ($craftSpell->canCreateItem())
+ {
+ $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 ($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[] = '"'.Util::parseHtmlText($this->getField('description', true), false).'"';
+
+ // readable
+ if ($this->curTpl['pageTextId'])
+ $xMisc[] = ''.Lang::item('readClick').'';
+
+ // charges
+ for ($i = 1; $i < 6; $i++)
+ {
+ if (in_array($this->curTpl['spellTrigger'.$i], [SPELL_TRIGGER_USE, SPELL_TRIGGER_SOULSTONE, SPELL_TRIGGER_USE_NODELAY, SPELL_TRIGGER_LEARN]) && $this->curTpl['spellCharges'.$i])
+ {
+ $xMisc[] = ''.Lang::item('charges', [abs($this->curTpl['spellCharges'.$i])]).'';
+ break;
+ }
+ }
+
+ // 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))
+ {
+ $itemId = $subOf ?: $this->id;
+
+ $x .= '';
+ }
+
+ return $x;
+ }
+
+ public function getRandEnchantForItem(int $randId) : bool
+ {
+ // 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` = %i AND `ench` = %i', abs($this->getField('randomEnchant')), abs($randId)))
+ if ($_ = DB::Aowow()->selectRow('SELECT * FROM ::itemrandomenchant WHERE `id` = %i', $randId))
+ $this->enhanceR = $_;
+
+ return !empty($this->enhanceR);
+ }
+
+ // from Trinity
+ public function generateEnchSuffixFactor() : float
+ {
+ if (empty($this->randPropPoints[$this->curTpl['itemLevel']]))
+ $this->randPropPoints[$this->curTpl['itemLevel']] = DB::Aowow()->selectRow('SELECT * FROM ::itemrandomproppoints WHERE `id` = %s', $this->curTpl['itemLevel']);
+
+ $rpp = &$this->randPropPoints[$this->curTpl['itemLevel']];
+
+ if (!$rpp)
+ return 0.0;
+
+ $fieldIdx = match((int)$this->curTpl['slot'])
+ {
+ INVTYPE_HEAD,
+ INVTYPE_BODY,
+ INVTYPE_CHEST,
+ INVTYPE_LEGS,
+ INVTYPE_2HWEAPON,
+ INVTYPE_ROBE => 1,
+ INVTYPE_SHOULDERS,
+ INVTYPE_WAIST,
+ INVTYPE_FEET,
+ INVTYPE_HANDS,
+ INVTYPE_TRINKET => 2,
+ INVTYPE_NECK,
+ INVTYPE_WRISTS,
+ INVTYPE_FINGER,
+ INVTYPE_SHIELD,
+ INVTYPE_CLOAK,
+ INVTYPE_HOLDABLE => 3,
+ INVTYPE_WEAPON,
+ INVTYPE_WEAPONMAINHAND,
+ INVTYPE_WEAPONOFFHAND => 4,
+ INVTYPE_RANGED,
+ INVTYPE_THROWN,
+ INVTYPE_RANGEDRIGHT => 5,
+ default => 0 // inv types that don`t have points
+ };
+
+ if (!$fieldIdx)
+ return 0.0;
+
+ // Select rare/epic modifier
+ return match((int)$this->curTpl['quality'])
+ {
+ ITEM_QUALITY_UNCOMMON => $rpp['uncommon'.$fieldIdx] / 10000,
+ ITEM_QUALITY_RARE => $rpp['rare'.$fieldIdx] / 10000,
+ ITEM_QUALITY_EPIC => $rpp['epic'.$fieldIdx] / 10000,
+ default => 0.0 // qualities that don't have random properties
+ };
+ }
+
+ public function extendJsonStats() : void
+ {
+ $enchantments = []; // buffer Ids for lookup id => src; src>0: socketBonus; src<0: gemEnchant
+
+ foreach ($this->iterate() as $__)
+ {
+ // 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()->selectAssoc('SELECT *, `typeId` AS ARRAY_KEY FROM ::item_stats WHERE `type` = %i AND `typeId` IN %in', Type::ENCHANTMENT, array_keys($enchantments));
+
+ // 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], 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() : ?StatsContainer
+ {
+ if ($this->curTpl['class'] != ITEM_CLASS_CONSUMABLE)
+ return null;
+
+ $onUseStats = new StatsContainer();
+
+ // convert Spells
+ for ($h = 1; $h <= 5; $h++)
+ {
+ if ($this->curTpl['spellId'.$h] <= 0)
+ continue;
+
+ if ($this->curTpl['spellTrigger'.$h] != SPELL_TRIGGER_USE)
+ continue;
+
+ if ($spell = DB::Aowow()->selectRow(
+ 'SELECT `effect1Id`, `effect1TriggerSpell`, `effect1AuraId`, `effect1MiscValue`, `effect1BasePoints`, `effect1DieSides`,
+ `effect2Id`, `effect2TriggerSpell`, `effect2AuraId`, `effect2MiscValue`, `effect2BasePoints`, `effect2DieSides`,
+ `effect3Id`, `effect3TriggerSpell`, `effect3AuraId`, `effect3MiscValue`, `effect3BasePoints`, `effect3DieSides`
+ FROM ::spell
+ WHERE `id` = %i',
+ $this->curTpl['spellId'.$h]
+ ))
+ $onUseStats->fromSpell($spell);
+ }
+
+ return $onUseStats;
+ }
+
+ public function getSourceData(int $id = 0) : array
+ {
+ $data = [];
+
+ foreach ($this->iterate() as $__)
+ {
+ if ($id && $id != $this->id)
+ continue;
+
+ $data[$this->id] = array(
+ 'n' => $this->getField('name', true),
+ 't' => Type::ITEM,
+ 'ti' => $this->id,
+ 'q' => $this->curTpl['quality'],
+ // 'p' => PvP [NYI]
+ 'icon' => $this->curTpl['iconString']
+ );
+ }
+
+ return $data;
+ }
+
+ private function canTeachSpell() : bool
+ {
+ if (!in_array($this->curTpl['spellId1'], LEARN_SPELLS))
+ return false;
+
+ // needs learnable spell
+ if (!$this->curTpl['spellId2'])
+ return false;
+
+ return true;
+ }
+
+ private function getFeralAP() : float
+ {
+ // must be weapon
+ if ($this->curTpl['class'] != ITEM_CLASS_WEAPON)
+ return 0.0;
+
+ // thats fucked up..
+ if (!$this->curTpl['delay'])
+ return 0.0;
+
+ // must have enough damage
+ $dps = ($this->curTpl['tplDmgMin1'] + $this->curTpl['dmgMin2'] + $this->curTpl['tplDmgMax1'] + $this->curTpl['dmgMax2']) / (2 * $this->curTpl['delay'] / 1000);
+ if ($dps <= 54.8)
+ return 0.0;
+
+ $subClasses = [ITEM_SUBCLASS_MISC_WEAPON];
+ $weaponTypeMask = DB::Aowow()->selectCell('SELECT `weaponTypeMask` FROM ::classes WHERE `id` = %i', ChrClass::DRUID->value);
+ if ($weaponTypeMask)
+ for ($i = 0; $i < 21; $i++)
+ if ($weaponTypeMask & (1 << $i))
+ $subClasses[] = $i;
+
+ // cannot be used by druids
+ if (!in_array($this->curTpl['subClass'], $subClasses))
+ return 0.0;
+
+ return round(($dps - 54.8) * 14);
+ }
+
+ public function isRangedWeapon() : bool
+ {
+ if ($this->curTpl['class'] != ITEM_CLASS_WEAPON)
+ return false;
+
+ return in_array($this->curTpl['subClassBak'], [ITEM_SUBCLASS_BOW, ITEM_SUBCLASS_GUN, ITEM_SUBCLASS_THROWN, ITEM_SUBCLASS_CROSSBOW, ITEM_SUBCLASS_WAND]);
+ }
+
+ public function isBodyArmor() : bool
+ {
+ if ($this->curTpl['class'] != ITEM_CLASS_ARMOR)
+ return false;
+
+ return in_array($this->curTpl['subClassBak'], [ITEM_SUBCLASS_CLOTH_ARMOR, ITEM_SUBCLASS_LEATHER_ARMOR, ITEM_SUBCLASS_MAIL_ARMOR, ITEM_SUBCLASS_PLATE_ARMOR]);
+ }
+
+ public function isDisplayable() : bool
+ {
+ if (!$this->curTpl['displayId'])
+ return false;
+
+ return in_array($this->curTpl['slot'], array(
+ INVTYPE_HEAD, INVTYPE_SHOULDERS, INVTYPE_BODY, INVTYPE_CHEST, INVTYPE_WAIST, INVTYPE_LEGS, INVTYPE_FEET, INVTYPE_WRISTS,
+ INVTYPE_HANDS, INVTYPE_WEAPON, INVTYPE_SHIELD, INVTYPE_RANGED, INVTYPE_CLOAK, INVTYPE_2HWEAPON, INVTYPE_TABARD, INVTYPE_ROBE,
+ INVTYPE_WEAPONMAINHAND, INVTYPE_WEAPONOFFHAND, INVTYPE_HOLDABLE, INVTYPE_THROWN, INVTYPE_RANGEDRIGHT));
+ }
+
+ private function formatRating(int $statId, int $itemMod, int $qty, bool $interactive = false, bool &$scaling = false) : string
+ {
+ // 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 (!$statId)
+ {
+ if (User::isInGroup(U_GROUP_EMPLOYEE))
+ return Lang::item('statType', count(Lang::item('statType')) - 1, [$itemMod, $qty]);
+ else
+ return '';
+ }
+
+ // level independent Bonus
+ if (Stat::isLevelIndependent($statId))
+ return Lang::item('trigger', SPELL_TRIGGER_EQUIP).str_replace('%d', ''.$qty, Lang::item('statType', $itemMod));
+
+ // rating-Bonuses
+ $scaling = true;
+
+ if ($interactive)
+ $js = ' ('.sprintf(Util::$changeLevelString, Util::setRatingLevel($level, $statId, $qty)).')';
+ else
+ $js = ' ('.Util::setRatingLevel($level, $statId, $qty).')';
+
+ return Lang::item('trigger', SPELL_TRIGGER_EQUIP).str_replace('%d', ''.$qty.$js, Lang::item('statType', $itemMod));
+ }
+
+ private function getSSDMod(string $type) : int
+ {
+ $mask = $this->curTpl['scalingStatValue'];
+
+ $mask &= match ($type)
+ {
+ 'stats' => 0x04001F,
+ 'armor' => 0xF001E0,
+ 'dps' => 0x007E00,
+ 'spell' => 0x008000,
+ 'fap' => 0x010000, // unused
+ default => 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 %n FROM ::scalingstatvalues WHERE `id` = %i', $field, $this->ssd[$this->id]['maxLevel']) : 0;
+ }
+
+ private function initScalingStats() : void
+ {
+ $this->ssd[$this->id] = DB::Aowow()->selectRow('SELECT * FROM ::scalingstatdistribution WHERE `id` = %i', $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]['tplDmgMin1'] = floor((1 - $range) * $average);
+ $this->templates[$this->id]['tplDmgMax1'] = 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() : void
+ {
+ 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()->selectAssoc(
+ 'SELECT CAST( `entry` AS SIGNED) AS ARRAY_KEY, CAST( `ench` AS SIGNED) AS ARRAY_KEY2, `chance` FROM item_enchantment_template WHERE `entry` IN %in UNION
+ SELECT CAST(-`entry` AS SIGNED) AS ARRAY_KEY, CAST(-`ench` AS SIGNED) AS ARRAY_KEY2, `chance` FROM item_enchantment_template WHERE `entry` IN %in',
+ array_keys(array_filter($subItemIds, fn($v) => $v > 0)) ?: [0],
+ array_keys(array_filter($subItemIds, fn($v) => $v < 0)) ?: [0]
+ );
+
+ $randIds = [];
+ foreach ($subItemTpls as $tpl)
+ $randIds = array_merge($randIds, array_keys($tpl));
+
+ if (!$randIds)
+ return;
+
+ $randEnchants = DB::Aowow()->selectAssoc('SELECT *, `id` AS ARRAY_KEY FROM ::itemrandomenchant WHERE `id` IN %in', $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]));
+ foreach ($enchants->iterate() as $eId => $_)
+ {
+ $this->rndEnchIds[$eId] = array(
+ 'text' => $enchants->getField('name', true),
+ 'stats' => $enchants->getStatGainForCurrent()
+ );
+ }
+
+ 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(int $class = 0, array $spec = [], int $mhItem = 0, int $ohItem = 0) : int
+ {
+ 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 (!empty($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() : void
+ {
+ $class = $this->curTpl['class'];
+ $subclass = $this->curTpl['subClass'];
+
+ $json = array(
+ 'id' => $this->id,
+ 'quality' => ITEM_QUALITY_HEIRLOOM - $this->curTpl['quality'],
+ 'classs' => $class,
+ 'subclass' => $subclass,
+ 'subsubclass' => $this->curTpl['subSubClass'],
+ 'heroic' => ($this->curTpl['flags'] & ITEM_FLAG_HEROIC) >> 3,
+ 'side' => $this->curTpl['flagsExtra'] & 0x3 ? SIDE_BOTH - ($this->curTpl['flagsExtra'] & 0x3) : ChrRace::sideFromMask($this->curTpl['requiredRace']),
+ 'slot' => $this->curTpl['slot'],
+ 'slotbak' => $this->curTpl['slotBak'],
+ 'level' => $this->curTpl['itemLevel'],
+ 'reqlevel' => $this->curTpl['requiredLevel'],
+ 'displayid' => $this->curTpl['displayId'],
+ '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' => $class != ITEM_CLASS_ARMOR ? 0 : max(0, intVal($this->curTpl['armorDamageModifier'])),
+ 'armor' => $this->curTpl['tplArmor'],
+ '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']
+ );
+
+ $json = array_map('intval', $json);
+
+ $json['name'] = $this->getField('name', true);
+ $json['icon'] = $this->curTpl['iconString'];
+
+ if ($class == ITEM_CLASS_AMMUNITION)
+ $json['dps'] = round(($this->curTpl['tplDmgMin1'] + $this->curTpl['dmgMin2'] + $this->curTpl['tplDmgMax1'] + $this->curTpl['dmgMax2']) / 2, 2);
+ else if ($class == ITEM_CLASS_WEAPON)
+ {
+ $json['dmgtype1'] = (int)$this->curTpl['dmgType1'];
+ $json['dmgmin1'] = (int)($this->curTpl['tplDmgMin1'] + $this->curTpl['dmgMin2']);
+ $json['dmgmax1'] = (int)($this->curTpl['tplDmgMax1'] + $this->curTpl['dmgMax2']);
+ $json['speed'] = round($this->curTpl['delay'] / 1000, 2);
+ $json['dps'] = $json['speed'] ? round(($json['dmgmin1'] + $json['dmgmax1']) / (2 * $json['speed']), 1) : 0.0;
+
+ if ($this->isRangedWeapon())
+ {
+ $json['rgddmgmin'] = $json['dmgmin1'];
+ $json['rgddmgmax'] = $json['dmgmax1'];
+ $json['rgdspeed'] = $json['speed'];
+ $json['rgddps'] = $json['dps'];
+ }
+ else
+ {
+ $json['mledmgmin'] = $json['dmgmin1'];
+ $json['mledmgmax'] = $json['dmgmax1'];
+ $json['mlespeed'] = $json['speed'];
+ $json['mledps'] = $json['dps'];
+ }
+
+ if ($fap = $this->getFeralAP())
+ $json['feratkpwr'] = $fap;
+ }
+
+ if ($class == ITEM_CLASS_ARMOR || $class == ITEM_CLASS_WEAPON)
+ $json['gearscore'] = Util::getEquipmentScore($json['level'], $this->getField('quality'), $json['slot'], $json['nsockets']);
+ else if ($class == ITEM_CLASS_GEM)
+ $json['gearscore'] = Util::getGemScore($json['level'], $this->getField('quality'), $this->getField('requiredSkill') == SKILL_JEWELCRAFTING, $this->id);
+
+ // clear zero-values afterwards
+ foreach ($json as $k => $v)
+ if (!$v && !in_array($k, ['classs', 'subclass', 'quality', 'side', 'gearscore']))
+ unset($json[$k]);
+
+ $this->json[$json['id']] = $json;
+ }
+}
+
+
+class ItemListFilter extends Filter
+{
+ public const /* int */ GROUP_BY_NONE = 0;
+ public const /* int */ GROUP_BY_SLOT = 1;
+ public const /* int */ GROUP_BY_LEVEL = 2;
+ public const /* int */ GROUP_BY_SOURCE = 3;
+
+ private array $ubFilter = []; // usable-by - limit weapon/armor selection per CharClass - itemClass => available itemsubclasses
+ private string $extCostQuery = 'SELECT `item` FROM npc_vendor WHERE `extendedCost` IN %in UNION
+ SELECT `item` FROM game_event_npc_vendor WHERE `extendedCost` IN %in';
+
+ protected string $type = 'items';
+ protected static array $enums = array(
+ 16 => parent::ENUM_ZONE, // drops in zone
+ 17 => parent::ENUM_FACTION, // requiresrepwith
+ 99 => parent::ENUM_PROFESSION, // requiresprof
+ 86 => parent::ENUM_PROFESSION, // craftedprof
+ 87 => parent::ENUM_PROFESSION, // reagentforability
+ 105 => parent::ENUM_HEROICDUNGEON, // drops in nh dungeon
+ 106 => parent::ENUM_HEROICDUNGEON, // drops in hc dungeon
+ 126 => parent::ENUM_ZONE, // rewardedbyquestin
+ 147 => parent::ENUM_MULTIMODERAID, // drops in nh raid 10
+ 148 => parent::ENUM_MULTIMODERAID, // drops in nh raid 25
+ 149 => parent::ENUM_HEROICRAID, // drops in hc raid 10
+ 150 => parent::ENUM_HEROICRAID, // drops in hc raid 25
+ 152 => parent::ENUM_CLASSS, // class-specific
+ 153 => parent::ENUM_RACE, // race-specific
+ 160 => parent::ENUM_EVENT, // relatedevent
+ 169 => parent::ENUM_EVENT, // requiresevent
+ 158 => parent::ENUM_CURRENCY, // purchasablewithcurrency
+ 118 => array( // itemcurrency
+ 52027, 52030, 52026, 52029, 52025, 52028, 47242, 47557, 47558, 47559, 45632, 45633, 45634, 45635, 45636, 45637, 45638, 45639, 45640, 45641,
+ 45642, 45643, 45644, 45645, 45646, 45647, 45648, 45649, 45650, 45651, 45652, 45653, 45654, 45655, 45656, 45657, 45658, 45659, 45660, 45661,
+ 40625, 40626, 40627, 40610, 40611, 40612, 40631, 40632, 40633, 40628, 40629, 40630, 40613, 40614, 40615, 40616, 40617, 40618, 40619, 40620,
+ 40621, 40634, 40635, 40636, 40637, 40638, 40639, 40622, 40623, 40624, 34853, 34854, 34855, 34856, 34857, 34858, 34848, 34851, 34852, 31089,
+ 31091, 31090, 31092, 31094, 31093, 31097, 31095, 31096, 31098, 31100, 31099, 31101, 31103, 31102, 30236, 30237, 30238, 30239, 30240, 30241,
+ 30242, 30243, 30244, 30245, 30246, 30247, 30248, 30249, 30250, 29754, 29753, 29755, 29757, 29758, 29756, 29760, 29761, 29759, 29766, 29767,
+ 29765, 29763, 29764, 29762, 34169, 34186, 34245, 34332, 34339, 34345, 34244, 34208, 34180, 34229, 34350, 34342, 34211, 34243, 34216, 34167,
+ 34170, 34192, 34233, 34234, 34202, 34195, 34209, 34193, 34212, 34351, 34215
+ ),
+ 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
+ ),
+ 91 => array( // tool
+ 3, 14, 162, 168, 141, 2, 4, 169, 161, 15, 167, 81, 21, 165, 12, 62, 10, 101, 189, 6,
+ 63, 41, 8, 7, 190, 9, 166, 121, 5
+ ),
+ 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
+ ),
+ 128 => array( // source
+ 1 => true, // Any
+ 2 => false, // None
+ 3 => SRC_CRAFTED,
+ 4 => SRC_DROP,
+ 5 => SRC_PVP,
+ 6 => SRC_QUEST,
+ 7 => SRC_VENDOR,
+ 9 => SRC_STARTER,
+ 10 => SRC_EVENT,
+ 11 => SRC_ACHIEVEMENT,
+ 12 => SRC_FISHING
+ )
+ );
+
+ protected static array $genericFilter = array(
+ 2 => [parent::CR_CALLBACK, 'cbFieldHasVal', 'bonding', 1 ], // bindonpickup [yn]
+ 3 => [parent::CR_CALLBACK, 'cbFieldHasVal', 'bonding', 2 ], // bindonequip [yn]
+ 4 => [parent::CR_CALLBACK, 'cbFieldHasVal', 'bonding', 3 ], // bindonuse [yn]
+ 5 => [parent::CR_CALLBACK, 'cbFieldHasVal', 'bonding', [4, 5] ], // questitem [yn]
+ 6 => [parent::CR_CALLBACK, 'cbQuestRelation', null, null ], // startsquest [side]
+ 7 => [parent::CR_BOOLEAN, 'description_loc0', true ], // hasflavortext
+ 8 => [parent::CR_BOOLEAN, 'requiredDisenchantSkill' ], // disenchantable
+ 9 => [parent::CR_FLAG, 'flags', ITEM_FLAG_CONJURED ], // conjureditem
+ 10 => [parent::CR_BOOLEAN, 'lockId' ], // locked
+ 11 => [parent::CR_FLAG, 'flags', ITEM_FLAG_OPENABLE ], // openable
+ 12 => [parent::CR_BOOLEAN, 'itemset' ], // partofset
+ 13 => [parent::CR_BOOLEAN, 'randomEnchant' ], // randomlyenchanted
+ 14 => [parent::CR_BOOLEAN, 'pageTextId' ], // readable
+ 15 => [parent::CR_CALLBACK, 'cbFieldHasVal', 'maxCount', 1 ], // unique [yn]
+ 16 => [parent::CR_CALLBACK, 'cbDropsInZone', null, null ], // dropsin [zone]
+ 17 => [parent::CR_ENUM, 'requiredFaction', true, true ], // requiresrepwith
+ 18 => [parent::CR_CALLBACK, 'cbFactionQuestReward', null, null ], // rewardedbyfactionquest [side]
+ 20 => [parent::CR_NUMERIC, 'is.str', NUM_CAST_INT, true ], // str
+ 21 => [parent::CR_NUMERIC, 'is.agi', NUM_CAST_INT, true ], // agi
+ 22 => [parent::CR_NUMERIC, 'is.sta', NUM_CAST_INT, true ], // sta
+ 23 => [parent::CR_NUMERIC, 'is.int', NUM_CAST_INT, true ], // int
+ 24 => [parent::CR_NUMERIC, 'is.spi', NUM_CAST_INT, true ], // spi
+ 25 => [parent::CR_NUMERIC, 'is.arcres', NUM_CAST_INT, true ], // arcres
+ 26 => [parent::CR_NUMERIC, 'is.firres', NUM_CAST_INT, true ], // firres
+ 27 => [parent::CR_NUMERIC, 'is.natres', NUM_CAST_INT, true ], // natres
+ 28 => [parent::CR_NUMERIC, 'is.frores', NUM_CAST_INT, true ], // frores
+ 29 => [parent::CR_NUMERIC, 'is.shares', NUM_CAST_INT, true ], // shares
+ 30 => [parent::CR_NUMERIC, 'is.holres', NUM_CAST_INT, true ], // holres
+ 32 => [parent::CR_NUMERIC, 'is.dps', NUM_CAST_FLOAT, true ], // dps
+ 33 => [parent::CR_NUMERIC, 'is.dmgmin1', NUM_CAST_INT, true ], // dmgmin1
+ 34 => [parent::CR_NUMERIC, 'is.dmgmax1', NUM_CAST_INT, true ], // dmgmax1
+ 35 => [parent::CR_CALLBACK, 'cbDamageType', null, null ], // damagetype [enum]
+ 36 => [parent::CR_NUMERIC, 'is.speed', NUM_CAST_FLOAT, true ], // speed
+ 37 => [parent::CR_NUMERIC, 'is.mleatkpwr', NUM_CAST_INT, true ], // mleatkpwr
+ 38 => [parent::CR_NUMERIC, 'is.rgdatkpwr', NUM_CAST_INT, true ], // rgdatkpwr
+ 39 => [parent::CR_NUMERIC, 'is.rgdhitrtng', NUM_CAST_INT, true ], // rgdhitrtng
+ 40 => [parent::CR_NUMERIC, 'is.rgdcritstrkrtng', NUM_CAST_INT, true ], // rgdcritstrkrtng
+ 41 => [parent::CR_NUMERIC, 'is.armor', NUM_CAST_INT, true ], // armor
+ 42 => [parent::CR_NUMERIC, 'is.defrtng', NUM_CAST_INT, true ], // defrtng
+ 43 => [parent::CR_NUMERIC, 'is.block', NUM_CAST_INT, true ], // block
+ 44 => [parent::CR_NUMERIC, 'is.blockrtng', NUM_CAST_INT, true ], // blockrtng
+ 45 => [parent::CR_NUMERIC, 'is.dodgertng', NUM_CAST_INT, true ], // dodgertng
+ 46 => [parent::CR_NUMERIC, 'is.parryrtng', NUM_CAST_INT, true ], // parryrtng
+ 48 => [parent::CR_NUMERIC, 'is.splhitrtng', NUM_CAST_INT, true ], // splhitrtng
+ 49 => [parent::CR_NUMERIC, 'is.splcritstrkrtng', NUM_CAST_INT, true ], // splcritstrkrtng
+ 50 => [parent::CR_NUMERIC, 'is.splheal', NUM_CAST_INT, true ], // splheal
+ 51 => [parent::CR_NUMERIC, 'is.spldmg', NUM_CAST_INT, true ], // spldmg
+ 52 => [parent::CR_NUMERIC, 'is.arcsplpwr', NUM_CAST_INT, true ], // arcsplpwr
+ 53 => [parent::CR_NUMERIC, 'is.firsplpwr', NUM_CAST_INT, true ], // firsplpwr
+ 54 => [parent::CR_NUMERIC, 'is.frosplpwr', NUM_CAST_INT, true ], // frosplpwr
+ 55 => [parent::CR_NUMERIC, 'is.holsplpwr', NUM_CAST_INT, true ], // holsplpwr
+ 56 => [parent::CR_NUMERIC, 'is.natsplpwr', NUM_CAST_INT, true ], // natsplpwr
+ 57 => [parent::CR_NUMERIC, 'is.shasplpwr', NUM_CAST_INT, true ], // shasplpwr
+ 59 => [parent::CR_NUMERIC, 'durability', NUM_CAST_INT, true ], // dura
+ 60 => [parent::CR_NUMERIC, 'is.healthrgn', NUM_CAST_INT, true ], // healthrgn
+ 61 => [parent::CR_NUMERIC, 'is.manargn', NUM_CAST_INT, true ], // manargn
+ 62 => [parent::CR_CALLBACK, 'cbCooldown', null, null ], // cooldown [op] [int]
+ 63 => [parent::CR_NUMERIC, 'buyPrice', NUM_CAST_INT, true ], // buyprice
+ 64 => [parent::CR_NUMERIC, 'sellPrice', NUM_CAST_INT, true ], // sellprice
+ 65 => [parent::CR_CALLBACK, 'cbAvgMoneyContent', null, null ], // avgmoney [op] [int]
+ 66 => [parent::CR_ENUM, 'requiredSpell' ], // requiresprofspec
+ 68 => [parent::CR_CALLBACK, 'cbObtainedBy', SRC_DISENCHANTMENT, null ], // otdisenchanting [yn]
+ 69 => [parent::CR_CALLBACK, 'cbObtainedBy', SRC_FISHING, null ], // otfishing [yn]
+ 70 => [parent::CR_CALLBACK, 'cbObtainedBy', SRC_GATHERING, null ], // otherbgathering [yn]
+ 71 => [parent::CR_FLAG, 'cuFlags', ITEM_CU_OT_ITEMLOOT ], // otitemopening [yn]
+ 72 => [parent::CR_CALLBACK, 'cbObtainedBy', SRC_DROP, null ], // otlooting [yn]
+ 73 => [parent::CR_CALLBACK, 'cbObtainedBy', SRC_MINING, null ], // otmining [yn]
+ 74 => [parent::CR_FLAG, 'cuFlags', ITEM_CU_OT_OBJECTLOOT ], // otobjectopening [yn]
+ 75 => [parent::CR_CALLBACK, 'cbObtainedBy', SRC_PICKPOCKETING, null ], // otpickpocketing [yn]
+ 76 => [parent::CR_CALLBACK, 'cbObtainedBy', SRC_SKINNING, null ], // otskinning [yn]
+ 77 => [parent::CR_NUMERIC, 'is.atkpwr', NUM_CAST_INT, true ], // atkpwr
+ 78 => [parent::CR_NUMERIC, 'is.mlehastertng', NUM_CAST_INT, true ], // mlehastertng
+ 79 => [parent::CR_NUMERIC, 'is.resirtng', NUM_CAST_INT, true ], // resirtng
+ 80 => [parent::CR_CALLBACK, 'cbHasSockets', null, null ], // has sockets [enum]
+ 81 => [parent::CR_CALLBACK, 'cbFitsGemSlot', null, null ], // fits gem slot [enum]
+ 83 => [parent::CR_FLAG, 'flags', ITEM_FLAG_UNIQUEEQUIPPED ], // uniqueequipped
+ 84 => [parent::CR_NUMERIC, 'is.mlecritstrkrtng', NUM_CAST_INT, true ], // mlecritstrkrtng
+ 85 => [parent::CR_CALLBACK, 'cbObjectiveOfQuest', null, null ], // objectivequest [side]
+ 86 => [parent::CR_CALLBACK, 'cbCraftedByProf', null, null ], // craftedprof [enum]
+ 87 => [parent::CR_CALLBACK, 'cbReagentForAbility', null, null ], // reagentforability [enum]
+ 88 => [parent::CR_CALLBACK, 'cbObtainedBy', SRC_PROSPECTING, null ], // otprospecting [yn]
+ 89 => [parent::CR_FLAG, 'flags', ITEM_FLAG_PROSPECTABLE ], // prospectable
+ 90 => [parent::CR_CALLBACK, 'cbAvgBuyout', null, null ], // avgbuyout [op] [int]
+ 91 => [parent::CR_ENUM, 'totemCategory', false, true ], // tool
+ 92 => [parent::CR_CALLBACK, 'cbObtainedBy', SRC_VENDOR, null ], // soldbyvendor [yn]
+ 93 => [parent::CR_CALLBACK, 'cbObtainedBy', SRC_PVP, null ], // otpvp [pvp]
+ 94 => [parent::CR_NUMERIC, 'is.splpen', NUM_CAST_INT, true ], // splpen
+ 95 => [parent::CR_NUMERIC, 'is.mlehitrtng', NUM_CAST_INT, true ], // mlehitrtng
+ 96 => [parent::CR_NUMERIC, 'is.critstrkrtng', NUM_CAST_INT, true ], // critstrkrtng
+ 97 => [parent::CR_NUMERIC, 'is.feratkpwr', NUM_CAST_INT, true ], // feratkpwr
+ 98 => [parent::CR_FLAG, 'flags', ITEM_FLAG_PARTYLOOT ], // partyloot
+ 99 => [parent::CR_ENUM, 'requiredSkill' ], // requiresprof
+ 100 => [parent::CR_NUMERIC, 'is.nsockets', NUM_CAST_INT ], // nsockets
+ 101 => [parent::CR_NUMERIC, 'is.rgdhastertng', NUM_CAST_INT, true ], // rgdhastertng
+ 102 => [parent::CR_NUMERIC, 'is.splhastertng', NUM_CAST_INT, true ], // splhastertng
+ 103 => [parent::CR_NUMERIC, 'is.hastertng', NUM_CAST_INT, true ], // hastertng
+ 104 => [parent::CR_STRING, 'description', STR_LOCALIZED, 'nml.nDescription'], // flavortext
+ 105 => [parent::CR_CALLBACK, 'cbDropsInInstance', SRC_FLAG_DUNGEON_DROP, 1 ], // dropsinnormal [heroicdungeon-any]
+ 106 => [parent::CR_CALLBACK, 'cbDropsInInstance', SRC_FLAG_DUNGEON_DROP, 2 ], // dropsinheroic [heroicdungeon-any]
+ 107 => [parent::CR_STRING, '', STR_LOCALIZED, 'nml.nEffects' ], // effecttext [str]
+ 109 => [parent::CR_CALLBACK, 'cbArmorBonus', null, null ], // armorbonus [op] [int]
+ 111 => [parent::CR_NUMERIC, 'requiredSkillRank', NUM_CAST_INT, true ], // reqskillrank
+ 113 => [parent::CR_FLAG, 'cuFlags', CUSTOM_HAS_SCREENSHOT ], // hasscreenshots
+ 114 => [parent::CR_NUMERIC, 'is.armorpenrtng', NUM_CAST_INT, true ], // armorpenrtng
+ 115 => [parent::CR_NUMERIC, 'is.health', NUM_CAST_INT, true ], // health
+ 116 => [parent::CR_NUMERIC, 'is.mana', NUM_CAST_INT, true ], // mana
+ 117 => [parent::CR_NUMERIC, 'is.exprtng', NUM_CAST_INT, true ], // exprtng
+ 118 => [parent::CR_CALLBACK, 'cbPurchasableWith', null, null ], // purchasablewithitem [enum]
+ 119 => [parent::CR_NUMERIC, 'is.hitrtng', NUM_CAST_INT, true ], // hitrtng
+ 123 => [parent::CR_NUMERIC, 'is.splpwr', NUM_CAST_INT, true ], // splpwr
+ 124 => [parent::CR_CALLBACK, 'cbHasRandEnchant', null, null ], // randomenchants [str]
+ 125 => [parent::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 => [parent::CR_CALLBACK, 'cbQuestRewardIn', null, null ], // rewardedbyquestin [zone-any]
+ 128 => [parent::CR_CALLBACK, 'cbSource', null, null ], // source [enum]
+ 129 => [parent::CR_CALLBACK, 'cbSoldByNPC', null, null ], // soldbynpc [str-small]
+ 130 => [parent::CR_FLAG, 'cuFlags', CUSTOM_HAS_COMMENT ], // hascomments
+ 132 => [parent::CR_CALLBACK, 'cbGlyphType', null, null ], // glyphtype [enum]
+ 133 => [parent::CR_FLAG, 'flags', ITEM_FLAG_ACCOUNTBOUND ], // accountbound
+ 134 => [parent::CR_NUMERIC, 'is.mledps', NUM_CAST_FLOAT, true ], // mledps
+ 135 => [parent::CR_NUMERIC, 'is.mledmgmin', NUM_CAST_INT, true ], // mledmgmin
+ 136 => [parent::CR_NUMERIC, 'is.mledmgmax', NUM_CAST_INT, true ], // mledmgmax
+ 137 => [parent::CR_NUMERIC, 'is.mlespeed', NUM_CAST_FLOAT, true ], // mlespeed
+ 138 => [parent::CR_NUMERIC, 'is.rgddps', NUM_CAST_FLOAT, true ], // rgddps
+ 139 => [parent::CR_NUMERIC, 'is.rgddmgmin', NUM_CAST_INT, true ], // rgddmgmin
+ 140 => [parent::CR_NUMERIC, 'is.rgddmgmax', NUM_CAST_INT, true ], // rgddmgmax
+ 141 => [parent::CR_NUMERIC, 'is.rgdspeed', NUM_CAST_FLOAT, true ], // rgdspeed
+ 142 => [parent::CR_STRING, 'ic.name' ], // icon
+ 143 => [parent::CR_CALLBACK, 'cbObtainedBy', SRC_MILLING, null ], // otmilling [yn]
+ 144 => [parent::CR_CALLBACK, 'cbPvpPurchasable', 'reqHonorPoints', null ], // purchasablewithhonor [yn]
+ 145 => [parent::CR_CALLBACK, 'cbPvpPurchasable', 'reqArenaPoints', null ], // purchasablewitharena [yn]
+ 146 => [parent::CR_FLAG, 'flags', ITEM_FLAG_HEROIC ], // heroic
+ 147 => [parent::CR_CALLBACK, 'cbDropsInInstance', SRC_FLAG_RAID_DROP, 1, ], // dropsinnormal10 [multimoderaid-any]
+ 148 => [parent::CR_CALLBACK, 'cbDropsInInstance', SRC_FLAG_RAID_DROP, 2, ], // dropsinnormal25 [multimoderaid-any]
+ 149 => [parent::CR_CALLBACK, 'cbDropsInInstance', SRC_FLAG_RAID_DROP, 4, ], // dropsinheroic10 [heroicraid-any]
+ 150 => [parent::CR_CALLBACK, 'cbDropsInInstance', SRC_FLAG_RAID_DROP, 8, ], // dropsinheroic25 [heroicraid-any]
+ 151 => [parent::CR_NUMERIC, 'id', NUM_CAST_INT, true ], // id
+ 152 => [parent::CR_CALLBACK, 'cbClassRaceSpec', 'requiredClass' ], // classspecific [enum]
+ 153 => [parent::CR_CALLBACK, 'cbClassRaceSpec', 'requiredRace' ], // racespecific [enum]
+ 154 => [parent::CR_FLAG, 'flags', ITEM_FLAG_REFUNDABLE ], // refundable
+ 155 => [parent::CR_FLAG, 'flags', ITEM_FLAG_USABLE_ARENA ], // usableinarenas
+ 156 => [parent::CR_FLAG, 'flags', ITEM_FLAG_USABLE_SHAPED ], // usablewhenshapeshifted
+ 157 => [parent::CR_FLAG, 'flags', ITEM_FLAG_SMARTLOOT ], // smartloot
+ 158 => [parent::CR_CALLBACK, 'cbPurchasableWith', null, null ], // purchasablewithcurrency [enum]
+ 159 => [parent::CR_FLAG, 'flags', ITEM_FLAG_MILLABLE ], // millable
+ 160 => [parent::CR_NYI_PH, null, 1, ], // relatedevent [enum] like 169 .. crawl though npc_vendor and loot_templates of event-related spawns
+ 161 => [parent::CR_CALLBACK, 'cbAvailable', null, null ], // availabletoplayers [yn]
+ 162 => [parent::CR_FLAG, 'flags', ITEM_FLAG_DEPRECATED ], // deprecated
+ 163 => [parent::CR_CALLBACK, 'cbDisenchantsInto', null, null ], // disenchantsinto [disenchanting]
+ 165 => [parent::CR_NUMERIC, 'repairPrice', NUM_CAST_INT, true ], // repaircost
+ 167 => [parent::CR_FLAG, 'cuFlags', CUSTOM_HAS_VIDEO ], // hasvideos
+ 168 => [parent::CR_CALLBACK, 'cbFieldHasVal', 'spellId1', LEARN_SPELLS ], // teachesspell [yn]
+ 169 => [parent::CR_ENUM, 'e.holidayId', true, true ], // requiresevent
+ 171 => [parent::CR_CALLBACK, 'cbObtainedBy', SRC_REDEMPTION, null ], // otredemption [yn]
+ 172 => [parent::CR_CALLBACK, 'cbObtainedBy', SRC_ACHIEVEMENT, null ], // rewardedbyachievement [yn]
+ 176 => [parent::CR_STAFFFLAG, 'flags' ], // flags
+ 177 => [parent::CR_STAFFFLAG, 'flagsExtra' ], // flags2
+ );
+
+ protected static array $inputFields = array(
+ 'wt' => [parent::V_CALLBACK, 'cbWeightKeyCheck', true ], // weight keys
+ 'wtv' => [parent::V_RANGE, [1, 999], true ], // weight values
+ 'jc' => [parent::V_LIST, [1], false], // use jewelcrafter gems for weight calculation
+ 'gm' => [parent::V_LIST, [2, 3, 4], false], // gem rarity for weight calculation
+ 'cr' => [parent::V_RANGE, [1, 177], 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
+ 'upg' => [parent::V_REGEX, '/[^\d:]/ui', true ], // upgrade item ids
+ 'gb' => [parent::V_LIST, [0, 1, 2, 3], false], // search result grouping
+ 'na' => [parent::V_NAME, false, false], // name - only printable chars, no delimiter
+ 'ma' => [parent::V_EQUAL, 1, false], // match any / all filter
+ 'ub' => [parent::V_LIST, [[1, 9], 11], false], // usable by classId
+ 'qu' => [parent::V_RANGE, [0, 7], true ], // quality ids
+ 'ty' => [parent::V_CALLBACK, 'cbTypeCheck', true ], // item type - dynamic by current group
+ 'sl' => [parent::V_CALLBACK, 'cbSlotCheck', true ], // item slot - dynamic by current group
+ 'si' => [parent::V_LIST, [-SIDE_HORDE, -SIDE_ALLIANCE, SIDE_ALLIANCE, SIDE_HORDE, SIDE_BOTH], false], // side
+ 'minle' => [parent::V_RANGE, [0, 999], false], // item level min
+ 'maxle' => [parent::V_RANGE, [0, 999], false], // item level max
+ 'minrl' => [parent::V_RANGE, [0, MAX_LEVEL], false], // required level min
+ 'maxrl' => [parent::V_RANGE, [0, MAX_LEVEL], false] // required level max
+ );
+
+ public array $extraOpts = []; // score for statWeights
+ public array $wtCnd = [];
+
+ public function createConditionsForWeights() : array
+ {
+ if (empty($this->values['wt']))
+ return [];
+
+ $this->wtCnd = [];
+ $select = [];
+ $wtSum = 0;
+
+ foreach ($this->values['wt'] as $k => $v)
+ {
+ if ($str = Stat::getWeightJson($v))
+ {
+ $qty = intVal($this->values['wtv'][$k]);
+
+ $select[] = '(IFNULL(`is`.`'.$str.'`, 0) * '.$qty.')';
+ $this->wtCnd[] = ['is.'.$str, 0, '>'];
+ $wtSum += $qty;
+ }
+ }
+
+ if (count($this->wtCnd) > 1)
+ array_unshift($this->wtCnd, DB::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;
+ }
+
+ public function getConditions() : array
+ {
+ if (!$this->ubFilter)
+ {
+ $classes = DB::Aowow()->selectAssoc('SELECT `id` AS ARRAY_KEY, `weaponTypeMask` AS "0", `armorTypeMask` AS "1" FROM ::classes');
+ foreach ($classes as $cId => [$weaponTypeMask, $armorTypeMask])
+ {
+ // preselect misc subclasses
+ $this->ubFilter[$cId] = [ITEM_CLASS_WEAPON => [ITEM_SUBCLASS_MISC_WEAPON], ITEM_CLASS_ARMOR => [ITEM_SUBCLASS_MISC_ARMOR]];
+
+ for ($i = 0; $i < 21; $i++)
+ if ($weaponTypeMask & (1 << $i))
+ $this->ubFilter[$cId][ITEM_CLASS_WEAPON][] = $i;
+
+ for ($i = 0; $i < 11; $i++)
+ if ($armorTypeMask & (1 << $i))
+ $this->ubFilter[$cId][ITEM_CLASS_ARMOR][] = $i;
+ }
+ }
+
+ return parent::getConditions();
+ }
+
+ protected function createSQLForValues() : array
+ {
+ $parts = [];
+ $_v = $this->values;
+
+ // weights [list]
+ if ($_v['wt'] && $_v['wtv'])
+ {
+ // gm - gem quality (qualityId)
+ // jc - jc-gems included (bool)
+
+ if ($_ = $this->createConditionsForWeights())
+ $parts[] = $_;
+
+ foreach ($_v['wt'] as $_)
+ $this->fiExtraCols[] = $_;
+ }
+
+ // upgrade for [list]
+ if ($_v['upg'])
+ {
+ if ($this->upgrades = DB::Aowow()->selectCol('SELECT `id` AS ARRAY_KEY, `slot` FROM ::items WHERE `class` IN %in AND `id` IN %in', [ITEM_CLASS_WEAPON, ITEM_CLASS_GEM, ITEM_CLASS_ARMOR], $_v['upg']))
+ $parts[] = ['slot', $this->upgrades];
+ else
+ $_v['upg'] = null;
+ }
+
+ // name
+ if ($_v['na'])
+ {
+ if ($_ = $this->buildMatchLookup([['na', 'nml.nName']]))
+ $parts[] = $_;
+ else if ($_ = $this->buildLikeLookup([['na', 'name_loc'.Lang::getLocale()->value]]))
+ $parts[] = $_;
+ }
+
+ // usable-by (not excluded by requiredClass && armor or weapons match mask from ::classes)
+ if ($_v['ub'])
+ {
+ $parts[] = array(
+ DB::AND,
+ [DB::OR, ['requiredClass', 0], ['requiredClass', $this->list2Mask((array)$_v['ub']), '&']],
+ [
+ DB::OR,
+ ['class', [ITEM_CLASS_WEAPON, ITEM_CLASS_ARMOR], '!'],
+ [DB::AND, ['class', ITEM_CLASS_WEAPON], ['subclassbak', $this->ubFilter[$_v['ub']][ITEM_CLASS_WEAPON]]],
+ [DB::AND, ['class', ITEM_CLASS_ARMOR], ['subclassbak', $this->ubFilter[$_v['ub']][ITEM_CLASS_ARMOR]]]
+ ]
+ );
+ }
+
+ // quality [list]
+ if ($_v['qu'])
+ $parts[] = ['quality', $_v['qu']];
+
+ // type [list]
+ if ($_v['ty'])
+ $parts[] = ['subclass', $_v['ty']];
+
+ // slot [list]
+ if ($_v['sl'])
+ $parts[] = ['slot', $_v['sl']];
+
+ // side
+ if ($_v['si'])
+ {
+ $parts[] = match ($_v['si'])
+ {
+ SIDE_BOTH => [DB::OR, [['flagsExtra', 0x3, '&'], [0, 3]], ['requiredRace', 0]],
+ -SIDE_HORDE => [DB::OR, [['flagsExtra', 0x3, '&'], 1], ['requiredRace', ChrRace::MASK_HORDE, '&']],
+ -SIDE_ALLIANCE => [DB::OR, [['flagsExtra', 0x3, '&'], 2], ['requiredRace', ChrRace::MASK_ALLIANCE, '&']],
+ SIDE_HORDE => [DB::AND, [['flagsExtra', 0x3, '&'], [0, 1]], [DB::OR, ['requiredRace', 0], ['requiredRace', ChrRace::MASK_HORDE, '&']]],
+ SIDE_ALLIANCE => [DB::AND, [['flagsExtra', 0x3, '&'], [0, 2]], [DB::OR, ['requiredRace', 0], ['requiredRace', ChrRace::MASK_ALLIANCE, '&']]],
+ };
+ }
+
+ // itemLevel min
+ if ($_v['minle'])
+ $parts[] = ['itemLevel', $_v['minle'], '>='];
+
+ // itemLevel max
+ if ($_v['maxle'])
+ $parts[] = ['itemLevel', $_v['maxle'], '<='];
+
+ // reqLevel min
+ if ($_v['minrl'])
+ $parts[] = ['requiredLevel', $_v['minrl'], '>='];
+
+ // reqLevel max
+ if ($_v['maxrl'])
+ $parts[] = ['requiredLevel', $_v['maxrl'], '<='];
+
+ return $parts;
+ }
+
+ protected function cbFactionQuestReward(int $cr, int $crs, string $crv) : ?array
+ {
+ return match ($crs)
+ {
+ 1 => ['src.src4', null, '!'], // Yes
+ 2 => ['src.src4', SIDE_ALLIANCE], // Alliance
+ 3 => ['src.src4', SIDE_HORDE], // Horde
+ 4 => ['src.src4', SIDE_BOTH], // Both
+ 5 => ['src.src4', null], // No
+ default => null
+ };
+ }
+
+ protected function cbAvailable(int $cr, int $crs, string $crv) : ?array
+ {
+ if ($this->int2Bool($crs))
+ return [['cuFlags', CUSTOM_UNAVAILABLE, '&'], 0, $crs ? null : '!'];
+
+ return null;
+ }
+
+ protected function cbHasSockets(int $cr, int $crs, string $crv) : ?array
+ {
+ return match ($crs)
+ {
+ // Meta, Red, Yellow, Blue
+ 1, 2, 3, 4 => [DB::OR, ['socketColor1', 1 << ($crs - 1)], ['socketColor2', 1 << ($crs - 1)], ['socketColor3', 1 << ($crs - 1)]],
+ 5 => ['is.nsockets', 0, '!'], // Yes
+ 6 => ['is.nsockets', 0], // No
+ default => null
+ };
+ }
+
+ protected function cbFitsGemSlot(int $cr, int $crs, string $crv) : ?array
+ {
+ return match ($crs)
+ {
+ // Meta, Red, Yellow, Blue
+ 1, 2, 3, 4 => [DB::AND, ['gemEnchantmentId', 0, '!'], ['gemColorMask', 1 << ($crs - 1), '&']],
+ 5 => ['gemEnchantmentId', 0, '!'], // Yes
+ 6 => ['gemEnchantmentId', 0], // No
+ default => null
+ };
+ }
+
+ protected function cbGlyphType(int $cr, int $crs, string $crv) : ?array
+ {
+ return match ($crs)
+ {
+ // major, minor
+ 1, 2 => [DB::AND, ['class', ITEM_CLASS_GLYPH], ['subSubClass', $crs]],
+ default => null
+ };
+ }
+
+ protected function cbHasRandEnchant(int $cr, int $crs, string $crv) : ?array
+ {
+ $n = preg_replace(parent::PATTERN_NAME, '', $crv);
+ if (!$this->tokenizeString($cr, $n))
+ return null;
+
+ $where = [];
+ foreach ($this->inTokens[$cr] ?? [] as $tok)
+ $where[] = ['name_loc%i LIKE %~like~', Lang::getLocale()->value, $tok];
+ foreach ($this->exTokens[$cr] ?? [] as $tok)
+ $where[] = ['name_loc%i NOT LIKE %~like~', Lang::getLocale()->value, $tok];
+
+ $randIds = DB::Aowow()->selectAssoc('SELECT `id` AS ARRAY_KEY, ABS(`id`) AS `id`, name_loc%i, `name_loc0` FROM ::itemrandomenchant WHERE %and', Lang::getLocale()->value, $where);
+ $tplIds = $randIds ? DB::World()->selectAssoc('SELECT `entry`, `ench` FROM item_enchantment_template WHERE `ench` IN %in', array_column($randIds, 'id')) : [];
+ foreach ($tplIds as &$set)
+ {
+ $z = array_column($randIds, 'id');
+ $x = array_search($set['ench'], $z);
+ if (isset($randIds[-$z[$x]]))
+ {
+ $set['entry'] *= -1;
+ $set['ench'] *= -1;
+ }
+
+ $set['name'] = Util::localizedString($randIds[$set['ench']], 'name', true);
+ }
+
+ // only enhance search results if enchantment by name is unique (implies only one enchantment per item is available)
+ if (count(array_unique(array_column($randIds, 'name_loc0'))) == 1)
+ $this->extraOpts['relEnchant'] = $tplIds;
+
+ if ($tplIds)
+ return ['randomEnchant', array_column($tplIds, 'entry')];
+ else
+ return [0]; // no results aren't really input errors
+ }
+
+ protected function cbReqArenaRating(int $cr, int $crs, string $crv) : ?array
+ {
+ if (!Util::checkNumeric($crv, NUM_CAST_INT) || !$this->int2Op($crs))
+ return null;
+
+ $this->fiExtraCols[] = $cr;
+
+ $items = [0];
+ if ($costs = DB::Aowow()->selectCol('SELECT `id` FROM ::itemextendedcost WHERE `reqPersonalrating` %SQL %i', $crs, $crv))
+ $items = DB::World()->selectCol($this->extCostQuery, $costs, $costs);
+
+ return ['id', $items];
+ }
+
+ protected function cbClassRaceSpec(int $cr, int $crs, string $crv, string $field) : ?array
+ {
+ if (!isset(self::$enums[$cr][$crs]))
+ return null;
+
+ $_ = self::$enums[$cr][$crs];
+ if (is_bool($_))
+ return $_ ? [$field, 0, '>'] : [$field, 0];
+ else if (is_int($_))
+ return [$field, 1 << ($_ - 1), '&'];
+
+ return null;
+ }
+
+ protected function cbDamageType(int $cr, int $crs, string $crv) : ?array
+ {
+ if (!$this->checkInput(parent::V_RANGE, [SPELL_SCHOOL_NORMAL, SPELL_SCHOOL_ARCANE], $crs))
+ return null;
+
+ return [DB::OR, ['dmgType1', $crs], ['dmgType2', $crs]];
+ }
+
+ protected function cbArmorBonus(int $cr, int $crs, string $crv) : ?array
+ {
+ if (!Util::checkNumeric($crv, NUM_CAST_FLOAT) || !$this->int2Op($crs))
+ return null;
+
+ $this->fiExtraCols[] = $cr;
+ return [DB::AND, ['armordamagemodifier', $crv, $crs], ['class', ITEM_CLASS_ARMOR]];
+ }
+
+ protected function cbCraftedByProf(int $cr, int $crs, string $crv) : ?array
+ {
+ if (!isset(self::$enums[$cr][$crs]))
+ return null;
+
+ $_ = self::$enums[$cr][$crs];
+ if (is_bool($_))
+ return ['src.src1', null, $_ ? '!' : null];
+ else if (is_int($_))
+ return ['s.skillLine1', $_];
+
+ return null;
+ }
+
+ protected function cbQuestRewardIn(int $cr, int $crs, string $crv) : ?array
+ {
+ if (in_array($crs, self::$enums[$cr]))
+ return [DB::AND, ['src.src4', null, '!'], ['src.moreZoneId', $crs]];
+ else if ($crs == parent::ENUM_ANY)
+ return ['src.src4', null, '!']; // well, this seems a bit redundant..
+
+ return null;
+ }
+
+ protected function cbDropsInZone(int $cr, int $crs, string $crv) : ?array
+ {
+ if (in_array($crs, self::$enums[$cr]))
+ return [DB::AND, ['src.src2', null, '!'], ['src.moreZoneId', $crs]];
+ else if ($crs == parent::ENUM_ANY)
+ return ['src.src2', null, '!']; // well, this seems a bit redundant..
+
+ return null;
+ }
+
+ protected function cbDropsInInstance(int $cr, int $crs, string $crv, int $moreFlag, int $modeBit) : ?array
+ {
+ if (in_array($crs, self::$enums[$cr]))
+ return [DB::AND, ['src.src2', $modeBit, '&'], ['src.moreMask', $moreFlag, '&'], ['src.moreZoneId', $crs]];
+ else if ($crs == parent::ENUM_ANY)
+ return [DB::AND, ['src.src2', $modeBit, '&'], ['src.moreMask', $moreFlag, '&']];
+
+ return null;
+ }
+
+ protected function cbPurchasableWith(int $cr, int $crs, string $crv) : ?array
+ {
+ if (in_array($crs, self::$enums[$cr]))
+ $_ = (array)$crs;
+ else if ($crs == parent::ENUM_ANY)
+ $_ = self::$enums[$cr];
+ else
+ return null;
+
+ $costs = DB::Aowow()->selectCol(
+ 'SELECT `id` FROM ::itemextendedcost WHERE `reqItemId1` IN %in OR `reqItemId2` IN %in OR `reqItemId3` IN %in OR `reqItemId4` IN %in OR `reqItemId5` IN %in',
+ $_, $_, $_, $_, $_
+ );
+ if ($items = DB::World()->selectCol($this->extCostQuery, $costs, $costs))
+ return ['id', $items];
+
+ return null;
+ }
+
+ protected function cbSoldByNPC(int $cr, int $crs, string $crv) : ?array
+ {
+ if (!Util::checkNumeric($crv, NUM_CAST_INT))
+ return null;
+
+ if ($iIds = DB::World()->selectCol('SELECT `item` FROM npc_vendor WHERE `entry` = %i UNION SELECT `item` FROM game_event_npc_vendor v JOIN creature c ON c.`guid` = v.`guid` WHERE c.`id` = %i', $crv, $crv))
+ return ['i.id', $iIds];
+ else
+ return [0];
+ }
+
+ protected function cbAvgBuyout(int $cr, int $crs, string $crv) : ?array
+ {
+ if (!Util::checkNumeric($crv, NUM_CAST_INT) || !$this->int2Op($crs))
+ return null;
+
+ foreach (Profiler::getRealms() as $rId => $__)
+ {
+ // todo: do something sensible..
+ // // todo (med): get the avgbuyout into the listview
+ // if ($_ = DB::Characters()->selectAssoc('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 buyout '.$crs.' %f', $c[1]))
+ // return ['i.id', array_keys($_)];
+ // else
+ // return [0];
+ return [1];
+ }
+
+ return [0];
+ }
+
+ protected function cbAvgMoneyContent(int $cr, int $crs, string $crv) : ?array
+ {
+ if (!Util::checkNumeric($crv, NUM_CAST_INT) || !$this->int2Op($crs))
+ return null;
+
+ $this->fiExtraCols[] = $cr;
+ return [DB::AND, ['flags', ITEM_FLAG_OPENABLE, '&'], ['((minMoneyLoot + maxMoneyLoot) / 2)', $crv, $crs]];
+ }
+
+ protected function cbCooldown(int $cr, int $crs, string $crv) : ?array
+ {
+ if (!Util::checkNumeric($crv, NUM_CAST_INT) || !$this->int2Op($crs))
+ return null;
+
+ $crv *= 1000; // field supplied in milliseconds
+
+ $this->fiExtraCols[] = $cr;
+ $this->extraOpts['is']['s'][] = ', GREATEST(`spellCooldown1`, `spellCooldown2`, `spellCooldown3`, `spellCooldown4`, `spellCooldown5`) AS "cooldown"';
+
+ return [
+ DB::OR,
+ [DB::AND, ['spellTrigger1', SPELL_TRIGGER_USE], ['spellId1', 0, '!'], ['spellCooldown1', 0, '>'], ['spellCooldown1', $crv, $crs]],
+ [DB::AND, ['spellTrigger2', SPELL_TRIGGER_USE], ['spellId2', 0, '!'], ['spellCooldown2', 0, '>'], ['spellCooldown2', $crv, $crs]],
+ [DB::AND, ['spellTrigger3', SPELL_TRIGGER_USE], ['spellId3', 0, '!'], ['spellCooldown3', 0, '>'], ['spellCooldown3', $crv, $crs]],
+ [DB::AND, ['spellTrigger4', SPELL_TRIGGER_USE], ['spellId4', 0, '!'], ['spellCooldown4', 0, '>'], ['spellCooldown4', $crv, $crs]],
+ [DB::AND, ['spellTrigger5', SPELL_TRIGGER_USE], ['spellId5', 0, '!'], ['spellCooldown5', 0, '>'], ['spellCooldown5', $crv, $crs]],
+ ];
+ }
+
+ protected function cbQuestRelation(int $cr, int $crs, string $crv) : ?array
+ {
+ return match ($crs)
+ {
+ // any
+ 1 => ['startQuest', 0, '>'],
+ // exclude horde only
+ 2 => [DB::AND, ['startQuest', 0, '>'], [['flagsExtra', 0x3, '&'], SIDE_HORDE]],
+ // exclude alliance only
+ 3 => [DB::AND, ['startQuest', 0, '>'], [['flagsExtra', 0x3, '&'], SIDE_ALLIANCE]],
+ // both
+ 4 => [DB::AND, ['startQuest', 0, '>'], [['flagsExtra', 0x3, '&'], 0]],
+ // none
+ 5 => ['startQuest', 0],
+ default => null
+ };
+ }
+
+ protected function cbFieldHasVal(int $cr, int $crs, string $crv, string $field, mixed $val) : ?array
+ {
+ if ($this->int2Bool($crs))
+ return [$field, $val, $crs ? null : '!'];
+
+ return null;
+ }
+
+ protected function cbObtainedBy(int $cr, int $crs, string $crv, string $field) : ?array
+ {
+ if ($this->int2Bool($crs))
+ return ['src.src'.$field, null, $crs ? '!' : null];
+
+ return null;
+ }
+
+ protected function cbPvpPurchasable(int $cr, int $crs, string $crv, string $field) : ?array
+ {
+ if (!$this->int2Bool($crs))
+ return null;
+
+ $costs = DB::Aowow()->selectCol('SELECT `id` FROM ::itemextendedcost WHERE %n > 0', $field);
+ if ($items = DB::World()->selectCol($this->extCostQuery, $costs, $costs))
+ return ['id', $items, $crs ? null : '!'];
+
+ return null;
+ }
+
+ protected function cbDisenchantsInto(int $cr, int $crs, string $crv) : ?array
+ {
+ if (!Util::checkNumeric($crs, NUM_CAST_INT))
+ return null;
+
+ if (!in_array($crs, self::$enums[$cr]))
+ return null;
+
+ $refResults = [];
+ $newRefs = DB::World()->selectCol('SELECT `entry` FROM %n WHERE `item` = %i AND `reference` = 0', Loot::REFERENCE, $crs);
+ while ($newRefs)
+ {
+ $refResults += $newRefs;
+ $newRefs = DB::World()->selectCol('SELECT `entry` FROM %n WHERE `reference` IN %in', Loot::REFERENCE, $newRefs);
+ }
+
+ $lootIds = DB::World()->selectCol('SELECT `entry` FROM %n', Loot::DISENCHANT, 'WHERE %if', $refResults, '`reference` IN %in OR', $refResults, '%end (`reference` = 0 AND `item` = %i)', $crs);
+
+ return $lootIds ? ['disenchantId', $lootIds] : [0];
+ }
+
+ protected function cbObjectiveOfQuest(int $cr, int $crs, string $crv) : ?array
+ {
+ $where = match ($crs)
+ {
+ // Yes / No
+ 1, 5 => [1],
+ // Alliance
+ 2 => [['`reqRaceMask` & %i', ChrRace::MASK_ALLIANCE], ['(`reqRaceMask` & %i) = 0', ChrRace::MASK_HORDE]],
+ // Horde
+ 3 => [['`reqRaceMask` & %i', ChrRace::MASK_HORDE], ['(`reqRaceMask` & %i) = 0', ChrRace::MASK_ALLIANCE]],
+ // Both
+ 4 => [[DB::OR, [['`reqRaceMask` = 0'], [DB::AND, [['`reqRaceMask` & %i', ChrRace::MASK_ALLIANCE], ['`reqRaceMask` & %i', ChrRace::MASK_HORDE]]]]]],
+ default => null
+ };
+
+ if (!$where)
+ return [0];
+
+ $itemIds = DB::Aowow()->selectCol(
+ 'SELECT `reqItemId1` FROM ::quests WHERE %and UNION SELECT `reqItemId2` FROM ::quests WHERE %and UNION
+ SELECT `reqItemId3` FROM ::quests WHERE %and UNION SELECT `reqItemId4` FROM ::quests WHERE %and UNION
+ SELECT `reqItemId5` FROM ::quests WHERE %and UNION SELECT `reqItemId6` FROM ::quests WHERE %and',
+ $where, $where, $where, $where, $where, $where
+ );
+
+ if ($itemIds)
+ return ['id', $itemIds, $crs == 5 ? '!' : null];
+
+ return [0];
+ }
+
+ protected function cbReagentForAbility(int $cr, int $crs, string $crv) : ?array
+ {
+ if (!isset(self::$enums[$cr][$crs]))
+ return null;
+
+ $_ = self::$enums[$cr][$crs];
+ if ($_ === null)
+ return null;
+
+ $ids = [];
+ $spells = DB::Aowow()->selectAssoc( // 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 %in',
+ is_bool($_) ? array_filter(self::$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(int $cr, int $crs, string $crv) : ?array
+ {
+ if (!isset(self::$enums[$cr][$crs]))
+ return null;
+
+ $_ = self::$enums[$cr][$crs];
+ if (is_int($_)) // specific
+ return ['src.src'.$_, null, '!'];
+ else if ($_) // any
+ {
+ $foo = [DB::OR];
+ foreach (self::$enums[$cr] as $bar)
+ if (is_int($bar))
+ $foo[] = ['src.src'.$bar, null, '!'];
+
+ return $foo;
+ }
+ else // none
+ return ['src.typeId', null];
+ }
+
+ protected function cbTypeCheck(string &$v) : bool
+ {
+ if (!$this->parentCats)
+ return false;
+
+ if (!Util::checkNumeric($v, NUM_CAST_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] == ITEM_CLASS_CONSUMABLE)
+ return in_array($v, array_keys(Lang::item('cat', 0, 1)));
+ // weapons - only if parent
+ else if ($c[0] == ITEM_CLASS_WEAPON && !isset($c[1]))
+ return in_array($v, array_keys(Lang::spell('weaponSubClass')));
+ // armor - only if parent
+ else if ($c[0] == ITEM_CLASS_ARMOR && !isset($c[1]))
+ return in_array($v, array_keys(Lang::item('cat', ITEM_CLASS_ARMOR, 1)));
+ // uh ... other stuff...
+ else if (!isset($c[1]) && in_array($c[0], [ITEM_CLASS_CONTAINER, ITEM_CLASS_GEM, ITEM_CLASS_TRADEGOOD, ITEM_CLASS_RECIPE, ITEM_CLASS_MISC]))
+ return in_array($v, array_keys($catList[1]));
+
+ return false;
+ }
+
+ protected function cbSlotCheck(string &$v) : bool
+ {
+ if (!Util::checkNumeric($v, NUM_CAST_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] == ITEM_CLASS_CONSUMABLE && (!isset($c[1]) || in_array($c[1], [-3, 6])))
+ return in_array($v, $sl);
+
+ // weapons - always
+ else if ($c[0] == ITEM_CLASS_WEAPON)
+ return in_array($v, $sl);
+
+ // armor - any; any armor
+ else if ($c[0] == ITEM_CLASS_ARMOR && (!isset($c[1]) || in_array($c[1], [ITEM_SUBCLASS_CLOTH_ARMOR, ITEM_SUBCLASS_LEATHER_ARMOR, ITEM_SUBCLASS_MAIL_ARMOR, ITEM_SUBCLASS_PLATE_ARMOR])))
+ return in_array($v, $sl);
+
+ return false;
+ }
+
+ protected function cbWeightKeyCheck(string &$v) : bool
+ {
+ if (preg_match('/\W/i', $v))
+ return false;
+
+ return Stat::getIndexFrom(Stat::IDX_FILTER_CR_ID, $v) > 0;
+ }
+}
+
+?>
diff --git a/includes/dbtypes/itemset.class.php b/includes/dbtypes/itemset.class.php
new file mode 100644
index 00000000..a80e5886
--- /dev/null
+++ b/includes/dbtypes/itemset.class.php
@@ -0,0 +1,251 @@
+ ['o' => 'maxlevel DESC'],
+ 'e' => ['j' => ['::events e ON `e`.`id` = `set`.`eventId`', true], 's' => ', e.`holidayId`'],
+ 'src' => ['j' => ['::source src ON `src`.`typeId` = `set`.`id` AND `src`.`type` = 4', true], 's' => ', `src1`, `src2`, `src3`, `src4`, `src5`, `src6`, `src7`, `src8`, `src9`, `src10`, `src11`, `src12`, `src13`, `src14`, `src15`, `src16`, `src17`, `src18`, `src19`, `src20`, `src21`, `src22`, `src23`, `src24`']
+ );
+
+ public function __construct(array $conditions = [], array $miscData = [])
+ {
+ parent::__construct($conditions, $miscData);
+
+ // post processing
+ foreach ($this->iterate() as &$_curTpl)
+ {
+ $_curTpl['classes'] = ChrClass::fromMask($_curTpl['classMask']);
+ $this->classes = array_merge($this->classes, $_curTpl['classes']);
+
+ $_curTpl['pieces'] = [];
+ for ($i = 1; $i < 10; $i++)
+ {
+ if ($piece = $_curTpl['item'.$i])
+ {
+ $_curTpl['pieces'][] = $piece;
+ $this->pieceToSet[$piece] = $this->id;
+ }
+ }
+ }
+ $this->classes = array_unique($this->classes);
+ }
+
+ public function getListviewData() : array
+ {
+ $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(int $addMask = GLOBALINFO_ANY) : array
+ {
+ $data = [];
+
+ if ($this->classes && ($addMask & GLOBALINFO_RELATED))
+ $data[Type::CHR_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() : ?string
+ {
+ if (!$this->curTpl)
+ return null;
+
+ $x = '';
+ $x .= ''.$this->getField('name', true).' ';
+
+ $nCl = 0;
+ if ($_ = $this->getField('classMask'))
+ {
+ $jsg = [];
+ $cl = Lang::getClassString($_, $jsg);
+ $t = count($jsg) == 1 ? Lang::game('class') : Lang::game('classes');
+ $x .= Util::ucFirst($t).Lang::main('colon').$cl.' ';
+ }
+
+ if ($_ = $this->getField('contentGroup'))
+ $x .= Lang::itemset('notes', $_).($this->getField('heroic') ? ' ('.Lang::item('heroic').')' : '').' ';
+
+ if (!$nCl || !$this->getField('type'))
+ $x.= Lang::itemset('types', $this->getField('type')).' ';
+
+ if ($bonuses = $this->getBonuses())
+ {
+ $x .= '';
+
+ foreach ($bonuses as [$nItems, , $text])
+ $x .= ' '.Lang::itemset('_pieces', [$nItems]).''.$text;
+
+ $x .= '';
+ }
+
+ $x .= ' | ';
+
+ return $x;
+ }
+
+ public function getBonuses() : array
+ {
+ $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[] = [$qty, $spl];
+ }
+
+ // sort by required pieces ASC
+ usort($spells, fn(array $a, array $b) => $a[0] <=> $b[0]);
+
+ $setSpells = new SpellList(array(['s.id', array_column($spells, 1)]));
+ foreach ($spells as &$s)
+ {
+ if ($setSpells->getEntry($s[1]))
+ $s[2] = $setSpells->parseText('description', $this->getField('reqLevel') ?: MAX_LEVEL)[0];
+ else
+ $s[2] = Lang::spell('unkAura', [$s[1]]);
+ }
+
+ return $spells;
+ }
+}
+
+
+// missing filter: "Available to Players"
+class ItemsetListFilter extends Filter
+{
+ protected string $type = 'itemsets';
+ protected static array $enums = array(
+ 6 => parent::ENUM_EVENT
+ );
+
+ protected static array $genericFilter = array(
+ 2 => [parent::CR_NUMERIC, 'id', NUM_CAST_INT, true], // id
+ 3 => [parent::CR_NUMERIC, 'npieces', NUM_CAST_INT ], // pieces
+ 4 => [parent::CR_STRING, 'bonusText', STR_LOCALIZED ], // bonustext
+ 5 => [parent::CR_BOOLEAN, 'heroic' ], // heroic
+ 6 => [parent::CR_ENUM, 'e.holidayId', true, true], // relatedevent
+ 8 => [parent::CR_FLAG, 'cuFlags', CUSTOM_HAS_COMMENT ], // hascomments
+ 9 => [parent::CR_FLAG, 'cuFlags', CUSTOM_HAS_SCREENSHOT ], // hasscreenshots
+ 10 => [parent::CR_FLAG, 'cuFlags', CUSTOM_HAS_VIDEO ], // hasvideos
+ 12 => [parent::CR_CALLBACK, 'cbAvaliable', ] // available to players [yn]
+ );
+
+ protected static array $inputFields = array(
+ 'cr' => [parent::V_RANGE, [2, 12], true ], // criteria ids
+ 'crs' => [parent::V_LIST, [parent::ENUM_NONE, parent::ENUM_ANY, [0, 424]], true ], // criteria operators
+ 'crv' => [parent::V_REGEX, parent::PATTERN_CRV, true ], // criteria values - only printable chars, no delimiters
+ 'na' => [parent::V_NAME, false, false], // name / description - only printable chars, no delimiter
+ 'ma' => [parent::V_EQUAL, 1, false], // match any / all filter
+ 'qu' => [parent::V_RANGE, [0, 7], true ], // quality
+ 'ty' => [parent::V_RANGE, [1, 12], true ], // set type
+ 'minle' => [parent::V_RANGE, [0, 999], false], // min item level
+ 'maxle' => [parent::V_RANGE, [0, 999], false], // max itemlevel
+ 'minrl' => [parent::V_RANGE, [0, MAX_LEVEL], false], // min required level
+ 'maxrl' => [parent::V_RANGE, [0, MAX_LEVEL], false], // max required level
+ 'cl' => [parent::V_LIST, [[1, 9], 11], false], // class
+ 'ta' => [parent::V_RANGE, [1, 30], false] // tag / content group
+ );
+
+ protected function createSQLForValues() : array
+ {
+ $parts = [];
+ $_v = &$this->values;
+
+ // name [str]
+ if ($_v['na'])
+ if ($_ = $this->buildLikeLookup([['na', 'name_loc'.Lang::getLocale()->value]]))
+ $parts[] = $_;
+
+ // quality [enum]
+ if ($_v['qu'])
+ $parts[] = ['quality', $_v['qu']];
+
+ // type [enum]
+ if ($_v['ty'])
+ $parts[] = ['type', $_v['ty']];
+
+ // itemLevel min [int]
+ if ($_v['minle'])
+ $parts[] = ['minLevel', $_v['minle'], '>='];
+
+ // itemLevel max [int]
+ if ($_v['maxle'])
+ $parts[] = ['maxLevel', $_v['maxle'], '<='];
+
+ // reqLevel min [int]
+ if ($_v['minrl'])
+ $parts[] = ['reqLevel', $_v['minrl'], '>='];
+
+ // reqLevel max [int]
+ if ($_v['maxrl'])
+ $parts[] = ['reqLevel', $_v['maxrl'], '<='];
+
+ // class [enum]
+ if ($_v['cl'])
+ $parts[] = ['classMask', $this->list2Mask([$_v['cl']]), '&'];
+
+ // tag [enum]
+ if ($_v['ta'])
+ $parts[] = ['contentGroup', intVal($_v['ta'])];
+
+ return $parts;
+ }
+
+ protected function cbAvaliable(int $cr, int $crs, string $crv) : ?array
+ {
+ return match ($crs)
+ {
+ 1 => ['src.typeId', null, '!'], // Yes
+ 2 => ['src.typeId', null], // No
+ default => null
+ };
+ }
+}
+
+?>
diff --git a/includes/dbtypes/mail.class.php b/includes/dbtypes/mail.class.php
new file mode 100644
index 00000000..0e142428
--- /dev/null
+++ b/includes/dbtypes/mail.class.php
@@ -0,0 +1,77 @@
+error)
+ return;
+
+ // post processing
+ foreach ($this->iterate() as $_id => &$_curTpl)
+ {
+ $_curTpl['name'] = Util::localizedString($_curTpl, 'subject', true);
+ if (!$_curTpl['name'])
+ {
+ $_curTpl['name'] = sprintf(Lang::mail('untitled'), $_id);
+ $_curTpl['subject_loc0'] = $_curTpl['name'];
+ }
+ }
+ }
+
+ public static function getName(int $id) : ?LocString
+ {
+ if ($n = DB::Aowow()->SelectRow('SELECT `subject_loc0`, `subject_loc2`, `subject_loc3`, `subject_loc4`, `subject_loc6`, `subject_loc8` FROM %n WHERE `id` = %i', self::$dataTable, $id))
+ return new LocString($n, 'subject');
+ return null;
+ }
+
+ public function getListviewData() : array
+ {
+ $data = [];
+
+ foreach ($this->iterate() as $__)
+ {
+ $body = str_replace('[br]', ' ', Util::parseHtmlText($this->getField('text', true), true));
+
+ $data[$this->id] = array(
+ 'id' => $this->id,
+ 'subject' => $this->getField('subject', true),
+ 'body' => Lang::trimTextClean($body),
+ 'attachments' => [$this->getField('attachment')]
+ );
+ }
+
+ return $data;
+ }
+
+ public function getJSGlobals(int $addMask = 0) : array
+ {
+ $data = [];
+
+ foreach ($this->iterate() as $__)
+ if ($a = $this->curTpl['attachment'])
+ $data[Type::ITEM][$a] = $a;
+
+ return $data;
+ }
+
+ public function renderTooltip() : ?string { return null; }
+}
+
+?>
diff --git a/includes/types/pet.class.php b/includes/dbtypes/pet.class.php
similarity index 66%
rename from includes/types/pet.class.php
rename to includes/dbtypes/pet.class.php
index 0b27cd2e..86834fa5 100644
--- a/includes/types/pet.class.php
+++ b/includes/dbtypes/pet.class.php
@@ -1,24 +1,26 @@
[['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()
+ public function getListviewData() : array
{
$data = [];
@@ -50,7 +52,7 @@ class PetList extends BaseType
return $data;
}
- public function getJSGlobals($addMask = GLOBALINFO_ANY)
+ public function getJSGlobals(int $addMask = GLOBALINFO_ANY) : array
{
$data = [];
@@ -59,16 +61,16 @@ class PetList extends BaseType
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() { }
+ public function renderTooltip() : ?string { return null; }
}
?>
diff --git a/includes/dbtypes/profile.class.php b/includes/dbtypes/profile.class.php
new file mode 100644
index 00000000..541e4b1a
--- /dev/null
+++ b/includes/dbtypes/profile.class.php
@@ -0,0 +1,750 @@
+iterate() as $__)
+ {
+ if (!$this->isVisibleToUser())
+ continue;
+
+ if (($addInfoMask & PROFILEINFO_PROFILE) && !$this->isCustom())
+ continue;
+
+ if (($addInfoMask & 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' => ChrRace::tryFrom($this->getField('race'))?->isAlliance() ? 0 : 1,
+ 'talenttree1' => $this->getField('talenttree1'),
+ 'talenttree2' => $this->getField('talenttree2'),
+ 'talenttree3' => $this->getField('talenttree3'),
+ 'talentspec' => $this->getField('activespec') + 1, // 0 => 1; 1 => 2
+ 'achievementpoints' => $this->getField('achievementpoints'),
+ 'guild' => $this->curTpl['guildname'] ? '$"'.str_replace ('"', '', $this->curTpl['guildname']).'"' : '', // force this to be a string
+ 'guildrank' => $this->getField('guildrank'),
+ 'realm' => Profiler::urlize($this->getField('realmName'), true),
+ 'realmname' => $this->getField('realmName'),
+ // 'battlegroup' => Profiler::urlize($this->getField('battlegroup')), // was renamed to subregion somewhere around cata release
+ // 'battlegroupname' => $this->getField('battlegroup'),
+ 'gearscore' => $this->getField('gearscore')
+ );
+
+ if ($addInfoMask & PROFILEINFO_USER)
+ $data[$this->id]['published'] = (int)!!($this->getField('cuFlags') & PROFILER_CU_PUBLISHED);
+
+ // for the lv this determins if the link is profile= or profile=..
+ if (!$this->isCustom())
+ $data[$this->id]['region'] = Profiler::urlize($this->getField('region'));
+
+ if ($addInfoMask & 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 ($addInfoMask & PROFILEINFO_PROFILE)
+ {
+ if ($_ = $this->getField('description'))
+ $data[$this->id]['description'] = $_;
+
+ if ($_ = $this->getField('icon'))
+ $data[$this->id]['icon'] = $_;
+ }
+
+ if ($addInfoMask & PROFILEINFO_CHARACTER)
+ if ($_ = $this->getField('renameItr'))
+ $data[$this->id]['renameItr'] = $_;
+
+ if ($this->getField('cuFlags') & PROFILER_CU_PINNED)
+ $data[$this->id]['pinned'] = 1;
+
+ if ($this->getField('deleted'))
+ $data[$this->id]['deleted'] = 1;
+ }
+
+ return $data;
+ }
+
+ public function renderTooltip() : ?string
+ {
+ if (!$this->curTpl)
+ return null;
+
+ $title = '';
+ $name = $this->getField('name');
+ if ($_ = $this->getField('title'))
+ $title = (new TitleList(array(['id', $_])))->getField($this->getField('gender') ? 'female' : 'male', true);
+
+ if ($this->isCustom())
+ $name .= Lang::profiler('customProfile');
+ else if ($title)
+ $name = sprintf($title, $name);
+
+ $x = '';
+ $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(int $addMask = 0) : array
+ {
+ $data = [];
+ $realms = Profiler::getRealms();
+
+ foreach ($this->iterate() as $id => $__)
+ {
+ if (($addMask & PROFILEINFO_PROFILE) && $this->isCustom())
+ {
+ $profile = array(
+ 'id' => $this->getField('id'),
+ 'name' => $this->getField('name'),
+ 'race' => $this->getField('race'),
+ 'classs' => $this->getField('class'),
+ 'level' => $this->getField('level'),
+ 'gender' => $this->getField('gender')
+ );
+
+ if ($_ = $this->getField('icon'))
+ $profile['icon'] = $_;
+
+ $data[] = $profile;
+
+ continue;
+ }
+
+ if ($addMask & PROFILEINFO_CHARACTER && !$this->isCustom())
+ {
+ if (!isset($realms[$this->getField('realm')]))
+ continue;
+
+ $data[] = array(
+ 'id' => $this->getField('id'),
+ 'name' => $this->getField('name'),
+ 'realmname' => $realms[$this->getField('realm')]['name'],
+ 'region' => $realms[$this->getField('realm')]['region'],
+ 'realm' => Profiler::urlize($realms[$this->getField('realm')]['name']),
+ 'race' => $this->getField('race'),
+ 'classs' => $this->getField('class'),
+ 'level' => $this->getField('level'),
+ 'gender' => $this->getField('gender'),
+ 'pinned' => $this->getField('cuFlags') & PROFILER_CU_PINNED ? 1 : 0
+ );
+ }
+ }
+
+ return $data;
+ }
+
+ public function isCustom() : bool
+ {
+ return $this->getField('custom');
+ }
+
+ public function isVisibleToUser() : bool
+ {
+ if (!$this->isCustom() || User::isInGroup(U_GROUP_ADMIN | U_GROUP_BUREAU))
+ return true;
+
+ if ($this->getField('deleted'))
+ return false;
+
+ if (User::$id == $this->getField('user'))
+ return true;
+
+ return (bool)($this->getField('cuFlags') & PROFILER_CU_PUBLISHED);
+ }
+
+ public function getIcon() : string
+ {
+ if ($_ = $this->getField('icon'))
+ return $_;
+
+ return sprintf('chr_%s_%s_%s%02d',
+ ChrRace::from($this->getField('race'))->json(),
+ $this->getField('gender') ? 'female' : 'male',
+ ChrClass::from($this->getField('class'))->json(),
+ max(1, floor(($this->getField('level') - 60) / 10) + 2)
+ );
+ }
+
+ public static function getName(int $id) : ?LocString { return null; }
+}
+
+
+class ProfileListFilter extends Filter
+{
+ use TrProfilerFilter;
+
+ protected string $type = 'profiles';
+ protected static array $genericFilter = array(
+ 2 => [parent::CR_NUMERIC, 'gearscore', NUM_CAST_INT ], // gearscore [num]
+ 3 => [parent::CR_CALLBACK, 'cbAchievs', null, null], // achievementpoints [num]
+ 5 => [parent::CR_NUMERIC, 'talenttree1', NUM_CAST_INT ], // talenttree1 [num]
+ 6 => [parent::CR_NUMERIC, 'talenttree2', NUM_CAST_INT ], // talenttree2 [num]
+ 7 => [parent::CR_NUMERIC, 'talenttree3', NUM_CAST_INT ], // talenttree3 [num]
+ 9 => [parent::CR_STRING, 'g.name' ], // guildname
+ 10 => [parent::CR_CALLBACK, 'cbHasGuildRank', null, null], // guildrank
+ 12 => [parent::CR_CALLBACK, 'cbTeamName', 2, null], // teamname2v2
+ 15 => [parent::CR_CALLBACK, 'cbTeamName', 3, null], // teamname3v3
+ 18 => [parent::CR_CALLBACK, 'cbTeamName', 5, null], // teamname5v5
+ 13 => [parent::CR_CALLBACK, 'cbTeamRating', 2, null], // teamrtng2v2
+ 16 => [parent::CR_CALLBACK, 'cbTeamRating', 3, null], // teamrtng3v3
+ 19 => [parent::CR_CALLBACK, 'cbTeamRating', 5, null], // teamrtng5v5
+ 14 => [parent::CR_NYI_PH, null, 0 /* 2 */ ], // teamcontrib2v2 [num]
+ 17 => [parent::CR_NYI_PH, null, 0 /* 3 */ ], // teamcontrib3v3 [num]
+ 20 => [parent::CR_NYI_PH, null, 0 /* 5 */ ], // teamcontrib5v5 [num]
+ 21 => [parent::CR_CALLBACK, 'cbWearsItems', null, null], // wearingitem [str]
+ 23 => [parent::CR_CALLBACK, 'cbCompletedAcv', null, null], // completedachievement
+ 25 => [parent::CR_CALLBACK, 'cbProfession', SKILL_ALCHEMY, null], // alchemy [num]
+ 26 => [parent::CR_CALLBACK, 'cbProfession', SKILL_BLACKSMITHING, null], // blacksmithing [num]
+ 27 => [parent::CR_CALLBACK, 'cbProfession', SKILL_ENCHANTING, null], // enchanting [num]
+ 28 => [parent::CR_CALLBACK, 'cbProfession', SKILL_ENGINEERING, null], // engineering [num]
+ 29 => [parent::CR_CALLBACK, 'cbProfession', SKILL_HERBALISM, null], // herbalism [num]
+ 30 => [parent::CR_CALLBACK, 'cbProfession', SKILL_INSCRIPTION, null], // inscription [num]
+ 31 => [parent::CR_CALLBACK, 'cbProfession', SKILL_JEWELCRAFTING, null], // jewelcrafting [num]
+ 32 => [parent::CR_CALLBACK, 'cbProfession', SKILL_LEATHERWORKING, null], // leatherworking [num]
+ 33 => [parent::CR_CALLBACK, 'cbProfession', SKILL_MINING, null], // mining [num]
+ 34 => [parent::CR_CALLBACK, 'cbProfession', SKILL_SKINNING, null], // skinning [num]
+ 35 => [parent::CR_CALLBACK, 'cbProfession', SKILL_TAILORING, null], // tailoring [num]
+ 36 => [parent::CR_CALLBACK, 'cbHasGuild', null, null] // hasguild [yn]
+ );
+
+ protected static array $inputFields = array(
+ 'cr' => [parent::V_RANGE, [1, 36], true ], // criteria ids
+ 'crs' => [parent::V_LIST, [parent::ENUM_NONE, parent::ENUM_ANY, [0, 5000]], true ], // criteria operators
+ 'crv' => [parent::V_REGEX, parent::PATTERN_CRV, true ], // criteria values
+ '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
+ 'ra' => [parent::V_LIST, [[1, 8], 10, 11], true ], // race
+ 'cl' => [parent::V_LIST, [[1, 9], 11], true ], // class
+ 'minle' => [parent::V_RANGE, [1, MAX_LEVEL], false], // min level
+ 'maxle' => [parent::V_RANGE, [1, MAX_LEVEL], false], // max level
+ 'rg' => [parent::V_CALLBACK, 'cbRegionCheck', false], // region
+ 'bg' => [parent::V_EQUAL, null, false], // battlegroup - unsued here, but var expected by template
+ 'sv' => [parent::V_CALLBACK, 'cbServerCheck', false] // server
+ );
+
+ public bool $useLocalList = false;
+ public array $extraOpts = [];
+
+ /* heads up!
+ a couple of filters are too complex to be run against the characters database
+ if they are selected, force useage of LocalProfileList
+ */
+
+ public function __construct(string|array $data, array $opts = [])
+ {
+ parent::__construct($data, $opts);
+
+ if (!empty($this->values['cr']))
+ if (array_intersect($this->values['cr'], [2, 5, 6, 7, 21]))
+ $this->useLocalList = true;
+ }
+
+ protected function createSQLForValues() : array
+ {
+ $parts = [];
+ $_v = $this->values;
+
+ // region (rg), battlegroup (bg) and server (sv) are passed to ProflieList as miscData and handled there
+
+ // table key differs between remote and local :<
+ $k = $this->useLocalList ? 'p' : 'c';
+
+ // name [str]
+ if ($_v['na'])
+ {
+ // issue: the table is case sensitive. so we need to alter the tokens for multiple cases
+ foreach (['inTokens', 'exTokens'] as $prop)
+ {
+ if (empty($this->{$prop}['na']))
+ continue;
+
+ $this->{$prop}['na'] = array_map(Util::lower(...), $this->{$prop}['na']);
+ $this->{$prop}['_na'] = array_map(Util::ucWords(...), $this->{$prop}['na']);
+ };
+
+ $parts[] = $this->buildLikeLookup([['na', $k.'.name'], ['_na', $k.'.name']], $_v['ex'] == 'on');
+ }
+
+ // side [list]
+ if ($_v['si'] == SIDE_ALLIANCE)
+ $parts[] = [$k.'.race', ChrRace::fromMask(ChrRace::MASK_ALLIANCE)];
+ else if ($_v['si'] == SIDE_HORDE)
+ $parts[] = [$k.'.race', ChrRace::fromMask(ChrRace::MASK_HORDE)];
+
+ // race [list]
+ if ($_v['ra'])
+ $parts[] = [$k.'.race', $_v['ra']];
+
+ // class [list]
+ if ($_v['cl'])
+ $parts[] = [$k.'.class', $_v['cl']];
+
+ // min level [int]
+ if ($_v['minle'])
+ $parts[] = [$k.'.level', $_v['minle'], '>='];
+
+ // max level [int]
+ if ($_v['maxle'])
+ $parts[] = [$k.'.level', $_v['maxle'], '<='];
+
+ return $parts;
+ }
+
+ protected function cbProfession(int $cr, int $crs, string $crv, $skillId) : ?array
+ {
+ if (!Util::checkNumeric($crv, NUM_CAST_INT) || !$this->int2Op($crs))
+ return null;
+
+ $k = 'sk_'.Util::createHash(12);
+ $col = 'skill-'.$skillId;
+
+ $this->fiExtraCols[$skillId] = $col;
+
+ if ($this->useLocalList)
+ {
+ $this->extraOpts[$k] = array(
+ 'j' => [sprintf('::profiler_completion_skills %1$s ON `%1$s`.`id` = p.`id` AND `%1$s`.`skillId` = %2$d AND `%1$s`.`value` %3$s %4$d', $k, $skillId, $crs, $crv), true],
+ 's' => [', '.$k.'.`value` AS "'.$col.'"']
+ );
+ return [$k.'.skillId', null, '!'];
+ }
+ else
+ {
+ $this->extraOpts[$k] = array(
+ 'j' => [sprintf('character_skills %1$s ON `%1$s`.`guid` = c.`guid` AND `%1$s`.`skill` = %2$d AND `%1$s`.`value` %3$s %4$d', $k, $skillId, $crs, $crv), true],
+ 's' => [', '.$k.'.`value` AS "'.$col.'"']
+ );
+ return [$k.'.skill', null, '!'];
+ }
+ }
+
+ protected function cbCompletedAcv(int $cr, int $crs, string $crv) : ?array
+ {
+ if (!Util::checkNumeric($crv, NUM_CAST_INT))
+ return null;
+
+ if (!Type::validateIds(Type::ACHIEVEMENT, $crv))
+ return null;
+
+ $k = 'acv_'.Util::createHash(12);
+
+ if ($this->useLocalList)
+ {
+ $this->extraOpts[$k] = ['j' => [sprintf('::profiler_completion_achievements %1$s ON `%1$s`.`id` = p.`id` AND `%1$s`.`achievementId` = %2$d', $k, $crv), true]];
+ return [$k.'.achievementId', null, '!'];
+ }
+ else
+ {
+ $this->extraOpts[$k] = ['j' => [sprintf('character_achievement %1$s ON `%1$s`.`guid` = c.`guid` AND `%1$s`.`achievement` = %2$d', $k, $crv), true]];
+ return [$k.'.achievement', null, '!'];
+ }
+ }
+
+ protected function cbWearsItems(int $cr, int $crs, string $crv) : ?array
+ {
+ if (!Util::checkNumeric($crv, NUM_CAST_INT))
+ return null;
+
+ if (!Type::validateIds(Type::ITEM, $crv))
+ return null;
+
+ $k = 'i_'.Util::createHash(12);
+
+ $this->extraOpts[$k] = ['j' => [sprintf('::profiler_items %1$s ON `%1$s`.`id` = p.`id` AND `%1$s`.`item` = %2$d', $k, $crv), true]];
+ return [$k.'.item', null, '!'];
+ }
+
+ protected function cbHasGuild(int $cr, int $crs, string $crv) : ?array
+ {
+ if (!$this->int2Bool($crs))
+ return null;
+
+ if ($this->useLocalList)
+ return ['p.guild', null, $crs ? '!' : null];
+ else
+ return ['gm.guildId', null, $crs ? '!' : null];
+ }
+
+ protected function cbHasGuildRank(int $cr, int $crs, string $crv) : ?array
+ {
+ if (!Util::checkNumeric($crv, NUM_CAST_INT) || !$this->int2Op($crs))
+ return null;
+
+ if ($this->useLocalList)
+ return ['p.guildrank', $crv, $crs];
+ else
+ return ['gm.rank', $crv, $crs];
+ }
+
+ protected function cbTeamName(int $cr, int $crs, string $crv, $size) : ?array
+ {
+ $n = preg_replace(parent::PATTERN_NAME, '', $crv);
+ if ($this->tokenizeString($cr, $n))
+ if ($_ = $this->buildLikeLookup([[$cr, 'at.name']]))
+ return [DB::AND, ['at.type', $size], $_];
+
+ return null;
+ }
+
+ protected function cbTeamRating(int $cr, int $crs, string $crv, $size) : ?array
+ {
+ if (!Util::checkNumeric($crv, NUM_CAST_INT) || !$this->int2Op($crs))
+ return null;
+
+ return [DB::AND, ['at.type', $size], ['at.rating', $crv, $crs]];
+ }
+
+ protected function cbAchievs(int $cr, int $crs, string $crv) : ?array
+ {
+ if (!Util::checkNumeric($crv, NUM_CAST_INT) || !$this->int2Op($crs))
+ return null;
+
+ if ($this->useLocalList)
+ return ['p.achievementpoints', $crv, $crs];
+ else
+ return ['cap.counter', $crv, $crs];
+ }
+}
+
+
+class RemoteProfileList extends ProfileList
+{
+ protected string $queryBase = 'SELECT `c`.*, `c`.`guid` AS ARRAY_KEY FROM characters c';
+ protected array $queryOpts = array(
+ 'c' => [['gm', 'g', 'cap']], // 12698: use criteria of Achievement 4496 as shortcut to get total achievement points
+ 'cap' => ['j' => ['character_achievement_progress cap ON cap.`guid` = c.`guid` AND cap.`criteria` = 12698', true], 's' => ', IFNULL(cap.`counter`, 0) AS "achievementpoints"'],
+ 'gm' => ['j' => ['guild_member gm ON gm.`guid` = c.`guid`', true], 's' => ', gm.`rank` AS "guildrank"'],
+ 'g' => ['j' => ['guild g ON g.`guildid` = gm.`guildid`', true], 's' => ', g.`guildid` AS "guild", g.`name` AS "guildname"'],
+ 'atm' => ['j' => ['arena_team_member atm ON atm.`guid` = c.`guid`', true], 's' => ', atm.`personalRating` AS "rating"'],
+ 'at' => [['atm'], 'j' => 'arena_team at ON atm.`arenaTeamId` = at.`arenaTeamId`', 's' => ', at.`name` AS "arenateam", IF(at.`captainGuid` = c.`guid`, 1, 0) AS "captain"']
+ );
+
+ private array $rnItr = []; // rename iterator [name => nCharsWithThisName]
+
+ public function __construct(array $conditions = [], array $miscData = [])
+ {
+ // select DB by realm
+ if (!$this->selectRealms($miscData))
+ {
+ trigger_error('RemoteProfileList::__construct - cannot access any realm.', E_USER_WARNING);
+ return;
+ }
+
+ parent::__construct($conditions, $miscData);
+
+ if ($this->error)
+ return;
+
+ reset($this->dbNames); // only use when querying single realm
+ $realmId = key($this->dbNames);
+ $realms = Profiler::getRealms();
+ $talentSpells = [];
+ $talentLookup = [];
+ $distrib = [];
+
+ // post processing
+ foreach ($this->iterate() as $guid => &$curTpl)
+ {
+ // battlegroup
+ $curTpl['battlegroup'] = Cfg::get('BATTLEGROUP');
+
+ // realm
+ [$r, $g] = explode(':', $guid);
+ if (!empty($realms[$r]))
+ {
+ $curTpl['realm'] = $r;
+ $curTpl['realmName'] = $realms[$r]['name'];
+ $curTpl['region'] = $realms[$r]['region'];
+ }
+ else
+ {
+ trigger_error('char #'.$guid.' belongs to nonexistent realm #'.$r, E_USER_WARNING);
+ unset($this->templates[$guid]);
+ continue;
+ }
+
+ // empty name
+ if (!$curTpl['name'])
+ {
+ trigger_error('char #'.$guid.' on realm #'.$r.' has empty name.', E_USER_WARNING);
+ unset($this->templates[$guid]);
+ continue;
+ }
+
+ // temp id
+ $curTpl['id'] = 0;
+
+ // talent points pre
+ $talentLookup[$r][$g] = [];
+ $talentSpells[] = $curTpl['class'];
+ $curTpl['activespec'] = $curTpl['activeTalentGroup'];
+
+ // equalize distribution
+ if (empty($distrib[$curTpl['realm']]))
+ $distrib[$curTpl['realm']] = 1;
+ else
+ $distrib[$curTpl['realm']]++;
+
+ // char is pending rename
+ if ($curTpl['at_login'] & 0x1)
+ {
+ if (!isset($this->rnItr[$curTpl['name']]))
+ $this->rnItr[$curTpl['name']] = DB::Aowow()->selectCell('SELECT MAX(`renameItr`) FROM ::profiler_profiles WHERE `realm` = %i AND `custom` = 0 AND `name` = %s', $r, $curTpl['name']) ?: 0;
+
+ // already saved as "pending rename"
+ if ($rnItr = DB::Aowow()->selectCell('SELECT `renameItr` FROM ::profiler_profiles WHERE `realm` = %i AND `realmGUID` = %i', $r, $g))
+ $curTpl['renameItr'] = $rnItr;
+ // not yet recognized: get max itr
+ else
+ $curTpl['renameItr'] = ++$this->rnItr[$curTpl['name']];
+ }
+ else
+ $curTpl['renameItr'] = 0;
+
+ $curTpl['cuFlags'] = 0;
+ }
+
+ foreach ($talentLookup as $realm => $chars)
+ $talentLookup[$realm] = DB::Characters($realm)->selectCol('SELECT `guid` AS ARRAY_KEY, `spell` AS ARRAY_KEY2, `talentGroup` FROM character_talent ct WHERE `guid` IN %in', array_keys($chars));
+
+ $talentSpells = DB::Aowow()->selectAssoc('SELECT `spell` AS ARRAY_KEY, `tab`, `rank` FROM ::talents WHERE `class` IN %in', array_unique($talentSpells));
+
+ // equalize subject distribution across realms
+ $limit = 0;
+ foreach ($conditions as $c)
+ if (is_numeric($c))
+ $limit = max(0, (int)$c);
+
+ if (!$limit) // int:0 means unlimited, so skip process
+ $distrib = [];
+
+ $total = array_sum($distrib);
+ foreach ($distrib as &$d)
+ $d = ceil($limit * $d / $total);
+
+ foreach ($this->iterate() as $guid => &$curTpl)
+ {
+ if ($distrib)
+ {
+ if ($limit <= 0 || $distrib[$curTpl['realm']] <= 0)
+ {
+ unset($this->templates[$guid]);
+ continue;
+ }
+
+ $distrib[$curTpl['realm']]--;
+ $limit--;
+ }
+
+ [$r, $g] = explode(':', $guid);
+
+ // talent points post
+ $curTpl['talenttree1'] = 0;
+ $curTpl['talenttree2'] = 0;
+ $curTpl['talenttree3'] = 0;
+ if (!empty($talentLookup[$r][$g]))
+ {
+ $talents = array_filter($talentLookup[$r][$g], function($v) use ($curTpl) { return $curTpl['activespec'] == $v; } );
+ foreach (array_intersect_key($talentSpells, $talents) as $spell => $data)
+ $curTpl['talenttree'.($data['tab'] + 1)] += $data['rank'];
+ }
+ }
+ }
+
+ public function getListviewData(int $addInfoMask = 0, array $reqCols = []) : array
+ {
+ $data = parent::getListviewData($addInfoMask, $reqCols);
+
+ // not wanted on server list
+ foreach ($data as &$d)
+ unset($d['published']);
+
+ return $data;
+ }
+
+ public function initializeLocalEntries() : void
+ {
+ if (!$this->templates)
+ return;
+
+ $baseData = $guildData = [];
+ foreach ($this->iterate() as $guid => $__)
+ {
+ $realmId = $this->getField('realm');
+ $guildGUID = $this->getField('guild');
+
+ $baseData['realm'][$guid] = $realmId;
+ $baseData['realmGUID'][$guid] = $this->getField('guid');
+ $baseData['name'][$guid] = $this->getField('name');
+ $baseData['renameItr'][$guid] = $this->getField('renameItr');
+ $baseData['race'][$guid] = $this->getField('race');
+ $baseData['class'][$guid] = $this->getField('class');
+ $baseData['level'][$guid] = $this->getField('level');
+ $baseData['gender'][$guid] = $this->getField('gender');
+ $baseData['guild'][$guid] = $guildGUID ?: null;
+ $baseData['guildrank'][$guid] = $guildGUID ? $this->getField('guildrank') : null;
+ $baseData['stub'][$guid] = 1;
+
+ if ($guildGUID)
+ {
+ $guildData['realm'][$realmId.'-'.$guildGUID] = $realmId;
+ $guildData['realmGUID'][$realmId.'-'.$guildGUID] = $guildGUID;
+ $guildData['name'][$realmId.'-'.$guildGUID] = $this->getField('guildname');
+ $guildData['nameUrl'][$realmId.'-'.$guildGUID] = Profiler::urlize($this->getField('guildname'));
+ $guildData['stub'][$realmId.'-'.$guildGUID] = 1;
+ }
+ }
+
+ // basic guild data (satisfying table constraints)
+ if ($guildData)
+ {
+ DB::Aowow()->qry('INSERT INTO ::profiler_guild %m ON DUPLICATE KEY UPDATE `id` = `id`', $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 %in AND `realmGUID` IN %in',
+ $guildData['realm'], $guildData['realmGUID']
+ );
+
+ foreach ($baseData['guild'] as $i => &$g)
+ $g = $localGuilds[$baseData['realm'][$i]][$baseData['guild'][$i]] ?? null;
+ }
+
+ // basic char data (enough for tooltips)
+ if ($baseData)
+ {
+ // this could have been an INSERT ON DUPLICATE KEY UPDATE if MariaDB and MySQL would behave for once!
+ $insertOrUpdate = $baseData;
+ $existing = DB::Aowow()->selectAssoc('SELECT `realm` AS ARRAY_KEY, `realmGUID` AS ARRAY_KEY2, 1 FROM ::profiler_profiles WHERE `realm` IN %in AND `realmGUID` IN %in', $insertOrUpdate['realm'], $insertOrUpdate['realmGUID']);
+ foreach ($insertOrUpdate['realm'] as $guid => $_)
+ {
+ if (!isset($existing[$insertOrUpdate['realm'][$guid]][$insertOrUpdate['realmGUID'][$guid]]))
+ continue;
+
+ // ... ON DUPLICATE KEY UPDATE
+ DB::Aowow()->qry('UPDATE ::profiler_profiles SET `name` = %s, `renameItr` = %i WHERE `realm` = %i AND `realmGUID` = %i', $insertOrUpdate['name'][$guid], $insertOrUpdate['renameItr'][$guid], $insertOrUpdate['realm'][$guid], $insertOrUpdate['realmGUID'][$guid]);
+ foreach($insertOrUpdate as $col => $__)
+ unset($insertOrUpdate[$col][$guid]);
+ }
+
+ // INSERT ...
+ if (current($insertOrUpdate))
+ DB::Aowow()->qry('INSERT INTO ::profiler_profiles %m', $insertOrUpdate);
+
+ // merge back local ids
+ $localIds = DB::Aowow()->selectAssoc('SELECT CONCAT(`realm`, ":", `realmGUID`) AS ARRAY_KEY, `id`, `gearscore` FROM ::profiler_profiles WHERE `custom` = 0 AND `realm` IN %in AND `realmGUID` IN %in',
+ $baseData['realm'], $baseData['realmGUID']
+ );
+
+ foreach ($this->iterate() as $guid => &$_curTpl)
+ if (isset($localIds[$guid]))
+ $_curTpl = array_merge($_curTpl, $localIds[$guid]);
+ }
+ }
+}
+
+
+class LocalProfileList extends ProfileList
+{
+ protected string $queryBase = 'SELECT p.*, p.`id` AS ARRAY_KEY FROM ::profiler_profiles p';
+ protected array $queryOpts = array(
+ 'p' => [['g'], 'g' => 'p.`id`'],
+ 'ap' => ['j' => ['::account_profiles ap ON ap.`profileId` = p.`id`', true], 's' => ', (IFNULL(ap.`extraFlags`, 0) | p.`cuFlags`) AS "cuFlags"'],
+ 'atm' => ['j' => ['::profiler_arena_team_member atm ON atm.`profileId` = p.`id`', true], 's' => ', atm.`captain`, atm.`personalRating` AS "rating", atm.`seasonGames`, atm.`seasonWins`'],
+ 'at' => [['atm'], 'j' => ['::profiler_arena_team at ON at.`id` = atm.`arenaTeamId`', true], 's' => ', at.`type`'],
+ 'g' => ['j' => ['::profiler_guild g ON g.`id` = p.`guild`', true], 's' => ', g.`name` AS "guildname"']
+ );
+
+ public function __construct(array $conditions = [], array $miscData = [])
+ {
+ $realms = Profiler::getRealms();
+
+ // graft realm selection from miscData onto conditions
+ $realmIds = [];
+ if (isset($miscData['sv']))
+ $realmIds = array_keys(array_filter($realms, fn($x) => Profiler::urlize($x['name']) == Profiler::urlize($miscData['sv'])));
+
+ if (isset($miscData['rg']))
+ $realmIds = array_merge($realmIds, array_keys(array_filter($realms, fn($x) => $x['region'] == $miscData['rg'])));
+
+ if ($conditions && $realmIds)
+ {
+ array_unshift($conditions, DB::AND);
+ $conditions = [DB::AND, ['realm', $realmIds], $conditions];
+ }
+ else if ($realmIds)
+ $conditions = [['realm', $realmIds]];
+
+ parent::__construct($conditions, $miscData);
+
+ if ($this->error)
+ return;
+
+ foreach ($this->iterate() as $id => &$curTpl)
+ {
+ if (!$curTpl['realm']) // custom profile w/o realminfo
+ continue;
+
+ if (!isset($realms[$curTpl['realm']]))
+ {
+ unset($this->templates[$id]);
+ continue;
+ }
+
+ $curTpl['realmName'] = $realms[$curTpl['realm']]['name'];
+ $curTpl['region'] = $realms[$curTpl['realm']]['region'];
+ $curTpl['battlegroup'] = Cfg::get('BATTLEGROUP');
+ }
+ }
+
+ public function getProfileUrl() : string
+ {
+ $url = '?profile=';
+
+ if ($this->isCustom())
+ return $url.$this->getField('id');
+
+ return $url.implode('.', array(
+ $this->getField('region'),
+ Profiler::urlize($this->getField('realmName'), true),
+ urlencode($this->getField('name'))
+ ));
+ }
+}
+
+
+?>
diff --git a/includes/dbtypes/quest.class.php b/includes/dbtypes/quest.class.php
new file mode 100644
index 00000000..c24c2573
--- /dev/null
+++ b/includes/dbtypes/quest.class.php
@@ -0,0 +1,727 @@
+ [],
+ 'nml' => ['j' => '::quests_search nml ON nml.`id` = q.`id` AND nml.`locale` = DB_LOC_I'],
+ 'rsc' => ['j' => '::spell rsc ON q.`rewardSpellCast` = rsc.`id`'], // limit rewardSpellCasts
+ 'qse' => ['j' => '::quests_startend qse ON q.`id` = qse.`questId`', 's' => ', qse.`method`'], // groupConcat..?
+ 'e' => ['j' => ['::events e ON e.`id` = q.`eventId`', true], 's' => ', e.`holidayId`']
+ );
+
+ public function __construct(array $conditions = [], array $miscData = [])
+ {
+ 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['questSortId']; // should probably be in a method...
+ $_curTpl['cat2'] = 0;
+
+ foreach (Game::QUEST_CLASSES 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][CURRENCY_HONOR_POINTS] = $_;
+
+ if ($_ = $_curTpl['rewardArenaPoints'])
+ $rewards[Type::CURRENCY][CURRENCY_ARENA_POINTS] = $_;
+
+ 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;
+ }
+ }
+
+ public function isRepeatable() : bool
+ {
+ return $this->curTpl['specialFlags'] & QUEST_FLAG_SPECIAL_REPEATABLE;
+ }
+
+ public function isDaily() : int
+ {
+ 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;
+ }
+
+ public function isAutoAccept() : bool
+ {
+ return $this->curTpl['flags'] & QUEST_FLAG_AUTO_ACCEPT || $this->curTpl['specialFlags'] & QUEST_FLAG_SPECIAL_AUTO_ACCEPT;
+ }
+
+ // by TC definition
+ public function isSeasonal() : bool
+ {
+ return in_array($this->getField('questSortIdBak'), [-22, -284, -366, -369, -370, -376, -374]) && !$this->isRepeatable();
+ }
+
+ public function getSourceData(int $id = 0) : array
+ {
+ $data = [];
+
+ foreach ($this->iterate() as $__)
+ {
+ if ($id && $id != $this->id)
+ continue;
+
+ $data[$this->id] = array(
+ "n" => $this->getField('name', true),
+ "t" => Type::QUEST,
+ "ti" => $this->id,
+ "c" => $this->curTpl['cat1'],
+ "c2" => $this->curTpl['cat2']
+ );
+ }
+
+ return $data;
+ }
+
+ public function getSOMData(int $side = SIDE_BOTH) : array
+ {
+ $data = [];
+
+ foreach ($this->iterate() as $__)
+ {
+ if (!(ChrRace::sideFromMask($this->curTpl['reqRaceMask']) & $side))
+ continue;
+
+ [$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` = %i',
+ $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(int $extraFactionId = 0) : array
+ {
+ $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' => Lang::unescapeUISequences($this->getField('name', true), Lang::FMT_RAW),
+ 'side' => ChrRace::sideFromMask($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['questInfoId'])
+ $data[$this->id]['type'] = $_;
+
+ if ($_ = $this->curTpl['reqClassMask'])
+ $data[$this->id]['reqclass'] = $_;
+
+ if ($_ = ($this->curTpl['reqRaceMask'] & ChrRace::MASK_ALL))
+ if ((($_ & ChrRace::MASK_ALLIANCE) != ChrRace::MASK_ALLIANCE) && (($_ & ChrRace::MASK_HORDE) != ChrRace::MASK_HORDE))
+ $data[$this->id]['reqrace'] = $_;
+
+ if ($_ = $this->curTpl['rewardOrReqMoney'])
+ 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_TRACKING) // not shown in log
+ $data[$this->id]['wflags'] |= QUEST_CU_SKIP_LOG;
+
+ if ($this->isAutoAccept())
+ $data[$this->id]['wflags'] |= QUEST_CU_AUTO_ACCEPT;
+
+ if ($this->curTpl['flags'] & QUEST_FLAG_FLAGS_PVP) // this flag is only displayed if auto-accept is also set. not sure why.
+ $data[$this->id]['wflags'] |= 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(string $type = 'objectives', bool $jsEscaped = true) : string
+ {
+ $text = $this->getField($type, true);
+ if (!$text)
+ return '';
+
+ $text = Util::parseHtmlText($text);
+
+ if ($jsEscaped)
+ $text = Util::jsEscape($text);
+
+ return $text;
+ }
+
+ public function renderTooltip() : ?string
+ {
+ if (!$this->curTpl)
+ return null;
+
+ $title = Lang::unescapeUISequences(Util::htmlEscape($this->getField('name', true)), Lang::FMT_HTML);
+ $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', false);
+
+
+ $xReq = '';
+ for ($i = 1; $i < 5; $i++)
+ {
+ $ot = $this->getField('objectiveText'.$i, true);
+ $rng = $this->curTpl['reqNpcOrGo'.$i];
+ $rngQty = $this->curTpl['reqNpcOrGoCount'.$i];
+
+ if (!$ot && ($rngQty < 1 || !$rng))
+ continue;
+
+ if ($ot)
+ $name = $ot;
+ else if ($rng > 0)
+ $name = CreatureList::getName($rng);
+ else if ($rng < 0)
+ $name = Lang::unescapeUISequences(GameObjectList::getName(-$rng), Lang::FMT_HTML);
+
+ if (!$name)
+ $name = Util::ucFirst(Lang::game($rng > 0 ? 'npc' : 'object')).' #'.abs($rng);
+
+ $xReq .= ' - '.$name.($rngQty > 1 ? ' x '.$rngQty : '');
+ }
+
+ for ($i = 1; $i < 7; $i++)
+ {
+ $ri = $this->curTpl['reqItemId'.$i];
+ $riQty = $this->curTpl['reqItemCount'.$i];
+
+ if (!$ri || $riQty < 1)
+ continue;
+
+ $name = Lang::unescapeUISequences(ItemList::getName($ri), Lang::FMT_HTML) ?: Util::ucFirst(Lang::game('item')).' #'.$ri;
+
+ $xReq .= ' - '.$name.($riQty > 1 ? ' x '.$riQty : '');
+ }
+
+ if ($et = $this->getField('end', true))
+ $xReq .= ' - '.$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(int $addMask = GLOBALINFO_ANY) : array
+ {
+ $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)];
+
+ if ($this->curTpl['flags'] & QUEST_FLAG_DAILY)
+ $data[Type::QUEST][$this->id]['daily'] = true;
+
+ if ($this->curTpl['flags'] & QUEST_FLAG_WEEKLY)
+ $data[Type::QUEST][$this->id]['weekly'] = true;
+ }
+ }
+
+ return $data;
+ }
+}
+
+
+class QuestListFilter extends Filter
+{
+ protected string $type = 'quests';
+ protected static array $enums = array(
+ 37 => parent::ENUM_CLASSS, // classspecific
+ 38 => parent::ENUM_RACE, // racespecific
+ 9 => parent::ENUM_FACTION, // objectiveearnrepwith
+ 33 => parent::ENUM_EVENT, // relatedevent
+ 43 => parent::ENUM_CURRENCY, // currencyrewarded
+ 1 => parent::ENUM_FACTION, // increasesrepwith
+ 10 => parent::ENUM_FACTION // decreasesrepwith
+ );
+
+ protected static array $genericFilter = array(
+ 1 => [parent::CR_CALLBACK, 'cbReputation', '>', null], // increasesrepwith
+ 2 => [parent::CR_NUMERIC, 'rewardXP', NUM_CAST_INT ], // experiencegained
+ 3 => [parent::CR_NUMERIC, 'rewardOrReqMoney', NUM_CAST_INT ], // moneyrewarded
+ 4 => [parent::CR_CALLBACK, 'cbSpellRewards', null, null], // spellrewarded [yn]
+ 5 => [parent::CR_FLAG, 'flags', QUEST_FLAG_SHARABLE ], // sharable
+ 6 => [parent::CR_NUMERIC, 'timeLimit', NUM_CAST_INT ], // timer
+ 7 => [parent::CR_FLAG, 'cuFlags', QUEST_CU_FIRST_SERIES ], // firstquestseries
+ 9 => [parent::CR_CALLBACK, 'cbEarnReputation', null, null], // objectiveearnrepwith [enum]
+ 10 => [parent::CR_CALLBACK, 'cbReputation', '<', null], // decreasesrepwith
+ 11 => [parent::CR_NUMERIC, 'suggestedPlayers', NUM_CAST_INT ], // suggestedplayers
+ 15 => [parent::CR_FLAG, 'cuFlags', QUEST_CU_LAST_SERIES ], // lastquestseries
+ 16 => [parent::CR_FLAG, 'cuFlags', QUEST_CU_PART_OF_SERIES ], // partseries
+ 18 => [parent::CR_FLAG, 'cuFlags', CUSTOM_HAS_SCREENSHOT ], // hasscreenshots
+ 19 => [parent::CR_CALLBACK, 'cbQuestRelation', 0x1, null], // startsfrom [enum]
+ 21 => [parent::CR_CALLBACK, 'cbQuestRelation', 0x2, null], // endsat [enum]
+ 22 => [parent::CR_CALLBACK, 'cbItemRewards', null, null], // itemrewards [op] [int]
+ 23 => [parent::CR_CALLBACK, 'cbItemChoices', null, null], // itemchoices [op] [int]
+ 24 => [parent::CR_CALLBACK, 'cbLacksStartEnd', null, null], // lacksstartend [yn]
+ 25 => [parent::CR_FLAG, 'cuFlags', CUSTOM_HAS_COMMENT ], // hascomments
+ 27 => [parent::CR_FLAG, 'flags', QUEST_FLAG_DAILY ], // daily
+ 28 => [parent::CR_FLAG, 'flags', QUEST_FLAG_WEEKLY ], // weekly
+ 29 => [parent::CR_FLAG, 'specialFlags', QUEST_FLAG_SPECIAL_REPEATABLE], // repeatable
+ 30 => [parent::CR_NUMERIC, 'id', NUM_CAST_INT, true], // id
+ 33 => [parent::CR_ENUM, 'e.holidayId', true, true], // relatedevent
+ 34 => [parent::CR_CALLBACK, 'cbAvailable', null, null], // availabletoplayers [yn]
+ 36 => [parent::CR_FLAG, 'cuFlags', CUSTOM_HAS_VIDEO ], // hasvideos
+ 37 => [parent::CR_CALLBACK, 'cbClassSpec', null, null], // classspecific [enum]
+ 38 => [parent::CR_CALLBACK, 'cbRaceSpec', null, null], // racespecific [enum]
+ 42 => [parent::CR_STAFFFLAG, 'flags' ], // flags
+ 43 => [parent::CR_CALLBACK, 'cbCurrencyReward', null, null], // currencyrewarded [enum]
+ 44 => [parent::CR_CALLBACK, 'cbLoremaster', null, null], // countsforloremaster_stc [yn]
+ 45 => [parent::CR_BOOLEAN, 'rewardTitleId' ], // titlerewarded
+ 47 => [parent::CR_FLAG, 'flags', QUEST_FLAG_FLAGS_PVP ] // setspvpflag
+ );
+
+ protected static array $inputFields = array(
+ 'cr' => [parent::V_RANGE, [1, 47], true ], // criteria ids
+ 'crs' => [parent::V_LIST, [parent::ENUM_NONE, parent::ENUM_ANY, [0, 99999]], true ], // criteria operators
+ 'crv' => [parent::V_REGEX, parent::PATTERN_INT, true ], // criteria values - only numerals
+ 'na' => [parent::V_NAME, false, false], // name / text - only printable chars, no delimiter
+ 'ex' => [parent::V_EQUAL, 'on', false], // also match subname
+ 'ma' => [parent::V_EQUAL, 1, false], // match any / all filter
+ 'minle' => [parent::V_RANGE, [0, 99], false], // min quest level
+ 'maxle' => [parent::V_RANGE, [0, 99], false], // max quest level
+ 'minrl' => [parent::V_RANGE, [0, 99], false], // min required level
+ 'maxrl' => [parent::V_RANGE, [0, 99], false], // max required level
+ 'si' => [parent::V_LIST, [-SIDE_HORDE, -SIDE_ALLIANCE, SIDE_ALLIANCE, SIDE_HORDE, SIDE_BOTH], false], // side
+ 'ty' => [parent::V_LIST, [0, 1, 21, 41, 62, [81, 85], 88, 89], true ] // type
+ );
+
+ public array $extraOpts = [];
+
+ protected function createSQLForValues() : array
+ {
+ $parts = [];
+ $_v = $this->values;
+
+ // name
+ if ($_v['na'])
+ {
+ $f = [['na', ['nml.nName', 'nml.nObjectives', 'nml.nDetails']]];
+ if ($_v['ex'] != 'on')
+ $f = [['na', 'nml.nName']];
+
+ if ($_ = $this->buildMatchLookup($f))
+ $parts[] = $_;
+ else
+ {
+ $f = [['na', 'name_loc'.Lang::getLocale()->value], ['na', 'objectives_loc'.Lang::getLocale()->value], ['na', 'details_loc'.Lang::getLocale()->value]];
+ if ($_v['ex'] != 'on')
+ $f = [$f[0]];
+
+ if ($_ = $this->buildLikeLookup($f))
+ $parts[] = $_;
+ }
+ }
+
+ // level min
+ if ($_v['minle'])
+ $parts[] = ['level', $_v['minle'], '>=']; // not considering quests that are always at player level (-1)
+
+ // level max
+ if ($_v['maxle'])
+ $parts[] = ['level', $_v['maxle'], '<='];
+
+ // reqLevel min
+ if ($_v['minrl'])
+ $parts[] = ['minLevel', $_v['minrl'], '>=']; // ignoring maxLevel
+
+ // reqLevel max
+ if ($_v['maxrl'])
+ $parts[] = ['minLevel', $_v['maxrl'], '<=']; // ignoring maxLevel
+
+ // side
+ if ($_v['si'])
+ {
+ $excl = [['reqRaceMask', ChrRace::MASK_ALL, '&'], ChrRace::MASK_ALL, '!'];
+ $incl = [DB::OR, ['reqRaceMask', 0], [['reqRaceMask', ChrRace::MASK_ALL, '&'], ChrRace::MASK_ALL]];
+
+ $parts[] = match ($_v['si'])
+ {
+ SIDE_BOTH => $incl,
+ SIDE_HORDE => [DB::OR, $incl, ['reqRaceMask', ChrRace::MASK_HORDE, '&']],
+ -SIDE_HORDE => [DB::AND, $excl, ['reqRaceMask', ChrRace::MASK_HORDE, '&']],
+ SIDE_ALLIANCE => [DB::OR, $incl, ['reqRaceMask', ChrRace::MASK_ALLIANCE, '&']],
+ -SIDE_ALLIANCE => [DB::AND, $excl, ['reqRaceMask', ChrRace::MASK_ALLIANCE, '&']]
+ };
+ }
+
+ // questInfoId [list]
+ if ($_v['ty'])
+ $parts[] = ['questInfoId', $_v['ty']];
+
+ return $parts;
+ }
+
+ protected function cbReputation(int $cr, int $crs, string $crv, string $sign) : ?array
+ {
+ if (!Util::checkNumeric($crs, NUM_CAST_INT))
+ return null;
+
+ if (!in_array($crs, self::$enums[$cr]))
+ return null;
+
+ if ($_ = DB::Aowow()->selectRow('SELECT * FROM ::factions WHERE `id` = %i', $crs))
+ $this->fiReputationCols[] = [$crs, Util::localizedString($_, 'name')];
+
+ return [
+ DB::OR,
+ [DB::AND, ['rewardFactionId1', $crs], ['rewardFactionValue1', 0, $sign]],
+ [DB::AND, ['rewardFactionId2', $crs], ['rewardFactionValue2', 0, $sign]],
+ [DB::AND, ['rewardFactionId3', $crs], ['rewardFactionValue3', 0, $sign]],
+ [DB::AND, ['rewardFactionId4', $crs], ['rewardFactionValue4', 0, $sign]],
+ [DB::AND, ['rewardFactionId5', $crs], ['rewardFactionValue5', 0, $sign]]
+ ];
+ }
+
+ protected function cbQuestRelation(int $cr, int $crs, string $crv, $flags) : ?array
+ {
+ return match ($crs)
+ {
+ Type::NPC,
+ Type::OBJECT,
+ Type::ITEM => [DB::AND, ['qse.type', $crs], ['qse.method', $flags, '&']],
+ default => null
+ };
+ }
+
+ protected function cbCurrencyReward(int $cr, int $crs, string $crv) : ?array
+ {
+ if (!Util::checkNumeric($crs, NUM_CAST_INT))
+ return null;
+
+ if (!in_array($crs, self::$enums[$cr]))
+ return null;
+
+ return [
+ DB::OR,
+ ['rewardItemId1', $crs], ['rewardItemId2', $crs], ['rewardItemId3', $crs], ['rewardItemId4', $crs],
+ ['rewardChoiceItemId1', $crs], ['rewardChoiceItemId2', $crs], ['rewardChoiceItemId3', $crs], ['rewardChoiceItemId4', $crs], ['rewardChoiceItemId5', $crs], ['rewardChoiceItemId6', $crs]
+ ];
+ }
+
+ protected function cbAvailable(int $cr, int $crs, string $crv) : ?array
+ {
+ if (!$this->int2Bool($crs))
+ return null;
+
+ if ($crs)
+ return [['cuFlags', CUSTOM_UNAVAILABLE | CUSTOM_DISABLED, '&'], 0];
+ else
+ return ['cuFlags', CUSTOM_UNAVAILABLE | CUSTOM_DISABLED, '&'];
+ }
+
+ protected function cbItemChoices(int $cr, int $crs, string $crv) : ?array
+ {
+ if (!Util::checkNumeric($crv, NUM_CAST_INT) || !$this->int2Op($crs))
+ return null;
+
+ $this->extraOpts['q']['s'][] = ', (IF(`rewardChoiceItemId1`, 1, 0) + IF(`rewardChoiceItemId2`, 1, 0) + IF(`rewardChoiceItemId3`, 1, 0) + IF(`rewardChoiceItemId4`, 1, 0) + IF(`rewardChoiceItemId5`, 1, 0) + IF(`rewardChoiceItemId6`, 1, 0)) AS "numChoices"';
+ $this->extraOpts['q']['h'][] = '`numChoices` '.$crs.' '.$crv;
+ return [1];
+ }
+
+ protected function cbItemRewards(int $cr, int $crs, string $crv) : ?array
+ {
+ if (!Util::checkNumeric($crv, NUM_CAST_INT) || !$this->int2Op($crs))
+ return null;
+
+ $this->extraOpts['q']['s'][] = ', (IF(`rewardItemId1`, 1, 0) + IF(`rewardItemId2`, 1, 0) + IF(`rewardItemId3`, 1, 0) + IF(`rewardItemId4`, 1, 0)) AS "numRewards"';
+ $this->extraOpts['q']['h'][] = '`numRewards` '.$crs.' '.$crv;
+ return [1];
+ }
+
+ protected function cbLoremaster(int $cr, int $crs, string $crv) : ?array
+ {
+ if (!$this->int2Bool($crs))
+ return null;
+
+ if ($crs)
+ return [DB::AND, ['questSortId', 0, '>'], [['flags', QUEST_FLAG_DAILY | QUEST_FLAG_WEEKLY, '&'], 0], [['specialFlags', QUEST_FLAG_SPECIAL_REPEATABLE | QUEST_FLAG_SPECIAL_MONTHLY, '&'], 0]];
+ else
+ return [DB::OR, ['questSortId', 0, '<'], ['flags', QUEST_FLAG_DAILY | QUEST_FLAG_WEEKLY, '&'], ['specialFlags', QUEST_FLAG_SPECIAL_REPEATABLE | QUEST_FLAG_SPECIAL_MONTHLY, '&']];
+ }
+
+ protected function cbSpellRewards(int $cr, int $crs, string $crv) : ?array
+ {
+ if (!$this->int2Bool($crs))
+ return null;
+
+ if ($crs)
+ return [DB::OR, ['sourceSpellId', 0, '>'], ['rewardSpell', 0, '>'], ['rsc.effect1Id', SpellList::EFFECTS_TEACH], ['rsc.effect2Id', SpellList::EFFECTS_TEACH], ['rsc.effect3Id', SpellList::EFFECTS_TEACH]];
+ else
+ return [DB::AND, ['sourceSpellId', 0], ['rewardSpell', 0], ['rewardSpellCast', 0]];
+ }
+
+ protected function cbEarnReputation(int $cr, int $crs, string $crv) : ?array
+ {
+ if (!Util::checkNumeric($crs, NUM_CAST_INT))
+ return null;
+
+ if ($crs == parent::ENUM_ANY)
+ return [DB::OR, ['reqFactionId1', 0, '>'], ['reqFactionId2', 0, '>']];
+ else if ($crs == parent::ENUM_NONE)
+ return [DB::AND, ['reqFactionId1', 0], ['reqFactionId2', 0]];
+ else if (in_array($crs, self::$enums[$cr]))
+ return [DB::OR, ['reqFactionId1', $crs], ['reqFactionId2', $crs]];
+
+ return null;
+ }
+
+ protected function cbClassSpec(int $cr, int $crs, string $crv) : ?array
+ {
+ if (!isset(self::$enums[$cr][$crs]))
+ return null;
+
+ $_ = self::$enums[$cr][$crs];
+ if ($_ === true)
+ return [DB::AND, ['reqClassMask', 0, '!'], [['reqClassMask', ChrClass::MASK_ALL, '&'], ChrClass::MASK_ALL, '!']];
+ else if ($_ === false)
+ return [DB::OR, ['reqClassMask', 0], [['reqClassMask', ChrClass::MASK_ALL, '&'], ChrClass::MASK_ALL]];
+ else if (is_int($_))
+ return [DB::AND, ['reqClassMask', ChrClass::from($_)->toMask(), '&'], [['reqClassMask', ChrClass::MASK_ALL, '&'], ChrClass::MASK_ALL, '!']];
+
+ return null;
+ }
+
+ protected function cbRaceSpec(int $cr, int $crs, string $crv) : ?array
+ {
+ if (!isset(self::$enums[$cr][$crs]))
+ return null;
+
+ $_ = self::$enums[$cr][$crs];
+ if ($_ === true)
+ return [DB::AND, ['reqRaceMask', 0, '!'], [['reqRaceMask', ChrRace::MASK_ALL, '&'], ChrRace::MASK_ALL, '!'], [['reqRaceMask', ChrRace::MASK_ALLIANCE, '&'], ChrRace::MASK_ALLIANCE, '!'], [['reqRaceMask', ChrRace::MASK_HORDE, '&'], ChrRace::MASK_HORDE, '!']];
+ else if ($_ === false)
+ return [DB::OR, ['reqRaceMask', 0], ['reqRaceMask', ChrRace::MASK_ALL], ['reqRaceMask', ChrRace::MASK_ALLIANCE], ['reqRaceMask', ChrRace::MASK_HORDE]];
+ else if (is_int($_))
+ return [DB::AND, ['reqRaceMask', ChrRace::from($_)->toMask(), '&'], [['reqRaceMask', ChrRace::MASK_ALLIANCE, '&'], ChrRace::MASK_ALLIANCE, '!'], [['reqRaceMask', ChrRace::MASK_HORDE, '&'], ChrRace::MASK_HORDE, '!']];
+
+ return null;
+ }
+
+ protected function cbLacksStartEnd(int $cr, int $crs, string $crv) : ?array
+ {
+ if (!$this->int2Bool($crs))
+ return null;
+
+ $missing = DB::Aowow()->selectCol('SELECT `questId`, BIT_OR(`method`) AS "se" FROM ::quests_startend GROUP BY `questId` HAVING "se" <> 3');
+ if ($crs)
+ return ['id', $missing];
+ else
+ return ['id', $missing, '!'];
+ }
+}
+
+
+?>
diff --git a/includes/dbtypes/skill.class.php b/includes/dbtypes/skill.class.php
new file mode 100644
index 00000000..9aafbcb1
--- /dev/null
+++ b/includes/dbtypes/skill.class.php
@@ -0,0 +1,73 @@
+ [['ic']],
+ 'ic' => ['j' => ['::icons ic ON ic.`id` = sl.`iconId`', true], 's' => ', ic.`name` AS "iconString"'],
+ );
+
+ public function __construct(array $conditions = [], array $miscData = [])
+ {
+ parent::__construct($conditions, $miscData);
+
+ // post processing
+ foreach ($this->iterate() as &$_curTpl)
+ {
+ $_ = &$_curTpl['specializations']; // shorthand
+ if (!$_)
+ $_ = [0, 0, 0, 0, 0];
+ else
+ $_ = array_pad(explode(' ', $_), 5, 0);
+
+ if (!$_curTpl['iconId'])
+ $_curTpl['iconString'] = DEFAULT_ICON;
+ }
+ }
+
+ public function getListviewData() : array
+ {
+ $data = [];
+
+ foreach ($this->iterate() as $__)
+ {
+ $data[$this->id] = array(
+ 'category' => $this->curTpl['typeCat'],
+ 'categorybak' => $this->curTpl['categoryId'],
+ 'id' => $this->id,
+ 'name' => $this->getField('name', true),
+ 'profession' => $this->curTpl['professionMask'],
+ 'recipeSubclass' => $this->curTpl['recipeSubClass'],
+ 'specializations' => Util::toJSON($this->curTpl['specializations'], JSON_NUMERIC_CHECK),
+ 'icon' => $this->curTpl['iconString']
+ );
+ }
+
+ return $data;
+ }
+
+ public function getJSGlobals(int $addMask = 0) : array
+ {
+ $data = [];
+
+ foreach ($this->iterate() as $__)
+ $data[self::$type][$this->id] = ['name' => $this->getField('name', true), 'icon' => $this->curTpl['iconString']];
+
+ return $data;
+ }
+
+ public function renderTooltip() : ?string { return null; }
+}
+
+?>
diff --git a/includes/dbtypes/sound.class.php b/includes/dbtypes/sound.class.php
new file mode 100644
index 00000000..f43a8fb3
--- /dev/null
+++ b/includes/dbtypes/sound.class.php
@@ -0,0 +1,129 @@
+ MIME_TYPE_OGG, SOUND_TYPE_MP3 => MIME_TYPE_MP3];
+
+ public function __construct(array $conditions = [], array $miscData = [])
+ {
+ parent::__construct($conditions, $miscData);
+
+ // post processing
+ foreach ($this->iterate() as $id => &$_curTpl)
+ {
+ $_curTpl['files'] = [];
+ for ($i = 1; $i < 11; $i++)
+ {
+ if ($_curTpl['soundFile'.$i])
+ {
+ $this->fileBuffer[$_curTpl['soundFile'.$i]] = null;
+ $_curTpl['files'][] = &$this->fileBuffer[$_curTpl['soundFile'.$i]];
+ }
+
+ unset($_curTpl['soundFile'.$i]);
+ }
+ }
+
+ if ($this->fileBuffer)
+ {
+ $files = DB::Aowow()->selectAssoc('SELECT `id` AS ARRAY_KEY, `id`, `file` AS "title", CAST(`type` AS UNSIGNED) AS "type", `path` FROM ::sounds_files sf WHERE `id` IN %in', array_keys($this->fileBuffer));
+ foreach ($files as $id => $data)
+ {
+ // 3.3.5 bandaid - need fullpath to play via wow API, remove for cata and later
+ $data['path'] = str_replace('\\', '\\\\', $data['path'] ? $data['path'] . '\\' . $data['title'] : $data['title']);
+ // skip file extension
+ $data['title'] = substr($data['title'], 0, -4);
+ // enum to string
+ $data['type'] = self::$fileTypes[$data['type']];
+ // get real url
+ $data['url'] = Cfg::get('STATIC_URL') . '/wowsounds/' . $data['id'];
+ // v push v
+ $this->fileBuffer[$id] = $data;
+ }
+ }
+ }
+
+ public static function getName(int $id) : ?LocString
+ {
+ if ($n = DB::Aowow()->SelectRow('SELECT `name` AS "name_loc0" FROM %n WHERE `id` = %i', self::$dataTable, $id))
+ return new LocString($n);
+ return null;
+ }
+
+ public function getListviewData() : array
+ {
+ $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(int $addMask = 0) : array
+ {
+ $data = [];
+
+ foreach ($this->iterate() as $__)
+ $data[self::$type][$this->id] = array(
+ 'name' => $this->getField('name', true),
+ 'type' => $this->getField('cat'),
+ 'files' => array_values(array_filter($this->getField('files')))
+ );
+
+ return $data;
+ }
+
+ public function renderTooltip() : ?string { return null; }
+}
+
+class SoundListFilter extends Filter
+{
+ protected string $type = 'sounds';
+ protected static array $inputFields = array(
+ 'na' => [parent::V_NAME, false, false], // name - only printable chars, no delimiter
+ 'ty' => [parent::V_LIST, [[1, 4], 6, 9, 10, 12, 13, 14, 16, 17, [19, 31], 50, 52, 53], true ] // type
+ );
+
+ protected function createSQLForValues() : array
+ {
+ $parts = [];
+ $_v = &$this->values;
+
+ // name [str]
+ if ($_v['na'])
+ if ($_ = $this->buildLikeLookup([['na', 'name']]))
+ $parts[] = $_;
+
+ // type [list]
+ if ($_v['ty'])
+ $parts[] = ['cat', $_v['ty']];
+
+ return $parts;
+ }
+}
+
+?>
diff --git a/includes/dbtypes/spell.class.php b/includes/dbtypes/spell.class.php
new file mode 100644
index 00000000..6aefde19
--- /dev/null
+++ b/includes/dbtypes/spell.class.php
@@ -0,0 +1,2879 @@
+ [ 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 => SKILLS_TRADE_SECONDARY, // sec. Professions
+ 10 => [ 98, 109, 111, 113, 115, 137, 138, 139, 140, 141, 313, 315, 673, 759], // Languages
+ 11 => SKILLS_TRADE_PRIMARY // prim. Professions
+ );
+
+ public const EFFECTS_SCALING_HEAL = array( // as per Unit::SpellHealingBonusDone() calls in TC
+ SPELL_EFFECT_HEAL, SPELL_EFFECT_HEAL_PCT, SPELL_EFFECT_HEAL_MECHANICAL, SPELL_EFFECT_HEALTH_LEECH
+ );
+ public const EFFECTS_SCALING_DAMAGE = array( // as per Unit::SpellDamageBonusDone() calls in TC
+ SPELL_EFFECT_SCHOOL_DAMAGE, SPELL_EFFECT_HEALTH_LEECH, SPELL_EFFECT_POWER_BURN
+ );
+ public const EFFECTS_LDC_SCALING = array(
+ SPELL_EFFECT_SCHOOL_DAMAGE, SPELL_EFFECT_DUMMY, SPELL_EFFECT_POWER_DRAIN, SPELL_EFFECT_HEALTH_LEECH, SPELL_EFFECT_HEAL,
+ SPELL_EFFECT_WEAPON_DAMAGE, SPELL_EFFECT_POWER_BURN, SPELL_EFFECT_SCRIPT_EFFECT, SPELL_EFFECT_NORMALIZED_WEAPON_DMG, SPELL_EFFECT_FORCE_CAST_WITH_VALUE,
+ SPELL_EFFECT_TRIGGER_SPELL_WITH_VALUE, SPELL_EFFECT_TRIGGER_MISSILE_SPELL_WITH_VALUE
+ );
+ public const EFFECTS_ITEM_CREATE = array(
+ SPELL_EFFECT_CREATE_ITEM, SPELL_EFFECT_SUMMON_CHANGE_ITEM, SPELL_EFFECT_CREATE_RANDOM_ITEM, SPELL_EFFECT_CREATE_MANA_GEM, SPELL_EFFECT_CREATE_ITEM_2
+ );
+ public const EFFECTS_TRIGGER = array(
+ SPELL_EFFECT_DUMMY, SPELL_EFFECT_TRIGGER_MISSILE, SPELL_EFFECT_TRIGGER_SPELL, SPELL_EFFECT_FEED_PET, SPELL_EFFECT_FORCE_CAST,
+ SPELL_EFFECT_FORCE_CAST_WITH_VALUE, SPELL_EFFECT_TRIGGER_SPELL_WITH_VALUE, SPELL_EFFECT_TRIGGER_MISSILE_SPELL_WITH_VALUE, SPELL_EFFECT_TRIGGER_SPELL_2, SPELL_EFFECT_SUMMON_RAF_FRIEND,
+ SPELL_EFFECT_TITAN_GRIP, SPELL_EFFECT_FORCE_CAST_2, SPELL_EFFECT_REMOVE_AURA
+ );
+ public const EFFECTS_TEACH = array(
+ SPELL_EFFECT_LEARN_SPELL, SPELL_EFFECT_LEARN_PET_SPELL /*SPELL_EFFECT_UNLEARN_SPECIALIZATION*/
+ );
+ public const EFFECTS_MODEL_OBJECT = array(
+ SPELL_EFFECT_TRANS_DOOR, SPELL_EFFECT_SUMMON_OBJECT_WILD, SPELL_EFFECT_SUMMON_OBJECT_SLOT1, SPELL_EFFECT_SUMMON_OBJECT_SLOT2, SPELL_EFFECT_SUMMON_OBJECT_SLOT3,
+ SPELL_EFFECT_SUMMON_OBJECT_SLOT4
+ );
+ public const EFFECTS_MODEL_NPC = array(
+ SPELL_EFFECT_SUMMON, SPELL_EFFECT_SUMMON_PET, SPELL_EFFECT_SUMMON_DEMON, SPELL_EFFECT_KILL_CREDIT, SPELL_EFFECT_KILL_CREDIT2
+ );
+ public const EFFECTS_DIRECT_SCALING = array( // as per Unit::GetCastingTimeForBonus()
+ SPELL_EFFECT_SCHOOL_DAMAGE, SPELL_EFFECT_ENVIRONMENTAL_DAMAGE, SPELL_EFFECT_POWER_DRAIN, SPELL_EFFECT_HEALTH_LEECH, SPELL_EFFECT_POWER_BURN,
+ SPELL_EFFECT_HEAL
+ );
+ public const EFFECTS_ENCHANTMENT = array(
+ SPELL_EFFECT_ENCHANT_ITEM, SPELL_EFFECT_ENCHANT_ITEM_TEMPORARY, SPELL_EFFECT_ENCHANT_HELD_ITEM, SPELL_EFFECT_ENCHANT_ITEM_PRISMATIC
+ );
+
+ public const AURAS_SCALING_HEAL = array( // as per Unit::SpellHealingBonusDone() calls in TC (SPELL_AURA_SCHOOL_ABSORB + SPELL_AURA_MANA_SHIELD priest/mage cases are scripted)
+ SPELL_AURA_PERIODIC_HEAL, SPELL_AURA_PERIODIC_LEECH, SPELL_AURA_OBS_MOD_HEALTH
+ );
+ public const AURAS_SCALING_DAMAGE = array( // as per Unit::SpellDamageBonusDone() calls in TC
+ SPELL_AURA_PERIODIC_DAMAGE, SPELL_AURA_PERIODIC_LEECH, SPELL_AURA_DAMAGE_SHIELD, SPELL_AURA_PROC_TRIGGER_DAMAGE
+ );
+ public const AURAS_LDC_SCALING = array(
+ SPELL_AURA_PERIODIC_DAMAGE, SPELL_AURA_DUMMY, SPELL_AURA_PERIODIC_HEAL, SPELL_AURA_DAMAGE_SHIELD, SPELL_AURA_PROC_TRIGGER_DAMAGE,
+ SPELL_AURA_PERIODIC_LEECH, SPELL_AURA_PERIODIC_MANA_LEECH, SPELL_AURA_SCHOOL_ABSORB, SPELL_AURA_PERIODIC_TRIGGER_SPELL_WITH_VALUE
+ );
+ public const AURAS_ITEM_CREATE = array(
+ SPELL_AURA_CHANNEL_DEATH_ITEM
+ );
+ public const AURAS_TRIGGER = array(
+ SPELL_AURA_DUMMY, SPELL_AURA_PERIODIC_TRIGGER_SPELL, SPELL_AURA_PROC_TRIGGER_SPELL, SPELL_AURA_PERIODIC_TRIGGER_SPELL_FROM_CLIENT, SPELL_AURA_ADD_TARGET_TRIGGER,
+ SPELL_AURA_PERIODIC_DUMMY, SPELL_AURA_PERIODIC_TRIGGER_SPELL_WITH_VALUE, SPELL_AURA_PROC_TRIGGER_SPELL_WITH_VALUE, SPELL_AURA_CONTROL_VEHICLE, SPELL_AURA_LINKED
+ );
+ public const AURAS_MODEL_NPC = array(
+ SPELL_AURA_TRANSFORM, SPELL_AURA_MOUNTED, SPELL_AURA_CHANGE_MODEL_FOR_ALL_HUMANOIDS, SPELL_AURA_X_RAY,
+ SPELL_AURA_MOD_FAKE_INEBRIATE
+ );
+ public const AURAS_PERIODIC_SCALING = array( // as per Unit::GetCastingTimeForBonus()
+ SPELL_AURA_PERIODIC_DAMAGE, SPELL_AURA_PERIODIC_HEAL, SPELL_AURA_PERIODIC_LEECH
+ );
+
+ private array $spellVars = [];
+ private array $refSpells = [];
+ private array $tools = [];
+ private int $interactive = self::INTERACTIVE_EMBEDDED;
+ private int $charLevel = MAX_LEVEL;
+ private array $scaling = [];
+ private array $parsedText = [];
+ private static array $spellTypes = array(
+ 6 => 1,
+ 8 => 2,
+ 10 => 4
+ );
+
+ protected string $queryBase = 'SELECT s.*, s.`id` AS ARRAY_KEY FROM ::spell s';
+ protected array $queryOpts = array(
+ 's' => [['src', 'sr', 'ic', 'ica']], // 6: Type::SPELL
+ 'nml' => ['j' => ['::spell_search nml ON nml.`id` = s.`id` AND nml.`locale` = DB_LOC_I']],
+ 'ic' => ['j' => ['::icons ic ON ic.`id` = s.`iconId`', true], 's' => ', ic.`name` AS "iconString"'],
+ 'ica' => ['j' => ['::icons ica ON ica.`id` = s.`iconIdAlt`', true], 's' => ', ica.`name` AS "iconStringAlt"'],
+ 'sr' => ['j' => ['::spellrange sr ON sr.`id` = s.`rangeId`'], 's' => ', sr.`rangeMinHostile`, sr.`rangeMinFriend`, sr.`rangeMaxHostile`, sr.`rangeMaxFriend`, sr.`name_loc0` AS "rangeText_loc0", sr.`name_loc2` AS "rangeText_loc2", sr.`name_loc3` AS "rangeText_loc3", sr.`name_loc4` AS "rangeText_loc4", sr.`name_loc6` AS "rangeText_loc6", sr.`name_loc8` AS "rangeText_loc8"'],
+ 'src' => ['j' => ['::source src ON `type` = 6 AND `typeId` = s.`id`', true], 's' => ', `moreType`, `moreTypeId`, `moreZoneId`, `moreMask`, `src1`, `src2`, `src3`, `src4`, `src5`, `src6`, `src7`, `src8`, `src9`, `src10`, `src11`, `src12`, `src13`, `src14`, `src15`, `src16`, `src17`, `src18`, `src19`, `src20`, `src21`, `src22`, `src23`, `src24`']
+ );
+
+ public function __construct(array $conditions = [], array $miscData = [])
+ {
+ parent::__construct($conditions, $miscData);
+
+ if ($this->error)
+ return;
+
+ if (isset($miscData['interactive']))
+ $this->interactive = $miscData['interactive'];
+
+ if (isset($miscData['charLevel']))
+ $this->charLevel = $miscData['charLevel'];
+
+ // post processing
+ $foo = DB::World()->selectCol('SELECT `perfectItemType` FROM skill_perfect_item_template WHERE `spellId` IN %in', $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'] &= ChrClass::MASK_ALL;
+ if ($_curTpl['reqClassMask'] == ChrClass::MASK_ALL)
+ $_curTpl['reqClassMask'] = 0;
+
+ $_curTpl['reqRaceMask'] &= ChrRace::MASK_ALL;
+ if ($_curTpl['reqRaceMask'] == ChrRace::MASK_ALL)
+ $_curTpl['reqRaceMask'] = 0;
+
+ // unpack skillLines
+ $_curTpl['skillLines'] = [];
+ if ($_curTpl['skillLine1'] < 0)
+ {
+ foreach (Game::$skillLineMask[$_curTpl['skillLine1']] as $idx => [, $skillLineId])
+ if ($_curTpl['skillLine2OrMask'] & (1 << $idx))
+ $_curTpl['skillLines'][] = $skillLineId;
+ }
+ 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']);
+
+ // fix missing icons
+ $_curTpl['iconString'] = $_curTpl['iconString'] ?: DEFAULT_ICON;
+
+ $this->scaling[$this->id] = false;
+ }
+
+ if ($foo)
+ $this->relItems = new ItemList(array(['i.id', array_unique($foo)]));
+ }
+
+ // required for item-comparison
+ public function getStatGain() : array
+ {
+ $data = [];
+
+ foreach ($this->iterate() as $__)
+ {
+ $data[$this->id] = new StatsContainer();
+
+ foreach ($this->canEnchantmentItem() as $i)
+ $data[$this->id]->fromDB(Type::ENCHANTMENT, $this->curTpl['effect'.$i.'MiscValue']);
+
+ // todo: should enchantments be included here...?
+ $data[$this->id]->fromSpell($this->curTpl);
+ }
+
+ return $data;
+ }
+
+ public function getProfilerMods() : array
+ {
+ // 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 = []; // flat gains
+ foreach ($this->getStatGain() as $id => $spellData)
+ {
+ $data[$id] = $spellData->toJson(STAT::FLAG_ITEM | STAT::FLAG_PROFILER, false);
+
+ // 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 ($data[$id] as $key => $amt)
+ $data[$id][$key] = [1, 'functionOf', sprintf($whCheck, $slot, $class, $subClass, $amt)];
+ }
+
+ // 4 possible modifiers found
+ // => [0.15, 'functionOf', ]
+ // => [0.33, 'percentOf', ]
+ // => [123, 'add']
+ // => ... as from getStatGain()
+
+ $modXByStat = function (array &$arr, int $srcStat, ?string $destStat, int $pts) : void
+ {
+ match ($srcStat)
+ {
+ STAT_STRENGTH => $arr[$destStat ?: 'str'] = [$pts / 100, 'percentOf', 'str'],
+ STAT_AGILITY => $arr[$destStat ?: 'agi'] = [$pts / 100, 'percentOf', 'agi'],
+ STAT_STAMINA => $arr[$destStat ?: 'sta'] = [$pts / 100, 'percentOf', 'sta'],
+ STAT_INTELLECT => $arr[$destStat ?: 'int'] = [$pts / 100, 'percentOf', 'int'],
+ STAT_SPIRIT => $arr[$destStat ?: 'spi'] = [$pts / 100, 'percentOf', 'spi']
+ };
+ };
+
+ $modXBySchool = function (array &$arr, int $srcStat, string $destStat, array|int $val) : void
+ {
+ if ($srcStat & (1 << SPELL_SCHOOL_HOLY))
+ $arr['hol'.$destStat] = is_array($val) ? $val : [$val / 100, 'percentOf', 'hol'.$destStat];
+ if ($srcStat & (1 << SPELL_SCHOOL_FIRE))
+ $arr['fir'.$destStat] = is_array($val) ? $val : [$val / 100, 'percentOf', 'fir'.$destStat];
+ if ($srcStat & (1 << SPELL_SCHOOL_NATURE))
+ $arr['nat'.$destStat] = is_array($val) ? $val : [$val / 100, 'percentOf', 'nat'.$destStat];
+ if ($srcStat & (1 << SPELL_SCHOOL_FROST))
+ $arr['fro'.$destStat] = is_array($val) ? $val : [$val / 100, 'percentOf', 'fro'.$destStat];
+ if ($srcStat & (1 << SPELL_SCHOOL_SHADOW))
+ $arr['sha'.$destStat] = is_array($val) ? $val : [$val / 100, 'percentOf', 'sha'.$destStat];
+ if ($srcStat & (1 << SPELL_SCHOOL_ARCANE))
+ $arr['arc'.$destStat] = is_array($val) ? $val : [$val / 100, 'percentOf', 'arc'.$destStat];
+ };
+
+ $jsonStat = function (int $statId) : string
+ {
+ return match ($statId)
+ {
+ STAT_STRENGTH => Stat::getJsonString(Stat::STRENGTH),
+ STAT_AGILITY => Stat::getJsonString(Stat::AGILITY),
+ STAT_STAMINA => Stat::getJsonString(Stat::STAMINA),
+ STAT_INTELLECT => Stat::getJsonString(Stat::INTELLECT),
+ STAT_SPIRIT => Stat::getJsonString(Stat::SPIRIT)
+ };
+ };
+
+ foreach ($this->iterate() as $id => $__)
+ {
+ // Shaman - Spirit Weapons (16268) (parry is normaly stored in g_statistics)
+ // i should recurse into SPELL_EFFECT_LEARN_SPELL and apply SPELL_EFFECT_PARRY from there
+ if ($id == 16268)
+ {
+ $data[$id]['parrypct'] = [5, 'add'];
+ continue;
+ }
+
+ if (!($this->getField('attributes0') & SPELL_ATTR0_PASSIVE))
+ 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 SPELL_AURA_MOD_RESISTANCE_PCT:
+ case SPELL_AURA_MOD_BASE_RESISTANCE_PCT:
+ // Armor only if explicitly specified only affects armor from equippment
+ if ($mv == (1 << SPELL_SCHOOL_NORMAL))
+ $data[$id]['armor'] = [$pts / 100, 'percentOf', ['armor', 0]];
+ else if ($mv)
+ $modXBySchool($data[$id], $mv, 'res', $pts);
+ break;
+ case SPELL_AURA_MOD_RESISTANCE_OF_STAT_PERCENT:
+ // Armor only if explicitly specified
+ if ($mv == (1 << SPELL_SCHOOL_NORMAL))
+ $data[$id]['armor'] = [$pts / 100, 'percentOf', $jsonStat($mvB)];
+ else if ($mv)
+ $modXBySchool($data[$id], $mv, 'res', [$pts / 100, 'percentOf', $jsonStat($mvB)]);
+ break;
+ case SPELL_AURA_MOD_TOTAL_STAT_PERCENTAGE:
+ if ($mv > -1) // one stat
+ $modXByStat($data[$id], $mv, null, $pts);
+ else if ($mv < 0) // all stats
+ for ($iMod = ITEM_MOD_AGILITY; $iMod <= ITEM_MOD_STAMINA; $iMod++)
+ if ($idx = Stat::getIndexFrom(Stat::IDX_ITEM_MOD, $iMod))
+ if ($key = Stat::getJsonString($idx))
+ $data[$id][$key] = [$pts / 100, 'percentOf', $key];
+ break;
+ case SPELL_AURA_MOD_SPELL_DAMAGE_OF_STAT_PERCENT:
+ $mv = $mv ?: SPELL_MAGIC_SCHOOLS;
+ $modXBySchool($data[$id], $mv, 'spldmg', [$pts / 100, 'percentOf', $jsonStat($mvB)]);
+ break;
+ case SPELL_AURA_MOD_RANGED_ATTACK_POWER_OF_STAT_PERCENT:
+ $modXByStat($data[$id], $mv, 'rgdatkpwr', $pts);
+ break;
+ case SPELL_AURA_MOD_ATTACK_POWER_OF_STAT_PERCENT:
+ $modXByStat($data[$id], $mv, 'mleatkpwr', $pts);
+ break;
+ case SPELL_AURA_MOD_SPELL_HEALING_OF_STAT_PERCENT:
+ $modXByStat($data[$id], $mv, 'splheal', $pts);
+ break;
+ case SPELL_AURA_MOD_MANA_REGEN_FROM_STAT:
+ $modXByStat($data[$id], $mv, 'manargn', $pts);
+ break;
+ case SPELL_AURA_MOD_MANA_REGEN_INTERRUPT:
+ $data[$id]['icmanargn'] = [$pts / 100, 'percentOf', 'oocmanargn'];
+ break;
+ case SPELL_AURA_MOD_SPELL_CRIT_CHANCE:
+ case SPELL_AURA_MOD_SPELL_CRIT_CHANCE_SCHOOL:
+ $mv = $mv ?: SPELL_MAGIC_SCHOOLS;
+ $modXBySchool($data[$id], $mv, 'splcritstrkpct', [$pts, 'add']);
+ if (($mv & SPELL_MAGIC_SCHOOLS) == SPELL_MAGIC_SCHOOLS)
+ $data[$id]['splcritstrkpct'] = [$pts, 'add'];
+ break;
+ case SPELL_AURA_MOD_ATTACK_POWER_OF_ARMOR:
+ $data[$id]['mleatkpwr'] = [1 / $pts, 'percentOf', 'fullarmor'];
+ $data[$id]['rgdatkpwr'] = [1 / $pts, 'percentOf', 'fullarmor'];
+ break;
+ case SPELL_AURA_MOD_WEAPON_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 SPELL_AURA_MOD_PARRY_PERCENT:
+ $data[$id]['parrypct'] = [$pts, 'add'];
+ break;
+ case SPELL_AURA_MOD_DODGE_PERCENT:
+ $data[$id]['dodgepct'] = [$pts, 'add'];
+ break;
+ case SPELL_AURA_MOD_BLOCK_PERCENT:
+ $data[$id]['blockpct'] = [$pts, 'add'];
+ break;
+ case SPELL_AURA_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 SPELL_AURA_MOD_INCREASE_HEALTH_PERCENT:
+ $data[$id]['health'] = [$pts / 100, 'percentOf', 'health'];
+ break;
+ case SPELL_AURA_MOD_BASE_HEALTH_PCT: // only Tauren - Endurance (20550) ... if you are looking for something elegant, look away!
+ $data[$id]['health'] = [$pts / 100, 'functionOf', '$(x) => g_statistics.combo[x.classs][x.level][5]'];
+ break;
+ case SPELL_AURA_MOD_SHIELD_BLOCKVALUE_PCT:
+ $data[$id]['block'] = [$pts / 100, 'percentOf', 'block'];
+ break;
+ case SPELL_AURA_MOD_CRIT_PCT:
+ $data[$id]['mlecritstrkpct'] = [$pts, 'add'];
+ $data[$id]['rgdcritstrkpct'] = [$pts, 'add'];
+ $data[$id]['splcritstrkpct'] = [$pts, 'add'];
+ break;
+ case SPELL_AURA_MOD_SPELL_DAMAGE_OF_ATTACK_POWER:
+ $mv = $mv ?: SPELL_MAGIC_SCHOOLS;
+ $modXBySchool($data[$id], $mv, 'spldmg', [$pts / 100, 'percentOf', 'mleatkpwr']);
+ break;
+ case SPELL_AURA_MOD_SPELL_HEALING_OF_ATTACK_POWER:
+ $data[$id]['splheal'] = [$pts / 100, 'percentOf', 'mleatkpwr'];
+ break;
+ case SPELL_AURA_MOD_ATTACK_POWER_PCT: // ingame only melee..?
+ $data[$id]['mleatkpwr'] = [$pts / 100, 'percentOf', 'mleatkpwr'];
+ break;
+ case SPELL_AURA_MOD_HEALTH_REGEN_PERCENT:
+ $data[$id]['healthrgn'] = [$pts / 100, 'percentOf', 'healthrgn'];
+ break;
+ }
+ }
+ }
+
+ return $data;
+ }
+
+ // halper
+ public function getReagentsForCurrent() : array
+ {
+ $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() : array
+ {
+ 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` = %i', $_);
+ $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(int $spellId = 0, int $effIdx = 0) : array
+ {
+ $displays = $results = [];
+
+ foreach ($this->iterate() as $id => $__)
+ {
+ if ($spellId && $spellId != $id)
+ continue;
+
+ for ($i = 1; $i < 4; $i++)
+ {
+ if ($spellId && $effIdx && $effIdx != $i)
+ continue;
+
+ $effMV = $this->curTpl['effect'.$i.'MiscValue'];
+ if (!$effMV)
+ continue;
+
+ // GO Model from MiscVal
+ if (in_array($this->curTpl['effect'.$i.'Id'], SpellList::EFFECTS_MODEL_OBJECT))
+ {
+ 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'], SpellList::EFFECTS_MODEL_NPC) || in_array($this->curTpl['effect'.$i.'AuraId'], SpellList::AURAS_MODEL_NPC))
+ {
+ 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'] == SPELL_AURA_MOD_SHAPESHIFT)
+ {
+ $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` = %i', $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]])];
+
+ $results[$id][$i] = array(
+ 'type' => Type::NPC,
+ 'creatureType' => $st['creatureType'],
+ 'displayId' => $st['model2'] ? $st['model'.rand(1, 2)] : $st['model1'],
+ 'displayName' => Lang::game('st', $effMV)
+ );
+ }
+ }
+ }
+ }
+
+ if (!empty($displays[Type::NPC]))
+ {
+ $nModels = new CreatureList(array(['id', array_column($displays[Type::NPC], 1)]));
+ foreach ($nModels->iterate() as $nId => $__)
+ {
+ foreach ($displays[Type::NPC] as $srcId => [$indizes, $npcId])
+ {
+ if ($npcId == $nId)
+ {
+ foreach ($indizes as $idx)
+ {
+ $res = array(
+ 'type' => Type::NPC,
+ 'typeId' => $nId,
+ 'displayId' => $nModels->getRandomModelId(),
+ 'displayName' => $nModels->getField('name', true)
+ );
+
+ if ($nModels->getField('humanoid'))
+ $res['humanoid'] = 1;
+
+ $results[$srcId][$idx] = $res;
+ }
+ }
+ }
+ }
+ }
+
+ if (!empty($displays[Type::OBJECT]))
+ {
+ $oModels = new GameObjectList(array(['id', array_column($displays[Type::OBJECT], 1)]));
+ foreach ($oModels->iterate() as $oId => $__)
+ {
+ foreach ($displays[Type::OBJECT] as $srcId => [$indizes, $objId])
+ {
+ if ($objId == $oId)
+ {
+ foreach ($indizes as $idx)
+ {
+ $results[$srcId][$idx] = array(
+ 'type' => Type::OBJECT,
+ 'typeId' => $oId,
+ 'displayId' => $oModels->getField('displayId'),
+ 'displayName' => $oModels->getField('name', true)
+ );
+ }
+ }
+ }
+ }
+ }
+
+ if ($spellId && $effIdx)
+ return $results[$spellId][$effIdx] ?? [];
+
+ if ($spellId)
+ return $results[$spellId] ?? [];
+
+ return $results;
+ }
+
+ private function createRangesForCurrent() : string
+ {
+ if (!$this->curTpl['rangeMaxHostile'])
+ return '';
+
+ if ($this->curTpl['attributes3'] & SPELL_ATTR3_DONT_DISPLAY_RANGE)
+ return '';
+
+ // minRange exists; show as range
+ if ($this->curTpl['rangeMinHostile'])
+ return Lang::spell('range', [$this->curTpl['rangeMinHostile'].' - '.$this->curTpl['rangeMaxHostile']]);
+ // friend and hostile differ; do color
+ else if ($this->curTpl['rangeMaxHostile'] != $this->curTpl['rangeMaxFriend'])
+ return 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 Lang::spell('range', [$this->curTpl['rangeMaxHostile']]);
+ }
+
+ public function createPowerCostForCurrent() : string
+ {
+ $str = '';
+
+ $pt = $this->curTpl['powerType'];
+ $pc = $this->curTpl['powerCost'];
+ $pcp = $this->curTpl['powerCostPercent'];
+ $pps = $this->curTpl['powerPerSecond'];
+ $pcpl = $this->curTpl['powerCostPerLevel'];
+
+ // some potion effects have this set, but it's not displayed by client (or enforced by core)
+ if ($pt == POWER_HAPPINESS)
+ return '';
+
+ if ($pt == POWER_RAGE || $pt == POWER_RUNIC_POWER)
+ $pc /= 10;
+
+ if ($pt == POWER_RUNE && ($rCost = ($this->curTpl['powerCostRunes'] & 0x333)))
+ { // Blood 2|1 - Unholy 2|1 - Frost 2|1
+ $runes = [];
+
+ for ($i = 0; $i < 3; $i++)
+ {
+ if ($rCost & 0x3)
+ $runes[] = Lang::spell('powerCostRunes', $i, [$rCost & 0x3]);
+
+ $rCost >>= 4;
+ }
+
+ $str .= implode(' ', $runes);
+ }
+ else if ($pcp > 0) // power cost: pct over static
+ $str .= $pcp."% ".Lang::spell('pctCostOf', [mb_strtolower(Lang::spell('powerTypes', $pt))]);
+ else if ($pc > 0 || $pps > 0 || $pcpl > 0)
+ {
+ if ($this->curTpl['attributes0'] & SPELL_ATTR0_LEVEL_DAMAGE_CALCULATION)
+ $str .= '';
+
+ if (Lang::exist('spell', 'powerCost', $pt))
+ $str .= Lang::spell('powerCost', $pt, intVal($pps > 0), [$pc, $pps]);
+ else
+ $str .= Lang::spell('powerDisplayCost', intVal($pps > 0), [$pc, Lang::spell('powerTypes', $pt), $pps]);
+ }
+
+ // append level cost (todo (low): work in as scaling cost)
+ if ($pcpl > 0)
+ $str .= Lang::spell('costPerLevel', [$pcpl]);
+
+ return $str;
+ }
+
+ public function createCastTimeForCurrent(bool $short = true, bool $noInstant = true) : string
+ {
+ if (!$this->curTpl['castTime'] && $this->isChanneledSpell())
+ return Lang::spell('channeled');
+ // SPELL_ATTR0_ABILITY instant ability.. yeah, wording thing only (todo (low): rule is imperfect)
+ else if (!$this->curTpl['castTime'] && ($this->curTpl['damageClass'] != SPELL_DAMAGE_CLASS_MAGIC || $this->curTpl['attributes0'] & SPELL_ATTR0_ABILITY))
+ return Lang::spell('instantPhys');
+ // 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 '';
+ else
+ return $short ? Lang::formatTime($this->curTpl['castTime'] * 1000, 'spell', 'castTime') : DateTime::formatTimeElapsedFloat($this->curTpl['castTime'] * 1000);
+ }
+
+ private function createCooldownForCurrent() : string
+ {
+ if ($this->curTpl['attributes6'] & SPELL_ATTR6_DONT_DISPLAY_COOLDOWN)
+ return '';
+ else if ($this->curTpl['recoveryTime'])
+ return Lang::formatTime($this->curTpl['recoveryTime'], 'spell', 'cooldown');
+ else if ($this->curTpl['recoveryCategory'])
+ return Lang::formatTime($this->curTpl['recoveryCategory'], 'spell', 'cooldown');
+
+ return '';
+ }
+
+ // formulae base from TC
+ private function calculateAmountForCurrent(int $effIdx, int $nTicks = 1) : array
+ {
+ $level = $this->charLevel;
+ $maxBase = 0;
+ $rppl = $this->getField('effect'.$effIdx.'RealPointsPerLevel');
+ $base = $this->getField('effect'.$effIdx.'BasePoints');
+ $add = $this->getField('effect'.$effIdx.'DieSides');
+ $maxLvl = $this->getField('maxLevel');
+ $baseLvl = $this->getField('baseLevel');
+ $spellLvl = $this->getField('spellLevel');
+ $LDSEffs = $this->canLevelDamageScale();
+ $modMin =
+ $modMax = null;
+
+ if ($rppl)
+ {
+ if ($level > $maxLvl && $maxLvl > 0)
+ $level = $maxLvl;
+ else if ($level < $baseLvl)
+ $level = $baseLvl;
+
+ if (!$this->getField('attributes0') & SPELL_ATTR0_PASSIVE)
+ $level -= $spellLvl;
+
+ $maxBase += (int)(($level - $baseLvl) * $rppl);
+ $maxBase *= $nTicks;
+
+ }
+
+ $min = $nTicks * ($add ? $base + 1 : $base);
+ $max = $nTicks * ($add + $base);
+
+ if ($rppl)
+ {
+ $modMin = '';
+ $modMax = '';
+ }
+ else if ($this->getField('attributes0') & SPELL_ATTR0_LEVEL_DAMAGE_CALCULATION && in_array($effIdx, $LDSEffs) && $spellLvl)
+ {
+ $modMin = '';
+ $modMax = '';
+ }
+
+ return [$min + $maxBase, $max + $maxBase, $modMin, $modMax];
+ }
+
+ public function canCreateItem() : array
+ {
+ $idx = [];
+ for ($i = 1; $i < 4; $i++)
+ if (in_array($this->curTpl['effect'.$i.'Id'], SpellList::EFFECTS_ITEM_CREATE) || in_array($this->curTpl['effect'.$i.'AuraId'], SpellList::AURAS_ITEM_CREATE))
+ if ($this->curTpl['effect'.$i.'CreateItemId'] > 0)
+ $idx[] = $i;
+
+ return $idx;
+ }
+
+ public function canTriggerSpell() : array
+ {
+ $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.'AuraId'] == SPELL_AURA_DUMMY || $this->curTpl['effect'.$i.'TriggerSpell'] > 0 || ($this->curTpl['effect'.$i.'Id'] == SPELL_EFFECT_TITAN_GRIP && $this->curTpl['effect'.$i.'MiscValue'] > 0))
+ $idx[] = $i;
+
+ return $idx;
+ }
+
+ public function canTeachSpell() : array
+ {
+ $idx = [];
+ for ($i = 1; $i < 4; $i++)
+ if (in_array($this->curTpl['effect'.$i.'Id'], SpellList::EFFECTS_TEACH))
+ if ($this->curTpl['effect'.$i.'TriggerSpell'] > 0)
+ $idx[] = $i;
+
+ return $idx;
+ }
+
+ public function canEnchantmentItem() : array
+ {
+ $idx = [];
+ for ($i = 1; $i < 4; $i++)
+ if (in_array($this->curTpl['effect'.$i.'Id'], SpellList::EFFECTS_ENCHANTMENT))
+ $idx[] = $i;
+
+ return $idx;
+ }
+
+ public function canLevelDamageScale() : array
+ {
+ $idx = [];
+ for ($i = 1; $i < 4; $i++)
+ if (in_array($this->curTpl['effect'.$i.'Id'], SpellList::EFFECTS_LDC_SCALING) || in_array($this->curTpl['effect'.$i.'AuraId'], SpellList::AURAS_LDC_SCALING))
+ $idx[] = $i;
+
+ return $idx;
+ }
+
+ public function isChanneledSpell() : bool
+ {
+ return $this->curTpl['attributes1'] & (SPELL_ATTR1_CHANNELED_1 | SPELL_ATTR1_CHANNELED_2);
+ }
+
+ public function isScalableHealingSpell() : bool
+ {
+ for ($i = 1; $i < 4; $i++)
+ if (in_array($this->curTpl['effect'.$i.'Id'], SpellList::EFFECTS_SCALING_HEAL) || in_array($this->curTpl['effect'.$i.'AuraId'], SpellList::AURAS_SCALING_HEAL))
+ return true;
+
+ return false;
+ }
+
+ public function isScalableDamagingSpell() : bool
+ {
+ for ($i = 1; $i < 4; $i++)
+ if (in_array($this->curTpl['effect'.$i.'Id'], SpellList::EFFECTS_SCALING_DAMAGE) || in_array($this->curTpl['effect'.$i.'AuraId'], SpellList::AURAS_SCALING_DAMAGE))
+ return true;
+
+ return false;
+ }
+
+ public function periodicEffectsMask() : int
+ {
+ $effMask = 0x0;
+
+ for ($i = 1; $i < 4; $i++)
+ if ($this->curTpl['effect'.$i.'Periode'] > 0)
+ $effMask |= 1 << ($i - 1);
+
+ return $effMask;
+ }
+
+ private function dfnText(string $tooltip, string $text) : string
+ {
+ if ($this->interactive < self::INTERACTIVE_FULL)
+ return $text;
+
+ return sprintf(Util::$dfnString, $tooltip, $text);
+ }
+
+ // description-, buff-parsing component
+ private function resolveEvaluation(string $formula) : string
+ {
+ // see Traits in javascript locales
+
+ $PlayerName = Lang::main('name');
+ $pl = $PL = /* playerLevel set manually ? $this->charLevel : */ $this->dfnText('LANG.level', Lang::game('level'));
+ $ap = $AP = $this->dfnText('LANG.traits.atkpwr[0]', Lang::spell('traitShort', 'atkpwr'));
+ $rap = $RAP = $this->dfnText('LANG.traits.rgdatkpwr[0]', Lang::spell('traitShort', 'rgdatkpwr'));
+ $sp = $SP = $this->dfnText('LANG.traits.splpwr[0]', Lang::spell('traitShort', 'splpwr'));
+ $spa = $SPA = $this->dfnText('LANG.traits.arcsplpwr[0]', Lang::spell('traitShort', 'arcsplpwr'));
+ $spfi = $SPFI = $this->dfnText('LANG.traits.firsplpwr[0]', Lang::spell('traitShort', 'firsplpwr'));
+ $spfr = $SPFR = $this->dfnText('LANG.traits.frosplpwr[0]', Lang::spell('traitShort', 'frosplpwr'));
+ $sph = $SPH = $this->dfnText('LANG.traits.holsplpwr[0]', Lang::spell('traitShort', 'holsplpwr'));
+ $spn = $SPN = $this->dfnText('LANG.traits.natsplpwr[0]', Lang::spell('traitShort', 'natsplpwr'));
+ $sps = $SPS = $this->dfnText('LANG.traits.shasplpwr[0]', Lang::spell('traitShort', 'shasplpwr'));
+ $bh = $BH = $this->dfnText('LANG.traits.splheal[0]', Lang::spell('traitShort', 'splheal'));
+ $spi = $SPI = $this->dfnText('LANG.traits.spi[0]', Lang::spell('traitShort', 'spi'));
+ $sta = $STA = $this->dfnText('LANG.traits.sta[0]', Lang::spell('traitShort', 'sta'));
+ $str = $STR = $this->dfnText('LANG.traits.str[0]', Lang::spell('traitShort', 'str'));
+ $agi = $AGI = $this->dfnText('LANG.traits.agi[0]', Lang::spell('traitShort', 'agi'));
+ $int = $INT = $this->dfnText('LANG.traits.int[0]', 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->dfnText('[Hands required by weapon]', 'HND'); // todo (med): localize this one
+ $MWS = $mws = $this->dfnText('LANG.traits.mlespeed[0]', 'MWS');
+ $mw = $this->dfnText('LANG.traits.dmgmin1[0]', 'mw');
+ $MW = $this->dfnText('LANG.traits.dmgmax1[0]', 'MW');
+ $mwb = $this->dfnText('LANG.traits.mledmgmin[0]', 'mwb');
+ $MWB = $this->dfnText('LANG.traits.mledmgmax[0]', 'MWB');
+ $rwb = $this->dfnText('LANG.traits.rgddmgmin[0]', 'rwb');
+ $RWB = $this->dfnText('LANG.traits.rgddmgmax[0]', 'RWB');
+
+ $cond = $COND = fn($a, $b, $c) => $a ? $b : $c;
+ $eq = $EQ = fn($a, $b) => $a == $b;
+ $gt = $GT = fn($a, $b) => $a > $b;
+ $gte = $GTE = fn($a, $b) => $a >= $b;
+ $floor = $FLOOR = fn($a) => floor($a);
+ $max = $MAX = fn($a, $b) => max($a, $b);
+ $min = $MIN = fn($a, $b) => 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->dfnText('COND(a, b, c) a ? b : c', 'COND');
+ $eq = $EQ = $this->dfnText('EQ(a, b) a == b', 'EQ');
+ $gt = $GT = $this->dfnText('GT(a, b) a > b', 'GT');
+ $gte = $GTE = $this->dfnText('GTE(a, b) a >= b', 'GTE');
+ $floor = $FLOOR = $this->dfnText('FLOOR(a)', 'FLOOR');
+ $min = $MIN = $this->dfnText('MIN(a, b)', 'MIN');
+ $max = $MAX = $this->dfnText('MAX(a, b)', 'MAX');
+ $pl = $PL = $this->dfnText('LANG.level', 'PL');
+
+ // space out operators for better readability
+ $formula = preg_replace('/(\+|-|\*|\/)/', ' \1 ', $formula);
+
+ // 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)';
+ $statId = $stats[0]; // could be multiple ratings in theory, but not expected to be
+ }
+ /*
+ todo: export to and solve formulas in javascript e.g.: spell 10187 - ${$42213m1*8*$} with $mult = ${${$?s31678[${1.05}][${${$?s31677[${1.04}][${${$?s31676[${1.03}][${${$?s31675[${1.02}][${${$?s31674[${1.01}][${1}]}}]}}]}}]}}]}*${$?s12953[${1.06}][${${$?s12952[${1.04}][${${$?s11151[${1.02}][${1}]}}]}}]}}
+ else if ($this->interactive == self::INTERACTIVE_FULL && ($modStrMin || $modStrMax))
+ {
+ $this->scaling[$this->id] = true;
+ $fmtStringMin = $modStrMin.'%s';
+ }
+ */
+ $minPoints = ctype_lower($var) ? $min : $max;
+ 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;");
+
+ $minPoints = $base;
+ break;
+ case 'o': // TotalAmount for periodic auras (with variance)
+ case 'O':
+ $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 == SPELL_AURA_MOD_REGEN || $aura == SPELL_AURA_MOD_POWER_REGEN)
+ $periode = 5000;
+ else
+ $periode = 3000;
+ }
+
+ [$min, $max, $modStrMin, $modStrMax] = $srcSpell->calculateAmountForCurrent($effIdx, intVal($duration / $periode));
+
+ if (in_array($op, $signs) && is_numeric($oparg))
+ {
+ eval("\$min = $min $op $oparg;");
+ eval("\$max = $max $op $oparg;");
+ }
+
+ if ($this->interactive >= self::INTERACTIVE_EMBEDDED && ($modStrMin || $modStrMax))
+ {
+ $this->scaling[$this->id] = true;
+
+ $fmtStringMin = $modStrMin.'%s';
+ $fmtStringMax = $modStrMax.'%s';
+ }
+
+ $minPoints = $min;
+ $maxPoints = $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;");
+
+ $minPoints = $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;");
+
+ $minPoints = $base;
+ break;
+ case 's': // BasePoints (with variance)
+ case 'S':
+ [$min, $max, $modStrMin, $modStrMax] = $srcSpell->calculateAmountForCurrent($effIdx);
+ $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
+ $stats = [];
+ if ($aura == SPELL_AURA_MOD_RATING)
+ if ($stats = StatsContainer::convertCombatRating($mv))
+ $this->scaling[$this->id] = true;
+ // Aura end
+
+ if ($stats && $this->interactive >= self::INTERACTIVE_EMBEDDED)
+ {
+ $fmtStringMin = '%s (%s)';
+ $statId = $stats[0]; // could be multiple ratings in theory, but not expected to be
+ }
+ else if (($modStrMin || $modStrMax) && $this->interactive == self::INTERACTIVE_FULL)
+ {
+ $this->scaling[$this->id] = true;
+ $fmtStringMin = $modStrMin.'%s';
+ $fmtStringMax = $modStrMax.'%s';
+ }
+
+ $minPoints = $min;
+ $maxPoints = $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;");
+
+ $minPoints = $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;");
+
+ $minPoints = $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;");
+
+ $minPoints = $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;");
+
+ $minPoints = $base;
+ break;
+ case 'z': // HomeZone
+ $fmtStringMin = Lang::spell('home');
+ break;
+ }
+
+ // handle excessively precise floats
+ if (is_float($minPoints))
+ $minPoints = round($minPoints, 2);
+ if (isset($maxPoints) && is_float($maxPoints))
+ $maxPoints = round($maxPoints, 2);
+
+ return [$minPoints, $maxPoints, $fmtStringMin, $fmtStringMax, $statId];
+ }
+
+ // description-, buff-parsing component
+ private function resolveFormulaString(string $formula, int $precision = 0) : array
+ {
+ $fSuffix = '%s';
+ $fStat = 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..
+ }
+
+ [$formOutStr, $fSuffix, $fStat] = $this->resolveFormulaString($formOutStr, $formPrecision);
+
+ $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
+ [$minPoints, , $fmtStringMin, , $statId] = $this->resolveVariableString($varParts);
+
+ // time within formula -> rebase to seconds and omit timeUnit
+ if (strtolower($varParts['var']) == 'd')
+ {
+ $minPoints /= 1000;
+ unset($fmtStringMin);
+ }
+
+ $str .= $minPoints;
+
+ // overwrite eventually inherited strings
+ if (isset($fmtStringMin))
+ $fSuffix = $fmtStringMin;
+
+ // overwrite eventually inherited stat
+ if (isset($statId))
+ $fStat = $statId;
+ }
+ $str .= substr($formula, $pos);
+ $str = str_replace('#', '$', $str); // reset markers
+
+ // step 3: try to evaluate result
+ $evaled = $this->resolveEvaluation($str);
+
+ $return = is_numeric($evaled) ? round($evaled, $precision) : $evaled;
+
+ return [$return, $fSuffix, $fStat];
+ }
+
+ // 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(string $type = 'description', int $level = MAX_LEVEL) : array
+ {
+ // 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(%s)
+ $<> - 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->charLevel = $level;
+
+ // step -1: already handled?
+ if (isset($this->parsedText[$this->id][$type][Lang::getLocale()->value][$this->charLevel][$this->interactive]))
+ return $this->parsedText[$this->id][$type][Lang::getLocale()->value][$this->charLevel][$this->interactive];
+
+ // step 0: get text
+ $data = $this->getField($type, true);
+ if (empty($data) || $data == "[]") // empty tooltip shouldn't be displayed anyway
+ return ['', [], false];
+
+ // 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` = %i', $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, $relSpells, true);
+
+ // step 3: unpack formulas ${ .. }.X
+ $data = $this->handleFormulas($data, true);
+
+ // step 4: find and eliminate regular variables
+ $data = $this->handleVariables($data, true);
+
+ // step 5: variable-dependent 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" => ' ']);
+
+ // cache result
+ $this->parsedText[$this->id][$type][Lang::getLocale()->value][$this->charLevel][$this->interactive] = [$data, $relSpells, $this->scaling[$this->id]];
+
+ return [$data, $relSpells, $this->scaling[$this->id]];
+ }
+
+ private function handleFormulas(string $data, bool $topLevel = false) : string
+ {
+ // 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;
+ }
+ [$formOutVal, $formOutStr, $statId] = $this->resolveFormulaString($formOutStr, $formPrecision ?: ($topLevel ? 0 : 10));
+
+ if ($statId && Util::checkNumeric($formOutVal))
+ $resolved = sprintf($formOutStr, $statId, abs($formOutVal), Util::setRatingLevel($this->charLevel, $statId, abs($formOutVal), $this->interactive == self::INTERACTIVE_FULL));
+ else
+ $resolved = sprintf($formOutStr, Util::checkNumeric($formOutVal) ? abs($formOutVal) : $formOutVal);
+
+ $data = substr_replace($data, $resolved, $formStartPos, ($formCurPos - $formStartPos));
+ }
+
+ return $data;
+ }
+
+ private function handleVariables(string $data, bool $topLevel = false) : string
+ {
+ $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;
+
+ [$minPoints, $maxPoints, $fmtStringMin, $fmtStringMax, $statId] = $this->resolveVariableString($varParts);
+ $resolved = is_numeric($minPoints) ? abs($minPoints) : $minPoints;
+ if (isset($fmtStringMin))
+ {
+ if (isset($statId))
+ $resolved = sprintf($fmtStringMin, $statId, abs($minPoints), Util::setRatingLevel($this->charLevel, $statId, abs($minPoints), $this->interactive == self::INTERACTIVE_FULL));
+ else
+ $resolved = sprintf($fmtStringMin, $resolved);
+ }
+
+ if (isset($maxPoints) && $minPoints != $maxPoints && !isset($statId))
+ {
+ $_ = is_numeric($maxPoints) ? abs($maxPoints) : $maxPoints;
+ $resolved .= Lang::game('valueDelim');
+ $resolved .= isset($fmtStringMax) ? sprintf($fmtStringMax, $_) : $_;
+ }
+
+ $str .= $resolved;
+ }
+ $str .= substr($data, $pos);
+ $str = str_replace('#', '$', $str); // reset marker
+
+ return $str;
+ }
+
+ private function handleConditions(string $data, array &$relSpells, bool $topLevel = false) : string
+ {
+ 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;
+ }
+ }
+
+ // this should be 0 if all went well
+ if ($condBrktCnt > 0)
+ {
+ trigger_error('SpellList::handleConditions() - string contains unbalanced condition', E_USER_WARNING);
+ $condParts[3] = $condParts[3] ?? '';
+ }
+
+ // 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], $relSpells);
+
+ if ($know && $topLevel)
+ {
+ foreach ([1, 3] as $pos)
+ {
+ if (strstr($condParts[$pos], '${'))
+ $condParts[$pos] = $this->handleFormulas($condParts[$pos]);
+
+ if (strstr($condParts[$pos], '$'))
+ $condParts[$pos] = $this->handleVariables($condParts[$pos]);
+ }
+
+ // 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(int $level = MAX_LEVEL, int $interactive = self::INTERACTIVE_EMBEDDED, ?array &$buffSpells = []) : ?string
+ {
+ $buffSpells = [];
+
+ if (!$this->curTpl)
+ return null;
+
+ // doesn't have a buff
+ if (!$this->getField('buff', true))
+ return null;
+
+ $this->interactive = $interactive;
+ $this->charLevel = $level;
+
+ $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
+ [$buffTT, $buffSp, ] = $this->parseText('buff');
+
+ $buffSpells = Util::parseHtmlText($buffSp);
+
+ $x .= $buffTT.' ';
+
+ // duration
+ if ($this->curTpl['duration'] > 0 && !($this->curTpl['attributes5'] & SPELL_ATTR5_HIDE_DURATION))
+ $x .= ''.Lang::formatTime($this->curTpl['duration'], 'spell', 'timeRemaining').'';
+
+ $x .= ' | ';
+
+ $min = $this->scaling[$this->id] ? ($this->getField('baseLevel') ?: 1) : 1;
+ $max = $this->scaling[$this->id] ? MAX_LEVEL : 1;
+ // scaling information - spellId:min:max:curr
+ $x .= '';
+
+ return $x;
+ }
+
+ public function renderTooltip(int $level = MAX_LEVEL, int $interactive = self::INTERACTIVE_EMBEDDED, ?array &$ttSpells = []) : ?string
+ {
+ $ttSpells = [];
+
+ if (!$this->curTpl)
+ return null;
+
+ $this->interactive = $interactive;
+ $this->charLevel = $level;
+
+ // fetch needed texts
+ $name = $this->getField('name', true);
+ $rank = $this->getField('rank', true);
+ $tools = $this->getToolsForCurrent();
+ $cool = $this->createCooldownForCurrent();
+ $cast = $this->createCastTimeForCurrent();
+ $cost = $this->createPowerCostForCurrent();
+ $range = $this->createRangesForCurrent();
+
+ [$desc, $spells, ] = $this->parseText('description');
+
+ $ttSpells = Util::parseHtmlText($spells);
+
+ // get reagents
+ $reagents = $this->getReagentsForCurrent();
+ foreach ($reagents as $k => $r)
+ {
+ if ($item = $this->relItems->getEntry($r[0]))
+ $reagents[$k] += [2 => new LocString($item), 3 => true];
+ else
+ $reagents[$k] += [2 => 'Item #'.$r[0], 3 => false];
+ }
+
+ $reagents = array_reverse($reagents);
+
+ // get stances
+ $stances = '';
+ if ($this->curTpl['stanceMask'] && !($this->curTpl['attributes2'] & SPELL_ATTR2_NOT_NEED_SHAPESHIFT))
+ $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)
+ {
+ // has createItem Scroll of Enchantment
+ if ($this->curTpl['effect'.$idx.'Id'] == SPELL_EFFECT_ENCHANT_ITEM)
+ 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 ([$iId, $qty, $text, $exists] = array_pop($reagents))
+ {
+ $_ .= $exists ? ' '.$text.'' : $text;
+ if ($qty > 1)
+ $_ .= ' ('.$qty.')';
+
+ $_ .= empty($reagents) ? ' ' : ', ';
+ }
+
+ $xTmp[] = $_.' ';
+ }
+
+ if ($reqItems)
+ $xTmp[] = Lang::game('requires2').' '.$reqItems;
+
+ if ($desc)
+ $xTmp[] = ''.$desc.'';
+
+ if ($createItem)
+ $xTmp[] = $createItem;
+
+ if ($xTmp)
+ $x .= '';
+
+ // scaling information - spellId:min:max:curr[:scalingDistribution:ScalingFlags]
+ $scalingInfo = array(
+ $this->id,
+ $this->scaling[$this->id] ? ($this->getField('baseLevel') ?: 1) : 1,
+ $this->scaling[$this->id] ? ($this->getField('maxLevel') ?: MAX_LEVEL) : 1
+ );
+
+ if ($this->getField('attributes0') & SPELL_ATTR0_LEVEL_DAMAGE_CALCULATION)
+ {
+ $scalingInfo[] = $this->getField('spellLevel') ?: 1;
+ $scalingInfo[] = 1; // in 4.x+ proper scaling information; for us just to flag a npc spell as level damage scaling
+ $scalingInfo[] = 1;
+ }
+ else
+ $scalingInfo[] = min($this->charLevel, $scalingInfo[2]);
+
+ $x .= '';
+
+ return $x;
+ }
+
+ public function getTalentHeadForCurrent() : string
+ {
+ // 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() : array
+ {
+ $gry = $this->curTpl['skillLevelGrey'];
+ $ylw = $this->curTpl['skillLevelYellow'];
+ $grn = (int)(($ylw + $gry) / 2);
+ $org = $this->curTpl['learnedAt'];
+
+ if ($ylw < $org)
+ $ylw = 0;
+
+ if ($grn < $org || $grn < $ylw)
+ $grn = 0;
+
+ if ($org >= $ylw || $org >= $grn || $org >= $gry)
+ $org = 0;
+
+ return $gry > 1 ? [$org, $ylw, $grn, $gry] : [];
+ }
+
+ public function getListviewData(int $addInfoMask = 0x0) : array
+ {
+ $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 ($this->getSources($s, $sm))
+ {
+ $data[$this->id]['source'] = $s;
+ if ($sm)
+ $data[$this->id]['sourcemore'] = $sm;
+ }
+
+ // 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(int $addMask = GLOBALINFO_SELF, ?array &$extra = []) : array
+ {
+ $data = [];
+
+ if ($this->relItems && ($addMask & GLOBALINFO_RELATED))
+ $data = $this->relItems->getJSGlobals();
+
+ foreach ($this->iterate() as $id => $__)
+ {
+ if ($addMask & GLOBALINFO_RELATED)
+ {
+ foreach (ChrClass::fromMask($this->curTpl['reqClassMask']) as $cId)
+ $data[Type::CHR_CLASS][$cId] = $cId;
+
+ foreach (ChrRace::fromMask($this->curTpl['reqRaceMask']) as $rId)
+ $data[Type::CHR_RACE][$rId] = $rId;
+
+ // play sound effect
+ for ($i = 1; $i < 4; $i++)
+ if ($this->getField('effect'.$i.'Id') == SPELL_EFFECT_PLAY_SOUND || $this->getField('effect'.$i.'Id') == SPELL_EFFECT_PLAY_MUSIC)
+ $data[Type::SOUND][$this->getField('effect'.$i.'MiscValue')] = $this->getField('effect'.$i.'MiscValue');
+ }
+
+ if ($addMask & GLOBALINFO_SELF)
+ {
+ $data[Type::SPELL][$id] = array(
+ 'icon' => $this->curTpl['iconStringAlt'] ?: $this->curTpl['iconString'],
+ 'name' => $this->getField('name', true)
+ );
+
+ if (($_ = $this->curTpl['typeCat']) && in_array($_, [-5, -6, 9, 11]))
+ $data[Type::SPELL][$id]['completion_category'] = $_;
+ }
+
+ if ($addMask & GLOBALINFO_EXTRA)
+ {
+ $buff = $this->renderBuff(MAX_LEVEL, true, $buffSpells);
+ $tTip = $this->renderTooltip(MAX_LEVEL, true, $spells);
+
+ foreach ($spells as $relId => $_)
+ if (empty($data[Type::SPELL][$relId]))
+ $data[Type::SPELL][$relId] = $relId;
+
+ foreach ($buffSpells as $relId => $_)
+ if (empty($data[Type::SPELL][$relId]))
+ $data[Type::SPELL][$relId] = $relId;
+
+ $extra[$id] = array(
+ // 'id' => $id,
+ 'tooltip' => $tTip,
+ 'buff' => $buff ?: null,
+ 'spells' => $spells,
+ 'buffspells' => $buffSpells ?: null
+ );
+ }
+ }
+
+ return $data;
+ }
+
+ // mostly similar to TC
+ public function getCastingTimeForBonus(bool $asDOT = false) : int
+ {
+ $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'] * 1000);
+
+ 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'], SpellLIst::EFFECTS_DIRECT_SCALING))
+ $isDirect = true;
+ else if (in_array($this->curTpl['effect'.$i.'AuraId'], SpellList::AURAS_PERIODIC_SCALING))
+ 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'] * 1000;
+ if ($this->curTpl['attributes0'] & SPELL_ATTR0_REQ_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(int $id = 0) : array
+ {
+ $data = [];
+
+ foreach ($this->iterate() as $__)
+ {
+ if ($id && $id != $this->id)
+ continue;
+
+ $data[$this->id] = array(
+ 'n' => $this->getField('name', true),
+ 't' => Type::SPELL,
+ 'ti' => $this->id,
+ 's' => empty($this->curTpl['skillLines']) ? 0 : $this->curTpl['skillLines'][0],
+ 'c' => $this->curTpl['typeCat'],
+ 'icon' => $this->curTpl['iconStringAlt'] ?: $this->curTpl['iconString'],
+ );
+ }
+
+ return $data;
+ }
+}
+
+
+class SpellListFilter extends Filter
+{
+ const MAX_SPELL_EFFECT = 167;
+ const MAX_SPELL_AURA = 316;
+
+ public static array $attributesFilter = array( // attrFieldId => [attrBit => cr, ...]; if cr < 0 ? filter is negated
+ 0 => array(
+ SPELL_ATTR0_REQ_AMMO => 48,
+ SPELL_ATTR0_ON_NEXT_SWING => 49,
+ SPELL_ATTR0_PASSIVE => 50,
+ SPELL_ATTR0_HIDDEN_CLIENTSIDE => 51,
+ SPELL_ATTR0_HIDE_IN_COMBAT_LOG => 84,
+ SPELL_ATTR0_ON_NEXT_SWING_2 => 52,
+ SPELL_ATTR0_DAYTIME_ONLY => 53,
+ SPELL_ATTR0_NIGHT_ONLY => 54,
+ SPELL_ATTR0_INDOORS_ONLY => 55,
+ SPELL_ATTR0_OUTDOORS_ONLY => 56,
+ SPELL_ATTR0_NOT_SHAPESHIFT => -31,
+ SPELL_ATTR0_ONLY_STEALTHED => 38,
+ SPELL_ATTR0_LEVEL_DAMAGE_CALCULATION => 58,
+ SPELL_ATTR0_STOP_ATTACK_TARGET => 59,
+ SPELL_ATTR0_IMPOSSIBLE_DODGE_PARRY_BLOCK => 60,
+ SPELL_ATTR0_CASTABLE_WHILE_DEAD => 61,
+ SPELL_ATTR0_CASTABLE_WHILE_MOUNTED => 62,
+ SPELL_ATTR0_DISABLED_WHILE_ACTIVE => 63,
+ SPELL_ATTR0_NEGATIVE_1 => 69,
+ SPELL_ATTR0_CASTABLE_WHILE_SITTING => 64,
+ SPELL_ATTR0_CANT_USED_IN_COMBAT => -33,
+ SPELL_ATTR0_UNAFFECTED_BY_INVULNERABILITY => 46,
+ SPELL_ATTR0_CANT_CANCEL => 57
+ ),
+ 1 => array(
+ SPELL_ATTR1_DRAIN_ALL_POWER => 65,
+ SPELL_ATTR1_CHANNELED_1 => 27, // general filter
+ SPELL_ATTR1_NOT_BREAK_STEALTH => 68,
+ SPELL_ATTR1_CHANNELED_2 => 66, // attributes filter
+ SPELL_ATTR1_CANT_BE_REFLECTED => 67, // WH - 69: all effects are harmful points here
+ SPELL_ATTR1_CANT_TARGET_IN_COMBAT => 70,
+ SPELL_ATTR1_NO_THREAT => 71,
+ SPELL_ATTR1_IS_PICKPOCKET => 72,
+ SPELL_ATTR1_DISPEL_AURAS_ON_IMMUNITY => 73,
+ SPELL_ATTR1_UNAFFECTED_BY_SCHOOL_IMMUNE => 47,
+ SPELL_ATTR1_IS_FISHING => 74
+ ),
+ 2 => array(
+ SPELL_ATTR2_CANT_TARGET_TAPPED => 75,
+ SPELL_ATTR2_PRESERVE_ENCHANT_IN_ARENA => 76,
+ SPELL_ATTR2_NOT_NEED_SHAPESHIFT => 77,
+ SPELL_ATTR2_CANT_CRIT => -34,
+ SPELL_ATTR2_FOOD_BUFF => 78
+ ),
+ 3 => array(
+ SPELL_ATTR3_ONLY_TARGET_PLAYERS => 79,
+ SPELL_ATTR3_MAIN_HAND => 80,
+ SPELL_ATTR3_BATTLEGROUND => 43,
+ SPELL_ATTR3_NO_INITIAL_AGGRO => 81,
+ SPELL_ATTR3_DEATH_PERSISTENT => 36,
+ SPELL_ATTR3_IGNORE_HIT_RESULT => -35,
+ SPELL_ATTR3_REQ_WAND => 82, // unused attribute
+ SPELL_ATTR3_REQ_OFFHAND => 83
+ ),
+ 4 => array(
+ SPELL_ATTR4_FADES_WHILE_LOGGED_OUT => 85,
+ SPELL_ATTR4_NOT_STEALABLE => -39,
+ SPELL_ATTR4_NOT_USABLE_IN_ARENA => -44,
+ SPELL_ATTR4_USABLE_IN_ARENA => 44
+ ),
+ 5 => array(
+ SPELL_ATTR5_USABLE_WHILE_STUNNED => 42,
+ SPELL_ATTR5_SINGLE_TARGET_SPELL => 86,
+ SPELL_ATTR5_START_PERIODIC_AT_APPLY => 87,
+ SPELL_ATTR5_USABLE_WHILE_FEARED => 89,
+ SPELL_ATTR5_USABLE_WHILE_CONFUSED => 88
+ ),
+ 6 => array(
+ SPELL_ATTR6_ONLY_IN_ARENA => 90, // unused attribute
+ SPELL_ATTR6_NOT_IN_RAID_INSTANCE => 91
+ ),
+ 7 => array(
+ SPELL_ATTR7_DISABLE_AURA_WHILE_DEAD => 92, // aka Paladin Aura
+ SPELL_ATTR7_SUMMON_PLAYER_TOTEM => 93
+ )
+ );
+
+ protected string $type = 'spells';
+ protected static array $enums = array(
+ 9 => array( // sources index
+ 1 => true, // Any
+ 2 => false, // None
+ 3 => SRC_CRAFTED,
+ 4 => SRC_DROP,
+ 6 => SRC_QUEST,
+ 7 => SRC_VENDOR,
+ 8 => SRC_TRAINER,
+ 9 => SRC_DISCOVERY,
+ 10 => SRC_TALENT
+ ),
+ 22 => array(
+ 1 => true, // Weapons
+ 2 => true, // Armor
+ 3 => true, // Armor Proficiencies
+ 4 => true, // Armor Specializations
+ 5 => true // Languages
+ ),
+ 40 => array( // damage class index
+ 1 => 0, // none
+ 2 => 1, // magic
+ 3 => 2, // melee
+ 4 => 3 // ranged
+ ),
+ 45 => array( // power type index
+ // 1 => ??, // burning embers
+ // 2 => ??, // chi
+ // 3 => ??, // demonic fury
+ 4 => POWER_ENERGY, // energy
+ 5 => POWER_FOCUS, // focus
+ 6 => POWER_HEALTH, // health
+ // 7 => ??, // holy power
+ 8 => POWER_MANA, // mana
+ 9 => POWER_RAGE, // rage
+ 10 => POWER_RUNE, // runes
+ 11 => POWER_RUNIC_POWER, // runic power
+ // 12 => ??, // shadow orbs
+ // 13 => ??, // soul shard
+ 14 => POWER_HAPPINESS, // happiness v custom v
+ 15 => -1, // ammo
+ 16 => -41, // pyrite
+ 17 => -61, // steam pressure
+ 18 => -101, // heat
+ 19 => -121, // ooze
+ 20 => -141, // blood power
+ 21 => -142 // wrath
+ )
+ );
+
+ protected static array $genericFilter = array(
+ 1 => [parent::CR_CALLBACK, 'cbCost', ], // costAbs [op] [int]
+ 2 => [parent::CR_NUMERIC, 'powerCostPercent', NUM_CAST_INT ], // prcntbasemanarequired
+ 3 => [parent::CR_BOOLEAN, 'spellFocusObject' ], // requiresnearbyobject
+ 4 => [parent::CR_NUMERIC, 'trainingcost', NUM_CAST_INT ], // trainingcost
+ 5 => [parent::CR_BOOLEAN, 'reqSpellId' ], // requiresprofspec
+ 8 => [parent::CR_FLAG, 'cuFlags', CUSTOM_HAS_SCREENSHOT ], // hasscreenshots
+ 9 => [parent::CR_CALLBACK, 'cbSource', ], // source [enum]
+ 10 => [parent::CR_FLAG, 'cuFlags', SPELL_CU_FIRST_RANK ], // firstrank
+ 11 => [parent::CR_FLAG, 'cuFlags', CUSTOM_HAS_COMMENT ], // hascomments
+ 12 => [parent::CR_FLAG, 'cuFlags', SPELL_CU_LAST_RANK ], // lastrank
+ 13 => [parent::CR_NUMERIC, 'rankNo', NUM_CAST_INT ], // rankno
+ 14 => [parent::CR_NUMERIC, 'id', NUM_CAST_INT, true ], // id
+ 15 => [parent::CR_STRING, 'ic.name', ], // icon
+ 17 => [parent::CR_FLAG, 'cuFlags', CUSTOM_HAS_VIDEO ], // hasvideos
+ 19 => [parent::CR_FLAG, 'attributes0', SPELL_ATTR0_LEVEL_DAMAGE_CALCULATION ], // scaling
+ 20 => [parent::CR_CALLBACK, 'cbReagents', ], // has Reagents [yn]
+ 22 => [parent::CR_CALLBACK, 'cbProficiency', null, null ], // proficiencytype [proficiencytype]
+ // 26 => [parent::CR_NUMERIC, 'startRecoveryCategory', NUM_CAST_INT, false ], // gcd-cat
+ 25 => [parent::CR_BOOLEAN, 'skillLevelYellow' ], // rewardsskillups
+ 27 => [parent::CR_FLAG, 'attributes1', SPELL_ATTR1_CHANNELED_1, true ], // channeled [yn]
+ 28 => [parent::CR_NUMERIC, 'castTime', NUM_CAST_FLOAT ], // casttime [num]
+ 29 => [parent::CR_CALLBACK, 'cbAuraNames', ], // appliesaura [effectauranames]
+ 31 => [parent::CR_FLAG, 'attributes0', SPELL_ATTR0_NOT_SHAPESHIFT ], // usablewhenshapeshifted [yn]
+ 33 => [parent::CR_CALLBACK, 'cbInverseFlag', 'attributes0', SPELL_ATTR0_CANT_USED_IN_COMBAT], // combatcastable [yn]
+ 34 => [parent::CR_CALLBACK, 'cbInverseFlag', 'attributes2', SPELL_ATTR2_CANT_CRIT ], // chancetocrit [yn]
+ 35 => [parent::CR_CALLBACK, 'cbInverseFlag', 'attributes3', SPELL_ATTR3_IGNORE_HIT_RESULT ], // chancetomiss [yn]
+ 36 => [parent::CR_FLAG, 'attributes3', SPELL_ATTR3_DEATH_PERSISTENT ], // persiststhroughdeath [yn]
+ 38 => [parent::CR_FLAG, 'attributes0', SPELL_ATTR0_ONLY_STEALTHED ], // requiresstealth [yn]
+ 39 => [parent::CR_FLAG, 'attributes4', SPELL_ATTR4_NOT_STEALABLE ], // spellstealable [yn]
+ 40 => [parent::CR_ENUM, 'damageClass' ], // damagetype [damagetype]
+ 41 => [parent::CR_FLAG, 'stanceMask', (1 << (22 - 1)) ], // requiresmetamorphosis [yn]
+ 42 => [parent::CR_FLAG, 'attributes5', SPELL_ATTR5_USABLE_WHILE_STUNNED ], // usablewhenstunned [yn]
+ 43 => [parent::CR_FLAG, 'attributes3', SPELL_ATTR3_BATTLEGROUND ], // usableinbgs [yn]
+ 44 => [parent::CR_FLAG, 'attributes4', SPELL_ATTR4_USABLE_IN_ARENA ], // usableinarenas [yn]
+ 45 => [parent::CR_ENUM, 'powerType' ], // resourcetype [resourcetype]
+ 46 => [parent::CR_FLAG, 'attributes0', SPELL_ATTR0_UNAFFECTED_BY_INVULNERABILITY ], // disregardimmunity [yn]
+ 47 => [parent::CR_FLAG, 'attributes1', SPELL_ATTR1_UNAFFECTED_BY_SCHOOL_IMMUNE ], // disregardschoolimmunity [yn]
+ 48 => [parent::CR_FLAG, 'attributes0', SPELL_ATTR0_REQ_AMMO ], // reqrangedweapon [yn]
+ 49 => [parent::CR_FLAG, 'attributes0', SPELL_ATTR0_ON_NEXT_SWING ], // onnextswingplayers [yn]
+ 50 => [parent::CR_FLAG, 'attributes0', SPELL_ATTR0_PASSIVE ], // passivespell [yn]
+ 51 => [parent::CR_FLAG, 'attributes1', SPELL_ATTR1_DONT_DISPLAY_IN_AURA_BAR ], // hiddenaura [yn]
+ 52 => [parent::CR_FLAG, 'attributes0', SPELL_ATTR0_ON_NEXT_SWING_2 ], // onnextswingnpcs [yn]
+ 53 => [parent::CR_FLAG, 'attributes0', SPELL_ATTR0_DAYTIME_ONLY ], // daytimeonly [yn]
+ 54 => [parent::CR_FLAG, 'attributes0', SPELL_ATTR0_NIGHT_ONLY ], // nighttimeonly [yn]
+ 55 => [parent::CR_FLAG, 'attributes0', SPELL_ATTR0_INDOORS_ONLY ], // indoorsonly [yn]
+ 56 => [parent::CR_FLAG, 'attributes0', SPELL_ATTR0_OUTDOORS_ONLY ], // outdoorsonly [yn]
+ 57 => [parent::CR_FLAG, 'attributes0', SPELL_ATTR0_CANT_CANCEL ], // uncancellableaura [yn]
+ 58 => [parent::CR_FLAG, 'attributes0', SPELL_ATTR0_LEVEL_DAMAGE_CALCULATION ], // damagedependsonlevel [yn]
+ 59 => [parent::CR_FLAG, 'attributes0', SPELL_ATTR0_STOP_ATTACK_TARGET ], // stopsautoattack [yn]
+ 60 => [parent::CR_FLAG, 'attributes0', SPELL_ATTR0_IMPOSSIBLE_DODGE_PARRY_BLOCK ], // cannotavoid [yn]
+ 61 => [parent::CR_FLAG, 'attributes0', SPELL_ATTR0_CASTABLE_WHILE_DEAD ], // usabledead [yn]
+ 62 => [parent::CR_FLAG, 'attributes0', SPELL_ATTR0_CASTABLE_WHILE_MOUNTED ], // usablemounted [yn]
+ 63 => [parent::CR_FLAG, 'attributes0', SPELL_ATTR0_DISABLED_WHILE_ACTIVE ], // delayedrecoverystarttime [yn]
+ 64 => [parent::CR_FLAG, 'attributes0', SPELL_ATTR0_CASTABLE_WHILE_SITTING ], // usablesitting [yn]
+ 65 => [parent::CR_FLAG, 'attributes1', SPELL_ATTR1_DRAIN_ALL_POWER ], // usesallpower [yn]
+ 66 => [parent::CR_FLAG, 'attributes1', SPELL_ATTR1_CHANNELED_2 ], // channeled [yn]
+ 67 => [parent::CR_FLAG, 'attributes1', SPELL_ATTR1_CANT_BE_REFLECTED ], // cannotreflect [yn]
+ 68 => [parent::CR_FLAG, 'attributes1', SPELL_ATTR1_NOT_BREAK_STEALTH ], // usablestealthed [yn]
+ 69 => [parent::CR_FLAG, 'attributes0', SPELL_ATTR0_NEGATIVE_1 ], // harmful [yn] - WH interprets attributes1 0x80 as "all effects are harmful", but it really is CANT_BE_REFLECTED. So here is an approximation.
+ 70 => [parent::CR_FLAG, 'attributes1', SPELL_ATTR1_CANT_TARGET_IN_COMBAT ], // targetnotincombat [yn]
+ 71 => [parent::CR_FLAG, 'attributes1', SPELL_ATTR1_NO_THREAT ], // nothreat [yn]
+ 72 => [parent::CR_FLAG, 'attributes1', SPELL_ATTR1_IS_PICKPOCKET ], // pickpocket [yn]
+ 73 => [parent::CR_FLAG, 'attributes1', SPELL_ATTR1_DISPEL_AURAS_ON_IMMUNITY ], // dispelauraonimmunity [yn]
+ 74 => [parent::CR_FLAG, 'attributes1', SPELL_ATTR1_IS_FISHING ], // reqfishingpole [yn]
+ 75 => [parent::CR_FLAG, 'attributes2', SPELL_ATTR2_CANT_TARGET_TAPPED ], // requntappedtarget [yn]
+ 76 => [parent::CR_FLAG, 'attributes2', SPELL_ATTR2_PRESERVE_ENCHANT_IN_ARENA ], // targetownitem [yn
+ 77 => [parent::CR_FLAG, 'attributes2', SPELL_ATTR2_NOT_NEED_SHAPESHIFT ], // doesntreqshapeshift [yn]
+ 78 => [parent::CR_FLAG, 'attributes2', SPELL_ATTR2_FOOD_BUFF ], // foodbuff [yn]
+ 79 => [parent::CR_FLAG, 'attributes3', SPELL_ATTR3_ONLY_TARGET_PLAYERS ], // targetonlyplayer [yn]
+ 80 => [parent::CR_FLAG, 'attributes3', SPELL_ATTR3_MAIN_HAND ], // reqmainhand [yn]
+ 81 => [parent::CR_FLAG, 'attributes3', SPELL_ATTR3_NO_INITIAL_AGGRO ], // doesntengagetarget [yn]
+ 82 => [parent::CR_FLAG, 'attributes3', SPELL_ATTR3_REQ_WAND ], // reqwand [yn]
+ 83 => [parent::CR_FLAG, 'attributes3', SPELL_ATTR3_REQ_OFFHAND ], // reqoffhand [yn]
+ 84 => [parent::CR_FLAG, 'attributes0', SPELL_ATTR0_HIDE_IN_COMBAT_LOG ], // nolog [yn]
+ 85 => [parent::CR_FLAG, 'attributes4', SPELL_ATTR4_FADES_WHILE_LOGGED_OUT ], // auratickswhileloggedout [yn]
+ 86 => [parent::CR_FLAG, 'attributes5', SPELL_ATTR5_SINGLE_TARGET_SPELL ], // onlyaffectsonetarget [yn]
+ 87 => [parent::CR_FLAG, 'attributes5', SPELL_ATTR5_START_PERIODIC_AT_APPLY ], // startstickingatapplication [yn]
+ 88 => [parent::CR_FLAG, 'attributes5', SPELL_ATTR5_USABLE_WHILE_CONFUSED ], // usableconfused [yn]
+ 89 => [parent::CR_FLAG, 'attributes5', SPELL_ATTR5_USABLE_WHILE_FEARED ], // usablefeared [yn]
+ 90 => [parent::CR_FLAG, 'attributes6', SPELL_ATTR6_ONLY_IN_ARENA ], // onlyarena [yn]
+ 91 => [parent::CR_FLAG, 'attributes6', SPELL_ATTR6_NOT_IN_RAID_INSTANCE ], // notinraid [yn]
+ 92 => [parent::CR_FLAG, 'attributes7', SPELL_ATTR7_DISABLE_AURA_WHILE_DEAD ], // paladinaura [yn]
+ 93 => [parent::CR_FLAG, 'attributes7', SPELL_ATTR7_SUMMON_PLAYER_TOTEM ], // totemspell [yn]
+ 95 => [parent::CR_CALLBACK, 'cbBandageSpell' ], // bandagespell [yn] - was that an attribute at one point?
+ 96 => [parent::CR_STAFFFLAG, 'attributes0' ], // flags1 [flags]
+ 97 => [parent::CR_STAFFFLAG, 'attributes1' ], // flags2 [flags]
+ 98 => [parent::CR_STAFFFLAG, 'attributes2' ], // flags3 [flags]
+ 99 => [parent::CR_STAFFFLAG, 'attributes3' ], // flags4 [flags]
+ 100 => [parent::CR_STAFFFLAG, 'attributes4' ], // flags5 [flags]
+ 101 => [parent::CR_STAFFFLAG, 'attributes5' ], // flags6 [flags]
+ 102 => [parent::CR_STAFFFLAG, 'attributes6' ], // flags7 [flags]
+ 103 => [parent::CR_STAFFFLAG, 'attributes7' ], // flags8 [flags]
+ 104 => [parent::CR_STAFFFLAG, 'targets' ], // flags9 [flags]
+ 105 => [parent::CR_STAFFFLAG, 'stanceMaskNot' ], // flags10 [flags]
+ 106 => [parent::CR_STAFFFLAG, 'spellFamilyFlags1' ], // flags11 [flags]
+ 107 => [parent::CR_STAFFFLAG, 'spellFamilyFlags2' ], // flags12 [flags]
+ 108 => [parent::CR_STAFFFLAG, 'spellFamilyFlags3' ], // flags13 [flags]
+ 109 => [parent::CR_CALLBACK, 'cbEffectNames', ], // effecttype [effecttype]
+ // 110 => [parent::CR_NYI_PH, null, null, null ], // scalingap [yn] // unreasonably complex for now
+ // 111 => [parent::CR_NYI_PH, null, null, null ], // scalingsp [yn] // unreasonably complex for now
+ 114 => [parent::CR_CALLBACK, 'cbReqFaction' ], // requiresfaction [side]
+ 116 => [parent::CR_BOOLEAN, 'startRecoveryTime' ] // onGlobalCooldown [yn]
+ );
+
+ protected static array $inputFields = array(
+ 'cr' => [parent::V_RANGE, [1, 116], 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 / text - only printable chars, no delimiter
+ 'ex' => [parent::V_EQUAL, 'on', false], // extended name search
+ 'ma' => [parent::V_EQUAL, 1, false], // match any / all filter
+ 'minle' => [parent::V_RANGE, [0, 99], false], // spell level min
+ 'maxle' => [parent::V_RANGE, [0, 99], false], // spell level max
+ 'minrs' => [parent::V_RANGE, [0, 999], false], // required skill level min
+ 'maxrs' => [parent::V_RANGE, [0, 999], false], // required skill level max
+ 'ra' => [parent::V_LIST, [[1, 8], 10, 11], false], // races
+ 'cl' => [parent::V_CALLBACK, 'cbClasses', true ], // classes
+ 'gl' => [parent::V_CALLBACK, 'cbGlyphs', true ], // glyph type
+ 'sc' => [parent::V_RANGE, [0, 6], true ], // magic schools
+ 'dt' => [parent::V_LIST, [[1, 6], 9], false], // dispel types
+ 'me' => [parent::V_RANGE, [1, 31], false] // mechanics
+ );
+
+ protected function createSQLForValues() : array
+ {
+ $parts = [];
+ $_v = &$this->values;
+
+ // string (extended)
+ if ($_v['na'])
+ {
+ $f = [['na', ['nml.nName', 'nml.nBuff', 'nml.nDescription']]];
+ if ($_v['ex'] != 'on')
+ $f = [['na', 'nml.nName']];
+
+ if ($_ = $this->buildMatchLookup($f))
+ $parts[] = $_;
+ else
+ {
+ $f = [['na', 'name_loc'.Lang::getLocale()->value], ['na', 'buff_loc'.Lang::getLocale()->value], ['na', 'description_loc'.Lang::getLocale()->value]];
+ if ($_v['ex'] != 'on')
+ $f = [$f[0]];
+
+ if ($_ = $this->buildLikeLookup($f))
+ $parts[] = $_;
+ }
+ }
+
+ // spellLevel min todo (low): talentSpells (typeCat -2) commonly have spellLevel 1 (and talentLevel >1) -> query is inaccurate
+ if ($_v['minle'])
+ $parts[] = ['spellLevel', $_v['minle'], '>='];
+
+ // spellLevel max
+ if ($_v['maxle'])
+ $parts[] = ['spellLevel', $_v['maxle'], '<='];
+
+ // skillLevel min
+ if ($_v['minrs'])
+ $parts[] = ['learnedAt', $_v['minrs'], '>='];
+
+ // skillLevel max
+ if ($_v['maxrs'])
+ $parts[] = ['learnedAt', $_v['maxrs'], '<='];
+
+ // race
+ if ($_v['ra'])
+ $parts[] = [DB::AND, [['reqRaceMask', ChrRace::MASK_ALL, '&'], ChrRace::MASK_ALL, '!'], ['reqRaceMask', $this->list2Mask([$_v['ra']]), '&']];
+
+ // class [list]
+ if ($_v['cl'])
+ $parts[] = ['reqClassMask', $this->list2Mask($_v['cl']), '&'];
+
+ // school [list]
+ if ($_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 ($_v['gl'])
+ $parts[] = ['cuFlags', ($this->list2Mask($_v['gl']) << 6), '&'];
+
+ // dispel type
+ if ($_v['dt'])
+ $parts[] = ['dispelType', $_v['dt']];
+
+ // mechanic
+ if ($_v['me'])
+ $parts[] = [DB::OR, ['mechanic', $_v['me']], ['effect1Mechanic', $_v['me']], ['effect2Mechanic', $_v['me']], ['effect3Mechanic', $_v['me']]];
+
+ return $parts;
+ }
+
+ protected function cbClasses(string &$val) : bool
+ {
+ if (!$this->parentCats || !in_array($this->parentCats[0], [-13, -2, 7]))
+ return false;
+
+ if (!Util::checkNumeric($val, NUM_CAST_INT))
+ return false;
+
+ $type = parent::V_LIST;
+ $valid = ChrClass::fromMask(ChrClass::MASK_ALL);
+
+ return $this->checkInput($type, $valid, $val);
+ }
+
+ protected function cbGlyphs(string &$val) : bool
+ {
+ if (!$this->parentCats || $this->parentCats[0] != -13)
+ return false;
+
+ if (!Util::checkNumeric($val, NUM_CAST_INT))
+ return false;
+
+ $type = parent::V_LIST;
+ $valid = [1, 2];
+
+ return $this->checkInput($type, $valid, $val);
+ }
+
+ protected function cbCost(int $cr, int $crs, string $crv) : ?array
+ {
+ if (!Util::checkNumeric($crv, NUM_CAST_INT) || !$this->int2Op($crs))
+ return null;
+
+ return [DB::OR,
+ [DB::AND, ['powerType', [POWER_RAGE, POWER_RUNIC_POWER]], ['powerCost', (10 * $crv), $crs]],
+ [DB::AND, ['powerType', [POWER_RAGE, POWER_RUNIC_POWER], '!'], ['powerCost', $crv, $crs]]
+ ];
+ }
+
+ protected function cbSource(int $cr, int $crs, string $crv) : ?array
+ {
+ if (!isset(self::$enums[$cr][$crs]))
+ return null;
+
+ $_ = self::$enums[$cr][$crs];
+ if (is_int($_)) // specific
+ return ['src.src'.$_, null, '!'];
+ else if ($_) // any
+ {
+ $foo = [DB::OR];
+ foreach (self::$enums[$cr] as $bar)
+ if (is_int($bar))
+ $foo[] = ['src.src'.$bar, null, '!'];
+
+ return $foo;
+ }
+ else // none
+ return ['src.typeId', null];
+
+ return null;
+ }
+
+ protected function cbReagents(int $cr, int $crs, string $crv) : ?array
+ {
+ if (!$this->int2Bool($crs))
+ return null;
+
+ if ($crs)
+ return [DB::OR, ['reagent1', 0, '>'], ['reagent2', 0, '>'], ['reagent3', 0, '>'], ['reagent4', 0, '>'], ['reagent5', 0, '>'], ['reagent6', 0, '>'], ['reagent7', 0, '>'], ['reagent8', 0, '>']];
+ else
+ return [DB::AND, ['reagent1', 0], ['reagent2', 0], ['reagent3', 0], ['reagent4', 0], ['reagent5', 0], ['reagent6', 0], ['reagent7', 0], ['reagent8', 0]];
+ }
+
+ protected function cbAuraNames(int $cr, int $crs, string $crv) : ?array
+ {
+ if (!$this->checkInput(parent::V_RANGE, [1, self::MAX_SPELL_AURA], $crs))
+ return null;
+
+ return [DB::OR, ['effect1AuraId', $crs], ['effect2AuraId', $crs], ['effect3AuraId', $crs]];
+ }
+
+ protected function cbEffectNames(int $cr, int $crs, string $crv) : ?array
+ {
+ if (!$this->checkInput(parent::V_RANGE, [1, self::MAX_SPELL_EFFECT], $crs))
+ return null;
+
+ return [DB::OR, ['effect1Id', $crs], ['effect2Id', $crs], ['effect3Id', $crs]];
+ }
+
+ protected function cbInverseFlag(int $cr, int $crs, string $crv, string $field, int $flag) : ?array
+ {
+ if (!$this->int2Bool($crs))
+ return null;
+
+ if ($crs)
+ return [[$field, $flag, '&'], 0];
+ else
+ return [$field, $flag, '&'];
+ }
+
+ protected function cbSpellstealable(int $cr, int $crs, string $crv, string $field, int $flag) : ?array
+ {
+ if (!$this->int2Bool($crs))
+ return null;
+
+ if ($crs)
+ return [DB::AND, [[$field, $flag, '&'], 0], ['dispelType', SPELL_DAMAGE_CLASS_MAGIC]];
+ else
+ return [DB::OR, [$field, $flag, '&'], ['dispelType', SPELL_DAMAGE_CLASS_MAGIC, '!']];
+ }
+
+ protected function cbReqFaction(int $cr, int $crs, string $crv) : ?array
+ {
+ return match ($crs)
+ {
+ // yes
+ 1 => ['reqRaceMask', 0, '!'],
+ // alliance
+ 2 => [DB::AND, [['reqRaceMask', ChrRace::MASK_HORDE, '&'], 0], ['reqRaceMask', ChrRace::MASK_ALLIANCE, '&']],
+ // horde
+ 3 => [DB::AND, [['reqRaceMask', ChrRace::MASK_ALLIANCE, '&'], 0], ['reqRaceMask', ChrRace::MASK_HORDE, '&']],
+ // both
+ 4 => [DB::AND, ['reqRaceMask', ChrRace::MASK_ALLIANCE, '&'], ['reqRaceMask', ChrRace::MASK_HORDE, '&']],
+ // no
+ 5 => ['reqRaceMask', 0],
+ default => null
+ };
+ }
+
+ /* unused - for reference: attribute flag or item class mask */
+ protected function cbEquippedWeapon(int $cr, int $crs, string $crv, int $mask, bool $useInvType) : ?array
+ {
+ if (!$this->int2Bool($crs))
+ return null;
+
+ $field = $useInvType ? 'equippedItemInventoryTypeMask' : 'equippedItemSubClassMask';
+
+ if ($crs)
+ return [DB::AND, ['equippedItemClass', ITEM_CLASS_WEAPON], [$field, $mask, '&']];
+ else
+ return [DB::OR, ['equippedItemClass', ITEM_CLASS_WEAPON, '!'], [[$field, $mask, '&'], 0]];
+ }
+
+ /* unused - for reference: attribute flag or cooldown time constraint */
+ protected function cbUsableInArena(int $cr, int $crs, string $crv) : ?array
+ {
+ if (!$this->int2Bool($crs))
+ return null;
+
+ if ($crs)
+ return [DB::AND,
+ [['attributes4', SPELL_ATTR4_NOT_USABLE_IN_ARENA, '&'], 0],
+ [DB::OR, ['recoveryTime', 10 * MINUTE * 1000, '<='], ['attributes4', SPELL_ATTR4_USABLE_IN_ARENA, '&']]
+ ];
+ else
+ return [DB::OR,
+ ['attributes4', SPELL_ATTR4_NOT_USABLE_IN_ARENA, '&'],
+ [DB::AND, ['recoveryTime', 10 * MINUTE * 1000, '>'], [['attributes4', SPELL_ATTR4_USABLE_IN_ARENA, '&'], 0]]
+ ];
+ }
+
+ protected function cbBandageSpell(int $cr, int $crs, string $crv) : ?array
+ {
+ if (!$this->int2Bool($crs))
+ return null;
+
+ if ($crs) // match exact, not as flag
+ return [DB::AND, ['attributes1', SPELL_ATTR1_CHANNELED_1 | SPELL_ATTR1_CHANNELED_2 | SPELL_ATTR1_CHANNEL_TRACK_TARGET], ['effect1ImplicitTargetA', 21]];
+ else
+ return [DB::OR, ['attributes1', SPELL_ATTR1_CHANNELED_1 | SPELL_ATTR1_CHANNELED_2 | SPELL_ATTR1_CHANNEL_TRACK_TARGET, '!'], ['effect1ImplicitTargetA', 21, '!']];
+ }
+
+ protected function cbProficiency(int $cr, int $crs, string $crv) : ?array
+ {
+ if (!isset(self::$enums[$cr][$crs]))
+ return null;
+
+ $skill1Ids = [];
+ $skill2Mask = 0x0;
+
+ switch($crs)
+ {
+ case 1: // Weapons
+ foreach (Game::$skillLineMask[-3] as $bit => $_)
+ $skill2Mask |= (1 << $bit);
+ $skill1Ids = DB::Aowow()->selectCol('SELECT `id` FROM ::skillline WHERE `typeCat` = 6');
+ break;
+ case 2: // Armor (Proficiencies + Specializations: so for us it's the same)
+ case 3: // Armor Proficiencies
+ $skill1Ids = DB::Aowow()->selectCol('SELECT `id` FROM ::skillline WHERE `typeCat` = 8');
+ break;
+ case 4: // Armor Specializations
+ return [0]; // 4.x+ feature where using purely one type of armor increases your primary stat
+ case 5: // Languages
+ $skill1Ids = DB::Aowow()->selectCol('SELECT `id` FROM ::skillline WHERE `typeCat` = 10');
+ break;
+ }
+
+ if (!$skill1Ids)
+ return [0];
+
+ $cnd = ['skillLine1', $skill1Ids];
+ if ($skill2Mask)
+ $cnd = [DB::OR, $cnd, [DB::AND, ['skillLine1', -3], ['skillLine2OrMask', $skill2Mask, '&']]];
+
+ return $cnd;
+ }
+}
+
+?>
diff --git a/includes/dbtypes/title.class.php b/includes/dbtypes/title.class.php
new file mode 100644
index 00000000..b583a290
--- /dev/null
+++ b/includes/dbtypes/title.class.php
@@ -0,0 +1,180 @@
+ [['src']], // 11: Type::TITLE
+ 'src' => ['j' => ['::source src ON `type` = 11 AND `typeId` = t.`id`', true], 's' => ', `src13`, `moreType`, `moreTypeId`']
+ );
+
+ public function __construct(array $conditions = [], array $miscData = [])
+ {
+ parent::__construct($conditions, $miscData);
+
+ // post processing
+ foreach ($this->iterate() as &$_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][SRC_ACHIEVEMENT][] = $_curTpl['moreTypeId'];
+ else if ($_curTpl['moreType'] == Type::QUEST)
+ $this->sources[$this->id][SRC_QUEST][] = $_curTpl['moreTypeId'];
+ else if ($_curTpl['src13'])
+ $this->sources[$this->id][SRC_CUSTOM_STRING][] = $_curTpl['src13'];
+
+ // titles display up to two achievements at once
+ if ($_curTpl['src12Ext'])
+ $this->sources[$this->id][SRC_ACHIEVEMENT][] = $_curTpl['src12Ext'];
+
+ unset($_curTpl['src12Ext']);
+ unset($_curTpl['moreType']);
+ unset($_curTpl['moreTypeId']);
+ unset($_curTpl['src3']);
+
+ // shorthand for more generic access; required by CommunityContent to determine subject
+ foreach (Locale::cases() as $loc)
+ if ($loc->validate())
+ $_curTpl['name'] = new LocString($_curTpl, 'male', fn($x) => trim(str_replace('%s', '', $x)));
+ // $_curTpl['name_loc'.$loc->value] = trim(str_replace('%s', '', $_curTpl['male_loc'.$loc->value]));
+ }
+ }
+
+ public static function getName(int $id) : ?LocString
+ {
+ if ($n = DB::Aowow()->SelectRow('SELECT `male_loc0`, `male_loc2`, `male_loc3`, `male_loc4`, `male_loc6`, `male_loc8` FROM %n WHERE `id` = %i', self::$dataTable, $id))
+ return new LocString($n, 'male', fn($x) => trim(str_replace('%s', '', $x)));
+ return null;
+ }
+
+ public function getListviewData() : array
+ {
+ $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(int $addMask = 0) : array
+ {
+ $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() : void
+ {
+ $sources = array(
+ SRC_QUEST => [],
+ SRC_ACHIEVEMENT => [],
+ SRC_CUSTOM_STRING => []
+ );
+
+ 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[SRC_QUEST]))
+ $sources[SRC_QUEST] = (new QuestList(array(['id', $sources[SRC_QUEST]])))->getSourceData();
+
+ if (!empty($sources[SRC_ACHIEVEMENT]))
+ $sources[SRC_ACHIEVEMENT] = (new AchievementList(array(['id', $sources[SRC_ACHIEVEMENT]])))->getSourceData();
+
+ foreach ($this->sources as $Id => $src)
+ {
+ $tmp = [];
+
+ // Quest-source
+ if (isset($src[SRC_QUEST]))
+ {
+ foreach ($src[SRC_QUEST] as $s)
+ {
+ if (isset($sources[SRC_QUEST][$s]['s']))
+ $this->faction2Side($sources[SRC_QUEST][$s]['s']);
+
+ $tmp[SRC_QUEST][] = $sources[SRC_QUEST][$s];
+ }
+ }
+
+ // Achievement-source
+ if (isset($src[SRC_ACHIEVEMENT]))
+ {
+ foreach ($src[SRC_ACHIEVEMENT] as $s)
+ {
+ if (isset($sources[SRC_ACHIEVEMENT][$s]['s']))
+ $this->faction2Side($sources[SRC_ACHIEVEMENT][$s]['s']);
+
+ $tmp[SRC_ACHIEVEMENT][] = $sources[SRC_ACHIEVEMENT][$s];
+ }
+ }
+
+ // other source (only one item possible, so no iteration needed)
+ if (isset($src[SRC_CUSTOM_STRING]))
+ $tmp[SRC_CUSTOM_STRING] = [Lang::game('pvpSources', $Id)];
+
+ $this->templates[$Id]['source'] = $tmp;
+ }
+ }
+
+ public function getHtmlizedName(int $gender = GENDER_MALE) : string
+ {
+ $field = $gender == GENDER_FEMALE ? 'female' : 'male';
+ return str_replace('%s', '<'.Util::ucFirst(Lang::main('name')).'>', $this->getField($field, true));
+ }
+
+ public function renderTooltip() : ?string { return null; }
+
+ private function faction2Side(int &$faction) : void // 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/dbtypes/user.class.php b/includes/dbtypes/user.class.php
new file mode 100644
index 00000000..18122f6b
--- /dev/null
+++ b/includes/dbtypes/user.class.php
@@ -0,0 +1,89 @@
+ [['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 getJSGlobals(int $addMask = 0) : array
+ {
+ $data = [];
+
+ foreach ($this->iterate() as $userId => $__)
+ {
+ $data[$this->curTpl['username']] = array(
+ 'border' => $this->getPremiumborder(),
+ '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 seen on user page..?)
+ if ($_ = $this->curTpl['title'])
+ $data[$this->curTpl['username']]['title'] = $_;
+
+ switch ($this->curTpl['avatar'])
+ {
+ case 1:
+ $data[$this->curTpl['username']]['avatar'] = $this->curTpl['avatar'];
+ $data[$this->curTpl['username']]['avatarmore'] = $this->curTpl['wowicon'];
+ break;
+ case 2:
+ if ($this->isPremium())
+ {
+ if ($av = DB::Aowow()->selectCell('SELECT `id` FROM ::account_avatars WHERE `userId` = %i AND `current` = 1 AND `status` <> %i', $userId, AvatarMgr::STATUS_REJECTED))
+ {
+ $data[$this->curTpl['username']]['avatar'] = $this->curTpl['avatar'];
+ $data[$this->curTpl['username']]['avatarmore'] = $av;
+ }
+ }
+ break;
+ }
+
+ // more optional data
+ // sig: markdown formated string (only used in forum?)
+ }
+
+ return [Type::USER => $data];
+ }
+
+ // seen as null|1|3 .. changes the border around the avatar (chosen from account > premium tab?)
+ // changed at the end of MoP. No longer a jsBool but index to Icon.premiumBorderClasses
+ private function getPremiumBorder() : int
+ {
+ if (!$this->isPremium() || !$this->curTpl['avatar'])
+ return 2; // 2 is "none"
+
+ return $this->curTpl['avatarborder'];
+ }
+
+ public function isPremium() : bool
+ {
+ return $this->curTpl['userGroups'] & U_GROUP_PREMIUM || $this->curTpl['reputation'] >= Cfg::get('REP_REQ_PREMIUM');
+ }
+
+ public function getListviewData() : array { return []; }
+ public function renderTooltip() : ?string { return null; }
+
+ public static function getName($id) : ?LocString { return null; }
+}
+
+?>
diff --git a/includes/dbtypes/worldevent.class.php b/includes/dbtypes/worldevent.class.php
new file mode 100644
index 00000000..399d6083
--- /dev/null
+++ b/includes/dbtypes/worldevent.class.php
@@ -0,0 +1,176 @@
+ [['h', 'ic']],
+ 'h' => ['j' => ['::holidays h ON e.`holidayId` = h.`id`', true], 's' => ', h.*', 'o' => '-e.`id` ASC'],
+ 'ic' => ['j' => ['::icons ic ON ic.`id` = h.`iconId`', true], 's' => ', ic.`name` AS "iconString"']
+ );
+
+ public function __construct(array $conditions = [], array $miscData = [])
+ {
+ parent::__construct($conditions, $miscData);
+
+ // 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['eventId']] = $data;
+ }
+ }
+
+ public static function getName(int $id) : ?LocString
+ {
+ $row = DB::Aowow()->SelectRow(
+ 'SELECT IFNULL(h.`name_loc0`, e.`description`) AS "name_loc0", h.`name_loc2`, h.`name_loc3`, h.`name_loc4`, h.`name_loc6`, h.`name_loc8`
+ FROM ::events e
+ LEFT JOIN ::holidays h ON e.`holidayId` = h.`id`
+ WHERE e.`id` = %i',
+ $id
+ );
+
+ return $row ? new LocString($row) : null;
+ }
+
+ public static function updateDates(?array $date = null, ?int &$start = null, ?int &$end = null, ?int &$rec = null) : bool
+ {
+ if (!$date || empty($date['firstDate']) || empty($date['length']))
+ return false;
+
+ $start = $date['firstDate'];
+ $end = $date['firstDate'] + $date['length'];
+ $rec = $date['rec'] ?: -1; // interval
+
+ if ($rec < 0 || $date['lastDate'] < time())
+ return true;
+
+ $nIntervals = (int)ceil((time() - $end) / $rec);
+
+ $start += $nIntervals * $rec;
+ $end += $nIntervals * $rec;
+
+ return true;
+ }
+
+ public static function updateListview(Listview &$listview) : void
+ {
+ foreach ($listview->iterate() as &$row)
+ {
+ WorldEventList::updateDates($row['_date'] ?? null, $start, $end, $rec);
+
+ $row['startDate'] = $start ? date(Util::$dateFormatInternal, $start) : null;
+ $row['endDate'] = $end ? date(Util::$dateFormatInternal, $end - 1) : null;
+ $row['rec'] = $rec;
+
+ unset($row['_date']);
+ }
+ }
+
+ public function getListviewData() : array
+ {
+ $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']
+ )
+ );
+ }
+
+ return $data;
+ }
+
+ public function getJSGlobals(int $addMask = 0) : array
+ {
+ $data = [];
+
+ foreach ($this->iterate() as $__)
+ $data[Type::WORLDEVENT][$this->id] = ['name' => $this->getField('name', true), 'icon' => $this->curTpl['iconString']];
+
+ return $data;
+ }
+
+ public function renderTooltip() : ?string
+ {
+ if (!$this->curTpl)
+ return null;
+
+ $x = '';
+
+ // head v that extra % is nesecary because we are using sprintf later on
+ $x .= '| '.$this->getField('name', true).' | '.Lang::event('category', $this->getField('category')).' |
|---|
';
+
+ // use string-placeholder for dates
+ // start
+ $x .= Lang::event('start').'%s ';
+ // end
+ $x .= Lang::event('end').'%s';
+
+ $x .= ' | ';
+
+ // desc
+ if ($this->getField('holidayId'))
+ if ($_ = $this->getField('description', true))
+ $x .= '';
+
+ return $x;
+ }
+}
+
+?>
diff --git a/includes/types/zone.class.php b/includes/dbtypes/zone.class.php
similarity index 76%
rename from includes/types/zone.class.php
rename to includes/dbtypes/zone.class.php
index e1e97d98..668eac6e 100644
--- a/includes/types/zone.class.php
+++ b/includes/dbtypes/zone.class.php
@@ -1,20 +1,22 @@
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()
+ public function getListviewData() : array
{
$data = [];
@@ -95,17 +90,17 @@ class ZoneList extends BaseType
return $data;
}
- public function getJSGlobals($addMask = 0)
+ public function getJSGlobals(int $addMask = 0) : array
{
$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() { }
+ public function renderTooltip() : ?string { return null; }
}
?>
diff --git a/includes/defines.php b/includes/defines.php
index 4f92bbdc..4075085a 100644
--- a/includes/defines.php
+++ b/includes/defines.php
@@ -1,5 +1,7 @@
10min
+define('SPELL_ATTR4_AREA_TARGET_CHAIN', 0x00040000); // Chain area targets DESCRIPTION [NYI] Hits area targets over time instead of all at once
+define('SPELL_ATTR4_ALLOW_PROC_WHILE_SITTING', 0x00080000); // [WoWDev Wiki]
+define('SPELL_ATTR4_NOT_CHECK_SELFCAST_POWER', 0x00100000); // Allow self-cast to override stronger aura (client only) - NOTE! modern name AURA_NEVER_BOUNCES (similar meaning)
+define('SPELL_ATTR4_DONT_REMOVE_IN_ARENA', 0x00200000); // Keep when entering arena
+define('SPELL_ATTR4_PROC_SUPPRESS_SWING_ANIM', 0x00400000); // [WoWDev Wiki] Disables client side weapon swing animation.
+define('SPELL_ATTR4_CANT_TRIGGER_ITEM_SPELLS', 0x00800000); // Cannot trigger item spells
+define('SPELL_ATTR4_AUTO_RANGED_COMBAT', 0x01000000); // [WoWDev Wiki]
+define('SPELL_ATTR4_IS_PET_SCALING', 0x02000000); // Pet Scaling aura
+define('SPELL_ATTR4_CAST_ONLY_IN_OUTLAND', 0x04000000); // Only in Outland/Northrend - NOTE! modern client name is ONLY_FLYING_AREAS (similar, more correct), WH is "Allow Equip While Casting", (wtf, seriously)
+define('SPELL_ATTR4_FORCE_DISPLAY_CASTBAR', 0x08000000); //
+define('SPELL_ATTR4_IGNORE_COMBAT_TIMER', 0x10000000); // [WoWDev Wiki]
+define('SPELL_ATTR4_AURA_BOUNCE_FAILS_SPELL', 0x20000000); // [WoWDev Wiki]
+define('SPELL_ATTR4_OBSOLETE', 0x40000000); // [WoWDev Wiki] Deprecates the spell making it greyed out and gives "You can't use that here" error. Still usable with the triggered flag command though.
+define('SPELL_ATTR4_USE_FACING_FROM_SPELL', 0x80000000); // [WoWDev Wiki] Affects orientation. The value used is likely related to FacingCasterFlags in Spell.dbc for 3.3.5.
+
+define('SPELL_ATTR5_CAN_CHANNEL_WHEN_MOVING', 0x00000001); // Can be channeled while moving
+define('SPELL_ATTR5_NO_REAGENT_WHILE_PREP', 0x00000002); // No reagents during arena preparation
+define('SPELL_ATTR5_REMOVE_ON_ARENA_ENTER', 0x00000004); // Remove when entering arena DESCRIPTION Force this aura to be removed on entering arena, regardless of other properties
+define('SPELL_ATTR5_USABLE_WHILE_STUNNED', 0x00000008); // Usable while stunned
+define('SPELL_ATTR5_TRIGGERS_CHANNELING', 0x00000010); // [WoWDev Wiki] Likely more script oriented.
+define('SPELL_ATTR5_SINGLE_TARGET_SPELL', 0x00000020); // Single-target aura DESCRIPTION Remove previous application to another unit if applied
+define('SPELL_ATTR5_IGNORE_AREA_EFFECT_PVP_CHECK', 0x00000040); // [WoWDev Wiki] Possible world PvP flag for objectives such as Spirit Towers?
+define('SPELL_ATTR5_NOT_ON_PLAYER', 0x00000080); // [WoWDev Wiki] Opposite of SPELL_ATTR3_ONLY_TARGET_PLAYERS
+define('SPELL_ATTR5_CANT_TARGET_PLAYER_CONTROLLED', 0x00000100); // Cannot target player controlled units but can target players
+define('SPELL_ATTR5_START_PERIODIC_AT_APPLY', 0x00000200); // Immediately do periodic tick on apply
+define('SPELL_ATTR5_HIDE_DURATION', 0x00000400); // Do not send aura duration to client
+define('SPELL_ATTR5_ALLOW_TARGET_OF_TARGET_AS_TARGET', 0x00000800); // Auto-target target of target (client only)
+define('SPELL_ATTR5_MELEE_CHAIN_TARGETING', 0x00001000); // [WoWDev Wiki] Cleave related?
+define('SPELL_ATTR5_HASTE_AFFECT_DURATION', 0x00002000); // Duration scales with Haste Rating
+define('SPELL_ATTR5_NOT_USABLE_WHILE_CHARMED', 0x00004000); // Charmed units cannot cast this spell
+define('SPELL_ATTR5_TREAT_AS_AREA_EFFECT', 0x00008000); // [WoWDev Wiki] Related to multi-target spells?
+define('SPELL_ATTR5_AURA_AFFECTS_NOT_JUST_REQ_EQUIPPED_ITEM', 0x00010000); // [WoWDev Wiki]
+define('SPELL_ATTR5_USABLE_WHILE_FEARED', 0x00020000); // Usable while feared
+define('SPELL_ATTR5_USABLE_WHILE_CONFUSED', 0x00040000); // Usable while confused
+define('SPELL_ATTR5_DONT_TURN_DURING_CAST', 0x00080000); // Do not auto-turn while casting
+define('SPELL_ATTR5_DO_NOT_ATTEMPT_A_PET_RESUMMON_WHEN_DISMOUNTING', 0x00100000); // [WoWDev Wiki]
+define('SPELL_ATTR5_IGNORE_TARGET_REQUIREMENTS', 0x00200000); // [WoWDev Wiki]
+define('SPELL_ATTR5_NOT_ON_TRIVIAL', 0x00400000); // [WoWDev Wiki]
+define('SPELL_ATTR5_NO_PARTIAL_RESISTS', 0x00800000); // [WoWDev Wiki] Spell will either be fully resisted or deal the full amount of damage.
+define('SPELL_ATTR5_IGNORE_CASTER_REQUIREMENTS', 0x01000000); // [WoWDev Wiki]
+define('SPELL_ATTR5_ALWAYS_LINE_OF_SIGHT', 0x02000000); // [WoWDev Wiki] Constant line of sight required for spell duration.
+define('SPELL_ATTR5_SKIP_CHECKCAST_LOS_CHECK', 0x04000000); // Ignore line of sight checks
+define('SPELL_ATTR5_DONT_SHOW_AURA_IF_SELF_CAST', 0x08000000); // Don't show aura if self-cast (client only)
+define('SPELL_ATTR5_DONT_SHOW_AURA_IF_NOT_SELF_CAST', 0x10000000); // Don't show aura unless self-cast (client only)
+define('SPELL_ATTR5_AURA_UNIQUE_PER_CASTER', 0x20000000); // [WoWDev Wiki] Could be used for debuff grouping.
+define('SPELL_ATTR5_ALWAYS_SHOW_GROUND_TEXTURE', 0x40000000); // [WoWDev Wiki] Likely refers to the Projected Texture setting and will cause this spell to ignore its value.
+define('SPELL_ATTR5_ADD_MELEE_HIT_RATING', 0x80000000); // [WoWDev Wiki] (Forces nearby enemies to attack caster?)
+
+define('SPELL_ATTR6_DONT_DISPLAY_COOLDOWN', 0x00000001); // Don't display cooldown (client only)
+define('SPELL_ATTR6_ONLY_IN_ARENA', 0x00000002); // Only usable in arena
+define('SPELL_ATTR6_IGNORE_CASTER_AURAS', 0x00000004); // Ignore all preventing caster auras - NOTE! leak Data and WH name this NOT_AN_ATTACK
+define('SPELL_ATTR6_ASSIST_IGNORE_IMMUNE_FLAG', 0x00000008); // Ignore immunity flags when assisting
+define('SPELL_ATTR6_IGNORE_FOR_MOD_TIME_RATE', 0x00000010); // [WoWDev Wiki]
+define('SPELL_ATTR6_DONT_CONSUME_PROC_CHARGES', 0x00000020); // Don't consume proc charges
+define('SPELL_ATTR6_USE_SPELL_CAST_EVENT', 0x00000040); // Generate spell_cast event instead of aura_start (client only) - NOTE! FLOATING_COMBAT_TEXT_ON_CAST in modern client, but visual UI procs are not in 335
+define('SPELL_ATTR6_AURA_IS_WEAPON_PROC', 0x00000080); // [WoWDev Wiki]
+define('SPELL_ATTR6_CANT_TARGET_CROWD_CONTROLLED', 0x00000100); // Do not implicitly target in CC DESCRIPTION Implicit targeting (chaining and area targeting) will not impact crowd controlled targets
+define('SPELL_ATTR6_ALLOW_ON_CHARMED_TARGETS', 0x00000200); // [WoWDev Wiki]
+define('SPELL_ATTR6_CAN_TARGET_POSSESSED_FRIENDS', 0x00000400); // Can target possessed friends DESCRIPTION [NYI] - NOTE! leak data and WH name this NO_AURA_LOG and it really prevents aura apply/remove messages in combat log
+define('SPELL_ATTR6_NOT_IN_RAID_INSTANCE', 0x00000800); // Unusable in raid instances
+define('SPELL_ATTR6_CASTABLE_WHILE_ON_VEHICLE', 0x00001000); // Castable while caster is on vehicle
+define('SPELL_ATTR6_CAN_TARGET_INVISIBLE', 0x00002000); // Can target invisible units
+define('SPELL_ATTR6_AI_PRIMARY_RANGED_ATTACK', 0x00004000); // [WoWDev Wiki] Related to Shoot? Needs description.
+define('SPELL_ATTR6_NO_PUSHBACK', 0x00008000); // [WoWDev Wiki]
+define('SPELL_ATTR6_NO_JUMP_PATHING', 0x00010000); // [WoWDev Wiki]
+define('SPELL_ATTR6_ALLOW_EQUIP_WHILE_CASTING', 0x00020000); // [WoWDev Wiki] Mount related?
+define('SPELL_ATTR6_CAST_BY_CHARMER', 0x00040000); // Spell is cast by charmer DESCRIPTION Client will prevent casting if not possessed, charmer will be caster for all intents and purposes
+define('SPELL_ATTR6_DELAY_COMBAT_TIMER_DURING_CAST', 0x00080000); // [WoWDev Wiki]
+define('SPELL_ATTR6_ONLY_VISIBLE_TO_CASTER', 0x00100000); // Only visible to caster (client only)
+define('SPELL_ATTR6_CLIENT_UI_TARGET_EFFECTS', 0x00200000); // Client UI target effects (client only) - NOTE! SHOW_MECHANIC_AS_COMBAT_TEXT in modern client .. neither descriptor seems to be true
+define('SPELL_ATTR6_ABSORB_CANNOT_BE_IGNORE', 0x00400000); // [WoWDev Wiki]
+define('SPELL_ATTR6_TAPS_IMMEDIATELY', 0x00800000); // [WoWDev Wiki]
+define('SPELL_ATTR6_CAN_TARGET_UNTARGETABLE', 0x01000000); // Can target untargetable units
+define('SPELL_ATTR6_NOT_RESET_SWING_IF_INSTANT', 0x02000000); // Do not reset swing timer if cast time is instant
+define('SPELL_ATTR6_VEHICLE_IMMUNITY_CATEGORY', 0x04000000); // [WoWDev Wiki] immunity to some buffs for some vehicles.
+define('SPELL_ATTR6_LIMIT_PCT_HEALING_MODS', 0x08000000); // Limit applicable %healing modifiers DESCRIPTION This prevents certain healing modifiers from applying - see implementation if you really care about details
+define('SPELL_ATTR6_DO_NOT_AUTO_SELECT_TARGET_WITH_INITIATES_COMBAT', 0x10000000); // [WoWDev Wiki] Death grip?
+define('SPELL_ATTR6_LIMIT_PCT_DAMAGE_MODS', 0x20000000); // Limit applicable %damage modifiers DESCRIPTION This prevents certain damage modifiers from applying - see implementation if you really care about details
+define('SPELL_ATTR6_DISABLE_TIED_EFFECT_POINTS', 0x40000000); // [WoWDev Wiki] The value used is likely from the SpellEffect column EffectBasePoints
+define('SPELL_ATTR6_IGNORE_CATEGORY_COOLDOWN_MODS', 0x80000000); // Ignore cooldown modifiers for category cooldown
+
+define('SPELL_ATTR7_ALLOW_SPELL_REFLECTION', 0x00000001); // [WoWDev Wiki] Allow spell to be reflected. Will likely interfere if used with SPELL_ATTR1_CANT_BE_REFLECTED.
+define('SPELL_ATTR7_IGNORE_DURATION_MODS', 0x00000002); // Ignore duration modifiers
+define('SPELL_ATTR7_DISABLE_AURA_WHILE_DEAD', 0x00000004); // Reactivate at resurrect (client only)
+define('SPELL_ATTR7_IS_CHEAT_SPELL', 0x00000008); // Is cheat spell DESCRIPTION Cannot cast if caster doesn't have UnitFlag2 & UNIT_FLAG2_ALLOW_CHEAT_SPELLS
+define('SPELL_ATTR7_TREAT_AS_RAID_BUFF', 0x00000010); // [WoWDev Wiki] Spell assumes certain properties that would classify it as a "raid buff". (This is only a guess.)
+define('SPELL_ATTR7_SUMMON_PLAYER_TOTEM', 0x00000020); // Summons player-owned totem
+define('SPELL_ATTR7_NO_PUSHBACK_ON_DAMAGE', 0x00000040); // Damage dealt by this does not cause spell pushback
+define('SPELL_ATTR7_PREPARE_FOR_VEHICLE_CONTROL_END', 0x00000080); // [WoWDev Wiki] Attribute is most likely server side only.
+define('SPELL_ATTR7_HORDE_ONLY', 0x00000100); // Horde only
+define('SPELL_ATTR7_ALLIANCE_ONLY', 0x00000200); // Alliance only
+define('SPELL_ATTR7_DISPEL_CHARGES', 0x00000400); // Dispel/Spellsteal remove individual charges
+define('SPELL_ATTR7_INTERRUPT_ONLY_NONPLAYER', 0x00000800); // Only interrupt non-player casting
+define('SPELL_ATTR7_CAN_CAUSE_SILENCE', 0x00001000); // [WoWDev Wiki] Will only Silence NPCs/creatures. (Not confirmed.)
+define('SPELL_ATTR7_NO_UI_NOT_INTERRUPTIBLE', 0x00002000); // [WoWDev Wiki] Can always be interrupted, even if caster is immune.
+define('SPELL_ATTR7_RECAST_ON_RESUMMON', 0x00004000); // [WoWDev Wiki] only on 52150 Raise Dead.
+define('SPELL_ATTR7_RESET_SWING_TIMER_AT_SPELL_START', 0x00008000); // [WoWDev Wiki] (Exorcism - guaranteed crit vs families?)
+define('SPELL_ATTR7_CAN_RESTORE_SECONDARY_POWER', 0x00010000); // Can restore secondary power DESCRIPTION Only spells with this attribute can replenish a non-active power type - NOTE! replaed with ONLY_IN_SPELLBOOK_UNTIL_LEARNED in modern client
+define('SPELL_ATTR7_DO_NOT_LOG_PVP_KILL', 0x00020000); // [WoWDev Wiki]
+define('SPELL_ATTR7_HAS_CHARGE_EFFECT', 0x00040000); // Has charge effect
+define('SPELL_ATTR7_ZONE_TELEPORT', 0x00080000); // Is zone teleport - NOTE! REPORT_SPELL_FAILURE_TO_UNIT_TARGET in modern client, but may still serve the same purpose as teleport spell ofter use custom error messages
+define('SPELL_ATTR7_NO_CLIENT_FAIL_WHILE_STUNNED_FLEEING_CONFUSED', 0x00100000); // [WoWDev Wiki] Client will skip or bypass checking for stunned, fleeing, and confused states.
+define('SPELL_ATTR7_RETAIN_COOLDOWN_THROUGH_LOAD', 0x00200000); // [WoWDev Wiki]
+define('SPELL_ATTR7_IGNORE_COLD_WEATHER_FLYING', 0x00400000); // Ignore cold weather flying restriction DESCRIPTION Set for loaner mounts, allows them to be used despite lacking required flight skill
+define('SPELL_ATTR7_CANT_DODGE', 0x00800000); // Spell cannot be dodged
+define('SPELL_ATTR7_CANT_PARRY', 0x01000000); // Spell cannot be parried
+define('SPELL_ATTR7_CANT_MISS', 0x02000000); // Spell cannot be missed
+define('SPELL_ATTR7_TREAT_AS_NPC_AOE', 0x04000000); // [WoWDev Wiki]
+define('SPELL_ATTR7_BYPASS_NO_RESURRECT_AURA', 0x08000000); // Bypasses the prevent resurrection aura
+define('SPELL_ATTR7_CONSOLIDATED_RAID_BUFF', 0x10000000); // Consolidate in raid buff frame (client only)
+define('SPELL_ATTR7_REFLECTION_ONLY_DEFENDS', 0x20000000); // [WoWDev Wiki] This possibly allows for a spell to be reflected but not damage the target and instead act more as a deflect.
+define('SPELL_ATTR7_CAN_PROC_FROM_SUPPRESSED_TARGET_PROCS', 0x40000000); // [WoWDev Wiki]
+define('SPELL_ATTR7_CLIENT_INDICATOR', 0x80000000); // Client indicator (client only)
+
+
+// (some) Skill ids
+define('SKILL_FIRST_AID', 129);
+define('SKILL_BLACKSMITHING', 164);
+define('SKILL_LEATHERWORKING', 165);
+define('SKILL_ALCHEMY', 171);
+define('SKILL_HERBALISM', 182);
+define('SKILL_COOKING', 185);
+define('SKILL_MINING', 186);
+define('SKILL_TAILORING', 197);
+define('SKILL_ENGINEERING', 202);
+define('SKILL_ENCHANTING', 333);
+define('SKILL_FISHING', 356);
+define('SKILL_SKINNING', 393);
+define('SKILL_LOCKPICKING', 633);
+define('SKILL_JEWELCRAFTING', 755);
+define('SKILL_RIDING', 762);
+define('SKILL_INSCRIPTION', 773);
+define('SKILL_MOUNTS', 777);
+define('SKILL_COMPANIONS', 778);
+
+define('SKILLS_TRADE_PRIMARY', [SKILL_BLACKSMITHING, SKILL_LEATHERWORKING, SKILL_ALCHEMY, SKILL_HERBALISM, SKILL_MINING, SKILL_TAILORING, SKILL_ENGINEERING, SKILL_ENCHANTING, SKILL_SKINNING, SKILL_JEWELCRAFTING, SKILL_INSCRIPTION]);
+define('SKILLS_TRADE_SECONDARY', [SKILL_FIRST_AID, SKILL_COOKING, SKILL_FISHING, SKILL_RIDING]);
+
+// (some) key currencies
+define('CURRENCY_ARENA_POINTS', 103);
+define('CURRENCY_HONOR_POINTS', 104);
// AchievementCriteriaCondition
define('ACHIEVEMENT_CRITERIA_CONDITION_NO_DEATH', 1); // reset progress on death
@@ -707,11 +1890,13 @@ define('ACHIEVEMENT_CRITERIA_CONDITION_NOT_IN_GROUP', 10);
define('ACHIEVEMENT_FLAG_COUNTER', 0x0001); // Just count statistic (never stop and complete)
define('ACHIEVEMENT_FLAG_HIDDEN', 0x0002); // Not sent to client - internal use only
define('ACHIEVEMENT_FLAG_STORE_MAX_VALUE', 0x0004); // Store only max value? used only in "Reach level xx"
-define('ACHIEVEMENT_FLAG_SUMM', 0x0008); // Use summ criteria value from all reqirements (and calculate max value)
+define('ACHIEVEMENT_FLAG_SUM', 0x0008); // Use sum criteria value from all reqirements (and calculate max value)
define('ACHIEVEMENT_FLAG_MAX_USED', 0x0010); // Show max criteria (and calculate max value ??)
define('ACHIEVEMENT_FLAG_REQ_COUNT', 0x0020); // Use not zero req count (and calculate max value)
define('ACHIEVEMENT_FLAG_AVERAGE', 0x0040); // Show as average value (value / time_in_days) depend from other flag (by def use last criteria value)
-define('ACHIEVEMENT_FLAG_BAR', 0x0080); // Show as progress bar (value / max vale) depend from other flag (by def use last criteria value)
+define('ACHIEVEMENT_FLAG_PROGRESS_BAR', 0x0080); // Show as progress bar (value / max vale) depend from other flag (by def use last criteria value)
+define('ACHIEVEMENT_FLAG_REALM_FIRST', 0x0100); // first max race/class/profession
+define('ACHIEVEMENT_FLAG_REALM_FIRST_KILL', 0x0200); // first boss kill
// AchievementCriteriaFlags
define('ACHIEVEMENT_CRITERIA_FLAG_SHOW_PROGRESS_BAR', 0x0001); // Show progress as bar
@@ -781,7 +1966,7 @@ define('ACHIEVEMENT_CRITERIA_TYPE_USE_GAMEOBJECT', 68);
define('ACHIEVEMENT_CRITERIA_TYPE_BE_SPELL_TARGET2', 69);
// define('ACHIEVEMENT_CRITERIA_TYPE_SPECIAL_PVP_KILL', 70);
define('ACHIEVEMENT_CRITERIA_TYPE_FISH_IN_GAMEOBJECT', 72);
-define('ACHIEVEMENT_CRITERIA_TYPE_EARNED_PVP_TITLE', 74);
+define('ACHIEVEMENT_CRITERIA_TYPE_ON_LOGIN', 74);
define('ACHIEVEMENT_CRITERIA_TYPE_LEARN_SKILLLINE_SPELLS', 75);
// define('ACHIEVEMENT_CRITERIA_TYPE_WIN_DUEL', 76);
// define('ACHIEVEMENT_CRITERIA_TYPE_LOSE_DUEL', 77);
@@ -821,95 +2006,79 @@ define('ACHIEVEMENT_CRITERIA_TYPE_LEARN_SKILL_LINE', 112);
// define('ACHIEVEMENT_CRITERIA_TYPE_DISENCHANT_ROLLS', 117);
// define('ACHIEVEMENT_CRITERIA_TYPE_USE_LFD_TO_GROUP_WITH_PLAYERS', 119);
-// TrinityCore - Condition System
-define('CND_SRC_CREATURE_LOOT_TEMPLATE', 1);
-define('CND_SRC_DISENCHANT_LOOT_TEMPLATE', 2);
-define('CND_SRC_FISHING_LOOT_TEMPLATE', 3);
-define('CND_SRC_GAMEOBJECT_LOOT_TEMPLATE', 4);
-define('CND_SRC_ITEM_LOOT_TEMPLATE', 5);
-define('CND_SRC_MAIL_LOOT_TEMPLATE', 6);
-define('CND_SRC_MILLING_LOOT_TEMPLATE', 7);
-define('CND_SRC_PICKPOCKETING_LOOT_TEMPLATE', 8);
-define('CND_SRC_PROSPECTING_LOOT_TEMPLATE', 9);
-define('CND_SRC_REFERENCE_LOOT_TEMPLATE', 10);
-define('CND_SRC_SKINNING_LOOT_TEMPLATE', 11);
-define('CND_SRC_SPELL_LOOT_TEMPLATE', 12);
-define('CND_SRC_SPELL_IMPLICIT_TARGET', 13);
-define('CND_SRC_GOSSIP_MENU', 14);
-define('CND_SRC_GOSSIP_MENU_OPTION', 15);
-define('CND_SRC_CREATURE_TEMPLATE_VEHICLE', 16);
-define('CND_SRC_SPELL', 17);
-define('CND_SRC_SPELL_CLICK_EVENT', 18);
-define('CND_SRC_QUEST_ACCEPT', 19);
-define('CND_SRC_QUEST_SHOW_MARK', 20);
-define('CND_SRC_VEHICLE_SPELL', 21);
-define('CND_SRC_SMART_EVENT', 22);
-define('CND_SRC_NPC_VENDOR', 23);
-define('CND_SRC_SPELL_PROC', 24);
+// TrinityCore - Achievement Criteria Data
+define('ACHIEVEMENT_CRITERIA_DATA_TYPE_NONE', 0);
+define('ACHIEVEMENT_CRITERIA_DATA_TYPE_T_CREATURE', 1);
+define('ACHIEVEMENT_CRITERIA_DATA_TYPE_T_PLAYER_CLASS_RACE', 2);
+define('ACHIEVEMENT_CRITERIA_DATA_TYPE_T_PLAYER_LESS_HEALTH', 3);
+define('ACHIEVEMENT_CRITERIA_DATA_TYPE_T_PLAYER_DEAD', 4);
+define('ACHIEVEMENT_CRITERIA_DATA_TYPE_S_AURA', 5);
+define('ACHIEVEMENT_CRITERIA_DATA_TYPE_S_AREA', 6);
+define('ACHIEVEMENT_CRITERIA_DATA_TYPE_T_AURA', 7);
+define('ACHIEVEMENT_CRITERIA_DATA_TYPE_VALUE', 8);
+define('ACHIEVEMENT_CRITERIA_DATA_TYPE_T_LEVEL', 9);
+define('ACHIEVEMENT_CRITERIA_DATA_TYPE_T_GENDER', 10);
+define('ACHIEVEMENT_CRITERIA_DATA_TYPE_SCRIPT', 11);
+define('ACHIEVEMENT_CRITERIA_DATA_TYPE_MAP_DIFFICULTY', 12);
+define('ACHIEVEMENT_CRITERIA_DATA_TYPE_MAP_PLAYER_COUNT', 13);
+define('ACHIEVEMENT_CRITERIA_DATA_TYPE_T_TEAM', 14);
+define('ACHIEVEMENT_CRITERIA_DATA_TYPE_S_DRUNK', 15);
+define('ACHIEVEMENT_CRITERIA_DATA_TYPE_HOLIDAY', 16);
+define('ACHIEVEMENT_CRITERIA_DATA_TYPE_BG_LOSS_TEAM_SCORE', 17);
+define('ACHIEVEMENT_CRITERIA_DATA_TYPE_INSTANCE_SCRIPT', 18);
+define('ACHIEVEMENT_CRITERIA_DATA_TYPE_S_EQUIPED_ITEM', 19);
+define('ACHIEVEMENT_CRITERIA_DATA_TYPE_MAP_ID', 20);
+define('ACHIEVEMENT_CRITERIA_DATA_TYPE_S_PLAYER_CLASS_RACE', 21);
+define('ACHIEVEMENT_CRITERIA_DATA_TYPE_NTH_BIRTHDAY', 22);
+define('ACHIEVEMENT_CRITERIA_DATA_TYPE_S_KNOWN_TITLE', 23);
+// define('ACHIEVEMENT_CRITERIA_DATA_TYPE_GAME_EVENT', 24); // not in 3.3.5a
+define('ACHIEVEMENT_CRITERIA_DATA_TYPE_S_ITEM_QUALITY', 25);
-define('CND_AURA', 1); // aura is applied: spellId, UNUSED, NULL
-define('CND_ITEM', 2); // owns item: itemId, count, UNUSED
-define('CND_ITEM_EQUIPPED', 3); // has item equipped: itemId, NULL, NULL
-define('CND_ZONEID', 4); // is in zone: areaId, NULL, NULL
-define('CND_REPUTATION_RANK', 5); // reputation status: factionId, rankMask, NULL
-define('CND_TEAM', 6); // is on team: teamId, NULL, NULL
-define('CND_SKILL', 7); // has skill: skillId, value, NULL
-define('CND_QUESTREWARDED', 8); // has finished quest: questId, NULL, NULL
-define('CND_QUESTTAKEN', 9); // has accepted quest: questId, NULL, NULL
-define('CND_DRUNKENSTATE', 10); // has drunken status: stateId, NULL, NULL
-define('CND_WORLD_STATE', 11);
-define('CND_ACTIVE_EVENT', 12); // world event is active: eventId, NULL, NULL
-define('CND_INSTANCE_INFO', 13);
-define('CND_QUEST_NONE', 14); // never seen quest: questId, NULL, NULL
-define('CND_CLASS', 15); // belongs to classes: classMask, NULL, NULL
-define('CND_RACE', 16); // belongs to races: raceMask, NULL, NULL
-define('CND_ACHIEVEMENT', 17); // obtained achievement: achievementId, NULL, NULL
-define('CND_TITLE', 18); // obtained title: titleId, NULL, NULL
-define('CND_SPAWNMASK', 19);
-define('CND_GENDER', 20); // has gender: genderId, NULL, NULL
-define('CND_UNIT_STATE', 21);
-define('CND_MAPID', 22); // is on map: mapId, NULL, NULL
-define('CND_AREAID', 23); // is in area: areaId, NULL, NULL
-define('CND_UNUSED_24', 24);
-define('CND_SPELL', 25); // knows spell: spellId, NULL, NULL
-define('CND_PHASEMASK', 26); // is in phase: phaseMask, NULL, NULL
-define('CND_LEVEL', 27); // player level is..: level, operator, NULL
-define('CND_QUEST_COMPLETE', 28); // has completed quest: questId, NULL, NULL
-define('CND_NEAR_CREATURE', 29); // is near creature: creatureId, dist, NULL
-define('CND_NEAR_GAMEOBJECT', 30); // is near gameObject: gameObjectId, dist, NULL
-define('CND_OBJECT_ENTRY', 31); // target is ???: objectType, id, NULL
-define('CND_TYPE_MASK', 32); // target is type: typeMask, NULL, NULL
-define('CND_RELATION_TO', 33);
-define('CND_REACTION_TO', 34);
-define('CND_DISTANCE_TO', 35); // distance to target targetType, dist, operator
-define('CND_ALIVE', 36); // target is alive: NULL, NULL, NULL
-define('CND_HP_VAL', 37); // targets absolute health: amount, operator, NULL
-define('CND_HP_PCT', 38); // targets relative health: amount, operator, NULL
+// TrinityCore - Account Security
+define('SEC_PLAYER', 0);
+define('SEC_MODERATOR', 1);
+define('SEC_GAMEMASTER', 2);
+define('SEC_ADMINISTRATOR', 3);
+define('SEC_CONSOLE', 4); // console only - should not be encountered
-// profiler queue interactions
-define('PR_QUEUE_STATUS_ENDED', 0);
-define('PR_QUEUE_STATUS_WAITING', 1);
-define('PR_QUEUE_STATUS_WORKING', 2);
-define('PR_QUEUE_STATUS_READY', 3);
-define('PR_QUEUE_STATUS_ERROR', 4);
-define('PR_QUEUE_ERROR_UNK', 0);
-define('PR_QUEUE_ERROR_CHAR', 1);
-define('PR_QUEUE_ERROR_ARMORY', 2);
+// Areatrigger types
+define('AT_TYPE_NONE', 0);
+define('AT_TYPE_TAVERN', 1);
+define('AT_TYPE_TELEPORT', 2);
+define('AT_TYPE_OBJECTIVE', 3);
+define('AT_TYPE_SMART', 4);
+define('AT_TYPE_SCRIPT', 5);
-// profiler completion manager
-define('PR_EXCLUDE_GROUP_UNAVAILABLE', 0x001);
-define('PR_EXCLUDE_GROUP_TCG', 0x002);
-define('PR_EXCLUDE_GROUP_COLLECTORS_EDITION', 0x004);
-define('PR_EXCLUDE_GROUP_PROMOTION', 0x008);
-define('PR_EXCLUDE_GROUP_WRONG_REGION', 0x010);
-define('PR_EXCLUDE_GROUP_REQ_ALLIANCE', 0x020);
-define('PR_EXCLUDE_GROUP_REQ_HORDE', 0x040);
-define('PR_EXCLUDE_GROUP_OTHER_FACTION', PR_EXCLUDE_GROUP_REQ_ALLIANCE | PR_EXCLUDE_GROUP_REQ_HORDE);
-define('PR_EXCLUDE_GROUP_REQ_FISHING', 0x080);
-define('PR_EXCLUDE_GROUP_REQ_ENGINEERING', 0x100);
-define('PR_EXCLUDE_GROUP_REQ_TAILORING', 0x200);
-define('PR_EXCLUDE_GROUP_WRONG_PROFESSION', PR_EXCLUDE_GROUP_REQ_FISHING | PR_EXCLUDE_GROUP_REQ_ENGINEERING | PR_EXCLUDE_GROUP_REQ_TAILORING);
-define('PR_EXCLUDE_GROUP_REQ_CANT_BE_EXALTED', 0x400);
-define('PR_EXCLUDE_GROUP_ANY', 0x7FF);
+// summon types
+define('SUMMONER_TYPE_CREATURE', 0);
+define('SUMMONER_TYPE_GAMEOBJECT', 1);
+
+// Map Types
+define('MAP_TYPE_ZONE', 0);
+define('MAP_TYPE_TRANSIT', 1);
+define('MAP_TYPE_DUNGEON', 2);
+define('MAP_TYPE_RAID', 3);
+define('MAP_TYPE_BATTLEGROUND', 4);
+define('MAP_TYPE_DUNGEON_HC', 5);
+define('MAP_TYPE_ARENA', 6);
+define('MAP_TYPE_MMODE_RAID', 7);
+define('MAP_TYPE_MMODE_RAID_HC', 8);
+
+define('EMOTE_FLAG_ONLY_STANDING', 0x0001); // Only while standig
+define('EMOTE_FLAG_USE_MOUNT', 0x0002); // Emote applies to mount
+define('EMOTE_FLAG_NOT_CHANNELING', 0x0004); // Not while channeling
+define('EMOTE_FLAG_ANIM_TALK', 0x0008); // Talk anim - talk
+define('EMOTE_FLAG_ANIM_QUESTION', 0x0010); // Talk anim - question
+define('EMOTE_FLAG_ANIM_EXCLAIM', 0x0020); // Talk anim - exclamation
+define('EMOTE_FLAG_ANIM_SHOUT', 0x0040); // Talk anim - shout
+define('EMOTE_FLAG_NOT_SWIMMING', 0x0080); // Not while swimming
+define('EMOTE_FLAG_ANIM_LAUGH', 0x0100); // Talk anim - laugh
+define('EMOTE_FLAG_CAN_LIE_ON_GROUND', 0x0200); // Ok while sleeping or dead
+define('EMOTE_FLAG_NOT_FROM_CLIENT', 0x0400); // Disallow from client
+define('EMOTE_FLAG_NOT_CASTING', 0x0800); // Not while casting
+define('EMOTE_FLAG_END_MOVEMENT', 0x1000); // Movement ends
+define('EMOTE_FLAG_INTERRUPT_ON_ATTACK', 0x2000); // Interrupt on attack
+define('EMOTE_FLAG_ONLY_STILL', 0x4000); // Only while still
+define('EMOTE_FLAG_NOT_FLYING', 0x8000); // Not while flying
?>
diff --git a/includes/game.php b/includes/game.php
deleted file mode 100644
index 07ef3279..00000000
--- a/includes/game.php
+++ /dev/null
@@ -1,242 +0,0 @@
- [ 0],
- 0 => [ 1, 3, 4, 8, 10, 11, 12, 25, 28, 33, 36, 38, 40, 41, 44, 45, 46, 47, 51, 85, 130, 139, 267, 279, 1497, 1519, 1537, 2257, 3430, 3433, 3487, 4080, 4298],
- 1 => [ 14, 15, 16, 17, 141, 148, 215, 331, 357, 361, 400, 405, 406, 440, 490, 493, 618, 1216, 1377, 1637, 1638, 1657, 3524, 3525, 3557],
-/*todo*/ 2 => [ 133, 206, 209, 491, 717, 718, 719, 722, 796, 978, 1196, 1337, 1417, 1581, 1583, 1584, 1941, 2017, 2057, 2100, 2366, 2367, 2437, 2557, 3477, 3562, 3713, 3714, 3715, 3716, 3717, 3789, 3790, 3791, 3792, 3845, 3846, 3847, 3849, 3905, 4095, 4100, 4120, 4196, 4228, 4264, 4272, 4375, 4415, 4494, 4723],
-/*todo*/ 3 => [ 1977, 2159, 2562, 2677, 2717, 3428, 3429, 3456, 3606, 3805, 3836, 3840, 3842, 4273, 4500, 4722, 4812],
- 4 => [ -372, -263, -262, -261, -162, -161, -141, -82, -81, -61],
- 5 => [ -373, -371, -324, -304, -264, -201, -182, -181, -121, -101, -24],
- 6 => [ -25, 2597, 3277, 3358, 3820, 4384, 4710],
- 7 => [-1010, -368, -367, -365, -344, -241, -1],
- 8 => [ 3483, 3518, 3519, 3520, 3521, 3522, 3523, 3679, 3703], // Skettis is no parent
- 9 => [-1006, -1005, -1003, -1002, -1001, -376, -375, -374, -370, -369, -366, -364, -284, -41, -22], // 22: seasonal, 284: special => not in the actual menu
- 10 => [ 65, 66, 67, 210, 394, 495, 3537, 3711, 4024, 4197, 4395, 4742] // Coldara is no parent
- );
-
- /* why:
- Because petSkills (and ranged weapon skills) are the only ones with more than two skillLines attached. Because Left Joining ?_spell with ?_skillLineability causes more trouble than it has uses.
- Because this is more or less the only reaonable way to fit all that information into one database field, so..
- .. the indizes of this array are bits of skillLine2OrMask in ?_spell if skillLineId1 is negative
- */
- public static $skillLineMask = array( // idx => [familyId, skillLineId]
- -1 => array( // Pets (Hunter)
- [ 1, 208], [ 2, 209], [ 3, 203], [ 4, 210], [ 5, 211], [ 6, 212], [ 7, 213], // Wolf, Cat, Spider, Bear, Boar, Crocolisk, Carrion Bird
- [ 8, 214], [ 9, 215], [11, 217], [12, 218], [20, 236], [21, 251], [24, 653], // Crab, Gorilla, Raptor, Tallstrider, Scorpid, Turtle, Bat
- [25, 654], [26, 655], [27, 656], [30, 763], [31, 767], [32, 766], [33, 765], // Hyena, Bird of Prey, Wind Serpent, Dragonhawk, Ravager, Warp Stalker, Sporebat
- [34, 764], [35, 768], [37, 775], [38, 780], [39, 781], [41, 783], [42, 784], // Nether Ray, Serpent, Moth, Chimaera, Devilsaur, Silithid, Worm
- [43, 786], [44, 785], [45, 787], [46, 788] // Rhino, Wasp, Core Hound, Spirit Beast
- ),
- -2 => array( // Pets (Warlock)
- [15, 189], [16, 204], [17, 205], [19, 207], [23, 188], [29, 761] // Felhunter, Voidwalker, Succubus, Doomguard, Imp, Felguard
- ),
- -3 => array( // Ranged Weapons
- [null, 45], [null, 46], [null, 226] // Bow, Gun, Crossbow
- )
- );
-
- public static $trainerTemplates = array( // TYPE => Id => templateList
- TYPE_CLASS => array(
- 1 => [-200001, -200002], // Warrior
- 2 => [-200003, -200004, -200020, -200021], // Paladin
- 3 => [-200013, -200014], // Hunter
- 4 => [-200015, -200016], // Rogue
- 5 => [-200011, -200012], // Priest
- 6 => [-200019], // DK
- 7 => [-200017, -200018], // Shaman (HighlevelAlly Id missing..?)
- 8 => [-200007, -200008], // Mage
- 9 => [-200009, -200010], // Warlock
- 11 => [-200005, -200006] // Druid
- ),
- TYPE_SKILL => array(
- 171 => [-201001, -201002, -201003], // Alchemy
- 164 => [-201004, -201005, -201006, -201007, -201008],// Blacksmithing
- 333 => [-201009, -201010, -201011], // Enchanting
- 202 => [-201012, -201013, -201014, -201015, -201016, -201017], // Engineering
- 182 => [-201018, -201019, -201020], // Herbalism
- 773 => [-201021, -201022, -201023], // Inscription
- 755 => [-201024, -201025, -201026], // Jewelcrafting
- 165 => [-201027, -201028, -201029, -201030, -201031, -201032], // Leatherworking
- 186 => [-201033, -201034, -201035], // Mining
- 393 => [-201036, -201037, -201038], // Skinning
- 197 => [-201039, -201040, -201041, -201042], // Tailoring
- 356 => [-202001, -202002, -202003], // Fishing
- 185 => [-202004, -202005, -202006], // Cooking
- 129 => [-202007, -202008, -202009], // First Aid
- 762 => [-202010, -202011, -202012] // Riding
- )
- );
-
- public static $sockets = array( // jsStyle Strings
- 'meta', 'red', 'yellow', 'blue'
- );
-
- // 'replicates' $WH.g_statToJson
- public static $itemMods = array( // zero-indexed; "mastrtng": unused mastery; _[a-z] => taken mods..
- 'dmg', 'mana', 'health', 'agi', 'str', 'int', 'spi',
- 'sta', 'energy', 'rage', 'focus', 'runicpwr', 'defrtng', 'dodgertng',
- 'parryrtng', 'blockrtng', 'mlehitrtng', 'rgdhitrtng', 'splhitrtng', 'mlecritstrkrtng', 'rgdcritstrkrtng',
- 'splcritstrkrtng', '_mlehitrtng', '_rgdhitrtng', '_splhitrtng', '_mlecritstrkrtng', '_rgdcritstrkrtng', '_splcritstrkrtng',
- 'mlehastertng', 'rgdhastertng', 'splhastertng', 'hitrtng', 'critstrkrtng', '_hitrtng', '_critstrkrtng',
- 'resirtng', 'hastertng', 'exprtng', 'atkpwr', 'rgdatkpwr', 'feratkpwr', 'splheal',
- 'spldmg', 'manargn', 'armorpenrtng', 'splpwr', 'healthrgn', 'splpen', 'block', // ITEM_MOD_BLOCK_VALUE
- 'mastrtng', 'armor', 'firres', 'frores', 'holres', 'shares', 'natres',
- 'arcres', 'firsplpwr', 'frosplpwr', 'holsplpwr', 'shasplpwr', 'natsplpwr', 'arcsplpwr'
- );
-
- public static $class2SpellFamily = array(
- // null Warrior Paladin Hunter Rogue Priest DK Shaman Mage Warlock null Druid
- null, 4, 10, 9, 8, 6, 15, 11, 3, 5, null, 7
- );
-
- public static function itemModByRatingMask($mask)
- {
- if (($mask & 0x1C000) == 0x1C000) // special case resilience
- return ITEM_MOD_RESILIENCE_RATING;
-
- if (($mask & 0x00E0) == 0x00E0) // hit rating - all subcats (mle, rgd, spl)
- return ITEM_MOD_HIT_RATING;
-
- if (($mask & 0x0700) == 0x0700) // crit rating - all subcats (mle, rgd, spl)
- return ITEM_MOD_CRIT_RATING;
-
- for ($j = 0; $j < count(self::$combatRatingToItemMod); $j++)
- {
- if (!self::$combatRatingToItemMod[$j])
- continue;
-
- if (!($mask & (1 << $j)))
- continue;
-
- return self::$combatRatingToItemMod[$j];
- }
-
- return 0;
- }
-
- public static function sideByRaceMask($race)
- {
- // Any
- if (!$race || ($race & RACE_MASK_ALL) == RACE_MASK_ALL)
- return SIDE_BOTH;
-
- // Horde
- if ($race & RACE_MASK_HORDE && !($race & RACE_MASK_ALLIANCE))
- return SIDE_HORDE;
-
- // Alliance
- if ($race & RACE_MASK_ALLIANCE && !($race & RACE_MASK_HORDE))
- return SIDE_ALLIANCE;
-
- return SIDE_BOTH;
- }
-
- public static function getReputationLevelForPoints($pts)
- {
- if ($pts >= 41999)
- return REP_EXALTED;
- else if ($pts >= 20999)
- return REP_REVERED;
- else if ($pts >= 8999)
- return REP_HONORED;
- else if ($pts >= 2999)
- return REP_FRIENDLY;
- else if ($pts >= 0)
- return REP_NEUTRAL;
- else if ($pts >= -3000)
- return REP_UNFRIENDLY;
- else if ($pts >= -6000)
- return REP_HOSTILE;
- else
- return REP_HATED;
- }
-
- public static function getTaughtSpells(&$spell)
- {
- $extraIds = [-1]; // init with -1 to prevent empty-array errors
- $lookup = [-1];
- switch (gettype($spell))
- {
- case 'object':
- if (get_class($spell) != 'SpellList')
- return [];
-
- $lookup[] = $spell->id;
- foreach ($spell->canTeachSpell() as $idx)
- $extraIds[] = $spell->getField('effect'.$idx.'TriggerSpell');
-
- break;
- case 'integer':
- $lookup[] = $spell;
- break;
- case 'array':
- $lookup = $spell;
- break;
- default:
- return [];
- }
-
- // note: omits required spell and chance in skill_discovery_template
- $data = array_merge(
- DB::World()->selectCol('SELECT spellId FROM spell_learn_spell WHERE entry IN (?a)', $lookup),
- DB::World()->selectCol('SELECT spellId FROM skill_discovery_template WHERE reqSpell IN (?a)', $lookup),
- $extraIds
- );
-
- // return list of integers, not strings
- array_walk($data, function (&$v, $k) {
- $v = intVal($v);
- });
-
- return $data;
- }
-
- public static function getPageText($ptId)
- {
- $pages = [];
- while ($ptId)
- {
- if ($row = DB::World()->selectRow('SELECT ptl.Text AS Text_loc?d, pt.* FROM page_text pt LEFT JOIN page_text_locale ptl ON pt.ID = ptl.ID AND locale = ? WHERE pt.ID = ?d', User::$localeId, User::$localeString, $ptId))
- {
- $ptId = $row['NextPageID'];
- $pages[] = Util::parseHtmlText(Util::localizedString($row, 'Text'));
- }
- else
- {
- trigger_error('Referenced PageTextId #'.$ptId.' is not in DB', E_USER_WARNING);
- break;
- }
- }
-
- return $pages;
- }
-
-}
-
-?>
diff --git a/includes/game/chrclass.class.php b/includes/game/chrclass.class.php
new file mode 100644
index 00000000..3a94bc4b
--- /dev/null
+++ b/includes/game/chrclass.class.php
@@ -0,0 +1,79 @@
+value & $classMask;
+ }
+
+ public function toMask() : int
+ {
+ return 1 << ($this->value - 1);
+ }
+
+ public static function fromMask(int $classMask = self::MASK_ALL) : array
+ {
+ $x = [];
+ foreach (self::cases() as $cl)
+ if ($cl->toMask() & $classMask)
+ $x[] = $cl->value;
+
+ return $x;
+ }
+
+ public function json() : string
+ {
+ return match ($this)
+ {
+ self::WARRIOR => 'warrior',
+ self::PALADIN => 'paladin',
+ self::HUNTER => 'hunter',
+ self::ROGUE => 'rogue',
+ self::PRIEST => 'priest',
+ self::DEATHKNIGHT => 'deathknight',
+ self::SHAMAN => 'shaman',
+ self::MAGE => 'mage',
+ self::WARLOCK => 'warlock',
+ self::DRUID => 'druid'
+ };
+ }
+
+ public function spellFamily() : int
+ {
+ return match ($this)
+ {
+ self::WARRIOR => SPELLFAMILY_WARRIOR,
+ self::PALADIN => SPELLFAMILY_PALADIN,
+ self::HUNTER => SPELLFAMILY_HUNTER,
+ self::ROGUE => SPELLFAMILY_ROGUE,
+ self::PRIEST => SPELLFAMILY_PRIEST,
+ self::DEATHKNIGHT => SPELLFAMILY_DEATHKNIGHT,
+ self::SHAMAN => SPELLFAMILY_SHAMAN,
+ self::MAGE => SPELLFAMILY_MAGE,
+ self::WARLOCK => SPELLFAMILY_WARLOCK,
+ self::DRUID => SPELLFAMILY_DRUID
+ };
+ }
+}
+
+?>
diff --git a/includes/game/chrrace.class.php b/includes/game/chrrace.class.php
new file mode 100644
index 00000000..70e481c1
--- /dev/null
+++ b/includes/game/chrrace.class.php
@@ -0,0 +1,144 @@
+value & $raceMask;
+ }
+
+ public function toMask() : int
+ {
+ return 1 << ($this->value - 1);
+ }
+
+ public function isAlliance() : bool
+ {
+ return $this->toMask() & self::MASK_ALLIANCE;
+ }
+
+ public function isHorde() : bool
+ {
+ return $this->toMask() & self::MASK_HORDE;
+ }
+
+ public function getSide() : int
+ {
+ if ($this->isHorde() && $this->isAlliance())
+ return SIDE_BOTH;
+ else if ($this->isHorde())
+ return SIDE_HORDE;
+ else if ($this->isAlliance())
+ return SIDE_ALLIANCE;
+ else
+ return SIDE_NONE;
+ }
+
+ public function getTeam() : int
+ {
+ if ($this->isHorde() && $this->isAlliance())
+ return TEAM_NEUTRAL;
+ else if ($this->isHorde())
+ return TEAM_HORDE;
+ else if ($this->isAlliance())
+ return TEAM_ALLIANCE;
+ else
+ return TEAM_NEUTRAL;
+ }
+
+ public function json() : string
+ {
+ return match ($this)
+ {
+ self::HUMAN => 'human',
+ self::ORC => 'orc',
+ self::DWARF => 'dwarf',
+ self::NIGHTELF => 'nightelf',
+ self::UNDEAD => 'scourge',
+ self::TAUREN => 'tauren',
+ self::GNOME => 'gnome',
+ self::TROLL => 'troll',
+ self::BLOODELF => 'bloodelf',
+ self::DRAENEI => 'draenei',
+ default => ''
+ };
+ }
+
+ public static function fromMask(int $raceMask = self::MASK_ALL) : array
+ {
+ $x = [];
+ foreach (self::cases() as $cl)
+ if ($cl->toMask() & $raceMask)
+ $x[] = $cl->value;
+
+ return $x;
+ }
+
+ public static function sideFromMask(int $raceMask) : int
+ {
+ // Any
+ if (!$raceMask || ($raceMask & self::MASK_ALL) == self::MASK_ALL)
+ return SIDE_BOTH;
+
+ // Horde
+ if ($raceMask & self::MASK_HORDE && !($raceMask & self::MASK_ALLIANCE))
+ return SIDE_HORDE;
+
+ // Alliance
+ if ($raceMask & self::MASK_ALLIANCE && !($raceMask & self::MASK_HORDE))
+ return SIDE_ALLIANCE;
+
+ return SIDE_BOTH;
+ }
+
+ public static function teamFromMask(int $raceMask) : int
+ {
+ // Any
+ if (!$raceMask || ($raceMask & self::MASK_ALL) == self::MASK_ALL)
+ return TEAM_NEUTRAL;
+
+ // Horde
+ if ($raceMask & self::MASK_HORDE && !($raceMask & self::MASK_ALLIANCE))
+ return TEAM_HORDE;
+
+ // Alliance
+ if ($raceMask & self::MASK_ALLIANCE && !($raceMask & self::MASK_HORDE))
+ return TEAM_ALLIANCE;
+
+ return TEAM_NEUTRAL;
+ }
+}
+
+?>
diff --git a/includes/game/chrstatistics.php b/includes/game/chrstatistics.php
new file mode 100644
index 00000000..1777e31c
--- /dev/null
+++ b/includes/game/chrstatistics.php
@@ -0,0 +1,724 @@
+ ['health', ITEM_MOD_HEALTH, null, 115, self::FLAG_ITEM],
+ self::MANA => ['mana', ITEM_MOD_MANA, null, 116, self::FLAG_ITEM],
+ self::AGILITY => ['agi', ITEM_MOD_AGILITY, null, 21, self::FLAG_ITEM],
+ self::STRENGTH => ['str', ITEM_MOD_STRENGTH, null, 20, self::FLAG_ITEM],
+ self::INTELLECT => ['int', ITEM_MOD_INTELLECT, null, 23, self::FLAG_ITEM],
+ self::SPIRIT => ['spi', ITEM_MOD_SPIRIT, null, 24, self::FLAG_ITEM],
+ self::STAMINA => ['sta', ITEM_MOD_STAMINA, null, 22, self::FLAG_ITEM],
+ self::ENERGY => ['energy', null, null, null, self::FLAG_ITEM],
+ self::RAGE => ['rage', null, null, null, self::FLAG_ITEM],
+ self::FOCUS => ['focus', null, null, null, self::FLAG_ITEM],
+ self::RUNIC_POWER => ['runic', null, null, null, self::FLAG_ITEM | self::FLAG_SERVERSIDE],
+ self::DEFENSE_RTG => ['defrtng', ITEM_MOD_DEFENSE_SKILL_RATING, CR_DEFENSE_SKILL, 42, self::FLAG_ITEM | self::FLAG_LVL_SCALING],
+ self::DODGE_RTG => ['dodgertng', ITEM_MOD_DODGE_RATING, CR_DODGE, 45, self::FLAG_ITEM | self::FLAG_LVL_SCALING],
+ self::PARRY_RTG => ['parryrtng', ITEM_MOD_PARRY_RATING, CR_PARRY, 46, self::FLAG_ITEM | self::FLAG_LVL_SCALING],
+ self::BLOCK_RTG => ['blockrtng', ITEM_MOD_BLOCK_RATING, CR_BLOCK, 44, self::FLAG_ITEM | self::FLAG_LVL_SCALING],
+ self::MELEE_HIT_RTG => ['mlehitrtng', ITEM_MOD_HIT_MELEE_RATING, CR_HIT_MELEE, 95, self::FLAG_ITEM | self::FLAG_LVL_SCALING],
+ self::RANGED_HIT_RTG => ['rgdhitrtng', ITEM_MOD_HIT_RANGED_RATING, CR_HIT_RANGED, 39, self::FLAG_ITEM | self::FLAG_LVL_SCALING],
+ self::SPELL_HIT_RTG => ['splhitrtng', ITEM_MOD_HIT_SPELL_RATING, CR_HIT_SPELL, 48, self::FLAG_ITEM | self::FLAG_LVL_SCALING],
+ self::MELEE_CRIT_RTG => ['mlecritstrkrtng', ITEM_MOD_CRIT_MELEE_RATING, CR_CRIT_MELEE, 84, self::FLAG_ITEM | self::FLAG_LVL_SCALING],
+ self::RANGED_CRIT_RTG => ['rgdcritstrkrtng', ITEM_MOD_CRIT_RANGED_RATING, CR_CRIT_RANGED, 40, self::FLAG_ITEM | self::FLAG_LVL_SCALING],
+ self::SPELL_CRIT_RTG => ['splcritstrkrtng', ITEM_MOD_CRIT_SPELL_RATING, CR_CRIT_SPELL, 49, self::FLAG_ITEM | self::FLAG_LVL_SCALING],
+ self::MELEE_HIT_TAKEN_RTG => ['_mlehitrtng', ITEM_MOD_HIT_TAKEN_MELEE_RATING, CR_HIT_TAKEN_MELEE, null, self::FLAG_ITEM | self::FLAG_LVL_SCALING],
+ self::RANGED_HIT_TAKEN_RTG => ['_rgdhitrtng', ITEM_MOD_HIT_TAKEN_RANGED_RATING, CR_HIT_TAKEN_RANGED, null, self::FLAG_ITEM | self::FLAG_LVL_SCALING],
+ self::SPELL_HIT_TAKEN_RTG => ['_splhitrtng', ITEM_MOD_HIT_TAKEN_SPELL_RATING, CR_HIT_TAKEN_SPELL, null, self::FLAG_ITEM | self::FLAG_LVL_SCALING],
+ self::MELEE_CRIT_TAKEN_RTG => ['_mlecritstrkrtng', ITEM_MOD_CRIT_TAKEN_MELEE_RATING, CR_CRIT_TAKEN_MELEE, null, self::FLAG_ITEM | self::FLAG_LVL_SCALING],
+ self::RANGED_CRIT_TAKEN_RTG => ['_rgdcritstrkrtng', ITEM_MOD_CRIT_TAKEN_RANGED_RATING, CR_CRIT_TAKEN_RANGED, null, self::FLAG_ITEM | self::FLAG_LVL_SCALING],
+ self::SPELL_CRIT_TAKEN_RTG => ['_splcritstrkrtng', ITEM_MOD_CRIT_TAKEN_SPELL_RATING, CR_CRIT_TAKEN_SPELL, null, self::FLAG_ITEM | self::FLAG_LVL_SCALING],
+ self::MELEE_HASTE_RTG => ['mlehastertng', ITEM_MOD_HASTE_MELEE_RATING, CR_HASTE_MELEE, 78, self::FLAG_ITEM | self::FLAG_LVL_SCALING],
+ self::RANGED_HASTE_RTG => ['rgdhastertng', ITEM_MOD_HASTE_RANGED_RATING, CR_HASTE_RANGED, 101, self::FLAG_ITEM | self::FLAG_LVL_SCALING],
+ self::SPELL_HASTE_RTG => ['splhastertng', ITEM_MOD_HASTE_SPELL_RATING, CR_HASTE_SPELL, 102, self::FLAG_ITEM | self::FLAG_LVL_SCALING],
+ self::HIT_RTG => ['hitrtng', ITEM_MOD_HIT_RATING, -CR_HIT_MELEE, 119, self::FLAG_ITEM | self::FLAG_LVL_SCALING],
+ self::CRIT_RTG => ['critstrkrtng', ITEM_MOD_CRIT_RATING, -CR_CRIT_MELEE, 96, self::FLAG_ITEM | self::FLAG_LVL_SCALING],
+ self::HIT_TAKEN_RTG => ['_hitrtng', ITEM_MOD_HIT_TAKEN_RATING, -CR_HIT_TAKEN_MELEE, null, self::FLAG_ITEM | self::FLAG_LVL_SCALING],
+ self::CRIT_TAKEN_RTG => ['_critstrkrtng', ITEM_MOD_CRIT_TAKEN_RATING, -CR_CRIT_TAKEN_MELEE, null, self::FLAG_ITEM | self::FLAG_LVL_SCALING],
+ self::RESILIENCE_RTG => ['resirtng', ITEM_MOD_RESILIENCE_RATING, -CR_CRIT_TAKEN_MELEE, 79, self::FLAG_ITEM | self::FLAG_LVL_SCALING],
+ self::HASTE_RTG => ['hastertng', ITEM_MOD_HASTE_RATING, -CR_HASTE_MELEE, 103, self::FLAG_ITEM | self::FLAG_LVL_SCALING],
+ self::EXPERTISE_RTG => ['exprtng', ITEM_MOD_EXPERTISE_RATING, CR_EXPERTISE, 117, self::FLAG_ITEM | self::FLAG_LVL_SCALING],
+ self::ATTACK_POWER => ['atkpwr', ITEM_MOD_ATTACK_POWER, null, 77, self::FLAG_ITEM],
+ self::RANGED_ATTACK_POWER => ['rgdatkpwr', ITEM_MOD_RANGED_ATTACK_POWER, null, 38, self::FLAG_ITEM],
+ self::FERAL_ATTACK_POWER => ['feratkpwr', ITEM_MOD_FERAL_ATTACK_POWER, null, 97, self::FLAG_ITEM],
+ self::HEALING_SPELL_POWER => ['splheal', ITEM_MOD_SPELL_HEALING_DONE, null, 50, self::FLAG_ITEM],
+ self::DAMAGE_SPELL_POWER => ['spldmg', ITEM_MOD_SPELL_DAMAGE_DONE, null, 51, self::FLAG_ITEM],
+ self::MANA_REGENERATION => ['manargn', ITEM_MOD_MANA_REGENERATION, null, 61, self::FLAG_ITEM],
+ self::ARMOR_PENETRATION_RTG => ['armorpenrtng', ITEM_MOD_ARMOR_PENETRATION_RATING, CR_ARMOR_PENETRATION, 114, self::FLAG_ITEM | self::FLAG_LVL_SCALING],
+ self::SPELL_POWER => ['splpwr', ITEM_MOD_SPELL_POWER, null, 123, self::FLAG_ITEM],
+ self::HEALTH_REGENERATION => ['healthrgn', ITEM_MOD_HEALTH_REGEN, null, 60, self::FLAG_ITEM],
+ self::SPELL_PENETRATION => ['splpen', ITEM_MOD_SPELL_PENETRATION, null, 94, self::FLAG_ITEM],
+ self::BLOCK => ['block', ITEM_MOD_BLOCK_VALUE, null, 43, self::FLAG_ITEM],
+ // self::MASTERY_RTG => ['mastrtng', ITEM_MOD_MASTERY_RATING, CR_MASTERY, null, self::FLAG_NONE],
+ self::ARMOR => ['armor', null,/*ITEM_MOD_EXTRA_ARMOR */null, 41, self::FLAG_ITEM],
+ self::FIRE_RESISTANCE => ['firres', null,/*ITEM_MOD_FIRE_RESISTANCE */null, 26, self::FLAG_ITEM],
+ self::FROST_RESISTANCE => ['frores', null,/*ITEM_MOD_FROST_RESISTANCE */null, 28, self::FLAG_ITEM],
+ self::HOLY_RESISTANCE => ['holres', null,/*ITEM_MOD_HOLY_RESISTANCE */null, 30, self::FLAG_ITEM],
+ self::SHADOW_RESISTANCE => ['shares', null,/*ITEM_MOD_SHADOW_RESISTANCE*/null, 29, self::FLAG_ITEM],
+ self::NATURE_RESISTANCE => ['natres', null,/*ITEM_MOD_NATURE_RESISTANCE*/null, 27, self::FLAG_ITEM],
+ self::ARCANE_RESISTANCE => ['arcres', null,/*ITEM_MOD_ARCANE_RESISTANCE*/null, 25, self::FLAG_ITEM],
+ self::FIRE_SPELL_POWER => ['firsplpwr', null, null, 53, self::FLAG_ITEM],
+ self::FROST_SPELL_POWER => ['frosplpwr', null, null, 54, self::FLAG_ITEM],
+ self::HOLY_SPELL_POWER => ['holsplpwr', null, null, 55, self::FLAG_ITEM],
+ self::SHADOW_SPELL_POWER => ['shasplpwr', null, null, 57, self::FLAG_ITEM],
+ self::NATURE_SPELL_POWER => ['natsplpwr', null, null, 56, self::FLAG_ITEM],
+ self::ARCANE_SPELL_POWER => ['arcsplpwr', null, null, 52, self::FLAG_ITEM],
+ // v not part of g_statToJson v
+ self::WEAPON_DAMAGE => ['dmg', null, null, null, self::FLAG_SERVERSIDE | self::FLAG_FLOAT_VALUE],
+ self::WEAPON_DAMAGE_TYPE => ['damagetype', null, null, 35, self::FLAG_SERVERSIDE | self::FLAG_NO_WEIGHT],
+ self::WEAPON_DAMAGE_MIN => ['dmgmin1', null, null, 33, self::FLAG_SERVERSIDE],
+ self::WEAPON_DAMAGE_MAX => ['dmgmax1', null, null, 34, self::FLAG_SERVERSIDE],
+ self::WEAPON_SPEED => ['speed', null, null, 36, self::FLAG_SERVERSIDE | self::FLAG_FLOAT_VALUE],
+ self::WEAPON_DPS => ['dps', null, null, 32, self::FLAG_SERVERSIDE | self::FLAG_FLOAT_VALUE],
+ self::MELEE_DAMAGE_MIN => ['mledmgmin', null, null, 135, self::FLAG_SERVERSIDE],
+ self::MELEE_DAMAGE_MAX => ['mledmgmax', null, null, 136, self::FLAG_SERVERSIDE],
+ self::MELEE_SPEED => ['mlespeed', null, null, 137, self::FLAG_SERVERSIDE | self::FLAG_FLOAT_VALUE],
+ self::MELEE_DPS => ['mledps', null, null, 134, self::FLAG_SERVERSIDE | self::FLAG_FLOAT_VALUE | self::FLAG_PROFILER],
+ self::RANGED_DAMAGE_MIN => ['rgddmgmin', null, null, 139, self::FLAG_SERVERSIDE],
+ self::RANGED_DAMAGE_MAX => ['rgddmgmax', null, null, 140, self::FLAG_SERVERSIDE],
+ self::RANGED_SPEED => ['rgdspeed', null, null, 141, self::FLAG_SERVERSIDE | self::FLAG_FLOAT_VALUE],
+ self::RANGED_DPS => ['rgddps', null, null, 138, self::FLAG_SERVERSIDE | self::FLAG_FLOAT_VALUE | self::FLAG_PROFILER],
+ self::EXTRA_SOCKETS => ['nsockets', null, null, 100, self::FLAG_SERVERSIDE | self::FLAG_NO_WEIGHT],
+ self::ARMOR_BONUS => ['armorbonus', null, null, 109, self::FLAG_SERVERSIDE],
+ self::MELEE_ATTACK_POWER => ['mleatkpwr', null, null, 37, self::FLAG_SERVERSIDE | self::FLAG_PROFILER],
+ // v Profiler only v
+ self::EXPERTISE => ['exp', null, null, null, self::FLAG_PROFILER],
+ self::ARMOR_PENETRATION_PCT => ['armorpenpct', null, null, null, self::FLAG_PROFILER],
+ self::MELEE_HIT_PCT => ['mlehitpct', null, null, null, self::FLAG_PROFILER],
+ self::MELEE_CRIT_PCT => ['mlecritstrkpct', null, null, null, self::FLAG_PROFILER],
+ self::MELEE_HASTE_PCT => ['mlehastepct', null, null, null, self::FLAG_PROFILER],
+ self::RANGED_HIT_PCT => ['rgdhitpct', null, null, null, self::FLAG_PROFILER],
+ self::RANGED_CRIT_PCT => ['rgdcritstrkpct', null, null, null, self::FLAG_PROFILER],
+ self::RANGED_HASTE_PCT => ['rgdhastepct', null, null, null, self::FLAG_PROFILER],
+ self::SPELL_HIT_PCT => ['splhitpct', null, null, null, self::FLAG_PROFILER],
+ self::SPELL_CRIT_PCT => ['splcritstrkpct', null, null, null, self::FLAG_PROFILER],
+ self::SPELL_HASTE_PCT => ['splhastepct', null, null, null, self::FLAG_PROFILER],
+ self::MANA_REGENERATION_SPI => ['spimanargn', null, null, null, self::FLAG_PROFILER],
+ self::MANA_REGENERATION_OC => ['oocmanargn', null, null, null, self::FLAG_PROFILER],
+ self::MANA_REGENERATION_IC => ['icmanargn', null, null, null, self::FLAG_PROFILER],
+ self::ARMOR_TOTAL => ['fullarmor', null, null, null, self::FLAG_PROFILER],
+ self::DEFENSE => ['def', null, null, null, self::FLAG_PROFILER],
+ self::DODGE_PCT => ['dodgepct', null, null, null, self::FLAG_PROFILER],
+ self::PARRY_PCT => ['parrypct', null, null, null, self::FLAG_PROFILER],
+ self::BLOCK_PCT => ['blockpct', null, null, null, self::FLAG_PROFILER],
+ self::RESILIENCE_PCT => ['resipct', null, null, null, self::FLAG_PROFILER]
+ );
+
+ /* Combat Rating needed for 1% effect at level 60 (Note: Shaman, Druid, Paladin and Death Knight have a /1.3 modifier on HASTE not set here)
+ * Data taken from gtcombatratings.dbc for level 60 [idx % 100 = 59]
+ * Corrections from gtoctclasscombatratingscalar.dbc with Warrior as base [idx = ratingId + 1]
+ * Maybe create this data during setup, but then again it will never change for 3.3.5a
+ */
+ private static $crPerPctPoint = array(
+ CR_WEAPON_SKILL => 2.50, CR_DEFENSE_SKILL => 1.50, CR_DODGE => 13.80, CR_PARRY => 13.80, CR_BLOCK => 5.00,
+ CR_HIT_MELEE => 10.00, CR_HIT_RANGED => 10.00, CR_HIT_SPELL => 8.00, CR_CRIT_MELEE => 14.00, CR_CRIT_RANGED => 14.00,
+ CR_CRIT_SPELL => 14.00, CR_HIT_TAKEN_MELEE => 10.00, CR_HIT_TAKEN_RANGED => 10.00, CR_HIT_TAKEN_SPELL => 8.00, CR_CRIT_TAKEN_MELEE => 28.75,
+ CR_CRIT_TAKEN_RANGED => 28.75, CR_CRIT_TAKEN_SPELL => 28.75, CR_HASTE_MELEE => 10.00, CR_HASTE_RANGED => 10.00, CR_HASTE_SPELL => 10.00,
+ CR_WEAPON_SKILL_MAINHAND => 2.50, CR_WEAPON_SKILL_OFFHAND => 2.50, CR_WEAPON_SKILL_RANGED => 2.50, CR_EXPERTISE => 2.50, CR_ARMOR_PENETRATION => 4.69512 / 1.1,
+ );
+
+ public static function isLevelIndependent(int $stat) : bool
+ {
+ if (!isset(self::$data[$stat]))
+ return false;
+
+ return !(self::$data[$stat][self::IDX_FLAGS] & self::FLAG_LVL_SCALING);
+ }
+
+ public static function getWeightJson(string|int $jsonOrCriteriaId) : string
+ {
+ if (is_numeric($jsonOrCriteriaId))
+ $row = array_find(self::$data, fn($x) => $x[self::IDX_FILTER_CR_ID] == $jsonOrCriteriaId);
+ else
+ $row = array_find(self::$data, fn($x) => $x[self::IDX_JSON_STR] == $jsonOrCriteriaId);
+
+ return $row && $row[self::IDX_FILTER_CR_ID] && !($row[self::IDX_FLAGS] & self::FLAG_NO_WEIGHT) ? $row[self::IDX_JSON_STR] : '';
+ }
+
+ public static function getRatingPctFactor(int $stat) : float
+ {
+ // Note: this makes the weapon skill related combat ratings inaccessible. Is this relevant..?
+ if (!isset(self::$data[$stat]) || self::$data[$stat][self::IDX_COMBAT_RATING] === null)
+ return 0.0;
+
+ // note: originally any CRIT_TAKEN_RTG stat was set to 0 in favor of RESILIENCE_RTG
+ // we keep the dbc value and just link RESILIENCE_RTG to CRIT_TAKEN_RTG
+ // note2: the js expects some stats to be directly mapped to a combat rating that doesn't exist
+ // picked the next best one in this case and denoted it with a negative value in the $data dump
+ return self::$crPerPctPoint[abs(self::$data[$stat][self::IDX_COMBAT_RATING])];
+ }
+
+ public static function getJsonString(int $stat) : string
+ {
+ if (!isset(self::$data[$stat]))
+ return '';
+
+ return self::$data[$stat][self::IDX_JSON_STR];
+ }
+
+ public static function getFilterCriteriumId(int $stat) : ?int
+ {
+ if (!isset(self::$data[$stat]))
+ return null;
+
+ return self::$data[$stat][self::IDX_FILTER_CR_ID];
+ }
+
+ public static function getFlags(int $stat) : int
+ {
+ if (!isset(self::$data[$stat]))
+ return 0;
+
+ return self::$data[$stat][self::IDX_FLAGS];
+ }
+
+ public static function getJsonStringsFor(int $flags = Stat::FLAG_NONE) : array
+ {
+ $x = [];
+ foreach (self::$data as $k => [$s, , , , $f])
+ if ($s && (!$flags || $flags & $f))
+ $x[$k] = $s;
+
+ return $x;
+ }
+
+ public static function getCombatRatingsFor(int $flags = Stat::FLAG_NONE) : array
+ {
+ $x = [];
+ foreach (self::$data as $k => [, , $c, , $f])
+ if ($c > 0 && (!$flags || $flags & $f))
+ $x[$k] = $c;
+
+ return $x;
+ }
+
+ public static function getFilterCriteriumIdFor(int $flags = Stat::FLAG_NONE) : array
+ {
+ $x = [];
+ foreach (self::$data as $k => [, , , $cr, $f])
+ if ($cr && (!$flags || $flags & $f))
+ $x[$k] = $cr;
+
+ return $x;
+ }
+
+ public static function getIndexFrom(int $idx, string $search) : int
+ {
+ return array_find_key(self::$data, fn($x) => $x[$idx] == $search) ?: 0;
+ }
+}
+
+class StatsContainer implements \Countable
+{
+ private array $store = [];
+
+ private array $relSpells = [];
+ private array $relEnchantments = [];
+
+ private static array $combinedSpellStats = array (
+ Stat::ATTACK_POWER => [Stat::RANGED_ATTACK_POWER, Stat::MELEE_ATTACK_POWER],
+ Stat::SPELL_POWER => [Stat::DAMAGE_SPELL_POWER, Stat::HEALING_SPELL_POWER],
+ // combat ratings below could be merged like this, but easier to handle as they are already in the same bitmask of the same spell effect
+ // Stat::HIT_RTG => [Stat::MELEE_HIT_RTG, Stat::RANGED_HIT_RTG, Stat::SPELL_HIT_RTG],
+ // Stat::CRIT_RTG => [Stat::MELEE_CRIT_TAKEN_RTG, Stat::RANGED_CRIT_RTG, Stat::SPELL_CRIT_RTG],
+ // Stat::RESILIENCE_RTG => [Stat::MELEE_CRIT_RTG, Stat::RANGED_CRIT_TAKEN_RTG, Stat::SPELL_CRIT_TAKEN_RTG]
+ );
+
+ public function __construct(array $relSpells = [], array $relEnchantments = [])
+ {
+ if ($relSpells)
+ $this->relSpells = $relSpells;
+
+ if ($relEnchantments)
+ $this->relEnchantments = $relEnchantments;
+ }
+
+ /**********/
+ /* Source */
+ /**********/
+
+ public function fromItem(array $item) : self
+ {
+ if (!$item)
+ return $this;
+
+ // convert itemMods to stats
+ for ($i = 1; $i <= 10; $i++)
+ {
+ $mod = $item['statType'.$i];
+ $val = $item['statValue'.$i];
+ if (!$mod || !$val)
+ continue;
+
+ if ($idx = Stat::getIndexFrom(Stat::IDX_ITEM_MOD, $mod))
+ Util::arraySumByKey($this->store, [$idx => $val]);
+ }
+
+ // also occurs as seperate field (gets summed in calculation but not in tooltip)
+ if ($item['tplBlock'])
+ Util::arraySumByKey($this->store, [Stat::BLOCK => $item['tplBlock']]);
+
+ // convert spells to stats
+ for ($i = 1; $i <= 5; $i++)
+ if (in_array($item['spellTrigger'.$i], [SPELL_TRIGGER_EQUIP, SPELL_TRIGGER_USE, SPELL_TRIGGER_USE_NODELAY]))
+ if ($relS = $this->relS($item['spellId'.$i]))
+ $this->fromSpell($relS);
+
+ // for ITEM_CLASS_GEM get stats from enchantment
+ if ($relE = $this->relE($item['gemEnchantmentId']))
+ $this->fromEnchantment($relE);
+
+ return $this;
+ }
+
+ public function fromEnchantment(array $enchantment) : self
+ {
+ if (!$enchantment)
+ return $this;
+
+ for ($i = 1; $i <= 3; $i++)
+ {
+ $type = $enchantment['type'.$i];
+ $object = $enchantment['object'.$i];
+ $amount = $enchantment['amount'.$i]; // !CAUTION! scaling enchantments are initialized with "0" as amount. 0 is a valid amount!
+
+ if ($type == ENCHANTMENT_TYPE_EQUIP_SPELL && ($relS = $this->relS($object)))
+ $this->fromSpell($relS);
+ else
+ foreach ($this->convertEnchantment($type, $object) as $idx)
+ Util::arraySumByKey($this->store, [$idx => $amount]);
+ }
+
+ return $this;
+ }
+
+ public function fromSpell(array $spell, bool $onlyFoodBuff = false) : self
+ {
+ if (!$spell)
+ return $this;
+
+ if ($onlyFoodBuff && !($spell['attributes2'] & SPELL_ATTR2_FOOD_BUFF))
+ return $this;
+
+ $tmpStore = [];
+
+ for ($i = 1; $i <= 3; $i++)
+ {
+ $eff = $spell['effect'.$i.'Id'];
+ $aura = $spell['effect'.$i.'AuraId'];
+ $mVal = $spell['effect'.$i.'MiscValue'];
+ $amt = $spell['effect'.$i.'BasePoints'] + $spell['effect'.$i.'DieSides'];
+
+ if (in_array($eff, SpellList::EFFECTS_ENCHANTMENT) && ($relE = $this->relE($mVal)))
+ $this->fromEnchantment($relE);
+ else if ($aura == SPELL_AURA_PERIODIC_TRIGGER_SPELL && ($ts = $spell['effect'.$i.'TriggerSpell']))
+ {
+ if ($relS = $this->relS($ts))
+ $this->fromSpell($relS, true);
+ }
+ else
+ foreach ($this->convertSpellEffect($aura, $mVal, $amt) as $idx)
+ Util::arraySumByKey($tmpStore, [$idx => $amt]);
+ }
+
+ foreach (self::$combinedSpellStats as $combined => $stats)
+ {
+ for ($i = 0; $i < count($stats); $i++)
+ {
+ if (empty($tmpStore[$stats[$i]]))
+ continue 2;
+
+ if ($i && $tmpStore[$stats[$i]] != $tmpStore[$stats[$i - 1]])
+ continue 2;
+ }
+
+ Util::arraySumByKey($tmpStore, [$combined => $tmpStore[$stats[0]]]);
+ foreach ($stats as $stat)
+ unset($tmpStore[$stat]);
+ }
+
+ Util::arraySumByKey($this->store, $tmpStore);
+
+ return $this;
+ }
+
+ public function fromJson(array &$json, bool $pruneFromSrc = false) : self
+ {
+ if (!$json)
+ return $this;
+
+ foreach (Stat::getJsonStringsFor() as $idx => $key)
+ {
+ if (isset($json[$key])) // 0 is a valid amount!
+ {
+ if (Stat::getFlags($idx) & Stat::FLAG_FLOAT_VALUE)
+ Util::arraySumByKey($this->store, [$idx => (float)$json[$key]]);
+ else
+ Util::arraySumByKey($this->store, [$idx => (int)$json[$key]]);
+ }
+
+ if ($pruneFromSrc)
+ unset($json[$key]);
+ }
+
+ return $this;
+ }
+
+ public function fromDB(int $type, int $typeId, int $fieldFlags = Stat::FLAG_NONE) : self
+ {
+ foreach (DB::Aowow()->selectRow('SELECT (%n) FROM ::item_stats WHERE `type` = %i AND `typeId` = %i', Stat::getJsonStringsFor($fieldFlags ?: (Stat::FLAG_ITEM | Stat::FLAG_SERVERSIDE)), $type, $typeId) as $key => $amt)
+ {
+ if ($amt === null)
+ continue;
+
+ $idx = Stat::getIndexFrom(Stat::IDX_JSON_STR, $key);
+ $float = Stat::getFlags($idx) & Stat::FLAG_FLOAT_VALUE;
+
+ if (Util::checkNumeric($amt, $float ? NUM_CAST_FLOAT : NUM_CAST_INT))
+ Util::arraySumByKey($this->store, [$idx => $amt]);
+ }
+
+ return $this;
+ }
+
+ public function fromContainer(StatsContainer ...$container) : self
+ {
+ foreach ($container as $c)
+ Util::arraySumByKey($this->store, $c->toRaw());
+
+ return $this;
+ }
+
+
+ /**********/
+ /* Output */
+ /**********/
+
+ public function toJson(int $outFlags = Stat::FLAG_NONE, bool $includeEmpty = true) : array
+ {
+ $out = [];
+ foreach ($this->store as $stat => $amt)
+ if ((!$outFlags || (Stat::getFlags($stat) & $outFlags)) && ($amt || $includeEmpty))
+ $out[Stat::getJsonString($stat)] = $amt;
+
+ return $out;
+ }
+
+ public function toRaw() : array
+ {
+ return $this->store;
+ }
+
+ public function filter(?callable $filterFn = null) : self
+ {
+ $this->store = array_filter($this->store, $filterFn, ARRAY_FILTER_USE_BOTH);
+ return $this;
+ }
+
+ public function count() : int
+ {
+ return count($this->store);
+ }
+
+ /****************/
+ /* internal use */
+ /****************/
+
+ private function relE(int $enchantmentId) : array
+ {
+ return $this->relEnchantments[$enchantmentId] ?? [];
+ }
+
+ private function relS(int $spellId) : array
+ {
+ return $this->relSpells[$spellId] ?? [];
+ }
+
+ private static function convertEnchantment(int $type, int $object) : array
+ {
+ switch ($type)
+ {
+ case ENCHANTMENT_TYPE_PRISMATIC_SOCKET:
+ return [Stat::EXTRA_SOCKETS];
+ case ENCHANTMENT_TYPE_DAMAGE:
+ return [Stat::WEAPON_DAMAGE];
+ case ENCHANTMENT_TYPE_TOTEM:
+ return [Stat::WEAPON_DPS];
+ case ENCHANTMENT_TYPE_STAT: // ITEM_MOD_*
+ return [Stat::getIndexFrom(Stat::IDX_ITEM_MOD, $object)];
+ case ENCHANTMENT_TYPE_RESISTANCE:
+ return match ($object)
+ {
+ SPELL_SCHOOL_NORMAL => [Stat::ARMOR],
+ SPELL_SCHOOL_HOLY => [Stat::HOLY_RESISTANCE],
+ SPELL_SCHOOL_FIRE => [Stat::FIRE_RESISTANCE],
+ SPELL_SCHOOL_NATURE => [Stat::NATURE_RESISTANCE],
+ SPELL_SCHOOL_FROST => [Stat::FROST_RESISTANCE],
+ SPELL_SCHOOL_SHADOW => [Stat::SHADOW_RESISTANCE],
+ SPELL_SCHOOL_ARCANE => [Stat::ARCANE_RESISTANCE],
+ default => []
+ };
+ case ENCHANTMENT_TYPE_EQUIP_SPELL: // handled one level up
+ case ENCHANTMENT_TYPE_COMBAT_SPELL: // we do not average effects, so skip
+ case ENCHANTMENT_TYPE_USE_SPELL:
+ default:
+ return [];
+ }
+
+ return [];
+ }
+
+ public static function convertCombatRating(int $mask) : array
+ {
+ $hitMask = (1 << CR_HIT_MELEE) | (1 << CR_HIT_RANGED) | (1 << CR_HIT_SPELL);
+ if (($mask & $hitMask) == $hitMask)
+ return [Stat::HIT_RTG]; // generic hit rating
+
+ $critMask = (1 << CR_CRIT_MELEE) | (1 << CR_CRIT_RANGED) | (1 << CR_CRIT_SPELL);
+ if (($mask & $critMask) == $critMask)
+ return [Stat::CRIT_RTG]; // generic crit rating
+
+ $takentMask = (1 << CR_CRIT_TAKEN_MELEE) | (1 << CR_CRIT_TAKEN_RANGED) | (1 << CR_CRIT_TAKEN_SPELL);
+ if (($mask & $takentMask) == $takentMask)
+ return [Stat::RESILIENCE_RTG]; // resilience
+
+ $result = []; // there really shouldn't be multiple ratings in that mask besides the cases above, but who knows..
+ foreach (Stat::getCombatRatingsFor() as $stat => $cr)
+ if ($mask & (1 << $cr))
+ $result[] = $stat;
+
+ return $result;
+ }
+
+ private static function convertSpellEffect(int $auraId, int $miscValue, int &$amount) : array
+ {
+ $stats = [];
+
+ switch ($auraId)
+ {
+ case SPELL_AURA_MOD_STAT:
+ return match ($miscValue)
+ {
+ STAT_STRENGTH => [Stat::STRENGTH],
+ STAT_AGILITY => [Stat::AGILITY],
+ STAT_STAMINA => [Stat::STAMINA],
+ STAT_INTELLECT => [Stat::INTELLECT],
+ STAT_SPIRIT => [Stat::SPIRIT],
+ default => $miscValue < 0 ? [Stat::AGILITY, Stat::STRENGTH, Stat::INTELLECT, Stat::SPIRIT, Stat::STAMINA] : []
+ };
+ case SPELL_AURA_MOD_INCREASE_HEALTH:
+ case SPELL_AURA_MOD_INCREASE_HEALTH_NONSTACK:
+ case SPELL_AURA_MOD_INCREASE_HEALTH_2:
+ return [Stat::HEALTH];
+ case SPELL_AURA_MOD_DAMAGE_DONE:
+ // + weapon damage
+ if ($miscValue == (1 << SPELL_SCHOOL_NORMAL))
+ return [Stat::WEAPON_DAMAGE];
+
+ // full magic mask
+ if ($miscValue == SPELL_MAGIC_SCHOOLS)
+ return [Stat::DAMAGE_SPELL_POWER];
+
+ if ($miscValue & (1 << SPELL_SCHOOL_HOLY))
+ $stats[] = Stat::HOLY_SPELL_POWER;
+ if ($miscValue & (1 << SPELL_SCHOOL_FIRE))
+ $stats[] = Stat::FIRE_SPELL_POWER;
+ if ($miscValue & (1 << SPELL_SCHOOL_NATURE))
+ $stats[] = Stat::NATURE_SPELL_POWER;
+ if ($miscValue & (1 << SPELL_SCHOOL_FROST))
+ $stats[] = Stat::FROST_SPELL_POWER;
+ if ($miscValue & (1 << SPELL_SCHOOL_SHADOW))
+ $stats[] = Stat::SHADOW_SPELL_POWER;
+ if ($miscValue & (1 << SPELL_SCHOOL_ARCANE))
+ $stats[] = Stat::ARCANE_SPELL_POWER;
+
+ return $stats;
+ case SPELL_AURA_MOD_HEALING_DONE: // not as a mask..
+ return [Stat::HEALING_SPELL_POWER];
+ case SPELL_AURA_MOD_INCREASE_ENERGY: // MiscVal:type see defined Powers only energy/mana in use
+ return match ($miscValue)
+ {
+ POWER_ENERGY => [Stat::ENERGY],
+ POWER_RAGE => [Stat::RAGE],
+ POWER_MANA => [Stat::MANA],
+ POWER_RUNIC_POWER => [Stat::RUNIC_POWER],
+ default => []
+ };
+ case SPELL_AURA_MOD_RATING:
+ case SPELL_AURA_MOD_RATING_FROM_STAT:
+ if ($stat = self::convertCombatRating($miscValue))
+ return $stat;
+
+ return [];
+ case SPELL_AURA_MOD_RESISTANCE_EXCLUSIVE:
+ case SPELL_AURA_MOD_BASE_RESISTANCE:
+ case SPELL_AURA_MOD_RESISTANCE:
+ // Armor only if explicitly specified
+ if ($miscValue == (1 << SPELL_SCHOOL_NORMAL))
+ return [Stat::ARMOR];
+
+ // Holy resistance only if explicitly specified (should it even exist...?)
+ if ($miscValue == (1 << SPELL_SCHOOL_HOLY))
+ return [Stat::HOLY_RESISTANCE];
+
+ if ($miscValue & (1 << SPELL_SCHOOL_FIRE))
+ $stats[] = Stat::FIRE_RESISTANCE;
+ if ($miscValue & (1 << SPELL_SCHOOL_NATURE))
+ $stats[] = Stat::NATURE_RESISTANCE;
+ if ($miscValue & (1 << SPELL_SCHOOL_FROST))
+ $stats[] = Stat::FROST_RESISTANCE;
+ if ($miscValue & (1 << SPELL_SCHOOL_SHADOW))
+ $stats[] = Stat::SHADOW_RESISTANCE;
+ if ($miscValue & (1 << SPELL_SCHOOL_ARCANE))
+ $stats[] = Stat::ARCANE_RESISTANCE;
+
+ return $stats;
+ case SPELL_AURA_PERIODIC_HEAL: // hp5
+ case SPELL_AURA_MOD_REGEN:
+ case SPELL_AURA_MOD_HEALTH_REGEN_IN_COMBAT:
+ return [Stat::HEALTH_REGENERATION];
+ case SPELL_AURA_MOD_POWER_REGEN: // mp5
+ return [Stat::MANA_REGENERATION];
+ case SPELL_AURA_MOD_ATTACK_POWER:
+ return [Stat::MELEE_ATTACK_POWER];
+ case SPELL_AURA_MOD_RANGED_ATTACK_POWER:
+ return [Stat::RANGED_ATTACK_POWER];
+ case SPELL_AURA_MOD_SHIELD_BLOCKVALUE:
+ return [Stat::BLOCK];
+ case SPELL_AURA_MOD_EXPERTISE:
+ return [Stat::EXPERTISE];
+ case SPELL_AURA_MOD_TARGET_RESISTANCE:
+ $amount = abs($amount); // functionally negative, but we work with the absolute amount
+ if ($miscValue == 0x7C) // SPELL_MAGIC_SCHOOLS & ~SPELL_SCHOOL_HOLY
+ return [Stat::SPELL_PENETRATION];
+ }
+
+ return [];
+ }
+}
+
+?>
diff --git a/includes/game/loot/loot.class.php b/includes/game/loot/loot.class.php
new file mode 100644
index 00000000..55c6f513
--- /dev/null
+++ b/includes/game/loot/loot.class.php
@@ -0,0 +1,71 @@
+jsGlobals, $data);
+ }
+}
+
+?>
diff --git a/includes/game/loot/lootbycontainer.class.php b/includes/game/loot/lootbycontainer.class.php
new file mode 100644
index 00000000..64d287d4
--- /dev/null
+++ b/includes/game/loot/lootbycontainer.class.php
@@ -0,0 +1,333 @@
+results;
+ }
+
+ /**
+ * recurse through reference loot while applying modifiers from parent container
+ *
+ * @param string $tableName a known loot template table name
+ * @param int $lootId a loot template entry
+ * @param int $groupId [optional] limit result to provided loot group
+ * @param float $baseChance [optional] chance multiplier passed down from parent container
+ * @return array [[lootRows], [itemIds]]
+ */
+ private function getByContainerRecursive(string $tableName, int $lootId, int $groupId = 0, float $baseChance = 1.0) : array
+ {
+ $loot = [];
+ $rawItems = [];
+
+ if (!$tableName || !$lootId)
+ return [null, null];
+
+ $rows = DB::World()->selectAssoc('SELECT * FROM %n', $tableName, 'WHERE %if', $groupId, '`groupid` = %i AND', $groupId, '%end `entry` = %i', $lootId);
+ if (!$rows)
+ return [null, null];
+
+ $groupChances = [];
+ $nGroupEquals = [];
+ $cnd = new Conditions();
+ foreach ($rows as $entry)
+ {
+ $set = array(
+ 'quest' => $entry['QuestRequired'],
+ 'group' => $entry['GroupId'],
+ 'parentRef' => $tableName == self::REFERENCE ? $lootId : 0,
+ 'realChanceMod' => $baseChance,
+ 'groupChance' => 0
+ );
+
+ $where = [['(`cuFlags` & %i) = 0', CUSTOM_EXCLUDE_FOR_LISTVIEW | CUSTOM_UNAVAILABLE], [DB::OR, []]];
+ for ($i = 1; $i < 5; $i++)
+ $where[1][1][] = ["`reqSourceItemId$i` = %i", $entry['Item']];
+ for ($i = 1; $i < 7; $i++)
+ $where[1][1][] = ["`reqItemId$i` = %i", $entry['Item']];
+
+ if ($entry['QuestRequired'] && ($quests = DB::Aowow()->selectCol('SELECT `id` FROM ::quests WHERE %and', $where)))
+ foreach ($quests as $questId)
+ $cnd->addExternalCondition(Conditions::lootTableToConditionSource($tableName), $lootId . ':' . $entry['Item'], [Conditions::QUESTTAKEN, $questId], true);
+
+ // TC 'mode' (dynamic loot modifier)
+ $buff = [];
+ for ($i = 0; $i < 8; $i++)
+ if ($entry['LootMode'] & (1 << $i))
+ $buff[] = $i + 1;
+
+ $set['mode'] = implode(', ', $buff);
+
+ if ($entry['Reference'])
+ {
+ if (!in_array($entry['Reference'], $this->knownRefs))
+ $this->knownRefs[$entry['Reference']] = $this->getByContainerRecursive(self::REFERENCE, $entry['Reference'], 0, $entry['Chance'] / 100);
+
+ [$data, $raw] = $this->knownRefs[$entry['Reference']];
+
+ $loot = array_merge($loot, $data);
+ $rawItems = array_merge($rawItems, $raw);
+
+ $set['reference'] = $entry['Reference'];
+ $set['multiplier'] = $entry['MaxCount'];
+ }
+ else
+ {
+ $rawItems[] = $entry['Item'];
+ $set['content'] = $entry['Item'];
+ $set['min'] = $entry['MinCount'];
+ $set['max'] = $entry['MaxCount'];
+ }
+
+ if (!isset($groupChances[$entry['GroupId']]))
+ {
+ $groupChances[$entry['GroupId']] = 0;
+ $nGroupEquals[$entry['GroupId']] = 0;
+ }
+
+ if ($set['quest'] || !$set['group'])
+ $set['groupChance'] = $entry['Chance'];
+ else if ($entry['GroupId'] && !$entry['Chance'])
+ {
+ $nGroupEquals[$entry['GroupId']]++;
+ $set['groupChance'] = &$groupChances[$entry['GroupId']];
+ }
+ else if ($entry['GroupId'] && $entry['Chance'])
+ {
+ $set['groupChance'] = $entry['Chance'];
+
+ if (!$entry['Reference'])
+ {
+ if (empty($groupChances[$entry['GroupId']]))
+ $groupChances[$entry['GroupId']] = 0;
+
+ $groupChances[$entry['GroupId']] += $entry['Chance'];
+ }
+ }
+ else // shouldn't have happened
+ {
+ trigger_error('Unhandled case in calculating chance for item '.$entry['Item'].'!', E_USER_WARNING);
+ continue;
+ }
+
+ $loot[] = $set;
+ }
+
+ foreach (array_keys($nGroupEquals) as $k)
+ {
+ $sum = $groupChances[$k];
+ if (!$sum)
+ $sum = 0;
+ else if ($sum >= 100.01)
+ {
+ trigger_error('Loot entry '.$lootId.' / group '.$k.' has a total chance of '.number_format($sum, 2).'%. Some items cannot drop!', E_USER_WARNING);
+ $sum = 100;
+ }
+ // is applied as backReference to items with 0-chance
+ $groupChances[$k] = (100 - $sum) / ($nGroupEquals[$k] ?: 1);
+ }
+
+ if ($cnd->getBySource(Conditions::lootTableToConditionSource($tableName), group: $lootId)->prepare())
+ {
+ $this->storeJSGlobals($cnd->getJsGlobals());
+ $cnd->toListviewColumn($loot, $this->extraCols, $lootId, 'content');
+ }
+
+ return [$loot, array_unique($rawItems)];
+ }
+
+ /**
+ * fetch loot for given loot container and optionally merge multiple container while adding mode info.
+ * If difficultyBit is 0, no merge will occur
+ *
+ * @param string $table a known loote template table name
+ * @param array $lootEntries array of [difficultyBit => entry].
+ * @return bool success and found loot
+ */
+ public function getByContainer(string $table, array $lootEntries): bool
+ {
+ if (!in_array($table, self::TEMPLATES))
+ return false;
+
+ foreach ($lootEntries as $modeBit => $entry)
+ {
+ if (!$entry)
+ continue;
+
+ [$lootRows, $itemIds] = $this->getByContainerRecursive($table, $entry);
+ if (!$lootRows)
+ continue;
+
+ $items = new ItemList(array(['i.id', $itemIds]));
+ $this->storeJSGlobals($items->getJSGlobals(GLOBALINFO_SELF | GLOBALINFO_RELATED));
+ $itemRows = $items->getListviewData();
+
+ // assign listview LV rows to loot rows, not the other way round! The same item may be contained multiple times
+ foreach ($lootRows as $loot)
+ {
+ $count = ceil($loot['groupChance'] * $loot['realChanceMod'] * 100);
+
+ /* on modes...
+ * modes.mode is the (masked) sum of all modes where this item has been seen
+ * modes.mode & 1 dungeon normal
+ * modes.mode & 2 dungeon heroic
+ * modes.mode & 4 generic case (never included in mask for instanced creatures/gos or always === 4 for non-instanced creatures/gos)
+ * modes.mode & 8 raid 10 nh
+ * modes.mode & 16 raid 25 nh
+ * modes.mode & 32 raid 10 hc
+ * modes.mode & 64 raid 25 hc
+ *
+ * modes[4] is _always_ included and is the sum total over all modes:
+ * ex: modes:{"mode":1,"1":{"count":4408,"outof":16013},"4":{"count":4408,"outof":22531}}
+ */
+ if ($modeBit)
+ {
+ $modes = array( // emulate 'percent' with precision: 2
+ 'mode' => $modeBit,
+ $modeBit => ['count' => $count, 'outof' => 10000]
+ );
+ if ($modeBit != 4)
+ $modes[4] = $modes[$modeBit];
+
+
+ // unsure: force display as noteworthy
+ // if (!empty($loot['content']) && !empty($itemRows[$loot['content']]) && $itemRows[$loot['content']]['name'][0] == 7 - ITEM_QUALITY_POOR)
+ // $modes['mode'] = 4;
+ // else if ($count < 100) // chance < 1%
+ // $modes['mode'] = 4;
+
+
+ // existing result row; merge modes and move on
+ if (!is_null($k = array_find_key($this->results, function($x) use ($loot) {
+ if (!empty($loot['reference']))
+ return $x['id'] == $loot['reference'] && $x['mode'] == $loot['mode'] && $x['group'] == $loot['group'] && $x['stack'] == [$loot['multiplier'], $loot['multiplier']];
+ else
+ return $x['id'] == $loot['content'] && $x['mode'] == $loot['mode'] && $x['group'] == $loot['group'];
+ })))
+ {
+ $this->results[$k]['modes']['mode'] |= $modes['mode'];
+ $this->results[$k]['modes'][$modeBit] = $modes[$modeBit];
+ $this->results[$k]['modes'][4]['count'] = max($modes[4]['count'], $this->results[$k]['modes'][4]['count']);
+
+ continue;
+ }
+ }
+
+ $base = array(
+ 'count' => $count,
+ 'outof' => 10000,
+ 'group' => $loot['group'],
+ 'quest' => $loot['quest'],
+ 'mode' => $loot['mode'] ?: null, // dyn loot mode
+ 'modes' => $modes ?? null, // difficulties
+ 'reference' => $loot['parentRef'] ?: null,
+ 'condition' => $loot['condition'] ?? null,
+ 'pctstack' => self::buildStack($loot['min'] ?? 0, $loot['max'] ?? 0)
+ );
+
+ $base = array_filter($base, fn($x) => $x !== null);
+
+ if (empty($loot['reference'])) // regular drop
+ {
+ if ($itemRow = $itemRows[$loot['content']] ?? null)
+ {
+ $extra = ['stack' => [$loot['min'], $loot['max']]];
+
+ // unsure if correct - tag item as trash if chance < 1% and tagged as having many sources
+ if ($base['count'] < 100 && $items->getEntry($loot['content'])['moreMask'] & SRC_FLAG_COMMON)
+ $extra['commondrop'] = 1;
+
+ if (!User::isInGroup(U_GROUP_EMPLOYEE))
+ {
+ if (!isset($this->results[$loot['content']]))
+ $this->results[$loot['content']] = array_merge($itemRow, $base, $extra);
+ else
+ $this->results[$loot['content']]['count'] += $base['count'];
+ }
+ else
+ $this->results[] = array_merge($itemRow, $base, $extra);
+ }
+ else
+ trigger_error('Item #'.$loot['content'].' referenced by loot does not exist!', E_USER_WARNING);
+ }
+ else if (User::isInGroup(U_GROUP_EMPLOYEE)) // create dummy for ref-drop
+ {
+ $data = array(
+ 'id' => $loot['reference'],
+ 'name' => '@REFERENCE: '.$loot['reference'],
+ 'icon' => 'trade_engineering',
+ 'stack' => [$loot['multiplier'], $loot['multiplier']],
+ 'commondrop' => 1
+ );
+ $this->results[] = array_merge($base, $data);
+
+ $this->jsGlobals[Type::ITEM][$loot['reference']] = $data;
+ }
+ }
+ }
+
+ // move excessive % to extra loot
+ if (!User::isInGroup(U_GROUP_EMPLOYEE))
+ {
+ foreach ($this->results as &$_)
+ {
+ // remember 'count' is always relative to a base of 10000
+ if ($_['count'] <= 10000)
+ continue;
+
+ while ($_['count'] > 20000)
+ {
+ $_['stack'][0]++;
+ $_['stack'][1]++;
+ $_['count'] -= 10000;
+ }
+
+ $_['stack'][1]++;
+ $_['count'] = 10000;
+ }
+ }
+ else
+ {
+ $fields = [['mode', 'Dyn. Mode'], ['reference', 'Reference']];
+ $base = [];
+ $set = 0;
+ foreach ($this->results as $foo)
+ {
+ foreach ($fields as $idx => [$field, $title])
+ {
+ $val = $foo[$field] ?? 0;
+ if (!isset($base[$idx]))
+ $base[$idx] = $val;
+ else if ($base[$idx] != $val)
+ $set |= 1 << $idx;
+ }
+
+ if ($set == (pow(2, count($fields)) - 1))
+ break;
+ }
+
+ $this->extraCols[] = "\$Listview.funcBox.createSimpleCol('group', 'Group', '7%', 'group')";
+ foreach ($fields as $idx => [$field, $title])
+ if ($set & (1 << $idx))
+ $this->extraCols[] = "\$Listview.funcBox.createSimpleCol('".$field."', '".$title."', '7%', '".$field."')";
+ }
+
+ return !empty($this->results);
+ }
+}
+
+?>
diff --git a/includes/game/loot/lootbyitem.class.php b/includes/game/loot/lootbyitem.class.php
new file mode 100644
index 00000000..ba7a29ce
--- /dev/null
+++ b/includes/game/loot/lootbyitem.class.php
@@ -0,0 +1,441 @@
+ [Type::NPC, [], '$LANG.tab_droppedby', 'dropped-by', [], [], []],
+ self::QUEST_REWARD => [Type::QUEST, [], '$LANG.tab_rewardfrom', 'reward-from-quest', [], [], []],
+ self::ITEM_CONTAINED => [Type::ITEM, [], '$LANG.tab_containedin', 'contained-in-item', [], [], []],
+ self::OBJECT_CONTAINED => [Type::OBJECT, [], '$LANG.tab_containedin', 'contained-in-object', [], [], []],
+ self::NPC_PICKPOCKETED => [Type::NPC, [], '$LANG.tab_pickpocketedfrom', 'pickpocketed-from', [], [], []],
+ self::NPC_SKINNED => [Type::NPC, [], '$LANG.tab_skinnedfrom', 'skinned-from', [], [], []],
+ self::ITEM_DISENCHANTED => [Type::ITEM, [], '$LANG.tab_disenchantedfrom', 'disenchanted-from', [], [], []],
+ self::ITEM_PROSPECTED => [Type::ITEM, [], '$LANG.tab_prospectedfrom', 'prospected-from', [], [], []],
+ self::ITEM_MILLED => [Type::ITEM, [], '$LANG.tab_milledfrom', 'milled-from', [], [], []],
+ self::NPC_MINED => [Type::NPC, [], '$LANG.tab_minedfromnpc', 'mined-from-npc', [], [], []],
+ self::NPC_SALVAGED => [Type::NPC, [], '$LANG.tab_salvagedfrom', 'salvaged-from', [], [], []],
+ self::NPC_GATHERED => [Type::NPC, [], '$LANG.tab_gatheredfromnpc', 'gathered-from-npc', [], [], []],
+ self::OBJECT_MINED => [Type::OBJECT, [], '$LANG.tab_minedfrom', 'mined-from-object', [], [], []],
+ self::OBJECT_GATHERED => [Type::OBJECT, [], '$LANG.tab_gatheredfrom', 'gathered-from-object', [], [], []],
+ self::ZONE_FISHED => [Type::ZONE, [], '$LANG.tab_fishedin', 'fished-in-zone', [], [], []],
+ self::OBJECT_FISHED => [Type::OBJECT, [], '$LANG.tab_fishedin', 'fished-in-object', [], [], []],
+ self::SPELL_CREATED => [Type::SPELL, [], '$LANG.tab_createdby', 'created-by', [], [], []],
+ self::ACHIEVEMENT_REWARD => [Type::ACHIEVEMENT, [], '$LANG.tab_rewardfrom', 'reward-from-achievement', [], [], []]
+ );
+ private string $queryTemplate =
+ 'SELECT lt1.`entry` AS ARRAY_KEY,
+ IF(lt1.`reference` = 0, lt1.`item`, lt1.`reference`) AS "item",
+ lt1.`chance` AS "chance",
+ SUM(IF(lt2.`chance` = 0, 1, 0)) AS "nZeroItems",
+ SUM(IF(lt2.`reference` = 0, lt2.`chance`, 0)) AS "sumChance",
+ IF(lt1.`groupid` > 0, 1, 0) AS "isGrouped",
+ IF(lt1.`reference` = 0, lt1.`mincount`, 1) AS "min",
+ IF(lt1.`reference` = 0, lt1.`maxcount`, 1) AS "max",
+ IF(lt1.`reference` > 0, lt1.`maxcount`, 1) AS "multiplier"
+ FROM %n lt1
+ LEFT JOIN %n lt2 ON lt1.`entry` = lt2.`entry` AND lt1.`groupid` = lt2.`groupid`
+ WHERE %and
+ GROUP BY lt2.`entry`, lt2.`groupid`';
+
+ /**
+ * @param int $entry item id to find loot container for
+ * @return void
+ */
+ public function __construct(private int $entry)
+ {
+
+ }
+
+ /**
+ * iterate over result set
+ *
+ * @return iterable [tabIdx => [lvTemplate, lvData]]
+ */
+ public function &iterate() : \Generator
+ {
+ reset($this->results);
+
+ foreach ($this->results as $k => [, $tabData])
+ if ($tabData['data']) // only yield tabs with content
+ yield $k => $this->results[$k];
+ }
+
+ /**
+ * calculate chance and stack info and apply to loot rows
+ *
+ * @param array $refs loot rows to apply chance + stack info to
+ * @param array $parents [optional] ref loot ids this call is derived from
+ * @return array [entry => stack+chance-info]
+ */
+ private function calcChance(array $refs, array $parents = []) : array
+ {
+ $result = [];
+
+ foreach ($refs as $rId => $ref)
+ {
+ // check for possible database inconsistencies
+ if (!$ref['chance'] && !$ref['isGrouped'])
+ trigger_error('Loot by Item: Ungrouped Item/Ref '.$ref['item'].' has 0% chance assigned!', E_USER_WARNING);
+
+ if ($ref['isGrouped'] && $ref['sumChance'] > 100)
+ trigger_error('Loot by Item: Group with Item/Ref '.$ref['item'].' has '.number_format($ref['sumChance'], 2).'% total chance! Some items cannot drop!', E_USER_WARNING);
+
+ if ($ref['isGrouped'] && $ref['sumChance'] >= 100 && !$ref['chance'])
+ trigger_error('Loot by Item: Item/Ref '.$ref['item'].' with adaptive chance cannot drop. Group already at 100%!', E_USER_WARNING);
+
+ $chance = abs($ref['chance'] ?: (100 - $ref['sumChance']) / $ref['nZeroItems']) / 100;
+
+ // apply inherited chanceMods
+ if (isset($this->chanceMods[$ref['item']]))
+ {
+ $chance *= $this->chanceMods[$ref['item']][0];
+ $chance = 1 - pow(1 - $chance, $this->chanceMods[$ref['item']][1]);
+ }
+
+ // save chance for parent-ref
+ $this->chanceMods[$rId] = [$chance, $ref['multiplier']];
+
+ // refTemplate doesn't point to a new ref -> we are done
+ if (in_array($rId, $parents))
+ continue;
+
+ $result[$rId] = array(
+ 'percent' => $chance,
+ 'stack' => [$ref['min'], $ref['max']],
+ 'count' => 1 // ..and one for the sort script
+ );
+
+ if ($_ = self::buildStack($ref['min'], $ref['max']))
+ $result[$rId]['pctstack'] = $_;
+ }
+
+ // sort by % DESC
+ uasort($result, fn($a, $b) => $b['percent'] <=> $a['percent']);
+
+ return $result;
+ }
+
+ /**
+ * fetch loot container for item provided to __construct
+ *
+ * @param int $maxResults [optional] SQL_LIMIT override
+ * @param array $lootTableList [optional] limit lookup to provided loot template table names
+ * @return bool success
+ */
+ public function getByItem(int $maxResults = Listview::DEFAULT_SIZE, array $lootTableList = []) : bool
+ {
+ if (!$this->entry)
+ return false;
+
+ $refResults = [];
+
+ /*
+ get references containing the item
+ */
+ $newRefs = DB::World()->selectAssoc(
+ $this->queryTemplate,
+ Loot::REFERENCE, Loot::REFERENCE,
+ [['lt1.`item` = %i', $this->entry], ['lt1.`reference` = 0']]
+ );
+
+ /*
+ i'm currently not seeing a reasonable way to blend this into creature/gobject/etc tabs as one entity may drop the same item multiple times, with and without conditions.
+ if ($newRefs)
+ {
+ $cnd = new Conditions();
+ if ($cnd->getBySource(Conditions::SRC_REFERENCE_LOOT_TEMPLATE, entry: $this->entry))
+ if ($cnd->toListviewColumn($newRefs, $x, $this->entry))
+ $this->storejsGlobals($cnd->getJsGlobals());
+ }
+ */
+
+ while ($newRefs)
+ {
+ $curRefs = $newRefs;
+ $newRefs = DB::World()->selectAssoc(
+ $this->queryTemplate,
+ Loot::REFERENCE, Loot::REFERENCE,
+ [['lt1.`reference` IN %in', array_keys($curRefs)]]
+ );
+
+ $refResults += $this->calcChance($curRefs, array_column($newRefs, 'item'));
+ }
+
+ /*
+ search the real loot-templates for the itemId and gathered refs
+ */
+ foreach (self::TEMPLATES as $lootTemplate)
+ {
+ if ($lootTableList && !in_array($lootTemplate, $lootTableList))
+ continue;
+
+ if ($lootTemplate == Loot::REFERENCE)
+ continue;
+
+ $where = [[DB::OR, [[DB::AND, [['lt1.`reference` = 0'], ['lt1.`item` = %i', $this->entry]]]]]];
+ if ($refResults)
+ $where[0][1][] = ['lt1.`reference` IN %in', array_keys($refResults)];
+
+ $result = $this->calcChance(DB::World()->selectAssoc(
+ $this->queryTemplate,
+ $lootTemplate, $lootTemplate,
+ $where
+ ));
+
+ // do not skip here if $result is empty. Additional loot for spells and quest is added separately
+
+ // format for actual use
+ foreach ($result as $k => $v)
+ {
+ unset($result[$k]);
+ $v['percent'] = round($v['percent'] * 100, 3);
+ $result[abs($k)] = $v;
+ }
+
+ // cap fetched entries to the sql-limit to guarantee that the highest chance items get selected first
+ // screws with GO-loot and skinning-loot as these templates are shared for several tabs (fish, herb, ore) and (herb, ore, leather)
+ $ids = array_slice(array_keys($result), 0, $maxResults);
+
+ // fill ListviewTabs
+ match ($lootTemplate)
+ {
+ Loot::GAMEOBJECT => $this->handleObjectLoot( $ids, $result),
+ Loot::MAIL => $this->handleMailLoot( $ids, $result),
+ Loot::SPELL => $this->handleSpellLoot( $ids, $result),
+ Loot::CREATURE => $this->handleNpcLoot( $ids, $result, self::NPC_DROPPED, 'lootId'),
+ Loot::PICKPOCKET => $this->handleNpcLoot( $ids, $result, self::NPC_PICKPOCKETED, 'pickpocketLootId'),
+ Loot::SKINNING => $this->handleNpcLoot( $ids, $result, self::NPC_SKINNED, 'skinLootId'), // tabId < 0: assigned real id later
+ Loot::PROSPECTING => $this->handleGenericLoot($ids, $result, self::ITEM_PROSPECTED, 'id'),
+ Loot::MILLING => $this->handleGenericLoot($ids, $result, self::ITEM_MILLED, 'id'),
+ Loot::ITEM => $this->handleGenericLoot($ids, $result, self::ITEM_CONTAINED, 'id'),
+ Loot::DISENCHANT => $this->handleGenericLoot($ids, $result, self::ITEM_DISENCHANTED, 'disenchantId'),
+ Loot::FISHING => $this->handleGenericLoot($ids, $result, self::ZONE_FISHED, 'id') // subAreas are currently ignored
+ };
+ }
+
+ // finalize tabs
+ foreach ($this->listviewTabs as $idx => [$type, $data, $name, $id, $extraCols, $hiddenCols, $visibleCols])
+ {
+ $tabData = array(
+ 'data' => $data,
+ 'name' => $name,
+ 'id' => $id
+ );
+
+ if ($extraCols)
+ $tabData['extraCols'] = array_unique($extraCols);
+
+ if ($hiddenCols)
+ $tabData['hiddenCols'] = array_unique($hiddenCols);
+
+ if ($visibleCols)
+ $tabData['visibleCols'] = array_unique($visibleCols);
+
+ $this->results[$idx] = [Type::getFileString($type), $tabData];
+ }
+
+ return true;
+ }
+
+ private function handleGenericLoot(array $ids, array $result, int $tabId, string $dbField) : bool
+ {
+ if (!$ids)
+ return false;
+
+ [$type, &$data, , , &$extraCols, ,] = $this->listviewTabs[$tabId];
+
+ $srcObj = Type::newList($type, [[$dbField, $ids]]);
+ if (!$srcObj || $srcObj->error)
+ return false;
+
+ $srcData = $srcObj->getListviewData();
+ $this->storeJSGlobals($srcObj->getJSGlobals(GLOBALINFO_SELF | GLOBALINFO_RELATED));
+
+ $extraCols[] = '$Listview.extraCols.percent';
+
+ foreach ($srcObj->iterate() as $__)
+ $data[] = array_merge($srcData[$srcObj->id], $result[$srcObj->getField($dbField)]);
+
+ return true;
+ }
+
+ private function handleNpcLoot(array $ids, array $result, int $tabId, string $dbField) : bool
+ {
+ if (!$ids)
+ return false;
+
+ if ($baseIds = DB::Aowow()->selectPairs(
+ 'SELECT `difficultyEntry1` AS ARRAY_KEY, `id` FROM ::creature WHERE `difficultyEntry1` IN %in UNION
+ SELECT `difficultyEntry2` AS ARRAY_KEY, `id` FROM ::creature WHERE `difficultyEntry2` IN %in UNION
+ SELECT `difficultyEntry3` AS ARRAY_KEY, `id` FROM ::creature WHERE `difficultyEntry3` IN %in',
+ $ids, $ids, $ids
+ ))
+ {
+ $parentObj = new CreatureList(array(['id', $baseIds]));
+ if (!$parentObj->error)
+ {
+ $this->storeJSGlobals($parentObj->getJSGlobals());
+ $parentData = $parentObj->getListviewData();
+ $ids = array_diff($ids, $baseIds);
+ }
+ }
+
+ $npc = new CreatureList(array([$dbField, $ids]));
+ if ($npc->error)
+ return false;
+
+ $srcData = $npc->getListviewData();
+ $this->storeJSGlobals($npc->getJSGlobals(GLOBALINFO_SELF | GLOBALINFO_RELATED));
+ [, &$data, , , &$extraCols, ,] = $this->listviewTabs[$tabId];
+
+ foreach ($npc->iterate() as $__)
+ {
+ if ($tabId == self::NPC_SKINNED)
+ {
+ if ($npc->isMineable())
+ $tabId = self::NPC_MINED;
+ else if ($npc->isGatherable())
+ $tabId = self::NPC_GATHERED;
+ else if ($npc->isSalvageable())
+ $tabId = self::NPC_SALVAGED;
+ }
+
+ $p = $npc->getField('parentId');
+
+ $data[] = array_merge($parentData[$p] ?? $srcData[$npc->id], $result[$npc->getField($dbField)]);
+ $extraCols[] = '$Listview.extraCols.percent';
+ }
+
+ return true;
+ }
+
+ private function handleSpellLoot(array $ids, array $result) : bool
+ {
+ $conditions = array(
+ DB::OR,
+ [DB::AND, ['effect1CreateItemId', $this->entry], [DB::OR, ['effect1Id', SpellList::EFFECTS_ITEM_CREATE], ['effect1AuraId', SpellList::AURAS_ITEM_CREATE]]],
+ [DB::AND, ['effect2CreateItemId', $this->entry], [DB::OR, ['effect2Id', SpellList::EFFECTS_ITEM_CREATE], ['effect2AuraId', SpellList::AURAS_ITEM_CREATE]]],
+ [DB::AND, ['effect3CreateItemId', $this->entry], [DB::OR, ['effect3Id', SpellList::EFFECTS_ITEM_CREATE], ['effect3AuraId', SpellList::AURAS_ITEM_CREATE]]]
+ );
+ if ($ids)
+ $conditions[] = ['id', $ids];
+
+ $srcObj = new SpellList($conditions);
+ if ($srcObj->error)
+ return false;
+
+ $this->storeJSGlobals($srcObj->getJSGlobals(GLOBALINFO_SELF | GLOBALINFO_RELATED));
+ [, &$data, , , &$extraCols, , &$visibleCols] = $this->listviewTabs[self::SPELL_CREATED];
+
+ if (!empty($result))
+ $extraCols[] = '$Listview.extraCols.percent';
+
+ if ($srcObj->hasSetFields('reagent1', 'reagent2', 'reagent3', 'reagent4', 'reagent5', 'reagent6', 'reagent7', 'reagent8'))
+ $visibleCols[] = 'reagents';
+
+ foreach ($srcObj->getListviewData() as $id => $row)
+ $data[] = array_merge($row, $result[$id] ?? ['percent' => -1]);
+
+ return true;
+ }
+
+ private function handleMailLoot(array $ids, array $result) : bool
+ {
+ // quest part
+ $conditions = array(DB::OR,
+ ['rewardChoiceItemId1', $this->entry], ['rewardChoiceItemId2', $this->entry], ['rewardChoiceItemId3', $this->entry], ['rewardChoiceItemId4', $this->entry], ['rewardChoiceItemId5', $this->entry],
+ ['rewardChoiceItemId6', $this->entry], ['rewardItemId1', $this->entry], ['rewardItemId2', $this->entry], ['rewardItemId3', $this->entry], ['rewardItemId4', $this->entry]
+ );
+ if ($ids)
+ $conditions[] = ['rewardMailTemplateId', $ids];
+
+ $quests = new QuestList($conditions);
+ if (!$quests->error)
+ {
+ $this->storeJSGlobals($quests->getJSGlobals(GLOBALINFO_SELF | GLOBALINFO_REWARDS));
+ [, &$qData, , , , , ] = $this->listviewTabs[self::QUEST_REWARD];
+
+ foreach ($quests->getListviewData() as $id => $row)
+ $qData[] = array_merge($row, $result[$id] ?? ['percent' => -1]);
+ }
+
+ // achievement part
+ $conditions = array(['itemExtra', $this->entry]);
+ if ($ar = DB::World()->selectCol('SELECT `ID` FROM achievement_reward WHERE %if', $ids, '`MailTemplateID` IN %in OR %end', $ids, '`ItemID` = %i', $this->entry))
+ array_push($conditions, ['id', $ar], DB::OR);
+
+ $achievements = new AchievementList($conditions);
+ if (!$achievements->error)
+ {
+ $this->storeJSGlobals($achievements->getJSGlobals(GLOBALINFO_SELF | GLOBALINFO_REWARDS));
+ [, &$aData, , , , &$hiddenCols, &$visibleCols] = $this->listviewTabs[self::ACHIEVEMENT_REWARD];
+
+ foreach ($achievements->getListviewData() as $id => $row)
+ $aData[] = array_merge($row, $result[$id] ?? ['percent' => -1]);
+
+ $hiddenCols[] = 'rewards';
+ $visibleCols[] = 'category';
+ }
+
+ return !$quests->error || !$achievements->error;
+ }
+
+ private function handleObjectLoot(array $ids, array $result) : bool
+ {
+ if (!$ids)
+ return false;
+
+ $srcObj = new GameObjectList(array(['lootId', $ids]));
+ if ($srcObj->error)
+ return false;
+
+ foreach ($srcObj->getListviewData() as $id => $row)
+ {
+ $tabId = match($row['type'])
+ {
+ 25 => self::OBJECT_FISHED, // fishing node
+ -3 => self::OBJECT_GATHERED, // herb
+ -4 => self::OBJECT_MINED, // vein
+ default => self::OBJECT_CONTAINED // general chest loot
+ };
+
+ [, &$tabData, , , &$extraCols, , &$visibleCols] = $this->listviewTabs[$tabId];
+
+ $tabData[] = array_merge($row, $result[$srcObj->getEntry($id)['lootId']]);
+ $extraCols[] = '$Listview.extraCols.percent';
+ if ($tabId != 15)
+ $visibleCols[] = 'skill';
+ }
+
+ return true;
+ }
+}
+
+?>
diff --git a/includes/game/misc.php b/includes/game/misc.php
new file mode 100644
index 00000000..11cc6ab1
--- /dev/null
+++ b/includes/game/misc.php
@@ -0,0 +1,376 @@
+ 'inv_misc_questionmark',
+ 0 => 'spell_nature_elementalabsorption',
+ 6 => ['spell_deathknight_bloodpresence', 'spell_deathknight_frostpresence', 'spell_deathknight_unholypresence' ],
+ 11 => ['spell_nature_starfall', 'ability_racial_bearform', 'spell_nature_healingtouch' ],
+ 3 => ['ability_hunter_beasttaming', 'ability_marksmanship', 'ability_hunter_swiftstrike' ],
+ 8 => ['spell_holy_magicalsentry', 'spell_fire_firebolt02', 'spell_frost_frostbolt02' ],
+ 2 => ['spell_holy_holybolt', 'spell_holy_devotionaura', 'spell_holy_auraoflight' ],
+ 5 => ['spell_holy_wordfortitude', 'spell_holy_holybolt', 'spell_shadow_shadowwordpain' ],
+ 4 => ['ability_rogue_eviscerate', 'ability_backstab', 'ability_stealth' ],
+ 7 => ['spell_nature_lightning', 'spell_nature_lightningshield', 'spell_nature_magicimmunity' ],
+ 9 => ['spell_shadow_deathcoil', 'spell_shadow_metamorphosis', 'spell_shadow_rainoffire' ],
+ 1 => ['ability_rogue_eviscerate', 'ability_warrior_innerrage', 'ability_warrior_defensivestance' ]
+ );
+
+ public const /* array */ QUEST_CLASSES = array(
+ -2 => [ 0],
+ 0 => [ 1, 3, 4, 8, 9, 10, 11, 12, 25, 28, 33, 36, 38, 40, 41, 44, 45, 46, 47, 51, 85, 130, 132, 139, 154, 267, 1497, 1519, 1537, 2257, 3430, 3431, 3433, 3487, 4080, 4298],
+ 1 => [ 14, 15, 16, 17, 141, 148, 188, 215, 220, 331, 357, 361, 363, 400, 405, 406, 440, 490, 493, 618, 1377, 1637, 1638, 1657, 1769, 3524, 3525, 3526, 3557],
+ 2 => [ 206, 209, 491, 717, 718, 719, 721, 722, 796, 1176, 1196, 1337, 1477, 1581, 1583, 1584, 1941, 2017, 2057, 2100, 2366, 2367, 2437, 2557, 3535, 3562, 3688, 3713, 3714, 3715, 3716, 3717, 3789, 3790, 3791, 3792, 3842, 3847, 3848, 3849, 3905, 4100, 4131, 4196, 4228, 4264, 4265, 4272, 4277, 4415, 4416, 4494, 4522, 4723, 4809, 4813, 4820],
+ 3 => [ 1977, 2159, 2677, 2717, 3428, 3429, 3456, 3457, 3606, 3607, 3805, 3836, 3845, 3923, 3959, 4075, 4273, 4493, 4500, 4603, 4722, 4812, 4987],
+ 4 => [ -372, -263, -262, -261, -162, -161, -141, -82, -81, -61],
+ 5 => [ -373, -371, -324, -304, -264, -201, -182, -181, -121, -101, -24],
+ 6 => [ -25, 2597, 3277, 3358, 3820, 4384, 4710],
+ 7 => [-1010, -368, -367, -365, -344, -241, -1],
+ 8 => [ 3483, 3518, 3519, 3520, 3521, 3522, 3523, 3679, 3703],
+ 9 => [-1005, -1003, -1002, -1001, -376, -375, -374, -370, -369, -366, -364, -41, -22], // 22: seasonal
+ 10 => [ 65, 66, 67, 210, 394, 495, 2817, 3537, 3711, 4024, 4197, 4395, 4742]
+ );
+
+ // questSortId for quests need updating
+ // partially points non-instanced area with identical name for instance quests
+ public static array $questSortFix = array(
+ -221 => 440, // Treasure Map => Tanaris
+ -284 => 0, // Special => Misc (some quests get shuffled into seasonal)
+ 151 => 0, // Designer Island => Misc
+ 22 => 0, // Programmer Isle
+ 35 => 33, // Booty Bay => Stranglethorn Vale
+ 131 => 132, // Kharanos => Coldridge Valley
+ 24 => 9, // Northshire Abbey => Northshire Valley
+ 279 => 36, // Dalaran Crater => Alterac Mountains
+ 4342 => 4298, // Acherus: The Ebon Hold => The Scarlet Enclave
+ 2079 => 15, // Alcaz Island => Dustwallow Marsh
+ 1939 => 440, // Abyssal Sands => Tanaris
+ 393 => 363, // Darkspeer Strand => Valley of Trials
+ 702 => 141, // Rut'theran Village => Teldrassil
+ 221 => 220, // Camp Narache => Red Cloud Mesa
+ 1116 => 357, // Feathermoon Stronghold => Feralas
+ 236 => 209, // Shadowfang Keep
+ 4769 => 4742, // Hrothgar's Landing => Hrothgar's Landing
+ 4613 => 4395, // Dalaran City => Dalaran
+ 4522 => 210, // Icecrown Citadell => Icecrown
+ 3896 => 3703, // Aldor Rise => Shattrath City
+ 3696 => 3522, // The Barrier Hills => Blade's Edge Mountains
+ 2839 => 2597, // Alterac Valley
+ 19 => 1977, // Zul'Gurub
+ 4445 => 4273, // Ulduar
+ 2300 => 1941, // Caverns of Time
+ 3545 => 3535, // Hellfire Citadel
+ 2562 => 3457, // Karazhan
+ 3840 => 3959, // Black Temple
+ 1717 => 491, // Razorfen Kraul
+ 978 => 1176, // Zul'Farrak
+ 133 => 721, // Gnomeregan
+ 3607 => 3905, // Serpentshrine Cavern
+ 3845 => 3842, // Tempest Keep
+ 1517 => 1337, // Uldaman
+ 1417 => 1477 // Sunken Temple
+ );
+
+ public static array $questSubCats = array(
+ 1 => [132], // Dun Morogh: Coldridge Valley
+ 12 => [9], // Elwynn Forest: Northshire Valley
+ 141 => [188], // Teldrassil: Shadowglen
+ 3524 => [3526], // Azuremyst Isle: Ammen Vale
+
+ 14 => [363], // Durotar: Valley of Trials
+ 85 => [154], // Tirisfal Glades: Deathknell
+ 215 => [220], // Mulgore: Red Cloud Mesa
+ 3430 => [3431], // Eversong Woods: Sunstrider Isle
+
+ 46 => [25], // Burning Steppes: Blackrock Mountain
+ 361 => [1769], // Felwood: Timbermaw Hold
+ 3519 => [3679], // Terokkar: Skettis
+ 3535 => [3562, 3713, 3714], // Hellfire Citadel
+ 3905 => [3715, 3716, 3717], // Coilfang Reservoir
+ 3688 => [3789, 3790, 3792], // Auchindoun
+ 1941 => [2366, 2367, 4100], // Caverns of Time
+ 3842 => [3847, 3848, 3849], // Tempest Keep
+ 4522 => [4809, 4813, 4820] // Icecrown Citadel
+ );
+
+ /* why:
+ Because petSkills (and ranged weapon skills) are the only ones with more than two skillLines attached. Because Left Joining ::spell with ::skillLineability causes more trouble than it has uses.
+ Because this is more or less the only reaonable way to fit all that information into one database field, so..
+ .. the indizes of this array are bits of skillLine2OrMask in ::spell if skillLineId1 is negative
+ */
+ public static array $skillLineMask = array( // idx => [familyId, skillLineId]
+ -1 => array( // Pets (Hunter)
+ [ 1, 208], [ 2, 209], [ 3, 203], [ 4, 210], [ 5, 211], [ 6, 212], [ 7, 213], // Wolf, Cat, Spider, Bear, Boar, Crocolisk, Carrion Bird
+ [ 8, 214], [ 9, 215], [11, 217], [12, 218], [20, 236], [21, 251], [24, 653], // Crab, Gorilla, Raptor, Tallstrider, Scorpid, Turtle, Bat
+ [25, 654], [26, 655], [27, 656], [30, 763], [31, 767], [32, 766], [33, 765], // Hyena, Bird of Prey, Wind Serpent, Dragonhawk, Ravager, Warp Stalker, Sporebat
+ [34, 764], [35, 768], [37, 775], [38, 780], [39, 781], [41, 783], [42, 784], // Nether Ray, Serpent, Moth, Chimaera, Devilsaur, Silithid, Worm
+ [43, 786], [44, 785], [45, 787], [46, 788] // Rhino, Wasp, Core Hound, Spirit Beast
+ ),
+ -2 => array( // Pets (Warlock)
+ [15, 189], [16, 204], [17, 205], [19, 207], [23, 188], [29, 761] // Felhunter, Voidwalker, Succubus, Doomguard, Imp, Felguard
+ ),
+ -3 => array( // Ranged Weapons
+ [null, 45], [null, 46], [null, 226] // Bow, Gun, Crossbow
+ )
+ );
+
+ public static array $sockets = array( // jsStyle Strings
+ 'meta', 'red', 'yellow', 'blue'
+ );
+
+ public static function getReputationLevelForPoints(int $pts) : int
+ {
+ return match (true) {
+ $pts >= 42000 => REP_EXALTED,
+ $pts >= 21000 => REP_REVERED,
+ $pts >= 9000 => REP_HONORED,
+ $pts >= 3000 => REP_FRIENDLY,
+ $pts >= 0 => REP_NEUTRAL,
+ $pts >= -3000 => REP_UNFRIENDLY,
+ $pts >= -6000 => REP_HOSTILE,
+ default => REP_HATED,
+ };
+ }
+
+ public static function getTaughtSpells(mixed &$spell) : array
+ {
+ $extraIds = [-1]; // init with -1 to prevent empty-array errors
+ $lookup = [-1];
+ switch (gettype($spell))
+ {
+ case 'object':
+ if (get_class($spell) != SpellList::class)
+ return [];
+
+ $lookup[] = $spell->id;
+ foreach ($spell->canTeachSpell() as $idx)
+ $extraIds[] = $spell->getField('effect'.$idx.'TriggerSpell');
+
+ break;
+ case 'integer':
+ $lookup[] = $spell;
+ break;
+ case 'array':
+ $lookup = $spell;
+ break;
+ default:
+ return [];
+ }
+
+ // note: omits required spell and chance in skill_discovery_template
+ $data = array_merge(
+ DB::World()->selectCol('SELECT spellId FROM spell_learn_spell WHERE entry IN %in', $lookup),
+ DB::World()->selectCol('SELECT spellId FROM skill_discovery_template WHERE reqSpell IN %in', $lookup),
+ $extraIds
+ );
+
+ // return list of integers, not strings
+ $data = array_map('intVal', $data);
+
+ return $data;
+ }
+
+ public static function getBook(int $ptId, ?int $startPage = null) : ?Book
+ {
+ $pages = [];
+ while ($ptId)
+ {
+ if ($row = DB::World()->selectRow('SELECT ptl.`Text` AS Text_loc%i, pt.* FROM page_text pt LEFT JOIN page_text_locale ptl ON pt.`ID` = ptl.`ID` AND locale = %s WHERE pt.`ID` = %i', Lang::getLocale()->value, Lang::getLocale()->json(), $ptId))
+ {
+ $ptId = $row['NextPageID'];
+ $pages[] = Util::localizedString($row, 'Text');
+ continue;
+ }
+
+ trigger_error('Referenced PageTextId #'.$ptId.' is not in DB', E_USER_WARNING);
+ break;
+ }
+
+ return $pages ? new Book($pages, page: $startPage) : null;
+ }
+
+ public static function getQuotesForCreature(int $creatureId, bool $asHTML = false, string $talkSource = '') : array
+ {
+ $nQuotes = 0;
+ $quotes = [];
+ $soundIds = [];
+
+ $quoteSrc = DB::World()->selectAssoc(
+ 'SELECT ct.`GroupID` AS ARRAY_KEY, ct.`ID` AS ARRAY_KEY2, ct.`Type` AS "talkType", ct.TextRange AS "range",
+ IFNULL(bct.`LanguageID`, ct.`Language`) AS "lang",
+ IFNULL(NULLIF(bct.`Text`, ""), IFNULL(NULLIF(bct.`Text1`, ""), IFNULL(ct.`Text`, ""))) AS "text_loc0",
+ %if', Lang::getLocale()->value,
+ 'IFNULL(NULLIF(bctl.`Text`, ""), IFNULL(NULLIF(bctl.`Text1`, ""), IFNULL(ctl.`Text`, ""))) AS text_loc%i,', Lang::getLocale()->value,
+ '%end
+ IF(bct.`SoundEntriesID` > 0, bct.`SoundEntriesID`, ct.`Sound`) AS "soundId"
+ FROM creature_text ct
+ LEFT JOIN broadcast_text bct ON ct.`BroadcastTextId` = bct.`ID`
+ %if', Lang::getLocale()->value,
+ 'LEFT JOIN creature_text_locale ctl ON ct.`CreatureID` = ctl.`CreatureID` AND ct.`GroupID` = ctl.`GroupID` AND ct.`ID` = ctl.`ID` AND ctl.`Locale` = %s', Lang::getLocale()->json(),
+ 'LEFT JOIN broadcast_text_locale bctl ON ct.`BroadcastTextId` = bctl.`ID` AND bctl.`locale` = %s', Lang::getLocale()->json(),
+ '%end
+ WHERE ct.`CreatureID` = %i',
+ $creatureId
+ );
+
+ foreach ($quoteSrc as $grp => $text)
+ {
+ $group = [];
+ foreach ($text as $t)
+ {
+ if ($t['soundId'])
+ $soundIds[] = $t['soundId'];
+
+ $msg = Util::localizedString($t, 'text');
+ if (!$msg)
+ continue;
+
+ // fixup .. either set %s for emotes or dont >.<
+ if (in_array($t['talkType'], [2, 16]) && strpos($msg, '%s') === false)
+ $msg = '%s '.$msg;
+
+ // fixup: bad case-insensitivity
+ $msg = Util::parseHtmlText(str_replace('%S', '%s', htmlentities($msg)), !$asHTML);
+
+ if ($talkSource)
+ $msg = sprintf($msg, $talkSource);
+
+ // convert [old, new] talkType to css compatible
+ $t['talkType'] = match ((int)$t['talkType'])
+ {
+ 0, 12 => 2, // say - yellow-ish
+ 1, 14 => 1, // yell - dark red
+ 2, 16, // emote
+ 3, 41 => 4, // boss emote - orange
+ 4, 15, // whisper
+ 5, 42 => 3, // boss whisper - pink-ish
+ default => 2
+ };
+
+ // prefix
+ $prefix = '';
+ if ($t['talkType'] != 4)
+ $prefix = ($talkSource ?: '%s').' '.Lang::npc('textTypes', $t['talkType']).Lang::main('colon').($t['lang'] ? '['.Lang::game('languages', $t['lang']).'] ' : ' ');
+
+ if ($asHTML)
+ $msg = ''.$prefix.($t['range'] ? sprintf(Util::$dfnString, Lang::npc('textRanges', $t['range']), $msg) : $msg).' ';
+ else
+ $msg = '[div][span class=s'.$t['talkType'].']'.$prefix.html_entity_decode($msg).'[/span][/div]';
+
+ $line = array(
+ 'range' => $t['range'],
+ 'text' => $msg
+ );
+
+ $nQuotes++;
+ $group[] = $line;
+ }
+
+ if ($group)
+ $quotes[$grp] = $group;
+ }
+
+ return [$quotes, $nQuotes, $soundIds];
+ }
+
+ public static function getBreakpointsForSkill(int $skillId, int $reqLevel) : array
+ {
+ if ($skillId == SKILL_FISHING)
+ return array(
+ round(sqrt(.25) * $reqLevel), // 25% valid catches
+ round(sqrt(.50) * $reqLevel), // 50% valid catches
+ round(sqrt(.75) * $reqLevel), // 75% valid catches
+ $reqLevel // 100% valid catches
+ );
+
+ switch ($skillId)
+ {
+ case SKILL_SKINNING:
+ $reqLevel /= 5; // we pass creature level * 5 (so, skill value), but formula depends on actual creature level
+ if ($reqLevel < 10)
+ $reqLevel = 0;
+ else if ($reqLevel < 20)
+ $reqLevel = ($reqLevel - 10) * 10;
+ else
+ $reqLevel *= 5;
+ case SKILL_HERBALISM:
+ case SKILL_LOCKPICKING:
+ case SKILL_JEWELCRAFTING:
+ case SKILL_INSCRIPTION:
+ case SKILL_MINING:
+ case SKILL_ENGINEERING:
+ $points = [$reqLevel]; // red/orange
+
+ if ($reqLevel + 25 <= MAX_SKILL) // orange/yellow
+ $points[] = $reqLevel + 25;
+
+ if ($reqLevel + 50 <= MAX_SKILL) // yellow/green
+ $points[] = $reqLevel + 50;
+
+ if ($reqLevel + 100 <= MAX_SKILL) // green/grey
+ $points[] = $reqLevel + 100;
+
+ return $points;
+ default:
+ return [$reqLevel];
+ }
+ }
+
+ public static function getEnchantmentCondition(int $conditionId, bool $interactive = false) : string
+ {
+ $gemCnd = DB::Aowow()->selectRow('SELECT * FROM ::itemenchantmentcondition WHERE `id` = %i', $conditionId);
+ if (!$gemCnd)
+ return '';
+
+ $x = '';
+ for ($i = 1; $i < 6; $i++)
+ {
+ if (!$gemCnd['color'.$i])
+ continue;
+
+ $fiColors = function (int $idx)
+ {
+ return match ($idx)
+ {
+ 2 => '0:3:5', // red
+ 3 => '2:4:5', // yellow
+ 4 => '1:3:4', // blue
+ default => '' // uhhh....
+ };
+ };
+
+ $bLink = $gemCnd['color'.$i] ? ($interactive ? ''. Lang::item('gemColors', $gemCnd['color'.$i] - 1).'' : Lang::item('gemColors', $gemCnd['color'.$i] - 1)) : '';
+ $cLink = $gemCnd['cmpColor'.$i] ? ($interactive ? ''.Lang::item('gemColors', $gemCnd['cmpColor'.$i] - 1).'' : Lang::item('gemColors', $gemCnd['cmpColor'.$i] - 1)) : '';
+
+ switch ($gemCnd['comparator'.$i])
+ {
+ case ENCHANT_CONDITION_LESS_VALUE: // requires less than N gems
+ case ENCHANT_CONDITION_MORE_VALUE: // requires at least N gems
+ $x .= ''.Lang::item('gemRequires').Lang::item('gemConditions', $gemCnd['comparator'.$i], [$gemCnd['value'.$i], $bLink]).' ';
+ break;
+ case ENCHANT_CONDITION_MORE_COMPARE: // requires more gems than gems
+ $x .= ''.Lang::item('gemRequires').Lang::item('gemConditions', $gemCnd['comparator'.$i], [$bLink, $cLink]).' ';
+ break;
+ }
+ }
+
+ return $x;
+ }
+}
+
+?>
diff --git a/includes/game/worldposition.class.php b/includes/game/worldposition.class.php
new file mode 100644
index 00000000..df13aaf6
--- /dev/null
+++ b/includes/game/worldposition.class.php
@@ -0,0 +1,196 @@
+= 100 || $set['posY'] >= 100)
+ {
+ $set = null;
+ return true;
+ }
+
+ if (empty(self::$alphaMapCache[$areaId]))
+ self::$alphaMapCache[$areaId] = imagecreatefrompng($file);
+
+ // alphaMaps are 1000 x 1000, adapt points [black => valid point]
+ if (!imagecolorat(self::$alphaMapCache[$areaId], $set['posX'] * 10, $set['posY'] * 10))
+ $set = null;
+
+ return true;
+ }
+
+ public static function checkZonePos(array $points) : array
+ {
+ $result = [];
+
+ foreach ($points as $res)
+ {
+ if (self::alphaMapCheck($res['areaId'], $res))
+ {
+ if (!$res)
+ continue;
+
+ // some rough measure how central the spawn is on the map (the lower the number, the better)
+ // 0: perfect center; 1: touches a border
+ $q = abs( (($res['posX'] - 50) / 50) * (($res['posY'] - 50) / 50) );
+
+ if (empty($result) || $result[0] > $q)
+ $result = [$q, $res];
+ }
+ // capitals (auto-discovered) and no hand-made alphaMap available
+ else if (in_array($res['areaId'], self::$capitalCities))
+ return $res;
+ // add with lowest quality if alpha map is missing
+ else if (empty($result))
+ $result = [1.0, $res];
+ }
+
+ // spawn does not really match on a map, but we need at least one result
+ if (!$result)
+ {
+ usort($points, fn($a, $b) => $a['dist'] <=> $b['dist']);
+ $result = [1.0, $points[0]];
+ }
+
+ return $result[1];
+ }
+
+ public static function getForGUID(int $type, int ...$guids) : array
+ {
+ $result = [];
+
+ switch ($type)
+ {
+ case Type::NPC:
+ $result = DB::World()->selectAssoc('SELECT `guid` AS ARRAY_KEY, `id`, `map` AS `mapId`, `position_x` AS `posX`, `position_y` AS `posY` FROM creature WHERE `guid` IN %in', $guids);
+ break;
+ case Type::OBJECT:
+ $result = DB::World()->selectAssoc('SELECT `guid` AS ARRAY_KEY, `id`, `map` AS `mapId`, `position_x` AS `posX`, `position_y` AS `posY` FROM gameobject WHERE `guid` IN %in', $guids);
+ break;
+ case Type::SOUND:
+ $result = DB::AoWoW()->selectAssoc('SELECT `id` AS ARRAY_KEY, `soundId` AS `id`, `mapId`, `posX`, `posY` FROM ::soundemitters WHERE `id` IN %in', $guids);
+ break;
+ case Type::ZONE:
+ $result = DB::Aowow()->selectAssoc('SELECT -`id` AS ARRAY_KEY, `id`, `parentMapId` AS `mapId`, `parentX` AS `posX`, `parentY` AS `posY` FROM ::zones WHERE -`id` IN %in', $guids);
+ break;
+ case Type::AREATRIGGER:
+ $result = [];
+ if ($base = array_filter($guids, fn($x) => $x > 0))
+ $result = array_replace($result, DB::AoWoW()->selectAssoc('SELECT `id` AS ARRAY_KEY, `id`, `mapId`, `posX`, `posY` FROM ::areatrigger WHERE `id` IN %in', $base));
+ if ($endpoints = array_filter($guids, fn($x) => $x < 0))
+ $result = array_replace($result, DB::World()->selectAssoc(
+ 'SELECT -`ID` AS ARRAY_KEY, ID AS `id`, `target_map` AS `mapId`, `target_position_x` AS `posX`, `target_position_y` AS `posY` FROM areatrigger_teleport WHERE -`id` IN %in UNION
+ SELECT -`entryorguid` AS ARRAY_KEY, entryorguid AS `id`, `action_param1` AS `mapId`, `target_x` AS `posX`, `target_y` AS `posY` FROM smart_scripts WHERE -`entryorguid` IN %in AND `source_type` = %i AND `action_type` = %i',
+ $endpoints, $endpoints, SmartAI::SRC_TYPE_AREATRIGGER, SmartAction::ACTION_TELEPORT
+ ));
+ break;
+ default:
+ trigger_error('WorldPosition::getForGUID - unsupported TYPE #'.$type, E_USER_WARNING);
+ }
+
+ if ($diff = array_diff($guids, array_keys($result)))
+ trigger_error('WorldPosition::getForGUID - no spawn points for TYPE #'.$type.' GUIDS: '.implode(', ', $diff), E_USER_WARNING);
+
+ return $result;
+ }
+
+ public static function toZonePos(int $mapId, float $mapX, float $mapY, int $preferedAreaId = 0, int $preferedFloor = -1) : array
+ {
+ if (!$mapId < 0)
+ return [];
+
+ if (!isset(self::$zoneMapCache[$mapId]))
+ self::initZoneMaps($mapId);
+
+ $points = [];
+ for ($i = 0; $i < 2; $i++)
+ {
+ foreach (self::$zoneMapCache[$mapId] as $area)
+ {
+ if (!$i && $preferedAreaId != 0 && $area['areaId'] != $preferedAreaId)
+ continue;
+
+ if (!$i && $preferedFloor >= 0 && $area['floor'] != $preferedFloor)
+ continue;
+
+ if ($mapX < $area['minX'] || $mapX > $area['maxX'] ||
+ $mapY < $area['minY'] || $mapY > $area['maxY'])
+ continue;
+
+ // dist BETWEEN 0 (center) AND 70.7 (corner)
+ $posX = round(($area['maxY'] - $mapY) * 100 / ($area['maxY'] - $area['minY']), 1);
+ $posY = round(($area['maxX'] - $mapX) * 100 / ($area['maxX'] - $area['minX']), 1);
+ $dist = sqrt(pow(abs($posX - 50), 2) + pow(abs($posY - 50), 2));
+
+ $points[] = array(
+ 'id' => $area['id'],
+ 'areaId' => $area['areaId'],
+ 'floor' => $area['floor'],
+ 'multifloor' => $area['multifloor'],
+ 'srcPrio' => $area['srcPrio'],
+ 'posX' => $posX,
+ 'posY' => $posY,
+ 'dist' => $dist
+ );
+ }
+
+ // retry: pre-instance subareas belong to the instance-maps but are displayed on the outside. There also cases where the zone reaches outside it's own map.
+ if ($points)
+ break;
+ }
+
+ // sort by srcPrio DESC (primary), dist ASC (secondary)
+ usort($points, fn($a, $b) => ($b['srcPrio'] <=> $a['srcPrio']) ?: ($a['dist'] <=> $b['dist']));
+
+ return $points;
+ }
+
+ private static function initZoneMaps(int $mapId) : void
+ {
+ self::$zoneMapCache[$mapId] = DB::Aowow()->selectAssoc(
+ 'SELECT
+ x.`id`,
+ x.`areaId`,
+ x.`minX`, x.`maxX`, x.`minY`, x.`maxY`,
+ IF(x.`defaultDungeonMapId` < 0, x.`floor` + 1, x.`floor`) AS `floor`,
+ IF(useDM.`id` IS NOT NULL OR x.`defaultDungeonMapId` < 0, 1, 0) AS `srcPrio`,
+ IF(multiDM.`id` IS NOT NULL OR x.`defaultDungeonMapId` < 0, 1, 0) AS `multifloor`
+ FROM
+ (SELECT 0 AS `id`, `areaId`, `mapId`, `right` AS `minY`, `left` AS `maxY`, `top` AS `maxX`, `bottom` AS `minX`, 0 AS `floor`, 0 AS `worldMapAreaId`, `defaultDungeonMapId` FROM aowow_worldmaparea wma UNION
+ SELECT dm.`id`, `areaId`, wma.`mapId`, `minY`, `maxY`, `maxX`, `minX`, `floor`, `worldMapAreaId`, `defaultDungeonMapId` FROM aowow_worldmaparea wma
+ JOIN aowow_dungeonmap dm ON dm.`mapId` = wma.`mapId` WHERE wma.`mapId` NOT IN (0, 1, 530, 571) OR wma.`areaId` = 4395) x
+ LEFT JOIN
+ aowow_dungeonmap useDM ON useDM.`mapId` = x.`mapId` AND useDM.`worldMapAreaId` = x.`worldMapAreaId` AND useDM.`floor` = x.`floor` AND useDM.`worldMapAreaId` > 0
+ LEFT JOIN
+ aowow_dungeonmap multiDM ON multiDM.`mapId` = x.`mapId` AND multiDM.`worldMapAreaId` = x.`worldMapAreaId` AND multiDM.`floor` <> x.`floor` AND multiDM.`worldMapAreaId` > 0
+ WHERE
+ x.`mapId` = %i AND x.`areaId` <> 0 AND
+ x.`minX` <> 0 AND x.`maxX` <> 0 AND x.`minY` <> 0 AND x.`maxY` <> 0
+ GROUP BY
+ x.`id`, x.`areaId`',
+ $mapId
+ ) ?: [];
+ }
+}
+
+?>
diff --git a/includes/kernel.php b/includes/kernel.php
index d2f0a283..6afe3dbb 100644
--- a/includes/kernel.php
+++ b/includes/kernel.php
@@ -1,81 +1,237 @@
!extension_loaded($x)))
+ $error .= 'Required Extension '.implode(', ', $ext)." was not found. Please check if it should exist, using \"php -m\"\n\n";
+
+if ($ext = array_filter($badExt, fn($x) => extension_loaded($x)))
+ $error .= 'Loaded Extension '.implode(', ', $ext)." is incompatible and must be disabled.\n\n";
+
+if (version_compare(PHP_VERSION, '8.2.0') < 0)
+ $error .= 'PHP Version 8.2 or higher required! Your version is '.PHP_VERSION.".\nCore functions are unavailable!\n";
+
+if ($error)
+ die(CLI ? strip_tags($error) : $error);
+
+
+require_once 'includes/defines.php';
+require_once 'includes/locale.class.php';
+require_once 'localization/lang.class.php';
+require_once 'localization/datetime.class.php';
+require_once 'includes/libs/autoload.php'; // Composer libraries
+require_once 'includes/database.php'; // wrap dg/dibi (https://https://dibi.nette.org/)
+require_once 'includes/utilities.php'; // helper functions
+require_once 'includes/type.class.php'; // DB types storage and factory
+require_once 'includes/cfg.class.php'; // Config holder
+require_once 'includes/user.class.php'; // Session handling (could be skipped for CLI context except for username and password validation used in account creation)
+require_once 'includes/game/misc.php'; // Misc game related data & functions
+
+// game client data interfaces
+spl_autoload_register(function (string $class) : void
+{
+ if ($i = strrpos($class, '\\'))
+ $class = substr($class, $i + 1);
+
+ if (preg_match('/[^\w]/i', $class))
+ return;
+
+ if ($class == 'Stat' || $class == 'StatsContainer') // entity statistics conversion
+ require_once 'includes/game/chrstatistics.php';
+ else if (file_exists('includes/game/'.strtolower($class).'.class.php'))
+ require_once 'includes/game/'.strtolower($class).'.class.php';
+ else if (file_exists('includes/game/loot/'.strtolower($class).'.class.php'))
+ require_once 'includes/game/loot/'.strtolower($class).'.class.php';
+});
+
+// our site components
+spl_autoload_register(function (string $class) : void
+{
+ if ($i = strrpos($class, '\\'))
+ $class = substr($class, $i + 1);
+
+ if (preg_match('/[^\w]/i', $class))
+ return;
+
+ if (file_exists('includes/components/'.strtolower($class).'.class.php'))
+ require_once 'includes/components/'.strtolower($class).'.class.php';
+ else if (file_exists('includes/components/frontend/'.strtolower($class).'.class.php'))
+ require_once 'includes/components/frontend/'.strtolower($class).'.class.php';
+ else if (file_exists('includes/components/response/'.strtolower($class).'.class.php'))
+ require_once 'includes/components/response/'.strtolower($class).'.class.php';
+});
+
+// TC systems in components
+spl_autoload_register(function (string $class) : void
+{
+ switch ($class)
+ {
+ case __NAMESPACE__.'\SmartAI':
+ case __NAMESPACE__.'\SmartEvent':
+ case __NAMESPACE__.'\SmartAction':
+ case __NAMESPACE__.'\SmartTarget':
+ require_once 'includes/components/SmartAI/SmartAI.class.php';
+ require_once 'includes/components/SmartAI/SmartEvent.class.php';
+ require_once 'includes/components/SmartAI/SmartAction.class.php';
+ require_once 'includes/components/SmartAI/SmartTarget.class.php';
+ break;
+ case __NAMESPACE__.'\Conditions':
+ require_once 'includes/components/Conditions/Conditions.class.php';
+ break;
+ }
+});
+
+// autoload List-classes, associated filters
+spl_autoload_register(function (string $class) : void
+{
+ if ($i = strrpos($class, '\\'))
+ $class = substr($class, $i + 1);
+
+ if (preg_match('/[^\w]/i', $class))
+ return;
+
+ if (!stripos($class, 'list'))
+ return;
+
+ $class = strtolower(str_replace('ListFilter', 'List', $class));
+
+ $cl = match ($class)
+ {
+ 'localprofilelist',
+ 'remoteprofilelist' => 'profile',
+ 'localarenateamlist',
+ 'remotearenateamlist' => 'arenateam',
+ 'localguildlist',
+ 'remoteguildlist' => 'guild',
+ default => strtr($class, ['list' => ''])
+ };
+
+ if (file_exists('includes/dbtypes/'.$cl.'.class.php'))
+ require_once 'includes/dbtypes/'.$cl.'.class.php';
+ else
+ throw new \Exception('could not register type class: '.$cl);
+});
+
+set_error_handler(function(int $errNo, string $errStr, string $errFile, int $errLine) : bool
+{
+ // either from test function or handled separately
+ if (strstr($errStr, 'mysqli_connect') && $errNo == E_WARNING)
+ return true;
+
+ // do not log XDebug shenanigans
+ if (strstr($errFile, 'xdebug://'))
+ return true;
+
+ // we do not log deprecation notices
+ if ($errNo & (E_DEPRECATED | E_USER_DEPRECATED))
+ return true;
+
+ $logLevel = match($errNo)
+ {
+ E_RECOVERABLE_ERROR, E_USER_ERROR => LOG_LEVEL_ERROR,
+ E_WARNING, E_USER_WARNING => LOG_LEVEL_WARN,
+ E_NOTICE, E_USER_NOTICE => LOG_LEVEL_INFO,
+ default => 0
+ };
+ $errName = match($errNo)
+ {
+ E_RECOVERABLE_ERROR => 'RECOVERABLE_ERROR',
+ E_USER_ERROR => 'USER_ERROR',
+ E_USER_WARNING, E_WARNING => 'WARNING',
+ E_USER_NOTICE, E_NOTICE => 'NOTICE',
+ default => 'UNKNOWN_ERROR' // errors not in this list can not be handled by set_error_handler (as per documentation) or are ignored
+ };
+
+ if (!empty($_POST['password']))
+ $_POST['password'] = '******';
+ if (!empty($_POST['c_password']))
+ $_POST['c_password'] = '******';
+
+ if (DB::isConnected(DB_AOWOW))
+ DB::Aowow()->qry('INSERT INTO ::errors (`date`, `version`, `phpError`, `file`, `line`, `query`, `post`, `userGroups`, `message`) VALUES (UNIX_TIMESTAMP(), %i, %i, %s, %i, %s, %s, %i, %s) ON DUPLICATE KEY UPDATE `date` = UNIX_TIMESTAMP()',
+ AOWOW_REVISION, $errNo, $errFile, $errLine, CLI ? 'CLI' : substr($_SERVER['QUERY_STRING'] ?? '', 0, 250), empty($_POST) ? '' : http_build_query($_POST), User::$groups, $errStr
+ );
+
+ $logMsg = $errName.' - '.$errStr.' @ '.$errFile. ':'.$errLine;
+ if (CLI && class_exists(__NAMESPACE__.'\CLI'))
+ CLI::write($logMsg, $logLevel);
+ else if (CLI)
+ fwrite(STDERR, $logMsg);
+ else if (Cfg::get('DEBUG') >= $logLevel)
+ Util::addNote($logMsg, U_GROUP_EMPLOYEE, $logLevel);
+
+ return true;
+}, E_ALL);
+
+// handle exceptions
+set_exception_handler(function (\Throwable $e) : void
+{
+ if (!empty($_POST['password']))
+ $_POST['password'] = '******';
+ if (!empty($_POST['c_password']))
+ $_POST['c_password'] = '******';
+
+ if (DB::isConnected(DB_AOWOW))
+ DB::Aowow()->qry('INSERT INTO ::errors (`date`, `version`, `phpError`, `file`, `line`, `query`, `post`, `userGroups`, `message`) VALUES (UNIX_TIMESTAMP(), %i, %i, %s, %i, %s, %s, %i, %s) ON DUPLICATE KEY UPDATE `date` = UNIX_TIMESTAMP()',
+ AOWOW_REVISION, $e->getCode(), $e->getFile(), $e->getLine(), CLI ? 'CLI' : substr($_SERVER['QUERY_STRING'] ?? '', 0, 250), empty($_POST) ? '' : http_build_query($_POST), User::$groups, $e->getMessage()
+ );
+
+ if (CLI)
+ fwrite(STDERR, "\nException - ".$e->getMessage()."\n ".$e->getFile(). '('.$e->getLine().")\n".$e->getTraceAsString()."\n\n");
+ else
+ {
+ Util::addNote('Exception - '.$e->getMessage().' @ '.$e->getFile(). ':'.$e->getLine()."\n".$e->getTraceAsString(), U_GROUP_EMPLOYEE, LOG_LEVEL_ERROR);
+ (new TemplateResponse())->generateError();
+ }
+});
+
+// handle fatal errors
+register_shutdown_function(function() : void
+{
+ // defer undisplayed error/exception notes
+ if (!CLI && ($n = Util::getNotes()))
+ $_SESSION['notes'][] = [$n[0], $n[1], 'Deferred issues from previous request'];
+
+ if ($e = error_get_last())
+ {
+ if (!empty($_POST['password']))
+ $_POST['password'] = '******';
+ if (!empty($_POST['c_password']))
+ $_POST['c_password'] = '******';
+
+ if (DB::isConnected(DB_AOWOW))
+ DB::Aowow()->qry('INSERT INTO ::errors (`date`, `version`, `phpError`, `file`, `line`, `query`, `post`, `userGroups`, `message`) VALUES (UNIX_TIMESTAMP(), %i, %i, %s, %i, %s, %s, %i, %s) ON DUPLICATE KEY UPDATE `date` = UNIX_TIMESTAMP()',
+ AOWOW_REVISION, $e['type'], $e['file'], $e['line'], CLI ? 'CLI' : substr($_SERVER['QUERY_STRING'] ?? '', 0, 250), empty($_POST) ? '' : http_build_query($_POST), User::$groups, $e['message']
+ );
+
+ if (CLI)
+ fwrite(STDERR, "\nFatal Error - ".$e['message'].' @ '.$e['file']. ':'.$e['line']."\n\n");
+ else if (User::isInGroup(U_GROUP_EMPLOYEE))
+ echo "\nFatal Error - ".$e['message'].' @ '.$e['file']. ':'.$e['line']."\n\n";
+ }
+});
+
+// Setup DB-Wrapper
if (file_exists('config/config.php'))
require_once 'config/config.php';
else
$AoWoWconf = [];
-
-mb_internal_encoding('UTF-8');
-
-
-define('OS_WIN', substr(PHP_OS, 0, 3) == 'WIN');
-
-
-require_once 'includes/defines.php';
-require_once 'includes/libs/DbSimple/Generic.php'; // Libraray: http://en.dklab.ru/lib/DbSimple (using variant: https://github.com/ivan1986/DbSimple/tree/master)
-require_once 'includes/utilities.php'; // helper functions
-require_once 'includes/game.php'; // game related data & functions
-require_once 'includes/profiler.class.php';
-require_once 'includes/user.class.php';
-require_once 'includes/markup.class.php'; // manipulate markup text
-require_once 'includes/database.class.php'; // wrap DBSimple
-require_once 'includes/community.class.php'; // handle comments, screenshots and videos
-require_once 'includes/loot.class.php'; // build lv-tabs containing loot-information
-require_once 'localization/lang.class.php';
-require_once 'pages/genericPage.class.php';
-
-
-// autoload List-classes, associated filters and pages
-spl_autoload_register(function ($class) {
- $class = strtolower(str_replace('ListFilter', 'List', $class));
-
- if (class_exists($class)) // already registered
- return;
-
- if (preg_match('/[^\w]/i', $class)) // name should contain only letters
- return;
-
- if (stripos($class, 'list'))
- {
- require_once 'includes/basetype.class.php';
-
- $cl = strtr($class, ['list' => '']);
- if ($cl == 'remoteprofile' || $cl == 'localprofile')
- $cl = 'profile';
- if ($cl == 'remotearenateam' || $cl == 'localarenateam')
- $cl = 'arenateam';
- if ($cl == 'remoteguild' || $cl == 'localguild')
- $cl = 'guild';
-
- if (file_exists('includes/types/'.$cl.'.class.php'))
- require_once 'includes/types/'.$cl.'.class.php';
- else
- throw new Exception('could not register type class: '.$cl);
-
- return;
- }
- else if (stripos($class, 'ajax') === 0)
- {
- require_once 'includes/ajaxHandler.class.php'; // handles ajax and jsonp requests
-
- if (file_exists('includes/ajaxHandler/'.strtr($class, ['ajax' => '']).'.class.php'))
- require_once 'includes/ajaxHandler/'.strtr($class, ['ajax' => '']).'.class.php';
- else
- throw new Exception('could not register ajaxHandler class: '.$class);
-
- return;
- }
- else if (file_exists('pages/'.strtr($class, ['page' => '']).'.php'))
- require_once 'pages/'.strtr($class, ['page' => '']).'.php';
-});
-
-
-// Setup DB-Wrapper
if (!empty($AoWoWconf['aowow']['db']))
DB::load(DB_AOWOW, $AoWoWconf['aowow']);
@@ -90,161 +246,43 @@ if (!empty($AoWoWconf['characters']))
if (!empty($charDBInfo))
DB::load(DB_CHARACTERS . $realm, $charDBInfo);
-
-// load config to constants
-$sets = DB::isConnectable(DB_AOWOW) ? DB::Aowow()->select('SELECT `key` AS ARRAY_KEY, `value`, `flags` FROM ?_config') : [];
-foreach ($sets as $k => $v)
-{
- $php = $v['flags'] & CON_FLAG_PHP;
-
- // this should not have been possible
- if (!strlen($v['value']) && !($v['flags'] & CON_FLAG_TYPE_STRING) && !$php)
- {
- trigger_error('Aowow config value CFG_'.strtoupper($k).' is empty - config will not be used!', E_USER_ERROR);
- continue;
- }
-
- if ($v['flags'] & CON_FLAG_TYPE_INT)
- $val = intVal($v['value']);
- else if ($v['flags'] & CON_FLAG_TYPE_FLOAT)
- $val = floatVal($v['value']);
- else if ($v['flags'] & CON_FLAG_TYPE_BOOL)
- $val = (bool)$v['value'];
- else if ($v['flags'] & CON_FLAG_TYPE_STRING)
- $val = preg_replace("/[\p{C}]/ui", '', $v['value']);
- else if ($php)
- {
- trigger_error('PHP config value '.strtolower($k).' has no type set - config will not be used!', E_USER_ERROR);
- continue;
- }
- else // if (!$php)
- {
- trigger_error('Aowow config value CFG_'.strtoupper($k).' has no type set - value forced to 0!', E_USER_ERROR);
- $val = 0;
- }
-
- if ($php)
- ini_set(strtolower($k), $val);
- else
- define('CFG_'.strtoupper($k), $val);
-}
+$AoWoWconf = null; // empty auths
-// handle non-fatal errors and notices
-error_reporting(!empty($AoWoWconf['aowow']) && CFG_DEBUG ? E_AOWOW : 0);
-set_error_handler(function($errNo, $errStr, $errFile, $errLine)
-{
- $errName = 'unknown error'; // errors not in this list can not be handled by set_error_handler (as per documentation) or are ignored
- $uGroup = U_GROUP_EMPLOYEE;
+// for CLI and early errors in erb context
+Lang::load(Locale::EN);
- if ($errNo == E_WARNING) // 0x0002
- $errName = 'E_WARNING';
- else if ($errNo == E_PARSE) // 0x0004
- $errName = 'E_PARSE';
- else if ($errNo == E_NOTICE) // 0x0008
- $errName = 'E_NOTICE';
- else if ($errNo == E_USER_ERROR) // 0x0100
- $errName = 'E_USER_ERROR';
- else if ($errNo == E_USER_WARNING) // 0x0200
- $errName = 'E_USER_WARNING';
- else if ($errNo == E_USER_NOTICE) // 0x0400
- {
- $errName = 'E_USER_NOTICE';
- $uGroup = U_GROUP_STAFF;
- }
- else if ($errNo == E_RECOVERABLE_ERROR) // 0x1000
- $errName = 'E_RECOVERABLE_ERROR';
-
- Util::addNote($uGroup, $errName.' - '.$errStr.' @ '.$errFile. ':'.$errLine);
-
- if (DB::isConnectable(DB_AOWOW))
- DB::Aowow()->query('INSERT INTO ?_errors (`date`, `version`, `phpError`, `file`, `line`, `query`, `userGroups`, `message`) VALUES (UNIX_TIMESTAMP(), ?d, ?d, ?, ?d, ?, ?d, ?) ON DUPLICATE KEY UPDATE `date` = UNIX_TIMESTAMP()',
- AOWOW_REVISION, $errNo, $errFile, $errLine, CLI ? 'CLI' : $_SERVER['QUERY_STRING'], User::$groups, $errStr
- );
-
- return true;
-}, E_AOWOW);
-
-// handle exceptions
-set_exception_handler(function ($ex)
-{
- Util::addNote(U_GROUP_EMPLOYEE, 'Exception - '.$ex->getMessage().' @ '.$ex->getFile(). ':'.$ex->getLine()."\n".$ex->getTraceAsString());
-
- if (DB::isConnectable(DB_AOWOW))
- DB::Aowow()->query('INSERT INTO ?_errors (`date`, `version`, `phpError`, `file`, `line`, `query`, `userGroups`, `message`) VALUES (UNIX_TIMESTAMP(), ?d, ?d, ?, ?d, ?, ?d, ?) ON DUPLICATE KEY UPDATE `date` = UNIX_TIMESTAMP()',
- AOWOW_REVISION, $ex->getCode(), $ex->getFile(), $ex->getLine(), CLI ? 'CLI' : $_SERVER['QUERY_STRING'], User::$groups, $ex->getMessage()
- );
-
- if (!CLI)
- (new GenericPage(null))->error();
- else
- echo 'Exception - '.$ex->getMessage()."\n ".$ex->getFile(). '('.$ex->getLine().")\n".$ex->getTraceAsString()."\n";
-});
-
-// handle fatal errors
-register_shutdown_function(function()
-{
- if (($e = error_get_last()) && $e['type'] & (E_ERROR | E_COMPILE_ERROR | E_CORE_ERROR))
- {
- Util::addNote(U_GROUP_EMPLOYEE, 'Fatal Error - '.$e['message'].' @ '.$e['file']. ':'.$e['line']);
-
- if (DB::isConnectable(DB_AOWOW))
- DB::Aowow()->query('INSERT INTO ?_errors (`date`, `version`, `phpError`, `file`, `line`, `query`, `userGroups`, `message`) VALUES (UNIX_TIMESTAMP(), ?d, ?d, ?, ?d, ?, ?d, ?) ON DUPLICATE KEY UPDATE `date` = UNIX_TIMESTAMP()',
- AOWOW_REVISION, $e['type'], $e['file'], $e['line'], CLI ? 'CLI' : $_SERVER['QUERY_STRING'], User::$groups, $e['message']
- );
-
- if (CLI)
- echo 'Fatal Error - '.$e['message'].' @ '.$e['file']. ':'.$e['line']."\n";
-
- // cant generate a page for web view :(
- die();
- }
-});
-
-$secure = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] != 'off') || (!empty($AoWoWconf['aowow']) && CFG_FORCE_SSL);
-if (defined('CFG_STATIC_HOST')) // points js to images & scripts
- define('STATIC_URL', ($secure ? 'https://' : 'http://').CFG_STATIC_HOST);
-
-if (defined('CFG_SITE_HOST')) // points js to executable files
- define('HOST_URL', ($secure ? 'https://' : 'http://').CFG_SITE_HOST);
+// load config from DB
+Cfg::load();
if (!CLI)
{
- if (!defined('CFG_SITE_HOST') || !defined('CFG_STATIC_HOST'))
- die('error: SITE_HOST or STATIC_HOST not configured');
+ // not displaying the brb gnomes as static_host is missing, but eh...
+ if (!DB::isConnected(DB_AOWOW) || !DB::isConnected(DB_WORLD) || !Cfg::get('HOST_URL') || !Cfg::get('STATIC_URL'))
+ (new TemplateResponse())->generateMaintenance();
// Setup Session
- if (CFG_SESSION_CACHE_DIR && Util::writeDir(CFG_SESSION_CACHE_DIR))
- session_save_path(getcwd().'/'.CFG_SESSION_CACHE_DIR);
+ $cacheDir = Cfg::get('SESSION_CACHE_DIR');
+ if ($cacheDir && Util::writeDir($cacheDir))
+ session_save_path(getcwd().'/'.$cacheDir);
- session_set_cookie_params(15 * YEAR, '/', '', $secure, true);
+ session_set_cookie_params(15 * YEAR, '/', '', (($_SERVER['HTTPS'] ?? 'off') != 'off') || Cfg::get('FORCE_SSL'), true);
session_cache_limiter('private');
- session_start();
- if (!empty($AoWoWconf['aowow']) && User::init())
- User::save(); // save user-variables in session
-
- // hard-override locale for this call (should this be here..?)
- // all strings attached..
- if (!empty($AoWoWconf['aowow']))
+ if (!session_start())
{
- if (isset($_GET['locale']) && (CFG_LOCALES & (1 << (int)$_GET['locale'])))
- User::useLocale($_GET['locale']);
-
- Lang::load(User::$localeString);
+ trigger_error('failed to start session', E_USER_ERROR);
+ (new TemplateResponse())->generateError();
}
- // parse page-parameters .. sanitize before use!
- $str = explode('&', $_SERVER['QUERY_STRING'], 2)[0];
- $_ = explode('=', $str, 2);
- $pageCall = $_[0];
- $pageParam = isset($_[1]) ? $_[1] : null;
+ if (User::init())
+ User::save(); // save user-variables in session
- Util::$wowheadLink = 'http://'.Util::$subDomains[User::$localeId].'.wowhead.com/'.$str;
+ // hard override locale for this call (should this be here..?)
+ if (isset($_GET['locale']) && ($loc = Locale::tryFrom((int)$_GET['locale'])))
+ Lang::load($loc);
+ else
+ Lang::load(User::$preferedLoc);
}
-else if (!empty($AoWoWconf['aowow']))
- Lang::load('enus');
-
-$AoWoWconf = null; // empty auths
?>
diff --git a/includes/libs/DbSimple/CacherImpl.php b/includes/libs/DbSimple/CacherImpl.php
deleted file mode 100644
index 986cb130..00000000
--- a/includes/libs/DbSimple/CacherImpl.php
+++ /dev/null
@@ -1,45 +0,0 @@
-callback = $callback;
- } else {
- $this->callback = $this->callbackDummy;
- }
- }
-
- public function clean($mode = Zend_Cache::CLEANING_MODE_ALL, $tags = array()) {}
-
- public function remove($id) {}
-
- public function test($id) {}
-
- public function save($data, $id, $tags = array(), $specificLifetime = false)
- {
- return call_user_func($this->callback, $id, $data);
- }
-
- public function load($id, $doNotTestCacheValidity = false)
- {
- return call_user_func($this->callback, $id);
- }
-
- public function setDirectives($directives) {}
-
- protected function callbackDummy($k, $v)
- {
- return null;
- }
-
-} // CacherImpl class
\ No newline at end of file
diff --git a/includes/libs/DbSimple/Connect.php b/includes/libs/DbSimple/Connect.php
deleted file mode 100644
index 6489c89a..00000000
--- a/includes/libs/DbSimple/Connect.php
+++ /dev/null
@@ -1,262 +0,0 @@
-нужен для ленивой инициализации коннекта к базе
- *
- * @package DbSimple
- * @method mixed transaction(string $mode=null)
- * @method mixed commit()
- * @method mixed rollback()
- * @method mixed select(string $query [, $arg1] [,$arg2] ...)
- * @method mixed selectRow(string $query [, $arg1] [,$arg2] ...)
- * @method array selectCol(string $query [, $arg1] [,$arg2] ...)
- * @method string selectCell(string $query [, $arg1] [,$arg2] ...)
- * @method mixed query(string $query [, $arg1] [,$arg2] ...)
- * @method string escape(mixed $s, bool $isIdent=false)
- * @method DbSimple_SubQuery subquery(string $query [, $arg1] [,$arg2] ...)
- * @method callback setLogger(callback $logger)
- * @method callback setCacher(callback $cacher)
- * @method string setIdentPrefix($prx)
- * @method string setCachePrefix($prx)
- */
-class DbSimple_Connect
-{
- /** @var DbSimple_Generic_Database База данных */
- protected $DbSimple;
- /** @var string DSN подключения */
- protected $DSN;
- /** @var string Тип базы данных */
- protected $shema;
- /** @var array Что выставить при коннекте */
- protected $init;
- /** @var integer код ошибки */
- public $error = null;
- /** @var string сообщение об ошибке */
- public $errmsg = null;
-
- /**
- * Конструктор только запоминает переданный DSN
- * создание класса и коннект происходит позже
- *
- * @param string $dsn DSN строка БД
- */
- public function __construct($dsn)
- {
- $this->DbSimple = null;
- $this->DSN = $dsn;
- $this->init = array();
- $this->shema = ucfirst(substr($dsn, 0, strpos($dsn, ':')));
- }
-
- /**
- * Взять базу из пула коннектов
- *
- * @param string $dsn DSN строка БД
- * @return DbSimple_Connect
- */
- public static function get($dsn)
- {
- static $pool = array();
- return isset($pool[$dsn]) ? $pool[$dsn] : $pool[$dsn] = new self($dsn);
- }
-
- /**
- * Возвращает тип базы данных
- *
- * @return string имя типа БД
- */
- public function getShema()
- {
- return $this->shema;
- }
-
- /**
- * Коннект при первом запросе к базе данных
- */
- public function __call($method, $params)
- {
- if ($this->DbSimple === null)
- $this->connect($this->DSN);
- return call_user_func_array(array(&$this->DbSimple, $method), $params);
- }
-
- /**
- * mixed selectPage(int &$total, string $query [, $arg1] [,$arg2] ...)
- * Функцию нужно вызвать отдельно из-за передачи по ссылке
- */
- public function selectPage(&$total, $query)
- {
- if ($this->DbSimple === null)
- $this->connect($this->DSN);
- $args = func_get_args();
- $args[0] = &$total;
- return call_user_func_array(array(&$this->DbSimple, 'selectPage'), $args);
- }
-
- /**
- * Подключение к базе данных
- * @param string $dsn DSN строка БД
- */
- protected function connect($dsn)
- {
- $parsed = $this->parseDSN($dsn);
- if (!$parsed)
- $this->errorHandler('Ошибка разбора строки DSN', $dsn);
- if (!isset($parsed['scheme']))
- $this->errorHandler('Невозможно загрузить драйвер базы данных', $parsed);
- $this->shema = ucfirst($parsed['scheme']);
- require_once __DIR__.'/'.$this->shema.'.php';
- $class = 'DbSimple_'.$this->shema;
- $this->DbSimple = new $class($parsed);
- $this->errmsg = &$this->DbSimple->errmsg;
- $this->error = &$this->DbSimple->error;
- $prefix = isset($parsed['prefix']) ? $parsed['prefix'] : ($this->_identPrefix ? $this->_identPrefix : false);
- if ($prefix)
- $this->DbSimple->setIdentPrefix($prefix);
- if ($this->_cachePrefix) $this->DbSimple->setCachePrefix($this->_cachePrefix);
- if ($this->_cacher) $this->DbSimple->setCacher($this->_cacher);
- if ($this->_logger) $this->DbSimple->setLogger($this->_logger);
- $this->DbSimple->setErrorHandler($this->errorHandler!==null ? $this->errorHandler : array(&$this, 'errorHandler'));
- //выставление переменных
- foreach($this->init as $query)
- call_user_func_array(array(&$this->DbSimple, 'query'), $query);
- $this->init = array();
- }
-
- /**
- * Функция обработки ошибок - стандартный обработчик
- * Все вызовы без @ прекращают выполнение скрипта
- *
- * @param string $msg Сообщение об ошибке
- * @param array $info Подробная информация о контексте ошибки
- */
- public function errorHandler($msg, $info)
- {
- // Если использовалась @, ничего не делать.
- if (!error_reporting()) return;
- // Выводим подробную информацию об ошибке.
- echo "SQL Error: $msg
";
- print_r($info);
- echo " ";
- exit();
- }
-
- /**
- * Выставляет запрос для инициализации
- *
- * @param string $query запрос
- */
- public function addInit($query)
- {
- $args = func_get_args();
- if ($this->DbSimple !== null)
- return call_user_func_array(array(&$this->DbSimple, 'query'), $args);
- $this->init[] = $args;
- }
-
- /**
- * Устанавливает новый обработчик ошибок
- * Обработчик получает 2 аргумента:
- * - сообщение об ошибке
- * - массив (код, сообщение, запрос, контекст)
- *
- * @param callback|null|false $handler обработчик ошибок
- * null - по умолчанию
- * false - отключен
- * @return callback|null|false предыдущий обработчик
- */
- public function setErrorHandler($handler)
- {
- $prev = $this->errorHandler;
- $this->errorHandler = $handler;
- if ($this->DbSimple)
- $this->DbSimple->setErrorHandler($handler);
- return $prev;
- }
-
- /** @var callback обработчик ошибок */
- private $errorHandler = null;
- private $_cachePrefix = '';
- private $_identPrefix = null;
- private $_logger = null;
- private $_cacher = null;
-
- /**
- * callback setLogger(callback $logger)
- * Set query logger called before each query is executed.
- * Returns previous logger.
- */
- public function setLogger($logger)
- {
- $prev = $this->_logger;
- $this->_logger = $logger;
- if ($this->DbSimple)
- $this->DbSimple->setLogger($logger);
- return $prev;
- }
-
- /**
- * callback setCacher(callback $cacher)
- * Set cache mechanism called during each query if specified.
- * Returns previous handler.
- */
- public function setCacher(Zend_Cache_Backend_Interface $cacher=null)
- {
- $prev = $this->_cacher;
- $this->_cacher = $cacher;
- if ($this->DbSimple)
- $this->DbSimple->setCacher($cacher);
- return $prev;
- }
-
- /**
- * string setIdentPrefix($prx)
- * Set identifier prefix used for $_ placeholder.
- */
- public function setIdentPrefix($prx)
- {
- $old = $this->_identPrefix;
- if ($prx !== null) $this->_identPrefix = $prx;
- if ($this->DbSimple)
- $this->DbSimple->setIdentPrefix($prx);
- return $old;
- }
-
- /**
- * string setCachePrefix($prx)
- * Set cache prefix used in key caclulation.
- */
- public function setCachePrefix($prx)
- {
- $old = $this->_cachePrefix;
- if ($prx !== null) $this->_cachePrefix = $prx;
- if ($this->DbSimple)
- $this->DbSimple->setCachePrefix($prx);
- return $old;
- }
-
- /**
- * Разбирает строку DSN в массив параметров подключения к базе
- *
- * @param string $dsn строка DSN для разбора
- * @return array Параметры коннекта
- */
- protected function parseDSN($dsn)
- {
- return DbSimple_Generic::parseDSN($dsn);
- }
-
-}
diff --git a/includes/libs/DbSimple/Database.php b/includes/libs/DbSimple/Database.php
deleted file mode 100644
index 86719ae1..00000000
--- a/includes/libs/DbSimple/Database.php
+++ /dev/null
@@ -1,1412 +0,0 @@
-.
- *
- * Contains 3 classes:
- * - DbSimple_Database: common database methods
- * - DbSimple_Blob: common BLOB support
- * - DbSimple_LastError: error reporting and tracking
- *
- * Special result-set fields:
- * - ARRAY_KEY* ("*" means "anything")
- * - PARENT_KEY
- *
- * Transforms:
- * - GET_ATTRIBUTES
- * - CALC_TOTAL
- * - GET_TOTAL
- * - UNIQ_KEY
- *
- * Query attributes:
- * - BLOB_OBJ
- * - CACHE
- *
- * @author Dmitry Koterov, http://forum.dklab.ru/users/DmitryKoterov/
- * @author Konstantin Zhinko, http://forum.dklab.ru/users/KonstantinGinkoTit/
- * @author Ivan Borzenkov, http://forum.dklab.ru/users/Ivan1986/
- *
- * @version 2.x $Id$
- */
-
-/**
- * Use this constant as placeholder value to skip optional SQL block [...].
- */
-if (!defined('DBSIMPLE_SKIP'))
- define('DBSIMPLE_SKIP', log(0));
-
-/**
- * Names of special columns in result-set which is used
- * as array key (or karent key in forest-based resultsets) in
- * resulting hash.
- */
-if (!defined('DBSIMPLE_ARRAY_KEY'))
- define('DBSIMPLE_ARRAY_KEY', 'ARRAY_KEY'); // hash-based resultset support
-if (!defined('DBSIMPLE_PARENT_KEY'))
- define('DBSIMPLE_PARENT_KEY', 'PARENT_KEY'); // forrest-based resultset support
-
-
-if ( !interface_exists('Zend_Cache_Backend_Interface', false) ) {
- require_once __DIR__ . '/Zend/Cache.php';
- require_once __DIR__ . '/Zend/Cache/Backend/Interface.php';
-}
-
-require_once __DIR__ . '/CacherImpl.php';
-
-
-/**
- *
- * Base class for all databases.
- * Can create transactions and new BLOBs, parse DSNs.
- *
- * Logger is COMMON for multiple transactions.
- * Error handler is private for each transaction and database.
- */
-abstract class DbSimple_Database extends DbSimple_LastError
-{
- /**
- * Public methods.
- */
-
- /**
- * object blob($blob_id)
- * Create new blob
- */
- public function blob($blob_id = null)
- {
- $this->_resetLastError();
- return $this->_performNewBlob($blob_id);
- }
-
- /**
- * void transaction($mode)
- * Create new transaction.
- */
- public function transaction($mode=null)
- {
- $this->_resetLastError();
- $this->_logQuery('-- START TRANSACTION '.$mode);
- return $this->_performTransaction($mode);
- }
-
- /**
- * mixed commit()
- * Commit the transaction.
- */
- public function commit()
- {
- $this->_resetLastError();
- $this->_logQuery('-- COMMIT');
- return $this->_performCommit();
- }
-
- /**
- * mixed rollback()
- * Rollback the transaction.
- */
- public function rollback()
- {
- $this->_resetLastError();
- $this->_logQuery('-- ROLLBACK');
- return $this->_performRollback();
- }
-
- /**
- * mixed select(string $query [, $arg1] [,$arg2] ...)
- * Execute query and return the result.
- */
- public function select($query)
- {
- $args = func_get_args();
- $total = false;
- return $this->_query($args, $total);
- }
-
- /**
- * mixed selectPage(int &$total, string $query [, $arg1] [,$arg2] ...)
- * Execute query and return the result.
- * Total number of found rows (independent to LIMIT) is returned in $total
- * (in most cases second query is performed to calculate $total).
- */
- public function selectPage(&$total, $query)
- {
- $args = func_get_args();
- array_shift($args);
- $total = true;
- return $this->_query($args, $total);
- }
-
- /**
- * hash selectRow(string $query [, $arg1] [,$arg2] ...)
- * Return the first row of query result.
- * On errors return false and set last error.
- * If no one row found, return array()! It is useful while debugging,
- * because PHP DOES NOT generates notice on $row['abc'] if $row === null
- * or $row === false (but, if $row is empty array, notice is generated).
- */
- public function selectRow()
- {
- $args = func_get_args();
- $total = false;
- $rows = $this->_query($args, $total);
- if (!is_array($rows)) return $rows;
- if (!count($rows)) return array();
- reset($rows);
- return current($rows);
- }
-
- /**
- * array selectCol(string $query [, $arg1] [,$arg2] ...)
- * Return the first column of query result as array.
- */
- public function selectCol()
- {
- $args = func_get_args();
- $total = false;
- $rows = $this->_query($args, $total);
- if (!is_array($rows)) return $rows;
- $this->_shrinkLastArrayDimensionCallback($rows);
- return $rows;
- }
-
- /**
- * scalar selectCell(string $query [, $arg1] [,$arg2] ...)
- * Return the first cell of the first column of query result.
- * If no one row selected, return null.
- */
- public function selectCell()
- {
- $args = func_get_args();
- $total = false;
- $rows = $this->_query($args, $total);
- if (!is_array($rows)) return $rows;
- if (!count($rows)) return null;
- reset($rows);
- $row = current($rows);
- if (!is_array($row)) return $row;
- reset($row);
- return current($row);
- }
-
- /**
- * mixed query(string $query [, $arg1] [,$arg2] ...)
- * Alias for select(). May be used for INSERT or UPDATE queries.
- */
- public function query()
- {
- $args = func_get_args();
- $total = false;
- return $this->_query($args, $total);
- }
-
- /**
- * string escape(mixed $s, bool $isIdent=false)
- * Enclose the string into database quotes correctly escaping
- * special characters. If $isIdent is true, value quoted as identifier
- * (e.g.: `value` in MySQL, "value" in Firebird, [value] in MSSQL).
- */
- public function escape($s, $isIdent=false)
- {
- return $this->_performEscape($s, $isIdent);
- }
-
-
- /**
- * DbSimple_SubQuery subquery(string $query [, $arg1] [,$arg2] ...)
- * Выполняет разворачивание плейсхолдеров без коннекта к базе
- * Нужно для сложных запросов, состоящих из кусков, которые полезно сохранить
- *
- */
- public function subquery()
- {
- $args = func_get_args();
- $this->_expandPlaceholders($args,$this->_placeholderNativeArgs !== null);
- return new DbSimple_SubQuery($args);
- }
-
-
- /**
- * callback setLogger(callback $logger)
- * Set query logger called before each query is executed.
- * Returns previous logger.
- */
- public function setLogger($logger)
- {
- $prev = $this->_logger;
- $this->_logger = $logger;
- return $prev;
- }
-
- /**
- * callback setCacher(callback $cacher)
- * Set cache mechanism called during each query if specified.
- * Returns previous handler.
- */
- public function setCacher($cacher=null)
- {
- $prev = $this->_cacher;
-
- if ( is_null($cacher) ) {
- return $prev;
- }
-
- if ($cacher instanceof Zend_Cache_Backend_Interface) {
- $this->_cacher = $cacher;
- return $prev;
- }
-
- if ( is_callable($cacher) ) {
- $this->_cacher = new CacherImpl($cacher);
- return $prev;
- }
-
- return $prev;
- }
-
- /**
- * string setIdentPrefix($prx)
- * Set identifier prefix used for $_ placeholder.
- */
- public function setIdentPrefix($prx)
- {
- $old = $this->_identPrefix;
- if ($prx !== null) $this->_identPrefix = $prx;
- return $old;
- }
-
- /**
- * string setCachePrefix($prx)
- * Set cache prefix used in key caclulation.
- */
- public function setCachePrefix($prx)
- {
- $old = $this->_cachePrefix;
- if ($prx !== null) $this->_cachePrefix = $prx;
- return $old;
- }
-
- /**
- * Задает имя класса строки
- *
- * для следующего запроса каждая строка будет
- * заменена классом, конструктору которого передается
- * массив поле=>значение для этой строки
- *
- * @param string $name имя класса
- * @return DbSimple_Generic_Database указатель на себя
- */
- public function setClassName($name)
- {
- $this->_className = $name;
- return $this;
- }
-
- /**
- * array getStatistics()
- * Returns various statistical information.
- */
- public function getStatistics()
- {
- return $this->_statistics;
- }
-
-
- /**
- * string _performEscape(mixed $s, bool $isIdent=false)
- */
- abstract protected function _performEscape($s, $isIdent=false);
-
- /**
- * object _performNewBlob($id)
- *
- * Returns new blob object.
- */
- abstract protected function _performNewBlob($id=null);
-
- /**
- * list _performGetBlobFieldNames($resultResource)
- * Get list of all BLOB field names in result-set.
- */
- abstract protected function _performGetBlobFieldNames($result);
-
- /**
- * mixed _performTransformQuery(array &$query, string $how)
- *
- * Transform query different way specified by $how.
- * May return some information about performed transform.
- */
- abstract protected function _performTransformQuery(&$queryMain, $how);
-
-
- /**
- * resource _performQuery($arrayQuery)
- * Must return:
- * - For SELECT queries: ID of result-set (PHP resource).
- * - For other queries: query status (scalar).
- * - For error queries: false (and call _setLastError()).
- */
- abstract protected function _performQuery($arrayQuery);
-
- /**
- * mixed _performFetch($resultResource)
- * Fetch ONE NEXT row from result-set.
- * Must return:
- * - For SELECT queries: all the rows of the query (2d arrray).
- * - For INSERT queries: ID of inserted row.
- * - For UPDATE queries: number of updated rows.
- * - For other queries: query status (scalar).
- * - For error queries: false (and call _setLastError()).
- */
- abstract protected function _performFetch($result);
-
- /**
- * mixed _performTransaction($mode)
- * Start new transaction.
- */
- abstract protected function _performTransaction($mode=null);
-
- /**
- * mixed _performCommit()
- * Commit the transaction.
- */
- abstract protected function _performCommit();
-
- /**
- * mixed _performRollback()
- * Rollback the transaction.
- */
- abstract protected function _performRollback();
-
- /**
- * string _performGetPlaceholderIgnoreRe()
- * Return regular expression which matches ignored query parts.
- * This is needed to skip placeholder replacement inside comments, constants etc.
- */
- protected function _performGetPlaceholderIgnoreRe()
- {
- return '';
- }
-
- /**
- * Returns marker for native database placeholder. E.g. in FireBird it is '?',
- * in PostgreSQL - '$1', '$2' etc.
- *
- * @param int $n Number of native placeholder from the beginning of the query (begins from 0!).
- * @return string String representation of native placeholder marker (by default - '?').
- */
- protected function _performGetNativePlaceholderMarker($n)
- {
- return '?';
- }
-
-
- /**
- * array parseDSN(mixed $dsn)
- * Parse a data source name.
- * See parse_url() for details.
- */
- protected function parseDSN($dsn)
- {
- if (is_array($dsn)) return $dsn;
- $parsed = @parse_url($dsn);
- if (!$parsed) return null;
- $params = null;
- if (!empty($parsed['query'])) {
- parse_str($parsed['query'], $params);
- $parsed += $params;
- }
- $parsed['dsn'] = $dsn;
- return $parsed;
- }
-
-
- /**
- * array _query($query, &$total)
- * See _performQuery().
- */
- private function _query($query, &$total)
- {
- $this->_resetLastError();
-
- // Fetch query attributes.
- $this->attributes = $this->_transformQuery($query, 'GET_ATTRIBUTES');
-
- // Modify query if needed for total counting.
- if ($total)
- $this->_transformQuery($query, 'CALC_TOTAL');
-
- $rows = false;
- $cache_it = false;
- // Кешер у нас либо null либо соответствует Zend интерфейсу
- if ( !empty($this->attributes['CACHE']) && ($this->_cacher instanceof Zend_Cache_Backend_Interface) )
- {
-
- $hash = $this->_cachePrefix . md5(serialize($query));
- // Getting data from cache if possible
- $fetchTime = $firstFetchTime = 0;
- $qStart = microtime(true);
- $cacheData = unserialize($this->_cacher->load($hash));
- $queryTime = microtime(true) - $qStart;
-
- $invalCache = isset($cacheData['invalCache']) ? $cacheData['invalCache'] : null;
- $result = isset($cacheData['result']) ? $cacheData['result'] : null;
- $rows = isset($cacheData['rows']) ? $cacheData['rows'] : null;
-
-
- $cache_params = $this->attributes['CACHE'];
-
- // Calculating cache time to live
- $re = '/
- (?>
- ([0-9]+) #1 - hours
- h)? [ \t]*
- (?>
- ([0-9]+) #2 - minutes
- m)? [ \t]*
- (?>
- ([0-9]+) #3 - seconds
- s?)? (,)?
- /sx';
- $m = null;
- preg_match($re, $cache_params, $m);
- $ttl = (isset($m[3])?$m[3]:0)
- + (isset($m[2])?$m[2]:0) * 60
- + (isset($m[1])?$m[1]:0) * 3600;
- // Cutting out time param - now there are just fields for uniqKey or nothing
- $cache_params = trim(preg_replace($re, '', $cache_params, 1));
-
- $uniq_key = null;
-
- // UNIQ_KEY calculation
- if (!empty($cache_params)) {
- $dummy = null;
- // There is no need in query, cos' needle in $this->attributes['CACHE']
- $this->_transformQuery($dummy, 'UNIQ_KEY');
- $uniq_key = call_user_func(array(&$this, 'select'), $dummy);
- $uniq_key = md5(serialize($uniq_key));
- }
- // Check TTL?
- $ok = empty($ttl) || $cacheData;
-
- // Invalidate cache?
- if ($ok && $uniq_key == $invalCache) {
- $this->_logQuery($query);
- $this->_logQueryStat($queryTime, $fetchTime, $firstFetchTime, $rows);
-
- }
- else $cache_it = true;
- }
-
- if (false === $rows || true === $cache_it) {
- $this->_logQuery($query);
-
- // Run the query (counting time).
- $qStart = microtime(true);
- $result = $this->_performQuery($query);
- $fetchTime = $firstFetchTime = 0;
-
- if (is_resource($result) || is_object($result)) {
- $rows = array();
- // Fetch result row by row.
- $fStart = microtime(true);
- $row = $this->_performFetch($result);
- $firstFetchTime = microtime(true) - $fStart;
- if (!empty($row)) {
- $rows[] = $row;
- while ($row=$this->_performFetch($result)) {
- $rows[] = $row;
- }
- }
- $fetchTime = microtime(true) - $fStart;
- } else {
- $rows = $result;
- }
- $queryTime = microtime(true) - $qStart;
-
- // Log query statistics.
- $this->_logQueryStat($queryTime, $fetchTime, $firstFetchTime, $rows);
-
- // Prepare BLOB objects if needed.
- if (is_array($rows) && !empty($this->attributes['BLOB_OBJ'])) {
- $blobFieldNames = $this->_performGetBlobFieldNames($result);
- foreach ($blobFieldNames as $name) {
- for ($r = count($rows)-1; $r>=0; $r--) {
- $rows[$r][$name] =& $this->_performNewBlob($rows[$r][$name]);
- }
- }
- }
-
- // Transform resulting rows.
- $result = $this->_transformResult($rows);
-
- // Storing data in cache
- if ($cache_it && $this->_cacher)
- {
- $this->_cacher->save(
- serialize(array(
- 'invalCache' => $uniq_key,
- 'result' => $result,
- 'rows' => $rows
- )),
- $hash,
- array(),
- $ttl==0?false:$ttl
- );
- }
-
- }
- // Count total number of rows if needed.
- if (is_array($result) && $total) {
- $this->_transformQuery($query, 'GET_TOTAL');
- $total = call_user_func_array(array(&$this, 'selectCell'), $query);
- }
-
- if ($this->_className)
- {
- foreach($result as $k=>$v)
- $result[$k] = new $this->_className($v);
- $this->_className = '';
- }
-
- return $result;
- }
-
-
- /**
- * mixed _transformQuery(array &$query, string $how)
- *
- * Transform query different way specified by $how.
- * May return some information about performed transform.
- */
- private function _transformQuery(&$query, $how)
- {
- // Do overriden transformation.
- $result = $this->_performTransformQuery($query, $how);
- if ($result === true) return $result;
- // Common transformations.
- switch ($how) {
- case 'GET_ATTRIBUTES':
- // Extract query attributes.
- $options = array();
- $q = $query[0];
- $m = null;
- while (preg_match('/^ \s* -- [ \t]+ (\w+): ([^\r\n]+) [\r\n]* /sx', $q, $m)) {
- $options[$m[1]] = trim($m[2]);
- $q = substr($q, strlen($m[0]));
- }
- return $options;
- case 'UNIQ_KEY':
- $q = $this->attributes['CACHE'];
- $query = array();
- while(preg_match('/(\w+)\.\w+/sx', $q, $m)) {
- $query[] = 'SELECT MAX('.$m[0].') AS M, COUNT(*) AS C FROM '.$m[1];
- $q = substr($q, strlen($m[0]));
- }
- $query = " -- UNIQ_KEY\n".
- join("\nUNION\n", $query);
- return true;
- }
- // No such transform.
- $this->_setLastError(-1, "No such transform type: $how", $query);
- }
-
-
- /**
- * void _expandPlaceholders(array &$queryAndArgs, bool $useNative=false)
- * Replace placeholders by quoted values.
- * Modify $queryAndArgs.
- */
- protected function _expandPlaceholders(&$queryAndArgs, $useNative=false)
- {
- $cacheCode = null;
- if ($this->_logger) {
- // Serialize is much faster than placeholder expansion. So use caching.
- $cacheCode = md5(serialize($queryAndArgs) . '|' . $useNative . '|' . $this->_identPrefix);
- if (isset($this->_placeholderCache[$cacheCode])) {
- $queryAndArgs = $this->_placeholderCache[$cacheCode];
- return;
- }
- }
-
- if (!is_array($queryAndArgs)) {
- $queryAndArgs = array($queryAndArgs);
- }
-
- $this->_placeholderNativeArgs = $useNative? array() : null;
- $this->_placeholderArgs = array_reverse($queryAndArgs);
-
- $query = array_pop($this->_placeholderArgs); // array_pop is faster than array_shift
-
- // Do all the work.
- $this->_placeholderNoValueFound = false;
- $query = $this->_expandPlaceholdersFlow($query);
-
- if ($useNative) {
- array_unshift($this->_placeholderNativeArgs, $query);
- $queryAndArgs = $this->_placeholderNativeArgs;
- } else {
- $queryAndArgs = array($query);
- }
-
- if ($cacheCode) {
- $this->_placeholderCache[$cacheCode] = $queryAndArgs;
- }
- }
-
-
- /**
- * Do real placeholder processing.
- * Imply that all interval variables (_placeholder_*) already prepared.
- * May be called recurrent!
- */
- private function _expandPlaceholdersFlow($query)
- {
- $re = '{
- (?>
- # Ignored chunks.
- (?>
- # Comment.
- -- [^\r\n]*
- )
- |
- (?>
- # DB-specifics.
- ' . trim($this->_performGetPlaceholderIgnoreRe()) . '
- )
- )
- |
- (?>
- # Optional blocks
- \{
- # Use "+" here, not "*"! Else nested blocks are not processed well.
- ( (?> (?>(\??)[^{}]+) | (?R) )* ) #1
- \}
- )
- |
- (?>
- # Placeholder
- (\?) ( [_dsafn&|\#]? ) #2 #3
- )
- }sx';
- $query = preg_replace_callback(
- $re,
- array(&$this, '_expandPlaceholdersCallback'),
- $query
- );
- return $query;
- }
-
- static $join = array(
- '|' => array('inner' => ' AND ', 'outer' => ') OR (',),
- '&' => array('inner' => ' OR ', 'outer' => ') AND (',),
- 'a' => array('inner' => ', ', 'outer' => '), (',),
- );
-
- /**
- * string _expandPlaceholdersCallback(list $m)
- * Internal function to replace placeholders (see preg_replace_callback).
- */
- private function _expandPlaceholdersCallback($m)
- {
- // Placeholder.
- if (!empty($m[3])) {
- $type = $m[4];
-
- // Idenifier prefix.
- if ($type == '_') {
- return $this->_identPrefix;
- }
-
- // Value-based placeholder.
- if (!$this->_placeholderArgs) return 'DBSIMPLE_ERROR_NO_VALUE';
- $value = array_pop($this->_placeholderArgs);
-
- // Skip this value?
- if ($value === DBSIMPLE_SKIP) {
- $this->_placeholderNoValueFound = true;
- return '';
- }
-
- // First process guaranteed non-native placeholders.
- switch ($type) {
- case 's':
- if (!($value instanceof DbSimple_SubQuery))
- return 'DBSIMPLE_ERROR_VALUE_NOT_SUBQUERY';
- return $value->get($this->_placeholderNativeArgs);
- case '|':
- case '&':
- case 'a':
- if (!$value) $this->_placeholderNoValueFound = true;
- if (!is_array($value)) return 'DBSIMPLE_ERROR_VALUE_NOT_ARRAY';
- $parts = array();
- $multi = array(); //массив для двойной вложенности
- $mult = $type!='a' || is_int(key($value)) && is_array(current($value));
- foreach ($value as $prefix => $field) {
- //превращаем $value в двумерный нуменованный массив
- if (!is_array($field)) {
- $field = array($prefix => $field);
- $prefix = 0;
- }
- $prefix = is_int($prefix) ? '' :
- $this->escape($this->_addPrefix2Table($prefix), true) . '.';
- //для мультиинсерта очищаем ключи - их быть не может по синтаксису
- if ($mult && $type=='a')
- $field = array_values($field);
- foreach ($field as $k => $v)
- {
- if ($v instanceof DbSimple_SubQuery)
- $v = $v->get($this->_placeholderNativeArgs);
- else
- $v = $v === null? 'NULL' : $this->escape($v);
- if (!is_int($k)) {
- $k = $this->escape($k, true);
- $parts[] = "$prefix$k=$v";
- } else {
- $parts[] = $v;
- }
- }
- if ($mult)
- {
- $multi[] = join(self::$join[$type]['inner'], $parts);
- $parts = array();
- }
- }
- return $mult ? join(self::$join[$type]['outer'], $multi) : join(', ', $parts);
- case '#':
- // Identifier.
- if (!is_array($value))
- {
- if ($value instanceof DbSimple_SubQuery)
- return $value->get($this->_placeholderNativeArgs);
- return $this->escape($this->_addPrefix2Table($value), true);
- }
- $parts = array();
- foreach ($value as $table => $identifiers)
- {
- if (!is_array($identifiers))
- $identifiers = array($identifiers);
- $prefix = '';
- if (!is_int($table))
- $prefix = $this->escape($this->_addPrefix2Table($table), true) . '.';
- foreach ($identifiers as $identifier)
- if ($identifier instanceof DbSimple_SubQuery)
- $parts[] = $identifier->get($this->_placeholderNativeArgs);
- elseif (!is_string($identifier))
- return 'DBSIMPLE_ERROR_ARRAY_VALUE_NOT_STRING';
- else
- $parts[] = $prefix . ($identifier=='*' ? '*' :
- $this->escape($this->_addPrefix2Table($identifier), true));
- }
- return join(', ', $parts);
- case 'n':
- // NULL-based placeholder.
- return empty($value)? 'NULL' : intval($value);
- }
-
- // Native arguments are not processed.
- if ($this->_placeholderNativeArgs !== null) {
- $this->_placeholderNativeArgs[] = $value;
- return $this->_performGetNativePlaceholderMarker(count($this->_placeholderNativeArgs) - 1);
- }
-
- // In non-native mode arguments are quoted.
- if ($value === null) return 'NULL';
- switch ($type) {
- case '':
- if (!is_scalar($value)) return 'DBSIMPLE_ERROR_VALUE_NOT_SCALAR';
- return $this->escape($value);
- case 'd':
- return intval($value);
- case 'f':
- return str_replace(',', '.', floatval($value));
- }
- // By default - escape as string.
- return $this->escape($value);
- }
-
- // Optional block.
- if (isset($m[1]) && strlen($block=$m[1]))
- {
- $prev = $this->_placeholderNoValueFound;
- if ($this->_placeholderNativeArgs !== null)
- $prevPh = $this->_placeholderNativeArgs;
-
- // Проверка на {? } - условный блок
- $skip = false;
- if ($m[2]=='?')
- {
- $skip = array_pop($this->_placeholderArgs) === DBSIMPLE_SKIP;
- $block[0] = ' ';
- }
-
- $block = $this->_expandOptionalBlock($block);
-
- if ($skip)
- $block = '';
-
- if ($this->_placeholderNativeArgs !== null)
- if ($this->_placeholderNoValueFound)
- $this->_placeholderNativeArgs = $prevPh;
- $this->_placeholderNoValueFound = $prev; // recurrent-safe
- return $block;
- }
-
- // Default: skipped part of the string.
- return $m[0];
- }
-
-
- /**
- * Заменяет ?_ на текущий префикс
- *
- * @param string $table имя таблицы
- * @return string имя таблицы
- */
- private function _addPrefix2Table($table)
- {
- if (substr($table, 0, 2) == '?_')
- $table = $this->_identPrefix . substr($table, 2);
- return $table;
- }
-
-
- /**
- * Разбирает опциональный блок - условие |
- *
- * @param string $block блок, который нужно разобрать
- * @return string что получается в результате разбора блока
- */
- private function _expandOptionalBlock($block)
- {
- $alts = array();
- $alt = '';
- $sub=0;
- $exp = explode('|',$block);
- // Оптимизация, так как в большинстве случаев | не используется
- if (count($exp)==1)
- $alts=$exp;
- else
- foreach ($exp as $v)
- {
- // Реализуем автоматный магазин для нахождения нужной скобки
- // На суммарную парность скобок проверять нет необходимости - об этом заботится регулярка
- $sub+=substr_count($v,'{');
- $sub-=substr_count($v,'}');
- if ($sub>0)
- $alt.=$v.'|';
- else
- {
- $alts[]=$alt.$v;
- $alt='';
- }
- }
- $r='';
- foreach ($alts as $block)
- {
- $this->_placeholderNoValueFound = false;
- $block = $this->_expandPlaceholdersFlow($block);
- // Необходимо пройти все блоки, так как если пропустить оставшиесь,
- // то это нарушит порядок подставляемых значений
- if ($this->_placeholderNoValueFound == false && $r=='')
- $r = ' '.$block.' ';
- }
- return $r;
- }
-
-
- /**
- * void _setLastError($code, $msg, $query)
- * Set last database error context.
- * Aditionally expand placeholders.
- */
- protected function _setLastError($code, $msg, $query)
- {
- if (is_array($query)) {
- $this->_expandPlaceholders($query, false);
- $query = $query[0];
- }
- return parent::_setLastError($code, $msg, $query);
- }
-
-
- /**
- * Convert SQL field-list to COUNT(...) clause
- * (e.g. 'DISTINCT a AS aa, b AS bb' -> 'COUNT(DISTINCT a, b)').
- */
- private function _fieldList2Count($fields)
- {
- $m = null;
- if (preg_match('/^\s* DISTINCT \s* (.*)/sx', $fields, $m)) {
- $fields = $m[1];
- $fields = preg_replace('/\s+ AS \s+ .*? (?=,|$)/sx', '', $fields);
- return "COUNT(DISTINCT $fields)";
- } else {
- return 'COUNT(*)';
- }
- }
-
-
- /**
- * array _transformResult(list $rows)
- * Transform resulting rows to various formats.
- */
- private function _transformResult($rows)
- {
- // is not array
- if (!is_array($rows) || !$rows)
- return $rows;
-
- // Find ARRAY_KEY* AND PARENT_KEY fields in field list.
- $pk = null;
- $ak = array();
- foreach (array_keys(current($rows)) as $fieldName)
- if (0 == strncasecmp($fieldName, DBSIMPLE_ARRAY_KEY, strlen(DBSIMPLE_ARRAY_KEY)))
- $ak[] = $fieldName;
- elseif (0 == strncasecmp($fieldName, DBSIMPLE_PARENT_KEY, strlen(DBSIMPLE_PARENT_KEY)))
- $pk = $fieldName;
-
- if (!$ak)
- return $rows;
-
- natsort($ak); // sort ARRAY_KEY* using natural comparision
- // Tree-based array? Fields: ARRAY_KEY, PARENT_KEY
- if ($pk !== null)
- return $this->_transformResultToForest($rows, $ak[0], $pk);
- // Key-based array? Fields: ARRAY_KEY.
- return $this->_transformResultToHash($rows, $ak);
- }
-
-
- /**
- * Converts rowset to key-based array.
- *
- * @param array $rows Two-dimensional array of resulting rows.
- * @param array $ak List of ARRAY_KEY* field names.
- * @return array Transformed array.
- */
- private function _transformResultToHash(array $rows, array $arrayKeys)
- {
- $result = array();
- foreach ($rows as $row) {
- // Iterate over all of ARRAY_KEY* fields and build array dimensions.
- $current =& $result;
- foreach ($arrayKeys as $ak) {
- $key = $row[$ak];
- unset($row[$ak]); // remove ARRAY_KEY* field from result row
- if ($key !== null) {
- $current =& $current[$key];
- } else {
- // IF ARRAY_KEY field === null, use array auto-indices.
- $tmp = array();
- $current[] =& $tmp;
- $current =& $tmp;
- unset($tmp); // we use $tmp, because don't know the value of auto-index
- }
- }
- $current = $row; // save the row in last dimension
- }
- return $result;
- }
-
-
- /**
- * Converts rowset to the forest.
- *
- * @param array $rows Two-dimensional array of resulting rows.
- * @param string $idName Name of ID field.
- * @param string $pidName Name of PARENT_ID field.
- * @return array Transformed array (tree).
- */
- private function _transformResultToForest(array $rows, $idName, $pidName)
- {
- $children = array(); // children of each ID
- $ids = array();
- // Collect who are children of whom.
- foreach ($rows as $i=>$r) {
- $row =& $rows[$i];
- $id = $row[$idName];
- if ($id === null) {
- // Rows without an ID are totally invalid and makes the result tree to
- // be empty (because PARENT_ID = null means "a root of the tree"). So
- // skip them totally.
- continue;
- }
- $pid = $row[$pidName];
- if ($id == $pid) $pid = null;
- $children[$pid][$id] =& $row;
- if (!isset($children[$id])) $children[$id] = array();
- $row['childNodes'] =& $children[$id];
- $ids[$id] = true;
- }
- // Root elements are elements with non-found PIDs.
- $forest = array();
- foreach ($rows as $i=>$r) {
- $row =& $rows[$i];
- $id = $row[$idName];
- $pid = $row[$pidName];
- if ($pid == $id) $pid = null;
- if (!isset($ids[$pid])) {
- $forest[$row[$idName]] =& $row;
- }
- unset($row[$idName]);
- unset($row[$pidName]);
- }
- return $forest;
- }
-
-
- /**
- * Replaces the last array in a multi-dimensional array $V by its first value.
- * Used for selectCol(), when we need to transform (N+1)d resulting array
- * to Nd array (column).
- */
- private function _shrinkLastArrayDimensionCallback(&$v)
- {
- if (!$v) return;
- reset($v);
- if (!is_array($firstCell = current($v))) {
- $v = $firstCell;
- } else {
- array_walk($v, array(&$this, '_shrinkLastArrayDimensionCallback'));
- }
- }
-
-
- /**
- * void _logQuery($query, $noTrace=false)
- * Must be called on each query.
- * If $noTrace is true, library caller is not solved (speed improvement).
- */
- protected function _logQuery($query, $noTrace=false)
- {
- if (!$this->_logger) return;
- $this->_expandPlaceholders($query, false);
- $args = array();
- $args[] =& $this;
- $args[] = $query[0];
- $args[] = $noTrace? null : $this->findLibraryCaller();
- return call_user_func_array($this->_logger, $args);
- }
-
-
- /**
- * void _logQueryStat($queryTime, $fetchTime, $firstFetchTime, $rows)
- * Log information about performed query statistics.
- */
- private function _logQueryStat($queryTime, $fetchTime, $firstFetchTime, $rows)
- {
- // Always increment counters.
- $this->_statistics['time'] += $queryTime;
- $this->_statistics['count']++;
-
- // If no logger, economize CPU resources and actually log nothing.
- if (!$this->_logger) return;
-
- $dt = round($queryTime * 1000);
- $firstFetchTime = round($firstFetchTime*1000);
- $tailFetchTime = round($fetchTime * 1000) - $firstFetchTime;
- $log = " -- ";
- if ($firstFetchTime + $tailFetchTime) {
- $log = sprintf(" -- %d ms = %d+%d".($tailFetchTime? "+%d" : ""), $dt, $dt-$firstFetchTime-$tailFetchTime, $firstFetchTime, $tailFetchTime);
- } else {
- $log = sprintf(" -- %d ms", $dt);
- }
- $log .= "; returned ";
-
- if (!is_array($rows)) {
- $log .= $this->escape($rows);
- } else {
- $detailed = null;
- if (count($rows) == 1) {
- $len = 0;
- $values = array();
- foreach ($rows[0] as $k=>$v) {
- $len += strlen($v);
- if ($len > $this->MAX_LOG_ROW_LEN) {
- break;
- }
- $values[] = $v === null? 'NULL' : $this->escape($v);
- }
- if ($len <= $this->MAX_LOG_ROW_LEN) {
- $detailed = "(" . preg_replace("/\r?\n/", "\\n", join(', ', $values)) . ")";
- }
- }
- if ($detailed) {
- $log .= $detailed;
- } else {
- $log .= count($rows). " row(s)";
- }
- }
-
- $this->_logQuery($log, true);
- }
-
-
- // Identifiers prefix (used for ?_ placeholder).
- private $_identPrefix = '';
-
- // Queries statistics.
- private $_statistics = array(
- 'time' => 0,
- 'count' => 0,
- );
-
- private $_cachePrefix = '';
- private $_className = '';
-
- private $_logger = null;
- private $_cacher = null;
- private $_placeholderArgs, $_placeholderNativeArgs, $_placeholderCache=array();
- private $_placeholderNoValueFound;
-
- /**
- * When string representation of row (in characters) is greater than this,
- * row data will not be logged.
- */
- private $MAX_LOG_ROW_LEN = 128;
-}
-
-
-/**
- * Database BLOB.
- * Can read blob chunk by chunk, write data to BLOB.
- */
-interface DbSimple_Blob
-{
- /**
- * string read(int $length)
- * Returns following $length bytes from the blob.
- */
- public function read($len);
-
- /**
- * string write($data)
- * Appends data to blob.
- */
- public function write($data);
-
- /**
- * int length()
- * Returns length of the blob.
- */
- public function length();
-
- /**
- * blobid close()
- * Closes the blob. Return its ID. No other way to obtain this ID!
- */
- public function close();
-}
-
-
-/**
- * Класс для хранения подзапроса - результата выполнения функции
- * DbSimple_Generic_Database::subquery
- *
- */
-class DbSimple_SubQuery
-{
- private $query=array();
-
- public function __construct(array $q)
- {
- $this->query = $q;
- }
-
- /**
- * Возвращает сам запрос и добавляет плейсхолдеры в массив переданный по ссылке
- *
- * @param &array|null - ссылка на массив плейсхолдеров
- * @return string
- */
- public function get(&$ph)
- {
- if ($ph !== null)
- $ph = array_merge($ph, array_slice($this->query,1,null,true));
- return $this->query[0];
- }
-}
-
-
-/**
- * Support for error tracking.
- * Can hold error messages, error queries and build proper stacktraces.
- */
-abstract class DbSimple_LastError
-{
- public $error = null;
- public $errmsg = null;
- private $errorHandler = null;
- private $ignoresInTraceRe = 'DbSimple_.*::.* | call_user_func.*';
-
- /**
- * abstract void _logQuery($query)
- * Must be overriden in derived class.
- */
- abstract protected function _logQuery($query);
-
- /**
- * void _resetLastError()
- * Reset the last error. Must be called on correct queries.
- */
- protected function _resetLastError()
- {
- $this->error = $this->errmsg = null;
- }
-
- /**
- * void _setLastError(int $code, string $message, string $query)
- * Fill $this->error property with error information. Error context
- * (code initiated the query outside DbSimple) is assigned automatically.
- */
- protected function _setLastError($code, $msg, $query)
- {
- $context = "unknown";
- if ($t = $this->findLibraryCaller()) {
- $context = (isset($t['file'])? $t['file'] : '?') . ' line ' . (isset($t['line'])? $t['line'] : '?');
- }
- $this->error = array(
- 'code' => $code,
- 'message' => rtrim($msg),
- 'query' => $query,
- 'context' => $context,
- );
- $this->errmsg = rtrim($msg) . ($context? " at $context" : "");
-
- $this->_logQuery(" -- error #".$code.": ".preg_replace('/(\r?\n)+/s', ' ', $this->errmsg));
-
- if (is_callable($this->errorHandler)) {
- call_user_func($this->errorHandler, $this->errmsg, $this->error);
- }
-
- return false;
- }
-
-
- /**
- * callback setErrorHandler(callback $handler)
- * Set new error handler called on database errors.
- * Handler gets 3 arguments:
- * - error message
- * - full error context information (last query etc.)
- */
- public function setErrorHandler($handler)
- {
- $prev = $this->errorHandler;
- $this->errorHandler = $handler;
- // In case of setting first error handler for already existed
- // error - call the handler now (usual after connect()).
- if (!$prev && $this->error && $this->errorHandler) {
- call_user_func($this->errorHandler, $this->errmsg, $this->error);
- }
- return $prev;
- }
-
- /**
- * void addIgnoreInTrace($reName)
- * Add regular expression matching ClassName::functionName or functionName.
- * Matched stack frames will be ignored in stack traces passed to query logger.
- */
- public function addIgnoreInTrace($name)
- {
- $this->ignoresInTraceRe .= "|" . $name;
- }
-
- /**
- * array of array findLibraryCaller()
- * Return part of stacktrace before calling first library method.
- * Used in debug purposes (query logging etc.).
- */
- public function findLibraryCaller()
- {
- $caller = call_user_func(
- array(&$this, 'debug_backtrace_smart'),
- $this->ignoresInTraceRe,
- true
- );
- return $caller;
- }
-
- /**
- * array debug_backtrace_smart($ignoresRe=null, $returnCaller=false)
- *
- * Return stacktrace. Correctly work with call_user_func*
- * (totally skip them correcting caller references).
- * If $returnCaller is true, return only first matched caller,
- * not all stacktrace.
- *
- * @version 2.03
- */
- private function debug_backtrace_smart($ignoresRe=null, $returnCaller=false)
- {
- $trace = debug_backtrace();
-
- if ($ignoresRe !== null)
- $ignoresRe = "/^(?>{$ignoresRe})$/six";
- $smart = array();
- $framesSeen = 0;
- for ($i=0, $n=count($trace); $i<$n; $i++) {
- $t = $trace[$i];
- if (!$t) continue;
-
- // Next frame.
- $next = isset($trace[$i+1])? $trace[$i+1] : null;
-
- // Dummy frame before call_user_func* frames.
- if (!isset($t['file'])) {
- $t['over_function'] = $trace[$i+1]['function'];
- $t = $t + $trace[$i+1];
- $trace[$i+1] = null; // skip call_user_func on next iteration
- $next = isset($trace[$i+2])? $trace[$i+2] : null; // Correct Next frame.
- }
-
- // Skip myself frame.
- if (++$framesSeen < 2) continue;
-
- // 'class' and 'function' field of next frame define where
- // this frame function situated. Skip frames for functions
- // situated in ignored places.
- if ($ignoresRe && $next) {
- // Name of function "inside which" frame was generated.
- $frameCaller = (isset($next['class'])? $next['class'].'::' : '') . (isset($next['function'])? $next['function'] : '');
- if (preg_match($ignoresRe, $frameCaller)) continue;
- }
-
- // On each iteration we consider ability to add PREVIOUS frame
- // to $smart stack.
- if ($returnCaller) return $t;
- $smart[] = $t;
- }
- return $smart;
- }
-
-}
diff --git a/includes/libs/DbSimple/Generic.php b/includes/libs/DbSimple/Generic.php
deleted file mode 100644
index 5dc2f144..00000000
--- a/includes/libs/DbSimple/Generic.php
+++ /dev/null
@@ -1,193 +0,0 @@
-.
- *
- * Contains 3 classes:
- * - DbSimple_Generic: database factory class
- * - DbSimple_Generic_Database: common database methods
- * - DbSimple_Generic_Blob: common BLOB support
- * - DbSimple_Generic_LastError: error reporting and tracking
- *
- * Special result-set fields:
- * - ARRAY_KEY* ("*" means "anything")
- * - PARENT_KEY
- *
- * Transforms:
- * - GET_ATTRIBUTES
- * - CALC_TOTAL
- * - GET_TOTAL
- * - UNIQ_KEY
- *
- * Query attributes:
- * - BLOB_OBJ
- * - CACHE
- *
- * @author Dmitry Koterov, http://forum.dklab.ru/users/DmitryKoterov/
- * @author Konstantin Zhinko, http://forum.dklab.ru/users/KonstantinGinkoTit/
- *
- * @version 2.x $Id$
- */
-
-/**
- * Use this constant as placeholder value to skip optional SQL block [...].
- */
-if (!defined('DBSIMPLE_SKIP'))
- define('DBSIMPLE_SKIP', log(0));
-
-/**
- * Names of special columns in result-set which is used
- * as array key (or karent key in forest-based resultsets) in
- * resulting hash.
- */
-if (!defined('DBSIMPLE_ARRAY_KEY'))
- define('DBSIMPLE_ARRAY_KEY', 'ARRAY_KEY'); // hash-based resultset support
-if (!defined('DBSIMPLE_PARENT_KEY'))
- define('DBSIMPLE_PARENT_KEY', 'PARENT_KEY'); // forrest-based resultset support
-
-
-/**
- * DbSimple factory.
- */
-class DbSimple_Generic
-{
- /**
- * DbSimple_Generic connect(mixed $dsn)
- *
- * Universal static function to connect ANY database using DSN syntax.
- * Choose database driver according to DSN. Return new instance
- * of this driver.
- *
- * You can connect to MySQL by socket using this new syntax (like PDO DSN):
- * $dsn = 'mysqli:unix_socket=/cloudsql/app:instance;user=root;pass=;dbname=testdb';
- * $dsn = 'mypdo:unix_socket=/cloudsql/app:instance;charset=utf8;user=testuser;pass=mypassword;dbname=testdb';
- *
- * Connection by host also can be made with this syntax.
- * Or you can use old syntax:
- * $dsn = 'mysql://testuser:mypassword@127.0.0.1/testdb';
- *
- */
- public static function connect($dsn)
- {
- // Load database driver and create its instance.
- $parsed = DbSimple_Generic::parseDSN($dsn);
- if (!$parsed) {
- $dummy = null;
- return $dummy;
- }
- $class = 'DbSimple_'.ucfirst($parsed['scheme']);
- if (!class_exists($class)) {
- $file = __DIR__.'/'.ucfirst($parsed['scheme']). ".php";
- if (is_file($file)) {
- require_once($file);
- } else {
- trigger_error("Error loading database driver: no file $file", E_USER_ERROR);
- return null;
- }
- }
- $object = new $class($parsed);
- if (isset($parsed['ident_prefix'])) {
- $object->setIdentPrefix($parsed['ident_prefix']);
- }
- $object->setCachePrefix(md5(serialize($parsed['dsn'])));
- return $object;
- }
-
-
- /**
- * array parseDSN(mixed $dsn)
- * Parse a data source name.
- * See parse_url() for details.
- */
- public static function parseDSN($dsn)
- {
- if (is_array($dsn)) return $dsn;
- $parsed = parse_url($dsn);
- if (!$parsed) return null;
-
- $params = null;
- if (!empty($parsed['query'])) {
- parse_str($parsed['query'], $params);
- $parsed += $params;
- }
-
- if ( empty($parsed['host']) && empty($parsed['socket']) ) {
- // Parse as DBO DSN string
- $parsedPdo = self::parseDsnPdo($parsed['path']);
- unset($parsed['path']);
- $parsed = array_merge($parsed, $parsedPdo);
- }
-
- $parsed['dsn'] = $dsn;
- return $parsed;
- } // parseDSN
-
-
- /**
- * Parse string as DBO DSN string.
- *
- * @param $str
- * @return array
- */
- public static function parseDsnPdo($str) {
-
- if (substr($str, 0, strlen('mysql:')) == 'mysql:') {
- $str = substr($str, strlen('mysql:'));
- }
-
- $arr = explode(';', $str);
-
- $result = array();
- foreach ($arr as $k=>$v) {
- $v = explode('=', $v);
- if (count($v) == 2)
- $result[ $v[0] ] = $v[1];
- }
-
- if ( isset($result['unix_socket']) ) {
- $result['socket'] = $result['unix_socket'];
- unset($result['unix_socket']);
- }
-
- if ( isset($result['dbname']) ) {
- $result['path'] = $result['dbname'];
- unset($result['dbname']);
- }
-
- if ( isset($result['charset']) ) {
- $result['enc'] = $result['charset'];
- unset($result['charset']);
- }
-
- return $result;
- } // parseDsnPdo
-
-} // DbSimple_Generic class
diff --git a/includes/libs/DbSimple/Mysqli.php b/includes/libs/DbSimple/Mysqli.php
deleted file mode 100644
index 6b67dd14..00000000
--- a/includes/libs/DbSimple/Mysqli.php
+++ /dev/null
@@ -1,231 +0,0 @@
-_setLastError("-1", "MySQLi extension is not loaded", "mysqli_connect");
-
- if (!empty($dsn["persist"])) {
- if (version_compare(PHP_VERSION, '5.3') < 0) {
- return $this->_setLastError("-1", "Persistent connections in MySQLi is allowable since PHP 5.3", "mysqli_connect");
- } else {
- $dsn["host"] = "p:".$dsn["host"];
- }
- }
-
- if ( isset($dsn['socket']) ) {
- // Socket connection
- $this->link = mysqli_connect(
- null // host
- ,empty($dsn['user']) ? 'root' : $dsn['user'] // user
- ,empty($dsn['pass']) ? '' : $dsn['pass'] // password
- ,preg_replace('{^/}s', '', $dsn['path']) // schema
- ,null // port
- ,$dsn['socket'] // socket
- );
- } else if (isset($dsn['host']) ) {
- // Host connection
- $this->link = mysqli_connect(
- $dsn['host']
- ,empty($dsn['user']) ? 'root' : $dsn['user']
- ,empty($dsn['pass']) ? '' : $dsn['pass']
- ,preg_replace('{^/}s', '', $dsn['path'])
- ,empty($dsn['port']) ? null : $dsn['port']
- );
- } else {
- return $this->_setDbError('mysqli_connect()');
- }
- $this->_resetLastError();
- if (!$this->link) return $this->_setDbError('mysqli_connect()');
-
- mysqli_set_charset($this->link, isset($dsn['enc']) ? $dsn['enc'] : 'UTF8');
- }
-
-
- protected function _performEscape($s, $isIdent=false)
- {
- if (!$isIdent)
- return "'" . mysqli_real_escape_string($this->link, $s) . "'";
- else
- return "`" . str_replace('`', '``', $s) . "`";
- }
-
-
- protected function _performNewBlob($blobid=null)
- {
- return new DbSimple_Mysqli_Blob($this, $blobid);
- }
-
-
- protected function _performGetBlobFieldNames($result)
- {
- $allFields = mysqli_fetch_fields($result);
- $blobFields = array();
-
- if (!empty($allFields))
- {
- foreach ($allFields as $field)
- if (stripos($field["type"], "BLOB") !== false)
- $blobFields[] = $field["name"];
- }
- return $blobFields;
- }
-
-
- protected function _performGetPlaceholderIgnoreRe()
- {
- return '
- " (?> [^"\\\\]+|\\\\"|\\\\)* " |
- \' (?> [^\'\\\\]+|\\\\\'|\\\\)* \' |
- ` (?> [^`]+ | ``)* ` | # backticks
- /\* .*? \*/ # comments
- ';
- }
-
-
- protected function _performTransaction($parameters=null)
- {
- return mysqli_begin_transaction($this->link);
- }
-
-
- protected function _performCommit()
- {
- return mysqli_commit($this->link);
- }
-
-
- protected function _performRollback()
- {
- return mysqli_rollback($this->link);
- }
-
-
- protected function _performTransformQuery(&$queryMain, $how)
- {
- // If we also need to calculate total number of found rows...
- switch ($how)
- {
- // Prepare total calculation (if possible)
- case 'CALC_TOTAL':
- $m = null;
- if (preg_match('/^(\s* SELECT)(.*)/six', $queryMain[0], $m))
- $queryMain[0] = $m[1] . ' SQL_CALC_FOUND_ROWS' . $m[2];
- return true;
-
- // Perform total calculation.
- case 'GET_TOTAL':
- // Built-in calculation available?
- $queryMain = array('SELECT FOUND_ROWS()');
- return true;
- }
-
- return false;
- }
-
-
- protected function _performQuery($queryMain)
- {
- $this->_lastQuery = $queryMain;
- $this->_expandPlaceholders($queryMain, false);
- $result = mysqli_query($this->link, $queryMain[0]);
- if ($result === false)
- return $this->_setDbError($queryMain[0]);
- if (!is_object($result)) {
- if (preg_match('/^\s* INSERT \s+/six', $queryMain[0]))
- {
- // INSERT queries return generated ID.
- return mysqli_insert_id($this->link);
- }
- // Non-SELECT queries return number of affected rows, SELECT - resource.
- return mysqli_affected_rows($this->link);
- }
- return $result;
- }
-
-
- protected function _performFetch($result)
- {
- $row = mysqli_fetch_assoc($result);
- if (mysqli_error($this->link)) return $this->_setDbError($this->_lastQuery);
- if ($row === false) return null;
- return $row;
- }
-
-
- protected function _setDbError($query)
- {
- if ($this->link) {
- return $this->_setLastError(mysqli_errno($this->link), mysqli_error($this->link), $query);
- } else {
- return $this->_setLastError(mysqli_connect_errno(), mysqli_connect_error(), $query);
- }
- }
-}
-
-
-class DbSimple_Mysqli_Blob implements DbSimple_Blob
-{
- // MySQL does not support separate BLOB fetching.
- private $blobdata = null;
- private $curSeek = 0;
-
- public function __construct(&$database, $blobdata=null)
- {
- $this->blobdata = $blobdata;
- $this->curSeek = 0;
- }
-
- public function read($len)
- {
- $p = $this->curSeek;
- $this->curSeek = min($this->curSeek + $len, strlen($this->blobdata));
- return substr($this->blobdata, $p, $len);
- }
-
- public function write($data)
- {
- $this->blobdata .= $data;
- }
-
- public function close()
- {
- return $this->blobdata;
- }
-
- public function length()
- {
- return strlen($this->blobdata);
- }
-}
diff --git a/includes/libs/DbSimple/Zend/Cache.php b/includes/libs/DbSimple/Zend/Cache.php
deleted file mode 100644
index aff2e653..00000000
--- a/includes/libs/DbSimple/Zend/Cache.php
+++ /dev/null
@@ -1,250 +0,0 @@
-setBackend($backendObject);
- return $frontendObject;
- }
-
- /**
- * Backend Constructor
- *
- * @param string $backend
- * @param array $backendOptions
- * @param boolean $customBackendNaming
- * @param boolean $autoload
- * @return Zend_Cache_Backend
- */
- public static function _makeBackend($backend, $backendOptions, $customBackendNaming = false, $autoload = false)
- {
- if (!$customBackendNaming) {
- $backend = self::_normalizeName($backend);
- }
- if (in_array($backend, Zend_Cache::$standardBackends)) {
- // we use a standard backend
- $backendClass = 'Zend_Cache_Backend_' . $backend;
- // security controls are explicit
- require_once str_replace('_', DIRECTORY_SEPARATOR, $backendClass) . '.php';
- } else {
- // we use a custom backend
- if (!preg_match('~^[\w\\\\]+$~D', $backend)) {
- Zend_Cache::throwException("Invalid backend name [$backend]");
- }
- if (!$customBackendNaming) {
- // we use this boolean to avoid an API break
- $backendClass = 'Zend_Cache_Backend_' . $backend;
- } else {
- $backendClass = $backend;
- }
- if (!$autoload) {
- $file = str_replace('_', DIRECTORY_SEPARATOR, $backendClass) . '.php';
- if (!(self::_isReadable($file))) {
- self::throwException("file $file not found in include_path");
- }
- require_once $file;
- }
- }
- return new $backendClass($backendOptions);
- }
-
- /**
- * Frontend Constructor
- *
- * @param string $frontend
- * @param array $frontendOptions
- * @param boolean $customFrontendNaming
- * @param boolean $autoload
- * @return Zend_Cache_Core|Zend_Cache_Frontend
- */
- public static function _makeFrontend($frontend, $frontendOptions = array(), $customFrontendNaming = false, $autoload = false)
- {
- if (!$customFrontendNaming) {
- $frontend = self::_normalizeName($frontend);
- }
- if (in_array($frontend, self::$standardFrontends)) {
- // we use a standard frontend
- // For perfs reasons, with frontend == 'Core', we can interact with the Core itself
- $frontendClass = 'Zend_Cache_' . ($frontend != 'Core' ? 'Frontend_' : '') . $frontend;
- // security controls are explicit
- require_once str_replace('_', DIRECTORY_SEPARATOR, $frontendClass) . '.php';
- } else {
- // we use a custom frontend
- if (!preg_match('~^[\w\\\\]+$~D', $frontend)) {
- Zend_Cache::throwException("Invalid frontend name [$frontend]");
- }
- if (!$customFrontendNaming) {
- // we use this boolean to avoid an API break
- $frontendClass = 'Zend_Cache_Frontend_' . $frontend;
- } else {
- $frontendClass = $frontend;
- }
- if (!$autoload) {
- $file = str_replace('_', DIRECTORY_SEPARATOR, $frontendClass) . '.php';
- if (!(self::_isReadable($file))) {
- self::throwException("file $file not found in include_path");
- }
- require_once $file;
- }
- }
- return new $frontendClass($frontendOptions);
- }
-
- /**
- * Throw an exception
- *
- * Note : for perf reasons, the "load" of Zend/Cache/Exception is dynamic
- * @param string $msg Message for the exception
- * @throws Zend_Cache_Exception
- */
- public static function throwException($msg, Exception $e = null)
- {
- // For perfs reasons, we use this dynamic inclusion
- require_once 'Zend/Cache/Exception.php';
- throw new Zend_Cache_Exception($msg, 0, $e);
- }
-
- /**
- * Normalize frontend and backend names to allow multiple words TitleCased
- *
- * @param string $name Name to normalize
- * @return string
- */
- protected static function _normalizeName($name)
- {
- $name = ucfirst(strtolower($name));
- $name = str_replace(array('-', '_', '.'), ' ', $name);
- $name = ucwords($name);
- $name = str_replace(' ', '', $name);
- if (stripos($name, 'ZendServer') === 0) {
- $name = 'ZendServer_' . substr($name, strlen('ZendServer'));
- }
-
- return $name;
- }
-
- /**
- * Returns TRUE if the $filename is readable, or FALSE otherwise.
- * This function uses the PHP include_path, where PHP's is_readable()
- * does not.
- *
- * Note : this method comes from Zend_Loader (see #ZF-2891 for details)
- *
- * @param string $filename
- * @return boolean
- */
- private static function _isReadable($filename)
- {
- if (!$fh = @fopen($filename, 'r', true)) {
- return false;
- }
- @fclose($fh);
- return true;
- }
-
-}
diff --git a/includes/libs/DbSimple/Zend/Cache/Backend/Interface.php b/includes/libs/DbSimple/Zend/Cache/Backend/Interface.php
deleted file mode 100644
index 3f44e2e1..00000000
--- a/includes/libs/DbSimple/Zend/Cache/Backend/Interface.php
+++ /dev/null
@@ -1,99 +0,0 @@
- infinite lifetime)
- * @return boolean true if no problem
- */
- public function save($data, $id, $tags = array(), $specificLifetime = false);
-
- /**
- * Remove a cache record
- *
- * @param string $id Cache id
- * @return boolean True if no problem
- */
- public function remove($id);
-
- /**
- * Clean some cache records
- *
- * Available modes are :
- * Zend_Cache::CLEANING_MODE_ALL (default) => remove all cache entries ($tags is not used)
- * Zend_Cache::CLEANING_MODE_OLD => remove too old cache entries ($tags is not used)
- * Zend_Cache::CLEANING_MODE_MATCHING_TAG => remove cache entries matching all given tags
- * ($tags can be an array of strings or a single string)
- * Zend_Cache::CLEANING_MODE_NOT_MATCHING_TAG => remove cache entries not {matching one of the given tags}
- * ($tags can be an array of strings or a single string)
- * Zend_Cache::CLEANING_MODE_MATCHING_ANY_TAG => remove cache entries matching any given tags
- * ($tags can be an array of strings or a single string)
- *
- * @param string $mode Clean mode
- * @param array $tags Array of tags
- * @return boolean true if no problem
- */
- public function clean($mode = Zend_Cache::CLEANING_MODE_ALL, $tags = array());
-
-}
diff --git a/includes/libs/qqFileUploader.class.php b/includes/libs/qqFileUploader.class.php
new file mode 100644
index 00000000..dae92a58
--- /dev/null
+++ b/includes/libs/qqFileUploader.class.php
@@ -0,0 +1,187 @@
+handleUpload('uploads/');
+
+// to pass data through iframe you will need to encode all html tags
+echo htmlspecialchars(json_encode($result), ENT_NOQUOTES);
+
+/******************************************/
+
+
+
+/**
+ * Handle file uploads via XMLHttpRequest
+ */
+class qqUploadedFileXhr
+{
+ /**
+ * Save the file to the specified path
+ * @return boolean TRUE on success
+ */
+ function save(string $path) : bool
+ {
+ $input = fopen("php://input", "r");
+ $temp = tmpfile();
+ $realSize = stream_copy_to_stream($input, $temp);
+ fclose($input);
+
+ if ($realSize != $this->getSize())
+ return false;
+
+ $target = fopen($path, "w");
+ fseek($temp, 0, SEEK_SET);
+ stream_copy_to_stream($temp, $target);
+ fclose($target);
+
+ return true;
+ }
+
+ function getName() : string
+ {
+ return $_GET['qqfile'];
+ }
+
+ function getSize(): int
+ {
+ if (isset($_SERVER["CONTENT_LENGTH"]))
+ return (int)$_SERVER["CONTENT_LENGTH"];
+
+ throw new Exception('Getting content length is not supported.');
+ return 0;
+ }
+}
+
+/**
+ * Handle file uploads via regular form post (uses the $_FILES array)
+ */
+class qqUploadedFileForm
+{
+ /**
+ * Save the file to the specified path
+ * @return boolean TRUE on success
+ */
+ function save(string $path) : bool
+ {
+ if(!move_uploaded_file($_FILES['qqfile']['tmp_name'], $path))
+ return false;
+
+ return true;
+ }
+
+ function getName() : string
+ {
+ return $_FILES['qqfile']['name'];
+ }
+
+ function getSize() : int
+ {
+ return $_FILES['qqfile']['size'];
+ }
+}
+
+class qqFileUploader
+{
+ private $allowedExtensions = array();
+ private $sizeLimit = 10485760;
+ private $file;
+
+ public function __construct(array $allowedExtensions = array(), $sizeLimit = 10485760)
+ {
+ $this->allowedExtensions = array_map("strtolower", $allowedExtensions);
+ $this->sizeLimit = $sizeLimit;
+
+ $this->checkServerSettings();
+
+ if (isset($_GET['qqfile']))
+ $this->file = new qqUploadedFileXhr();
+ else if (isset($_FILES['qqfile']))
+ $this->file = new qqUploadedFileForm();
+ else
+ $this->file = null;
+ }
+
+ public function getName() : string
+ {
+ return $this->file?->getName() ?? '';
+ }
+
+ private function checkServerSettings() : void
+ {
+ $postSize = $this->toBytes(ini_get('post_max_size'));
+ $uploadSize = $this->toBytes(ini_get('upload_max_filesize'));
+
+ if ($postSize < $this->sizeLimit || $uploadSize < $this->sizeLimit)
+ {
+ $size = max(1, $this->sizeLimit / 1024 / 1024) . 'M';
+ die("{'error':'increase post_max_size and upload_max_filesize to $size'}");
+ }
+ }
+
+ private function toBytes(string $str) : int
+ {
+ $val = substr(trim($str), 0, -1);
+ $last = strtolower(substr($str, -1, 1));
+ switch ($last)
+ {
+ case 'g': $val *= 1024;
+ case 'm': $val *= 1024;
+ case 'k': $val *= 1024;
+ }
+
+ return $val;
+ }
+
+ /**
+ * Returns array('success' => true, 'newFilename' => 'myDoc123.doc') or array('error' => 'error message')
+ */
+ function handleUpload(string $uploadDirectory, string $newName = '', bool $replaceOldFile = FALSE) : array
+ {
+ if (!is_writable($uploadDirectory))
+ return ['error' => "Server error. Upload directory isn't writable."];
+
+ if (!$this->file)
+ return ['error' => 'No files were uploaded.'];
+
+ $size = $this->file->getSize();
+
+ if ($size == 0)
+ return ['error' => 'File is empty'];
+
+ if ($size > $this->sizeLimit)
+ return ['error' => 'File is too large'];
+
+ $pathinfo = pathinfo($this->getName());
+ $filename = $newName ?: $pathinfo['filename'];
+ //$filename = md5(uniqid());
+ $ext = @$pathinfo['extension']; // hide notices if extension is empty
+
+ if ($this->allowedExtensions && !in_array(strtolower($ext), $this->allowedExtensions))
+ {
+ $these = implode(', ', $this->allowedExtensions);
+ return ['error' => 'File has an invalid extension, it should be one of '. $these . '.'];
+ }
+
+ // don't overwrite previous files that were uploaded
+ if (!$replaceOldFile)
+ while (file_exists($uploadDirectory . $filename . '.' . $ext))
+ $filename .= rand(10, 99);
+
+ if ($this->file->save($uploadDirectory . $filename . '.' . $ext))
+ return ['success' => true, 'newFilename' => $filename . '.' . $ext];
+ else
+ return ['error' => 'Could not save uploaded file. The upload was cancelled, or server error encountered'];
+ }
+}
diff --git a/includes/locale.class.php b/includes/locale.class.php
new file mode 100644
index 00000000..a8d8923e
--- /dev/null
+++ b/includes/locale.class.php
@@ -0,0 +1,179 @@
+ 'en',
+ self::KR => 'ko',
+ self::FR => 'fr',
+ self::DE => 'de',
+ self::CN => 'cn',
+ self::TW => 'tw',
+ self::ES => 'es',
+ self::MX => 'mx',
+ self::RU => 'ru',
+ self::JP => 'jp',
+ self::PT => 'pt',
+ self::IT => 'it'
+ };
+ }
+
+ public function json() : string // internal usage / json string
+ {
+ return match ($this)
+ {
+ self::EN => 'enus',
+ self::KR => 'kokr',
+ self::FR => 'frfr',
+ self::DE => 'dede',
+ self::CN => 'zhcn',
+ self::TW => 'zhtw',
+ self::ES => 'eses',
+ self::MX => 'esmx',
+ self::RU => 'ruru',
+ self::JP => 'jajp',
+ self::PT => 'ptpt',
+ self::IT => 'itit'
+ };
+ }
+
+ public function title() : string // localized language name
+ {
+ return match ($this)
+ {
+ self::EN => 'English',
+ self::KR => '한국어',
+ self::FR => 'Français',
+ self::DE => 'Deutsch',
+ self::CN => '简体中文',
+ self::TW => '繁體中文',
+ self::ES => 'Español',
+ self::MX => 'Mexicano',
+ self::RU => 'Русский',
+ self::JP => '日本語',
+ self::PT => 'Português',
+ self::IT => 'Italiano'
+ };
+ }
+
+ public function gameDirs() : array // setup data source / wow client locale code
+ {
+ return match ($this)
+ {
+ self::EN => ['enGB', 'enUS', ''],
+ self::KR => ['koKR'],
+ self::FR => ['frFR'],
+ self::DE => ['deDE'],
+ self::CN => ['zhCN', 'enCN'],
+ self::TW => ['zhTW', 'enTW'],
+ self::ES => ['esES'],
+ self::MX => ['esMX'],
+ self::RU => ['ruRU'],
+ self::JP => ['jaJP'],
+ self::PT => ['ptPT', 'ptBR'],
+ self::IT => ['itIT']
+ };
+ }
+
+ public function httpCode() : array // HTTP_ACCEPT_LANGUAGE
+ {
+ return match ($this)
+ {
+ self::EN => ['en', 'en-au', 'en-bz', 'en-ca', 'en-ie', 'en-jm', 'en-nz', 'en-ph', 'en-za', 'en-tt', 'en-gb', 'en-us', 'en-zw'],
+ self::KR => ['ko', 'ko-kp', 'ko-kr'],
+ self::FR => ['fr', 'fr-be', 'fr-ca', 'fr-fr', 'fr-lu', 'fr-mc', 'fr-ch'],
+ self::DE => ['de', 'de-at', 'de-de', 'de-li', 'de-lu', 'de-ch'],
+ self::CN => ['zh', 'zh-hk', 'zh-cn', 'zh-sg'],
+ self::TW => ['tw', 'zh-tw'],
+ self::ES => ['es', 'es-ar', 'es-bo', 'es-cl', 'es-co', 'es-cr', 'es-do', 'es-ec', 'es-sv', 'es-gt', 'es-hn', 'es-ni', 'es-pa', 'es-py', 'es-pe', 'es-pr', 'es-es', 'es-uy', 'es-ve'],
+ self::MX => ['mx', 'es-mx'],
+ self::RU => ['ru', 'ru-mo'],
+ self::JP => ['ja'],
+ self::PT => ['pt', 'pt-br'],
+ self::IT => ['it', 'it-ch']
+ };
+ }
+
+ public function isLogographic() : bool
+ {
+ return $this == Locale::CN || $this == Locale::TW || $this == Locale::KR;
+ }
+
+ public function validate() : ?self
+ {
+ return ($this->maskBit() & self::MASK_ALL & (Cfg::get('LOCALES') ?: 0xFFFF)) ? $this : null;
+ }
+
+ public function maskBit() : int
+ {
+ return (1 << $this->value);
+ }
+
+ public static function tryFromDomain(string $str) : ?self
+ {
+ foreach (self::cases() as $l)
+ if ($l->validate() && $str == $l->domain())
+ return $l;
+
+ return null;
+ }
+
+ public static function tryFromHttpAcceptLanguage(string $httpAccept) : ?self
+ {
+ if (!$httpAccept)
+ return null;
+
+ $available = [];
+
+ // e.g.: de,de-DE;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6
+ foreach (explode(',', $_SERVER['HTTP_ACCEPT_LANGUAGE']) as $loc)
+ if (preg_match('/([a-z\-]+)(?:\s*;\s*q\s*=\s*([\.\d]+))?/ui', $loc, $m, PREG_UNMATCHED_AS_NULL))
+ $available[Util::lower($m[1])] = floatVal($m[2] ?? 1); // no quality set: assume 100%
+
+ arsort($available, SORT_NUMERIC); // highest quality on top
+
+ foreach ($available as $code => $_)
+ foreach (self::cases() as $l)
+ if ($l->validate() && in_array($code, $l->httpCode()))
+ return $l;
+
+ return null;
+ }
+
+ public static function getFallback() : self
+ {
+ foreach (Locale::cases() as $l)
+ if ($l->validate())
+ return $l;
+
+ // wow, you really fucked up your config mate!
+ trigger_error('Locale::getFallback - there are no valid locales', E_USER_ERROR);
+ return self::EN;
+ }
+}
+
+?>
diff --git a/includes/loot.class.php b/includes/loot.class.php
deleted file mode 100644
index bfa3be85..00000000
--- a/includes/loot.class.php
+++ /dev/null
@@ -1,636 +0,0 @@
-results);
-
- while (list($k, $__) = each($this->results))
- yield $k => $this->results[$k];
- }
-
- public function getResult()
- {
- return $this->results;
- }
-
- private function createStack($l) // issue: TC always has an equal distribution between min/max
- {
- if (empty($l['min']) || empty($l['max']) || $l['max'] <= $l['min'])
- return null;
-
- $stack = [];
- for ($i = $l['min']; $i <= $l['max']; $i++)
- $stack[$i] = round(100 / (1 + $l['max'] - $l['min']), 3);
-
- // yes, it wants a string .. how weired is that..
- return json_encode($stack, JSON_NUMERIC_CHECK); // do not replace with Util::toJSON !
- }
-
- private function storeJSGlobals($data)
- {
- foreach ($data as $type => $jsData)
- {
- foreach ($jsData as $k => $v)
- {
- // was already set at some point with full data
- if (isset($this->jsGlobals[$type][$k]) && is_array($this->jsGlobals[$type][$k]))
- continue;
-
- $this->jsGlobals[$type][$k] = $v;
- }
- }
- }
-
- private function getByContainerRecursive($tableName, $lootId, &$handledRefs, $groupId = 0, $baseChance = 1.0)
- {
- $loot = [];
- $rawItems = [];
-
- if (!$tableName || !$lootId)
- return null;
-
- $rows = DB::World()->select('SELECT * FROM ?# WHERE entry = ?d{ AND groupid = ?d}', $tableName, $lootId, $groupId ?: DBSIMPLE_SKIP);
- if (!$rows)
- return null;
-
- $groupChances = [];
- $nGroupEquals = [];
- foreach ($rows as $entry)
- {
- $set = array(
- 'quest' => $entry['QuestRequired'],
- 'group' => $entry['GroupId'],
- 'parentRef' => $tableName == LOOT_REFERENCE ? $lootId : 0,
- 'realChanceMod' => $baseChance
- );
-
- // if ($entry['LootMode'] > 1)
- // {
- $buff = [];
- for ($i = 0; $i < 8; $i++)
- if ($entry['LootMode'] & (1 << $i))
- $buff[] = $i + 1;
-
- $set['mode'] = implode(', ', $buff);
- // }
- // else
- // $set['mode'] = 0;
-
- /*
- modes:{"mode":8,"4":{"count":7173,"outof":17619},"8":{"count":7173,"outof":10684}}
- ignore lootmodes from sharedDefines.h use different creatures/GOs from each template
- modes.mode = b6543210
- ||||||'dungeon heroic
- |||||'dungeon normal
- ||||'
- |||'10man normal
- ||'25man normal
- |'10man heroic
- '25man heroic
- */
-
- if ($entry['Reference'])
- {
- // bandaid.. remove when propperly handling lootmodes
- if (!in_array($entry['Reference'], $handledRefs))
- { // todo (high): find out, why i used this in the first place. (don't do drugs, kids)
- list($data, $raw) = self::getByContainerRecursive(LOOT_REFERENCE, $entry['Reference'], $handledRefs, /*$entry['GroupId'],*/ 0, $entry['Chance'] / 100);
-
- $handledRefs[] = $entry['Reference'];
-
- $loot = array_merge($loot, $data);
- $rawItems = array_merge($rawItems, $raw);
- }
- $set['reference'] = $entry['Reference'];
- $set['multiplier'] = $entry['MaxCount'];
- }
- else
- {
- $rawItems[] = $entry['Item'];
- $set['content'] = $entry['Item'];
- $set['min'] = $entry['MinCount'];
- $set['max'] = $entry['MaxCount'];
- }
-
- if (!isset($groupChances[$entry['GroupId']]))
- {
- $groupChances[$entry['GroupId']] = 0;
- $nGroupEquals[$entry['GroupId']] = 0;
- }
-
- if ($set['quest'] || !$set['group'])
- $set['groupChance'] = $entry['Chance'];
- else if ($entry['GroupId'] && !$entry['Chance'])
- {
- $nGroupEquals[$entry['GroupId']]++;
- $set['groupChance'] = &$groupChances[$entry['GroupId']];
- }
- else if ($entry['GroupId'] && $entry['Chance'])
- {
- if (empty($groupChances[$entry['GroupId']]))
- $groupChances[$entry['GroupId']] = 0;
-
- $groupChances[$entry['GroupId']] += $entry['Chance'];
- $set['groupChance'] = $entry['Chance'];
- }
- else // shouldn't have happened
- {
- trigger_error('Unhandled case in calculating chance for item '.$entry['Item'].'!', E_USER_WARNING);
- continue;
- }
-
- $loot[] = $set;
- }
-
- foreach (array_keys($nGroupEquals) as $k)
- {
- $sum = $groupChances[$k];
- if (!$sum)
- $sum = 0;
- else if ($sum >= 100.01)
- {
- trigger_error('Loot entry '.$lootId.' / group '.$k.' has a total chance of '.number_format($sum, 2).'%. Some items cannot drop!', E_USER_WARNING);
- $sum = 100;
- }
-
- $cnt = empty($nGroupEquals[$k]) ? 1 : $nGroupEquals[$k];
-
- $groupChances[$k] = (100 - $sum) / $cnt; // is applied as backReference to items with 0-chance
- }
-
- return [$loot, array_unique($rawItems)];
- }
-
- public function getByContainer($table, $entry)
- {
- $this->entry = intVal($entry);
-
- if (!in_array($table, $this->lootTemplates) || !$this->entry)
- return null;
-
- /*
- todo (high): implement conditions on loot (and conditions in general)
-
- also
-
- // if (is_array($this->entry) && in_array($table, [LOOT_CREATURE, LOOT_GAMEOBJECT])
- // iterate over the 4 available difficulties and assign modes
-
-
- modes:{"mode":1,"1":{"count":4408,"outof":16013},"4":{"count":4408,"outof":22531}}
- */
- $handledRefs = [];
- $struct = self::getByContainerRecursive($table, $this->entry, $handledRefs);
- if (!$struct)
- return false;
-
- $items = new ItemList(array(['i.id', $struct[1]], CFG_SQL_LIMIT_NONE));
- $this->jsGlobals = $items->getJSGlobals(GLOBALINFO_SELF | GLOBALINFO_RELATED);
- $foo = $items->getListviewData();
-
- // assign listview LV rows to loot rows, not the other way round! The same item may be contained multiple times
- foreach ($struct[0] as $loot)
- {
- $base = array(
- 'percent' => round($loot['groupChance'] * $loot['realChanceMod'], 3),
- 'group' => $loot['group'],
- 'quest' => $loot['quest'],
- 'count' => 1 // satisfies the sort-script
- );
-
- if ($_ = $loot['mode'])
- $base['mode'] = $_;
-
- if ($_ = $loot['parentRef'])
- $base['reference'] = $_;
-
- if ($_ = self::createStack($loot))
- $base['pctstack'] = $_;
-
- if (empty($loot['reference'])) // regular drop
- {
- if (!User::isInGroup(U_GROUP_EMPLOYEE))
- {
- if (!isset($this->results[$loot['content']]))
- $this->results[$loot['content']] = array_merge($foo[$loot['content']], $base, ['stack' => [$loot['min'], $loot['max']]]);
- else
- $this->results[$loot['content']]['percent'] += $base['percent'];
- }
- else // in case of limited trash loot, check if $foo[] exists
- $this->results[] = array_merge($foo[$loot['content']], $base, ['stack' => [$loot['min'], $loot['max']]]);
- }
- else if (User::isInGroup(U_GROUP_EMPLOYEE)) // create dummy for ref-drop
- {
- $data = array(
- 'id' => $loot['reference'],
- 'name' => '@REFERENCE: '.$loot['reference'],
- 'icon' => 'trade_engineering',
- 'stack' => [$loot['multiplier'], $loot['multiplier']]
- );
- $this->results[] = array_merge($base, $data);
-
- $this->jsGlobals[TYPE_ITEM][$loot['reference']] = $data;
- }
- }
-
- // move excessive % to extra loot
- if (!User::isInGroup(U_GROUP_EMPLOYEE))
- {
- foreach ($this->results as &$_)
- {
- if ($_['percent'] <= 100)
- continue;
-
- while ($_['percent'] > 200)
- {
- $_['stack'][0]++;
- $_['stack'][1]++;
- $_['percent'] -= 100;
- }
-
- $_['stack'][1]++;
- $_['percent'] = 100;
- }
- }
- else
- {
- $fields = ['mode', 'reference'];
- $base = [];
- $set = 0;
- foreach ($this->results as $foo)
- {
- foreach ($fields as $idx => $field)
- {
- $val = isset($foo[$field]) ? $foo[$field] : 0;
- if (!isset($base[$idx]))
- $base[$idx] = $val;
- else if ($base[$idx] != $val)
- $set |= 1 << $idx;
- }
-
- if ($set == (pow(2, count($fields)) - 1))
- break;
- }
-
- $this->extraCols[] = "\$Listview.funcBox.createSimpleCol('group', 'Group', '7%', 'group')";
- foreach ($fields as $idx => $field)
- if ($set & (1 << $idx))
- $this->extraCols[] = "\$Listview.funcBox.createSimpleCol('".$field."', '".Util::ucFirst($field)."', '7%', '".$field."')";
- }
-
- return true;
- }
-
- public function getByItem($entry, $maxResults = CFG_SQL_LIMIT_DEFAULT, $lootTableList = [])
- {
- $this->entry = intVal($entry);
-
- if (!$this->entry)
- return false;
-
- // [fileName, tabData, tabName, tabId, extraCols, hiddenCols, visibleCols]
- $tabsFinal = array(
- ['item', [], '$LANG.tab_containedin', 'contained-in-item', [], [], []],
- ['item', [], '$LANG.tab_disenchantedfrom', 'disenchanted-from', [], [], []],
- ['item', [], '$LANG.tab_prospectedfrom', 'prospected-from', [], [], []],
- ['item', [], '$LANG.tab_milledfrom', 'milled-from', [], [], []],
- ['creature', [], '$LANG.tab_droppedby', 'dropped-by', [], [], []],
- ['creature', [], '$LANG.tab_pickpocketedfrom', 'pickpocketed-from', [], [], []],
- ['creature', [], '$LANG.tab_skinnedfrom', 'skinned-from', [], [], []],
- ['creature', [], '$LANG.tab_minedfromnpc', 'mined-from-npc', [], [], []],
- ['creature', [], '$LANG.tab_salvagedfrom', 'salvaged-from', [], [], []],
- ['creature', [], '$LANG.tab_gatheredfromnpc', 'gathered-from-npc', [], [], []],
- ['quest', [], '$LANG.tab_rewardfrom', 'reward-from-quest', [], [], []],
- ['zone', [], '$LANG.tab_fishedin', 'fished-in-zone', [], [], []],
- ['object', [], '$LANG.tab_containedin', 'contained-in-object', [], [], []],
- ['object', [], '$LANG.tab_minedfrom', 'mined-from-object', [], [], []],
- ['object', [], '$LANG.tab_gatheredfrom', 'gathered-from-object', [], [], []],
- ['object', [], '$LANG.tab_fishedin', 'fished-in-object', [], [], []],
- ['spell', [], '$LANG.tab_createdby', 'created-by', [], [], []],
- ['achievement', [], '$LANG.tab_rewardfrom', 'reward-from-achievement', [], [], []]
- );
- $refResults = [];
- $chanceMods = [];
- $query = 'SELECT
- lt1.entry AS ARRAY_KEY,
- IF(lt1.reference = 0, lt1.item, lt1.reference) AS item,
- lt1.chance,
- SUM(IF(lt2.chance = 0, 1, 0)) AS nZeroItems,
- SUM(lt2.chance) AS sumChance,
- IF(lt1.groupid > 0, 1, 0) AS isGrouped,
- IF(lt1.reference = 0, lt1.mincount, 1) AS min,
- IF(lt1.reference = 0, lt1.maxcount, 1) AS max,
- IF(lt1.reference > 0, lt1.maxcount, 1) AS multiplier
- FROM
- ?# lt1
- LEFT JOIN
- ?# lt2 ON lt1.entry = lt2.entry AND lt1.groupid = lt2.groupid
- WHERE
- %s
- GROUP BY lt2.entry, lt2.groupid';
-
- $calcChance = function ($refs, $parents = []) use (&$chanceMods)
- {
- $retData = [];
- $retKeys = [];
-
- foreach ($refs as $rId => $ref)
- {
- // check for possible database inconsistencies
- if (!$ref['chance'] && !$ref['isGrouped'])
- trigger_error('Loot by Item: Ungrouped Item/Ref '.$ref['item'].' has 0% chance assigned!', E_USER_WARNING);
-
- if ($ref['isGrouped'] && $ref['sumChance'] > 100)
- trigger_error('Loot by Item: Group with Item/Ref '.$ref['item'].' has '.number_format($ref['sumChance'], 2).'% total chance! Some items cannot drop!', E_USER_WARNING);
-
- if ($ref['isGrouped'] && $ref['sumChance'] >= 100 && !$ref['chance'])
- trigger_error('Loot by Item: Item/Ref '.$ref['item'].' with adaptive chance cannot drop. Group already at 100%!', E_USER_WARNING);
-
- $chance = abs($ref['chance'] ?: (100 - $ref['sumChance']) / $ref['nZeroItems']) / 100;
-
- // apply inherited chanceMods
- if (isset($chanceMods[$ref['item']]))
- {
- $chance *= $chanceMods[$ref['item']][0];
- $chance = 1 - pow(1 - $chance, $chanceMods[$ref['item']][1]);
- }
-
- // save chance for parent-ref
- $chanceMods[$rId] = [$chance, $ref['multiplier']];
-
- // refTemplate doesn't point to a new ref -> we are done
- if (!in_array($rId, $parents))
- {
- $data = array(
- 'percent' => $chance,
- 'stack' => [$ref['min'], $ref['max']],
- 'count' => 1 // ..and one for the sort script
- );
-
- if ($_ = self::createStack($ref))
- $data['pctstack'] = $_;
-
- // sort highest chances first
- $i = 0;
- for (; $i < count($retData); $i++)
- if ($retData[$i]['percent'] < $data['percent'])
- break;
-
- array_splice($retData, $i, 0, [$data]);
- array_splice($retKeys, $i, 0, [$rId]);
- }
- }
-
- return array_combine($retKeys, $retData);
- };
-
- /*
- get references containing the item
- */
- $newRefs = DB::World()->select(
- sprintf($query, 'lt1.item = ?d AND lt1.reference = 0'),
- LOOT_REFERENCE, LOOT_REFERENCE,
- $this->entry
- );
-
- while ($newRefs)
- {
- $curRefs = $newRefs;
- $newRefs = DB::World()->select(
- sprintf($query, 'lt1.reference IN (?a)'),
- LOOT_REFERENCE, LOOT_REFERENCE,
- array_keys($curRefs)
- );
-
- $refResults += $calcChance($curRefs, array_column($newRefs, 'item'));
- }
-
- /*
- search the real loot-templates for the itemId and gathered refds
- */
- for ($i = 1; $i < count($this->lootTemplates); $i++)
- {
- if ($lootTableList && !in_array($this->lootTemplates[$i], $lootTableList))
- continue;
-
- $result = $calcChance(DB::World()->select(
- sprintf($query, '{lt1.reference IN (?a) OR }(lt1.reference = 0 AND lt1.item = ?d)'),
- $this->lootTemplates[$i], $this->lootTemplates[$i],
- $refResults ? array_keys($refResults) : DBSIMPLE_SKIP,
- $this->entry
- ));
-
- // do not skip here if $result is empty. Additional loot for spells and quest is added separately
-
- // format for actual use
- foreach ($result as $k => $v)
- {
- unset($result[$k]);
- $v['percent'] = round($v['percent'] * 100, 3);
- $result[abs($k)] = $v;
- }
-
- // cap fetched entries to the sql-limit to guarantee, that the highest chance items get selected first
- // screws with GO-loot and skinnig-loot as these templates are shared for several tabs (fish, herb, ore) (herb, ore, leather)
- $ids = array_slice(array_keys($result), 0, $maxResults);
-
- switch ($this->lootTemplates[$i])
- {
- case LOOT_CREATURE: $field = 'lootId'; $tabId = 4; break;
- case LOOT_PICKPOCKET: $field = 'pickpocketLootId'; $tabId = 5; break;
- case LOOT_SKINNING: $field = 'skinLootId'; $tabId = -6; break; // assigned later
- case LOOT_PROSPECTING: $field = 'id'; $tabId = 2; break;
- case LOOT_MILLING: $field = 'id'; $tabId = 3; break;
- case LOOT_ITEM: $field = 'id'; $tabId = 0; break;
- case LOOT_DISENCHANT: $field = 'disenchantId'; $tabId = 1; break;
- case LOOT_FISHING: $field = 'id'; $tabId = 11; break; // subAreas are currently ignored
- case LOOT_GAMEOBJECT:
- if (!$ids)
- continue 2;
-
- $srcObj = new GameObjectList(array(['lootId', $ids]));
- if ($srcObj->error)
- continue 2;
-
- $srcData = $srcObj->getListviewData();
-
- foreach ($srcObj->iterate() as $__id => $curTpl)
- {
- switch ($curTpl['typeCat'])
- {
- case 25: $tabId = 15; break; // fishing node
- case -3: $tabId = 14; break; // herb
- case -4: $tabId = 13; break; // vein
- default: $tabId = 12; break; // general chest loot
- }
-
- $tabsFinal[$tabId][1][] = array_merge($srcData[$srcObj->id], $result[$srcObj->getField('lootId')]);
- $tabsFinal[$tabId][4][] = '$Listview.extraCols.percent';
- if ($tabId != 15)
- $tabsFinal[$tabId][6][] = 'skill';
- }
- continue 2;
- case LOOT_MAIL:
- // quest part
- $conditions = array(['rewardChoiceItemId1', $this->entry], ['rewardChoiceItemId2', $this->entry], ['rewardChoiceItemId3', $this->entry], ['rewardChoiceItemId4', $this->entry], ['rewardChoiceItemId5', $this->entry],
- ['rewardChoiceItemId6', $this->entry], ['rewardItemId1', $this->entry], ['rewardItemId2', $this->entry], ['rewardItemId3', $this->entry], ['rewardItemId4', $this->entry],
- 'OR');
- if ($ids)
- $conditions[] = ['rewardMailTemplateId', $ids];
-
- $srcObj = new QuestList($conditions);
- if (!$srcObj->error)
- {
- self::storeJSGlobals($srcObj->getJSGlobals(GLOBALINFO_SELF | GLOBALINFO_REWARDS));
- $srcData = $srcObj->getListviewData();
-
- foreach ($srcObj->iterate() as $_)
- $tabsFinal[10][1][] = array_merge($srcData[$srcObj->id], empty($result[$srcObj->id]) ? ['percent' => -1] : $result[$srcObj->id]);
- }
-
- // achievement part
- $conditions = array(['itemExtra', $this->entry]);
- if ($ar = DB::World()->selectCol('SELECT ID FROM achievement_reward WHERE ItemID = ?d{ OR MailTemplateID IN (?a)}', $this->entry, $ids ?: DBSIMPLE_SKIP))
- array_push($conditions, ['id', $ar], 'OR');
-
- $srcObj = new AchievementList($conditions);
- if (!$srcObj->error)
- {
- self::storeJSGlobals($srcObj->getJSGlobals(GLOBALINFO_SELF | GLOBALINFO_REWARDS));
- $srcData = $srcObj->getListviewData();
-
- foreach ($srcObj->iterate() as $_)
- $tabsFinal[17][1][] = array_merge($srcData[$srcObj->id], empty($result[$srcObj->id]) ? ['percent' => -1] : $result[$srcObj->id]);
-
- $tabsFinal[17][5][] = 'rewards';
- $tabsFinal[17][6][] = 'category';
- }
- continue 2;
- case LOOT_SPELL:
- $conditions = array(
- 'OR',
- ['AND', ['effect1CreateItemId', $this->entry], ['OR', ['effect1Id', SpellList::$effects['itemCreate']], ['effect1AuraId', SpellList::$auras['itemCreate']]]],
- ['AND', ['effect2CreateItemId', $this->entry], ['OR', ['effect2Id', SpellList::$effects['itemCreate']], ['effect2AuraId', SpellList::$auras['itemCreate']]]],
- ['AND', ['effect3CreateItemId', $this->entry], ['OR', ['effect3Id', SpellList::$effects['itemCreate']], ['effect3AuraId', SpellList::$auras['itemCreate']]]],
- );
- if ($ids)
- $conditions[] = ['id', $ids];
-
- $srcObj = new SpellList($conditions);
- if (!$srcObj->error)
- {
- self::storeJSGlobals($srcObj->getJSGlobals(GLOBALINFO_SELF | GLOBALINFO_RELATED));
- $srcData = $srcObj->getListviewData();
-
- if (!empty($result))
- $tabsFinal[16][4][] = '$Listview.extraCols.percent';
-
- if ($srcObj->hasSetFields(['reagent1']))
- $tabsFinal[16][6][] = 'reagents';
-
- foreach ($srcObj->iterate() as $_)
- $tabsFinal[16][1][] = array_merge($srcData[$srcObj->id], empty($result[$srcObj->id]) ? ['percent' => -1] : $result[$srcObj->id]);
- }
- continue 2;
- }
-
- if (!$ids)
- continue;
-
- switch ($tabsFinal[abs($tabId)][0])
- {
- case 'creature': // new CreatureList
- case 'item': // new ItemList
- case 'zone': // new ZoneList
- $oName = ucFirst($tabsFinal[abs($tabId)][0]).'List';
- $srcObj = new $oName(array([$field, $ids]));
- if (!$srcObj->error)
- {
- $srcData = $srcObj->getListviewData();
- self::storeJSGlobals($srcObj->getJSGlobals(GLOBALINFO_SELF | GLOBALINFO_RELATED));
-
- foreach ($srcObj->iterate() as $curTpl)
- {
- if ($tabId < 0 && $curTpl['typeFlags'] & NPC_TYPEFLAG_HERBLOOT)
- $tabId = 9;
- else if ($tabId < 0 && $curTpl['typeFlags'] & NPC_TYPEFLAG_ENGINEERLOOT)
- $tabId = 8;
- else if ($tabId < 0 && $curTpl['typeFlags'] & NPC_TYPEFLAG_MININGLOOT)
- $tabId = 7;
- else if ($tabId < 0)
- $tabId = abs($tabId); // general case (skinning)
-
- $tabsFinal[$tabId][1][] = array_merge($srcData[$srcObj->id], $result[$srcObj->getField($field)]);
- $tabsFinal[$tabId][4][] = '$Listview.extraCols.percent';
- }
- }
- break;
- }
- }
-
- foreach ($tabsFinal as $tabId => $data)
- {
- $tabData = array(
- 'data' => $data[1],
- 'name' => $data[2],
- 'id' => $data[3]
- );
-
- if ($data[4])
- $tabData['extraCols'] = array_unique($data[4]);
-
- if ($data[5])
- $tabData['hiddenCols'] = array_unique($data[5]);
-
- if ($data[6])
- $tabData['visibleCols'] = array_unique($data[6]);
-
- $this->results[$tabId] = [$data[0], $tabData];
- }
-
-
- return true;
- }
-}
-
-?>
\ No newline at end of file
diff --git a/includes/markup.class.php b/includes/markup.class.php
deleted file mode 100644
index 8a4f3fc8..00000000
--- a/includes/markup.class.php
+++ /dev/null
@@ -1,148 +0,0 @@
-text = $text;
- }
-
- public function parseGlobalsFromText(&$jsg = [])
- {
- if (preg_match_all(self::$dbTagPattern, $this->text, $matches, PREG_SET_ORDER))
- {
- foreach ($matches as $match)
- {
- if ($match[1] == 'statistic')
- $match[1] = 'achievement';
- else if ($match[1] == 'icondb')
- $match[1] = 'icon';
- else if ($match[1] == 'money')
- {
- if (stripos($match[0], 'items'))
- {
- if (preg_match('/items=([0-9,]+)/i', $match[0], $submatch))
- {
- $sm = explode(',', $submatch[1]);
- for ($i = 0; $i < count($sm); $i+=2)
- $this->jsGlobals[TYPE_ITEM][$sm[$i]] = $sm[$i];
- }
- }
-
- if (stripos($match[0], 'currency'))
- {
- if (preg_match('/currency=([0-9,]+)/i', $match[0], $submatch))
- {
- $sm = explode(',', $submatch[1]);
- for ($i = 0; $i < count($sm); $i+=2)
- $this->jsGlobals[TYPE_CURRENCY][$sm[$i]] = $sm[$i];
- }
- }
- }
- else if ($type = array_search($match[1], Util::$typeStrings))
- $this->jsGlobals[$type][$match[2]] = $match[2];
- }
- }
-
- Util::mergeJsGlobals($jsg, $this->jsGlobals);
-
- return $this->jsGlobals;
- }
-
- public function stripTags($globals = [])
- {
- // since this is an article the db-tags should already be parsed
- $text = preg_replace_callback(self::$dbTagPattern, function ($match) use ($globals) {
- if ($match[1] == 'statistic')
- $match[1] = 'achievement';
- else if ($match[1] == 'icondb')
- $match[1] = 'icon';
- else if ($match[1] == 'money')
- {
- $moneys = [];
- if (stripos($match[0], 'items'))
- {
- if (preg_match('/items=([0-9,]+)/i', $match[0], $submatch))
- {
- $sm = explode(',', $submatch[1]);
- for ($i = 0; $i < count($sm); $i += 2)
- {
- if (!empty($globals[TYPE_ITEM][1][$sm[$i]]))
- $moneys[] = $globals[TYPE_ITEM][1][$sm[$i]]['name'];
- else
- $moneys[] = Util::ucFirst(Lang::game('item')).' #'.$sm[$i];
- }
- }
- }
-
- if (stripos($match[0], 'currency'))
- {
- if (preg_match('/currency=([0-9,]+)/i', $match[0], $submatch))
- {
- $sm = explode(',', $submatch[1]);
- for ($i = 0; $i < count($sm); $i += 2)
- {
- if (!empty($globals[TYPE_CURRENCY][1][$sm[$i]]))
- $moneys[] = $globals[TYPE_CURRENCY][1][$sm[$i]]['name'];
- else
- $moneys[] = Util::ucFirst(Lang::game('curency')).' #'.$sm[$i];
- }
- }
- }
-
- return Lang::concat($moneys);
- }
-
- if ($type = array_search($match[1], Util::$typeStrings))
- {
- if (!empty($globals[$type][1][$match[2]]))
- return $globals[$type][1][$match[2]]['name'];
- else
- return Util::ucFirst(Lang::game($match[1])).' #'.$match[2];
- }
-
- trigger_error('Markup::stripTags() - encountered unhandled db-tag: '.var_export($match));
- return '';
- }, $this->text);
-
- $text = str_replace('[br]', "\n", $text);
- $stripped = '';
-
- $inTag = false;
- for ($i = 0; $i < strlen($text); $i++)
- {
- if ($text[$i] == '[')
- $inTag = true;
- if (!$inTag)
- $stripped .= $text[$i];
- if ($text[$i] == ']')
- $inTag = false;
- }
-
- return $stripped;
- }
-
- public function fromHtml()
- {
- }
-
- public function toHtml()
- {
- }
-}
-
-?>
diff --git a/includes/profiler.class.php b/includes/profiler.class.php
deleted file mode 100644
index 6be872d5..00000000
--- a/includes/profiler.class.php
+++ /dev/null
@@ -1,828 +0,0 @@
- [INVTYPE_HEAD], // head
- 2 => [INVTYPE_NECK], // neck
- 3 => [INVTYPE_SHOULDERS], // shoulder
- 4 => [INVTYPE_BODY], // shirt
- 5 => [INVTYPE_CHEST, INVTYPE_ROBE], // chest
- 6 => [INVTYPE_WAIST], // waist
- 7 => [INVTYPE_LEGS], // legs
- 8 => [INVTYPE_FEET], // feet
- 9 => [INVTYPE_WRISTS], // wrists
- 10 => [INVTYPE_HANDS], // hands
- 11 => [INVTYPE_FINGER], // finger1
- 12 => [INVTYPE_FINGER], // finger2
- 13 => [INVTYPE_TRINKET], // trinket1
- 14 => [INVTYPE_TRINKET], // trinket2
- 15 => [INVTYPE_CLOAK], // chest
- 16 => [INVTYPE_WEAPONMAINHAND, INVTYPE_WEAPON, INVTYPE_2HWEAPON], // mainhand
- 17 => [INVTYPE_WEAPONOFFHAND, INVTYPE_WEAPON, INVTYPE_HOLDABLE, INVTYPE_SHIELD], // offhand
- 18 => [INVTYPE_RANGED, INVTYPE_THROWN, INVTYPE_RELIC], // ranged + relic
- 19 => [INVTYPE_TABARD], // tabard
- );
-
- public static $raidProgression = array( // statisticAchievement => relevantCriterium
- 1098 => 3271, // Onyxia's Lair 10
- 1756 => 13345, // Onyxia's Lair 25
- 4031 => 12230, 4034 => 12234, 4038 => 12238, 4042 => 12242, 4046 => 12246, // Trial of the Crusader 25 nh
- 4029 => 12231, 4035 => 12235, 4039 => 12239, 4043 => 12243, 4047 => 12247, // Trial of the Crusader 25 hc
- 4030 => 12229, 4033 => 12233, 4037 => 12237, 4041 => 12241, 4045 => 12245, // Trial of the Crusader 10 hc
- 4028 => 12228, 4032 => 12232, 4036 => 12236, 4040 => 12240, 4044 => 12244, // Trial of the Crusader 10 nh
- 4642 => 13091, 4656 => 13106, 4661 => 13111, 4664 => 13114, 4667 => 13117, 4670 => 13120, 4673 => 13123, 4676 => 13126, 4679 => 13129, 4682 => 13132, 4685 => 13135, 4688 => 13138, // Icecrown Citadel 25 hc
- 4641 => 13092, 4655 => 13105, 4660 => 13109, 4663 => 13112, 4666 => 13115, 4669 => 13118, 4672 => 13121, 4675 => 13124, 4678 => 13127, 4681 => 13130, 4683 => 13133, 4687 => 13136, // Icecrown Citadel 25 nh
- 4640 => 13090, 4654 => 13104, 4659 => 13110, 4662 => 13113, 4665 => 13116, 4668 => 13119, 4671 => 13122, 4674 => 13125, 4677 => 13128, 4680 => 13131, 4684 => 13134, 4686 => 13137, // Icecrown Citadel 10 hc
- 4639 => 13089, 4643 => 13093, 4644 => 13094, 4645 => 13095, 4646 => 13096, 4647 => 13097, 4648 => 13098, 4649 => 13099, 4650 => 13100, 4651 => 13101, 4652 => 13102, 4653 => 13103, // Icecrown Citadel 10 nh
- // 4823 => 13467, // Ruby Sanctum 25 hc
- // 4820 => 13465, // Ruby Sanctum 25 nh
- // 4822 => 13468, // Ruby Sanctum 10 hc
- // 4821 => 13466, // Ruby Sanctum 10 nh
- );
-
- public static function getBuyoutForItem($itemId)
- {
- if (!$itemId)
- return 0;
-
- // try, when having filled char-DB at hand
- // return DB::Characters()->selectCell('SELECT SUM(a.buyoutprice) / SUM(ii.count) FROM auctionhouse a JOIN item_instance ii ON ii.guid = a.itemguid WHERE ii.itemEntry = ?d', $itemId);
- return 0;
- }
-
- public static function queueStart(&$msg = '')
- {
- $queuePID = self::queueStatus();
-
- if ($queuePID)
- {
- $msg = 'queue already running';
- return true;
- }
-
- if (OS_WIN) // here be gremlins! .. suggested was "start /B php prQueue" as background process. but that closes itself
- pclose(popen('start php prQueue --log=cache/profiling.log', 'r'));
- else
- exec('php prQueue --log=cache/profiling.log > /dev/null 2>/dev/null &');
-
- usleep(500000);
- if (self::queueStatus())
- return true;
- else
- {
- $msg = 'failed to start queue';
- return false;
- }
- }
-
- public static function queueStatus()
- {
- if (!file_exists(self::PID_FILE))
- return 0;
-
- $pid = file_get_contents(self::PID_FILE);
- $cmd = OS_WIN ? 'tasklist /NH /FO CSV /FI "PID eq %d"' : 'ps --no-headers p %d';
-
- exec(sprintf($cmd, $pid), $out);
- if ($out && stripos($out[0], $pid) !== false)
- return $pid;
-
- // have pidFile but no process with this pid
- self::queueFree();
- return 0;
- }
-
- public static function queueLock($pid)
- {
- $queuePID = self::queueStatus();
- if ($queuePID && $queuePID != $pid)
- {
- trigger_error('pSync - another queue with PID #'.$queuePID.' is already running', E_USER_ERROR);
- CLI::write('Profiler::queueLock() - another queue with PID #'.$queuePID.' is already runnung', CLI::LOG_ERROR);
- return false;
- }
-
- // no queue running; create or overwrite pidFile
- $ok = false;
- if ($fh = fopen(self::PID_FILE, 'w'))
- {
- if (fwrite($fh, $pid))
- $ok = true;
-
- fclose($fh);
- }
-
- return $ok;
- }
-
- public static function queueFree()
- {
- unlink(self::PID_FILE);
- }
-
- public static function urlize($str, $allowLocales = false, $profile = false)
- {
- $search = ['<', '>', ' / ', "'"];
- $replace = ['<', '>', '-', '' ];
- $str = str_replace($search, $replace, $str);
-
- if ($profile)
- {
- $str = str_replace(['(', ')'], ['', ''], $str);
- $accents = array(
- "ß" => "ss",
- "á" => "a", "ä" => "a", "à" => "a", "â" => "a",
- "è" => "e", "ê" => "e", "é" => "e", "ë" => "e",
- "í" => "i", "î" => "i", "ì" => "i", "ï" => "i",
- "ñ" => "n",
- "ò" => "o", "ó" => "o", "ö" => "o", "ô" => "o",
- "ú" => "u", "ü" => "u", "û" => "u", "ù" => "u",
- "œ" => "oe",
- "Á" => "A", "Ä" => "A", "À" => "A", "Â" => "A",
- "È" => "E", "Ê" => "E", "É" => "E", "Ë" => "E",
- "Í" => "I", "Î" => "I", "Ì" => "I", "Ï" => "I",
- "Ñ" => "N",
- "Ò" => "O", "Ó" => "O", "Ö" => "O", "Ô" => "O",
- "Ú" => "U", "Ü" => "U", "Û" => "U", "Ù" => "U",
- "Œ" => "Oe"
- );
- $str = strtr($str, $accents);
- }
-
- $str = trim($str);
-
- if ($allowLocales)
- $str = str_replace(' ', '-', $str);
- else
- $str = preg_replace('/[^a-z0-9]/i', '-', $str);
-
- $str = str_replace('--', '-', $str);
- $str = str_replace('--', '-', $str);
-
- $str = rtrim($str, '-');
- $str = strtolower($str);
-
- return $str;
- }
-
- public static function getRealms()
- {
- if (DB::isConnectable(DB_AUTH) && !self::$realms)
- {
- self::$realms = DB::Auth()->select('SELECT id AS ARRAY_KEY, name, IF(timezone IN (8, 9, 10, 11, 12), "eu", "us") AS region FROM realmlist WHERE allowedSecurityLevel = 0 AND gamebuild = ?d', WOW_BUILD);
- foreach (self::$realms as $rId => $rData)
- {
- if (DB::isConnectable(DB_CHARACTERS . $rId))
- continue;
-
- // realm in db but no connection info set
- unset(self::$realms[$rId]);
- }
- }
-
- return self::$realms;
- }
-
- private static function queueInsert($realmId, $guid, $type, $localId)
- {
- if ($rData = DB::Aowow()->selectRow('SELECT requestTime AS time, status FROM ?_profiler_sync WHERE realm = ?d AND realmGUID = ?d AND `type` = ?d AND typeId = ?d AND status <> ?d', $realmId, $guid, $type, $localId, PR_QUEUE_STATUS_WORKING))
- {
- // not on already scheduled - recalc time and set status to PR_QUEUE_STATUS_WAITING
- if ($rData['status'] != PR_QUEUE_STATUS_WAITING)
- {
- $newTime = CFG_DEBUG ? time() : max($rData['time'] + CFG_PROFILER_RESYNC_DELAY, time());
- DB::Aowow()->query('UPDATE ?_profiler_sync SET requestTime = ?d, status = ?d, errorCode = 0 WHERE realm = ?d AND realmGUID = ?d AND `type` = ?d AND typeId = ?d', $newTime, PR_QUEUE_STATUS_WAITING, $realmId, $guid, $type, $localId);
- }
- }
- else
- DB::Aowow()->query('REPLACE INTO ?_profiler_sync (realm, realmGUID, `type`, typeId, requestTime, status, errorCode) VALUES (?d, ?d, ?d, ?d, UNIX_TIMESTAMP(), ?d, 0)', $realmId, $guid, $type, $localId, PR_QUEUE_STATUS_WAITING);
- }
-
- public static function scheduleResync($type, $realmId, $guid)
- {
- $newId = 0;
-
- switch ($type)
- {
- case TYPE_PROFILE:
- if ($newId = DB::Aowow()->selectCell('SELECT id FROM ?_profiler_profiles WHERE realm = ?d AND realmGUID = ?d', $realmId, $guid))
- self::queueInsert($realmId, $guid, TYPE_PROFILE, $newId);
-
- break;
- case TYPE_GUILD:
- if ($newId = DB::Aowow()->selectCell('SELECT id FROM ?_profiler_guild WHERE realm = ?d AND realmGUID = ?d', $realmId, $guid))
- self::queueInsert($realmId, $guid, TYPE_GUILD, $newId);
-
- break;
- case TYPE_ARENA_TEAM:
- if ($newId = DB::Aowow()->selectCell('SELECT id FROM ?_profiler_arena_team WHERE realm = ?d AND realmGUID = ?d', $realmId, $guid))
- self::queueInsert($realmId, $guid, TYPE_ARENA_TEAM, $newId);
-
- break;
- default:
- trigger_error('scheduling resync for unknown type #'.$type.' omiting..', E_USER_WARNING);
- return 0;
- }
-
- if (!$newId)
- trigger_error('Profiler::scheduleResync() - tried to resync type #'.$type.' guid #'.$guid.' from realm #'.$realmId.' without preloaded data', E_USER_ERROR);
- else if (!self::queueStart($msg))
- trigger_error('Profiler::scheduleResync() - '.$msg, E_USER_ERROR);
-
- return $newId;
- }
-
- public static function resyncStatus($type, array $subjectGUIDs)
- {
- $response = [CFG_PROFILER_QUEUE ? 2 : 0]; // in theory you could have multiple queues; used as divisor for: (15 / x) + 2
- if (!$subjectGUIDs)
- $response[] = [PR_QUEUE_STATUS_ENDED, 0, 0, PR_QUEUE_ERROR_CHAR];
- else
- {
- // error out all profiles with status WORKING, that are older than 60sec
- DB::Aowow()->query('UPDATE ?_profiler_sync SET status = ?d, errorCode = ?d WHERE status = ?d AND requestTime < ?d', PR_QUEUE_STATUS_ERROR, PR_QUEUE_ERROR_UNK, PR_QUEUE_STATUS_WORKING, time() - MINUTE);
-
- $subjectStatus = DB::Aowow()->select('SELECT typeId AS ARRAY_KEY, status, realm FROM ?_profiler_sync WHERE `type` = ?d AND typeId IN (?a)', $type, $subjectGUIDs);
- $queue = DB::Aowow()->selectCol('SELECT CONCAT(type, ":", typeId) FROM ?_profiler_sync WHERE status = ?d AND requestTime < UNIX_TIMESTAMP() ORDER BY requestTime ASC', PR_QUEUE_STATUS_WAITING);
- foreach ($subjectGUIDs as $guid)
- {
- if (empty($subjectStatus[$guid])) // whelp, thats some error..
- $response[] = [PR_QUEUE_STATUS_ERROR, 0, 0, PR_QUEUE_ERROR_UNK];
- else if ($subjectStatus[$guid]['status'] == PR_QUEUE_STATUS_ERROR)
- $response[] = [PR_QUEUE_STATUS_ERROR, 0, 0, $subjectStatus[$guid]['errCode']];
- else
- $response[] = array(
- $subjectStatus[$guid]['status'],
- $subjectStatus[$guid]['status'] != PR_QUEUE_STATUS_READY ? CFG_PROFILER_RESYNC_PING : 0,
- array_search($type.':'.$guid, $queue) + 1,
- 0,
- 1 // nResycTries - unsure about this one
- );
- }
- }
-
- return $response;
- }
-
- public static function getCharFromRealm($realmId, $charGuid)
- {
- $char = DB::Characters($realmId)->selectRow('SELECT c.* FROM characters c WHERE c.guid = ?d', $charGuid);
- if (!$char)
- return false;
-
- // reminder: this query should not fail: a placeholder entry is created as soon as a char listview is created or profile detail page is called
- $profileId = DB::Aowow()->selectCell('SELECT id FROM ?_profiler_profiles WHERE realm = ?d AND realmGUID = ?d', $realmId, $char['guid']);
-
- CLI::write('fetching char #'.$charGuid.' from realm #'.$realmId);
- CLI::write('writing...');
-
-
- /*************/
- /* equipment */
- /*************/
-
- /* enchantment-Indizes
- * 0: permEnchant
- * 3: tempEnchant
- * 6: gem1
- * 9: gem2
- * 12: gem3
- * 15: socketBonus [not used]
- * 18: extraSocket [only check existance]
- * 21 - 30: randomProp enchantments
- */
-
-
- DB::Aowow()->query('DELETE FROM ?_profiler_items WHERE id = ?d', $profileId);
- $items = DB::Characters($realmId)->select('SELECT ci.slot AS ARRAY_KEY, ii.itemEntry, ii.enchantments, ii.randomPropertyId FROM character_inventory ci JOIN item_instance ii ON ci.item = ii.guid WHERE ci.guid = ?d AND bag = 0 AND slot BETWEEN 0 AND 18', $char['guid']);
-
- $gemItems = [];
- $permEnch = [];
- $mhItem = 0;
- $ohItem = 0;
-
- foreach ($items as $slot => $item)
- {
- $ench = explode(' ', $item['enchantments']);
- $gEnch = [];
- foreach ([6, 9, 12] as $idx)
- if ($ench[$idx])
- $gEnch[$idx] = $ench[$idx];
-
- if ($gEnch)
- {
- $gi = DB::Aowow()->selectCol('SELECT gemEnchantmentId AS ARRAY_KEY, id FROM ?_items WHERE class = 3 AND gemEnchantmentId IN (?a)', $gEnch);
- foreach ($gEnch as $eId)
- {
- if (isset($gemItems[$eId]))
- $gemItems[$eId][1]++;
- else
- $gemItems[$eId] = [$gi[$eId], 1];
- }
- }
-
- if ($slot + 1 == 16)
- $mhItem = $item['itemEntry'];
- if ($slot + 1 == 17)
- $ohItem = $item['itemEntry'];
-
- if ($ench[0])
- $permEnch[$slot] = $ench[0];
-
- $data = array(
- 'id' => $profileId,
- 'slot' => $slot + 1,
- 'item' => $item['itemEntry'],
- 'subItem' => $item['randomPropertyId'],
- 'permEnchant' => $ench[0],
- 'tempEnchant' => $ench[3],
- 'extraSocket' => (int)!!$ench[18],
- 'gem1' => isset($gemItems[$ench[6]]) ? $gemItems[$ench[6]][0] : 0,
- 'gem2' => isset($gemItems[$ench[9]]) ? $gemItems[$ench[9]][0] : 0,
- 'gem3' => isset($gemItems[$ench[12]]) ? $gemItems[$ench[12]][0] : 0,
- 'gem4' => 0 // serverside items cant have more than 3 sockets. (custom profile thing)
- );
-
- DB::Aowow()->query('INSERT INTO ?_profiler_items (?#) VALUES (?a)', array_keys($data), array_values($data));
- }
-
- CLI::write(' ..inventory');
-
-
- /**************/
- /* basic info */
- /**************/
-
- $data = array(
- 'realm' => $realmId,
- 'realmGUID' => $charGuid,
- 'name' => $char['name'],
- 'race' => $char['race'],
- 'class' => $char['class'],
- 'level' => $char['level'],
- 'gender' => $char['gender'],
- 'skincolor' => $char['playerBytes'] & 0xFF,
- 'facetype' => ($char['playerBytes'] >> 8) & 0xFF, // maybe features
- 'hairstyle' => ($char['playerBytes'] >> 16) & 0xFF,
- 'haircolor' => ($char['playerBytes'] >> 24) & 0xFF,
- 'features' => $char['playerBytes2'] & 0xFF, // maybe facetype
- 'title' => $char['chosenTitle'] ? DB::Aowow()->selectCell('SELECT id FROM ?_titles WHERE bitIdx = ?d', $char['chosenTitle']) : 0,
- 'playedtime' => $char['totaltime'],
- 'nomodelMask' => ($char['playerFlags'] & 0x400 ? (1 << SLOT_HEAD) : 0) | ($char['playerFlags'] & 0x800 ? (1 << SLOT_BACK) : 0),
- 'talenttree1' => 0,
- 'talenttree2' => 0,
- 'talenttree3' => 0,
- 'talentbuild1' => '',
- 'talentbuild2' => '',
- 'glyphs1' => '',
- 'glyphs2' => '',
- 'activespec' => $char['activespec'],
- 'guild' => null,
- 'guildRank' => null,
- 'gearscore' => 0,
- 'achievementpoints' => 0
- );
-
-
- /********************/
- /* talents + glyphs */
- /********************/
-
- $t = DB::Characters($realmId)->selectCol('SELECT spec AS ARRAY_KEY, spell AS ARRAY_KEY2, spell FROM character_talent WHERE guid = ?d', $char['guid']);
- $g = DB::Characters($realmId)->select('SELECT spec AS ARRAY_KEY, glyph1 AS g1, glyph2 AS g4, glyph3 AS g5, glyph4 AS g2, glyph5 AS g3, glyph6 AS g6 FROM character_glyphs WHERE guid = ?d', $char['guid']);
- for ($i = 0; $i < 2; $i++)
- {
- // talents
- for ($j = 0; $j < 3; $j++)
- {
- $_ = DB::Aowow()->selectCol('SELECT spell AS ARRAY_KEY, MAX(IF(spell in (?a), rank, 0)) FROM ?_talents WHERE class = ?d AND tab = ?d GROUP BY id ORDER BY row, col ASC', !empty($t[$i]) ? $t[$i] : [0], $char['class'], $j);
- $data['talentbuild'.($i + 1)] .= implode('', $_);
- if ($char['activespec'] == $i)
- $data['talenttree'.($j + 1)] = array_sum($_);
- }
-
- // glyphs
- if (isset($g[$i]))
- {
- $gProps = [];
- for ($j = 1; $j <= 6; $j++)
- if ($g[$i]['g'.$j])
- $gProps[$j] = $g[$i]['g'.$j];
-
- if ($gProps)
- if ($gItems = DB::Aowow()->selectCol('SELECT i.id FROM ?_glyphproperties gp JOIN ?_spell s ON s.effect1MiscValue = gp.id AND s.effect1Id = 74 JOIN ?_items i ON i.class = 16 AND i.spellId1 = s.id WHERE gp.id IN (?a)', $gProps))
- $data['glyphs'.($i + 1)] = implode(':', $gItems);
- }
- }
-
- $t = array(
- 'spent' => [$data['talenttree1'], $data['talenttree2'], $data['talenttree3']],
- 'spec' => 0
- );
- if ($t['spent'][0] > $t['spent'][1] && $t['spent'][0] > $t['spent'][2])
- $t['spec'] = 1;
- else if ($t['spent'][1] > $t['spent'][0] && $t['spent'][1] > $t['spent'][2])
- $t['spec'] = 2;
- else if ($t['spent'][2] > $t['spent'][1] && $t['spent'][2] > $t['spent'][0])
- $t['spec'] = 3;
-
- // calc gearscore
- if ($items)
- $data['gearscore'] += (new ItemList(array(['id', array_column($items, 'itemEntry')])))->getScoreTotal($data['class'], $t, $mhItem, $ohItem);
-
- if ($gemItems)
- {
- $gemScores = new ItemList(array(['id', array_column($gemItems, 0)]));
- foreach ($gemItems as list($itemId, $mult))
- if (isset($gemScores->json[$itemId]['gearscore']))
- $data['gearscore'] += $gemScores->json[$itemId]['gearscore'] * $mult;
- }
-
- if ($permEnch) // fuck this shit .. we are guestimating this!
- {
- // enchantId => multiple spells => multiple items with varying itemlevels, quality, whatevs
- // cant reasonably get to the castItem from enchantId and slot
-
- $profSpec = DB::Aowow()->selectCol('SELECT id AS ARRAY_KEY, skillLevel AS "1", skillLine AS "0" FROM ?_itemenchantment WHERE id IN (?a)', $permEnch);
- foreach ($permEnch as $eId)
- {
- if ($x = Util::getEnchantmentScore(0, 0, !!$profSpec[$eId][1], $eId))
- $data['gearscore'] += $x;
- else if ($profSpec[$eId][0] != 776) // not runeforging
- $data['gearscore'] += 17; // assume high quality enchantment for unknown cases
- }
- }
-
- $data['lastupdated'] = time();
-
- CLI::write(' ..basic info');
-
-
- /***************/
- /* hunter pets */
- /***************/
-
- if ((1 << ($char['class'] - 1)) == CLASS_HUNTER)
- {
- DB::Aowow()->query('DELETE FROM ?_profiler_pets WHERE owner = ?d', $profileId);
- $pets = DB::Characters($realmId)->select('SELECT id AS ARRAY_KEY, id, entry, modelId, name FROM character_pet WHERE owner = ?d', $charGuid);
- foreach ($pets as $petGuid => $petData)
- {
- $morePet = DB::Aowow()->selectRow('SELECT p.`type`, c.family FROM ?_pet p JOIN ?_creature c ON c.family = p.id WHERE c.id = ?d', $petData['entry']);
- $petSpells = DB::Characters($realmId)->selectCol('SELECT spell FROM pet_spell WHERE guid = ?d', $petGuid);
-
- $_ = DB::Aowow()->selectCol('SELECT spell AS ARRAY_KEY, MAX(IF(spell in (?a), rank, 0)) FROM ?_talents WHERE class = 0 AND petTypeMask = ?d GROUP BY id ORDER BY row, col ASC', $petSpells ?: [0], 1 << $morePet['type']);
- $pet = array(
- 'id' => $petGuid,
- 'owner' => $profileId,
- 'name' => $petData['name'],
- 'family' => $morePet['family'],
- 'npc' => $petData['entry'],
- 'displayId' => $petData['modelId'],
- 'talents' => implode('', $_)
- );
-
- DB::Aowow()->query('INSERT INTO ?_profiler_pets (?#) VALUES (?a)', array_keys($pet), array_values($pet));
- }
-
- CLI::write(' ..hunter pets');
- }
-
-
- /*******************/
- /* completion data */
- /*******************/
-
- DB::Aowow()->query('DELETE FROM ?_profiler_completion WHERE id = ?d', $profileId);
-
- // done quests
- if ($quests = DB::Characters($realmId)->select('SELECT ?d AS id, ?d AS `type`, quest AS typeId FROM character_queststatus_rewarded WHERE guid = ?d', $profileId, TYPE_QUEST, $char['guid']))
- foreach (Util::createSqlBatchInsert($quests) as $q)
- DB::Aowow()->query('INSERT INTO ?_profiler_completion (?#) VALUES '.$q, array_keys($quests[0]));
-
- CLI::write(' ..quests');
-
-
- // known skills (professions only)
- $skAllowed = DB::Aowow()->selectCol('SELECT id FROM ?_skillline WHERE typeCat IN (9, 11) AND (cuFlags & ?d) = 0', CUSTOM_EXCLUDE_FOR_LISTVIEW);
- $skills = DB::Characters($realmId)->select('SELECT ?d AS id, ?d AS `type`, skill AS typeId, `value` AS cur, max FROM character_skills WHERE guid = ?d AND skill IN (?a)', $profileId, TYPE_SKILL, $char['guid'], $skAllowed);
-
- // manually apply racial profession bonuses
- foreach ($skills as &$sk)
- {
- // Blood Elves - Arcane Affinity
- if ($sk['typeId'] == 333 && $char['race'] == 10)
- {
- $sk['cur'] += 10;
- $sk['max'] += 10;
- }
- // Draenei - Gemcutting
- if ($sk['typeId'] == 755 && $char['race'] == 11)
- {
- $sk['cur'] += 5;
- $sk['max'] += 5;
- }
- // Tauren - Cultivation
- // Gnomes - Engineering Specialization
- if (($sk['typeId'] == 182 && $char['race'] == 6) ||
- ($sk['typeId'] == 202 && $char['race'] == 7))
- {
- $sk['cur'] += 15;
- $sk['max'] += 15;
- }
- }
- unset($sk);
-
- if ($skills)
- foreach (Util::createSqlBatchInsert($skills) as $sk)
- DB::Aowow()->query('INSERT INTO ?_profiler_completion (?#) VALUES '.$sk, array_keys($skills[0]));
-
- CLI::write(' ..professions');
-
-
- // reputation
- if ($reputation = DB::Characters($realmId)->select('SELECT ?d AS id, ?d AS `type`, faction AS typeId, standing AS cur FROM character_reputation WHERE guid = ?d AND (flags & 0xC) = 0', $profileId, TYPE_FACTION, $char['guid']))
- foreach (Util::createSqlBatchInsert($reputation) as $rep)
- DB::Aowow()->query('INSERT INTO ?_profiler_completion (?#) VALUES '.$rep, array_keys($reputation[0]));
-
- CLI::write(' ..reputation');
-
-
- // known titles
- $tBlocks = explode(' ', $char['knownTitles']);
- $indizes = [];
- for ($i = 0; $i < 6; $i++)
- for ($j = 0; $j < 32; $j++)
- if ($tBlocks[$i] & (1 << $j))
- $indizes[] = $j + ($i * 32);
-
- if ($indizes)
- DB::Aowow()->query('INSERT INTO ?_profiler_completion SELECT ?d, ?d, id, NULL, NULL FROM ?_titles WHERE bitIdx IN (?a)', $profileId, TYPE_TITLE, $indizes);
-
- CLI::write(' ..titles');
-
-
- // achievements
- if ($achievements = DB::Characters($realmId)->select('SELECT ?d AS id, ?d AS `type`, achievement AS typeId, date AS cur FROM character_achievement WHERE guid = ?d', $profileId, TYPE_ACHIEVEMENT, $char['guid']))
- {
- foreach (Util::createSqlBatchInsert($achievements) as $a)
- DB::Aowow()->query('INSERT INTO ?_profiler_completion (?#) VALUES '.$a, array_keys($achievements[0]));
-
- $data['achievementpoints'] = DB::Aowow()->selectCell('SELECT SUM(points) FROM ?_achievement WHERE id IN (?a)', array_column($achievements, 'typeId'));
- }
-
- CLI::write(' ..achievements');
-
-
- // raid progression
- if ($progress = DB::Characters($realmId)->select('SELECT ?d AS id, ?d AS `type`, criteria AS typeId, date AS cur, counter AS `max` FROM character_achievement_progress WHERE guid = ?d AND criteria IN (?a)', $profileId, TYPE_ACHIEVEMENT, $char['guid'], self::$raidProgression))
- {
- array_walk($progress, function (&$val) { $val['typeId'] = array_search($val['typeId'], self::$raidProgression); });
- foreach (Util::createSqlBatchInsert($progress) as $p)
- DB::Aowow()->query('INSERT INTO ?_profiler_completion (?#) VALUES '.$p, array_keys($progress[0]));
- }
-
- CLI::write(' ..raid progression');
-
-
- // known spells
- if ($spells = DB::Characters($realmId)->select('SELECT ?d AS id, ?d AS `type`, spell AS typeId FROM character_spell WHERE guid = ?d AND disabled = 0', $profileId, TYPE_SPELL, $char['guid']))
- foreach (Util::createSqlBatchInsert($spells) as $s)
- DB::Aowow()->query('INSERT INTO ?_profiler_completion (?#) VALUES '.$s, array_keys($spells[0]));
-
- CLI::write(' ..known spells (vanity pets & mounts)');
-
-
- /****************/
- /* related data */
- /****************/
-
- // guilds
- if ($guild = DB::Characters($realmId)->selectRow('SELECT g.name AS name, g.guildid AS id, gm.rank FROM guild_member gm JOIN guild g ON g.guildid = gm.guildid WHERE gm.guid = ?d', $char['guid']))
- {
- $guildId = 0;
- if (!($guildId = DB::Aowow()->selectCell('SELECT id FROM ?_profiler_guild WHERE realm = ?d AND realmGUID = ?d', $realmId, $guild['id'])))
- {
- $gData = array( // only most basic data
- 'realm' => $realmId,
- 'realmGUID' => $guild['id'],
- 'name' => $guild['name'],
- 'nameUrl' => self::urlize($guild['name']),
- 'cuFlags' => PROFILER_CU_NEEDS_RESYNC
- );
-
- $guildId = DB::Aowow()->query('INSERT IGNORE INTO ?_profiler_guild (?#) VALUES (?a)', array_keys($gData), array_values($gData));
- }
-
- $data['guild'] = $guildId;
- $data['guildRank'] = $guild['rank'];
- }
-
-
- // arena teams
- $teams = DB::Characters($realmId)->select('SELECT at.arenaTeamId AS ARRAY_KEY, at.name, at.type, IF(at.captainGuid = atm.guid, 1, 0) AS captain, atm.* FROM arena_team at JOIN arena_team_member atm ON atm.arenaTeamId = at.arenaTeamId WHERE atm.guid = ?d', $char['guid']);
- foreach ($teams as $rGuid => $t)
- {
- $teamId = 0;
- if (!($teamId = DB::Aowow()->selectCell('SELECT id FROM ?_profiler_arena_team WHERE realm = ?d AND realmGUID = ?d', $realmId, $rGuid)))
- {
- $team = array( // only most basic data
- 'realm' => $realmId,
- 'realmGUID' => $rGuid,
- 'name' => $t['name'],
- 'nameUrl' => self::urlize($t['name']),
- 'type' => $t['type'],
- 'cuFlags' => PROFILER_CU_NEEDS_RESYNC
- );
-
- $teamId = DB::Aowow()->query('INSERT IGNORE INTO ?_profiler_arena_team (?#) VALUES (?a)', array_keys($team), array_values($team));
- }
-
- $member = array(
- 'arenaTeamId' => $teamId,
- 'profileId' => $profileId,
- 'captain' => $t['captain'],
- 'weekGames' => $t['weekGames'],
- 'weekWins' => $t['weekWins'],
- 'seasonGames' => $t['seasonGames'],
- 'seasonWins' => $t['seasonWins'],
- 'personalRating' => $t['personalRating']
- );
-
- DB::Aowow()->query('INSERT INTO ?_profiler_arena_team_member (?#) VALUES (?a) ON DUPLICATE KEY UPDATE ?a', array_keys($member), array_values($member), array_slice($member, 2));
- }
-
- CLI::write(' ..associated arena teams');
-
- /*********************/
- /* mark char as done */
- /*********************/
-
- if (DB::Aowow()->query('UPDATE ?_profiler_profiles SET ?a WHERE realm = ?d AND realmGUID = ?d', $data, $realmId, $charGuid) !== null)
- DB::Aowow()->query('UPDATE ?_profiler_profiles SET cuFlags = cuFlags & ?d WHERE id = ?d', ~PROFILER_CU_NEEDS_RESYNC, $profileId);
-
- return true;
- }
-
- public static function getGuildFromRealm($realmId, $guildGuid)
- {
- $guild = DB::Characters($realmId)->selectRow('SELECT guildId, name, createDate, info, backgroundColor, emblemStyle, emblemColor, borderStyle, borderColor FROM guild WHERE guildId = ?d', $guildGuid);
- if (!$guild)
- return false;
-
- // reminder: this query should not fail: a placeholder entry is created as soon as a team listview is created or team detail page is called
- $guildId = DB::Aowow()->selectCell('SELECT id FROM ?_profiler_guild WHERE realm = ?d AND realmGUID = ?d', $realmId, $guild['guildId']);
-
- CLI::write('fetching guild #'.$guildGuid.' from realm #'.$realmId);
- CLI::write('writing...');
-
-
- /**************/
- /* Guild Data */
- /**************/
-
- unset($guild['guildId']);
- $guild['nameUrl'] = self::urlize($guild['name']);
-
- DB::Aowow()->query('UPDATE ?_profiler_guild SET ?a WHERE realm = ?d AND realmGUID = ?d', $guild, $realmId, $guildGuid);
-
- // ranks
- DB::Aowow()->query('DELETE FROM ?_profiler_guild_rank WHERE guildId = ?d', $guildId);
- if ($ranks = DB::Characters($realmId)->select('SELECT ?d AS guildId, rid AS rank, rname AS name FROM guild_rank WHERE guildid = ?d', $guildId, $guildGuid))
- foreach (Util::createSqlBatchInsert($ranks) as $r)
- DB::Aowow()->query('INSERT INTO ?_profiler_guild_rank (?#) VALUES '.$r, array_keys(reset($ranks)));
-
- CLI::write(' ..guild data');
-
-
- /***************/
- /* Member Data */
- /***************/
-
- $conditions = array(
- ['g.guildid', $guildGuid],
- ['deleteInfos_Account', null],
- ['level', MAX_LEVEL, '<='], // prevents JS errors
- [['extra_flags', self::CHAR_GMFLAGS, '&'], 0] // not a staff char
- );
-
- // this here should all happen within ProfileList
- $members = new RemoteProfileList($conditions, ['sv' => $realmId]);
- if (!$members->error)
- $members->initializeLocalEntries();
- else
- return false;
-
- CLI::write(' ..guild members');
-
-
- /*********************/
- /* mark guild as done */
- /*********************/
-
- DB::Aowow()->query('UPDATE ?_profiler_guild SET cuFlags = cuFlags & ?d WHERE id = ?d', ~PROFILER_CU_NEEDS_RESYNC, $guildId);
-
- return true;
- }
-
- public static function getArenaTeamFromRealm($realmId, $teamGuid)
- {
- $team = DB::Characters($realmId)->selectRow('SELECT arenaTeamId, name, type, captainGuid, rating, seasonGames, seasonWins, weekGames, weekWins, rank, backgroundColor, emblemStyle, emblemColor, borderStyle, borderColor FROM arena_team WHERE arenaTeamId = ?d', $teamGuid);
- if (!$team)
- return false;
-
- // reminder: this query should not fail: a placeholder entry is created as soon as a team listview is created or team detail page is called
- $teamId = DB::Aowow()->selectCell('SELECT id FROM ?_profiler_arena_team WHERE realm = ?d AND realmGUID = ?d', $realmId, $team['arenaTeamId']);
-
- CLI::write('fetching arena team #'.$teamGuid.' from realm #'.$realmId);
- CLI::write('writing...');
-
-
- /*************/
- /* Team Data */
- /*************/
-
- $captain = $team['captainGuid'];
- unset($team['captainGuid']);
- unset($team['arenaTeamId']);
- $team['nameUrl'] = self::urlize($team['name']);
-
- DB::Aowow()->query('UPDATE ?_profiler_arena_team SET ?a WHERE realm = ?d AND realmGUID = ?d', $team, $realmId, $teamGuid);
-
- CLI::write(' ..team data');
-
-
- /***************/
- /* Member Data */
- /***************/
-
- $members = DB::Characters($realmId)->select('
- SELECT
- atm.guid AS ARRAY_KEY, atm.arenaTeamId, atm.weekGames, atm.weekWins, atm.seasonGames, atm.seasonWins, atm.personalrating
- FROM
- arena_team_member atm
- JOIN
- characters c ON c.guid = atm.guid AND
- c.deleteInfos_Account IS NULL AND
- c.level <= ?d AND
- (c.extra_flags & ?d) = 0
- WHERE
- arenaTeamId = ?d',
- MAX_LEVEL,
- self::CHAR_GMFLAGS,
- $teamGuid
- );
-
- $conditions = array(
- ['c.guid', array_keys($members)],
- ['deleteInfos_Account', null],
- ['level', MAX_LEVEL, '<='], // prevents JS errors
- [['extra_flags', self::CHAR_GMFLAGS, '&'], 0] // not a staff char
- );
-
- $mProfiles = new RemoteProfileList($conditions, ['sv' => $realmId]);
- if (!$mProfiles->error)
- {
- $mProfiles->initializeLocalEntries();
- foreach ($mProfiles->iterate() as $__)
- {
-
- $mGuid = $mProfiles->getField('guid');
-
- $members[$mGuid]['arenaTeamId'] = $teamId;
- $members[$mGuid]['captain'] = (int)($mGuid == $captain);
- $members[$mGuid]['profileId'] = $mProfiles->getField('id');
- }
-
- DB::Aowow()->query('DELETE FROM ?_profiler_arena_team_member WHERE arenaTeamId = ?d', $teamId);
-
- foreach (Util::createSqlBatchInsert($members) as $m)
- DB::Aowow()->query('INSERT INTO ?_profiler_arena_team_member (?#) VALUES '.$m, array_keys(reset($members)));
-
- }
- else
- return false;
-
- CLI::write(' ..team members');
-
- /*********************/
- /* mark team as done */
- /*********************/
-
- DB::Aowow()->query('UPDATE ?_profiler_arena_team SET cuFlags = cuFlags & ?d WHERE id = ?d', ~PROFILER_CU_NEEDS_RESYNC, $teamId);
-
- return true;
- }
-}
-
-?>
diff --git a/includes/setup/cli.class.php b/includes/setup/cli.class.php
new file mode 100644
index 00000000..ef16296f
--- /dev/null
+++ b/includes/setup/cli.class.php
@@ -0,0 +1,350 @@
+ $row)
+ {
+ if (!is_array($out[0]))
+ {
+ unset($out[$i]);
+ continue;
+ }
+
+ $nCols = max($nCols, count($row));
+
+ for ($j = 0; $j < $nCols; $j++)
+ $pads[$j] = max($pads[$j] ?? 0, mb_strlen(self::purgeEscapes($row[$j] ?? '')));
+ }
+
+ foreach ($out as $i => $row)
+ {
+ for ($j = 0; $j < $nCols; $j++)
+ {
+ if (!isset($row[$j]))
+ break;
+
+ $len = ($pads[$j] - mb_strlen(self::purgeEscapes($row[$j])));
+ for ($k = 0; $k < $len; $k++) // can't use str_pad(). it counts invisible chars.
+ $row[$j] .= ' ';
+ }
+
+ if ($i || $headless)
+ self::write(' '.implode(' ' . self::tblDelim(' ') . ' ', $row), self::LOG_NONE, $timestamp);
+ else
+ self::write(self::tblHead(' '.implode(' ', $row)), self::LOG_NONE, $timestamp);
+ }
+
+ if (!$headless)
+ self::write(self::tblHead(str_pad('', array_sum($pads) + count($pads) * 3 - 2)), self::LOG_NONE, $timestamp);
+
+ self::write();
+ }
+
+
+ /***********/
+ /* logging */
+ /***********/
+
+ public static function initLogFile(string $file = '') : void
+ {
+ if (!$file)
+ return;
+
+ $file = self::nicePath($file);
+ if (!file_exists($file))
+ self::$logHandle = fopen($file, 'w');
+ else
+ {
+ $logFileParts = pathinfo($file);
+
+ $i = 1;
+ while (file_exists($logFileParts['dirname'].'/'.$logFileParts['filename'].$i.(isset($logFileParts['extension']) ? '.'.$logFileParts['extension'] : '')))
+ $i++;
+
+ $file = $logFileParts['dirname'].'/'.$logFileParts['filename'].$i.(isset($logFileParts['extension']) ? '.'.$logFileParts['extension'] : '');
+ self::$logHandle = fopen($file, 'w');
+ }
+ }
+
+ private static function tblHead(string $str) : string
+ {
+ return CLI_HAS_E ? "\e[1;48;5;236m".$str."\e[0m" : $str;
+ }
+
+ private static function tblDelim(string $str) : string
+ {
+ return CLI_HAS_E ? "\e[48;5;236m".$str."\e[0m" : $str;
+ }
+
+ public static function grey(string $str) : string
+ {
+ return CLI_HAS_E ? "\e[90m".$str."\e[0m" : $str;
+ }
+
+ public static function red(string $str) : string
+ {
+ return CLI_HAS_E ? "\e[31m".$str."\e[0m" : $str;
+ }
+
+ public static function green(string $str) : string
+ {
+ return CLI_HAS_E ? "\e[32m".$str."\e[0m" : $str;
+ }
+
+ public static function yellow(string $str) : string
+ {
+ return CLI_HAS_E ? "\e[33m".$str."\e[0m" : $str;
+ }
+
+ public static function blue(string $str) : string
+ {
+ return CLI_HAS_E ? "\e[36m".$str."\e[0m" : $str;
+ }
+
+ public static function bold(string $str) : string
+ {
+ return CLI_HAS_E ? "\e[1m".$str."\e[0m" : $str;
+ }
+
+ public static function write(string $txt = '', int $lvl = self::LOG_BLANK, bool $timestamp = true, bool $tmpRow = false) : void
+ {
+ $msg = '';
+ if ($txt)
+ {
+ if ($timestamp)
+ $msg = str_pad(date('H:i:s'), 10);
+
+ $msg .= match ($lvl)
+ {
+ self::LOG_ERROR => '['.self::red('ERR').'] ', // red critical error
+ self::LOG_WARN => '['.self::yellow('WARN').'] ', // yellow notice
+ self::LOG_OK => '['.self::green('OK').'] ', // green success
+ self::LOG_INFO => '['.self::blue('INFO').'] ', // blue info
+ default => ' '
+ };
+
+ $msg .= $txt;
+ }
+
+ // https://shiroyasha.svbtle.com/escape-sequences-a-quick-guide-1#movement_1
+ $msg = (self::$overwriteLast && CLI_HAS_E ? "\e[1G\e[0K" : "\n") . $msg;
+ self::$overwriteLast = $tmpRow;
+
+ fwrite($lvl == self::LOG_ERROR ? STDERR : STDOUT, $msg);
+
+ if (self::$logHandle) // remove control sequences from log
+ fwrite(self::$logHandle, self::purgeEscapes($msg));
+
+ flush();
+ }
+
+ private static function purgeEscapes(string $msg) : string
+ {
+ return preg_replace(["/\e\[[\d;]+[mK]/", "/\e\[\d+G/"], ['', "\n"], $msg);
+ }
+
+ public static function nicePath(string $fileOrPath, string ...$pathParts) : string
+ {
+ $path = '';
+
+ if ($pathParts)
+ {
+ foreach ($pathParts as &$pp)
+ $pp = trim($pp);
+
+ $path .= implode(DIRECTORY_SEPARATOR, $pathParts);
+ }
+
+ $path .= ($path ? DIRECTORY_SEPARATOR : '').trim($fileOrPath);
+
+ // remove double quotes (from erroneous user input), single quotes are
+ // valid chars for filenames and removing those mutilates several wow icons
+ $path = str_replace('"', '', $path);
+
+ if (!$path) // empty strings given. (faulty dbc data?)
+ return '';
+
+ if (DIRECTORY_SEPARATOR == '/') // *nix
+ {
+ $path = str_replace('\\', '/', $path);
+ $path = preg_replace('/\/+/i', '/', $path);
+ }
+ else if (DIRECTORY_SEPARATOR == '\\') // win
+ {
+ $path = str_replace('/', '\\', $path);
+ $path = preg_replace('/\\\\+/i', '\\', $path);
+ }
+ else
+ self::write('Dafuq! Your directory separator is "'.DIRECTORY_SEPARATOR.'". Please report this!', self::LOG_ERROR);
+
+ // resolve *nix home shorthand
+ if (!OS_WIN)
+ {
+ if (preg_match('/^~(\w+)\/.*/i', $path, $m))
+ $path = '/home/'.substr($path, 1);
+ else if (substr($path, 0, 2) == '~/')
+ $path = getenv('HOME').substr($path, 1);
+ else if ($path[0] == DIRECTORY_SEPARATOR && substr($path, 0, 6) != '/home/')
+ $path = substr($path, 1);
+ }
+
+ return $path;
+ }
+
+
+ /**************/
+ /* read input */
+ /**************/
+
+ /*
+ since the CLI on WIN ist not interactive, the following things have to be considered
+ you do not receive keystrokes but whole strings upon pressing (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 read(array $fields, ?array &$userInput = []) : bool
+ {
+ // first time set
+ if (self::$hasReadline === null)
+ self::$hasReadline = function_exists('readline_callback_handler_install');
+
+ // prevent default output if able
+ if (self::$hasReadline)
+ readline_callback_handler_install('', function() { });
+
+ if (!STDIN)
+ return false;
+
+ stream_set_blocking(STDIN, false);
+
+ // pad default values onto $fields
+ array_walk($fields, function(&$val, $_, $pad) { $val += $pad; }, ['', false, false, '']);
+
+ foreach ($fields as $name => [$desc, $isHidden, $singleChar, $validPattern])
+ {
+ $charBuff = '';
+
+ if ($desc)
+ fwrite(STDOUT, "\n".$desc.": ");
+
+ while (true) {
+ if (feof(STDIN))
+ return false;
+
+ $r = [STDIN];
+ $w = $e = null;
+ $n = stream_select($r, $w, $e, 200000);
+
+ if (!$n || !in_array(STDIN, $r))
+ continue;
+
+ // stream_get_contents is always blocking under WIN - fgets should work similary as php always receives a terminated line of text
+ $chars = str_split(OS_WIN ? fgets(STDIN) : stream_get_contents(STDIN));
+
+ // $chars can be empty if used non-interactive
+ if (!$chars)
+ return false;
+
+ $ordinals = array_map('ord', $chars);
+
+ if ($ordinals[0] == self::CHR_ESC)
+ {
+ if (count($ordinals) == 1)
+ {
+ fwrite(STDOUT, chr(self::CHR_BELL));
+ return false;
+ }
+ else
+ continue;
+ }
+
+ foreach ($chars as $idx => $char)
+ {
+ $keyId = $ordinals[$idx];
+
+ // skip char if horizontal tab or \r if followed by \n
+ if ($keyId == self::CHR_TAB || ($keyId == self::CHR_CR && ($ordinals[$idx + 1] ?? '') == self::CHR_LF))
+ continue;
+
+ if ($keyId == self::CHR_BACKSPACE)
+ {
+ if (!$charBuff)
+ continue 2;
+
+ $charBuff = mb_substr($charBuff, 0, -1);
+ if (!$isHidden && self::$hasReadline)
+ fwrite(STDOUT, chr(self::CHR_BACK)." ".chr(self::CHR_BACK));
+ }
+ // standalone \n or \r
+ else if ($keyId == self::CHR_LF || $keyId == self::CHR_CR)
+ {
+ $userInput[$name] = $charBuff;
+ break 2;
+ }
+ else if (!$validPattern || preg_match($validPattern, $char))
+ {
+ $charBuff .= $char;
+ if (!$isHidden && self::$hasReadline)
+ fwrite(STDOUT, $char);
+
+ if ($singleChar && self::$hasReadline)
+ {
+ $userInput[$name] = $charBuff;
+ break 2;
+ }
+ }
+ }
+ }
+ }
+
+ fwrite(STDOUT, chr(self::CHR_BELL));
+
+ foreach ($userInput as $ui)
+ if (strlen($ui))
+ return true;
+
+ $userInput = null;
+ return true;
+ }
+}
+
+?>
diff --git a/includes/setup/datatypes/primitives.php b/includes/setup/datatypes/primitives.php
new file mode 100644
index 00000000..2bf355bc
--- /dev/null
+++ b/includes/setup/datatypes/primitives.php
@@ -0,0 +1,104 @@
+data = $data;
+ else
+ $this->data = $data->read(static::SIZE);
+ }
+
+ public function pack() : string
+ {
+ return $this->data;
+ }
+
+ public function unpack() : mixed
+ {
+ return current(unpack(static::PACK_FMT, $this->data));
+ }
+
+ public function __debugInfo() : array
+ {
+ return [$this->unpack()];
+ }
+}
+
+class Char extends Primitive
+{
+ public const /* int */ SIZE = 1;
+ public const /* string */ PACK_FMT = 'C';
+
+ public function unpack() : string
+ {
+ return chr(parent::unpack());
+ }
+}
+
+class Boolean extends Primitive
+{
+ public const /* int */ SIZE = 1;
+ public const /* string */ PACK_FMT = 'C';
+
+ public function unpack() : string
+ {
+ return !!(parent::unpack());
+ }
+}
+
+class UInt8 extends Primitive
+{
+ public const /* int */ SIZE = 1;
+ public const /* string */ PACK_FMT = 'C';
+}
+
+class Int8 extends Primitive
+{
+ public const /* int */ SIZE = 1;
+ public const /* string */ PACK_FMT = 'c';
+}
+
+class UInt16 extends Primitive
+{
+ public const /* int */ SIZE = 2;
+ public const /* string */ PACK_FMT = 'v';
+}
+
+class Int16 extends Primitive
+{
+ public const /* int */ SIZE = 2;
+ public const /* string */ PACK_FMT = 's';
+}
+
+class UInt32 extends Primitive
+{
+ public const /* int */ SIZE = 4;
+ public const /* string */ PACK_FMT = 'V';
+}
+
+class Int32 extends Primitive
+{
+ public const /* int */ SIZE = 4;
+ public const /* string */ PACK_FMT = 'l';
+}
+
+class Double extends Primitive
+{
+ public const /* int */ SIZE = 4;
+ public const /* string */ PACK_FMT = 'f';
+}
+
+?>
diff --git a/includes/setup/files/binaryfile.class.php b/includes/setup/files/binaryfile.class.php
new file mode 100644
index 00000000..c34dba3d
--- /dev/null
+++ b/includes/setup/files/binaryfile.class.php
@@ -0,0 +1,202 @@
+error = 'file '.$file.' not found';
+ return;
+ }
+
+ if (!$this->handle = fopen($file, 'rb'))
+ {
+ $this->error = 'failed to open file '.$file;
+ return;
+ }
+
+ $this->filesize = filesize($file);
+
+ if ($inRAM)
+ $this->data = file_get_contents($file);
+ }
+
+ public function __destruct()
+ {
+ $this->close();
+ }
+
+
+ /**********************/
+ /* direct file access */
+ /**********************/
+
+ public function read(int $bytes) : ?string
+ {
+ if ($this->error || !is_resource($this->handle) || $bytes < 0)
+ return null;
+
+ $start = $this->pos;
+ $this->pos += $bytes;
+
+ if ($this->inRAM)
+ return substr($this->data, $start, $bytes);
+ else
+ return fread($this->handle, $bytes);
+ }
+
+ public function readOffset(int $bytes, int $offset, bool $jumpBack = true) : ?string
+ {
+ if ($this->error || !is_resource($this->handle))
+ return null;
+
+ if ($jumpBack)
+ $curPos = $this->inRAM ? $this->pos : ftell($this->handle);
+
+ $this->seek($offset);
+
+ $str = $this->read($bytes);
+
+ if ($jumpBack)
+ $this->seek($curPos);
+
+ return $str;
+ }
+
+ public function seek(int $pos) : int
+ {
+ if (!is_resource($this->handle))
+ return 0;
+
+ if ($pos < 0)
+ $pos = 0;
+ if ($pos > $this->filesize)
+ $pos = $this->filesize;
+
+ $this->pos = $pos;
+
+ if (!$this->inRAM)
+ fseek($this->handle, $pos, SEEK_SET);
+
+ return $pos;
+ }
+
+ public function ffwd(int $bytes) : int
+ {
+ if (!is_resource($this->handle))
+ return 0;
+
+ $curPos = $this->inRAM ? $this->pos : ftell($this->handle);
+
+ if ($curPos + $bytes < 0)
+ $bytes -= $curPos;
+ if ($curPos + $bytes > $this->filesize)
+ $bytes -= $this->filesize;
+
+ $this->pos += $bytes;
+
+ if ($this->inRAM)
+ return $this->pos;
+
+ fseek($this->handle, $bytes, SEEK_CUR);
+ return ftell($this->handle);
+ }
+
+ public function close() : void
+ {
+ if (is_resource($this->handle))
+ fclose($this->handle);
+ }
+
+ public function tell() : int
+ {
+ if (!is_resource($this->handle))
+ return 0;
+
+ return $this->inRAM ? $this->pos : ftell($this->handle);
+ }
+
+ /******************/
+ /* read Primitive */
+ /******************/
+
+ public function readInt8() : ?Int8
+ {
+ if (!is_resource($this->handle))
+ return null;
+ return new Int8($this);
+ }
+
+ public function readInt16() : ?Int16
+ {
+ if (!is_resource($this->handle))
+ return null;
+ return new Int16($this);
+ }
+
+ public function readInt32() : ?Int32
+ {
+ if (!is_resource($this->handle))
+ return null;
+ return new Int32($this);
+ }
+
+ public function readUInt8() : ?UInt8
+ {
+ if (!is_resource($this->handle))
+ return null;
+ return new UInt8($this);
+ }
+
+ public function readUInt16() : ?UInt16
+ {
+ if (!is_resource($this->handle))
+ return null;
+ return new UInt16($this);
+ }
+
+ public function readUInt32() : ?UInt32
+ {
+ if (!is_resource($this->handle))
+ return null;
+ return new UInt32($this);
+ }
+
+ public function readFloat() : ?Double
+ {
+ if (!is_resource($this->handle))
+ return null;
+ return new Double($this);
+ }
+
+ public function readChar() : ?Char
+ {
+ if (!is_resource($this->handle))
+ return null;
+ return new Char($this);
+ }
+
+ public function readBool() : ?Boolean
+ {
+ if (!is_resource($this->handle))
+ return null;
+ return new Boolean($this);
+ }
+}
+
+?>
diff --git a/includes/setup/files/dbcfile.class.php b/includes/setup/files/dbcfile.class.php
new file mode 100644
index 00000000..25d9d170
--- /dev/null
+++ b/includes/setup/files/dbcfile.class.php
@@ -0,0 +1,83 @@
+filesize < strlen(self::MAGIC) + self::HEADER_SIZE)
+ {
+ $this->error = 'file '.$file.' too small for a dbc';
+ $this->close();
+ return;
+ }
+
+ if ($this->read(4) != self::MAGIC)
+ {
+ $this->error = 'file '.$file.' has incorrect magic bytes';
+ $this->close();
+ return;
+ }
+
+ [, $this->nRows, $this->nCols, $this->recordSize, $this->stringSize] = unpack(UInt32::PACK_FMT.'4', $this->read(self::HEADER_SIZE));
+ $this->stringOffset = strlen(self::MAGIC) + self::HEADER_SIZE + $this->recordSize * $this->nRows;
+
+ if ($this->stringOffset + $this->stringSize != $this->filesize)
+ {
+ $this->error = 'file '.$file.' has unexpected size - expected: '.($this->stringOffset + $this->stringSize).' has: '.$this->filesize;
+ $this->close();
+ return;
+ }
+ }
+
+ public function readRecord(string $colFmt = "V*") : array
+ {
+ return unpack($colFmt, $this->read($this->recordSize));
+ }
+
+ public function readString() : ?string
+ {
+ $x = $this->readUInt32();
+ if (is_null($x))
+ return null;
+
+ return $this->getStringFromBlock($x->unpack());
+ }
+
+ public function getStringFromBlock(int $offset) : ?string
+ {
+ $curPos = $this->tell();
+
+ $this->seek($this->stringOffset + $offset);
+
+ // apparently it is more efficient to read more than one byte at once..?
+ $str = '';
+ while (($pos = strpos($str, "\0")) === false)
+ $str .= $this->read(255);
+
+ $str = substr($str, 0, $pos);
+
+ $this->seek($curPos);
+
+ return $pos ? $str : null;
+ }
+}
+
+?>
diff --git a/includes/setup/timer.class.php b/includes/setup/timer.class.php
new file mode 100644
index 00000000..7c36620d
--- /dev/null
+++ b/includes/setup/timer.class.php
@@ -0,0 +1,39 @@
+intv = $intervall / 1000; // in msec
+ $this->t_cur = microtime(true);
+ }
+
+ public function update() : bool
+ {
+ $this->t_new = microtime(true);
+ if ($this->t_new > $this->t_cur + $this->intv)
+ {
+ $this->t_cur = $this->t_cur + $this->intv;
+ return true;
+ }
+
+ return false;
+ }
+
+ public function reset() : void
+ {
+ $this->t_cur = microtime(true) - $this->intv;
+ }
+}
+
+?>
diff --git a/includes/shared.php b/includes/shared.php
deleted file mode 100644
index 41f768d6..00000000
--- a/includes/shared.php
+++ /dev/null
@@ -1,26 +0,0 @@
-'.$r." was not found. Please check if it should exist, using \"php -m\"\n\n";
-
-if (version_compare(PHP_VERSION, '5.5.0') < 0)
- $error .= 'PHP Version 5.5.0 or higher required! Your version is '.PHP_VERSION.".\nCore functions are unavailable!\n";
-
-if ($error)
-{
- echo CLI ? strip_tags($error) : $error;
- die();
-}
-
-
-// include all necessities, set up basics
-require_once 'includes/kernel.php';
-
-?>
diff --git a/includes/type.class.php b/includes/type.class.php
new file mode 100644
index 00000000..6676009b
--- /dev/null
+++ b/includes/type.class.php
@@ -0,0 +1,260 @@
+ [CreatureList::class, 'npc', 'g_npcs', self::FLAG_RANDOM_SEARCHABLE | self::FLAG_FILTRABLE | self::FLAG_DB_TYPE],
+ self::OBJECT => [GameObjectList::class, 'object', 'g_objects', self::FLAG_RANDOM_SEARCHABLE | self::FLAG_FILTRABLE | self::FLAG_DB_TYPE],
+ self::ITEM => [ItemList::class, 'item', 'g_items', self::FLAG_RANDOM_SEARCHABLE | self::FLAG_FILTRABLE | self::FLAG_DB_TYPE | self::FLAG_HAS_ICON],
+ self::ITEMSET => [ItemsetList::class, 'itemset', 'g_itemsets', self::FLAG_RANDOM_SEARCHABLE | self::FLAG_FILTRABLE | self::FLAG_DB_TYPE],
+ self::QUEST => [QuestList::class, 'quest', 'g_quests', self::FLAG_RANDOM_SEARCHABLE | self::FLAG_FILTRABLE | self::FLAG_DB_TYPE],
+ self::SPELL => [SpellList::class, 'spell', 'g_spells', self::FLAG_RANDOM_SEARCHABLE | self::FLAG_FILTRABLE | self::FLAG_DB_TYPE | self::FLAG_HAS_ICON],
+ self::ZONE => [ZoneList::class, 'zone', 'g_gatheredzones', self::FLAG_RANDOM_SEARCHABLE | self::FLAG_DB_TYPE],
+ self::FACTION => [FactionList::class, 'faction', 'g_factions', self::FLAG_RANDOM_SEARCHABLE | self::FLAG_DB_TYPE],
+ self::PET => [PetList::class, 'pet', 'g_pets', self::FLAG_RANDOM_SEARCHABLE | self::FLAG_DB_TYPE | self::FLAG_HAS_ICON],
+ self::ACHIEVEMENT => [AchievementList::class, 'achievement', 'g_achievements', self::FLAG_RANDOM_SEARCHABLE | self::FLAG_FILTRABLE | self::FLAG_DB_TYPE | self::FLAG_HAS_ICON],
+ self::TITLE => [TitleList::class, 'title', 'g_titles', self::FLAG_RANDOM_SEARCHABLE | self::FLAG_DB_TYPE],
+ self::WORLDEVENT => [WorldEventList::class, 'event', 'g_holidays', self::FLAG_RANDOM_SEARCHABLE | self::FLAG_DB_TYPE | self::FLAG_HAS_ICON],
+ self::CHR_CLASS => [CharClassList::class, 'class', 'g_classes', self::FLAG_RANDOM_SEARCHABLE | self::FLAG_DB_TYPE],
+ self::CHR_RACE => [CharRaceList::class, 'race', 'g_races', self::FLAG_RANDOM_SEARCHABLE | self::FLAG_DB_TYPE],
+ self::SKILL => [SkillList::class, 'skill', 'g_skills', self::FLAG_RANDOM_SEARCHABLE | self::FLAG_DB_TYPE | self::FLAG_HAS_ICON],
+ self::STATISTIC => [AchievementList::class, 'achievement', 'g_achievements', self::FLAG_NONE], // alias for achievements; exists only for Markup
+ self::CURRENCY => [CurrencyList::class, 'currency', 'g_gatheredcurrencies', self::FLAG_RANDOM_SEARCHABLE | self::FLAG_DB_TYPE | self::FLAG_HAS_ICON],
+ self::SOUND => [SoundList::class, 'sound', 'g_sounds', self::FLAG_RANDOM_SEARCHABLE | self::FLAG_FILTRABLE | self::FLAG_DB_TYPE],
+ self::ICON => [IconList::class, 'icon', 'g_icons', self::FLAG_RANDOM_SEARCHABLE | self::FLAG_FILTRABLE | self::FLAG_DB_TYPE | self::FLAG_HAS_ICON],
+ self::GUIDE => [GuideList::class, 'guide', '', self::FLAG_DB_TYPE],
+ self::PROFILE => [ProfileList::class, 'profile', '', self::FLAG_FILTRABLE], // x - not known in javascript
+ self::GUILD => [GuildList::class, 'guild', '', self::FLAG_FILTRABLE], // x
+ self::ARENA_TEAM => [ArenaTeamList::class, 'arena-team', '', self::FLAG_FILTRABLE], // x
+ self::USER => [UserList::class, 'user', 'g_users', self::FLAG_NONE], // x
+ self::EMOTE => [EmoteList::class, 'emote', 'g_emotes', self::FLAG_RANDOM_SEARCHABLE | self::FLAG_DB_TYPE],
+ self::ENCHANTMENT => [EnchantmentList::class, 'enchantment', 'g_enchantments', self::FLAG_RANDOM_SEARCHABLE | self::FLAG_FILTRABLE | self::FLAG_DB_TYPE],
+ self::AREATRIGGER => [AreatriggerList::class, 'areatrigger', '', self::FLAG_FILTRABLE | self::FLAG_DB_TYPE],
+ self::MAIL => [MailList::class, 'mail', '', self::FLAG_RANDOM_SEARCHABLE | self::FLAG_DB_TYPE]
+ );
+
+
+ /********************/
+ /* Field Operations */
+ /********************/
+
+ public static function newList(int $type, array $conditions = []) : ?DBTypeList
+ {
+ if (!self::exists($type))
+ return null;
+
+ return new (self::$data[$type][self::IDX_LIST_OBJ])($conditions);
+ }
+
+ public static function newFilter(string $fileStr, array|string $data, array $opts = []) : ?Filter
+ {
+ $x = self::getFileStringsFor(self::FLAG_FILTRABLE);
+ if ($type = array_search($fileStr, $x))
+ return new (self::$data[$type][self::IDX_LIST_OBJ].'Filter')($data, $opts);
+
+ return null;
+ }
+
+ public static function validateIds(int $type, int|array $ids) : array
+ {
+ if (!self::exists($type))
+ return [];
+
+ if (!(self::$data[$type][self::IDX_FLAGS] & self::FLAG_DB_TYPE))
+ return [];
+
+ return DB::Aowow()->selectCol('SELECT `id` FROM %n WHERE `id` IN %in', self::$data[$type][self::IDX_LIST_OBJ]::$dataTable, (array)$ids);
+ }
+
+ public static function hasIcon(int $type) : bool
+ {
+ return self::exists($type) && self::$data[$type][self::IDX_FLAGS] & self::FLAG_HAS_ICON;
+ }
+
+ public static function isRandomSearchable(int $type) : bool
+ {
+ return self::exists($type) && self::$data[$type][self::IDX_FLAGS] & self::FLAG_RANDOM_SEARCHABLE;
+ }
+
+ public static function getFileString(int $type) : string
+ {
+ if (!self::exists($type))
+ return '';
+
+ return self::$data[$type][self::IDX_FILE_STR];
+ }
+
+ public static function getJSGlobalString(int $type) : string
+ {
+ if (!self::exists($type))
+ return '';
+
+ return self::$data[$type][self::IDX_JSG_TPL];
+ }
+
+ public static function getJSGlobalTemplate(int $type) : array
+ {
+ if (!self::exists($type) || !self::$data[$type][self::IDX_JSG_TPL])
+ return [];
+
+ // [key, [data], [extraData]]
+ return [self::$data[$type][self::IDX_JSG_TPL], [], []];
+ }
+
+ public static function checkClassAttrib(int $type, string $attr, ?int $attrVal = null) : bool
+ {
+ if (!self::exists($type))
+ return false;
+
+ return isset((self::$data[$type][self::IDX_LIST_OBJ])::$$attr) && ($attrVal === null || ((self::$data[$type][self::IDX_LIST_OBJ])::$$attr & $attrVal));
+ }
+
+ public static function getClassAttrib(int $type, string $attr) : mixed
+ {
+ if (!self::exists($type))
+ return null;
+
+ return (self::$data[$type][self::IDX_LIST_OBJ])::$$attr ?? null;
+ }
+
+ public static function exists(int $type) : ?int
+ {
+ return !empty(self::$data[$type]) ? $type : null;
+ }
+
+ public static function getIndexFrom(int $idx, string $match) : int
+ {
+ $i = array_search($match, array_column(self::$data, $idx));
+ if ($i === false)
+ return 0;
+
+ return array_keys(self::$data)[$i];
+ }
+
+
+ /*********************/
+ /* Column Operations */
+ /*********************/
+
+ public static function getClassesFor(int $flags = 0x0, string $attr = '', ?int $attrVal = null) : array
+ {
+ $x = [];
+ foreach (self::$data as $k => [$o, , , $f])
+ if ($o && (!$flags || $flags & $f))
+ if (!$attr || self::checkClassAttrib($k, $attr, $attrVal))
+ $x[$k] = $o;
+
+ return $x;
+ }
+
+ public static function getFileStringsFor(int $flags = 0x0) : array
+ {
+ $x = [];
+ foreach (self::$data as $k => [, $s, , $f])
+ if ($s && (!$flags || $flags & $f))
+ $x[$k] = $s;
+
+ return $x;
+ }
+
+ public static function getJSGTemplatesFor(int $flags = 0x0) : array
+ {
+ $x = [];
+ foreach (self::$data as $k => [, , $a, $f])
+ if ($a && (!$flags || $flags & $f))
+ $x[$k] = $a;
+
+ return $x;
+ }
+}
+
+?>
diff --git a/includes/types/arenateam.class.php b/includes/types/arenateam.class.php
deleted file mode 100644
index 7bb5ebc8..00000000
--- a/includes/types/arenateam.class.php
+++ /dev/null
@@ -1,346 +0,0 @@
-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/types/charrace.class.php b/includes/types/charrace.class.php
deleted file mode 100644
index 9fcfd4c5..00000000
--- a/includes/types/charrace.class.php
+++ /dev/null
@@ -1,52 +0,0 @@
-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
deleted file mode 100644
index 191f14ec..00000000
--- a/includes/types/creature.class.php
+++ /dev/null
@@ -1,553 +0,0 @@
- [['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
deleted file mode 100644
index f0437552..00000000
--- a/includes/types/currency.class.php
+++ /dev/null
@@ -1,87 +0,0 @@
- [['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
deleted file mode 100644
index 523777d9..00000000
--- a/includes/types/emote.class.php
+++ /dev/null
@@ -1,58 +0,0 @@
-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
deleted file mode 100644
index cb245818..00000000
--- a/includes/types/enchantment.class.php
+++ /dev/null
@@ -1,353 +0,0 @@
- 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/types/gameobject.class.php b/includes/types/gameobject.class.php
deleted file mode 100644
index 7838ac21..00000000
--- a/includes/types/gameobject.class.php
+++ /dev/null
@@ -1,247 +0,0 @@
- [['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/types/icon.class.php b/includes/types/icon.class.php
deleted file mode 100644
index 94ce25c0..00000000
--- a/includes/types/icon.class.php
+++ /dev/null
@@ -1,237 +0,0 @@
- '?_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
deleted file mode 100644
index c1d21903..00000000
--- a/includes/types/item.class.php
+++ /dev/null
@@ -1,2588 +0,0 @@
- 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
deleted file mode 100644
index 87fd3acb..00000000
--- a/includes/types/itemset.class.php
+++ /dev/null
@@ -1,259 +0,0 @@
- ['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/types/profile.class.php b/includes/types/profile.class.php
deleted file mode 100644
index 02561231..00000000
--- a/includes/types/profile.class.php
+++ /dev/null
@@ -1,711 +0,0 @@
-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
deleted file mode 100644
index 417d8797..00000000
--- a/includes/types/quest.class.php
+++ /dev/null
@@ -1,720 +0,0 @@
- [],
- '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
deleted file mode 100644
index 2aa2dd13..00000000
--- a/includes/types/skill.class.php
+++ /dev/null
@@ -1,81 +0,0 @@
- [['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
deleted file mode 100644
index 86001e76..00000000
--- a/includes/types/sound.class.php
+++ /dev/null
@@ -1,136 +0,0 @@
- '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
deleted file mode 100644
index 9bc9cb4a..00000000
--- a/includes/types/spell.class.php
+++ /dev/null
@@ -1,2542 +0,0 @@
- [ 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
deleted file mode 100644
index 00ac972a..00000000
--- a/includes/types/title.class.php
+++ /dev/null
@@ -1,174 +0,0 @@
- [['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
deleted file mode 100644
index 2a23413c..00000000
--- a/includes/types/user.class.php
+++ /dev/null
@@ -1,61 +0,0 @@
- [['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
deleted file mode 100644
index b124771c..00000000
--- a/includes/types/worldevent.class.php
+++ /dev/null
@@ -1,196 +0,0 @@
- [['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/user.class.php b/includes/user.class.php
index 9144eaed..9fa1bd59 100644
--- a/includes/user.class.php
+++ b/includes/user.class.php
@@ -1,448 +1,213 @@
selectRow('SELECT count, unbanDate FROM ?_account_bannedips WHERE ip = ? AND type = 0', self::$ip))
- {
- if ($ipBan['count'] > CFG_ACC_FAILED_AUTH_COUNT && $ipBan['unbanDate'] > time())
- return false;
- else if ($ipBan['unbanDate'] <= time())
- DB::Aowow()->query('DELETE FROM ?_account_bannedips WHERE ip = ?', self::$ip);
- }
-
- // try to restore session
- if (empty($_SESSION['user']))
- return false;
-
- // 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 (!$query)
- return false;
-
- // password changed, terminate session
- if (AUTH_MODE_SELF && $query['passHash'] != $_SESSION['hash'])
- {
- self::destroy();
- return false;
- }
-
- 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)
- {
- $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)
- self::$dailyVotes = self::getMaxDailyVotes();
-
- 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 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)
- // 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()->query('UPDATE ?_account SET consecutiveVisits = consecutiveVisits + 1 WHERE id = ?d', self::$id);
- else
- DB::Aowow()->query('UPDATE ?_account SET consecutiveVisits = 0 WHERE id = ?d', self::$id);
- }
- }
-
- return true;
- }
-
- 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)
+ foreach (['HTTP_CLIENT_IP', 'HTTP_X_FORWARDED_FOR', 'HTTP_X_FORWARDED', 'HTTP_FORWARDED_FOR', 'HTTP_FORWARDED', 'REMOTE_ADDR'] as $env)
{
- if ($rawIp = getenv($m))
+ if ($rawIp = getenv($env))
{
- if ($m == 'HTTP_X_FORWARDED')
+ if ($env == '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;
+ # set locale #
- // 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"]))
+ if (isset($_SESSION['locale']) && $_SESSION['locale'] instanceof Locale)
+ self::$preferedLoc = $_SESSION['locale']->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
+
+ 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))
{
- $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;
- }
+ if ($ipBan['count'] > Cfg::get('ACC_FAILED_AUTH_COUNT') && $ipBan['active'])
+ return false;
+ else if (!$ipBan['active'])
+ DB::Aowow()->qry('DELETE FROM ::account_bannedips WHERE `ip` = %s', self::$ip);
}
- // 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);
+ # try to restore session #
- self::useLocale($loc);
- }
+ if (empty($_SESSION['user']))
+ return false;
- // 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
+ $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`',
+ $_SESSION['user']
);
- 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)
+ if (!$session || !$userData)
{
- $min = 4;
- $max = 16;
+ self::destroy();
+ return false;
}
- else if (CFG_ACC_AUTH_MODE == AUTH_MODE_REALM)
+ else if ($session['expires'] && $session['expires'] < time())
{
- $min = 3;
- $max = 32;
+ 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;
}
- if (($min && mb_strlen($name) < $min) || ($max && mb_strlen($name) > $max))
- $errCode = 1;
- else if (preg_match('/[^\w\d\-]/i', $name))
- $errCode = 2;
+ 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());
- return $errCode == 0;
+ 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);
+ // 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)
+ self::$dailyVotes = self::getMaxDailyVotes();
+
+ DB::Aowow()->qry(
+ 'UPDATE ::account
+ SET `dailyVotes` = %i, `prevLogin` = `curLogin`, `curLogin` = UNIX_TIMESTAMP(), `prevIP` = `curIP`, `curIP` = ?
+ WHERE `id` = %i',
+ self::$dailyVotes,
+ self::$ip,
+ self::$id
+ );
+
+ // - gain reputation for daily visit
+ if (!(self::isBanned()) && !self::isInGroup(U_GROUP_PENDING))
+ Util::gainSiteReputation(self::$id, SITEREP_ACTION_DAILYVISIT);
+
+ // - increment consecutive visits (next day or first of new month and not more than 48h)
+ 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);
+ else
+ DB::Aowow()->qry('UPDATE ::account SET `consecutiveVisits` = 0 WHERE `id` = %i', self::$id);
+ }
+ }
+
+ return true;
}
- 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()
+ public static function save(bool $toDB = false)
{
$_SESSION['user'] = self::$id;
- $_SESSION['hash'] = self::$passHash;
- $_SESSION['locale'] = self::$localeId;
- $_SESSION['timeout'] = self::$expires ? time() + CFG_SESSION_TIMEOUT_DELAY : 0;
+ $_SESSION['locale'] = self::$preferedLoc;
// $_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()
@@ -450,124 +215,342 @@ class User
session_regenerate_id(true); // session itself is not destroyed; status changed => regenerate id
session_unset();
- $_SESSION['locale'] = self::$localeId; // keep locale
+ $_SESSION['locale'] = self::$preferedLoc; // keep locale
$_SESSION['dataKey'] = self::$dataKey; // keep dataKey
- self::$id = 0;
- self::$displayName = '';
- self::$perms = 0;
- self::$groups = U_GROUP_NONE;
+ self::$id = 0;
+ self::$username = '';
+ 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($group)
+ public static function isInGroup(int $group) : bool
{
- return (self::$groups & $group) != 0;
+ return $group == U_GROUP_NONE || (self::$groups & $group) != U_GROUP_NONE;
}
- public static function canComment()
+ public static function canComment() : bool
{
- if (!self::$id || self::$banStatus & (ACC_BAN_COMMENT | ACC_BAN_PERM | ACC_BAN_TEMP))
+ if (!self::isLoggedIn() || self::isBanned(ACC_BAN_COMMENT))
return false;
- return self::$perms || self::$reputation >= CFG_REP_REQ_COMMENT;
+ return self::$perms || self::$reputation >= Cfg::get('REP_REQ_COMMENT');
}
- public static function canReply()
+ public static function canReply() : bool
{
- if (!self::$id || self::$banStatus & (ACC_BAN_COMMENT | ACC_BAN_PERM | ACC_BAN_TEMP))
+ if (!self::isLoggedIn() || self::isBanned(ACC_BAN_COMMENT))
return false;
- return self::$perms || self::$reputation >= CFG_REP_REQ_REPLY;
+ return self::$perms || self::$reputation >= Cfg::get('REP_REQ_REPLY');
}
- public static function canUpvote()
+ public static function canUpvote() : bool
{
- if (!self::$id || self::$banStatus & (ACC_BAN_COMMENT | ACC_BAN_PERM | ACC_BAN_TEMP))
+ if (!self::isLoggedIn() || self::isBanned(ACC_BAN_COMMENT))
return false;
- return self::$perms || (self::$reputation >= CFG_REP_REQ_UPVOTE && self::$dailyVotes > 0);
+ return self::$perms || (self::$reputation >= Cfg::get('REP_REQ_UPVOTE') && self::$dailyVotes > 0);
}
- public static function canDownvote()
+ public static function canDownvote() : bool
{
- if (!self::$id || self::$banStatus & (ACC_BAN_RATE | ACC_BAN_PERM | ACC_BAN_TEMP))
+ if (!self::isLoggedIn() || self::isBanned(ACC_BAN_RATE))
return false;
- return self::$perms || (self::$reputation >= CFG_REP_REQ_DOWNVOTE && self::$dailyVotes > 0);
+ return self::$perms || (self::$reputation >= Cfg::get('REP_REQ_DOWNVOTE') && self::$dailyVotes > 0);
}
- public static function canSupervote()
+ public static function canSupervote() : bool
{
- if (!self::$id || self::$banStatus & (ACC_BAN_RATE | ACC_BAN_PERM | ACC_BAN_TEMP))
+ if (!self::isLoggedIn() || self::isBanned(ACC_BAN_RATE) || self::isInGroup(U_GROUP_PENDING))
return false;
- return self::$reputation >= CFG_REP_REQ_SUPERVOTE;
+ return self::$reputation >= Cfg::get('REP_REQ_SUPERVOTE');
}
- public static function canUploadScreenshot()
+ public static function canUploadScreenshot() : bool
{
- if (!self::$id || self::$banStatus & (ACC_BAN_SCREENSHOT | ACC_BAN_PERM | ACC_BAN_TEMP))
+ if (!self::isLoggedIn() || self::isBanned(ACC_BAN_SCREENSHOT) || self::isInGroup(U_GROUP_PENDING))
return false;
return true;
}
- public static function canSuggestVideo()
+ public static function canWriteGuide() : bool
{
- if (!self::$id || self::$banStatus & (ACC_BAN_VIDEO | ACC_BAN_PERM | ACC_BAN_TEMP))
+ if (!self::isLoggedIn() || self::isBanned(ACC_BAN_GUIDE) || self::isInGroup(U_GROUP_PENDING))
return false;
return true;
}
- public static function isPremium()
+ public static function canSuggestVideo() : bool
{
- return self::isInGroup(U_GROUP_PREMIUM) || self::$reputation >= CFG_REP_REQ_PREMIUM;
+ if (!self::isLoggedIn() || self::isBanned(ACC_BAN_VIDEO) || self::isInGroup(U_GROUP_PENDING))
+ return false;
+
+ return true;
}
+ 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()
+ public static function decrementDailyVotes() : void
{
+ if (!self::isLoggedIn() || self::isBanned(ACC_BAN_RATE))
+ return;
+
self::$dailyVotes--;
- DB::Aowow()->query('UPDATE ?_account SET dailyVotes = ?d WHERE id = ?d', self::$dailyVotes, self::$id);
+ DB::Aowow()->qry('UPDATE ::account SET `dailyVotes` = %i WHERE `id` = %i', self::$dailyVotes, self::$id);
}
- public static function getCurDailyVotes()
+ public static function getCurrentDailyVotes() : int
{
+ if (!self::isLoggedIn() || self::isBanned(ACC_BAN_RATE) || self::$dailyVotes < 0)
+ return 0;
+
return self::$dailyVotes;
}
- public static function getMaxDailyVotes()
+ public static function getMaxDailyVotes() : int
{
- if (!self::$id || self::$banStatus & (ACC_BAN_PERM | ACC_BAN_TEMP))
+ if (!self::isLoggedIn() || self::isBanned(ACC_BAN_RATE))
return 0;
- 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);
+ $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));
}
- public static function getReputation()
+ public static function getReputation() : int
{
+ if (!self::isLoggedIn() || self::$reputation < 0)
+ return 0;
+
return self::$reputation;
}
- public static function getUserGlobals()
+ public static function getUserGlobal() : array
{
$gUser = array(
'id' => self::$id,
- 'name' => self::$displayName,
+ 'name' => self::$username,
'roles' => self::$groups,
'permissions' => self::$perms,
'cookies' => []
);
- if (!self::$id || self::$banStatus & (ACC_BAN_TEMP | ACC_BAN_PERM))
+ if (!self::isLoggedIn() || self::isBanned())
return $gUser;
$gUser['commentban'] = !self::canComment();
@@ -575,11 +558,22 @@ class User
$gUser['canDownvote'] = self::canDownvote();
$gUser['canPostReplies'] = self::canReply();
$gUser['superCommentVotes'] = self::canSupervote();
- $gUser['downvoteRep'] = CFG_REP_REQ_DOWNVOTE;
- $gUser['upvoteRep'] = CFG_REP_REQ_UPVOTE;
+ $gUser['downvoteRep'] = Cfg::get('REP_REQ_DOWNVOTE');
+ $gUser['upvoteRep'] = Cfg::get('REP_REQ_UPVOTE');
$gUser['characters'] = self::getCharacters();
+ $gUser['completion'] = self::getCompletion();
$gUser['excludegroups'] = self::$excludeGroups;
- $gUser['settings'] = (new StdClass); // profiler requires this to be set; has property premiumborder (NYI)
+
+ 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} ?
if ($_ = self::getProfilerExclusions())
$gUser = array_merge($gUser, $_);
@@ -587,6 +581,9 @@ class User
if ($_ = self::getProfiles())
$gUser['profiles'] = $_;
+ if ($_ = self::getGuides())
+ $gUser['guides'] = $_;
+
if ($_ = self::getWeightScales())
$gUser['weightscales'] = $_;
@@ -596,58 +593,203 @@ class User
return $gUser;
}
- public static function getWeightScales()
+ public static function getWeightScales() : array
{
$result = [];
- $res = DB::Aowow()->selectCol('SELECT id AS ARRAY_KEY, name FROM ?_account_weightscales WHERE userId = ?d', self::$id);
+ if (!self::isLoggedIn() || self::isBanned())
+ return $result;
+
+ $res = DB::Aowow()->selectPairs('SELECT `id`, `name` FROM ::account_weightscales WHERE `userId` = %i', 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 (?a)', array_keys($res));
+ $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));
foreach ($weights as $id => $data)
$result[] = array_merge(['name' => $res[$id], 'id' => $id], $data);
return $result;
}
- public static function getProfilerExclusions()
+ public static function getProfilerExclusions() : array
{
$result = [];
- $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))
+
+ 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))
foreach ($ex as $type => $ids)
$result[$field][$type] = array_values($ids);
return $result;
}
- public static function getCharacters()
+ public static function getCharacters() : array
{
- if (!self::$profiles)
+ if (!self::loadProfiles())
return [];
return self::$profiles->getJSGlobals(PROFILEINFO_CHARACTER);
}
- public static function getProfiles()
+ public static function getProfiles() : array
{
- if (!self::$profiles)
+ if (!self::loadProfiles())
return [];
return self::$profiles->getJSGlobals(PROFILEINFO_PROFILE);
}
- public static function getCookies()
+ public static function getPinnedCharacter() : array
{
- $data = [];
+ if (!self::loadProfiles())
+ return [];
- if (self::$id)
- $data = DB::Aowow()->selectCol('SELECT name AS ARRAY_KEY, data FROM ?_account_cookies WHERE userId = ?d', self::$id);
+ $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];
+ }
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 35591265..39e9f297 100644
--- a/includes/utilities.php
+++ b/includes/utilities.php
@@ -1,325 +1,63 @@
$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
{
$node = dom_import_simplexml($this);
$no = $node->ownerDocument;
- $node->appendChild($no->createCDATASection($str));
+ $node->appendChild($no->createCDATASection($cData));
return $this;
}
}
-class CLI
+abstract class Util
{
- 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;
+ /* 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 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 const GEM_SCORE_BASE_WOTLK = 16; // rare quality wotlk gem score
+ private const GEM_SCORE_BASE_BC = 8; // rare quality bc gem score
private static $perfectGems = null;
- 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 $regions = array(
+ 'us', 'eu', 'kr', 'tw', 'cn', 'dev'
);
public static $ssdMaskFields = array(
@@ -331,23 +69,11 @@ 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(\'\', \'\')';
@@ -358,172 +84,65 @@ class Util
public static $mapSelectorString = '%s (%d)';
- 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 $expansionString = [null, 'bc', 'wotlk'];
public static $tcEncoding = '0zMcmVokRsaqbdrfwihuGINALpTjnyxtgevElBCDFHJKOPQSUWXYZ123456789';
- public static $wowheadLink = '';
private static $notes = [];
- public static function addNote($uGroupMask, $str)
+ public static function addNote(string $note, int $uGroupMask = U_GROUP_EMPLOYEE, int $level = LOG_LEVEL_ERROR) : void
{
- self::$notes[] = [$uGroupMask, $str];
+ self::$notes[] = [$note, $uGroupMask, $level];
}
- public static function getNotes()
+ public static function getNotes() : array
{
$notes = [];
+ $severity = LOG_LEVEL_INFO;
+ foreach (self::$notes as $k => [$note, $uGroup, $level])
+ {
+ if ($uGroup && !User::isInGroup($uGroup))
+ continue;
- foreach (self::$notes as $data)
- if (!$data[0] || User::isInGroup($data[0]))
- $notes[] = $data[1];
+ if ($level < $severity)
+ $severity = $level;
- return $notes;
+ $notes[] = $note;
+ unset(self::$notes[$k]);
+ }
+
+ return [$notes, $severity];
}
- private static $execTime = 0.0;
-
- public static function execTime($set = false)
+ public static function formatMoney(int $qty) : string
{
- if ($set)
- {
- self::$execTime = microTime(true);
- return;
- }
+ if ($qty <= 0)
+ return '';
- if (!self::$execTime)
- return;
+ $parts = [];
- $newTime = microTime(true);
- $tDiff = $newTime - self::$execTime;
- self::$execTime = $newTime;
+ if ($g = intdiv($qty, 10000))
+ $parts[] = ''.$g.'';
- return self::formatTime($tDiff * 1000, true);
+ if ($s = intdiv($qty % 10000, 100))
+ $parts[] = ''.$s.'';
+
+ if ($c = ($qty % 100))
+ $parts[] = ''.$c.'';
+
+ return implode(' ', $parts);
}
- public static function formatMoney($qty)
+ // pageTexts, questTexts and mails
+ public static function parseHtmlText(string|array $text, bool $markdown = false) : string|array
{
- $money = '';
-
- if ($qty >= 10000)
+ if (is_array($text))
{
- $g = floor($qty / 10000);
- $money .= ''.$g.' ';
- $qty -= $g * 10000;
+ foreach ($text as &$t)
+ $t = self::parseHtmlText($t, $markdown);
+
+ return $text;
}
- 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(
@@ -531,57 +150,61 @@ class Util
'' => '',
'' => '',
'' => '',
- ' ' => ' '
+ ' ' => $markdown ? '[br]' : ' '
);
// html may contain 'Pictures' and FlavorImages and "stuff"
$text = preg_replace_callback(
'/src="([^"]+)"/i',
- function ($m) { return 'src="'.STATIC_URL.'/images/wow/'.strtr($m[1], ['\\' => '/']).'.png"'; },
+ function ($m) { return sprintf('src="%s/images/wow/%s.png"', Cfg::get('STATIC_URL'), strtr($m[1], ['\\' => '/'])); },
strtr($text, $pairs)
);
}
else
- $text = strtr($text, ["\n" => ' ', "\r" => '']);
+ $text = strtr($text, ["\n" => $markdown ? '[br]' : ' ', "\r" => '']);
+
+ // escape fake html-ish tags the browser skipsh dishplaying ...!
+ $text = preg_replace('/<([^\s\/]+)>/iu', '<\1>', $text);
$from = array(
- '/\|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:::
- '/\$t([^;]+);/ui', // nonsense, that the client apparently ignores
- '/\|\d\-?\d?\((\$\w)\)/ui', // and another modifier for something russian |3-6($r)
+ '/\$g\s*([^:;]*)\s*:\s*([^:;]*)\s*(:?[^:;]*);/ui',// directed gender-reference $g::
+ '/\$t([^;]+);/ui', // HK rank. $t |