3 Commits
Author SHA1 Message Date
M_Viper e00deceae1 mc-player-history.php aktualisiert 2026-05-11 06:41:54 +00:00
Git Manager GUI 67f893067e Upload via Git Manager GUI 2026-05-10 15:08:05 +02:00
Git Manager GUI 21164b56c0 Upload via Git Manager GUI 2026-05-05 21:17:12 +02:00
+229 -34
View File
@@ -3,7 +3,7 @@
Plugin Name: MC Player History
Plugin URI: https://git.viper.ipv64.net/M_Viper/MC-Player-History---WordPress-Plugin
Description: Spielerverlauf deines Minecraft Servers.
Version: 1.3.0
Version: 1.4.0
Author: M_Viper
Author URI: https://m-viper.de
Requires at least: 6.8
@@ -214,10 +214,13 @@ function mcph_get_litebans_punishments( $uuid, $username ) {
$bans_table = esc_sql( $prefix . 'bans' );
$mutes_table = esc_sql( $prefix . 'mutes' );
$warns_table = esc_sql( $prefix . 'warnings' );
$kicks_table = esc_sql( $prefix . 'kicks' );
$bans = intval( $lbdb->get_var( $lbdb->prepare( "SELECT COUNT(*) FROM {$bans_table} WHERE uuid = %s", $search_uuid ) ) );
$bans = intval( $lbdb->get_var( $lbdb->prepare( "SELECT COUNT(*) FROM {$bans_table} WHERE uuid = %s", $search_uuid ) ) );
$mutes = intval( $lbdb->get_var( $lbdb->prepare( "SELECT COUNT(*) FROM {$mutes_table} WHERE uuid = %s", $search_uuid ) ) );
$warns = intval( $lbdb->get_var( $lbdb->prepare( "SELECT COUNT(*) FROM {$warns_table} WHERE uuid = %s", $search_uuid ) ) );
// FIX 1: kicks ebenfalls direkt aus LiteBans zählen
$kicks = intval( $lbdb->get_var( $lbdb->prepare( "SELECT COUNT(*) FROM {$kicks_table} WHERE uuid = %s", $search_uuid ) ) );
if ( ! empty( $lbdb->last_error ) ) {
mcph_log( 'LiteBans Query Fehler: ' . $lbdb->last_error );
@@ -229,6 +232,7 @@ function mcph_get_litebans_punishments( $uuid, $username ) {
'bans' => $bans,
'mutes' => $mutes,
'warns' => $warns,
'kicks' => $kicks,
);
return $count_cache[ $cache_key ];
@@ -334,6 +338,7 @@ function mcph_install() {
bans INT NOT NULL DEFAULT 0,
mutes INT NOT NULL DEFAULT 0,
warns INT NOT NULL DEFAULT 0,
kicks INT NOT NULL DEFAULT 0,
PRIMARY KEY (id),
UNIQUE KEY uuid (uuid),
KEY username (username),
@@ -395,6 +400,10 @@ function mcph_add_columns_v130() {
if ( ! $wpdb->get_var( "SHOW COLUMNS FROM $table_name LIKE 'warns'" ) ) {
$wpdb->query( "ALTER TABLE $table_name ADD COLUMN warns INT NOT NULL DEFAULT 0 AFTER mutes" );
}
// FIX 2: kicks Spalte hinzufügen
if ( ! $wpdb->get_var( "SHOW COLUMNS FROM $table_name LIKE 'kicks'" ) ) {
$wpdb->query( "ALTER TABLE $table_name ADD COLUMN kicks INT NOT NULL DEFAULT 0 AFTER warns" );
}
}
/**
@@ -463,6 +472,7 @@ function mcph_sync_from_statusapi() {
$bans = 0;
$mutes = 0;
$warns = 0;
$kicks = 0;
// Primär aus StatusAPI lesen (falls geliefert).
if ( isset( $p->punishments ) && is_object( $p->punishments ) ) {
@@ -471,12 +481,13 @@ function mcph_sync_from_statusapi() {
$warns = isset( $p->punishments->warns ) ? intval( $p->punishments->warns ) : 0;
}
// Falls LiteBans konfiguriert ist, dortige Werte bevorzugen.
// FIX 4: LiteBans bevorzugen (inkl. kicks) — überschreibt StatusAPI-Werte
$litebans_punishments = mcph_get_litebans_punishments( $uuid, $name );
if ( is_array( $litebans_punishments ) ) {
$bans = intval( $litebans_punishments['bans'] ?? 0 );
$bans = intval( $litebans_punishments['bans'] ?? 0 );
$mutes = intval( $litebans_punishments['mutes'] ?? 0 );
$warns = intval( $litebans_punishments['warns'] ?? 0 );
$kicks = intval( $litebans_punishments['kicks'] ?? 0 );
}
if ( empty( $name ) ) continue;
@@ -492,14 +503,18 @@ function mcph_sync_from_statusapi() {
'last_seen' => current_time( 'mysql' ),
'is_online' => 1,
'playtime_seconds' => $playtime,
'kills' => $kills,
'deaths' => $deaths,
'balance' => $balance,
'is_bedrock' => $is_bedrock,
'joins' => $joins,
'bans' => $bans,
'mutes' => $mutes,
'warns' => $warns
'warns' => $warns,
'kicks' => $kicks
);
$update_format = array( '%s', '%s', '%d', '%d', '%s', '%d', '%d', '%d', '%d', '%d' );
// FIX BUG-2 + FIX 5: kills/deaths/kicks im Fallback-UPDATE
$update_format = array( '%s', '%s', '%d', '%d', '%d', '%d', '%s', '%d', '%d', '%d', '%d', '%d', '%d' );
if ( ! empty( $prefix ) ) {
$update_data['prefix'] = $prefix;
@@ -512,11 +527,12 @@ function mcph_sync_from_statusapi() {
// Zeitkonvertierung
$last_seen_time = isset( $p->last_seen ) ? intval( $p->last_seen ) : time();
$last_seen_mysql = date( 'Y-m-d H:i:s', $last_seen_time );
// FIX BUG-3: wp_date() statt date() nutzt WordPress-Timezone (konsistent mit current_time())
$last_seen_mysql = wp_date( 'Y-m-d H:i:s', $last_seen_time );
$first_seen_mysql = current_time('mysql');
if ( isset( $p->first_seen ) && ! $exists ) {
$first_seen_mysql = date( 'Y-m-d H:i:s', intval( $p->first_seen ) );
$first_seen_mysql = wp_date( 'Y-m-d H:i:s', intval( $p->first_seen ) );
}
if ( $exists ) {
@@ -525,14 +541,18 @@ function mcph_sync_from_statusapi() {
'last_seen' => $last_seen_mysql,
'is_online' => 1,
'playtime_seconds' => $playtime,
'kills' => $kills,
'deaths' => $deaths,
'balance' => $balance,
'is_bedrock' => $is_bedrock,
'joins' => $joins,
'bans' => $bans,
'mutes' => $mutes,
'warns' => $warns
'warns' => $warns,
'kicks' => $kicks
);
$update_format = array( '%s', '%s', '%d', '%d', '%s', '%d', '%d', '%d', '%d', '%d' );
// FIX BUG-2 + FIX 5: kills/deaths/kicks werden jetzt in DB gespeichert
$update_format = array( '%s', '%s', '%d', '%d', '%d', '%d', '%s', '%d', '%d', '%d', '%d', '%d', '%d' );
if ( ! empty( $prefix ) ) {
$update_data['prefix'] = $prefix;
@@ -540,6 +560,7 @@ function mcph_sync_from_statusapi() {
}
$wpdb->update( $table_name, $update_data, array( 'uuid' => $uuid ), $update_format, array( '%s' ) );
} else {
// FIX BUG-2: kills/deaths im INSERT mitgespeichert
$wpdb->insert( $table_name, array(
'uuid' => $uuid,
'username' => $name,
@@ -548,13 +569,16 @@ function mcph_sync_from_statusapi() {
'last_seen' => $last_seen_mysql,
'is_online' => 1,
'playtime_seconds' => $playtime,
'kills' => $kills,
'deaths' => $deaths,
'balance' => $balance,
'is_bedrock' => $is_bedrock,
'joins' => $joins,
'bans' => $bans,
'mutes' => $mutes,
'warns' => $warns
), array( '%s', '%s', '%s', '%s', '%s', '%d', '%d', '%s', '%d', '%d', '%d', '%d', '%d' ) );
'warns' => $warns,
'kicks' => $kicks
), array( '%s', '%s', '%s', '%s', '%s', '%d', '%d', '%d', '%d', '%s', '%d', '%d', '%d', '%d', '%d', '%d' ) );
}
}
}
@@ -681,9 +705,24 @@ function mcph_ajax_player_profile() {
}
$is_online = isset( $live_uuids[ $player->uuid ] );
// Live-Daten bevorzugen, falls verfügbar.
// FIX BUG-4: Live-Daten für Online-Spieler bevorzugen (Playtime, Kills, Deaths, Balance).
if ( $matched_live_player ) {
$is_bedrock = (bool) mcph_detect_bedrock_player( $matched_live_player, $player->uuid, $player->username );
// Live-Playtime: enthält laufende Session, immer aktueller als DB-Wert
if ( isset( $matched_live_player->playtime ) ) {
$live_playtime = intval( $matched_live_player->playtime );
if ( $live_playtime >= intval( $player->playtime_seconds ) ) {
$player->playtime_seconds = $live_playtime;
}
}
// Live-Kills/Deaths
if ( isset( $matched_live_player->kills ) ) {
$player->kills = intval( $matched_live_player->kills );
}
if ( isset( $matched_live_player->deaths ) ) {
$player->deaths = intval( $matched_live_player->deaths );
}
// Live-Balance
if ( isset( $matched_live_player->economy ) && is_object( $matched_live_player->economy ) && isset( $matched_live_player->economy->balance ) ) {
$balance_value = floatval( $matched_live_player->economy->balance );
}
@@ -692,6 +731,19 @@ function mcph_ajax_player_profile() {
$kd = ( $player->deaths > 0 ) ? round( $player->kills / $player->deaths, 2 ) : ( $player->kills > 0 ? $player->kills : 0 );
// FIX 6: Bans/Mutes/Warns/Kicks live aus LiteBans holen (immer aktuell)
$bans_val = intval( $player->bans );
$mutes_val = intval( $player->mutes );
$warns_val = intval( $player->warns );
$kicks_val = intval( $player->kicks ?? 0 );
$lb_live = mcph_get_litebans_punishments( $player->uuid, $player->username );
if ( is_array( $lb_live ) ) {
$bans_val = intval( $lb_live['bans'] ?? $bans_val );
$mutes_val = intval( $lb_live['mutes'] ?? $mutes_val );
$warns_val = intval( $lb_live['warns'] ?? $warns_val );
$kicks_val = intval( $lb_live['kicks'] ?? $kicks_val );
}
wp_send_json_success( array(
'uuid' => $player->uuid,
'username' => $player->username,
@@ -706,9 +758,10 @@ function mcph_ajax_player_profile() {
'balance' => $balance_value,
'is_bedrock' => $is_bedrock,
'joins' => intval( $player->joins ),
'bans' => intval( $player->bans ),
'mutes' => intval( $player->mutes ),
'warns' => intval( $player->warns ),
'bans' => $bans_val,
'mutes' => $mutes_val,
'warns' => $warns_val,
'kicks' => $kicks_val,
'forum_banner' => mcph_get_forum_banner( $player->uuid ),
) );
}
@@ -745,6 +798,7 @@ function mcph_generate_player_html( $limit = 500, $only_online = false, $is_ajax
ob_start();
echo '<div class="mc-grid">';
$displayed_count = 0;
$ds = mcph_get_display_settings();
foreach ( $rows as $row ) {
$is_online = $has_live_data ? isset( $live_online_uuids[ $row->uuid ] ) : ( $row->is_online == 1 );
@@ -775,7 +829,7 @@ function mcph_generate_player_html( $limit = 500, $only_online = false, $is_ajax
echo '<div class="mc-player-card" data-uuid="' . esc_attr( $row->uuid ) . '" data-username="' . esc_attr( $row->username ) . '" style="' . $anim_style . '">';
echo '<img src="' . $avatar . '" class="mc-avatar" alt="' . $username . '" loading="lazy">';
echo '<div class="mc-info-stack">';
if ( ! empty( $row->prefix ) ) echo '<span class="mc-prefix">' . $prefix . '</span>';
if ( ! empty( $row->prefix ) && ! empty( $ds['show_prefix'] ) ) echo '<span class="mc-prefix">' . $prefix . '</span>';
echo '<span class="mc-name">' . $username . '</span>';
echo '<div class="mc-spacer"></div>';
echo '<div class="mc-status-line">' . $status_html . '</div>';
@@ -797,6 +851,79 @@ add_action( 'admin_init', 'mcph_settings_init' );
function mcph_settings_init() {
register_setting( 'mcph_plugin_options', 'mcph_statusapi_url' );
}
// Display-Settings werden direkt per admin_post gespeichert (nicht über options.php),
// damit auch alle-deaktiviert korrekt als 0 gespeichert wird.
add_action( 'admin_post_mcph_save_display_settings', 'mcph_handle_save_display_settings' );
function mcph_handle_save_display_settings() {
if ( ! current_user_can( 'manage_options' ) ) {
wp_die( 'Keine Berechtigung.' );
}
check_admin_referer( 'mcph_display_settings_action' );
$defaults = mcph_default_display_settings();
$input = isset( $_POST['mcph_display_settings'] ) && is_array( $_POST['mcph_display_settings'] )
? $_POST['mcph_display_settings']
: array();
$output = array();
foreach ( $defaults as $key => $default ) {
$output[ $key ] = isset( $input[ $key ] ) ? 1 : 0;
}
update_option( 'mcph_display_settings', $output );
wp_redirect( add_query_arg(
array( 'page' => 'mc_player_history', 'mcph_ds_saved' => '1' ),
admin_url( 'options-general.php' )
) );
exit;
}
function mcph_default_display_settings() {
return array(
'show_prefix' => 1,
'show_playtime' => 1,
'show_balance' => 1,
'show_joins' => 1,
'show_kills' => 1,
'show_deaths' => 1,
'show_kd' => 1,
'show_bans' => 1,
'show_mutes' => 1,
'show_warns' => 1,
'show_kicks' => 1,
'show_first_seen'=> 1,
'show_last_seen' => 1,
'show_platform' => 1,
'show_uuid' => 1,
);
}
function mcph_sanitize_display_settings( $input ) {
$defaults = mcph_default_display_settings();
$output = array();
foreach ( $defaults as $key => $default ) {
$output[ $key ] = isset( $input[ $key ] ) ? 1 : 0;
}
return $output;
}
function mcph_get_display_settings() {
$saved = get_option( 'mcph_display_settings', null );
$defaults = mcph_default_display_settings();
// Wenn noch nie gespeichert wurde (null), Defaults zurückgeben.
// Wenn gespeichert (auch leeres Array = alle deaktiviert), gespeicherten Wert nehmen.
if ( $saved === null || ! is_array( $saved ) ) {
return $defaults;
}
// Fehlende Keys mit Default auffüllen (für neue Keys bei Plugin-Updates)
foreach ( $defaults as $key => $default ) {
if ( ! array_key_exists( $key, $saved ) ) {
$saved[ $key ] = $default;
}
}
return $saved;
}
function mcph_options_page() {
if ( isset( $_POST['mcph_cleanup_duplicates'] ) && check_admin_referer( 'mcph_cleanup_action' ) ) {
global $wpdb; $table_name = $wpdb->prefix . 'mc_players';
@@ -813,9 +940,40 @@ function mcph_options_page() {
delete_transient( 'mcph_api_cache_data' );
echo '<div class="notice notice-success"><p>Manueller Sync ausgeführt.</p></div>';
}
if ( isset( $_GET['mcph_ds_saved'] ) ) {
echo '<div class="notice notice-success is-dismissible"><p>Anzeige-Einstellungen gespeichert.</p></div>';
}
$ds = mcph_get_display_settings();
$toggle_labels = array(
'show_prefix' => array( 'label' => 'Rang / Prefix', 'group' => 'Spielerkarte & Profil-Header' ),
'show_platform' => array( 'label' => 'Plattform (Java/Bedrock)', 'group' => 'Spielerkarte & Profil-Header' ),
'show_uuid' => array( 'label' => 'UUID (im Profil)', 'group' => 'Spielerkarte & Profil-Header' ),
'show_playtime' => array( 'label' => 'Spielzeit ⏱️', 'group' => 'Statistik-Karten' ),
'show_balance' => array( 'label' => 'Kontostand 💰', 'group' => 'Statistik-Karten' ),
'show_joins' => array( 'label' => 'Joins 🚀', 'group' => 'Statistik-Karten' ),
'show_kills' => array( 'label' => 'Kills ⚔️', 'group' => 'Statistik-Karten' ),
'show_deaths' => array( 'label' => 'Tode 💀', 'group' => 'Statistik-Karten' ),
'show_kd' => array( 'label' => 'K/D Ratio 📊', 'group' => 'Statistik-Karten' ),
'show_bans' => array( 'label' => 'Bans 🚫', 'group' => 'Strafen' ),
'show_mutes' => array( 'label' => 'Mutes 🔇', 'group' => 'Strafen' ),
'show_warns' => array( 'label' => 'Warns ⚠️', 'group' => 'Strafen' ),
'show_kicks' => array( 'label' => 'Kicks 👢', 'group' => 'Strafen' ),
'show_first_seen'=> array( 'label' => 'Zuerst gesehen 📅', 'group' => 'Zeitangaben' ),
'show_last_seen' => array( 'label' => 'Zuletzt online 🕐', 'group' => 'Zeitangaben' ),
);
// Toggles nach Gruppe gruppieren
$groups = array();
foreach ( $toggle_labels as $key => $info ) {
$groups[ $info['group'] ][ $key ] = $info['label'];
}
?>
<div class="wrap">
<h1>MC Player History Einstellungen</h1>
<form method="post" action="options.php">
<?php settings_fields( 'mcph_plugin_options' ); do_settings_sections( 'mcph_plugin_options' ); ?>
<table class="form-table">
@@ -827,13 +985,45 @@ function mcph_options_page() {
</td>
</tr>
</table>
<?php submit_button(); ?>
<?php submit_button( 'API-Einstellungen speichern' ); ?>
</form>
<hr>
<h2>🎛️ Anzeige-Einstellungen</h2>
<p class="description">Wähle aus, welche Felder im Spieler-Profil und in den Statistik-Karten angezeigt werden sollen.</p>
<form method="post" action="<?php echo esc_url( admin_url('admin-post.php') ); ?>">
<?php wp_nonce_field( 'mcph_display_settings_action' ); ?>
<input type="hidden" name="action" value="mcph_save_display_settings">
<div style="display:flex; flex-wrap:wrap; gap:24px; margin-top:16px;">
<?php foreach ( $groups as $group_name => $fields ) : ?>
<div style="background:#fff; border:1px solid #c3c4c7; border-radius:6px; padding:16px 20px; min-width:220px; flex:1;">
<h3 style="margin-top:0; margin-bottom:12px; font-size:13px; text-transform:uppercase; color:#646970; letter-spacing:.5px;"><?php echo esc_html( $group_name ); ?></h3>
<?php foreach ( $fields as $key => $label ) : ?>
<label style="display:flex; align-items:center; gap:8px; margin-bottom:10px; cursor:pointer; font-size:14px;">
<input type="checkbox"
name="mcph_display_settings[<?php echo esc_attr( $key ); ?>]"
value="1"
<?php checked( 1, $ds[ $key ] ?? 1 ); ?>
style="width:16px; height:16px; cursor:pointer;">
<?php echo esc_html( $label ); ?>
</label>
<?php endforeach; ?>
</div>
<?php endforeach; ?>
</div>
<p style="margin-top:20px;">
<?php submit_button( 'Anzeige-Einstellungen speichern', 'primary', 'submit', false ); ?>
</p>
</form>
<hr>
<h2>Duplikate bereinigen</h2>
<form method="post"><?php wp_nonce_field( 'mcph_cleanup_action' ); ?>
<p class="description">Entfernt doppelte Spieler-Einträge aus der Datenbank.</p>
<input type="submit" name="mcph_cleanup_duplicates" class="button button-secondary" value="Duplikate jetzt entfernen" />
</form>
<h2>Manueller Sync</h2>
<form method="post"><?php wp_nonce_field( 'mcph_manual_sync_action' ); ?>
<input type="submit" name="mcph_manual_sync" class="button" value="Jetzt synchronisieren" />
@@ -855,7 +1045,11 @@ function mcph_shortcode( $atts ) {
echo '</div>';
$profile_id = 'mc-profile-' . substr( $container_id, -6 );
echo '<div id="' . $profile_id . '" class="mc-profile-panel" style="display:none;"></div>';
$ds_js = mcph_get_display_settings();
?>
<script>
var mcphDisplaySettings = <?php echo json_encode( $ds_js ); ?>;
</script>
<script>
(function() {
const intervalSeconds = Math.max(2, <?php echo intval( $atts['interval'] ); ?>);
@@ -986,6 +1180,8 @@ function mcph_shortcode( $atts ) {
headerBannerOverlay = '<div class="mc-profile-header-overlay"></div>';
}
const ds = (typeof mcphDisplaySettings !== 'undefined') ? mcphDisplaySettings : {};
panel.innerHTML = `
<div class="mc-profile-inner">
<button class="mc-back-btn" id="mc-back-${profileId}">← Zurück</button>
@@ -993,12 +1189,12 @@ function mcph_shortcode( $atts ) {
<div class="mc-profile-header${headerBannerClass}"${headerBannerStyle}>
${headerBannerOverlay}
<div class="mc-profile-header-left">
${prefixHtml ? `<div class="mc-profile-prefix">${prefixHtml}</div>` : ''}
${(ds.show_prefix !== 0 && prefixHtml) ? `<div class="mc-profile-prefix">${prefixHtml}</div>` : ''}
<div class="mc-profile-name">${p.username}</div>
<div class="mc-profile-uuid">${p.uuid}</div>
${ds.show_uuid !== 0 ? `<div class="mc-profile-uuid">${p.uuid}</div>` : ''}
<div class="mc-profile-status-row">
${onlineHtml}
<span class="mc-platform-badge">${platformIcon} ${platformLabel}</span>
${ds.show_platform !== 0 ? `<span class="mc-platform-badge">${platformIcon} ${platformLabel}</span>` : ''}
</div>
</div>
<div class="mc-profile-header-right">
@@ -1009,19 +1205,18 @@ function mcph_shortcode( $atts ) {
<div class="mc-profile-divider"></div>
<div class="mc-stats-grid">
${statCard('⏱️', 'Spielzeit', formatPlaytime(p.playtime_seconds), true)}
${statCard('💰', 'Kontostand', balanceFormatted, true)}
${statCard('🚀', 'Joins', p.joins.toLocaleString('de-DE'), false)}
${statCard('⚔️', 'Kills', p.kills.toLocaleString('de-DE'), false)}
${statCard('💀', 'Tode', p.deaths.toLocaleString('de-DE'), false)}
${statCard('📊', 'K/D Ratio', kd, false)}
${statCard('🚫', 'Bans', p.bans, false)}
${statCard('🔇', 'Mutes', p.mutes, false)}
${statCard('⚠️', 'Warns', p.warns, false)}
${statCard('📅', 'Zuerst gesehen', formatDate(p.first_seen), false)}
${statCard('🕐', 'Zuletzt online', formatDate(p.last_seen), false)}
${ds.show_playtime !== 0 ? statCard('⏱️', 'Spielzeit', formatPlaytime(p.playtime_seconds), true) : ''}
${ds.show_balance !== 0 ? statCard('💰', 'Kontostand', balanceFormatted, true) : ''}
${ds.show_joins !== 0 ? statCard('🚀', 'Joins', p.joins.toLocaleString('de-DE'), false) : ''}
${ds.show_kills !== 0 ? statCard('⚔️', 'Kills', p.kills.toLocaleString('de-DE'), false) : ''}
${ds.show_deaths !== 0 ? statCard('💀', 'Tode', p.deaths.toLocaleString('de-DE'), false) : ''}
${ds.show_kd !== 0 ? statCard('📊', 'K/D Ratio', kd, false) : ''}
${ds.show_bans !== 0 ? statCard('🚫', 'Bans', p.bans, false) : ''}
${ds.show_mutes !== 0 ? statCard('🔇', 'Mutes', p.mutes, false) : ''}
${ds.show_warns !== 0 ? statCard('⚠️', 'Warns', p.warns, false) : ''}
${ds.show_kicks !== 0 ? statCard('👢', 'Kicks', p.kicks ?? 0, false) : ''}
${ds.show_first_seen!== 0 ? statCard('📅', 'Zuerst gesehen', formatDate(p.first_seen), false) : ''}
${ds.show_last_seen !== 0 ? statCard('🕐', 'Zuletzt online', formatDate(p.last_seen), false) : ''}
</div>
</div>
`;