10] ); if ( ! is_wp_error($response) && 200 === wp_remote_retrieve_response_code($response) ) { $body = wp_remote_retrieve_body($response); $data = json_decode($body, true); if ( $data && isset($data['tag_name']) ) { $tag = ltrim( $data['tag_name'], 'v' ); $release_info = [ 'version' => $tag, 'download_url' => $data['zipball_url'] ?? '', 'notes' => $data['body'] ?? '', 'published_at' => $data['published_at'] ?? '', ]; set_transient( $transient_key, $release_info, 6 * HOUR_IN_SECONDS ); } else { set_transient( $transient_key, [], HOUR_IN_SECONDS ); } } else { set_transient( $transient_key, [], HOUR_IN_SECONDS ); } } return $release_info; } // Admin-Update-Hinweis function mcph_show_update_notice() { if ( ! current_user_can('manage_options') ) { return; } $current_version = mcph_get_plugin_version(); $latest_release = mcph_get_latest_release_info(); if ( ! empty($latest_release['version']) && version_compare($current_version, $latest_release['version'], '<') ) { $refresh_url = wp_nonce_url( admin_url('plugins.php?mcph_clear_cache=1'), 'mcph_clear_cache_action' ); ?>

MC Player History – Update verfügbar

Aktuelle Version:
Neueste Version:

Update herunterladen Release Notes Jetzt neu prüfen

