Core/Cleanup

* try to give included files a logical structure
 * move objects from Util and Game to their own files
 * make non-essential files auto-loaded
This commit is contained in:
Sarjuuk
2025-04-01 19:46:19 +02:00
parent 3a6c86092b
commit db1d3ccace
32 changed files with 1440 additions and 1323 deletions

View File

@@ -390,9 +390,9 @@ class AjaxAdmin extends AjaxHandler
DB::Aowow()->query('REPLACE INTO ?_spawns_override VALUES (?d, ?d, ?d, ?d, ?d)', $type, $guid, $area, $floor, AOWOW_REVISION);
if ($wPos = Game::getWorldPosForGUID($type, $guid))
if ($wPos = WorldPosition::getForGUID($type, $guid))
{
if ($point = Game::worldPosToZonePos($wPos[$guid]['mapId'], $wPos[$guid]['posX'], $wPos[$guid]['posY'], $area, $floor))
if ($point = WorldPosition::toZonePos($wPos[$guid]['mapId'], $wPos[$guid]['posX'], $wPos[$guid]['posY'], $area, $floor))
{
$updGUIDs = [$guid];
$newPos = array(
@@ -417,7 +417,7 @@ class AjaxAdmin extends AjaxHandler
{
foreach ($swp as $w)
{
if ($point = Game::worldPosToZonePos($wPos[$guid]['mapId'], $w['posX'], $w['posY'], $area, $floor))
if ($point = WorldPosition::toZonePos($wPos[$guid]['mapId'], $w['posX'], $w['posY'], $area, $floor))
{
$p = array(
'posX' => $point[0]['posX'],

View File

@@ -480,7 +480,7 @@ class SmartAction
if ($this->smartAI->teleportTargetArea)
$this->param[10] = $this->smartAI->teleportTargetArea;
// try calc from SmartTarget data
else if ($pos = Game::worldPosToZonePos($this->param[0], $x, $y))
else if ($pos = WorldPosition::toZonePos($this->param[0], $x, $y))
{
$this->param[10] = $pos[0]['areaId'];
$this->param[11] = str_pad($pos[0]['posX'] * 10, 3, '0', STR_PAD_LEFT).str_pad($pos[0]['posY'] * 10, 3, '0', STR_PAD_LEFT);

View File

@@ -0,0 +1,39 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
class LocString
{
private \WeakMap $store;
public function __construct(array $data, string $key = 'name', ?callable $callback = null)
{
$this->store = new \WeakMap();
$callback ??= fn($x) => $x;
if (!array_filter($data, fn($v, $k) => $v && strstr($k, $key.'_loc'), ARRAY_FILTER_USE_BOTH))
trigger_error('LocString - is entrirely empty', E_USER_WARNING);
foreach (Locale::cases() as $l)
$this->store[$l] = (string)$callback($data[$key.'_loc'.$l->value] ?? '');
}
public function __toString() : string
{
if ($str = $this->store[Lang::getLocale()])
return $str;
foreach (Locale::cases() as $l) // desired loc not set, use any other
if ($str = $this->store[$l])
return Cfg::get('DEBUG') ? '['.$str.']' : $str;
return Cfg::get('DEBUG') ? '[LOCSTRING]' : '';
}
}
?>

View File

@@ -0,0 +1,274 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
class Report
{
public const MODE_GENERAL = 0;
public const MODE_COMMENT = 1;
public const MODE_FORUM_POST = 2;
public const MODE_SCREENSHOT = 3;
public const MODE_CHARACTER = 4;
public const MODE_VIDEO = 5;
public const MODE_GUIDE = 6;
public const GEN_FEEDBACK = 1;
public const GEN_BUG_REPORT = 2;
public const GEN_TYPO_TRANSLATION = 3;
public const GEN_OP_ADVERTISING = 4;
public const GEN_OP_PARTNERSHIP = 5;
public const GEN_PRESS_INQUIRY = 6;
public const GEN_MISCELLANEOUS = 7;
public const GEN_MISINFORMATION = 8;
public const CO_ADVERTISING = 15;
public const CO_INACCURATE = 16;
public const CO_OUT_OF_DATE = 17;
public const CO_SPAM = 18;
public const CO_INAPPROPRIATE = 19;
public const CO_MISCELLANEOUS = 20;
public const FO_ADVERTISING = 30;
public const FO_AVATAR = 31;
public const FO_INACCURATE = 32;
public const FO_OUT_OF_DATE = 33;
public const FO_SPAM = 34;
public const FO_STICKY_REQUEST = 35;
public const FO_INAPPROPRIATE = 36;
public const FO_MISCELLANEOUS = 37;
public const SS_INACCURATE = 45;
public const SS_OUT_OF_DATE = 46;
public const SS_INAPPROPRIATE = 47;
public const SS_MISCELLANEOUS = 48;
public const PR_INACCURATE_DATA = 60;
public const PR_MISCELLANEOUS = 61;
public const VI_INACCURATE = 45;
public const VI_OUT_OF_DATE = 46;
public const VI_INAPPROPRIATE = 47;
public const VI_MISCELLANEOUS = 48;
public const AR_INACCURATE = 45;
public const AR_OUT_OF_DATE = 46;
public const AR_MISCELLANEOUS = 48;
private array $context = array(
self::MODE_GENERAL => array(
self::GEN_FEEDBACK => true,
self::GEN_BUG_REPORT => true,
self::GEN_TYPO_TRANSLATION => true,
self::GEN_OP_ADVERTISING => true,
self::GEN_OP_PARTNERSHIP => true,
self::GEN_PRESS_INQUIRY => true,
self::GEN_MISCELLANEOUS => true,
self::GEN_MISINFORMATION => true
),
self::MODE_COMMENT => array(
self::CO_ADVERTISING => U_GROUP_MODERATOR,
self::CO_INACCURATE => true,
self::CO_OUT_OF_DATE => true,
self::CO_SPAM => U_GROUP_MODERATOR,
self::CO_INAPPROPRIATE => U_GROUP_MODERATOR,
self::CO_MISCELLANEOUS => U_GROUP_MODERATOR
),
self::MODE_FORUM_POST => array(
self::FO_ADVERTISING => U_GROUP_MODERATOR,
self::FO_AVATAR => true,
self::FO_INACCURATE => true,
self::FO_OUT_OF_DATE => U_GROUP_MODERATOR,
self::FO_SPAM => U_GROUP_MODERATOR,
self::FO_STICKY_REQUEST => U_GROUP_MODERATOR,
self::FO_INAPPROPRIATE => U_GROUP_MODERATOR
),
self::MODE_SCREENSHOT => array(
self::SS_INACCURATE => true,
self::SS_OUT_OF_DATE => true,
self::SS_INAPPROPRIATE => U_GROUP_MODERATOR,
self::SS_MISCELLANEOUS => U_GROUP_MODERATOR
),
self::MODE_CHARACTER => array(
self::PR_INACCURATE_DATA => true,
self::PR_MISCELLANEOUS => true
),
self::MODE_VIDEO => array(
self::VI_INACCURATE => true,
self::VI_OUT_OF_DATE => true,
self::VI_INAPPROPRIATE => U_GROUP_MODERATOR,
self::VI_MISCELLANEOUS => U_GROUP_MODERATOR
),
self::MODE_GUIDE => array(
self::AR_INACCURATE => true,
self::AR_OUT_OF_DATE => true,
self::AR_MISCELLANEOUS => true
)
);
private const ERR_NONE = 0; // aka: success
private const ERR_INVALID_CAPTCHA = 1; // captcha not in use
private const ERR_DESC_TOO_LONG = 2;
private const ERR_NO_DESC = 3;
private const ERR_ALREADY_REPORTED = 7;
private const ERR_MISCELLANEOUS = -1;
public const STATUS_OPEN = 0;
public const STATUS_ASSIGNED = 1;
public const STATUS_CLOSED_WONTFIX = 2;
public const STATUS_CLOSED_SOLVED = 3;
private int $errorCode = self::ERR_NONE;
public function __construct(private int $mode, private int $reason, private ?int $subject = 0)
{
if ($mode < 0 || $reason <= 0)
{
trigger_error('Report - malformed contact request received', E_USER_ERROR);
$this->errorCode = self::ERR_MISCELLANEOUS;
return;
}
if (!isset($this->context[$mode][$reason]))
{
trigger_error('Report - report has invalid context (mode:'.$mode.' / reason:'.$reason.')', E_USER_ERROR);
$this->errorCode = self::ERR_MISCELLANEOUS;
return;
}
if (!User::$id && !User::$ip)
{
trigger_error('Report - could not determine IP for anonymous user', E_USER_ERROR);
$this->errorCode = self::ERR_MISCELLANEOUS;
return;
}
$this->subject ??= 0; // 0 for utility, tools and misc pages?
}
private function checkTargetContext() : int
{
// check already reported
$field = User::$id ? 'userId' : 'ip';
if (DB::Aowow()->selectCell('SELECT 1 FROM ?_reports WHERE `mode` = ?d AND `reason`= ?d AND `subject` = ?d AND ?# = ?', $this->mode, $this->reason, $this->subject, $field, User::$id ?: User::$ip))
return self::ERR_ALREADY_REPORTED;
// check targeted post/postOwner staff status
$ctxCheck = $this->context[$this->mode][$this->reason];
if (is_int($ctxCheck))
{
$roles = User::$groups;
if ($this->mode == self::MODE_COMMENT)
$roles = DB::Aowow()->selectCell('SELECT `roles` FROM ?_comments WHERE `id` = ?d', $this->subject);
// else if if ($this->mode == self::MODE_FORUM_POST)
// $roles = DB::Aowow()->selectCell('SELECT `roles` FROM ?_forum_posts WHERE `id` = ?d', $this->subject);
return $roles & $ctxCheck ? self::ERR_NONE : self::ERR_MISCELLANEOUS;
}
else
return $ctxCheck ? self::ERR_NONE : self::ERR_MISCELLANEOUS;
// Forum not in use, else:
// check post owner
// User::$id == post.op && !post.sticky;
// check user custom avatar
// g_users[post.user].avatar == 2 && (post.roles & U_GROUP_MODERATOR) == 0
}
public function create(string $desc, ?string $userAgent = null, ?string $appName = null, ?string $pageUrl = null, ?string $relUrl = null, ?string $email = null) : bool
{
if ($this->errorCode)
return false;
if (!$desc)
{
$this->errorCode = self::ERR_NO_DESC;
return false;
}
if (mb_strlen($desc) > 500)
{
$this->errorCode = self::ERR_DESC_TOO_LONG;
return false;
}
if($err = $this->checkTargetContext())
{
$this->errorCode = $err;
return false;
}
$update = array(
'userId' => User::$id,
'createDate' => time(),
'mode' => $this->mode,
'reason' => $this->reason,
'subject' => $this->subject,
'ip' => User::$ip,
'description' => $desc,
'userAgent' => $userAgent ?: $_SERVER['HTTP_USER_AGENT'],
'appName' => $appName ?: (get_browser(null, true)['browser'] ?: '')
);
if ($pageUrl)
$update['url'] = $pageUrl;
if ($relUrl)
$update['relatedurl'] = $relUrl;
if ($email)
$update['email'] = $email;
return DB::Aowow()->query('INSERT INTO ?_reports (?#) VALUES (?a)', array_keys($update), array_values($update));
}
public function getSimilar(int ...$status) : array
{
if ($this->errorCode)
return [];
foreach ($status as &$s)
if ($s < self::STATUS_OPEN || $s > self::STATUS_CLOSED_SOLVED)
unset($s);
return DB::Aowow()->select('SELECT `id` AS ARRAY_KEY, r.* FROM ?_reports r WHERE {`status` IN (?a) AND }`mode` = ?d AND `reason` = ?d AND `subject` = ?d',
$status ?: DBSIMPLE_SKIP, $this->mode, $this->reason, $this->subject);
}
public function close(int $closeStatus, bool $inclAssigned = false) : bool
{
if ($closeStatus != self::STATUS_CLOSED_SOLVED && $closeStatus != self::STATUS_CLOSED_WONTFIX)
return false;
if (!User::isInGroup(U_GROUP_ADMIN | U_GROUP_BUREAU | U_GROUP_MOD))
return false;
$fromStatus = [self::STATUS_OPEN];
if ($inclAssigned)
$fromStatus[] = self::STATUS_ASSIGNED;
if ($reports = DB::Aowow()->selectCol('SELECT `id` AS ARRAY_KEY, `userId` FROM ?_reports WHERE `status` IN (?a) AND `mode` = ?d AND `reason` = ?d AND `subject` = ?d',
$fromStatus, $this->mode, $this->reason, $this->subject))
{
DB::Aowow()->query('UPDATE ?_reports SET `status` = ?d, `assigned` = 0 WHERE `id` IN (?a)', $closeStatus, array_keys($reports));
foreach ($reports as $rId => $uId)
Util::gainSiteReputation($uId, $closeStatus == self::STATUS_CLOSED_SOLVED ? SITEREP_ACTION_GOOD_REPORT : SITEREP_ACTION_BAD_REPORT, ['id' => $rId]);
return true;
}
return false;
}
public function reopen(int $assignedTo = 0) : bool
{
// assignedTo = 0 ? status = STATUS_OPEN : status = STATUS_ASSIGNED, userId = assignedTo
return false;
}
public function getError() : int
{
return $this->errorCode;
}
}
?>

View File

@@ -84,7 +84,7 @@ class DB
// make number sensible again
$data['code'] = abs($data['code']);
if (Cfg::get('DEBUG') >= CLI::LOG_INFO)
if (Cfg::get('DEBUG') >= LOG_LEVEL_INFO)
{
echo "\nDB ERROR\n";
foreach ($data as $k => $v)

View File

@@ -19,6 +19,10 @@ define('TDB_WORLD_EXPECTED_VER', 24041);
// https://www.wowhead.com/wotlk/es/search=vuelo
define('WOWHEAD_LINK', 'https://www.wowhead.com/wotlk/%s/%s=%s');
define('LOG_LEVEL_ERROR', 1);
define('LOG_LEVEL_WARN', 2);
define('LOG_LEVEL_INFO', 3);
define('MIME_TYPE_TEXT', 'Content-Type: text/plain; charset=utf-8');
define('MIME_TYPE_XML', 'Content-Type: text/xml; charset=utf-8');
define('MIME_TYPE_JSON', 'Content-Type: application/x-javascript; charset=utf-8');
@@ -387,182 +391,6 @@ define('EXP_CLASSIC', 0);
define('EXP_BC', 1);
define('EXP_WOTLK', 2);
enum ChrClass : int
{
case WARRIOR = 1;
case PALADIN = 2;
case HUNTER = 3;
case ROGUE = 4;
case PRIEST = 5;
case DEATHKNIGHT = 6;
case SHAMAN = 7;
case MAGE = 8;
case WARLOCK = 9;
case DRUID = 11;
public const MASK_ALL = 0x5FF;
public function matches(int $classMask) : bool
{
return !$classMask || $this->value & $classMask;
}
public function toMask() : int
{
return 1 << ($this->value - 1);
}
public static function fromMask(int $classMask = self::MASK_ALL) : array
{
$x = [];
foreach (self::cases() as $cl)
if ($cl->toMask() & $classMask)
$x[] = $cl->value;
return $x;
}
public function json() : string
{
return match ($this)
{
self::WARRIOR => 'warrior',
self::PALADIN => 'paladin',
self::HUNTER => 'hunter',
self::ROGUE => 'rogue',
self::PRIEST => 'priest',
self::DEATHKNIGHT => 'deathknight',
self::SHAMAN => 'shaman',
self::MAGE => 'mage',
self::WARLOCK => 'warlock',
self::DRUID => 'druid'
};
}
}
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;
public const MASK_ALLIANCE = 0x44D;
public const MASK_HORDE = 0x2B2;
public const MASK_ALL = 0x6FF;
public function matches(int $raceMask) : bool
{
return !$raceMask || $this->value & $raceMask;
}
public function toMask() : int
{
return 1 << ($this->value - 1);
}
public function isAlliance() : bool
{
return $this->toMask() & self::MASK_ALLIANCE;
}
public function isHorde() : bool
{
return $this->toMask() & self::MASK_HORDE;
}
public function getSide() : int
{
if ($this->isHorde() && $this->isAlliance())
return SIDE_BOTH;
else if ($this->isHorde())
return SIDE_HORDE;
else if ($this->isAlliance())
return SIDE_ALLIANCE;
else
return SIDE_NONE;
}
public function getTeam() : int
{
if ($this->isHorde() && $this->isAlliance())
return TEAM_NEUTRAL;
else if ($this->isHorde())
return TEAM_HORDE;
else if ($this->isAlliance())
return TEAM_ALLIANCE;
else
return TEAM_NEUTRAL;
}
public function json() : string
{
return match ($this)
{
self::HUMAN => 'human',
self::ORC => 'orc',
self::DWARF => 'dwarf',
self::NIGHTELF => 'nightelf',
self::UNDEAD => 'undead',
self::TAUREN => 'tauren',
self::GNOME => 'gnome',
self::TROLL => 'troll',
self::BLOODELF => 'bloodelf',
self::DRAENEI => 'draenei'
};
}
public static function fromMask(int $raceMask = self::MASK_ALL) : array
{
$x = [];
foreach (self::cases() as $cl)
if ($cl->toMask() & $raceMask)
$x[] = $cl->value;
return $x;
}
public static function sideFromMask(int $raceMask) : int
{
// Any
if (!$raceMask || ($raceMask & self::MASK_ALL) == self::MASK_ALL)
return SIDE_BOTH;
// Horde
if ($raceMask & self::MASK_HORDE && !($raceMask & self::MASK_ALLIANCE))
return SIDE_HORDE;
// Alliance
if ($raceMask & self::MASK_ALLIANCE && !($raceMask & self::MASK_HORDE))
return SIDE_ALLIANCE;
return SIDE_BOTH;
}
public static function teamFromMask(int $raceMask) : int
{
// Any
if (!$raceMask || ($raceMask & self::MASK_ALL) == self::MASK_ALL)
return TEAM_NEUTRAL;
// Horde
if ($raceMask & self::MASK_HORDE && !($raceMask & self::MASK_ALLIANCE))
return TEAM_HORDE;
// Alliance
if ($raceMask & self::MASK_ALLIANCE && !($raceMask & self::MASK_HORDE))
return TEAM_ALLIANCE;
return TEAM_NEUTRAL;
}
}
// SpellFamilyNames
define('SPELLFAMILY_GENERIC', 0);
define('SPELLFAMILY_UNK1', 1); // events, holidays

View File

@@ -0,0 +1,79 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
enum ChrClass : int
{
case WARRIOR = 1;
case PALADIN = 2;
case HUNTER = 3;
case ROGUE = 4;
case PRIEST = 5;
case DEATHKNIGHT = 6;
case SHAMAN = 7;
case MAGE = 8;
case WARLOCK = 9;
case DRUID = 11;
public const MASK_ALL = 0x5FF;
public function matches(int $classMask) : bool
{
return !$classMask || $this->value & $classMask;
}
public function toMask() : int
{
return 1 << ($this->value - 1);
}
public static function fromMask(int $classMask = self::MASK_ALL) : array
{
$x = [];
foreach (self::cases() as $cl)
if ($cl->toMask() & $classMask)
$x[] = $cl->value;
return $x;
}
public function json() : string
{
return match ($this)
{
self::WARRIOR => 'warrior',
self::PALADIN => 'paladin',
self::HUNTER => 'hunter',
self::ROGUE => 'rogue',
self::PRIEST => 'priest',
self::DEATHKNIGHT => 'deathknight',
self::SHAMAN => 'shaman',
self::MAGE => 'mage',
self::WARLOCK => 'warlock',
self::DRUID => 'druid'
};
}
public function spellFamily() : int
{
return match ($this)
{
self::WARRIOR => SPELLFAMILY_WARRIOR,
self::PALADIN => SPELLFAMILY_PALADIN,
self::HUNTER => SPELLFAMILY_HUNTER,
self::ROGUE => SPELLFAMILY_ROGUE,
self::PRIEST => SPELLFAMILY_PRIEST,
self::DEATHKNIGHT => SPELLFAMILY_DEATHKNIGHT,
self::SHAMAN => SPELLFAMILY_SHAMAN,
self::MAGE => SPELLFAMILY_MAGE,
self::WARLOCK => SPELLFAMILY_WARLOCK,
self::DRUID => SPELLFAMILY_DRUID
};
}
}
?>

View File

@@ -0,0 +1,132 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
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;
public const MASK_ALLIANCE = 0x44D;
public const MASK_HORDE = 0x2B2;
public const MASK_ALL = 0x6FF;
public function matches(int $raceMask) : bool
{
return !$raceMask || $this->value & $raceMask;
}
public function toMask() : int
{
return 1 << ($this->value - 1);
}
public function isAlliance() : bool
{
return $this->toMask() & self::MASK_ALLIANCE;
}
public function isHorde() : bool
{
return $this->toMask() & self::MASK_HORDE;
}
public function getSide() : int
{
if ($this->isHorde() && $this->isAlliance())
return SIDE_BOTH;
else if ($this->isHorde())
return SIDE_HORDE;
else if ($this->isAlliance())
return SIDE_ALLIANCE;
else
return SIDE_NONE;
}
public function getTeam() : int
{
if ($this->isHorde() && $this->isAlliance())
return TEAM_NEUTRAL;
else if ($this->isHorde())
return TEAM_HORDE;
else if ($this->isAlliance())
return TEAM_ALLIANCE;
else
return TEAM_NEUTRAL;
}
public function json() : string
{
return match ($this)
{
self::HUMAN => 'human',
self::ORC => 'orc',
self::DWARF => 'dwarf',
self::NIGHTELF => 'nightelf',
self::UNDEAD => 'undead',
self::TAUREN => 'tauren',
self::GNOME => 'gnome',
self::TROLL => 'troll',
self::BLOODELF => 'bloodelf',
self::DRAENEI => 'draenei'
};
}
public static function fromMask(int $raceMask = self::MASK_ALL) : array
{
$x = [];
foreach (self::cases() as $cl)
if ($cl->toMask() & $raceMask)
$x[] = $cl->value;
return $x;
}
public static function sideFromMask(int $raceMask) : int
{
// Any
if (!$raceMask || ($raceMask & self::MASK_ALL) == self::MASK_ALL)
return SIDE_BOTH;
// Horde
if ($raceMask & self::MASK_HORDE && !($raceMask & self::MASK_ALLIANCE))
return SIDE_HORDE;
// Alliance
if ($raceMask & self::MASK_ALLIANCE && !($raceMask & self::MASK_HORDE))
return SIDE_ALLIANCE;
return SIDE_BOTH;
}
public static function teamFromMask(int $raceMask) : int
{
// Any
if (!$raceMask || ($raceMask & self::MASK_ALL) == self::MASK_ALL)
return TEAM_NEUTRAL;
// Horde
if ($raceMask & self::MASK_HORDE && !($raceMask & self::MASK_ALLIANCE))
return TEAM_HORDE;
// Alliance
if ($raceMask & self::MASK_ALLIANCE && !($raceMask & self::MASK_HORDE))
return TEAM_ALLIANCE;
return TEAM_NEUTRAL;
}
}
?>

View File

@@ -133,11 +133,6 @@ class Game
'meta', 'red', 'yellow', 'blue'
);
public static $class2SpellFamily = array(
// null Warrior Paladin Hunter Rogue Priest DK Shaman Mage Warlock null Druid
null, 4, 10, 9, 8, 6, 15, 11, 3, 5, null, 7
);
public static function getReputationLevelForPoints($pts)
{
if ($pts >= 41999)
@@ -216,157 +211,6 @@ class Game
return $pages;
}
/*********************/
/* World Pos. Checks */
/*********************/
private static $alphaMapCache = [];
private static function alphaMapCheck(int $areaId, array &$set) : bool
{
$file = 'cache/alphaMaps/'.$areaId.'.png';
if (!file_exists($file)) // file does not exist (probably instanced area)
return false;
// invalid and corner cases (literally)
if (!is_array($set) || empty($set['posX']) || empty($set['posY']) || $set['posX'] >= 100 || $set['posY'] >= 100)
{
$set = null;
return true;
}
if (empty(self::$alphaMapCache[$areaId]))
self::$alphaMapCache[$areaId] = imagecreatefrompng($file);
// alphaMaps are 1000 x 1000, adapt points [black => valid point]
if (!imagecolorat(self::$alphaMapCache[$areaId], $set['posX'] * 10, $set['posY'] * 10))
$set = null;
return true;
}
public static function checkCoords(array $points) : array
{
$result = [];
$capitals = array( // capitals take precedence over their surroundings
1497, 1637, 1638, 3487, // Undercity, Ogrimmar, Thunder Bluff, Silvermoon City
1519, 1537, 1657, 3557, // Stormwind City, Ironforge, Darnassus, The Exodar
3703, 4395 // Shattrath City, Dalaran
);
foreach ($points as $res)
{
if (self::alphaMapCheck($res['areaId'], $res))
{
if (!$res)
continue;
// some rough measure how central the spawn is on the map (the lower the number, the better)
// 0: perfect center; 1: touches a border
$q = abs( (($res['posX'] - 50) / 50) * (($res['posY'] - 50) / 50) );
if (empty($result) || $result[0] > $q)
$result = [$q, $res];
}
else if (in_array($res['areaId'], $capitals)) // capitals (auto-discovered) and no hand-made alphaMap available
return $res;
else if (empty($result)) // add with lowest quality if alpha map is missing
$result = [1.0, $res];
}
// 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; });
$result = [1.0, $points[0]];
}
return $result[1];
}
public static function getWorldPosForGUID(int $type, int ...$guids) : array
{
$result = [];
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);
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);
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);
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);
break;
case Type::AREATRIGGER:
$result = [];
if ($base = array_filter($guids, function ($x) { return $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));
if ($endpoints = array_filter($guids, function ($x) { return $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',
$endpoints, $endpoints, SmartAI::SRC_TYPE_AREATRIGGER, SmartAction::ACTION_TELEPORT
));
break;
default:
trigger_error('Game::getWorldPosForGUID - instanced with unsupported TYPE #'.$type, E_USER_WARNING);
}
if ($diff = array_diff($guids, array_keys($result)))
trigger_error('Game::getWorldPosForGUID - no spawn points for TYPE #'.$type.' GUIDS: '.implode(', ', $diff), E_USER_WARNING);
return $result;
}
public static function worldPosToZonePos(int $mapId, float $posX, float $posY, int $areaId = 0, int $floor = -1) : array
{
if (!$mapId < 0)
return [];
$query =
'SELECT
x.`id`,
x.`areaId`,
IF(x.`defaultDungeonMapId` < 0, x.`floor` + 1, x.`floor`) AS `floor`,
IF(dm.`id` IS NOT NULL OR x.`defaultDungeonMapId` < 0, 1, 0) AS `multifloor`,
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`
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
LEFT JOIN
?_dungeonmap dm ON dm.`mapId` = x.`mapId` AND dm.`worldMapAreaId` = x.`worldMapAreaId` AND dm.`floor` <> x.`floor` AND dm.`worldMapAreaId` > 0
WHERE
x.`mapId` = ?d AND IF(?d, x.`areaId` = ?d, x.`areaId` <> 0){ AND x.`floor` = ?d - IF(x.`defaultDungeonMapId` < 0, 1, 0)}
GROUP BY
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, $posY, $posX, $posY, $posX, $mapId, $areaId, $areaId, $floor < 0 ? DBSIMPLE_SKIP : $floor);
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, $posY, $posX, $posY, $posX, $mapId, 0, 0, DBSIMPLE_SKIP);
if (!is_array($points))
{
trigger_error('Game::worldPosToZonePos - dbc query failed', E_USER_ERROR);
return [];
}
return $points;
}
public static function getQuotesForCreature(int $creatureId, bool $asHTML = false, string $talkSource = '') : array
{
$nQuotes = 0;

View File

@@ -0,0 +1,160 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
abstract class WorldPosition
{
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
1519, 1537, 1657, 3557, // Stormwind City, Ironforge, Darnassus, The Exodar
3703, 4395 // Shattrath City, Dalaran
);
private static function alphaMapCheck(int $areaId, array &$set) : bool
{
$file = 'cache/alphaMaps/'.$areaId.'.png';
if (!file_exists($file)) // file does not exist (probably instanced area)
return false;
// invalid and corner cases (literally)
if (empty($set['posX']) || empty($set['posY']) || $set['posX'] >= 100 || $set['posY'] >= 100)
{
$set = null;
return true;
}
if (empty(self::$alphaMapCache[$areaId]))
self::$alphaMapCache[$areaId] = imagecreatefrompng($file);
// alphaMaps are 1000 x 1000, adapt points [black => valid point]
if (!imagecolorat(self::$alphaMapCache[$areaId], $set['posX'] * 10, $set['posY'] * 10))
$set = null;
return true;
}
public static function checkZonePos(array $points) : array
{
$result = [];
foreach ($points as $res)
{
if (self::alphaMapCheck($res['areaId'], $res))
{
if (!$res)
continue;
// some rough measure how central the spawn is on the map (the lower the number, the better)
// 0: perfect center; 1: touches a border
$q = abs( (($res['posX'] - 50) / 50) * (($res['posY'] - 50) / 50) );
if (empty($result) || $result[0] > $q)
$result = [$q, $res];
}
// capitals (auto-discovered) and no hand-made alphaMap available
else if (in_array($res['areaId'], self::$capitalCities))
return $res;
// add with lowest quality if alpha map is missing
else if (empty($result))
$result = [1.0, $res];
}
// spawn does not really match on a map, but we need at least one result
if (!$result)
{
usort($points, function ($a, $b) { return ($a['dist'] < $b['dist']) ? -1 : 1; });
$result = [1.0, $points[0]];
}
return $result[1];
}
public static function getForGUID(int $type, int ...$guids) : array
{
$result = [];
switch ($type)
{
case Type::NPC:
$result = DB::World()->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);
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);
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);
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);
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));
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',
$endpoints, $endpoints, SmartAI::SRC_TYPE_AREATRIGGER, SmartAction::ACTION_TELEPORT
));
break;
default:
trigger_error('WorldPosition::getForGUID - unsupported TYPE #'.$type, E_USER_WARNING);
}
if ($diff = array_diff($guids, array_keys($result)))
trigger_error('WorldPosition::getForGUID - no spawn points for TYPE #'.$type.' GUIDS: '.implode(', ', $diff), E_USER_WARNING);
return $result;
}
public static function toZonePos(int $mapId, float $mapX, float $mapY, int $preferedAreaId = 0, int $preferedFloor = -1) : array
{
if (!$mapId < 0)
return [];
$query =
'SELECT
x.`id`,
x.`areaId`,
IF(x.`defaultDungeonMapId` < 0, x.`floor` + 1, x.`floor`) AS `floor`,
IF(dm.`id` IS NOT NULL OR x.`defaultDungeonMapId` < 0, 1, 0) AS `multifloor`,
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`
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
LEFT JOIN
?_dungeonmap dm ON dm.`mapId` = x.`mapId` AND dm.`worldMapAreaId` = x.`worldMapAreaId` AND dm.`floor` <> x.`floor` AND dm.`worldMapAreaId` > 0
WHERE
x.`mapId` = ?d AND IF(?d, x.`areaId` = ?d, x.`areaId` <> 0){ AND x.`floor` = ?d - IF(x.`defaultDungeonMapId` < 0, 1, 0)}
GROUP BY
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;
}
}
?>

View File

@@ -34,19 +34,40 @@ 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 'includes/utilities.php'; // helper functions
require_once 'includes/config.class.php'; // Config holder
require_once 'includes/type.class.php'; // DB types storage and factory
require_once 'includes/cfg.class.php'; // Config holder
require_once 'includes/user.class.php'; // Session handling (could be skipped for CLI context except for username and password validation used in account creation)
require_once 'includes/game/misc.php'; // Misc game related data & functions
// todo: make everything below autoloaded
require_once 'includes/stats.class.php'; // Game entity statistics conversion
require_once 'includes/game.php'; // game related data & functions
require_once 'includes/profiler.class.php'; // Profiler feature
require_once 'includes/markup.class.php'; // manipulate markup text
require_once 'includes/community.class.php'; // handle comments, screenshots and videos
require_once 'includes/loot.class.php'; // build lv-tabs containing loot-information
require_once 'pages/genericPage.class.php';
// game client data interfaces
spl_autoload_register(function ($class)
{
if ($i = strrpos($class, '\\'))
$class = substr($class, $i + 1);
// TC systems
if (preg_match('/[^\w]/i', $class))
return;
if ($class == 'Stats' || $class == 'StatsContainer') // entity statistics conversion
require_once 'includes/game/chrstatistics.php';
else if (file_exists('includes/game/'.strtolower($class).'.class.php'))
require_once 'includes/game/'.strtolower($class).'.class.php';
});
// our site components
spl_autoload_register(function ($class)
{
if ($i = strrpos($class, '\\'))
$class = substr($class, $i + 1);
if (preg_match('/[^\w]/i', $class))
return;
if (file_exists('includes/components/'.strtolower($class).'.class.php'))
require_once 'includes/components/'.strtolower($class).'.class.php';
});
// TC systems in components
spl_autoload_register(function ($class)
{
switch ($class)
@@ -66,52 +87,73 @@ spl_autoload_register(function ($class)
}
});
// autoload List-classes, associated filters and pages
// autoload List-classes, associated filters
spl_autoload_register(function ($class)
{
$class = strtolower(str_replace('ListFilter', 'List', $class));
if (class_exists($class)) // already registered
return;
if ($i = strrpos($class, '\\'))
$class = substr($class, $i + 1);
if (preg_match('/[^\w]/i', $class)) // name should contain only letters
if (preg_match('/[^\w]/i', $class))
return;
if (stripos($class, 'list'))
if (!stripos($class, 'list'))
return;
$class = strtolower(str_replace('ListFilter', 'List', $class));
$cl = match ($class)
{
require_once 'includes/basetype.class.php';
'localprofilelist',
'remoteprofilelist' => 'profile',
'localarenateamlist',
'remotearenateamlist' => 'arenateam',
'localguildlist',
'remoteguildlist' => 'guild',
default => strtr($class, ['list' => ''])
};
$cl = strtr($class, ['list' => '']);
if ($cl == 'remoteprofile' || $cl == 'localprofile')
$cl = 'profile';
if ($cl == 'remotearenateam' || $cl == 'localarenateam')
$cl = 'arenateam';
if ($cl == 'remoteguild' || $cl == 'localguild')
$cl = 'guild';
if (file_exists('includes/types/'.$cl.'.class.php'))
require_once 'includes/types/'.$cl.'.class.php';
else
throw new \Exception('could not register type class: '.$cl);
return;
if (file_exists('includes/types/'.$cl.'.class.php'))
{
require_once 'includes/types/basetype.class.php';
require_once 'includes/types/'.$cl.'.class.php';
}
else if (stripos($class, 'ajax') === 0)
{
require_once 'includes/ajaxHandler.class.php'; // handles ajax and jsonp requests
else
throw new \Exception('could not register type class: '.$cl);
});
// endpoint loader
spl_autoload_register(function ($class)
{
if ($i = strrpos($class, '\\'))
$class = substr($class, $i + 1);
if (preg_match('/[^\w]/i', $class))
return;
$class = strtolower($class);
if (stripos($class, 'ajax') === 0) // handles ajax and jsonp requests
{
if (file_exists('includes/ajaxHandler/'.strtr($class, ['ajax' => '']).'.class.php'))
{
require_once 'includes/ajaxHandler/ajaxHandler.class.php';
require_once 'includes/ajaxHandler/'.strtr($class, ['ajax' => '']).'.class.php';
}
else
throw new \Exception('could not register ajaxHandler class: '.$class);
return;
}
else if (file_exists('pages/'.strtr($class, ['page' => '']).'.php'))
require_once 'pages/'.strtr($class, ['page' => '']).'.php';
else if (stripos($class, 'page')) // handles templated pages
{
if (file_exists('pages/'.strtr($class, ['page' => '']).'.php'))
{
require_once 'pages/genericPage.class.php';
require_once 'pages/'.strtr($class, ['page' => '']).'.php';
}
else if ($class == 'genericpage') // may be called directly in fatal error case
require_once 'pages/genericPage.class.php';
}
});
set_error_handler(function($errNo, $errStr, $errFile, $errLine)
@@ -120,28 +162,25 @@ set_error_handler(function($errNo, $errStr, $errFile, $errLine)
if (strstr($errStr, 'mysqli_connect') && $errNo == E_WARNING)
return true;
$errName = 'unknown error'; // errors not in this list can not be handled by set_error_handler (as per documentation) or are ignored
$logLevel = CLI::logLevelFromE($errNo);
// we do not log deprecation notices
if ($errNo & (E_DEPRECATED | E_USER_DEPRECATED))
return true;
switch ($errNo)
$logLevel = match($errNo)
{
case E_WARNING:
case E_USER_WARNING:
$errName = 'WARNING';
break;
case E_NOTICE:
case E_USER_NOTICE:
$errName = 'NOTICE';
break;
case E_USER_ERROR:
$errName = 'USER_ERROR';
case E_USER_ERROR:
$errName = 'RECOVERABLE_ERROR';
case E_STRICT: // ignore STRICT and DEPRECATED
case E_DEPRECATED:
case E_USER_DEPRECATED:
return true;
}
E_RECOVERABLE_ERROR, E_USER_ERROR => LOG_LEVEL_ERROR,
E_WARNING, E_USER_WARNING => LOG_LEVEL_WARN,
E_NOTICE, E_USER_NOTICE => LOG_LEVEL_INFO,
default => 0
};
$errName = match($errNo)
{
E_RECOVERABLE_ERROR => 'RECOVERABLE_ERROR',
E_USER_ERROR => 'USER_ERROR',
E_USER_WARNING, E_WARNING => 'WARNING',
E_USER_NOTICE, E_NOTICE => 'NOTICE',
default => 'UNKNOWN_ERROR' // errors not in this list can not be handled by set_error_handler (as per documentation) or are ignored
};
if (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()',
@@ -168,7 +207,7 @@ set_exception_handler(function ($e)
fwrite(STDERR, "\nException - ".$e->getMessage()."\n ".$e->getFile(). '('.$e->getLine().")\n".$e->getTraceAsString()."\n\n");
else
{
Util::addNote('Exception - '.$e->getMessage().' @ '.$e->getFile(). ':'.$e->getLine()."\n".$e->getTraceAsString(), U_GROUP_EMPLOYEE, CLI::LOG_ERROR);
Util::addNote('Exception - '.$e->getMessage().' @ '.$e->getFile(). ':'.$e->getLine()."\n".$e->getTraceAsString(), U_GROUP_EMPLOYEE, LOG_LEVEL_ERROR);
(new GenericPage())->error();
}
});
@@ -253,7 +292,7 @@ if (!CLI)
Lang::load(User::$preferedLoc);
// set up some logging (~10 queries will execute before we init the user and load the config)
if (Cfg::get('DEBUG') >= CLI::LOG_INFO && User::isInGroup(U_GROUP_DEV | U_GROUP_ADMIN))
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(...));

View File

@@ -176,36 +176,4 @@ enum Locale : int
}
}
/* The shape of things to come */
class LocString
{
private \WeakMap $store;
public function __construct(array $data, string $key = 'name', ?callable $callback = null)
{
$this->store = new \WeakMap();
$callback ??= fn($x) => $x;
if (!array_filter($data, fn($v, $k) => $v && strstr($k, $key.'_loc'), ARRAY_FILTER_USE_BOTH))
trigger_error('LocString - is entrirely empty', E_USER_WARNING);
foreach (Locale::cases() as $l)
$this->store[$l] = (string)$callback($data[$key.'_loc'.$l->value] ?? '');
}
public function __toString() : string
{
if ($str = $this->store[Lang::getLocale()])
return $str;
foreach (Locale::cases() as $l) // desired loc not set, use any other
if ($str = $this->store[$l])
return Cfg::get('DEBUG') ? '['.$str.']' : $str;
return Cfg::get('DEBUG') ? '[LOCSTRING]' : '';
}
}
?>

