mirror of
https://github.com/Sarjuuk/aowow.git
synced 2025-11-29 15:58:16 +08:00
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:
@@ -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'],
|
||||
|
||||
@@ -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);
|
||||
|
||||
39
includes/components/locstring.class.php
Normal file
39
includes/components/locstring.class.php
Normal 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]' : '';
|
||||
}
|
||||
}
|
||||
|
||||
?>
|
||||
274
includes/components/report.class.php
Normal file
274
includes/components/report.class.php
Normal 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;
|
||||
}
|
||||
}
|
||||
|
||||
?>
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
79
includes/game/chrclass.class.php
Normal file
79
includes/game/chrclass.class.php
Normal 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
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
?>
|
||||
132
includes/game/chrrace.class.php
Normal file
132
includes/game/chrrace.class.php
Normal 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;
|
||||
}
|
||||
}
|
||||
|
||||
?>
|
||||
@@ -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;
|
||||
160
includes/game/worldposition.class.php
Normal file
160
includes/game/worldposition.class.php
Normal 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;
|
||||
}
|
||||
}
|
||||
|
||||
?>
|
||||
@@ -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(...));
|
||||
|
||||
@@ -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]' : '';
|
||||
}
|
||||
}
|
||||
|
||||
?>
|
||||
|
||||
355
includes/setup/cli.class.php
Normal file
355
includes/setup/cli.class.php
Normal 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;
|
||||
}
|
||||
}
|
||||
|
||||
?>
|
||||
39
includes/setup/timer.class.php
Normal file
39
includes/setup/timer.class.php
Normal 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
228
includes/type.class.php
Normal 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;
|
||||
}
|
||||
}
|
||||
|
||||
?>
|
||||
@@ -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)
|
||||
{
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
?>
|
||||
|
||||
Reference in New Issue
Block a user