'', '&1' => '', '&2' => '', '&3' => '', '&4' => '', '&5' => '', '&6' => '', '&7' => '', '&8' => '', '&9' => '', '&a' => '', '&b' => '', '&c' => '', '&d' => '', '&e' => '', '&f' => '', '&l' => '', '&m' => '', '&n' => '', '&o' => '', '&r' => '', ); foreach ( $map as $code => $html ) { if ( $code === '&r' ) { $text = str_replace( $code, $html . '', $text ); } else { $text = str_replace( $code, $html, $text ); } } return $text; } /** * 1. Tabelle beim Aktivieren anlegen + Playtime Spalte */ register_activation_hook( __FILE__, 'mcph_install' ); function mcph_install() { global $wpdb, $mc_player_history_db_version; // FIX: Variable global verfügbar machen $table_name = $wpdb->prefix . 'mc_players'; $charset_collate = $wpdb->get_charset_collate(); $sql = "CREATE TABLE $table_name ( id bigint(20) NOT NULL AUTO_INCREMENT, uuid varchar(64) NOT NULL, username varchar(64) NOT NULL, prefix varchar(255) DEFAULT NULL, first_seen datetime NOT NULL, last_seen datetime NOT NULL, is_online tinyint(1) NOT NULL DEFAULT 0, playtime_seconds INT NOT NULL DEFAULT 0, PRIMARY KEY (id), UNIQUE KEY uuid (uuid), KEY username (username), KEY is_online (is_online), KEY playtime_seconds (playtime_seconds) ) $charset_collate;"; require_once( ABSPATH . 'wp-admin/includes/upgrade.php' ); dbDelta( $sql ); update_option( 'mc_player_history_db_version', $mc_player_history_db_version ); } register_deactivation_hook( __FILE__, 'mcph_deactivate' ); function mcph_deactivate() { wp_clear_scheduled_hook( 'mcph_sync_event' ); } /** * NEU: Playtime Spalte hinzufügen (wird beim Laden geprüft) */ add_action( 'init', 'mcph_add_playtime_column' ); function mcph_add_playtime_column() { global $wpdb; $table_name = $wpdb->prefix . 'mc_players'; // Prüfen, ob Spalte existiert $column_exists = $wpdb->get_var( "SHOW COLUMNS FROM $table_name LIKE 'playtime_seconds'" ); if ( empty( $column_exists ) ) { $wpdb->query( "ALTER TABLE $table_name ADD COLUMN playtime_seconds INT NOT NULL DEFAULT 0 AFTER last_seen" ); } } /** * 2. Cron-Job */ add_action( 'init', 'mcph_setup_schedule' ); function mcph_setup_schedule() { if ( ! wp_next_scheduled( 'mcph_sync_event' ) ) { wp_schedule_event( time(), 'mcph_2min', 'mcph_sync_event' ); } } add_filter( 'cron_schedules', 'mcph_add_cron_interval' ); function mcph_add_cron_interval( $schedules ) { $schedules['mcph_2min'] = array( 'interval' => 2 * MINUTE_IN_SECONDS, 'display' => 'Alle 2 Minuten' ); return $schedules; } add_action( 'mcph_sync_event', 'mcph_sync_from_statusapi' ); function mcph_sync_from_statusapi() { $api_url = get_option( 'mcph_statusapi_url' ); if ( ! empty( $api_url ) && strpos( $api_url, 'http' ) !== 0 ) { $api_url = 'http://' . $api_url; } if ( empty( $api_url ) ) { $api_url = 'http://localhost:9191'; } $response = wp_remote_get( $api_url, array( 'timeout' => 5 ) ); if ( is_wp_error( $response ) ) { return; } $json = json_decode( wp_remote_retrieve_body( $response ) ); if ( ! $json ) { return; } global $wpdb; $table_name = $wpdb->prefix . 'mc_players'; if ( $wpdb->get_var( "SHOW TABLES LIKE '$table_name'" ) != $table_name ) { return; } // 1. Alle Spieler auf Offline setzen (Reset) $wpdb->query( "UPDATE $table_name SET is_online = 0" ); $players = isset( $json->players ) && is_array( $json->players ) ? $json->players : array(); // 2. Spieler durchgehen foreach ( $players as $p ) { $name = isset( $p->name ) ? sanitize_text_field( $p->name ) : ''; $prefix = isset( $p->prefix ) ? sanitize_text_field( $p->prefix ) : ''; // UUID aus API übernehmen, falls vorhanden, sonst legacy-Hash if ( isset( $p->uuid ) && !empty( $p->uuid ) ) { $uuid = sanitize_text_field( $p->uuid ); } else { $uuid = 'legacy-' . md5( strtolower( trim( $name ) ) ); } if ( empty( $name ) ) continue; // Prüfe ob Spieler bereits existiert (nach UUID) $exists = $wpdb->get_var( $wpdb->prepare( "SELECT id FROM $table_name WHERE uuid = %s", $uuid ) ); // Fallback: Falls nicht per UUID gefunden, prüfe nach Username if ( ! $exists ) { $old_entry = $wpdb->get_row( $wpdb->prepare( "SELECT id, uuid FROM $table_name WHERE username = %s", $name ) ); if ( $old_entry ) { // FIX: Basis-Daten für Update (ohne Prefix) $update_data = array( 'uuid' => $uuid, 'last_seen' => current_time( 'mysql' ), 'is_online' => 1 ); $update_format = array( '%s', '%s', '%d' ); // FIX: Prefix nur aktualisieren, wenn API einen Wert liefert (nicht leer) if ( ! empty( $prefix ) ) { $update_data['prefix'] = $prefix; $update_format[] = '%s'; } $wpdb->update( $table_name, $update_data, array( 'id' => $old_entry->id ), $update_format, array( '%d' ) ); continue; } } // NEU: Playtime erhöhen (2 Minuten = 120 Sekunden pro Sync) $wpdb->query( $wpdb->prepare( "UPDATE $table_name SET playtime_seconds = playtime_seconds + 120 WHERE uuid = %s", $uuid ) ); $now = current_time( 'mysql' ); if ( $exists ) { // FIX: Basis-Daten für Update (ohne Prefix) $update_data = array( 'username' => $name, 'last_seen' => $now, 'is_online' => 1 ); $update_format = array( '%s', '%s', '%d' ); // FIX: Prefix nur aktualisieren, wenn API einen Wert liefert (nicht leer) if ( ! empty( $prefix ) ) { $update_data['prefix'] = $prefix; $update_format[] = '%s'; } $wpdb->update( $table_name, $update_data, array( 'uuid' => $uuid ), $update_format, array( '%s' ) ); } else { // Bei neuen Spielern fügen wir das Prefix ein (auch wenn es leer ist) $wpdb->insert( $table_name, array( 'uuid' => $uuid, 'username' => $name, 'prefix' => $prefix, 'first_seen' => $now, 'last_seen' => $now, 'is_online' => 1, 'playtime_seconds' => 0 ), array( '%s', '%s', '%s', '%s', '%s', '%d', '%d' ) ); } } } /** * 3. AJAX Handler */ add_action( 'wp_ajax_mcph_refresh', 'mcph_ajax_refresh' ); add_action( 'wp_ajax_nopriv_mcph_refresh', 'mcph_ajax_refresh' ); function mcph_ajax_refresh() { $limit = isset( $_POST['limit'] ) ? intval( $_POST['limit'] ) : 500; $only_online = isset( $_POST['only_online'] ) && $_POST['only_online'] === 'true'; $html = mcph_generate_player_html( $limit, $only_online, true ); wp_send_json_success( array( 'html' => $html ) ); } /** * 4. HTML Generator (Strikte Reihenfolge) */ function mcph_generate_player_html( $limit = 500, $only_online = false, $is_ajax = false ) { global $wpdb; $table_name = $wpdb->prefix . 'mc_players'; // --- LIVE STATUS CHECK MIT CACHE --- $cache_key = 'mcph_api_cache_data'; $api_response = get_transient( $cache_key ); if ( false === $api_response ) { $api_url = get_option( 'mcph_statusapi_url' ); if ( ! empty( $api_url ) && strpos( $api_url, 'http' ) !== 0 ) { $api_url = 'http://' . $api_url; } if ( empty( $api_url ) ) { $api_url = 'http://localhost:9191'; } $response = wp_remote_get( $api_url, array( 'timeout' => 2 ) ); if ( ! is_wp_error( $response ) ) { $body = wp_remote_retrieve_body( $response ); $json = json_decode( $body ); if ( $json && isset( $json->players ) && is_array( $json->players ) ) { set_transient( $cache_key, $json->players, 5 ); $api_response = $json->players; } } } $live_online_uuids = array(); $has_live_data = false; if ( is_array( $api_response ) && ! empty( $api_response ) ) { $has_live_data = true; foreach ( $api_response as $p ) { if ( isset( $p->name ) ) { // UUID aus API übernehmen, falls vorhanden, sonst legacy-Hash if ( isset( $p->uuid ) && !empty( $p->uuid ) ) { $uuid = sanitize_text_field( $p->uuid ); } else { $uuid = 'legacy-' . md5( strtolower( trim( $p->name ) ) ); } $live_online_uuids[ $uuid ] = true; } } } $sql_limit = $only_online ? ( $limit * 2 ) : $limit; $rows = $wpdb->get_results( $wpdb->prepare( "SELECT * FROM $table_name ORDER BY last_seen DESC LIMIT %d", $sql_limit ) ); if ( empty( $rows ) ) { return '

Keine Spieler gefunden.

'; } ob_start(); echo '
'; $displayed_count = 0; foreach ( $rows as $row ) { $is_online = false; if ( $has_live_data ) { $is_online = isset( $live_online_uuids[ $row->uuid ] ); } else { $is_online = ( $row->is_online == 1 ); } if ( $only_online && ! $is_online ) { continue; } if ( $displayed_count >= $limit ) { break; } $displayed_count++; $username = esc_html( $row->username ); $prefix = mcph_parse_minecraft_colors( $row->prefix ); // Avatar-Logik: Moderne 3D-Köpfe (alle in gleiche Richtung) $avatar = ''; if ( strpos( $username, '.' ) !== false || ( isset($row->uuid) && strpos($row->uuid, 'xuid') === 0 ) ) { // Bedrock: mc-heads.net mit 3D Head $avatar = isset($row->uuid) && !empty($row->uuid) ? 'https://mc-heads.net/head/' . esc_attr($row->uuid) . '/100' : 'https://mc-heads.net/head/' . $username . '/100'; } else { // Java: mc-heads.net mit 3D Head (gleiche Richtung wie Bedrock) $avatar = 'https://mc-heads.net/head/' . $username . '/100'; } $anim_style = ''; if ( ! $is_ajax ) { $anim_style = 'animation: fadeInUp 0.5s ease forwards; opacity: 0; animation-delay: ' . ( $displayed_count * 0.05 ) . 's;'; } if ( $is_online ) { $status_html = 'Online'; } else { $time = strtotime( $row->last_seen ); $date_str = ( date('d.m.Y', $time) == date('d.m.Y') ) ? date('H:i', $time) : date('d.m.Y H:i', $time); $status_html = 'Zuletzt: ' . $date_str . ''; } // STRUKTUR: Bild -> Prefix -> Name -> Spacer -> Status echo '
'; echo '' . $username . ''; echo '
'; // 1. PREFIX if ( ! empty( $row->prefix ) ) { echo '' . $prefix . ''; } // 2. NAME echo '' . $username . ''; // 3. SPACER (Nimmt den verbleibenden Platz ein) echo '
'; // 4. STATUS echo '
' . $status_html . '
'; echo '
'; echo '
'; } echo '
'; echo '
Aktualisiert: ' . current_time('H:i:s') . '
'; return ob_get_clean(); } /** * 5. Admin Menü */ add_action( 'admin_menu', 'mcph_admin_menu' ); function mcph_admin_menu() { add_options_page( 'MC Player History', 'MC Player History', 'manage_options', 'mc_player_history', 'mcph_options_page' ); } add_action( 'admin_init', 'mcph_settings_init' ); function mcph_settings_init() { register_setting( 'mcph_plugin_options', 'mcph_statusapi_url' ); } function mcph_options_page() { // Duplikate bereinigen if ( isset( $_POST['mcph_cleanup_duplicates'] ) && check_admin_referer( 'mcph_cleanup_action' ) ) { global $wpdb; $table_name = $wpdb->prefix . 'mc_players'; // Finde Duplikate (gleicher Username, verschiedene UUIDs) $duplicates = $wpdb->get_results( "SELECT username, COUNT(*) as count, GROUP_CONCAT(id ORDER BY last_seen DESC) as ids FROM $table_name GROUP BY username HAVING count > 1" ); $cleaned = 0; foreach ( $duplicates as $dup ) { $ids = explode( ',', $dup->ids ); // Behalte den neuesten (ersten), lösche die anderen array_shift( $ids ); foreach ( $ids as $id ) { $wpdb->delete( $table_name, array( 'id' => $id ), array( '%d' ) ); $cleaned++; } } echo '

' . $cleaned . ' Duplikate wurden entfernt.

'; } if ( isset( $_POST['mcph_manual_sync'] ) && check_admin_referer( 'mcph_manual_sync_action' ) ) { mcph_sync_from_statusapi(); delete_transient( 'mcph_api_cache_data' ); echo '

Manueller Sync ausgeführt.

'; } ?>

MC Player History Einstellungen

StatusAPI URL

Bitte unbedingt mit http:// angeben.

Duplikate bereinigen

Entfernt doppelte Spieler-Einträge aus der Datenbank.

Manueller Sync

500, 'interval' => 2, 'only_online' => 'false' ), $atts ); $container_id = 'mc-player-wrapper-' . uniqid(); ob_start(); echo '
'; echo mcph_generate_player_html( $atts['limit'], $atts['only_online'] === 'true', false ); echo '
'; ?> .mc-player-list { width: 100%; } .mc-grid { display: grid; gap: 20px; grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); justify-content: center; align-items: stretch; } @keyframes fadeInUp { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } } /* KARTE */ .mc-player-card { display: flex; flex-direction: column; align-items: center; gap: 8px; padding: 20px 10px; background: linear-gradient(145deg, #ffffff 0%, #f8f9fa 100%); border: 1px solid #e0e0e0; border-radius: 12px; transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); cursor: default; position: relative; box-shadow: 0 2px 8px rgba(0,0,0,0.08); } .mc-player-card:hover { transform: translateY(-8px) scale(1.02); box-shadow: 0 12px 24px rgba(0,0,0,0.15); border-color: #667eea; background: linear-gradient(145deg, #ffffff 0%, #f0f2ff 100%); } .mc-avatar { border-radius: 8px; border: 3px solid #e0e0e0; background: transparent; width: 80px; height: 80px; transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1); flex-shrink: 0; box-shadow: 0 4px 12px rgba(0,0,0,0.15); object-fit: contain; padding: 0; } .mc-player-card:hover .mc-avatar { border-color: #667eea; transform: scale(1.15) rotateY(10deg); box-shadow: 0 8px 20px rgba(102, 126, 234, 0.4); } /* INFOS BEREICH */ .mc-info-stack { display: flex; flex-direction: column; align-items: center; width: 100%; flex-grow: 1; position: relative; padding-bottom: 35px; justify-content: flex-start; } /* 1. PREFIX */ .mc-prefix { font-size: 0.9em; display: block; width: 100%; text-align: center; margin-bottom: 4px; line-height: 1.2; text-shadow: 0 1px 2px rgba(0,0,0,0.1); } /* 2. NAME */ .mc-name { font-weight: bold; font-size: 1.1em; color: #2d3748; word-break: break-word; display: block; width: 100%; text-align: center; text-shadow: 0 1px 2px rgba(0,0,0,0.05); } /* 3. SPACER */ .mc-spacer { flex-grow: 1; } /* 4. STATUS (UNTEN) */ .mc-status-line { position: absolute; bottom: 0; left: 0; width: 100%; text-align: center; margin: 0; padding-bottom: 5px; } .mc-status { display: inline-block; padding: 5px 14px; border-radius: 20px; font-size: 0.8em; font-weight: 600; transition: all 0.3s ease; } .mc-online { background: linear-gradient(135deg, #d4f8e8 0%, #b8f2d9 100%); color: #0a7340; border: 1px solid #81e6b8; box-shadow: 0 2px 4px rgba(10, 115, 64, 0.1); } .mc-offline { background: linear-gradient(135deg, #fff5f5 0%, #ffe5e5 100%); color: #c53030; border: 1px solid #fbb6b6; box-shadow: 0 2px 4px rgba(197, 48, 48, 0.1); } .mc-player-card:hover .mc-status { transform: scale(1.05); } .mc-update-time { font-size: 0.85em; color: #718096; text-align: center; margin-top: 20px; font-style: italic; padding: 8px; background: rgba(102, 126, 234, 0.05); border-radius: 6px; display: inline-block; width: 100%; } @media (min-width: 1200px) { .mc-grid { grid-template-columns: repeat(6, 1fr); } } @media (max-width: 600px) { .mc-grid { grid-template-columns: repeat(2, 1fr); gap: 12px; } .mc-avatar { width: 70px; height: 70px; } .mc-name { font-size: 1em; } .mc-player-card { padding: 15px 8px; } } '; return ob_get_clean(); } /* ================= SIDEBAR WIDGET: TOP SPIELZEIT (1/2 Spalten + sichere Inline-Farben) ================= */ class MC_Top_Playtime_Widget extends WP_Widget { public function __construct() { parent::__construct( 'mc_top_playtime_widget', 'MC Top Spieler', array( 'description' => 'Zeigt die 6 Spieler mit der meisten Spielzeit an. Wahl: 1 oder 2 Spieler pro Zeile.' ) ); } // Zeitformatierung public static function format_duration( $seconds ) { if ( $seconds < 60 ) { return $seconds . 's'; } elseif ( $seconds < 3600 ) { $minutes = floor( $seconds / 60 ); return $minutes . 'm'; } elseif ( $seconds < 86400 ) { $hours = floor( $seconds / 3600 ); $minutes = floor( ( $seconds % 3600 ) / 60 ); return $hours . 'h ' . $minutes . 'm'; } else { $days = floor( $seconds / 86400 ); $hours = floor( ( $seconds % 86400 ) / 3600 ); return $days . 'd ' . $hours . 'h'; } } // Konvertiert Minecraft-Farbcodes (&0-&f, &r) in inline Text private static function parse_minecraft_color_codes_inline( $text ) { if ( $text === null || $text === '' ) { return ''; } // Mapping Minecraft color codes -> HEX $map = array( '0' => '#000000', '1' => '#0000AA', '2' => '#00AA00', '3' => '#00AAAA', '4' => '#AA0000', '5' => '#AA00AA', '6' => '#FFAA00', '7' => '#AAAAAA', '8' => '#555555', '9' => '#5555FF', 'a' => '#55FF55', 'b' => '#55FFFF', 'c' => '#FF5555', 'd' => '#FF55FF', 'e' => '#FFFF55', 'f' => '#FFFFFF', ); // Ersetze alternative §-Codes durch & falls nötig $text = str_replace('§', '&', $text); // Teile so, dass die Codes erhalten bleiben $parts = preg_split('/(&[0-9a-frk-or])/i', $text, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY); $out = ''; $open = false; foreach ( $parts as $part ) { // Ist es ein Code? if ( preg_match('/^&([0-9a-frk-orA-FK-OR])$/', $part, $m) ) { $code = strtolower( $m[1] ); // Reset if ( $code === 'r' ) { if ( $open ) { $out .= '
'; $open = false; } continue; } // Farbe if ( isset( $map[$code] ) ) { if ( $open ) { $out .= '
'; $open = false; } $hex = $map[$code]; $out .= ''; $open = true; continue; } // Formatcodes: &l &o &n &m &k -> wir implementieren nur fett (&l) &o kursiv optional if ( $code === 'l' ) { // fett $out .= ''; continue; } if ( $code === 'o' ) { // italic $out .= ''; continue; } if ( in_array( $code, array('m','n','k'), true) ) { // durchgestrichen / underline / obfuscated - ignorieren oder erweitern falls gewünscht continue; } continue; } // Normaler Text: escapen $escaped = esc_html( $part ); $out .= $escaped; } // Schließe eventuell offene Tags: zuerst format, dann color spans // (Wir schlossen format tags nicht oben -> vereinfachung: entfernen ungeschlossene format-tags) // Schließe color span falls offen if ( $open ) { $out .= ''; $open = false; } // Hinweis: Wir benutzten esc_html pro Textteil; das ist sicher. return $out; } // Backend-Form public function form( $instance ) { $title = isset( $instance['title'] ) ? $instance['title'] : 'Top 6 Spielzeit'; $columns = isset( $instance['columns'] ) ? intval( $instance['columns'] ) : 1; ?>

prefix . 'mc_players'; // Top 6 Spieler abfragen $top_players = $wpdb->get_results( "SELECT * FROM $table_name WHERE playtime_seconds > 0 ORDER BY playtime_seconds DESC LIMIT 6" ); $container_class = 'mc-top-list' . ( $columns === 2 ? ' mc-two-columns' : ' mc-one-column' ); ?>

Noch keine Daten.

username ); $time_string = self::format_duration( $row->playtime_seconds ); // Rohes Prefix aus DB (z.B. "&7Member") $raw_prefix = isset( $row->prefix ) ? $row->prefix : ''; $prefix_html = self::parse_minecraft_color_codes_inline( $raw_prefix ); // Avatar URL (UUID bevorzugt) if ( isset( $row->uuid ) && ! empty( $row->uuid ) ) { $avatar = 'https://mc-heads.net/head/' . esc_attr( $row->uuid ) . '/50'; } else { $avatar = 'https://mc-heads.net/head/' . rawurlencode( $username ) . '/50'; } ?>
<?php echo $username; ?>
<?php echo $username; ?>