View File

@@ -0,0 +1,355 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
abstract class CLI
{
private const CHR_BELL = 7;
private const CHR_BACK = 8;
private const CHR_TAB = 9;
private const CHR_LF = 10;
private const CHR_CR = 13;
private const CHR_ESC = 27;
private const CHR_BACKSPACE = 127;
public const LOG_NONE = -1;
public const LOG_BLANK = 0;
public const LOG_ERROR = LOG_LEVEL_ERROR;
public const LOG_WARN = LOG_LEVEL_WARN;
public const LOG_INFO = LOG_LEVEL_INFO;
public const LOG_OK = 4;
private static $logHandle = null;
private static $hasReadline = null;
private static $overwriteLast = false;
/********************/
/* formatted output */
/********************/
public static function writeTable(array $out, bool $timestamp = false, bool $headless = false) : void
{
if (!$out)
return;
$pads = [];
$nCols = 0;
foreach ($out as $i => $row)
{
if (!is_array($out[0]))
{
unset($out[$i]);
continue;
}
$nCols = max($nCols, count($row));
for ($j = 0; $j < $nCols; $j++)
$pads[$j] = max($pads[$j] ?? 0, mb_strlen(self::purgeEscapes($row[$j] ?? '')));
}
foreach ($out as $i => $row)
{
for ($j = 0; $j < $nCols; $j++)
{
if (!isset($row[$j]))
break;
$len = ($pads[$j] - mb_strlen(self::purgeEscapes($row[$j])));
for ($k = 0; $k < $len; $k++) // can't use str_pad(). it counts invisible chars.
$row[$j] .= ' ';
}
if ($i || $headless)
self::write(' '.implode(' ' . self::tblDelim(' ') . ' ', $row), self::LOG_NONE, $timestamp);
else
self::write(self::tblHead(' '.implode(' ', $row)), self::LOG_NONE, $timestamp);
}
if (!$headless)
self::write(self::tblHead(str_pad('', array_sum($pads) + count($pads) * 3 - 2)), self::LOG_NONE, $timestamp);
self::write();
}
/***********/
/* logging */
/***********/
public static function initLogFile(string $file = '') : void
{
if (!$file)
return;
$file = self::nicePath($file);
if (!file_exists($file))
self::$logHandle = fopen($file, 'w');
else
{
$logFileParts = pathinfo($file);
$i = 1;
while (file_exists($logFileParts['dirname'].'/'.$logFileParts['filename'].$i.(isset($logFileParts['extension']) ? '.'.$logFileParts['extension'] : '')))
$i++;
$file = $logFileParts['dirname'].'/'.$logFileParts['filename'].$i.(isset($logFileParts['extension']) ? '.'.$logFileParts['extension'] : '');
self::$logHandle = fopen($file, 'w');
}
}
private static function tblHead(string $str) : string
{
return CLI_HAS_E ? "\e[1;48;5;236m".$str."\e[0m" : $str;
}
private static function tblDelim(string $str) : string
{
return CLI_HAS_E ? "\e[48;5;236m".$str."\e[0m" : $str;
}
public static function grey(string $str) : string
{
return CLI_HAS_E ? "\e[90m".$str."\e[0m" : $str;
}
public static function red(string $str) : string
{
return CLI_HAS_E ? "\e[31m".$str."\e[0m" : $str;
}
public static function green(string $str) : string
{
return CLI_HAS_E ? "\e[32m".$str."\e[0m" : $str;
}
public static function yellow(string $str) : string
{
return CLI_HAS_E ? "\e[33m".$str."\e[0m" : $str;
}
public static function blue(string $str) : string
{
return CLI_HAS_E ? "\e[36m".$str."\e[0m" : $str;
}
public static function bold(string $str) : string
{
return CLI_HAS_E ? "\e[1m".$str."\e[0m" : $str;
}
public static function write(string $txt = '', int $lvl = self::LOG_BLANK, bool $timestamp = true, bool $tmpRow = false) : void
{
$msg = '';
if ($txt)
{
if ($timestamp)
$msg = str_pad(date('H:i:s'), 10);
switch ($lvl)
{
case self::LOG_ERROR: // red critical error
$msg .= '['.self::red('ERR').'] ';
break;
case self::LOG_WARN: // yellow notice
$msg .= '['.self::yellow('WARN').'] ';
break;
case self::LOG_OK: // green success
$msg .= '['.self::green('OK').'] ';
break;
case self::LOG_INFO: // blue info
$msg .= '['.self::blue('INFO').'] ';
break;
case self::LOG_BLANK:
$msg .= ' ';
break;
}
$msg .= $txt;
}
// https://shiroyasha.svbtle.com/escape-sequences-a-quick-guide-1#movement_1
$msg = (self::$overwriteLast && CLI_HAS_E ? "\e[1G\e[0K" : "\n") . $msg;
self::$overwriteLast = $tmpRow;
fwrite($lvl == self::LOG_ERROR ? STDERR : STDOUT, $msg);
if (self::$logHandle) // remove control sequences from log
fwrite(self::$logHandle, self::purgeEscapes($msg));
flush();
}
private static function purgeEscapes(string $msg) : string
{
return preg_replace(["/\e\[[\d;]+[mK]/", "/\e\[\d+G/"], ['', "\n"], $msg);
}
public static function nicePath(string $fileOrPath, string ...$pathParts) : string
{
$path = '';
if ($pathParts)
{
foreach ($pathParts as &$pp)
$pp = trim($pp);
$path .= implode(DIRECTORY_SEPARATOR, $pathParts);
}
$path .= ($path ? DIRECTORY_SEPARATOR : '').trim($fileOrPath);
// remove double quotes (from erronous user input), single quotes are
// valid chars for filenames and removing those mutilates several wow icons
$path = str_replace('"', '', $path);
if (!$path) // empty strings given. (faulty dbc data?)
return '';
if (DIRECTORY_SEPARATOR == '/') // *nix
{
$path = str_replace('\\', '/', $path);
$path = preg_replace('/\/+/i', '/', $path);
}
else if (DIRECTORY_SEPARATOR == '\\') // win
{
$path = str_replace('/', '\\', $path);
$path = preg_replace('/\\\\+/i', '\\', $path);
}
else
self::write('Dafuq! Your directory separator is "'.DIRECTORY_SEPARATOR.'". Please report this!', self::LOG_ERROR);
// resolve *nix home shorthand
if (!OS_WIN)
{
if (preg_match('/^~(\w+)\/.*/i', $path, $m))
$path = '/home/'.substr($path, 1);
else if (substr($path, 0, 2) == '~/')
$path = getenv('HOME').substr($path, 1);
else if ($path[0] == DIRECTORY_SEPARATOR && substr($path, 0, 6) != '/home/')
$path = substr($path, 1);
}
return $path;
}
/**************/
/* read input */
/**************/
/*
since the CLI on WIN ist not interactive, the following things have to be considered
you do not receive keystrokes but whole strings upon pressing <Enter> (wich also appends a \r)
as such <ESC> and probably other control chars can not be registered
this also means, you can't hide input at all, least process it
*/
public static function read(array $fields, ?array &$userInput = []) : bool
{
// first time set
if (self::$hasReadline === null)
self::$hasReadline = function_exists('readline_callback_handler_install');
// prevent default output if able
if (self::$hasReadline)
readline_callback_handler_install('', function() { });
if (!STDIN)
return false;
stream_set_blocking(STDIN, false);
// pad default values onto $fields
array_walk($fields, function(&$val, $_, $pad) { $val += $pad; }, ['', false, false, '']);
foreach ($fields as $name => [$desc, $isHidden, $singleChar, $validPattern])
{
$charBuff = '';
if ($desc)
fwrite(STDOUT, "\n".$desc.": ");
while (true) {
if (feof(STDIN))
return false;
$r = [STDIN];
$w = $e = null;
$n = stream_select($r, $w, $e, 200000);
if (!$n || !in_array(STDIN, $r))
continue;
// stream_get_contents is always blocking under WIN - fgets should work similary as php always receives a terminated line of text
$chars = str_split(OS_WIN ? fgets(STDIN) : stream_get_contents(STDIN));
$ordinals = array_map('ord', $chars);
if ($ordinals[0] == self::CHR_ESC)
{
if (count($ordinals) == 1)
{
fwrite(STDOUT, chr(self::CHR_BELL));
return false;
}
else
continue;
}
foreach ($chars as $idx => $char)
{
$keyId = $ordinals[$idx];
// skip char if horizontal tab or \r if followed by \n
if ($keyId == self::CHR_TAB || ($keyId == self::CHR_CR && ($ordinals[$idx + 1] ?? '') == self::CHR_LF))
continue;
if ($keyId == self::CHR_BACKSPACE)
{
if (!$charBuff)
continue 2;
$charBuff = mb_substr($charBuff, 0, -1);
if (!$isHidden && self::$hasReadline)
fwrite(STDOUT, chr(self::CHR_BACK)." ".chr(self::CHR_BACK));
}
// standalone \n or \r
else if ($keyId == self::CHR_LF || $keyId == self::CHR_CR)
{
$userInput[$name] = $charBuff;
break 2;
}
else if (!$validPattern || preg_match($validPattern, $char))
{
$charBuff .= $char;
if (!$isHidden && self::$hasReadline)
fwrite(STDOUT, $char);
if ($singleChar && self::$hasReadline)
{
$userInput[$name] = $charBuff;
break 2;
}
}
}
}
}
fwrite(STDOUT, chr(self::CHR_BELL));
foreach ($userInput as $ui)
if (strlen($ui))
return true;
$userInput = null;
return true;
}
}
?>

