';
- while ($reagent = array_pop($reagents))
+ while ([$iId, $qty, $text, $exists] = array_pop($reagents))
{
- $_ .= '
'.$reagent[2].'';
- if ($reagent[1] > 1)
- $_ .= ' ('.$reagent[1].')';
+ $_ .= $exists ? '
'.$text.'' : $text;
+ if ($qty > 1)
+ $_ .= ' ('.$qty.')';
$_ .= empty($reagents) ? '
' : ', ';
}
@@ -1965,10 +2012,23 @@ class SpellList extends DBTypeList
if ($xTmp)
$x .= '
';
- $min = $this->scaling[$this->id] ? ($this->getField('baseLevel') ?: 1) : 1;
- $max = $this->scaling[$this->id] ? ($this->getField('maxLevel') ?: MAX_LEVEL) : 1;
- // scaling information - spellId:min:max:curr
- $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;
}
@@ -2149,8 +2209,11 @@ class SpellList extends DBTypeList
{
$data[Type::SPELL][$id] = array(
'icon' => $this->curTpl['iconStringAlt'] ?: $this->curTpl['iconString'],
- 'name' => $this->getField('name', true),
+ 'name' => $this->getField('name', true)
);
+
+ if (($_ = $this->curTpl['typeCat']) && in_array($_, [-5, -6, 9, 11]))
+ $data[Type::SPELL][$id]['completion_category'] = $_;
}
if ($addMask & GLOBALINFO_EXTRA)
@@ -2445,6 +2508,7 @@ class SpellListFilter extends Filter
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]
@@ -2520,13 +2584,13 @@ class SpellListFilter extends Filter
'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_REGEX, parent::PATTERN_NAME, false], // name / text - only printable chars, no delimiter
+ '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, [1, 99], false], // spell level min
- 'maxle' => [parent::V_RANGE, [1, 99], false], // spell level max
- 'minrs' => [parent::V_RANGE, [1, 999], false], // required skill level min
- 'maxrs' => [parent::V_RANGE, [1, 999], false], // required skill level max
+ 'minle' => [parent::V_RANGE, [0, 99], false], // spell level min
+ 'maxle' => [parent::V_RANGE, [0, 99], false], // spell level max
+ 'minrs' => [parent::V_RANGE, [0, 999], false], // required skill level min
+ 'maxrs' => [parent::V_RANGE, [0, 999], false], // required skill level max
'ra' => [parent::V_LIST, [[1, 8], 10, 11], false], // races
'cl' => [parent::V_CALLBACK, 'cbClasses', true ], // classes
'gl' => [parent::V_CALLBACK, 'cbGlyphs', true ], // glyph type
@@ -2540,17 +2604,24 @@ class SpellListFilter extends Filter
$parts = [];
$_v = &$this->values;
- //string (extended)
+ // string (extended)
if ($_v['na'])
{
- $_ = [];
- if ($_v['ex'] == 'on')
- $_ = $this->tokenizeString(['name_loc'.Lang::getLocale()->value, 'buff_loc'.Lang::getLocale()->value, 'description_loc'.Lang::getLocale()->value]);
- else
- $_ = $this->tokenizeString(['name_loc'.Lang::getLocale()->value]);
+ $f = [['na', ['nml.nName', 'nml.nBuff', 'nml.nDescription']]];
+ if ($_v['ex'] != 'on')
+ $f = [['na', 'nml.nName']];
- if ($_)
+ 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
@@ -2571,7 +2642,7 @@ class SpellListFilter extends Filter
// race
if ($_v['ra'])
- $parts[] = ['AND', [['reqRaceMask', ChrRace::MASK_ALL, '&'], ChrRace::MASK_ALL, '!'], ['reqRaceMask', $this->list2Mask([$_v['ra']]), '&']];
+ $parts[] = [DB::AND, [['reqRaceMask', ChrRace::MASK_ALL, '&'], ChrRace::MASK_ALL, '!'], ['reqRaceMask', $this->list2Mask([$_v['ra']]), '&']];
// class [list]
if ($_v['cl'])
@@ -2591,7 +2662,7 @@ class SpellListFilter extends Filter
// mechanic
if ($_v['me'])
- $parts[] = ['OR', ['mechanic', $_v['me']], ['effect1Mechanic', $_v['me']], ['effect2Mechanic', $_v['me']], ['effect3Mechanic', $_v['me']]];
+ $parts[] = [DB::OR, ['mechanic', $_v['me']], ['effect1Mechanic', $_v['me']], ['effect2Mechanic', $_v['me']], ['effect3Mechanic', $_v['me']]];
return $parts;
}
@@ -2629,9 +2700,9 @@ class SpellListFilter extends Filter
if (!Util::checkNumeric($crv, NUM_CAST_INT) || !$this->int2Op($crs))
return null;
- return ['OR',
- ['AND', ['powerType', [POWER_RAGE, POWER_RUNIC_POWER]], ['powerCost', (10 * $crv), $crs]],
- ['AND', ['powerType', [POWER_RAGE, POWER_RUNIC_POWER], '!'], ['powerCost', $crv, $crs]]
+ 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]]
];
}
@@ -2645,7 +2716,7 @@ class SpellListFilter extends Filter
return ['src.src'.$_, null, '!'];
else if ($_) // any
{
- $foo = ['OR'];
+ $foo = [DB::OR];
foreach (self::$enums[$cr] as $bar)
if (is_int($bar))
$foo[] = ['src.src'.$bar, null, '!'];
@@ -2664,9 +2735,9 @@ class SpellListFilter extends Filter
return null;
if ($crs)
- return ['OR', ['reagent1', 0, '>'], ['reagent2', 0, '>'], ['reagent3', 0, '>'], ['reagent4', 0, '>'], ['reagent5', 0, '>'], ['reagent6', 0, '>'], ['reagent7', 0, '>'], ['reagent8', 0, '>']];
+ return [DB::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]];
+ 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
@@ -2674,7 +2745,7 @@ class SpellListFilter extends Filter
if (!$this->checkInput(parent::V_RANGE, [1, self::MAX_SPELL_AURA], $crs))
return null;
- return ['OR', ['effect1AuraId', $crs], ['effect2AuraId', $crs], ['effect3AuraId', $crs]];
+ return [DB::OR, ['effect1AuraId', $crs], ['effect2AuraId', $crs], ['effect3AuraId', $crs]];
}
protected function cbEffectNames(int $cr, int $crs, string $crv) : ?array
@@ -2682,7 +2753,7 @@ class SpellListFilter extends Filter
if (!$this->checkInput(parent::V_RANGE, [1, self::MAX_SPELL_EFFECT], $crs))
return null;
- return ['OR', ['effect1Id', $crs], ['effect2Id', $crs], ['effect3Id', $crs]];
+ return [DB::OR, ['effect1Id', $crs], ['effect2Id', $crs], ['effect3Id', $crs]];
}
protected function cbInverseFlag(int $cr, int $crs, string $crv, string $field, int $flag) : ?array
@@ -2702,9 +2773,9 @@ class SpellListFilter extends Filter
return null;
if ($crs)
- return ['AND', [[$field, $flag, '&'], 0], ['dispelType', SPELL_DAMAGE_CLASS_MAGIC]];
+ return [DB::AND, [[$field, $flag, '&'], 0], ['dispelType', SPELL_DAMAGE_CLASS_MAGIC]];
else
- return ['OR', [$field, $flag, '&'], ['dispelType', SPELL_DAMAGE_CLASS_MAGIC, '!']];
+ return [DB::OR, [$field, $flag, '&'], ['dispelType', SPELL_DAMAGE_CLASS_MAGIC, '!']];
}
protected function cbReqFaction(int $cr, int $crs, string $crv) : ?array
@@ -2714,11 +2785,11 @@ class SpellListFilter extends Filter
// yes
1 => ['reqRaceMask', 0, '!'],
// alliance
- 2 => ['AND', [['reqRaceMask', ChrRace::MASK_HORDE, '&'], 0], ['reqRaceMask', ChrRace::MASK_ALLIANCE, '&']],
+ 2 => [DB::AND, [['reqRaceMask', ChrRace::MASK_HORDE, '&'], 0], ['reqRaceMask', ChrRace::MASK_ALLIANCE, '&']],
// horde
- 3 => ['AND', [['reqRaceMask', ChrRace::MASK_ALLIANCE, '&'], 0], ['reqRaceMask', ChrRace::MASK_HORDE, '&']],
+ 3 => [DB::AND, [['reqRaceMask', ChrRace::MASK_ALLIANCE, '&'], 0], ['reqRaceMask', ChrRace::MASK_HORDE, '&']],
// both
- 4 => ['AND', ['reqRaceMask', ChrRace::MASK_ALLIANCE, '&'], ['reqRaceMask', ChrRace::MASK_HORDE, '&']],
+ 4 => [DB::AND, ['reqRaceMask', ChrRace::MASK_ALLIANCE, '&'], ['reqRaceMask', ChrRace::MASK_HORDE, '&']],
// no
5 => ['reqRaceMask', 0],
default => null
@@ -2734,9 +2805,9 @@ class SpellListFilter extends Filter
$field = $useInvType ? 'equippedItemInventoryTypeMask' : 'equippedItemSubClassMask';
if ($crs)
- return ['AND', ['equippedItemClass', ITEM_CLASS_WEAPON], [$field, $mask, '&']];
+ return [DB::AND, ['equippedItemClass', ITEM_CLASS_WEAPON], [$field, $mask, '&']];
else
- return ['OR', ['equippedItemClass', ITEM_CLASS_WEAPON, '!'], [[$field, $mask, '&'], 0]];
+ return [DB::OR, ['equippedItemClass', ITEM_CLASS_WEAPON, '!'], [[$field, $mask, '&'], 0]];
}
/* unused - for reference: attribute flag or cooldown time constraint */
@@ -2746,14 +2817,14 @@ class SpellListFilter extends Filter
return null;
if ($crs)
- return ['AND',
+ return [DB::AND,
[['attributes4', SPELL_ATTR4_NOT_USABLE_IN_ARENA, '&'], 0],
- ['OR', ['recoveryTime', 10 * MINUTE * 1000, '<='], ['attributes4', SPELL_ATTR4_USABLE_IN_ARENA, '&']]
+ [DB::OR, ['recoveryTime', 10 * MINUTE * 1000, '<='], ['attributes4', SPELL_ATTR4_USABLE_IN_ARENA, '&']]
];
else
- return ['OR',
+ return [DB::OR,
['attributes4', SPELL_ATTR4_NOT_USABLE_IN_ARENA, '&'],
- ['AND', ['recoveryTime', 10 * MINUTE * 1000, '>'], [['attributes4', SPELL_ATTR4_USABLE_IN_ARENA, '&'], 0]]
+ [DB::AND, ['recoveryTime', 10 * MINUTE * 1000, '>'], [['attributes4', SPELL_ATTR4_USABLE_IN_ARENA, '&'], 0]]
];
}
@@ -2763,9 +2834,9 @@ class SpellListFilter extends Filter
return null;
if ($crs) // match exact, not as flag
- return ['AND', ['attributes1', SPELL_ATTR1_CHANNELED_1 | SPELL_ATTR1_CHANNELED_2 | SPELL_ATTR1_CHANNEL_TRACK_TARGET], ['effect1ImplicitTargetA', 21]];
+ return [DB::AND, ['attributes1', SPELL_ATTR1_CHANNELED_1 | SPELL_ATTR1_CHANNELED_2 | SPELL_ATTR1_CHANNEL_TRACK_TARGET], ['effect1ImplicitTargetA', 21]];
else
- return ['OR', ['attributes1', SPELL_ATTR1_CHANNELED_1 | SPELL_ATTR1_CHANNELED_2 | SPELL_ATTR1_CHANNEL_TRACK_TARGET, '!'], ['effect1ImplicitTargetA', 21, '!']];
+ 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
@@ -2781,16 +2852,16 @@ class SpellListFilter extends Filter
case 1: // Weapons
foreach (Game::$skillLineMask[-3] as $bit => $_)
$skill2Mask |= (1 << $bit);
- $skill1Ids = DB::Aowow()->selectCol('SELECT `id` FROM ?_skillline WHERE `typeCat` = 6');
+ $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');
+ $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');
+ $skill1Ids = DB::Aowow()->selectCol('SELECT `id` FROM ::skillline WHERE `typeCat` = 10');
break;
}
@@ -2799,7 +2870,7 @@ class SpellListFilter extends Filter
$cnd = ['skillLine1', $skill1Ids];
if ($skill2Mask)
- $cnd = ['OR', $cnd, ['AND', ['skillLine1', -3], ['skillLine2OrMask', $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
index b8cd1cb2..b583a290 100644
--- a/includes/dbtypes/title.class.php
+++ b/includes/dbtypes/title.class.php
@@ -12,13 +12,13 @@ class TitleList extends DBTypeList
public static int $type = Type::TITLE;
public static string $brickFile = 'title';
- public static string $dataTable = '?_titles';
+ public static string $dataTable = '::titles';
public array $sources = [];
- protected string $queryBase = 'SELECT t.*, t.`id` AS ARRAY_KEY FROM ?_titles t';
+ protected string $queryBase = 'SELECT t.*, t.`id` AS ARRAY_KEY FROM ::titles t';
protected array $queryOpts = array(
't' => [['src']], // 11: Type::TITLE
- 'src' => ['j' => ['?_source src ON `type` = 11 AND `typeId` = t.`id`', true], 's' => ', `src13`, `moreType`, `moreTypeId`']
+ 'src' => ['j' => ['::source src ON `type` = 11 AND `typeId` = t.`id`', true], 's' => ', `src13`, `moreType`, `moreTypeId`']
);
public function __construct(array $conditions = [], array $miscData = [])
@@ -55,7 +55,7 @@ class TitleList extends DBTypeList
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 ?# WHERE `id` = ?d', self::$dataTable, $id))
+ 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;
}
diff --git a/includes/dbtypes/user.class.php b/includes/dbtypes/user.class.php
index 44f4f83b..18122f6b 100644
--- a/includes/dbtypes/user.class.php
+++ b/includes/dbtypes/user.class.php
@@ -13,10 +13,10 @@ class UserList extends DBTypeList
public static string $dataTable = '';
public static int $contribute = CONTRIBUTE_NONE;
- protected string $queryBase = 'SELECT *, a.`id` AS ARRAY_KEY FROM ?_account a';
+ protected string $queryBase = 'SELECT *, a.`id` AS ARRAY_KEY FROM ::account a';
protected array $queryOpts = array(
'a' => [['r']],
- 'r' => ['j' => ['?_account_reputation r ON r.`userId` = a.`id`', true], 's' => ', IFNULL(SUM(r.`amount`), 0) AS "reputation"', 'g' => 'a.`id`']
+ 'r' => ['j' => ['::account_reputation r ON r.`userId` = a.`id`', true], 's' => ', IFNULL(SUM(r.`amount`), 0) AS "reputation"', 'g' => 'a.`id`']
);
public function getJSGlobals(int $addMask = 0) : array
@@ -49,7 +49,7 @@ class UserList extends DBTypeList
case 2:
if ($this->isPremium())
{
- if ($av = DB::Aowow()->selectCell('SELECT `id` FROM ?_account_avatars WHERE `userId` = ?d AND `current` = 1 AND `status` <> ?d', $userId, AvatarMgr::STATUS_REJECTED))
+ 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;
diff --git a/includes/dbtypes/worldevent.class.php b/includes/dbtypes/worldevent.class.php
index 831b57c3..399d6083 100644
--- a/includes/dbtypes/worldevent.class.php
+++ b/includes/dbtypes/worldevent.class.php
@@ -10,12 +10,13 @@ class WorldEventList extends DBTypeList
{
public static int $type = Type::WORLDEVENT;
public static string $brickFile = 'event';
- public static string $dataTable = '?_events';
+ public static string $dataTable = '::events';
- protected string $queryBase = 'SELECT e.`holidayId`, e.`cuFlags`, e.`startTime`, e.`endTime`, e.`occurence`, e.`length`, e.`requires`, e.`description` AS "nameINT", e.`id` AS "eventId", e.`id` AS "ARRAY_KEY", h.* FROM ?_events e';
+ protected string $queryBase = 'SELECT e.`holidayId`, e.`cuFlags`, e.`startTime`, e.`endTime`, e.`occurence`, e.`length`, e.`requires`, e.`description` AS "nameINT", e.`id` AS "eventId", e.`id` AS ARRAY_KEY FROM ::events e';
protected array $queryOpts = array(
- 'e' => [['h']],
- 'h' => ['j' => ['?_holidays h ON e.`holidayId` = h.`id`', true], 'o' => '-e.`id` ASC']
+ 'e' => [['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 = [])
@@ -70,9 +71,9 @@ class WorldEventList extends DBTypeList
{
$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` = ?d',
+ FROM ::events e
+ LEFT JOIN ::holidays h ON e.`holidayId` = h.`id`
+ WHERE e.`id` = %i',
$id
);
@@ -91,7 +92,7 @@ class WorldEventList extends DBTypeList
if ($rec < 0 || $date['lastDate'] < time())
return true;
- $nIntervals = ceil((time() - $start) / $rec);
+ $nIntervals = (int)ceil((time() - $end) / $rec);
$start += $nIntervals * $rec;
$end += $nIntervals * $rec;
@@ -105,8 +106,8 @@ class WorldEventList extends DBTypeList
{
WorldEventList::updateDates($row['_date'] ?? null, $start, $end, $rec);
- $row['startDate'] = $start ? date(Util::$dateFormatInternal, $start) : null;
- $row['endDate'] = $end ? date(Util::$dateFormatInternal, $end) : null;
+ $row['startDate'] = $start ? date(Util::$dateFormatInternal, $start) : null;
+ $row['endDate'] = $end ? date(Util::$dateFormatInternal, $end - 1) : null;
$row['rec'] = $rec;
unset($row['_date']);
@@ -157,9 +158,9 @@ class WorldEventList extends DBTypeList
// use string-placeholder for dates
// start
- $x .= Lang::event('start').Lang::main('colon').'%s
';
+ $x .= Lang::event('start').'%s
';
// end
- $x .= Lang::event('end').Lang::main('colon').'%s';
+ $x .= Lang::event('end').'%s';
$x .= '';
diff --git a/includes/dbtypes/zone.class.php b/includes/dbtypes/zone.class.php
index f3bc02d7..668eac6e 100644
--- a/includes/dbtypes/zone.class.php
+++ b/includes/dbtypes/zone.class.php
@@ -12,9 +12,9 @@ class ZoneList extends DBTypeList
public static int $type = Type::ZONE;
public static string $brickFile = 'zone';
- public static string $dataTable = '?_zones';
+ public static string $dataTable = '::zones';
- protected string $queryBase = 'SELECT z.*, z.`id` AS ARRAY_KEY FROM ?_zones z';
+ protected string $queryBase = 'SELECT z.*, z.`id` AS ARRAY_KEY FROM ::zones z';
public function __construct(array $conditions = [], array $miscData = [])
{
diff --git a/includes/defines.php b/includes/defines.php
index e73a84d0..4075085a 100644
--- a/includes/defines.php
+++ b/includes/defines.php
@@ -12,8 +12,8 @@ if (!defined('AOWOW_REVISION'))
define('JSON_AOWOW_POWER', JSON_PRETTY_PRINT | JSON_NUMERIC_CHECK | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
define('FILTER_FLAG_STRIP_AOWOW', FILTER_FLAG_STRIP_LOW | FILTER_FLAG_STRIP_HIGH | FILTER_FLAG_STRIP_BACKTICK);
-define('TDB_WORLD_MINIMUM_VER', 21101);
-define('TDB_WORLD_EXPECTED_VER', 24041);
+define('TDB_WORLD_MINIMUM_VER', 25101);
+define('TDB_WORLD_EXPECTED_VER', 25101);
// as of 01.01.2024 https://www.wowhead.com/wotlk/de/spell=40120/{seo}
// https://www.wowhead.com/wotlk/es/search=vuelo
@@ -41,6 +41,8 @@ define('CACHE_TYPE_PAGE', 1);
define('CACHE_TYPE_TOOLTIP', 2);
define('CACHE_TYPE_SEARCH', 3);
define('CACHE_TYPE_XML', 4); // only used by items
+define('CACHE_TYPE_LIST_PAGE', 5);
+define('CACHE_TYPE_DETAIL_PAGE', 6);
define('CACHE_MODE_FILECACHE', 0x1);
define('CACHE_MODE_MEMCACHED', 0x2);
@@ -53,6 +55,7 @@ define ('SC_FLAG_PREFIX', 0x01);
define ('SC_FLAG_NO_TIMESTAMP', 0x02);
define ('SC_FLAG_APPEND_LOCALE', 0x04);
define ('SC_FLAG_LOCALIZED', 0x08);
+define ('SC_FLAG_NOCACHE', 0x10);
define('ICON_SIZE_TINY', 15);
define('ICON_SIZE_SMALL', 18);
@@ -128,7 +131,7 @@ define('AUTH_IPBANNED', 4);
define('AUTH_ACC_INACTIVE', 5);
define('AUTH_INTERNAL_ERR', 6);
-define('AUTH_MODE_SELF', 0); // uses ?_accounts
+define('AUTH_MODE_SELF', 0); // uses ::accounts
define('AUTH_MODE_REALM', 1); // uses given realm-table
define('AUTH_MODE_EXTERNAL', 2); // uses external script
@@ -372,9 +375,9 @@ define('QUEST_CU_PART_OF_SERIES', 0x0200);
define('PROFILER_CU_PUBLISHED', 0x01);
define('PROFILER_CU_PINNED', 0x02);
-define('PROFILER_CU_DELETED', 0x04);
-define('PROFILER_CU_PROFILE', 0x08);
-define('PROFILER_CU_NEEDS_RESYNC', 0x10);
+// define('PROFILER_CU_DELETED', 0x04); // migrated to separate db cols
+// define('PROFILER_CU_PROFILE', 0x08);
+// define('PROFILER_CU_NEEDS_RESYNC', 0x10);
define('GUIDE_CU_NO_QUICKFACTS', 0x100); // merge with CC_FLAG_*
define('GUIDE_CU_NO_RATING', 0x200);
@@ -383,20 +386,6 @@ define('MAX_LEVEL', 80);
define('MAX_SKILL', 450);
define('WOW_BUILD', 12340);
-// Loot handles
-define('LOOT_FISHING', 'fishing_loot_template');
-define('LOOT_CREATURE', 'creature_loot_template');
-define('LOOT_GAMEOBJECT', 'gameobject_loot_template');
-define('LOOT_ITEM', 'item_loot_template');
-define('LOOT_DISENCHANT', 'disenchant_loot_template');
-define('LOOT_PROSPECTING', 'prospecting_loot_template');
-define('LOOT_MILLING', 'milling_loot_template');
-define('LOOT_PICKPOCKET', 'pickpocketing_loot_template');
-define('LOOT_SKINNING', 'skinning_loot_template');
-define('LOOT_MAIL', 'mail_loot_template'); // used by achievements and quests
-define('LOOT_SPELL', 'spell_loot_template');
-define('LOOT_REFERENCE', 'reference_loot_template');
-
// Sides
define('SIDE_NONE', 0);
define('SIDE_ALLIANCE', 1);
@@ -492,6 +481,15 @@ define('ITEM_MOD_SPELL_POWER', 45);
define('ITEM_MOD_HEALTH_REGEN', 46);
define('ITEM_MOD_SPELL_PENETRATION', 47);
define('ITEM_MOD_BLOCK_VALUE', 48);
+// unknown by 335a client but still used by several item_templates
+// define('ITEM_MOD_MASTERY_RATING', 49);
+// define('ITEM_MOD_EXTRA_ARMOR', 50);
+// define('ITEM_MOD_FIRE_RESISTANCE', 51);
+// define('ITEM_MOD_FROST_RESISTANCE', 52);
+// define('ITEM_MOD_HOLY_RESISTANCE', 53);
+// define('ITEM_MOD_SHADOW_RESISTANCE', 54);
+// define('ITEM_MOD_NATURE_RESISTANCE', 55);
+// define('ITEM_MOD_ARCANE_RESISTANCE', 56);
// Combat Ratings
define('CR_WEAPON_SKILL', 0);
@@ -598,12 +596,23 @@ define('TEAM_NEUTRAL', 2);
// Lock Types
define('LOCK_TYPE_ITEM', 1);
define('LOCK_TYPE_SKILL', 2);
+define('LOCK_TYPE_SPELL', 3);
// Lock-Properties (also categorizes GOs)
define('LOCK_PROPERTY_FOOTLOCKER', 1);
define('LOCK_PROPERTY_HERBALISM', 2);
define('LOCK_PROPERTY_MINING', 3);
+// FactionFlags
+define('FACTION_FLAG_VISIBLE', 0x01);
+define('FACTION_FLAG_AT_WAR', 0x02);
+define('FACTION_FLAG_HIDDEN', 0x04);
+define('FACTION_FLAG_INVISIBLE_FORCED', 0x08);
+define('FACTION_FLAG_PEACE_FORCED', 0x10);
+define('FACTION_FLAG_INACTIVE', 0x20);
+define('FACTION_FLAG_RIVAL', 0x40);
+define('FACTION_FLAG_SPECIAL', 0x80);
+
// Creature
define('NPC_TYPEFLAG_TAMEABLE', 0x00000001);
define('NPC_TYPEFLAG_VISIBLE_TO_GHOSTS', 0x00000002);
@@ -669,6 +678,7 @@ define('NPC_FLAG_STABLE_MASTER', 0x00400000);
define('NPC_FLAG_GUILD_BANK', 0x00800000);
define('NPC_FLAG_SPELLCLICK', 0x01000000);
define('NPC_FLAG_MAILBOX', 0x04000000);
+define('NPC_FLAG_VALIDATE', 0x05FFFFF3);
define('CREATURE_FLAG_EXTRA_INSTANCE_BIND', 0x00000001); // creature kill binds instance to killer and killer's group
define('CREATURE_FLAG_EXTRA_CIVILIAN', 0x00000002); // creature does not aggro (ignore faction/reputation hostility)
@@ -708,7 +718,7 @@ define('UNIT_FLAG_IMMUNE_TO_PC', 0x00000100); // disables combat/a
define('UNIT_FLAG_IMMUNE_TO_NPC', 0x00000200); // disables combat/assistance with NonPlayerCharacters (NPC)
define('UNIT_FLAG_LOOTING', 0x00000400); // Loot animation
define('UNIT_FLAG_PET_IN_COMBAT', 0x00000800); // In combat? 2.0.8
-define('UNIT_FLAG_PVP', 0x00001000); // Changed in 3.0.3
+define('UNIT_FLAG_PVP_ENABLING', 0x00001000); // Changed in 3.0.3
define('UNIT_FLAG_SILENCED', 0x00002000); // Can't cast spells
define('UNIT_FLAG_CANNOT_SWIM', 0x00004000); // 2.0.8
define('UNIT_FLAG_UNK_15', 0x00008000); // Only Swim ('OnlySwim' from UnitFlags.cs in WPP)
@@ -728,6 +738,7 @@ define('UNIT_FLAG_UNK_28', 0x10000000); // (PreventKneelingW
define('UNIT_FLAG_UNK_29', 0x20000000); // Used in Feign Death spell or NPC will play dead. (PreventEmotes)
define('UNIT_FLAG_SHEATHE', 0x40000000); //
define('UNIT_FLAG_UNK_31', 0x80000000); //
+define('UNIT_FLAG_VALIDATE', 0x7FFFFFFF); //
define('UNIT_FLAG2_FEIGN_DEATH', 0x00000001); //
define('UNIT_FLAG2_UNK1', 0x00000002); // Hide unit model (show only player equip)
@@ -747,6 +758,7 @@ define('UNIT_FLAG2_DISABLE_TURN', 0x00008000); //
define('UNIT_FLAG2_UNK2', 0x00010000); //
define('UNIT_FLAG2_PLAY_DEATH_ANIM', 0x00020000); // Plays special death animation upon death
define('UNIT_FLAG2_ALLOW_CHEAT_SPELLS', 0x00040000); // allows casting spells with AttributesEx7 & SPELL_ATTR7_IS_CHEAT_SPELL
+define('UNIT_FLAG2_VALIDATE', 0x0006FDFF); //
// UNIT_FIELD_BYTES_1 - idx 0 (UnitStandStateType)
define('UNIT_STAND_STATE_STAND', 0);
@@ -768,11 +780,16 @@ define('UNIT_VIS_FLAGS_UNK4', 0x08);
define('UNIT_VIS_FLAGS_UNK5', 0x10);
// UNIT_FIELD_BYTES_1 - idx 3 (UnitAnimTier)
-define('UNIT_BYTE1_ANIM_TIER_GROUND', 0);
-define('UNIT_BYTE1_ANIM_TIER_SWIM', 1);
-define('UNIT_BYTE1_ANIM_TIER_HOVER', 2);
-define('UNIT_BYTE1_ANIM_TIER_FLY', 3);
-define('UNIT_BYTE1_ANIM_TIER_SUMBERGED', 4);
+define('UNIT_ANIM_TIER_GROUND', 0);
+define('UNIT_ANIM_TIER_SWIM', 1);
+define('UNIT_ANIM_TIER_HOVER', 2);
+define('UNIT_ANIM_TIER_FLY', 3);
+define('UNIT_ANIM_TIER_SUMBERGED', 4);
+
+// UNIT_FIELD_BYTES_2 - idx 1 (UnitPvPStateFlags)
+define('UNIT_PVPSTATE_FLAG_PVP', 0x01);
+define('UNIT_PVPSTATE_FLAG_FFA_PVP', 0x04); // not expected to be on NPCs, buuuut...
+define('UNIT_PVPSTATE_FLAG_SANCTUARY', 0x08);
define('UNIT_DYNFLAG_LOOTABLE', 0x01); //
define('UNIT_DYNFLAG_TRACK_UNIT', 0x02); // Creature's location will be seen as a small dot in the minimap
@@ -782,30 +799,40 @@ define('UNIT_DYNFLAG_SPECIALINFO', 0x10); //
define('UNIT_DYNFLAG_DEAD', 0x20); // Makes the creature appear dead (this DOES NOT make the creature's name grey or not attack players).
define('UNIT_DYNFLAG_REFER_A_FRIEND', 0x40); //
define('UNIT_DYNFLAG_TAPPED_BY_ALL_THREAT_LIST', 0x80); // Lua_UnitIsTappedByAllThreatList
+define('UNIT_DYNFLAG_VALIDATE', 0xFF); //
define('PET_TALENT_TYPE_FEROCITY', 0);
define('PET_TALENT_TYPE_TENACITY', 1);
define('PET_TALENT_TYPE_CUNNING', 2);
// quest
-define('QUEST_FLAG_STAY_ALIVE', 0x00001);
-define('QUEST_FLAG_PARTY_ACCEPT', 0x00002);
-define('QUEST_FLAG_EXPLORATION', 0x00004);
-define('QUEST_FLAG_SHARABLE', 0x00008);
-define('QUEST_FLAG_AUTO_REWARDED', 0x00400);
-define('QUEST_FLAG_DAILY', 0x01000);
-define('QUEST_FLAG_REPEATABLE', 0x02000);
-define('QUEST_FLAG_UNAVAILABLE', 0x04000);
-define('QUEST_FLAG_WEEKLY', 0x08000);
-define('QUEST_FLAG_AUTO_COMPLETE', 0x10000);
-define('QUEST_FLAG_AUTO_ACCEPT', 0x80000);
+define('QUEST_FLAG_STAY_ALIVE', 0x00001);
+define('QUEST_FLAG_PARTY_ACCEPT', 0x00002);
+define('QUEST_FLAG_EXPLORATION', 0x00004);
+define('QUEST_FLAG_SHARABLE', 0x00008);
+define('QUEST_FLAG_HAS_CONDITION', 0x00010); // TC: Not used currently
+define('QUEST_FLAG_HIDE_REWARD_POI', 0x00020); // TC: Not used currently: Unsure of content
+define('QUEST_FLAG_RAID', 0x00040);
+define('QUEST_FLAG_TBC', 0x00080);
+define('QUEST_FLAG_NO_MONEY_FROM_XP', 0x00100);
+define('QUEST_FLAG_HIDDEN_REWARDS', 0x00200);
+define('QUEST_FLAG_TRACKING', 0x00400); // TC: These quests are automatically rewarded on quest complete and they will never appear in quest log client side.
+define('QUEST_FLAG_DEPRECATE_REPUTATION', 0x00800); // TC: Not used currently
+define('QUEST_FLAG_DAILY', 0x01000);
+define('QUEST_FLAG_FLAGS_PVP', 0x02000);
+define('QUEST_FLAG_UNAVAILABLE', 0x04000);
+define('QUEST_FLAG_WEEKLY', 0x08000);
+define('QUEST_FLAG_AUTO_COMPLETE', 0x10000);
+define('QUEST_FLAG_DISPLAY_ITEM_IN_TRACKER', 0x20000); // TC: Displays usable item in quest tracker
+define('QUEST_FLAG_OBJ_TEXT', 0x40000); // TC: use Objective text as Complete text
+define('QUEST_FLAG_AUTO_ACCEPT', 0x80000);
-define('QUEST_FLAG_SPECIAL_REPEATABLE', 0x01);
-define('QUEST_FLAG_SPECIAL_EXT_COMPLETE', 0x02);
-define('QUEST_FLAG_SPECIAL_AUTO_ACCEPT', 0x04);
-define('QUEST_FLAG_SPECIAL_DUNGEON_FINDER', 0x08);
-define('QUEST_FLAG_SPECIAL_MONTHLY', 0x10);
-define('QUEST_FLAG_SPECIAL_SPELLCAST', 0x20); // not documented in wiki! :[
+define('QUEST_FLAG_SPECIAL_REPEATABLE', 0x01);
+define('QUEST_FLAG_SPECIAL_EXT_COMPLETE', 0x02);
+define('QUEST_FLAG_SPECIAL_AUTO_ACCEPT', 0x04);
+define('QUEST_FLAG_SPECIAL_DUNGEON_FINDER', 0x08);
+define('QUEST_FLAG_SPECIAL_MONTHLY', 0x10);
+define('QUEST_FLAG_SPECIAL_SPELLCAST', 0x20); // not documented in wiki! :[
// GameObject
define('OBJECT_DOOR', 0);
@@ -851,9 +878,11 @@ define('GO_FLAG_INTERACT_COND', 0x0004); // Untargetable, can
define('GO_FLAG_TRANSPORT', 0x0008); // Gameobject can transport (boat, elevator, car)
define('GO_FLAG_NOT_SELECTABLE', 0x0010); // Not selectable (Not even in GM-mode)
define('GO_FLAG_NODESPAWN', 0x0020); // Never despawns. Typical for gameobjects with on/off state (doors for example)
-define('GO_FLAG_TRIGGERED', 0x0040); // typically, summoned objects. Triggered by spell or other events
+define('GO_FLAG_AI_OBSTACLE', 0x0040); // makes the client register the object in something called AIObstacleMgr, unknown what it does
+define('GO_FLAG_FREEZE_ANIMATION',0x0080); //
define('GO_FLAG_DAMAGED', 0x0200); // Gameobject has been siege damaged
define('GO_FLAG_DESTROYED', 0x0400); // Gameobject has been destroyed
+define('GO_FLAG_VALIDATE', 0x06FF); //
define('GO_STATE_ACTIVE', 0); // show in world as used and not reset (closed door open)
define('GO_STATE_READY', 1); // show in world as ready (closed door close)
@@ -1058,7 +1087,7 @@ define('ENCHANTMENT_TYPE_PRISMATIC_SOCKET', 8);
// define('ENCHANT_CONDITION_EQUAL_VALUE', ?);
define('ENCHANT_CONDITION_LESS_VALUE', 2);
define('ENCHANT_CONDITION_MORE_COMPARE', 3);
-// define('ENCHANT_CONDITION_MORE_EQUAL_COMPARE', ?);
+// define('ENCHANT_CONDITION_MORE_EQUAL_COMPARE', %s);
define('ENCHANT_CONDITION_MORE_VALUE', 5);
// define('ENCHANT_CONDITION_NOT_EQUAL_COMPARE', ?);
// define('ENCHANT_CONDITION_NOT_EQUAL_VALUE', ?);
diff --git a/includes/game/chrrace.class.php b/includes/game/chrrace.class.php
index aab28ed7..70e481c1 100644
--- a/includes/game/chrrace.class.php
+++ b/includes/game/chrrace.class.php
@@ -8,20 +8,31 @@ if (!defined('AOWOW_REVISION'))
enum ChrRace : int
{
- case HUMAN = 1;
- case ORC = 2;
- case DWARF = 3;
- case NIGHTELF = 4;
- case UNDEAD = 5;
- case TAUREN = 6;
- case GNOME = 7;
- case TROLL = 8;
- case BLOODELF = 10;
- case DRAENEI = 11;
+ case HUMAN = 1;
+ case ORC = 2;
+ case DWARF = 3;
+ case NIGHTELF = 4;
+ case UNDEAD = 5;
+ case TAUREN = 6;
+ case GNOME = 7;
+ case TROLL = 8;
+ case GOBLIN = 9;
+ case BLOODELF = 10;
+ case DRAENEI = 11;
+ case FEL_ORC = 12;
+ case NAGA = 13;
+ case BROKEN = 14;
+ case SKELETON = 15;
+ case VRYKUL = 16;
+ case TUSKARR = 17;
+ case FOREST_TROLL = 18;
+ case TAUNKA = 19;
+ case NORTHREND_SKELETON = 20;
+ case ICE_TROLL = 21;
- public const MASK_ALLIANCE = 0x44D;
- public const MASK_HORDE = 0x2B2;
- public const MASK_ALL = 0x6FF;
+ public const MASK_ALLIANCE = 0x44D; // HUMAN, DWARF, NIGHTELF, GNOME, DRAENEI
+ public const MASK_HORDE = 0x2B2; // ORC, UNDEAD, TAUREN, TROLL, BLOODELF
+ public const MASK_ALL = self::MASK_ALLIANCE | self::MASK_HORDE;
public function matches(int $raceMask) : bool
{
@@ -75,12 +86,13 @@ enum ChrRace : int
self::ORC => 'orc',
self::DWARF => 'dwarf',
self::NIGHTELF => 'nightelf',
- self::UNDEAD => 'undead',
+ self::UNDEAD => 'scourge',
self::TAUREN => 'tauren',
self::GNOME => 'gnome',
self::TROLL => 'troll',
self::BLOODELF => 'bloodelf',
- self::DRAENEI => 'draenei'
+ self::DRAENEI => 'draenei',
+ default => ''
};
}
diff --git a/includes/game/chrstatistics.php b/includes/game/chrstatistics.php
index c1819501..1777e31c 100644
--- a/includes/game/chrstatistics.php
+++ b/includes/game/chrstatistics.php
@@ -116,6 +116,7 @@ abstract class Stat // based on g_statTo
public const FLAG_PROFILER = 0x04; // stat used in profiler only
public const FLAG_LVL_SCALING = 0x08; // rating effectivenes scales with level
public const FLAG_FLOAT_VALUE = 0x10; // not an int
+ public const FLAG_NO_WEIGHT = 0x20; // for item summary and filter .. basically any fi_filters.items of type: num thats not excluded by noweights: 1 is weightable
public const IDX_JSON_STR = 0;
public const IDX_ITEM_MOD = 1; // granted by items
@@ -123,7 +124,7 @@ abstract class Stat // based on g_statTo
public const IDX_FILTER_CR_ID = 3; // also references listview cols
public const IDX_FLAGS = 4;
- private static /* array */ $data = array(
+ private static array $data = array(
self::HEALTH => ['health', ITEM_MOD_HEALTH, null, 115, self::FLAG_ITEM],
self::MANA => ['mana', ITEM_MOD_MANA, null, 116, self::FLAG_ITEM],
self::AGILITY => ['agi', ITEM_MOD_AGILITY, null, 21, self::FLAG_ITEM],
@@ -172,14 +173,14 @@ abstract class Stat // based on g_statTo
self::HEALTH_REGENERATION => ['healthrgn', ITEM_MOD_HEALTH_REGEN, null, 60, self::FLAG_ITEM],
self::SPELL_PENETRATION => ['splpen', ITEM_MOD_SPELL_PENETRATION, null, 94, self::FLAG_ITEM],
self::BLOCK => ['block', ITEM_MOD_BLOCK_VALUE, null, 43, self::FLAG_ITEM],
- // self::MASTERY_RTG => ['mastrtng', null, CR_MASTERY, null, self::FLAG_NONE],
- self::ARMOR => ['armor', null, null, 41, self::FLAG_ITEM],
- self::FIRE_RESISTANCE => ['firres', null, null, 26, self::FLAG_ITEM],
- self::FROST_RESISTANCE => ['frores', null, null, 28, self::FLAG_ITEM],
- self::HOLY_RESISTANCE => ['holres', null, null, 30, self::FLAG_ITEM],
- self::SHADOW_RESISTANCE => ['shares', null, null, 29, self::FLAG_ITEM],
- self::NATURE_RESISTANCE => ['natres', null, null, 27, self::FLAG_ITEM],
- self::ARCANE_RESISTANCE => ['arcres', null, null, 25, self::FLAG_ITEM],
+ // self::MASTERY_RTG => ['mastrtng', ITEM_MOD_MASTERY_RATING, CR_MASTERY, null, self::FLAG_NONE],
+ self::ARMOR => ['armor', null,/*ITEM_MOD_EXTRA_ARMOR */null, 41, self::FLAG_ITEM],
+ self::FIRE_RESISTANCE => ['firres', null,/*ITEM_MOD_FIRE_RESISTANCE */null, 26, self::FLAG_ITEM],
+ self::FROST_RESISTANCE => ['frores', null,/*ITEM_MOD_FROST_RESISTANCE */null, 28, self::FLAG_ITEM],
+ self::HOLY_RESISTANCE => ['holres', null,/*ITEM_MOD_HOLY_RESISTANCE */null, 30, self::FLAG_ITEM],
+ self::SHADOW_RESISTANCE => ['shares', null,/*ITEM_MOD_SHADOW_RESISTANCE*/null, 29, self::FLAG_ITEM],
+ self::NATURE_RESISTANCE => ['natres', null,/*ITEM_MOD_NATURE_RESISTANCE*/null, 27, self::FLAG_ITEM],
+ self::ARCANE_RESISTANCE => ['arcres', null,/*ITEM_MOD_ARCANE_RESISTANCE*/null, 25, self::FLAG_ITEM],
self::FIRE_SPELL_POWER => ['firsplpwr', null, null, 53, self::FLAG_ITEM],
self::FROST_SPELL_POWER => ['frosplpwr', null, null, 54, self::FLAG_ITEM],
self::HOLY_SPELL_POWER => ['holsplpwr', null, null, 55, self::FLAG_ITEM],
@@ -188,7 +189,7 @@ abstract class Stat // based on g_statTo
self::ARCANE_SPELL_POWER => ['arcsplpwr', null, null, 52, self::FLAG_ITEM],
// v not part of g_statToJson v
self::WEAPON_DAMAGE => ['dmg', null, null, null, self::FLAG_SERVERSIDE | self::FLAG_FLOAT_VALUE],
- self::WEAPON_DAMAGE_TYPE => ['damagetype', null, null, 35, self::FLAG_SERVERSIDE],
+ self::WEAPON_DAMAGE_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],
@@ -201,7 +202,7 @@ abstract class Stat // based on g_statTo
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::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
@@ -248,6 +249,16 @@ abstract class Stat // based on g_statTo
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..?
@@ -315,22 +326,27 @@ abstract class Stat // based on g_statTo
return $x;
}
- public static function getIndexFrom(int $idx, string $match) : int
+ public static function getIndexFrom(int $idx, string $search) : int
{
- $i = array_search($match, array_column(self::$data, $idx));
- if ($i === false)
- return 0;
-
- return array_keys(self::$data)[$i];
+ return array_find_key(self::$data, fn($x) => $x[$idx] == $search) ?: 0;
}
}
class StatsContainer implements \Countable
{
- private $store = [];
+ private array $store = [];
- private $relSpells = [];
- private $relEnchantments = [];
+ 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 = [])
{
@@ -400,13 +416,14 @@ class StatsContainer implements \Countable
return $this;
}
- public function fromSpell(array $spell) : self
+ public function fromSpell(array $spell, bool $onlyFoodBuff = false) : self
{
if (!$spell)
return $this;
- // if spells grant an equal, non-zero amount of SPELL_DAMAGE and SPELL_HEALING, combine them to SPELL_POWER
- // this probably does not affect enchantments
+ if ($onlyFoodBuff && !($spell['attributes2'] & SPELL_ATTR2_FOOD_BUFF))
+ return $this;
+
$tmpStore = [];
for ($i = 1; $i <= 3; $i++)
@@ -418,16 +435,30 @@ class StatsContainer implements \Countable
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]);
}
- if (!empty($tmpStore[Stat::HEALING_SPELL_POWER]) && !empty($tmpStore[Stat::DAMAGE_SPELL_POWER]) && $tmpStore[Stat::HEALING_SPELL_POWER] == $tmpStore[Stat::DAMAGE_SPELL_POWER])
+ foreach (self::$combinedSpellStats as $combined => $stats)
{
- Util::arraySumByKey($tmpStore, [Stat::SPELL_POWER => $tmpStore[Stat::HEALING_SPELL_POWER]]);
- unset($tmpStore[Stat::HEALING_SPELL_POWER]);
- unset($tmpStore[Stat::DAMAGE_SPELL_POWER]);
+ 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);
@@ -459,7 +490,7 @@ class StatsContainer implements \Countable
public function fromDB(int $type, int $typeId, int $fieldFlags = Stat::FLAG_NONE) : self
{
- foreach (DB::Aowow()->selectRow('SELECT (?#) FROM ?_item_stats WHERE `type` = ?d AND `typeId` = ?d', Stat::getJsonStringsFor($fieldFlags ?: (Stat::FLAG_ITEM | Stat::FLAG_SERVERSIDE)), $type, $typeId) as $key => $amt)
+ 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;
@@ -519,18 +550,12 @@ class StatsContainer implements \Countable
private function relE(int $enchantmentId) : array
{
- if ($enchantmentId <= 0 || !isset($this->relEnchantments[$enchantmentId]))
- return [];
-
- return $this->relEnchantments[$enchantmentId];
+ return $this->relEnchantments[$enchantmentId] ?? [];
}
private function relS(int $spellId) : array
{
- if ($spellId <= 0 || !isset($this->relSpells[$spellId]))
- return [];
-
- return $this->relSpells[$spellId];
+ return $this->relSpells[$spellId] ?? [];
}
private static function convertEnchantment(int $type, int $object) : array
@@ -546,22 +571,17 @@ class StatsContainer implements \Countable
case ENCHANTMENT_TYPE_STAT: // ITEM_MOD_*
return [Stat::getIndexFrom(Stat::IDX_ITEM_MOD, $object)];
case ENCHANTMENT_TYPE_RESISTANCE:
- if ($object == SPELL_SCHOOL_NORMAL)
- return [Stat::ARMOR];
- if ($object == SPELL_SCHOOL_HOLY)
- return [Stat::HOLY_RESISTANCE];
- if ($object == SPELL_SCHOOL_FIRE)
- return [Stat::FIRE_RESISTANCE];
- if ($object == SPELL_SCHOOL_NATURE)
- return [Stat::NATURE_RESISTANCE];
- if ($object == SPELL_SCHOOL_FROST)
- return [Stat::FROST_RESISTANCE];
- if ($object == SPELL_SCHOOL_SHADOW)
- return [Stat::SHADOW_RESISTANCE];
- if ($object == SPELL_SCHOOL_ARCANE)
- return [Stat::ARCANE_RESISTANCE];
-
- return [];
+ return match ($object)
+ {
+ SPELL_SCHOOL_NORMAL => [Stat::ARMOR],
+ SPELL_SCHOOL_HOLY => [Stat::HOLY_RESISTANCE],
+ SPELL_SCHOOL_FIRE => [Stat::FIRE_RESISTANCE],
+ SPELL_SCHOOL_NATURE => [Stat::NATURE_RESISTANCE],
+ SPELL_SCHOOL_FROST => [Stat::FROST_RESISTANCE],
+ SPELL_SCHOOL_SHADOW => [Stat::SHADOW_RESISTANCE],
+ SPELL_SCHOOL_ARCANE => [Stat::ARCANE_RESISTANCE],
+ default => []
+ };
case ENCHANTMENT_TYPE_EQUIP_SPELL: // handled one level up
case ENCHANTMENT_TYPE_COMBAT_SPELL: // we do not average effects, so skip
case ENCHANTMENT_TYPE_USE_SPELL:
@@ -582,7 +602,6 @@ class StatsContainer implements \Countable
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
@@ -602,20 +621,15 @@ class StatsContainer implements \Countable
switch ($auraId)
{
case SPELL_AURA_MOD_STAT:
- if ($miscValue < 0) // all stats
- return [Stat::AGILITY, Stat::STRENGTH, Stat::INTELLECT, Stat::SPIRIT, Stat::STAMINA];
- if ($miscValue == STAT_STRENGTH) // one stat
- return [Stat::STRENGTH];
- if ($miscValue == STAT_AGILITY)
- return [Stat::AGILITY];
- if ($miscValue == STAT_STAMINA)
- return [Stat::STAMINA];
- if ($miscValue == STAT_INTELLECT)
- return [Stat::INTELLECT];
- if ($miscValue == STAT_SPIRIT)
- return [Stat::SPIRIT];
-
- return []; // one bullshit
+ return match ($miscValue)
+ {
+ STAT_STRENGTH => [Stat::STRENGTH],
+ STAT_AGILITY => [Stat::AGILITY],
+ STAT_STAMINA => [Stat::STAMINA],
+ STAT_INTELLECT => [Stat::INTELLECT],
+ STAT_SPIRIT => [Stat::SPIRIT],
+ default => $miscValue < 0 ? [Stat::AGILITY, Stat::STRENGTH, Stat::INTELLECT, Stat::SPIRIT, Stat::STAMINA] : []
+ };
case SPELL_AURA_MOD_INCREASE_HEALTH:
case SPELL_AURA_MOD_INCREASE_HEALTH_NONSTACK:
case SPELL_AURA_MOD_INCREASE_HEALTH_2:
@@ -629,27 +643,16 @@ class StatsContainer implements \Countable
if ($miscValue == SPELL_MAGIC_SCHOOLS)
return [Stat::DAMAGE_SPELL_POWER];
- // HolySpellpower (deprecated; still used in randomproperties)
if ($miscValue & (1 << SPELL_SCHOOL_HOLY))
$stats[] = Stat::HOLY_SPELL_POWER;
-
- // FireSpellpower (deprecated; still used in randomproperties)
if ($miscValue & (1 << SPELL_SCHOOL_FIRE))
$stats[] = Stat::FIRE_SPELL_POWER;
-
- // NatureSpellpower (deprecated; still used in randomproperties)
if ($miscValue & (1 << SPELL_SCHOOL_NATURE))
$stats[] = Stat::NATURE_SPELL_POWER;
-
- // FrostSpellpower (deprecated; still used in randomproperties)
if ($miscValue & (1 << SPELL_SCHOOL_FROST))
$stats[] = Stat::FROST_SPELL_POWER;
-
- // ShadowSpellpower (deprecated; still used in randomproperties)
if ($miscValue & (1 << SPELL_SCHOOL_SHADOW))
$stats[] = Stat::SHADOW_SPELL_POWER;
-
- // ArcaneSpellpower (deprecated; still used in randomproperties)
if ($miscValue & (1 << SPELL_SCHOOL_ARCANE))
$stats[] = Stat::ARCANE_SPELL_POWER;
@@ -657,16 +660,14 @@ class StatsContainer implements \Countable
case SPELL_AURA_MOD_HEALING_DONE: // not as a mask..
return [Stat::HEALING_SPELL_POWER];
case SPELL_AURA_MOD_INCREASE_ENERGY: // MiscVal:type see defined Powers only energy/mana in use
- if ($miscValue == POWER_ENERGY)
- return [Stat::ENERGY];
- if ($miscValue == POWER_RAGE)
- return [Stat::RAGE];
- if ($miscValue == POWER_MANA)
- return [Stat::MANA];
- if ($miscValue == POWER_RUNIC_POWER)
- return [Stat::RUNIC_POWER];
-
- return [];
+ return match ($miscValue)
+ {
+ POWER_ENERGY => [Stat::ENERGY],
+ POWER_RAGE => [Stat::RAGE],
+ POWER_MANA => [Stat::MANA],
+ POWER_RUNIC_POWER => [Stat::RUNIC_POWER],
+ default => []
+ };
case SPELL_AURA_MOD_RATING:
case SPELL_AURA_MOD_RATING_FROM_STAT:
if ($stat = self::convertCombatRating($miscValue))
@@ -703,7 +704,7 @@ class StatsContainer implements \Countable
case SPELL_AURA_MOD_POWER_REGEN: // mp5
return [Stat::MANA_REGENERATION];
case SPELL_AURA_MOD_ATTACK_POWER:
- return [Stat::ATTACK_POWER/*, Stat::RANGED_ATTACK_POWER*/];
+ return [Stat::MELEE_ATTACK_POWER];
case SPELL_AURA_MOD_RANGED_ATTACK_POWER:
return [Stat::RANGED_ATTACK_POWER];
case SPELL_AURA_MOD_SHIELD_BLOCKVALUE:
diff --git a/includes/game/loot.class.php b/includes/game/loot.class.php
deleted file mode 100644
index 1102fd24..00000000
--- a/includes/game/loot.class.php
+++ /dev/null
@@ -1,693 +0,0 @@
-results);
-
- foreach ($this->results as $k => [, $tabData])
- if ($tabData['data']) // only yield tabs with content
- yield $k => $this->results[$k];
- }
-
- public function getResult() : array
- {
- return $this->results;
- }
-
- private function createStack(array $l) : string // issue: TC always has an equal distribution between min/max
- {
- if (empty($l['min']) || empty($l['max']) || $l['max'] <= $l['min'])
- return '';
-
- $stack = [];
- for ($i = $l['min']; $i <= $l['max']; $i++)
- $stack[$i] = round(100 / (1 + $l['max'] - $l['min']), 3);
-
- // yes, it wants a string .. how weired is that..
- return json_encode($stack, JSON_NUMERIC_CHECK); // do not replace with Util::toJSON !
- }
-
- private function storeJSGlobals(array $data) : void
- {
- foreach ($data as $type => $jsData)
- {
- foreach ($jsData as $k => $v)
- {
- // was already set at some point with full data
- if (isset($this->jsGlobals[$type][$k]) && is_array($this->jsGlobals[$type][$k]))
- continue;
-
- $this->jsGlobals[$type][$k] = $v;
- }
- }
- }
-
- private function calcChance(array $refs, array $parents = []) : array
- {
- $retData = [];
- $retKeys = [];
-
- foreach ($refs as $rId => $ref)
- {
- // check for possible database inconsistencies
- if (!$ref['chance'] && !$ref['isGrouped'])
- trigger_error('Loot by Item: Ungrouped Item/Ref '.$ref['item'].' has 0% chance assigned!', E_USER_WARNING);
-
- if ($ref['isGrouped'] && $ref['sumChance'] > 100)
- trigger_error('Loot by Item: Group with Item/Ref '.$ref['item'].' has '.number_format($ref['sumChance'], 2).'% total chance! Some items cannot drop!', E_USER_WARNING);
-
- if ($ref['isGrouped'] && $ref['sumChance'] >= 100 && !$ref['chance'])
- trigger_error('Loot by Item: Item/Ref '.$ref['item'].' with adaptive chance cannot drop. Group already at 100%!', E_USER_WARNING);
-
- $chance = abs($ref['chance'] ?: (100 - $ref['sumChance']) / $ref['nZeroItems']) / 100;
-
- // apply inherited chanceMods
- if (isset($this->chanceMods[$ref['item']]))
- {
- $chance *= $this->chanceMods[$ref['item']][0];
- $chance = 1 - pow(1 - $chance, $this->chanceMods[$ref['item']][1]);
- }
-
- // save chance for parent-ref
- $this->chanceMods[$rId] = [$chance, $ref['multiplier']];
-
- // refTemplate doesn't point to a new ref -> we are done
- if (!in_array($rId, $parents))
- {
- $data = array(
- 'percent' => $chance,
- 'stack' => [$ref['min'], $ref['max']],
- 'count' => 1 // ..and one for the sort script
- );
-
- if ($_ = self::createStack($ref))
- $data['pctstack'] = $_;
-
- // sort highest chances first
- $i = 0;
- for (; $i < count($retData); $i++)
- if ($retData[$i]['percent'] < $data['percent'])
- break;
-
- array_splice($retData, $i, 0, [$data]);
- array_splice($retKeys, $i, 0, [$rId]);
- }
- }
-
- return array_combine($retKeys, $retData);
- }
-
- private function getByContainerRecursive(string $tableName, int $lootId, array &$handledRefs, int $groupId = 0, float $baseChance = 1.0) : array
- {
- $loot = [];
- $rawItems = [];
-
- if (!$tableName || !$lootId)
- return [null, null];
-
- $rows = DB::World()->select('SELECT * FROM ?# WHERE entry = ?d{ AND groupid = ?d}', $tableName, $lootId, $groupId ?: DBSIMPLE_SKIP);
- if (!$rows)
- return [null, null];
-
- $groupChances = [];
- $nGroupEquals = [];
- $cnd = new Conditions();
- foreach ($rows as $entry)
- {
- $set = array(
- 'quest' => $entry['QuestRequired'],
- 'group' => $entry['GroupId'],
- 'parentRef' => $tableName == LOOT_REFERENCE ? $lootId : 0,
- 'realChanceMod' => $baseChance,
- 'groupChance' => 0
- );
-
- if ($entry['QuestRequired'])
- foreach (DB::Aowow()->selectCol('SELECT id FROM ?_quests WHERE (`reqSourceItemId1` = ?d OR `reqSourceItemId2` = ?d OR `reqSourceItemId3` = ?d OR `reqSourceItemId4` = ?d OR `reqItemId1` = ?d OR `reqItemId2` = ?d OR `reqItemId3` = ?d OR `reqItemId4` = ?d OR `reqItemId5` = ?d OR `reqItemId6` = ?d) AND (`cuFlags` & ?d) = 0',
- $entry['Item'], $entry['Item'], $entry['Item'], $entry['Item'], $entry['Item'], $entry['Item'], $entry['Item'], $entry['Item'], $entry['Item'], $entry['Item'], CUSTOM_EXCLUDE_FOR_LISTVIEW | CUSTOM_UNAVAILABLE) as $questId)
- $cnd->addExternalCondition(Conditions::lootTableToConditionSource($tableName), $lootId . ':' . $entry['Item'], [Conditions::QUESTTAKEN, $questId], true);
-
- // if ($entry['LootMode'] > 1)
- // {
- $buff = [];
- for ($i = 0; $i < 8; $i++)
- if ($entry['LootMode'] & (1 << $i))
- $buff[] = $i + 1;
-
- $set['mode'] = implode(', ', $buff);
- // }
- // else
- // $set['mode'] = 0;
-
- /*
- modes:{"mode":8,"4":{"count":7173,"outof":17619},"8":{"count":7173,"outof":10684}}
- ignore lootmodes from sharedDefines.h use different creatures/GOs from each template
- modes.mode = b6543210
- ||||||'dungeon heroic
- |||||'dungeon normal
- ||||'
- |||'10man normal
- ||'25man normal
- |'10man heroic
- '25man heroic
- */
-
- if ($entry['Reference'])
- {
- // bandaid.. remove when propperly handling lootmodes
- if (!in_array($entry['Reference'], $handledRefs))
- { // todo (high): find out, why i used this in the first place. (don't do drugs, kids)
- [$data, $raw] = self::getByContainerRecursive(LOOT_REFERENCE, $entry['Reference'], $handledRefs, /*$entry['GroupId'],*/ 0, $entry['Chance'] / 100);
-
- $handledRefs[] = $entry['Reference'];
-
- $loot = array_merge($loot, $data);
- $rawItems = array_merge($rawItems, $raw);
- }
- $set['reference'] = $entry['Reference'];
- $set['multiplier'] = $entry['MaxCount'];
- }
- else
- {
- $rawItems[] = $entry['Item'];
- $set['content'] = $entry['Item'];
- $set['min'] = $entry['MinCount'];
- $set['max'] = $entry['MaxCount'];
- }
-
- if (!isset($groupChances[$entry['GroupId']]))
- {
- $groupChances[$entry['GroupId']] = 0;
- $nGroupEquals[$entry['GroupId']] = 0;
- }
-
- if ($set['quest'] || !$set['group'])
- $set['groupChance'] = $entry['Chance'];
- else if ($entry['GroupId'] && !$entry['Chance'])
- {
- $nGroupEquals[$entry['GroupId']]++;
- $set['groupChance'] = &$groupChances[$entry['GroupId']];
- }
- else if ($entry['GroupId'] && $entry['Chance'])
- {
- $set['groupChance'] = $entry['Chance'];
-
- if (!$entry['Reference'])
- {
- if (empty($groupChances[$entry['GroupId']]))
- $groupChances[$entry['GroupId']] = 0;
-
- $groupChances[$entry['GroupId']] += $entry['Chance'];
- }
- }
- else // shouldn't have happened
- {
- trigger_error('Unhandled case in calculating chance for item '.$entry['Item'].'!', E_USER_WARNING);
- continue;
- }
-
- $loot[] = $set;
- }
-
- foreach (array_keys($nGroupEquals) as $k)
- {
- $sum = $groupChances[$k];
- if (!$sum)
- $sum = 0;
- else if ($sum >= 100.01)
- {
- trigger_error('Loot entry '.$lootId.' / group '.$k.' has a total chance of '.number_format($sum, 2).'%. Some items cannot drop!', E_USER_WARNING);
- $sum = 100;
- }
- // is applied as backReference to items with 0-chance
- $groupChances[$k] = (100 - $sum) / ($nGroupEquals[$k] ?: 1);
- }
-
- if ($cnd->getBySourceGroup($lootId, Conditions::lootTableToConditionSource($tableName))->prepare())
- {
- self::storeJSGlobals($cnd->getJsGlobals());
- $cnd->toListviewColumn($loot, $this->extraCols, $lootId, 'content');
- }
-
- return [$loot, array_unique($rawItems)];
- }
-
- public function getByContainer(string $table, int $entry): bool
- {
- $this->entry = intVal($entry);
-
- if (!in_array($table, $this->lootTemplates) || !$this->entry)
- return false;
-
- /*
- // if (is_array($this->entry) && in_array($table, [LOOT_CREATURE, LOOT_GAMEOBJECT])
- // iterate over the 4 available difficulties and assign modes
-
-
- modes:{"mode":1,"1":{"count":4408,"outof":16013},"4":{"count":4408,"outof":22531}}
- */
- $handledRefs = [];
- [$lootRows, $itemIds] = self::getByContainerRecursive($table, $this->entry, $handledRefs);
- if (!$lootRows)
- return false;
-
- $items = new ItemList(array(['i.id', $itemIds], Cfg::get('SQL_LIMIT_NONE')));
- self::storeJSGlobals($items->getJSGlobals(GLOBALINFO_SELF | GLOBALINFO_RELATED));
- $foo = $items->getListviewData();
-
- // assign listview LV rows to loot rows, not the other way round! The same item may be contained multiple times
- foreach ($lootRows as $loot)
- {
- $base = array(
- 'percent' => round($loot['groupChance'] * $loot['realChanceMod'], 3),
- 'group' => $loot['group'],
- 'quest' => $loot['quest'],
- 'count' => 1 // satisfies the sort-script
- );
-
- if ($_ = $loot['mode'])
- $base['mode'] = $_;
-
- if ($_ = $loot['parentRef'])
- $base['reference'] = $_;
-
- if (isset($loot['condition']))
- $base['condition'] = $loot['condition'];
-
- if ($_ = self::createStack($loot))
- $base['pctstack'] = $_;
-
- if (empty($loot['reference'])) // regular drop
- {
- if (!isset($foo[$loot['content']]))
- {
- trigger_error('Item #'.$loot['content'].' referenced by loot does not exist!', E_USER_WARNING);
- continue;
- }
-
- if (!User::isInGroup(U_GROUP_EMPLOYEE))
- {
- if (!isset($this->results[$loot['content']]))
- $this->results[$loot['content']] = array_merge($foo[$loot['content']], $base, ['stack' => [$loot['min'], $loot['max']]]);
- else
- $this->results[$loot['content']]['percent'] += $base['percent'];
- }
- else // in case of limited trash loot, check if $foo[] exists
- $this->results[] = array_merge($foo[$loot['content']], $base, ['stack' => [$loot['min'], $loot['max']]]);
- }
- else if (User::isInGroup(U_GROUP_EMPLOYEE)) // create dummy for ref-drop
- {
- $data = array(
- 'id' => $loot['reference'],
- 'name' => '@REFERENCE: '.$loot['reference'],
- 'icon' => 'trade_engineering',
- 'stack' => [$loot['multiplier'], $loot['multiplier']]
- );
- $this->results[] = array_merge($base, $data);
-
- $this->jsGlobals[Type::ITEM][$loot['reference']] = $data;
- }
- }
-
- // move excessive % to extra loot
- if (!User::isInGroup(U_GROUP_EMPLOYEE))
- {
- foreach ($this->results as &$_)
- {
- if ($_['percent'] <= 100)
- continue;
-
- while ($_['percent'] > 200)
- {
- $_['stack'][0]++;
- $_['stack'][1]++;
- $_['percent'] -= 100;
- }
-
- $_['stack'][1]++;
- $_['percent'] = 100;
- }
- }
- else
- {
- $fields = ['mode', 'reference'];
- $base = [];
- $set = 0;
- foreach ($this->results as $foo)
- {
- foreach ($fields as $idx => $field)
- {
- $val = isset($foo[$field]) ? $foo[$field] : 0;
- if (!isset($base[$idx]))
- $base[$idx] = $val;
- else if ($base[$idx] != $val)
- $set |= 1 << $idx;
- }
-
- if ($set == (pow(2, count($fields)) - 1))
- break;
- }
-
- $this->extraCols[] = "\$Listview.funcBox.createSimpleCol('group', 'Group', '7%', 'group')";
- foreach ($fields as $idx => $field)
- if ($set & (1 << $idx))
- $this->extraCols[] = "\$Listview.funcBox.createSimpleCol('".$field."', '".Util::ucFirst($field)."', '7%', '".$field."')";
- }
-
- return true;
- }
-
- public function getByItem(int $entry, int $maxResults = -1, array $lootTableList = []) : bool
- {
- $this->entry = $entry;
-
- if (!$this->entry)
- return false;
-
- if ($maxResults < 0)
- $maxResults = Cfg::get('SQL_LIMIT_DEFAULT');
-
- // [fileName, tabData, tabName, tabId, extraCols, hiddenCols, visibleCols]
- $tabsFinal = array(
- [Type::ITEM, [], '$LANG.tab_containedin', 'contained-in-item', [], [], []],
- [Type::ITEM, [], '$LANG.tab_disenchantedfrom', 'disenchanted-from', [], [], []],
- [Type::ITEM, [], '$LANG.tab_prospectedfrom', 'prospected-from', [], [], []],
- [Type::ITEM, [], '$LANG.tab_milledfrom', 'milled-from', [], [], []],
- [Type::NPC, [], '$LANG.tab_droppedby', 'dropped-by', [], [], []],
- [Type::NPC, [], '$LANG.tab_pickpocketedfrom', 'pickpocketed-from', [], [], []],
- [Type::NPC, [], '$LANG.tab_skinnedfrom', 'skinned-from', [], [], []],
- [Type::NPC, [], '$LANG.tab_minedfromnpc', 'mined-from-npc', [], [], []],
- [Type::NPC, [], '$LANG.tab_salvagedfrom', 'salvaged-from', [], [], []],
- [Type::NPC, [], '$LANG.tab_gatheredfromnpc', 'gathered-from-npc', [], [], []],
- [Type::QUEST, [], '$LANG.tab_rewardfrom', 'reward-from-quest', [], [], []],
- [Type::ZONE, [], '$LANG.tab_fishedin', 'fished-in-zone', [], [], []],
- [Type::OBJECT, [], '$LANG.tab_containedin', 'contained-in-object', [], [], []],
- [Type::OBJECT, [], '$LANG.tab_minedfrom', 'mined-from-object', [], [], []],
- [Type::OBJECT, [], '$LANG.tab_gatheredfrom', 'gathered-from-object', [], [], []],
- [Type::OBJECT, [], '$LANG.tab_fishedin', 'fished-in-object', [], [], []],
- [Type::SPELL, [], '$LANG.tab_createdby', 'created-by', [], [], []],
- [Type::ACHIEVEMENT, [], '$LANG.tab_rewardfrom', 'reward-from-achievement', [], [], []]
- );
- $refResults = [];
- $query = 'SELECT
- lt1.entry AS ARRAY_KEY,
- IF(lt1.reference = 0, lt1.item, lt1.reference) AS item,
- lt1.chance,
- SUM(IF(lt2.chance = 0, 1, 0)) AS nZeroItems,
- SUM(IF(lt2.reference = 0, lt2.chance, 0)) AS sumChance,
- IF(lt1.groupid > 0, 1, 0) AS isGrouped,
- IF(lt1.reference = 0, lt1.mincount, 1) AS min,
- IF(lt1.reference = 0, lt1.maxcount, 1) AS max,
- IF(lt1.reference > 0, lt1.maxcount, 1) AS multiplier
- FROM
- ?# lt1
- LEFT JOIN
- ?# lt2 ON lt1.entry = lt2.entry AND lt1.groupid = lt2.groupid
- WHERE
- %s
- GROUP BY lt2.entry, lt2.groupid';
-
- /*
- get references containing the item
- */
- $newRefs = DB::World()->select(
- sprintf($query, 'lt1.item = ?d AND lt1.reference = 0'),
- LOOT_REFERENCE, LOOT_REFERENCE,
- $this->entry
- );
-
- /* i'm currently not seeing a reasonable way to blend this into creature/gobject/etc tabs as one entity may drop the same item multiple times, with and without conditions.
- if ($newRefs)
- {
- $cnd = new Conditions();
- if ($cnd->getBySourceEntry($this->entry, Conditions::SRC_REFERENCE_LOOT_TEMPLATE))
- if ($cnd->toListviewColumn($newRefs, $x, $this->entry))
- self::storejsGlobals($cnd->getJsGlobals());
- }
- */
-
- while ($newRefs)
- {
- $curRefs = $newRefs;
- $newRefs = DB::World()->select(
- sprintf($query, 'lt1.reference IN (?a)'),
- LOOT_REFERENCE, LOOT_REFERENCE,
- array_keys($curRefs)
- );
-
- $refResults += $this->calcChance($curRefs, array_column($newRefs, 'item'));
- }
-
- /*
- search the real loot-templates for the itemId and gathered refds
- */
- foreach ($this->lootTemplates as $lootTemplate)
- {
- if ($lootTableList && !in_array($lootTemplate, $lootTableList))
- continue;
-
- if ($lootTemplate == LOOT_REFERENCE)
- continue;
-
- $result = $this->calcChance(DB::World()->select(
- sprintf($query, '{lt1.reference IN (?a) OR }(lt1.reference = 0 AND lt1.item = ?d)'),
- $lootTemplate, $lootTemplate,
- $refResults ? array_keys($refResults) : DBSIMPLE_SKIP,
- $this->entry
- ));
-
- // do not skip here if $result is empty. Additional loot for spells and quest is added separately
-
- // format for actual use
- foreach ($result as $k => $v)
- {
- unset($result[$k]);
- $v['percent'] = round($v['percent'] * 100, 3);
- $result[abs($k)] = $v;
- }
-
- // cap fetched entries to the sql-limit to guarantee, that the highest chance items get selected first
- // screws with GO-loot and skinning-loot as these templates are shared for several tabs (fish, herb, ore) (herb, ore, leather)
- $ids = array_slice(array_keys($result), 0, $maxResults);
-
- switch ($lootTemplate)
- {
- case LOOT_CREATURE: $field = 'lootId'; $tabId = 4; break;
- case LOOT_PICKPOCKET: $field = 'pickpocketLootId'; $tabId = 5; break;
- case LOOT_SKINNING: $field = 'skinLootId'; $tabId = -6; break; // assigned later
- case LOOT_PROSPECTING: $field = 'id'; $tabId = 2; break;
- case LOOT_MILLING: $field = 'id'; $tabId = 3; break;
- case LOOT_ITEM: $field = 'id'; $tabId = 0; break;
- case LOOT_DISENCHANT: $field = 'disenchantId'; $tabId = 1; break;
- case LOOT_FISHING: $field = 'id'; $tabId = 11; break; // subAreas are currently ignored
- case LOOT_GAMEOBJECT:
- if (!$ids)
- continue 2;
-
- $srcObj = new GameObjectList(array(['lootId', $ids]));
- if ($srcObj->error)
- continue 2;
-
- $srcData = $srcObj->getListviewData();
-
- foreach ($srcObj->iterate() as $curTpl)
- {
- switch ($curTpl['typeCat'])
- {
- case 25: $tabId = 15; break; // fishing node
- case -3: $tabId = 14; break; // herb
- case -4: $tabId = 13; break; // vein
- default: $tabId = 12; break; // general chest loot
- }
-
- $tabsFinal[$tabId][1][] = array_merge($srcData[$srcObj->id], $result[$srcObj->getField('lootId')]);
- $tabsFinal[$tabId][4][] = '$Listview.extraCols.percent';
- if ($tabId != 15)
- $tabsFinal[$tabId][6][] = 'skill';
- }
- continue 2;
- case LOOT_MAIL:
- // quest part
- $conditions = array(['rewardChoiceItemId1', $this->entry], ['rewardChoiceItemId2', $this->entry], ['rewardChoiceItemId3', $this->entry], ['rewardChoiceItemId4', $this->entry], ['rewardChoiceItemId5', $this->entry],
- ['rewardChoiceItemId6', $this->entry], ['rewardItemId1', $this->entry], ['rewardItemId2', $this->entry], ['rewardItemId3', $this->entry], ['rewardItemId4', $this->entry],
- 'OR');
- if ($ids)
- $conditions[] = ['rewardMailTemplateId', $ids];
-
- $srcObj = new QuestList($conditions);
- if (!$srcObj->error)
- {
- self::storeJSGlobals($srcObj->getJSGlobals(GLOBALINFO_SELF | GLOBALINFO_REWARDS));
- $srcData = $srcObj->getListviewData();
-
- foreach ($srcObj->iterate() as $_)
- $tabsFinal[10][1][] = array_merge($srcData[$srcObj->id], empty($result[$srcObj->id]) ? ['percent' => -1] : $result[$srcObj->id]);
- }
-
- // achievement part
- $conditions = array(['itemExtra', $this->entry]);
- if ($ar = DB::World()->selectCol('SELECT ID FROM achievement_reward WHERE ItemID = ?d{ OR MailTemplateID IN (?a)}', $this->entry, $ids ?: DBSIMPLE_SKIP))
- array_push($conditions, ['id', $ar], 'OR');
-
- $srcObj = new AchievementList($conditions);
- if (!$srcObj->error)
- {
- self::storeJSGlobals($srcObj->getJSGlobals(GLOBALINFO_SELF | GLOBALINFO_REWARDS));
- $srcData = $srcObj->getListviewData();
-
- foreach ($srcObj->iterate() as $_)
- $tabsFinal[17][1][] = array_merge($srcData[$srcObj->id], empty($result[$srcObj->id]) ? ['percent' => -1] : $result[$srcObj->id]);
-
- $tabsFinal[17][5][] = 'rewards';
- $tabsFinal[17][6][] = 'category';
- }
- continue 2;
- case LOOT_SPELL:
- $conditions = array(
- 'OR',
- ['AND', ['effect1CreateItemId', $this->entry], ['OR', ['effect1Id', SpellList::EFFECTS_ITEM_CREATE], ['effect1AuraId', SpellList::AURAS_ITEM_CREATE]]],
- ['AND', ['effect2CreateItemId', $this->entry], ['OR', ['effect2Id', SpellList::EFFECTS_ITEM_CREATE], ['effect2AuraId', SpellList::AURAS_ITEM_CREATE]]],
- ['AND', ['effect3CreateItemId', $this->entry], ['OR', ['effect3Id', SpellList::EFFECTS_ITEM_CREATE], ['effect3AuraId', SpellList::AURAS_ITEM_CREATE]]],
- );
- if ($ids)
- $conditions[] = ['id', $ids];
-
- $srcObj = new SpellList($conditions);
- if (!$srcObj->error)
- {
- self::storeJSGlobals($srcObj->getJSGlobals(GLOBALINFO_SELF | GLOBALINFO_RELATED));
- $srcData = $srcObj->getListviewData();
-
- if (!empty($result))
- $tabsFinal[16][4][] = '$Listview.extraCols.percent';
-
- if ($srcObj->hasSetFields('reagent1', 'reagent2', 'reagent3', 'reagent4', 'reagent5', 'reagent6', 'reagent7', 'reagent8'))
- $tabsFinal[16][6][] = 'reagents';
-
- foreach ($srcObj->iterate() as $_)
- $tabsFinal[16][1][] = array_merge($srcData[$srcObj->id], empty($result[$srcObj->id]) ? ['percent' => -1] : $result[$srcObj->id]);
- }
- continue 2;
- }
-
- if (!$ids)
- continue;
-
- $parentData = [];
- switch ($tabsFinal[abs($tabId)][0])
- {
- case TYPE::NPC: // new CreatureList
- if ($baseIds = DB::Aowow()->selectCol(
- 'SELECT `difficultyEntry1` AS ARRAY_KEY, `id` FROM ?_creature WHERE difficultyEntry1 IN (?a) UNION
- SELECT `difficultyEntry2` AS ARRAY_KEY, `id` FROM ?_creature WHERE difficultyEntry2 IN (?a) UNION
- SELECT `difficultyEntry3` AS ARRAY_KEY, `id` FROM ?_creature WHERE difficultyEntry3 IN (?a)',
- $ids, $ids, $ids))
- {
- $parentObj = new CreatureList(array(['id', $baseIds]));
- if (!$parentObj->error)
- {
- self::storeJSGlobals($parentObj->getJSGlobals());
- $parentData = $parentObj->getListviewData();
- $ids = array_diff($ids, $baseIds);
- }
- }
-
- case Type::ITEM: // new ItemList
- case Type::ZONE: // new ZoneList
- $srcObj = Type::newList($tabsFinal[abs($tabId)][0], array([$field, $ids]));
- if (!$srcObj->error)
- {
- $srcData = $srcObj->getListviewData();
- self::storeJSGlobals($srcObj->getJSGlobals(GLOBALINFO_SELF | GLOBALINFO_RELATED));
-
- foreach ($srcObj->iterate() as $curTpl)
- {
- if ($tabId < 0 && $curTpl['typeFlags'] & NPC_TYPEFLAG_SKIN_WITH_HERBALISM)
- $tabId = 9;
- else if ($tabId < 0 && $curTpl['typeFlags'] & NPC_TYPEFLAG_SKIN_WITH_ENGINEERING)
- $tabId = 8;
- else if ($tabId < 0 && $curTpl['typeFlags'] & NPC_TYPEFLAG_SKIN_WITH_MINING)
- $tabId = 7;
- else if ($tabId < 0)
- $tabId = abs($tabId); // general case (skinning)
-
- if (($p = $srcObj->getField('parentId')) && ($d = $parentData[$p] ?? null))
- $tabsFinal[$tabId][1][] = array_merge($d, $result[$srcObj->getField($field)]);
- else
- $tabsFinal[$tabId][1][] = array_merge($srcData[$srcObj->id], $result[$srcObj->getField($field)]);
-
- $tabsFinal[$tabId][4][] = '$Listview.extraCols.percent';
- }
- }
- break;
- }
- }
-
- foreach ($tabsFinal as $tabId => $data)
- {
- $tabData = array(
- 'data' => $data[1],
- 'name' => $data[2],
- 'id' => $data[3]
- );
-
- if ($data[4])
- $tabData['extraCols'] = array_unique($data[4]);
-
- if ($data[5])
- $tabData['hiddenCols'] = array_unique($data[5]);
-
- if ($data[6])
- $tabData['visibleCols'] = array_unique($data[6]);
-
- $this->results[$tabId] = [Type::getFileString($data[0]), $tabData];
- }
-
- return true;
- }
-}
-
-?>
diff --git a/includes/game/loot/loot.class.php b/includes/game/loot/loot.class.php
new file mode 100644
index 00000000..55c6f513
--- /dev/null
+++ b/includes/game/loot/loot.class.php
@@ -0,0 +1,71 @@
+jsGlobals, $data);
+ }
+}
+
+?>
diff --git a/includes/game/loot/lootbycontainer.class.php b/includes/game/loot/lootbycontainer.class.php
new file mode 100644
index 00000000..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
index 67bfc553..11cc6ab1 100644
--- a/includes/game/misc.php
+++ b/includes/game/misc.php
@@ -8,15 +8,15 @@ if (!defined('AOWOW_REVISION'))
class Game
{
- public static $resistanceFields = array(
+ public static array $resistanceFields = array(
null, 'resHoly', 'resFire', 'resNature', 'resFrost', 'resShadow', 'resArcane'
);
- public static $rarityColorStings = array( // zero-indexed
+ public static array $rarityColorStings = array( // zero-indexed
'9d9d9d', 'ffffff', '1eff00', '0070dd', 'a335ee', 'ff8000', 'e5cc80', 'e6cc80'
);
- public static $specIconStrings = array(
+ public static array $specIconStrings = array(
-1 => 'inv_misc_questionmark',
0 => 'spell_nature_elementalabsorption',
6 => ['spell_deathknight_bloodpresence', 'spell_deathknight_frostpresence', 'spell_deathknight_unholypresence' ],
@@ -46,9 +46,9 @@ class Game
10 => [ 65, 66, 67, 210, 394, 495, 2817, 3537, 3711, 4024, 4197, 4395, 4742]
);
- // zoneorsort for quests need updating
+ // questSortId for quests need updating
// partially points non-instanced area with identical name for instance quests
- public static $questSortFix = array(
+ public static array $questSortFix = array(
-221 => 440, // Treasure Map => Tanaris
-284 => 0, // Special => Misc (some quests get shuffled into seasonal)
151 => 0, // Designer Island => Misc
@@ -86,7 +86,7 @@ class Game
1417 => 1477 // Sunken Temple
);
- public static $questSubCats = array(
+ public static array $questSubCats = array(
1 => [132], // Dun Morogh: Coldridge Valley
12 => [9], // Elwynn Forest: Northshire Valley
141 => [188], // Teldrassil: Shadowglen
@@ -109,11 +109,11 @@ class Game
);
/* 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 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
+ .. the indizes of this array are bits of skillLine2OrMask in ::spell if skillLineId1 is negative
*/
- public static $skillLineMask = array( // idx => [familyId, skillLineId]
+ public static array $skillLineMask = array( // idx => [familyId, skillLineId]
-1 => array( // Pets (Hunter)
[ 1, 208], [ 2, 209], [ 3, 203], [ 4, 210], [ 5, 211], [ 6, 212], [ 7, 213], // Wolf, Cat, Spider, Bear, Boar, Crocolisk, Carrion Bird
[ 8, 214], [ 9, 215], [11, 217], [12, 218], [20, 236], [21, 251], [24, 653], // Crab, Gorilla, Raptor, Tallstrider, Scorpid, Turtle, Bat
@@ -129,31 +129,25 @@ class Game
)
);
- public static $sockets = array( // jsStyle Strings
+ public static array $sockets = array( // jsStyle Strings
'meta', 'red', 'yellow', 'blue'
);
- public static function getReputationLevelForPoints($pts)
+ public static function getReputationLevelForPoints(int $pts) : int
{
- if ($pts >= 41999)
- return REP_EXALTED;
- else if ($pts >= 20999)
- 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;
+ 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(&$spell)
+ public static function getTaughtSpells(mixed &$spell) : array
{
$extraIds = [-1]; // init with -1 to prevent empty-array errors
$lookup = [-1];
@@ -180,8 +174,8 @@ class Game
// 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),
+ 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
);
@@ -196,7 +190,7 @@ class Game
$pages = [];
while ($ptId)
{
- if ($row = DB::World()->selectRow('SELECT ptl.`Text` AS Text_loc?d, pt.* FROM page_text pt LEFT JOIN page_text_locale ptl ON pt.`ID` = ptl.`ID` AND locale = ? WHERE pt.`ID` = ?d', Lang::getLocale()->value, Lang::getLocale()->json(), $ptId))
+ 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');
@@ -216,20 +210,21 @@ class Game
$quotes = [];
$soundIds = [];
- $quoteSrc = DB::World()->select(
+ $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",
- { IFNULL(NULLIF(bctl.`Text`, ""), IFNULL(NULLIF(bctl.`Text1`, ""), IFNULL(ctl.`Text`, ""))) AS text_loc?d, }
+ %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 creature_text_locale ctl ON ct.`CreatureID` = ctl.`CreatureID` AND ct.`GroupID` = ctl.`GroupID` AND ct.`ID` = ctl.`ID` AND ctl.`Locale` = ? }
LEFT JOIN broadcast_text bct ON ct.`BroadcastTextId` = bct.`ID`
- { LEFT JOIN broadcast_text_locale bctl ON ct.`BroadcastTextId` = bctl.`ID` AND bctl.`locale` = ? }
- WHERE ct.`CreatureID` = ?d',
- Lang::getLocale()->value ?: DBSIMPLE_SKIP,
- Lang::getLocale()->value ? Lang::getLocale()->json() : DBSIMPLE_SKIP,
- Lang::getLocale()->value ? Lang::getLocale()->json() : DBSIMPLE_SKIP,
+ %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
);
@@ -268,7 +263,7 @@ class Game
};
// prefix
- $prefix = '%s ';
+ $prefix = '';
if ($t['talkType'] != 4)
$prefix = ($talkSource ?: '%s').' '.Lang::npc('textTypes', $t['talkType']).Lang::main('colon').($t['lang'] ? '['.Lang::game('languages', $t['lang']).'] ' : ' ');
@@ -338,7 +333,7 @@ class Game
public static function getEnchantmentCondition(int $conditionId, bool $interactive = false) : string
{
- $gemCnd = DB::Aowow()->selectRow('SELECT * FROM ?_itemenchantmentcondition WHERE `id` = ?d', $conditionId);
+ $gemCnd = DB::Aowow()->selectRow('SELECT * FROM ::itemenchantmentcondition WHERE `id` = %i', $conditionId);
if (!$gemCnd)
return '';
diff --git a/includes/game/worldposition.class.php b/includes/game/worldposition.class.php
index 9f8d4ff3..df13aaf6 100644
--- a/includes/game/worldposition.class.php
+++ b/includes/game/worldposition.class.php
@@ -8,6 +8,7 @@ if (!defined('AOWOW_REVISION'))
abstract class WorldPosition
{
+ private static array $zoneMapCache = [];
private static array $alphaMapCache = [];
private static array $capitalCities = array( // capitals take precedence over their surrounding area
1497, 1637, 1638, 3487, // Undercity, Ogrimmar, Thunder Bluff, Silvermoon City
@@ -67,7 +68,7 @@ abstract class WorldPosition
// spawn does not really match on a map, but we need at least one result
if (!$result)
{
- usort($points, function ($a, $b) { return ($a['dist'] < $b['dist']) ? -1 : 1; });
+ usort($points, fn($a, $b) => $a['dist'] <=> $b['dist']);
$result = [1.0, $points[0]];
}
@@ -81,25 +82,25 @@ abstract class WorldPosition
switch ($type)
{
case Type::NPC:
- $result = DB::World()->select('SELECT `guid` AS ARRAY_KEY, `id`, `map` AS `mapId`, `position_x` AS `posX`, `position_y` AS `posY` FROM creature WHERE `guid` IN (?a)', $guids);
+ $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()->select('SELECT `guid` AS ARRAY_KEY, `id`, `map` AS `mapId`, `position_x` AS `posX`, `position_y` AS `posY` FROM gameobject WHERE `guid` IN (?a)', $guids);
+ $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()->select('SELECT `id` AS ARRAY_KEY, `soundId` AS `id`, `mapId`, `posX`, `posY` FROM ?_soundemitters WHERE `id` IN (?a)', $guids);
+ $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()->select('SELECT -`id` AS ARRAY_KEY, `id`, `parentMapId` AS `mapId`, `parentX` AS `posX`, `parentY` AS `posY` FROM ?_zones WHERE -`id` IN (?a)', $guids);
+ $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()->select('SELECT `id` AS ARRAY_KEY, `id`, `mapId`, `posX`, `posY` FROM ?_areatrigger WHERE `id` IN (?a)', $base));
+ $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()->select(
- 'SELECT -`ID` AS ARRAY_KEY, ID AS `id`, `target_map` AS `mapId`, `target_position_x` AS `posX`, `target_position_y` AS `posY` FROM areatrigger_teleport WHERE -`id` IN (?a) UNION
- SELECT -`entryorguid` AS ARRAY_KEY, entryorguid AS `id`, `action_param1` AS `mapId`, `target_x` AS `posX`, `target_y` AS `posY` FROM smart_scripts WHERE -`entryorguid` IN (?a) AND `source_type` = ?d AND `action_type` = ?d',
+ $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;
@@ -118,42 +119,77 @@ abstract class WorldPosition
if (!$mapId < 0)
return [];
- $query =
+ 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(dm.`id` IS NOT NULL OR x.`defaultDungeonMapId` < 0, 1, 0) AS `multifloor`,
- ROUND((x.`maxY` - ?d) * 100 / (x.`maxY` - x.`minY`), 1) AS `posX`,
- ROUND((x.`maxX` - ?d) * 100 / (x.`maxX` - x.`minX`), 1) AS `posY`,
- SQRT(POWER(ABS((x.`maxY` - ?d) * 100 / (x.`maxY` - x.`minY`) - 50), 2) +
- POWER(ABS((x.`maxX` - ?d) * 100 / (x.`maxX` - x.`minX`) - 50), 2)) AS `dist`
+ 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 ?_worldmaparea wma UNION
- SELECT dm.`id`, `areaId`, wma.`mapId`, `minY`, `maxY`, `maxX`, `minX`, `floor`, `worldMapAreaId`, `defaultDungeonMapId` FROM ?_worldmaparea wma
- JOIN ?_dungeonmap dm ON dm.`mapId` = wma.`mapId` WHERE wma.`mapId` NOT IN (0, 1, 530, 571) OR wma.`areaId` = 4395) x
+ (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
- ?_dungeonmap dm ON dm.`mapId` = x.`mapId` AND dm.`worldMapAreaId` = x.`worldMapAreaId` AND dm.`floor` <> x.`floor` AND dm.`worldMapAreaId` > 0
+ 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` = ?d AND IF(?d, x.`areaId` = ?d, x.`areaId` <> 0){ AND x.`floor` = ?d - IF(x.`defaultDungeonMapId` < 0, 1, 0)}
+ 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`
- HAVING
- (`posX` BETWEEN 0.1 AND 99.9 AND `posY` BETWEEN 0.1 AND 99.9)
- ORDER BY
- `multifloor` DESC, `dist` ASC';
-
- // dist BETWEEN 0 (center) AND 70.7 (corner)
- $points = DB::Aowow()->select($query, $mapY, $mapX, $mapY, $mapX, $mapId, $preferedAreaId, $preferedAreaId, $preferedFloor < 0 ? DBSIMPLE_SKIP : $preferedFloor);
- if (!$points) // retry: pre-instance subareas belong to the instance-maps but are displayed on the outside. There also cases where the zone reaches outside it's own map.
- $points = DB::Aowow()->select($query, $mapY, $mapX, $mapY, $mapX, $mapId, 0, 0, DBSIMPLE_SKIP);
- if (!is_array($points))
- {
- trigger_error('WorldPosition::toZonePos - query failed', E_USER_ERROR);
- return [];
- }
-
- return $points;
+ x.`id`, x.`areaId`',
+ $mapId
+ ) ?: [];
}
}
diff --git a/includes/kernel.php b/includes/kernel.php
index cb4c6ac4..6afe3dbb 100644
--- a/includes/kernel.php
+++ b/includes/kernel.php
@@ -3,10 +3,11 @@
namespace Aowow;
mb_internal_encoding('UTF-8');
+mb_substitute_character('none'); // drop invalid chars entirely instead of replacing them with '?'
error_reporting(E_ALL);
mysqli_report(MYSQLI_REPORT_ERROR);
-define('AOWOW_REVISION', 41);
+define('AOWOW_REVISION', 47);
define('OS_WIN', substr(PHP_OS, 0, 3) == 'WIN'); // OS_WIN as per compile info of php
define('CLI', PHP_SAPI === 'cli');
define('CLI_HAS_E', CLI && // WIN10 and later usually support ANSI escape sequences
@@ -32,8 +33,9 @@ if ($error)
require_once 'includes/defines.php';
require_once 'includes/locale.class.php';
require_once 'localization/lang.class.php';
-require_once 'includes/libs/DbSimple/Generic.php'; // Libraray: http://en.dklab.ru/lib/DbSimple (using variant: https://github.com/ivan1986/DbSimple/tree/master)
-require_once 'includes/database.class.php'; // wrap DBSimple
+require_once '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
@@ -53,6 +55,8 @@ spl_autoload_register(function (string $class) : void
require_once 'includes/game/chrstatistics.php';
else if (file_exists('includes/game/'.strtolower($class).'.class.php'))
require_once 'includes/game/'.strtolower($class).'.class.php';
+ else if (file_exists('includes/game/loot/'.strtolower($class).'.class.php'))
+ require_once 'includes/game/loot/'.strtolower($class).'.class.php';
});
// our site components
@@ -129,6 +133,10 @@ set_error_handler(function(int $errNo, string $errStr, string $errFile, int $err
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;
@@ -149,15 +157,23 @@ set_error_handler(function(int $errNo, string $errStr, string $errFile, int $err
default => 'UNKNOWN_ERROR' // errors not in this list can not be handled by set_error_handler (as per documentation) or are ignored
};
+ if (!empty($_POST['password']))
+ $_POST['password'] = '******';
+ if (!empty($_POST['c_password']))
+ $_POST['c_password'] = '******';
+
if (DB::isConnected(DB_AOWOW))
- DB::Aowow()->query('INSERT INTO ?_errors (`date`, `version`, `phpError`, `file`, `line`, `query`, `post`, `userGroups`, `message`) VALUES (UNIX_TIMESTAMP(), ?d, ?d, ?, ?d, ?, ?, ?d, ?) ON DUPLICATE KEY UPDATE `date` = UNIX_TIMESTAMP()',
+ 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
);
- if (CLI)
- CLI::write($errName.' - '.$errStr.' @ '.$errFile. ':'.$errLine, $logLevel);
+ $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($errName.' - '.$errStr.' @ '.$errFile. ':'.$errLine, U_GROUP_EMPLOYEE, $logLevel);
+ Util::addNote($logMsg, U_GROUP_EMPLOYEE, $logLevel);
return true;
}, E_ALL);
@@ -165,8 +181,13 @@ set_error_handler(function(int $errNo, string $errStr, string $errFile, int $err
// handle exceptions
set_exception_handler(function (\Throwable $e) : void
{
+ if (!empty($_POST['password']))
+ $_POST['password'] = '******';
+ if (!empty($_POST['c_password']))
+ $_POST['c_password'] = '******';
+
if (DB::isConnected(DB_AOWOW))
- DB::Aowow()->query('INSERT INTO ?_errors (`date`, `version`, `phpError`, `file`, `line`, `query`, `post`, `userGroups`, `message`) VALUES (UNIX_TIMESTAMP(), ?d, ?d, ?, ?d, ?, ?, ?d, ?) ON DUPLICATE KEY UPDATE `date` = UNIX_TIMESTAMP()',
+ 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()
);
@@ -188,8 +209,13 @@ register_shutdown_function(function() : void
if ($e = error_get_last())
{
+ if (!empty($_POST['password']))
+ $_POST['password'] = '******';
+ if (!empty($_POST['c_password']))
+ $_POST['c_password'] = '******';
+
if (DB::isConnected(DB_AOWOW))
- DB::Aowow()->query('INSERT INTO ?_errors (`date`, `version`, `phpError`, `file`, `line`, `query`, `post`, `userGroups`, `message`) VALUES (UNIX_TIMESTAMP(), ?d, ?d, ?, ?d, ?, ?, ?d, ?) ON DUPLICATE KEY UPDATE `date` = UNIX_TIMESTAMP()',
+ 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']
);
@@ -257,20 +283,6 @@ if (!CLI)
Lang::load($loc);
else
Lang::load(User::$preferedLoc);
-
- // set up some logging (some queries will execute before we init the user and load the config)
- if (Cfg::get('DEBUG') >= LOG_LEVEL_INFO && User::isInGroup(U_GROUP_DEV | U_GROUP_ADMIN))
- {
- DB::Aowow()->setLogger(DB::profiler(...));
- DB::World()->setLogger(DB::profiler(...));
- if (DB::isConnected(DB_AUTH))
- DB::Auth()->setLogger(DB::profiler(...));
-
- if (!empty($AoWoWconf['characters']))
- foreach ($AoWoWconf['characters'] as $idx => $__)
- if (DB::isConnected(DB_CHARACTERS . $idx))
- DB::Characters($idx)->setLogger(DB::profiler(...));
- }
}
?>
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 b9d19c62..00000000
--- a/includes/libs/DbSimple/Connect.php
+++ /dev/null
@@ -1,261 +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(...$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 d6a6a5bf..00000000
--- a/includes/libs/DbSimple/Database.php
+++ /dev/null
@@ -1,1406 +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
-{
- private $attributes;
-
- /**
- * 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(...$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, ...$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)
- {
- $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)
- {
- $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)
- {
- $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)
- {
- $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)
- {
- $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 2c1fcccd..00000000
--- a/includes/libs/DbSimple/Mysqli.php
+++ /dev/null
@@ -1,245 +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);
- mysqli_ping($this->link);
- $result = mysqli_query($this->link, $queryMain[0]);
- if ($result === false)
- return $this->_setDbError($queryMain[0]);
-
- if ($this->link->warning_count) {
- if ($warn = $this->link->query("SHOW WARNINGS")) {
- while ($warnRow = $warn->fetch_row())
- if ($warnRow[0] === 'Warning')
- $this->_setLastError(-$warnRow[1], $warnRow[2], $queryMain[0]);
-
- $warn->close();
- }
- }
-
- if (!is_object($result)) {
- if (preg_match('/^\s* INSERT \s+/six', $queryMain[0]))
- {
- // 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/setup/cli.class.php b/includes/setup/cli.class.php
index 14cda6cf..ef16296f 100644
--- a/includes/setup/cli.class.php
+++ b/includes/setup/cli.class.php
@@ -152,24 +152,14 @@ abstract class CLI
if ($timestamp)
$msg = str_pad(date('H:i:s'), 10);
- switch ($lvl)
+ $msg .= match ($lvl)
{
- case self::LOG_ERROR: // red critical error
- $msg .= '['.self::red('ERR').'] ';
- break;
- case self::LOG_WARN: // yellow notice
- $msg .= '['.self::yellow('WARN').'] ';
- break;
- case self::LOG_OK: // green success
- $msg .= '['.self::green('OK').'] ';
- break;
- case self::LOG_INFO: // blue info
- $msg .= '['.self::blue('INFO').'] ';
- break;
- case self::LOG_BLANK:
- $msg .= ' ';
- break;
- }
+ self::LOG_ERROR => '['.self::red('ERR').'] ', // red critical error
+ self::LOG_WARN => '['.self::yellow('WARN').'] ', // yellow notice
+ self::LOG_OK => '['.self::green('OK').'] ', // green success
+ self::LOG_INFO => '['.self::blue('INFO').'] ', // blue info
+ default => ' '
+ };
$msg .= $txt;
}
@@ -288,7 +278,12 @@ abstract class CLI
continue;
// stream_get_contents is always blocking under WIN - fgets should work similary as php always receives a terminated line of text
- $chars = str_split(OS_WIN ? fgets(STDIN) : stream_get_contents(STDIN));
+ $chars = str_split(OS_WIN ? fgets(STDIN) : stream_get_contents(STDIN));
+
+ // $chars can be empty if used non-interactive
+ if (!$chars)
+ return false;
+
$ordinals = array_map('ord', $chars);
if ($ordinals[0] == self::CHR_ESC)
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
index a9c22c15..7c36620d 100644
--- a/includes/setup/timer.class.php
+++ b/includes/setup/timer.class.php
@@ -8,9 +8,9 @@ if (!defined('AOWOW_REVISION'))
class Timer
{
- private $t_cur = 0;
- private $t_new = 0;
- private $intv = 0;
+ private float $t_cur = 0;
+ private float $t_new = 0;
+ private float $intv = 0;
public function __construct(int $intervall)
{
diff --git a/includes/type.class.php b/includes/type.class.php
index 822b31f1..6676009b 100644
--- a/includes/type.class.php
+++ b/includes/type.class.php
@@ -152,7 +152,7 @@ abstract class Type
if (!(self::$data[$type][self::IDX_FLAGS] & self::FLAG_DB_TYPE))
return [];
- return DB::Aowow()->selectCol('SELECT `id` FROM ?# WHERE `id` IN (?a)', self::$data[$type][self::IDX_LIST_OBJ]::$dataTable, (array)$ids);
+ 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
diff --git a/includes/user.class.php b/includes/user.class.php
index e310239f..9fa1bd59 100644
--- a/includes/user.class.php
+++ b/includes/user.class.php
@@ -29,7 +29,28 @@ class User
public static function init()
{
- self::setIP();
+ # set ip #
+
+ $ipAddr = '';
+ foreach (['HTTP_CLIENT_IP', 'HTTP_X_FORWARDED_FOR', 'HTTP_X_FORWARDED', 'HTTP_FORWARDED_FOR', 'HTTP_FORWARDED', 'REMOTE_ADDR'] as $env)
+ {
+ if ($rawIp = getenv($env))
+ {
+ if ($env == 'HTTP_X_FORWARDED')
+ $rawIp = explode(',', $rawIp)[0]; // [ip, proxy1, proxy2]
+
+ if ($ipAddr = filter_var($rawIp, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4))
+ break;
+
+ if ($ipAddr = filter_var($rawIp, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6))
+ break;
+ }
+ }
+
+ self::$ip = $ipAddr ?: null;
+
+
+ # set locale #
if (isset($_SESSION['locale']) && $_SESSION['locale'] instanceof Locale)
self::$preferedLoc = $_SESSION['locale']->validate() ?? Locale::getFallback();
@@ -38,36 +59,42 @@ class User
else
self::$preferedLoc = Locale::getFallback();
- // session have a dataKey to access the JScripts (yes, also the anons)
- if (empty($_SESSION['dataKey']))
+
+ # set basic data #
+
+ if (empty($_SESSION['dataKey'])) // session have a dataKey to access the JScripts (yes, also the anons)
$_SESSION['dataKey'] = Util::createHash(); // just some random numbers for identification purpose
self::$dataKey = $_SESSION['dataKey'];
- self::$agent = $_SERVER['HTTP_USER_AGENT'];
+ 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` = ? AND `type` = ?d', self::$ip, IP_BAN_TYPE_LOGIN_ATTEMPT))
+
+ # 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))
{
if ($ipBan['count'] > Cfg::get('ACC_FAILED_AUTH_COUNT') && $ipBan['active'])
return false;
else if (!$ipBan['active'])
- DB::Aowow()->query('DELETE FROM ?_account_bannedips WHERE `ip` = ?', self::$ip);
+ DB::Aowow()->qry('DELETE FROM ::account_bannedips WHERE `ip` = %s', self::$ip);
}
- // try to restore session
+
+ # try to restore session #
+
if (empty($_SESSION['user']))
return false;
- $session = DB::Aowow()->selectRow('SELECT `userId`, `expires` FROM ?_account_sessions WHERE `status` = ?d AND `sessionId` = ?', SESSION_ACTIVE, session_id());
+ $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` = ?d
+ 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']
);
@@ -79,20 +106,20 @@ class User
}
else if ($session['expires'] && $session['expires'] < time())
{
- DB::Aowow()->query('UPDATE ?_account_sessions SET `touched` = ?d, `status` = ?d WHERE `sessionId` = ?', time(), SESSION_EXPIRED, session_id());
+ DB::Aowow()->qry('UPDATE ::account_sessions SET `touched` = %i, `status` = %i WHERE `sessionId` = %s', time(), SESSION_EXPIRED, session_id());
self::destroy();
return false;
}
else if ($session['userId'] != $userData['id']) // what in the name of fuck..?
{
// Don't know why, don't know how .. doesn't matter, both parties are out.
- DB::Aowow()->query('UPDATE ?_account_sessions SET `touched` = ?d, `status` = ?d WHERE `userId` IN (?a) AND `status` = ?d', time(), SESSION_FORCED_LOGOUT, [$userData['id'], $session['userId']], SESSION_ACTIVE);
+ DB::Aowow()->qry('UPDATE ::account_sessions SET `touched` = %i, `status` = %i WHERE `userId` IN %in AND `status` = %i', time(), SESSION_FORCED_LOGOUT, [$userData['id'], $session['userId']], SESSION_ACTIVE);
trigger_error('User::init - tried to resume session "'.session_id().'" of user #'.$_SESSION['user'].' linked to session data for user #'.$session['userId'].' Kicked both!', E_USER_ERROR);
self::destroy();
return false;
}
- DB::Aowow()->query('UPDATE ?_account_sessions SET `touched` = ?d, `expires` = IF(`expires`, ?d, 0) WHERE `sessionId` = ?', time(), time() + Cfg::get('SESSION_TIMEOUT_DELAY'), session_id());
+ DB::Aowow()->qry('UPDATE ::account_sessions SET `touched` = %i, `expires` = IF(`expires`, %i, 0) WHERE `sessionId` = %s', time(), time() + Cfg::get('SESSION_TIMEOUT_DELAY'), session_id());
if ($loc = Locale::tryFrom($userData['locale']))
self::$preferedLoc = $loc;
@@ -100,7 +127,7 @@ class User
// reset expired account statuses
if ($userData['statusTimer'] && $userData['statusTimer'] < time() && $userData['status'] != ACC_STATUS_NEW)
{
- DB::Aowow()->query('UPDATE ?_account SET `status` = ?d, `statusTimer` = 0, `token` = "", `updateValue` = "" WHERE `id` = ?d', ACC_STATUS_NONE, User::$id);
+ 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;
}
@@ -122,91 +149,57 @@ class User
self::$email = $userData['email'];
self::$avatarborder = $userData['avatarborder'];
- if (Cfg::get('PROFILER_ENABLE'))
- {
- $conditions = [['OR', ['user', self::$id], ['ap.accountId', self::$id]]];
- if (!self::isInGroup(U_GROUP_ADMIN | U_GROUP_BUREAU))
- $conditions[] = [['cuFlags', PROFILER_CU_DELETED, '&'], 0];
- self::$profiles = (new LocalProfileList($conditions));
- }
+ # reset premium options #
- // reset premium options
if (!self::isPremium())
{
if ($userData['avatar'] == 2)
{
- DB::Aowow()->query('UPDATE ?_account SET `avatar` = 1 WHERE `id` = ?d', self::$id);
- DB::Aowow()->query('UPDATE ?_account_avatars SET `current` = 0 WHERE `userId` = ?d', self::$id);
+ 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
}
- // stuff, that updates on a daily basis goes here (if you keep you session alive indefinitly, the signin-handler doesn't do very much)
- // - consecutive visits
- // - votes per day
- // - reputation for daily visit
+
+ # update daily limits #
+
if (!self::isBanned())
{
- $lastLogin = DB::Aowow()->selectCell('SELECT `curLogin` FROM ?_account WHERE `id` = ?d', self::$id);
+ $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)
+ // - 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',
+ 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 rep for daily visit
+ // - gain reputation for daily visit
if (!(self::isBanned()) && !self::isInGroup(U_GROUP_PENDING))
Util::gainSiteReputation(self::$id, SITEREP_ACTION_DAILYVISIT);
- // increment consecutive visits (next day or first of new month and not more than 48h)
- // i bet my ass i forgot a corner case
+ // - increment consecutive visits (next day or first of new month and not more than 48h)
if ((date('j', $lastLogin) + 1 == date('j') || (date('j') == 1 && date('n', $lastLogin) != date('n'))) && (time() - $lastLogin) < 2 * DAY)
- DB::Aowow()->query('UPDATE ?_account SET `consecutiveVisits` = `consecutiveVisits` + 1 WHERE `id` = ?d', self::$id);
+ DB::Aowow()->qry('UPDATE ::account SET `consecutiveVisits` = `consecutiveVisits` + 1 WHERE `id` = %i', self::$id);
else
- DB::Aowow()->query('UPDATE ?_account SET `consecutiveVisits` = 0 WHERE `id` = ?d', self::$id);
+ DB::Aowow()->qry('UPDATE ::account SET `consecutiveVisits` = 0 WHERE `id` = %i', self::$id);
}
}
return true;
}
- private static function setIP() : void
- {
- $ipAddr = '';
- $method = ['HTTP_CLIENT_IP', 'HTTP_X_FORWARDED_FOR', 'HTTP_X_FORWARDED', 'HTTP_FORWARDED_FOR', 'HTTP_FORWARDED', 'REMOTE_ADDR'];
-
- foreach ($method as $m)
- {
- if ($rawIp = getenv($m))
- {
- if ($m == 'HTTP_X_FORWARDED')
- $rawIp = explode(',', $rawIp)[0]; // [ip, proxy1, proxy2]
-
- // check IPv4
- if ($ipAddr = filter_var($rawIp, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4))
- break;
-
- // check IPv6
- if ($ipAddr = filter_var($rawIp, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6))
- break;
- }
- }
-
- self::$ip = $ipAddr ?: null;
- }
-
public static function save(bool $toDB = false)
{
$_SESSION['user'] = self::$id;
@@ -214,7 +207,7 @@ class User
// $_SESSION['dataKey'] does not depend on user login status and is set in User::init()
if (self::isLoggedIn() && $toDB)
- DB::Aowow()->query('UPDATE ?_account SET `locale` = ? WHERE `id` = ?', self::$preferedLoc->value, self::$id);
+ DB::Aowow()->qry('UPDATE ::account SET `locale` = %s WHERE `id` = %s', self::$preferedLoc->value, self::$id);
}
public static function destroy()
@@ -236,7 +229,7 @@ class User
/* auth mechanisms */
/*******************/
- public static function authenticate(string $login, string $password) : int
+ public static function authenticate(string $login, #[\SensitiveParameter] string $password) : int
{
$userId = 0;
@@ -259,17 +252,17 @@ class User
return $result;
}
- private static function authSelf(string $nameOrEmail, string $password, int &$userId) : int
+ private static function authSelf(string $nameOrEmail, #[\SensitiveParameter] string $password, int &$userId) : int
{
if (!self::$ip)
return AUTH_INTERNAL_ERR;
// handle login try limitation
- $ipBan = DB::Aowow()->selectRow('SELECT `ip`, `count`, IF(`unbanDate` > UNIX_TIMESTAMP(), 1, 0) AS "active" FROM ?_account_bannedips WHERE `type` = ?d AND `ip` = ?', IP_BAN_TYPE_LOGIN_ATTEMPT, self::$ip);
+ $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()->query('REPLACE INTO ?_account_bannedips (`ip`, `type`, `count`, `unbanDate`) VALUES (?, ?d, 1, UNIX_TIMESTAMP() + ?d)', self::$ip, IP_BAN_TYPE_LOGIN_ATTEMPT, Cfg::get('ACC_FAILED_AUTH_BLOCK'));
+ 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()->query('UPDATE ?_account_bannedips SET `count` = `count` + 1, `unbanDate` = UNIX_TIMESTAMP() + ?d WHERE `ip` = ?', Cfg::get('ACC_FAILED_AUTH_BLOCK'), self::$ip);
+ 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;
@@ -278,12 +271,11 @@ class User
$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.`email` = ? } { a.`login` = ? } AND `status` <> ?d
+ 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`',
- $email ?: DBSIMPLE_SKIP,
- !$email ? $nameOrEmail : DBSIMPLE_SKIP,
+ $nameOrEmail,
ACC_STATUS_DELETED
);
@@ -294,7 +286,7 @@ class User
return AUTH_WRONGPASS;
// successfull auth; clear bans for this IP
- DB::Aowow()->query('DELETE FROM ?_account_bannedips WHERE `type` = ?d AND `ip` = ?', IP_BAN_TYPE_LOGIN_ATTEMPT, self::$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;
@@ -304,12 +296,12 @@ class User
return AUTH_OK;
}
- private static function authRealm(string $name, string $password, int &$userId) : int
+ private static function authRealm(string $name, #[\SensitiveParameter] string $password, int &$userId) : int
{
if (!DB::isConnectable(DB_AUTH))
return AUTH_INTERNAL_ERR;
- $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 = ? LIMIT 1', $name);
+ $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;
@@ -327,7 +319,7 @@ class User
return AUTH_OK;
}
- private static function authExtern(string $nameOrEmail, string $password, int &$userId) : int
+ private static function authExtern(string $nameOrEmail, #[\SensitiveParameter] string $password, int &$userId) : int
{
if (!file_exists('config/extAuth.php'))
{
@@ -365,14 +357,14 @@ class User
// 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` = ?d', $extId))
+ if ($_ = DB::Aowow()->selectCell('SELECT `id` FROM ::account WHERE `extId` = %i', $extId))
{
if ($userGroup >= U_GROUP_NONE)
- DB::Aowow()->query('UPDATE ?_account SET `userGroups` = ?d WHERE `extId` = ?d', $userGroup, $extId);
+ DB::Aowow()->qry('UPDATE ::account SET `userGroups` = %i WHERE `extId` = %i', $userGroup, $extId);
return $_;
}
- $newId = DB::Aowow()->query('INSERT IGNORE INTO ?_account (`extId`, `passHash`, `username`, `joinDate`, `prevIP`, `prevLogin`, `locale`, `status`, `userGroups`) VALUES (?d, "", ?, UNIX_TIMESTAMP(), ?, UNIX_TIMESTAMP(), ?d, ?d, ?d)',
+ $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"] ?? '',
@@ -387,24 +379,15 @@ class User
return $newId ?: 0;
}
- private static function createSalt() : string
+ // crypt used by us
+ public static function hashCrypt(#[\SensitiveParameter] string $pass) : string
{
- $algo = '$2a';
- $strength = '$09';
- $salt = '$'.Util::createHash(22);
-
- return $algo.$strength.$salt;
+ return password_hash($pass, PASSWORD_BCRYPT, ['cost' => 15]);
}
- // crypt used by aowow
- public static function hashCrypt(string $pass) : string
+ public static function verifyCrypt(#[\SensitiveParameter] string $pass, string $hash) : bool
{
- return crypt($pass, self::createSalt());
- }
-
- public static function verifyCrypt(string $pass, string $hash) : bool
- {
- return $hash === crypt($pass, $hash);
+ return password_verify($pass, $hash);
}
// SRP6 used by TC
@@ -526,7 +509,7 @@ class User
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 getCurrentDailyVotes() : int
@@ -578,6 +561,7 @@ class User
$gUser['downvoteRep'] = Cfg::get('REP_REQ_DOWNVOTE');
$gUser['upvoteRep'] = Cfg::get('REP_REQ_UPVOTE');
$gUser['characters'] = self::getCharacters();
+ $gUser['completion'] = self::getCompletion();
$gUser['excludegroups'] = self::$excludeGroups;
if (self::$debug)
@@ -616,11 +600,11 @@ class User
if (!self::isLoggedIn() || self::isBanned())
return $result;
- $res = DB::Aowow()->selectCol('SELECT `id` AS ARRAY_KEY, `name` FROM ?_account_weightscales WHERE `userId` = ?d', self::$id);
+ $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);
@@ -637,9 +621,8 @@ class User
if (!Cfg::get('PROFILER_ENABLE'))
return $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))
+ 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);
@@ -648,7 +631,7 @@ class User
public static function getCharacters() : array
{
- if (!self::$profiles)
+ if (!self::loadProfiles())
return [];
return self::$profiles->getJSGlobals(PROFILEINFO_CHARACTER);
@@ -656,7 +639,7 @@ class User
public static function getProfiles() : array
{
- if (!self::$profiles)
+ if (!self::loadProfiles())
return [];
return self::$profiles->getJSGlobals(PROFILEINFO_PROFILE);
@@ -664,7 +647,7 @@ class User
public static function getPinnedCharacter() : array
{
- if (!self::$profiles)
+ if (!self::loadProfiles())
return [];
$realms = Profiler::getRealms();
@@ -688,7 +671,7 @@ class User
if (!self::isLoggedIn() || self::isBanned(ACC_BAN_GUIDE))
return $result;
- if ($guides = DB::Aowow()->select('SELECT `id`, `title`, `url` FROM ?_guides WHERE `userId` = ?d AND `status` <> ?d', self::$id, GuideMgr::STATUS_ARCHIVED))
+ 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']));
@@ -703,7 +686,7 @@ class User
if (!self::isLoggedIn())
return [];
- return DB::Aowow()->selectCol('SELECT `name` AS ARRAY_KEY, `data` FROM ?_account_cookies WHERE `userId` = ?d', self::$id);
+ return DB::Aowow()->selectPairs('SELECT `name`, `data` FROM ::account_cookies WHERE `userId` = %i', self::$id);
}
public static function getFavorites() : array
@@ -711,14 +694,14 @@ class User
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` = ?d', self::$id);
+ $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', array_values($ids)]]);
+ $tc = Type::newList($type, [['id', $ids]]);
if (!$tc || $tc->error)
continue;
@@ -732,6 +715,81 @@ class User
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 61084146..39e9f297 100644
--- a/includes/utilities.php
+++ b/includes/utilities.php
@@ -6,6 +6,27 @@ if (!defined('AOWOW_REVISION'))
die('illegal access');
+
+// PHP 8.4 polyfill
+if (version_compare(PHP_VERSION, '8.4.0') < 0)
+{
+ function array_find(array $array, callable $callback) : mixed
+ {
+ foreach ($array as $k => $v)
+ if ($callback($v, $k))
+ return $array[$k];
+ return null;
+ }
+
+ function array_find_key(array $array, callable $callback) : mixed
+ {
+ foreach ($array as $k => $v)
+ if ($callback($v, $k))
+ return $k;
+ return null;
+ }
+}
+
class SimpleXML extends \SimpleXMLElement
{
public function addCData(string $cData) : \SimpleXMLElement
@@ -35,7 +56,7 @@ abstract class Util
private static $perfectGems = null;
- public static $regions = array(
+ public static $regions = array(
'us', 'eu', 'kr', 'tw', 'cn', 'dev'
);
@@ -48,18 +69,6 @@ abstract class Util
'clothChestArmor', 'leatherChestArmor', 'mailChestArmor', 'plateChestArmor'
);
- public static $weightScales = array(
- 'agi', 'int', 'sta', 'spi', 'str', 'health', 'mana', 'healthrgn', 'manargn',
- 'armor', 'blockrtng', 'block', 'defrtng', 'dodgertng', 'parryrtng', 'resirtng',
- 'atkpwr', 'feratkpwr', 'armorpenrtng', 'critstrkrtng', 'exprtng', 'hastertng', 'hitrtng', 'splpen',
- 'splpwr', 'arcsplpwr', 'firsplpwr', 'frosplpwr', 'holsplpwr', 'natsplpwr', 'shasplpwr',
- 'dmg', 'mledps', 'rgddps', 'mledmgmin', 'rgddmgmin', 'mledmgmax', 'rgddmgmax', 'mlespeed', 'rgdspeed',
- 'arcres', 'firres', 'frores', 'holres', 'natres', 'shares',
- 'mleatkpwr', 'mlecritstrkrtng', 'mlehastertng', 'mlehitrtng', 'rgdatkpwr', 'rgdcritstrkrtng', 'rgdhastertng', 'rgdhitrtng',
- 'splcritstrkrtng', 'splhastertng', 'splhitrtng', 'spldmg', 'splheal',
- 'nsockets'
- );
-
public static $dateFormatInternal = "Y/m/d H:i:s";
public static $changeLevelString = '%s';
@@ -104,152 +113,23 @@ abstract class Util
return [$notes, $severity];
}
- private static $execTime = 0.0;
-
- public static function execTime(bool $set = false) : string
- {
- if ($set)
- {
- self::$execTime = microTime(true);
- return '';
- }
-
- if (!self::$execTime)
- return '';
-
- $newTime = microTime(true);
- $tDiff = $newTime - self::$execTime;
- self::$execTime = $newTime;
-
- return self::formatTime($tDiff * 1000, true);
- }
-
public static function formatMoney(int $qty) : string
{
- $money = '';
+ if ($qty <= 0)
+ return '';
- if ($qty >= 10000)
- {
- $g = floor($qty / 10000);
- $money .= ''.$g.' ';
- $qty -= $g * 10000;
- }
+ $parts = [];
- if ($qty >= 100)
- {
- $s = floor($qty / 100);
- $money .= ''.$s.' ';
- $qty -= $s * 100;
- }
+ if ($g = intdiv($qty, 10000))
+ $parts[] = ''.$g.'';
- if ($qty > 0)
- $money .= ''.$qty.'';
+ if ($s = intdiv($qty % 10000, 100))
+ $parts[] = ''.$s.'';
- return $money;
- }
+ if ($c = ($qty % 100))
+ $parts[] = ''.$c.'';
- public static function parseTime(int $msec) : array
- {
- $time = [0, 0, 0, 0, 0];
-
- if ($_ = ($msec % 1000))
- $time[0] = $_;
-
- $sec = $msec / 1000;
-
- if ($sec >= 3600 * 24)
- {
- $time[4] = floor($sec / 3600 / 24);
- $sec -= $time[4] * 3600 * 24;
- }
-
- if ($sec >= 3600)
- {
- $time[3] = floor($sec / 3600);
- $sec -= $time[3] * 3600;
- }
-
- if ($sec >= 60)
- {
- $time[2] = floor($sec / 60);
- $sec -= $time[2] * 60;
- }
-
- if ($sec > 0)
- {
- $time[1] = (int)$sec;
- $sec -= $time[1];
- }
-
- return $time;
- }
-
- public static function formatTime(int $msec, bool $short = false) : string
- {
- [$ms, $s, $m, $h, $d] = self::parseTime(abs($msec));
- // \u00A0 is  , but also usable by js
-
- if ($short)
- {
- if ($_ = round($d / 365))
- return $_."\u{00A0}".Lang::timeUnits('ab', 0);
- if ($_ = round($d / 30))
- return $_."\u{00A0}".Lang::timeUnits('ab', 1);
- if ($_ = round($d / 7))
- return $_."\u{00A0}".Lang::timeUnits('ab', 2);
- if ($_ = round($d))
- return $_."\u{00A0}".Lang::timeUnits('ab', 3);
- if ($_ = round($h))
- return $_."\u{00A0}".Lang::timeUnits('ab', 4);
- if ($_ = round($m))
- return $_."\u{00A0}".Lang::timeUnits('ab', 5);
- if ($_ = round($s + $ms / 1000, 2))
- return $_."\u{00A0}".Lang::timeUnits('ab', 6);
- if ($ms)
- return $ms."\u{00A0}".Lang::timeUnits('ab', 7);
-
- return "0\u{00A0}".Lang::timeUnits('ab', 6);
- }
- else
- {
- $_ = $d + $h / 24;
- if ($_ > 1 && !($_ % 365)) // whole years
- return round(($d + $h / 24) / 365, 2)."\u{00A0}".Lang::timeUnits($d / 365 == 1 && !$h ? 'sg' : 'pl', 0);
- if ($_ > 1 && !($_ % 30)) // whole months
- return round(($d + $h / 24) / 30, 2)."\u{00A0}".Lang::timeUnits($d / 30 == 1 && !$h ? 'sg' : 'pl', 1);
- if ($_ > 1 && !($_ % 7)) // whole weeks
- return round(($d + $h / 24) / 7, 2)."\u{00A0}".Lang::timeUnits($d / 7 == 1 && !$h ? 'sg' : 'pl', 2);
- if ($d)
- return round($d + $h / 24, 2)."\u{00A0}".Lang::timeUnits($d == 1 && !$h ? 'sg' : 'pl', 3);
- if ($h)
- return round($h + $m / 60, 2)."\u{00A0}".Lang::timeUnits($h == 1 && !$m ? 'sg' : 'pl', 4);
- if ($m)
- return round($m + $s / 60, 2)."\u{00A0}".Lang::timeUnits($m == 1 && !$s ? 'sg' : 'pl', 5);
- if ($s)
- return round($s + $ms / 1000, 2)."\u{00A0}".Lang::timeUnits($s == 1 && !$ms ? 'sg' : 'pl', 6);
- if ($ms)
- return $ms."\u{00A0}".Lang::timeUnits($ms == 1 ? 'sg' : 'pl', 7);
-
- return "0\u{00A0}".Lang::timeUnits('pl', 6);
- }
- }
-
- public static function formatTimeDiff(int $sec) : string
- {
- $delta = abs(time() - $sec);
-
- [, $s, $m, $h, $d] = self::parseTime($delta * 1000);
-
- if ($delta > (1 * MONTH)) // use absolute
- return date(Lang::main('dateFmtLong'), $sec);
- else if ($delta > (2 * DAY)) // days ago
- return Lang::main('timeAgo', [$d . ' ' . Lang::timeUnits('pl', 3)]);
- else if ($h) // hours, minutes ago
- return Lang::main('timeAgo', [$h . ' ' . Lang::timeUnits('ab', 4) . ' ' . $m . ' ' . Lang::timeUnits('ab', 5)]);
- else if ($m) // minutes, seconds ago
- return Lang::main('timeAgo', [$m . ' ' . Lang::timeUnits('ab', 5) . ' ' . $s . ' ' . Lang::timeUnits('ab', 6)]);
- else // seconds ago
- return Lang::main('timeAgo', [$s . ' ' . Lang::timeUnits($s == 1 ? 'sg' : 'pl', 6)]);
+ return implode(' ', $parts);
}
// pageTexts, questTexts and mails
@@ -288,9 +168,9 @@ abstract class Util
$from = array(
'/\$g\s*([^:;]*)\s*:\s*([^:;]*)\s*(:?[^:;]*);/ui',// directed gender-reference $g::
- '/\$t([^;]+);/ui', // nonsense, that the client apparently ignores
+ '/\$t([^;]+);/ui', // HK rank. $t:; (maybe male/female if pvp unranked? Gets replaced with current HK rank.)
'/<([^\"=\/>]+\s[^\"=\/>]+)>/ui', // emotes (workaround: at least one whitespace and never " or = between brackets)
- '/\$(\d+)w/ui', // worldState(?)-ref found on some pageTexts $1234w
+ '/\$(\d+)w/ui', // worldState(%d)-ref found on some pageTexts $1234w
'/\$c/i', // class-ref
'/\$r/i', // race-ref
'/\$n/i', // name-ref
@@ -299,7 +179,7 @@ abstract class Util
$toMD = array(
'<\1/\2>',
- '',
+ '<'.implode('/', Lang::game('pvpRank', 1)).'>',
'<\1>',
'[span class=q0>WorldState #\1[/span]',
'<'.Lang::game('class').'>',
@@ -310,7 +190,7 @@ abstract class Util
$toHTML = array(
'<\1/\2>',
- '',
+ '<'.implode('/', Lang::game('pvpRank', 1)).'>',
'<\1>',
'WorldState #\1',
'<'.Lang::game('class').'>',
@@ -342,8 +222,11 @@ abstract class Util
return 'b'.$_;
}
- public static function htmlEscape($data)
+ public static function htmlEscape(string|array|null $data) : string|array
{
+ if (empty($data)) // null, '', [] and not "0"
+ return '';
+
if (is_array($data))
{
foreach ($data as &$v)
@@ -355,8 +238,11 @@ abstract class Util
return htmlspecialchars($data, ENT_QUOTES | ENT_DISALLOWED | ENT_HTML5, 'utf-8');
}
- public static function jsEscape($data)
+ public static function jsEscape(string|array|null $data) : string|array
{
+ if (empty($data)) // null, '', [] and not "0"
+ return '';
+
if (is_array($data))
{
foreach ($data as &$v)
@@ -424,7 +310,7 @@ abstract class Util
}
// for item and spells
- public static function setRatingLevel(int $level, int $statId, int $val) : string
+ public static function setRatingLevel(int $level, int $statId, int $val, bool $interactive = false) : string
{
if (in_array($statId, [Stat::DEFENSE_RTG, Stat::DODGE_RTG, Stat::PARRY_RTG, Stat::BLOCK_RTG, Stat::RESILIENCE_RTG]) && $level < 34)
$level = 34;
@@ -450,7 +336,9 @@ abstract class Util
if (!in_array($statId, [Stat::DEFENSE_RTG, Stat::EXPERTISE_RTG]))
$result .= '%';
- return Lang::item('ratingString', [$statId, $result, $level]);
+ $result = Lang::item('ratingString', [$statId, $result, $level]);
+
+ return $interactive ? sprintf(self::$setRatingLevelString, $level, $statId, $val, $result) : $result;
}
// default ucFirst doesn't convert UTF-8 chars (php 8.4 finally implemented this .. see ya in 2027)
@@ -472,6 +360,15 @@ abstract class Util
return mb_strtolower($str);
}
+ public static function strrev(string $str) : string
+ {
+ $out = '';
+ for ($i = 1, $len = mb_strlen($str); $i <= $len; $i++)
+ $out .= mb_substr($str, -$i, 1);
+
+ return $out;
+ }
+
// doesn't handle scientific notation .. why would you input 3e3 for 3000..?
public static function checkNumeric(mixed &$data, int $typeCast = NUM_ANY) : bool
{
@@ -554,6 +451,18 @@ abstract class Util
}
}
+ public static function createNumRange(int $min, int $max, string $delim = '', ?callable $fn = null) : string
+ {
+ if (!$min && !$max)
+ return '';
+
+ $fn ??= fn($x) => $x;
+ $_min = $fn($min);
+ $_max = $fn($max);
+
+ return $max > $min ? $_min . ($delim ?: Lang::game('valueDelim')) . $_max : $_min;
+ }
+
public static function validateLogin(?string $val) : string
{
if ($_ = self::validateEmail($val))
@@ -712,8 +621,8 @@ abstract class Util
if (empty($miscData['id']) || empty($miscData['voterId']))
return false;
- DB::Aowow()->query( // delete old votes the user has cast
- 'DELETE FROM ?_account_reputation WHERE sourceA = ?d AND sourceB = ?d AND userId = ?d AND action IN (?a)',
+ DB::Aowow()->qry( // delete old votes the user has cast
+ 'DELETE FROM ::account_reputation WHERE sourceA = %i AND sourceB = %i AND userId = %i AND action IN %in',
$miscData['id'],
$miscData['voterId'],
$user,
@@ -757,13 +666,13 @@ abstract class Util
break;
}
- $x = array_merge($x, array(
+ $x += array(
'userId' => $user,
'action' => $action,
- 'date' => !empty($miscData['date']) ? $miscData['date'] : time()
- ));
+ 'date' => $miscData['date'] ?? time()
+ );
- return DB::Aowow()->query('INSERT IGNORE INTO ?_account_reputation (?#) VALUES (?a)', array_keys($x), array_values($x));
+ return DB::Aowow()->qry('INSERT IGNORE INTO ::account_reputation %v', $x);
}
public static function toJSON($data, $forceFlags = 0)
@@ -782,43 +691,12 @@ abstract class Util
return $json;
}
- public static function createSqlBatchInsert(array $data) : array
- {
- if (!count($data) || !is_array(reset($data)))
- return [];
-
- $nRows = 100;
- $nItems = count(reset($data));
- $result = [];
- $buff = [];
-
- foreach ($data as $d)
- {
- if (count($d) != $nItems)
- return [];
-
- $d = array_map(fn($x) => $x === null ? 'NULL' : DB::Aowow()->escape($x), $d);
-
- $buff[] = implode(',', $d);
-
- if (count($buff) >= $nRows)
- {
- $result[] = '('.implode('),(', $buff).')';
- $buff = [];
- }
- }
-
- if ($buff)
- $result[] = '('.implode('),(', $buff).')';
-
- return $result;
- }
/*****************/
/* file handling */
/*****************/
- public static function writeFile($file, $content)
+ public static function writeFile(string $file, string $content) : bool
{
$success = false;
@@ -966,7 +844,7 @@ abstract class Util
{
// prepare score-lookup
if (empty(self::$perfectGems))
- self::$perfectGems = DB::World()->selectCol('SELECT perfectItemType FROM skill_perfect_item_template WHERE requiredSpecialization = ?d', 55534);
+ self::$perfectGems = DB::World()->selectCol('SELECT perfectItemType FROM skill_perfect_item_template WHERE requiredSpecialization = %i', 55534);
// epic - WotLK - increased stats / profession specific (Dragon's Eyes)
if ($profSpec)
@@ -1142,6 +1020,55 @@ abstract class Util
return $bits;
}
+ public static function indexBitBlob(string $bitBlob, int $blobSize = 32) : array
+ {
+ $indizes = [];
+ $blocks = explode(' ', trim($bitBlob));
+ for ($i = 0; $i < count($blocks); $i++)
+ for ($j = 0; $j < $blobSize; $j++)
+ if ($blocks[$i] & (1 << $j))
+ $indizes[] = $j + ($i * $blobSize);
+
+ return $indizes;
+ }
+
+ public static function toString(mixed $var) : string
+ {
+ if (is_array($var))
+ return '[' . implode(', ', array_map(self::toString(...), $var)) . ']';
+
+ if (is_object($var))
+ {
+ // hm, respect object stringability?
+ // if ($var instanceof Stringable)
+ // return (string)$var;
+
+ $buff = [];
+ foreach ($var as $k => $v)
+ $buff[] = $k.':'.self::toString($v);
+
+ return '{' . implode(', ', $buff) . '}';
+ }
+
+ return (string)$var;
+ }
+
+ public static function nodeAttributes(?array $attribs) : string
+ {
+ if (!$attribs)
+ return '';
+
+ return array_reduce(array_keys($attribs), fn($carry, $name) => $carry . match(gettype($attribs[$name]))
+ {
+ 'boolean' => ' ' . $attribs[$name] ? $name : '',
+ 'integer',
+ 'double' => ' ' . $name . '="' . $attribs[$name] . '"',
+ 'string' => ' ' . $name . '="' . self::htmlEscape($attribs[$name]) . '"',
+ 'array' => ' ' . $name . '="' . implode(' ', self::htmlEscape($attribs[$name])) . '"',
+ default => ''
+ }, '');
+ }
+
public static function buildPosFixMenu(int $mapId, float $posX, float $posY, int $type, int $guid, int $parentArea = 0, int $parentFloor = 0) : array
{
$points = WorldPosition::toZonePos($mapId, $posX, $posY);
@@ -1215,7 +1142,7 @@ abstract class Util
if ($expiration)
{
$vars += array_fill(0, 9, null); // vsprintf requires all unused indizes to also be set...
- $vars[9] = Util::formatTime($expiration * 1000);
+ $vars[9] = DateTime::formatTimeElapsed($expiration * 1000, 0);
}
if ($vars)
diff --git a/index.php b/index.php
index 672173b2..6be334f8 100644
--- a/index.php
+++ b/index.php
@@ -13,9 +13,23 @@ $pageParam = '';
parse_str(parse_url($_SERVER['REQUEST_URI'], PHP_URL_QUERY), $query);
foreach ($query as $page => $param)
{
- $page = preg_replace('/[^\w\-]/i', '', $page);
+ // could be an array
+ if (!is_string($param))
+ {
+ $pageCall = ''; // just .. fail
+ break;
+ }
- $pageCall = Util::lower($page);
+ // fix page calls - pages like search use the page call directly and expect it as lower case
+ if (preg_match('/[A-Z]/', $page))
+ {
+ $url = explode('=', $_SERVER['REQUEST_URI'], 2);
+ $page = Util::lower(array_shift($url)).($url ? '=' . $url[0] : '');
+ header('Location: '.$page, true, 302);
+ exit;
+ }
+
+ $pageCall = preg_replace('/[^\w\-]/i', '', $page);
$pageParam = $param ?? '';
break; // only use first k/v-pair to determine page
}
@@ -50,7 +64,7 @@ try {
$responder = new \StdClass;
// 1. try specialized response
- if (file_exists('endpoints/'.$pageCall.'/'.$file.'.php'))
+ if (file_exists('endpoints/'.$pageCall.'/'.$file.'.php') && $pageCall != $file)
{
require_once 'endpoints/'.$pageCall.'/'.$file.'.php';
diff --git a/localization/datetime.class.php b/localization/datetime.class.php
new file mode 100644
index 00000000..98d60c6e
--- /dev/null
+++ b/localization/datetime.class.php
@@ -0,0 +1,222 @@
+getTimestamp() - $timestamp);
+
+ $today = new DateTime();
+ $eventDay = new DateTime(time() - $elapsed);
+
+ $todayMidnight = $today->setTime(0, 0);
+ $eventDayMidnight = $eventDay->setTime(0, 0);
+
+ $delta = $todayMidnight->diff($eventDayMidnight, true)->days;
+
+ if ($elapsed >= 2592000) /* More than a month ago */
+ $txt = Lang::main('date_on') . $eventDay->formatDateSimple($withTime);
+ else if ($delta > 1)
+ $txt = Lang::main('ddaysago', [$delta]);
+ else if ($elapsed >= 43200)
+ {
+ if ($today->format('j') == $eventDay->format('j'))
+ $txt = Lang::main('today');
+ else
+ $txt = Lang::main('yesterday');
+
+ $txt = $eventDay->formatTimeSimple($txt);
+ }
+ else /* Less than 12 hours ago */
+ $txt = Lang::main('date_ago', [self::formatTimeElapsed($elapsed * 1000)]);
+
+ return $txt;
+ }
+
+ /**
+ * Human-readable format of a date. Optionally append time of day.
+ *
+ * @param bool $withTime [optional] affixes day time
+ * @return string a formatted date string.
+ */
+ public function formatDateSimple(bool $withTime = false) : string
+ {
+ $txt = '';
+ $day = $this->format('d');
+ $month = $this->format('m');
+ $year = $this->format('Y');
+
+ if ($year <= 1970)
+ $txt .= Lang::main('unknowndate_stc');
+ else
+ $txt .= Lang::main('date_simple', [$day, $month, $year]);
+
+ if ($withTime)
+ $txt = $this->formatTimeSimple($txt);
+
+ return $txt;
+ }
+
+ /**
+ * Human-readable format of the time of day.
+ *
+ * @param string $txt [optional] text to affeix the day time to
+ * @param bool $noPrefix [optional] don't use " at " to affix time of day to $txt
+ * @return string a formatted time of day string.
+ */
+ public function formatTimeSimple(string $txt = '', bool $noPrefix = false) : string
+ {
+ $hours = $this->format('G');
+ $minutes = $this->format('i');
+
+ $txt .= ($noPrefix ? ' ' : Lang::main('date_at'));
+
+ if ($hours == 12)
+ $txt .= Lang::main('noon');
+ else if ($hours == 0)
+ $txt .= Lang::main('midnight');
+ else if ($hours > 12)
+ $txt .= ($hours - 12) . ':' . $minutes . ' ' . Lang::main('pm');
+ else
+ $txt .= $hours . ':' . $minutes . ' ' . Lang::main('am');
+
+ return $txt;
+ }
+
+ /**
+ * Calculate component values from timestamp
+ *
+ * @param int $msec time in milliseconds to parse
+ * @return int[] [msec, sec, min, hr, day]
+ */
+ public static function parse(int $msec) : array
+ {
+ $time = [0, 0, 0, 0, 0];
+ $msec = abs($msec);
+
+ for ($i = 3; $i < count(self::RANGE); ++$i)
+ {
+ if ($msec < self::RANGE[$i])
+ continue;
+
+ $time[7 - $i] = intVal($msec / self::RANGE[$i]);
+ $msec %= self::RANGE[$i];
+ }
+
+ return $time;
+ }
+
+ /**
+ * Human-readable longform format of a timespan.
+ *
+ * @param int $delay time in milliseconds to format
+ * @return string a formatted time string. If an error occured "n/a" (localized) is returned
+ */
+ public static function formatTimeElapsedFloat(int $delay) : string
+ {
+ $delay = abs($delay);
+ $nbsp = Lang::getLocale()->isLogographic() ? '' : self::NBSP;
+
+ for ($i = 0; $i < count(self::RANGE); ++$i)
+ {
+ if ($delay < self::RANGE[$i])
+ continue;
+
+ $v = round($delay / self::RANGE[$i], 2);
+ return $v . $nbsp . Lang::timeUnits($v === 1.0 ? 'sg' : 'pl', $i);
+ }
+
+ return '0' . $nbsp . Lang::timeUnits('pl', 6); // 0 seconds
+ }
+
+ /**
+ * Human-readable format of a timespan.
+ *
+ * @param int $delay time in milliseconds to format
+ * @param int $maxRange [optional] time unit index - 0 (year) ... 7 (milliseconds)
+ * @return string a formatted time string. If an error occured "n/a" (localized) is returned
+ */
+ public static function formatTimeElapsed(int $delay, int $maxRange = 3) : string
+ {
+ if ($maxRange > 7 || $maxRange < 0)
+ $maxRange = 3; // default: days
+
+ $subunit = [1, 3, 3, -1, 5, -1, 7, -1];
+ $delay = max($delay, 1);
+
+ for ($i = $maxRange; $i < count(self::RANGE); ++$i)
+ {
+ if ($delay >= self::RANGE[$i])
+ {
+ $i1 = $i;
+ $v1 = floor($delay / self::RANGE[$i1]);
+
+ if ($subunit[$i1] != -1)
+ {
+ $i2 = $subunit[$i1];
+ $delay %= self::RANGE[$i1];
+ $v2 = floor($delay / self::RANGE[$i2]);
+ $nbsp = Lang::getLocale()->isLogographic() ? '' : self::NBSP;
+
+ if ($v2 > 0)
+ return self::OMG($v1, $i1, true) . $nbsp . self::OMG($v2, $i2, true);
+ }
+
+ return self::OMG($v1, $i1, false);
+ }
+ }
+
+ return Lang::main('n_a');
+ }
+
+ /**
+ * internal number formatter
+ *
+ * @param int $value unit value
+ * @param int $unit time unit index 0 (year) ... 7 (milliseconds)
+ * @param bool $abbrv use abbreviation
+ * @return string value + unit
+ */
+ private static function OMG(int $value, int $unit, bool $abbrv) : string
+ {
+ if ($abbrv && !Lang::timeUnits('ab', $unit))
+ $abbrv = false;
+
+ $nbsp = Lang::getLocale()->isLogographic() ? '' : self::NBSP;
+
+ return $value .= $nbsp . match(true)
+ {
+ $abbrv => Lang::timeUnits('ab', $unit),
+ $value == 1 => Lang::timeUnits('sg', $unit),
+ default => Lang::timeUnits('pl', $unit)
+ };
+ }
+}
+
+?>
diff --git a/localization/lang.class.php b/localization/lang.class.php
index 8e7cfa79..7579c1de 100644
--- a/localization/lang.class.php
+++ b/localization/lang.class.php
@@ -239,10 +239,10 @@ class Lang
$tmp = [];
if ($cuFlags & CUSTOM_DISABLED)
- $tmp[] = '[tooltip name=disabledHint]'.Util::jsEscape(self::main('disabledHint')).'[/tooltip][span class=tip tooltip=disabledHint]'.Util::jsEscape(self::main('disabled')).'[/span]';
+ $tmp[] = '[tooltip name=disabledHint]'.self::main('disabledHint').'[/tooltip][span class=tip tooltip=disabledHint]'.self::main('disabled').'[/span]';
if ($cuFlags & CUSTOM_SERVERSIDE)
- $tmp[] = '[tooltip name=serversideHint]'.Util::jsEscape(self::main('serversideHint')).'[/tooltip][span class=tip tooltip=serversideHint]'.Util::jsEscape(self::main('serverside')).'[/span]';
+ $tmp[] = '[tooltip name=serversideHint]'.self::main('serversideHint').'[/tooltip][span class=tip tooltip=serversideHint]'.self::main('serverside').'[/span]';
if ($cuFlags & CUSTOM_UNAVAILABLE)
$tmp[] = self::main('unavailable');
@@ -257,7 +257,7 @@ class Lang
{
$locks = [];
$ids = [];
- $lock = DB::Aowow()->selectRow('SELECT * FROM ?_lock WHERE `id` = ?d', $lockId);
+ $lock = DB::Aowow()->selectRow('SELECT * FROM ::lock WHERE `id` = %i', $lockId);
if (!$lock)
return $locks;
@@ -267,75 +267,87 @@ class Lang
$rank = $lock['reqSkill'.$i];
$name = '';
- if ($lock['type'.$i] == LOCK_TYPE_ITEM)
+ switch ($lock['type'.$i])
{
- $name = ItemList::getName($prop);
- if (!$name)
- continue;
-
- if ($fmt == self::FMT_HTML)
- $name = $interactive ? ''.$name.'' : ''.$name.'';
- else if ($interactive && $fmt == self::FMT_MARKUP)
- {
- $name = '[item='.$prop.']';
- $ids[Type::ITEM][] = $prop;
- }
- else
- $name = $prop;
-
- }
- else if ($lock['type'.$i] == LOCK_TYPE_SKILL)
- {
- $name = self::spell('lockType', $prop);
- if (!$name)
- continue;
-
- // skills
- if (in_array($prop, [1, 2, 3, 20]))
- {
- $skills = array(
- 1 => SKILL_LOCKPICKING,
- 2 => SKILL_HERBALISM,
- 3 => SKILL_MINING,
- 20 => SKILL_INSCRIPTION
- );
+ case LOCK_TYPE_ITEM:
+ $name = ItemList::getName($prop);
+ if (!$name)
+ continue 2;
if ($fmt == self::FMT_HTML)
- $name = $interactive ? ''.$name.'' : ''.$name.'';
+ $name = $interactive ? ''.$name.'' : ''.$name.'';
else if ($interactive && $fmt == self::FMT_MARKUP)
{
- $name = '[skill='.$skills[$prop].']';
- $ids[Type::SKILL][] = $skills[$prop];
+ $name = '[item='.$prop.']';
+ $ids[Type::ITEM][] = $prop;
+ }
+
+ break;
+ case LOCK_TYPE_SKILL:
+ $name = self::spell('lockType', $prop);
+ if (!$name)
+ continue 2;
+
+ // skills
+ if (in_array($prop, [1, 2, 3, 20]))
+ {
+ $skills = array(
+ 1 => SKILL_LOCKPICKING,
+ 2 => SKILL_HERBALISM,
+ 3 => SKILL_MINING,
+ 20 => SKILL_INSCRIPTION
+ );
+
+ if ($fmt == self::FMT_HTML)
+ $name = $interactive ? ''.$name.'' : ''.$name.'';
+ else if ($interactive && $fmt == self::FMT_MARKUP)
+ {
+ $name = '[skill='.$skills[$prop].']';
+ $ids[Type::SKILL][] = $skills[$prop];
+ }
+ else
+ $name = SkillList::getName($prop);
+
+ if ($rank > 0)
+ $name .= ' ('.$rank.')';
+ }
+ // Lockpicking
+ else if ($prop == 4)
+ {
+ if ($fmt == self::FMT_HTML)
+ $name = $interactive ? ''.$name.'' : ''.$name.'';
+ else if ($interactive && $fmt == self::FMT_MARKUP)
+ {
+ $name = '[spell=1842]';
+ $ids[Type::SPELL][] = 1842;
+ }
+ }
+ // exclude unusual stuff
+ else if (User::isInGroup(U_GROUP_STAFF))
+ {
+ if ($rank > 0)
+ $name .= ' ('.$rank.')';
}
else
- $name = $skills[$prop];
+ continue 2;
+ break;
+ case LOCK_TYPE_SPELL:
+ $name = SpellList::getName($prop);
+ if (!$name)
+ continue 2;
- if ($rank > 0)
- $name .= ' ('.$rank.')';
- }
- // Lockpicking
- else if ($prop == 4)
- {
if ($fmt == self::FMT_HTML)
- $name = $interactive ? ''.$name.'' : ''.$name.'';
+ $name = $interactive ? ''.$name.'' : ''.$name.'';
else if ($interactive && $fmt == self::FMT_MARKUP)
{
- $name = '[spell=1842]';
- $ids[Type::SPELL][] = 1842;
+ $name = '[spell='.$prop.']';
+ $ids[Type::SPELL][] = $prop;
}
- // else $name = $name
- }
- // exclude unusual stuff
- else if (User::isInGroup(U_GROUP_STAFF))
- {
- if ($rank > 0)
- $name .= ' ('.$rank.')';
- }
- else
- continue;
+
+ break;
+ default:
+ continue 2;
}
- else
- continue;
$locks[$lock['type'.$i] == LOCK_TYPE_ITEM ? $prop : -$prop] = $name;
}
@@ -435,7 +447,7 @@ class Lang
return implode(', ', $tmp);
}
- public static function getClassString(int $classMask, ?array &$ids = [], int $fmt = self::FMT_HTML) : string
+ public static function getClassString(int $classMask, array &$ids = [], int $fmt = self::FMT_HTML) : string
{
$classMask &= ChrClass::MASK_ALL; // clamp to available classes..
@@ -459,7 +471,7 @@ class Lang
return implode(', ', $tmp);
}
- public static function getRaceString(int $raceMask, ?array &$ids = [], int $fmt = self::FMT_HTML) : string
+ public static function getRaceString(int $raceMask, array &$ids = [], int $fmt = self::FMT_HTML) : string
{
$raceMask &= ChrRace::MASK_ALL; // clamp to available races..
@@ -523,7 +535,7 @@ class Lang
if ($msec < 0)
$msec = 0;
- $time = Util::parseTime($msec); // [$ms, $s, $m, $h, $d]
+ $time = DateTime::parse($msec); // [$ms, $s, $m, $h, $d]
$mult = [0, 1000, 60, 60, 24];
$total = 0;
$ref = [];
@@ -540,33 +552,22 @@ class Lang
if (!$msec)
return self::vspf($ref[0], [0]);
- if ($concat)
- {
- for ($i = 4; $i > 0; $i--)
- {
- $total += $time[$i];
- if (isset($ref[$i]) && ($total || ($i == 1 && !$result)))
- {
- $result[] = self::vspf($ref[$i], [$total]);
- $total = 0;
- }
- else
- $total *= $mult[$i];
- }
-
- return implode(', ', $result);
- }
-
for ($i = 4; $i > 0; $i--)
{
$total += $time[$i];
- if (isset($ref[$i]) && ($total || $i == 1))
- return self::vspf($ref[$i], [$total + ($time[$i-1] ?? 0) / $mult[$i]]);
+ if (isset($ref[$i]) && ($total || ($i == 1 && !$result)))
+ {
+ if (!$concat)
+ return self::vspf($ref[$i], [$total + ($time[$i-1] ?? 0) / $mult[$i]]);
+
+ $result[] = self::vspf($ref[$i], [$total]);
+ $total = 0;
+ }
else
$total *= $mult[$i];
}
- return '';
+ return implode(', ', $result);
}
private static function vspf(null|array|string $var, array $args = []) : null|array|string
@@ -708,7 +709,7 @@ class Lang
$spfVars[0] = $linkType;
break;
case 'talent':
- if ($spell = DB::Aowow()->selectCell('SELECT `spell` FROM ?_talents WHERE `id` = ?d AND `rank` = ?d', $linkVars[0], $linkVars[1]))
+ if ($spell = DB::Aowow()->selectCell('SELECT `spell` FROM ::talents WHERE `id` = %i AND `rank` = %i', $linkVars[0], $linkVars[1]))
{
$spfVars[0] = 'spell';
$spfVars[1] = $spell;
@@ -746,28 +747,28 @@ class Lang
}, $var);
// |2 - frFR preposition: de |2
- $var = preg_replace_callback('/\|2\s?(\w)/i', function ($m)
+ $var = preg_replace_callback('/\|2\s?(.)/i', function ($m)
{
- [$_, $word] = $m;
+ [$_, $char] = $m;
- switch (strtolower($word[1]))
+ switch (strtolower($char))
{
case 'h':
if (self::$locale != Locale::FR)
- return 'de ' . $word;
+ return 'de ' . $char;
case 'a':
case 'e':
case 'i':
case 'o':
case 'u':
- return "d'" . $word;
+ return "d'" . $char;
default:
- return 'de ' . $word;
+ return 'de ' . $char;
}
}, $var);
// |3 - ruRU declinations |3-()
- $var = preg_replace_callback('/\|3-(\d)\(([^\)]+)\)/iu', function ($m)
+ $var = preg_replace_callback('/\|3-(\d+)\(([^\)]+)\)/iu', function ($m)
{
[$_, $caseIdx, $word] = $m;
@@ -777,7 +778,7 @@ class Lang
if (preg_match('/\P{Cyrillic}/iu', $word)) // not in cyrillic script
return $word;
- if ($declWord = DB::Aowow()->selectCell('SELECT dwc.word FROM ?_declinedwordcases dwc JOIN ?_declinedword dc ON dwc.wordId = dc.id WHERE dwc.caseIdx = ?d AND dc.word = ?', $caseIdx, $word))
+ if ($declWord = DB::Aowow()->selectCell('SELECT dwc.`word` FROM ::declinedwordcases dwc JOIN ::declinedword dc ON dwc.`wordId` = dc.`id` WHERE dwc.`caseIdx` = %i AND dc.`word` = %s', $caseIdx, $word))
return $declWord;
return $word;
diff --git a/localization/locale_dede.php b/localization/locale_dede.php
index af068ae1..1ca688b4 100644
--- a/localization/locale_dede.php
+++ b/localization/locale_dede.php
@@ -134,8 +134,25 @@ $lang = array(
'dateFmtShort' => "d.m.Y",
'dateFmtLong' => "d.m.Y \u\m H:i",
'dateFmtIntl' => "d. MMMM y",
- 'timeAgo' => 'vor %s',
'nfSeparators' => ['.', ','],
+ 'n_a' => "n. v.",
+
+ // date time
+ 'date' => "Datum",
+ 'date_colon' => "Datum: ",
+ 'date_on' => "am ",
+ 'date_ago' => "vor %s",
+ 'date_at' => " um ",
+ 'date_to' => " bis ",
+ 'date_simple' => '%1$d.%2$d.%3$d',
+ 'unknowndate' => "Unbekanntes Datum",
+ 'ddaysago' => "vor %d Tagen",
+ 'today' => "heute",
+ 'yesterday' => "gestern",
+ 'noon' => "Mittag",
+ 'midnight' => "Mitternacht",
+ 'am' => "vormittags",
+ 'pm' => "nachmittags",
// error
'intError' => "Ein interner Fehler ist aufgetreten.",
@@ -236,6 +253,8 @@ $lang = array(
'atCaptain' => "Teamkapitän",
'atSize' => "Größe: ",
'profiler' => "Charakter-Profiler",
+ 'completion' => "Vervollständigung: ",
+ 'attainedBy' => "Erlangt von %d%% der Profile",
'notFound' => array(
'guild' => "Diese Gilde existiert nicht oder wurde noch nicht in die Datenbank übernommen.",
'arenateam' => "Dieses Arena Team existiert nicht oder wurde noch nicht in die Datenbank übernommen.",
@@ -364,6 +383,7 @@ $lang = array(
'school' => "Magieart",
'type' => "Art: ",
'valueDelim' => " - ", // " bis "
+ 'target' => "",
'pvp' => "PvP",
'honorPoints' => "Ehrenpunkte",
@@ -378,7 +398,11 @@ $lang = array(
'phases' => "Phasen",
'mode' => "Modus: ",
- 'modes' => [-1 => "Beliebig", "Normal / Normal 10", "Heroisch / Normal 25", "Heroisch 10", "Heroisch 25"],
+ 'modes' => array(
+ [-1 => "Beliebig", "Normal / Normal 10", "Heroisch / Normal 25", "Heroisch 10", "Heroisch 25"],
+ ["Normal", "Heroisch"],
+ ["Normal 10", "Normal 25", "Heroisch 10", "Heroisch 25"]
+ ),
'expansions' => ["Classic", "The Burning Crusade", "Wrath of the Lich King"],
'stats' => ["Stärke", "Beweglichkeit", "Ausdauer", "Intelligenz", "Willenskraft"],
'timeAbbrev' => array(
@@ -478,7 +502,7 @@ $lang = array(
UNIT_FLAG_IMMUNE_TO_NPC => 'Immun gegen Kreaturen',
UNIT_FLAG_LOOTING => 'Lootanimation',
UNIT_FLAG_PET_IN_COMBAT => 'Pet im Kampf',
- UNIT_FLAG_PVP => 'PvP',
+ UNIT_FLAG_PVP_ENABLING => 'PvP',
UNIT_FLAG_SILENCED => 'Zum Schweigen gebracht',
UNIT_FLAG_CANNOT_SWIM => 'Kann nicht schwimmen',
UNIT_FLAG_UNK_15 => 'UNK-15 (kann nur schwimmen)',
@@ -551,11 +575,11 @@ $lang = array(
UNIT_VIS_FLAGS_UNK5 => 'UNK-5'
),
/*idx:3*/ array(
- UNIT_BYTE1_ANIM_TIER_GROUND => 'Bodenanimationen',
- UNIT_BYTE1_ANIM_TIER_SWIM => 'Schwimmanimationen',
- UNIT_BYTE1_ANIM_TIER_HOVER => 'Schwebeanimationen',
- UNIT_BYTE1_ANIM_TIER_FLY => 'Fluganimationen',
- UNIT_BYTE1_ANIM_TIER_SUMBERGED => 'abgetauchte Animationen'
+ UNIT_ANIM_TIER_GROUND => 'Bodenanimationen',
+ UNIT_ANIM_TIER_SWIM => 'Schwimmanimationen',
+ UNIT_ANIM_TIER_HOVER => 'Schwebeanimationen',
+ UNIT_ANIM_TIER_FLY => 'Fluganimationen',
+ UNIT_ANIM_TIER_SUMBERGED => 'abgetauchte Animationen'
),
'bytesIdx' => ['StandState', null, 'VisFlags', 'AnimTier'],
'valueUNK' => '[span class=q10]unbenutzter Wert [b class=q1]%d[/b] übergeben an UnitFieldBytes1 auf Offset [b class=q1]%d[/b][/span]',
@@ -641,18 +665,22 @@ $lang = array(
SmartEvent::EVENT_ACTION_DONE => ['Aktion #[b]%1$d[/b] angefordert von anderem Skript', ''],
SmartEvent::EVENT_ON_SPELLCLICK => ['Ein \'SpellClick\' wurde ausgelöst', ''],
SmartEvent::EVENT_FRIENDLY_HEALTH_PCT => ['Gesundheit von #target# ist %11$s%%', 'Wiederhole alle %s'],
- SmartEvent::EVENT_DISTANCE_CREATURE => ['[npc=%11$d](%1$d)? [small class=q0](GUID\u003A %1$d)[/small]:; ist in einem %3$dm Umkreis', 'Wiederhole alle %s'],
- SmartEvent::EVENT_DISTANCE_GAMEOBJECT => ['[object=%11$d](%1$d)? [small class=q0](GUID\u003A %1$d)[/small]:; ist in einem %3$dm Umkreis', 'Wiederhole alle %s'],
+ SmartEvent::EVENT_DISTANCE_CREATURE => ['[npc=%11$d](%1$d)? [small class=q0](GUID %1$d)[/small]:; ist in einem %3$dm Umkreis', 'Wiederhole alle %s'],
+ SmartEvent::EVENT_DISTANCE_GAMEOBJECT => ['[object=%11$d](%1$d)? [small class=q0](GUID %1$d)[/small]:; ist in einem %3$dm Umkreis', 'Wiederhole alle %s'],
SmartEvent::EVENT_COUNTER_SET => ['Zähler #[b]%1$d[/b] ist gleich [b]%2$d[/b]', 'Abklingzeit: %s'],
SmartEvent::EVENT_SCENE_START => null,
SmartEvent::EVENT_SCENE_TRIGGER => null,
/* 80*/ SmartEvent::EVENT_SCENE_CANCEL => null,
SmartEvent::EVENT_SCENE_COMPLETE => null,
SmartEvent::EVENT_SUMMONED_UNIT_DIES => ['Durch mich beschworener (%1$d)?[npc=%1$d]:NPC; stirbt', 'Abklingzeit: %s'],
- SmartEvent::EVENT_ON_SPELL_CAST => ['Bei \'cast success\' von [spell=%1$d] ', 'Abklingzeit: %s'],
- SmartEvent::EVENT_ON_SPELL_FAILED => ['Bei \'cast failed\' von [spell=%1$d] ', 'Abklingzeit: %s'],
- SmartEvent::EVENT_ON_SPELL_START => ['Bei \'cast start\' von [spell=%1$d] ', 'Abklingzeit: %s'],
+ SmartEvent::EVENT_ON_SPELL_CAST => ['Bei \'cast success\' von [spell=%1$d]', 'Abklingzeit: %s'],
+ SmartEvent::EVENT_ON_SPELL_FAILED => ['Bei \'cast failed\' von [spell=%1$d]', 'Abklingzeit: %s'],
+ SmartEvent::EVENT_ON_SPELL_START => ['Bei \'cast start\' von [spell=%1$d]', 'Abklingzeit: %s'],
SmartEvent::EVENT_ON_DESPAWN => ['Beim Verschwinden', ''],
+ SmartEvent::EVENT_SEND_EVENT_TRIGGER => null,
+ SmartEvent::EVENT_AREATRIGGER_EXIT => null,
+ SmartEvent::EVENT_ON_AURA_APPLIED => ['Wenn Aura [spell=%1$d] angewendet wird', 'Abklingzeit: %s'],
+ SmartEvent::EVENT_ON_AURA_REMOVED => ['Wenn Aura [spell=%1$d] endet', 'Abklingzeit: %s']
),
'eventFlags' => array(
SmartEvent::FLAG_NO_REPEAT => 'Nicht wiederholbar',
@@ -696,7 +724,7 @@ $lang = array(
SmartAction::ACTION_CALL_GROUPEVENTHAPPENS => ['Erfülle Entdeckungsereignis von [quest=%1$d] für Gruppe von #target#.', ''],
SmartAction::ACTION_COMBAT_STOP => ['Beende aktuellen Kampf.', ''],
SmartAction::ACTION_REMOVEAURASFROMSPELL => ['Entferne(%2$d)? %2$d Aufladungen von:;(%1$d)? alle Auren:Aura [spell=%1$d]; von #target#.', 'Nur eigene Auren'],
- SmartAction::ACTION_FOLLOW => ['Folge #target#(%1$d)? mit %1$dm Abstand:;(%3$d)? bis zum Erreichen von [npc=%3$d]:;.(%12$d)? Am Ende wird ein Entdeckungsereignis für [quest=%4$d] erfüllt.:;(%13$d)? Am Ende wird ein Tod von [npc=%4$d] gutgeschrieben.:;', '(%11$d)?Folgt im Winkel von\u003A %11$.2f°:;'],
+ SmartAction::ACTION_FOLLOW => ['Folge #target#(%1$d)? mit %1$dm Abstand:;(%3$d)? bis zum Erreichen von [npc=%3$d]:;.(%12$d)? Am Ende wird ein Entdeckungsereignis für [quest=%4$d] erfüllt.:;(%13$d)? Am Ende wird ein Tod von [npc=%4$d] gutgeschrieben.:;', '(%11$d)?Folgt im Winkel von %11$.2f°:;'],
/* 30*/ SmartAction::ACTION_RANDOM_PHASE => ['Wähle zufällige Ereignisphase aus %11$s.', ''],
SmartAction::ACTION_RANDOM_PHASE_RANGE => ['Wähle zufällige Ereignisphase zwischen %1$d und %2$d.', ''],
SmartAction::ACTION_RESET_GOBJECT => ['Setze #target# zurück.', ''],
@@ -798,8 +826,8 @@ $lang = array(
SmartAction::ACTION_PLAY_ANIMKIT => null,
SmartAction::ACTION_SCENE_PLAY => null,
/*130*/ SmartAction::ACTION_SCENE_CANCEL => null,
- SmartAction::ACTION_SPAWN_SPAWNGROUP => ['Spawne SpawnGroup [b]%11$s[/b](%12$s)? SpawnFlags\u003A %12$s:; %13$s', 'Abklingzeit: %s'],
- SmartAction::ACTION_DESPAWN_SPAWNGROUP => ['Despawne SpawnGroup [b]%11$s[/b](%12$s)? SpawnFlags\u003A %12$s:; %13$s', 'Abklingzeit: %s'],
+ SmartAction::ACTION_SPAWN_SPAWNGROUP => ['Spawne SpawnGroup [b]%11$s[/b](%12$s)? SpawnFlags %12$s:; %13$s', 'Abklingzeit: %s'],
+ SmartAction::ACTION_DESPAWN_SPAWNGROUP => ['Despawne SpawnGroup [b]%11$s[/b](%12$s)? SpawnFlags %12$s:; %13$s', 'Abklingzeit: %s'],
SmartAction::ACTION_RESPAWN_BY_SPAWNID => ['Respawne %11$s [small class=q0](GUID: %2$d)[/small]', ''],
SmartAction::ACTION_INVOKER_CAST => ['Auslöser wirkt [spell=%1$d] auf #target#.(%4$d)? (max. %4$d |4Ziel:Ziele;):;', '%1$s'],
SmartAction::ACTION_PLAY_CINEMATIC => ['Gebe Film #[b]%1$d[/b] für #target# wieder.', ''],
@@ -1070,6 +1098,7 @@ $lang = array(
'posts' => "Forenbeiträge: "
),
'emote' => array(
+ 'id' => "Emote-ID: ",
'notFound' => "Dieses Emote existiert nicht.",
// 'self' => "An Euch selbst",
// 'target' => "An Andere mit Ziel",
@@ -1102,9 +1131,10 @@ $lang = array(
'state' => ['Einmalig', 'Stetiger Zustand', 'Stetiges Emote']
),
'enchantment' => array(
+ 'id' => "Verzauberungs-ID: ",
+ 'notFound' => "Diese Verzauberung existiert nicht.",
'details' => "Details",
'activation' => "Aktivierung",
- 'notFound' => "Diese Verzauberung existiert nicht.",
'types' => array(
1 => "Zauber (Auslösung)", 3 => "Zauber (Anlegen)", 7 => "Zauber (Benutzen)", 8 => "Prismatischer Sockel",
5 => "Statistik", 2 => "Waffenschaden", 6 => "DPS", 4 => "Verteidigung"
@@ -1113,10 +1143,13 @@ $lang = array(
'areatrigger' => array(
'notFound' => "Dieser Areatrigger existiert nicht.",
'foundIn' => "Dieser Areatrigger befindet sich in",
+ 'unnamed' => "Unbenannter Areatrigger #%d",
'types' => ['Unbenutzt', 'Gasthaus', 'Teleporter', 'Questziel', 'Smarter Trigger', 'Script']
),
'gameObject' => array(
+ 'id' => "Objekt-ID: ",
'notFound' => "Dieses Objekt existiert nicht.",
+ 'unnamed' => "Unbenanntes Objekt #%d",
'cat' => [0 => "Anderes", 3 => "Behälter", 6 => "Fallen", 9 => "Bücher", 25 => "Fischschwärme", -5 => "Truhen", -3 => "Kräuter", -4 => "Erzadern", -2 => "Quest", -6 => "Werkzeuge"],
'type' => [ 3 => "Behälter", 6 => "", 9 => "Buch", 25 => "", -5 => "Truhe", -3 => "Kraut", -4 => "Erzvorkommen", -2 => "Quest", -6 => ""],
'unkPosition' => "Der Standort dieses Objekts ist nicht bekannt.",
@@ -1130,14 +1163,15 @@ $lang = array(
'foundIn' => "Dieses Objekt befindet sich in",
'restock' => "Wird alle %s wieder aufgefüllt.",
'goFlags' => array(
- GO_FLAG_IN_USE => 'In Benutzung',
- GO_FLAG_LOCKED => 'Verschlossen',
- GO_FLAG_INTERACT_COND => 'Nicht interagierbar',
- GO_FLAG_TRANSPORT => 'Transporter',
- GO_FLAG_NOT_SELECTABLE => 'Nicht anwählbar',
- GO_FLAG_TRIGGERED => 'Ausgelöst',
- GO_FLAG_DAMAGED => 'Belagerung beschädigt',
- GO_FLAG_DESTROYED => 'Belagerung zerstört'
+ GO_FLAG_IN_USE => 'In Benutzung',
+ GO_FLAG_LOCKED => 'Verschlossen',
+ GO_FLAG_INTERACT_COND => 'Nicht interagierbar',
+ GO_FLAG_TRANSPORT => 'Transporter',
+ GO_FLAG_NOT_SELECTABLE => 'Nicht anwählbar',
+ GO_FLAG_AI_OBSTACLE => 'Ausgelöst',
+ GO_FLAG_FREEZE_ANIMATION => 'Pausiert Animation',
+ GO_FLAG_DAMAGED => 'Belagerung beschädigt',
+ GO_FLAG_DESTROYED => 'Belagerung zerstört'
),
'actions' => array(
"None", "Animate Custom 0", "Animate Custom 1", "Animate Custom 2", "Animate Custom 3",
@@ -1148,6 +1182,7 @@ $lang = array(
)
),
'npc' => array(
+ 'id' => "NPC-ID: ",
'notFound' => "Dieser NPC existiert nicht.",
'classification'=> "Einstufung: %s",
'petFamily' => "Tierart: ",
@@ -1180,10 +1215,6 @@ $lang = array(
'mechanicimmune'=> 'Nicht anfällig für Mechanik: %s',
'_extraFlags' => 'Extra Flags: ',
'versions' => 'Schwierigkeitsgrade: ',
- 'modes' => array(
- 1 => ["Normal", "Heroisch"],
- 2 => ["10-Spieler Normal", "25-Spieler Normal", "10-Spieler Heroisch", "25-Spieler Heroisch"]
- ),
'cat' => array(
"Nicht kategorisiert", "Wildtiere", "Drachkin", "Dämonen", "Elementare", "Riesen", "Untote", "Humanoide",
"Tiere", "Mechanisch", "Nicht spezifiziert", "Totems", "Haustiere", "Gaswolken"
@@ -1243,6 +1274,7 @@ $lang = array(
)
),
'event' => array(
+ 'id' => "Weltereignis-ID: ",
'notFound' => "Dieses Weltereignis existiert nicht.",
'start' => "Anfang: ",
'end' => "Ende: ",
@@ -1251,6 +1283,7 @@ $lang = array(
'category' => ["Nicht kategorisiert", "Feiertage", "Wiederkehrend", "Spieler vs. Spieler"]
),
'achievement' => array(
+ 'id' => "Erfolgs-ID: ",
'notFound' => "Dieser Erfolg existiert nicht.",
'criteria' => "Kriterien",
'points' => "Punkte",
@@ -1309,9 +1342,11 @@ $lang = array(
)
),
'chrClass' => array(
+ 'id' => "Klassen-ID: ",
'notFound' => "Diese Klasse existiert nicht."
),
'race' => array(
+ 'id' => "Volks-ID: ",
'notFound' => "Dieses Volk existiert nicht.",
'racialLeader' => "Volksanführer: ",
'startZone' => "Startgebiet",
@@ -1349,6 +1384,7 @@ $lang = array(
)
),
'zone' => array(
+ 'id' => "Gebiets-ID: ",
'notFound' => "Dieses Gebiet existiert nicht.",
'attunement' => ["Einstimmung: ", "Heroische Einstimmung: "],
'key' => ["Schlüssel: ", "Heroischer Schlüssel: "],
@@ -1377,6 +1413,7 @@ $lang = array(
)
),
'quest' => array(
+ 'id' => "Quest-ID: ",
'notFound' => "Diese Quest existiert nicht.",
'_transfer' => 'Dieses Quest wird mit %s vertauscht, wenn Ihr zur %s wechselt.',
'questLevel' => "Stufe %s",
@@ -1514,6 +1551,7 @@ $lang = array(
'notFound' => "Dieses Icon existiert nicht."
),
'title' => array(
+ 'id' => "Titel-ID: ",
'notFound' => "Dieser Titel existiert nicht.",
'_transfer' => 'Dieser Titel wird mit %s vertauscht, wenn Ihr zur %s wechselt.',
'cat' => array(
@@ -1521,6 +1559,7 @@ $lang = array(
)
),
'skill' => array(
+ 'id' => "Fertigkeits-ID: ",
'notFound' => "Diese Fertigkeit existiert nicht.",
'cat' => array(
-6 => "Haustiere", -5 => "Reittiere", -4 => "Völkerfertigkeiten", 5 => "Attribute", 6 => "Waffenfertigkeiten", 7 => "Klassenfertigkeiten", 8 => "Rüstungssachverstand",
@@ -1528,6 +1567,7 @@ $lang = array(
)
),
'currency' => array(
+ 'id' => "Währungs-ID: ",
'notFound' => "Diese Währung existiert nicht.",
'cap' => "Obergrenze: ",
'cat' => array(
@@ -1549,6 +1589,7 @@ $lang = array(
)
),
'mail' => array(
+ 'id' => "Brief-ID: ",
'notFound' => "Dieser Brief existiert nicht.",
'attachment' => "Anlage",
'mailDelivery' => 'Ihr werdet diesen Brief%s%s erhalten',
@@ -1559,12 +1600,14 @@ $lang = array(
'untitled' => "Unbetitelter Brief #%d"
),
'pet' => array(
+ 'id' => "Tierart-ID: ",
'notFound' => "Diese Tierart existiert nicht.",
'exotic' => "Exotisch",
'cat' => ["Wildheit", "Hartnäckigkeit", "Gerissenheit"],
'food' => ["Fleisch", "Fisch", "Käse", "Brot", "Fungus", "Obst", "Rohes Fleisch", "Roher Fisch"]
),
'faction' => array(
+ 'id' => "Fraktions-ID: ",
'notFound' => "Diese Fraktion existiert nicht.",
'spillover' => "Reputationsüberlauf",
'spilloverDesc' => "Für diese Fraktion erhaltener Ruf wird zusätzlich mit den unten aufgeführten Fraktionen anteilig verrechnet.",
@@ -1580,12 +1623,13 @@ $lang = array(
)
),
'itemset' => array(
+ 'id' => "Ausrüstungsset-ID: ",
'notFound' => "Dieses Ausrüstungsset existiert nicht.",
'_desc' => "%s ist das %s. Es enthält %s Teile.",
'_descTagless' => "%s ist ein Ausrüstungsset, das %s Teile enthält.",
'_setBonuses' => "Setboni",
'_conveyBonus' => "Das Tragen mehrerer Gegenstände aus diesem Set gewährt Eurem Charakter Boni.",
- '_pieces' => "Teile",
+ '_pieces' => "%d Teile: ",
'_unavailable' => "Dieses Ausrüstungsset ist nicht für Spieler verfügbar.",
'_tag' => "Tag: ",
'summary' => "Zusammenfassung",
@@ -1605,13 +1649,14 @@ $lang = array(
)
),
'spell' => array(
+ 'id' => "Zauber-ID: ",
'notFound' => "Dieser Zauber existiert nicht.",
'_spellDetails' => "Zauberdetails",
'_cost' => "Kosten",
'_range' => "Reichweite",
'_castTime' => "Zauberzeit",
'_cooldown' => "Abklingzeit",
- '_distUnit' => "Meter",
+ '_distUnit' => " Meter",
'_forms' => "Gestalten",
'_aura' => "Aura",
'_effect' => "Effekt",
@@ -1631,14 +1676,13 @@ $lang = array(
'_rankRange' => "Rang: %d - %d",
'_showXmore' => "Zeige %d weitere",
- 'n_a' => "n. v.",
'normal' => "Normal",
'special' => "Besonders",
'currentArea' => '<Momentanes Gebiet>',
'discovered' => "Durch Geistesblitz erlernt",
- 'ppm' => "(%s Auslösungen pro Minute)",
- 'procChance' => "Procchance: ",
+ 'ppm' => "(%.1f Auslösungen pro Minute)",
+ 'procChance' => "Procchance: %.4g%%",
'starter' => "Basiszauber",
'trainingCost' => "Trainingskosten: ",
'channeled' => "Kanalisiert",
@@ -1654,7 +1698,8 @@ $lang = array(
'pointsPerCP' => ", plus %s pro Combopunkt",
'stackGroup' => "Stack Gruppierung",
'linkedWith' => "Verknüpft mit",
- '_scaling' => "Skalierung",
+ 'apMod' => " (AP mod: %.3g)",
+ 'spMod' => " (ZM mod: %.3g)",
'instantPhys' => "Sofort",
'castTime' => array(
"Spontanzauber",
@@ -1701,10 +1746,6 @@ $lang = array(
-1 => "Munition", -41 => "Pyrit", -61 => "Dampfdruck", -101 => "Hitze", -121 => "Schlamm", -141 => "Blutmacht",
-142 => "Wrath"
),
- 'scaling' => array(
- 'directSP' => "+%.2f%% der Zaubermacht zum direkten Effekt", 'directAP' => "+%.2f%% der Angriffskraft zum direkten Effekt",
- 'dotSP' => "+%.2f%% der Zaubermacht pro Tick", 'dotAP' => "+%.2f%% der Angriffskraft pro Tick"
- ),
'relItems' => array(
'base' => "%s im Zusammenhang mit %s anzeigen",
'link' => " oder ",
@@ -1818,14 +1859,14 @@ $lang = array(
/*102+ */ 'Dismiss Pet', 'Give Reputation', 'Summon Object (Trap)', 'Summon Object (Battle S.)','Summon Object (#3)', 'Summon Object (#4)',
/*108+ */ 'Dispel Mechanic', 'Summon Dead Pet', 'Destroy All Totems', 'Durability Damage - Flat', 'Summon Demon', 'Resurrect with Flat Health',
/*114+ */ 'Taunt', 'Durability Damage - %', 'Skin Player Corpse (PvP)', 'AoE Resurrect with % Health','Learn Skill', 'Apply Area Aura - Pet',
-/*120+ */ 'Teleport to Graveyard', 'Normalized Weapon Damage', null, 'Take Flight Path', 'Pull Towards', 'Modify Threat - %',
+/*120+ */ 'Teleport to Graveyard', 'Normalized Weapon Damage', '', 'Take Flight Path', 'Pull Towards', 'Modify Threat - %',
/*126+ */ 'Spell Steal ', 'Prospect', 'Apply Area Aura - Friend', 'Apply Area Aura - Enemy', 'Redirect Done Threat %', 'Play Sound',
/*132+ */ 'Play Music', 'Unlearn Specialization', 'Kill Credit 2', 'Call Pet', 'Heal for % of Total Health','Give % of Total Power',
/*138+ */ 'Leap Back', 'Abandon Quest', 'Force Cast', 'Force Spell Cast with Value','Trigger Spell with Value','Apply Area Aura - Pet Owner',
-/*144+ */ 'Knockback to Dest.', 'Pull Towards Dest.', 'Activate Rune', 'Fail Quest', null, 'Charge to Dest',
+/*144+ */ 'Knockback to Dest.', 'Pull Towards Dest.', 'Activate Rune', 'Fail Quest', 'Trigger Missile with Value','Charge to Dest',
/*150+ */ 'Start Quest', 'Trigger Spell 2', 'Summon - Refer-A-Friend', 'Create Tamed Pet', 'Discover Flight Path', 'Dual Wield 2H Weapons',
/*156+ */ 'Add Socket to Item', 'Create Tradeskill Item', 'Milling', 'Rename Pet', 'Force Cast 2', 'Change Talent Spec. Count',
-/*162-167*/ 'Activate Talent Spec.', null, 'Remove Aura', null, null, 'Update Player Phase'
+/*162-167*/ 'Activate Talent Spec.', '', 'Remove Aura'
),
'unkAura' => 'Unknown Aura (%1$d)',
'auras' => array(
@@ -1838,7 +1879,7 @@ $lang = array(
'Mod Skill - Temporary', 'Increase Run Speed %', 'Mod Mounted Speed %', 'Decrease Run Speed %', 'Mod Maximum Health - Flat',
'Mod Maximum Power - Flat', 'Shapeshift', 'Spell Effect Immunity', 'Spell Aura Immunity', 'Spell School Immunity',
'Damage Immunity', 'Dispel Type Immunity', 'Proc Trigger Spell', 'Proc Trigger Damage', 'Track Creatures',
- 'Track Resources', 'Ignore All Gear', 'Mod Parry %', null, 'Mod Dodge %',
+ 'Track Resources', 'Ignore All Gear', 'Mod Parry %', 'Periodic Trigger Spell from Client', 'Mod Dodge %',
/*50+ */ 'Mod Critical Healing Amount %', 'Mod Block %', 'Mod Physical Crit Chance', 'Periodically Drain Health', 'Mod Physical Hit Chance',
'Mod Spell Hit Chance', 'Transform', 'Mod Spell Crit Chance', 'Increase Swim Speed %', 'Mod Damage Done Versus Creature',
'Pacify & Silence', 'Mod Size %', 'Periodically Transfer Health', 'Periodic Transfer Power', 'Periodic Drain Power',
@@ -1861,19 +1902,19 @@ $lang = array(
'Increase Pet Talent Points', 'Allow Exotic Pets Taming', 'Mechanic Immunity Mask', 'Retain Combo Points', 'Reduce Pushback Time %',
/*150+ */ 'Mod Shield Block Value - %', 'Track Stealthed', 'Mod Player Aggro Range', 'Split Damage - Flat', 'Mod Stealth Level',
'Mod Underwater Breathing %', 'Mod All Reputation Gained by %', 'Done Pet Damage Multiplier', 'Mod Shield Block Value - Flat', 'No PvP Credit',
- 'Mod AoE Avoidance', 'Mod Health Regen During Combat', 'Mana Burn', 'Mod Melee Critical Damage %', null,
+ 'Mod AoE Avoidance', 'Mod Health Regen During Combat', 'Mana Burn', 'Mod Melee Critical Damage %', '',
'Mod Attacker Melee Attack Power', 'Mod Melee Attack Power - %', 'Mod Ranged Attack Power - %', 'Mod Damage Done vs Creature', 'Mod Crit Chance vs Creature',
- 'Change Object Visibility for Player', 'Mod Run Speed (not stacking)', 'Mod Mounted Speed (not stacking)', null, 'Mod Spell Power by % of Stat',
+ 'Change Object Visibility for Player', 'Mod Run Speed (not stacking)', 'Mod Mounted Speed (not stacking)', '', 'Mod Spell Power by % of Stat',
/*175+ */ 'Mod Healing Power by % of Stat', 'Spirit of Redemption', 'AoE Charm', 'Mod Debuff Resistance - %', 'Mod Attacker Spell Crit Chance',
- 'Mod Spell Power vs Creature', null, 'Mod Resistance by % of Stat', 'Mod Threat % of Critical Hits', 'Mod Attacker Melee Hit Chance',
+ 'Mod Spell Power vs Creature', '', 'Mod Resistance by % of Stat', 'Mod Threat % of Critical Hits', 'Mod Attacker Melee Hit Chance',
'Mod Attacker Ranged Hit Chance', 'Mod Attacker Spell Hit Chance', 'Mod Attacker Melee Crit Chance', 'Mod Attacker Ranged Crit Chance', 'Mod Rating',
'Mod Reputation Gained %', 'Limit Movement Speed', 'Mod Attack Speed %', 'Mod Haste % (gain)', 'Mod Target School Absorb %',
- 'Mod Target School Absorb for Ability', 'Mod Cooldowns', 'Mod Attacker Crit Chance', null, 'Mod Spell Hit Chance',
+ 'Mod Target School Absorb for Ability', 'Mod Cooldowns', 'Mod Attacker Crit Chance', '', 'Mod Spell Hit Chance',
/*200+ */ 'Mod Kill Experience Gained %', 'Can Fly', 'Ignore Combat Result', 'Mod Attacker Melee Crit Damage %', 'Mod Attacker Ranged Crit Damage %',
'Mod Attacker Spell Crit Damage %', 'Mod Vehicle Flight Speed %', 'Mod Mounted Flight Speed %', 'Mod Flight Speed %', 'Mod Mounted Flight Speed % (always)',
'Mod Vehicle Speed % (always)', 'Mod Flight Speed % (not stacking)', 'Mod Ranged Attack Power by % of Stat', 'Mod Rage Generated from Damage Dealt', 'Tamed Pet Passive',
'Arena Preparation', 'Mod Spell Haste %', 'Killing Spree', 'Mod Ranged Haste %', 'Mod Mana Regeneration by % of Stat',
- 'Mod Combat Rating by % of Stat', 'Ignore Threat', null, 'Raid Proc from Charge', null,
+ 'Mod Combat Rating by % of Stat', 'Ignore Threat', '', 'Raid Proc from Charge', '',
/*225+ */ 'Raid Proc from Charge with Value', 'Periodic Dummy', 'Periodically Trigger Spell with Value','Detect Stealth', 'Mod AoE Damage Taken %',
'Mod Maximum Health - Flat (no stacking)','Proc Trigger Spell with Value', 'Mod Mechanic Duration %', 'Change other Humanoid Display', 'Mod Mechanic Duration % (not stacking)',
'Mod Dispel Resistance %', 'Control Vehicle', 'Mod Spell Power by % of Attack Power', 'Mod Healing Power by % of Attack Power','Mod Size % (not stacking)',
@@ -1881,17 +1922,17 @@ $lang = array(
'Mod Aura Duration by Dispel Type', 'Mod Aura Duration by Dispel Type (not stacking)', 'Clone Caster', 'Mod Combat Result Chance', 'Convert Rune',
/*250+ */ 'Mod Maximum Health - Flat (stacking)', 'Mod Enemy Dodge Chance', 'Mod Haste % (loss)', 'Mod Critical Block Chance', 'Disarm Offhand',
'Mod Mechanic Damage Taken %', 'No Reagent Cost', 'Mod Target Resistance by Spell Class', 'Mod Spell Visual', 'Mod Periodic Healing Taken %',
- 'Screen Effect', 'Phase', 'Ability Ignore Aurastate', 'Allow Only Ability', null,
- null, null, 'Cancel Aura Buffer at % of Caster Health','Mod Attack Power by % of Stat', 'Ignore Target Resistance',
+ 'Screen Effect', 'Phase', 'Ability Ignore Aurastate', 'Allow Only Ability', '',
+ '', '', 'Cancel Aura Buffer at % of Caster Health','Mod Attack Power by % of Stat', 'Ignore Target Resistance',
'Ignore Target Resistance for Ability', 'Mod Damage Taken % from Caster', 'Ignore Swing Timer Reset', 'X-Ray', 'Ability Consume No Ammo',
/*275+ */ 'Mod Ability Ignore Shapeshift', 'Mod Mechanic Damage Done %', 'Mod Max Affected Targets', 'Disarm Ranged Weapon', 'Spawn Effect',
'Mod Armor Penetration %', 'Mod Honor Gain %', 'Mod Base Health %', 'Mod Healing Taken % from Caster', 'Linked Aura',
- 'Mod Attack Power by School Resistance','Allow Periodic Ability to Crit', 'Mod Spell Deflect Chance', 'Ignore Hit Direction', null,
+ 'Mod Attack Power by School Resistance','Allow Periodic Ability to Crit', 'Mod Spell Deflect Chance', 'Ignore Hit Direction', '',
'Mod Crit Chance', 'Mod Quest Experience Gained %', 'Open Stable', 'Override Spells', 'Prevent Power Regeneration',
- null, 'Set Vehicle Id', 'Spirit Burst', 'Strangulate', null,
-/*300+ */ 'Share Damage %', 'Mod Absorb School Healing', null, 'Mod Damage Done vs Aurastate - %', 'Fake Inebriate',
- 'Mod Minimum Speed %', null, 'Heal Absorb Test', 'Mod Critical Strike Chance for Caster',null,
- 'Mod Pet AoE Damage Avoidance', null, null, null, 'Prevent Ressurection',
+ '', 'Set Vehicle Id', 'Spirit Burst', 'Strangulate', '',
+/*300+ */ 'Share Damage %', 'Mod Absorb School Healing', '', 'Mod Damage Done vs Aurastate - %', 'Fake Inebriate',
+ 'Mod Minimum Speed %', '', 'Heal Absorb Test', 'Mod Critical Strike Chance for Caster','',
+ 'Mod Pet AoE Damage Avoidance', '', '', '', 'Prevent Ressurection',
/* -316*/ 'Underwater Walking', 'Periodic Haste'
),
'attributes0' => array(
@@ -2168,6 +2209,7 @@ $lang = array(
)
),
'item' => array(
+ 'id' => "Gegenstands-ID: ",
'notFound' => "Dieser Gegenstand existiert nicht .",
'armor' => "%s Rüstung",
'block' => "%s Blocken",
@@ -2226,8 +2268,10 @@ $lang = array(
'uniqueEquipped'=> ["Einzigartig anlegbar", null, "Einzigartig angelegt: %s (%d)"],
'speed' => "Tempo",
'dps' => "(%.1f Schaden pro Sekunde)",
- 'vendorIn' => "Händlerstandpunkte",
+ 'vendorLoc' => "Händlerstandpunkte",
'purchasedIn' => "Dieser Gegenstand kann gekauft werden in",
+ 'fishingLoc' => "Angelplätze",
+ 'fishedIn' => "Dieser Gegenstand kann geangelt werden in",
'duration' => array( // ITEM_DURATION_*
'',
"Dauer: %d Sek.",
diff --git a/localization/locale_enus.php b/localization/locale_enus.php
index 0901c76b..6735239a 100644
--- a/localization/locale_enus.php
+++ b/localization/locale_enus.php
@@ -134,8 +134,25 @@ $lang = array(
'dateFmtShort' => "Y/m/d",
'dateFmtLong' => "Y/m/d \a\\t g:i A",
'dateFmtIntl' => "MMMM d, y",
- 'timeAgo' => "%s ago",
'nfSeparators' => [',', '.'],
+ 'n_a' => "n/a",
+
+ // date time
+ 'date' => "Date",
+ 'date_colon' => "Date: ",
+ 'date_on' => "on ",
+ 'date_ago' => "%s ago",
+ 'date_at' => " at ",
+ 'date_to' => " to ",
+ 'date_simple' => '%2$d/%1$d/%3$d',
+ 'unknowndate' => "Unknown date",
+ 'ddaysago' => "%d days ago",
+ 'today' => "today",
+ 'yesterday' => "yesterday",
+ 'noon' => "noon",
+ 'midnight' => "midnight",
+ 'am' => "AM",
+ 'pm' => "PM",
// error
'intError' => "An internal error has occurred.",
@@ -236,6 +253,8 @@ $lang = array(
'atCaptain' => "Arena Team Captain",
'atSize' => "Size: ",
'profiler' => "Character Profiler",
+ 'completion' => "Completion: ",
+ 'attainedBy' => "Attained by %d%% of profiles",
'notFound' => array(
'guild' => "This Guild doesn't exist or is not yet in the database.",
'arenateam' => "This Arena Team doesn't exist or is not yet in the database.",
@@ -364,6 +383,7 @@ $lang = array(
'school' => "School",
'type' => "Type: ",
'valueDelim' => " to ",
+ 'target' => "",
'pvp' => "PvP", // PVP
'honorPoints' => "Honor Points", // HONOR_POINTS
@@ -378,7 +398,11 @@ $lang = array(
'phases' => "Phases",
'mode' => "Mode: ",
- 'modes' => [-1 => "Any", "Normal / Normal 10", "Heroic / Normal 25", "Heroic 10", "Heroic 25"],
+ 'modes' => array(
+ [-1 => "Any", "Normal / Normal 10", "Heroic / Normal 25", "Heroic 10", "Heroic 25"],
+ ["Normal", "Heroic"],
+ ["Normal 10", "Normal 25", "Heroic 10", "Heroic 25"]
+ ),
'expansions' => ["Classic", "The Burning Crusade", "Wrath of the Lich King"],
'stats' => ["Strength", "Agility", "Stamina", "Intellect", "Spirit"],
'timeAbbrev' => array( //