diff --git a/mc-player-history.php b/mc-player-history.php
index c93def8..1331231 100644
--- a/mc-player-history.php
+++ b/mc-player-history.php
@@ -1,1525 +1,1661 @@
- 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' );
- ?>
-
- last_error ) ) {
- mcph_log( 'LiteBans Verbindung fehlgeschlagen: ' . $lbdb->last_error );
- $connection = null;
- return $connection;
- }
-
- $connection = array(
- 'db' => $lbdb,
- 'prefix' => $prefix,
- );
-
- return $connection;
-}
-
-/**
- * Holt Bans/Mutes/Warns aus LiteBans für einen Spieler.
- */
-function mcph_get_litebans_punishments( $uuid, $username ) {
- static $count_cache = array();
-
- $cache_key = (string) $uuid . '|' . strtolower( (string) $username );
- if ( isset( $count_cache[ $cache_key ] ) ) {
- return $count_cache[ $cache_key ];
- }
-
- $conn = mcph_get_litebans_connection();
- if ( empty( $conn['db'] ) || empty( $conn['prefix'] ) ) {
- $count_cache[ $cache_key ] = null;
- return null;
- }
-
- $lbdb = $conn['db'];
- $prefix = $conn['prefix'];
-
- $search_uuid = '';
- if ( ! empty( $uuid ) && strpos( $uuid, 'legacy-' ) !== 0 ) {
- $search_uuid = $uuid;
- }
-
- if ( empty( $search_uuid ) && ! empty( $username ) ) {
- $history_table = esc_sql( $prefix . 'history' );
- $search_uuid = $lbdb->get_var(
- $lbdb->prepare(
- "SELECT uuid FROM {$history_table} WHERE name = %s ORDER BY date DESC LIMIT 1",
- $username
- )
- );
- }
-
- if ( empty( $search_uuid ) ) {
- $count_cache[ $cache_key ] = null;
- return null;
- }
-
- $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 ) ) );
- $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 );
- $count_cache[ $cache_key ] = null;
- return null;
- }
-
- $count_cache[ $cache_key ] = array(
- 'bans' => $bans,
- 'mutes' => $mutes,
- 'warns' => $warns,
- 'kicks' => $kicks,
- );
-
- return $count_cache[ $cache_key ];
-}
-
-/**
- * Hilfsfunktion: Minecraft Color Codes umwandeln
- */
-function mcph_parse_minecraft_colors( $text ) {
- if ( empty( $text ) ) return '';
- $map = array(
- '&0' => '', '&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;
-}
-
-/**
- * Erkennt Bedrock-Spieler robust aus API-Objekt, UUID/XUID und Namen.
- */
-function mcph_detect_bedrock_player( $player, $uuid = '', $name = '' ) {
- if ( is_object( $player ) ) {
- $flag_keys = array( 'isBedrock', 'is_bedrock', 'bedrock', 'floodgate' );
- foreach ( $flag_keys as $key ) {
- if ( isset( $player->$key ) ) {
- $value = $player->$key;
- if ( is_bool( $value ) ) {
- return $value ? 1 : 0;
- }
- if ( is_numeric( $value ) ) {
- return intval( $value ) > 0 ? 1 : 0;
- }
- if ( is_string( $value ) ) {
- $normalized = strtolower( trim( $value ) );
- if ( in_array( $normalized, array( '1', 'true', 'yes', 'y', 'on' ), true ) ) {
- return 1;
- }
- if ( in_array( $normalized, array( '0', 'false', 'no', 'n', 'off' ), true ) ) {
- return 0;
- }
- }
- }
- }
-
- if ( isset( $player->xuid ) && ! empty( $player->xuid ) ) {
- return 1;
- }
- }
-
- $uuid = strtolower( (string) $uuid );
- if ( strpos( $uuid, 'xuid' ) === 0 || strpos( $uuid, 'floodgate' ) === 0 ) {
- return 1;
- }
-
- // Java-Namen enthalten keinen Punkt; viele Bedrock-Setups nutzen Punkt-Präfix.
- $name = trim( (string) $name );
- if ( strpos( $name, '.' ) !== false ) {
- return 1;
- }
-
- return 0;
-}
-
-/**
- * 1. Tabelle beim Aktivieren anlegen + NEUE FELDER
- */
-register_activation_hook( __FILE__, 'mcph_install' );
-function mcph_install() {
- global $wpdb, $mc_player_history_db_version;
- $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,
- kills INT NOT NULL DEFAULT 0,
- deaths INT NOT NULL DEFAULT 0,
- balance DOUBLE NOT NULL DEFAULT 0,
- is_bedrock tinyint(1) NOT NULL DEFAULT 0,
- joins INT NOT NULL DEFAULT 0,
- 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),
- 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' );
-}
-
-/**
- * Erweiterung der Datenbank beim Laden (falls Update)
- */
-add_action( 'init', 'mcph_add_columns_v130' );
-function mcph_add_columns_v130() {
- global $wpdb;
- $table_name = $wpdb->prefix . 'mc_players';
-
- // Playtime
- if ( ! $wpdb->get_var( "SHOW COLUMNS FROM $table_name LIKE 'playtime_seconds'" ) ) {
- $wpdb->query( "ALTER TABLE $table_name ADD COLUMN playtime_seconds INT NOT NULL DEFAULT 0 AFTER last_seen" );
- }
- // Kills
- if ( ! $wpdb->get_var( "SHOW COLUMNS FROM $table_name LIKE 'kills'" ) ) {
- $wpdb->query( "ALTER TABLE $table_name ADD COLUMN kills INT NOT NULL DEFAULT 0 AFTER playtime_seconds" );
- }
- // Deaths
- if ( ! $wpdb->get_var( "SHOW COLUMNS FROM $table_name LIKE 'deaths'" ) ) {
- $wpdb->query( "ALTER TABLE $table_name ADD COLUMN deaths INT NOT NULL DEFAULT 0 AFTER kills" );
- }
- // Balance
- if ( ! $wpdb->get_var( "SHOW COLUMNS FROM $table_name LIKE 'balance'" ) ) {
- $wpdb->query( "ALTER TABLE $table_name ADD COLUMN balance DOUBLE NOT NULL DEFAULT 0 AFTER deaths" );
- }
- // Is Bedrock
- if ( ! $wpdb->get_var( "SHOW COLUMNS FROM $table_name LIKE 'is_bedrock'" ) ) {
- $wpdb->query( "ALTER TABLE $table_name ADD COLUMN is_bedrock tinyint(1) NOT NULL DEFAULT 0 AFTER balance" );
- }
- // Joins
- if ( ! $wpdb->get_var( "SHOW COLUMNS FROM $table_name LIKE 'joins'" ) ) {
- $wpdb->query( "ALTER TABLE $table_name ADD COLUMN joins INT NOT NULL DEFAULT 0 AFTER is_bedrock" );
- }
- // Bans
- if ( ! $wpdb->get_var( "SHOW COLUMNS FROM $table_name LIKE 'bans'" ) ) {
- $wpdb->query( "ALTER TABLE $table_name ADD COLUMN bans INT NOT NULL DEFAULT 0 AFTER joins" );
- }
- // Mutes
- if ( ! $wpdb->get_var( "SHOW COLUMNS FROM $table_name LIKE 'mutes'" ) ) {
- $wpdb->query( "ALTER TABLE $table_name ADD COLUMN mutes INT NOT NULL DEFAULT 0 AFTER bans" );
- }
- // Warns
- 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" );
- }
-}
-
-/**
- * 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; }
-
- // Alle Offline setzen
- $wpdb->query( "UPDATE $table_name SET is_online = 0" );
- $players = isset( $json->players ) && is_array( $json->players ) ? $json->players : array();
-
- foreach ( $players as $p ) {
- $name = isset( $p->name ) ? sanitize_text_field( $p->name ) : '';
- $prefix = isset( $p->prefix ) ? sanitize_text_field( $p->prefix ) : '';
-
- // Basis Stats
- $playtime = isset( $p->playtime ) ? intval( $p->playtime ) : 0;
-
- // Economy
- $balance = 0;
- if ( isset( $p->economy ) && is_object( $p->economy ) && isset( $p->economy->balance ) ) {
- $balance = floatval( $p->economy->balance );
- }
-
- // Kills/Deaths (Fallback wenn nicht in API)
- $kills = isset( $p->kills ) ? intval( $p->kills ) : 0;
- $deaths = isset( $p->deaths ) ? intval( $p->deaths ) : 0;
-
- // NEU: Weitere Stats
- $joins = isset( $p->joins ) ? intval( $p->joins ) : 0;
-
- // UUID
- if ( isset( $p->uuid ) && !empty( $p->uuid ) ) {
- $uuid = sanitize_text_field( $p->uuid );
- } else {
- $uuid = 'legacy-' . md5( strtolower( trim( $name ) ) );
- }
-
- $is_bedrock = mcph_detect_bedrock_player( $p, $uuid, $name );
-
- $bans = 0;
- $mutes = 0;
- $warns = 0;
- $kicks = 0;
-
- // Primär aus StatusAPI lesen (falls geliefert).
- if ( isset( $p->punishments ) && is_object( $p->punishments ) ) {
- $bans = isset( $p->punishments->bans ) ? intval( $p->punishments->bans ) : 0;
- $mutes = isset( $p->punishments->mutes ) ? intval( $p->punishments->mutes ) : 0;
- $warns = isset( $p->punishments->warns ) ? intval( $p->punishments->warns ) : 0;
- }
-
- // 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 );
- $mutes = intval( $litebans_punishments['mutes'] ?? 0 );
- $warns = intval( $litebans_punishments['warns'] ?? 0 );
- $kicks = intval( $litebans_punishments['kicks'] ?? 0 );
- }
-
- if ( empty( $name ) ) continue;
-
- $exists = $wpdb->get_var( $wpdb->prepare( "SELECT id FROM $table_name WHERE uuid = %s", $uuid ) );
-
- // Fallback alte Einträge
- if ( ! $exists ) {
- $old_entry = $wpdb->get_row( $wpdb->prepare( "SELECT id, uuid FROM $table_name WHERE username = %s", $name ) );
- if ( $old_entry ) {
- $update_data = array(
- 'uuid' => $uuid,
- '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,
- 'kicks' => $kicks
- );
- // 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;
- $update_format[] = '%s';
- }
- $wpdb->update( $table_name, $update_data, array( 'id' => $old_entry->id ), $update_format, array( '%d' ) );
- continue;
- }
- }
-
- // Zeitkonvertierung
- $last_seen_time = isset( $p->last_seen ) ? intval( $p->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 = wp_date( 'Y-m-d H:i:s', intval( $p->first_seen ) );
- }
-
- if ( $exists ) {
- $update_data = array(
- 'username' => $name,
- '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,
- 'kicks' => $kicks
- );
- // 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;
- $update_format[] = '%s';
- }
- $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,
- 'prefix' => $prefix,
- 'first_seen' => $first_seen_mysql,
- '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,
- 'kicks' => $kicks
- ), array( '%s', '%s', '%s', '%s', '%s', '%d', '%d', '%d', '%d', '%s', '%d', '%d', '%d', '%d', '%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 ) );
-}
-
-/**
- * Holt StatusAPI-Spieler (optional mit Refresh).
- */
-function mcph_fetch_statusapi_players( $force_refresh = false ) {
- $cache_key = 'mcph_api_cache_data';
- if ( $force_refresh ) {
- delete_transient( $cache_key );
- }
-
- $api_response = get_transient( $cache_key );
- if ( false !== $api_response ) {
- return $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 ) ) {
- return false;
- }
-
- $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, 2 );
- return $json->players;
- }
-
- return false;
-}
-
-/**
- * 3b. AJAX Handler: Spielerprofil (Erweitert)
- */
-/**
- * Forum-Banner: Holt Forumsdaten anhand der MC-UUID.
- * Erfordert WP Business Forum Plugin (WBF_DB, WBF_Levels).
- */
-function mcph_get_forum_banner( $uuid ) {
- if ( ! class_exists( 'WBF_DB' ) || ! class_exists( 'WBF_Levels' ) ) return null;
- global $wpdb;
- $user_id = $wpdb->get_var( $wpdb->prepare(
- "SELECT user_id FROM {$wpdb->prefix}forum_user_meta WHERE meta_key = 'mc_uuid' AND meta_value = %s LIMIT 1",
- $uuid
- ) );
- if ( ! $user_id ) return null;
- $forum_user = WBF_DB::get_user( (int) $user_id );
- if ( ! $forum_user ) return null;
- $post_count = intval( $forum_user->post_count );
- return array(
- 'display_name' => $forum_user->display_name,
- 'avatar_url' => $forum_user->avatar_url,
- 'banner_url' => $forum_user->banner_url ?? '',
- 'role' => $forum_user->role,
- 'post_count' => $post_count,
- 'registered' => $forum_user->registered,
- 'last_active' => $forum_user->last_active,
- 'bio' => $forum_user->bio,
- 'level_badge_html' => WBF_Levels::is_enabled() ? WBF_Levels::badge( $post_count ) : '',
- 'level_progress_html' => WBF_Levels::is_enabled() ? WBF_Levels::progress_bar( $post_count ) : '',
- );
-}
-
-add_action( 'wp_ajax_mcph_player_profile', 'mcph_ajax_player_profile' );
-add_action( 'wp_ajax_nopriv_mcph_player_profile', 'mcph_ajax_player_profile' );
-function mcph_ajax_player_profile() {
- $uuid = isset( $_POST['uuid'] ) ? sanitize_text_field( $_POST['uuid'] ) : '';
- if ( empty( $uuid ) ) { wp_send_json_error( 'Kein UUID angegeben.' ); return; }
-
- global $wpdb;
- $table_name = $wpdb->prefix . 'mc_players';
- $player = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM $table_name WHERE uuid = %s", $uuid ) );
-
- if ( ! $player ) { wp_send_json_error( 'Spieler nicht gefunden.' ); return; }
-
- // Live-Status prüfen
- $is_online = (bool) $player->is_online;
- $is_bedrock = (bool) $player->is_bedrock;
- $balance_value = floatval( $player->balance );
- $api_response = mcph_fetch_statusapi_players( true );
- if ( is_array( $api_response ) ) {
- $live_uuids = array();
- $matched_live_player = null;
- foreach ( $api_response as $p ) {
- $candidate_uuid = '';
- if ( isset( $p->uuid ) && ! empty( $p->uuid ) ) {
- $candidate_uuid = sanitize_text_field( $p->uuid );
- } elseif ( isset( $p->name ) ) {
- $candidate_uuid = 'legacy-' . md5( strtolower( trim( $p->name ) ) );
- }
-
- if ( ! empty( $candidate_uuid ) ) {
- $live_uuids[ $candidate_uuid ] = true;
- if ( $candidate_uuid === $player->uuid ) {
- $matched_live_player = $p;
- }
- }
-
- if ( ! $matched_live_player && isset( $p->name ) && strtolower( trim( (string) $p->name ) ) === strtolower( trim( (string) $player->username ) ) ) {
- $matched_live_player = $p;
- }
- }
- $is_online = isset( $live_uuids[ $player->uuid ] );
-
- // 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 );
- }
- }
- }
-
- $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,
- 'prefix' => $player->prefix ?? '',
- 'first_seen' => $player->first_seen,
- 'last_seen' => $player->last_seen,
- 'is_online' => $is_online,
- 'playtime_seconds' => intval( $player->playtime_seconds ),
- 'kills' => intval( $player->kills ),
- 'deaths' => intval( $player->deaths ),
- 'kd' => $kd,
- 'balance' => $balance_value,
- 'is_bedrock' => $is_bedrock,
- 'joins' => intval( $player->joins ),
- 'bans' => $bans_val,
- 'mutes' => $mutes_val,
- 'warns' => $warns_val,
- 'kicks' => $kicks_val,
- 'forum_banner' => mcph_get_forum_banner( $player->uuid ),
- ) );
-}
-
-/**
- * 4. HTML Generator (Karten-Ansicht)
- */
-function mcph_generate_player_html( $limit = 500, $only_online = false, $is_ajax = false ) {
- global $wpdb;
- $table_name = $wpdb->prefix . 'mc_players';
-
- // Live Cache
- $api_response = mcph_fetch_statusapi_players( $is_ajax );
-
- $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 = ( isset( $p->uuid ) && !empty( $p->uuid ) ) ? sanitize_text_field( $p->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;
- $ds = mcph_get_display_settings();
-
- foreach ( $rows as $row ) {
- $is_online = $has_live_data ? isset( $live_online_uuids[ $row->uuid ] ) : ( $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
- if ( strpos( $username, '.' ) !== false || ( isset($row->uuid) && strpos($row->uuid, 'xuid') === 0 ) ) {
- $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 {
- $avatar = 'https://mc-heads.net/head/' . $username . '/100';
- }
-
- $anim_style = ! $is_ajax ? '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 . '';
- }
-
- echo '
';
- echo '

';
- echo '
';
- if ( ! empty( $row->prefix ) && ! empty( $ds['show_prefix'] ) ) echo '
' . $prefix . '';
- echo '
' . $username . '';
- echo '
';
- echo '
' . $status_html . '
';
- 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' );
-}
-
-// 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';
- $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 ); 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.
';
- }
-
- if ( isset( $_GET['mcph_ds_saved'] ) ) {
- echo 'Anzeige-Einstellungen gespeichert.
';
- }
-
- $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'];
- }
- ?>
-
-
MC Player History Einstellungen
-
-
-
-
-
🎛️ Anzeige-Einstellungen
-
Wähle aus, welche Felder im Spieler-Profil und in den Statistik-Karten angezeigt werden sollen.
-
-
-
-
-
Duplikate bereinigen
-
-
-
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 '
';
- $profile_id = 'mc-profile-' . substr( $container_id, -6 );
- echo '';
- $ds_js = mcph_get_display_settings();
- ?>
-
-
-
- @import url("https://fonts.googleapis.com/css2?family=Rajdhani:wght@500;600;700&family=Barlow:wght@400;500;600&display=swap");
-
- .mc-player-list, .mc-profile-panel {
- --mc-bg: #1f232a;
- --mc-surface: #2a2f37;
- --mc-surface-2: #242a33;
- --mc-ink: #e6f1ff;
- --mc-muted: #9aa7b2;
- --mc-border: #3a4450;
- --mc-accent: #18c7ff;
- --mc-accent-2: #00e5ff;
- --mc-glow: 0 0 24px rgba(24, 199, 255, 0.28);
- --mc-radius: 16px;
- }
-
- .mc-player-list { width: 100%; font-family: "Barlow", "Segoe UI", Arial, sans-serif; }
- .mc-grid { display: grid; gap: 18px; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); justify-content: center; align-items: stretch; }
- @keyframes fadeInUp { from { opacity: 0; transform: translateY(12px); } to { opacity: 1; transform: translateY(0); } }
-
- /* KARTE */
- .mc-player-card {
- position: relative;
- display: flex; flex-direction: column; align-items: center; gap: 10px; padding: 18px 12px;
- background: linear-gradient(180deg, #2a2f37 0%, #242a33 100%);
- border: 1px solid var(--mc-border);
- border-radius: var(--mc-radius);
- box-shadow: 0 10px 24px rgba(12, 16, 22, 0.35);
- transition: transform 0.25s ease, box-shadow 0.25s ease, border-color 0.25s ease;
- cursor: pointer; overflow: hidden;
- }
- .mc-player-card::before {
- content: "";
- position: absolute; inset: 0;
- background: radial-gradient(220px 80px at 20% 0%, rgba(24,199,255,0.15), transparent 70%);
- opacity: 0; transition: opacity 0.25s ease;
- }
- .mc-player-card:hover { transform: translateY(-6px); border-color: rgba(24,199,255,0.45); box-shadow: 0 16px 34px rgba(12,16,22,0.45); }
- .mc-player-card:hover::before { opacity: 1; }
- .mc-avatar {
- width: 82px; height: 82px; border-radius: 12px; background: #11151b; padding: 4px;
- border: 1px solid rgba(24,199,255,0.4);
- box-shadow: 0 8px 18px rgba(0, 0, 0, 0.35);
- transition: transform 0.25s ease; object-fit: contain;
- }
- .mc-player-card:hover .mc-avatar { transform: translateY(-4px) scale(1.05); }
- .mc-info-stack { display: flex; flex-direction: column; align-items: center; width: 100%; flex-grow: 1; position: relative; padding-bottom: 34px; justify-content: flex-start; z-index: 1; }
- .mc-prefix { font-size: 0.88em; display: block; width: 100%; text-align: center; margin-bottom: 4px; line-height: 1.2; }
- .mc-name { font-weight: 700; font-size: 1.05em; color: var(--mc-ink); word-break: break-word; display: block; width: 100%; text-align: center; }
- .mc-spacer { flex-grow: 1; }
- .mc-status-line { position: absolute; bottom: 0; left: 0; width: 100%; text-align: center; margin: 0; padding-bottom: 5px; }
- .mc-status { display: inline-flex; align-items: center; gap: 6px; padding: 5px 12px; border-radius: 999px; font-size: 0.78em; font-weight: 700; letter-spacing: 0.2px; background: #1f2937; color: #e2e8f0; border: 1px solid #334155; }
- .mc-online { background: rgba(24,199,255,0.15); color: #baf1ff; border: 1px solid rgba(24,199,255,0.5); box-shadow: var(--mc-glow); }
- .mc-offline { background: rgba(248,113,113,0.14); color: #fecaca; border: 1px solid rgba(248,113,113,0.4); }
- .mc-update-time { font-size: 0.82em; color: var(--mc-muted); text-align: center; margin-top: 20px; font-style: italic; padding: 8px; border: 1px dashed #3a4450; border-radius: 12px; }
-
- @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: 0.98em; } .mc-player-card { padding: 14px 8px; } }
-
- /* ===== SPIELERPROFIL ===== */
- .mc-profile-panel { width: 100%; font-family: "Barlow", "Segoe UI", Arial, sans-serif; }
- .mc-profile-inner {
- background: #2a2f37;
- border: 1px solid #3a4450;
- border-radius: 18px; padding: 26px;
- box-shadow: 0 18px 40px rgba(10, 12, 16, 0.55);
- }
- .mc-back-btn {
- display: inline-flex; align-items: center; gap: 6px; background: #1f232a;
- border: 1.5px solid var(--mc-accent); color: #baf1ff; font-size: 0.9em; font-weight: 700;
- padding: 6px 14px; border-radius: 999px; cursor: pointer; margin-bottom: 20px; transition: all 0.2s ease;
- box-shadow: var(--mc-glow);
- }
- .mc-back-btn:hover { background: rgba(24,199,255,0.18); color: #e6f9ff; transform: translateX(-2px); }
- .mc-profile-header { display: flex; align-items: flex-end; justify-content: space-between; gap: 20px; position: relative; overflow: hidden; border-radius: 14px; }
- .mc-profile-header > * { position: relative; z-index: 1; }
- .mc-profile-header::before {
- content: ""; position: absolute; inset: 0;
- background: linear-gradient(120deg, rgba(24,199,255,0.15), rgba(0,0,0,0.25));
- opacity: 0.7; z-index: 0;
- }
- .mc-profile-header-banner { background-size: cover; background-position: center; padding: 20px 22px; border-radius: 14px; border: 1px solid rgba(24,199,255,0.3); }
- .mc-profile-header-overlay { position: absolute; inset: 0; background: linear-gradient(90deg, rgba(8,12,18,0.45), rgba(8,12,18,0.7)); z-index: 0; }
- .mc-profile-header-left { flex: 1; display: flex; flex-direction: column; gap: 6px; }
- .mc-profile-prefix { font-size: 1em; font-weight: 600; line-height: 1.2; }
- .mc-profile-name { font-size: 2.05em; font-weight: 700; color: #e6f1ff; line-height: 1.05; letter-spacing: 0.2px; font-family: "Rajdhani", "Segoe UI", Arial, sans-serif; }
- .mc-profile-uuid { font-size: 0.72em; color: var(--mc-muted); font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace; word-break: break-all; margin-top: 2px; }
- .mc-profile-status-row { margin-top: 8px; display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
- .mc-platform-badge { font-size: 0.85em; background: rgba(24,199,255,0.12); color: #baf1ff; padding: 4px 10px; border-radius: 999px; font-weight: 700; border: 1px solid rgba(24,199,255,0.3); display: inline-flex; align-items: center; gap: 4px; }
- .mc-profile-header-right { flex-shrink: 0; }
- .mc-profile-avatar { width: 110px; height: auto; display: block; filter: drop-shadow(0 12px 22px rgba(0, 0, 0, 0.5)); transition: transform 0.3s ease; transform: scaleX(-1); }
- .mc-profile-avatar:hover { transform: scale(1.04); }
- .mc-profile-divider { height: 1px; background: linear-gradient(90deg, transparent, rgba(24,199,255,0.35), transparent); margin: 22px 0; }
-
- /* Grid: 4 Spalten fuer Desktop, 2 fuer Mobile */
- .mc-stats-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 12px; }
- .mc-stat-card {
- background: #232832; border: 1px solid #34404c; border-radius: 12px; padding: 14px 10px; text-align: center;
- box-shadow: 0 10px 22px rgba(8, 10, 14, 0.4); transition: transform 0.2s ease, box-shadow 0.2s ease, border-color 0.2s ease;
- }
- .mc-stat-card:hover { transform: translateY(-3px); border-color: rgba(24,199,255,0.45); box-shadow: 0 16px 28px rgba(10, 12, 16, 0.55); }
- .mc-stat-highlight {
- background: linear-gradient(135deg, rgba(24,199,255,0.35) 0%, rgba(0,229,255,0.12) 100%);
- border-color: rgba(24,199,255,0.55);
- }
- .mc-stat-highlight .mc-stat-icon, .mc-stat-highlight .mc-stat-value, .mc-stat-highlight .mc-stat-label { color: #e6f9ff !important; }
- .mc-stat-icon { font-size: 1.4em; line-height: 1; margin-bottom: 8px; }
- .mc-stat-value { font-size: 1.12em; font-weight: 700; color: var(--mc-ink); line-height: 1.2; word-break: break-word; }
- .mc-stat-label { font-size: 0.7em; font-weight: 700; color: var(--mc-muted); margin-top: 4px; text-transform: uppercase; letter-spacing: 0.6px; }
-
- .mc-profile-loading { display: flex; align-items: center; justify-content: center; gap: 12px; padding: 60px 20px; color: var(--mc-muted); font-size: 0.95em; }
- .mc-spinner { width: 22px; height: 22px; border: 3px solid #2e3642; border-top-color: var(--mc-accent); border-radius: 50%; animation: mc-spin 0.7s linear infinite; }
- @keyframes mc-spin { to { transform: rotate(360deg); } }
-
- @media (max-width: 600px) {
- .mc-profile-inner { padding: 18px 14px; }
- .mc-profile-name { font-size: 1.6em; }
- .mc-profile-avatar { width: 82px; }
- .mc-stats-grid { grid-template-columns: repeat(2, 1fr); gap: 10px; }
- }
-
- @media (max-width: 600px) { .mc-profile-header-banner { padding: 12px; } }
- ';
- return ob_get_clean();
-}
-
-/* ================= SIDEBAR WIDGET ================= */
-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.' ) );
- }
- public static function format_duration( $seconds ) {
- if ( $seconds < 60 ) return $seconds . 's';
- elseif ( $seconds < 3600 ) return floor( $seconds / 60 ) . 'm';
- elseif ( $seconds < 86400 ) { $h = floor( $seconds / 3600 ); $m = floor( ( $seconds % 3600 ) / 60 ); return $h . 'h ' . $m . 'm'; }
- else { $d = floor( $seconds / 86400 ); $h = floor( ( $seconds % 86400 ) / 3600 ); return $d . 'd ' . $h . 'h'; }
- }
- private static function parse_minecraft_color_codes_inline( $text ) {
- if ( $text === null || $text === '' ) return '';
- $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');
- $text = str_replace('§', '&', $text);
- $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 ) {
- if ( preg_match('/^&([0-9a-frk-orA-FK-OR])$/', $part, $m) ) {
- $code = strtolower( $m[1] );
- if ( $code === 'r' ) { if ( $open ) { $out .= ''; $open = false; } continue; }
- if ( isset( $map[$code] ) ) { if ( $open ) { $out .= ''; $open = false; } $out .= ''; $open = true; continue; }
- if ( $code === 'l' ) { $out .= ''; continue; }
- }
- $out .= esc_html( $part );
- }
- if ( $open ) $out .= '';
- return $out;
- }
- 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_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 );
- $raw_prefix = isset( $row->prefix ) ? $row->prefix : '';
- $prefix_html = self::parse_minecraft_color_codes_inline( $raw_prefix );
- $avatar = isset( $row->uuid ) && ! empty( $row->uuid ) ? 'https://mc-heads.net/head/' . esc_attr( $row->uuid ) . '/50' : 'https://mc-heads.net/head/' . rawurlencode( $username ) . '/50';
- ?>
-
-
-
-
; ?>)
-
-
-
-
-
-
-
; ?>)
-
-
-
-
-
-
-
-
- minotar.net -> crafatar.com
+// ===============================
+
+/**
+ * Liefert eine Liste von Bild-URLs (Primär-Dienst zuerst, danach Fallbacks).
+ * $type: 'head' | 'body'. Crafatar kennt nur UUIDs und wird sonst übersprungen.
+ */
+function mcph_avatar_urls( $id, $size = 100, $type = 'head', $uuid = '' ) {
+ $id = trim( (string) $id );
+ $uuid = trim( (string) $uuid );
+ $size = max( 16, intval( $size ) );
+ $enc = rawurlencode( $id );
+ $uuid_re = '/^[0-9a-f]{8}-?[0-9a-f]{4}-?[0-9a-f]{4}-?[0-9a-f]{4}-?[0-9a-f]{12}$/i';
+ // Echte UUID für Dienste, die keine Namen kennen (mineatar)
+ $real_uuid = preg_match( $uuid_re, $uuid ) ? $uuid : ( preg_match( $uuid_re, $id ) ? $id : '' );
+ $scale = max( 1, (int) round( $size / 12.5 ) ); // mineatar arbeitet mit Skalierungsfaktor
+
+ $urls = array();
+ if ( $type === 'body' ) {
+ $urls[] = "https://mc-heads.net/body/{$enc}/{$size}";
+ $urls[] = "https://minotar.net/armor/body/{$enc}/{$size}.png";
+ // Letzte Reserve mit 3D-Optik: Kopf
+ if ( $real_uuid ) $urls[] = 'https://api.mineatar.io/head/' . rawurlencode( $real_uuid ) . "?scale={$scale}&overlay";
+ } else {
+ // 1. mc-heads.net (3D-Kopf) 2. mineatar.io (3D-Kopf, nur UUID) 3. minotar.net (flach, letzte Reserve)
+ $urls[] = "https://mc-heads.net/head/{$enc}/{$size}";
+ if ( $real_uuid ) $urls[] = 'https://api.mineatar.io/head/' . rawurlencode( $real_uuid ) . "?scale={$scale}&overlay";
+ $urls[] = "https://minotar.net/helm/{$enc}/{$size}.png";
+ }
+ return $urls;
+}
+
+/** Gibt die Attribute src + data-mcph-fallbacks für ein
aus. */
+function mcph_avatar_attrs( $id, $size = 100, $type = 'head', $uuid = '' ) {
+ $urls = mcph_avatar_urls( $id, $size, $type, $uuid );
+ return 'src="' . esc_url( $urls[0] ) . '" data-mcph-fallbacks="' . esc_attr( wp_json_encode( $urls ) ) . '"';
+}
+
+/**
+ * Welche Kennung wird für den Avatar genutzt? (gleiche Logik wie vorher:
+ * Bedrock-/Punkt-Namen und xuid -> UUID, sonst Benutzername)
+ */
+function mcph_avatar_identifier( $row ) {
+ $name = isset( $row->username ) ? (string) $row->username : '';
+ $uuid = isset( $row->uuid ) ? (string) $row->uuid : '';
+ if ( ( strpos( $name, '.' ) !== false || strpos( $uuid, 'xuid' ) === 0 ) && $uuid !== '' ) {
+ return $uuid;
+ }
+ return $name;
+}
+
+/**
+ * Globaler Fallback-Handler: wechselt bei Ladefehler ODER nach 4 s ohne Antwort
+ * auf die nächste URL. Arbeitet auch für per AJAX nachgeladene Bilder.
+ */
+add_action( 'wp_head', 'mcph_print_avatar_fallback_script', 1 );
+function mcph_print_avatar_fallback_script() {
+ ?>
+
+ 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' );
+ ?>
+
+ last_error ) ) {
+ mcph_log( 'LiteBans Verbindung fehlgeschlagen: ' . $lbdb->last_error );
+ $connection = null;
+ return $connection;
+ }
+
+ $connection = array(
+ 'db' => $lbdb,
+ 'prefix' => $prefix,
+ );
+
+ return $connection;
+}
+
+/**
+ * Holt Bans/Mutes/Warns aus LiteBans für einen Spieler.
+ */
+function mcph_get_litebans_punishments( $uuid, $username ) {
+ static $count_cache = array();
+
+ $cache_key = (string) $uuid . '|' . strtolower( (string) $username );
+ if ( isset( $count_cache[ $cache_key ] ) ) {
+ return $count_cache[ $cache_key ];
+ }
+
+ $conn = mcph_get_litebans_connection();
+ if ( empty( $conn['db'] ) || empty( $conn['prefix'] ) ) {
+ $count_cache[ $cache_key ] = null;
+ return null;
+ }
+
+ $lbdb = $conn['db'];
+ $prefix = $conn['prefix'];
+
+ $search_uuid = '';
+ if ( ! empty( $uuid ) && strpos( $uuid, 'legacy-' ) !== 0 ) {
+ $search_uuid = $uuid;
+ }
+
+ if ( empty( $search_uuid ) && ! empty( $username ) ) {
+ $history_table = esc_sql( $prefix . 'history' );
+ $search_uuid = $lbdb->get_var(
+ $lbdb->prepare(
+ "SELECT uuid FROM {$history_table} WHERE name = %s ORDER BY date DESC LIMIT 1",
+ $username
+ )
+ );
+ }
+
+ if ( empty( $search_uuid ) ) {
+ $count_cache[ $cache_key ] = null;
+ return null;
+ }
+
+ $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 ) ) );
+ $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 );
+ $count_cache[ $cache_key ] = null;
+ return null;
+ }
+
+ $count_cache[ $cache_key ] = array(
+ 'bans' => $bans,
+ 'mutes' => $mutes,
+ 'warns' => $warns,
+ 'kicks' => $kicks,
+ );
+
+ return $count_cache[ $cache_key ];
+}
+
+/**
+ * Hilfsfunktion: Minecraft Color Codes umwandeln
+ */
+function mcph_parse_minecraft_colors( $text ) {
+ if ( empty( $text ) ) return '';
+ $map = array(
+ '&0' => '', '&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;
+}
+
+/**
+ * Erkennt Bedrock-Spieler robust aus API-Objekt, UUID/XUID und Namen.
+ */
+function mcph_detect_bedrock_player( $player, $uuid = '', $name = '' ) {
+ if ( is_object( $player ) ) {
+ $flag_keys = array( 'isBedrock', 'is_bedrock', 'bedrock', 'floodgate' );
+ foreach ( $flag_keys as $key ) {
+ if ( isset( $player->$key ) ) {
+ $value = $player->$key;
+ if ( is_bool( $value ) ) {
+ return $value ? 1 : 0;
+ }
+ if ( is_numeric( $value ) ) {
+ return intval( $value ) > 0 ? 1 : 0;
+ }
+ if ( is_string( $value ) ) {
+ $normalized = strtolower( trim( $value ) );
+ if ( in_array( $normalized, array( '1', 'true', 'yes', 'y', 'on' ), true ) ) {
+ return 1;
+ }
+ if ( in_array( $normalized, array( '0', 'false', 'no', 'n', 'off' ), true ) ) {
+ return 0;
+ }
+ }
+ }
+ }
+
+ if ( isset( $player->xuid ) && ! empty( $player->xuid ) ) {
+ return 1;
+ }
+ }
+
+ $uuid = strtolower( (string) $uuid );
+ if ( strpos( $uuid, 'xuid' ) === 0 || strpos( $uuid, 'floodgate' ) === 0 ) {
+ return 1;
+ }
+
+ // Java-Namen enthalten keinen Punkt; viele Bedrock-Setups nutzen Punkt-Präfix.
+ $name = trim( (string) $name );
+ if ( strpos( $name, '.' ) !== false ) {
+ return 1;
+ }
+
+ return 0;
+}
+
+/**
+ * 1. Tabelle beim Aktivieren anlegen + NEUE FELDER
+ */
+register_activation_hook( __FILE__, 'mcph_install' );
+function mcph_install() {
+ global $wpdb, $mc_player_history_db_version;
+ $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,
+ kills INT NOT NULL DEFAULT 0,
+ deaths INT NOT NULL DEFAULT 0,
+ balance DOUBLE NOT NULL DEFAULT 0,
+ is_bedrock tinyint(1) NOT NULL DEFAULT 0,
+ joins INT NOT NULL DEFAULT 0,
+ 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),
+ 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' );
+}
+
+/**
+ * Erweiterung der Datenbank beim Laden (falls Update)
+ */
+add_action( 'init', 'mcph_add_columns_v130' );
+function mcph_add_columns_v130() {
+ global $wpdb;
+ $table_name = $wpdb->prefix . 'mc_players';
+
+ // Playtime
+ if ( ! $wpdb->get_var( "SHOW COLUMNS FROM $table_name LIKE 'playtime_seconds'" ) ) {
+ $wpdb->query( "ALTER TABLE $table_name ADD COLUMN playtime_seconds INT NOT NULL DEFAULT 0 AFTER last_seen" );
+ }
+ // Kills
+ if ( ! $wpdb->get_var( "SHOW COLUMNS FROM $table_name LIKE 'kills'" ) ) {
+ $wpdb->query( "ALTER TABLE $table_name ADD COLUMN kills INT NOT NULL DEFAULT 0 AFTER playtime_seconds" );
+ }
+ // Deaths
+ if ( ! $wpdb->get_var( "SHOW COLUMNS FROM $table_name LIKE 'deaths'" ) ) {
+ $wpdb->query( "ALTER TABLE $table_name ADD COLUMN deaths INT NOT NULL DEFAULT 0 AFTER kills" );
+ }
+ // Balance
+ if ( ! $wpdb->get_var( "SHOW COLUMNS FROM $table_name LIKE 'balance'" ) ) {
+ $wpdb->query( "ALTER TABLE $table_name ADD COLUMN balance DOUBLE NOT NULL DEFAULT 0 AFTER deaths" );
+ }
+ // Is Bedrock
+ if ( ! $wpdb->get_var( "SHOW COLUMNS FROM $table_name LIKE 'is_bedrock'" ) ) {
+ $wpdb->query( "ALTER TABLE $table_name ADD COLUMN is_bedrock tinyint(1) NOT NULL DEFAULT 0 AFTER balance" );
+ }
+ // Joins
+ if ( ! $wpdb->get_var( "SHOW COLUMNS FROM $table_name LIKE 'joins'" ) ) {
+ $wpdb->query( "ALTER TABLE $table_name ADD COLUMN joins INT NOT NULL DEFAULT 0 AFTER is_bedrock" );
+ }
+ // Bans
+ if ( ! $wpdb->get_var( "SHOW COLUMNS FROM $table_name LIKE 'bans'" ) ) {
+ $wpdb->query( "ALTER TABLE $table_name ADD COLUMN bans INT NOT NULL DEFAULT 0 AFTER joins" );
+ }
+ // Mutes
+ if ( ! $wpdb->get_var( "SHOW COLUMNS FROM $table_name LIKE 'mutes'" ) ) {
+ $wpdb->query( "ALTER TABLE $table_name ADD COLUMN mutes INT NOT NULL DEFAULT 0 AFTER bans" );
+ }
+ // Warns
+ 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" );
+ }
+}
+
+/**
+ * 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; }
+
+ // Alle Offline setzen
+ $wpdb->query( "UPDATE $table_name SET is_online = 0" );
+ $players = isset( $json->players ) && is_array( $json->players ) ? $json->players : array();
+
+ foreach ( $players as $p ) {
+ $name = isset( $p->name ) ? sanitize_text_field( $p->name ) : '';
+ $prefix = isset( $p->prefix ) ? sanitize_text_field( $p->prefix ) : '';
+
+ // Basis Stats
+ $playtime = isset( $p->playtime ) ? intval( $p->playtime ) : 0;
+
+ // Economy
+ $balance = 0;
+ if ( isset( $p->economy ) && is_object( $p->economy ) && isset( $p->economy->balance ) ) {
+ $balance = floatval( $p->economy->balance );
+ }
+
+ // Kills/Deaths (Fallback wenn nicht in API)
+ $kills = isset( $p->kills ) ? intval( $p->kills ) : 0;
+ $deaths = isset( $p->deaths ) ? intval( $p->deaths ) : 0;
+
+ // NEU: Weitere Stats
+ $joins = isset( $p->joins ) ? intval( $p->joins ) : 0;
+
+ // UUID
+ if ( isset( $p->uuid ) && !empty( $p->uuid ) ) {
+ $uuid = sanitize_text_field( $p->uuid );
+ } else {
+ $uuid = 'legacy-' . md5( strtolower( trim( $name ) ) );
+ }
+
+ $is_bedrock = mcph_detect_bedrock_player( $p, $uuid, $name );
+
+ $bans = 0;
+ $mutes = 0;
+ $warns = 0;
+ $kicks = 0;
+
+ // Primär aus StatusAPI lesen (falls geliefert).
+ if ( isset( $p->punishments ) && is_object( $p->punishments ) ) {
+ $bans = isset( $p->punishments->bans ) ? intval( $p->punishments->bans ) : 0;
+ $mutes = isset( $p->punishments->mutes ) ? intval( $p->punishments->mutes ) : 0;
+ $warns = isset( $p->punishments->warns ) ? intval( $p->punishments->warns ) : 0;
+ }
+
+ // 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 );
+ $mutes = intval( $litebans_punishments['mutes'] ?? 0 );
+ $warns = intval( $litebans_punishments['warns'] ?? 0 );
+ $kicks = intval( $litebans_punishments['kicks'] ?? 0 );
+ }
+
+ if ( empty( $name ) ) continue;
+
+ $exists = $wpdb->get_var( $wpdb->prepare( "SELECT id FROM $table_name WHERE uuid = %s", $uuid ) );
+
+ // Fallback alte Einträge
+ if ( ! $exists ) {
+ $old_entry = $wpdb->get_row( $wpdb->prepare( "SELECT id, uuid FROM $table_name WHERE username = %s", $name ) );
+ if ( $old_entry ) {
+ $update_data = array(
+ 'uuid' => $uuid,
+ '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,
+ 'kicks' => $kicks
+ );
+ // 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;
+ $update_format[] = '%s';
+ }
+ $wpdb->update( $table_name, $update_data, array( 'id' => $old_entry->id ), $update_format, array( '%d' ) );
+ continue;
+ }
+ }
+
+ // Zeitkonvertierung
+ $last_seen_time = isset( $p->last_seen ) ? intval( $p->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 = wp_date( 'Y-m-d H:i:s', intval( $p->first_seen ) );
+ }
+
+ if ( $exists ) {
+ $update_data = array(
+ 'username' => $name,
+ '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,
+ 'kicks' => $kicks
+ );
+ // 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;
+ $update_format[] = '%s';
+ }
+ $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,
+ 'prefix' => $prefix,
+ 'first_seen' => $first_seen_mysql,
+ '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,
+ 'kicks' => $kicks
+ ), array( '%s', '%s', '%s', '%s', '%s', '%d', '%d', '%d', '%d', '%s', '%d', '%d', '%d', '%d', '%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 ) );
+}
+
+/**
+ * Holt StatusAPI-Spieler (optional mit Refresh).
+ */
+function mcph_fetch_statusapi_players( $force_refresh = false ) {
+ $cache_key = 'mcph_api_cache_data';
+ if ( $force_refresh ) {
+ delete_transient( $cache_key );
+ }
+
+ $api_response = get_transient( $cache_key );
+ if ( false !== $api_response ) {
+ return $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 ) ) {
+ return false;
+ }
+
+ $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, 2 );
+ return $json->players;
+ }
+
+ return false;
+}
+
+/**
+ * 3b. AJAX Handler: Spielerprofil (Erweitert)
+ */
+/**
+ * Forum-Banner: Holt Forumsdaten anhand der MC-UUID.
+ * Erfordert WP Business Forum Plugin (WBF_DB, WBF_Levels).
+ */
+function mcph_get_forum_banner( $uuid ) {
+ if ( ! class_exists( 'WBF_DB' ) || ! class_exists( 'WBF_Levels' ) ) return null;
+ global $wpdb;
+ $user_id = $wpdb->get_var( $wpdb->prepare(
+ "SELECT user_id FROM {$wpdb->prefix}forum_user_meta WHERE meta_key = 'mc_uuid' AND meta_value = %s LIMIT 1",
+ $uuid
+ ) );
+ if ( ! $user_id ) return null;
+ $forum_user = WBF_DB::get_user( (int) $user_id );
+ if ( ! $forum_user ) return null;
+ $post_count = intval( $forum_user->post_count );
+ return array(
+ 'display_name' => $forum_user->display_name,
+ 'avatar_url' => $forum_user->avatar_url,
+ 'banner_url' => $forum_user->banner_url ?? '',
+ 'role' => $forum_user->role,
+ 'post_count' => $post_count,
+ 'registered' => $forum_user->registered,
+ 'last_active' => $forum_user->last_active,
+ 'bio' => $forum_user->bio,
+ 'level_badge_html' => WBF_Levels::is_enabled() ? WBF_Levels::badge( $post_count ) : '',
+ 'level_progress_html' => WBF_Levels::is_enabled() ? WBF_Levels::progress_bar( $post_count ) : '',
+ );
+}
+
+add_action( 'wp_ajax_mcph_player_profile', 'mcph_ajax_player_profile' );
+add_action( 'wp_ajax_nopriv_mcph_player_profile', 'mcph_ajax_player_profile' );
+function mcph_ajax_player_profile() {
+ $uuid = isset( $_POST['uuid'] ) ? sanitize_text_field( $_POST['uuid'] ) : '';
+ if ( empty( $uuid ) ) { wp_send_json_error( 'Kein UUID angegeben.' ); return; }
+
+ global $wpdb;
+ $table_name = $wpdb->prefix . 'mc_players';
+ $player = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM $table_name WHERE uuid = %s", $uuid ) );
+
+ if ( ! $player ) { wp_send_json_error( 'Spieler nicht gefunden.' ); return; }
+
+ // Live-Status prüfen
+ $is_online = (bool) $player->is_online;
+ $is_bedrock = (bool) $player->is_bedrock;
+ $balance_value = floatval( $player->balance );
+ $api_response = mcph_fetch_statusapi_players( true );
+ if ( is_array( $api_response ) ) {
+ $live_uuids = array();
+ $matched_live_player = null;
+ foreach ( $api_response as $p ) {
+ $candidate_uuid = '';
+ if ( isset( $p->uuid ) && ! empty( $p->uuid ) ) {
+ $candidate_uuid = sanitize_text_field( $p->uuid );
+ } elseif ( isset( $p->name ) ) {
+ $candidate_uuid = 'legacy-' . md5( strtolower( trim( $p->name ) ) );
+ }
+
+ if ( ! empty( $candidate_uuid ) ) {
+ $live_uuids[ $candidate_uuid ] = true;
+ if ( $candidate_uuid === $player->uuid ) {
+ $matched_live_player = $p;
+ }
+ }
+
+ if ( ! $matched_live_player && isset( $p->name ) && strtolower( trim( (string) $p->name ) ) === strtolower( trim( (string) $player->username ) ) ) {
+ $matched_live_player = $p;
+ }
+ }
+ $is_online = isset( $live_uuids[ $player->uuid ] );
+
+ // 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 );
+ }
+ }
+ }
+
+ $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,
+ 'prefix' => $player->prefix ?? '',
+ 'first_seen' => $player->first_seen,
+ 'last_seen' => $player->last_seen,
+ 'is_online' => $is_online,
+ 'playtime_seconds' => intval( $player->playtime_seconds ),
+ 'kills' => intval( $player->kills ),
+ 'deaths' => intval( $player->deaths ),
+ 'kd' => $kd,
+ 'balance' => $balance_value,
+ 'is_bedrock' => $is_bedrock,
+ 'joins' => intval( $player->joins ),
+ 'bans' => $bans_val,
+ 'mutes' => $mutes_val,
+ 'warns' => $warns_val,
+ 'kicks' => $kicks_val,
+ 'forum_banner' => mcph_get_forum_banner( $player->uuid ),
+ ) );
+}
+
+/**
+ * 4. HTML Generator (Karten-Ansicht)
+ */
+function mcph_generate_player_html( $limit = 500, $only_online = false, $is_ajax = false ) {
+ global $wpdb;
+ $table_name = $wpdb->prefix . 'mc_players';
+
+ // Live Cache
+ $api_response = mcph_fetch_statusapi_players( $is_ajax );
+
+ $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 = ( isset( $p->uuid ) && !empty( $p->uuid ) ) ? sanitize_text_field( $p->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;
+ $ds = mcph_get_display_settings();
+
+ foreach ( $rows as $row ) {
+ $is_online = $has_live_data ? isset( $live_online_uuids[ $row->uuid ] ) : ( $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 (mit Fallback-Diensten)
+ $avatar_attrs = mcph_avatar_attrs( mcph_avatar_identifier( $row ), 100, 'head', isset( $row->uuid ) ? $row->uuid : '' );
+
+ $anim_style = ! $is_ajax ? '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 . '';
+ }
+
+ echo '
';
+ echo '
![' . $username . ']()
';
+ echo '
';
+ if ( ! empty( $row->prefix ) && ! empty( $ds['show_prefix'] ) ) echo '
' . $prefix . '';
+ echo '
' . $username . '';
+ echo '
';
+ echo '
' . $status_html . '
';
+ 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' );
+}
+
+// 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';
+ $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 ); 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.
';
+ }
+
+ if ( isset( $_GET['mcph_ds_saved'] ) ) {
+ echo 'Anzeige-Einstellungen gespeichert.
';
+ }
+
+ $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'];
+ }
+ ?>
+
+
MC Player History Einstellungen
+
+
+
+
+
🎛️ Anzeige-Einstellungen
+
Wähle aus, welche Felder im Spieler-Profil und in den Statistik-Karten angezeigt werden sollen.
+
+
+
+
+
Duplikate bereinigen
+
+
+
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 '
';
+ $profile_id = 'mc-profile-' . substr( $container_id, -6 );
+ echo '';
+ $ds_js = mcph_get_display_settings();
+ ?>
+
+
+
+ @import url("https://fonts.googleapis.com/css2?family=Rajdhani:wght@500;600;700&family=Barlow:wght@400;500;600&display=swap");
+
+ .mc-player-list, .mc-profile-panel {
+ --mc-bg: #1f232a;
+ --mc-surface: #2a2f37;
+ --mc-surface-2: #242a33;
+ --mc-ink: #e6f1ff;
+ --mc-muted: #9aa7b2;
+ --mc-border: #3a4450;
+ --mc-accent: #18c7ff;
+ --mc-accent-2: #00e5ff;
+ --mc-glow: 0 0 24px rgba(24, 199, 255, 0.28);
+ --mc-radius: 16px;
+ }
+
+ .mc-player-list { width: 100%; font-family: "Barlow", "Segoe UI", Arial, sans-serif; }
+ .mc-grid { display: grid; gap: 18px; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); justify-content: center; align-items: stretch; }
+ @keyframes fadeInUp { from { opacity: 0; transform: translateY(12px); } to { opacity: 1; transform: translateY(0); } }
+
+ /* KARTE */
+ .mc-player-card {
+ position: relative;
+ display: flex; flex-direction: column; align-items: center; gap: 10px; padding: 18px 12px;
+ background: linear-gradient(180deg, #2a2f37 0%, #242a33 100%);
+ border: 1px solid var(--mc-border);
+ border-radius: var(--mc-radius);
+ box-shadow: 0 10px 24px rgba(12, 16, 22, 0.35);
+ transition: transform 0.25s ease, box-shadow 0.25s ease, border-color 0.25s ease;
+ cursor: pointer; overflow: hidden;
+ }
+ .mc-player-card::before {
+ content: "";
+ position: absolute; inset: 0;
+ background: radial-gradient(220px 80px at 20% 0%, rgba(24,199,255,0.15), transparent 70%);
+ opacity: 0; transition: opacity 0.25s ease;
+ }
+ .mc-player-card:hover { transform: translateY(-6px); border-color: rgba(24,199,255,0.45); box-shadow: 0 16px 34px rgba(12,16,22,0.45); }
+ .mc-player-card:hover::before { opacity: 1; }
+ .mc-avatar {
+ width: 82px; height: 82px; border-radius: 12px; background: #11151b; padding: 4px;
+ border: 1px solid rgba(24,199,255,0.4);
+ box-shadow: 0 8px 18px rgba(0, 0, 0, 0.35);
+ transition: transform 0.25s ease; object-fit: contain;
+ }
+ .mc-player-card:hover .mc-avatar { transform: translateY(-4px) scale(1.05); }
+ .mc-info-stack { display: flex; flex-direction: column; align-items: center; width: 100%; flex-grow: 1; position: relative; padding-bottom: 34px; justify-content: flex-start; z-index: 1; }
+ .mc-prefix { font-size: 0.88em; display: block; width: 100%; text-align: center; margin-bottom: 4px; line-height: 1.2; }
+ .mc-name { font-weight: 700; font-size: 1.05em; color: var(--mc-ink); word-break: break-word; display: block; width: 100%; text-align: center; }
+ .mc-spacer { flex-grow: 1; }
+ .mc-status-line { position: absolute; bottom: 0; left: 0; width: 100%; text-align: center; margin: 0; padding-bottom: 5px; }
+ .mc-status { display: inline-flex; align-items: center; gap: 6px; padding: 5px 12px; border-radius: 999px; font-size: 0.78em; font-weight: 700; letter-spacing: 0.2px; background: #1f2937; color: #e2e8f0; border: 1px solid #334155; }
+ .mc-online { background: rgba(24,199,255,0.15); color: #baf1ff; border: 1px solid rgba(24,199,255,0.5); box-shadow: var(--mc-glow); }
+ .mc-offline { background: rgba(248,113,113,0.14); color: #fecaca; border: 1px solid rgba(248,113,113,0.4); }
+ .mc-update-time { font-size: 0.82em; color: var(--mc-muted); text-align: center; margin-top: 20px; font-style: italic; padding: 8px; border: 1px dashed #3a4450; border-radius: 12px; }
+
+ @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: 0.98em; } .mc-player-card { padding: 14px 8px; } }
+
+ /* ===== SPIELERPROFIL ===== */
+ .mc-profile-panel { width: 100%; font-family: "Barlow", "Segoe UI", Arial, sans-serif; }
+ .mc-profile-inner {
+ background: #2a2f37;
+ border: 1px solid #3a4450;
+ border-radius: 18px; padding: 26px;
+ box-shadow: 0 18px 40px rgba(10, 12, 16, 0.55);
+ }
+ .mc-back-btn {
+ display: inline-flex; align-items: center; gap: 6px; background: #1f232a;
+ border: 1.5px solid var(--mc-accent); color: #baf1ff; font-size: 0.9em; font-weight: 700;
+ padding: 6px 14px; border-radius: 999px; cursor: pointer; margin-bottom: 20px; transition: all 0.2s ease;
+ box-shadow: var(--mc-glow);
+ }
+ .mc-back-btn:hover { background: rgba(24,199,255,0.18); color: #e6f9ff; transform: translateX(-2px); }
+ .mc-profile-header { display: flex; align-items: flex-end; justify-content: space-between; gap: 20px; position: relative; overflow: hidden; border-radius: 14px; }
+ .mc-profile-header > * { position: relative; z-index: 1; }
+ .mc-profile-header::before {
+ content: ""; position: absolute; inset: 0;
+ background: linear-gradient(120deg, rgba(24,199,255,0.15), rgba(0,0,0,0.25));
+ opacity: 0.7; z-index: 0;
+ }
+ .mc-profile-header-banner { background-size: cover; background-position: center; padding: 20px 22px; border-radius: 14px; border: 1px solid rgba(24,199,255,0.3); }
+ .mc-profile-header-overlay { position: absolute; inset: 0; background: linear-gradient(90deg, rgba(8,12,18,0.45), rgba(8,12,18,0.7)); z-index: 0; }
+ .mc-profile-header-left { flex: 1; display: flex; flex-direction: column; gap: 6px; }
+ .mc-profile-prefix { font-size: 1em; font-weight: 600; line-height: 1.2; }
+ .mc-profile-name { font-size: 2.05em; font-weight: 700; color: #e6f1ff; line-height: 1.05; letter-spacing: 0.2px; font-family: "Rajdhani", "Segoe UI", Arial, sans-serif; }
+ .mc-profile-uuid { font-size: 0.72em; color: var(--mc-muted); font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace; word-break: break-all; margin-top: 2px; }
+ .mc-profile-status-row { margin-top: 8px; display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
+ .mc-platform-badge { font-size: 0.85em; background: rgba(24,199,255,0.12); color: #baf1ff; padding: 4px 10px; border-radius: 999px; font-weight: 700; border: 1px solid rgba(24,199,255,0.3); display: inline-flex; align-items: center; gap: 4px; }
+ .mc-profile-header-right { flex-shrink: 0; }
+ .mc-profile-avatar { width: 110px; height: auto; display: block; filter: drop-shadow(0 12px 22px rgba(0, 0, 0, 0.5)); transition: transform 0.3s ease; transform: scaleX(-1); }
+ .mc-profile-avatar:hover { transform: scale(1.04); }
+ .mc-profile-divider { height: 1px; background: linear-gradient(90deg, transparent, rgba(24,199,255,0.35), transparent); margin: 22px 0; }
+
+ /* Grid: 4 Spalten fuer Desktop, 2 fuer Mobile */
+ .mc-stats-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 12px; }
+ .mc-stat-card {
+ background: #232832; border: 1px solid #34404c; border-radius: 12px; padding: 14px 10px; text-align: center;
+ box-shadow: 0 10px 22px rgba(8, 10, 14, 0.4); transition: transform 0.2s ease, box-shadow 0.2s ease, border-color 0.2s ease;
+ }
+ .mc-stat-card:hover { transform: translateY(-3px); border-color: rgba(24,199,255,0.45); box-shadow: 0 16px 28px rgba(10, 12, 16, 0.55); }
+ .mc-stat-highlight {
+ background: linear-gradient(135deg, rgba(24,199,255,0.35) 0%, rgba(0,229,255,0.12) 100%);
+ border-color: rgba(24,199,255,0.55);
+ }
+ .mc-stat-highlight .mc-stat-icon, .mc-stat-highlight .mc-stat-value, .mc-stat-highlight .mc-stat-label { color: #e6f9ff !important; }
+ .mc-stat-icon { font-size: 1.4em; line-height: 1; margin-bottom: 8px; }
+ .mc-stat-value { font-size: 1.12em; font-weight: 700; color: var(--mc-ink); line-height: 1.2; word-break: break-word; }
+ .mc-stat-label { font-size: 0.7em; font-weight: 700; color: var(--mc-muted); margin-top: 4px; text-transform: uppercase; letter-spacing: 0.6px; }
+
+ .mc-profile-loading { display: flex; align-items: center; justify-content: center; gap: 12px; padding: 60px 20px; color: var(--mc-muted); font-size: 0.95em; }
+ .mc-spinner { width: 22px; height: 22px; border: 3px solid #2e3642; border-top-color: var(--mc-accent); border-radius: 50%; animation: mc-spin 0.7s linear infinite; }
+ @keyframes mc-spin { to { transform: rotate(360deg); } }
+
+ @media (max-width: 600px) {
+ .mc-profile-inner { padding: 18px 14px; }
+ .mc-profile-name { font-size: 1.6em; }
+ .mc-profile-avatar { width: 82px; }
+ .mc-stats-grid { grid-template-columns: repeat(2, 1fr); gap: 10px; }
+ }
+
+ @media (max-width: 600px) { .mc-profile-header-banner { padding: 12px; } }
+ ';
+ return ob_get_clean();
+}
+
+/* ================= SIDEBAR WIDGET ================= */
+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.' ) );
+ }
+ public static function format_duration( $seconds ) {
+ if ( $seconds < 60 ) return $seconds . 's';
+ elseif ( $seconds < 3600 ) return floor( $seconds / 60 ) . 'm';
+ elseif ( $seconds < 86400 ) { $h = floor( $seconds / 3600 ); $m = floor( ( $seconds % 3600 ) / 60 ); return $h . 'h ' . $m . 'm'; }
+ else { $d = floor( $seconds / 86400 ); $h = floor( ( $seconds % 86400 ) / 3600 ); return $d . 'd ' . $h . 'h'; }
+ }
+ private static function parse_minecraft_color_codes_inline( $text ) {
+ if ( $text === null || $text === '' ) return '';
+ $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');
+ $text = str_replace('§', '&', $text);
+ $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 ) {
+ if ( preg_match('/^&([0-9a-frk-orA-FK-OR])$/', $part, $m) ) {
+ $code = strtolower( $m[1] );
+ if ( $code === 'r' ) { if ( $open ) { $out .= ''; $open = false; } continue; }
+ if ( isset( $map[$code] ) ) { if ( $open ) { $out .= ''; $open = false; } $out .= ''; $open = true; continue; }
+ if ( $code === 'l' ) { $out .= ''; continue; }
+ }
+ $out .= esc_html( $part );
+ }
+ if ( $open ) $out .= '';
+ return $out;
+ }
+ 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_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 );
+ $raw_prefix = isset( $row->prefix ) ? $row->prefix : '';
+ $prefix_html = self::parse_minecraft_color_codes_inline( $raw_prefix );
+ $avatar_attrs = mcph_avatar_attrs( ( isset( $row->uuid ) && ! empty( $row->uuid ) ) ? $row->uuid : $row->username, 50, 'head' );
+ ?>
+
+
+
+
![]()
class="mc-top-avatar" alt="">
+
+
+
+
+
+
+
![]()
class="mc-top-avatar" alt="">
+
+
+
+
+
+
+
+
+