View File

@@ -0,0 +1,39 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
class Timer
{
private $t_cur = 0;
private $t_new = 0;
private $intv = 0;
public function __construct(int $intervall)
{
$this->intv = $intervall / 1000; // in msec
$this->t_cur = microtime(true);
}
public function update() : bool
{
$this->t_new = microtime(true);
if ($this->t_new > $this->t_cur + $this->intv)
{
$this->t_cur = $this->t_cur + $this->intv;
return true;
}
return false;
}
public function reset() : void
{
$this->t_cur = microtime(true) - $this->intv;
}
}
?>

228
includes/type.class.php Normal file
View File

@@ -0,0 +1,228 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
abstract class Type
{
public const NPC = 1;
public const OBJECT = 2;
public const ITEM = 3;
public const ITEMSET = 4;
public const QUEST = 5;
public const SPELL = 6;
public const ZONE = 7;
public const FACTION = 8;
public const PET = 9;
public const ACHIEVEMENT = 10;
public const TITLE = 11;
public const WORLDEVENT = 12;
public const CHR_CLASS = 13;
public const CHR_RACE = 14;
public const SKILL = 15;
public const STATISTIC = 16;
public const CURRENCY = 17;
// PROJECT = 18;
public const SOUND = 19;
// BUILDING = 20;
// FOLLOWER = 21;
// MISSION_ABILITY = 22;
// MISSION = 23;
// SHIP = 25;
// THREAT = 26;
// RESOURCE = 27;
// CHAMPION = 28;
public const ICON = 29;
// ORDER_ADVANCEMENT = 30;
// FOLLOWER_ALLIANCE = 31;
// FOLLOWER_HORDE = 32;
// SHIP_ALLIANCE = 33;
// SHIP_HORDE = 34;
// CHAMPION_ALLIANCE = 35;
// CHAMPION_HORDE = 36;
// TRANSMOG_ITEM = 37;
// BFA_CHAMPION = 38;
// BFA_CHAMPION_ALLIANCE = 39;
// AFFIX = 40;
// BFA_CHAMPION_HORDE = 41;
// AZERITE_ESSENCE_POWER = 42;
// AZERITE_ESSENCE = 43;
// STORYLINE = 44;
// ADVENTURE_COMBATANT_ABILITY = 46;
// ENCOUNTER = 47;
// COVENANT = 48;
// SOULBIND = 49;
// DI_ITEM = 50;
// GATHERER_SCREENSHOT = 91;
// GATHERER_GUIDE_IMAGE = 98;
public const PROFILE = 100;
// our own things
public const GUILD = 101;
// TRANSMOG_SET = 101; // future conflict inc.
public const ARENA_TEAM = 102;
// OUTFIT = 110;
// GEAR_SET = 111;
// GATHERER_LISTVIEW = 158;
// GATHERER_SURVEY_COVENANTS = 161;
// NEWS_POST = 162;
// BATTLE_PET_ABILITY = 200;
public const GUIDE = 300; // should have been 100, but conflicts with old version of Profile/List
public const USER = 500;
public const EMOTE = 501;
public const ENCHANTMENT = 502;
public const AREATRIGGER = 503;
public const MAIL = 504;
// Blizzard API things
// MOUNT = -1000;
// RECIPE = -1001;
// BATTLE_PET = -1002;
public const FLAG_NONE = 0x0;
public const FLAG_RANDOM_SEARCHABLE = 0x1;
/* public const FLAG_SEARCHABLE = 0x2 general search? */
public const IDX_LIST_OBJ = 0;
public const IDX_FILE_STR = 1;
public const IDX_JSG_TPL = 2;
public const IDX_FLAGS = 3;
private static array $data = array(
self::NPC => [__NAMESPACE__ . '\CreatureList', 'npc', 'g_npcs', 0x1],
self::OBJECT => [__NAMESPACE__ . '\GameObjectList', 'object', 'g_objects', 0x1],
self::ITEM => [__NAMESPACE__ . '\ItemList', 'item', 'g_items', 0x1],
self::ITEMSET => [__NAMESPACE__ . '\ItemsetList', 'itemset', 'g_itemsets', 0x1],
self::QUEST => [__NAMESPACE__ . '\QuestList', 'quest', 'g_quests', 0x1],
self::SPELL => [__NAMESPACE__ . '\SpellList', 'spell', 'g_spells', 0x1],
self::ZONE => [__NAMESPACE__ . '\ZoneList', 'zone', 'g_gatheredzones', 0x1],
self::FACTION => [__NAMESPACE__ . '\FactionList', 'faction', 'g_factions', 0x1],
self::PET => [__NAMESPACE__ . '\PetList', 'pet', 'g_pets', 0x1],
self::ACHIEVEMENT => [__NAMESPACE__ . '\AchievementList', 'achievement', 'g_achievements', 0x1],
self::TITLE => [__NAMESPACE__ . '\TitleList', 'title', 'g_titles', 0x1],
self::WORLDEVENT => [__NAMESPACE__ . '\WorldEventList', 'event', 'g_holidays', 0x1],
self::CHR_CLASS => [__NAMESPACE__ . '\CharClassList', 'class', 'g_classes', 0x1],
self::CHR_RACE => [__NAMESPACE__ . '\CharRaceList', 'race', 'g_races', 0x1],
self::SKILL => [__NAMESPACE__ . '\SkillList', 'skill', 'g_skills', 0x1],
self::STATISTIC => [__NAMESPACE__ . '\AchievementList', 'achievement', 'g_achievements', 0x0], // alias for achievements; exists only for Markup
self::CURRENCY => [__NAMESPACE__ . '\CurrencyList', 'currency', 'g_gatheredcurrencies',0x1],
self::SOUND => [__NAMESPACE__ . '\SoundList', 'sound', 'g_sounds', 0x1],
self::ICON => [__NAMESPACE__ . '\IconList', 'icon', 'g_icons', 0x1],
self::GUIDE => [__NAMESPACE__ . '\GuideList', 'guide', '', 0x0],
self::PROFILE => [__NAMESPACE__ . '\ProfileList', '', '', 0x0], // x - not known in javascript
self::GUILD => [__NAMESPACE__ . '\GuildList', '', '', 0x0], // x
self::ARENA_TEAM => [__NAMESPACE__ . '\ArenaTeamList', '', '', 0x0], // x
self::USER => [__NAMESPACE__ . '\UserList', 'user', 'g_users', 0x0], // x
self::EMOTE => [__NAMESPACE__ . '\EmoteList', 'emote', 'g_emotes', 0x1],
self::ENCHANTMENT => [__NAMESPACE__ . '\EnchantmentList', 'enchantment', 'g_enchantments', 0x1],
self::AREATRIGGER => [__NAMESPACE__ . '\AreatriggerList', 'areatrigger', '', 0x0],
self::MAIL => [__NAMESPACE__ . '\MailList', 'mail', '', 0x1]
);
/********************/
/* Field Operations */
/********************/
public static function newList(int $type, array $conditions = []) : ?BaseType
{
if (!self::exists($type))
return null;
return new (self::$data[$type][self::IDX_LIST_OBJ])($conditions);
}
public static function getFileString(int $type) : string
{
if (!self::exists($type))
return '';
return self::$data[$type][self::IDX_FILE_STR];
}
public static function getJSGlobalString(int $type) : string
{
if (!self::exists($type))
return '';
return self::$data[$type][self::IDX_JSG_TPL];
}
public static function getJSGlobalTemplate(int $type) : array
{
if (!self::exists($type) || !self::$data[$type][self::IDX_JSG_TPL])
return [];
// [key, [data], [extraData]]
return [self::$data[$type][self::IDX_JSG_TPL], [], []];
}
public static function checkClassAttrib(int $type, string $attr, ?int $attrVal = null) : bool
{
if (!self::exists($type))
return false;
return isset((self::$data[$type][self::IDX_LIST_OBJ])::$$attr) && ($attrVal === null || ((self::$data[$type][self::IDX_LIST_OBJ])::$$attr & $attrVal));
}
public static function getClassAttrib(int $type, string $attr) : mixed
{
if (!self::exists($type))
return null;
return (self::$data[$type][self::IDX_LIST_OBJ])::$$attr ?? null;
}
public static function exists(int $type) : bool
{
return !empty(self::$data[$type]);
}
public static function getIndexFrom(int $idx, string $match) : int
{
$i = array_search($match, array_column(self::$data, $idx));
if ($i === false)
return 0;
return array_keys(self::$data)[$i];
}
/*********************/
/* Column Operations */
/*********************/
public static function getClassesFor(int $flags = 0x0, string $attr = '', ?int $attrVal = null) : array
{
$x = [];
foreach (self::$data as $k => [$o, , , $f])
if ($o && (!$flags || $flags & $f))
if (!$attr || self::checkClassAttrib($k, $attr, $attrVal))
$x[$k] = $o;
return $x;
}
public static function getFileStringsFor(int $flags = 0x0) : array
{
$x = [];
foreach (self::$data as $k => [, $s, , $f])
if ($s && (!$flags || $flags & $f))
$x[$k] = $s;
return $x;
}
public static function getJSGTemplatesFor(int $flags = 0x0) : array
{
$x = [];
foreach (self::$data as $k => [, , $a, $f])
if ($a && (!$flags || $flags & $f))
$x[$k] = $a;
return $x;
}
}
?>

