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' ); - ?> -
-

MC Player History – Update verfügbar

-

- Aktuelle Version:
- Neueste Version: -

-

- Update herunterladen - Release Notes - Jetzt neu prüfen -

-
- 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 '' . $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

- -
- - - - - - -
StatusAPI URL - -

Bitte unbedingt mit http:// angeben.

-
- -
- -
-

🎛️ Anzeige-Einstellungen

-

Wähle aus, welche Felder im Spieler-Profil und in den Statistik-Karten angezeigt werden sollen.

- -
- - -
- $fields ) : ?> -
-

- $label ) : ?> - - -
- -
-

- -

-
- -
-

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 '
'; - $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'; - ?> - -
-
- <?php echo $username; ?> -
-
-
-
-
-
- - -
- <?php echo $username; ?> -
-
-
- - - -
- - 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' ); + ?> +
+

MC Player History – Update verfügbar

+

+ Aktuelle Version:
+ Neueste Version: +

+

+ Update herunterladen + Release Notes + Jetzt neu prüfen +

+
+ 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

+ +
+ + + + + + +
StatusAPI URL + +

Bitte unbedingt mit http:// angeben.

+
+ +
+ +
+

🎛️ Anzeige-Einstellungen

+

Wähle aus, welche Felder im Spieler-Profil und in den Statistik-Karten angezeigt werden sollen.

+ +
+ + +
+ $fields ) : ?> +
+

+ $label ) : ?> + + +
+ +
+

+ +

+
+ +
+

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 '
'; + $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=""> +
+
+
+ + + +
+ +