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

';
echo '
';
// 1. PREFIX
if ( ! empty( $row->prefix ) ) {
echo '
' . $prefix . '';
}
// 2. NAME
echo '
' . $username . '';
// 3. SPACER (Nimmt den verbleibenden Platz ein)
echo '
';
// 4. STATUS
echo '
' . $status_html . '
';
echo '
';
echo '
';
}
echo '
';
echo 'Aktualisiert: ' . current_time('H:i:s') . '
';
return ob_get_clean();
}
/**
* 5. Admin Menü
*/
add_action( 'admin_menu', 'mcph_admin_menu' );
function mcph_admin_menu() {
add_options_page( 'MC Player History', 'MC Player History', 'manage_options', 'mc_player_history', 'mcph_options_page' );
}
add_action( 'admin_init', 'mcph_settings_init' );
function mcph_settings_init() {
register_setting( 'mcph_plugin_options', 'mcph_statusapi_url' );
}
function mcph_options_page() {
// Duplikate bereinigen
if ( isset( $_POST['mcph_cleanup_duplicates'] ) && check_admin_referer( 'mcph_cleanup_action' ) ) {
global $wpdb;
$table_name = $wpdb->prefix . 'mc_players';
// Finde Duplikate (gleicher Username, verschiedene UUIDs)
$duplicates = $wpdb->get_results(
"SELECT username, COUNT(*) as count, GROUP_CONCAT(id ORDER BY last_seen DESC) as ids
FROM $table_name
GROUP BY username
HAVING count > 1"
);
$cleaned = 0;
foreach ( $duplicates as $dup ) {
$ids = explode( ',', $dup->ids );
// Behalte den neuesten (ersten), lösche die anderen
array_shift( $ids );
foreach ( $ids as $id ) {
$wpdb->delete( $table_name, array( 'id' => $id ), array( '%d' ) );
$cleaned++;
}
}
echo '' . $cleaned . ' Duplikate wurden entfernt.
';
}
if ( isset( $_POST['mcph_manual_sync'] ) && check_admin_referer( 'mcph_manual_sync_action' ) ) {
mcph_sync_from_statusapi();
delete_transient( 'mcph_api_cache_data' );
echo 'Manueller Sync ausgeführt.
';
}
?>
MC Player History Einstellungen
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 '
';
?>
.mc-player-list { width: 100%; }
.mc-grid {
display: grid;
gap: 20px;
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
justify-content: center;
align-items: stretch;
}
@keyframes fadeInUp {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
/* KARTE */
.mc-player-card {
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
padding: 20px 10px;
background: linear-gradient(145deg, #ffffff 0%, #f8f9fa 100%);
border: 1px solid #e0e0e0;
border-radius: 12px;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
cursor: default;
position: relative;
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
}
.mc-player-card:hover {
transform: translateY(-8px) scale(1.02);
box-shadow: 0 12px 24px rgba(0,0,0,0.15);
border-color: #667eea;
background: linear-gradient(145deg, #ffffff 0%, #f0f2ff 100%);
}
.mc-avatar {
border-radius: 8px;
border: 3px solid #e0e0e0;
background: transparent;
width: 80px;
height: 80px;
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
flex-shrink: 0;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
object-fit: contain;
padding: 0;
}
.mc-player-card:hover .mc-avatar {
border-color: #667eea;
transform: scale(1.15) rotateY(10deg);
box-shadow: 0 8px 20px rgba(102, 126, 234, 0.4);
}
/* INFOS BEREICH */
.mc-info-stack {
display: flex;
flex-direction: column;
align-items: center;
width: 100%;
flex-grow: 1;
position: relative;
padding-bottom: 35px;
justify-content: flex-start;
}
/* 1. PREFIX */
.mc-prefix {
font-size: 0.9em;
display: block;
width: 100%;
text-align: center;
margin-bottom: 4px;
line-height: 1.2;
text-shadow: 0 1px 2px rgba(0,0,0,0.1);
}
/* 2. NAME */
.mc-name {
font-weight: bold;
font-size: 1.1em;
color: #2d3748;
word-break: break-word;
display: block;
width: 100%;
text-align: center;
text-shadow: 0 1px 2px rgba(0,0,0,0.05);
}
/* 3. SPACER */
.mc-spacer {
flex-grow: 1;
}
/* 4. STATUS (UNTEN) */
.mc-status-line {
position: absolute;
bottom: 0;
left: 0;
width: 100%;
text-align: center;
margin: 0;
padding-bottom: 5px;
}
.mc-status {
display: inline-block;
padding: 5px 14px;
border-radius: 20px;
font-size: 0.8em;
font-weight: 600;
transition: all 0.3s ease;
}
.mc-online {
background: linear-gradient(135deg, #d4f8e8 0%, #b8f2d9 100%);
color: #0a7340;
border: 1px solid #81e6b8;
box-shadow: 0 2px 4px rgba(10, 115, 64, 0.1);
}
.mc-offline {
background: linear-gradient(135deg, #fff5f5 0%, #ffe5e5 100%);
color: #c53030;
border: 1px solid #fbb6b6;
box-shadow: 0 2px 4px rgba(197, 48, 48, 0.1);
}
.mc-player-card:hover .mc-status {
transform: scale(1.05);
}
.mc-update-time {
font-size: 0.85em;
color: #718096;
text-align: center;
margin-top: 20px;
font-style: italic;
padding: 8px;
background: rgba(102, 126, 234, 0.05);
border-radius: 6px;
display: inline-block;
width: 100%;
}
@media (min-width: 1200px) {
.mc-grid { grid-template-columns: repeat(6, 1fr); }
}
@media (max-width: 600px) {
.mc-grid {
grid-template-columns: repeat(2, 1fr);
gap: 12px;
}
.mc-avatar {
width: 70px;
height: 70px;
}
.mc-name {
font-size: 1em;
}
.mc-player-card {
padding: 15px 8px;
}
}
';
return ob_get_clean();
}
/* ================= SIDEBAR WIDGET: TOP SPIELZEIT (1/2 Spalten + sichere Inline-Farben) ================= */
class MC_Top_Playtime_Widget extends WP_Widget {
public function __construct() {
parent::__construct(
'mc_top_playtime_widget',
'MC Top Spieler',
array( 'description' => 'Zeigt die 6 Spieler mit der meisten Spielzeit an. Wahl: 1 oder 2 Spieler pro Zeile.' )
);
}
// Zeitformatierung
public static function format_duration( $seconds ) {
if ( $seconds < 60 ) {
return $seconds . 's';
} elseif ( $seconds < 3600 ) {
$minutes = floor( $seconds / 60 );
return $minutes . 'm';
} elseif ( $seconds < 86400 ) {
$hours = floor( $seconds / 3600 );
$minutes = floor( ( $seconds % 3600 ) / 60 );
return $hours . 'h ' . $minutes . 'm';
} else {
$days = floor( $seconds / 86400 );
$hours = floor( ( $seconds % 86400 ) / 3600 );
return $days . 'd ' . $hours . 'h';
}
}
// Konvertiert Minecraft-Farbcodes (&0-&f, &r) in inline Text
private static function parse_minecraft_color_codes_inline( $text ) {
if ( $text === null || $text === '' ) {
return '';
}
// Mapping Minecraft color codes -> HEX
$map = array(
'0' => '#000000',
'1' => '#0000AA',
'2' => '#00AA00',
'3' => '#00AAAA',
'4' => '#AA0000',
'5' => '#AA00AA',
'6' => '#FFAA00',
'7' => '#AAAAAA',
'8' => '#555555',
'9' => '#5555FF',
'a' => '#55FF55',
'b' => '#55FFFF',
'c' => '#FF5555',
'd' => '#FF55FF',
'e' => '#FFFF55',
'f' => '#FFFFFF',
);
// Ersetze alternative §-Codes durch & falls nötig
$text = str_replace('§', '&', $text);
// Teile so, dass die Codes erhalten bleiben
$parts = preg_split('/(&[0-9a-frk-or])/i', $text, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY);
$out = '';
$open = false;
foreach ( $parts as $part ) {
// Ist es ein Code?
if ( preg_match('/^&([0-9a-frk-orA-FK-OR])$/', $part, $m) ) {
$code = strtolower( $m[1] );
// Reset
if ( $code === 'r' ) {
if ( $open ) {
$out .= '';
$open = false;
}
continue;
}
// Farbe
if ( isset( $map[$code] ) ) {
if ( $open ) {
$out .= '';
$open = false;
}
$hex = $map[$code];
$out .= '';
$open = true;
continue;
}
// Formatcodes: &l &o &n &m &k -> wir implementieren nur fett (&l) &o kursiv optional
if ( $code === 'l' ) { // fett
$out .= '';
continue;
}
if ( $code === 'o' ) { // italic
$out .= '';
continue;
}
if ( in_array( $code, array('m','n','k'), true) ) {
// durchgestrichen / underline / obfuscated - ignorieren oder erweitern falls gewünscht
continue;
}
continue;
}
// Normaler Text: escapen
$escaped = esc_html( $part );
$out .= $escaped;
}
// Schließe eventuell offene Tags: zuerst format, dann color spans
// (Wir schlossen format tags nicht oben -> vereinfachung: entfernen ungeschlossene format-tags)
// Schließe color span falls offen
if ( $open ) {
$out .= '';
$open = false;
}
// Hinweis: Wir benutzten esc_html pro Textteil; das ist sicher.
return $out;
}
// Backend-Form
public function form( $instance ) {
$title = isset( $instance['title'] ) ? $instance['title'] : 'Top 6 Spielzeit';
$columns = isset( $instance['columns'] ) ? intval( $instance['columns'] ) : 1;
?>
prefix . 'mc_players';
// Top 6 Spieler abfragen
$top_players = $wpdb->get_results(
"SELECT * FROM $table_name WHERE playtime_seconds > 0 ORDER BY playtime_seconds DESC LIMIT 6"
);
$container_class = 'mc-top-list' . ( $columns === 2 ? ' mc-two-columns' : ' mc-one-column' );
?>
Noch keine Daten.
username );
$time_string = self::format_duration( $row->playtime_seconds );
// Rohes Prefix aus DB (z.B. "&7Member")
$raw_prefix = isset( $row->prefix ) ? $row->prefix : '';
$prefix_html = self::parse_minecraft_color_codes_inline( $raw_prefix );
// Avatar URL (UUID bevorzugt)
if ( isset( $row->uuid ) && ! empty( $row->uuid ) ) {
$avatar = 'https://mc-heads.net/head/' . esc_attr( $row->uuid ) . '/50';
} else {
$avatar = 'https://mc-heads.net/head/' . rawurlencode( $username ) . '/50';
}
?>