Merge branch 'develop' into feature/refactor-account-lost

This commit is contained in:
slawkens
2026-01-16 23:18:34 +01:00
18 changed files with 219 additions and 32 deletions

View File

@@ -1,9 +1,9 @@
name: Cypress
on:
pull_request:
branches: [main]
branches: [develop]
push:
branches: [main]
branches: [develop]
jobs:
cypress:

View File

@@ -1,9 +1,9 @@
name: PHP Linting
on:
pull_request:
branches: [main]
branches: [develop]
push:
branches: [main]
branches: [develop]
jobs:
phplint:

View File

@@ -2,9 +2,9 @@ name: "PHPStan"
on:
pull_request:
branches: [main]
branches: [develop]
push:
branches: [main]
branches: [develop]
jobs:
tests:

View File

@@ -1,9 +1,12 @@
## [2.0-dev - x.x.2025]
### Added
* Add an "access" option to Menus (#340)
* Possibility to hide menus for unauthorized users
* Add the possibility to fetch skills in the getTopPlayers function
### Changed
* Better handling of vocations: (#345)
* Load from vocations.xml (No need to manually set)
* Support for Monk vocation
* Add an access option to Menus (#340)
* Possibility to hide menus for unauthorized users
* Reworked account action logs to use a single IP column as varchar(45) for both ipv4 and ipv6 (#289)

View File

@@ -631,6 +631,7 @@ else if (isset($_REQUEST['search'])) {
</div>
</form>
</div>
<?php if (USE_ACCOUNT_NAME): ?>
<div class="col-6 col-lg-12">
<form action="<?php echo $admin_base; ?>" method="post">
<?php csrf(); ?>
@@ -641,6 +642,7 @@ else if (isset($_REQUEST['search'])) {
</div>
</form>
</div>
<?php endif; ?>
<div class="col-6 col-lg-12">
<form action="<?php echo $admin_base; ?>" method="post">
<?php csrf(); ?>

View File

@@ -10,6 +10,7 @@
use MyAAC\Forum;
use MyAAC\Models\Player;
use MyAAC\Server\XML\Vocations;
defined('MYAAC') or die('Direct access not allowed!');
@@ -237,7 +238,30 @@ else if (isset($_REQUEST['search'])) {
$player->setGroup($groups->getGroup($group));
$player->setLevel($level);
$player->setExperience($experience);
if ($db->hasColumn('players', 'promotion')) {
$promotion = 0;
$vocationOriginal = Vocations::getOriginal($vocation);
if ($vocation != $vocationOriginal) {
$tmpId = $vocationOriginal;
while($promoted = Vocations::getPromoted($tmpId)) {
$promotion++;
$tmpId = $promoted;
if ($promoted == $vocation) {
break;
}
}
$vocation = $vocationOriginal;
}
$player->setPromotion($promotion);
}
$player->setVocation($vocation);
$player->setHealth($health);
$player->setHealthMax($health_max);
$player->setMagLevel($magic_level);

View File

@@ -17,6 +17,8 @@ use MyAAC\Models\Guild;
use MyAAC\Models\House;
use MyAAC\Models\Pages;
use MyAAC\Models\Player;
use MyAAC\Models\PlayerDeath;
use MyAAC\Models\PlayerKillers;
use MyAAC\News;
use MyAAC\Plugins;
use MyAAC\Settings;
@@ -878,11 +880,12 @@ function getWorldName($id)
*
* @param string $to Recipient email address.
* @param string $subject Subject of the message.
* @param string $body Message body in html format.
* @param string $body Message body in HTML format.
* @param string $altBody Alternative message body, plain text.
* @return bool PHPMailer status returned (success/failure).
* @throws \PHPMailer\PHPMailer\Exception
*/
function _mail($to, $subject, $body, $altBody = '', $add_html_tags = true)
function _mail(string $to, string $subject, string $body, string $altBody = ''): bool
{
global $mailer, $config;
@@ -900,12 +903,6 @@ function _mail($to, $subject, $body, $altBody = '', $add_html_tags = true)
$mailer->clearAllRecipients();
}
$signature_html = setting('core.mail_signature_html');
if($add_html_tags && isset($body[0]))
$tmp_body = '<html><head></head><body>' . $body . '<br/><br/>' . $signature_html . '</body></html>';
else
$tmp_body = $body . '<br/><br/>' . $signature_html;
$mailOption = setting('core.mail_option');
if($mailOption == MAIL_SMTP)
{
@@ -932,6 +929,9 @@ function _mail($to, $subject, $body, $altBody = '', $add_html_tags = true)
$mailer->isMail();
}
$signature_html = setting('core.mail_signature_html');
$tmp_body = $body . '<br/><br/>' . $signature_html;
$mailer->isHTML(isset($body[0]) > 0);
$mailer->From = setting('core.mail_address');
$mailer->Sender = setting('core.mail_address');
@@ -1135,11 +1135,44 @@ function csrfProtect(): void
}
}
function getTopPlayers($limit = 5, $skill = 'level') {
function getSkillIdByName(string $name): int|null
{
$skills = [
'level' => POT::SKILL_LEVEL,
'experience' => POT::SKILL_LEVEL,
'magic' => POT::SKILL_MAGIC,
'maglevel' => POT::SKILL_MAGIC,
'balance' => SKILL_BALANCE,
'frags' => SKILL_FRAGS,
'club' => POT::SKILL_CLUB,
'sword' => POT::SKILL_SWORD,
'axe' => POT::SKILL_AXE,
'dist' => POT::SKILL_DIST,
'distance' => POT::SKILL_DIST,
'shield' => POT::SKILL_SHIELD,
'shielding' => POT::SKILL_SHIELD,
'fish' => POT::SKILL_FISH,
'fishing' => POT::SKILL_FISH,
];
return $skills[$name] ?? null;
}
function getTopPlayers($limit = 5, $skill = POT::SKILL_LEVEL)
{
global $db;
if ($skill === 'level') {
$skill = 'experience';
$skillOriginal = $skill;
if (is_string($skill)) {
$skill = getSkillIdByName($skill);
}
if (!is_numeric($skill)) {
throw new RuntimeException("getTopPlayers: Invalid skill: $skillOriginal");
}
return Cache::remember("top_{$limit}_{$skill}", 2 * 60, function () use ($db, $limit, $skill) {
@@ -1160,15 +1193,64 @@ function getTopPlayers($limit = 5, $skill = 'level') {
$columns[] = 'lookmount';
}
return Player::query()
$query = Player::query()
->select($columns)
->withOnlineStatus()
->notDeleted()
->where('group_id', '<', setting('core.highscores_groups_hidden'))
->whereNotIn('id', setting('core.highscores_ids_hidden'))
->where('account_id', '!=', 1)
->orderByDesc($skill)
->limit($limit)
->orderByDesc('value');
if ($limit > 0) {
$query->limit($limit);
}
if ($skill >= POT::SKILL_FIRST && $skill <= POT::SKILL_LAST) { // skills
if ($db->hasColumn('players', 'skill_fist')) {// tfs 1.0
$skill_ids = array(
POT::SKILL_FIST => 'skill_fist',
POT::SKILL_CLUB => 'skill_club',
POT::SKILL_SWORD => 'skill_sword',
POT::SKILL_AXE => 'skill_axe',
POT::SKILL_DIST => 'skill_dist',
POT::SKILL_SHIELD => 'skill_shielding',
POT::SKILL_FISH => 'skill_fishing',
);
$query
->addSelect($skill_ids[$skill] . ' as value')
->orderByDesc($skill_ids[$skill] . '_tries');
} else {
$query
->join('player_skills', 'player_skills.player_id', '=', 'players.id')
->where('skillid', $skill)
->addSelect('player_skills.value as value');
}
} else if ($skill == SKILL_FRAGS) // frags
{
if ($db->hasTable('player_killers')) {
$query->addSelect(['value' => PlayerKillers::whereColumn('player_killers.player_id', 'players.id')->selectRaw('COUNT(*)')]);
} else {
$query->addSelect(['value' => PlayerDeath::unjustified()->whereColumn('player_deaths.killed_by', 'players.name')->selectRaw('COUNT(*)')]);
}
} else if ($skill == SKILL_BALANCE) // balance
{
$query
->addSelect('players.balance as value');
} else {
if ($skill == POT::SKILL_MAGIC) {
$query
->addSelect('players.maglevel as value', 'players.maglevel')
->orderByDesc('manaspent');
} else { // level
$query
->addSelect('players.level as value', 'players.experience')
->orderByDesc('experience');
}
}
return $query
->get()
->map(function ($e, $i) {
$row = $e->toArray();

View File

@@ -0,0 +1,29 @@
<?php
/**
* Example of using getTopPlayers() function
* to display the best players for each skill
*/
defined('MYAAC') or die('Direct access not allowed!');
$skills = [
'magic', 'level',
'balance', 'frags',
POT::SKILL_FIST, POT::SKILL_CLUB,
POT::SKILL_SWORD, POT::SKILL_AXE,
POT::SKILL_DISTANCE, POT::SKILL_SHIELD,
POT::SKILL_FISH
];
foreach ($skills as $skill) {?>
<ul>
<?php
echo '<strong>' . ucwords(is_string($skill) ? $skill : getSkillName($skill)) . '</strong>';
foreach (getTopPlayers(5, $skill) as $player) {?>
<li><?= $player['rank'] . '. ' . $player['name'] . ' - ' . $player['value']; ?></li>
<?php
}
?>
</ul>
<?php
}

View File

@@ -51,6 +51,8 @@ if($player_name != null) {
'description' => 'The character information has been changed.'
));
$show_form = false;
$hooks->trigger(HOOK_ACCOUNT_CHARACTERS_CHANGE_COMMENT_AFTER_SUCCESS, ['player' => $player]);
}
}
} else {
@@ -70,9 +72,11 @@ if($show_form) {
}
if(isset($player) && $player) {
$twig->display('account.characters.change-comment.html.twig', array(
'player' => $player->toArray()
));
$_player = $player->toArray();
$_player['id'] = $player->id; // Hack, as it's somehow missing in the toArray() function
$twig->display('account.characters.change-comment.html.twig', [
'player' => $_player,
]);
}
}
?>

View File

@@ -70,7 +70,7 @@ foreach($posts as &$post) {
}
$post['group'] = $groupName;
$post['player_link'] = getPlayerLink($player->getName());
$post['player_link'] = '<a href="' . getPlayerLink($player, false) . '" style="position: relative;">' . $player->getName() . '</a>';
$post['vocation'] = $player->getVocationName();

View File

@@ -319,6 +319,34 @@ return [
},
],
],
/**
* @deprecated
* To be removed in v3.0
*/
'vocations_amount' => [
'hidden' => true,
'type' => 'number',
//'name' => 'Vocations Amount',
//'desc' => 'How many basic vocations your server got (without promotion)',
'default' => 4,
'callbacks' => [
'get' => function () {
return config('vocations_amount');
},
],
],
'vocations' => [
'hidden' => true,
'type' => 'textarea',
//'name' => 'Vocation Names',
//'desc' => 'Separated by comma. Must be in the same order as in vocations.xml, starting with id: 0.',
'default' => 'None, Sorcerer, Druid, Paladin, Knight, Master Sorcerer, Elder Druid,Royal Paladin, Elite Knight',
'callbacks' => [
'get' => function () {
return config('vocations');
},
],
],
[
'type' => 'category',
'title' => 'Database',

View File

@@ -45,7 +45,7 @@ class PHP
rename($tmp, $file);
}
public function get($key): string
public function get($key)
{
$tmp = '';
if ($this->fetch($key, $tmp)) {

View File

@@ -77,7 +77,15 @@ class Vocations
}
public static function getOriginal(int $id): ?int {
return self::$vocationsFrom[$id] ?? null;
while ($tmpId = self::$vocationsFrom[$id]) {
if ($tmpId == $id) {
break;
}
$id = $tmpId;
}
return $id;
}
public static function getBase($includingRook = true): array {

View File

@@ -28,6 +28,10 @@ define('HOOK_CHARACTERS_AFTER_CHARACTERS', ++$i);
define('HOOK_LOGIN', ++$i);
define('HOOK_LOGIN_ATTEMPT', ++$i);
define('HOOK_LOGOUT', ++$i);
define('HOOK_ACCOUNT_CHARACTERS_CHANGE_COMMENT_AFTER_SUCCESS', ++$i);
define('HOOK_ACCOUNT_CHARACTERS_CHANGE_COMMENT_AFTER_NAME', ++$i);
define('HOOK_ACCOUNT_CHARACTERS_CHANGE_COMMENT_AFTER_HIDE_ACCOUNT', ++$i);
define('HOOK_ACCOUNT_CHARACTERS_CHANGE_COMMENT_AFTER_COMMENT', ++$i);
define('HOOK_ACCOUNT_CHANGE_PASSWORD_AFTER_OLD_PASSWORD', ++$i);
define('HOOK_ACCOUNT_CHANGE_PASSWORD_AFTER_NEW_PASSWORD', ++$i);
define('HOOK_ACCOUNT_CHANGE_PASSWORD_POST', ++$i);

View File

@@ -33,6 +33,7 @@ If you do not want to specify a certain field, just leave it blank.<br/><br/>
<td class="LabelV">Name:</td>
<td style="width:80%;" >{{ player.name }}</td>
</tr>
{{ hook('HOOK_ACCOUNT_CHARACTERS_CHANGE_COMMENT_AFTER_NAME') }}
<tr>
<td class="LabelV" >Hide Account:</td>
<td>
@@ -42,6 +43,7 @@ If you do not want to specify a certain field, just leave it blank.<br/><br/>
{% if player.group_id > 1 %} (you will be also hidden on the Team page!){% endif %}
</td>
</tr>
{{ hook('HOOK_ACCOUNT_CHARACTERS_CHANGE_COMMENT_AFTER_HIDE_ACCOUNT') }}
</table>
</div>
</div>
@@ -65,6 +67,7 @@ If you do not want to specify a certain field, just leave it blank.<br/><br/>
<td class="LabelV" ><span>Comment:</span></td>
<td style="width:80%;"><textarea name="comment" rows="10" cols="50" wrap="virtual">{{ player.comment|raw }}</textarea><br>[max. length: 2000 chars, 50 lines (ENTERs)]</td>
</tr>
{{ hook('HOOK_ACCOUNT_CHARACTERS_CHANGE_COMMENT_AFTER_COMMENT') }}
</table>
</div>
</div>

View File

@@ -114,7 +114,7 @@
</tr>
<tr style="background-color: {{ config.darkborder }};" >
<td>Last Login:</td>
<td>{{ "now"|date("j F Y, G:i:s") }}</td>
<td>{{ account_logged.getCustomField('web_lastlogin')|date("j F Y, G:i:s") }}</td>
</tr>
{% autoescape false %}
<tr style="background-color: {{ config.lightborder }};" >

View File

@@ -26,7 +26,7 @@
<tr>
{% for vocationId in baseVocations %}
<td><img src="images/{{ config('vocations')[vocationId]|lower }}.png" width="150" height="200"/></td>
<td><img src="images/{{ config('vocations')[vocationId]|lower }}.png" width="130" height="190"/></td>
{% endfor %}
</tr>

View File

@@ -151,7 +151,7 @@
</tr>
<tr style="background-color: {{ config.darkborder }};" >
<td class="LabelV" >Last Login:</td>
<td>{{ "now"|date("j F Y, G:i:s") }}</td>
<td>{{ account_logged.getCustomField('web_lastlogin')|date("j F Y, G:i:s") }}</td>
</tr>
{% autoescape false %}
<tr style="background-color: {{ config.lightborder }};">