View File

@@ -612,7 +612,7 @@ trait spawnHelper
if (User::isInGroup(U_GROUP_MODERATOR))
if ($guids = array_column(array_filter($spawns, fn($x) => $x['guid'] > 0 || $x['type'] != Type::NPC), 'guid'))
$worldPos = Game::getWorldPosForGUID(self::$type, ...$guids);
$worldPos = WorldPosition::getForGUID(self::$type, ...$guids);
foreach ($spawns as $s)
{

View File

@@ -116,397 +116,6 @@ trait TrRequestData
}
}
abstract class CLI
{
private const CHR_BELL = 7;
private const CHR_BACK = 8;
private const CHR_TAB = 9;
private const CHR_LF = 10;
private const CHR_CR = 13;
private const CHR_ESC = 27;
private const CHR_BACKSPACE = 127;
public const LOG_NONE = -1;
public const LOG_BLANK = 0;
public const LOG_ERROR = 1;
public const LOG_WARN = 2;
public const LOG_INFO = 3;
public const LOG_OK = 4;
private static $logHandle = null;
private static $hasReadline = null;
private static $overwriteLast = false;
/********************/
/* formatted output */
/********************/
public static function writeTable(array $out, bool $timestamp = false, bool $headless = false) : void
{
if (!$out)
return;
$pads = [];
$nCols = 0;
foreach ($out as $i => $row)
{
if (!is_array($out[0]))
{
unset($out[$i]);
continue;
}
$nCols = max($nCols, count($row));
for ($j = 0; $j < $nCols; $j++)
$pads[$j] = max($pads[$j] ?? 0, mb_strlen(self::purgeEscapes($row[$j] ?? '')));
}
foreach ($out as $i => $row)
{
for ($j = 0; $j < $nCols; $j++)
{
if (!isset($row[$j]))
break;
$len = ($pads[$j] - mb_strlen(self::purgeEscapes($row[$j])));
for ($k = 0; $k < $len; $k++) // can't use str_pad(). it counts invisible chars.
$row[$j] .= ' ';
}
if ($i || $headless)
self::write(' '.implode(' ' . self::tblDelim(' ') . ' ', $row), CLI::LOG_NONE, $timestamp);
else
self::write(self::tblHead(' '.implode(' ', $row)), CLI::LOG_NONE, $timestamp);
}
if (!$headless)
self::write(self::tblHead(str_pad('', array_sum($pads) + count($pads) * 3 - 2)), CLI::LOG_NONE, $timestamp);
self::write();
}
/***********/
/* logging */
/***********/
public static function initLogFile(string $file = '') : void
{
if (!$file)
return;
$file = self::nicePath($file);
if (!file_exists($file))
self::$logHandle = fopen($file, 'w');
else
{
$logFileParts = pathinfo($file);
$i = 1;
while (file_exists($logFileParts['dirname'].'/'.$logFileParts['filename'].$i.(isset($logFileParts['extension']) ? '.'.$logFileParts['extension'] : '')))
$i++;
$file = $logFileParts['dirname'].'/'.$logFileParts['filename'].$i.(isset($logFileParts['extension']) ? '.'.$logFileParts['extension'] : '');
self::$logHandle = fopen($file, 'w');
}
}
private static function tblHead(string $str) : string
{
return CLI_HAS_E ? "\e[1;48;5;236m".$str."\e[0m" : $str;
}
private static function tblDelim(string $str) : string
{
return CLI_HAS_E ? "\e[48;5;236m".$str."\e[0m" : $str;
}
public static function grey(string $str) : string
{
return CLI_HAS_E ? "\e[90m".$str."\e[0m" : $str;
}
public static function red(string $str) : string
{
return CLI_HAS_E ? "\e[31m".$str."\e[0m" : $str;
}
public static function green(string $str) : string
{
return CLI_HAS_E ? "\e[32m".$str."\e[0m" : $str;
}
public static function yellow(string $str) : string
{
return CLI_HAS_E ? "\e[33m".$str."\e[0m" : $str;
}
public static function blue(string $str) : string
{
return CLI_HAS_E ? "\e[36m".$str."\e[0m" : $str;
}
public static function bold(string $str) : string
{
return CLI_HAS_E ? "\e[1m".$str."\e[0m" : $str;
}
public static function write(string $txt = '', int $lvl = self::LOG_BLANK, bool $timestamp = true, bool $tmpRow = false) : void
{
$msg = '';
if ($txt)
{
if ($timestamp)
$msg = str_pad(date('H:i:s'), 10);
switch ($lvl)
{
case self::LOG_ERROR: // red critical error
$msg .= '['.self::red('ERR').'] ';
break;
case self::LOG_WARN: // yellow notice
$msg .= '['.self::yellow('WARN').'] ';
break;
case self::LOG_OK: // green success
$msg .= '['.self::green('OK').'] ';
break;
case self::LOG_INFO: // blue info
$msg .= '['.self::blue('INFO').'] ';
break;
case self::LOG_BLANK:
$msg .= ' ';
break;
}
$msg .= $txt;
}
// https://shiroyasha.svbtle.com/escape-sequences-a-quick-guide-1#movement_1
$msg = (self::$overwriteLast && CLI_HAS_E ? "\e[1G\e[0K" : "\n") . $msg;
self::$overwriteLast = $tmpRow;
fwrite($lvl == self::LOG_ERROR ? STDERR : STDOUT, $msg);
if (self::$logHandle) // remove control sequences from log
fwrite(self::$logHandle, self::purgeEscapes($msg));
flush();
}
public static function logLevelFromE(int $phpError) : int
{
if ($phpError & (E_ERROR | E_PARSE | E_CORE_ERROR | E_COMPILE_ERROR | E_USER_ERROR | E_RECOVERABLE_ERROR))
return self::LOG_ERROR;
if ($phpError & (E_WARNING | E_USER_WARNING | E_NOTICE | E_USER_NOTICE | E_CORE_WARNING | E_COMPILE_WARNING))
return self::LOG_WARN;
if ($phpError & (E_STRICT | E_NOTICE | E_USER_NOTICE | E_DEPRECATED | E_USER_DEPRECATED))
return self::LOG_INFO;
return self::LOG_BLANK;
}
private static function purgeEscapes(string $msg) : string
{
return preg_replace(["/\e\[[\d;]+[mK]/", "/\e\[\d+G/"], ['', "\n"], $msg);
}
public static function nicePath(string $fileOrPath, string ...$pathParts) : string
{
$path = '';
if ($pathParts)
{
foreach ($pathParts as &$pp)
$pp = trim($pp);
$path .= implode(DIRECTORY_SEPARATOR, $pathParts);
}
$path .= ($path ? DIRECTORY_SEPARATOR : '').trim($fileOrPath);
// remove double quotes (from erronous user input), single quotes are
// valid chars for filenames and removing those mutilates several wow icons
$path = str_replace('"', '', $path);
if (!$path) // empty strings given. (faulty dbc data?)
return '';
if (DIRECTORY_SEPARATOR == '/') // *nix
{
$path = str_replace('\\', '/', $path);
$path = preg_replace('/\/+/i', '/', $path);
}
else if (DIRECTORY_SEPARATOR == '\\') // win
{
$path = str_replace('/', '\\', $path);
$path = preg_replace('/\\\\+/i', '\\', $path);
}
else
CLI::write('Dafuq! Your directory separator is "'.DIRECTORY_SEPARATOR.'". Please report this!', CLI::LOG_ERROR);
// resolve *nix home shorthand
if (!OS_WIN)
{
if (preg_match('/^~(\w+)\/.*/i', $path, $m))
$path = '/home/'.substr($path, 1);
else if (substr($path, 0, 2) == '~/')
$path = getenv('HOME').substr($path, 1);
else if ($path[0] == DIRECTORY_SEPARATOR && substr($path, 0, 6) != '/home/')
$path = substr($path, 1);
}
return $path;
}
/**************/
/* read input */
/**************/
/*
since the CLI on WIN ist not interactive, the following things have to be considered
you do not receive keystrokes but whole strings upon pressing <Enter> (wich also appends a \r)
as such <ESC> and probably other control chars can not be registered
this also means, you can't hide input at all, least process it
*/
public static function read(array $fields, ?array &$userInput = []) : bool
{
// first time set
if (self::$hasReadline === null)
self::$hasReadline = function_exists('readline_callback_handler_install');
// prevent default output if able
if (self::$hasReadline)
readline_callback_handler_install('', function() { });
if (!STDIN)
return false;
stream_set_blocking(STDIN, false);
// pad default values onto $fields
array_walk($fields, function(&$val, $_, $pad) { $val += $pad; }, ['', false, false, '']);
foreach ($fields as $name => [$desc, $isHidden, $singleChar, $validPattern])
{
$charBuff = '';
if ($desc)
fwrite(STDOUT, "\n".$desc.": ");
while (true) {
if (feof(STDIN))
return false;
$r = [STDIN];
$w = $e = null;
$n = stream_select($r, $w, $e, 200000);
if (!$n || !in_array(STDIN, $r))
continue;
// stream_get_contents is always blocking under WIN - fgets should work similary as php always receives a terminated line of text
$chars = str_split(OS_WIN ? fgets(STDIN) : stream_get_contents(STDIN));
$ordinals = array_map('ord', $chars);
if ($ordinals[0] == self::CHR_ESC)
{
if (count($ordinals) == 1)
{
fwrite(STDOUT, chr(self::CHR_BELL));
return false;
}
else
continue;
}
foreach ($chars as $idx => $char)
{
$keyId = $ordinals[$idx];
// skip char if horizontal tab or \r if followed by \n
if ($keyId == self::CHR_TAB || ($keyId == self::CHR_CR && ($ordinals[$idx + 1] ?? '') == self::CHR_LF))
continue;
if ($keyId == self::CHR_BACKSPACE)
{
if (!$charBuff)
continue 2;
$charBuff = mb_substr($charBuff, 0, -1);
if (!$isHidden && self::$hasReadline)
fwrite(STDOUT, chr(self::CHR_BACK)." ".chr(self::CHR_BACK));
}
// standalone \n or \r
else if ($keyId == self::CHR_LF || $keyId == self::CHR_CR)
{
$userInput[$name] = $charBuff;
break 2;
}
else if (!$validPattern || preg_match($validPattern, $char))
{
$charBuff .= $char;
if (!$isHidden && self::$hasReadline)
fwrite(STDOUT, $char);
if ($singleChar && self::$hasReadline)
{
$userInput[$name] = $charBuff;
break 2;
}
}
}
}
}
fwrite(STDOUT, chr(self::CHR_BELL));
foreach ($userInput as $ui)
if (strlen($ui))
return true;
$userInput = null;
return true;
}
}
class Timer
{
private $t_cur = 0;
private $t_new = 0;
private $intv = 0;
public function __construct(int $intervall)
{
$this->intv = $intervall / 1000; // in msec
$this->t_cur = microtime(true);
}
public function update() : bool
{
$this->t_new = microtime(true);
if ($this->t_new > $this->t_cur + $this->intv)
{
$this->t_cur = $this->t_cur + $this->intv;
return true;
}
return false;
}
public function reset() : void
{
$this->t_cur = microtime(true) - $this->intv;
}
}
abstract class Util
{
@@ -573,7 +182,7 @@ abstract class Util
public static $tcEncoding = '0zMcmVokRsaqbdrfwihuGINALpTjnyxtgevElBCDFHJKOPQSUWXYZ123456789';
private static $notes = [];
public static function addNote(string $note, int $uGroupMask = U_GROUP_EMPLOYEE, int $level = CLI::LOG_ERROR) : void
public static function addNote(string $note, int $uGroupMask = U_GROUP_EMPLOYEE, int $level = LOG_LEVEL_ERROR) : void
{
self::$notes[] = [$note, $uGroupMask, $level];
}
@@ -581,7 +190,7 @@ abstract class Util
public static function getNotes() : array
{
$notes = [];
$severity = CLI::LOG_INFO;
$severity = LOG_LEVEL_INFO;
foreach (self::$notes as [$note, $uGroup, $level])
{
if ($uGroup && !User::isInGroup($uGroup))
@@ -1581,7 +1190,7 @@ abstract class Util
public static function buildPosFixMenu(int $mapId, float $posX, float $posY, int $type, int $guid, int $parentArea = 0, int $parentFloor = 0) : array
{
$points = Game::worldPosToZonePos($mapId, $posX, $posY);
$points = WorldPosition::toZonePos($mapId, $posX, $posY);
if (!$points || count($points) < 2)
return [];
@@ -1620,490 +1229,4 @@ abstract class Util
}
}
abstract class Type
{
public const NPC = 1;
public const OBJECT = 2;
public const ITEM = 3;
public const ITEMSET = 4;
public const QUEST = 5;
public const SPELL = 6;
public const ZONE = 7;
public const FACTION = 8;
public const PET = 9;
public const ACHIEVEMENT = 10;
public const TITLE = 11;
public const WORLDEVENT = 12;
public const CHR_CLASS = 13;
public const CHR_RACE = 14;
public const SKILL = 15;
public const STATISTIC = 16;
public const CURRENCY = 17;
// PROJECT = 18;
public const SOUND = 19;
// BUILDING = 20;
// FOLLOWER = 21;
// MISSION_ABILITY = 22;
// MISSION = 23;
// SHIP = 25;
// THREAT = 26;
// RESOURCE = 27;
// CHAMPION = 28;
public const ICON = 29;
// ORDER_ADVANCEMENT = 30;
// FOLLOWER_ALLIANCE = 31;
// FOLLOWER_HORDE = 32;
// SHIP_ALLIANCE = 33;
// SHIP_HORDE = 34;
// CHAMPION_ALLIANCE = 35;
// CHAMPION_HORDE = 36;
// TRANSMOG_ITEM = 37;
// BFA_CHAMPION = 38;
// BFA_CHAMPION_ALLIANCE = 39;
// AFFIX = 40;
// BFA_CHAMPION_HORDE = 41;
// AZERITE_ESSENCE_POWER = 42;
// AZERITE_ESSENCE = 43;
// STORYLINE = 44;
// ADVENTURE_COMBATANT_ABILITY = 46;
// ENCOUNTER = 47;
// COVENANT = 48;
// SOULBIND = 49;
// DI_ITEM = 50;
// GATHERER_SCREENSHOT = 91;
// GATHERER_GUIDE_IMAGE = 98;
public const PROFILE = 100;
// our own things
public const GUILD = 101;
// TRANSMOG_SET = 101; // future conflict inc.
public const ARENA_TEAM = 102;
// OUTFIT = 110;
// GEAR_SET = 111;
// GATHERER_LISTVIEW = 158;
// GATHERER_SURVEY_COVENANTS = 161;
// NEWS_POST = 162;
// BATTLE_PET_ABILITY = 200;
public const GUIDE = 300; // should have been 100, but conflicts with old version of Profile/List
public const USER = 500;
public const EMOTE = 501;
public const ENCHANTMENT = 502;
public const AREATRIGGER = 503;
public const MAIL = 504;
// Blizzard API things
// MOUNT = -1000;
// RECIPE = -1001;
// BATTLE_PET = -1002;
public const FLAG_NONE = 0x0;
public const FLAG_RANDOM_SEARCHABLE = 0x1;
/* public const FLAG_SEARCHABLE = 0x2 general search? */
public const IDX_LIST_OBJ = 0;
public const IDX_FILE_STR = 1;
public const IDX_JSG_TPL = 2;
public const IDX_FLAGS = 3;
private static array $data = array(
self::NPC => [__NAMESPACE__ . '\CreatureList', 'npc', 'g_npcs', 0x1],
self::OBJECT => [__NAMESPACE__ . '\GameObjectList', 'object', 'g_objects', 0x1],
self::ITEM => [__NAMESPACE__ . '\ItemList', 'item', 'g_items', 0x1],
self::ITEMSET => [__NAMESPACE__ . '\ItemsetList', 'itemset', 'g_itemsets', 0x1],
self::QUEST => [__NAMESPACE__ . '\QuestList', 'quest', 'g_quests', 0x1],
self::SPELL => [__NAMESPACE__ . '\SpellList', 'spell', 'g_spells', 0x1],
self::ZONE => [__NAMESPACE__ . '\ZoneList', 'zone', 'g_gatheredzones', 0x1],
self::FACTION => [__NAMESPACE__ . '\FactionList', 'faction', 'g_factions', 0x1],
self::PET => [__NAMESPACE__ . '\PetList', 'pet', 'g_pets', 0x1],
self::ACHIEVEMENT => [__NAMESPACE__ . '\AchievementList', 'achievement', 'g_achievements', 0x1],
self::TITLE => [__NAMESPACE__ . '\TitleList', 'title', 'g_titles', 0x1],
self::WORLDEVENT => [__NAMESPACE__ . '\WorldEventList', 'event', 'g_holidays', 0x1],
self::CHR_CLASS => [__NAMESPACE__ . '\CharClassList', 'class', 'g_classes', 0x1],
self::CHR_RACE => [__NAMESPACE__ . '\CharRaceList', 'race', 'g_races', 0x1],
self::SKILL => [__NAMESPACE__ . '\SkillList', 'skill', 'g_skills', 0x1],
self::STATISTIC => [__NAMESPACE__ . '\AchievementList', 'achievement', 'g_achievements', 0x0], // alias for achievements; exists only for Markup
self::CURRENCY => [__NAMESPACE__ . '\CurrencyList', 'currency', 'g_gatheredcurrencies',0x1],
self::SOUND => [__NAMESPACE__ . '\SoundList', 'sound', 'g_sounds', 0x1],
self::ICON => [__NAMESPACE__ . '\IconList', 'icon', 'g_icons', 0x1],
self::GUIDE => [__NAMESPACE__ . '\GuideList', 'guide', '', 0x0],
self::PROFILE => [__NAMESPACE__ . '\ProfileList', '', '', 0x0], // x - not known in javascript
self::GUILD => [__NAMESPACE__ . '\GuildList', '', '', 0x0], // x
self::ARENA_TEAM => [__NAMESPACE__ . '\ArenaTeamList', '', '', 0x0], // x
self::USER => [__NAMESPACE__ . '\UserList', 'user', 'g_users', 0x0], // x
self::EMOTE => [__NAMESPACE__ . '\EmoteList', 'emote', 'g_emotes', 0x1],
self::ENCHANTMENT => [__NAMESPACE__ . '\EnchantmentList', 'enchantment', 'g_enchantments', 0x1],
self::AREATRIGGER => [__NAMESPACE__ . '\AreatriggerList', 'areatrigger', '', 0x0],
self::MAIL => [__NAMESPACE__ . '\MailList', 'mail', '', 0x1]
);
/********************/
/* Field Operations */
/********************/
public static function newList(int $type, array $conditions = []) : ?BaseType
{
if (!self::exists($type))
return null;
return new (self::$data[$type][self::IDX_LIST_OBJ])($conditions);
}
public static function getFileString(int $type) : string
{
if (!self::exists($type))
return '';
return self::$data[$type][self::IDX_FILE_STR];
}
public static function getJSGlobalString(int $type) : string
{
if (!self::exists($type))
return '';
return self::$data[$type][self::IDX_JSG_TPL];
}
public static function getJSGlobalTemplate(int $type) : array
{
if (!self::exists($type) || !self::$data[$type][self::IDX_JSG_TPL])
return [];
// [key, [data], [extraData]]
return [self::$data[$type][self::IDX_JSG_TPL], [], []];
}
public static function checkClassAttrib(int $type, string $attr, ?int $attrVal = null) : bool
{
if (!self::exists($type))
return false;
return isset((self::$data[$type][self::IDX_LIST_OBJ])::$$attr) && ($attrVal === null || ((self::$data[$type][self::IDX_LIST_OBJ])::$$attr & $attrVal));
}
public static function getClassAttrib(int $type, string $attr) : mixed
{
if (!self::exists($type))
return null;
return (self::$data[$type][self::IDX_LIST_OBJ])::$$attr ?? null;
}
public static function exists(int $type) : bool
{
return !empty(self::$data[$type]);
}
public static function getIndexFrom(int $idx, string $match) : int
{
$i = array_search($match, array_column(self::$data, $idx));
if ($i === false)
return 0;
return array_keys(self::$data)[$i];
}
/*********************/
/* Column Operations */
/*********************/
public static function getClassesFor(int $flags = 0x0, string $attr = '', ?int $attrVal = null) : array
{
$x = [];
foreach (self::$data as $k => [$o, , , $f])
if ($o && (!$flags || $flags & $f))
if (!$attr || self::checkClassAttrib($k, $attr, $attrVal))
$x[$k] = $o;
return $x;
}
public static function getFileStringsFor(int $flags = 0x0) : array
{
$x = [];
foreach (self::$data as $k => [, $s, , $f])
if ($s && (!$flags || $flags & $f))
$x[$k] = $s;
return $x;
}
public static function getJSGTemplatesFor(int $flags = 0x0) : array
{
$x = [];
foreach (self::$data as $k => [, , $a, $f])
if ($a && (!$flags || $flags & $f))
$x[$k] = $a;
return $x;
}
}
class Report
{
public const MODE_GENERAL = 0;
public const MODE_COMMENT = 1;
public const MODE_FORUM_POST = 2;
public const MODE_SCREENSHOT = 3;
public const MODE_CHARACTER = 4;
public const MODE_VIDEO = 5;
public const MODE_GUIDE = 6;
public const GEN_FEEDBACK = 1;
public const GEN_BUG_REPORT = 2;
public const GEN_TYPO_TRANSLATION = 3;
public const GEN_OP_ADVERTISING = 4;
public const GEN_OP_PARTNERSHIP = 5;
public const GEN_PRESS_INQUIRY = 6;
public const GEN_MISCELLANEOUS = 7;
public const GEN_MISINFORMATION = 8;
public const CO_ADVERTISING = 15;
public const CO_INACCURATE = 16;
public const CO_OUT_OF_DATE = 17;
public const CO_SPAM = 18;
public const CO_INAPPROPRIATE = 19;
public const CO_MISCELLANEOUS = 20;
public const FO_ADVERTISING = 30;
public const FO_AVATAR = 31;
public const FO_INACCURATE = 32;
public const FO_OUT_OF_DATE = 33;
public const FO_SPAM = 34;
public const FO_STICKY_REQUEST = 35;
public const FO_INAPPROPRIATE = 36;
public const FO_MISCELLANEOUS = 37;
public const SS_INACCURATE = 45;
public const SS_OUT_OF_DATE = 46;
public const SS_INAPPROPRIATE = 47;
public const SS_MISCELLANEOUS = 48;
public const PR_INACCURATE_DATA = 60;
public const PR_MISCELLANEOUS = 61;
public const VI_INACCURATE = 45;
public const VI_OUT_OF_DATE = 46;
public const VI_INAPPROPRIATE = 47;
public const VI_MISCELLANEOUS = 48;
public const AR_INACCURATE = 45;
public const AR_OUT_OF_DATE = 46;
public const AR_MISCELLANEOUS = 48;
private array $context = array(
self::MODE_GENERAL => array(
self::GEN_FEEDBACK => true,
self::GEN_BUG_REPORT => true,
self::GEN_TYPO_TRANSLATION => true,
self::GEN_OP_ADVERTISING => true,
self::GEN_OP_PARTNERSHIP => true,
self::GEN_PRESS_INQUIRY => true,
self::GEN_MISCELLANEOUS => true,
self::GEN_MISINFORMATION => true
),
self::MODE_COMMENT => array(
self::CO_ADVERTISING => U_GROUP_MODERATOR,
self::CO_INACCURATE => true,
self::CO_OUT_OF_DATE => true,
self::CO_SPAM => U_GROUP_MODERATOR,
self::CO_INAPPROPRIATE => U_GROUP_MODERATOR,
self::CO_MISCELLANEOUS => U_GROUP_MODERATOR
),
self::MODE_FORUM_POST => array(
self::FO_ADVERTISING => U_GROUP_MODERATOR,
self::FO_AVATAR => true,
self::FO_INACCURATE => true,
self::FO_OUT_OF_DATE => U_GROUP_MODERATOR,
self::FO_SPAM => U_GROUP_MODERATOR,
self::FO_STICKY_REQUEST => U_GROUP_MODERATOR,
self::FO_INAPPROPRIATE => U_GROUP_MODERATOR
),
self::MODE_SCREENSHOT => array(
self::SS_INACCURATE => true,
self::SS_OUT_OF_DATE => true,
self::SS_INAPPROPRIATE => U_GROUP_MODERATOR,
self::SS_MISCELLANEOUS => U_GROUP_MODERATOR
),
self::MODE_CHARACTER => array(
self::PR_INACCURATE_DATA => true,
self::PR_MISCELLANEOUS => true
),
self::MODE_VIDEO => array(
self::VI_INACCURATE => true,
self::VI_OUT_OF_DATE => true,
self::VI_INAPPROPRIATE => U_GROUP_MODERATOR,
self::VI_MISCELLANEOUS => U_GROUP_MODERATOR
),
self::MODE_GUIDE => array(
self::AR_INACCURATE => true,
self::AR_OUT_OF_DATE => true,
self::AR_MISCELLANEOUS => true
)
);
private const ERR_NONE = 0; // aka: success
private const ERR_INVALID_CAPTCHA = 1; // captcha not in use
private const ERR_DESC_TOO_LONG = 2;
private const ERR_NO_DESC = 3;
private const ERR_ALREADY_REPORTED = 7;
private const ERR_MISCELLANEOUS = -1;
public const STATUS_OPEN = 0;
public const STATUS_ASSIGNED = 1;
public const STATUS_CLOSED_WONTFIX = 2;
public const STATUS_CLOSED_SOLVED = 3;
private int $errorCode = self::ERR_NONE;
public function __construct(private int $mode, private int $reason, private ?int $subject = 0)
{
if ($mode < 0 || $reason <= 0)
{
trigger_error('Report - malformed contact request received', E_USER_ERROR);
$this->errorCode = self::ERR_MISCELLANEOUS;
return;
}
if (!isset($this->context[$mode][$reason]))
{
trigger_error('Report - report has invalid context (mode:'.$mode.' / reason:'.$reason.')', E_USER_ERROR);
$this->errorCode = self::ERR_MISCELLANEOUS;
return;
}
if (!User::$id && !User::$ip)
{
trigger_error('Report - could not determine IP for anonymous user', E_USER_ERROR);
$this->errorCode = self::ERR_MISCELLANEOUS;
return;
}
$this->subject ??= 0; // 0 for utility, tools and misc pages?
}
private function checkTargetContext() : int
{
// check already reported
$field = User::$id ? 'userId' : 'ip';
if (DB::Aowow()->selectCell('SELECT 1 FROM ?_reports WHERE `mode` = ?d AND `reason`= ?d AND `subject` = ?d AND ?# = ?', $this->mode, $this->reason, $this->subject, $field, User::$id ?: User::$ip))
return self::ERR_ALREADY_REPORTED;
// check targeted post/postOwner staff status
$ctxCheck = $this->context[$this->mode][$this->reason];
if (is_int($ctxCheck))
{
$roles = User::$groups;
if ($this->mode == self::MODE_COMMENT)
$roles = DB::Aowow()->selectCell('SELECT `roles` FROM ?_comments WHERE `id` = ?d', $this->subject);
// else if if ($this->mode == self::MODE_FORUM_POST)
// $roles = DB::Aowow()->selectCell('SELECT `roles` FROM ?_forum_posts WHERE `id` = ?d', $this->subject);
return $roles & $ctxCheck ? self::ERR_NONE : self::ERR_MISCELLANEOUS;
}
else
return $ctxCheck ? self::ERR_NONE : self::ERR_MISCELLANEOUS;
// Forum not in use, else:
// check post owner
// User::$id == post.op && !post.sticky;
// check user custom avatar
// g_users[post.user].avatar == 2 && (post.roles & U_GROUP_MODERATOR) == 0
}
public function create(string $desc, ?string $userAgent = null, ?string $appName = null, ?string $pageUrl = null, ?string $relUrl = null, ?string $email = null) : bool
{
if ($this->errorCode)
return false;
if (!$desc)
{
$this->errorCode = self::ERR_NO_DESC;
return false;
}
if (mb_strlen($desc) > 500)
{
$this->errorCode = self::ERR_DESC_TOO_LONG;
return false;
}
if($err = $this->checkTargetContext())
{
$this->errorCode = $err;
return false;
}
$update = array(
'userId' => User::$id,
'createDate' => time(),
'mode' => $this->mode,
'reason' => $this->reason,
'subject' => $this->subject,
'ip' => User::$ip,
'description' => $desc,
'userAgent' => $userAgent ?: $_SERVER['HTTP_USER_AGENT'],
'appName' => $appName ?: (get_browser(null, true)['browser'] ?: '')
);
if ($pageUrl)
$update['url'] = $pageUrl;
if ($relUrl)
$update['relatedurl'] = $relUrl;
if ($email)
$update['email'] = $email;
return DB::Aowow()->query('INSERT INTO ?_reports (?#) VALUES (?a)', array_keys($update), array_values($update));
}
public function getSimilar(int ...$status) : array
{
if ($this->errorCode)
return [];
foreach ($status as &$s)
if ($s < self::STATUS_OPEN || $s > self::STATUS_CLOSED_SOLVED)
unset($s);
return DB::Aowow()->select('SELECT `id` AS ARRAY_KEY, r.* FROM ?_reports r WHERE {`status` IN (?a) AND }`mode` = ?d AND `reason` = ?d AND `subject` = ?d',
$status ?: DBSIMPLE_SKIP, $this->mode, $this->reason, $this->subject);
}
public function close(int $closeStatus, bool $inclAssigned = false) : bool
{
if ($closeStatus != self::STATUS_CLOSED_SOLVED && $closeStatus != self::STATUS_CLOSED_WONTFIX)
return false;
if (!User::isInGroup(U_GROUP_ADMIN | U_GROUP_BUREAU | U_GROUP_MOD))
return false;
$fromStatus = [self::STATUS_OPEN];
if ($inclAssigned)
$fromStatus[] = self::STATUS_ASSIGNED;
if ($reports = DB::Aowow()->selectCol('SELECT `id` AS ARRAY_KEY, `userId` FROM ?_reports WHERE `status` IN (?a) AND `mode` = ?d AND `reason` = ?d AND `subject` = ?d',
$fromStatus, $this->mode, $this->reason, $this->subject))
{
DB::Aowow()->query('UPDATE ?_reports SET `status` = ?d, `assigned` = 0 WHERE `id` IN (?a)', $closeStatus, array_keys($reports));
foreach ($reports as $rId => $uId)
Util::gainSiteReputation($uId, $closeStatus == self::STATUS_CLOSED_SOLVED ? SITEREP_ACTION_GOOD_REPORT : SITEREP_ACTION_BAD_REPORT, ['id' => $rId]);
return true;
}
return false;
}
public function reopen(int $assignedTo = 0) : bool
{
// assignedTo = 0 ? status = STATUS_OPEN : status = STATUS_ASSIGNED, userId = assignedTo
return false;
}
public function getError() : int
{
return $this->errorCode;
}
}
?>